Repository navigation
Conversation
🤖 CI report✅ Comment density — 1% of added code lines are comments (12 of 883)This section warns when comments are more than 3% of the code lines a PR adds, and alerts above 6%. Before agent-assisted PRs, the typical share was about 2%. Only full-line comments count. Docstrings, generated files, snapshots, migrations, and workflow files are left out. Comments that restate the code, record how the change came about, or narrate the next line add noise for the next reader. Keep the comments that explain a reason the code cannot show, and remove the rest. See Files with the most added comment lines:
This check does not block merging. It updates on every push and clears when the share drops. |
This was referenced Oct 25, 2026
Silthus
force-pushed
the
email-v2/01-cdp-code-send
branch
from
October 29, 2026 05:28
24644bc to
4182790
Compare
…il route A code send posts its accepted content to transactional_email/send. The route runs the same reserve, replay, refusal and enqueue steps as a template send. The accepted send binds the idempotency fingerprint, and a miss on a new workflow reads it from the primary. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Silthus
force-pushed
the
email-v2/01-cdp-code-send
branch
from
October 31, 2026 08:39
4182790 to
969011f
Compare
This was referenced Oct 31, 2026
…flow Co-Authored-By: Claude Opus 5.5 <[email protected]>
Owner
Author
|
Moved to PostHog#127122. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Part of the code-first rebuild of the email send stack; replaces parts of PostHog#121272 (part 2, the cdp-api half).
Problem
sendfield. A cdp-api without the change dropped the field and answered202. It then sent nothing, or the stored template under the same key.Impact
Pays into Grow Workflows through billed
workflows_emailsactions per paying org, once the SDKs send code-rendered email. In scope: Messaging, email API sends. This PR only adds the delivery path; the route that opens it is behind theworkflows-email-from-codeflag, which gates everything the code-first send adds.Merge Danger
Warning
🔴 Merge risk: high · two-way door once the email queue drains: reverting removes the route, and Django then answers 503 for code sends
state.sendover the email step: content, sender, reply-to and capture choice. A wrong overlay sends the wrong content for a code send.workflows-email-from-codeon only after cdp-api and both CDP workers (hogflow, email) run this code. A worker without it ignoresstate.send. The mixed-rollout live case shows a202that sends the stored template under the same key, or nothing for a new key.503and sends nothing.state.send. A template send takes the unchanged path, and the template route ignores asendfield.Changes
Rebased onto master
43333f8. The code-send route shares the template route's handler, so it inherits each upstream change:Customer.io is not on this path (fix(email): stop falling back to customer.io for transactional emails PostHog/posthog#125322 removed the Django fallback), so a refused code send never falls back to it.
A code send now goes out with the caller's content or not at all. cdp-api takes it on its own route,
POST /api/projects/:team_id/hog_flows/:id/transactional_email/send, which requiressend.A cdp-api without this route answers 404 (401 when an internal secret is set). Django maps both to
503, nothing sent. The live crossing below shows it.A first send under a new key finds its workflow. Django creates the key's hidden workflow and posts at once. Both transactional routes and the hogflow and email workers now read a flow the cache misses from the primary database. Replica lag can neither refuse that first send nor drop it after the
202.Jobs of a workflow the database no longer has share one primary read per workflow, so a hard-deleted workflow's backlog costs one query, not one per job.
A reused idempotency key can't swap the content. On the send route the key also binds the
sendpayload and the route. A retry with changed content, or the same key on the other route, answers422 idempotency_key_reused. The template route keeps its fingerprint, so its existing keys still replay.The template route
.../transactional_emailkeeps its schema and drops anysendfield, so a stored template send never picks up code content.The internal API auth middleware exempts the new route by exact path, as it does the template route. Its handler checks the scoped JWT.
The worker lays
state.sendover the email step's rendered inputs: content, sender, reply-to. Recipients stay as the step renders them.capture_engagement: falseturns off the send's$workflows_email_sentand_failedevents now. Its opens, clicks and bounces follow once tracking code v3 is minted (below).truenever turns capture on over a team that turned it off.A rerun of a code send restores
state.send. A sensitive run's lifecycle snapshot drops it, as it drops the variables, so no rendered reset link is stored.Tracking code v3 carries the capture choice to opens, clicks and bounces. This PR ships the v3 reader with minting off.
Minting turns on with
EVERY_TRACKING_DEPLOYMENT_PARSES_CAPTURE_CHOICE. The flag-on gate forworkflows-email-from-codeflips it once this PR has rolled out to every deployment that reads tracking codes, before any real caller can sendcapture_engagement: false.The recipient check stays master's. A malformed
toanswers422 recipient_unreachableon both routes, as fix(cdp): treat malformed transactional email recipients as unreachable PostHog/posthog#125908 set it. An earlier version of this PR loosened thetopattern. The rebase drops that, because the looser pattern let a fullwidth-dot domain through.Mechanical: the comments this slice added are gone (house rule); their reasons became names such as
signedCaptureChoiceandEVERY_TRACKING_DEPLOYMENT_PARSES_CAPTURE_CHOICE.How did you test this code?
Test seams: the cdp-api routes through
supertest(cdp-api.serial.test.ts), the auth middleware (internal-api-auth.test.ts), the executor's email hand-off (hogflow-executor.service.test.ts), and the capture decision inemail.serviceandemail-tracking.service.Test rationale:
stamps %s on the queued runon the new route, andqueues a template send without an accepted send, even when the request carries one: content leaking onto the template route, or not reaching the run.refuses a send on the accepted send route without the send: the new route silently sending a stored template.authenticates every send on %s: the new route skipping the scoped JWT check.trueoverriding a team that turned capture off.rejects the key for a request with changed contentand... with the other route: a reused key replaying a different email.sends on transactional_email under a workflow created after the cached read missed it, both routes: a first send under a new key refused by a lagging replica.reads the primary once for concurrent jobs of a workflow the database no longer has: a deleted workflow's backlog sending one primary query per job.replays a keyed send whose workflow stopped being active: the test storesstatus = 'draft'and runs the reload, so a manager that stops reading the stored status fails it.Proofs at
920e7c9:typescript:check, eslint, oxlint and prettier pass. The nine changed test files pass: 289 parallel and 163 serial tests.43333f8, move 39 outcomes, each one named by the goal (the code-send route, the overlay, the capture choice, the primary read, rerun and snapshot ofsend). No test that passes on base fails on HEAD. Evidence is saved withfleet proof.dj3of map Map: send code-managed email templates with one PostHog call #461 runs them. Not run here.Proofs at the pre-rebase head
4182790c4095dd, a curl into maildev, saved withfleet proof. The goal curl delivers<p>Hello</p>from[email protected]. Cases S1 to S5 and a template control pass.d38fb25and cdp-api atmaster: three code sends answer503and maildev gets nothing. A template send still delivers its stored template.masterand cdp-api at this head: a template send delivers the stored template, unchanged.d91ec2dand both workers atmaster: a code send under a stored template's key answers202and delivers the stored template; a new key delivers nothing. This is the rollout order in the merge-risk callout.Not run locally: the whole Node suite. CI runs the rest. The fork has no Depot runners, so the Depot jobs never start there.
Release status
Docs update
None. The docs land with the Django route.
🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: Claude Code, Claude Opus 5.5 (claude-opus-5-5)
srv2of map Map: send code-managed email templates with one PostHog call #461, restacked onto master by lanenode3on 2026-10-31 (modernize-pr,stacking-prs). The node diff of the reviewed stack (fork feat(cdp): deliver a send django accepted as given #554) moved over as is. Fix A, the own route, is new.tdd,pr-description,writing-pr-descriptions,writing-tests,writing-code-comments,qa-swarm.capture_engagementdo against the team switch?workflows-email-from-code, per organization; master's surfaces keepworkflows-transactional-email-apiEVERY_TRACKING_DEPLOYMENT_PARSES_CAPTURE_CHOICE, after this PR rolls out everywhere; minting v3 codes is a condition for turningworkflows-email-from-codeondistinctIdout instead of tracking code v3?topattern after the rebase?toasrecipient_unreachable, and that holdsnode3, restack of 2026-10-31Involvement: one brief from Michael with decisions; QA swarm round 1 (4 lanes), round 2 (3 lanes), a final read, a full panel (8 lanes) and a narrow round (2 lanes); live, cross-version and mixed-rollout proof captured.
🤖 Generated with Claude Code