首页
/ PostHog 仪表盘系统全景:从领域模型到多渲染表面(Placement)的前后端所有权地图

PostHog 仪表盘系统全景:从领域模型到多渲染表面(Placement)的前后端所有权地图

2026-09-09 15:58:15作者:明树来

PostHog 的仪表盘(Dashboard)并不是一个只存在于标准仪表盘页面的功能——同一份仪表盘数据会在标准页面、项目主页、功能标志详情页、群组/用户详情页、公开分享链接、导出打印等多种“渲染表面”上呈现。本文基于 PostHog 仓库中仪表盘技能的参考文档 surfaces-and-ownership.md,结合其上下文 managing-dashboards/SKILL.md 与真实源码,完整梳理仪表盘领域的领域模型划分、DashboardPlacement 前端放置契约、前后端文件的“所有权地图”,以及 API 契约变更的标准流程。读完本文,你将能够准确判断一个仪表盘改动应落在哪些文件、哪些渲染表面必须逐一验证,以及如何安全地执行序列化器/视图集级别的 API 契约变更。

一、从领域模型开始:谁拥有什么数据

文档要求任何仪表盘改动都应“从领域模型开始”(Start from the domain model),其核心结论是四个模型各自拥有不同的数据职责,改动前必须先判断你要动的状态属于哪一层:

  • Dashboard 模型拥有项目作用域的元数据:仪表盘过滤器(filters)、变量(variables)、分享状态(sharing state)、限制级别(restriction_level)以及与磁贴(tile)的关系。
  • DashboardTile 模型拥有一条磁贴关系以及每个断点(breakpoint)的布局 JSON。
  • 一个磁贴恰好是一个 insight、文本卡片(text card)、按钮(button)或部件(widget)之一。
  • DashboardTemplate 存储的是可复制的仪表盘定义,它不是一个活的仪表盘。
  • 仪表盘 widget 使用独立的模型,widget 相关的改动应走 manage-dashboard-widgets 这条独立技能线。

Dashboard 模型:元数据与可见性

从源码 dashboard.py 可以完整印证文档中“Dashboard 拥有什么”的说法:

  • filters = models.JSONField(default=dict)variables = models.JSONField(...) 承载仪表盘级过滤器和变量;
  • restriction_level 默认值为 RestrictionLevel.EVERYONE_IN_PROJECT_CAN_EDIT,与“限制级别”归 Dashboard 所有对应;
  • 旧的 share_token / is_shared 字段已被标记为 DEPRECATED,注释明确写着“use the new 'sharing' relation instead”,当前分享状态由独立的 SharingConfiguration 关系承载,is_sharing_enabled 属性(dashboard.py)从 sharingconfiguration_set 读取——这正是文档中 “sharing state” 的落地方式;
  • 模型还定义了网格间距档位 DASHBOARD_GRID_SPACING_GAPS(tight 8px / condensed 12px / standard 16px / relaxed 32px / wide 48px)和布局压缩模式 LayoutCompaction(vertical / horizontal / stable),说明布局几何参数也归 Dashboard 层管辖;
  • 软删除通过 deleted 字段加自定义 DashboardManager 实现:默认查询集自动 exclude(deleted=True),需要包含已删除行时用 objects_including_soft_deleted

CreationMode.UNLISTED:产品内嵌的“隐藏”仪表盘

文档特别强调的一点是:产品创建的非公开(unlisted)仪表盘必须纳入检查范围Dashboard.CreationMode.UNLISTED 只是让这些仪表盘从常规列表中隐藏,但并不会移除任何仪表盘规则。源码中该枚举定义及其注释(dashboard.py)给出了典型场景:

class CreationMode(models.TextChoices):
    DEFAULT = "default", "Default"
    TEMPLATE = ("template", "Template")          # 由预定义模板创建
    DUPLICATE = ("duplicate", "Duplicate")      # 从另一个仪表盘复制
    UNLISTED = ("unlisted", "Unlisted (product-embedded)")
    # Product dashboards (e.g. AI observability) - hidden from general lists,
    # accessed via tag queries

即 AI observability 等产品内嵌仪表盘使用 unlisted 模式创建,通过 tag 查询访问。对工程实践的含义是:任何基于“仪表盘列表”做假设的改动(列表接口、列表页 UI、批量操作)都要意识到存在一个不在列表里但依然完整受权限、过滤器、布局规则约束的仪表盘集合。

DashboardTile 模型:一个磁贴恰好一个内容

dashboard_tile.py 精确实现了文档中“一个磁贴恰好是 insight、text、button 或 widget 之一”的约束,且用双重手段保证:

  1. 数据库层:build_unique_relationship_check(("insight", "text", "button_tile", "widget")) 生成 CheckConstraint(约束名 dash_tile_exactly_one_related_object);同时针对每个 (dashboard, 内容) 组合建立了部分唯一约束(unique_dashboard_insightunique_dashboard_text 等)。
  2. 应用层:clean() 方法显式校验“related_fields != 1 就抛 ValidationError”,并规定刷新相关字段(filters_hashrefreshingrefresh_attemptlast_refresh)只允许出现在 insight 磁贴上。

磁贴的其余字段也印证了“每断点布局 JSON 归磁贴所有”:layouts = models.JSONField(default=dict),其结构形如 {"sm": {"h": 3, "w": 4, "x": 0, "y": 2, "minH": 3, "minW": 3}, "xs": {...}}——可以推断 sm/xs 等键就是文档所指的 breakpoint。排序工具函数 sort_tiles_by_layout 按 (y, x, id) 排序,id 作为兜底 tiebreak 以避免数据库返回顺序的随机性。

此外,save() 中会自动从 dashboard.team_id 回填 team_id(该字段被反规范化,以便该表能通过 HogQL 暴露——HogQL 打印器会对每张 Postgres 表注入 WHERE team_id = <ctx.team_id>),这与技能主文档中“保持仪表盘与磁贴数据团队作用域化”的规则相呼应。

DashboardTemplate 模型:模板不是活仪表盘

dashboard_templates.py 证实了“DashboardTemplate 存储可复制的仪表盘定义,它不是活仪表盘”:

  • 模板的磁贴内容是一整个 JSON 字段 tiles = models.JSONField(...),而非对 DashboardTile 表的外键引用;dashboard_filtersvariables 同样是 JSON 快照;
  • 模板有独立的作用域枚举 Scopeteam / organization / global / feature_flag),以及 availability_contexts(例如 generalonboarding)与 is_featured 等分发字段;
  • 团队内模板名唯一(unique_template_name_per_team 约束);
  • 模型内置了两个硬编码的“Product analytics”模板:legacy_signup_template()(旧版 DEFAULT_APP 种子)与 default_signup_template()(新版注册默认布局,含 TEXT/INSIGHT/BUTTON 混合磁贴与 sm/xs 双断点布局),源码注释说明系统假设该模板始终存在、不会等待从模板仓库导入。功能标志的用量仪表盘则由 feature_flag_template(feature_flag_key) 生成——这正是后文 FeatureFlag 渲染表面的数据来源。

二、渲染表面:DashboardPlacement 前端放置契约

文档给出的核心规范是:使用 DashboardPlacement 作为前端放置契约(frontend placement contract),即任何在前端渲染仪表盘 UI 的组件,都必须显式声明自己处于哪个放置位置,并按该位置执行对应行为。

Placement 要求的行为
Dashboard 完整的已认证仪表盘。可能可用编辑能力。
ProjectHomepageBuiltin 仪表盘内容渲染在另一个已认证的产品表面中。检查操作可见性与可用宽度。
Public 公开分享。只读。不得暴露作者信息、文件夹、私有配置或强制刷新操作。
Export 导出渲染。不得添加交互控件,也不得假设有浏览器用户会话。
FeatureFlagGroup 嵌入式仪表盘上下文。检查宿主页面、URL 状态、权限与刷新行为。

前端枚举的实际定义

DashboardPlacement 枚举定义于 frontend/src/types.ts,其成员比文档表格更完整——文档表只挑了改动决策中最关键的一批,实际枚举还包含若干产品内嵌表面:

export enum DashboardPlacement {
    Dashboard = 'dashboard', // When on the standard dashboard page
    CustomerAnalytics = 'customer-analytics', // When embedded on the customer analytics page
    ProjectHomepage = 'project-homepage', // When embedded on the project homepage
    FeatureFlag = 'feature-flag',
    Public = 'public', // When viewing the dashboard publicly
    Export = 'export', // When the dashboard is being exported (alike to being printed)
    Person = 'person', // When the dashboard is being viewed on a person page
    Group = 'group', // When the dashboard is being viewed on a group page
    Builtin = 'builtin', // Dashboard built into product UI with external controls provided by parent context
    DataOps = 'data-ops', // When embedded on the data ops scene dashboard tab
}

仓库中已有大量组件以此契约分支行为,例如:

这说明放置契约不是纸面约定,而是贯穿组件树的真实分派依据。

每个表面的行为差异要点

结合文档表格,各表面的关键差异可以归纳为:

  • Dashboard(标准页):功能全集,编辑是否可用取决于 restriction_level 与 RBAC,对应 dashboardLogic.tsx 承载的场景状态与 DashboardItems.tsx 的主布局。
  • ProjectHomepage / Builtin:内容被嵌入另一个已认证产品表面。两条检查项必须落实——操作可见性(编辑、分享等入口可能应隐藏)和可用宽度(宿主容器比标准页窄,布局断点与栅格宽度都要按实际宽度计算)。Builtin 的枚举注释还补充了一条语义:外部控件由父上下文提供。
  • Public:公开分享表面。只读;必须屏蔽作者(authorship)、文件夹、私有配置以及“强制刷新”这类依赖登录态会话的操作。
  • Export:导出渲染等价于“打印”。不允许出现交互控件(按钮、下拉、刷新按钮等),也不能假设存在浏览器用户会话——从源码结构看,该表面由 Exporter.tsxExporterDashboardScene.tsx 承载,运行在无登录态的导出管线中。
  • FeatureFlag / Group:嵌入在产品详情上下文里。必须检查宿主页面(功能标志详情页、群组页)、URL 状态(宿主路由的参数如何映射到仪表盘过滤器/变量)、权限(宿主页面可见是否等同于仪表盘可见)与刷新行为(嵌入表面通常不应沿用标准页的自动刷新策略)。

技能主文档 SKILL.md 的“请求路由”一节进一步要求:编码前必须对七个表面——已认证仪表盘、公开分享、嵌入式、导出、产品内嵌、模板、仪表盘列表与项目主页——逐一记录 affected / unaffected / not applicable。这可以理解为对本文所述放置契约的工程化落地:先做表面影响面分析,再动手改代码。

三、所有权地图:改动落在哪些文件

文档给出的所有权地图(Ownership map)规定了各关注点的“责任文件”,这是避免改动散落错层的关键:

领域 文件
模型 dashboard.pydashboard_tile.pydashboard_templates.py
仪表盘端点 dashboard.py(API)
模板端点 dashboard_templates.py
产品路由 routes.py
场景状态 dashboardLogic.tsx
主布局 DashboardItems.tsxtileLayouts.ts
共享/导出宿主 ExporterDashboardScene.tsxExporter.tsx

上表全部路径均经核实存在于当前仓库。结合技能主文档的代码地图,还可以补全两条相邻边界:布局几何与磁贴尺寸约束的辅助逻辑在 dashboardUtils.ts,刷新默认值与共享安全钳制在 refresh_policy.py。理解所有权地图的实践价值在于:

  1. 模型层改动(新增字段、约束、软删除语义)归 products/dashboards/backend/models/ 三个文件,并伴随 Django 迁移;
  2. 契约层改动(序列化器、视图集动作)归 api/dashboard.pyapi/dashboard_templates.py,产品对外路由注册在 routes.py
  3. 前端状态改动dashboardLogic.tsx(Kea logic,负责场景状态、刷新与布局持久化);
  4. 布局渲染改动DashboardItems.tsx + tileLayouts.ts——前者是主布局容器,后者负责断点几何计算;
  5. 共享与导出宿主改动归 exporter 目录下的两个文件,不要试图在标准仪表盘场景里“顺手”修共享渲染。

这种分层与文档第二节的“跨层实现”规则一致:当持久化契约改变时,产品模型、序列化器、API 动作与生成类型要一起改;数据查找与变更必须走仪表盘所属的 team 作用域;旧行与旧布局 JSON 要当作版本化输入来保护。

四、API 契约变更的标准流程

文档最后规定了当改动触及序列化器或视图集时必须执行的流程:

  1. 添加或更新请求与响应 schema(request and response schema);
  2. 运行 hogli build:openapi 重新生成 OpenAPI 输出;
  3. 在前端代码中使用生成的 API 类型,而不是手写的类型定义;
  4. 同时测试 API 端点与 UI 契约
  5. 不要直接编辑生成的文件(Do not edit generated files directly)。

仓库中可以直接验证这一流程的接线:OpenAPI 任务定义在 hogli.yaml 中,包括 build:openapi-schemabuild:openapi-types 等任务(见 hogli.yaml)。技能主文档的配套技能表也印证了这条链路的分工:improving-drf-endpoints 负责视图集/序列化器契约与 OpenAPI 输出,adopting-generated-api-types 负责在前端消费变更后的生成类型,django-migrations 负责模型 schema 变更。测试边界的对应要求见 SKILL.md 第 5 节的清单——其中“序列化器变更后的 API schema 与生成类型”是显式的必测边界之一。

对 Agent 或工程师的实际约束可以概括为三点:生成文件永远只读(它是 schema 的编译产物);前端不得在生成类型之外私自定义仪表盘 DTO;任何序列化器字段变更都必须能回答“哪个表面会渲染这个字段、公开/导出表面是否会因此泄露内部信息”——这与第二节中 Public 表面的“不得暴露私有配置”要求是同一条安全边界的两个侧面。

五、小结:一张表看懂决策顺序

把整份文档压缩成可执行的决策顺序:

  1. 先定位数据所有者:要改的状态在 DashboardDashboardTile 还是 DashboardTemplate?widget 归另一条技能线;
  2. 再列出渲染表面:用 DashboardPlacement 枚举逐一确认 affected/unaffected,特别注意 UNLISTED 产品内嵌仪表盘与导出/公开表面的只读约束;
  3. 按所有权地图选文件:模型 → models/;契约 → api/;状态 → dashboardLogic.tsx;布局 → DashboardItems.tsx + tileLayouts.ts;共享/导出 → exporter 场景;
  4. 触碰序列化器/视图集时走契约流程:更新 schema → hogli build:openapi → 前端消费生成类型 → 双端测试,且绝不手改生成文件。

这套“领域模型 → 渲染表面 → 文件所有权 → 契约流程”的骨架,配合 SKILL.md 中的功能准入标准、变更契约清单与边界测试清单,构成了 PostHog 仪表盘系统改动从设计到验证的完整路径;本文引用的每一处实现事实都可以直接在对应仓库路径中复核。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
docsdocs
暂无描述
Markdown
900
5.83 K
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.14 K
2.75 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
860
1.35 K
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
927
1.85 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.84 K
1.02 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
533
603
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.37 K
1.46 K
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
548
397
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
1.04 K
525