@usero/sdk 0.3.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/plugins/session-replay.cjs +1 -3
- package/dist/plugins/session-replay.cjs.map +1 -1
- package/dist/plugins/session-replay.js +1 -3
- package/dist/plugins/session-replay.js.map +1 -1
- package/dist/rrweb-IQA3KVSA.js +17065 -0
- package/dist/rrweb-IQA3KVSA.js.map +1 -0
- package/dist/rrweb-SDFFFL7O.cjs +17077 -0
- package/dist/rrweb-SDFFFL7O.cjs.map +1 -0
- package/package.json +2 -2
- package/dist/all-M6KEAHE5.cjs +0 -9110
- package/dist/all-M6KEAHE5.cjs.map +0 -1
- package/dist/all-T4CCPHSL.js +0 -9095
- package/dist/all-T4CCPHSL.js.map +0 -1
- package/dist/chunk-5BLDMQED.cjs +0 -18
- package/dist/chunk-5BLDMQED.cjs.map +0 -1
- package/dist/chunk-NSBPE2FW.js +0 -15
- package/dist/chunk-NSBPE2FW.js.map +0 -1
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
require('../chunk-5BLDMQED.cjs');
|
|
4
|
-
|
|
5
3
|
// src/plugins/session-replay.ts
|
|
6
4
|
var DEFAULT_OPTIONS = {
|
|
7
5
|
bufferSeconds: 30,
|
|
@@ -45,7 +43,7 @@ async function loadRrwebRecord() {
|
|
|
45
43
|
try {
|
|
46
44
|
const mod = await import(
|
|
47
45
|
/* webpackChunkName: "rrweb" */
|
|
48
|
-
'../
|
|
46
|
+
'../rrweb-SDFFFL7O.cjs'
|
|
49
47
|
);
|
|
50
48
|
if (mod && typeof mod === "object" && "record" in mod && typeof mod.record === "function") {
|
|
51
49
|
return mod.record;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/session-replay.ts"],"names":[],"mappings":"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","file":"session-replay.cjs","sourcesContent":["// Session replay plugin for the Usero widget.\n//\n// Lazy-loads rrweb the first time it's actually needed and keeps a rolling\n// in-memory buffer of the last `bufferSeconds` of events. On feedback\n// submit, the buffer is gzipped via the native CompressionStream API and\n// attached as `replayEvents` (base64 string) on the outgoing payload.\n//\n// Bundle hygiene is the whole point of this plugin existing as its own\n// subpath export. Importing this module pulls in the small wrapper below;\n// the heavy rrweb dependency only loads at runtime via dynamic `import()`.\n//\n// Privacy defaults err on the side of safety: all <input>/<textarea> values\n// are masked, anything tagged `data-usero-mask` is masked too, and inline\n// styles are inlined so we never leak external stylesheet URLs.\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport type { FeedbackSubmission } from '../types'\n\n// We deliberately avoid importing rrweb's types at the top level — that\n// would force consumers to install rrweb as a peer dep and would also pull\n// the type declarations into our published .d.ts files. Plugin internals\n// use a minimal local shape and rely on rrweb's runtime API only.\n\n// rrweb event sample-rate map. Keys match rrweb's `sampling` option. See\n// https://github.com/rrweb-io/rrweb/blob/master/guide.md#options for the\n// full list. We expose the two that matter most (mousemove, scroll).\nexport interface ReplaySampling {\n\t// Capture a mousemove event at most every N ms (default 50).\n\tmousemove?: number\n\t// Capture a scroll event at most every N ms (default 100).\n\tscroll?: number\n\t// Capture a media-interaction event at most every N ms.\n\tmedia?: number\n\t// Capture an input event at most every N ms, or 'last' to keep only the\n\t// final input value per element.\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Length of the rolling buffer in seconds. Older events are evicted as\n\t// new ones arrive. Default 30.\n\tbufferSeconds?: number\n\t// Wait this many ms after page load before loading rrweb and starting\n\t// to record. If the user navigates away before this elapses, rrweb is\n\t// never loaded. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once at\n\t// init via Math.random(). Sessions that lose the dice roll never load\n\t// rrweb. Default 1 (always record).\n\tsampleRate?: number\n\t// rrweb sampling rates per event type. See ReplaySampling above.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`. Pass an empty string to disable selector masking.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets into the recording so the replay viewer\n\t// renders correctly without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n}\n\ntype RrwebRecord = (opts: RrwebRecordOptions) => () => void\n\ninterface ReplayStore {\n\tevents: RrwebEvent[]\n\tstopRecording: (() => void) | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\toptions: Required<Omit<SessionReplayOptions, 'sampling'>> & { sampling: ReplaySampling }\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<SessionReplayOptions, 'sampling'>> & {\n\tsampling: ReplaySampling\n} = {\n\tbufferSeconds: 30,\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n}\n\nfunction evictOldEvents(events: RrwebEvent[], bufferSeconds: number, now: number): void {\n\tif (events.length === 0) return\n\tconst cutoff = now - bufferSeconds * 1000\n\t// Events are appended in chronological order; find the first event\n\t// inside the window with a linear scan from the head and splice once.\n\tlet dropCount = 0\n\tfor (const e of events) {\n\t\tif (e.timestamp >= cutoff) break\n\t\tdropCount++\n\t}\n\tif (dropCount > 0) events.splice(0, dropCount)\n}\n\n// Convert a Uint8Array to base64 without bringing in a dependency. Chunked\n// to avoid blowing the call stack on large buffers.\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst chunk = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(chunk))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipString(input: string): Promise<string> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Browsers without CompressionStream (very old) fall back to raw\n\t\t// base64 of the JSON. The server can detect this by sniffing the\n\t\t// gzip magic bytes; cheaper than shipping a JS gzip lib.\n\t\tconst bytes = new TextEncoder().encode(input)\n\t\treturn uint8ToBase64(bytes)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst compressed = await new Response(stream).arrayBuffer()\n\treturn uint8ToBase64(new Uint8Array(compressed))\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\t// Window may have been torn down between the import resolving and\n\t\t// us getting here. Don't start a recording into a destroyed plugin.\n\t\tif (store.cancelled || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tstore.events.push(event)\n\t\t\t\t\tevictOldEvents(store.events, store.options.bufferSeconds, event.timestamp)\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged = {\n\t\t...DEFAULT_OPTIONS,\n\t\t...options,\n\t\tsampling: { ...DEFAULT_OPTIONS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\t// Lose the dice roll? Don't even prepare to load rrweb.\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof window === 'undefined') return\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\tevents: [],\n\t\t\t\tstopRecording: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\toptions: merged,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst begin = (): void => {\n\t\t\t\tif (store.startTimer) {\n\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\tstore.startTimer = null\n\t\t\t\t}\n\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t}\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\t// Engagement gate: only load rrweb if the user is still on the\n\t\t\t\t// page after `startAfterMs`. If they navigate away first we\n\t\t\t\t// cancel and never pull the heavy module.\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstore.pageHideHandler = cancelOnExit\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(begin, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tbegin()\n\t\t\t}\n\t\t},\n\t\tasync onFeedbackSubmit(ctx): Promise<Partial<FeedbackSubmission> | undefined> {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.events.length === 0) return undefined\n\t\t\t// Snapshot the current buffer so concurrent emits can't mutate\n\t\t\t// what we're serializing.\n\t\t\tconst snapshot = store.events.slice()\n\t\t\ttry {\n\t\t\t\tconst json = JSON.stringify(snapshot)\n\t\t\t\tconst replayEvents = await gzipString(json)\n\t\t\t\treturn { replayEvents }\n\t\t\t} catch (err) {\n\t\t\t\tctx.logger.error('failed to encode replay buffer', err)\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) clearTimeout(store.startTimer)\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t}\n\t\t\tif (store.stopRecording) {\n\t\t\t\ttry {\n\t\t\t\t\tstore.stopRecording()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('rrweb stop threw', err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstore.events.length = 0\n\t\t},\n\t}\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = { evictOldEvents, gzipString, uint8ToBase64 }\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/session-replay.ts"],"names":[],"mappings":"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","file":"session-replay.cjs","sourcesContent":["// Session replay plugin for the Usero widget.\n//\n// Lazy-loads rrweb the first time it's actually needed and keeps a rolling\n// in-memory buffer of the last `bufferSeconds` of events. On feedback\n// submit, the buffer is gzipped via the native CompressionStream API and\n// attached as `replayEvents` (base64 string) on the outgoing payload.\n//\n// Bundle hygiene is the whole point of this plugin existing as its own\n// subpath export. Importing this module pulls in the small wrapper below;\n// the heavy rrweb dependency only loads at runtime via dynamic `import()`.\n//\n// Privacy defaults err on the side of safety: all <input>/<textarea> values\n// are masked, anything tagged `data-usero-mask` is masked too, and inline\n// styles are inlined so we never leak external stylesheet URLs.\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport type { FeedbackSubmission } from '../types'\n\n// We deliberately avoid importing rrweb's types at the top level — that\n// would force consumers to install rrweb as a peer dep and would also pull\n// the type declarations into our published .d.ts files. Plugin internals\n// use a minimal local shape and rely on rrweb's runtime API only.\n\n// rrweb event sample-rate map. Keys match rrweb's `sampling` option. See\n// https://github.com/rrweb-io/rrweb/blob/master/guide.md#options for the\n// full list. We expose the two that matter most (mousemove, scroll).\nexport interface ReplaySampling {\n\t// Capture a mousemove event at most every N ms (default 50).\n\tmousemove?: number\n\t// Capture a scroll event at most every N ms (default 100).\n\tscroll?: number\n\t// Capture a media-interaction event at most every N ms.\n\tmedia?: number\n\t// Capture an input event at most every N ms, or 'last' to keep only the\n\t// final input value per element.\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Length of the rolling buffer in seconds. Older events are evicted as\n\t// new ones arrive. Default 30.\n\tbufferSeconds?: number\n\t// Wait this many ms after page load before loading rrweb and starting\n\t// to record. If the user navigates away before this elapses, rrweb is\n\t// never loaded. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once at\n\t// init via Math.random(). Sessions that lose the dice roll never load\n\t// rrweb. Default 1 (always record).\n\tsampleRate?: number\n\t// rrweb sampling rates per event type. See ReplaySampling above.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`. Pass an empty string to disable selector masking.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets into the recording so the replay viewer\n\t// renders correctly without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n}\n\ntype RrwebRecord = (opts: RrwebRecordOptions) => () => void\n\ninterface ReplayStore {\n\tevents: RrwebEvent[]\n\tstopRecording: (() => void) | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\toptions: Required<Omit<SessionReplayOptions, 'sampling'>> & { sampling: ReplaySampling }\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<SessionReplayOptions, 'sampling'>> & {\n\tsampling: ReplaySampling\n} = {\n\tbufferSeconds: 30,\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n}\n\nfunction evictOldEvents(events: RrwebEvent[], bufferSeconds: number, now: number): void {\n\tif (events.length === 0) return\n\tconst cutoff = now - bufferSeconds * 1000\n\t// Events are appended in chronological order; find the first event\n\t// inside the window with a linear scan from the head and splice once.\n\tlet dropCount = 0\n\tfor (const e of events) {\n\t\tif (e.timestamp >= cutoff) break\n\t\tdropCount++\n\t}\n\tif (dropCount > 0) events.splice(0, dropCount)\n}\n\n// Convert a Uint8Array to base64 without bringing in a dependency. Chunked\n// to avoid blowing the call stack on large buffers.\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst chunk = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(chunk))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipString(input: string): Promise<string> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Browsers without CompressionStream (very old) fall back to raw\n\t\t// base64 of the JSON. The server can detect this by sniffing the\n\t\t// gzip magic bytes; cheaper than shipping a JS gzip lib.\n\t\tconst bytes = new TextEncoder().encode(input)\n\t\treturn uint8ToBase64(bytes)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst compressed = await new Response(stream).arrayBuffer()\n\treturn uint8ToBase64(new Uint8Array(compressed))\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\t// Window may have been torn down between the import resolving and\n\t\t// us getting here. Don't start a recording into a destroyed plugin.\n\t\tif (store.cancelled || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tstore.events.push(event)\n\t\t\t\t\tevictOldEvents(store.events, store.options.bufferSeconds, event.timestamp)\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged = {\n\t\t...DEFAULT_OPTIONS,\n\t\t...options,\n\t\tsampling: { ...DEFAULT_OPTIONS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\t// Lose the dice roll? Don't even prepare to load rrweb.\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof window === 'undefined') return\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\tevents: [],\n\t\t\t\tstopRecording: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\toptions: merged,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst begin = (): void => {\n\t\t\t\tif (store.startTimer) {\n\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\tstore.startTimer = null\n\t\t\t\t}\n\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t}\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\t// Engagement gate: only load rrweb if the user is still on the\n\t\t\t\t// page after `startAfterMs`. If they navigate away first we\n\t\t\t\t// cancel and never pull the heavy module.\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstore.pageHideHandler = cancelOnExit\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(begin, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tbegin()\n\t\t\t}\n\t\t},\n\t\tasync onFeedbackSubmit(ctx): Promise<Partial<FeedbackSubmission> | undefined> {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.events.length === 0) return undefined\n\t\t\t// Snapshot the current buffer so concurrent emits can't mutate\n\t\t\t// what we're serializing.\n\t\t\tconst snapshot = store.events.slice()\n\t\t\ttry {\n\t\t\t\tconst json = JSON.stringify(snapshot)\n\t\t\t\tconst replayEvents = await gzipString(json)\n\t\t\t\treturn { replayEvents }\n\t\t\t} catch (err) {\n\t\t\t\tctx.logger.error('failed to encode replay buffer', err)\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) clearTimeout(store.startTimer)\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t}\n\t\t\tif (store.stopRecording) {\n\t\t\t\ttry {\n\t\t\t\t\tstore.stopRecording()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('rrweb stop threw', err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstore.events.length = 0\n\t\t},\n\t}\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = { evictOldEvents, gzipString, uint8ToBase64 }\n"]}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import '../chunk-NSBPE2FW.js';
|
|
2
|
-
|
|
3
1
|
// src/plugins/session-replay.ts
|
|
4
2
|
var DEFAULT_OPTIONS = {
|
|
5
3
|
bufferSeconds: 30,
|
|
@@ -43,7 +41,7 @@ async function loadRrwebRecord() {
|
|
|
43
41
|
try {
|
|
44
42
|
const mod = await import(
|
|
45
43
|
/* webpackChunkName: "rrweb" */
|
|
46
|
-
'../
|
|
44
|
+
'../rrweb-IQA3KVSA.js'
|
|
47
45
|
);
|
|
48
46
|
if (mod && typeof mod === "object" && "record" in mod && typeof mod.record === "function") {
|
|
49
47
|
return mod.record;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/plugins/session-replay.ts"],"names":[],"mappings":"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","file":"session-replay.js","sourcesContent":["// Session replay plugin for the Usero widget.\n//\n// Lazy-loads rrweb the first time it's actually needed and keeps a rolling\n// in-memory buffer of the last `bufferSeconds` of events. On feedback\n// submit, the buffer is gzipped via the native CompressionStream API and\n// attached as `replayEvents` (base64 string) on the outgoing payload.\n//\n// Bundle hygiene is the whole point of this plugin existing as its own\n// subpath export. Importing this module pulls in the small wrapper below;\n// the heavy rrweb dependency only loads at runtime via dynamic `import()`.\n//\n// Privacy defaults err on the side of safety: all <input>/<textarea> values\n// are masked, anything tagged `data-usero-mask` is masked too, and inline\n// styles are inlined so we never leak external stylesheet URLs.\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport type { FeedbackSubmission } from '../types'\n\n// We deliberately avoid importing rrweb's types at the top level — that\n// would force consumers to install rrweb as a peer dep and would also pull\n// the type declarations into our published .d.ts files. Plugin internals\n// use a minimal local shape and rely on rrweb's runtime API only.\n\n// rrweb event sample-rate map. Keys match rrweb's `sampling` option. See\n// https://github.com/rrweb-io/rrweb/blob/master/guide.md#options for the\n// full list. We expose the two that matter most (mousemove, scroll).\nexport interface ReplaySampling {\n\t// Capture a mousemove event at most every N ms (default 50).\n\tmousemove?: number\n\t// Capture a scroll event at most every N ms (default 100).\n\tscroll?: number\n\t// Capture a media-interaction event at most every N ms.\n\tmedia?: number\n\t// Capture an input event at most every N ms, or 'last' to keep only the\n\t// final input value per element.\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Length of the rolling buffer in seconds. Older events are evicted as\n\t// new ones arrive. Default 30.\n\tbufferSeconds?: number\n\t// Wait this many ms after page load before loading rrweb and starting\n\t// to record. If the user navigates away before this elapses, rrweb is\n\t// never loaded. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once at\n\t// init via Math.random(). Sessions that lose the dice roll never load\n\t// rrweb. Default 1 (always record).\n\tsampleRate?: number\n\t// rrweb sampling rates per event type. See ReplaySampling above.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`. Pass an empty string to disable selector masking.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets into the recording so the replay viewer\n\t// renders correctly without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n}\n\ntype RrwebRecord = (opts: RrwebRecordOptions) => () => void\n\ninterface ReplayStore {\n\tevents: RrwebEvent[]\n\tstopRecording: (() => void) | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\toptions: Required<Omit<SessionReplayOptions, 'sampling'>> & { sampling: ReplaySampling }\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<SessionReplayOptions, 'sampling'>> & {\n\tsampling: ReplaySampling\n} = {\n\tbufferSeconds: 30,\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n}\n\nfunction evictOldEvents(events: RrwebEvent[], bufferSeconds: number, now: number): void {\n\tif (events.length === 0) return\n\tconst cutoff = now - bufferSeconds * 1000\n\t// Events are appended in chronological order; find the first event\n\t// inside the window with a linear scan from the head and splice once.\n\tlet dropCount = 0\n\tfor (const e of events) {\n\t\tif (e.timestamp >= cutoff) break\n\t\tdropCount++\n\t}\n\tif (dropCount > 0) events.splice(0, dropCount)\n}\n\n// Convert a Uint8Array to base64 without bringing in a dependency. Chunked\n// to avoid blowing the call stack on large buffers.\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst chunk = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(chunk))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipString(input: string): Promise<string> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Browsers without CompressionStream (very old) fall back to raw\n\t\t// base64 of the JSON. The server can detect this by sniffing the\n\t\t// gzip magic bytes; cheaper than shipping a JS gzip lib.\n\t\tconst bytes = new TextEncoder().encode(input)\n\t\treturn uint8ToBase64(bytes)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst compressed = await new Response(stream).arrayBuffer()\n\treturn uint8ToBase64(new Uint8Array(compressed))\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\t// Window may have been torn down between the import resolving and\n\t\t// us getting here. Don't start a recording into a destroyed plugin.\n\t\tif (store.cancelled || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tstore.events.push(event)\n\t\t\t\t\tevictOldEvents(store.events, store.options.bufferSeconds, event.timestamp)\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged = {\n\t\t...DEFAULT_OPTIONS,\n\t\t...options,\n\t\tsampling: { ...DEFAULT_OPTIONS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\t// Lose the dice roll? Don't even prepare to load rrweb.\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof window === 'undefined') return\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\tevents: [],\n\t\t\t\tstopRecording: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\toptions: merged,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst begin = (): void => {\n\t\t\t\tif (store.startTimer) {\n\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\tstore.startTimer = null\n\t\t\t\t}\n\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t}\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\t// Engagement gate: only load rrweb if the user is still on the\n\t\t\t\t// page after `startAfterMs`. If they navigate away first we\n\t\t\t\t// cancel and never pull the heavy module.\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstore.pageHideHandler = cancelOnExit\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(begin, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tbegin()\n\t\t\t}\n\t\t},\n\t\tasync onFeedbackSubmit(ctx): Promise<Partial<FeedbackSubmission> | undefined> {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.events.length === 0) return undefined\n\t\t\t// Snapshot the current buffer so concurrent emits can't mutate\n\t\t\t// what we're serializing.\n\t\t\tconst snapshot = store.events.slice()\n\t\t\ttry {\n\t\t\t\tconst json = JSON.stringify(snapshot)\n\t\t\t\tconst replayEvents = await gzipString(json)\n\t\t\t\treturn { replayEvents }\n\t\t\t} catch (err) {\n\t\t\t\tctx.logger.error('failed to encode replay buffer', err)\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) clearTimeout(store.startTimer)\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t}\n\t\t\tif (store.stopRecording) {\n\t\t\t\ttry {\n\t\t\t\t\tstore.stopRecording()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('rrweb stop threw', err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstore.events.length = 0\n\t\t},\n\t}\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = { evictOldEvents, gzipString, uint8ToBase64 }\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/plugins/session-replay.ts"],"names":[],"mappings":"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","file":"session-replay.js","sourcesContent":["// Session replay plugin for the Usero widget.\n//\n// Lazy-loads rrweb the first time it's actually needed and keeps a rolling\n// in-memory buffer of the last `bufferSeconds` of events. On feedback\n// submit, the buffer is gzipped via the native CompressionStream API and\n// attached as `replayEvents` (base64 string) on the outgoing payload.\n//\n// Bundle hygiene is the whole point of this plugin existing as its own\n// subpath export. Importing this module pulls in the small wrapper below;\n// the heavy rrweb dependency only loads at runtime via dynamic `import()`.\n//\n// Privacy defaults err on the side of safety: all <input>/<textarea> values\n// are masked, anything tagged `data-usero-mask` is masked too, and inline\n// styles are inlined so we never leak external stylesheet URLs.\n\nimport type { UseroPlugin, PluginContext } from '../plugin'\nimport type { FeedbackSubmission } from '../types'\n\n// We deliberately avoid importing rrweb's types at the top level — that\n// would force consumers to install rrweb as a peer dep and would also pull\n// the type declarations into our published .d.ts files. Plugin internals\n// use a minimal local shape and rely on rrweb's runtime API only.\n\n// rrweb event sample-rate map. Keys match rrweb's `sampling` option. See\n// https://github.com/rrweb-io/rrweb/blob/master/guide.md#options for the\n// full list. We expose the two that matter most (mousemove, scroll).\nexport interface ReplaySampling {\n\t// Capture a mousemove event at most every N ms (default 50).\n\tmousemove?: number\n\t// Capture a scroll event at most every N ms (default 100).\n\tscroll?: number\n\t// Capture a media-interaction event at most every N ms.\n\tmedia?: number\n\t// Capture an input event at most every N ms, or 'last' to keep only the\n\t// final input value per element.\n\tinput?: number | 'last'\n}\n\nexport interface SessionReplayOptions {\n\t// Length of the rolling buffer in seconds. Older events are evicted as\n\t// new ones arrive. Default 30.\n\tbufferSeconds?: number\n\t// Wait this many ms after page load before loading rrweb and starting\n\t// to record. If the user navigates away before this elapses, rrweb is\n\t// never loaded. Default 0 (start immediately).\n\tstartAfterMs?: number\n\t// Probability (0..1) that this session records at all. Decided once at\n\t// init via Math.random(). Sessions that lose the dice roll never load\n\t// rrweb. Default 1 (always record).\n\tsampleRate?: number\n\t// rrweb sampling rates per event type. See ReplaySampling above.\n\tsampling?: ReplaySampling\n\t// Mask all <input>/<textarea> values in the recording. Default true.\n\tmaskAllInputs?: boolean\n\t// CSS selector for nodes whose text content should be masked. Default\n\t// `[data-usero-mask]`. Pass an empty string to disable selector masking.\n\tmaskTextSelector?: string\n\t// Inline external stylesheets into the recording so the replay viewer\n\t// renders correctly without network access. Default true.\n\tinlineStylesheet?: boolean\n\t// Block (entirely skip) DOM subtrees matching this selector. Default\n\t// `[data-usero-block]`.\n\tblockSelector?: string\n}\n\ninterface RrwebEvent {\n\ttype: number\n\tdata: unknown\n\ttimestamp: number\n}\n\ninterface RrwebRecordOptions {\n\temit: (event: RrwebEvent) => void\n\tmaskAllInputs?: boolean\n\tmaskTextSelector?: string\n\tinlineStylesheet?: boolean\n\tblockSelector?: string\n\tsampling?: ReplaySampling\n}\n\ntype RrwebRecord = (opts: RrwebRecordOptions) => () => void\n\ninterface ReplayStore {\n\tevents: RrwebEvent[]\n\tstopRecording: (() => void) | null\n\tstartTimer: ReturnType<typeof setTimeout> | null\n\tpageHideHandler: (() => void) | null\n\tloadInProgress: boolean\n\tcancelled: boolean\n\toptions: Required<Omit<SessionReplayOptions, 'sampling'>> & { sampling: ReplaySampling }\n}\n\nconst DEFAULT_OPTIONS: Required<Omit<SessionReplayOptions, 'sampling'>> & {\n\tsampling: ReplaySampling\n} = {\n\tbufferSeconds: 30,\n\tstartAfterMs: 0,\n\tsampleRate: 1,\n\tsampling: { mousemove: 50, scroll: 100 },\n\tmaskAllInputs: true,\n\tmaskTextSelector: '[data-usero-mask]',\n\tinlineStylesheet: true,\n\tblockSelector: '[data-usero-block]',\n}\n\nfunction evictOldEvents(events: RrwebEvent[], bufferSeconds: number, now: number): void {\n\tif (events.length === 0) return\n\tconst cutoff = now - bufferSeconds * 1000\n\t// Events are appended in chronological order; find the first event\n\t// inside the window with a linear scan from the head and splice once.\n\tlet dropCount = 0\n\tfor (const e of events) {\n\t\tif (e.timestamp >= cutoff) break\n\t\tdropCount++\n\t}\n\tif (dropCount > 0) events.splice(0, dropCount)\n}\n\n// Convert a Uint8Array to base64 without bringing in a dependency. Chunked\n// to avoid blowing the call stack on large buffers.\nfunction uint8ToBase64(bytes: Uint8Array): string {\n\tlet binary = ''\n\tconst chunkSize = 0x8000\n\tfor (let i = 0; i < bytes.length; i += chunkSize) {\n\t\tconst chunk = bytes.subarray(i, i + chunkSize)\n\t\tbinary += String.fromCharCode.apply(null, Array.from(chunk))\n\t}\n\treturn typeof btoa === 'function' ? btoa(binary) : ''\n}\n\nasync function gzipString(input: string): Promise<string> {\n\tif (typeof CompressionStream === 'undefined') {\n\t\t// Browsers without CompressionStream (very old) fall back to raw\n\t\t// base64 of the JSON. The server can detect this by sniffing the\n\t\t// gzip magic bytes; cheaper than shipping a JS gzip lib.\n\t\tconst bytes = new TextEncoder().encode(input)\n\t\treturn uint8ToBase64(bytes)\n\t}\n\tconst stream = new Blob([input]).stream().pipeThrough(new CompressionStream('gzip'))\n\tconst compressed = await new Response(stream).arrayBuffer()\n\treturn uint8ToBase64(new Uint8Array(compressed))\n}\n\nasync function loadRrwebRecord(): Promise<RrwebRecord | null> {\n\ttry {\n\t\tconst mod: unknown = await import(/* webpackChunkName: \"rrweb\" */ 'rrweb')\n\t\tif (\n\t\t\tmod &&\n\t\t\ttypeof mod === 'object' &&\n\t\t\t'record' in mod &&\n\t\t\ttypeof (mod as { record: unknown }).record === 'function'\n\t\t) {\n\t\t\treturn (mod as { record: RrwebRecord }).record\n\t\t}\n\t\treturn null\n\t} catch {\n\t\treturn null\n\t}\n}\n\nfunction startRecording(store: ReplayStore, ctx: PluginContext): void {\n\tif (store.cancelled || store.stopRecording || store.loadInProgress) return\n\tstore.loadInProgress = true\n\tvoid loadRrwebRecord().then(record => {\n\t\tstore.loadInProgress = false\n\t\t// Window may have been torn down between the import resolving and\n\t\t// us getting here. Don't start a recording into a destroyed plugin.\n\t\tif (store.cancelled || !record) {\n\t\t\tif (!record) ctx.logger.warn('rrweb failed to load, replay disabled')\n\t\t\treturn\n\t\t}\n\t\ttry {\n\t\t\tconst stop = record({\n\t\t\t\temit: event => {\n\t\t\t\t\tstore.events.push(event)\n\t\t\t\t\tevictOldEvents(store.events, store.options.bufferSeconds, event.timestamp)\n\t\t\t\t},\n\t\t\t\tmaskAllInputs: store.options.maskAllInputs,\n\t\t\t\tmaskTextSelector: store.options.maskTextSelector || undefined,\n\t\t\t\tinlineStylesheet: store.options.inlineStylesheet,\n\t\t\t\tblockSelector: store.options.blockSelector,\n\t\t\t\tsampling: store.options.sampling,\n\t\t\t})\n\t\t\tstore.stopRecording = stop\n\t\t} catch (err) {\n\t\t\tctx.logger.error('rrweb record() threw', err)\n\t\t}\n\t})\n}\n\nexport function sessionReplay(options: SessionReplayOptions = {}): UseroPlugin {\n\tconst merged = {\n\t\t...DEFAULT_OPTIONS,\n\t\t...options,\n\t\tsampling: { ...DEFAULT_OPTIONS.sampling, ...(options.sampling ?? {}) },\n\t}\n\n\treturn {\n\t\tname: 'session-replay',\n\t\tonInit(ctx) {\n\t\t\t// Lose the dice roll? Don't even prepare to load rrweb.\n\t\t\tif (merged.sampleRate < 1 && Math.random() >= merged.sampleRate) {\n\t\t\t\tctx.logger.debug('skipped by sampleRate')\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif (typeof window === 'undefined') return\n\n\t\t\tconst store: ReplayStore = {\n\t\t\t\tevents: [],\n\t\t\t\tstopRecording: null,\n\t\t\t\tstartTimer: null,\n\t\t\t\tpageHideHandler: null,\n\t\t\t\tloadInProgress: false,\n\t\t\t\tcancelled: false,\n\t\t\t\toptions: merged,\n\t\t\t}\n\t\t\tctx.setStore(store)\n\n\t\t\tconst begin = (): void => {\n\t\t\t\tif (store.startTimer) {\n\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\tstore.startTimer = null\n\t\t\t\t}\n\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t}\n\t\t\t\tstartRecording(store, ctx)\n\t\t\t}\n\n\t\t\tif (merged.startAfterMs > 0) {\n\t\t\t\t// Engagement gate: only load rrweb if the user is still on the\n\t\t\t\t// page after `startAfterMs`. If they navigate away first we\n\t\t\t\t// cancel and never pull the heavy module.\n\t\t\t\tconst cancelOnExit = (): void => {\n\t\t\t\t\tstore.cancelled = true\n\t\t\t\t\tif (store.startTimer) {\n\t\t\t\t\t\tclearTimeout(store.startTimer)\n\t\t\t\t\t\tstore.startTimer = null\n\t\t\t\t\t}\n\t\t\t\t\tif (store.pageHideHandler) {\n\t\t\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t\t\t\tstore.pageHideHandler = null\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tstore.pageHideHandler = cancelOnExit\n\t\t\t\twindow.addEventListener('pagehide', cancelOnExit, { once: true })\n\t\t\t\twindow.addEventListener('beforeunload', cancelOnExit, { once: true })\n\t\t\t\tstore.startTimer = setTimeout(begin, merged.startAfterMs)\n\t\t\t} else {\n\t\t\t\tbegin()\n\t\t\t}\n\t\t},\n\t\tasync onFeedbackSubmit(ctx): Promise<Partial<FeedbackSubmission> | undefined> {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store || store.cancelled || store.events.length === 0) return undefined\n\t\t\t// Snapshot the current buffer so concurrent emits can't mutate\n\t\t\t// what we're serializing.\n\t\t\tconst snapshot = store.events.slice()\n\t\t\ttry {\n\t\t\t\tconst json = JSON.stringify(snapshot)\n\t\t\t\tconst replayEvents = await gzipString(json)\n\t\t\t\treturn { replayEvents }\n\t\t\t} catch (err) {\n\t\t\t\tctx.logger.error('failed to encode replay buffer', err)\n\t\t\t\treturn undefined\n\t\t\t}\n\t\t},\n\t\tonDestroy(ctx) {\n\t\t\tconst store = ctx.getStore<ReplayStore>()\n\t\t\tif (!store) return\n\t\t\tstore.cancelled = true\n\t\t\tif (store.startTimer) clearTimeout(store.startTimer)\n\t\t\tif (store.pageHideHandler) {\n\t\t\t\twindow.removeEventListener('pagehide', store.pageHideHandler)\n\t\t\t\twindow.removeEventListener('beforeunload', store.pageHideHandler)\n\t\t\t}\n\t\t\tif (store.stopRecording) {\n\t\t\t\ttry {\n\t\t\t\t\tstore.stopRecording()\n\t\t\t\t} catch (err) {\n\t\t\t\t\tctx.logger.warn('rrweb stop threw', err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tstore.events.length = 0\n\t\t},\n\t}\n}\n\n// Internal helper exports for testing only. Not part of the public API.\nexport const __test__ = { evictOldEvents, gzipString, uint8ToBase64 }\n"]}
|