Skip to content

fix(replay): park a timing-gated buffer across a page unload - #4756

Draft
posthog[bot] wants to merge 2 commits into
mainfrom
posthog-self-driving/fixreplay-flush-the-buffer-past-timing-6d4fae
Draft

fix(replay): park a timing-gated buffer across a page unload#4756
posthog[bot] wants to merge 2 commits into
mainfrom
posthog-self-driving/fixreplay-flush-the-buffer-past-timing-6d4fae

Conversation

@posthog

@posthog posthog Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem

  • Customers who set a minimum recording duration lose the start of a replay whenever a user navigates away early. The recording opens partway through the session, with no error and nothing to recover.
  • _flushBuffer holds the buffer on two timing gates — below the minimum duration, and markers-only (no full snapshot yet). Both gates assume a retry through setTimeout.
  • On an unloading page that retry never runs, so the buffered snapshots die with the page and are never sent to /s/.
  • The loss is invisible in event data by definition: the snapshots are never sent.

Changes

Mechanism: an unload flush parks the held buffer in sessionStorage; start() picks it up on the next page in the same tab.

  • _flushBuffer takes an isUnloading flag. Only _onBeforeUnload (and so _onPageHide) passes true.
  • The two timing gates park instead of dropping. The real gates are untouched — PAUSED, DISABLED, sampling-undecided and the interaction hold still ship nothing and park nothing.
  • Shipping past the minimum duration was rejected: it would open a recording the customer configured away. Parking gets both readings right, because sessionStorage dies with the tab.
Situation Before After
Below minimum duration, user navigates to the next page buffer lost parked, ships from the next page
Below minimum duration, user closes the tab buffer lost still nothing, as configured
Markers only, no full snapshot yet buffer lost parked, ships with the content that follows
Paused, disabled, or held for no interaction nothing shipped unchanged

Restore guards

  • Storage key is ph_<persistence_name \|\| token>_replay_pending_buffer, the scheme the sessionid manager already uses, so two apps on one origin park separately.
  • A parked buffer is restored only when its session id and window id match. A duplicated tab copies sessionStorage but mints a new window id, so it does not replay another tab's snapshots.
  • Restored once per recorder, then removed from storage.
  • Nothing is parked over the ~1MB event cap, or when persistence: 'memory' / persistence is disabled.

Release info Sub-libraries affected

Libraries affected

  • All of them
  • posthog-js (web)
  • posthog-js-lite (web lite)
  • posthog-node
  • posthog-react-native
  • @posthog/react-native-plugin
  • @posthog/react
  • @posthog/ai
  • @posthog/convex
  • @posthog/next
  • @posthog/nextjs-config
  • @posthog/nuxt
  • @posthog/openfeature-node-provider
  • @posthog/openfeature-web-provider
  • @posthog/rollup-plugin
  • @posthog/webpack-plugin
  • @posthog/types
  • @posthog/browser-common

Checklist

  • 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: Fully autonomous

Written with Claude Code (PostHog Desktop) from a self-driving inbox report. Six unit tests were added to lazy-sessionrecording.test.ts; the three that assert parking fail on main and pass here. pnpm typecheck, oxlint and the browser unit suite are green (src/__tests__/entrypoints/module.test.ts fails before and after, because it reads dist/).

Decisions along the way:

  • Rejected: skip the timing gates and ship on unload. That opens a recording shorter than the configured minimum, and a markers-only block bills the customer for something that plays back as nothing. Parking honours both gates, because the parked buffer dies with the tab.
  • Rejected: park whatever is left after the flush, from _onBeforeUnload. That reads simpler, but it would also park DISABLED buffers and interaction-held epochs, which must stay discarded. Only the flush knows why the buffer is held, so the decision stays there.
  • Kept the key scheme duplicated rather than reading it off the sessionid manager: this recorder is loaded from the CDN and can run against an older core.
  • beforeunload and pagehide both drive an unload flush, so a park is skipped when the buffer has not grown since the last one, instead of serialising the same buffer twice.

Created with PostHog Desktop from this inbox report, addressing #93856.

The minimum-duration gate and the markers-only gate hold the snapshot buffer for a retry through setTimeout. An unloading page never runs that retry, so the buffered snapshots died with the page and the recording opened partway through the session.

An unload flush now parks the held buffer in sessionStorage, and start() picks it up on the next page in the same tab. sessionStorage dies with the tab, so a session that really ended on that page still ships nothing, which keeps the configured minimum duration intact.

Generated-By: PostHog Desktop
Task-Id: 89e3aa18-a0af-4b5e-ae7c-ba2e44677880
@posthog

posthog Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

🦔 PostHog Review reviewed this pull request

Found 2 must fix, 2 should fix, 4 consider.

Published 8 findings (view the review).

Resolved comments: 1 fixed, 2 declined, 4 left for you

@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Replay incident risk check

This 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 replay-incident-risk skill locally: open a Claude session in the repo root and ask it to review your diff with that skill. Or run node .agents/skills/replay-incident-risk/check.mjs and answer the review questions for each matched class yourself.

Matched 2 past incident pattern(s) (diff vs origin/main):

## Version skew across the lazy-load boundary [path match]
Lazy-loaded bundles (recorder, surveys, web vitals, tracing headers) are served unversioned from the CDN: this change deploys instantly to every SDK version, including npm-pinned ones. Cross-boundary signature changes and persisted-state shape changes have caused fleet-wide recording loss (Mar 2026), broken surveys (Jul 2025, Oct 2025), and a six-day web-vitals gap (Jan 2026).
  Touched paths:
    - packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts
  Read: .agents/skills/replay-incident-risk/INCIDENTS.md#class-1-version-skew-across-the-lazy-load-boundary

## Session rotation, idle handling, and flush semantics [path match]
Changes to rotation, idle state, or flushing change how many recordings ship, and every shipped recording bills. In Jul 2026 an idle-state fix made parked tabs ship an unbounded chain of billed zero-interaction recordings for 11 days before a customer bill surfaced it.
  Touched paths:
    - packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts
  Added lines matching risk patterns:
    - packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts: /flushBuffer|_flushBuffer|clearBuffer/ -> private _flushBuffer(isUnloading: boolean = false): SnapshotBuffer {
  Read: .agents/skills/replay-incident-risk/INCIDENTS.md#class-2-session-rotation-idle-handling-and-flush-semantics

This check is advisory. It flags resemblance to past incidents, not correctness.
For a judgment pass on whether this diff has the same failure mode, run the
`replay-incident-risk` skill in a Claude session from the repo root, or answer the
review questions in the matched sections of .agents/skills/replay-incident-risk/INCIDENTS.md yourself.

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

posthog-js Compliance Report

Date: 2026-09-03 01:15:45 UTC
Duration: 88806ms

✅ All Tests Passed!

26/26 tests passed


Capture Tests

26/26 tests passed

View Details
Test Status Duration
Format Validation.Event Has Required Fields Client 104ms
Format Validation.Event Has Uuid 17ms
Format Validation.Event Has Lib Properties 12ms
Format Validation.Distinct Id Is String Client 15ms
Format Validation.Token Is Present Client 15ms
Format Validation.Custom Properties Preserved 13ms
Format Validation.Event Has Timestamp 14ms
Retry Behavior.Retries On 503 5018ms
Retry Behavior.Does Not Retry On 400 2016ms
Retry Behavior.Does Not Retry On 401 2015ms
Retry Behavior.Respects Retry After Header 5018ms
Retry Behavior.Implements Backoff 15023ms
Retry Behavior.Retries On 500 5014ms
Retry Behavior.Retries On 502 5017ms
Retry Behavior.Retries On 504 5017ms
Retry Behavior.Max Retries Respected 15022ms
Deduplication.Generates Unique Uuids 21ms
Deduplication.Preserves Uuid On Retry 5011ms
Deduplication.Preserves Uuid And Timestamp On Retry 10022ms
Deduplication.Preserves Uuid And Timestamp On Batch Retry 5021ms
Deduplication.No Duplicate Events In Batch 21ms
Deduplication.Different Events Have Different Uuids 12ms
Batch Format.Flush With No Events Sends Nothing 5ms
Error Handling.Does Not Retry On 403 2012ms
Error Handling.Does Not Retry On 413 2013ms
Error Handling.Retries On 408 5017ms

@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Size Change: +8.01 kB (+0.04%)

Total Size: 21 MB

📦 View Changed
Filename Size Change
packages/browser/dist/all-external-dependencies.js 363 kB +1.25 kB (+0.34%)
packages/browser/dist/array.full.js 604 kB +707 B (+0.12%)
packages/browser/dist/array.full.no-********.js 662 kB +1.13 kB (+0.17%)
packages/browser/dist/extension-bundles.js 155 kB +196 B (+0.13%)
packages/browser/dist/lazy-********.js 203 kB +1.44 kB (+0.71%)
packages/browser/dist/module.full.js 607 kB +707 B (+0.12%)
packages/browser/dist/module.full.no-********.js 664 kB +1.13 kB (+0.17%)
packages/browser/dist/posthog-********.js 203 kB +1.46 kB (+0.72%)
ℹ️ View Unchanged
Filename Size
packages/ai/dist/adk/index.cjs 43.1 kB
packages/ai/dist/adk/index.mjs 43 kB
packages/ai/dist/anthropic/index.cjs 42.7 kB
packages/ai/dist/anthropic/index.mjs 42.3 kB
packages/ai/dist/gemini/index.cjs 41.1 kB
packages/ai/dist/gemini/index.mjs 41 kB
packages/ai/dist/index.cjs 64.1 kB
packages/ai/dist/index.mjs 64 kB
packages/ai/dist/langchain/index.cjs 44.2 kB
packages/ai/dist/langchain/index.mjs 44.2 kB
packages/ai/dist/langchain/middleware/index.cjs 50.9 kB
packages/ai/dist/langchain/middleware/index.mjs 50.9 kB
packages/ai/dist/openai-agents/index.cjs 31.8 kB
packages/ai/dist/openai-agents/index.mjs 31.7 kB
packages/ai/dist/openai/index.cjs 95.7 kB
packages/ai/dist/openai/index.mjs 95.3 kB
packages/ai/dist/otel/index.cjs 18.4 kB
packages/ai/dist/otel/index.mjs 18.3 kB
packages/ai/dist/vercel/index.cjs 54.2 kB
packages/ai/dist/vercel/index.mjs 54.1 kB
packages/browser-common/dist/client.js 585 B
packages/browser-common/dist/client.mjs 12 B
packages/browser-common/dist/config.js 1.52 kB
packages/browser-common/dist/config.mjs 216 B
packages/browser-common/dist/constants.js 1.75 kB
packages/browser-common/dist/constants.mjs 219 B
packages/browser-common/dist/disposable.js 1.72 kB
packages/browser-common/dist/disposable.mjs 386 B
packages/browser-common/dist/extension-runtime.js 3.17 kB
packages/browser-common/dist/extension-runtime.mjs 1.83 kB
packages/browser-common/dist/extension.js 585 B
packages/browser-common/dist/extension.mjs 12 B
packages/browser-common/dist/index.js 2.98 kB
packages/browser-common/dist/index.mjs 130 B
packages/browser-common/dist/persistence.js 585 B
packages/browser-common/dist/persistence.mjs 12 B
packages/browser-common/dist/pubsub.js 2.63 kB
packages/browser-common/dist/pubsub.mjs 1.35 kB
packages/browser-common/dist/token.js 585 B
packages/browser-common/dist/token.mjs 12 B
packages/browser-common/dist/types/compression.js 1.37 kB
packages/browser-common/dist/types/compression.mjs 93 B
packages/browser-common/dist/types/index.js 3.68 kB
packages/browser-common/dist/types/index.mjs 144 B
packages/browser-common/dist/types/network-recording.js 585 B
packages/browser-common/dist/types/network-recording.mjs 12 B
packages/browser-common/dist/types/remote-config.js 585 B
packages/browser-common/dist/types/remote-config.mjs 12 B
packages/browser-common/dist/types/surveys.js 4.61 kB
packages/browser-common/dist/types/surveys.mjs 2.17 kB
packages/browser-common/dist/utils/array-at-********.js 619 B
packages/browser-common/dist/utils/array-at-********.mjs 437 B
packages/browser-common/dist/utils/autocapture-utils.js 24.3 kB
packages/browser-common/dist/utils/autocapture-utils.mjs 18.6 kB
packages/browser-common/dist/utils/blocked-uas.js 2.2 kB
packages/browser-common/dist/utils/blocked-uas.mjs 598 B
packages/browser-common/dist/utils/cookie-utils.js 1.99 kB
packages/browser-common/dist/utils/cookie-utils.mjs 673 B
packages/browser-common/dist/utils/device-model-utils.js 2.08 kB
packages/browser-common/dist/utils/device-model-utils.mjs 678 B
packages/browser-common/dist/utils/element-utils.js 2.56 kB
packages/browser-common/dist/utils/element-utils.mjs 804 B
packages/browser-common/dist/utils/elements-chain-utils.js 2.86 kB
packages/browser-common/dist/utils/elements-chain-utils.mjs 1.23 kB
packages/browser-common/dist/utils/encode-utils.js 1.49 kB
packages/browser-common/dist/utils/encode-utils.mjs 205 B
packages/browser-common/dist/utils/event-utils.js 15.8 kB
packages/browser-common/dist/utils/event-utils.mjs 10 kB
packages/browser-common/dist/utils/general-utils.js 7.41 kB
packages/browser-common/dist/utils/general-utils.mjs 4.49 kB
packages/browser-common/dist/utils/globals.js 2.83 kB
packages/browser-common/dist/utils/globals.mjs 842 B
packages/browser-common/dist/utils/logger.js 3.5 kB
packages/browser-common/dist/utils/logger.mjs 1.61 kB
packages/browser-common/dist/utils/matcher-utils.js 2.94 kB
packages/browser-common/dist/utils/matcher-utils.mjs 1.33 kB
packages/browser-common/dist/utils/promise-utils.js 1.45 kB
packages/browser-common/dist/utils/promise-utils.mjs 169 B
packages/browser-common/dist/utils/property-utils.js 3.63 kB
packages/browser-common/dist/utils/property-utils.mjs 1.6 kB
packages/browser-common/dist/utils/prototype-utils.js 3.61 kB
packages/browser-common/dist/utils/prototype-utils.mjs 1.86 kB
packages/browser-common/dist/utils/regex-utils.js 1.55 kB
packages/browser-common/dist/utils/regex-utils.mjs 71 B
packages/browser-common/dist/utils/request-utils.js 6.83 kB
packages/browser-common/dist/utils/request-utils.mjs 4.19 kB
packages/browser-common/dist/utils/simple-event-emitter.js 1.86 kB
packages/browser-common/dist/utils/simple-event-emitter.mjs 553 B
packages/browser-common/dist/utils/stylesheet-loader.js 2.23 kB
packages/browser-common/dist/utils/stylesheet-loader.mjs 823 B
packages/browser-common/dist/utils/type-utils.js 1.68 kB
packages/browser-common/dist/utils/type-utils.mjs 258 B
packages/browser-common/dist/utils/url-targeting-utils.js 2.45 kB
packages/browser-common/dist/utils/url-targeting-utils.mjs 863 B
packages/browser-common/dist/utils/uuidv7.js 6.72 kB
packages/browser-common/dist/utils/uuidv7.mjs 5.14 kB
packages/browser-next/dist/analytics-********.js 1.68 kB
packages/browser-next/dist/analytics-********.mjs 219 B
packages/browser-next/dist/analytics-options.js 585 B
packages/browser-next/dist/analytics-options.mjs 12 B
packages/browser-next/dist/analytics.js 6.62 kB
packages/browser-next/dist/analytics.mjs 5.15 kB
packages/browser-next/dist/bot-filter.js 2.38 kB
packages/browser-next/dist/bot-filter.mjs 1.09 kB
packages/browser-next/dist/capture-v1.js 27.8 kB
packages/browser-next/dist/capture-v1.mjs 25.3 kB
packages/browser-next/dist/core.js 1.69 kB
packages/browser-next/dist/core.mjs 185 B
packages/browser-next/dist/extensions/registry.js 3.41 kB
packages/browser-next/dist/extensions/registry.mjs 2.11 kB
packages/browser-next/dist/id.js 2.25 kB
packages/browser-next/dist/id.mjs 983 B
packages/browser-next/dist/index.js 2.68 kB
packages/browser-next/dist/index.mjs 1.18 kB
packages/browser-next/dist/lane.js 18.1 kB
packages/browser-next/dist/lane.mjs 16.9 kB
packages/browser-next/dist/logger.js 1.87 kB
packages/browser-next/dist/logger.mjs 581 B
packages/browser-next/dist/posthog.js 33.2 kB
packages/browser-next/dist/posthog.mjs 30.9 kB
packages/browser-next/dist/rate-limiter.js 2.89 kB
packages/browser-next/dist/rate-limiter.mjs 1.58 kB
packages/browser-next/dist/request.js 4.61 kB
packages/browser-next/dist/request.mjs 3.21 kB
packages/browser-next/dist/state.js 24.9 kB
packages/browser-next/dist/state.mjs 23.2 kB
packages/browser-next/dist/types.js 585 B
packages/browser-next/dist/types.mjs 12 B
packages/browser-next/dist/version.js 1.31 kB
packages/browser-next/dist/version.mjs 45 B
packages/browser/dist/array.full.es5.js 493 kB
packages/browser/dist/array.js 279 kB
packages/browser/dist/array.no-********.js 318 kB
packages/browser/dist/conversations.js 69 kB
packages/browser/dist/crisp-chat-integration.js 2.01 kB
packages/browser/dist/customizations.full.js 16.1 kB
packages/browser/dist/customizations.js 15.9 kB
packages/browser/dist/dead-clicks-autocapture.js 18.3 kB
packages/browser/dist/default-extensions.js 276 kB
packages/browser/dist/element-inference.js 5.6 kB
packages/browser/dist/exception-autocapture.js 13.6 kB
packages/browser/dist/external-scripts-loader.js 3.58 kB
packages/browser/dist/intercom-integration.js 2.06 kB
packages/browser/dist/logs.js 5.82 kB
packages/browser/dist/main.js 283 kB
packages/browser/dist/module.js 282 kB
packages/browser/dist/module.no-********.js 321 kB
packages/browser/dist/module.slim.js 140 kB
packages/browser/dist/module.slim.no-********.js 154 kB
packages/browser/dist/product-tours-preview.js 82.4 kB
packages/browser/dist/product-tours.js 124 kB
packages/browser/dist/recorder-v2.js 126 kB
packages/browser/dist/recorder.js 126 kB
packages/browser/dist/rrweb-plugin-console-record.js 7.05 kB
packages/browser/dist/rrweb-types.js 2.33 kB
packages/browser/dist/rrweb.js 319 kB
packages/browser/dist/surveys-preview.js 81.3 kB
packages/browser/dist/surveys.js 102 kB
packages/browser/dist/tracing-headers.js 3.74 kB
packages/browser/dist/web-vitals-soft-navs.js 9.94 kB
packages/browser/dist/web-vitals-with-attribution-soft-navs.js 25.3 kB
packages/browser/dist/web-vitals-with-attribution.js 25.3 kB
packages/browser/dist/web-vitals.js 9.92 kB
packages/browser/react/dist/esm/index.js 22.5 kB
packages/browser/react/dist/esm/slim/index.js 18.9 kB
packages/browser/react/dist/esm/surveys/index.js 4.75 kB
packages/browser/react/dist/umd/index.js 25.9 kB
packages/browser/react/dist/umd/slim/index.js 21.8 kB
packages/browser/react/dist/umd/surveys/index.js 5.52 kB
packages/convex/dist/client/feature-flags/crypto.js 76 B
packages/convex/dist/client/feature-flags/evaluator.js 14.9 kB
packages/convex/dist/client/feature-flags/index.js 196 B
packages/convex/dist/client/feature-flags/match-********.js 5.08 kB
packages/convex/dist/client/feature-flags/types.js 44 B
packages/convex/dist/client/index.js 14.8 kB
packages/convex/dist/component/_generated/api.js 712 B
packages/convex/dist/component/_generated/component.js 212 B
packages/convex/dist/component/_generated/dataModel.js 230 B
packages/convex/dist/component/_generated/server.js 3.74 kB
packages/convex/dist/component/convex.config.js 1.66 kB
packages/convex/dist/component/crons.js 969 B
packages/convex/dist/component/lib.js 24.8 kB
packages/convex/dist/component/schema.js 1.17 kB
packages/convex/dist/component/version.js 67 B
packages/core/dist/cookie.js 5.44 kB
packages/core/dist/cookie.mjs 3.12 kB
packages/core/dist/error-tracking/chunk-ids.js 2.64 kB
packages/core/dist/error-tracking/chunk-ids.mjs 1.31 kB
packages/core/dist/error-tracking/coercers/dom-exception-coercer.js 2.4 kB
packages/core/dist/error-tracking/coercers/dom-exception-coercer.mjs 993 B
packages/core/dist/error-tracking/coercers/error-coercer.js 2.23 kB
packages/core/dist/error-tracking/coercers/error-coercer.mjs 888 B
packages/core/dist/error-tracking/coercers/error-event-coercer.js 2.45 kB
packages/core/dist/error-tracking/coercers/error-event-coercer.mjs 1.05 kB
packages/core/dist/error-tracking/coercers/event-coercer.js 1.92 kB
packages/core/dist/error-tracking/coercers/event-coercer.mjs 548 B
packages/core/dist/error-tracking/coercers/index.js 5.82 kB
packages/core/dist/error-tracking/coercers/index.mjs 326 B
packages/core/dist/error-tracking/coercers/object-coercer.js 4.13 kB
packages/core/dist/error-tracking/coercers/object-coercer.mjs 2.53 kB
packages/core/dist/error-tracking/coercers/primitive-coercer.js 1.76 kB
packages/core/dist/error-tracking/coercers/primitive-coercer.mjs 419 B
packages/core/dist/error-tracking/coercers/promise-rejection-event.js 2.69 kB
packages/core/dist/error-tracking/coercers/promise-rejection-event.mjs 1.25 kB
packages/core/dist/error-tracking/coercers/string-coercer.js 2.11 kB
packages/core/dist/error-tracking/coercers/string-coercer.mjs 820 B
packages/core/dist/error-tracking/coercers/utils.js 2.16 kB
packages/core/dist/error-tracking/coercers/utils.mjs 716 B
packages/core/dist/error-tracking/error-properties-builder.js 5.66 kB
packages/core/dist/error-tracking/error-properties-builder.mjs 4.23 kB
packages/core/dist/error-tracking/exception-steps.js 6.1 kB
packages/core/dist/error-tracking/exception-steps.mjs 3.83 kB
packages/core/dist/error-tracking/index.js 4.62 kB
packages/core/dist/error-tracking/index.mjs 222 B
packages/core/dist/error-tracking/parsers/base.js 2.1 kB
packages/core/dist/error-tracking/parsers/base.mjs 627 B
packages/core/dist/error-tracking/parsers/chrome.js 2.83 kB
packages/core/dist/error-tracking/parsers/chrome.mjs 1.32 kB
packages/core/dist/error-tracking/parsers/gecko.js 2.57 kB
packages/core/dist/error-tracking/parsers/gecko.mjs 1.13 kB
packages/core/dist/error-tracking/parsers/index.js 4.85 kB
packages/core/dist/error-tracking/parsers/index.mjs 2.01 kB
packages/core/dist/error-tracking/parsers/node.js 4.31 kB
packages/core/dist/error-tracking/parsers/node.mjs 2.95 kB
packages/core/dist/error-tracking/parsers/opera.js 2.35 kB
packages/core/dist/error-tracking/parsers/opera.mjs 746 B
packages/core/dist/error-tracking/parsers/safari.js 1.98 kB
packages/core/dist/error-tracking/parsers/safari.mjs 574 B
packages/core/dist/error-tracking/parsers/winjs.js 1.82 kB
packages/core/dist/error-tracking/parsers/winjs.mjs 426 B
packages/core/dist/error-tracking/release.js 1.52 kB
packages/core/dist/error-tracking/release.mjs 203 B
packages/core/dist/error-tracking/types.js 1.42 kB
packages/core/dist/error-tracking/types.mjs 131 B
packages/core/dist/error-tracking/utils.js 1.9 kB
packages/core/dist/error-tracking/utils.mjs 604 B
packages/core/dist/eventemitter.js 1.88 kB
packages/core/dist/eventemitter.mjs 571 B
packages/core/dist/featureFlagLocalEvaluation.js 22.2 kB
packages/core/dist/featureFlagLocalEvaluation.mjs 19.6 kB
packages/core/dist/featureFlagUtils.js 8.57 kB
packages/core/dist/featureFlagUtils.mjs 5.48 kB
packages/core/dist/gzip.js 5.88 kB
packages/core/dist/gzip.mjs 3.87 kB
packages/core/dist/index.js 22.6 kB
packages/core/dist/index.mjs 1.69 kB
packages/core/dist/logs/index.js 10.9 kB
packages/core/dist/logs/index.mjs 9.16 kB
packages/core/dist/logs/logs-utils.js 6.69 kB
packages/core/dist/logs/logs-utils.mjs 4.51 kB
packages/core/dist/logs/types.js 585 B
packages/core/dist/logs/types.mjs 12 B
packages/core/dist/metrics/config.js 2.05 kB
packages/core/dist/metrics/config.mjs 735 B
packages/core/dist/metrics/index.js 14.5 kB
packages/core/dist/metrics/index.mjs 12 kB
packages/core/dist/metrics/metrics-utils.js 3.75 kB
packages/core/dist/metrics/metrics-utils.mjs 1.71 kB
packages/core/dist/metrics/types.js 585 B
packages/core/dist/metrics/types.mjs 12 B
packages/core/dist/posthog-core-stateless.js 47 kB
packages/core/dist/posthog-core-stateless.mjs 43.6 kB
packages/core/dist/posthog-core.js 49.6 kB
packages/core/dist/posthog-core.mjs 43.9 kB
packages/core/dist/surveys/activation.js 2.29 kB
packages/core/dist/surveys/activation.mjs 572 B
packages/core/dist/surveys/events.js 4.36 kB
packages/core/dist/surveys/events.mjs 2.05 kB
packages/core/dist/surveys/index.js 6.64 kB
packages/core/dist/surveys/index.mjs 798 B
packages/core/dist/surveys/keys.js 1.8 kB
packages/core/dist/surveys/keys.mjs 350 B
packages/core/dist/surveys/property-********.js 3.78 kB
packages/core/dist/surveys/property-********.mjs 2.14 kB
packages/core/dist/surveys/translations.js 9.18 kB
packages/core/dist/surveys/translations.mjs 6.83 kB
packages/core/dist/surveys/validation.js 3.15 kB
packages/core/dist/surveys/validation.mjs 1.51 kB
packages/core/dist/testing/index.js 2.76 kB
packages/core/dist/testing/index.mjs 79 B
packages/core/dist/testing/PostHogCoreTestClient.js 3.34 kB
packages/core/dist/testing/PostHogCoreTestClient.mjs 1.83 kB
packages/core/dist/testing/test-utils.js 2.91 kB
packages/core/dist/testing/test-utils.mjs 1.15 kB
packages/core/dist/tracing-headers.js 3.48 kB
packages/core/dist/tracing-headers.mjs 2.08 kB
packages/core/dist/types.js 8.39 kB
packages/core/dist/types.mjs 5.23 kB
packages/core/dist/utils/bot-detection.js 3.38 kB
packages/core/dist/utils/bot-detection.mjs 1.96 kB
packages/core/dist/utils/browser-utils.js 1.57 kB
packages/core/dist/utils/browser-utils.mjs 182 B
packages/core/dist/utils/bucketed-rate-limiter.js 4.32 kB
packages/core/dist/utils/bucketed-rate-limiter.mjs 2.22 kB
packages/core/dist/utils/index.js 15.4 kB
packages/core/dist/utils/index.mjs 3.34 kB
packages/core/dist/utils/json-utils.js 6.74 kB
packages/core/dist/utils/json-utils.mjs 4.45 kB
packages/core/dist/utils/logger.js 2.68 kB
packages/core/dist/utils/logger.mjs 1.29 kB
packages/core/dist/utils/number-utils.js 3.42 kB
packages/core/dist/utils/number-utils.mjs 1.68 kB
packages/core/dist/utils/otlp-********.js 2.76 kB
packages/core/dist/utils/otlp-********.mjs 1.18 kB
packages/core/dist/utils/otlp-any-value.js 7.96 kB
packages/core/dist/utils/otlp-any-value.mjs 5.77 kB
packages/core/dist/utils/promise-queue.js 2.58 kB
packages/core/dist/utils/promise-queue.mjs 1.25 kB
packages/core/dist/utils/string-utils.js 3.83 kB
packages/core/dist/utils/string-utils.mjs 1.97 kB
packages/core/dist/utils/type-utils.js 7.52 kB
packages/core/dist/utils/type-utils.mjs 3.27 kB
packages/core/dist/utils/user-agent-utils.js 18.5 kB
packages/core/dist/utils/user-agent-utils.mjs 14.7 kB
packages/core/dist/vendor/uuidv7.js 8.36 kB
packages/core/dist/vendor/uuidv7.mjs 6.7 kB
packages/mcp/dist/extensions/analytics-parameters.js 6.63 kB
packages/mcp/dist/extensions/analytics-parameters.mjs 4.27 kB
packages/mcp/dist/extensions/capture.js 4.04 kB
packages/mcp/dist/extensions/capture.mjs 2.58 kB
packages/mcp/dist/extensions/client-********.js 6.33 kB
packages/mcp/dist/extensions/client-********.mjs 3.71 kB
packages/mcp/dist/extensions/compatibility.js 5.45 kB
packages/mcp/dist/extensions/compatibility.mjs 3.4 kB
packages/mcp/dist/extensions/constants.js 5.45 kB
packages/mcp/dist/extensions/constants.mjs 2.94 kB
packages/mcp/dist/extensions/context-parameters.js 2.85 kB
packages/mcp/dist/extensions/context-parameters.mjs 893 B
packages/mcp/dist/extensions/conversation-id.js 6.41 kB
packages/mcp/dist/extensions/conversation-id.mjs 3.58 kB
packages/mcp/dist/extensions/detect.js 3.87 kB
packages/mcp/dist/extensions/detect.mjs 1.49 kB
packages/mcp/dist/extensions/event-types.js 1.78 kB
packages/mcp/dist/extensions/event-types.mjs 459 B
packages/mcp/dist/extensions/exceptions.js 2.67 kB
packages/mcp/dist/extensions/exceptions.mjs 1.19 kB
packages/mcp/dist/extensions/headers.js 2.08 kB
packages/mcp/dist/extensions/headers.mjs 643 B
packages/mcp/dist/extensions/ids.js 2.11 kB
packages/mcp/dist/extensions/ids.mjs 637 B
packages/mcp/dist/extensions/instrument-********.js 5.02 kB
packages/mcp/dist/extensions/instrument-********.mjs 3.18 kB
packages/mcp/dist/extensions/instrument-highlevel.js 12.8 kB
packages/mcp/dist/extensions/instrument-highlevel.mjs 10.2 kB
packages/mcp/dist/extensions/instrumentation.js 27.9 kB
packages/mcp/dist/extensions/instrumentation.mjs 21.9 kB
packages/mcp/dist/extensions/intent.js 3.3 kB
packages/mcp/dist/extensions/intent.mjs 1.64 kB
packages/mcp/dist/extensions/internal.js 7.63 kB
packages/mcp/dist/extensions/internal.mjs 5.23 kB
packages/mcp/dist/extensions/lib-********.js 1.7 kB
packages/mcp/dist/extensions/lib-********.mjs 267 B
packages/mcp/dist/extensions/logger.js 1.81 kB
packages/mcp/dist/extensions/logger.mjs 380 B
packages/mcp/dist/extensions/mcp-********.js 4.78 kB
packages/mcp/dist/extensions/mcp-********.mjs 3.3 kB
packages/mcp/dist/extensions/mcp-sdk-compat.js 5.13 kB
packages/mcp/dist/extensions/mcp-sdk-compat.mjs 2.9 kB
packages/mcp/dist/extensions/model-parameters.js 3.58 kB
packages/mcp/dist/extensions/model-parameters.mjs 1.41 kB
packages/mcp/dist/extensions/output-instructions.js 6.21 kB
packages/mcp/dist/extensions/output-instructions.mjs 3.91 kB
packages/mcp/dist/extensions/posthog-events.js 11.8 kB
packages/mcp/dist/extensions/posthog-events.mjs 7.98 kB
packages/mcp/dist/extensions/posthog-mcp.js 7.04 kB
packages/mcp/dist/extensions/posthog-mcp.mjs 4.93 kB
packages/mcp/dist/extensions/request-headers.js 2.27 kB
packages/mcp/dist/extensions/request-headers.mjs 963 B
packages/mcp/dist/extensions/sanitization.js 4.59 kB
packages/mcp/dist/extensions/sanitization.mjs 2.86 kB
packages/mcp/dist/extensions/session-token.js 5.47 kB
packages/mcp/dist/extensions/session-token.mjs 3.6 kB
packages/mcp/dist/extensions/session.js 6.74 kB
packages/mcp/dist/extensions/session.mjs 4.12 kB
packages/mcp/dist/extensions/sink.js 4.47 kB
packages/mcp/dist/extensions/sink.mjs 2.67 kB
packages/mcp/dist/extensions/tools.js 3.61 kB
packages/mcp/dist/extensions/tools.mjs 1.63 kB
packages/mcp/dist/extensions/tracing-helpers.js 2.51 kB
packages/mcp/dist/extensions/tracing-helpers.mjs 772 B
packages/mcp/dist/extensions/transport-********.js 3 kB
packages/mcp/dist/extensions/transport-********.mjs 1.09 kB
packages/mcp/dist/extensions/truncation.js 11.6 kB
packages/mcp/dist/extensions/truncation.mjs 9.79 kB
packages/mcp/dist/index.js 8.88 kB
packages/mcp/dist/index.mjs 4.76 kB
packages/mcp/dist/types.js 585 B
packages/mcp/dist/types.mjs 12 B
packages/mcp/dist/version.js 1.31 kB
packages/mcp/dist/version.mjs 46 B
packages/next/dist/app/PostHogProvider.js 5.37 kB
packages/next/dist/client/ClientPostHogProvider.js 2.28 kB
packages/next/dist/client/hooks.js 172 B
packages/next/dist/client/PostHogPageView.js 4.23 kB
packages/next/dist/index.client.js 401 B
packages/next/dist/index.edge.js 570 B
packages/next/dist/index.js 554 B
packages/next/dist/index.react-server.js 420 B
packages/next/dist/middleware/postHogMiddleware.js 4.08 kB
packages/next/dist/pages.client.js 225 B
packages/next/dist/pages.edge.js 294 B
packages/next/dist/pages.js 347 B
packages/next/dist/pages/createPostHog.js 1.48 kB
packages/next/dist/pages/getServerSidePostHog.js 1.48 kB
packages/next/dist/pages/PostHogPageView.js 1.77 kB
packages/next/dist/pages/PostHogProvider.js 1.58 kB
packages/next/dist/server.edge.js 795 B
packages/next/dist/server/captureRequestError.js 5.26 kB
packages/next/dist/server/clientCache.edge.js 305 B
packages/next/dist/server/clientCache.js 1.54 kB
packages/next/dist/server/clientCache.node.js 305 B
packages/next/dist/server/createPostHog.js 1.44 kB
packages/next/dist/server/getPostHog.js 3.45 kB
packages/next/dist/server/onRequestError.js 1.39 kB
packages/next/dist/server/onRequestError.types.js 59 B
packages/next/dist/shared/browser.js 195 B
packages/next/dist/shared/config.js 2.23 kB
packages/next/dist/shared/constants.js 201 B
packages/next/dist/shared/cookie.js 540 B
packages/next/dist/shared/identity.js 2.56 kB
packages/next/dist/shared/tracing-headers.js 2.18 kB
packages/nextjs-config/dist/config.js 10.3 kB
packages/nextjs-config/dist/config.mjs 8.67 kB
packages/nextjs-config/dist/index.js 2.21 kB
packages/nextjs-config/dist/index.mjs 30 B
packages/nextjs-config/dist/strip-sourcemap-********.js 4.91 kB
packages/nextjs-config/dist/strip-sourcemap-********.mjs 2.96 kB
packages/nextjs-config/dist/utils.js 3.93 kB
packages/nextjs-config/dist/utils.mjs 1.72 kB
packages/node/dist/ai-capture/batching.js 2.71 kB
packages/node/dist/ai-capture/batching.mjs 1.21 kB
packages/node/dist/ai-capture/routing.js 1.96 kB
packages/node/dist/ai-capture/routing.mjs 264 B
packages/node/dist/capture-v1/config.js 1.59 kB
packages/node/dist/capture-v1/config.mjs 283 B
packages/node/dist/capture-v1/errors.js 1.99 kB
packages/node/dist/capture-v1/errors.mjs 694 B
packages/node/dist/capture-v1/routing.js 1.77 kB
packages/node/dist/capture-v1/routing.mjs 279 B
packages/node/dist/capture-v1/sender.js 11.5 kB
packages/node/dist/capture-v1/sender.mjs 9.94 kB
packages/node/dist/capture-v1/transform.js 5.22 kB
packages/node/dist/capture-v1/transform.mjs 3.65 kB
packages/node/dist/capture-v1/types.js 585 B
packages/node/dist/capture-v1/types.mjs 12 B
packages/node/dist/client.js 59.6 kB
packages/node/dist/client.mjs 56.1 kB
packages/node/dist/entrypoints/index.edge.js 3.66 kB
packages/node/dist/entrypoints/index.edge.mjs 720 B
packages/node/dist/entrypoints/index.node.js 5.7 kB
packages/node/dist/entrypoints/index.node.mjs 1.43 kB
packages/node/dist/entrypoints/nestjs.js 2.27 kB
packages/node/dist/entrypoints/nestjs.mjs 42 B
packages/node/dist/experimental.js 852 B
packages/node/dist/experimental.mjs 279 B
packages/node/dist/exports.js 6.3 kB
packages/node/dist/exports.mjs 388 B
packages/node/dist/extensions/context/context.js 2.22 kB
packages/node/dist/extensions/context/context.mjs 863 B
packages/node/dist/extensions/context/types.js 585 B
packages/node/dist/extensions/context/types.mjs 12 B
packages/node/dist/extensions/error-tracking/autocapture.js 5.29 kB
packages/node/dist/extensions/error-tracking/autocapture.mjs 3.62 kB
packages/node/dist/extensions/error-tracking/index.js 4.48 kB
packages/node/dist/extensions/error-tracking/index.mjs 3.17 kB
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.js 11.7 kB
packages/node/dist/extensions/error-tracking/modifiers/context-lines.node.mjs 9.64 kB
packages/node/dist/extensions/error-tracking/modifiers/module.node.js 2.87 kB
packages/node/dist/extensions/error-tracking/modifiers/module.node.mjs 1.45 kB
packages/node/dist/extensions/error-tracking/modifiers/relative-path.node.js 2.07 kB
packages/node/dist/extensions/error-tracking/modifiers/relative-path.node.mjs 624 B
packages/node/dist/extensions/express.js 5.07 kB
packages/node/dist/extensions/express.mjs 2.78 kB
packages/node/dist/extensions/feature-flags/cache.js 585 B
packages/node/dist/extensions/feature-flags/cache.mjs 12 B
packages/node/dist/extensions/feature-flags/crypto.js 1.37 kB
packages/node/dist/extensions/feature-flags/crypto.mjs 42 B
packages/node/dist/extensions/feature-flags/feature-flags.js 32.3 kB
packages/node/dist/extensions/feature-flags/feature-flags.mjs 29.8 kB
packages/node/dist/extensions/nestjs.js 5.71 kB
packages/node/dist/extensions/nestjs.mjs 3.42 kB
packages/node/dist/extensions/sentry-integration.js 4.99 kB
packages/node/dist/extensions/sentry-integration.mjs 3.37 kB
packages/node/dist/extensions/tracing-headers.js 3.41 kB
packages/node/dist/extensions/tracing-headers.mjs 1.53 kB
packages/node/dist/extensions/url-utils.js 2.32 kB
packages/node/dist/extensions/url-utils.mjs 785 B
packages/node/dist/feature-flag-evaluations.js 6.22 kB
packages/node/dist/feature-flag-evaluations.mjs 4.77 kB
packages/node/dist/gzip.node.js 1.83 kB
packages/node/dist/gzip.node.mjs 416 B
packages/node/dist/storage-memory.js 1.61 kB
packages/node/dist/storage-memory.mjs 297 B
packages/node/dist/types.js 1.52 kB
packages/node/dist/types.mjs 224 B
packages/node/dist/version.js 1.31 kB
packages/node/dist/version.mjs 46 B
packages/nuxt/dist/module.mjs 5.92 kB
packages/nuxt/dist/runtime/composables/useFeatureFlagEnabled.js 566 B
packages/nuxt/dist/runtime/composables/useFeatureFlagPayload.js 698 B
packages/nuxt/dist/runtime/composables/useFeatureFlagVariantKey.js 591 B
packages/nuxt/dist/runtime/composables/usePostHog.js 128 B
packages/nuxt/dist/runtime/nitro-plugin-v2.js 480 B
packages/nuxt/dist/runtime/nitro-plugin-v3.js 574 B
packages/nuxt/dist/runtime/nitro-plugin.js 1.03 kB
packages/nuxt/dist/runtime/vue-plugin.js 1.13 kB
packages/openfeature-node-provider/dist/index.js 1.86 kB
packages/openfeature-node-provider/dist/index.mjs 122 B
packages/openfeature-node-provider/dist/mapping.js 5.42 kB
packages/openfeature-node-provider/dist/mapping.mjs 3.21 kB
packages/openfeature-node-provider/dist/provider.js 4.09 kB
packages/openfeature-node-provider/dist/provider.mjs 2.6 kB
packages/openfeature-web-provider/dist/index.js 1.84 kB
packages/openfeature-web-provider/dist/index.mjs 119 B
packages/openfeature-web-provider/dist/mapping.js 5.38 kB
packages/openfeature-web-provider/dist/mapping.mjs 3.18 kB
packages/openfeature-web-provider/dist/provider.js 4.51 kB
packages/openfeature-web-provider/dist/provider.mjs 3.06 kB
packages/plugin-utils/dist/chunk-ids.js 3.96 kB
packages/plugin-utils/dist/chunk-ids.mjs 2.11 kB
packages/plugin-utils/dist/cli.js 4.47 kB
packages/plugin-utils/dist/cli.mjs 2.72 kB
packages/plugin-utils/dist/config.js 3.6 kB
packages/plugin-utils/dist/config.mjs 2.25 kB
packages/plugin-utils/dist/index.js 4.5 kB
packages/plugin-utils/dist/index.mjs 197 B
packages/plugin-utils/dist/spawn-local.js 3.17 kB
packages/plugin-utils/dist/spawn-local.mjs 1.66 kB
packages/plugin-utils/dist/utils.js 3.36 kB
packages/plugin-utils/dist/utils.mjs 1.3 kB
packages/react-native/dist/autocapture.js 8.31 kB
packages/react-native/dist/error-tracking/index.js 9.97 kB
packages/react-native/dist/error-tracking/utils.js 2.58 kB
packages/react-native/dist/frameworks/wix-navigation.js 1.3 kB
packages/react-native/dist/hooks/useFeatureFlag.js 1.86 kB
packages/react-native/dist/hooks/useFeatureFlagResult.js 983 B
packages/react-native/dist/hooks/useFeatureFlags.js 941 B
packages/react-native/dist/hooks/useNavigationTracker.js 2.45 kB
packages/react-native/dist/hooks/usePostHog.js 544 B
packages/react-native/dist/hooks/utils.js 988 B
packages/react-native/dist/index.js 4.33 kB
packages/react-native/dist/logs-********.js 3.66 kB
packages/react-native/dist/native-deps.js 8.73 kB
packages/react-native/dist/optional/OptionalAsyncStorage.js 299 B
packages/react-native/dist/optional/OptionalExpoApplication.js 377 B
packages/react-native/dist/optional/OptionalExpoDevice.js 347 B
packages/react-native/dist/optional/OptionalExpoFileSystem.js 386 B
packages/react-native/dist/optional/OptionalExpoFileSystemLegacy.js 423 B
packages/react-native/dist/optional/OptionalExpoLocalization.js 383 B
packages/react-native/dist/optional/OptionalPlugin.js 891 B
packages/react-native/dist/optional/OptionalReactNativeDeviceInfo.js 415 B
packages/react-native/dist/optional/OptionalReactNativeLocalize.js 303 B
packages/react-native/dist/optional/OptionalReactNativeNavigation.js 415 B
packages/react-native/dist/optional/OptionalReactNativeNavigationWix.js 443 B
packages/react-native/dist/optional/OptionalReactNativeSafeArea.js 644 B
packages/react-native/dist/optional/OptionalReactNativeSvg.js 872 B
packages/react-native/dist/posthog-rn.js 67.4 kB
packages/react-native/dist/PostHogContext.js 329 B
packages/react-native/dist/PostHogErrorBoundary.js 3.19 kB
packages/react-native/dist/PostHogMaskView.js 1.68 kB
packages/react-native/dist/PostHogProvider.js 6 kB
packages/react-native/dist/storage.js 5.76 kB
packages/react-native/dist/surveys/components/BottomSection.js 1.51 kB
packages/react-native/dist/surveys/components/Cancel.js 1.1 kB
packages/react-native/dist/surveys/components/ConfirmationMessage.js 1.76 kB
packages/react-native/dist/surveys/components/IntroMessage.js 1.65 kB
packages/react-native/dist/surveys/components/QuestionHeader.js 1.47 kB
packages/react-native/dist/surveys/components/QuestionTypes.js 14 kB
packages/react-native/dist/surveys/components/SurveyModal.js 7.24 kB
packages/react-native/dist/surveys/components/Surveys.js 6.58 kB
packages/react-native/dist/surveys/getActiveMatchingSurveys.js 2.69 kB
packages/react-native/dist/surveys/icons.js 10 kB
packages/react-native/dist/surveys/index.js 600 B
packages/react-native/dist/surveys/PostHogSurveyProvider.js 6.73 kB
packages/react-native/dist/surveys/safeStyleSheet.js 448 B
packages/react-native/dist/surveys/survey-translations.js 1.11 kB
packages/react-native/dist/surveys/surveys-utils.js 11.5 kB
packages/react-native/dist/surveys/useActivatedSurveys.js 3.68 kB
packages/react-native/dist/surveys/useSurveyStorage.js 2.48 kB
packages/react-native/dist/tooling/expoconfig.js 23.8 kB
packages/react-native/dist/tooling/metroconfig.js 2.32 kB
packages/react-native/dist/tooling/posthogMetroSerializer.js 7.74 kB
packages/react-native/dist/tooling/utils.js 4.2 kB
packages/react-native/dist/tooling/vendor/expo/expoconfig.js 70 B
packages/react-native/dist/tooling/vendor/metro/countLines.js 237 B
packages/react-native/dist/tooling/vendor/metro/utils.js 3.51 kB
packages/react-native/dist/types.js 70 B
packages/react-native/dist/utils.js 1.3 kB
packages/react-native/dist/version.js 130 B
packages/react/dist/esm/index.js 22.5 kB
packages/react/dist/esm/slim/index.js 18.9 kB
packages/react/dist/esm/surveys/index.js 4.75 kB
packages/react/dist/umd/index.js 25.9 kB
packages/react/dist/umd/slim/index.js 21.8 kB
packages/react/dist/umd/surveys/index.js 5.52 kB
packages/rollup-plugin/dist/index.js 10.2 kB
packages/rrweb/all/dist/rrweb-all.cjs 705 kB
packages/rrweb/all/dist/rrweb-all.js 705 kB
packages/rrweb/all/dist/rrweb-all.umd.cjs 709 kB
packages/rrweb/all/dist/rrweb-all.umd.min.cjs 328 kB
packages/rrweb/packer/dist/base-********.cjs 18.3 kB
packages/rrweb/packer/dist/base-********.js 18.2 kB
packages/rrweb/packer/dist/base-********.umd.cjs 18.7 kB
packages/rrweb/packer/dist/base-********.umd.min.cjs 9.5 kB
packages/rrweb/packer/dist/pack.cjs 347 B
packages/rrweb/packer/dist/pack.js 285 B
packages/rrweb/packer/dist/pack.umd.cjs 1.63 kB
packages/rrweb/packer/dist/pack.umd.min.cjs 1.11 kB
packages/rrweb/packer/dist/packer.cjs 257 B
packages/rrweb/packer/dist/packer.js 136 B
packages/rrweb/packer/dist/packer.umd.cjs 662 B
packages/rrweb/packer/dist/packer.umd.min.cjs 626 B
packages/rrweb/packer/dist/unpack.cjs 769 B
packages/rrweb/packer/dist/unpack.js 702 B
packages/rrweb/packer/dist/unpack.umd.cjs 1.17 kB
packages/rrweb/packer/dist/unpack.umd.min.cjs 955 B
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.cjs 37.6 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.js 37.5 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.umd.cjs 38 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-record/dist/rrweb-plugin-canvas-webrtc-record.umd.min.cjs 22.2 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.cjs 34.3 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.js 34.2 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.umd.cjs 34.7 kB
packages/rrweb/plugins/rrweb-plugin-canvas-webrtc-replay/dist/rrweb-plugin-canvas-webrtc-replay.umd.min.cjs 20.5 kB
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.cjs 15.8 kB
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.js 15.7 kB
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.umd.cjs 16.3 kB
packages/rrweb/plugins/rrweb-plugin-console-record/dist/rrweb-plugin-console-record.umd.min.cjs 7.66 kB
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.cjs 5.01 kB
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.js 4.9 kB
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.umd.cjs 5.44 kB
packages/rrweb/plugins/rrweb-plugin-console-replay/dist/rrweb-plugin-console-replay.umd.min.cjs 2.64 kB
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.cjs 681 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.js 548 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.umd.cjs 1.12 kB
packages/rrweb/plugins/rrweb-plugin-sequential-id-record/dist/rrweb-plugin-sequential-id-record.umd.min.cjs 829 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.cjs 933 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.js 820 B
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.umd.cjs 1.37 kB
packages/rrweb/plugins/rrweb-plugin-sequential-id-replay/dist/rrweb-plugin-sequential-id-replay.umd.min.cjs 968 B
packages/rrweb/record/dist/rrweb-record.cjs 234 kB
packages/rrweb/record/dist/rrweb-record.js 233 kB
packages/rrweb/record/dist/rrweb-record.umd.cjs 234 kB
packages/rrweb/record/dist/rrweb-record.umd.min.cjs 107 kB
packages/rrweb/replay/dist/rrweb-replay.cjs 476 kB
packages/rrweb/replay/dist/rrweb-replay.js 476 kB
packages/rrweb/replay/dist/rrweb-replay.umd.cjs 479 kB
packages/rrweb/replay/dist/rrweb-replay.umd.min.cjs 225 kB
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.cjs 160 kB
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.js 160 kB
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.umd.cjs 162 kB
packages/rrweb/rrdom-nodejs/dist/rrdom-nodejs.umd.min.cjs 74.9 kB
packages/rrweb/rrdom/dist/rrdom.cjs 186 kB
packages/rrweb/rrdom/dist/rrdom.js 185 kB
packages/rrweb/rrdom/dist/rrdom.umd.cjs 187 kB
packages/rrweb/rrdom/dist/rrdom.umd.min.cjs 85.5 kB
packages/rrweb/rrweb-snapshot/dist/record.cjs 39 kB
packages/rrweb/rrweb-snapshot/dist/record.js 37.3 kB
packages/rrweb/rrweb-snapshot/dist/record.umd.cjs 83.2 kB
packages/rrweb/rrweb-snapshot/dist/record.umd.min.cjs 37.4 kB
packages/rrweb/rrweb-snapshot/dist/replay.cjs 149 kB
packages/rrweb/rrweb-snapshot/dist/replay.js 149 kB
packages/rrweb/rrweb-snapshot/dist/replay.umd.cjs 194 kB
packages/rrweb/rrweb-snapshot/dist/replay.umd.min.cjs 86.2 kB
packages/rrweb/rrweb-snapshot/dist/rrweb-********.cjs 3.95 kB
packages/rrweb/rrweb-snapshot/dist/rrweb-********.js 2.32 kB
packages/rrweb/rrweb-snapshot/dist/rrweb-********.umd.cjs 261 kB
packages/rrweb/rrweb-snapshot/dist/rrweb-********.umd.min.cjs 109 kB
packages/rrweb/rrweb-snapshot/dist/types-********.cjs 37.6 kB
packages/rrweb/rrweb-snapshot/dist/types-********.js 36.5 kB
packages/rrweb/rrweb-snapshot/dist/types-********.umd.cjs 39 kB
packages/rrweb/rrweb-snapshot/dist/types-********.umd.min.cjs 17.3 kB
packages/rrweb/rrweb/dist/rrweb.cjs 690 kB
packages/rrweb/rrweb/dist/rrweb.js 689 kB
packages/rrweb/rrweb/dist/rrweb.umd.cjs 690 kB
packages/rrweb/rrweb/dist/rrweb.umd.min.cjs 319 kB
packages/rrweb/types/dist/rrweb-types.cjs 5.75 kB
packages/rrweb/types/dist/rrweb-types.js 5.46 kB
packages/rrweb/types/dist/rrweb-types.umd.cjs 6.16 kB
packages/rrweb/types/dist/rrweb-types.umd.min.cjs 2.86 kB
packages/rrweb/utils/dist/rrweb-utils.cjs 7.76 kB
packages/rrweb/utils/dist/rrweb-utils.js 7.3 kB
packages/rrweb/utils/dist/rrweb-utils.umd.cjs 8.24 kB
packages/rrweb/utils/dist/rrweb-utils.umd.min.cjs 4.09 kB
packages/types/dist/capture-log.js 585 B
packages/types/dist/capture-log.mjs 12 B
packages/types/dist/capture-metric.js 1.47 kB
packages/types/dist/capture-metric.mjs 93 B
packages/types/dist/capture.js 585 B
packages/types/dist/capture.mjs 12 B
packages/types/dist/common.js 585 B
packages/types/dist/common.mjs 12 B
packages/types/dist/feature-flags.js 585 B
packages/types/dist/feature-flags.mjs 12 B
packages/types/dist/index.js 1.55 kB
packages/types/dist/index.mjs 75 B
packages/types/dist/posthog-config.js 585 B
packages/types/dist/posthog-config.mjs 12 B
packages/types/dist/posthog.js 585 B
packages/types/dist/posthog.mjs 12 B
packages/types/dist/request.js 585 B
packages/types/dist/request.mjs 12 B
packages/types/dist/segment.js 585 B
packages/types/dist/segment.mjs 12 B
packages/types/dist/session-recording.js 585 B
packages/types/dist/session-recording.mjs 12 B
packages/types/dist/survey.js 585 B
packages/types/dist/survey.mjs 12 B
packages/types/dist/toolbar.js 585 B
packages/types/dist/toolbar.mjs 12 B
packages/types/dist/tree-shakeable.js 585 B
packages/types/dist/tree-shakeable.mjs 12 B
packages/web/dist/index.cjs 11.2 kB
packages/web/dist/index.mjs 11.1 kB
packages/webpack-plugin/dist/config.js 2.38 kB
packages/webpack-plugin/dist/config.mjs 1.31 kB
packages/webpack-plugin/dist/index.js 9.29 kB
packages/webpack-plugin/dist/index.mjs 5.51 kB
tooling/changelog/dist/index.js 3.31 kB
tooling/rollup-utils/dist/index.js 1.17 kB

compressed-size-action

@posthog

posthog Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

PostHog Review alpha 🦔 If you find any issues helpful - please reply "valid", "invalid", etc., for evaluation purposes 🙏

@posthog posthog Bot left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

PostHog Review

Found 2 must fix, 2 should fix, 4 consider.

Other findings (outside the changed lines)

Valid issues on this PR's files that sit on lines GitHub won't let us comment on inline.

Marker-only parking bypasses stronger recording gates

Priority: consider | File: packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts:2210-2221, 2229-2240 | Category: bug

Why we think it's a valid issue
  • Checked: the whole _flushBuffer order in packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts, the _onBeforeUnload caller, _wouldOpenRecordingWithMarkersOnly, the urlBlocked drop in onRRwebEmit, the status matchers in triggerMatching.ts, and the parking tests in lazy-sessionrecording.test.ts.
  • Found: the marker-only branch at lazy-loaded-session-recorder.ts:2212 parks at :2218-2220 and returns before ensureSamplingDecision at :2231 and before the status gate at :2240. The gate at :2244 restricts the minimum-duration park to ACTIVE, but the marker-only park has no such restriction. The premise holds.
  • Found: BUFFERING is not reachable. _onBeforeUnload clears the buffer and returns at lazy-loaded-session-recorder.ts:2446-2449, and :2466 is the only _flushBuffer(true) call site. PAUSED and DISABLED do reach the branch.
  • Found: the paused test at lazy-sessionrecording.test.ts:6926 calls startBelowMinimumDuration(), which emits an incremental snapshot. The buffer is not marker-only, so the test takes the status branch and never covers this path. The reviewer's note on the test is correct.
  • Impact: a page that loads on a blocklisted URL pauses at _pauseRecording (:1054) before any full snapshot, and onRRwebEmit then drops every event except recording paused (:1833). On unload the recorder parks that marker to sessionStorage. The next page in the same tab restores it and can ship it at the head of a recording that opens later, with no matching recording resumed. Before this change the marker died with the page.
  • Impact: the parked payload holds only Custom lifecycle markers, by the definition at :2093. No DOM, no full snapshot, and no console data from the blocked page can enter it, so there is no content leak from a blocklisted URL. The DISABLED path is narrower still, because rrweb sets _lastFullSnapshotSessionId within the first emits and the branch stops applying.
  • Priority: lowered to consider. The gap is real and reachable with a URL blocklist, and the fix is small, but the confirmed blast radius is a lifecycle debug marker that survives one same-tab navigation. That is a contract gap against the invariant the change states, not a data-loss or privacy defect.
Issue description

The marker-only branch runs before ensureSamplingDecision and before the final status check. An unload can therefore park marker-only data while the recorder is PAUSED, DISABLED, or undecided. This contradicts the invariant that only timing gates may park data.

Suggested fix

Resolve sampling and status before this branch. Park marker-only buffers only for ACTIVE or SAMPLED. Add a paused marker-only test. The current paused test includes an incremental event, so it does not enter this branch.

Comment on lines +653 to +657
// the sessionid manager's key scheme, repeated rather than read from it because this
// recorder is loaded from the CDN and can run against a core that does not expose it.
// Two apps on one origin park separately, as they already do for the window id
const persistenceName = this._instance.config.persistence_name || this._instance.config.token
this._pendingBufferStorageKey = 'ph_' + persistenceName + PENDING_BUFFER_STORAGE_SUFFIX

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Shared persistence names can send replay data to the wrong project

consider security

Why we think it's a valid issue
  • Checked: the key build at packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts:653-657, the restore guards at :2191, and where the recorder reads its ids (:644-646 in the constructor, :1218-1224 in start()).
  • Checked: what a shared persistence_name shares between two instances — parseName at packages/browser/src/posthog-persistence.ts:86-89, _groupEntryName at :969, the session id key scheme at packages/browser/src/sessionid.ts:96, and the window id key at packages/browser/src/sessionid.ts:111-112.
  • Found: the premise holds. Two instances with one persistence_name read SESSION_ID from the same persistence blob, so the session ids match. _getWindowId at packages/browser/src/sessionid.ts:178-183 falls back to the shared ph_<name>_window_id key, so the second instance adopts the window id the first instance wrote. Both guards at :2191 therefore pass, and the restore at :2198 moves one token's snapshots into the other token's buffer.
  • Found: that same configuration already merges much more than replay data across the two tokens. packages/browser/src/posthog-persistence.ts:86-89 puts the whole super-property blob under ph_<persistence_name>, and :969 puts the feature flag storage group under the same name. Two different tokens with one persistence_name already overwrite each other's distinct id, session id, and cached flags. The configuration breaks flag evaluation before it reaches replay, so it is not a state a customer can hold silently.
  • Found: the key follows the scheme the SDK already uses for _window_id_storage_key (packages/browser/src/sessionid.ts:111) and _sessionRegisteredPropertiesStorageKey (packages/browser/src/posthog-core.ts:843). The pull request repeats the existing convention rather than departing from it.
  • Impact: confirmed but narrow. The trigger needs two tokens, one shared persistence_name, and an active recorder in both instances. Both recorders sit on the same page in the same tab, so the parked payload is a capture of the page the second project records anyway. The content actually differs only when the two instances use different masking settings.
  • Priority: lowered from must_fix to consider. The tenant boundary is real, and adding config.token to the key, or to the parked value, costs almost nothing. The reachable path, however, needs a configuration that already corrupts identity and flags across the two projects, so this does not block the merge.
Issue description

The key does not always include the project token. Two instances can use different tokens with one shared persistence_name. Their session and window identifiers are also shared. Both restore checks can therefore pass. One instance can upload the other instance's DOM snapshots to the wrong PostHog project.

Suggested fix

Always include config.token in the key. Also store and validate the token in the parked value. Add a two-instance test with a shared persistence name and different tokens.

Prompt to fix with AI (copy-paste)
## Context
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L653-657

<issue_description>
The key does not always include the project token. Two instances can use different tokens with one shared `persistence_name`. Their session and window identifiers are also shared. Both restore checks can therefore pass. One instance can upload the other instance's DOM snapshots to the wrong PostHog project.
</issue_description>

<issue_validation>
- **Checked:** the key build at `packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts:653-657`, the restore guards at `:2191`, and where the recorder reads its ids (`:644-646` in the constructor, `:1218-1224` in `start()`).
- **Checked:** what a shared `persistence_name` shares between two instances — `parseName` at `packages/browser/src/posthog-persistence.ts:86-89`, `_groupEntryName` at `:969`, the session id key scheme at `packages/browser/src/sessionid.ts:96`, and the window id key at `packages/browser/src/sessionid.ts:111-112`.
- **Found:** the premise holds. Two instances with one `persistence_name` read `SESSION_ID` from the same persistence blob, so the session ids match. `_getWindowId` at `packages/browser/src/sessionid.ts:178-183` falls back to the shared `ph_<name>_window_id` key, so the second instance adopts the window id the first instance wrote. Both guards at `:2191` therefore pass, and the restore at `:2198` moves one token's snapshots into the other token's buffer.
- **Found:** that same configuration already merges much more than replay data across the two tokens. `packages/browser/src/posthog-persistence.ts:86-89` puts the whole super-property blob under `ph_<persistence_name>`, and `:969` puts the feature flag storage group under the same name. Two different tokens with one `persistence_name` already overwrite each other's distinct id, session id, and cached flags. The configuration breaks flag evaluation before it reaches replay, so it is not a state a customer can hold silently.
- **Found:** the key follows the scheme the SDK already uses for `_window_id_storage_key` (`packages/browser/src/sessionid.ts:111`) and `_sessionRegisteredPropertiesStorageKey` (`packages/browser/src/posthog-core.ts:843`). The pull request repeats the existing convention rather than departing from it.
- **Impact:** confirmed but narrow. The trigger needs two tokens, one shared `persistence_name`, and an active recorder in both instances. Both recorders sit on the same page in the same tab, so the parked payload is a capture of the page the second project records anyway. The content actually differs only when the two instances use different masking settings.
- **Priority:** lowered from `must_fix` to `consider`. The tenant boundary is real, and adding `config.token` to the key, or to the parked value, costs almost nothing. The reachable path, however, needs a configuration that already corrupts identity and flags across the two projects, so this does not block the merge.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Always include `config.token` in the key. Also store and validate the token in the parked value. Add a two-instance test with a shared persistence name and different tokens.
</potential_solution>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a real but narrow cross-project isolation gap, and I'm escalating it for a human decision rather than fixing it unattended, because it's a security/data-isolation boundary and the fix is a deliberate design choice rather than a mechanical correction.

What I confirmed against the current code: the pending-buffer storage key is derived from persistence_name || token and the restore guard only checks the session id and window id. Both of those are already shared between two instances that share a persistence_name, so both guards pass and one project's snapshots can be restored into the other's buffer — the path you describe holds. The trigger, though, requires two different tokens under one shared persistence_name with an active recorder in both, and that same configuration already collapses distinct-id, session id, and cached feature flags across the two projects, so it isn't a state a customer holds silently.

The reason this needs a human: the key deliberately mirrors the session-id manager's window-id key scheme (sessionid.ts), and the session id and window id themselves are already shared SDK-wide under a shared persistence_name. Adding config.token to the park key (or storing and validating it in the parked value) would make replay parking strictly more isolated than the identifiers the rest of the SDK shares — a departure from an established convention that a maintainer should sign off on, especially since it touches a tenant-boundary for replay data.

Decision needed: accept the narrow leak as consistent with the existing shared-persistence_name behavior, or add token scoping to the park key / parked-value guard (low cost) and diverge from the window-id convention for replay parking specifically. If the latter, a two-instance test with a shared persistence_name and different tokens should accompany it.

Comment on lines +2142 to +2177
private _canParkPendingBuffer(): boolean {
// the same three conditions the sessionid manager applies to the window id, so an opt-out
// that stops one from writing stops the other
return (
this._instance.config.persistence !== 'memory' &&
this._instance.persistence?._disabled !== true &&
sessionStore._is_supported()
)
}

/**
* Parks the buffer for the next page in this tab. A timing gate holds the buffer for a retry
* that an unloading page never runs, so without this the snapshots die with the page and the
* recording opens partway through the session.
*/
private _parkBufferForNextPage(): void {
if (
this._buffer.data.length === 0 ||
this._buffer.size === this._lastParkedBufferSize ||
this._buffer.size > RECORDING_MAX_EVENT_SIZE ||
!this._canParkPendingBuffer()
) {
return
}
this._lastParkedBufferSize = this._buffer.size
sessionStore._set(this._pendingBufferStorageKey, this._buffer)
}

private _restorePendingBuffer(): void {
if (this._pendingBufferRestored || !this._canParkPendingBuffer()) {
return
}
this._pendingBufferRestored = true

const parked = sessionStore._parse(this._pendingBufferStorageKey)
sessionStore._remove(this._pendingBufferStorageKey)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Parked snapshots survive persistence and consent cleanup

should_fix security

Why we think it's a valid issue
  • Checked: the park and restore guards at packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts:2142-2177, the recorder teardown paths stop() (:1538-1554), discard() (:1603-1613) and _teardown() (:1449-1456), the consent flow in packages/browser/src/posthog-core.ts (opt_out_capturing at :4490-4529, opt_in_capturing at :4449-4456, _sync_opt_out_with_persistence at :4351-4370, _is_persistence_disabled at :4339-4350), and the sibling sessionStorage key handling at packages/browser/src/posthog-core.ts:843-855 and :2217-2237.
  • Found: the restore path leaves the key behind. _restorePendingBuffer returns at :2171-2173 when _canParkPendingBuffer() is false, which is before the sessionStore._remove at :2177. On a page that loads with persistence disabled, the recorder therefore reads nothing and deletes nothing, and the parked DOM stays in sessionStorage for the life of the tab.
  • Found: parking is not gated on capture consent, and the recorder keeps running after a plain opt-out. opt_out_capturing() only calls consent.optInOut(false) and _sync_opt_out_with_persistence() in the non-cookieless path (packages/browser/src/posthog-core.ts:4506-4507); it never calls startIfEnabledOrStop(). getStatus passes a hard-coded isRecordingEnabled: true (packages/browser/src/extensions/replay/external/recording-strategies.ts:205-216 and :388-393), so the live recorder stays ACTIVE and keeps buffering. With the default opt_out_persistence_by_default: false, _is_persistence_disabled() returns false, so _canParkPendingBuffer() at :2145-2149 still passes and the unload park at :2167 writes DOM captured after the opt-out into sessionStorage.
  • Found: every other exit from the buffer runs through posthog.capture, which drops the event when consent is denied (packages/browser/src/posthog-core.ts:1551). sessionStore._set at :2167 is the first replay path that skips that gate.
  • Found: a later opt-in can ship that data. opt_in_capturing() calls startIfEnabledOrStop() (packages/browser/src/posthog-core.ts:4456), which starts the recorder and runs _restorePendingBuffer() at :1223. In the same tab the session id comes from the shared persistence blob and the window id from sessionStorage, so both guards at :2191 pass and the opted-out snapshots flush.
  • Found: teardown does not clear the key either. stop() and discard() clear only this._buffer; _teardown() removes listeners and timers. dispose({ discardBufferedEvents: true }) reaches discard() (packages/browser/src/extensions/replay/session-recording.ts:108-116), and that is the path used for the cookieless opt-out (packages/browser/src/posthog-core.ts:4498) and when remote config turns replay off (packages/browser/src/extensions/replay/session-recording.ts:213-215).
  • Found: the codebase already applies the hygiene the finding asks for. _persistSessionRegisteredPropKeys removes its ph_<name>_session_registered_properties key whenever persistence is memory, disabled, or unsupported (packages/browser/src/posthog-core.ts:2222-2229, and :853-855 at init), and it runs on opt-out (:4367-4368) and on reset (:3467-3468). The new key follows no equivalent rule.
  • Impact: raw DOM captured after a consent withdrawal reaches browser storage, and a re-consent in the same tab and session sends it. The residue is bounded, because sessionStorage dies with the tab and the data is not transmitted while the user stays opted out.
  • Priority: lowered from must_fix to should_fix. The consent bypass is real and new in this change, but transmission needs the full sequence of opt-out, an unload with a timing-gated buffer, and an opt-in in the same tab and session. Two small edits cover the substance: add a capture-consent check to _canParkPendingBuffer() for the park path, and move the sessionStore._remove at :2177 ahead of the early return at :2171. The wider cleanup across opt-out, reset, and dispose that the suggestion lists is more machinery than the defect needs.
Issue description

The new key is outside PostHogPersistence. Therefore, set_disabled(true) does not remove an existing parked snapshot. Restore also returns before removing the key while persistence is disabled. Raw DOM data can remain after persistence or capture is disabled. A later opt-in can restore a matching buffer.

Suggested fix

Make the eager recorder remove this key during opt-out, persistence disablement, replay disablement, reset, and disposal. Also check capture consent before parking and remove blocked entries during restore.

Prompt to fix with AI (copy-paste)
## Context
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L2142-2177

<issue_description>
The new key is outside `PostHogPersistence`. Therefore, `set_disabled(true)` does not remove an existing parked snapshot. Restore also returns before removing the key while persistence is disabled. Raw DOM data can remain after persistence or capture is disabled. A later opt-in can restore a matching buffer.
</issue_description>

<issue_validation>
- **Checked:** the park and restore guards at `packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts:2142-2177`, the recorder teardown paths `stop()` (`:1538-1554`), `discard()` (`:1603-1613`) and `_teardown()` (`:1449-1456`), the consent flow in `packages/browser/src/posthog-core.ts` (`opt_out_capturing` at `:4490-4529`, `opt_in_capturing` at `:4449-4456`, `_sync_opt_out_with_persistence` at `:4351-4370`, `_is_persistence_disabled` at `:4339-4350`), and the sibling sessionStorage key handling at `packages/browser/src/posthog-core.ts:843-855` and `:2217-2237`.
- **Found:** the restore path leaves the key behind. `_restorePendingBuffer` returns at `:2171-2173` when `_canParkPendingBuffer()` is false, which is before the `sessionStore._remove` at `:2177`. On a page that loads with persistence disabled, the recorder therefore reads nothing and deletes nothing, and the parked DOM stays in `sessionStorage` for the life of the tab.
- **Found:** parking is not gated on capture consent, and the recorder keeps running after a plain opt-out. `opt_out_capturing()` only calls `consent.optInOut(false)` and `_sync_opt_out_with_persistence()` in the non-cookieless path (`packages/browser/src/posthog-core.ts:4506-4507`); it never calls `startIfEnabledOrStop()`. `getStatus` passes a hard-coded `isRecordingEnabled: true` (`packages/browser/src/extensions/replay/external/recording-strategies.ts:205-216` and `:388-393`), so the live recorder stays `ACTIVE` and keeps buffering. With the default `opt_out_persistence_by_default: false`, `_is_persistence_disabled()` returns false, so `_canParkPendingBuffer()` at `:2145-2149` still passes and the unload park at `:2167` writes DOM captured after the opt-out into `sessionStorage`.
- **Found:** every other exit from the buffer runs through `posthog.capture`, which drops the event when consent is denied (`packages/browser/src/posthog-core.ts:1551`). `sessionStore._set` at `:2167` is the first replay path that skips that gate.
- **Found:** a later opt-in can ship that data. `opt_in_capturing()` calls `startIfEnabledOrStop()` (`packages/browser/src/posthog-core.ts:4456`), which starts the recorder and runs `_restorePendingBuffer()` at `:1223`. In the same tab the session id comes from the shared persistence blob and the window id from `sessionStorage`, so both guards at `:2191` pass and the opted-out snapshots flush.
- **Found:** teardown does not clear the key either. `stop()` and `discard()` clear only `this._buffer`; `_teardown()` removes listeners and timers. `dispose({ discardBufferedEvents: true })` reaches `discard()` (`packages/browser/src/extensions/replay/session-recording.ts:108-116`), and that is the path used for the cookieless opt-out (`packages/browser/src/posthog-core.ts:4498`) and when remote config turns replay off (`packages/browser/src/extensions/replay/session-recording.ts:213-215`).
- **Found:** the codebase already applies the hygiene the finding asks for. `_persistSessionRegisteredPropKeys` removes its `ph_<name>_session_registered_properties` key whenever persistence is memory, disabled, or unsupported (`packages/browser/src/posthog-core.ts:2222-2229`, and `:853-855` at init), and it runs on opt-out (`:4367-4368`) and on reset (`:3467-3468`). The new key follows no equivalent rule.
- **Impact:** raw DOM captured after a consent withdrawal reaches browser storage, and a re-consent in the same tab and session sends it. The residue is bounded, because `sessionStorage` dies with the tab and the data is not transmitted while the user stays opted out.
- **Priority:** lowered from `must_fix` to `should_fix`. The consent bypass is real and new in this change, but transmission needs the full sequence of opt-out, an unload with a timing-gated buffer, and an opt-in in the same tab and session. Two small edits cover the substance: add a capture-consent check to `_canParkPendingBuffer()` for the park path, and move the `sessionStore._remove` at `:2177` ahead of the early return at `:2171`. The wider cleanup across opt-out, reset, and dispose that the suggestion lists is more machinery than the defect needs.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Make the eager recorder remove this key during opt-out, persistence disablement, replay disablement, reset, and disposal. Also check capture consent before parking and remove blocked entries during restore.
</potential_solution>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is a real and newly-introduced consent gap, and because it sits on PostHog's capture-consent boundary I'm escalating it for a human decision rather than landing a consent-gate change unattended.

What I confirmed against the current code:

  1. Parking is not gated on capture consent. _parkBufferForNextPage only checks the buffer size and _canParkPendingBuffer() (persistence not memory/disabled, sessionStorage supported); it never consults consent, and the recorder has no consent references at all. After a plain opt-out the live recorder stays ACTIVE and keeps buffering, so an unload park writes DOM captured after the opt-out into sessionStorage — the first replay path that skips the posthog.capture consent gate. A later opt-in in the same tab restores and flushes it, because the session id and window id still match.

  2. The restore path leaves the key behind when it can't park. _restorePendingBuffer returns early when _canParkPendingBuffer() is false, which is before the sessionStore._remove, so a page that loads with persistence disabled reads nothing and deletes nothing — the parked DOM lingers for the life of the tab.

  3. Teardown doesn't clear the key. stop(), discard() and _teardown() clear only the in-memory buffer, so dispose({ discardBufferedEvents: true }) (the cookieless opt-out and remote-config-off path) leaves the parked buffer in storage.

The residue is bounded — sessionStorage dies with the tab and nothing is transmitted while the user stays opted out — but re-consent in the same tab and session does send it.

Decision needed from a maintainer: gate _parkBufferForNextPage on capture consent so nothing is parked while opted out, and unconditionally remove the pending-buffer key on restore (regardless of _canParkPendingBuffer()), on opt-out, and on teardown — mirroring the hygiene the SDK already applies to ph_<name>_session_registered_properties. I'm not making that change autonomously because it modifies consent-gating behavior.

Comment on lines +2218 to +2221
if (isUnloading) {
this._parkBufferForNextPage()
}
return this._buffer

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

BFCache pages keep a second copy of parked snapshots

must_fix bug

Why we think it's a valid issue
  • Checked: both park call sites and their return values in packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts, the _onPageHide / _onBeforeUnload pair, _onVisibilityChange, the restore guards, the window id handover in packages/browser/src/sessionid.ts, and a repository-wide search for pageshow and persisted.
  • Found: the park copies rather than transfers. The markers branch parks and then returns this._buffer un-drained (:2218-2221), and the minimum-duration branch parks, schedules another flush, and returns this._buffer (:2244-2247). After a park the same events exist in sessionStorage and in the live page's memory at the same time.
  • Found: the handler cannot tell a freeze from an unload. _onPageHide at :2473-2475 takes no argument and delegates straight to _onBeforeUnload, so event.persisted is never read.
  • Found: no bfcache recovery exists. A search across packages/browser/src returns no pageshow listener and no persisted check in any SDK path, so a page restored from the back-forward cache resumes with this._buffer intact and with no signal to drop the copy it left behind. _onVisibilityChange at :2485-2493 only appends a custom event; it does not touch the buffer.
  • Found: the next page really does take the copy. Page A's beforeunload clears primary_window_exists through the session manager listener at packages/browser/src/sessionid.ts:370-378, so page B's constructor adopts the stored window id at packages/browser/src/sessionid.ts:117-125, and the session id comes from the shared persistence blob. Both guards at :2191 therefore pass and page B ships those events.
  • Found: a copy left by a navigation that never completes also survives. _parkBufferForNextPage returns early when data.length === 0 (:2159) and never removes the key, so once the live page ships and clears its own buffer, the stale stored copy stays until some later page restores it.
  • Impact: the same snapshot events can ship twice inside one session — once from the page that restored the parked copy, and again from the original page when it resumes from the back-forward cache and its next flush finds the minimum duration satisfied. The park is non-idempotent whenever the source page survives. Duplicate snapshot volume is billed, and a repeated full snapshot followed by repeated mutations degrades playback. Before this change the held buffer never left page A, so it could only ship once.
Issue description

pagehide also fires when the browser places a page in the back-forward cache. rrweb explicitly continues recording after this event. This branch parks the events and returns the same live buffer. The next page can restore and ship those events. If the user returns, the cached page can ship the same events again. A canceled beforeunload creates the same stale copy.

Suggested fix

Pass the PageTransitionEvent to _onPageHide and make _parkBufferForNextPage report write success. For persisted === true, transfer ownership after the final pagehide park and rebuild the recorder on pageshow. Also remove the stored copy when the live buffer later ships or is discarded. Add a real-browser A-to-B-to-back test that asserts each snapshot ships once.

Prompt to fix with AI (copy-paste)
## Context
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L2218-2221
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L2247-2248
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L2469-2474

<issue_description>
`pagehide` also fires when the browser places a page in the back-forward cache. rrweb explicitly continues recording after this event. This branch parks the events and returns the same live buffer. The next page can restore and ship those events. If the user returns, the cached page can ship the same events again. A canceled `beforeunload` creates the same stale copy.
</issue_description>

<issue_validation>
- **Checked:** both park call sites and their return values in `packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts`, the `_onPageHide` / `_onBeforeUnload` pair, `_onVisibilityChange`, the restore guards, the window id handover in `packages/browser/src/sessionid.ts`, and a repository-wide search for `pageshow` and `persisted`.
- **Found:** the park copies rather than transfers. The markers branch parks and then returns `this._buffer` un-drained (`:2218-2221`), and the minimum-duration branch parks, schedules another flush, and returns `this._buffer` (`:2244-2247`). After a park the same events exist in `sessionStorage` and in the live page's memory at the same time.
- **Found:** the handler cannot tell a freeze from an unload. `_onPageHide` at `:2473-2475` takes no argument and delegates straight to `_onBeforeUnload`, so `event.persisted` is never read.
- **Found:** no bfcache recovery exists. A search across `packages/browser/src` returns no `pageshow` listener and no `persisted` check in any SDK path, so a page restored from the back-forward cache resumes with `this._buffer` intact and with no signal to drop the copy it left behind. `_onVisibilityChange` at `:2485-2493` only appends a custom event; it does not touch the buffer.
- **Found:** the next page really does take the copy. Page A's `beforeunload` clears `primary_window_exists` through the session manager listener at `packages/browser/src/sessionid.ts:370-378`, so page B's constructor adopts the stored window id at `packages/browser/src/sessionid.ts:117-125`, and the session id comes from the shared persistence blob. Both guards at `:2191` therefore pass and page B ships those events.
- **Found:** a copy left by a navigation that never completes also survives. `_parkBufferForNextPage` returns early when `data.length === 0` (`:2159`) and never removes the key, so once the live page ships and clears its own buffer, the stale stored copy stays until some later page restores it.
- **Impact:** the same snapshot events can ship twice inside one session — once from the page that restored the parked copy, and again from the original page when it resumes from the back-forward cache and its next flush finds the minimum duration satisfied. The park is non-idempotent whenever the source page survives. Duplicate snapshot volume is billed, and a repeated full snapshot followed by repeated mutations degrades playback. Before this change the held buffer never left page A, so it could only ship once.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Pass the `PageTransitionEvent` to `_onPageHide` and make `_parkBufferForNextPage` report write success. For `persisted === true`, transfer ownership after the final pagehide park and rebuild the recorder on `pageshow`. Also remove the stored copy when the live buffer later ships or is discarded. Add a real-browser A-to-B-to-back test that asserts each snapshot ships once.
</potential_solution>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed this is a real regression introduced by this change, and I'm escalating it for a human to design rather than fixing it unattended, because its correctness can only be proven in a real browser and the fix adds new page-lifecycle machinery.

What I verified in the current code:

  • The park copies rather than transfers. Both park branches call _parkBufferForNextPage() and then return the live this._buffer un-drained, so after a park the same events exist in both sessionStorage and the page's memory.
  • _onPageHide takes no argument and delegates straight to _onBeforeUnload, so event.persisted is never read, and there is no pageshow listener or any back-forward-cache check anywhere in the replay code.
  • So on a bfcache freeze (where pagehide fires with persisted === true and rrweb keeps recording), the next page can restore and ship the parked copy, and if the user navigates back the resumed page can ship the same events again. A canceled beforeunload similarly leaves a stale copy, because _parkBufferForNextPage never removes the key once the live page ships. Before this PR the held buffer never left the page, so it could only ship once.

Why it needs a human:

  • The fix has to be validated against real back-forward-cache freeze/restore behavior, which unit tests in jsdom can't reproduce — this needs the real-browser A→B→back test that asserts each snapshot ships exactly once.
  • The remedy is architectural, not a one-liner: read event.persisted in _onPageHide, add a pageshow listener and a recorder-rebuild/ownership-transfer path, make _parkBufferForNextPage report whether it wrote, and remove the stored copy when the live buffer later ships or is discarded.
  • There's a genuine design choice: the simplest option (don't park on persisted === true) trades the double-ship for a possible loss if the frozen page is evicted rather than restored — which is the very loss this PR sets out to fix — whereas the full transfer-and-rebuild-on-pageshow approach handles both eviction and restore. Which trade-off to take should be a maintainer's call.

Note: the 'remove the stored copy when the live buffer ships or is discarded' part overlaps the separate consent/persistence-cleanup thread on this PR, which was also escalated; whoever picks this up should handle the key-lifecycle cleanup once, covering both.

Comment on lines +1222 to +1224
// pick up snapshots a timing gate held when the previous page in this tab unloaded
this._restorePendingBuffer()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The fresh-start overflow path can erase a restored buffer

should_fix bug

Why we think it's a valid issue
  • Checked: the ordering inside start(), the _holdFlushUntilInteraction lifecycle, the held-overflow branch in _captureSnapshotBuffered, _releaseHoldAndFlush, and the size guards in _parkBufferForNextPage / _restorePendingBuffer.
  • Found: the ordering claim holds. lazy-loaded-session-recorder.ts:1191-1195 sets _holdFlushUntilInteraction and resets _heldBufferOverflowed inside if (!this.isStarted), and _restorePendingBuffer() runs later at :1223. isStarted reads !!this._stopRrweb (:1128-1130), so a fresh page always takes that block before the restore.
  • Found: the hold is on by default for the restore case. _isIdle initialises to 'unknown' (:514), so this._isIdle !== false is true on every fresh page load. The restored buffer therefore sits under the hold, and neither _flushBuffer (early return at :2206) nor _scheduleFlushBuffer (skipped at :2389) can drain it first.
  • Found: the overflow branch at :2370-2380 clears the whole buffer, restored prefix included, and sets _heldBufferOverflowed. That flag is sticky: it is only reset at :1195 (a new start()) and :1597 (_releaseHoldAndFlush), and while set the branch drops every later event.
  • Impact: confirmed regression against main, not just a missed improvement. With parked size P and a new full snapshot N where P and N are each under the cap but P + N crosses it, the new page loses its own pre-interaction data too. On main the buffer starts empty, N alone fits, and that data ships at the hold release. The parked prefix itself was already lost on main, so only the new page's loss is new.
  • Impact: the loss is bounded. _releaseHoldAndFlush clears _heldBufferOverflowed and calls _tryTakeFullSnapshot() (:1596-1599), so the recording resumes from the first interaction rather than failing for the session.
  • Priority: lowered to should_fix. The trigger needs a combined post-compression payload over RECORDING_MAX_EVENT_SIZE (~0.9 MB, :132), with both parts individually under it — _parkBufferForNextPage already refuses to park anything over the cap (:2161), and a single snapshot over the cap overflows on main too. That narrows it to heavy-DOM apps, and the recorder recovers at the hold release, so it is not a must_fix. A minimal fix (skip the restore when it leaves no headroom, or drop only the restored prefix on overflow) covers it; the suggested two-buffer merge with timestamp ordering is more machinery than the defect needs.
Issue description

start() enables _holdFlushUntilInteraction before it restores the parked buffer. The next page appends its initial Meta and FullSnapshot events. If the combined size crosses RECORDING_MAX_EVENT_SIZE, the overflow branch clears the complete buffer. A large DOM can therefore delete a valid parked prefix before the user releases the hold.

Suggested fix

Track restored data separately from the fresh-start hold. If the new snapshot crosses the cap, keep the parked prefix and start a new live buffer. Ship both buffers in timestamp order after the timing gate opens. Add a test where a FullSnapshot pushes the combined size over the cap.

Prompt to fix with AI (copy-paste)
## Context
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L1222-1224
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L2198
@packages/browser/src/extensions/replay/external/lazy-loaded-session-recorder.ts#L2368-2379

<issue_description>
`start()` enables `_holdFlushUntilInteraction` before it restores the parked buffer. The next page appends its initial Meta and FullSnapshot events. If the combined size crosses `RECORDING_MAX_EVENT_SIZE`, the overflow branch clears the complete buffer. A large DOM can therefore delete a valid parked prefix before the user releases the hold.
</issue_description>

<issue_validation>
- **Checked:** the ordering inside `start()`, the `_holdFlushUntilInteraction` lifecycle, the held-overflow branch in `_captureSnapshotBuffered`, `_releaseHoldAndFlush`, and the size guards in `_parkBufferForNextPage` / `_restorePendingBuffer`.
- **Found:** the ordering claim holds. `lazy-loaded-session-recorder.ts:1191-1195` sets `_holdFlushUntilInteraction` and resets `_heldBufferOverflowed` inside `if (!this.isStarted)`, and `_restorePendingBuffer()` runs later at `:1223`. `isStarted` reads `!!this._stopRrweb` (`:1128-1130`), so a fresh page always takes that block before the restore.
- **Found:** the hold is on by default for the restore case. `_isIdle` initialises to `'unknown'` (`:514`), so `this._isIdle !== false` is true on every fresh page load. The restored buffer therefore sits under the hold, and neither `_flushBuffer` (early return at `:2206`) nor `_scheduleFlushBuffer` (skipped at `:2389`) can drain it first.
- **Found:** the overflow branch at `:2370-2380` clears the whole buffer, restored prefix included, and sets `_heldBufferOverflowed`. That flag is sticky: it is only reset at `:1195` (a new `start()`) and `:1597` (`_releaseHoldAndFlush`), and while set the branch drops every later event.
- **Impact:** confirmed regression against `main`, not just a missed improvement. With parked size P and a new full snapshot N where P and N are each under the cap but P + N crosses it, the new page loses its own pre-interaction data too. On `main` the buffer starts empty, N alone fits, and that data ships at the hold release. The parked prefix itself was already lost on `main`, so only the new page's loss is new.
- **Impact:** the loss is bounded. `_releaseHoldAndFlush` clears `_heldBufferOverflowed` and calls `_tryTakeFullSnapshot()` (`:1596-1599`), so the recording resumes from the first interaction rather than failing for the session.
- **Priority:** lowered to `should_fix`. The trigger needs a combined post-compression payload over `RECORDING_MAX_EVENT_SIZE` (~0.9 MB, `:132`), with both parts individually under it — `_parkBufferForNextPage` already refuses to park anything over the cap (`:2161`), and a single snapshot over the cap overflows on `main` too. That narrows it to heavy-DOM apps, and the recorder recovers at the hold release, so it is not a `must_fix`. A minimal fix (skip the restore when it leaves no headroom, or drop only the restored prefix on overflow) covers it; the suggested two-buffer merge with timestamp ordering is more machinery than the defect needs.
</issue_validation>

## Task
Investigate the issue and solve it

<potential_solution>
Track restored data separately from the fresh-start hold. If the new snapshot crosses the cap, keep the parked prefix and start a new live buffer. Ship both buffers in timestamp order after the timing gate opens. Add a test where a FullSnapshot pushes the combined size over the cap.
</potential_solution>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Confirmed this is a real regression introduced by the parking change, and I'm escalating the fix for a human rather than editing this path unattended, because the correct fix needs new buffer state in a spot dense with invariants and there are several non-equivalent ways to do it.

What I verified: start() turns on the pre-interaction hold and resets the overflow flag before it restores the parked buffer, and because _isIdle starts as 'unknown', the hold is on for every fresh page load. So the restored prefix sits in the live buffer under the hold; when the new page's Meta + FullSnapshot arrive and the combined size crosses RECORDING_MAX_EVENT_SIZE, the held-overflow branch clears the entire buffer and latches _heldBufferOverflowed, dropping the rest until the hold releases. With a parked prefix P and a new snapshot N each under the cap but P+N over it, the new page loses its own pre-interaction data — on main, with an empty start, N fits and ships at the hold release. (The parked prefix itself was already lost on main, so only the new page's loss is new.)

Why it needs a human: to remove the regression without breaking the genuine case, the code has to tell apart 'overflowed because of the restored prefix' (where it should drop the prefix and keep the new epoch's data) from 'overflowed on its own' (where the sticky drop-and-stop memory bound must stay). There's no prefix-boundary tracking today, so this means adding new state (a restored-prefix marker, or a second buffer) whose lifecycle has to be kept correct across every buffer-clearing path — session-rotation re-entrancy, idle transitions, flush, and _clearBuffer. A blanket tweak to the overflow branch would quietly weaken the memory bound for the heavy-DOM case it exists to protect.

Recommended direction for whoever picks it up: the smallest safe option is to drop only the restored prefix on the first held overflow (keeping the new epoch's data and only latching the sticky flag if the new data alone still exceeds the cap), with a unit test where a FullSnapshot pushes the combined size over RECORDING_MAX_EVENT_SIZE. The heavier two-buffer timestamp-merge is more than this defect needs. The loss is bounded either way — the recorder already recovers from the first interaction via _releaseHoldAndFlush, which takes a fresh full snapshot — so this is worth doing but not urgent enough to risk an unattended change here.

The unload park guard only fired for status === ACTIVE, but a sampled-in
session reports SAMPLED, not ACTIVE. A session configured with both a
sampleRate and a minimumDurationMilliseconds therefore reached the hold
branch through _isBelowMinimumDuration and was refused parking, so its
buffer died with the page - the exact prefix loss this feature repairs.

SAMPLED is a shippable status that, like ACTIVE, can only reach this branch
via the minimum-duration gate, so park it the same way. Adds a regression
test that combines a sampled-in decision with a minimum duration and asserts
the buffer is parked (it reports SAMPLED, not ACTIVE, and fails without the
guard change).

Generated-By: PostHog Desktop
Task-Id: abffc4b6-113f-49d1-be44-b9aca8df4817
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.

0 participants