Skip to content

feat(node): set a recipient's email topic preferences - #5511

Merged
Silthus merged 9 commits into
mainfrom
feat/node-email-preferences
Nov 12, 2026
Merged

Silthus merged 9 commits into
mainfrom
feat/node-email-preferences

Conversation

@Silthus

@Silthus Silthus commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Problem

  • Node servers that send PostHog Workflows email cannot subscribe or unsubscribe a recipient from topics with the SDK.
  • They have to hand-roll an HTTP call with the right key, retries and error handling.

Impact

  • Grow Workflows: topic choices made in a customer's own app reach Workflows sends. Seen in messaging preferences api set events from the server route.

Caution

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

Changes

  • posthog.emails.setPreferences({ to, allMarketing?, topics? }) saves a recipient's choices in one call. true opts in, false opts out.
  • It resolves { data, error } and never rejects, like emails.send.
  • data holds the saved preferences by topic key. Opting back in to one marketing topic after an all-marketing opt-out returns allMarketing: true.
  • Bad input fails before any request. A missing secret key names the messaging_preference:write scope. Keys with hog_flow:write still work.
  • 503, 429 and network errors retry, as in emails.send. A 404 does not.
  • An unknown topic resolves code: unknown_topic. A plain not_found means the PostHog instance has no preferences API yet.
  • A reply with a non-boolean topic value fails with api_error.
  • The changeset, types export and API references are mechanical.

Release info Sub-libraries affected

Libraries affected

  • posthog-node

Checklist

  • All commits are signed (see Commit signing)
  • Tests for new code
  • Accounted for backwards compatibility of any changes (no breaking changes!)

If releasing new changes

  • Ran pnpm changeset to generate a changeset file

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

  • Claude Code, Claude Opus 5.5. Skills: /tdd, /merge-risk.
  • Recreates the closed PR 5173 inside posthog-node, next to emails.send, with a thinner contract.
  • Test seam: emails.spec.ts drives setPreferences with a mocked fetch: request shape, omitted fields, 404, 503 retry, missing key, invalid input.
  • Live run at 5c484f3: the built SDK against the server branch at 870b4fb on a devbox. It saved choices, kept earlier ones, resolved unknown_topic for an unknown topic, and refused empty input before any request.
  • Shadow diff: base 8c83275 and this branch ran the same 14 send and get scenarios against one scripted local app host. Requests and results are identical.
Live run output
setPreferences({ to: 'sdk4@example.com', topics: { 'product-updates': false } })
  { data: { to: 'sdk4@example.com', allMarketing: null, topics: { 'product-updates': false } }, error: null }
setPreferences({ to: 'sdk4@example.com', allMarketing: false })
  { data: { to: 'sdk4@example.com', allMarketing: false, topics: { 'product-updates': false } }, error: null }
setPreferences({ to: 'sdk4@example.com', topics: { nope: true } })
  { data: null, error: { name: 'not_found', code: 'unknown_topic', statusCode: 404, message: 'This project has no topic with the key nope.' } }
setPreferences({ to: 'sdk4@example.com' })
  { data: null, error: { name: 'validation_error', code: 'invalid_preferences', message: 'Set allMarketing, topics, or both.' } }

Stored row: {"$all": "OPTED_OUT", "<product-updates id>": "OPTED_OUT"}
Decisions
Question Answer Who Where
Where the SDK call lives posthog-node, next to emails.send 👤 Human comment
Contract shape Server route plus a thin SDK call, released after the route is live 👍 Accepted default comment
SDK call One declarative setPreferences 👍 Accepted default comment
SDK retries Same rule as emails.send 🤖 Agent comment

Involvement: 2 questions answered, design pass, 1 GPT-6.1 Sol review round (2 fixed, 1 rejected), QA swarm panel of 7 lenses (Opus 5.5 fresh eyes, GPT-6.1 Sol for the rest, SDK spec conformance as the team lens: 5 fixed, 1 rejected, 1 follow-up), proof judge (1 fixed), 4 GPT-6.1 Sol final reads with no findings, live run and shadow diff captured.

🤖 Generated with Claude Code

…Preferences

posthog.emails.setPreferences({ to, topics, allMarketing }) opts one recipient out of
email topics, or back in, in one request to /api/projects/@current/messaging/preferences/
with the client's project secret key. It resolves to { data, error } like send and get,
and retries network errors, rate limits and an unavailable service.

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

github-actions Bot commented Nov 2, 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 2, 2026 •

Copy link
Copy Markdown
Contributor

Node SDK acceptance v2

Commit: 0f9e590
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.

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

Silthus commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

Note

🤖 Claude Opus 5.5 responding on behalf of Michael

Decisions for PostHog/posthog#127204 and #5511:

  • 👤 Where the SDK call lives In posthog-node, next to emails.send, replacing the separate package of the closed PR 5173.
  • 👍 Contract shape One server route that a project secret key can call, plus a thin SDK call. The SDK releases only after the route is live.
  • 👍 SDK call One declarative emails.setPreferences({ to, topics, allMarketing }). true opts in and false opts out.
  • 🤖 Who may call the route Only a project secret key with hog_flow:write. Session and personal keys get 401, because the change comes from the customer's server.
  • 🤖 Unknown topic key The whole call returns 404 and saves nothing, instead of skipping unknown keys.
  • 🤖 Opting back in after an all-marketing opt-out The other marketing topics stay opted out, the same rule the opt-out UI uses.
  • 🤖 Rollout The route sits behind workflows-transactional-email-api, like the send API.
  • 🤖 SDK retries The same rule as emails.send: 503, 429 and network errors retry. Other errors do not.

Silthus and others added 3 commits November 2, 2026 12:07
…try claim

- An unknown topic now carries PostHog's `unknown_topic` code, so a caller can tell it from an instance without the preferences API.
- The JSDoc says a retry that lands after a newer change undoes it, and that `to` must match the address Workflows sends to.
- The preferences example test replies with the documented order, and the shared EmailSender docs cover preferences changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…arketing opt-out

Opting in to one topic after an all-marketing opt-out lifts that opt-out on the server, so the example reply keeps `all_marketing: true`. The `topics` JSDoc now says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A transactional topic opted back in leaves `allMarketing` as it was, so the `topics` JSDoc names marketing topics.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Silthus
Silthus marked this pull request as ready for review November 3, 2026 07:55
@Silthus
Silthus requested a review from a team as a code owner November 3, 2026 07:55
stamphog[bot]

This comment was marked as outdated.

…rences

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@stamphog
stamphog Bot dismissed their stale review November 3, 2026 11:05

New commits were pushed — dismissing the stamphog approval from an earlier head. This PR no longer qualifies for automatic review.

@stamphog stamphog Bot removed the stamphog label Nov 3, 2026

@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 — escalated to a human reviewer.

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

The public API and authenticated preference-write path need independent assurance; the author's discussion comment does not provide it. Request a current-head review from Harley Alexander, another teammate, or an agent reviewer covering the contract and write behavior before re-requesting.

  • Risky territory without independent assurance: Public API contracts — packages/node/src/emails/types.ts and packages/node/src/exports.ts expose a new recipient-preferences method and its public request and response types.; Data ingestion or write paths — packages/node/src/emails/emails.ts posts recipient subscription changes to the messaging preferences endpoint and retries transient failures.; Auth or security-sensitive surface — packages/node/src/emails/emails.ts adds a secret-key-authenticated operation that changes a recipient's email consent preferences.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 205L, 6F substantive, 327L/9F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1d-complex (327L, 9F, two-areas, feat)
stamphog 2.7.0 .stamphog/policy.yml @ 5b40c38 · reviewed head 5b40c38

@Silthus
Silthus requested a review from mayteio November 3, 2026 12:06
…rences

setPreferences now names the messaging_preference:write scope that the server requires since the scopes split.

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

github-actions Bot commented Nov 5, 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) 10.15 KiB/op 10.01 KiB/op 1.4% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Time per operation 23.75 µs/op 24.59 µs/op 3.5% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Idle process memory (RSS) 102.49 MiB 102.5 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 106.51 MiB 106.17 MiB 0.3% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Managed heap after settling 10.04 MiB 10.05 MiB <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 106.18 MiB 105.83 MiB 0.3% less memory Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Process CPU time per operation 41.1 µs/op 42.47 µs/op 3.3% more time Not flagged
Capture and deliver events (32-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 106.76 MiB 106.36 MiB 0.4% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Allocated memory per operation (estimated) 17.56 KiB/op 17.88 KiB/op 1.8% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Time per operation 39.19 µs/op 39.92 µs/op 1.9% more time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Idle process memory (RSS) 102.96 MiB 103.09 MiB 0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Observed peak process memory (RSS) 113.71 MiB 113.76 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% more memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process memory after settling (RSS) 109.64 MiB 109.64 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Process CPU time per operation 57.54 µs/op 57.89 µs/op 0.6% more time Not flagged
Capture and deliver events (4,096-byte payload, burst)
Shard: capture
Lifetime peak process memory (RSS) 116.41 MiB 116.36 MiB <0.1% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Allocated memory per operation (estimated) 10.4 KiB/op 10.41 KiB/op <0.1% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Time per operation 24.91 µs/op 24.83 µs/op 0.4% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Idle process memory (RSS) 102.45 MiB 102.27 MiB 0.2% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 106.73 MiB 106.38 MiB 0.3% 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% more memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 106.73 MiB 106.42 MiB 0.3% less memory Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Process CPU time per operation 41.94 µs/op 41.93 µs/op <0.1% less time Not flagged
Capture and deliver events (32-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 106.83 MiB 106.81 MiB <0.1% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Allocated memory per operation (estimated) 18.38 KiB/op 17.57 KiB/op 4.4% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Time per operation 36.13 µs/op 36.94 µs/op 2.2% more time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Idle process memory (RSS) 102.78 MiB 102.9 MiB 0.1% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Observed peak process memory (RSS) 109.62 MiB 109.36 MiB 0.2% 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% more memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process memory after settling (RSS) 108.46 MiB 107.89 MiB 0.5% less memory Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Process CPU time per operation 54.77 µs/op 55.76 µs/op 1.8% more time Not flagged
Capture and deliver events (4,096-byte payload, steady)
Shard: capture
Lifetime peak process memory (RSS) 109.67 MiB 109.39 MiB 0.3% less memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Allocated memory per operation (estimated) 8.7 KiB/op 8.7 KiB/op <0.1% more memory Not flagged
Queue an event (32-byte payload)
Shard: capture
Time per operation 17.66 µs/op 18.66 µs/op 5.7% more time Not flagged
Queue an event (32-byte payload)
Shard: capture
Process CPU time per operation 33.92 µs/op 34.34 µs/op 1.2% more time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Allocated memory per operation (estimated) 9.01 KiB/op 8.74 KiB/op 3% less memory Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Time per operation 17.86 µs/op 17.44 µs/op 2.4% less time Not flagged
Queue an event (4,096-byte payload)
Shard: capture
Process CPU time per operation 33.79 µs/op 32.92 µs/op 2.6% less time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 131.64 KiB/op 133.1 KiB/op 1.1% more memory Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Time per operation 186.88 µs/op 186.3 µs/op 0.3% less time Not flagged
Evaluate all 10 flags
Shard: flags-scaling
Process CPU time per operation 314.67 µs/op 313.29 µs/op 0.4% less time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 1.22 MiB/op 1.23 MiB/op 1.1% more memory Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Time per operation 1.61 ms/op 1.61 ms/op 0.2% less time Not flagged
Evaluate all 100 flags
Shard: flags-scaling
Process CPU time per operation 2.68 ms/op 2.69 ms/op 0.4% more time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Allocated memory per operation (estimated) 12.14 MiB/op 12.16 MiB/op 0.2% more memory Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Time per operation 16.8 ms/op 16.84 ms/op 0.2% more time Not flagged
Evaluate all 1,000 flags
Shard: flags-scaling
Process CPU time per operation 26.61 ms/op 26.66 ms/op 0.2% more time Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Idle process memory (RSS) 271.19 MiB 273.26 MiB 0.8% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Observed peak process memory (RSS) 271.44 MiB 273.26 MiB 0.7% 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% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Process memory after settling (RSS) 270.68 MiB 272.62 MiB 0.7% more memory Not flagged
Evaluate 1,000 flags under concurrent load
Shard: flags-memory
Lifetime peak process memory (RSS) 271.88 MiB 274.01 MiB 0.8% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Idle process memory (RSS) 222.88 MiB 227.68 MiB 2.2% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Observed peak process memory (RSS) 239.38 MiB 240.1 MiB 0.3% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Managed heap after settling 11.7 MiB 11.7 MiB <0.1% more memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Process memory after settling (RSS) 234.97 MiB 234.47 MiB 0.2% less memory Not flagged
Refresh 1,000 flag definitions
Shard: flags-memory
Lifetime peak process memory (RSS) 241.02 MiB 241.55 MiB 0.2% more memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Allocated memory per operation (estimated) 17.1 KiB/op 17.12 KiB/op 0.1% more memory Not flagged
Evaluate one boolean flag
Shard: flags-single
Time per operation 52.99 µs/op 52.25 µs/op 1.4% less time Not flagged
Evaluate one boolean flag
Shard: flags-single
Process CPU time per operation 57.47 µs/op 56.06 µs/op 2.5% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 14.27 KiB/op 14.23 KiB/op 0.2% less memory Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Time per operation 30.15 µs/op 29.88 µs/op 0.9% less time Not flagged
Evaluate a cohort-filtered flag
Shard: flags-single
Process CPU time per operation 35.69 µs/op 35.44 µs/op 0.7% less time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Allocated memory per operation (estimated) 17.43 KiB/op 17.55 KiB/op 0.7% more memory Not flagged
Evaluate a flag (early match)
Shard: flags-single
Time per operation 29.93 µs/op 30.89 µs/op 3.2% more time Not flagged
Evaluate a flag (early match)
Shard: flags-single
Process CPU time per operation 34.51 µs/op 35.83 µs/op 3.8% more time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Allocated memory per operation (estimated) 22.23 KiB/op 22.45 KiB/op 1% more memory Not flagged
Evaluate a flag (late match)
Shard: flags-single
Time per operation 31.03 µs/op 30.39 µs/op 2.1% less time Not flagged
Evaluate a flag (late match)
Shard: flags-single
Process CPU time per operation 35.95 µs/op 34.72 µs/op 3.4% less time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Allocated memory per operation (estimated) 16.96 KiB/op 16.92 KiB/op 0.2% less memory Not flagged
Evaluate a flag (no match)
Shard: flags-single
Time per operation 4.18 µs/op 4.11 µs/op 1.7% less time Not flagged
Evaluate a flag (no match)
Shard: flags-single
Process CPU time per operation 7.71 µs/op 7.55 µs/op 2% less time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 12.57 KiB/op 12.57 KiB/op <0.1% more memory Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Time per operation 28.52 µs/op 28.76 µs/op 0.9% more time Not flagged
Evaluate a property-filtered flag
Shard: flags-single
Process CPU time per operation 32.79 µs/op 33.31 µs/op 1.6% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Allocated memory per operation (estimated) 18.1 KiB/op 18.09 KiB/op <0.1% less memory Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Time per operation 56.75 µs/op 57.1 µs/op 0.6% more time Not flagged
Evaluate a regex-filtered flag
Shard: flags-single
Process CPU time per operation 61.23 µs/op 62.63 µs/op 2.3% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Allocated memory per operation (estimated) 14.32 KiB/op 14.33 KiB/op 0.1% more memory Not flagged
Evaluate a rollout flag
Shard: flags-single
Time per operation 40.32 µs/op 40.99 µs/op 1.7% more time Not flagged
Evaluate a rollout flag
Shard: flags-single
Process CPU time per operation 44.83 µs/op 46.23 µs/op 3.1% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Allocated memory per operation (estimated) 17.5 KiB/op 17.57 KiB/op 0.4% more memory Not flagged
Evaluate a multivariate flag
Shard: flags-single
Time per operation 52.32 µs/op 52.76 µs/op 0.8% more time Not flagged
Evaluate a multivariate flag
Shard: flags-single
Process CPU time per operation 57.2 µs/op 57.8 µs/op 1.1% more 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 f2af66f094f748ba20fe3c60f20f588290ba1171 → 0f9e5906febb9983379911430cd53426f5cdeb6f.

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.

@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.

…rences

# Conflicts:
#	packages/node/references/posthog-node-references-latest.json
#	packages/node/src/client.ts
#	packages/node/src/emails/emails.ts
#	packages/node/src/exports.ts
@veria-ai

veria-ai Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

Reviews

@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 ee18856: 1 Should 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

@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 current code leaves @PostHog's substantive retry-ordering concern unresolved, and no qualifying current-head review covers the risky changes. Fix the concern and request review from Vincent (Wen Yu) Ge, Harley Alexander, or another teammate before re-requesting.

  • PostHog Review (Standard) reviewed the current head: 1 finding, posted on the PR.
  • posthog[bot] reviewed the current head.
  • @PostHog on packages/node/src/emails/emails.ts: the retry-ordering concern remains unfixed. Lines 223-225 replay the same preferences POST through withRetries, whose loop waits and retries after network failures; the request has no deduplication key or ordering protection. An accepted opt-in with a lost response can therefore be replayed after a newer successful opt-out and overwrite it. Disable automatic retries for this operation or implement server-backed replay protection, then address the thread before re-requesting.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 213L, 6F substantive, 497L/11F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1d-complex (497L, 11F, two-areas, feat)
stamphog 2.8.0 .stamphog/policy.yml @ ee18856 · reviewed head ee18856

@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 0f9e590: no findings.

@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 public API and authenticated write path have independent assurance from the current-head PostHog Review. No concrete unresolved review concern or other policy refusal ground is present; the scanning-status discussion comment is not an eyes reaction.

  • PostHog Review (Standard) reviewed the current head: no findings.
  • posthog[bot] reviewed the current head.
Gate mechanics and policy version
Gate Result
prerequisites ✓ all clear
deny-list ✓ no deny categories matched
size ✓ 213L, 6F substantive, 497L/11F incl. docs/generated/snapshots — within ceiling
tier ✓ T1-agent / T1d-complex (497L, 11F, two-areas, feat)
stamphog 2.8.0 .stamphog/policy.yml @ 0f9e590 · reviewed head 0f9e590

@Silthus
Silthus added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit b64482a Nov 12, 2026
79 checks passed
@Silthus
Silthus deleted the feat/node-email-preferences branch November 12, 2026 10:16
Silthus added a commit that referenced this pull request Nov 12, 2026
Main added emails.setPreferences (#5511), whose reply parser reads
Reply.payload. This branch changed Reply to carry the raw text, so
parsePreferencesReply now parses that text like the other reply parsers.

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.

2 participants