@sessionsight/insights 1.0.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SessionSight
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,287 @@
1
+ # @sessionsight/insights
2
+
3
+ The SessionSight SDK captures user sessions on your website — recording DOM state, clicks, scrolls, mouse movement, form interactions, and page navigations. The data powers session replay, heatmaps, conversion funnels, and form analytics in the SessionSight dashboard.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @sessionsight/insights
9
+ ```
10
+
11
+ Or via script tag:
12
+
13
+ ```html
14
+ <script src="https://cdn.sessionsight.com/sessionsight.js"></script>
15
+ ```
16
+
17
+ ## Quick Start
18
+
19
+ ### npm / ES Module
20
+
21
+ ```ts
22
+ import SessionSight from '@sessionsight/insights';
23
+
24
+ SessionSight.init({
25
+ publicApiKey: 'sessionsight_pub_...',
26
+ propertyId: 'your-property-id',
27
+ });
28
+ ```
29
+
30
+ ### Script tag
31
+
32
+ ```html
33
+ <script src="https://cdn.sessionsight.com/sessionsight.js"></script>
34
+ <script>
35
+ SessionSight.init({
36
+ publicApiKey: 'sessionsight_pub_...',
37
+ propertyId: 'your-property-id',
38
+ });
39
+ </script>
40
+ ```
41
+
42
+ That's it. Recording starts automatically.
43
+
44
+ ## API
45
+
46
+ ### `SessionSight.init(config)`
47
+
48
+ Initializes the SDK. Must be called before any other method.
49
+
50
+ ```ts
51
+ SessionSight.init({
52
+ publicApiKey: 'sessionsight_pub_...', // Required. Your public API key from the dashboard.
53
+ propertyId: 'your-property-id', // Required. Your property ID from the dashboard.
54
+ apiUrl: 'https://api.sessionsight.com', // Optional. Override the API endpoint.
55
+ autoRecord: true, // Optional. Set to false for manual recording control.
56
+ });
57
+ ```
58
+
59
+ | Option | Type | Default | Description |
60
+ |--------|------|---------|-------------|
61
+ | `publicApiKey` | `string` | **required** | Your public API key from the SessionSight dashboard (API Keys page). |
62
+ | `propertyId` | `string` | `'dev'` | Your property ID from the SessionSight dashboard (Settings → Properties). Defaults to `'dev'` for local development. |
63
+ | `apiUrl` | `string` | `https://api.sessionsight.com` | API endpoint. Override for self-hosted or local development. |
64
+ | `autoRecord` | `boolean` | `true` | When `true`, recording starts immediately. When `false`, the SDK captures into a 5-second rolling pre-buffer but doesn't send data until `record()` is called. |
65
+ | `enabled` | `boolean \| (() => boolean)` | `true` | Controls whether the SDK is active. Pass `false` to defer setup, or a getter function for reactive consent (the SDK polls the getter every second and automatically starts/stops when the value changes). |
66
+
67
+ ### `SessionSight.setEnabled(enabled)`
68
+
69
+ Manually toggles the SDK on or off after `init()` has been called. Only needed when `enabled` was passed as a static `boolean`. If you passed a getter function, the SDK manages this automatically.
70
+
71
+ ```ts
72
+ // Initialize with tracking deferred
73
+ SessionSight.init({
74
+ publicApiKey: 'sessionsight_pub_...',
75
+ propertyId: 'your-property-id',
76
+ enabled: false,
77
+ });
78
+
79
+ // Later, after user grants consent:
80
+ SessionSight.setEnabled(true);
81
+
82
+ // If user revokes consent:
83
+ SessionSight.setEnabled(false);
84
+ ```
85
+
86
+ When disabled, the SDK tears down all listeners and flushes remaining events. When re-enabled, it creates a fresh session. Calling `setEnabled(true)` when already enabled (or `false` when already disabled) is a no-op.
87
+
88
+ ### `SessionSight.record(options?)`
89
+
90
+ Starts recording and sending data to the server. Only needed when `autoRecord: false`.
91
+
92
+ ```ts
93
+ // Start recording from this moment
94
+ SessionSight.record();
95
+
96
+ // Start recording, including the last 3 seconds of activity
97
+ SessionSight.record({ preRecordSecs: 3 });
98
+ ```
99
+
100
+ | Option | Type | Default | Description |
101
+ |--------|------|---------|-------------|
102
+ | `preRecordSecs` | `number` | `0` | Include up to 5 seconds of pre-buffered events captured before `record()` was called. |
103
+
104
+ Calling `record()` when already recording is a no-op.
105
+
106
+ ### `SessionSight.identify(userId, properties?)`
107
+
108
+ Associates the current session with a user ID and optional user properties. Call this after your user logs in.
109
+
110
+ ```ts
111
+ // Identify with just a user ID
112
+ SessionSight.identify('user-123');
113
+
114
+ // Identify with user properties for segmentation
115
+ SessionSight.identify('user-123', {
116
+ plan: 'pro',
117
+ accountType: 'enterprise',
118
+ role: 'admin',
119
+ });
120
+ ```
121
+
122
+ The user ID appears in the SessionSight dashboard alongside session data. No PII is required — use any stable identifier.
123
+
124
+ Properties are stored on the visitor profile and can be used for segmentation — filtering sessions, heatmaps, and funnels by user attributes. Use categorical/descriptive values here (plan tier, account type, role). For monetary values (purchase amounts, revenue), use the [@sessionsight/goals](../goals/README.md) SDK instead.
125
+
126
+ ### `SessionSight.getVisitorId()`
127
+
128
+ Returns the current visitor ID, or `null` if the SDK hasn't been initialized.
129
+
130
+ ```ts
131
+ const visitorId = SessionSight.getVisitorId();
132
+ // Send to your backend for use with the Flags SDK
133
+ ```
134
+
135
+ The visitor ID is also available as a first-party cookie (`ss_vid`) on your domain, so your backend can read it directly from the request without any frontend code.
136
+
137
+ ### `SessionSight.stop()`
138
+
139
+ Stops recording, flushes any remaining events, and tears down all listeners.
140
+
141
+ ```ts
142
+ SessionSight.stop();
143
+ ```
144
+
145
+ After calling `stop()`, you must call `init()` again to start a new session.
146
+
147
+ ## User-Triggered Recording
148
+
149
+ For cases where you only want to record specific interactions (e.g. after a user hits an error, or during a specific flow):
150
+
151
+ ```ts
152
+ // Initialize without auto-recording
153
+ SessionSight.init({
154
+ publicApiKey: 'sessionsight_pub_...',
155
+ propertyId: 'your-property-id',
156
+ autoRecord: false,
157
+ });
158
+
159
+ // Later, when something interesting happens:
160
+ document.getElementById('checkout-btn').addEventListener('click', () => {
161
+ // Start recording, including the last 5 seconds of what the user was doing
162
+ SessionSight.record({ preRecordSecs: 5 });
163
+ });
164
+ ```
165
+
166
+ The pre-buffer captures DOM state, mouse movements, clicks, and all other events in a rolling 5-second window. When `record()` is called, those buffered events are included in the recording so you can see what led up to the trigger.
167
+
168
+ ## Form Tracking
169
+
170
+ Forms are automatically tracked. The SDK captures:
171
+ - When a user starts interacting with a form
172
+ - Which fields they focus on and how long they spend on each
173
+ - Whether each field was filled (boolean only — **no field values are captured**)
174
+ - Form submission with completion metrics
175
+
176
+ ### Naming Forms
177
+
178
+ By default, forms are identified by their page URL and position. Add a `data-ss-form` attribute for a custom name:
179
+
180
+ ```html
181
+ <form data-ss-form="Signup Form">
182
+ <!-- fields -->
183
+ </form>
184
+ ```
185
+
186
+ The name appears in the Form Analytics section of the dashboard and can also be changed from there.
187
+
188
+ ## What Gets Captured
189
+
190
+ | Data | Captured | Notes |
191
+ |------|----------|-------|
192
+ | DOM state | Yes | Full page snapshot with inline CSS for replay |
193
+ | Mouse movement | Yes | Position sampled every 500ms |
194
+ | Clicks | Yes | All clicks with coordinates; button/link clicks with labels |
195
+ | Scroll depth | Yes | Scroll position sampled every 1s |
196
+ | Page navigations | Yes | SPA navigations detected via History API |
197
+ | Form field interactions | Yes | Focus/blur timing, filled state (boolean) |
198
+ | Form field values | **No** | Never captured — only whether a field has a value |
199
+ | Passwords | **No** | Never captured |
200
+ | Cookies / localStorage | **No** | Never captured |
201
+ | Network requests | **No** | Not captured |
202
+
203
+ ## Privacy & Data Attributes
204
+
205
+ The SDK uses a three-layer privacy system:
206
+
207
+ 1. **Automatic PII detection** (always active): emails, phone numbers, credit card numbers, SSNs, IBANs, and API keys are automatically replaced with `[REDACTED]` before data leaves the browser. This cannot be disabled.
208
+
209
+ 2. **Privacy mode** (configured per-property in the dashboard):
210
+ - **Default**: all text is scrambled using a character-width-preserving algorithm. Replays show layout and behavior but text is unreadable.
211
+ - **Relaxed**: text is shown as-is, with only detected PII redacted.
212
+
213
+ 3. **HTML data attributes** (element-level overrides):
214
+
215
+ | Attribute | Effect |
216
+ |-----------|--------|
217
+ | `data-ss-mask` | Force scrambling on this element and its children, even in relaxed mode |
218
+ | `data-ss-unmask` | Allow readable text (PII still redacted) on this element and its children, even in default mode |
219
+ | `data-ss-exclude` | Block recording entirely. Element is replaced with an empty placeholder that preserves dimensions |
220
+
221
+ ```html
222
+ <nav data-ss-unmask>Readable navigation links</nav>
223
+ <div data-ss-mask>Scrambled even in relaxed mode</div>
224
+ <div data-ss-exclude>Completely hidden from recordings</div>
225
+ ```
226
+
227
+ Data attributes cascade to child elements. The nearest ancestor with `data-ss-mask` or `data-ss-unmask` wins. `data-ss-exclude` always takes priority.
228
+
229
+ You can also exclude entire pages from recording using glob patterns configured in the dashboard (e.g., `/admin/*`, `/checkout`).
230
+
231
+ ## Visitor Tracking
232
+
233
+ The SDK generates a persistent `visitorId` stored in both `localStorage` (`sessionsight_visitor_id`) and a first-party cookie (`ss_vid`). This allows tracking the same visitor across sessions on the same device/browser. The ID is a random UUID with no connection to personal information.
234
+
235
+ The cookie makes the visitor ID available to your backend on every request, which is useful for passing it to the Feature Flags SDK for segment-based targeting.
236
+
237
+ In incognito/private browsing, a session-only ID is used instead.
238
+
239
+ ## Cookie Consent
240
+
241
+ If your site requires cookie consent before tracking, pass a getter function to `enabled`. The SDK polls it every second and automatically starts or stops when the value changes:
242
+
243
+ ```ts
244
+ SessionSight.init({
245
+ publicApiKey: 'sessionsight_pub_...',
246
+ propertyId: 'your-property-id',
247
+ enabled: () => hasConsent,
248
+ });
249
+ ```
250
+
251
+ No data is collected, no cookies are set, and no listeners are attached until the getter returns `true`. If the user later revokes consent, the SDK tears down automatically.
252
+
253
+ This works with any framework's reactive state:
254
+
255
+ ```ts
256
+ // Svelte 5
257
+ let consent = $state(false);
258
+ SessionSight.init({ ..., enabled: () => consent });
259
+
260
+ // Vue
261
+ const consent = ref(false);
262
+ SessionSight.init({ ..., enabled: () => consent.value });
263
+
264
+ // React (via ref)
265
+ const consentRef = useRef(false);
266
+ SessionSight.init({ ..., enabled: () => consentRef.current });
267
+
268
+ // Solid
269
+ const [consent] = createSignal(false);
270
+ SessionSight.init({ ..., enabled: consent });
271
+ ```
272
+
273
+ Alternatively, pass a static `boolean` and use `setEnabled()` to toggle manually.
274
+
275
+ ## Build Outputs
276
+
277
+ | File | Format | Use |
278
+ |------|--------|-----|
279
+ | `dist/index.js` | ES Module | `import SessionSight from '@sessionsight/insights'` |
280
+ | `dist/index.d.ts` | TypeScript declarations | Type support |
281
+ | `dist/sessionsight.js` | IIFE (minified) | `<script>` tag, exposes `window.SessionSight` |
282
+
283
+ Build with:
284
+
285
+ ```bash
286
+ bun run build
287
+ ```
package/build.ts ADDED
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Build script for the SessionSight Insights SDK.
3
+ *
4
+ * Supports two modes:
5
+ * bun run build:worker — generate _worker-bundle.ts only (for dev)
6
+ * bun run build — full build (worker + ESM + types + IIFE)
7
+ *
8
+ * The _worker-bundle.ts file is kept around (gitignored) so the dev
9
+ * server can resolve the static import in worker-inline.ts.
10
+ */
11
+
12
+ import { $ } from 'bun';
13
+
14
+ const workerOnly = process.argv.includes('--worker-only');
15
+
16
+ // Phase 1: Build the worker to a standalone minified bundle
17
+ const workerResult = await Bun.build({
18
+ entrypoints: ['src/worker.ts'],
19
+ format: 'esm',
20
+ target: 'browser',
21
+ minify: true,
22
+ });
23
+
24
+ if (!workerResult.success) {
25
+ console.error('Failed to build worker:');
26
+ for (const log of workerResult.logs) console.error(log);
27
+ process.exit(1);
28
+ }
29
+
30
+ const workerCode = await workerResult.outputs[0]!.text();
31
+ console.log(`Worker bundle: ${(workerCode.length / 1024).toFixed(1)}KB minified`);
32
+
33
+ // Phase 2: Write the generated bundle module
34
+ const bundleSource = `// AUTO-GENERATED by build.ts. Do not edit.\nexport const WORKER_SOURCE = ${JSON.stringify(workerCode)};\n`;
35
+ await Bun.write('src/_worker-bundle.ts', bundleSource);
36
+ console.log('Generated src/_worker-bundle.ts');
37
+
38
+ if (workerOnly) {
39
+ console.log('Worker build complete (dev mode).');
40
+ process.exit(0);
41
+ }
42
+
43
+ // Phase 3: Build ESM, declarations, and IIFE
44
+ await $`bun build src/index.ts --outdir dist --format esm --target browser`;
45
+ await $`bunx tsc --emitDeclarationOnly --declaration --outDir dist`;
46
+ await $`bun build src/iife.ts --outfile dist/sessionsight.js --format iife --target browser --minify`;
47
+ console.log('Full build complete.');
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@sessionsight/insights",
3
+ "version": "1.0.0",
4
+ "description": "Session replay, heatmaps, and user analytics SDK for SessionSight.",
5
+ "author": "SessionSight",
6
+ "license": "MIT",
7
+ "homepage": "https://sessionsight.com",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/SessionSight/sdks.git",
11
+ "directory": "packages/insights"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/SessionSight/sdks/issues"
15
+ },
16
+ "keywords": [
17
+ "analytics",
18
+ "session-replay",
19
+ "heatmaps",
20
+ "user-insights",
21
+ "sessionsight"
22
+ ],
23
+ "type": "module",
24
+ "main": "./src/index.ts",
25
+ "types": "./src/index.ts",
26
+ "exports": {
27
+ ".": {
28
+ "import": "./src/index.ts",
29
+ "types": "./src/index.ts"
30
+ }
31
+ },
32
+ "publishConfig": {
33
+ "main": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "import": "./dist/index.js",
38
+ "types": "./dist/index.d.ts"
39
+ }
40
+ }
41
+ },
42
+ "scripts": {
43
+ "build": "bun run build.ts",
44
+ "build:worker": "bun run build.ts --worker-only",
45
+ "dev:setup": "bun run build.ts --worker-only"
46
+ },
47
+ "dependencies": {
48
+ "@sessionsight/sdk-shared": "workspace:*",
49
+ "rrweb": "^2.0.0-alpha.20"
50
+ },
51
+ "peerDependencies": {
52
+ "typescript": "^5"
53
+ }
54
+ }
@@ -0,0 +1,3 @@
1
+ // Type stub for the build-time generated worker bundle.
2
+ // The actual file is created by build.ts and deleted after build.
3
+ export declare const WORKER_SOURCE: string;
package/src/iife.ts ADDED
@@ -0,0 +1,3 @@
1
+ import SessionSight from './index.js';
2
+
3
+ (window as any).SessionSight = SessionSight;
package/src/index.ts ADDED
@@ -0,0 +1,240 @@
1
+ import { Recorder } from './recorder.js';
2
+ import { WorkerBridge } from './worker-bridge.js';
3
+ import type { SessionSightConfig, RecordOptions, PrivacyConfig } from './types.js';
4
+ import { normalizeApiUrl, getOrCreateVisitorId } from '@sessionsight/sdk-shared';
5
+
6
+ const PRIVACY_CACHE_KEY = 'sessionsight_privacy_config';
7
+
8
+ let recorder: Recorder | null = null;
9
+ let pendingConfig: { bridge: WorkerBridge; propertyId: string; autoRecord: boolean } | null = null;
10
+ let enabledGetter: (() => boolean) | null = null;
11
+ let enabledPollTimer: ReturnType<typeof setInterval> | null = null;
12
+ let lastEnabledValue: boolean | null = null;
13
+ let visibilityResurrectionListener: (() => void) | null = null;
14
+ /** The last privacy config received from the server, used for session resurrection. */
15
+ let lastPrivacyConfig: PrivacyConfig = { privacyMode: 'default', excludePages: [] };
16
+
17
+ /** Stored config for recreating the recorder after visibility-based session end. */
18
+ let lastInitConfig: { bridge: WorkerBridge; propertyId: string; autoRecord: boolean } | null = null;
19
+ let storedVisitorId: string = '';
20
+
21
+ /** Read cached privacy config from sessionStorage, or return defaults. */
22
+ function getCachedPrivacyConfig(propertyId: string): PrivacyConfig {
23
+ try {
24
+ const raw = sessionStorage.getItem(PRIVACY_CACHE_KEY);
25
+ if (raw) {
26
+ const parsed = JSON.parse(raw);
27
+ if (parsed.propertyId === propertyId) {
28
+ return { privacyMode: parsed.privacyMode, excludePages: parsed.excludePages };
29
+ }
30
+ }
31
+ } catch {}
32
+ return { privacyMode: 'default', excludePages: [] };
33
+ }
34
+
35
+ /** Write privacy config to sessionStorage for use on subsequent page loads. */
36
+ function cachePrivacyConfig(propertyId: string, config: PrivacyConfig): void {
37
+ try {
38
+ sessionStorage.setItem(PRIVACY_CACHE_KEY, JSON.stringify({
39
+ propertyId,
40
+ privacyMode: config.privacyMode,
41
+ excludePages: config.excludePages,
42
+ }));
43
+ } catch {}
44
+ }
45
+
46
+ function pollEnabled(): void {
47
+ if (!enabledGetter) return;
48
+ const current = enabledGetter();
49
+ if (current === lastEnabledValue) return;
50
+ lastEnabledValue = current;
51
+
52
+ if (current) {
53
+ if (recorder || !pendingConfig) return;
54
+ const { bridge, propertyId, autoRecord } = pendingConfig;
55
+ pendingConfig = null;
56
+ recorder = new Recorder(bridge, propertyId, storedVisitorId, { privacyMode: lastPrivacyConfig.privacyMode, excludePages: lastPrivacyConfig.excludePages });
57
+ recorder.start(autoRecord);
58
+ } else {
59
+ if (!recorder) return;
60
+ pendingConfig = {
61
+ bridge: recorder.getBridge(),
62
+ propertyId: recorder.getPropertyId(),
63
+ autoRecord: true,
64
+ };
65
+ recorder.stop();
66
+ recorder = null;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * SDK-level visibility listener that lives independently of the recorder.
72
+ * When a recorder ends itself due to the tab being hidden for too long,
73
+ * this listener creates a fresh recorder when the tab becomes visible again.
74
+ */
75
+ function handleVisibilityResurrection(): void {
76
+ if (document.visibilityState !== 'visible') return;
77
+ if (!recorder?.endedByVisibility) return;
78
+ if (!lastInitConfig) return;
79
+
80
+ // If using an enabled getter and it currently returns false, don't resurrect
81
+ if (enabledGetter && !enabledGetter()) return;
82
+
83
+ const { bridge, propertyId, autoRecord } = lastInitConfig;
84
+ recorder = new Recorder(bridge, propertyId, storedVisitorId, { privacyMode: lastPrivacyConfig.privacyMode, excludePages: lastPrivacyConfig.excludePages });
85
+ recorder.start(autoRecord);
86
+ }
87
+
88
+ function startPolling(): void {
89
+ if (enabledPollTimer) return;
90
+ enabledPollTimer = setInterval(pollEnabled, 1000);
91
+ }
92
+
93
+ function stopPolling(): void {
94
+ if (enabledPollTimer) {
95
+ clearInterval(enabledPollTimer);
96
+ enabledPollTimer = null;
97
+ }
98
+ }
99
+
100
+ const SessionSight = {
101
+ init(config: SessionSightConfig): void {
102
+ try {
103
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
104
+ console.warn('SessionSight: browser environment required. Skipping initialization in SSR/Node.');
105
+ return;
106
+ }
107
+
108
+ if (recorder || pendingConfig) {
109
+ console.warn('SessionSight is already initialized.');
110
+ return;
111
+ }
112
+
113
+ if (!config.publicApiKey) {
114
+ console.error('SessionSight: publicApiKey is required.');
115
+ return;
116
+ }
117
+
118
+ const propertyId = config.propertyId || 'dev';
119
+ const apiUrl = normalizeApiUrl(config.apiUrl || '');
120
+ const autoRecord = config.autoRecord !== false;
121
+
122
+ // Use cached privacy config if available, otherwise start with most restrictive defaults
123
+ const cachedConfig = getCachedPrivacyConfig(propertyId);
124
+ lastPrivacyConfig = cachedConfig;
125
+ const privacyMode = cachedConfig.privacyMode;
126
+ const excludePages = cachedConfig.excludePages;
127
+
128
+ // Generate session context
129
+ const sessionId = typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
130
+ ? crypto.randomUUID()
131
+ : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
132
+ storedVisitorId = getOrCreateVisitorId();
133
+
134
+ const bridge = new WorkerBridge(apiUrl, config.publicApiKey, propertyId, sessionId, storedVisitorId);
135
+
136
+ // Listen for server-delivered privacy config from the worker/WebSocket
137
+ bridge.onPrivacy((serverConfig) => {
138
+ lastPrivacyConfig = serverConfig;
139
+ cachePrivacyConfig(propertyId, serverConfig);
140
+ if (recorder) {
141
+ recorder.applyPrivacyConfig(serverConfig);
142
+ }
143
+ });
144
+
145
+ // Store config for session resurrection after visibility-based session end
146
+ lastInitConfig = { bridge, propertyId, autoRecord };
147
+
148
+ // Register SDK-level visibility listener (lives independently of recorder lifecycle)
149
+ if (!visibilityResurrectionListener) {
150
+ visibilityResurrectionListener = handleVisibilityResurrection;
151
+ document.addEventListener('visibilitychange', visibilityResurrectionListener);
152
+ }
153
+
154
+ const enabledOption = config.enabled;
155
+
156
+ if (typeof enabledOption === 'function') {
157
+ enabledGetter = enabledOption;
158
+ const initialValue = enabledGetter();
159
+ lastEnabledValue = initialValue;
160
+
161
+ if (initialValue) {
162
+ recorder = new Recorder(bridge, propertyId, storedVisitorId, { privacyMode, excludePages });
163
+ recorder.start(autoRecord);
164
+ } else {
165
+ pendingConfig = { bridge, propertyId, autoRecord };
166
+ }
167
+
168
+ startPolling();
169
+ } else {
170
+ const enabled = enabledOption !== false;
171
+ if (enabled) {
172
+ recorder = new Recorder(bridge, propertyId, storedVisitorId, { privacyMode, excludePages });
173
+ recorder.start(autoRecord);
174
+ } else {
175
+ pendingConfig = { bridge, propertyId, autoRecord };
176
+ }
177
+ }
178
+ } catch (e) {
179
+ console.warn('SessionSight: failed to initialize', e);
180
+ }
181
+ },
182
+
183
+ setEnabled(enabled: boolean): void {
184
+ if (enabled) {
185
+ if (recorder) return;
186
+ if (!pendingConfig) {
187
+ console.warn('SessionSight: call init() before setEnabled().');
188
+ return;
189
+ }
190
+ const { bridge, propertyId, autoRecord } = pendingConfig;
191
+ pendingConfig = null;
192
+ recorder = new Recorder(bridge, propertyId, storedVisitorId, { privacyMode: lastPrivacyConfig.privacyMode, excludePages: lastPrivacyConfig.excludePages });
193
+ recorder.start(autoRecord);
194
+ } else {
195
+ if (!recorder) return;
196
+ pendingConfig = {
197
+ bridge: recorder.getBridge(),
198
+ propertyId: recorder.getPropertyId(),
199
+ autoRecord: true,
200
+ };
201
+ recorder.stop();
202
+ recorder = null;
203
+ }
204
+ lastEnabledValue = enabled;
205
+ },
206
+
207
+ record(options?: RecordOptions): void {
208
+ if (recorder) {
209
+ recorder.beginRecording(options);
210
+ }
211
+ },
212
+
213
+ stop(): void {
214
+ stopPolling();
215
+ enabledGetter = null;
216
+ lastEnabledValue = null;
217
+ lastInitConfig = null;
218
+ if (visibilityResurrectionListener) {
219
+ document.removeEventListener('visibilitychange', visibilityResurrectionListener);
220
+ visibilityResurrectionListener = null;
221
+ }
222
+ if (recorder) {
223
+ recorder.stop();
224
+ recorder = null;
225
+ }
226
+ pendingConfig = null;
227
+ },
228
+
229
+ identify(userId: string, properties?: Record<string, string | number | boolean>): void {
230
+ if (recorder) {
231
+ recorder.identify(userId, properties);
232
+ }
233
+ },
234
+
235
+ getVisitorId(): string | null {
236
+ return recorder ? recorder.getVisitorId() : null;
237
+ },
238
+ };
239
+
240
+ export default SessionSight;