Skip to content

feat(messaging): send attachments with a code send - #559

Draft
Silthus wants to merge 14 commits into
feat/email-send-fieldsfrom
feat/email-send-attachments
Draft

Silthus wants to merge 14 commits into
feat/email-send-fieldsfrom
feat/email-send-attachments

Conversation

@Silthus

@Silthus Silthus commented Oct 23, 2026 •

Copy link
Copy Markdown
Owner

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

Problem

  • A team moving from Resend keeps every call site that attaches a file on Resend, such as a calendar invite or a PDF receipt. PostHog's code-form send cannot attach anything, so the migration never finishes (Silthus/posthog#543).
  • Map: Silthus/posthog#461, slice 2 of strand Silthus/posthog#543. Contract: #120921. An amendment waits for approval before it is posted there.

Impact

Pays into Grow Workflows through billed workflows_emails actions from orgs that send by code (not measured yet: needs send attribution, #120628, merged and an insight of code sends per org). In scope: Messaging, email API sends. Expected move: small, up. Run cost: SES's attachment charge per GB sent, small at a 512 KB cap. The same queue and worker otherwise.

Merge Danger

Warning

🟡 Merge risk: medium · two-way door (merge-risk rubric)

  • The SES send that every workflow email takes gains an Attachments list on its simple message. A wrong mapping would change a file's name, type or inline placement.
  • A send without attachments builds the same SES message as before. The existing SES tests cover it unchanged, and the send route sits behind workflows-transactional-email-api with no SDK sending attachments yet.
  • Rollout precondition: the flag stays off until tracking code v3 minting is on and every stack PR has deployed on Django, cdp-api, the hogflow worker and the email worker (#121272 has the reason). An older cdp-api or worker drops the attachments and still answers 202, as a rollback after the flag turns on would.

Changes

Kind Files Lines
Production 11 +241 −6
Tests 7 +275 −1
Generated types and snapshots 6 +157 −0
Total (this layer) 24 +673 −7

What a caller can now do

  • The code form takes attachments: up to 10 files, 512 KB in total after decoding. Each has filename, base64 content, an optional content_type and an optional content_id.
  • A content_id shows the file inline, where the HTML references it as cid:<content_id>.
  • template.id refuses attachments, like the fields of the slice below.
  • The route refuses, naming the field and the fix: content that is not base64 or is empty, more than 10 files or 512 KB, a file type SES blocks (such as .exe, also as the whole name), a filename with a path or line break, a content_type that is not a MIME type, and a content_id with < or >. No error repeats the content.
  • content_type and content_id take at most 78 characters, the SES limit, so a longer value answers 400 instead of failing at SES after the 202.
  • An attachment key the route does not take, such as contentType, answers 400 invalid_request on attachments. Before, the route dropped it, and an invite could lose its method=REQUEST.
  • A refused attachment carries a why that says what attachments PostHog takes.
  • The same Idempotency-Key with another attachment answers 422 idempotency_key_reused.

How it works

  • Django decodes the content to check its size, and hands it to cdp-api base64-encoded in send.attachments.
  • The worker puts the attachments on the email step's hog function and checks them again with the same limits, so an attachment from any path fails closed.
  • SES: a send with attachments keeps the simple SES message and lists the files in Content.Simple.Attachments. A file with a content_id goes inline, any other as an attachment.
  • The headers, copies and reply-to stay where every other send puts them, so a send without attachments builds the same SES message as before.
  • An earlier revision built one raw MIME message for SES. Native SES attachments replace it, so the raw path and helpers/raw-mime.ts are gone.
  • Maildev gets the attachments through nodemailer, with file and URL access off.
  • Lifecycle rows list each file by name, type and size, never its content, and mark the row attachments_omitted.
  • A rerun skips a row marked attachments_omitted with a warning, because it can't rebuild the send. The other rows of the page still rerun.

Why the 512 KB cap: the content rides inside the queued workflow run, through the workflow queue and then the email queue, so it counts against the queue's message size. SES itself takes up to 40 MB.

Not in this PR: the SDK parameter (lanes Silthus/posthog#536 and Silthus/posthog#537).

How did you test this code?

Test seams:

  • The send serializer, without a database, for every attachment limit.
  • The send route at the HTTP boundary, with cdp-api faked at its HTTP client.
  • cdp-api's send route with the queue faked, for the worker's size cap.
  • EmailService against a faked SES client, for the SES attachments.
  • The hog flow executor delivering to real maildev, from state.send to the delivered file.
  • The lifecycle row producer.

Test rationale: what each new test catches:

  • Thirteen refusals at the serializer, one per limit, each asserting the content never appears in the error.
  • The route hands cdp-api the attachments with type and content id.
  • cdp-api refuses a send over 512 KB.
  • cdp-api answers 422 idempotency_key_reused for a key reused with another attachment.
  • A blocked file type's why says what attachments PostHog takes.
  • SES gets the simple message with the caller's and PostHog's headers in order, the reply-to, and Content.Simple.Attachments: the invite as an attachment with its type, and the logo inline by its content id.
  • A run's attachments reach maildev. This test failed with the wiring from state.send to the email step removed.
  • A lifecycle row keeps { filename, contentType, size }, drops the content and carries attachments_omitted.
  • A rerun page skips the code send whose attachments were not stored and reruns the rest.
  • The serializer refuses a 79-character content_type or content_id and takes 78, refuses a file named .exe, and refuses an unknown attachment key.

Each new test failed without its production change. Local gate at the stack top 987b927630e, which holds this head unchanged, all green: pytest 1764, CDP jest 3690 (1 skipped), cdp-api serial 288, MCP 5035, repo-wide mypy, nodejs tsc, and the rest of the gate. The pre-push preflight ran in strict mode with 0 failures.

Live run at 987b927630e (Django, cdp-api and the email worker on a migrated clone of the local test database, delivering to maildev), case 01: a code send with cc copy@example.com and invite.ics (text/calendar; method=REQUEST) answered 202. Maildev got one email to jane@example.com with the cc and the attachment invite.ics as text/calendar. Maildev receives through nodemailer, not SES, so the live run does not reach the SES message. The EmailService test covers its attachments.

Earlier live run on 9d7a26704b5, before the review fix and before native SES attachments: inline image and refusals
Send Answer Delivered
Rendered content with cc, bcc, headers and a tag, plus invite.ics (text/calendar; method=REQUEST) and an inline logo.png (content_id: logo) 202 invite.ics as text/calendar; method=REQUEST; name=invite.ics, and the logo as Content-ID: <logo>
Attachment setup.exe 400 attr: attachments__0__filename, "Mailbox providers block .exe files. Send a link to the file instead."
One attachment of 512 KB + 1 byte 400 attr: attachments, "Attachments can hold 512KB in total. Send a link to larger files."

Cross-version (base 6a82b47b1b2, head b41402aca63, real Django body, cdp-api route, workers and result store in each tree):

Crossing Result
Base Django, cdp-api or store into this head Pass. A base-stored row keeps the content, and this head reruns it with the attachments.
Flag-off handoff, both ways Pass. Attachments travel only in state.send.
This head's code send, base cdp-api or base worker Fails behind the flag. Base strips the attachments and answers 202; the invite arrives without its .ics.
This head's stored row, base rerun Fails behind the flag. Base ignores attachments_omitted and sends again without the files, or a worker from this head fails the step.
This head's code send recorded by a base pod Base keeps up to 512 KB of attachment content in the ClickHouse snapshot.

Not checked: real SES. A sandbox SES account was not available to this lane, so delivery of SES attachments, open and click tracking on a send with attachments, and how mail clients render the invite, are unproven. The queue size of a full 512 KB send is unverified. The base-built cross-version values are kept as fixtures in proof storage, not committed as tests. Prod shape: gap, accepted until read-only production access arrives on 2026-11-02.

Release status

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

Docs update

None. No doc under docs/ covers the send route.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

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

  • Built as one lane of a map conductor run for Michael, test-first, on the raw MIME spike from spike/email-attachments. The design pass replaced the spike's raw message with native SES attachments.
  • Skills: /tdd, /improving-drf-endpoints, /writing-tests, /writing-pr-descriptions, impact, merge-risk. .agents/security.md was read before the attachment handling.
  • Review: one GPT-6.1 Sol round found a medium issue, now fixed. Headers named key and value turned the raw message's header map into one malformed header.
  • Decisions: attachments go to SES as native Content.Simple.Attachments, over a raw MIME message, so every send keeps one SES message shape. Names that end in a dot or a space get the extension check too.
  • The stack's QA swarm then reviewed this layer. Round 1 fixed the rerun of a send whose attachments were not stored, the SES length limits on content_type and content_id, and a file named only .exe. Round 2 found nothing to fix. The rebase check in final read 3 found nothing.
  • Open: the rollout decision on #121272: keep the deploy order plus the flag, or give code sends their own cdp-api path (➡️ the current plan is the deploy order plus the flag).
  • Involvement: one GPT-6.1 Sol round in the lane. 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; cross-version proof (Claude Opus 5.5); live run at the stack top.

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

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

Total: 1020.76 MiB · no change

ℹ️ 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
ℹ️ MCP agent API — agent-facing tool changes

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

Tools changed (1):

Tool Params Scopes Annotations Schema chars
workflows-send-email +attachments 4,912 -> 5,737

@Silthus
Silthus force-pushed the feat/email-send-fields branch from ed45d34 to 2585fe8 Compare October 23, 2026 21:24
@Silthus
Silthus force-pushed the feat/email-send-attachments branch from 6095d91 to 320f9a6 Compare October 23, 2026 21:24
Silthus and others added 14 commits October 23, 2026 23:02
A code send can attach up to 10 files, 512KB in total: base64 content, a
filename, an optional content type and an optional content id to show the
file inline. The send route refuses blocked file types, paths and line
breaks in names, and unsafe MIME parameters, without repeating the content.

The worker checks the attachments again, sends them through SES as one raw
MIME message that keeps PostHog's tracking and unsubscribe headers, and
attaches them in maildev. Lifecycle snapshots list each file without its
content.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Nodemailer read a header map with `key` and `value` names as one header,
so a send with attachments and headers by those names went out with a
malformed header and without PostHog's tracking and Auto-Submitted
headers. Pass the prepared headers as a list.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A refused attachment answered with the why of a field PostHog doesn't
know. It now says what attachments PostHog takes. The rendered send's
expected payload also carries the default sender flag the code form added.

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>
A file named `.exe` has no suffix to the path parser, so it passed the
blocked file type check. The extension is now the text after the last dot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An attachment with `contentType` or `cid` got a 202 and went out without
them. Attachments now refuse an unknown key by name, as tags do.

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

The email provider takes at most 78 characters for each, so a longer one
got a 202 and then failed at send.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A lifecycle snapshot lists attachments without their content, and a
rerun restored that send as is, so its lifecycle row threw and failed the
whole page. The snapshot now marks the omission, and the rerun skips that
row with a warning, as it does for a sensitive email.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t caps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus force-pushed the feat/email-send-attachments branch from 320f9a6 to b41402a Compare October 24, 2026 17:28
@Silthus
Silthus force-pushed the feat/email-send-fields branch from 2585fe8 to 6a82b47 Compare October 24, 2026 17:28
@Silthus Silthus closed this Oct 28, 2026
@Silthus Silthus reopened this Oct 28, 2026

This branch has not been deployed

No deployments
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