Repository navigation
Conversation
posthog-js Compliance ReportDate: 2026-10-25 19:24:48 UTC ✅ All Tests Passed!26/26 tests passed Capture Tests✅ 26/26 tests passed View Details
|
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: a57fb72 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. |
|
Size Change: +39.6 kB (+0.16%) Total Size: 25.2 MB 📦 View Changed
ℹ️ View Unchanged
|
|
Note 🤖 Claude Opus 5.5 responding on behalf of Michael Decisions made in the agent session and the review rounds:
|
815f192 to
fb7925b
Compare
A send now takes one of two forms on the same send endpoint: a template made in PostHog by templateId, or content your code rendered with templateKey, subject, html or text, and an optional from and replyTo. The client refuses a field outside the chosen form and a second send argument before any request. Every send carries capture_engagement, true unless the send turns it off, and the migratedFrom client option adds the X-PostHog-Migrated-From header to every email request. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The send endpoint takes capture_engagement only on a send with template_key and refuses it on a template send. A template made in PostHog follows the project's engagement setting, so the client now refuses captureEngagement on the template form before any request and leaves it out of the template body. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2a110db to
a57fb72
Compare
Problem
A server that sends transactional email with PostHog wants one call on the client it already has. Most teams first send HTML their own code rendered, such as React Email output. PostHog#5418, now on
main, addedposthog.emails.send()for templates made in PostHog. This PR adds the second form, content from code, on the same call and the samesend/email/URL. The SDK contract is PostHog/sdk-specsopenspec/specs/emails/spec.md, plus the draft changeadd-emails-code-formfor the code form.Part of map Silthus/posthog#461. Rebased onto
mainafter PostHog#5418 merged, so this diff holds only the code form.Impact
Pays into Grow Workflows through billed
workflows_emailsfrom API sends, read as orgs that send from code and orgs that switched (not measured yet: send attribution plus its insight). In scope: Messaging, email. Expected move: medium, up, together with the server slices. Run cost: n/a.Merge Danger
Caution
🔴 Merge risk: high · one-way door (merge-risk rubric)
emails.send(), the exported form types and themigratedFromclient option. Once released, the names and error codes are a contract, and a send that reaches PostHog delivers a real email.send/email/with the flat body, which is not on PostHog master yet (feat(cdp): deliver a code send's content on its own transactional email route posthog#565 and fix: remove warning of duplicate nextjs import PostHog/posthog-js#566, restacked in this round). Until both deploy, a code send gets a400. Release this after them.Changes
main)templateKey(letters, digits,_and-, up to 400),subject, andhtml,textor both, with optionalfromandreplyTo.send()posts a flat body toPOST /api/projects/@current/messaging/send/email/:{ template_key, subject, html?, text?, from?, reply_to?, capture_engagement, to, distinct_id? }. A stringreplyTobecomes a one-itemreply_tolist.SendEmailOptionsis a union ofSendTemplateEmailOptionsandSendCodeEmailOptions. TypeScript refuses a mix of the two forms, a code send withoutsubject, or one withouthtmlandtext.invalid_formwith itsfield, before any request. When the other form takes the field,whyandfixname that form. A send with both or neither oftemplateIdandtemplateKeyreturnsinvalid_form. A template ID that isn't a UUID keepsinvalid_template_id, as the spec says. A bad key returnsinvalid_template_key.send(payload, { idempotencyKey })returnsinvalid_forminstead of sending with a random key. Anundefinedsecond argument, as wrappers pass, still sends.captureEngagementis sent ascapture_engagement,trueunless the send passesfalse. The template form refuses it withinvalid_formand its body never carries it: a template made in PostHog follows the project's setting.migratedFromclient option. AddsX-PostHog-Migrated-Fromto everyemails.sendandemails.getrequest.retryAfterMs, andreplayedandwarningsstay asmainhas them.Evidence
posthog.emails.send()on a realPostHogclient with an injectedfetch(emails.spec.ts); the accepted and refused option shapes as type tests (__tests__/types/emails-forms.ts, run bycheck-types).distinct_id;capture_engagementtrueby default andfalsewhen passed;main's template body unchanged; the migration header onsendandget; eachinvalid_form,invalid_template_keyand second-argument refusal before any request, with itsfield,whyandfix.a57fb728b: nodevitest(1498 passed), edge (1498 passed), corevitest,check-types,oxlint,oxfmt, build, references committed,pnpm check:public-apiclean.call-flags-project / add-to-project-board, which needs an upstream secret the fork lacks (recorded as acigap).identifiersbody.400, which maps tovalidation_error.mainhas no code form to compare against.Earlier live run, at
2a110db7before the restack (2026-10-29)Against Django at Silthus/posthog#566
0589d24fae3and cdp-api plus both CDP workers at PostHog#565418279044b2, on a local stack with a dev mail catcher. The code form then sentidentifiers.distinct_id.html,text,from,replyTo, a keyreplayed: falseReply-To, html and text partsreplayed: truesend/email/, a keyreplayed: falsecaptureEngagementinvalid_formoncaptureEngagement, no requestidempotency_key_reusedrecipient_suppressed, namevalidation_errorDecisions
main: extend its sender, error class and getter; replaced this fork's own transportretryAfterMs, as the spec says; replaced the 30 s clamptrueby default; the template form refuses it; replaced sending it on every formmigratedFromwhyandfixsendargumentinvalid_form,undefinedpassesdistinct_idgoes on a code sendidentifiers.distinct_idon the code forminvalid_form, named in the sdk-specs draft;invalid_template_idstays for a template ID that isn't a UUIDNo open questions.
Involvement: the earlier #5 had one design pass, review rounds and a final read on its own transport, and three GPT-6.1 Sol reads of the rebuild on PostHog#5418. This restack: one full-scope GPT-6.1 Sol read at
a57fb728b(1 finding, rejected: the flatdistinct_idis the round's decision); 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. Rebased ontomainafter feat(node): send transactional email from a template PostHog/posthog-js#5418 merged.fleet proof review; round 4 ran full scope ata57fb728b.🤖 Generated with Claude Code