Skip to content

feat(messaging): send an email with inline html from code - #566

Closed
Silthus wants to merge 9 commits into
masterfrom
email-v2/02-django-code-send
Closed

Silthus wants to merge 9 commits into
masterfrom
email-v2/02-django-code-send

Conversation

@Silthus

@Silthus Silthus commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Part of the code-first rebuild of the email send stack; replaces parts of PostHog#118924 (the template key), PostHog#121084 (the send command) and PostHog#121272 (parts 1 and 3).

Problem

  • An app that renders its own emails can't send them through PostHog. The send API only sends a template someone stored in PostHog first.
  • The SDKs and the MCP tool need one call that takes the rendered email and sends it.
  • Map: Silthus/posthog#461. Contract: #120921.

Impact

Pays into Grow Workflows through billed workflows_emails actions per paying org, once the SDKs ship the code send. In scope: Messaging, email API sends. Expected move: up, after the flag turns on.

Merge Danger

Warning

🟡 Merge risk: medium · one-way door: the new messaging_transactionalemailkey table and the key column stay after a revert, unused

Changes

  • POST /api/projects/:id/messaging/send/email/ now also takes content the caller rendered. No template has to exist first:

    curl "$HOST/api/projects/$PROJECT/messaging/send/email/" -H "Authorization: Bearer $PROJECT_SECRET" -H 'Content-Type: application/json' \
      -d '{"template_key":"welcome","from":"sender@example.com","to":"recipient@example.com","subject":"Hello","html":"<p>Hello</p>"}'
  • The code form is flat: template_key, to, subject, html and/or text, plus optional from, reply_to, distinct_id and capture_engagement.

  • template and template_key in one body, or neither, answer 400. A code-only field on a template send answers 400 on that field.

  • capture_engagement is code form only and defaults to true. The schema carries no default, so a generated client never adds it to a template send.

  • A top-level distinct_id works on both forms. A template send falls back to identifiers.distinct_id, then the address.

  • The email goes out as sent: a {{ }} is not rendered. text comes from html when only html is given.

  • A from off the project's verified domains answers 400 sender_not_verified.

  • Idempotency-Key replays a code send: the same key answers 202 with replayed: true and sends nothing new.

  • A code send cdp-api does not confirm answers 503 delivery_unavailable: "The email pipeline did not confirm the send, so the email may still go out. To retry, send it again within 24 hours with the same Idempotency-Key, so it goes out once. A retry without that key, or after that time, may send it twice." The template form keeps master's text.

  • A code send whose key workflow was deleted gets a new workflow on its next send.

How it works

  • The code form extends Harley's MessagingSendViewSet.email and TransactionalEmailSendMixin: one route, one view. The PSAK bypass, the access checks, the route telemetry and master's stage-duration metrics cover both forms.
  • Each key gets one unlisted workflow on its first send, recorded in TransactionalEmailKey. Two racing first sends make one workflow.
  • A key that a stored template uses sends through that template's workflow, with the caller's content.
  • MessageTemplate.key and its unique index land here. No route writes a key until the templates slice.
  • Senders resolve through IntegrationManager.email.credentials, as master now does.

Mechanical: the regenerated OpenAPI, frontend and MCP types. The migrations moved to 0012 to 0014, above master's 0011.

How did you test this code?

Test seams: the send/email/ route through the DRF client with cdp-api mocked (tests/api/test_transactional_email.py), the OpenAPI schema of that route, the key model's constraint, and the hidden workflow per key (products/workflows tests).

Test rationale:

  • test_a_code_send_carries_what_the_caller_set: from, reply_to and capture_engagement not reaching cdp-api.
  • test_the_send_schema_gives_a_template_send_no_code_only_field: a schema default that a generated client copies into a template send, which the server then refuses.
  • test_a_code_send_the_pipeline_does_not_confirm_warns_it_may_still_go_out: a timeout answer that tells the caller to just retry.
  • The flat_distinct_id_on_send_email case: a template send that ignores a top-level distinct_id.
  • test_two_racing_first_sends_of_a_key_make_one_workflow: two workflows for one key.

Proofs on this head 0d164b2 (saved with fleet proof):

  • Live run: Django at this head, cdp-api and both CDP workers at the node stack f9abed0, maildev as the sink. 21 requests through the public API with a project secret API key. Code sends with and without from, the idempotent replay, the refusals and template sends by id all answer as listed above.
  • Live row: a code send with from: "Support <sender@example.com>", reply_to and capture_engagement: false answers 202. The email arrives from Support <sender@example.com> with reply-to: help@example.com.
  • Shadow diff: the same 21 requests on master 43333f8. Each difference is a code send, the replay, the sender_not_verified refusal, addressed_by on usage events, or the id-send refusal in Merge Danger.
  • Cross-version: reused from 4ea02a6. No file that builds or posts the send payload changed since.
  • Prod shape: a gap, accepted by the standing rule until read-only production access exists on 2026-11-02.
  • Lane gate on this head: ruff, the IDOR check, the migration checks, repo-wide mypy, and the messaging and workflows tests pass, and so do the MCP server's typecheck, lint and unit tests.
Earlier proofs, at 4ea02a6

CI: the fork has no Depot runners, so those jobs never start. Semgrep fails only because its path filter job did not run. A local semgrep run with CI's image and rules, baselined on master, finds nothing, and the integration-model ratchet matches its baseline.

Follow-ups:

  • Links in a code send stay click-tracked. A link opts out with data-ph-no-track. A click on a capture_engagement: false send still records a person event until the node stack's tracking codes carry that choice. A sensitive option for code sends belongs with that change.
  • A cap on distinct keys per project is a product decision.

Open: Django checks that a sender is verified but not that it is still active before it forwards a code send. cdp-api refuses a sender without its email and name. A check in Django belongs in the admission slice.

Overlap: Harley's draft PostHog#126287 makes the template form flat too. The code form's flat fields line up with it, and a top-level distinct_id already works on template sends here. This PR does not depend on it.

Release status

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

Docs update

  • None. The send API docs follow when the flag opens.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)

  • Skills: tdd, django-migrations, modernize-pr, stacking-prs, qa-swarm, writing-tests, pr-description, writing-pr-descriptions.
Question Answer Who Where
What does the first PR send? One email with inline HTML by curl, no template first 👤 Human map #461, lane brief of 2026-10-25
Which flag gates the code form? A new workflows-email-from-code, per organization; the template form keeps workflows-transactional-email-api 👤 Human map #461, 2026-10-25
May the code carry comments? No; names carry the reasons 👤 Human map #461, 2026-10-25
Where does the code form live? On Harley's MessagingSendViewSet.email and its mixin: one route, one view 👤 Human map #461, 2026-10-28
Is the code form's body flat? Yes, distinct_id included; PostHog#126287 stays a separate draft 👤 Human lane brief of 2026-10-31
Does capture_engagement apply to template sends? No, code form only; the server refuses it there 👤 Human contract changelog 2026-10-29
What does a template send do with a top-level distinct_id? Honors it before identifiers.distinct_id, in line with PostHog#126287, instead of refusing it 🤖 Agent review round 1
What does an unconfirmed code send answer? 503 that says the email may still go out, and to retry within 24 hours with the same Idempotency-Key; the template form keeps master's text 🤖 Agent QA swarm 2026-11-01
What does a send by template id do with from or template_key? Answers 400 on that field instead of ignoring it, so a caller never sends from a sender it did not get 🤖 Agent shadow diff, QA swarm 2026-11-01
When may the flag turn on? After cdp-api and both CDP workers run #565 🤖 Agent pending approval

Open: accept the rollout order above, or gate code sends on the worker version. Recommended: accept the order, since the flag already holds the route closed.

Involvement: briefs from Michael with decisions; GPT-6.1 Sol review rounds 1 and 2; QA swarm on 2026-11-01 with two panel rounds (GPT-6.1 Sol review, Opus 5.5 fresh eyes) and a clean final read: 5 findings fixed, 6 rejected; live, shadow-diff, cross-version and SDK proof captured.

More context for AI agents
  • Restacked as lane dj3 of map Map: send code-managed email templates with one PostHog call #461 onto master 43333f8, then shrunk onto Harley's send view.
  • Review round 1 (GPT-6.1 Sol) found the unconfirmed code send that said "Try again shortly", the capture_engagement default in the generated schema, and a top-level distinct_id that template sends ignored. All three are fixed in 1f62568. Round 2 found nothing.
  • QA swarm on 2026-11-01: cf29862 renamed the key example to order-shipped, added the deleted-key-workflow test, and reworded the unconfirmed-send detail; 0d164b2 names the 24-hour retry window.
  • Earlier history: QA swarm rounds 1 and 2 and a final read on the pre-shrink branch, with live and mixed-rollout runs at c4095dd.

🤖 Generated with Claude Code

@github-actions

github-actions Bot commented Oct 25, 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 — 🔺 +3.0 KiB (+0.0%)

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

Total: 75.14 MiB · 🔺 +3.0 KiB (+0.0%)

No file changed by more than 1000 B.

Posted automatically by build-bundle-size-report · uncompressed bytes from dist-report

✅ Eager graph — within 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.02 MiB · 25 files 🔺 +133 B (+0.0%) █████████░ 88.8% 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.99 MiB · 622 files 🔺 +133 B (+0.0%) ██████████ 98.5% of 3.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
6.46 MiB · 2,235 files 🟢 -225 B (-0.0%) ██████████ 95.4% of 6.77 MiB
dashboard scene
src/scenes/dashboard/Dashboard.tsx
9.97 MiB · 3,730 files 🔺 +1.8 KiB (+0.0%) ██████████ 99.6% of 10.01 MiB
today home path
src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/today/TodayHome.tsx
6.71 MiB · 2,337 files 🟢 -225 B (-0.0%) ██████████ 96.4% of 6.96 MiB
AI-first home path
src/scenes/AuthenticatedShell.tsx + src/scenes/project-homepage/ProjectHomepage.tsx + src/scenes/project-homepage/ai-first/AiFirstHomepage.tsx
7.28 MiB · 2,426 files 🟢 -225 B (-0.0%) ██████████ 96.7% of 7.53 MiB
events scene
src/scenes/activity/explore/EventsScene.tsx
9.40 MiB · 3,502 files 🔺 +1.8 KiB (+0.0%) ██████████ 98.0% of 9.59 MiB
web analytics scene
src/scenes/web-analytics/WebAnalyticsScene.tsx
10.36 MiB · 3,769 files 🔺 +1.8 KiB (+0.0%) ██████████ 95.6% of 10.83 MiB
replay detail scene
src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
7.99 MiB · 2,970 files 🔺 +1.9 KiB (+0.0%) ████████░░ 84.4% 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
🟢 src/layout/today/TodayShell.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodayContextPanelFloating.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodaySidePanel.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/TodayTopBar.tsx stays out of src/scenes/AuthenticatedShell.tsx
🟢 src/layout/today/todayRailItems.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
🟢 packages/quill/packages/charts/ 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.7 KiB ../node_modules/.pnpm/@jspm+core@2.1.0/node_modules/@jspm/core/nodelibs/browser/process.js
4.0 KiB ../node_modules/.pnpm/scheduler@0.23.2/node_modules/scheduler/cjs/scheduler.production.min.js
1.7 KiB src/index.tsx
1.7 KiB ../node_modules/.pnpm/base64-js@1.5.1/node_modules/base64-js/index.js
1.7 KiB src/lib/utils/bootTiming.ts
1.2 KiB src/RootErrorBoundary.tsx
949 B src/scenes/ChunkLoadErrorBoundary.tsx
Largest files eagerly shipped from src/index.tsx + src/scenes/App.tsx + src/scenes/bootApp.ts
Size File
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.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
102.9 KiB src/products.tsx
86.6 KiB src/lib/api.ts
67.9 KiB src/lib/lemon-ui/icons/icons.tsx
41.9 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
36.4 KiB ../node_modules/.pnpm/kea@4.0.0-pre.6_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
27.0 KiB ../node_modules/.pnpm/tailwind-merge@2.2.2/node_modules/tailwind-merge/dist/bundle-mjs.mjs
24.6 KiB ../node_modules/.pnpm/buffer@6.0.3/node_modules/buffer/index.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
86.6 KiB src/lib/api.ts
Largest files eagerly shipped from src/scenes/dashboard/Dashboard.tsx
Size File
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
144.1 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
92.5 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
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/prosemirror-view@1.40.1/node_modules/prosemirror-view/dist/index.js
86.6 KiB src/lib/api.ts
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
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
144.1 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
92.5 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
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
144.1 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
92.5 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
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
187.9 KiB src/queries/validators.js
153.8 KiB ../node_modules/.pnpm/re2js@0.4.1/node_modules/re2js/build/index.esm.js
144.1 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
Largest files eagerly shipped from src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
Size File
325.0 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/module.mjs
316.5 KiB ../node_modules/.pnpm/posthog-js@1.443.0_@types+react@18.3.27_react@18.3.1/node_modules/posthog-js/dist/rrweb.js
286.4 KiB src/taxonomy/core-filter-definitions-by-group.json
239.9 KiB ../packages/quill/packages/quill/dist/index.js
217.5 KiB ../node_modules/.pnpm/@posthog+icons@0.39.0_react-dom@18.3.1_react@18.3.1__react@18.3.1/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 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
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@tiptap+core@3.20.6_@tiptap+pm@3.20.6/node_modules/@tiptap/core/dist/index.js
92.5 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.33 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.33 MiB · 19 files 🔺 +133 B (+0.0%) ████░░░░░░ 40.8% of 5.72 MiB
Deferred (lazy) 2.11 MiB · 42 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
941.8 KiB dist/toolbar/toolbar-app-CNZCJTDY.css
695.8 KiB dist/toolbar/chunk-chunk-OU5JIGRB.js
261.2 KiB dist/toolbar/chunk-chunk-T5DKNJQU.js
131.8 KiB dist/toolbar/chunk-chunk-FDH2IBXT.js
130.4 KiB dist/toolbar/chunk-chunk-H2B6C5E4.js
75.7 KiB dist/toolbar/toolbar-app-SI4MRMOT.js
69.0 KiB dist/toolbar/chunk-chunk-TSAL54PB.js
33.8 KiB dist/toolbar/chunk-chunk-FR2SFTP5.js
21.7 KiB dist/toolbar/chunk-chunk-SFGY4JKG.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 — 🔺 +27.9 KiB (+0.0%)

Total size of the built frontend/dist folder (all assets), compared against the base branch.

Total: 966.36 MiB · 🔺 +27.9 KiB (+0.0%)

ℹ️ MCP UI apps size — 35 app(s), 29120.9 KB JS

Built size of each MCP UI app (main.js + styles.css).

App JS CSS
debug 844.3 KB 211.6 KB
explain-chart 1117.7 KB 211.6 KB
action 767.7 KB 211.6 KB
action-list 835.3 KB 211.6 KB
cohort 766.7 KB 211.6 KB
cohort-list 834.3 KB 211.6 KB
email-template 766.6 KB 211.6 KB
error-details 771.6 KB 211.6 KB
error-issue 768.9 KB 211.6 KB
error-issue-list 836.8 KB 211.6 KB
experiment 832.4 KB 211.6 KB
experiment-list 836.1 KB 211.6 KB
experiment-results 834.3 KB 211.6 KB
feature-flag 834.7 KB 211.6 KB
feature-flag-list 838.4 KB 211.6 KB
feature-flag-testing 770.0 KB 211.6 KB
inline-scan 767.2 KB 211.6 KB
insight-actors 833.8 KB 211.6 KB
llm-costs 831.1 KB 211.6 KB
session-recording 769.0 KB 211.6 KB
survey 768.3 KB 211.6 KB
survey-global-stats 831.1 KB 211.6 KB
survey-list 836.0 KB 211.6 KB
survey-stats 831.1 KB 211.6 KB
trace-span 767.1 KB 211.6 KB
trace-span-list 835.2 KB 211.6 KB
vision-observation-list 834.4 KB 211.6 KB
workflow 767.1 KB 211.6 KB
workflow-list 834.7 KB 211.6 KB
workflow-setup-status 766.7 KB 211.6 KB
query-results 1089.6 KB 211.6 KB
render-ui 1193.6 KB 211.6 KB
visual-review-snapshots 771.6 KB 211.6 KB
workflow-data-suggestions 769.4 KB 211.6 KB
workflow-sending-rules 768.1 KB 211.6 KB
ℹ️ MCP agent API — agent-facing tool changes

What this PR changes for agents, from the tool schema snapshots and tool definitions.

Tools changed (3):

Tool Params Scopes Annotations Schema chars
broadcasts-create 19,872 -> 20,013
workflows-create 19,872 -> 20,013
workflows-list 3,193 -> 3,220
ℹ️ frontend-layers — no base comparison available

Static imports from a lower frontend layer into a higher one (layers, lowest first: kernel, shell, query, features). Compared with the base branch, so an import that a PR also adds to layer-baseline.txt still shows here. Sizes are the source bytes on the eager path of every logged-in page.

Layer Eager source bytes Δ vs base
kernel 4.06 MiB (no base measurement)
shell 2.98 MiB (no base measurement)
query 338.8 KiB (no base measurement)
features 2.91 MiB (no base measurement)

ℹ️ No base report, so new and removed imports are not listed.

Download the interactive layer graph for this PR.

Posted automatically by check-layers

Silthus and others added 5 commits October 31, 2026 08:32
…endpoint

The single send endpoint now takes either a stored template (`template.id`)
or content the caller's code rendered (`template_key`, `subject`, `html` or
`text`, `from`, `reply_to`, `capture_engagement`). The code form is gated by
`workflows-email-from-code` and goes out through cdp-api's accepted-send
route, under one hidden workflow per key. A key that names a template uses
that template's transactional workflow.

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>
Both SDKs send distinct_id as a top-level field with template_key, so the
code form refused their sends as an unknown field.

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 email-v2/02-django-code-send branch from 0589d24 to 403c182 Compare October 31, 2026 09:00
Silthus and others added 2 commits October 31, 2026 09:52
…s off template sends

- A code send the pipeline does not confirm answers 503 with a detail that says
  the email may still go out, so a caller checks before it sends again.
- capture_engagement has no schema default, so a generated client no longer adds
  it to a template send, which the server refuses.
- A template send honors a top-level distinct_id, like a code send does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 2 commits November 1, 2026 17:04
The 503 for an unconfirmed code send now says to send again with the same
Idempotency-Key. The template_key help no longer uses a password reset as
its example, because code sends track their links. A test covers a key
whose workflow was deleted.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The unconfirmed-send detail promised that a retry with the same
Idempotency-Key never sends twice, but the key is kept for 24 hours.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus

Silthus commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Moved to PostHog#127123.

@Silthus Silthus closed this Nov 2, 2026
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