From 9cc3d6a88b094eaf41abb807ca5ba57bf82c7862 Mon Sep 17 00:00:00 2001 From: grootbro Date: Mon, 7 Sep 2026 18:26:13 +0700 Subject: [PATCH 1/2] feat(web): opt-in session replay via @openpanel/web/replay Stop dynamically importing ./replay from the main SDK entry so bundlers no longer emit rrweb for apps that never enable replay. Consumers that want recording pass startReplayRecorder as sessionReplay.recorder; the script-tag build still loads op1-replay.js from the CDN. --- .../docs/(tracking)/session-replay.mdx | 5 +- apps/public/content/guides/session-replay.mdx | 4 + packages/sdks/web/package.json | 7 +- packages/sdks/web/src/index.ts | 155 +++++++++++------- .../web/src/resolve-replay-recorder.test.ts | 44 +++++ .../sdks/web/src/resolve-replay-recorder.ts | 48 ++++++ packages/sdks/web/tsup.config.ts | 37 +---- packages/sdks/web/vitest.config.ts | 10 ++ tooling/publish/publish.ts | 39 +++-- 9 files changed, 243 insertions(+), 106 deletions(-) create mode 100644 packages/sdks/web/src/resolve-replay-recorder.test.ts create mode 100644 packages/sdks/web/src/resolve-replay-recorder.ts create mode 100644 packages/sdks/web/vitest.config.ts diff --git a/apps/public/content/docs/(tracking)/session-replay.mdx b/apps/public/content/docs/(tracking)/session-replay.mdx index ef6e3019f..3fcf41a49 100644 --- a/apps/public/content/docs/(tracking)/session-replay.mdx +++ b/apps/public/content/docs/(tracking)/session-replay.mdx @@ -50,23 +50,26 @@ Add `sessionReplay` to your `init` call. The replay script loads automatically f ```ts title="op.ts" import { OpenPanel } from '@openpanel/web'; +import { startReplayRecorder } from '@openpanel/web/replay'; const op = new OpenPanel({ clientId: 'YOUR_CLIENT_ID', trackScreenViews: true, sessionReplay: { enabled: true, + recorder: startReplayRecorder, }, }); ``` -With the npm package, the replay module is a dynamic import code-split by your bundler. It is never included in your main bundle when session replay is disabled. +Pass `recorder` from `@openpanel/web/replay` when enabling replay in the npm package. That keeps rrweb out of apps that never import the subpath. Script-tag installs still load `op1-replay.js` from the CDN automatically. ## Options | Option | Type | Default | Description | |---|---|---|---| | `enabled` | `boolean` | `false` | Enable session replay recording | +| `recorder` | `function` | — | Required for the npm package: pass `startReplayRecorder` from `@openpanel/web/replay`. Not needed for script-tag installs | | `maskAllInputs` | `boolean` | `true` | Mask all input field values | | `maskAllText` | `boolean` | `true` | Mask all text content in the recording | | `unmaskTextSelector` | `string` | — | CSS selector for elements whose text should NOT be masked when `maskAllText` is true | diff --git a/apps/public/content/guides/session-replay.mdx b/apps/public/content/guides/session-replay.mdx index e52fade13..81252f0ec 100644 --- a/apps/public/content/guides/session-replay.mdx +++ b/apps/public/content/guides/session-replay.mdx @@ -80,6 +80,7 @@ The replay script (`op1-replay.js`) is fetched automatically alongside the main ```ts title="op.ts" import { OpenPanel } from '@openpanel/web'; +import { startReplayRecorder } from '@openpanel/web/replay'; const op = new OpenPanel({ clientId: 'YOUR_CLIENT_ID', @@ -87,10 +88,13 @@ const op = new OpenPanel({ trackOutgoingLinks: true, sessionReplay: { enabled: true, + recorder: startReplayRecorder, }, }); ``` +Import `startReplayRecorder` from `@openpanel/web/replay` and pass it as `recorder`. Apps that never import that subpath do not ship rrweb. + With the npm package, the replay module is a dynamic import resolved by your bundler. It is automatically code-split from your main bundle—if you don't enable replay, the module is never included. ### Next.js diff --git a/packages/sdks/web/package.json b/packages/sdks/web/package.json index ae2ac5c0b..8ab05d9d1 100644 --- a/packages/sdks/web/package.json +++ b/packages/sdks/web/package.json @@ -8,7 +8,12 @@ }, "scripts": { "build": "rm -rf dist && tsup", - "typecheck": "tsc --noEmit" + "typecheck": "tsc --noEmit", + "test": "vitest run" + }, + "exports": { + ".": "./index.ts", + "./replay": "./src/replay/index.ts" }, "dependencies": { "@openpanel/sdk": "workspace:1.3.1-local", diff --git a/packages/sdks/web/src/index.ts b/packages/sdks/web/src/index.ts index 3493717f1..ad17c4ce0 100644 --- a/packages/sdks/web/src/index.ts +++ b/packages/sdks/web/src/index.ts @@ -3,9 +3,18 @@ import type { TrackProperties, } from '@openpanel/sdk'; import { OpenPanel as OpenPanelBase } from '@openpanel/sdk'; +import { + resolveSessionReplayRecorder, + type SessionReplayRecorder, +} from './resolve-replay-recorder'; export type * from '@openpanel/sdk'; export { OpenPanel as OpenPanelBase } from '@openpanel/sdk'; +export type { + SessionReplayChunkPayload, + SessionReplayRecorder, + SessionReplayRecorderConfig, +} from './resolve-replay-recorder'; export type SessionReplayOptions = { enabled: boolean; @@ -31,10 +40,23 @@ export type SessionReplayOptions = { /** * URL to the replay recorder script. * Only used when loading the SDK via a script tag (IIFE / op1.js). - * When using the npm package with a bundler this option is ignored - * because the bundler resolves the replay module from the package. + * When using the npm package with a bundler this option is ignored — + * pass `recorder` from `@openpanel/web/replay` instead. */ scriptUrl?: string; + /** + * Recorder implementation. Required for the npm / bundler build when + * `enabled` is true, so rrweb is only included when you import + * `@openpanel/web/replay`. The script-tag build loads the CDN + * recorder automatically when this is omitted. + * + * @example + * import { startReplayRecorder } from '@openpanel/web/replay' + * new OpenPanel({ + * sessionReplay: { enabled: true, recorder: startReplayRecorder }, + * }) + */ + recorder?: SessionReplayRecorder; }; // Injected at build time only in the IIFE (tracker) build. @@ -42,7 +64,7 @@ export type SessionReplayOptions = { declare const __OPENPANEL_REPLAY_URL__: string | undefined; // Capture script element synchronously; currentScript is only set during sync execution. -// Used by loadReplayModule() to derive the replay script URL in the IIFE build. +// Used by loadIifeReplayRecorder() to derive the replay script URL in the IIFE build. const _replayScriptRef: HTMLScriptElement | null = typeof document !== 'undefined' ? (document.currentScript as HTMLScriptElement | null) @@ -113,75 +135,84 @@ export class OpenPanel extends OpenPanelBase { const sampleRate = this.options.sessionReplay.sampleRate ?? 1; const sampled = Math.random() < sampleRate; if (sampled) { - this.loadReplayModule().then((mod) => { - if (!mod) { - return; - } - mod.startReplayRecorder(this.options.sessionReplay!, (chunk) => { - // Replay chunks go through send() and are queued when disabled or waitForProfile - // until ready() is called (base SDK also queues replay until sessionId is set). - this.send({ - type: 'replay', - payload: { - ...chunk, - sessionId: this.sessionId, - }, - }); - }); - }); + void this.startSessionReplay(); } } } } /** - * Load the replay recorder module. - * - * - **IIFE build (op1.js)**: `__OPENPANEL_REPLAY_URL__` is replaced at - * build time with a CDN URL (e.g. `https://openpanel.dev/op1-replay.js`). - * The user can also override it via `sessionReplay.scriptUrl`. - * We load the IIFE replay script via a classic `