Repository navigation
Conversation
e057075 to
7386a38
Compare
ab5eb41 to
94f9bae
Compare
With workflows-email-from-code on, a new sender takes the domain's existing MAIL FROM label, an edit shares the label with every sender on the domain, and create, edit and verify hold a per-domain advisory lock. Worker reloads wait for that lock's commit. The flag helper now lives next to EmailIntegration so the API gate and the model evaluate it the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… edit The edit locked its own sender, then the other senders of the domain. A verification on the same domain locks them all in id order, so the two could deadlock. The edit now locks every sender of the domain, its own included, in one query ordered by id. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
94f9bae to
dc05208
Compare
The email flag helper now lives in posthog/models/integration/email.py and builds its inputs with flag_identity. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- an edit that saves the label the edited sender already stores still moves SES to it when another sender on the domain holds a different label, since SES may hold that other label - the sender page's name and setup-method saves leave the label out, so a page loaded before a label edit cannot revert the domain to its old label Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
🤖 CI report✅ Complexity (TypeScript) — cleanCyclomatic complexity above the limit in changed typescript files (10 for production files, 15 for test files). Warn only: worth simplifying when you next touch these functions. ✅ Bundle size — no changeUncompressed size of every built Total: 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 budgetHow 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
🟢 Largest files eagerly shipped from
|
| 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 | 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-NUWPCNXY.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-KDUJEIKP.js |
| 75.7 KiB | dist/toolbar/toolbar-app-TP2JYJFO.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-2P7IMPQ2.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 — 🔺 +2.4 KiB (+0.0%)
Total size of the built frontend/dist folder (all assets), compared against the base branch.
Total: 966.34 MiB · 🔺 +2.4 KiB (+0.0%)
…en edited The Configure modal resent the label it loaded, so a save made after another sender changed the domain label reverted it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Moved to PostHog#127298. |
Moved here from #116864, closed on 2026-10-25 so this fork stack is the only copy. It opens upstream again after review.
This revision
43333f81da9. Master's memoized workflows config is now an extension perTeaminstance, so this PR no longer clears it on commit and no longer touchesdefault_email_sender.py.workflows-email-from-code, the one rollout flag for the Workflows email work.dc05208509: a shared label edit locks every sender of the domain, the edited one included, in id order. Before, it locked its own row first and could deadlock against a verification.d6080d93ad: an edit moves SES to the chosen label when another sender on the domain holds a different one, even if the edited sender already stores it.d6080d93ad,1a311bf509: the sender page's name and setup-method saves, and the Configure modal unless its label changed, leave the label out. A page loaded before a label edit can no longer revert it.641380bd8c(4064cb8b50,4f59ef47cd). The email flag helper inposthog/models/integration/email.pynow builds its inputs withflag_identity.Problem
feedback).mail_from_subdomainre-sets the label in SES for every sender on the domain.This recreates the fork draft Silthus#345 on top of #568. Its earlier upstream draft, PostHog#111367, is closed.
Impact
Pays into Grow Workflows through organizations that send from a verified domain: a sender whose MAIL FROM label moves under it no longer matches the DNS records its owner published. Not measured on its own; it guards the
email domain verifiedpath of #568. Expected move: none on its own. Run cost: n/a.Caution
🔴 Merge risk: high · two-way door (merge-risk rubric)
workflows-email-from-codeon, an edit rewrites the shared delivery setting of every sender on the domain in the organization, other projects included, and moves SES with it. A wrong label breaks the domain's MAIL FROM until its DNS records match.Teamlookup on create, and the label left out of name-only saves in the web app.Changes
Everything below runs only when the
workflows-email-from-codeflag is on, the flag that also gates the agent sender tools in #568.SELECT ... FOR UPDATE OF posthog_integration ... ORDER BY id. That is the order a verification locks them in, so a flag-off verification and a flag-on edit cannot deadlock. NoTeamrow is locked.EmailIntegrationevaluates the flag for both the API gate and the model. It sends the web app's person and group inputs (flag_identity), so the project'sidproperty is the team id, not its UUID. The status route's check ofworkflows-email-domain-wizarduses the same inputs. Domain Connect reads the label throughEmailIntegration.Warning
Roll the flag out by organization. A teammate with the flag off still edits only their own sender, and their verify can re-set the old label for the domain.
The setup modal pre-fills
feedback. On a domain that already uses another label, the user sees the 400 in a toast and edits the field. The sender page and the Configure modal send the label only when the person edits it. No screen looks different.Read first:
EmailIntegration._domain_guard,_lock_edited_senderandcreate_native_integrationinposthog/models/integration/email.py.How did you test this code?
Test seams: the public methods of
EmailIntegration(create_native_integration,update_native_integration,verify) and the email edit endpoint. The SES facade andposthoganalytics.feature_enabledare mocked as external boundaries.Test rationale:
test_new_sender_keeps_the_domain_mail_from_labelandtest_new_sender_cannot_move_the_domain_mail_from_label: catch the create path, including a legacy blank label. No existing test sent a label on create.test_verifying_any_sender_keeps_a_changed_mail_from_label: catches the edit-then-verify path through instances loaded before the edit. It fails without the sibling write and without the re-read under the lock.test_editing_a_sender_without_a_label_keeps_the_current_domain_label: catches a name-only edit that re-sets a label changed since the sender loaded.test_each_sender_keeps_its_own_mail_from_label_while_the_flag_is_off: pins master's behavior with the flag off.test_editing_the_label_shares_it_when_the_flag_is_on_for_the_requesting_user: sends a realPATCHand fails if the view stops passing the request user.test_saving_a_label_a_sibling_lacks_moves_ses_to_it(resent label, name-only edit): fails if SES stays on a sibling's label while the rows move. Red befored6080d93ad.leaves the MAIL FROM label out when saving the sender name / recording the setup method(emailDomainLogic.test.ts) andleaves out an unchanged MAIL FROM label(emailSetupModalLogic.test.ts): fail if a name-only save resends a stale label. Red befored6080d93adand1a311bf509.test_flag_released_to_a_project_id_reaches_the_sender_update(from feat(workflows): let agents set up an email sending domain via mcp #568): fails if the flag check stops sending the project's numeric id.TestEmailIntegrationConcurrentLabelEdits.test_a_shared_label_edit_waits_for_a_lower_sender_before_locking_its_own_row(TransactionTestCase, three connections): one connection holds the lower-id sender, the edit of the higher-id sender waits on a row lock, and aNOWAITlock of the edited row from a third connection must succeed. Red beforedc05208509: the edit already held its own row.Lane gate on
48c043fd9c: the integration API, integration model, email status and eval tests (1,243 passed); the MCP unit tests (5,363 passed); theEmailDomainandEmailSetupfrontend tests (105 passed); the frontend typecheck; mypy and ruff on the changed Python. Before the last merge, the wider sweep withproducts/workflows/backendand Domain Connect passed excepttest_ses_provider.py::test_delete_identity_removes_tenant_associations_first, a master test outside this diff that fails in that ordering on the pre-fix head as well and passes alone.CI on the fork: the depot jobs never get a runner, so the gates that depend on them fail. The lane gate stands in until the PR opens upstream.
Proofs per qa-swarm
PROOF.md:👉 Stay up-to-date with PostHog coding conventions for a smoother review.
Release status
Docs update
None. No doc under
docs/covers the email sender MAIL FROM setting.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (
claude-opus-5-5)43333f81da9. The restack dropped this PR's memo-clear helper: master replaced the memo it cleared./writing-tests,/writing-pr-descriptions,pr-description,modernize-pr,merge-risk,tdd.Decisions
workflows-email-from-code, the one rollout flag for the Workflows email workInvolvement: no grilling rounds, no prototypes. GPT-6.1 Sol (high) reviewed the restack in round 1 (one finding, fixed) and in four final reads (one deadlock finding, fixed). On 2026-11-01 a two-family panel reviewed the whole diff: nine GPT-6.1 Sol (high) lenses and a Claude Opus 5.5 (high) fresh-eyes lane. Round 2 raised 5 distinct findings: 3 fixed, 1 rejected, 1 follow-up. Round 3 checked the fixes: 5 findings, 3 fixed, 2 rejected. Final reads follow. Proof: command output; live run and shadow diff owed.
🤖 Generated with Claude Code