PostHog 仪表盘后端契约与运维软删除生命周期、SSE 渐进加载与 REST/MCP 双契约【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog本文基于 PostHog 仓库中仪表盘技能的参考文档 backend-contracts-and-operations.md系统梳理 PostHog Dashboard 平台在后端侧需要遵守的六类契约资源软删除生命周期、项目树与跨项目迁移、列表与产品内嵌仪表盘、REST/OpenAPI/MCP 多消费者契约、SSE 流式渐进交付以及限额、审计与订阅去重等运维约束。读完本文你将能结合 Dashboard 模型、DashboardTile 模型 与 Dashboard API 的源码证据理解每条契约背后的实现依据并在改动仪表盘后端时预判其影响面。1. 资源生命周期Dashboard 与 Tile 都是软删除资源原文档的第一条核心规则是仪表盘和图块tile使用软删除不要用硬删除替代它。这条规则在源码中有完整的落地形态。1.1 双 Manager 设计默认视图与恢复路径分离Dashboard与DashboardTile都定义了成对的 ManagerDashboard.objects是DashboardManager其get_queryset()直接exclude(deletedTrue)即正常业务查询永远看不到已删除的仪表盘见 dashboard.py#L38-L40Dashboard.objects_including_soft_deleted是不过滤删除标记的RootTeamManager专门支撑“恢复restore”路径见 dashboard.py#L108-L109DashboardTile同理objects排除了deletedTrue且排除dashboard__deletedTrue的行objects_including_soft_deleted则暴露全部行见 dashboard_tile.py#L111-L112。这个模式意味着任何恢复类操作撤销删除、复制回原位置必须显式走objects_including_soft_deleted。源码中大量注释提醒开发者默认 Manager 排除deletedTrue后通过反向关联如tile.insight遍历会得到None必须用未过滤的 Manager 直接查询见 dashboard.py#L2287-L2304。1.2 删除、恢复与级联细节删除仪表盘可级联删除 insight当请求显式要求时删除仪表盘会连同其 tile 关联的 Insight 一起软删除API 层还处理了“恢复仪表盘时把被一起删掉的 insight 也恢复”的逻辑_undo_delete_related_tiles见 dashboard.py#L2287-L2315。删除 tile 保留底层内容DashboardTile是关联行而非内容本体删除 tile 只是把关联行标记deletedTrue其引用的Insight、Text、ButtonTile或DashboardWidget若被其他关系引用则继续存活。copy_to_dashboard的注释也印证了这一点复制 tile 时若目标已存在同一内容的软删除行走“解除删除”而不是二次插入以避免唯一约束冲突见 dashboard_tile.py#L219-L266。移动/复制必须保持“一个 tile 恰好一个关联对象”的约束与目标权限模型层用四条条件唯一约束加一条CheckConstraintdash_tile_exactly_one_related_object保证 insight/text/button_tile/widget 四选一见 dashboard_tile.py#L114-L141。API 层move_tile会检查目标仪表盘在同一 project 内、调用者对目标有编辑权限并校验公开链接不会暴露调用者无权执行的查询见 dashboard.py#L2832-L2880。模型层的prepare_move_to_dashboard还会清理目标端占用同一唯一键的软删除行遇到非删除行则拒绝移动见 dashboard_tile.py#L190-L217。多行变更使用窄的transaction.atomic()块move_tile、copy_tile、仪表盘复制等 API 都在单一with transaction.atomic():内完成“改 tile 归属 写日志”等组合操作见 dashboard.py#L2864、dashboard.py#L2915。1.3 批量更新绕过信号必须显式同步依赖资源这是原文档中一条很容易踩坑的规则Django 的bulk_update()/QuerySet.update()不触发模型信号因此依赖信号完成的副作用如项目树同步不会发生必须在批量更新后显式补齐。仓库源码中留下了直接的注释证据删除关联 insight 时先update(deletedTrue)随后手工调用_sync_filesystem_for_insights重新同步 FileSystem否则“Recents 侧边栏会残留陈旧条目点进去是 404”恢复路径上bulk_update后同样补一次同步见 dashboard.py#L2276-L2315。改动生命周期行为前原文档要求先阅读 dashboard_tile.py 和 dashboard.py 两个文件——本文上述结论均可在这两个文件中逐条核对。2. 项目树、标签与资源迁移resource transfer原文档指出Dashboard 不只是 API 里的一行数据它是项目树project tree资源。这一点在模型定义中一目了然Dashboard继承了FileSystemSyncMixin、ModelActivityMixin与RootTeamMixin见 dashboard.py#L43。由此产生一组运维约束创建、改名、移动、删除、恢复都可能更新文件系统条目。get_file_system_representation()返回typedashboard的文件系统表示且creation_mode template的模板仪表盘标记为should_deleteTrue不出现在项目树中见 dashboard.py#L139-L152。共享响应中保持文件夹数据私有共享/嵌入等受限表面不能把项目树结构暴露给未授权查看者。自定义序列化路径绕过 mixin 时要保持标签行为一致API 层对标签采用独立的创建/记录逻辑标签作为全局 tag 关系单独建立并单独记录 Dashboard 作用域的活动日志见 dashboard.py#L1556 与 dashboard.py#L2457-L2459。新增可跨项目复制的持久化字段时要更新 resource-transfer visitor。visitor 声明了excluded_fields即“不随迁移走的派生状态”。当前DashboardVisitor排除的正是分享、刷新、访问、缓存类字段class DashboardVisitor( ResourceTransferVisitor, kindDashboard, excluded_fields[ data_color_theme_id, data_color_theme, analytics_dashboards, last_refresh, last_accessed_at, share_token, is_shared, ], ):见 dashboard.pyvisitor。DashboardTileVisitor则排除filters_hash、last_refresh、refreshing、refresh_attempt四个纯缓存/刷新状态字段见 dashboard_tile.pyvisitor。这与原文档“排除分享、刷新、访问、缓存字段”的表述逐一对应。校验迁移结果迁移后的仪表盘必须有合法的 team 归属且不能携带源项目独有的标识符。3. 列表、发现与产品创建的仪表盘原文档强调Dashboard 列表是一套与详情独立的契约必须保留 pinned 排序、搜索、标签、文件夹数据与未列出unlisted仪表盘的排除行为。源码中的列表契约集中在DashboardViewSet基础排序即order_by(-pinned, name)配合条件索引idx_dashboard_deleted_team_id-pinned, name, deleted, team_idconditiondeletedFalse使排序走索引而非逐行查询见 dashboard.py#L113-L120 与 dashboard.py#L2459filter_queryset统一处理search带MAX_SEARCH_LENGTH上限校验、tags与folder参数文件夹过滤通过相关子查询匹配posthog_file_system中“直接位于该文件夹下”的条目利用posthog_fs_team_s_typeref索引完成避免了对每行仪表盘单独发起文件系统或标签查询见 dashboard.py#L2474-L2502列表请求额外用DashboardBasicSerializer而非详情序列化器降低开销见 dashboard.py#L2464-L2465。读操作会写last_accessed_at。每次仪表盘读取都会调用record_dashboard_view(dashboard, access_method)记录访问员工伪装身份时跳过访问方法用于区分人工/共享/嵌入/API 来源见 access.py#L43 与 dashboard.py#L2709-L2714。因此原文档提醒新增轮询、嵌入等读路径时要评估写放大。产品创建的未列出仪表盘Dashboard.CreationMode.UNLISTED注释为 “Product dashboards (e.g. AI observability) - hidden from general lists, accessed via tag queries”就是该契约的落点见 dashboard.py#L54-L57。这类仪表盘需要稳定的查找数据和并发创建保护同产品重复创建同一仪表盘时去重并且除非产品明确暴露否则不出现在普通列表中。3.1 持久化的列表状态原文档给出两条边界规则持久化的列表配置要与仪表盘元数据分开存储只持久化能重建列表的值不持久化临时 UI 状态。仓库中对应的真实模型是 dashboard_saved_view.py迁移 0016_dashboardsavedview.pyAPI 在 dashboard_saved_view.py 与 test_dashboard_saved_views.py 中有独立测试可作为该契约的验证参照。4. API、Schema 与 MCP 契约原文档把仪表盘行为定义为同时服务 REST、前端生成类型与 MCP 三类消费者并给出 7 步操作清单。结合仓库可把它落到具体文件与命令每个新增请求/响应字段都要加序列化器 schema 注解DRFextend_schema/SerializerMethodField注解OpenAPI 输出测试见 test_dashboard_openapi.py契约变更后运行hogli build:openapi该任务族build:openapi-schema、build:openapi-types、build:openapi-mcp等定义在 hogli.yaml#L513-L545MCP 操作定义在 products/dashboards/mcp/tools.yaml文件头注释说明工具条目由 OpenAPI schema 脚手架生成pnpm --filterposthog/mcp run scaffold-yaml -- --sync-all启用工具时必须写明scopes与annotations。例如dashboard-create工具声明了dashboard:write作用域和readOnly: false, destructive: false, idempotent: false注解见 tools.yaml#L10-L22OpenAPI 操作或工具定义变化后重新生成 MCP 代码同上 scaffold 命令族检查 API 作用域读、写、执行查询使用不同 scopeAPI 代码中通过required_scopes声明如move_tile要求dashboard:writedashboard.py#L2826subscribe_nudge同样要求dashboard:writedashboard.py#L3694一次性 filter/variable 覆盖保持非持久化stream_tiles与查询端点均通过FILTERS_OVERRIDE_PARAM/VARIABLES_OVERRIDE_PARAM接收覆盖参数除非端点显式持久化否则不写回模型保持共享 token 规则共享请求忽略仪表盘级 filter 与 variable 覆盖。原文档还特别指出MCP 响应可以有意省略 REST 端点返回的字段。这在tools.yaml中有直接体现——dashboard-create用exclude_params排除creation_mode、last_refresh、last_accessed_at、deleted等字段描述中明确“返回的 tiles 省略 insight 结果以节省上下文”见 tools.yaml#L46-L58。因此REST 与 MCP 行为必须分开测试不能假设两者响应字段一致。5. SSE 流式交付stream_tiles的渐进加载契约stream_tiles端点/projects/:id/dashboard/:id/stream_tiles通过 Server-Sent Events 先发送仪表盘元数据、再逐个发送 tile。原文档的 8 条契约在 dashboard.py#L2684-L2825 中逐条可见元数据先于其余 tile 发出实现上甚至把前 2 个 tile 直接内嵌进type: metadata事件减少首屏往返tile 顺序对选定的布局尺寸保持稳定按layouts[layoutSize].y再x排序无效值回退为smlayoutSize参数同时接受layout_size兼容别名见 dashboard.py#L2750-L2757 与 dashboard.py#L2818-L2830模型层还封装了同语义的DashboardTile.sort_tiles_by_layout无布局的 tile 以id打破平局避免数据库返回顺序漂移见 dashboard_tile.py#L268-L284单个 tile 序列化失败只产生 tile 级错误不中断整个流失败时仍发送同形状的type: tile事件、内部带error字段——源码注释解释了为什么不能发type: error前端会把该事件路由到onError临时 toast并丢弃 tile而 tile 级错误对象能渲染成占位错误块见 dashboard.py#L2780-L2805全部 tile 发送后发出完成事件{type: complete}见 dashboard.py#L2806-L2809同时支持 ASGI 与 WSGI 交付路径根据settings.SERVER_GATEWAY_INTERFACE决定直接消费 async generator 还是用async_to_sync包装见 dashboard.py#L2814-L2821不要在 async generator 里直接做数据库工作实现把每个 tile 的同步序列化包进database_sync_to_async(..., thread_sensitiveTrue)数据库操作全部前置到进入生成器之前完成对象获取、访问记录、序列化器上下文见 dashboard.py#L2710-L2758上下文标记计算面流式路径写入ComputeSurface.DASHBOARD_STREAM区别于详情的DASHBOARD_DETAIL与变更的DASHBOARD_MUTATE后者在 dashboard.py#L2470-L2476 按 action 动态设置改刷新默认值前检查chained_dashboard_tile_refresh门控与所有相关ComputeSurface仪表盘详情路径中该特性按组织维度feature_enabled判定见 dashboard.py#L2353-L2358。原文档的收尾建议同样值得作为设计原则把普通 retrieve 端点与stream_tiles视为两条独立的读契约分别设计与测试。前端消费侧生成的调用见 api.ts#L866-L867。6. 限额、门控与滥用抵抗原文档要求新增任何创建、加载或执行仪表盘工作的路径之前先检查限额。仓库中的具体锚点仪表盘创建数量限额创建时统计Dashboard.objects.filter(team_id..., deletedFalse).count()注意软删除的行不计入配额并调用check_count_limit(team, LimitKey.MAX_DASHBOARDS_PER_TEAM, ...)见 dashboard.py#L1546-L1552widget 数量限额_check_dashboard_widget_count_limit在复制/添加 widget tile 前检查见 dashboard.py#L1323-L1324 与 dashboard.py#L1769公共与嵌入访问不得绕过配额和产品访问检查widget tile 操作会检查dashboard_widgets_enabled特性与_check_widget_tile_product_access见 dashboard.py#L2854-L2862请求负载、tile ID、filter 尺寸、分页在到达查询执行前必须被约束列表搜索长度受MAX_SEARCH_LENGTH校验见 dashboard.py#L2478-L2483。原文档还指出默认分页加载仅当结果有界且功能必需时才全量加载widget 相关的限额、门控与节流细节应参阅manage-dashboard-widgets技能。7. 审计、分析、订阅与可观测性原文档把仪表盘变更定位为“产品事件 审计事件”并列出五条保持项每条都能在源码中验证保持模型活动日志Dashboard继承ModelActivityMixindashboard.py#L43产品侧还有 activity_logging.py保持用户行为事件tile/仪表盘的创建、更新、删除与 filter 变更对应posthog分析事件与活动日志记录访问与缓存指标按 human / shared / embedded / API 分类record_dashboard_view以dashboard_access_method区分访问来源stream 与 retrieve 路径都会传递该值见 access.py#L43保持端点监控并评估 SLO 覆盖DashboardViewSet的各 action 均包裹tracer.start_as_current_span如 dashboard.py#L2504、dashboard.py#L2531列表路径还额外记录dashboard.search.result_count/dashboard.search.empty两个 span 属性用于调优搜索相似度阈值见 dashboard.py#L2505-L2512订阅与订阅提示subscribe nudgesubscribe_nudge端点实现了原文档强调的双重去重——先用safe_cache_add写入缓存哨兵dashboard_subscribe_nudge:{user}:{dashboard}做短期去重再用has_been_dispatched(...)做持久化兜底缓存被清空或 Redis 故障时不能放行第二次创建通知失败或无接收人时还会回滚哨兵避免“一次机会”被静默烧掉见 dashboard.py#L3694-L3742 与测试 test_dashboard_subscribe_nudge.py。8. 后端测试矩阵原文档最后给出了一张“变更类型 → 测试边界”矩阵建议将其作为改动仪表盘后端时的回归清单变更类型应覆盖的测试边界持久化模型字段迁移、序列化器、OpenAPI、生成类型、MCP schema持久化列表状态适用载荷、稳定分页、列表位置、权限、MCP 决策创建或删除团队配额、软删除、活动日志、文件系统同步、恢复移动、复制或重复源/目标访问权限、事务回滚、tile 唯一性读端点REST 与流式行为、共享数据净化、缓存策略、错误载荷查询端点作用域、节流、访问方式、缓存结果、取消、部分失败模板或迁移旧载荷、源项目专属引用、目标团队、被排除的派生字段订阅路径权限、重复投递、缓存丢失、通知失败矩阵中涉及的测试资产在仓库中已有对应物OpenAPI 契约测试 test_dashboard_openapi.py、访问权限测试 test_access.py、widget 节流测试 test_widget_query_throttle.py 与订阅提示测试 test_dashboard_subscribe_nudge.py。小结这套后端契约的共同主题是仪表盘是一个被多方消费的活资源——它同时活在项目树、文件系统索引、REST/OpenAPI/MCP 三种接口、SSE 流与审计/订阅链路中。原文档的每一条规则软删除不硬删、批量更新后显式同步、列表与详情分治、共享请求忽略覆盖参数、MCP 与 REST 分开测、流式单 tile 失败不拖垮全流、双去重订阅提示都对应 products/dashboards/backend/ 中可核对的源码实现。改动前对照本文的源码路径逐条确认影响面是控制这类“多表面”功能回归成本的最直接方式。【免费下载链接】posthog:hedgehog: PostHog is the leading platform for building self-driving products. Our developer tools – AI observability, analytics, session replay, flags, experiments, error tracking, logs, and more – capture all the context agents need to diagnose problems, uncover opportunities, and ship fixes. Steer it all from Slack, web, desktop, or the MCP.项目地址: https://gitcode.com/GitHub_Trending/po/posthog创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考