swetrix 4.2.0 → 4.3.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/README.md +49 -8
- package/dist/esnext/Lib.d.ts +73 -11
- package/dist/esnext/Lib.js +414 -11
- package/dist/esnext/Lib.js.map +1 -1
- package/dist/esnext/index.d.ts +18 -15
- package/dist/esnext/index.js +23 -15
- package/dist/esnext/index.js.map +1 -1
- package/dist/replaylibrary.min.js +173 -0
- package/dist/swetrix.cjs.js +441 -25
- package/dist/swetrix.cjs.js.map +1 -1
- package/dist/swetrix.es5.js +441 -26
- package/dist/swetrix.es5.js.map +1 -1
- package/dist/swetrix.js +1 -1
- package/dist/swetrix.js.map +1 -1
- package/jest.config.js +2 -0
- package/package.json +10 -7
- package/rollup.config.mjs +20 -0
- package/src/Lib.ts +579 -12
- package/src/index.ts +29 -14
- package/src/types/rrweb-shim.d.ts +11 -0
- package/tests/sessionReplay.test.ts +389 -0
- package/tsconfig.esnext.json +5 -1
- package/tsconfig.json +6 -1
- package/tsconfig.test.json +7 -0
package/src/index.ts
CHANGED
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
IPageViewPayload,
|
|
12
12
|
FeatureFlagsOptions,
|
|
13
13
|
ExperimentOptions,
|
|
14
|
+
SessionReplayOptions,
|
|
15
|
+
SessionReplayActions,
|
|
16
|
+
defaultSessionReplayActions,
|
|
14
17
|
} from './Lib.js'
|
|
15
18
|
|
|
16
19
|
export let LIB_INSTANCE: Lib | null = null
|
|
@@ -82,6 +85,16 @@ export function trackErrors(options?: ErrorOptions): ErrorActions {
|
|
|
82
85
|
return LIB_INSTANCE.trackErrors(options)
|
|
83
86
|
}
|
|
84
87
|
|
|
88
|
+
export function startSessionReplay(
|
|
89
|
+
options?: SessionReplayOptions,
|
|
90
|
+
): Promise<SessionReplayActions> {
|
|
91
|
+
if (!LIB_INSTANCE) {
|
|
92
|
+
return Promise.resolve(defaultSessionReplayActions)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return LIB_INSTANCE.startSessionReplay(options)
|
|
96
|
+
}
|
|
97
|
+
|
|
85
98
|
/**
|
|
86
99
|
* This function is used to manually track an error event.
|
|
87
100
|
* It's useful if you want to track specific errors in your application.
|
|
@@ -124,17 +137,16 @@ export function pageview(options: IPageviewOptions): void {
|
|
|
124
137
|
|
|
125
138
|
/**
|
|
126
139
|
* Fetches all feature flags for the project.
|
|
127
|
-
* Results are cached for 5 minutes by default.
|
|
140
|
+
* Results are cached for 5 minutes by default and share a cache with experiments.
|
|
128
141
|
*
|
|
129
|
-
* @param options - Options for evaluating feature flags (
|
|
142
|
+
* @param options - Options for evaluating feature flags (`profileId` only).
|
|
130
143
|
* @param forceRefresh - If true, bypasses the cache and fetches fresh flags.
|
|
131
144
|
* @returns A promise that resolves to a record of flag keys to boolean values.
|
|
132
145
|
*
|
|
133
146
|
* @example
|
|
134
147
|
* ```typescript
|
|
135
148
|
* const flags = await getFeatureFlags({
|
|
136
|
-
*
|
|
137
|
-
* attributes: { cc: 'US', dv: 'desktop' }
|
|
149
|
+
* profileId: 'user-123'
|
|
138
150
|
* })
|
|
139
151
|
*
|
|
140
152
|
* if (flags['new-checkout']) {
|
|
@@ -155,13 +167,13 @@ export async function getFeatureFlags(
|
|
|
155
167
|
* Gets the value of a single feature flag.
|
|
156
168
|
*
|
|
157
169
|
* @param key - The feature flag key.
|
|
158
|
-
* @param options - Options for evaluating the feature flag (
|
|
159
|
-
* @param defaultValue -
|
|
170
|
+
* @param options - Options for evaluating the feature flag (`profileId` only).
|
|
171
|
+
* @param defaultValue - Optional default value to return if the flag is not found. Defaults to false.
|
|
160
172
|
* @returns A promise that resolves to the boolean value of the flag.
|
|
161
173
|
*
|
|
162
174
|
* @example
|
|
163
175
|
* ```typescript
|
|
164
|
-
* const isEnabled = await getFeatureFlag('dark-mode', {
|
|
176
|
+
* const isEnabled = await getFeatureFlag('dark-mode', { profileId: 'user-123' })
|
|
165
177
|
*
|
|
166
178
|
* if (isEnabled) {
|
|
167
179
|
* // Enable dark mode
|
|
@@ -179,8 +191,8 @@ export async function getFeatureFlag(
|
|
|
179
191
|
}
|
|
180
192
|
|
|
181
193
|
/**
|
|
182
|
-
* Clears the cached feature flags, forcing a fresh fetch on the next call.
|
|
183
|
-
* Useful when you know the user's
|
|
194
|
+
* Clears the cached feature flags and experiments, forcing a fresh fetch on the next call.
|
|
195
|
+
* Useful when you know the user's profile has changed.
|
|
184
196
|
*/
|
|
185
197
|
export function clearFeatureFlagsCache(): void {
|
|
186
198
|
if (!LIB_INSTANCE) return
|
|
@@ -189,10 +201,10 @@ export function clearFeatureFlagsCache(): void {
|
|
|
189
201
|
}
|
|
190
202
|
|
|
191
203
|
/**
|
|
192
|
-
* Fetches
|
|
204
|
+
* Fetches variant assignments for running A/B test experiments returned by feature flag evaluation.
|
|
193
205
|
* Results are cached for 5 minutes by default (shared cache with feature flags).
|
|
194
206
|
*
|
|
195
|
-
* @param options - Options for evaluating experiments.
|
|
207
|
+
* @param options - Options for evaluating experiments (`profileId` only).
|
|
196
208
|
* @param forceRefresh - If true, bypasses the cache and fetches fresh data.
|
|
197
209
|
* @returns A promise that resolves to a record of experiment IDs to variant keys.
|
|
198
210
|
*
|
|
@@ -223,13 +235,16 @@ export async function getExperiments(
|
|
|
223
235
|
* Gets the variant key for a specific A/B test experiment.
|
|
224
236
|
*
|
|
225
237
|
* @param experimentId - The experiment ID.
|
|
226
|
-
* @param options - Options for evaluating the experiment.
|
|
227
|
-
* @param defaultVariant -
|
|
238
|
+
* @param options - Options for evaluating the experiment (`profileId` only).
|
|
239
|
+
* @param defaultVariant - Optional default variant key to return if the experiment is not found. Defaults to null.
|
|
228
240
|
* @returns A promise that resolves to the variant key assigned to this user, or defaultVariant if not found.
|
|
229
241
|
*
|
|
230
242
|
* @example
|
|
231
243
|
* ```typescript
|
|
232
|
-
* const variant = await getExperiment('checkout-redesign-experiment-id')
|
|
244
|
+
* const variant = await getExperiment('checkout-redesign-experiment-id', { profileId: 'user-123' })
|
|
245
|
+
*
|
|
246
|
+
* // Optional fallback variant:
|
|
247
|
+
* const variantWithFallback = await getExperiment('checkout-redesign-experiment-id', undefined, 'control')
|
|
233
248
|
*
|
|
234
249
|
* if (variant === 'new-checkout') {
|
|
235
250
|
* // Show new checkout flow
|
|
@@ -0,0 +1,389 @@
|
|
|
1
|
+
/// <reference types="jest" />
|
|
2
|
+
|
|
3
|
+
import { Lib } from '../src/Lib'
|
|
4
|
+
import { setLocation } from './testUtils'
|
|
5
|
+
|
|
6
|
+
const PROJECT_ID = 'test-project-id'
|
|
7
|
+
const RRWEB_URL = 'https://cdn.jsdelivr.net/npm/swetrix@latest/dist/replaylibrary.min.js'
|
|
8
|
+
const mockRrwebRecord = jest.fn()
|
|
9
|
+
|
|
10
|
+
jest.mock('rrweb', () => ({
|
|
11
|
+
record: mockRrwebRecord,
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const loadTracker = async () => {
|
|
15
|
+
jest.resetModules()
|
|
16
|
+
return import('../src/index')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const resetReplayGlobals = () => {
|
|
20
|
+
delete (window as any).rrweb
|
|
21
|
+
delete (window as any).__SWETRIX_RRWEB_LOADING__
|
|
22
|
+
document.head.innerHTML = ''
|
|
23
|
+
document.body.innerHTML = ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const usePackageRrweb = () => {
|
|
27
|
+
const stopRecording = jest.fn()
|
|
28
|
+
mockRrwebRecord.mockImplementation((recordOptions) => {
|
|
29
|
+
;(mockRrwebRecord as any).options = recordOptions
|
|
30
|
+
return stopRecording
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
recordOptions: () => (mockRrwebRecord as any).options,
|
|
35
|
+
stopRecording,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('Session replay tracking', () => {
|
|
40
|
+
let fetchMock: jest.Mock
|
|
41
|
+
|
|
42
|
+
beforeEach(() => {
|
|
43
|
+
jest.clearAllMocks()
|
|
44
|
+
mockRrwebRecord.mockReset()
|
|
45
|
+
delete (mockRrwebRecord as any).options
|
|
46
|
+
jest.useRealTimers()
|
|
47
|
+
resetReplayGlobals()
|
|
48
|
+
setLocation({ hostname: 'example.com', pathname: '/checkout' })
|
|
49
|
+
|
|
50
|
+
Object.defineProperty(navigator, 'doNotTrack', {
|
|
51
|
+
value: null,
|
|
52
|
+
writable: true,
|
|
53
|
+
configurable: true,
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
fetchMock = jest.fn().mockResolvedValue({ ok: true })
|
|
57
|
+
Object.defineProperty(globalThis, 'fetch', {
|
|
58
|
+
value: fetchMock,
|
|
59
|
+
writable: true,
|
|
60
|
+
configurable: true,
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
test('init with preloadSessionReplay loads npm rrweb but does not record', async () => {
|
|
65
|
+
const { init } = await loadTracker()
|
|
66
|
+
|
|
67
|
+
init(PROJECT_ID, { devMode: true, preloadSessionReplay: true })
|
|
68
|
+
await (window as any).__SWETRIX_RRWEB_LOADING__
|
|
69
|
+
|
|
70
|
+
expect((window as any).rrweb.record).toBe(mockRrwebRecord)
|
|
71
|
+
expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
|
|
72
|
+
expect(mockRrwebRecord).not.toHaveBeenCalled()
|
|
73
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
test('session replay script uses jsDelivr for the public swetrix.org loader', async () => {
|
|
77
|
+
const script = document.createElement('script')
|
|
78
|
+
script.src = 'https://swetrix.org/swetrix.js'
|
|
79
|
+
document.head.appendChild(script)
|
|
80
|
+
|
|
81
|
+
const { init } = await loadTracker()
|
|
82
|
+
init(PROJECT_ID, { devMode: true, preloadSessionReplay: true })
|
|
83
|
+
|
|
84
|
+
expect(
|
|
85
|
+
document.querySelector<HTMLScriptElement>(
|
|
86
|
+
`script[src="${RRWEB_URL}"]`,
|
|
87
|
+
),
|
|
88
|
+
).toBeTruthy()
|
|
89
|
+
expect(mockRrwebRecord).not.toHaveBeenCalled()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('preloadSessionReplay can load rrweb from a custom script URL', async () => {
|
|
93
|
+
const rrwebUrl = 'https://cdn.example.com/rrweb.min.js'
|
|
94
|
+
const { init } = await loadTracker()
|
|
95
|
+
|
|
96
|
+
init(PROJECT_ID, {
|
|
97
|
+
devMode: true,
|
|
98
|
+
preloadSessionReplay: { rrwebUrl },
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(
|
|
102
|
+
document.querySelector<HTMLScriptElement>(`script[src="${rrwebUrl}"]`),
|
|
103
|
+
).toBeTruthy()
|
|
104
|
+
expect(mockRrwebRecord).not.toHaveBeenCalled()
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('startSessionReplay imports npm rrweb, records events, and flushes chunks', async () => {
|
|
108
|
+
const { recordOptions } = usePackageRrweb()
|
|
109
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
110
|
+
|
|
111
|
+
init(PROJECT_ID, { devMode: true })
|
|
112
|
+
const actions = await startSessionReplay({
|
|
113
|
+
flushIntervalMs: 60_000,
|
|
114
|
+
maxEventsPerChunk: 2,
|
|
115
|
+
})
|
|
116
|
+
const options = recordOptions()
|
|
117
|
+
expect(options.maskTextSelector).toBe('*')
|
|
118
|
+
expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
|
|
119
|
+
|
|
120
|
+
const startCall = fetchMock.mock.calls.find(([url]) =>
|
|
121
|
+
String(url).includes('/session-replay/start'),
|
|
122
|
+
)
|
|
123
|
+
expect(startCall).toBeTruthy()
|
|
124
|
+
expect(JSON.parse(startCall![1].body as string)).toEqual(
|
|
125
|
+
expect.objectContaining({ privacy: 'total' }),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
options.emit({ type: 2, timestamp: 100 })
|
|
129
|
+
options.emit({ type: 3, timestamp: 200 })
|
|
130
|
+
|
|
131
|
+
await actions.flush()
|
|
132
|
+
|
|
133
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
134
|
+
'https://api.swetrix.com/log/session-replay/start',
|
|
135
|
+
expect.objectContaining({ method: 'POST' }),
|
|
136
|
+
)
|
|
137
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
138
|
+
'https://api.swetrix.com/log/session-replay/chunk',
|
|
139
|
+
expect.objectContaining({
|
|
140
|
+
method: 'POST',
|
|
141
|
+
body: expect.stringContaining('"chunkIndex":0'),
|
|
142
|
+
}),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
await actions.stop()
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('concurrent startSessionReplay calls share one recorder', async () => {
|
|
149
|
+
const { stopRecording } = usePackageRrweb()
|
|
150
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
151
|
+
|
|
152
|
+
init(PROJECT_ID, { devMode: true })
|
|
153
|
+
const [firstActions, secondActions] = await Promise.all([
|
|
154
|
+
startSessionReplay({ flushIntervalMs: 60_000 }),
|
|
155
|
+
startSessionReplay({ flushIntervalMs: 10_000 }),
|
|
156
|
+
])
|
|
157
|
+
|
|
158
|
+
expect(firstActions).toBe(secondActions)
|
|
159
|
+
expect(mockRrwebRecord).toHaveBeenCalledTimes(1)
|
|
160
|
+
expect(
|
|
161
|
+
fetchMock.mock.calls.filter(([url]) =>
|
|
162
|
+
String(url).includes('/session-replay/start'),
|
|
163
|
+
),
|
|
164
|
+
).toHaveLength(1)
|
|
165
|
+
|
|
166
|
+
await firstActions.stop()
|
|
167
|
+
expect(stopRecording).toHaveBeenCalledTimes(1)
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
test('sampleRate can skip recording before loading rrweb', async () => {
|
|
171
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
172
|
+
|
|
173
|
+
init(PROJECT_ID, { devMode: true })
|
|
174
|
+
const actions = await startSessionReplay({ sampleRate: 0 })
|
|
175
|
+
await actions.flush()
|
|
176
|
+
await actions.stop()
|
|
177
|
+
|
|
178
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
179
|
+
expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
|
|
180
|
+
expect(mockRrwebRecord).not.toHaveBeenCalled()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
test('maxDurationMs stops recording and flushes buffered events', async () => {
|
|
184
|
+
jest.useFakeTimers()
|
|
185
|
+
const { recordOptions, stopRecording } = usePackageRrweb()
|
|
186
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
187
|
+
|
|
188
|
+
init(PROJECT_ID, { devMode: true })
|
|
189
|
+
await startSessionReplay({
|
|
190
|
+
flushIntervalMs: 60_000,
|
|
191
|
+
maxDurationMs: 1000,
|
|
192
|
+
})
|
|
193
|
+
recordOptions().emit({ type: 2, timestamp: 100 })
|
|
194
|
+
|
|
195
|
+
await jest.advanceTimersByTimeAsync(1000)
|
|
196
|
+
await Promise.resolve()
|
|
197
|
+
|
|
198
|
+
expect(stopRecording).toHaveBeenCalledTimes(1)
|
|
199
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
200
|
+
'https://api.swetrix.com/log/session-replay/chunk',
|
|
201
|
+
expect.objectContaining({
|
|
202
|
+
body: expect.stringContaining('"timestamp":100'),
|
|
203
|
+
}),
|
|
204
|
+
)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('idleTimeoutMs stops after inactivity and resets on activity', async () => {
|
|
208
|
+
jest.useFakeTimers()
|
|
209
|
+
const { recordOptions, stopRecording } = usePackageRrweb()
|
|
210
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
211
|
+
|
|
212
|
+
init(PROJECT_ID, { devMode: true })
|
|
213
|
+
await startSessionReplay({
|
|
214
|
+
flushIntervalMs: 60_000,
|
|
215
|
+
idleTimeoutMs: 1000,
|
|
216
|
+
})
|
|
217
|
+
recordOptions().emit({ type: 2, timestamp: 200 })
|
|
218
|
+
|
|
219
|
+
await jest.advanceTimersByTimeAsync(900)
|
|
220
|
+
window.dispatchEvent(new Event('mousemove'))
|
|
221
|
+
await jest.advanceTimersByTimeAsync(900)
|
|
222
|
+
|
|
223
|
+
expect(stopRecording).not.toHaveBeenCalled()
|
|
224
|
+
|
|
225
|
+
await jest.advanceTimersByTimeAsync(100)
|
|
226
|
+
await Promise.resolve()
|
|
227
|
+
|
|
228
|
+
expect(stopRecording).toHaveBeenCalledTimes(1)
|
|
229
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
230
|
+
'https://api.swetrix.com/log/session-replay/chunk',
|
|
231
|
+
expect.objectContaining({
|
|
232
|
+
body: expect.stringContaining('"timestamp":200'),
|
|
233
|
+
}),
|
|
234
|
+
)
|
|
235
|
+
})
|
|
236
|
+
|
|
237
|
+
test('privacy modes map to rrweb options and keep internal emit', () => {
|
|
238
|
+
const lib = new Lib(PROJECT_ID, { devMode: true })
|
|
239
|
+
const emit = jest.fn()
|
|
240
|
+
|
|
241
|
+
const total = (lib as any).getSessionReplayRecordOptions(
|
|
242
|
+
'total',
|
|
243
|
+
{ blockSelector: '.secret', emit: jest.fn() },
|
|
244
|
+
emit,
|
|
245
|
+
)
|
|
246
|
+
expect(total.maskAllInputs).toBe(true)
|
|
247
|
+
expect(total.maskTextSelector).toBe('*')
|
|
248
|
+
expect(total.blockSelector).toContain('.secret')
|
|
249
|
+
expect(total.blockSelector).toContain('img')
|
|
250
|
+
expect(total.recordCanvas).toBe(false)
|
|
251
|
+
expect(total.inlineImages).toBe(false)
|
|
252
|
+
expect(total.emit).toBe(emit)
|
|
253
|
+
|
|
254
|
+
const normal = (lib as any).getSessionReplayRecordOptions(
|
|
255
|
+
'normal',
|
|
256
|
+
{},
|
|
257
|
+
emit,
|
|
258
|
+
)
|
|
259
|
+
expect(normal.maskAllInputs).toBe(true)
|
|
260
|
+
expect(normal.emit).toBe(emit)
|
|
261
|
+
|
|
262
|
+
const freeLove = (lib as any).getSessionReplayRecordOptions(
|
|
263
|
+
'none',
|
|
264
|
+
{ maskInputOptions: { email: false } },
|
|
265
|
+
emit,
|
|
266
|
+
)
|
|
267
|
+
expect(freeLove.maskInputOptions).toEqual({
|
|
268
|
+
email: false,
|
|
269
|
+
password: true,
|
|
270
|
+
})
|
|
271
|
+
expect(freeLove.emit).toBe(emit)
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
test('invalid privacy values fall back to total privacy', async () => {
|
|
275
|
+
const { recordOptions } = usePackageRrweb()
|
|
276
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
277
|
+
|
|
278
|
+
init(PROJECT_ID, { devMode: true })
|
|
279
|
+
const actions = await startSessionReplay({
|
|
280
|
+
privacy: 'totl' as any,
|
|
281
|
+
})
|
|
282
|
+
const startCall = fetchMock.mock.calls.find(([url]) =>
|
|
283
|
+
String(url).includes('/session-replay/start'),
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
expect(startCall).toBeTruthy()
|
|
287
|
+
expect(JSON.parse(startCall![1].body as string)).toEqual(
|
|
288
|
+
expect.objectContaining({ privacy: 'total' }),
|
|
289
|
+
)
|
|
290
|
+
expect(recordOptions().maskTextSelector).toBe('*')
|
|
291
|
+
|
|
292
|
+
await actions.stop()
|
|
293
|
+
})
|
|
294
|
+
|
|
295
|
+
test('script rrweb loader clears failed loads so startSessionReplay can retry', async () => {
|
|
296
|
+
const record = jest.fn(() => jest.fn())
|
|
297
|
+
const trackerScript = document.createElement('script')
|
|
298
|
+
trackerScript.src = 'https://example.com/swetrix.js'
|
|
299
|
+
document.head.appendChild(trackerScript)
|
|
300
|
+
const rrwebUrl = 'https://example.com/replaylibrary.min.js'
|
|
301
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
302
|
+
|
|
303
|
+
init(PROJECT_ID, { devMode: true })
|
|
304
|
+
const failedStart = startSessionReplay()
|
|
305
|
+
const failedScript = document.querySelector<HTMLScriptElement>(
|
|
306
|
+
`script[src="${rrwebUrl}"]`,
|
|
307
|
+
)
|
|
308
|
+
expect(failedScript).toBeTruthy()
|
|
309
|
+
failedScript!.dispatchEvent(new Event('error'))
|
|
310
|
+
|
|
311
|
+
await failedStart
|
|
312
|
+
expect((window as any).__SWETRIX_RRWEB_LOADING__).toBeUndefined()
|
|
313
|
+
|
|
314
|
+
const retryStart = startSessionReplay()
|
|
315
|
+
const scripts = document.querySelectorAll<HTMLScriptElement>(
|
|
316
|
+
`script[src="${rrwebUrl}"]`,
|
|
317
|
+
)
|
|
318
|
+
expect(scripts[1]).toBeTruthy()
|
|
319
|
+
expect(scripts[1]).not.toBe(failedScript)
|
|
320
|
+
;(window as any).rrweb = { record }
|
|
321
|
+
scripts[1].dispatchEvent(new Event('load'))
|
|
322
|
+
|
|
323
|
+
const actions = await retryStart
|
|
324
|
+
expect((window as any).rrweb.record).toBe(record)
|
|
325
|
+
expect(record).toHaveBeenCalled()
|
|
326
|
+
|
|
327
|
+
await actions.stop()
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
test('user rrweb emit is composed with Swetrix uploads', async () => {
|
|
331
|
+
const { recordOptions } = usePackageRrweb()
|
|
332
|
+
const userEmit = jest.fn()
|
|
333
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
334
|
+
|
|
335
|
+
init(PROJECT_ID, { devMode: true })
|
|
336
|
+
const actions = await startSessionReplay({
|
|
337
|
+
rrweb: { emit: userEmit, maskAllInputs: true },
|
|
338
|
+
})
|
|
339
|
+
const event = { type: 2, timestamp: 300 }
|
|
340
|
+
recordOptions().emit(event)
|
|
341
|
+
await actions.flush()
|
|
342
|
+
|
|
343
|
+
expect(userEmit).toHaveBeenCalledWith(event)
|
|
344
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
345
|
+
'https://api.swetrix.com/log/session-replay/chunk',
|
|
346
|
+
expect.objectContaining({
|
|
347
|
+
body: expect.stringContaining('"timestamp":300'),
|
|
348
|
+
}),
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
await actions.stop()
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
test('DNT and disabled tracking return no-op controls without uploading', async () => {
|
|
355
|
+
Object.defineProperty(navigator, 'doNotTrack', {
|
|
356
|
+
value: '1',
|
|
357
|
+
writable: true,
|
|
358
|
+
configurable: true,
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
const { init, startSessionReplay } = await loadTracker()
|
|
362
|
+
init(PROJECT_ID, { devMode: true, respectDNT: true })
|
|
363
|
+
|
|
364
|
+
const actions = await startSessionReplay()
|
|
365
|
+
await actions.flush()
|
|
366
|
+
await actions.stop()
|
|
367
|
+
|
|
368
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
369
|
+
expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
|
|
370
|
+
expect(mockRrwebRecord).not.toHaveBeenCalled()
|
|
371
|
+
|
|
372
|
+
resetReplayGlobals()
|
|
373
|
+
mockRrwebRecord.mockReset()
|
|
374
|
+
Object.defineProperty(navigator, 'doNotTrack', {
|
|
375
|
+
value: null,
|
|
376
|
+
writable: true,
|
|
377
|
+
configurable: true,
|
|
378
|
+
})
|
|
379
|
+
|
|
380
|
+
const disabledModule = await loadTracker()
|
|
381
|
+
disabledModule.init(PROJECT_ID, { devMode: true, disabled: true })
|
|
382
|
+
const disabledActions = await disabledModule.startSessionReplay()
|
|
383
|
+
await disabledActions.flush()
|
|
384
|
+
|
|
385
|
+
expect(fetchMock).not.toHaveBeenCalled()
|
|
386
|
+
expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
|
|
387
|
+
expect(mockRrwebRecord).not.toHaveBeenCalled()
|
|
388
|
+
})
|
|
389
|
+
})
|
package/tsconfig.esnext.json
CHANGED
package/tsconfig.json
CHANGED
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
"strict": true,
|
|
8
8
|
"sourceMap": true,
|
|
9
9
|
"declaration": false,
|
|
10
|
-
"allowSyntheticDefaultImports": true
|
|
10
|
+
"allowSyntheticDefaultImports": true,
|
|
11
|
+
"baseUrl": ".",
|
|
12
|
+
"paths": {
|
|
13
|
+
"rrweb": ["src/types/rrweb-shim.d.ts"]
|
|
14
|
+
},
|
|
15
|
+
"types": []
|
|
11
16
|
},
|
|
12
17
|
"include": ["src"]
|
|
13
18
|
}
|