Skip to content

feat(emails): let a code send set its tracking and sensitivity - #5630

Merged
Silthus merged 11 commits into
mainfrom
feat/emails-track-opens-and-clicks
Nov 12, 2026
Merged

Silthus merged 11 commits into
mainfrom
feat/emails-track-opens-and-clicks

Conversation

@Silthus

@Silthus Silthus commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Note

👀 Reviewed together with #131780. The team question for this change is there. Once you're happy with #131780, please approve this one too.

Problem

A code send (posthog.emails.send() with templateKey) can't choose what PostHog tracks, or say whether its email is sensitive. Some emails must go out with no open pixel. A newsletter wants click tracking. PostHog can't tell a newsletter from a password reset email on its own, so the caller needs a way to say which one it is.

Changes

  • A code send takes a new optional sensitive: true or false. A sensitive email keeps its links as they are, leaves its subject off engagement events, and keeps its content out of PostHog's logs. Left out, PostHog keeps today's behavior: a send whose templateKey matches a saved template follows that template, and any other send is sensitive.

  • A code send takes a new optional tracking: true, false, or { opens?, clicks?, engagement? }, exported as EmailTracking.

    tracking Opens Clicks Engagement events
    left out on off when sensitive, on when not on
    true on on on
    false off off off
    object key, default on key, default follows sensitive key, default on
  • A send that sets neither option behaves exactly as it does today.

  • captureEngagement is deprecated in favor of tracking: { engagement: false }. It keeps working.

  • The client sends sensitive, tracking and capture_engagement only when the caller sets them. Before, capture_engagement: true went on every code send. The API reads a missing capture_engagement as true, so a default send is unchanged. Leaving it out lets the API refuse a send that sets both it and tracking.

  • The SDK checks no combination. The API refuses opens: false with clicks: true, an unknown key, and tracking together with captureEngagement.

  • The template form (templateId) refuses sensitive and tracking before any request, the same as captureEngagement. The types refuse them too.

  • A changeset (posthog-node, minor) says both options need PostHog's API support, which is rolling out.

Release once PostHog's API takes tracking and sensitive in both regions: #131780, #131756.

Tests

  • emails.spec.ts, emails over HTTP: each form goes through posthog.emails.send() and the client's real fetch to a local HTTP server, which checks the body it received. Left out sends none of sensitive, tracking or capture_engagement. sensitive: true, sensitive: false, tracking: false, true, an object and the deprecated captureEngagement go on the wire as given.
  • The same suite sends sensitive and tracking on the template form. Each returns an invalid_form error on that field, and the server receives no request.
  • types/emails-forms.ts: sensitive: false, tracking: false and tracking: { clicks: true } type-check. sensitive or tracking on the template form, and a misspelled key such as { open: false }, are type errors.
  • Mutation check: dropping sensitive, always sending it, or leaving it out of the code form's fields each fails the suite. So does letting the template form accept sensitive and tracking.
  • With Node 24 and pnpm 11.7: oxlint, oxfmt, check-types, the full posthog-node unit suite, generate-references and pnpm check:public-api.
  • The built package, run against a local capture server, sends the bodies above for each form.
  • Not run: a live send. PostHog's API refuses tracking and sensitive until the two PRs above deploy. The real send through this branch runs in the SDK release gate once they are live.

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)

  • Written by Claude Opus 5.5 (claude-opus-5-5) in Claude Code, as a worker on a task the PR's assignee directed.
  • This replaces the PR's earlier trackOpensAndClicks option, which never shipped, with the tracking and sensitive fields the API takes.
  • capture_engagement is now left out unless set, because the API refuses a send that sets both it and tracking.
  • The latest API reference file is regenerated because check:public-api requires it in the PR.

🤖 Generated with Claude Code

A send with templateKey now takes trackOpensAndClicks. With false, PostHog sends the email without
its open pixel and without links rewritten to count clicks. The client sends track_opens_and_clicks
only when the caller set it, so a default send keeps today's request body and still works against
an API that predates the field. The template form refuses it before any request, like
captureEngagement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

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 (*.spec.md). A spec file lists the rules for one part of the code.

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)

  • public api in packages/web/Web.spec.md. The input comes from: host-app.
  • webpack plugin in packages/webpack-plugin/Webpack-plugin.spec.md. The input comes from: build.
What this means and how to fix it

Input 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 ## entrances section of its spec file. Add one line under it:

  • If all of the input goes through one function that a rule in the spec protects, add guard: <function>.
  • If a rule in the spec already checks input from the same source, add the place's name to the entrances: line of that rule.
  • If the input needs no check, add control: none - <reason>.

To get the line to add, run npx --no -- coherence scaffold control "<name>".

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

Node SDK acceptance v2

Commit: 1d7c4e4
Workflow run and artifacts

Capture v0

✅ 83 passed / ❌ 4 non-passing / ⚠️ 954 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:
❌ 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

✅ 148 passed / ❌ 4 non-passing / ⚠️ 889 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:
❌ 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.

@github-actions

github-actions Bot commented Nov 9, 2026 •

Copy link
Copy Markdown
Contributor

SDK performance: posthog-node

Benchmarks completed. No changes flagged for review.

Flags require at least a 5% change in the median and all six pairs agreeing on direction. This is a noise filter, not proof of a performance change or a CI gate.

View all results
Test Measurement Before PR With PR Change Assessment
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Queue an event (32-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Queue an event (4,096-byte payload)
Shard: capture
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate all 10 flags
Shard: flags-scaling
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate all 100 flags
Shard: flags-scaling
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate all 1,000 flags
Shard: flags-scaling
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate one boolean flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a cohort-filtered flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a flag (early match)
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a flag (late match)
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a flag (no match)
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a property-filtered flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a regex-filtered flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a rollout flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Evaluate a multivariate flag
Shard: flags-single
Allocations per operation — — — unavailable; base:​ unsupported; head:​ unsupported; base:​ V8 samples do not expose total allocation count; sample count is not object count.​; head:​ V8 samples do not expose total allocation count; sample count is not object count.​
Capture and deliver events (32-byte payload, burst)
Shard: capture
Allocated memory per operation (estimated) 9.91 KiB/op 10.01 KiB/op 1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Time per operation 18.55 µs/op 17.72 µs/op 4.5% less time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Idle process memory (RSS) 102.73 MiB 102.67 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 106.39 MiB 106.41 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Managed heap after settling 10.04 MiB 10.04 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 106.07 MiB 106.15 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process CPU time per operation 32.03 µs/op 31.53 µs/op 1.6% less time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 106.61 MiB 106.63 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocated memory per operation (estimated) 17.58 KiB/op 17.66 KiB/op 0.5% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Time per operation 32.56 µs/op 32.39 µs/op 0.5% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Idle process memory (RSS) 103.22 MiB 103.18 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 113.75 MiB 113.77 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Managed heap after settling 10.05 MiB 10.05 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 109.64 MiB 109.73 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process CPU time per operation 47.12 µs/op 47.12 µs/op <0.1% less time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 116.41 MiB 116.59 MiB 0.2% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocated memory per operation (estimated) 10.51 KiB/op 9.93 KiB/op 5.5% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Time per operation 18.7 µs/op 18.49 µs/op 1.1% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Idle process memory (RSS) 102.57 MiB 102.57 MiB No observed change Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 106.89 MiB 106.69 MiB 0.2% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Managed heap after settling 10.12 MiB 10.12 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 107.01 MiB 106.7 MiB 0.3% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process CPU time per operation 32.42 µs/op 31.5 µs/op 2.8% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 107.19 MiB 107.02 MiB 0.2% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocated memory per operation (estimated) 17.52 KiB/op 17.59 KiB/op 0.4% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Time per operation 28.93 µs/op 27.9 µs/op 3.6% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Idle process memory (RSS) 102.9 MiB 102.96 MiB <0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 109.6 MiB 109.13 MiB 0.4% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Managed heap after settling 10.13 MiB 10.13 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 108.47 MiB 107.96 MiB 0.5% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process CPU time per operation 43.34 µs/op 42.47 µs/op 2% less time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 109.65 MiB 109.2 MiB 0.4% less memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Allocated memory per operation (estimated) 8.66 KiB/op 8.28 KiB/op 4.4% less memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Time per operation 12.54 µs/op 11.64 µs/op 7.1% less time Not flagged
Queue an event (32-byte payload)
Shard: capture
Process CPU time per operation 24.85 µs/op 22.4 µs/op 9.9% less time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Allocated memory per operation (estimated) 8.82 KiB/op 8.85 KiB/op 0.3% more memory Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Time per operation 12.63 µs/op 11.71 µs/op 7.3% less time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Process CPU time per operation 24.22 µs/op 22.78 µs/op 5.9% less time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 133.14 KiB/op 132.93 KiB/op 0.2% less memory Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Time per operation 300.26 µs/op 300.38 µs/op <0.1% more time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Process CPU time per operation 461.42 µs/op 460.6 µs/op 0.2% less time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 1.23 MiB/op 1.23 MiB/op <0.1% less memory Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Time per operation 2.54 ms/op 2.55 ms/op 0.6% more time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Process CPU time per operation 3.81 ms/op 3.85 ms/op 0.9% more time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 12.13 MiB/op 12.15 MiB/op 0.1% more memory Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Time per operation 28.19 ms/op 27.9 ms/op 1% less time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Process CPU time per operation 39.96 ms/op 39.62 ms/op 0.8% less time Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Idle process memory (RSS) 272.4 MiB 272.92 MiB 0.2% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Observed peak process memory (RSS) 272.57 MiB 273.14 MiB 0.2% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Managed heap after settling 9.68 MiB 9.68 MiB <0.1% less memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Process memory after settling (RSS) 272.57 MiB 273.14 MiB 0.2% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Lifetime peak process memory (RSS) 273.65 MiB 274.26 MiB 0.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Idle process memory (RSS) 227.52 MiB 227.45 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Observed peak process memory (RSS) 239.96 MiB 239.93 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Managed heap after settling 11.7 MiB 11.7 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Process memory after settling (RSS) 235.72 MiB 235.69 MiB <0.1% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Lifetime peak process memory (RSS) 242.14 MiB 241.97 MiB <0.1% less memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocated memory per operation (estimated) 17.16 KiB/op 17.12 KiB/op 0.3% less memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Time per operation 38.16 µs/op 38.43 µs/op 0.7% more time Not flagged
Evaluate one boolean flag
Shard: flags-single
Process CPU time per operation 40.71 µs/op 41.56 µs/op 2.1% more time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 14.21 KiB/op 14.28 KiB/op 0.5% more memory Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Time per operation 21.04 µs/op 20.82 µs/op 1.1% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Process CPU time per operation 25.45 µs/op 25.1 µs/op 1.4% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocated memory per operation (estimated) 17.64 KiB/op 17.41 KiB/op 1.3% less memory Not flagged
Evaluate a flag (early match)
Shard: flags-single
Time per operation 21.01 µs/op 20.96 µs/op 0.2% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Process CPU time per operation 24.59 µs/op 24.74 µs/op 0.6% more time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocated memory per operation (estimated) 22.48 KiB/op 22.22 KiB/op 1.2% less memory Not flagged
Evaluate a flag (late match)
Shard: flags-single
Time per operation 21.61 µs/op 21.61 µs/op <0.1% less time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Process CPU time per operation 25.35 µs/op 25.28 µs/op 0.3% less time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocated memory per operation (estimated) 17.09 KiB/op 16.84 KiB/op 1.5% less memory Not flagged
Evaluate a flag (no match)
Shard: flags-single
Time per operation 2.9 µs/op 2.93 µs/op 1% more time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Process CPU time per operation 5.77 µs/op 5.76 µs/op 0.2% less time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 12.7 KiB/op 12.61 KiB/op 0.7% less memory Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Time per operation 20.7 µs/op 20.46 µs/op 1.2% less time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Process CPU time per operation 24.3 µs/op 23.63 µs/op 2.7% less time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 18.41 KiB/op 18.23 KiB/op 1% less memory Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Time per operation 39.82 µs/op 39.58 µs/op 0.6% less time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Process CPU time per operation 43.53 µs/op 42.89 µs/op 1.5% less time Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocated memory per operation (estimated) 14.21 KiB/op 14.48 KiB/op 1.9% more memory Not flagged
Evaluate a rollout flag
Shard: flags-single
Time per operation 29.23 µs/op 29.48 µs/op 0.9% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Process CPU time per operation 32 µs/op 32.8 µs/op 2.5% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocated memory per operation (estimated) 17.4 KiB/op 17.42 KiB/op 0.2% more memory Not flagged
Evaluate a multivariate flag
Shard: flags-single
Time per operation 37.87 µs/op 38.04 µs/op 0.5% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Process CPU time per operation 41.47 µs/op 41.14 µs/op 0.8% less time Not flagged

Values are medians of paired measurements. Not flagged does not mean unchanged. Unavailable means a measurement could not be provided, not zero. Estimated allocation measurements retain their instrumentation uncertainty. RSS measures the whole process, not just SDK allocations; observed peaks are sampled checkpoints. Paired ranges in the diagnostics describe variability, not confidence intervals.

Each shard uses its own runner; compare Before PR against With PR within a row, not across shards.

Compared 6b845908db0a812b95cd504970b56f0ea9cd5826 → 1d7c4e400ee14700d61284ad1b1f495f9fb1f56b.

Full report and diagnostics

102 metric rows; 102 rows shown in all results. Omitted or shortened data remain in comparison.json, including per-runner environments, resource limits, measurement methods, samples and checkpoints.

…code send

PostHog never rewrites links on an email sent from code, so it counts no clicks with or without the
field. With false, the email goes out without PostHog's open pixel, so PostHog can't count its opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Silthus and others added 2 commits November 10, 2026 08:02
Replace trackOpensAndClicks with tracking: true, false, or
{ opens, clicks, engagement }. Leave tracking and captureEngagement
out of the body unless the caller sets them, so the API can refuse a
send that sets both. Deprecate captureEngagement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus Silthus changed the title feat(node): let a code send turn off open and click tracking feat(emails): let a code send choose its tracking Nov 10, 2026
Send each tracking form through the public send API to a local HTTP
server and assert the body it receives, instead of a stubbed transport.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 956c45a: 1 Must fix.

See this review and its findings in PostHog Review. Comment @posthog review --deep for a Deep review. A Deep review takes longer and looks deeper than a Standard review.

Comment thread packages/node/src/emails/emails.ts Resolved
Silthus and others added 2 commits November 11, 2026 04:02
A code send now takes an optional `sensitive`, sent only when set. Without
it, PostHog keeps today's behavior: a send whose templateKey matches a
saved template follows that template, and any other send is sensitive.
Click tracking follows it unless `tracking` sets clicks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The template-form refusals now go through a real local server and check
that no request arrived. The latest API reference documents `sensitive`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus Silthus changed the title feat(emails): let a code send choose its tracking feat(emails): let a code send set its tracking and sensitivity Nov 11, 2026

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PostHog Review · Standard · reviewed 4993044: 1 Must fix.

Comment thread packages/node/src/emails/emails.ts Resolved
@Silthus
Silthus marked this pull request as ready for review November 11, 2026 08:03
@Silthus
Silthus requested a review from a team as a code owner November 11, 2026 08:03
@veria-ai

veria-ai Bot commented Nov 11, 2026 •

Copy link
Copy Markdown

PR overview

No security issues were found in this pull request.

Reviews

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not approved — this change needs a human reviewer.

Re-add the stamphog label to request another review once you have addressed this.

The risky changes have tests and STRONG author familiarity, but the current-head review still reports a must-fix finding whose details are unavailable. Ask @PostHog[bot] or a teammate to clarify and clear that finding before re-requesting.

  • Author wrote 100% of the modified lines and has 1 merged PRs in these paths (familiarity STRONG).
  • PostHog Review (Standard) reviewed the current head: 1 finding, posted on the PR.
  • posthog[bot] reviewed the current head.
  • @PostHog[bot]'s current-head COMMENTED review reports one must-fix finding, but the supplied context contains no finding details or acceptance of it. Its resolution cannot be verified against the current code; obtain the finding details and address it or have the reviewer explicitly clear it before re-requesting.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 104L, 4F substantive, 203L/7F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1c-medium (203L, 7F, two-areas, feat)
stamphog 2.7.0 .stamphog/policy.yml @ 4993044 · reviewed head 4993044

Silthus and others added 2 commits November 12, 2026 08:44
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@marandaneto marandaneto left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code looks good; no qualifying findings. Verdict: correct.

Silthus and others added 2 commits November 12, 2026 11:05
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approved.

The risky changes have assurance through the author's STRONG familiarity and accompanying HTTP and type tests. All listed reviews are historical; the older bot summaries provide no concrete concern to verify, and no substantive blocker is evident in the current code.

  • Author wrote 75% of the modified lines and has 2 merged PRs in these paths (familiarity STRONG).
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 53L, 3F substantive, 184L/8F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1c-medium (184L, 8F, two-areas, feat)
stamphog 2.8.0 .stamphog/policy.yml @ 1d7c4e4 · reviewed head 1d7c4e4

@Silthus
Silthus added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit 2877954 Nov 12, 2026
79 checks passed
@Silthus
Silthus deleted the feat/emails-track-opens-and-clicks branch November 12, 2026 16:52
Silthus added a commit that referenced this pull request Nov 12, 2026
Main added tracking and sensitive to a code send (#5630). This branch sends
a code send's form fields through codeFormFields, so they now go out from
there, and capture_engagement is sent only when the caller sets it, as on
main. The fake PostHog takes tracking and sensitive as the send API does,
and a test sends a calendar invite with its file and tracking: { opens: false }.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

3 participants