Repository navigation
Conversation
… them Add $workflow_invocation_id to $workflows_email_sent, $workflows_email_failed and every SES callback event, and $email_message_id to accepted SES sends and their callbacks, so each email event joins to the run and send that caused it. The sent event from a workflow step now carries $workflow_version. The step records the version it rendered under, and the send, tracking code, email metrics, billable count and conversion attribution all use that version, so a send that spans a republish no longer splits across two versions. Copied unchanged from #123128 so it can get a PostHog Review pass from a branch in this repository. Co-authored-by: Reichenbach, Michael <755327+Silthus@users.noreply.github.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
|
@PostHog review --deep |
🤖 CI report✅ Trunk lane — non-backend laneThis PR is assigned to the non-backend lane. It does not run backend Python tests and may merge in parallel with PRs in other lanes. ✅ Duplication (Python) — cleanNew Python code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying. ✅ Duplication (TypeScript) — cleanNew TypeScript code duplication introduced by this branch. Fails at 70+ tokens in app code, or 150+ tokens when both copies live in test files. Advisory while the gate proves itself: extract a shared helper instead of copying. |
|
Risk: No findings This change pins workflow email events, metrics, and billing attribution to the workflow version an email actually rendered under (stored once in step state), and adds opaque send-reference properties ($workflow_invocation_id, SES $email_message_id) to sent/failed events and SES callback events. I traced every new data path — tracking-code minting, SNS-verified webhook properties, captured-event properties, and app-metric version stamping — and found no new attacker-reachable input, trust-boundary weakening, or cross-tenant exposure; all new event fields are opaque internal ids gated by the existing team-level engagement-capture opt-in. Sentinel reviewed |
There was a problem hiding this comment.
Not approved — this change needs a human reviewer.
Re-adding the stamphog label gives the same result unless the changed files change.
stamphog can't auto-review this pull request because two gates refused it. The deny-list gate matched billing, since the change touches nodejs/src/cdp/services/hogflows/billing-utils.ts and the email billable count. The tier gate classified it as T2-never (210 lines across 13 files, a single-area feature), a tier that is never auto-reviewed. The size gate passed, so splitting is not needed to get under the ceiling. The author should ask a human reviewer to look at it, and could separate the billing-related changes from the rest so a reviewer can assess them on their own.
- posthog[bot] reviewed the current head.
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✗ | matches: billing |
| size | ✓ | 87L, 8F substantive, 210L/13F incl. docs/generated/snapshots — within ceiling |
| tier | ✗ | classified as T2-never: T2-never (210L, 13F, single-area, feat) |
| stamphog 2.6.1 | .stamphog/policy.yml @ unknown · reviewed head c11a2ae |
GET /api/projects/:project_id/messaging/emails/:message_id/ returns { id, status, created_at, status_at, reason } for a transactional send.
- queued: the send API records the run before it answers 202, so a real send has a status from the start.
- sent: the email worker's record of every email the provider accepts (message_assets).
- skipped: the run finished without a send record, after a short grace for the record to arrive.
- failed: the run failed, or a $workflows_email_failed event exists.
- delivered, bounced, complained: from $workflows_email_* events keyed by $workflow_invocation_id, which need engagement capture and the change in #123902.
Reads go through the cdp facade (fetch_invocation_lifecycle) and the workflows facade (fetch_sent_email_asset), scoped to the project's transactional workflows so each ClickHouse read stays on its sort key. Reads have their own rate-limit buckets and need hog_flow:read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…failed The status now comes from two records PostHog writes itself: the run the send API records before it answers, and the worker's record of an email the provider accepted. queued while the run waits or its send record is on the way, sent once the send record exists, failed when the run errored (reason is the error kind) or finished without sending (reason not_sent). The checks before the send API answers already refuse every email the worker would skip, so a separate skipped status and its log read are dropped. Delivered, bounced and complained are dropped too: they need engagement capture, depend on #123902, and come from events anyone with the project token can capture. They can come back once provider outcomes have trusted storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
GET /api/projects/:project_id/messaging/emails/:message_id/ returns { id, status, created_at, status_at, reason } for a transactional send.
- queued: the send API records the run before it answers 202, so a real send has a status from the start.
- sent: the email worker's record of every email the provider accepts (message_assets).
- skipped: the run finished without a send record, after a short grace for the record to arrive.
- failed: the run failed, or a $workflows_email_failed event exists.
- delivered, bounced, complained: from $workflows_email_* events keyed by $workflow_invocation_id, which need engagement capture and the change in #123902.
Reads go through the cdp facade (fetch_invocation_lifecycle) and the workflows facade (fetch_sent_email_asset), scoped to the project's transactional workflows so each ClickHouse read stays on its sort key. Reads have their own rate-limit buckets and need hog_flow:read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…failed The status now comes from two records PostHog writes itself: the run the send API records before it answers, and the worker's record of an email the provider accepted. queued while the run waits or its send record is on the way, sent once the send record exists, failed when the run errored (reason is the error kind) or finished without sending (reason not_sent). The checks before the send API answers already refuse every email the worker would skip, so a separate skipped status and its log read are dropped. Delivered, bounced and complained are dropped too: they need engagement capture, depend on #123902, and come from events anyone with the project token can capture. They can come back once provider outcomes have trusted storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
GET /api/projects/:project_id/messaging/emails/:message_id/ returns { id, status, created_at, status_at, reason } for a transactional send.
- queued: the send API records the run before it answers 202, so a real send has a status from the start.
- sent: the email worker's record of every email the provider accepts (message_assets).
- skipped: the run finished without a send record, after a short grace for the record to arrive.
- failed: the run failed, or a $workflows_email_failed event exists.
- delivered, bounced, complained: from $workflows_email_* events keyed by $workflow_invocation_id, which need engagement capture and the change in #123902.
Reads go through the cdp facade (fetch_invocation_lifecycle) and the workflows facade (fetch_sent_email_asset), scoped to the project's transactional workflows so each ClickHouse read stays on its sort key. Reads have their own rate-limit buckets and need hog_flow:read.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
…failed The status now comes from two records PostHog writes itself: the run the send API records before it answers, and the worker's record of an email the provider accepted. queued while the run waits or its send record is on the way, sent once the send record exists, failed when the run errored (reason is the error kind) or finished without sending (reason not_sent). The checks before the send API answers already refuse every email the worker would skip, so a separate skipped status and its log read are dropped. Delivered, bounced and complained are dropped too: they need engagement capture, depend on #123902, and come from events anyone with the project token can capture. They can come back once provider outcomes have trusted storage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Generated-By: PostHog Desktop Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
This is a copy of #123128, opened from a branch in this repository so it can get a PostHog Review pass. The diff is unchanged.
Problem
$workflows_email_*event carries a per-send reference.$workflows_email_sentcarries no workflow version.$workflow_invocation_idfrom these events.This is the first of two PRs. The second (#123129) attributes unsubscribes to the send that carried the link.
Warning
🟡 Merge risk: medium · one-way door (merge-risk rubric)
Changes
Send reference:
$workflows_email_sentand$workflows_email_failednow carry$workflow_invocation_id, the run that sent the email.$workflow_action_id, that names the step and run of every send.$email_message_id, the id SES returned.$email_message_idand$workflow_invocation_id.Version:
$workflow_version. A plain hog function send carries none.$workflow_versionfrom the signed tracking code.master.email_sent,email_failedandemail_untrackedmetrics and the email billable count use that version too.Not in this PR:
email_suppressed,email_bounce_prevented,message_frequency_capped) still count under the live version, as push metrics do.master. A separate PR handles it.How did you test this code?
Test rationale: each new test fails if the matching property drops off its event. The test seams from #123128:
EmailService.executeSendEmail, asserting the captured sent event, plus a new row for a workflow step with a version.email_sentmetric. The untracked-send test asserts it onemail_untracked.SesWebhookHandler.handleWebhook, asserting the message id on each callback type.Run on this branch, on top of
masterat 2e6bb0a:email-tracking.service.test.ts,email.service.test.ts,helpers/ses.test.ts,actions/hog_function.test.tsandhogflow-executor.service.test.ts.ECONNREFUSEDto maildev (127.0.0.1:1025and:1080), which this sandbox does not run.should send an email,should include tracking code in the email with distinct_id, and the executor email queue round-trip. This diff does not touch them.tsc --noEmit -p nodejsreports no errors in the changed files. Its 31 errors are missing native session replay modules, unrelated to this diff.Not run:
Release status
Docs update
None. No existing doc lists the
$workflows_email_*properties.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Agent: PostHog Desktop (Claude Code), Claude Opus 5.5
master, ran the scoped checks above, and rewrote the body into the template./writing-pr-descriptions.gh pr list --state open --search "workflow_invocation_id email events"finds only feat(workflows): tie email events to the send and version that caused them #123128 (the source) and feat(workflows): attribute unsubscribes to the email that carried the link #123129 (its follow-up).🤖 Generated with Claude Code
Created with PostHog Desktop