Skip to content

feat(messaging): send a stored template by key on send/email - #574

Closed
Silthus wants to merge 12 commits into
email-v2/04c-template-lock-adoptfrom
email-v2/05-template-send
Closed

Silthus wants to merge 12 commits into
email-v2/04c-template-lock-adoptfrom
email-v2/05-template-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 send on send/email/).

Problem

  • A developer who uploaded a template from code can't send it by its key. send/email/ only takes rendered content, and master's template route needs the PostHog id.
  • The SDKs send one shape for both. A template send also needs capture_engagement on every form, and reply_to and from on a template uploaded from code.

Impact

Pays into Grow Workflows through billed workflows_emails actions per paying org, for template sends from code. In scope: Messaging, email API sends.

Merge Danger

Warning

🟡 Merge risk: medium · two-way door: a revert removes the template forms from send/email/

  • Deploy order: cdp-api and both CDP workers from the node stack go out before this Django change. With cdp-api on master, a code send, a send by id with capture_engagement and a send by key with from or reply_to answer 503 and send nothing. Template sends without those options deliver (cross-version proof below).
  • Dark while workflows-email-from-code is off. A send by id keeps master's route. One that sets capture_engagement answers 400 on that field and sends nothing, because the opt-out needs the beta. An existing caller that sends the field gets the 400 once this deploys.
  • The route sends real email and stays behind workflows-email-from-code.

Changes

Kind Files Lines
Production 8 +314 −43
Tests 3 +289 −12
Generated types and snapshots 10 +223 −54
  • POST messaging/send/email/ sends a stored template by template: {key} or template: {id}, next to the rendered-content form, in one serializer that refuses unknown fields.
  • message_data fills the template's {{ variables.* }}.
  • template.hash names the content the caller holds, as v1: and 64 lowercase hex characters. Another format answers 400 invalid_input on template.hash.
  • The first send of a new local template uploads it. A template: {key, hash} send to a key PostHog doesn't hold yet answers 409 template_outdated, as contract Transactional email SDK contract for posthog-python and posthog-node PostHog/posthog#120921 says, so the SDK uploads and sends again. It answered 404 before. A key sent without a hash still answers 404.
  • Content PostHog does not hold, as the current content or one of the last 100 different contents it replaced, answers 409 template_outdated. Content a newer upload replaced still sends, with a template_superseded warning.
  • capture_engagement (default true) works on every form, a send by id included, as map Map: send code-managed email templates with one PostHog call #461 decided.
  • A send by id takes capture_engagement only while workflows-email-from-code is on. While it is off, a send that sets the field answers 400 with attr: "capture_engagement": "A send by template.id takes this field only in the beta for sending email from code. Remove it to send the email." A send without it keeps master's route, with no send block. The field's help text says so too.
  • A send by template: {key} that cdp-api does not confirm answers the same 503 as a code send: the email may still go out, retry with the same Idempotency-Key.
  • Usage events name the form: addressed_by is template_key, template.key or template.id. The help text says a project with capture off captures nothing.
  • A send by template: {key} takes reply_to and from, as contract Transactional email SDK contract for posthog-python and posthog-node PostHog/posthog#120921 promises for a template uploaded from code. A named from must be a verified sender, and the email goes out from it instead of the template's. Without from, the template's own sender holds.
  • A send by template: {id} refuses subject, html, text, from and reply_to with 400 invalid_input on that field, with why and fix. A from or reply_to refusal points at template: {key}: a template made in PostHog sends with its own settings. A send by key still refuses the content fields.
  • Known limit, until the flag-on gate: opens and clicks of a capture_engagement: false send are still captured. cdp-api mints tracking codes that carry the choice only when EVERY_TRACKING_DEPLOYMENT_PARSES_CAPTURE_CHOICE is on (feat(cdp): deliver a code send's content on its own transactional email route #565). The flag-on gate for workflows-email-from-code flips that switch after feat(cdp): deliver a code send's content on its own transactional email route #565 rolls out to every deployment. The help text keeps the full rule.
  • template_key help says that a template uploaded under a key that code sends already used takes over those sends.
  • An uploaded template with no text sends a text part derived from its html, as an inline-HTML send does. HTML with nothing to derive, such as a single image, sends with no text part. Templates made in PostHog keep the text their editor saves.
  • A template uploaded before this change keeps an empty text until its next changed upload or save.
  • warnings returns with its first producer, template_superseded.
  • Known limit: the hash check reads the published template in Postgres. The email goes out from the workflow copy cdp-api holds, which follows once the workers reload. A send right after an upload can still carry the previous content without a warning.
  • Mechanical: the send names its template as TemplateByKey or TemplateById, never both.

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).

Test rationale:

  • test_a_send_by_template_key_runs_the_templates_workflow: capture_engagement, reply_to or a named from not reaching cdp-api on a send by key. The SDK depends on this seam.
  • test_a_send_by_template_id_carries_its_engagement_capture: a send by id that drops capture_engagement, or that leaves master's route while workflows-email-from-code is off.
  • test_refuses_engagement_capture_on_a_send_by_template_id_while_email_from_code_is_off: a send outside the beta that drops the opt-out instead of refusing it.
  • test_a_form_encoded_send_by_template_id_without_engagement_capture_keeps_masters_route: a form body that omits the field, which DRF fills with false, refused or routed as if the caller set it.
  • test_refuses_on_a_send_by_template_id_what_its_template_sets, one row per field: a send by id that takes a field the template sets. A red run showed reply_to passing.
  • test_a_send_from_code_the_pipeline_does_not_confirm_warns_it_may_still_go_out, over code sends and sends by key: a timeout that tells the caller to retry without the key.
  • The key-send sender row: an unverified named from that answers anything but sender_not_verified on from.
  • The hash rows: an outdated copy that sends, a superseded copy that sends without its warning, and a malformed hash that loops on template_outdated.
  • The unknown-key rows: a key PostHog doesn't hold, or a deleted one, sent with a hash, that answers 404 and so stops the SDK's upload.
  • test_an_upload_without_text_sends_the_text_its_html_holds: an upload with no text, or a blank text, that sends an empty text part. A single-image upload sends none.

Proofs on this head 33e700b (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: sends by key with the current and a superseded hash, an unknown key, a send by key with from, reply_to and capture_engagement: false, sends by id, code sends and uploads.
  • Shadow diff: the same 21 requests on master 43333f8. Each difference is a send by key, the 404 for an unknown key, capture_engagement on a send by id, the id refusal pointing at template.key, the derived text part, or the new usage event properties.
  • Cross-version: base producer, head consumer: Django at feat(messaging): lock code-managed templates and adopt templates into code #631 against the node stack sends as the shadow diff shows. Head producer, base consumer: Django at this head against cdp-api master. Sends by id or key without options and uploads behave as with the node stack. Code sends, the send by id with capture_engagement and the send by key with options answer 503 delivery_unavailable; nothing reaches maildev and nothing goes out twice. With workflows-email-from-code off, a send by id without capture_engagement answers 202 and delivers the stored template; with capture_engagement false or true it answers 400 and maildev gets nothing (run on 0f27c16).
  • 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 c4c5ff0, 7d9f7aa and 9836720
  • Live run of the two fixes, Django at feat(messaging): mark a code send that leaves the sender to the project #578 c4c5ff0, which carries this head, cdp-api and workers at feat(cdp): key a code send on the sender the caller chose #580 1a43bdd, curl into maildev (saved with fleet proof):

    Case Answer Delivered
    Send by key and hash, a key PostHog doesn't hold yet 409 template_outdated nothing
    Upload that key with if_code_managed: true, send again with the same idempotency key 202 one email, with the text Hi Ada, start here (https://example.com/start). derived from the html
    Upload a template whose html is one image, then send it 202 one email, no text part
    Inline-HTML send (control) 202 one email with a text part
  • Live run at 7d9f7aa, before the two fixes, cdp-api and workers from feat(cdp): deliver a code send's content on its own transactional email route #565 at 24644bc, curl into maildev (saved with fleet proof):

    Case Answer Delivered
    Upload a new key, then send by key and hash 202 "Welcome, Ada" from message_data
    Send with a hash a later upload replaced 202 with template_superseded the newer content
    Send with a hash PostHog never held 409 template_outdated nothing
    Send by id with reply_to 400 invalid_request, attr: "reply_to", with why and fix nothing
    Send by id with capture_engagement: false 202 the stored template
    Send by key with reply_to 202 the template, with Reply-To: [email protected]
    Send by key with from: "Security <[email protected]>" 202 from Security <[email protected]>, not the template's [email protected]
    Template-by-id route (control) 202 the stored template
  • Capture check at 9836720, the project's engagement capture on, captured events recorded per distinct id:

    Template send by id Sent event Link click event
    capture_engagement: false 0 1
    capture_engagement: true 1 1
    No capture_engagement (default) 1, under the recipient address 1

    The click under false is the known limit above. Later commits leave the capture path as it was at 9836720.

  • Cross-version at 7d9f7aa, Django there and cdp-api at master: sends by key and by id answer 503 with the "did not confirm the send" detail, and maildev gets nothing. The template-by-id route still delivers.

  • The two fixes change only Django, so the cdp-api window above still holds.

  • Prod shape: a gap until read-only production access exists on 2026-11-02.

CI: the fork has no Depot runners, so those jobs never start, and no secret for the project-board app, so flags-project-board fails; the other GitHub-hosted checks pass. The lane check passes: against master the PR changes only the Python and frontend side.

Release status

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

Docs update

None in this PR. 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)

Question Answer Who Where
What does capture_engagement default to on a template send? true on every form, template: {id} included 👤 Human map #461, lane brief of 2026-10-25
Does an upload under a key take over that key's code sends? Yes, and the template_key help says so 👤 Human map #461, lane brief of 2026-10-25
Which flag gates the template forms? workflows-email-from-code, per organization 👤 Human map #461, 2026-10-25
Which fields does a send by template.id refuse? subject, html, text, from and reply_to, as contract PostHog#120921 says; reply_to stays with template.key 🤖 Agent map #461 conductor, 2026-10-25
Does a send by template.key take from? Yes, the named verified sender overrides the template's, as contract PostHog#120921 promises 🤖 Agent map #461 conductor, 2026-10-25
When do clicks of a capture_engagement: false send stop being captured? When the flag-on gate flips EVERY_TRACKING_DEPLOYMENT_PARSES_CAPTURE_CHOICE, after #565 rolls out everywhere; the help text stays as it is 🤖 Agent map #461 conductor, 2026-10-25
What does a key PostHog doesn't know answer? 409 template_outdated with a hash, as contract PostHog#120921 says; 404 without one, where the contract is silent 🤖 Agent pending approval
What does an unconfirmed send by template.key answer? The code send's 503: the email may still go out, retry with the same Idempotency-Key 🤖 Agent QA swarm 2026-11-01
Which templates derive their text from html? Templates uploaded from code; templates made in PostHog keep their editor's text, and no backfill runs 🤖 Agent pending approval
Does a send by id take capture_engagement while workflows-email-from-code is off? No; it answers 400 on the field, so the opt-out is never dropped silently. Replaces ignoring the field 👤 Human map #461 conductor as review owner, 2026-11-02

Involvement: one brief from Michael with decisions; one review round (GPT-6.1 Sol: review, design and Michael's lens; Opus fresh eyes) with its fixes; the SDK lane's live test found the reply_to gap, fixed here; final reads by GPT-6.1 Sol; QA swarm on 2026-11-01 with two panel rounds and three final reads, the last clean: 6 findings fixed, none rejected; live, shadow-diff and cross-version proof captured. Fix reviews (GPT-6.1 Sol, correctness and systems): 6d6c5a2 clean; 30b7e40 found the form-body refusal, fixed in 0f27c16, and a flag-off caller now getting 400, rejected as the review owner's decision; the final read of 0f27c16 found nothing.

🤖 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 — no change

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

Total: 75.13 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 — 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 no change █████████░ 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 no change ██████████ 98.5% of 3.03 MiB
authenticated shell (every logged-in page)
src/scenes/AuthenticatedShell.tsx
6.46 MiB · 2,235 files no change ██████████ 95.4% of 6.77 MiB
dashboard scene
src/scenes/dashboard/Dashboard.tsx
9.97 MiB · 3,728 files no change ██████████ 99.5% 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 no change ██████████ 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 no change ██████████ 96.7% of 7.53 MiB
events scene
src/scenes/activity/explore/EventsScene.tsx
9.40 MiB · 3,500 files no change ██████████ 98.0% of 9.59 MiB
web analytics scene
src/scenes/web-analytics/WebAnalyticsScene.tsx
10.36 MiB · 3,767 files no change ██████████ 95.6% of 10.83 MiB
replay detail scene
src/scenes/session-recordings/detail/SessionRecordingDetail.tsx
7.99 MiB · 2,968 files no change ████████░░ 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/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
24.6 KiB ../node_modules/.pnpm/[email protected]/node_modules/buffer/index.js
6.3 KiB ../node_modules/.pnpm/[email protected]/node_modules/react/cjs/react.production.min.js
4.7 KiB ../node_modules/.pnpm/@[email protected]/node_modules/@jspm/core/nodelibs/browser/process.js
4.0 KiB ../node_modules/.pnpm/[email protected]/node_modules/scheduler/cjs/scheduler.production.min.js
1.7 KiB src/index.tsx
1.7 KiB ../node_modules/.pnpm/[email protected]/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/[email protected]_@[email protected][email protected]/node_modules/posthog-js/dist/module.mjs
217.5 KiB ../node_modules/.pnpm/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@dnd-kit/core/dist/core.esm.js
36.4 KiB ../node_modules/.pnpm/[email protected]_patch_hash=139b8d1f1304f9d9da452a9a1244c94ea679dbcb85687d8999563146879fb6f5_react@18.3.1/node_modules/kea/lib/index.cjs.js
27.0 KiB ../node_modules/.pnpm/[email protected]/node_modules/tailwind-merge/dist/bundle-mjs.mjs
24.6 KiB ../node_modules/.pnpm/[email protected]/node_modules/buffer/index.js
Largest files eagerly shipped from src/scenes/AuthenticatedShell.tsx
Size File
325.0 KiB ../node_modules/.pnpm/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/[email protected]/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/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
144.1 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/[email protected]/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/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/[email protected]/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/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
144.1 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/[email protected]/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/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
144.1 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/[email protected]/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/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
187.9 KiB src/queries/validators.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
144.1 KiB ../packages/quill/packages/primitives/dist/index.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/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/[email protected]_@[email protected][email protected]/node_modules/posthog-js/dist/module.mjs
316.5 KiB ../node_modules/.pnpm/[email protected]_@[email protected][email protected]/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/@[email protected][email protected][email protected][email protected]/node_modules/@posthog/icons/dist/posthog-icons.es.js
153.8 KiB ../node_modules/.pnpm/[email protected]/node_modules/re2js/build/index.esm.js
126.8 KiB ../node_modules/.pnpm/[email protected][email protected]/node_modules/react-dom/cjs/react-dom.production.min.js
102.9 KiB src/products.tsx
94.5 KiB ../node_modules/.pnpm/@[email protected]_@[email protected]/node_modules/@tiptap/core/dist/index.js
92.5 KiB ../node_modules/.pnpm/[email protected]/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 no change ████░░░░░░ 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.7 KiB dist/toolbar/toolbar-app-UY2NCWYC.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 — 🔺 +138 B (+0.0%)

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

Total: 966.34 MiB · 🔺 +138 B (+0.0%)

ℹ️ MCP UI apps size — 35 app(s), 29120.8 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.5 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 (2):

Tool Params Scopes Annotations Schema chars
workflows-send-email 2,190 -> 2,368
workflows-upload-email-template description changed 2,420 -> 2,476

@Silthus
Silthus force-pushed the email-v2/04-template-uploads branch from ba370c8 to 52e70ee Compare October 25, 2026 11:38
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from bf2e702 to fa61f6b Compare October 25, 2026 11:38
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from fa61f6b to 9836720 Compare October 25, 2026 14:59
@Silthus
Silthus force-pushed the email-v2/04-template-uploads branch from 3b06d8a to 3e24e8b Compare October 25, 2026 17:37
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from 9836720 to edc79a5 Compare October 25, 2026 17:37
@Silthus
Silthus force-pushed the email-v2/04-template-uploads branch from 3e24e8b to c4cf425 Compare October 26, 2026 02:33
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from e20426a to 5675299 Compare October 26, 2026 02:34
@Silthus
Silthus force-pushed the email-v2/04-template-uploads branch from c4cf425 to 5ce7197 Compare October 31, 2026 12:14
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from 5675299 to 4a41fc8 Compare October 31, 2026 12:15
@Silthus
Silthus changed the base branch from email-v2/04-template-uploads to email-v2/04c-template-lock-adopt October 31, 2026 12:15
@Silthus
Silthus force-pushed the email-v2/04c-template-lock-adopt branch from 4512563 to e8fff1d Compare October 31, 2026 12:45
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from 4a41fc8 to 6011954 Compare October 31, 2026 12:49
Silthus and others added 2 commits October 31, 2026 15:54
`POST /api/projects/:id/messaging/send/email/` now takes
`template: {key, hash?}` for a template uploaded from code. A send by key
may set `from`, `reply_to` and `capture_engagement`; a send by
`template.id` still refuses them. With `template.hash`, a hash PostHog
doesn't hold answers `409 template_outdated` and sends nothing, and a
superseded hash sends the current content with a `template_superseded`
warning. An unknown key answers `404 template_not_found`, or
`409 template_outdated` when the send carries a hash. A code-managed
template without text sends the text its html holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@Silthus
Silthus force-pushed the email-v2/05-template-send branch from 6011954 to be7252c Compare October 31, 2026 16:03
Silthus and others added 6 commits November 1, 2026 17:25
…late-send

# Conflicts:
#	products/messaging/backend/tests/api/test_template_upload.py
…answer an unconfirmed key send

A send by template.key now answers an unconfirmed send like a code send and
reports a distinct addressed_by. A send by template.id takes capture_engagement
and points from and reply_to to template.key.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
…late-send

# Conflicts:
#	products/messaging/backend/services/template_uploads.py
#	products/messaging/backend/tests/api/test_template_upload.py
…late-send

# Conflicts:
#	products/messaging/backend/presentation/views/message_templates.py
#	products/messaging/backend/tests/api/test_template_upload.py
@Silthus

Silthus commented Nov 2, 2026

Copy link
Copy Markdown
Owner Author

Moved to PostHog#127176.

@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