Skip to content

feat(messaging): read a transactional email's status by id or recipient - #562

Closed
Silthus wants to merge 22 commits into
feat/email-status-recordfrom
feat/email-status-read
Closed

Silthus wants to merge 22 commits into
feat/email-status-recordfrom
feat/email-status-read

Conversation

@Silthus

@Silthus Silthus commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

Moved here from #121898, closed on 2026-10-25 so this fork stack is the only copy. It opens upstream again after review.

Problem

  • A caller of the transactional email API cannot read what happened to an email after the 202. Contract #120921 promises GET emails/{message_id}, which posthog-node's emails.get calls.
  • A caller who did not keep the message_id has no way to find an email sent to an address. The lookup must never store the address in the status table.
  • The contract does not list the lookup yet. An amendment for it waits for approval before it is posted on #120921.
  • Map: Silthus/posthog#461, slice 08 of strand Silthus/posthog#535, layer 3 of 3.

Impact

Pays into Grow Workflows through billed workflows_emails actions 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: one argMax query 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)

  • Two new public read routes cover a whole project: any project secret API key with hog_flow:read reads the status of every transactional email in it, by id or by recipient.
  • A wrong query would show a wrong status or another team's email. Every query filters on team_id. Tests read another team's id as 404 and leave another team's email out of a lookup.
  • What limits it: both routes sit behind the workflows-transactional-email-api flag, take only a project secret API key, and answer 401 to a session or a personal API key.

Changes

Kind Files Lines
Production 5 +402 −0
Tests 2 +276 −0
Generated types and snapshots 3 +220 −20
Total (this layer) 10 +898 −20

What a caller can now do

  • GET /api/projects/@current/messaging/emails/{message_id}/, with a project secret API key that has hog_flow:read:
{ "id": "…", "status": "delivered", "created_at": "2026-10-22T09:00:00Z", "status_at": "2026-10-22T09:00:03Z" }
  • reason appears for skipped, failed, bounced and complained, such as recipient_suppressed, provider_error or hard.
  • An unknown id, another project's id, an id older than 30 days, an id whose time is after now, or an email sent a few seconds ago answers 404 not_found with why and fix.
  • Both reads take only a project secret API key. A session or a personal API key answers 401, with the same body as a send without credentials.
  • Both reads have their own throttles, at the send's rates. Polling a status never uses up the budget for sending.
  • 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.
  • The lookup answers { "results": [...], "next_cursor": ... }. Each item has the shape of the read by id.
  • limit takes 1 to 100, default 50. next_cursor is the last email's message_id, and null on the last page. The next page passes it as cursor, with to again.
  • The lookup matches only the to address of a send, whatever its case and surrounding spaces. An email that went to the address as a cc or bcc copy is not listed.
  • An address, page size or cursor the lookup can't use answers 400 invalid_request with attr on that field. A cursor that is not a message id any email can have is refused this way. No error repeats the address.
  • An address with no emails answers 200 with empty results, never 404.

How it works

  • get_email_status(team_id, message_id) runs one argMax(…, version) query on email_status, filtered on team_id and message_id. It reads only the minute around the message id's UUIDT time, so ClickHouse prunes to that day's partition.
  • An id older than 30 days answers None without 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 since since, newest first. cursor is the message_id of the last record of a page. The service takes the cursor's created_at from 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 on recipient_hash IN (...), which the table's skip index serves.
  • The address hash matches the Node hash: the same purpose string, the same key, trim then lowercase.
  • Both functions, message_created_at, and the EmailStatus and EmailStatusRecord contracts are on the facade, products/messaging/backend/facade/email_status.py.
  • Both routes have their own viewset, MessagingEmailsViewSet, registered at messaging/emails. Each read action checks for a project secret API key, because the routing mixin adds user credentials to every route.
  • The throttles subclass the send's four throttles with transactional_email_read_* scopes.
  • The OpenAPI schema types the lookup's answer as one page object, so the generated client does not type it as an array.

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:

  • The read route at the HTTP boundary, against the test ClickHouse with rows written straight into email_status_data:
    • The latest status wins: queued, then delivered, then a later sent reads as delivered with its status_at.
    • A bounce reads with its reason.
    • Another team's id, an unknown id, a non-UUID and an email older than 30 days answer 404 not_found with why and fix.
    • A key without hog_flow:read answers 403, on the read by id and on the lookup.
    • A session and a personal API key answer 401 on both reads.
    • Reads that use up their burst limit answer 429, and a send with the same key still answers 202.
  • The lookup route against the same rows:
    • Emails to jane.doe@example.com come back for Jane.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 a null cursor, and no cursor contains the address.
    • Another address, another team's email and an email older than 30 days stay out.
    • Five bad lookups answer 400 on 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.
    • A row hashed with an old salt key is listed after a new key goes first.
  • The rows carry the recipient hash vector the Node test asserts, as a literal, so the tests check Django's hash against Node's.

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:

Read Answer
GET messaging/emails/<message_id>/ every 2 seconds 200, sent at the first read, two seconds after the send
GET messaging/emails/?to=jane@example.com 200, the three emails sent to that address, newest first, next_cursor: null
GET messaging/emails/?to=copy@example.com, a cc of those sends 200, empty results
An unknown id 404 not_found, with why, fix and docs_url

Gaps:

  • Prod shape: gap, accepted until read-only production access arrives on 2026-11-02. The AUX point-read latency is unknown.

Release status

  • This change is behind a feature flag and is not available to users

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)

Question Answer Who Where
Can emails be looked up by recipient? Yes, through list_email_statuses_by_recipient on the facade 👤 Human pending approval
Where does the read route live? Its own viewset at messaging/emails, over growing the send viewset 🤖 Agent pending approval
Where does the recipient lookup route land? In this PR, as GET messaging/emails/?to=, over a separate follow-up 🤖 Agent pending approval
What does the lookup cursor carry? The last email's message_id, and the service reads its time from the UUIDT. Alternatives: a cursor of created_at and id, or a next URL that would repeat the address. 🤖 Agent pending approval

Open:

  • Should the lookup also match cc and bcc copies? The status row stores one hash, of to, so copies need a schema change. Recommendation: match to only for now.
  • Query string or request body for the address? The lookup takes ?to=, as the slice asked. The alternative is a POST with 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):

Finding Response
A message id outside the UUIDT range answers 500 Fixed: an id whose time is after now answers 404 without a query. Test: ffffffff-ffff-4fff-8fff-ffffffffffff.
A salt key rotation hides older emails from the recipient lookup Fixed: the lookup hashes with every salt key. Writers still use the first. Test: a row hashed with the old key is found after a new key goes first.
Final read No findings.

The stack's QA swarm then reviewed this layer. Round 1 fixed seven items, among them project secret keys only on both reads (401 otherwise) 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.

  • Skills invoked: /tdd, /improving-drf-endpoints, /writing-tests, /writing-code-comments, /writing-pr-descriptions.
  • Public artifact: all fixtures are invented and use example.com domains.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 23, 2026 •

Copy link
Copy Markdown

🤖 CI report

✅ Complexity (TypeScript) — clean

Cyclomatic 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 change

Uncompressed size of every built .js bundle, compared against the base branch.

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

⚠️ Eager graph — 2 over budget

How much code each root ships on the eager path — downloaded and parsed before the surface is interactive. Measured from the esbuild output chunks (post-tree-shake, static imports only); lazy import() / React.lazy chunks are not counted.

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 scene
src/scenes/dashboard/Dashboard.tsx
9.68 MiB · 3,703 files no change ██████████ 96.7% of 10.01 MiB
🟡 today home path
src/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 path
src/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 scene
src/scenes/activity/explore/EventsScene.tsx
9.15 MiB · 3,495 files no change ██████████ 95.4% of 9.59 MiB
web analytics scene
src/scenes/web-analytics/WebAnalyticsScene.tsx
10.01 MiB · 3,713 files no change █████████░ 92.4% of 10.83 MiB
replay detail scene
src/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

@Silthus
Silthus force-pushed the feat/email-status-read branch from 638ce6b to a012511 Compare October 23, 2026 21:24
Silthus and others added 22 commits October 23, 2026 23:05
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>
@Silthus
Silthus force-pushed the feat/email-status-read branch from a012511 to 987b927 Compare October 24, 2026 17:28
@Silthus Silthus closed this Oct 28, 2026
@Silthus Silthus reopened this Oct 28, 2026
@Silthus

Silthus commented Oct 29, 2026

Copy link
Copy Markdown
Owner Author

Closing: superseded by PostHog#123932 (status read by message id) and the get operation in PostHog/sdk-specs#159. Lookup by recipient is dropped until someone asks for it. Branch kept for reference.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant