Skip to content

feat(node): send a flat body from emails.send - #5487

Merged
mayteio merged 1 commit into
mainfrom
posthog/node-email-flat-body
Nov 2, 2026
Merged

mayteio merged 1 commit into
mainfrom
posthog/node-email-flat-body

Conversation

@mayteio

@mayteio mayteio commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Problem

The transactional email send API now takes template_id and distinct_id at the top level of the body (PostHog/posthog#126287), the same names emails.send takes. The nested template.id / identifiers.distinct_id shape stays accepted for 5.61.0, but new versions should send the documented shape.

Changes

  • emails.send posts { template_id, to, message_data, distinct_id }. distinct_id is still only sent when the caller passes distinctId.
  • Nothing callers write changes: arguments, results, errors, retries and the URL stay the same.
  • Patch changeset for posthog-node.

Note

Merge after PostHog/posthog#126287 is deployed, so a release never sends the flat body to a server that doesn't read it yet.

The sdk-specs contract update is PostHog/sdk-specs, branch posthog/flatten-email-send-body.

Release info Sub-libraries affected

Libraries affected

  • posthog-node

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)

  • Claude Code (claude-opus-5-5). The three body assertions in emails.spec.ts changed; with the old source they fail, with the new source all email tests pass (npx vitest run src/__tests__/emails.spec.ts from packages/node). oxfmt is clean.
  • message_data keeps its name, because the server's per-template route and this SDK already use it.

🤖 Generated with Claude Code

Created with PostHog Desktop

emails.send now posts template_id and distinct_id at the top level of the request body. The server accepts both shapes, so this changes only the wire format.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
@mayteio mayteio self-assigned this Oct 30, 2026
@github-actions

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

Copy link
Copy Markdown
Contributor

Node SDK acceptance v2

Commit: 2487e70
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.

@mayteio
mayteio marked this pull request as ready for review November 2, 2026 09:11
@mayteio
mayteio requested a review from a team as a code owner November 2, 2026 09:11
@mayteio

mayteio commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@PostHog review

@posthog

posthog Bot commented Nov 2, 2026

Copy link
Copy Markdown
Contributor

@mayteio PostHog Review is reviewing this pull request.

@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 2487e70: no findings.

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

🤖 An AI agent wrote this on behalf of @ioannisj.

No findings. The flat body matches what the serializer from PostHog/posthog#126287 reads, and the updated tests check the full body with and without distinctId.

@mayteio
mayteio added this pull request to the merge queue Nov 2, 2026
Merged via the queue into main with commit c019e96 Nov 2, 2026
81 checks passed
@mayteio
mayteio deleted the posthog/node-email-flat-body branch November 2, 2026 12:04
Silthus added a commit that referenced this pull request Nov 2, 2026
Take main's flat send body from #5487: the template form sends template_id and distinct_id at the top level, like the code form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
eli-r-ph added a commit that referenced this pull request Nov 4, 2026
* fix(replay): inline stylesheets that load late or after a link changes (#5490)

* fix(replay): inline stylesheets that load late or after a link changes

Three recorder bugs left a `<link rel=stylesheet>` external in recordings,
so replay had to load the CSS remotely or kept the wrong CSS:

- A sheet that loaded more than 5 s after the recorder serialized its link
  was never inlined. When the timer fired, it also removed the load
  listener (regression from cafa9cc). The timer now checks for the sheet
  and leaves the load listener in place. An error event, a disconnected
  link or a full snapshot stops the tracking.
- A `rel="preload"` link that changed to `rel="stylesheet"` after the
  snapshot was never inlined.
- An `href` change on a stylesheet link recorded the old URL, because the
  old sheet stays attached until the new one loads and transformAttribute
  prefers `link.sheet.href`. No `_cssText` followed for the new sheet.

After a `rel` or `href` mutation on a stylesheet link, the recorder now
waits until `link.sheet.href` equals `link.href` and then sends
`_cssText`, the same mutation that the budget path sends. Chrome 131 fires
no load event after such an href change (Chrome 156 does), so the recorder
also checks the sheet on a backoff.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): export link stylesheet helpers from the record entrypoint

The `@posthog/rrweb-record` build resolves `@posthog/rrweb-snapshot` to
`src/record.ts`, which did not export `onceLinkStylesheetUpToDate` and
`isStylesheetLink`, so the browser SDK build failed.

Review fixes for the changed-link inlining:

- Before it sends `_cssText`, the recorder checks again that the link is
  not blocked and still has a mirror id, so a link blocked or removed
  while its sheet loaded is not captured.
- `stop()` resets stylesheet load tracking, so no tracking outlives the
  recording.
- The checks go on at the last delay instead of ending, so a link that
  leaves the DOM without a load or error event is released.
- The `_cssText` mutation carries the link's current attributes (other
  than rel and href), because replay rebuilds the link from its
  snapshot-time attributes. A `media` change no longer reverts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): skip link stylesheet tracking after the buffer is torn down

A consumer can stop recording from inside the mutation callback. The
mutation buffer then registered load tracking for changed stylesheet links
after the stop had reset it, and that tracking held the stopped buffer
until the link loaded. The buffer now records its teardown in reset() and
registers no tracking after it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): keep changed-link stylesheet tracking across full snapshots

Review fixes for the changed-link inlining:

- A full snapshot taken while a changed link's new sheet was loading
  aborted its tracking, and the snapshot inlined the previous href's sheet,
  which is still attached at that time. Changed-link tracking now survives
  the reset at a full snapshot. Stop, and a new record() call, still end it.
- A tracker that ends deletes its map entry only when the entry is its own.
- The stop-inside-emit guard is now a mirror check at registration, which
  also covers shadow-root buffers. Their teardown does not call reset(), so
  the earlier tornDown flag missed them.
- A throwing consumer emit or mask callback in the changed-link listener is
  caught, as in the deferred path, because it runs from a load event or
  timer.
- The link check compares the lowercased nodeName, for XHTML documents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* refactor(replay): slice changed-link inlining and reuse the serializer

Remaining code-review findings for the changed-link stylesheet path:

- A changed link's sheet is stringified in idle slices through the
  existing deferred-inlining task (CSSOM invalidation included), not in one
  task. A full snapshot, a stop, or a block drops the work in flight; the
  full snapshot inlines the current sheet itself. whenIdle and the slice
  sizes move to record/idle.ts so the stylesheet manager can share them.
- The attributes sent with the CSS come from serializeNodeWithId with
  inlineStylesheet off, instead of a second transform-and-mask loop. The
  mutation buffer builds its serialization options in one method.
- The preference for link.sheet.href moves from transformAttribute into the
  snapshot serializer, where base-URL drift happens, so the mutation path
  needs no special case for a changed href.
- Repeated sheet checks wait at least 1 s, so a 0 ms stylesheetLoadTimeout
  does not poll on every timer tick, and tracking ends after 60 s, so a hung
  request does not keep the link and its listener until the next full
  snapshot.
- onceStylesheetLoaded reads link.sheet through readLinkSheet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): check blocking before every delayed stylesheet emit

The stylesheet manager emits a link's CSS after the link was serialized in
three paths: a late load, budget deferral, and a rel/href change. A link or
one of its ancestors can be blocked in between, and the late-load path
checked only the link itself (through re-serialization), which the
slow-sheet fix kept open for longer. The manager now gets blockClass and
blockSelector and checks the link and its ancestors, across shadow hosts,
before each of these emits. The shadow-crossing check moves from the
mutation buffer into utils so that both share it.

The changed-link path also keeps one in-flight task per link: a newer
change discards the older task, so repeated rel/href changes on a link
whose sheet is current do not stringify and emit the same sheet again.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* test(replay): assert changed-link inlining never emits masked values

The replay incident-risk check asks for a real-browser test that sensitive
values are absent from the emitted bytes. The changed-link path now sends
the link's current attributes with its CSS, so the test records a preload
link that becomes a stylesheet under maskAllElementAttributes and asserts
that neither an attribute value nor the CSS text is in any emitted event.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): end delayed stylesheet work when its recorder stops

A changed link's idle inlining task outlived stop(). A host that stops and
restarts recording (for example with maskAllElementAttributes turned on)
refills the shared mirror, so the old task's mirror check passed again. The
task then serialized the link with the old recorder's masking options and
emitted into the new recording.

The stylesheet manager now has a stop(), which the recorder's stop handler
calls after the deferred flush. A stopped manager schedules nothing,
drops its pending changed-link tasks, ends load tracking, and emits no
stylesheet mutation.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): send removed link attributes and detect new sheets after pushState

Replay rebuilds a changed link from the attributes recorded for it before,
so the _cssText mutation now sends null for each attribute in the previous
serialization that the link no longer has. Before, a removed media came back.

onceLinkStylesheetUpToDate also counts a new sheet object as up to date.
After a pushState, link.href resolves against the new base URL, so a
relative href never matched sheet.href and the check gave up. Each new
change to the link moves the baseline sheet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): restart changed-link inlining after a CSSOM change, free the baseline sheet

A CSSOM change to a changed link's new sheet during idle slicing cancelled
the task, and replay kept the previous sheet for good. The changed-link path
now starts again from the changed sheet, at most 3 times, then counts a
failure. The restarted text includes the change, and its _cssText arrives
after the recorded rule event.

The sheet attached at a link's latest change now lives in the tracking
entry, not in a module WeakMap, so it is released when tracking ends, at a
reset or stop, and at once when the sheet is already current.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): flush pending changed-link CSS at pagehide and stop

Changed-link inlining queued its idle work apart from the budget-deferred
queue, so a page that went away before the idle callback ran lost the new
sheet's CSS. The idle step and a new synchronous flush now share one slice
routine, and pagehide and stop run that flush after the deferred queue's.
Both share the existing 50-slice teardown cap, so the bound on synchronous
work at teardown is unchanged. A sheet that does not fit is dropped and
counted as abandoned.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): send removed attributes when a late-loading stylesheet is inlined

Replay rebuilds a link from the attributes recorded for it before when
_cssText arrives, so an attribute removed between the link's serialization
and its sheet's load came back. onStylesheetLoad now also gets the link's
serialization from before the load, and the late-load mutation sends null
for each attribute the link no longer has, as the changed-link path does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): guard the teardown flush against reentry, cancel idle work of dropped jobs

An emit during the pagehide or stop flush can stop the recorder, and that
teardown flushed again with a fresh 50-slice budget, so the cap did not
hold. The flush now returns at once when it is already running.

Changed-link jobs now live in one map from link to job. Releasing a job
also cancels its queued idle callback, and stop, the full-snapshot reset
and a newer change to the same link drop pending jobs at once, so a frozen
page does not keep a job and its CSS cursor alive.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 0bf0316)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 1a214aa)

* feat(ai): add Firebase Genkit integration (#5381)

* feat(ai): add Firebase Genkit integration

Genkit traces itself with OpenTelemetry, but its spans use the genkit:
attribute namespace. The @posthog/ai/otel processor only forwards gen_ai.*,
llm.*, ai.* and traceloop.* spans, so no Genkit flow, model call or tool
call reached PostHog.

The new @posthog/ai/genkit entry point exports PostHogGenkitSpanProcessor.
It reads Genkit spans as they end and captures them with the existing
helpers. The root span becomes an $ai_trace, each model action becomes an
$ai_generation, and other Genkit actions become $ai_span events.

Closes #3842

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(ai): address genkit review feedback

- Replace credential fields such as config.apiKey with [REDACTED] in
  span input and output state.
- Capture embedder actions as $ai_embedding events without the vectors.
- Send the googleai and vertexai plugins as the gemini and vertex providers.
- Map media parts to the shared image and inline data blocks.
- Rebuild span errors as Error objects with the original stack, and take
  the class name from the stack, because OpenTelemetry records error.code
  as exception.type.
- Format span errors with formatAiError, so privacy mode applies to them.
- Document the npm ERESOLVE workaround.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(ai): bound Genkit media parsing and preserve span identity

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: Manoel Aranda Neto <[email protected]>
(cherry picked from commit bac61c2)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 90150ac)

* fix(replay): replay a plain link when a changed link's new sheet cannot be inlined (#5512)

* fix(replay): replay a plain link when a changed link's new sheet cannot be inlined

When a <link rel=stylesheet> changes href to a sheet that is unreadable
(cross-origin without CORS) or stringifies to nothing, no _cssText was
sent. If replay already showed the link as a <style>, it kept the old
sheet's CSS, because replay cannot turn a <style> back into a <link>.

The recorder now sends a remove and an add of the same node, as for a moved
node, with a serialization that has no _cssText, so replay builds a <link>
that loads the new href. It does this only when replay shows the link as a
<style>: its last serialization has _cssText, or this recorder sent
_cssText for it after the last full snapshot.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): record the plain-link re-add through the mutation buffer

The re-add went out directly, so a move of the link that a frozen buffer
still held reached replay after it. Replay then failed to remove the old
<style>, and its CSS stayed. The buffer now gets one childList record that
removes the link and one that adds it back, as for a node taken out and put
back in place, so the re-add stays in order with buffered mutations.

The fallback also runs inside a try/catch: it can run from a load event or
timer, where a throwing consumer emit or mask callback must not surface on
the page.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(replay): take the observer's pending records before re-adding a link

The re-add of a link needs its next sibling in the mirror. If the page
inserted that sibling in the same task, for example just before stop(), the
observer had not delivered its record yet, so the buffer recorded the remove
but dropped the add, and replay lost the link. The buffer now processes the
observer's pending records (takeRecords) before the two re-add records.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
(cherry picked from commit bc6b54c)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 3987245)

* ci: remove watcher workflows and runner egress auditing (#5510)

* ci: remove watcher workflows and runner egress auditing

* test: remove obsolete runner egress auditing assertions

(cherry picked from commit 9564f12)

* feat(node): send a flat body from emails.send (#5487)

emails.send now posts template_id and distinct_id at the top level of the request body. The server accepts both shapes, so this changes only the wire format.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
(cherry picked from commit c019e96)

* chore: update versions and lockfile [version bump]

(cherry picked from commit fb0c941)

* fix(ai): capture vercel ai sdk file parts passed as bytes (#5496)

* fix(ai): capture vercel ai sdk file parts passed as bytes

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(ai): keep vercel files typed, sized and streamed

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(ai): share one file budget per vercel generation

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 3611fd2)

* chore: update versions and lockfile [version bump]

(cherry picked from commit a90eff1)

* fix(browser): sanitize survey description HTML (#5491)

* fix(browser): sanitize survey description HTML

* fix(surveys): preserve safe new-tab description links

* fix(browser): defer explicit survey renderer loading

* fix(surveys): block stylesheet and form description markup

* fix(surveys): isolate HTML sanitizer configuration

* fix(surveys): align HTML allowlists with the backend

(cherry picked from commit aedb488)

* fix(browser): enforce script loading policies before reuse (#5493)

(cherry picked from commit 5e872e1)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 2dc13a1)

* ci: disable error-tracking harness workflow (#5519)

(cherry picked from commit dc06863)

* fix(browser): sanitize web experiment transforms (#5492)

* fix(browser): sanitize web experiment transforms

* fix(browser): preserve experiment transform parsing contexts

* test(browser): normalize near-origin Firefox navigation timings

* fix(browser): preserve template and MathML experiment content

* fix(browser): preserve safe experiment link and form attributes

* chore(browser): update mangled experiment property names

* fix(browser): isolate web experiment sanitizer configuration

* fix(browser): preserve MathML annotation parsing contexts

(cherry picked from commit 84a5277)

* fix(browser): detect lightpanda headless browser traffic (#5505)

* fix(browser): detect lightpanda headless browser traffic

* fix: document lightpanda detection in shared SDK releases

* chore: limit Lightpanda changeset to affected packages

---------

Co-authored-by: Manoel Aranda Neto <[email protected]>
(cherry picked from commit 3fbb667)

* fix(ai): keep vercel file ids and one placeholder per file (#5520)

Co-authored-by: Claude Opus 5.5 <[email protected]>
(cherry picked from commit af74347)

* fix(surveys): respect the selected position for tab widget surveys (#5503)

* fix(surveys): respect the selected position for tab widget surveys

* fix(surveys): release tab popup position fix in posthog-js

---------

Co-authored-by: Manoel Aranda Neto <[email protected]>
(cherry picked from commit 89c3a6b)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 678b10f)

* fix(react-native): upload source maps from the SOURCEMAP_FILE directory in posthog-xcode.sh (#5525)

Co-authored-by: Claude Sonnet 5.5 <[email protected]>
(cherry picked from commit f3fe629)

* chore: update versions and lockfile [version bump]

(cherry picked from commit cbfc706)

* ci: move native plugin builds to macOS 15 (#5528)

(cherry picked from commit 57dae19)

* feat(browser): support customer-pinned script integrity (#5494)

* feat(browser): support customer-pinned script integrity

* test(browser): pin integrity fixtures to an exact loader version

* docs(browser): keep script integrity guidance in PR context

* test(browser): pin integrity to the selected compatibility core

* fix(browser): enforce bootstrap integrity and pin manifests to releases

* test(browser): distinguish config requests from script fallbacks

(cherry picked from commit b033b8c)

* ci: guard PR updates when PostHog upgrade has no changes (#5529)

(cherry picked from commit 611a754)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 76ec5c9)

* fix(ai): load OpenTelemetry integration in Cloudflare Workers (#5530)

* fix(ai): load OpenTelemetry integration in Cloudflare Workers

* fix(ai): bundle OTel batching and test supported bundlers

* test(ai): check declarations with browser exports

* chore(ai): remove standalone OpenTelemetry license file

* ci(ai): omit consumer log artifact uploads

(cherry picked from commit 9909fee)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 2908aea)

* fix(browser): only capture $opt_in when the user wasn't already opted in (#5504)

* fix(browser): only capture $opt_in when the user wasn't already opted in

opt_in_capturing() captured $opt_in on every call. Consent tools often
re-apply stored consent on every page load, which sent one billed event
per page. It now skips the event when consent was already granted;
opting out and back in, or reset() then opting in, still captures it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* chore(browser): update public API references for opt_in_capturing docs

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(browser): heal a stale cookieless distinct_id when opt_in_capturing() skips $opt_in

When consent was granted in another tab, this tab still holds the cookieless
sentinel. capture() used to heal it as a side effect of sending $opt_in; with
the event now skipped for already-granted consent, sync the identity directly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(browser): adopt the shared identity before re-enabling persistence on a repeat opt-in

Re-enabling persistence saves this tab's in-memory state, which overwrote the
identity the other tab had persisted and left a freshly minted id. Sync the
cookieless identity first, while the shared one can still be loaded.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 7e023a2)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 8006915)

* feat(next): tracing-header identity, request properties and a proxy wrapper for onRequestError (#5473)

- onRequestError takes identity from the X-POSTHOG-* tracing headers,
  then the PostHog cookie, then none. It drops a cookie session that
  posthog-js would have ended: 30 minutes idle or 24 hours old.
- It adds $current_url, $request_method, $request_path and $user_agent,
  without the query string. They keep the app's basePath, and a Pages
  Router data request is reported as its page.
- withPostHogProxy captures what proxy.ts or middleware throws, which
  Next.js never reports on 16 or for Node.js middleware. It rethrows at
  once and hands the send to event.waitUntil for at most 2 seconds, like
  Sentry. It marks the error it handles, so that onRequestError skips
  Next.js's own report of it, and it captures each failing request
  once, also when many throw one error object.
- An error that escapes middleware or a proxy is sent as unhandled, with
  mechanism type middleware and $exception_source nextjs.middleware or
  nextjs.proxy, as the exception metadata spec asks of a request
  boundary. Route, render and action errors keep posthog-node's manual
  capture metadata.
- @posthog/core: the cookie parser returns the session's last activity
  and start timestamps.

Co-authored-by: Claude Opus 5.5 <[email protected]>
(cherry picked from commit 5c8f314)

* chore: update versions and lockfile [version bump]

(cherry picked from commit 48f16d8)

* fix(flags): send flag exposures read before consent when the user opts in (#5481)

* fix(flags): send flag exposures read before consent when the user opts in

* fix(flags): keep pending flag exposures tied to the identity that read them

* fix(flags): recheck the identity before each pending flag exposure

* fix(flags): adopt a sibling cookie identity before the opt-in exposure drain

(cherry picked from commit 5764ddd)

* test(ai): assert the Genkit processor sends through captureAi on node 6

The synced Genkit tests spied on capture, but a posthog-node 6 client
sends AI events through captureAi, which captureAiEvent picks for it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* fix(next): type the error properties builder from posthog-node

On capture-v1, @posthog/next uses npm posthog-node 5.x, which ships its
own @posthog/core copy, while this package imports the workspace core.
ErrorPropertiesBuilder has a private field, so the two copies did not
match and the build failed. Take the type from posthog-node instead.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: Robbie <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Co-authored-by: releaser-posthog-js[bot] <249978072+releaser-posthog-js[bot]@users.noreply.github.com>
Co-authored-by: Shohruh Toyirov <[email protected]>
Co-authored-by: Manoel Aranda Neto <[email protected]>
Co-authored-by: Manoel Aranda Neto <[email protected]>
Co-authored-by: Harley Alexander <[email protected]>
Co-authored-by: Bernat Torres <[email protected]>
Co-authored-by: Hugues Pouillot <[email protected]>
Co-authored-by: Majicmaj <[email protected]>
Co-authored-by: Muhammad Abdullah Shah <[email protected]>
Co-authored-by: alexcastgonz <[email protected]>
Co-authored-by: Sean Deehan <[email protected]>
Co-authored-by: Aleksander Błaszkiewicz <[email protected]>
Co-authored-by: Gustavo H. Strassburger <[email protected]>
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