Repository navigation
feat(browser): add a local bootstrap and preserve SDK namespaces - #5470
Conversation
Replay incident risk checkThis diff touches code involved in past incidents. This is a heads-up, not a verdict: read the matched sections of INCIDENTS.md and answer their review questions before merging. For a judgment on whether this diff has the same failure mode, run the |
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 ( 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)
What this means and how to fix itInput 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
To get the line to add, run |
|
Size Change: +1.06 kB (0%) Total Size: 25.3 MB 📦 View Changed
ℹ️ View Unchanged
|
posthog-js Compliance ReportDate: 2026-10-30 09:03:21 UTC ✅ All Tests Passed!26/26 tests passed Capture Tests✅ 26/26 tests passed View Details
|
|
Final namespace policy: preserve the namespace the user selects. The local installation is additive, but this PR also intentionally changes mutable hosted loaders from selecting their own immutable build to following the corresponding SDK alias. This supersedes the earlier comparison.
The SDK download is asynchronous in every row. A hosted loader tag with For the local option, serve the small bootstrap with application assets and execute it before initialization and callers. Top-level and major aliases pick up SDK releases on later visits after normal cache expiry. Exact versions remain pinned. The unversioned alias can cross major versions, matching the original inline snippet. Already-running pages are not hot-upgraded. The locally generated method list remains fixed, so newly introduced methods may need a refreshed bootstrap for use before SDK readiness. No installation buffers browser interactions before SDK listeners start. Validation: 5,973 browser unit tests, 10 functional tests, and all 63 focused loader browser cases pass across Chromium/Firefox/WebKit. The complete deployment matrix passed 525 cells / 1,050 executions during snapshot regeneration, followed by a separate full read-only run with zero differences. Only the current-loader golden changed: core URL values and their resulting request sort order. No timing or privacy normalization changed. The hosted-loader policy and local selector are new since the earlier review, so a fresh review is requested. |
turnipdabeets
left a comment
There was a problem hiding this comment.
Works well: the loader suites pass locally (60/60), and the refactor into loader-stub.ts / loader-script.ts keeps loader.js behaving as before. One shape question before data-loader-src becomes public, plus two small ones inline.
The red "Browser deployment snapshots" check is historical/snippet/firefox/replay: repetitions differ ... fetchStart. That's timing noise in a historical snippet cell, not the loader, so a re-run should clear it.
|
Update in 8c8a0ff: exact-build pinning is no longer the default. Both hosted and local loading now honor the user's chosen namespace:
Mutable aliases provide automatic SDK upgrades after normal cache expiry, with no local recopy needed for each SDK release. Exact-version users retain explicit pinning. The locally generated method inventory remains fixed, and already-running pages are not hot-upgraded. The earlier intermediate design defaulted the local bootstrap to its build's major; the final default is now the unversioned alias, matching the original inline snippet. The PR description and comparison table are updated and fresh review is requested. Original concern, addressed by the policy above:
|
|
still figuring out a non blocking solution looks like we need 3 options the original one, totally async, with a copy-paste script (huge) the new one, #5406 downloads the loader sync, then async, might lose some events until the loader is downloaded the local one (current pr), similar to the new one, but people can copy-paste the loader during build time, behaving as the original one, but much simpler snippet and loads the list of methods dynamically |
Using the optional local bootstrapAfter installing an SDK release containing this PR, copy the SDK-maintained bootstrap into your application's public assets during the build: pnpm add -D posthog-js
mkdir -p public/scripts
cp node_modules/posthog-js/dist/loader-bootstrap.js public/scripts/posthog-bootstrap.jsRun the copy step as part of the application build. For Gatsby, use Include the local script before initialization and application code: <script
src="/scripts/posthog-bootstrap.js"
data-asset-host="https://us-assets.i.posthog.com"
data-sdk-version="1"
></script>
<script>
posthog.init("phc_YOUR_PROJECT_TOKEN", {
api_host: "https://us.i.posthog.com",
defaults: "2026-08-30",
before_send: function (event) {
return event.event === "private event" ? null : event;
},
});
posthog.capture("app_started");
</script>
<script type="module" src="/assets/app.js"></script>
Choose the SDK namespace
The example selects major 1. Remove the version attribute to match the original inline snippet's Loading behavior and constraints
This is usage guidance for the unreleased API in this PR. It does not require moving the SDK's detailed usage documentation back into the package README. |
ioannisj
left a comment
There was a problem hiding this comment.
🤖 An AI agent wrote this on behalf of @ioannisj.
The changeset is the one thing I'd flag: existing /static/loader.js and /static/1/loader.js tags move from a pinned build to the mutable alias and the line doesn't say so. I'd also want a call on the queue: array.js arriving before the app's init() drops everything queued on the bootstrap stub, and the body promises those calls survive. The rest inline are smaller: a //host gap in the asset-host check and two silent paths that could log.
| 'posthog-js': minor | ||
| --- | ||
|
|
||
| Add a nonblocking local bootstrap with early-call queuing, and make hosted and local loaders honor the selected latest, major, or exact-version SDK namespace. |
There was a problem hiding this comment.
🤖 An AI agent wrote this on behalf of @ioannisj.
The changeset reads as additive, but getSDKScriptURL in packages/browser/src/utils/loader-script.ts drops the pinning that loader.ts on main does on purpose (the comment there: cached mutable loaders must keep using their own release). So every existing /static/loader.js and /static/1/loader.js tag moves from the immutable build to the floating alias on the next release, and the README section that explained the old guarantee is removed in this PR.
I think the changeset line should say that outright, something like: hosted loader.js now loads array.js from its own namespace (/static/loader.js picks /static/array.js), use /static/<version>/loader.js to stay pinned. Worth naming dist/loader-bootstrap.js and its data-asset-host / data-sdk-version attributes there too, since with the README trimmed the changelog is the only in-repo mention of them. Minor still seems right to me given the loader is new.
There was a problem hiding this comment.
This loader is new and is not used by customers yet, so we are comfortable changing its namespace behavior now. The move from a loader-build pin to the selected latest/major/exact SDK namespace is intentional, and we will keep the minor changeset.
* test(browser): run deployment compatibility engines in parallel (#5384)
(cherry picked from commit 670189fb6a3a25944591fd5d638c652884ff86da)
* fix(node): stop esbuild failing when @opentelemetry/api is not installed (#5422)
Read the active span from the global registry of @opentelemetry/api v1. Do not import the optional peer. A literal import made esbuild fail the build when the peer was not installed. A variable import makes webpack warn and drop the module.
Add a test that bundles the built ESM and CJS entrypoints with esbuild when @opentelemetry/api cannot resolve.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 0d603b98-9825-4f1c-8261-e50c33c8a122
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
(cherry picked from commit b5036c46ccef4f653a9cb13b32543e3fa2a45c6d)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 196b7f9716bfba61ff8f94440823ddd4d6f9851a)
* chore: clear the Coherence spec problems reported on every PR (#5303)
* chore: clear the Coherence spec problems reported on every PR
Drop two commit ids from the add-an-invariant practice that exist in no branch of this repo, and give the two Otel spec bullets a name so spec --check passes.
Generated-By: PostHog Desktop
Task-Id: 822cb54b-55fa-4576-a107-1e406f72d9de
* chore: cite the Coherence adoption merge commit in place of its lost branch commits
ddaaf6808 is the squash merge of the PR that held 187a5c5c7 and 8922ab259, so the practice keeps its evidence and the citation resolves on main.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 4189fcb8-6fd5-428d-8c5f-4314d28e4dac
(cherry picked from commit 2e98ca8d6e44ed9d73fba85567b7f5de63e80b7d)
* fix(browser): stop parsing persistence on every capture() for Meta identifiers (#5429)
* fix(browser): only re-read Meta identifiers when persistence storage changed
capture() refreshed $fbc and $fbp from storage on every event, and every
persistence write did the same. Each refresh parsed the whole persistence
entry, and on localStorage+cookie it also wrote the entry back. With many
feature flags this made click handlers slow (#5428).
The refresh now compares the raw storage text with the text seen when the
identifiers last matched storage, and parses only when it differs.
Adds capture-hot-path tests that count parses of the persistence entry per
capture, so CI fails if hot-path storage work comes back.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 6ab57ee6-6279-4378-b074-ed4f92849780
* chore(browser): write mangled property names
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 6ab57ee6-6279-4378-b074-ed4f92849780
* fix(browser): skip Meta identifier refresh while disabled, and remember the pre-parse snapshot
refreshMetaIdentifiers() read storage even when persistence was disabled. It now returns first.
A refresh now remembers the storage text read before the parse, so a sibling write that lands during the parse is seen on the next refresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 6ab57ee6-6279-4378-b074-ed4f92849780
(cherry picked from commit 103494838c69ebb9457bcaa6567a437118d7f1ec)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 64f8a4325bde1b2c00338a1c13ba6c9bbbfa9a4f)
* fix(replay): enable synchronous fetch by default for new cores (#5335)
* fix(replay): enable synchronous fetch by default for new cores
* docs(types): correct replay sync fetch default changeset
(cherry picked from commit cb34fe03c49feb636ea4537f6f6b05144d163dac)
* chore: update versions and lockfile [version bump]
(cherry picked from commit dfff34b269e4aad905c7763260f011f982a4d421)
* chore(next): fix test lint errors (#5317)
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
(cherry picked from commit cb0482aad64d91d766888f8c8548fdd515018611)
* test(browser): record synchronous replay fetch in current-core goldens (#5437)
#5335 updated 6 of the 20 __preview_replay_sync_fetch values per file; this updates the remaining 14 in replay snapshot payloads.
(cherry picked from commit 7e3a5e36babca74a8f196822bc3758016783dbe0)
* feat(llma): support per-item offline evaluation models (#5438)
(cherry picked from commit acdbdd2123cb379fae9f0615ee0e9ef627fbe8f9)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 199de5e2f5c8159a6b47e215e09515a78b973f27)
* fix: Surveys: an optional rating question with response-based branching throws "The response type must be an integer" when skipped (#5279)
* Fix #5278: Surveys: an optional rating question with response-based branching throws "The r
* fix: complete surveys when skipping final optional ratings
* fix: adapt optional rating survey fix to shared renderer
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Dustin Byrne <dustin@posthog.com>
(cherry picked from commit d47c184b03040d6034bb15e7f7a0dcbd9b9a909f)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 615a550f3916eeac97e8546021d4aeb6f39913de)
* feat(react-native): add projectToken and deprecate apiKey (#5436)
* feat(react-native): add projectToken and deprecate apiKey
Rename the internal apiKey names in core and RN to projectToken.
* test(browser): record synchronous replay fetch in current-core goldens
#5335 updated 6 of the 20 __preview_replay_sync_fetch values per file; this updates the remaining 14 in replay snapshot payloads.
(cherry picked from commit 43ad061185c7f8b0a36ed87bb6a28214d4224e9d)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 183350588f47fcc8a983c37db33e3da5adaa362b)
* chore: upgrade posthog-android to pull in OkHttp 4.12.1 (#5441)
(cherry picked from commit 39ff01a86a20ca086e7b5dbbbd938e9d2115741b)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 1c165ccfee9d9b98dea93982cbd37bec8666fe5c)
* feat(browser): support latest configuration defaults (#5405)
* feat(browser): support latest configuration defaults
* docs(browser): regenerate latest defaults API reference
* test(nextjs): restore Next 16 fixture and dependency cooldown
* fix(browser): report resolved latest defaults date on events
* refactor(browser): derive latest defaults date from supported dates
(cherry picked from commit 31500798797e35187ca0a61bcf2af24d6ba45a96)
* chore: update versions and lockfile [version bump]
(cherry picked from commit b98cf1f29a232e0ddb488bb2bd822c762cb2f270)
* chore(react-native): require @posthog/react-native-plugin 2.12.7 or newer (#5445)
(cherry picked from commit b9fe0abf6f9c335b6ca49d9fb496662cca5806e5)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 87bca7a47ed4f12724c0d4b51fad759f721dad7b)
* perf(rrweb): port upstream mutation-buffer O(n²) fix (fixes #5227) (#5244)
* perf(rrweb): port upstream mutation-buffer O(n²) fix (fixes #5227)
Port the processBufferedMutations algorithmic core from upstream
rrweb-io/rrweb PR #1652 (merged 2026-09-17 by @eoghanmurray). The
addList-based drain used to tail-rescan every pass, so a single render
mounting many sibling nodes (e.g. a 50x35 table ~13k nodes) blocked the
main thread for ~10 s. The new topological-order drain iterates
this.addedSet with dom.previousSibling / dom.lastChild / dom.nextSibling
to reach a node whose parentId is already in the mirror BEFORE touching
its children, so pushAdd never defers onto a secondary queue that needs
rescanning. Nodes whose parent cannot be resolved for this emission land
in missingParents so their descendants early-out through the same set —
preserving the previous 'escape the dead while loop' guarantee without
the O(n²) cost.
PostHog customizations stay intact. pushAdd keeps isBlockedAtEmission,
shadow-host reparenting, serializationOptions caching,
canvasMaskingConfigured, iframe/stylesheet attach callbacks and the
cross-origin iframe shadow fix from #5150. It now returns boolean so
the new driver can mark deferred subtrees without silently dropping
nodes. mapRemoves is still drained before pushAdd so the reparent-
detection order documented in record/index.ts is preserved.
Added three untainted-prototype accessors to @posthog/rrweb-utils —
previousSibling, nextSibling, lastChild — matching the existing
getUntaintedAccessor('Node', ...) pattern. The new drain reads sibling
pointers in the hot path and native .previousSibling would bypass the
Angular Zone taint defense parentNode already uses. All three are added
to testableAccessors.Node so taint detection still fires if any of them
are patched by third-party code.
One pre-existing integration snapshot (record integration tests >
should record after DOMContentLoaded event) is updated to reflect the
new emission order of the standalone type:1 Load event, which upstream
rrweb #1652 also reordered. The event stream remains well-formed and
replayable because mutations stay in parent-before-child topological
order.
DoubleLinkedList class and the unused Optional type import are
removed — they were only referenced by the pre-fix drain loop.
New regression test in mutation-child-traversal.test.ts mounts 500
rows x 3 cells (~2000 nodes) in a single render and asserts every
emitted add has a resolved parentId plus a fully drained addedSet —
guards against the pre-fix regression where the 'escape the dead while
loop' fallback dropped entire subtrees once the addList rescan budget
blew up.
Attribution — algorithmic approach ported from upstream commits that
landed in rrweb-io/rrweb#1652:
565e390 Iterate over the added nodes in 'one pass'
90d4b01 Performance oriented refactor focusing on large-batch scenario
c013727 Utilize lastChild to avoid crawling through hundreds of nodes
325e211 Skip a step using existing nextSibling
* fix(rrweb): shadow DOM walk-up + warning + changeset per @TueHaulund review
Addresses all five review comments on PR #5244.
1. Shadow DOM content was silently dropped. When a node whose parent is
a ShadowRoot gets handled before the shadow host, the walk-up loop's
addedSet lookup never resolved — the walk-up stopped at the shadow
root while pushAdd translates ShadowRoot -> host for the parentId.
Result: shadow children emitted with parentId -1 and the push failed
silently. Fix: translate ShadowRoot -> host inside the walk-up loop
AND the missing-parents propagation step, mirroring pushAdd. The
existing 'should record moved shadow DOM 2' integration test is the
regression that catches this and passes with the fix.
2. Reverted the integration.test.ts.snap change (Load event reorder).
The ordering is environment-dependent locally for me, but per Tue's
review the original snapshot passes in the CI environment and the
updated one failed, so I defer to that.
3. Added a basic jsdom correctness test asserting the
parentId-resolvable invariant for every emitted add. Marked it
explicitly as a weaker coverage test — the strong regression lives
in integration.test.ts > 'should record moved shadow DOM 2' which
needs the puppeteer harness. A full port of upstream rrweb #1652's
mutation.test.ts fixture suite is a reasonable follow-up iteration
if the review wants more.
4. Added console.warn when a node is deferred past this emission,
matching upstream rrweb's phrasing. Silent drops here historically
masked the shadow DOM regression; the warning means future
regressions announce themselves in the browser console.
5. Added a changeset bumping posthog-js + @posthog/rrweb +
@posthog/rrweb-utils so release-please actually publishes the fix.
* test(rrweb): regression bench for long sibling runs under single parent (#5227)
@yuetong3yu reported a second independent repro on 2026-10-15 (Svelte 5 +
bits-ui Portal, ~50k empty comment/text anchors as direct children of
<body>, scaling ~n^1.8 on 1.438.7). The port of upstream rrweb-io/rrweb#1652
in this branch drains addedSet in topological order and never re-queues,
so per-node cost should stay flat.
This test locks the invariant: 500-node vs 5000-node batch under one
parent, assert per-node drain stays within 5x (well above jsdom noise,
well below the pre-fix n^1.8 regression). Also covers mixed
comment+text+element shape and the no-silent-drop property.
No src/ changes.
* fix(rrweb): sanitize masked-node descriptor in diagnostic warn (@marandaneto)
The warning added in 8aeafb2a to surface deferred nodes passed `n`
directly as the second console.warn arg. Verified against
`rrweb-plugin-console-record/src/stringify.ts` lines 153-155:
} else if (value instanceof Node) {
if (value instanceof HTMLElement) {
return value ? value.outerHTML : '';
With console recording enabled, the plugin stringifies any `Node`
argument via `outerHTML` — the diagnostic would persist the raw
masked content into a `rrweb/console@1` event. Reviewer-reproduced
on 2026-10-21 via the exact-head `review-5244-privacy.test.ts`
scenario (unblock container + insert `rr-mask` before an unmirrored
sibling → masked plaintext in the console payload).
Fix: build a sanitized descriptor from `tagName.toLowerCase()` + the
`nodeType` code and pass that string instead. The warning still
tells operators that a node was dropped (the reason the warn exists
after @TueHaulund review) but can no longer leak masked DOM to a
console-record consumer.
Also adds `test/record/review-5244-privacy.test.ts`, a static
regression test that greps the warn block for the pre-fix shape
(`, n,` / `, n)`) and asserts the descriptor + rationale comment
survive future refactors. Static-only so the vitest harness in
`packages/rrweb` can run it without a browser; CI covers it.
Reported-by: Marco Antonio Alvarez <marandaneto>
Thanks for the privacy catch on the diagnostic.
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
* test(rrweb): fix stale DOMContentLoaded Load-event snapshot ordering
The should record after DOMContentLoaded event snapshot carried
the pre-#4976 event order where `init()` ran synchronously inside
the DOMContentLoaded listener. PR #4976 deferred `init()` via
`setTimeout(init, 0)` in `src/record/index.ts` so page-level
non-capture DOMContentLoaded listeners (mask/block classes) can run
before the snapshot. The deferral means the window `load` event
reaches `wrappedEmit` *before* `init()` runs `takeFullSnapshot`,
so the `type: 1` (Load) record now follows the `type: 4` (Meta)
and `type: 2` (FullSnapshot) records instead of preceding them.
The snapshot update reflects the actual emission order the
production code produces. Verified locally with
`vitest run test/integration.test.ts` (50/50 pass).
This change is independent of the mutation-buffer port in this PR;
isolated it rather than bundling so the diff stays reviewable.
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
* test(rrweb): address mutation ordering review feedback
* fix(rrweb): address TueHaulund review on #5244
Six changes, one logic tweak, one test rewrite:
- src/record/mutation.ts: one summary console.warn per emit() instead
of one warn per dropped node. Per-node warns cascade through
subtrees (a 13k-node table loses its parent -> 13k warn lines, and
with console recording on those lines land in the replay).
- src/record/mutation.ts: comment on the pushAdd return value now
says "dropped" instead of "deferred" (the pending set is cleared
at the end of emit, nothing retries it).
- src/record/mutation.ts: strip reviewer names and PR references from
code comments. History belongs in the PR description, not the
source tree.
- src/record/mutation.ts: match upstream rrweb's self+ancestor exempt
for movedSet when deciding whether to drop an addedSet entry.
Upstream uses isSelfOrAncestorInSet here; we were only checking
ancestors.
- .changeset/rrweb-mutation-buffer-on2-fix.md: drop reviewer credit
from the public CHANGELOG entry, keep the behavior description.
- test/record/review-5244-privacy.test.ts: deleted. Replaced by
test/record/mutation-drop-warn-is-sanitized.test.ts, which spies
on console.warn and asserts no Node argument and no masked content
leaks, independent of source grep.
- test/record/mutation-child-traversal.test.ts: dropped the basic
drain correctness test — the 15 ordering tests ported from
rrweb-io/rrweb#1652 now cover the same invariant. Removed the
elapsedMs < 5000 wall-time gate; the addedSet-residue assertion
is the structural proof.
- test/record/mutation-sibling-run.test.ts: dropped per-node timing
comparison across batch sizes (flaky under GC jitter). The new
shape asserts residue and served-count invariants instead.
Local vitest run: 38/38 pass across the 8 mutation test files touched.
The three mutation.test.ts snapshot failures introduced in
02916d1f4 are untouched and remain for the author of that change.
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
* fix(rrweb): drain walk skips IGNORED_NODE siblings (fasyy612 review)
The forward sibling walk inside the topological drain broke on the
first sibling not in addedSet. But getNextId skips past IGNORED_NODE
siblings when computing nextId, so a node inserted BEFORE an ignored
sibling (e.g. a slimDOM-filtered <script>) could be dropped when the
next new sibling sits AFTER the ignored node — the walk would stop
at the ignored sibling and never reach the later addition.
The ported upstream #1652 fixtures all exercise contiguous addedSet
siblings and hit the lastChild jump above the walk, so they miss
this path entirely. @fasyy612 flagged it with the right fix shape:
probe past mirror.getId(ns) === IGNORED_NODE siblings before deciding
to break, matching getNextId's own loop.
New fixture at test/record/mutation-ignored-sibling-skip.test.ts
covers both the positive case (sibling after ignored node still ships)
and the termination case (sibling after ignored node not in addedSet
and the walk stops instead of looping).
Local vitest: 40/40 pass across the 9 mutation test files.
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
* fix(rrweb): three test fixes per TueHaulund second-pass review
- test/record/mutation-drop-warn-is-sanitized.test.ts: deleted. The
test called `console.warn` itself and asserted on that call, so it
did not exercise the mutation buffer and would still pass if the
real warn leaked a node. The real sanitization is now a one-line
`console.warn(string)` that cannot carry a Node argument by
construction; a self-referential test on top of that has no
catching power. Per @TueHaulund's offer to delete if a real repro
is a pain.
- test/record/mutation-ignored-sibling-skip.test.ts: rewritten to
actually hit the bug @fasyy612 flagged. The previous fixture
(a) added the <script> in the same batch, so it was not yet
IGNORED_NODE when the walk ran, and (b) put the new node as
lastChild, so the lastChild jump above the walk skipped the walk
entirely. Confirmed by reverting the probe-past-IGNORED fix
locally: the new fixture now fails on the pre-fix walk and passes
on d80c33fbe. The three conditions that must line up are called
out in a comment at the top of the file so the next refactor does
not quietly weaken the test.
- test/record/mutation-sibling-run.test.ts: the previous
`addedSet.size === 0` residue checks could never fire because
`emit()` swaps in a fresh empty addedSet before the drain runs.
Replaced with sibling-lookup counts via a patched dom.previousSibling
/ dom.nextSibling / dom.lastChild — lookups-per-node must stay
flat as batch size grows 10x. This is the invariant that actually
caught the pre-fix ~n^1.8 scaling in reports. Removed the symmetric
residue check in mutation-child-traversal.test.ts for the same
reason.
Local vitest after the three fixes: 36/36 pass across the eight
mutation test files (the sanitization file is gone). The ignored-
sibling test confirmed to fail without the probe-past-IGNORED
patch.
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
* fix(rrweb): scrub comment history, tighten assertions, interleaved-IGNORED guard
Follow-up to third-pass review on #5244.
Comment hygiene across mutation.ts, utils/src/index.ts, and the four
mutation-* test files: drop reviewer handles, PR numbers, and commit
SHAs from inline comments. Replace 'defer' with 'dropped' where the
code path actually drops nodes. Remove the stale addList-era narrative
in genAdds.
mutation-sibling-run.test.ts: reframe the test as a scaling guard on
the new drain rather than a direct regression on the old addList path
(which never called the sibling helpers). Fix the scaling-margin
narrative — n^1.8 total is ~6x per node for a 10x batch, not ~63x.
mutation-child-traversal.test.ts: assert exact add count (3500 =
500 rows + 1500 cells + 1500 text nodes) and parent-before-child
ordering by id, instead of the always-passing 1000 lower bound.
mutation-ignored-sibling-skip.test.ts: drop the always-zero
addedSet.size residue check — emit swaps in a fresh addedSet before
the drain so the check cannot fail.
mutation.ts drain: backward fast path at line 436 now probes past
IGNORED_NODE siblings symmetric with the forward walk at line 476.
Without this, slimDOM-ignored comments or scripts interleaved between
new siblings force the drain back to walk-up/descend per node,
regressing to O(n^2).
mutation-interleaved-ignored.test.ts (new): scaling guard with
<script>-interleaved siblings; per-node lookup count stays flat as
batch grows 10x.
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
---------
Signed-off-by: Abd Alrahaman <abdarahman10555@gmail.com>
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit 52c84e28c35fc4041b55dbc3aa8b0b5a97e33026)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 8e3b8f57d72906d48426133c29f35e84b2065266)
* ci: run the sourcemap plugins through the error tracking harness (#5402)
* ci: run the sourcemap plugins through the error tracking harness
Packs the plugins and the SDKs they are tested with, then runs PostHog/error-tracking-harness
against them: real posthog-cli releases, real bundlers, a mock PostHog and an app whose
exceptions must resolve to their source lines. The image is pinned by digest and runs with no
network. Advisory until the matrix has been quiet for a while.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ci: pull the harness image anonymously
The package is public, like sdk-test-harness-v2, so the job needs no registry login and
runs with contents: read only.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
* ci: pin merged lifecycle harness image and use named overrides
* ci: select affected error tracking fixtures and bind current core
* ci: bind complete fixture dependencies in error tracking harness
* ci: prepare selected harness fixtures in a subject image
* test(ci): describe prepared harness image smoke checks
* ci: pin published prepared-subject harness image
* ci: exercise lazy exception autocapture with PR tarballs
* ci: run fixed plugin smoke suite through reusable harness
* ci: build pinned harness source for PR smoke tests
* ci: upsert smoke results as a PR comment
* ci: pin published error-tracking harness image digest
* ci: exercise Next 16 smoke and secure PR reporting
* ci: pin published eight-fixture harness image
---------
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit ed9ef472f156de463e1ec7317d46e96ce9fa5142)
* fix(browser): Refresh identity on bfcache restore (#5427)
* fix(browser): Refresh identity on bfcache restore
Generated-By: PostHog Desktop
Task-Id: dd368b5c-40fc-49ac-9fc0-4c3c321bfb36
* fix(browser): Update mangled property names for bfcache identity
Generated-By: PostHog Desktop
Task-Id: dd368b5c-40fc-49ac-9fc0-4c3c321bfb36
---------
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
(cherry picked from commit 8a9a0661c6ccc21f1397a495855031e44fe7de49)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 90c73424ee622071d01bb2fb59ee663d0a6fd50e)
* fix: Expo 57 example: pnpm install fails with ERR_PNPM_TRUST_DOWNGRADE (#5443)
* Fix #5442: Expo 57 example: pnpm install fails with ERR_PNPM_TRUST_DOWNGRADE
* test: remove Expo resolver configuration assertion
---------
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Dustin Byrne <dustin@posthog.com>
(cherry picked from commit 6662c418aee37ae0c61bb2f48fec7ab86caf20a9)
* chore(react-native): deprecate flag APIs removed in the next major (#5431)
* chore(react-native): deprecate flag APIs removed in the next major
* chore(react-native): keep getFeatureFlagDetails, deprecate useFeatureFlagWithPayload
Add doc summaries to the deprecated flag APIs so the reference shows them.
* chore(react-native): deprecate overrideFeatureFlag in favor of updateFlags
(cherry picked from commit 96d7ff2e394126e2bd03a2422f60b5892a85d87f)
* feat(browser): add a single-tag SDK loader (#5406)
* feat(browser): add a single-tag SDK loader
* fix(browser): complete loader queues and report configuration errors
* feat(browser): support deferred initialization in the script loader
* docs(browser): clarify optional loader crossorigin attribute
* fix(browser): preserve SDK defaults and track loader build inputs
* fix(browser): pair loader aliases with immutable SDK releases
* test(browser): record static-loader deployment goldens
* test(browser): exercise automatic loader in deployment snapshots
* test(browser): refresh loader goldens after merging main
* fix(browser): preserve named loader initialization in automatic mode
(cherry picked from commit 285d874b7bc5914f3d7b0caa7006a3ba1a480b64)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 28b511b85c8a4d48ab4dd46a70dc62dcd133b002)
* feat(browser): add opt-in autocapture field values (#5269)
* feat(browser): add opt-in autocapture field values
* test(browser): cover opted-in autocapture values in compatibility matrix
* fix(browser): respect slot ancestry when capturing input values
* fix(browser): preserve autocapture events when value predicates throw
* fix(browser): respect autocapture ignorelist along slot ancestry
* fix(browser): respect selected option autocapture exclusions
* perf(browser): skip value privacy checks when text is masked
* fix(browser): preserve value capture scope across shared autocapture
(cherry picked from commit 950d03423605d1567c90a3f27f50d4603a89ae45)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 6d017a9bb225ebf1da331f4753b2e6c76ec4eeba)
* test(browser-common): use trimmed mean in console log perf tests (#5463)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 7fdc0aa612d134c1b3e5bb974bdac1b56f248fdb)
* feat(mcp): add a sendInitializeEvent option (#5401)
* feat(mcp): add a captureInitialize option
captureInitialize: false stops $mcp_initialize. The option is on by
default. Many clients reconnect before every few tool calls, so the
handshake event can outnumber $mcp_tool_call.
With the option off, the initialize wrapper skips captureEvent. Nothing
else changes: it still mints the session token and records the
negotiated protocol version, so tool calls keep their sessions and
client identity.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(mcp): rename captureInitialize to sendInitializeEvent
PostHogMCP already has a captureInitialize() method that sends the
event by hand. An option with the same name next to that method makes
readers link the two. The new name says what the option controls.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* docs(mcp): name the PostHog screens that read $mcp_initialize
MCP onboarding and the users and sessions tiles of the default MCP
dashboard read $mcp_initialize. The README now says that they show no
data while sendInitializeEvent is off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 8faad6aa2508b38959cbd27196aa805bf6fa5ab7)
* fix(browser): fall back to Math.random when crypto.getRandomValues throws (#5222)
* fix(browser): fall back to Math.random when crypto.getRandomValues throws
Firefox throws an OperationError when its random source fails. uuidv7() did not catch it, so the throw escaped from capture() into the host page and the event was lost. The generator now uses the existing Math.random fallback when the call throws.
Generated-By: PostHog Desktop
Task-Id: 3f144ec2-2e2c-413c-b864-11ca8ca05308
* test(browser): assert the uuidv7 fallback uses Math.random
The test checked that two UUIDs differ, but the counter alone makes them differ. A spy on Math.random now proves the fallback filled the buffer. The Math.random note moves into a doc comment on weakRandomValues.
Generated-By: PostHog Desktop
Task-Id: 8662b8b4-5fad-4c39-baef-5a6a4a9e36d2
---------
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
Co-authored-by: Steven Shults <steven@posthog.com>
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit 6d12d24ed24b2846415dbd95fc5d7ccf57779a99)
* chore: update versions and lockfile [version bump]
(cherry picked from commit fd7d0c9cbab001e3aabcd8c3162eeccc4ff73e13)
* fix(react-native): Capture touch autocapture events on Fabric instances (#5466)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit 8c136a8608187dc77b97cc4724639755b37d3c85)
* chore: update versions and lockfile [version bump]
(cherry picked from commit a3e9ec3d274e64f8cdfbb29e99c1ee385d34cf8f)
* feat(mcp): add a sendToolsListEvent option (#5404)
* feat(mcp): add a sendToolsListEvent option
sendToolsListEvent: false stops $mcp_tools_list. The option is on by
default. Clients list tools on every reconnect, so the listing event is
about as common as $mcp_tool_call.
All three places that send the event go through captureToolsListEvent,
which skips the capture when the option is off. That also drops a failed
listing's $exception. Nothing else changes: tools keep their injected
arguments, and the virtual tools stay listed.
The name says "event" to match sendInitializeEvent.
PostHogMCP.captureToolsList() already captures the event by hand.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(mcp): gate $mcp_tools_list with a plain if, like the initialize event
The captureToolsListEvent wrapper read the tracking data again through
its first argument. Now each of the three emit points checks
sendToolsListEvent directly, as handleInitializeRequest does for
sendInitializeEvent. getTracedToolsList gets the flag as an argument for
its failed-listing path. The behaviour does not change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 68336dfb097a78e08041c77f72bbcc59b6958e04)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 8dc2e88f6580c6f46107385bdf193f21b467bab7)
* feat(node): add request context to OpenTelemetry spans (#5314)
* feat(node): add OpenTelemetry request context processor
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* feat(node): add signal-controlled context lifetimes
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix(node): preserve exception snapshots and fresh scopes
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix(node): scope NestJS span context to the response
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* refactor(node): share abort signals for request lifetime
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix(node): enrich verified request server ancestors
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix(node): expose context enrichment without tracing imports
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix: respect node span privacy and request lifetime
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix(node): share context listeners across CommonJS and ESM builds
An app that uses require('posthog-node') and imports posthog-node/otel
loads two module copies. Each copy had its own listener registry, so the
incoming request span was not enriched. Keep one registry on globalThis.
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
* fix(node): keep request context after the response ends
Express and NestJS no longer end the request context when the response
finishes or closes. A promise or timer that the handler starts can
still belong to the same user action, so getContext(), captured events,
and new spans keep the request IDs as before. Apps that want a hard end
can still pass ContextOptions.signal.
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
(cherry picked from commit 096131dc7cf6115217996ff8fccace5598ed1e31)
Adapted for capture-v1: keep context options in the merged context scope;
pass the context snapshot after the level argument of buildEventMessage;
read context in the v1 _buildExceptionEvent and _prepareEventMessage; read
headers through readPostHogTracingHeaders; also copy context options into
the exception snapshot.
* feat: add next.js and nuxt server span context (#5315)
* feat(next): add optional server span context bridge
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* test(next): define sampler gap context boundary
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* feat(nuxt): add optional server request span context
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix(nuxt): enrich incoming spans at request entry
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* fix: share framework span enrichment without optional imports
Generated-By: PostHog Desktop
Task-Id: 5681be44-4986-48db-b140-1bda88f75f96
* chore(nuxt): use enabled clients in span context tests
Generated-By: PostHog Desktop
Task-Id: bfd18848-e498-4352-a58a-b9ba85de4198
* fix(next): keep nested context in span context callbacks
withPostHogContext passed a proxy that put the request identity back on
every call, so posthog.withContext inside the callback was ignored. The
callback already runs inside the request scope, so pass the client.
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
* fix(nuxt): keep application context in request scopes and errors
With requestContext on, the request scope now keeps properties from an
outer application context. The error hook now prefers the active
application identity over the saved header identity.
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
* fix(next,nuxt): keep request context for work after the handler
Match posthog-node: withPostHogContext and the Nitro request wrapper
no longer end the context when the handler returns or throws. Work the
handler starts keeps the request IDs.
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
* fix(next): share the Node.js client cache across webpack layers
Next.js 15 bundles instrumentation and App Router handlers in separate
webpack layers, each loading its own copy of clientCache.node.ts. That
gave each layer its own client for the same key, so the OTel span
processor (reading one client's context) and withPostHogContext
(writing another's) could disagree about the current request. Cache
clients on globalThis, keyed by Symbol.for, so every layer shares one.
Generated-By: PostHog Desktop
Task-Id: e73c8b25-9501-4f04-9fd8-e82415e1f9f9
(cherry picked from commit e4df2d57bb242c2d5c107f4f8b0a9edc8557a749)
* fix(rrweb): port upstream shadow-root release on host removal (fixes #5419) (#5421)
* fix(rrweb): release removed shadow DOM hosts from the recorder
Port the shadow-root release from rrweb-io/rrweb#1958 (as shipped in rrweb 2.2.0), adapted to the fork's per-document teardown. Fixes #5419.
* fix(rrweb): release shadow hosts removed from blocked containers
Also adds removeShadowRootsIn to the shadowDomManager test mocks and a frozen remove/re-insert recording test.
---------
Co-authored-by: frostfire575 <191205836+frostfire575@users.noreply.github.com>
(cherry picked from commit 34833f2df9a6252a6dd03bcc19ca6af755a23953)
* fix(browser-next): settle pending captureImmediate when the extension is disposed (#5448)
* fix(browser-next): settle pending captureImmediate when the extension is disposed
deliverImmediate awaited ensureDelivery('immediate') before checking
cancellation. If shutdown disposed the analytics extension while the lazy
delivery loader was still pending, the immediate capture stayed unresolved
until the loader settled, possibly forever.
Race the delivery wait against a disposal signal so the capture resolves
with the existing cancellation summary as soon as the extension is
disposed. A loader that resolves later does not send the event.
Fixes #5093
* fix(browser-next): release per-capture disposal waiters after immediate delivery
* chore: add changeset
* chore: remove changeset for private @posthog/browser
---------
Co-authored-by: Guilherme Praça <247475208+guilherme-praca@users.noreply.github.com>
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit 9d3fbe6e4eff5cdad2e4ab96efded61740d64cbc)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 4044a5c9ce65e2bc031d435a75b1353690c8b276)
* feat(browser): add single-script installation (#5395)
* feat(browser): add a standalone classic-script entry point
* test(browser): run standalone snippet coverage in CI
* fix(browser): preserve script CSP nonces in lazy chunks
* refactor(browser): name the hosted entry script.js
(cherry picked from commit a0a29d6955db32eb58a5a85dc1f4a7320506fc08)
* ci(browser): publish standalone assets to the CDN (#5396)
* ci(browser): publish versioned CDN assets before npm releases
* fix(browser): retain CDN artifacts and publish immutable entries last
* refactor(release): isolate posthog browser S3 publication
* fix(release): report posthog browser S3 failures
* refactor(release): decouple browser S3 uploads from npm
* fix(release): audit browser S3 failure notifications
* fix(release): reject superseded browser alias promotion
* refactor(release): use script.js for browser assets
(cherry picked from commit 6fba0c4c373dcc4bde13ae50bb37e856bbdd8693)
* feat(browser): publish the experimental npm package (#5397)
* feat(browser): enable experimental v0 npm releases
* docs(browser): record initial distribution size baseline
* docs(browser): refresh CSP-aware CDN measurements
* chore(browser): simplify npm release validation
* fix(release): gate posthog browser npm publication independently
* refactor(release): use the shared npm matrix for posthog browser
(cherry picked from commit 5a21286ec19a6c2ee37fe8ed2e585a2f2b8b3f5e)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 0745e2a2d446e0be857d83cf45cbaf77b83e5fe2)
* feat(node): honor sendFeatureFlagEvent in evaluateFlags snapshots, with a per-read override (#5252)
* fix(node): respect sendFeatureFlagEvent in evaluateFlags snapshots
A client created with `sendFeatureFlagEvent: false` sends no
`$feature_flag_called` events from the single-flag getters, but reads
from an `evaluateFlags()` snapshot (`isEnabled()`, `getFlag()`) still
sent one per key. The snapshot accessors take no per-call option, so
the client option now decides, defaulting to true as before. Access
tracking for `onlyAccessed()` is unchanged, and the single-flag
getters keep their per-call override.
* feat(node): add a per-read sendFeatureFlagEvents option to evaluateFlags snapshots
`isEnabled()` and `getFlag()` on an `evaluateFlags()` snapshot now take
`{ sendFeatureFlagEvents }`, like the single-flag getters. The per-read
value wins, then the client's `sendFeatureFlagEvent` option, then `true`.
One client helper resolves this for the snapshot reads and the
single-flag getters.
When events are off for a read, the snapshot sends no
`$feature_flag_called` event, missing keys included, and leaves the
dedupe cache untouched, so a later read that sends events still reports
the flag. The key is still marked accessed, so `onlyAccessed()` capture
enrichment is unchanged. Filtered snapshots from `only()` and
`onlyAccessed()` honor the option the same way.
Update the option docs and the public API references.
* fix(openfeature): follow the client's sendFeatureFlagEvent option in the Node provider
`PostHogServerProvider` defaulted its `sendFeatureFlagEvents` option to
`true` and passed it on every `getFeatureFlagResult()` call. That per-call
value overrode a client created with `sendFeatureFlagEvent: false`, so
every flag evaluation through the provider still sent
`$feature_flag_called`.
Leave the option undefined when it is not set, so the client option
decides. An explicit provider option still wins. Every posthog-node
version in the peer range resolves an undefined per-call value to the
client option, then `true`.
(cherry picked from commit 503c3ea1ee79d3ff8c6cce90c9e455a0455af50a)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 6f11d8ec6aefd127baf86125f778de23de02913b)
* feat: add conversations open() and close() (#5467)
* feat: add conversations open() and close()
* fixes
* terser
(cherry picked from commit afde45e3d38272d1512c2f5de66e9a249895ade2)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 2fc5f976d8356bd8183cebdcdb47d304d3fb9da2)
* feat(node): opt-in posthog-js cookie fallback for request context (#5270)
* feat(node): read the posthog-js cookie when tracing headers are missing
The Express and NestJS integrations read the distinct ID and the live session ID from the ph_<token>_posthog cookie as a fallback for each missing tracing header.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* fix(node): use core cookie helpers and respect opt-out
Use getPostHogCookieName and cookieStoreFromHeader from @posthog/core, ignore the cookie when the consent cookie opts out, and compare timestamps with an absolute difference.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* fix(node): make the posthog-js cookie fallback opt-in and identity-safe
Add the readPostHogCookie option (off by default), because the server cannot see an opt-out that posthog-js keeps in localStorage. Read the cookie only when neither tracing header is present, take the distinct ID only for identified users so anonymous visitors stay personless, and accept the older two-item $sesid.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* feat(node): allow a custom session idle timeout for the cookie fallback
readPostHogCookie also accepts { sessionIdleTimeoutSeconds } for projects with a custom posthog-js session_idle_timeout_seconds. The option doc lists the unsupported posthog-js configurations.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* fix(node): clamp the cookie session idle timeout like posthog-js
Clamp sessionIdleTimeoutSeconds to 60 seconds through 10 hours, the bounds posthog-js applies, and test the option parsing.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* feat(node): honor opt-out by default for the cookie fallback
readPostHogCookie accepts optOutCapturingByDefault, so a visitor without a consent cookie counts as opted out, like posthog-js with opt_out_capturing_by_default. Add a NestJS option gate test, an Express test that anonymous cookie visitors stay personless and an explicit distinctId wins, and regenerate the public API references.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* fix(node): treat a zero idle timeout as the default and return null when off
sessionIdleTimeoutSeconds: 0 now falls back to 30 minutes, like posthog-js, and getPostHogCookieReadOptions returns null when cookie reading is off.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 80f44d9e-704d-4f9c-93d2-bd37d55ab1c2
* fix(node): match browser cookie consent semantics
---------
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit 4b25f7f1d8fff0e442f6cf9403beec2b4a96b743)
Adapted for capture-v1: readPostHogTracingHeaders takes the cookie read
options, so captureTracingHeaders: false also turns off the cookie
fallback; the readPostHogCookie doc says so.
* chore: update versions and lockfile [version bump]
(cherry picked from commit 73d787a582d3c92b6ec29f3ceedd5e6abaf0bc29)
* fix(ai): capture an error event when a streaming OpenAI call fails before the stream opens (#5478)
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
(cherry picked from commit aa4ed5deb82510af1b684988e4fe83739f02b7c2)
* chore: update versions and lockfile [version bump]
(cherry picked from commit a3187e23b2f65932c850a7e723474b3237df4325)
* fix(browser): keep the full URL when masking query params that contain ? or # (#5341)
* fix(browser): keep the full URL when masking query params that contain ? or #
Signed-off-by: Sebastian Turner <sebastiant3484@gmail.com>
* fix(browser): fully mask query values with nested question marks
---------
Signed-off-by: Sebastian Turner <sebastiant3484@gmail.com>
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit de6b06f8a53faac5113ba979aa48472e6965a2b5)
* feat(Surveys): Implement survey popup delay (surveyPopupDelaySeconds)… (#5243)
* feat(Surveys): Implement survey popup delay (surveyPopupDelaySeconds) for React Native
* WIP fixing popup eligibility and delay bug
* Fix formatting of PostHogSurveyProvider.tsx
* Check for delay override flag before using default delay.
* fix(react-native): preserve pending survey delays and appearance defaults
* chore(react-native): release survey popup delay as minor
---------
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit a96d286acf5b313af96beea1ebd7ce1b0a66c153)
* chore: update versions and lockfile [version bump]
(cherry picked from commit dfb2e002db81eee1889841d272b54e04f4c722f5)
* fix(node): keep local flag evaluation consistent with the server (#5471)
(cherry picked from commit 0058ef9664f27224de6377ec18d786bdc0e67e4d)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 4949cecefbaa621d017b2e2761a30a4d6151009c)
* chore: upgrade @posthog/coherence to 1.10.1 (#5432)
* chore: upgrade @posthog/coherence to 1.10.0
1.10.0 makes the Coherence PR comment say what each finding means and what to do, and marks the findings that are in files the pull request changed. The existing workflow gets the marks with no change, because the report reads the pull request's base from the GitHub event. The release-age exclusion moves to the new exact version.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 4189fcb8-6fd5-428d-8c5f-4314d28e4dac
* chore: upgrade @posthog/coherence to 1.10.1
1.10.1 writes the PR comment in Simplified Technical English, folds explanations, colours the verdict, and adds icons. The release-age exclusion moves to the new exact version, and the lockfile changes only the Coherence entries.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 4189fcb8-6fd5-428d-8c5f-4314d28e4dac
(cherry picked from commit 2b0ae91bc43854a3a35d09604bdac1ef0f6b3a85)
* fix(ai): load @posthog/ai/otel without sdk-trace-base (OpenTelemetry JS 3.x) (#5439)
* fix(ai): load @posthog/ai/otel without sdk-trace-base
OpenTelemetry JS 3.x has no @opentelemetry/sdk-trace-base, so the runtime BatchSpanProcessor import crashed at startup. PostHogSpanProcessor now queues AI spans and exports them in batches through its own OTLPTraceExporter, with the BatchSpanProcessor defaults. All sdk-trace-base imports are now type-only.
Closes #5435
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: 6c8e40de-2d6f-465c-b51d-c5fd5ddef50e
* fix(ai): support both OpenTelemetry trace package layouts
---------
Co-authored-by: posthog[bot] <206114724+posthog[bot]@users.noreply.github.com>
Co-authored-by: Dustin Byrne <dustin@posthog.com>
(cherry picked from commit a22cbecd47f5a727747e1113c5c0be992cbeda1d)
* fix(browser): make test:typecheck pass and run it in check-types (#5210)
* fix(browser): make test:typecheck pass and run it in check-types
The browser test tsconfig is not strict. Two source files and three tests had type errors under it. Vitest does not check types, so nothing caught them.
Run test:typecheck from the browser check-types script, so CI runs it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e91bb021-a0e3-4e6f-abca-a7d22f162008
* fix(browser): hash sibling sources for the browser check-types task
test:typecheck compiles browser-next source and browser-common test helpers. Turbo did not hash those files for posthog-js#check-types, so a local cache could give a stale pass after an edit there.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e91bb021-a0e3-4e6f-abca-a7d22f162008
* chore(ci): keep 8-letter chunk names distinct in the bundle size check
The strip-hash pattern masked any 8-character word, so analytics-delivery and analytics-internal both became analytics-********. The action then compared one file with the other and reported a false change. Mask only groups that contain an uppercase letter or a digit, as build hashes do.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e91bb021-a0e3-4e6f-abca-a7d22f162008
* fix(browser): retain test typechecking after merging main
---------
Co-authored-by: Manoel Aranda Neto <marandaneto@gmail.com>
(cherry picked from commit 0ea261765e3bdddcc66019e5f0d403646edaf8a6)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 14dc382c5ed3171d3b387e768b619a7e7908a462)
* test(browser): synchronize replay compression before advancing clock (#5484)
(cherry picked from commit 8dba40ccba917292868b71873bbc4998a924498c)
* fix(react-native-plugin): require posthog-ios 3.92.7 (#5482)
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit dfd56e6e567b46b074a25f0e598877594dd8bab6)
* chore: update versions and lockfile [version bump]
(cherry picked from commit c145829e6e8cc327272116191c03f4bbfdb87e30)
* chore: let the PostHog watcher close its own stale draft PRs (#5380)
(cherry picked from commit 1f6a0156d127dff5889728c16a39bd6a1b387d12)
* feat(browser): add a local bootstrap and preserve SDK namespaces (#5470)
* feat(browser): add a nonblocking hosted loader bootstrap
* fix(browser): load the SDK directly from the local bootstrap
* fix(browser): reject malformed bootstrap asset hosts
* docs(browser): keep usage documentation on posthog.com
* fix(browser): keep local bootstrap SDK updates automatic
* feat(browser): preserve selected SDK loader namespaces
* fix(browser): preserve deferred loader queues and improve diagnostics
(cherry picked from commit 41ae7fd024a582c811e85bae16e284467ae4ee3d)
* chore: update versions and lockfile [version bump]
(cherry picked from commit 67d874e9c47f83b39f32b12ccb3dba6c34e45416)
* feat(node): send transactional email from a template (#5418)
* feat(node): send transactional email from a template
Add posthog.emails.send() for PostHog transactional email templates, with the appHost, emailRequestTimeoutMs and emailRequestMaxRetries client options. Sends go to the app host with the project secret key and an idempotency key that every retry reuses. send() resolves to { data, error }.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: a0274fa0-d9be-48bf-b601-f153decf241d
* fix(node): match the email contract and keep long Retry-After waits
- A custom host without appHost now fails with missing_app_host, the code the cross-SDK contract in PostHog/posthog#120921 defines.
- The request deadline uses a referenced timer, so a short-lived script can't exit while it awaits a send whose injected fetch holds no handle.
- error.retryAfterMs keeps the full server Retry-After. parseRetryAfterMs in @posthog/core takes an optional cap for this; the 30 second retry ceiling still applies separately.
- The email send responses entrance in Node.spec.md declares control: none with its reason, which closes the coherence spec gap.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* fix(node): name 422 send checks validation_error and release late responses
- The template send route answers 422 for recipient_suppressed, sending_paused and sending_suspended as well as idempotency_key_reused. Only idempotency_key_reused is now invalid_idempotent_request; the send checks are validation_error, as the cross-SDK contract names them.
- A response that an injected fetch returns after the deadline now has its body cancelled, as the core transport does, so repeated timeouts don't keep pooled connections busy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* fix(node): fail email sends from a disabled client with client_disabled
A disabled client returned { data: { id: null }, error: null }, so a caller that treats a result without an error as sent dropped the email silently. It now sends nothing and returns the error client_disabled (name application_error), as the cross-SDK contract in PostHog/posthog#120921 defines. SendEmailResult.id is always the queued message ID, so its type is string.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* fix(node): cancel a stalled email body read and refuse sends after shutdown
When an injected fetch answers before the deadline but its body read stalls, the email request now cancels the received body after the timeout, so it frees its connection.
After shutdown(), emails.send() now returns client_closed before any request, as the cross-SDK contract requires.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* fix(node): wait for email sends in flight when the client shuts down
EmailSender now tracks each send, retries included, and shutdown() waits for those sends within its existing timeout. A send that starts after shutdown began still fails with client_closed.
The sends are tracked apart from the client's pending-promise queue, because flush() waits on that queue and a send in its retry backoff must not hold back event delivery.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* fix(node): flush events alongside email sends on shutdown and keep Retry-After on a failed body read
Shutdown now runs the core flush at once and waits for email sends in flight next to it, under the same deadline, so a slow send can no longer use up the budget for queued events.
When the headers of an error answer arrive but the body read fails or times out, the send now keeps that status and its Retry-After, so the normal retry rules apply. A failed body read on a 2xx answer stays a retryable network error. The body is still cancelled when the read passes the deadline.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* feat(node): send transactional email through the single send endpoint
`posthog.emails.send()` now posts to `/api/projects/@current/messaging/send/email/` with the template ID in the body as `template.id`, instead of the template-scoped route.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* feat(node): read a transactional email's status with emails.get
`posthog.emails.get(id)` reads `/api/projects/@current/messaging/emails/{id}/` and resolves to `{ data, error }`, where `data` is `{ id, status, createdAt, statusAt, reason }`. It retries network errors, rate limits and an unavailable service like `send`, which now shares that attempt loop.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* chore(node): regenerate references and spec for emails.get
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
* docs(node): describe the email error name as a category
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Generated-By: PostHog Desktop
Task-Id: e72a2c10-f145-4f7c-8754-a4fcc06a18be
(cherry picked from commit c705824357b00a1187302224a58f478d1f652a54)
Adapted for capture-v1: shutdown waits for email sends next to the lane
drain and core flush; the backoff test mocks the v1 analytics endpoint
instead of /batch/.
* chore: update versions and lockfile [version bump]
(cherry picked from commit decaa166429ad781a53ad57853b8378d30fa65b3)
* chore(deps): bump the github-actions group across 1 directory with 7 updates (#5468)
Bumps the github-actions group with 7 updates in the / directory:
| Package | From | To |
| --- | --- | --- |
| [step-security/harden-runner](https://github.com/step-security/harden-runner) | `2.22.1` | `2.22.2` |
| [actions/upload-artifact](https://github.com/actions/upload-artifact) | `7.0.1` | `7.0.2` |
| [github/codeql-action/init](https://github.com/github/codeql-action) | `4.38.3` | `4.39.1` |
| [github/codeql-action/analyze](https://github.com/github/codeql-action) | `4.38.3` | `4.39.1` |
| [actions/download-artifact](https://github.com/actions/download-artifact) | `4.3.0` | `8.0.2` |
| [actions/setup-java](https://github.com/actions/setup-java) | `6.0.1` | `6.1.0` |
| [PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml](https://github.com/posthog/posthog-sdk-test-harness) | `1.15.0` | `1.20.0` |
Updates `step-security/harden-runner` from 2.22.1 to 2.22.2
- [Release notes](https://github.com/step-security/harden-runner/releases)
- [Commits](https://github.com/step-security/harden-runner/compare/ccd8616d44fd3846e67624a50d5aad6d37bf2d25...dc2769abcc0fcfbadfc1df54081c1608579ef6b2)
Updates `actions/upload-artifact` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v7.0.1...cf430e030ddbb5b0abf93d22962f4752f3646cd9)
Updates `github/codeql-action/init` from 4.38.3 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)
Updates `github/codeql-action/analyze` from 4.38.3 to 4.39.1
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/24c54180a607b1449ed407dd24f251e4e9147c8d...9b3254085494b55a0f7c4c2b3d2d52cc712ce370)
Updates `actions/download-artifact` from 4.3.0 to 8.0.2
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v4.3.0...9000827ccba6bdab643e8b6fd33ac0654aef8333)
Updates `actions/setup-java` from 6.0.1 to 6.1.0
- [Release notes](https://github.com/actions/setup-java/releases)
- [Commits](https://github.com/actions/setup-java/compare/de7274f081f381c8f8158605e0321c36c376e2e6...b843ebf9b913146b38da2eaa7aeddf76074ae1ad)
Updates `PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml` from 1.15.0 to 1.20.0
- [Release notes](https://github.com/posthog/posthog-sdk-test-harness/releases)
- [Changelog](https://github.com/PostHog/posthog-sdk-test-harness/blob/main/CHANGELOG.md)
- [Commits](https://github.com/posthog/posthog-sdk-test-harness/compare/64ffa6ab19166541880c077d349abe18a7e42395...aa725c89eec72cb8ac75f4e418662bf2e161feb8)
---
updated-dependencies:
- dependency-name: actions/download-artifact
dependency-version: 8.0.2
dependency-type: direct:production
update-type: version-update:semver-major
dependency-group: github-actions
- dependency-name: actions/setup-java
dependency-version: 6.1.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: actions/upload-artifact
dependency-version: 7.0.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
- dependency-name: github/codeql-action/analyze
dependency-version: 4.39.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: github/codeql-action/init
dependency-version: 4.39.1
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: PostHog/posthog-sdk-test-harness/.github/workflows/test-sdk-action.yml
dependency-version: 1.20.0
dependency-type: direct:production
update-type: version-update:semver-minor
dependency-group: github-actions
- dependency-name: step-security/harden-runner
dependency-version: 2.22.2
dependency-type: direct:production
update-type: version-update:semver-patch
dependency-group: github-actions
...
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
(cherry picked from commit 1950c3cceff895310392be75ef51d1984f779665)
* fix(node): capture unhandled rejections when the host has its own listener (#5472)
* fix(node): capture unhandled rejections when the host has its own listener
In Node's default throw mode (and warn-with-error-code), posthog-node did
not listen for unhandledRejection and relied on Node escalating the
rejection to uncaughtException. A host with its own unhandledRejection
listener, such as Next.js, stops that escalation, so the rejection was
never captured.
The SDK now listens for unhandledRejection only while another listener
exists. Such a listener has already stopped Node's default handling, so
adding ours changes nothing. It tracks hosts that add or remove their
listener after init through newListener and removeListener. A rejection
captured this way and then rethrown by the host listener is not captured
again by the uncaught-exception handler.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuJAD3n8f6Ra7L9p2UmPw
* fix(node): one capture per occurrence, and keep Node's behaviour, for host-handled rejections
Shares one set of process listeners across clients and bundled copies, with one capture per project. A rethrow, a
double delivery or a captureException from a framework hook counts once. Rejections wait a one-second grace period,
cancelled by rejectionHandled, and are sent on flush(), waitUntil, shutdown(), a fatal exit and beforeExit. React
and Next.js control-flow errors and AbortError rejections are skipped. Our listener is prepended so it sees the host's
listeners, and reproduces the runtime's handling when the host's listener disappears without an event (Bun).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FkuJAAYPkxG7cuvaUbEGCf
* fix(node): run deferred rejection captures in their own async context, and send what a flush owes
Addresses the review of the host-handled rejection capture:
- A deferred capture runs in a snapshot of the async context the rejection happened in, so it reports that
request's context and OpenTelemetry span, or none, ne…
Problem
Applications need an early PostHog queue without making application startup wait for an analytics-host response. The original inline snippet provides this. A synchronous hosted loader also provides a queue, but its own download can delay application startup.
This PR adds an SDK-maintained local bootstrap as a third installation option. It also makes hosted and local SDK selection follow the namespace the user chooses, rather than substituting the loader's build version.
Related website integration: PostHog/posthog.com#21670. That PR is unchanged and must wait for the SDK release before adopting the new option.
Changes
Fresh review requested: the local installation is additive, but the hosted loader's SDK URL policy intentionally changes. Earlier reviews evaluated a different policy that selected the loader's matching immutable SDK.
Hosted loader mapping
/static/loader.js/static/array.js/static/1/loader.js/static/1/array.js/static/1.441.2/loader.js/static/1.441.2/array.jsThe loader preserves its directory, including reverse-proxy prefixes, and replaces only the filename. Query parameters and fragments are not forwarded to the SDK. Existing initialization settings, queued methods, named instances, CORS, and CSP nonce behavior remain covered.
The hosted loader tag's startup behavior does not change: a synchronous tag still waits for
loader.js, while an async tag cannot provide the queue before its download completes. The SDK download itself remains asynchronous.Optional local bootstrap
Add
posthog-js/dist/loader-bootstrap.js. Serve the artifact with application assets before initialization and application scripts. It creates the queue immediately and downloads the SDK directly and asynchronously. There is no intermediateloader.jsrequest or queue-handoff protocol.data-asset-hostdata-sdk-versionomitted or empty/static/array.jsdata-sdk-version="1"/static/1/array.jsdata-sdk-version="1.441.2"/static/1.441.2/array.jsdata-asset-hostaccepts an HTTP(S) origin/proxy prefix or a root-relative proxy prefix such as/ingest. It excludes/staticand script filenames.data-sdk-versionaccepts a major or full release version, including prerelease namespaces. Invalid inputs produce a configuration error before creating a queue or requesting a script. Supplied token-bearing URLs are not logged.The local and hosted artifacts share generated queued methods and the existing initialization-queue implementation. Function-valued configuration, named instances, and calls during the SDK download remain supported. Call
posthog.init()explicitly for the local installation.The top-level and major aliases receive automatic SDK updates after normal CDN/browser cache expiry. Exact-version namespaces remain pinned. The unversioned alias can advance across majors, matching the original inline snippet's policy. Already-running pages are not hot-upgraded.
Documentation and release scope
Add a minor
posthog-jschangeset covering the local bootstrap and namespace behavior. Remove detailed usage sections from the browser README at the user's request, keeping the package overview and documentation link. Public instructions on posthog.com/docs will follow separately; they are not added here.Validation
pnpm turbo run test:unit test:built --filter=posthog-js: 164 files and 5,973 tests passed.pnpm --filter posthog-js test:functional: 10 tests passed.pnpm --filter posthog-js compatibility:updatecompleted 525 cells / 1,050 executions with no functional failures. A separate fullcompatibility:testcompleted another 525 cells / 1,050 executions and matched with zero differences.current-loader.jsonchanged. Verified structurally that its differences are exactly 129 core URL values moving from the immutable namespace to/static/array.js, plus the existing network-request sort order. The other six golden files are unchanged. No timing normalization, payload, or privacy expectations changed.autoreview --mode branch --base origin/mainfound no actionable issues on8c8a0ff12f19890c13f3f7d7747fcb4870c94778. TruffleHog passed.Not run locally: the complete monorepo validation set, Linux browser matrix, or live production ingestion. Fresh CI starts with this push.
Review feedback follow-up (d42d777)
init(). Replay once after a valid primary initialization. Invalid tokens and named-only initialization do not drain it; repeated SDK execution and reentrant initialization do not duplicate calls.Before the fix, 13 focused assertions failed on queue retention, host validation, and logging. Both new real-browser delayed-init cases also failed against the previous built SDK because the queued event was absent. They now pass. A further reentrant-init check caught duplicate replay during implementation and now passes.
Latest validation: 5,984 browser unit tests, build and types, package lint, 10 functional tests, and all 69 hosted/local loader browser cases across Brave, Firefox, and WebKit passed. The read-only Firefox/WebKit loader compatibility check passed 50 cells / 100 executions with zero snapshot differences. This is partial compatibility coverage; the complete 525-cell results above belong to the earlier head and were not rerun for this follow-up.
The queue fix is in the SDK core and introduces no new stub handshake, extension signature, or persisted-state format. Older exact-version cores keep their earlier behavior, so initialize before they arrive if using an old pin. No recorder start/stop or sampling policy changes are intended.
Rollout constraints
__SV,_i, and queued calls) must remain compatible. This PR adds no new core/extension call signature or persisted-state format.Checked the SDK setup and before-send-hook specifications. No divergence is intended for initialization or callback behavior.
Release info Sub-libraries affected
Libraries affected
Checklist
If releasing new changes
posthog-js.🤖 Agent context
Autonomy: Human-driven (agent-assisted)
Implemented with the Pi coding agent using Git, GitHub CLI, pnpm, Vitest, and Playwright. Pi autoreview performed the isolated final review. No public session link is available.
The user requested three installation options and a consistent namespace rule: top-level loads top-level, major loads major, and exact loads exact. The final local selector is explicit rather than tied to the copied artifact's release or its app-hosted filename. Existing website code is unchanged, and human review is required before merge.