Repository navigation
Conversation
ae2aecb to
5e7f579
Compare
9a4828b to
33b1193
Compare
5e7f579 to
86b2ad7
Compare
33b1193 to
4a62a77
Compare
Coherence📄 0 spec file errors · ⛔ 0 broken rules · 🚪 2 unchecked inputs · 🐛 0 bugs that came back What is Coherence?Coherence compares the code with its spec files ( This check read 44 parts of the code and 7 rules. Note These findings do not make this check fail. None of these findings are in files that this pull request changes. You do not need to fix them in this pull request. 🚪 Unchecked inputs (2)
What this means and how to fix itInput from outside this code comes in at these places. The spec does not say what checks that input. 10 more places were already like this when Coherence was added. This list does not show them. To close one of these, find the place by its name in the
To get the line to add, run |
Node SDK acceptance v2Commit: 1c241bb Capture v0✅ 83 passed / ❌ 4 non-passing / Drill-down: run artifacts → ❌ server_snapshot_payload_false — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_zero — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_null — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. Capture v1✅ 148 passed / ❌ 4 non-passing / Drill-down: run artifacts → ❌ server_snapshot_payload_false — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_zero — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ server_snapshot_payload_null — failed_assertionExpected native data result Code: Failed step: acceptance/public/evaluate-flags.feature:868 the public snapshot outcomes should have these semantics: ❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertionFlags field differs Code: Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134 the first flags request field "geoip_disable" should equal JSON false Operation: Field: Expected: false Actual value: true Harness exit: 1; report validation exit: 1. |
86b2ad7 to
db5ac2d
Compare
95be057 to
da59e27
Compare
db5ac2d to
8384b8c
Compare
A send with templateKey and templateData names content from code that PostHog fills in. The SDK sends the template's key and content hash, and uploads the template once when PostHog answers template_outdated, then sends again with the same idempotency key. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
da59e27 to
4e4bcb2
Compare
A 409 template_edited_in_posthog from a local-template upload is a permanent refusal, not a concurrent request, so it carries the name validation_error like template_outdated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A send now validates, copies its options and checks the client before its first await, so shutdown drains a send that started before it and a caller who changes replyTo afterwards doesn't change the email. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PostHog stores a local template on upload, and its upload route needs a subject and an HTML body. A text-only local template got 409 template_outdated, then a 400 on the upload, on every send. The SDK now refuses it before any request, and the types need html with templateData. The local-template mutation test no longer depends on which of two content hashes finishes first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The upload body is built in upload() from the local template the send already holds, so the TemplateUpload shape is gone. The content hash builds its input as one string and encodes it once, which drops the byte concatenation helper. The hash test vectors still match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Moved to PostHog#5513. |
Problem
A team that keeps its email templates in code wants PostHog to fill them with per-send data, without uploading by hand on every deploy. This PR adds the local template: a code send with
templateDatasends a content hash, and the SDK uploads the template by key only when PostHog doesn't hold that content.Stacks on #5. Needs the server's template upload by key and its
template: { key, hash }arm. Part of map Silthus/posthog#461.Impact
Same as the stack: Grow Workflows, billed
workflows_emailsfrom API sends.Merge Danger
Caution
🔴 Merge risk: high · one-way door (merge-risk rubric)
PUT messaging_templates/by_key/{key}/) from code. The SDK sendsif_code_managedandif_not_superseded, but the server branch it targets ignoresif_code_managedand adopts the template instead (see Server prerequisites).v1becomes a format the server and posthog-python must keep computing the same way.Changes
templateData): the SDK hashessubject,htmlandtextwith the content hashv1(emails/content-hash.ts) and sendstemplate: { key, hash }withmessage_data,identifiers.distinct_id,from,reply_toandcapture_engagement.409 template_outdatedit uploads by key (PUT messaging_templates/by_key/{key}/withif_code_managed: trueandif_not_superseded: true) and sends once more with the same idempotency key. It uploads at most once per send: a refused upload or a secondtemplate_outdatedreturns as the error.validation_error.409 template_edited_in_posthogmeans people edit the template in PostHog, so waiting won't fix it.send()changes nothing, andshutdown()drains a send that started before it.htmland a subject. PostHog stores it, and a stored template needs both. A local template with nohtml, an emptyhtmlor an empty subject returnsinvalid_formon that field before any request, andCodeEmailFormneedshtmlwithtemplateData.template_supersededwarning.templateDatanever uploads.Server prerequisites
if_code_managed: contract Transactional email SDK contract for posthog-python and posthog-node PostHog/posthog#120921 says the server refuses an upload over a template people edit in PostHog, with409 template_edited_in_posthog. The server branch this targets (email-v2/04c-template-lock-adopt) ignores the flag: it locks code-managed templates and adopts on upload. The SDK sends the flag and maps the contract's answer, so it needs no change either way. The server must settle which one ships.400 sender_unavailablewhen the project has no verified default sender, even when the send passesfrom. The contract gives the upload no sender field, so the SDK returns that answer as avalidation_errorand sends nothing.Evidence
posthog.emails.send()on a real client with an injectedfetch;emailContentHashthrough its public function, against the server'scontent_hashv1 test vectors.htmlor a subject refused before any request, and a text-only local template refused by the types; one request when PostHog holds the content; upload then resend with one idempotency key, the upload's headers and body; at most one upload per send; a refused upload namedvalidation_error; no upload for rendered content; options changed aftersend()(to,templateData,replyTo) change nothing; a send started beforeshutdown()still sends.1c241bb6c: nodevitestand edge (1512 tests), corevitest,check-types,oxlint,oxfmt, build, references committed,pnpm check:public-apiclean.3cc39e036: 22 invented inputs throughposthog.emails.send()with scripted replies, recording every request and result. 17 are identical, including every template-form case and every rendered code send. The 5 that differ are all local-template cases this PR adds: held, uploaded, refused upload, text only, and empty subject.1c241bb6conly drops two comments.call-flags-project / add-to-project-boardfails on every fork PR (recorded as acigap); the other checks run on the fork.409 template_outdatedfor a key it doesn't know, and a new key needs a verified default sender in the project.v1matches the server's test vectors inemail-content-hash.spec.ts; old-server runs are a gap, accepted.Earlier live run, at
da59e276before the restack (2026-10-26)Against Django at Silthus/posthog#578
c4c5ff05d2fand cdp-api at Silthus/posthog#5801a43bdda327, on a local stack with a dev mail catcher:409 template_outdated, upload201, send202, one idempotency key on all three202409, upload200, send202, one keyDecisions
distinct_idgoesidentifiers.distinct_idon a local template, like the template form; the flat field is only for thetemplate_keycode formhtmland a subject that isn't empty, checked before any request; a text-only local template is refused, since PostHog can't store it409 template_edited_in_posthogisvalidation_error, as contract #120921 says; other409s stayconcurrent_idempotent_requestsNo open questions.
Involvement: carries the PostHog#5371 review rounds. This rebuild: one full-scope GPT-6.1 Sol read at
660f6a0f1(2 findings, both fixed inbf8cfa2b3); a QA swarm atbf8cfa2b3with a full two-family panel (GPT-6.1 Sol on six lenses, Claude Opus 5.5 fresh eyes: 6 fixed, 2 server follow-ups) and a narrow round on the fixes (1 fix, comments dropped); shadow diff captured; live run owed to lane dj3.Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
pnpm changesetto generate a changeset file🤖 Agent context
Autonomy: Human-driven (agent-assisted)
claude-opus-5-5) in Claude Code, as lane sdk3 of map Map: send code-managed email templates with one PostHog call posthog#461, directed by Michael. Rebuilt on feat(node): send email content from code with emails.send #5 after feat(node): send transactional email from a template PostHog/posthog-js#5418 merged; the behavior comes from the reviewed feat(node): send email content from code, uploading local templates by hash PostHog/posthog-js#5371.fleet proof.🤖 Generated with Claude Code