Skip to content

feat(node): send a template by id on the send/email route - #6

Draft
Silthus wants to merge 4 commits into
emails-v2/01-send-inline-htmlfrom
emails-v2/02-template-form-route
Draft

Silthus wants to merge 4 commits into
emails-v2/01-send-inline-htmlfrom
emails-v2/02-template-form-route

Conversation

@Silthus

@Silthus Silthus commented Oct 25, 2026 •

Copy link
Copy Markdown
Owner

Part of the code-first rebuild of posthog-node's email API; replaces parts of PostHog#5371.

Problem

After #5, a template made in PostHog still sends on its own route, messaging_templates/{id}/send/, which ignores captureEngagement and the newer send checks. This PR sends it on send/email/ like code content, as template: { id }. No caller change.

Stacks on #5. Needs the server's template: { id } arm of send/email/.

Impact

Same as the stack: Grow Workflows, billed workflows_emails from API sends. This PR makes both forms behave the same, so captureEngagement: false works on a template send.

Merge Danger

Warning

🟠 Merge risk: medium · two-way door (merge-risk rubric)

  • Changes the route of an unreleased form. Released alone against a server without the template: { id } arm, every template send fails with 400 naming template, so this must ship with or after that server change.

Changes

Kind Files Lines (this PR, against #5)
Production 1 +15 −24
Tests 2 +19 −15
References and changesets 2 +7 −2
  • Template form posts to POST /api/projects/@current/messaging/send/email/ as { template: { id }, to, identifiers?, message_data?, capture_engagement }.
  • Docs. The captureEngagement note that the template route follows the project setting is gone.
  • The fake API serves one route for both forms.

Evidence

  • Test seams: posthog.emails.send() on a real client with FakeEmailsApi at the fetch boundary.
  • What the tests pin: the template form's request path and body per mapping row, with capture_engagement.
  • Red first: the path and body cases failed before the move, then green.
  • Local gate on 86b2ad76: node vitest (1322 passed), edge (1322 passed), tsc, oxlint, oxfmt, build, references committed; core vitest. db5ac2da is the same change rebased onto feat(node): send transactional email from code with emails.send #5's references commit, and pnpm check:public-api passes on it.
  • Owed proofs:
    • Live run, done on 2026-10-26 at 8384b8ce, against Django at feat(messaging): send each email once per idempotency key posthog#578 c4c5ff05d2f (slice 09, with 05 e20426aa1e5, 06 6730e409b4b and 08d 0eeb308f962 below it) and cdp-api at feat(cdp): replay a keyed send after its workflow or default sender changes posthog#580 1a43bdda327, on a local stack with a dev mail catcher:

      Case Answer Delivered
      template by id with templateData: { link } 202 "Reset your password", with the link filled from templateData
      template by id with replyTo invalid_form on replyTo, with why and fix; no request sent nothing
      Earlier live run, on the template-send slice (feat(messaging): send a stored template by key on send/email posthog#574 at bf2e702c864)
      Case Request Answer Delivered
      code form templateKey, subject, html 202 the caller's subject and html
      template by id templateId, templateData: { link }, captureEngagement: false; the body is { template: { id }, message_data, capture_engagement: false, ... } on send/email/ 202 the template's subject, with the link filled from templateData
      template by id with replyTo templateId, templateData, replyTo { data: null, error } with invalid_form on replyTo, why and fix; no request sent nothing

      In the template case, the link still goes through the click-tracking redirect although the send passes captureEngagement: false. That redirect comes from the server.

    • Cross-version (this SDK against a server without the arm): gap; expected 400 on template, see Merge Danger.

    • Production shape: gap until read-only production access is available.

Decisions

Question Answer Who Where
Where the template-form move goes Its own PR (#6), before local templates, so #7 equals the reviewed PostHog#5371 👤 Human Michael, 2026-10-25

Open: release order, see #5 (this PR ships with or after the server's template: { id } arm).

Involvement: review round 1 (GPT-6.1 Sol: Correctness, Risk, Design, Anti-Overengineering, two Team perspectives; Opus 5.5: Fresh eyes; 2 fixed, 1 for Michael), two final reads (clean), live run captured.

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common

Checklist

  • All commits are signed (see Commit signing)
  • Tests for new code
  • Accounted for the impact of any changes across different platforms
  • Accounted for backwards compatibility of any changes (no breaking changes!)
  • Took care not to unnecessarily increase the bundle size

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

🤖 Generated with Claude Code

@github-actions

Copy link
Copy Markdown

Coherence

4 spec problems · 0 failing chokepoints · 2 spec gaps · 0 guard failures

44 components, 7 bullets (5 invariants, 2 requirements), 4 problems

Spec problems (spec --check)

  • packages/ai/src/otel/Otel.spec.md:7 an invariant bullet starts with <name>: <sentence>
  • packages/ai/src/otel/Otel.spec.md:8 an invariant bullet starts with <name>: <sentence>
  • Posthog-js.practice.md:20 practice add an invariant cites 187a5c5, which is no commit in this repository
  • Posthog-js.practice.md:20 practice add an invariant cites 8922ab2, which is no commit in this repository

Chokepoints: none declared.

Spec gaps: 2 entrances with outside or unknown trust and no traced control, and 10 more held by the adoption baseline

  • packages/web/Web.spec.md entrance public api (trust host-app): packages/web
  • packages/webpack-plugin/Webpack-plugin.spec.md entrance webpack plugin (trust build): packages/webpack-plugin

Close one with guard: <chokepoint>, an invariant that names it on its entrances: line, or control: none — <reason>; scaffold control proposes it.

@github-actions

github-actions Bot commented Oct 25, 2026 •

Copy link
Copy Markdown

Node SDK compliance v2

Commit: 8384b8c
Workflow run and artifacts

Capture v0 — acceptance

✅ 28 passed / ❌ 3 non-passing / ⚠️ 853 not selected

Drill-down: run artifacts → node-v2-v0 → acceptance/report.json.diagnostics.json (match case_id below).

❌ server_snapshot_payload_false — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_zero — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_null — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:

Harness exit: 1; report validation exit: 1.

Capture v1 — acceptance

✅ 28 passed / ❌ 3 non-passing / ⚠️ 853 not selected

Drill-down: run artifacts → node-v2-v1 → acceptance/report.json.diagnostics.json (match case_id below).

❌ server_snapshot_payload_false — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_zero — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:
❌ server_snapshot_payload_null — failed_assertion

Expected native data result

Code: snapshot_result

Failed step: acceptance/public/evaluate-flags.feature:868

the public snapshot outcomes should have these semantics:

Harness exit: 1; report validation exit: 1.

Capture v0 — migration

✅ 55 passed / ❌ 1 non-passing / ⚠️ 101 not selected

Drill-down: run artifacts → node-v2-v0 → reports/report.json.diagnostics.json (match case_id below).

❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertion

Flags field differs

Code: flag_request_field

Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134

the first flags request field "geoip_disable" should equal JSON false

Operation: /flags/

Field: geoip_disable

Expected:

false

Actual value:

true

Harness exit: 1; report validation exit: 1.

Capture v1 — migration

✅ 120 passed / ❌ 1 non-passing / ⚠️ 36 not selected

Drill-down: run artifacts → node-v2-v1 → reports/report.json.diagnostics.json (match case_id below).

❌ migration/yaml-parity-v1/remote-flags-v1.feature::Omitted GeoIP settings default to enabled in flag requests — failed_assertion

Flags field differs

Code: flag_request_field

Failed step: migration/yaml-parity-v1/remote-flags-v1.feature:134

the first flags request field "geoip_disable" should equal JSON false

Operation: /flags/

Field: geoip_disable

Expected:

false

Actual value:

true

Harness exit: 1; report validation exit: 1.

@Silthus

Silthus commented Oct 25, 2026

Copy link
Copy Markdown
Owner Author

Note

🤖 Claude Opus 5.5 responding on behalf of Michael

Decisions made in the agent session:

@Silthus
Silthus force-pushed the emails-v2/02-template-form-route branch from 86b2ad7 to db5ac2d Compare October 25, 2026 10:44
Silthus and others added 4 commits October 25, 2026 19:09
The template form posts template: { id } to /messaging/send/email/ instead of
the template's own send route, once the server takes it there. Callers change
nothing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The fake email API no longer invents a template_not_found code, and the
changeset states the route change only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed second argument

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus force-pushed the emails-v2/02-template-form-route branch from db5ac2d to 8384b8c Compare October 25, 2026 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant