Skip to content

feat(node): send cc, bcc, headers, tags and attachments from code - #8

Draft
Silthus wants to merge 2 commits into
emails-v2/03-local-templatefrom
emails-v2/04-code-fields
Draft

Silthus wants to merge 2 commits into
emails-v2/03-local-templatefrom
emails-v2/04-code-fields

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 PostHog#5373.

Problem

A team moving from Resend sends receipts with a PDF, copies to a shared inbox, and tags for their own analytics. This PR adds cc, bcc, headers, tags and attachments to the code form, rendered content and local templates alike.

Stacks on #7. Needs the server's fields and attachments arms of send/email/.

Impact

Same as the stack: Grow Workflows, billed workflows_emails from API sends. These fields remove the most common reason a Resend caller cannot switch.

Merge Danger

Caution

🔴 Merge risk: high · one-way door (merge-risk rubric)

  • New public option names and wire fields, and attachment content (customer files) through the SDK. The SDK never logs content and no error repeats it; the limits stay on the server.

Changes

Kind Files Lines (this PR, against #7)
Production 2 +110 −9
Tests 2 +137 −0
References and changesets 2 +102 −2
  • Code form only. A template send with one of these fields returns invalid_form on that field instead of sending without it. The types refuse them there too.
  • On the wire, each left out when absent: cc and bcc (a string becomes a one-item list), headers and tags as given, attachments as { filename, content, content_type?, content_id? } with a Buffer or Uint8Array as Base64.
  • Once per send. Attachments are encoded when the body is built, before retries and the local template's hash. The upload never carries these fields.
  • Refused before any request: a Resend path, content that is not a Buffer, Uint8Array or string, or attachments that is not a list, as invalid_attachment named validation_error with field: attachments__<n>__....
  • Equal to the reviewed feat(node): send cc, bcc, headers, tags and attachments with a code send PostHog/posthog-js#5373, plus capture_engagement sent true when absent; the five fields join the code form's field set from feat(node): send transactional email from code with emails.send #5.

Evidence

  • Test seams: posthog.emails.send() on a real client with FakeEmailsApi; the accepted and refused shapes as type tests.
  • What the tests pin: each field's mapping row on rendered content and on a local template; Base64 for Buffer and Uint8Array; one encoding across retries and the resend after upload; each invalid_attachment and invalid_form refusal; no attachment content in an error.
  • Local gate on d5097112: node vitest (1340 passed), edge (1340 passed), tsc, oxlint, oxfmt, build, references committed; core vitest.
  • Owed proofs:
    • Live run (an email with cc, a tag and a PDF into the dev mail catcher): gap until the server arms exist.
    • Cross-version (these fields against a server without them): a 400 invalid_request naming the field in field; live check stays a gap.
    • Production shape: gap until read-only production access is available.

Decisions

No load-bearing decisions.

Involvement: carries the PostHog#5373 review rounds; a fresh HEAD read and a final read on the restack (both clean), no live proof yet.

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

@Silthus
Silthus force-pushed the emails-v2/04-code-fields branch from a370487 to 989b6c5 Compare October 25, 2026 03:24
@Silthus
Silthus force-pushed the emails-v2/03-local-template branch 2 times, most recently from 33b1193 to 4a62a77 Compare October 25, 2026 03:39
@Silthus
Silthus force-pushed the emails-v2/04-code-fields branch from 989b6c5 to d509711 Compare October 25, 2026 03:39
@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: fb9640e
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 force-pushed the emails-v2/04-code-fields branch from d509711 to f98f32c Compare October 25, 2026 10:44
Silthus and others added 2 commits October 25, 2026 19:09
Moves PostHog#5373 (feat/node-emails-code-fields at abeb197) onto
the local template slice: both code-form cases take copies, custom headers,
tags and attachments, an older server's refusal names the field, and the
template form refuses them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus force-pushed the emails-v2/04-code-fields branch from f98f32c to fb9640e Compare October 25, 2026 19:11
@Silthus
Silthus force-pushed the emails-v2/03-local-template branch from 95be057 to da59e27 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