Repository navigation
Conversation
🤖 CI report✅ Complexity (TypeScript) — cleanCyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions. ✅ Bundle size — no changeUncompressed size of every built Total: 76.03 MiB · no change No file changed by more than 1000 B. Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report
|
| Root | Eager (shipped) | Δ vs base | Budget |
|---|---|---|---|
entry (logged-out pages, app bootstrap)src/index.tsx |
1.01 MiB · 24 files | no change | █████████░ 88.6% of 1.14 MiB |
logged-out boot: index + App + bootApp (preloaded by every page, including /login)src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts |
2.94 MiB · 620 files | no change | ██████████ 96.9% of 3.03 MiB |
🟡 authenticated shell (every logged-in page)src/scenes/AuthenticatedShell.tsx |
7.65 MiB · 2,633 files | no change | ██████████ 102.1% of 7.50 MiB |
dashboard scenesrc/scenes/dashboard/Dashboard.tsx |
9.68 MiB · 3,703 files | no change | ██████████ 96.7% of 10.01 MiB |
🟡 today home pathsrc/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx |
8.06 MiB · 2,977 files | no change | ██████████ 104.1% of 7.74 MiB |
AI-first home pathsrc/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx |
7.90 MiB · 2,706 files | no change | █████████░ 88.1% of 8.96 MiB |
events scenesrc/scenes/activity/explore/EventsScene.tsx |
9.15 MiB · 3,495 files | no change | ██████████ 95.4% of 9.59 MiB |
web analytics scenesrc/scenes/web-analytics/WebAnalyticsScene.tsx |
10.01 MiB · 3,713 files | no change | █████████░ 92.4% of 10.83 MiB |
replay detail scenesrc/scenes/session-recordings/detail/SessionRecordingDetail.tsx |
8.22 MiB · 3,075 files | no change | █████████░ 86.8% of 9.47 MiB |
🟢 node_modules/monaco-editor/ stays out of src/index.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/index.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/svg/ stays out of src/index.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/components/ stays out of src/index.tsx
🟢 node_modules/monaco-editor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/layout/navigation-3000/navigationLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/dashboard/dashboardLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/lemon-ui/LemonMarkdown/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/RichContentEditor/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/CodeSnippet/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/taxonomy/core-filter-definitions-by-group.json stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/zod/ stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/scenes/billing/billingLogic.tsx stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 src/lib/components/ProductEmptyState/productSetupPreloadLogic.ts stays out of src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
🟢 node_modules/monaco-editor/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/lib/components/ActivityLog/describers stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/dashboards/frontend/widgets/previews/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/player/sessionRecordingPlayerLogic.ts stays out of src/scenes/AuthenticatedShell.tsx
🟢 products/wizard/frontend/runs/WizardRunDetailsDrawer.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 zod/v4/locales/de.js stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/svg/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 node_modules/@posthog/brand/dist/generated/hoggies/components/ stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/queries/validators.js stays out of src/scenes/dashboard/Dashboard.tsx
🟢 src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/scenes/project-homepage/today/TodayReportPage.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 src/queries/Query/Query.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
🟢 products/posthog_ai/frontend/components/tool/widgets/VisualizationWidget.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 products/posthog_ai/frontend/components/tool/widgets/RecordingsWidget.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 src/queries/Query/Query.tsx stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 products/posthog_ai/frontend/api/primitives.ts stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 src/scenes/notebooks/Notebook/notebookLogic.ts stays out of src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
🟢 src/scenes/session-recordings/playlist/SessionRecordingsPlaylist.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/scenes/web-analytics/tiles/WebAnalyticsTile.tsx stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/queries/validators.js stays out of src/scenes/activity/explore/EventsScene.tsx
🟢 src/scenes/web-analytics/LiveMetricsDashboard/LiveWebAnalyticsMetrics.tsx stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 products/product_analytics/frontend/insights/trends/WorldMap/countryVectors.tsx stays out of src/scenes/web-analytics/WebAnalyticsScene.tsx
🟢 src/scenes/settings/SettingsMap.tsx stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
🟢 @jspm/core/nodelibs/browser/fs stays out of src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
Largest files eagerly shipped from src/index.tsx
| Size | File |
|---|---|
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 24.6 KiB | ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js |
| 6.3 KiB | ../node_modules/.pnpm/react@18.3.1/node_modules/react/cjs/react.production.min.js |
| 4.5 KiB | ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js |
| 3.9 KiB | ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js |
| 1.4 KiB | ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js |
| 1.4 KiB | src/index.tsx |
| 1.3 KiB | src/RootErrorBoundary.tsx |
| 1.3 KiB | src/lib/utils/bootTiming.ts |
| 912 B | ../node_modules/.pnpm/ieee754@1.2.1/node_modules/ieee754/index.js |
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 69.4 KiB | src/lib/lemon-ui/icons/icons.tsx |
| 38.7 KiB | ../node_modules/.pnpm/@dnd-kit+core@6.0.8_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@dnd-kit/core/dist/core.esm.js |
| 33.9 KiB | ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js |
| 29.4 KiB | src/scenes/scenes.ts |
| 26.9 KiB | src/lib/constants.tsx |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 143.2 KiB | ../packages/quill/packages/primitives/dist/index.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Largest files eagerly shipped from src/scenes/dashboard/Dashboard.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 143.2 KiB | ../packages/quill/packages/primitives/dist/index.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 143.2 KiB | ../packages/quill/packages/primitives/dist/index.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 143.2 KiB | ../packages/quill/packages/primitives/dist/index.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Largest files eagerly shipped from src/scenes/activity/explore/EventsScene.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 143.2 KiB | ../packages/quill/packages/primitives/dist/index.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Largest files eagerly shipped from src/scenes/web-analytics/WebAnalyticsScene.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 183.2 KiB | src/queries/validators.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 143.2 KiB | ../packages/quill/packages/primitives/dist/index.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
Largest files eagerly shipped from src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
| Size | File |
|---|---|
| 321.9 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs |
| 316.1 KiB | ../node_modules/.pnpm/posthog-js@1.438.18_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js |
| 286.8 KiB | src/taxonomy/core-filter-definitions-by-group.json |
| 238.5 KiB | ../packages/quill/packages/quill/dist/index.js |
| 220.9 KiB | ../node_modules/.pnpm/@posthog+icons@0.38.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js |
| 153.7 KiB | ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js |
| 126.8 KiB | ../node_modules/.pnpm/react-dom@18.3.1_react@18.3.1/node_modules/react-dom/cjs/react-dom.production.min.js |
| 106.3 KiB | src/products.tsx |
| 98.9 KiB | src/lib/api.ts |
| 93.3 KiB | ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js |
Posted automatically by check-eager-graph · sizes are eager output bytes (shipped, post-tree-shake) from the esbuild metafile · part of PostHog#32479
✅ Toolbar bundle — eager 2.29 MiB within budget
What the toolbar ships to customer pages, measured from the esbuild output (minified, post-tree-shake). The eager set is the entry plus everything statically imported from it — fetched before any feature runs; deferred chunks load lazily. The eager guardrail is 5.72 MiB. Each output file must also stay below 10 MB, where CloudFront stops compressing it. The module boundary is enforced separately by check-toolbar-graph.
| Metric | Size | Δ vs base | Budget |
|---|---|---|---|
| Eager (shipped) entry + static imports |
2.29 MiB · 18 files | no change | ████░░░░░░ 39.9% of 5.72 MiB |
| Deferred (lazy) | 2.11 MiB · 40 files | no change | n/a — loads on demand |
Loader dist/toolbar.js |
1.2 KiB | no change | █░░░░░░░░░ 6.0% of 19.5 KiB |
Largest eagerly-shipped chunks
| Size | File |
|---|---|
| 899.4 KiB | dist/toolbar/toolbar-app-REMKF2SV.css |
| 691.4 KiB | dist/toolbar/chunk-chunk-DDSP4M6B.js |
| 260.7 KiB | dist/toolbar/chunk-chunk-HRRIOF34.js |
| 131.8 KiB | dist/toolbar/chunk-chunk-FDH2IBXT.js |
| 130.4 KiB | dist/toolbar/chunk-chunk-IT6XUU73.js |
| 74.8 KiB | dist/toolbar/toolbar-app-YPZZ2PIS.js |
| 69.0 KiB | dist/toolbar/chunk-chunk-TSAL54PB.js |
| 35.6 KiB | dist/toolbar/chunk-chunk-CGDYYVIR.js |
| 21.7 KiB | dist/toolbar/chunk-chunk-PM4VBMO5.js |
| 6.8 KiB | dist/toolbar/chunk-chunk-DV7IWQNF.js |
Posted automatically by check-toolbar-size · sizes are toolbar output bytes (shipped, post-tree-shake) from the esbuild metafile
✅ Dist folder size — 🔺 +5.4 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 1020.76 MiB · 🔺 +5.4 KiB (+0.0%)
ℹ️ MCP UI apps size — 34 app(s), 27964.5 KB JS
Built size of each MCP UI app (main.js + styles.css).
| App | JS | CSS |
|---|---|---|
| debug | 844.2 KB | 209.0 KB |
| action | 767.6 KB | 209.0 KB |
| action-list | 835.3 KB | 209.0 KB |
| cohort | 766.6 KB | 209.0 KB |
| cohort-list | 834.2 KB | 209.0 KB |
| email-template | 766.5 KB | 209.0 KB |
| error-details | 771.5 KB | 209.0 KB |
| error-issue | 768.0 KB | 209.0 KB |
| error-issue-list | 835.8 KB | 209.0 KB |
| experiment | 832.4 KB | 209.0 KB |
| experiment-list | 836.0 KB | 209.0 KB |
| experiment-results | 834.9 KB | 209.0 KB |
| feature-flag | 834.6 KB | 209.0 KB |
| feature-flag-list | 838.4 KB | 209.0 KB |
| feature-flag-testing | 769.9 KB | 209.0 KB |
| inline-scan | 767.1 KB | 209.0 KB |
| insight-actors | 833.7 KB | 209.0 KB |
| llm-costs | 831.0 KB | 209.0 KB |
| session-recording | 768.9 KB | 209.0 KB |
| survey | 768.2 KB | 209.0 KB |
| survey-global-stats | 831.1 KB | 209.0 KB |
| survey-list | 835.9 KB | 209.0 KB |
| survey-stats | 831.0 KB | 209.0 KB |
| trace-span | 767.0 KB | 209.0 KB |
| trace-span-list | 835.1 KB | 209.0 KB |
| vision-observation-list | 834.3 KB | 209.0 KB |
| workflow | 767.0 KB | 209.0 KB |
| workflow-list | 834.6 KB | 209.0 KB |
| workflow-setup-status | 766.7 KB | 209.0 KB |
| query-results | 1086.5 KB | 209.0 KB |
| render-ui | 1161.5 KB | 209.0 KB |
| visual-review-snapshots | 771.5 KB | 209.0 KB |
| workflow-data-suggestions | 769.3 KB | 209.0 KB |
| workflow-sending-rules | 768.0 KB | 209.0 KB |
638ce6b to
a012511
Compare
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Untagged reads raise under DEBUG, so the status endpoint answered 500 in a dev stack. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re ids The recipient lookup hashes the address with every configured salt key, so emails recorded before a key rotation still show up. A message id whose time is after now answers not found instead of a server error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GET /api/projects/@current/messaging/emails/?to= lists the status of each email sent to an address in the last 30 days, newest first, a page at a time. It takes the status read's auth, scope and throttles. The page cursor is opaque and never holds the address, and a refused lookup never repeats it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a012511 to
987b927
Compare
|
Closing: superseded by PostHog#123932 (status read by message id) and the |
Moved here from #121898, closed on 2026-10-25 so this fork stack is the only copy. It opens upstream again after review.
Problem
202. Contract #120921 promisesGET emails/{message_id}, which posthog-node'semails.getcalls.message_idhas no way to find an email sent to an address. The lookup must never store the address in the status table.Impact
Pays into Grow Workflows through billed
workflows_emailsactions per paying org (not measured yet as a monthly count). In scope: Messaging, email API sends. Expected move: small, up: a caller can confirm delivery, which an API that replaces another provider needs. Run cost: oneargMaxquery on AUX per request, under read throttles at the send's rates. A read by id is a point read. A lookup filters on the recipient hash's skip index inside 30 days.Merge Danger
Warning
🟡 Merge risk: medium · two-way door: two new read-only routes and read functions; a revert removes them (merge-risk rubric)
hog_flow:readreads the status of every transactional email in it, by id or by recipient.team_id. Tests read another team's id as404and leave another team's email out of a lookup.workflows-transactional-email-apiflag, take only a project secret API key, and answer401to a session or a personal API key.Changes
What a caller can now do
GET /api/projects/@current/messaging/emails/{message_id}/, with a project secret API key that hashog_flow:read:{ "id": "…", "status": "delivered", "created_at": "2026-10-22T09:00:00Z", "status_at": "2026-10-22T09:00:03Z" }reasonappears forskipped,failed,bouncedandcomplained, such asrecipient_suppressed,provider_errororhard.404 not_foundwith why and fix.401, with the same body as a send without credentials.GET /api/projects/@current/messaging/emails/?to=<address>lists the emails sent to an address in the last 30 days, newest first. It takes the same key, scope and throttles as the read by id.{ "results": [...], "next_cursor": ... }. Each item has the shape of the read by id.limittakes 1 to 100, default 50.next_cursoris the last email'smessage_id, andnullon the last page. The next page passes it ascursor, withtoagain.toaddress of a send, whatever its case and surrounding spaces. An email that went to the address as accorbcccopy is not listed.400 invalid_requestwithattron that field. A cursor that is not a message id any email can have is refused this way. No error repeats the address.200with emptyresults, never404.How it works
get_email_status(team_id, message_id)runs oneargMax(…, version)query onemail_status, filtered onteam_idandmessage_id. It reads only the minute around the message id's UUIDT time, so ClickHouse prunes to that day's partition.Nonewithout a query. The table keeps rows for 31 days, so the read enforces the 30 days.list_email_statuses_by_recipient(team_id, recipient, *, since, limit, cursor)returns the latest status of each email sent to the address sincesince, newest first.cursoris themessage_idof the last record of a page. The service takes the cursor'screated_atfrom its UUIDT time, in milliseconds, so two emails in the same second split across pages without a loss. It hashes the address with every configured salt key, so emails recorded before a key rotation still match, and filters onrecipient_hash IN (...), which the table's skip index serves.message_created_at, and theEmailStatusandEmailStatusRecordcontracts are on the facade,products/messaging/backend/facade/email_status.py.MessagingEmailsViewSet, registered atmessaging/emails. Each read action checks for a project secret API key, because the routing mixin adds user credentials to every route.transactional_email_read_*scopes.How did you test this code?
Test rationale: each case below failed before its change and passes after it. Of the first eight, seven ran against stubs, and the non-UUID case passed before, as the router's own
404.Test seams:
email_status_data:queued, thendelivered, then a latersentreads asdeliveredwith itsstatus_at.404 not_foundwith why and fix.hog_flow:readanswers403, on the read by id and on the lookup.401on both reads.429, and a send with the same key still answers202.jane.doe@example.comcome back forJane.Doe@Example.COM, newest first, two per page. Two of them fall in the same second across the page boundary, and the second page still lists the older one. The last page ends with anullcursor, and no cursor contains the address.400on their field and never repeat the address: no address, not an address, a page over 100, a cursor that is not a UUID, and a UUID no email can have.Local gate at
987b927630e, this head, all green: pytest 1764, ClickHouse 460, MCP 5035, repo-wide mypy, and the rest of the gate. The pre-push preflight ran in strict mode with 0 failures.Cross-version: none owed. The read adds no queued or serialized format.
Live run at
987b927630e(Django, cdp-api and the email worker on a migrated clone of the local test database, with ClickHouse and maildev), case 07:GET messaging/emails/<message_id>/every 2 seconds200,sentat the first read, two seconds after the sendGET messaging/emails/?to=jane@example.com200, the three emails sent to that address, newest first,next_cursor: nullGET messaging/emails/?to=copy@example.com, a cc of those sends200, emptyresults404 not_found, with why, fix anddocs_urlGaps:
Release status
Docs update
None. The transactional email API docs page is written after the stack lands.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5)list_email_statuses_by_recipienton the facademessaging/emails, over growing the send viewsetGET messaging/emails/?to=, over a separate follow-upmessage_id, and the service reads its time from the UUIDT. Alternatives: a cursor ofcreated_atand id, or a next URL that would repeat the address.Open:
ccandbcccopies? The status row stores one hash, ofto, so copies need a schema change. Recommendation: matchtoonly for now.?to=, as the slice asked. The alternative is aPOSTwith the address in the body. Recommendation: keep?to=, as the slice asked.Review (GPT-6.1 Sol, round 1 over the whole stack, then a final read of the fixes):
500404without a query. Test:ffffffff-ffff-4fff-8fff-ffffffffffff.The stack's QA swarm then reviewed this layer. Round 1 fixed seven items, among them project secret keys only on both reads (
401otherwise) and point-read tests that would age against the clock. Round 2 found only this body's stale line counts. The rebase check in final read 3 found nothing.Involvement: strand conductor relays answered, one choice from Michael on replay scope, no grilling rounds. Reviews: GPT-6.1 Sol round 1 over the whole stack and a final read of the fixes. Stack QA swarm: design pass (GPT-6.1 Sol high); round 1 (review and team lanes GPT-6.1 Sol high, fresh eyes Claude Opus 5.5 high); round 2 on the fixes (review lane GPT-6.1 Sol high, fresh eyes Claude Opus 5.5 high); final read 3 over the rebase (GPT-6.1 Sol high, clean); fixers Claude Opus 5.5 high; live run at this head.
/tdd,/improving-drf-endpoints,/writing-tests,/writing-code-comments,/writing-pr-descriptions.example.comdomains.🤖 Generated with Claude Code