swetrix 4.1.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.
@@ -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
+ })
@@ -0,0 +1,27 @@
1
+ interface LocationParts {
2
+ hostname?: string
3
+ pathname?: string
4
+ hash?: string
5
+ search?: string
6
+ protocol?: string
7
+ }
8
+
9
+ /**
10
+ * Sets `window.location` by reconfiguring the underlying jsdom instance via the
11
+ * global helper installed by `jsdomEnvironment.ts`. Defaults preserve a stable
12
+ * "example.com/" base so individual fields can be set in isolation.
13
+ */
14
+ export const setLocation = (parts: LocationParts = {}): void => {
15
+ const protocol = parts.protocol ?? 'http:'
16
+ const hostname = parts.hostname ?? 'example.com'
17
+ const pathname = parts.pathname ?? '/'
18
+ const search = parts.search ?? ''
19
+ const hash = parts.hash ?? ''
20
+
21
+ const setter = (globalThis as any).__setLocation as ((url: string) => void) | undefined
22
+ if (typeof setter !== 'function') {
23
+ throw new Error('__setLocation is not available - check that the custom jsdom environment is configured')
24
+ }
25
+
26
+ setter(`${protocol}//${hostname}${pathname}${search}${hash}`)
27
+ }
@@ -1,35 +1,32 @@
1
1
  import * as utils from '../src/utils'
2
+ import { setLocation } from './testUtils'
2
3
 
3
4
  describe('Utility Functions', () => {
4
5
  beforeEach(() => {
5
- Object.defineProperty(window, 'location', {
6
- value: {
7
- hostname: 'example.com',
8
- pathname: '/test-page',
9
- hash: '',
10
- search: '',
11
- },
12
- writable: true,
13
- })
6
+ setLocation({ hostname: 'example.com', pathname: '/test-page' })
14
7
 
15
8
  Object.defineProperty(document, 'referrer', {
16
9
  value: 'https://google.com',
17
10
  writable: true,
11
+ configurable: true,
18
12
  })
19
13
 
20
14
  Object.defineProperty(navigator, 'language', {
21
15
  value: 'en-US',
22
16
  writable: true,
17
+ configurable: true,
23
18
  })
24
19
 
25
20
  Object.defineProperty(navigator, 'languages', {
26
21
  value: ['en-US', 'en'],
27
22
  writable: true,
23
+ configurable: true,
28
24
  })
29
25
 
30
26
  Object.defineProperty(navigator, 'webdriver', {
31
27
  value: false,
32
28
  writable: true,
29
+ configurable: true,
33
30
  })
34
31
  })
35
32
 
@@ -38,50 +35,28 @@ describe('Utility Functions', () => {
38
35
  })
39
36
 
40
37
  test('isLocalhost should detect localhost', () => {
41
- // Arrange
42
- Object.defineProperty(window, 'location', {
43
- value: {
44
- hostname: 'localhost',
45
- },
46
- writable: true,
47
- })
48
-
49
- // Act & Assert
38
+ setLocation({ hostname: 'localhost' })
50
39
  expect(utils.isLocalhost()).toBe(true)
51
40
 
52
- // Test 127.0.0.1
53
- Object.defineProperty(window, 'location', {
54
- value: {
55
- hostname: '127.0.0.1',
56
- },
57
- writable: true,
58
- })
41
+ setLocation({ hostname: '127.0.0.1' })
59
42
  expect(utils.isLocalhost()).toBe(true)
60
43
 
61
- // Test non-localhost
62
- Object.defineProperty(window, 'location', {
63
- value: {
64
- hostname: 'example.com',
65
- },
66
- writable: true,
67
- })
44
+ setLocation({ hostname: 'example.com' })
68
45
  expect(utils.isLocalhost()).toBe(false)
69
46
  })
70
47
 
71
48
  test('isAutomated should detect webdriver', () => {
72
- // Arrange
73
49
  Object.defineProperty(navigator, 'webdriver', {
74
50
  value: true,
75
51
  writable: true,
52
+ configurable: true,
76
53
  })
77
-
78
- // Act & Assert
79
54
  expect(utils.isAutomated()).toBe(true)
80
55
 
81
- // Test non-automated
82
56
  Object.defineProperty(navigator, 'webdriver', {
83
57
  value: false,
84
58
  writable: true,
59
+ configurable: true,
85
60
  })
86
61
  expect(utils.isAutomated()).toBe(false)
87
62
  })
@@ -89,129 +64,77 @@ describe('Utility Functions', () => {
89
64
  test('getLocale should return the browser language', () => {
90
65
  expect(utils.getLocale()).toBe('en-US')
91
66
 
92
- // Test with navigator.languages undefined
93
67
  const originalLanguages = navigator.languages
94
- // Instead of deleting, set to undefined
95
68
  Object.defineProperty(navigator, 'languages', {
96
69
  value: undefined,
97
70
  writable: true,
71
+ configurable: true,
98
72
  })
99
- expect(utils.getLocale()).toBe('en-US') // Should use navigator.language as fallback
73
+ expect(utils.getLocale()).toBe('en-US')
100
74
 
101
- // Restore the original value
102
75
  Object.defineProperty(navigator, 'languages', {
103
76
  value: originalLanguages,
104
77
  writable: true,
78
+ configurable: true,
105
79
  })
106
80
  })
107
81
 
108
82
  test('getReferrer should return the document referrer', () => {
109
83
  expect(utils.getReferrer()).toBe('https://google.com')
110
84
 
111
- // Test with empty referrer
112
85
  Object.defineProperty(document, 'referrer', {
113
86
  value: '',
114
87
  writable: true,
88
+ configurable: true,
115
89
  })
116
90
  expect(utils.getReferrer()).toBeUndefined()
117
91
  })
118
92
 
119
- test('getPath should handle different URL formats', () => {
120
- // Arrange
121
- Object.defineProperty(window, 'location', {
122
- value: {
123
- pathname: '/test-page',
124
- hash: '',
125
- search: '',
126
- },
127
- writable: true,
128
- })
93
+ test('getQueryString should return the URL query string without the leading ?', () => {
94
+ setLocation({ pathname: '/landing', search: '?fbclid=AbCdEf123&utm_source=newsletter' })
95
+ expect(utils.getQueryString()).toBe('fbclid=AbCdEf123&utm_source=newsletter')
129
96
 
130
- // Act & Assert - basic path
97
+ setLocation({ pathname: '/landing' })
98
+ expect(utils.getQueryString()).toBeUndefined()
99
+
100
+ // Hash-routed SPAs sometimes carry the query string after the `#`.
101
+ setLocation({ pathname: '/', hash: '#/landing?gclid=xyz' })
102
+ expect(utils.getQueryString()).toBe('gclid=xyz')
103
+ })
104
+
105
+ test('getPath should handle different URL formats', () => {
106
+ setLocation({ pathname: '/test-page' })
131
107
  expect(utils.getPath({})).toBe('/test-page')
132
108
 
133
- // Test with hash
134
- Object.defineProperty(window, 'location', {
135
- value: {
136
- pathname: '/test-page',
137
- hash: '#section1',
138
- search: '',
139
- },
140
- writable: true,
141
- })
109
+ setLocation({ pathname: '/test-page', hash: '#section1' })
142
110
  expect(utils.getPath({ hash: true })).toBe('/test-page#section1')
143
111
 
144
- // Test with search
145
- Object.defineProperty(window, 'location', {
146
- value: {
147
- pathname: '/test-page',
148
- hash: '',
149
- search: '?param=value',
150
- },
151
- writable: true,
152
- })
112
+ setLocation({ pathname: '/test-page', search: '?param=value' })
153
113
  expect(utils.getPath({ search: true })).toBe('/test-page?param=value')
154
114
 
155
- // Test with both hash and search
156
- Object.defineProperty(window, 'location', {
157
- value: {
158
- pathname: '/test-page',
159
- hash: '#section1',
160
- search: '?param=value',
161
- },
162
- writable: true,
163
- })
115
+ setLocation({ pathname: '/test-page', hash: '#section1', search: '?param=value' })
164
116
  expect(utils.getPath({ hash: true, search: true })).toBe('/test-page#section1?param=value')
165
117
 
166
- // Test with search in hash
167
- Object.defineProperty(window, 'location', {
168
- value: {
169
- pathname: '/test-page',
170
- hash: '#section1?param=value',
171
- search: '',
172
- },
173
- writable: true,
174
- })
118
+ setLocation({ pathname: '/test-page', hash: '#section1?param=value' })
175
119
  expect(utils.getPath({ hash: true, search: true })).toBe('/test-page#section1?param=value')
176
120
  })
177
121
 
178
122
  test('getUTM* functions should extract UTM parameters', () => {
179
- // Arrange
180
- Object.defineProperty(window, 'location', {
181
- value: {
182
- pathname: '/landing',
183
- hash: '',
184
- search: '?utm_source=google&utm_medium=cpc&utm_campaign=summer&utm_term=analytics&utm_content=ad1',
185
- },
186
- writable: true,
123
+ setLocation({
124
+ pathname: '/landing',
125
+ search: '?utm_source=google&utm_medium=cpc&utm_campaign=summer&utm_term=analytics&utm_content=ad1',
187
126
  })
188
127
 
189
- // Act & Assert
190
128
  expect(utils.getUTMSource()).toBe('google')
191
129
  expect(utils.getUTMMedium()).toBe('cpc')
192
130
  expect(utils.getUTMCampaign()).toBe('summer')
193
131
  expect(utils.getUTMTerm()).toBe('analytics')
194
132
  expect(utils.getUTMContent()).toBe('ad1')
195
133
 
196
- // Test with 'ref' and 'source' parameters as well
197
- Object.defineProperty(window, 'location', {
198
- value: {
199
- pathname: '/landing',
200
- hash: '',
201
- search: '?ref=twitter',
202
- },
203
- writable: true,
204
- })
134
+ setLocation({ pathname: '/landing', search: '?ref=twitter' })
205
135
  expect(utils.getUTMSource()).toBe('twitter')
206
136
 
207
- Object.defineProperty(window, 'location', {
208
- value: {
209
- pathname: '/landing',
210
- hash: '',
211
- search: '?source=newsletter',
212
- },
213
- writable: true,
214
- })
137
+ setLocation({ pathname: '/landing', search: '?source=newsletter' })
215
138
  expect(utils.getUTMSource()).toBe('newsletter')
216
139
  })
217
140
  })
@@ -8,7 +8,11 @@
8
8
  "sourceMap": true,
9
9
  "declaration": true,
10
10
  "outDir": "dist/esnext",
11
- "typeRoots": ["node_modules/@types"]
11
+ "baseUrl": ".",
12
+ "paths": {
13
+ "rrweb": ["src/types/rrweb-shim.d.ts"]
14
+ },
15
+ "types": []
12
16
  },
13
17
  "include": ["src"]
14
18
  }
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
  }