swetrix 4.2.0 → 4.4.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,600 @@
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('uses backend replay id and chunk index across page loads without browser storage', async () => {
171
+ const replayId = 'server-replay-id'
172
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem')
173
+ const getItemSpy = jest.spyOn(Storage.prototype, 'getItem')
174
+ let startCount = 0
175
+
176
+ fetchMock.mockImplementation((url) => {
177
+ if (String(url).includes('/session-replay/start')) {
178
+ const nextChunkIndex = startCount === 0 ? 0 : 1200
179
+ startCount += 1
180
+
181
+ return Promise.resolve({
182
+ ok: true,
183
+ json: async () => ({ replayId, nextChunkIndex }),
184
+ })
185
+ }
186
+
187
+ return Promise.resolve({ ok: true })
188
+ })
189
+
190
+ const { recordOptions } = usePackageRrweb()
191
+ const { init, startSessionReplay } = await loadTracker()
192
+
193
+ init(PROJECT_ID, { devMode: true })
194
+ const firstActions = await startSessionReplay({ flushIntervalMs: 60_000 })
195
+
196
+ recordOptions().emit({ type: 2, timestamp: 100 })
197
+ await firstActions.flush()
198
+
199
+ const firstChunkCall = fetchMock.mock.calls.find(([url]) =>
200
+ String(url).includes('/session-replay/chunk'),
201
+ )
202
+ const firstChunkBody = JSON.parse(firstChunkCall![1].body as string)
203
+ expect(firstChunkBody).toEqual(
204
+ expect.objectContaining({
205
+ replayId,
206
+ chunkIndex: 0,
207
+ }),
208
+ )
209
+
210
+ const secondModule = await loadTracker()
211
+ secondModule.init(PROJECT_ID, { devMode: true })
212
+ const secondActions = await secondModule.startSessionReplay({
213
+ flushIntervalMs: 60_000,
214
+ })
215
+ const startCalls = fetchMock.mock.calls.filter(([url]) =>
216
+ String(url).includes('/session-replay/start'),
217
+ )
218
+ expect(startCalls).toHaveLength(2)
219
+
220
+ recordOptions().emit({ type: 2, timestamp: 200 })
221
+ await secondActions.flush()
222
+
223
+ const chunkCalls = fetchMock.mock.calls.filter(([url]) =>
224
+ String(url).includes('/session-replay/chunk'),
225
+ )
226
+ const secondChunkBody = JSON.parse(chunkCalls[1][1].body as string)
227
+ expect(secondChunkBody).toEqual(
228
+ expect.objectContaining({
229
+ replayId,
230
+ chunkIndex: 1200,
231
+ }),
232
+ )
233
+ expect(setItemSpy).not.toHaveBeenCalled()
234
+ expect(getItemSpy).not.toHaveBeenCalled()
235
+
236
+ await secondActions.stop()
237
+ await firstActions.stop()
238
+ setItemSpy.mockRestore()
239
+ getItemSpy.mockRestore()
240
+ })
241
+
242
+ test('sampleRate can skip recording before loading rrweb', async () => {
243
+ const { init, startSessionReplay } = await loadTracker()
244
+
245
+ init(PROJECT_ID, { devMode: true })
246
+ const actions = await startSessionReplay({ sampleRate: 0 })
247
+ await actions.flush()
248
+ await actions.stop()
249
+
250
+ expect(fetchMock).not.toHaveBeenCalled()
251
+ expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
252
+ expect(mockRrwebRecord).not.toHaveBeenCalled()
253
+ })
254
+
255
+ test('maxDurationMs stops recording and flushes buffered events', async () => {
256
+ jest.useFakeTimers()
257
+ const { recordOptions, stopRecording } = usePackageRrweb()
258
+ const { init, startSessionReplay } = await loadTracker()
259
+
260
+ init(PROJECT_ID, { devMode: true })
261
+ await startSessionReplay({
262
+ flushIntervalMs: 60_000,
263
+ maxDurationMs: 1000,
264
+ })
265
+ recordOptions().emit({ type: 2, timestamp: 100 })
266
+
267
+ await jest.advanceTimersByTimeAsync(1000)
268
+ await Promise.resolve()
269
+
270
+ expect(stopRecording).toHaveBeenCalledTimes(1)
271
+ expect(fetchMock).toHaveBeenCalledWith(
272
+ 'https://api.swetrix.com/log/session-replay/chunk',
273
+ expect.objectContaining({
274
+ body: expect.stringContaining('"timestamp":100'),
275
+ }),
276
+ )
277
+ })
278
+
279
+ test('idleTimeoutMs stops after inactivity and resets on activity', async () => {
280
+ jest.useFakeTimers()
281
+ const { recordOptions, stopRecording } = usePackageRrweb()
282
+ const { init, startSessionReplay } = await loadTracker()
283
+
284
+ init(PROJECT_ID, { devMode: true })
285
+ await startSessionReplay({
286
+ flushIntervalMs: 60_000,
287
+ idleTimeoutMs: 1000,
288
+ })
289
+ recordOptions().emit({ type: 2, timestamp: 200 })
290
+
291
+ await jest.advanceTimersByTimeAsync(900)
292
+ window.dispatchEvent(new Event('mousemove'))
293
+ await jest.advanceTimersByTimeAsync(900)
294
+
295
+ expect(stopRecording).not.toHaveBeenCalled()
296
+
297
+ await jest.advanceTimersByTimeAsync(100)
298
+ await Promise.resolve()
299
+
300
+ expect(stopRecording).toHaveBeenCalledTimes(1)
301
+ expect(fetchMock).toHaveBeenCalledWith(
302
+ 'https://api.swetrix.com/log/session-replay/chunk',
303
+ expect.objectContaining({
304
+ body: expect.stringContaining('"timestamp":200'),
305
+ }),
306
+ )
307
+ })
308
+
309
+ test('privacy modes map to rrweb options and keep internal emit', () => {
310
+ const lib = new Lib(PROJECT_ID, { devMode: true })
311
+ const emit = jest.fn()
312
+
313
+ const total = (lib as any).getSessionReplayRecordOptions(
314
+ 'total',
315
+ { blockSelector: '.secret', emit: jest.fn() },
316
+ emit,
317
+ false,
318
+ )
319
+ expect(total.maskAllInputs).toBe(true)
320
+ expect(total.maskTextSelector).toBe('*')
321
+ expect(total.blockSelector).toContain('.secret')
322
+ expect(total.blockSelector).toContain('iframe')
323
+ expect(total.blockSelector).toContain('img')
324
+ expect(total.recordCanvas).toBe(false)
325
+ expect(total.recordCrossOriginIframes).toBe(false)
326
+ expect(total.collectFonts).toBe(false)
327
+ expect(total.inlineImages).toBe(false)
328
+ expect(total.sampling).toEqual({
329
+ mousemove: 50,
330
+ scroll: 150,
331
+ input: 'last',
332
+ })
333
+ expect(total.slimDOMOptions).toEqual(
334
+ expect.objectContaining({
335
+ script: true,
336
+ comment: true,
337
+ headMetaSocial: true,
338
+ }),
339
+ )
340
+ expect(total.emit).toBe(emit)
341
+
342
+ const normal = (lib as any).getSessionReplayRecordOptions(
343
+ 'normal',
344
+ {},
345
+ emit,
346
+ false,
347
+ )
348
+ expect(normal.maskAllInputs).toBe(true)
349
+ expect(normal.blockSelector).toBe('iframe')
350
+ expect(normal.emit).toBe(emit)
351
+
352
+ const freeLove = (lib as any).getSessionReplayRecordOptions(
353
+ 'none',
354
+ { maskInputOptions: { email: false } },
355
+ emit,
356
+ false,
357
+ )
358
+ expect(freeLove.blockSelector).toBe('iframe')
359
+ expect(freeLove.maskInputOptions).toEqual({
360
+ email: false,
361
+ password: true,
362
+ })
363
+ expect(freeLove.emit).toBe(emit)
364
+ })
365
+
366
+ test('recordIframes opt-in keeps iframe capture available', () => {
367
+ const lib = new Lib(PROJECT_ID, { devMode: true })
368
+ const emit = jest.fn()
369
+
370
+ const options = (lib as any).getSessionReplayRecordOptions(
371
+ 'normal',
372
+ {
373
+ blockSelector: '.secret',
374
+ recordCrossOriginIframes: true,
375
+ sampling: { scroll: 500 },
376
+ },
377
+ emit,
378
+ true,
379
+ )
380
+
381
+ expect(options.blockSelector).toBe('.secret')
382
+ expect(options.recordCrossOriginIframes).toBe(true)
383
+ expect(options.sampling).toEqual({
384
+ mousemove: 50,
385
+ scroll: 500,
386
+ input: 'last',
387
+ })
388
+ expect(options.emit).toBe(emit)
389
+ })
390
+
391
+ test('maskAllText can be enabled outside total privacy', () => {
392
+ const lib = new Lib(PROJECT_ID, { devMode: true })
393
+ const emit = jest.fn()
394
+
395
+ const options = (lib as any).getSessionReplayRecordOptions(
396
+ 'normal',
397
+ {},
398
+ emit,
399
+ false,
400
+ true,
401
+ )
402
+
403
+ expect(options.maskAllInputs).toBe(true)
404
+ expect(options.maskTextSelector).toBe('*')
405
+ expect(options.emit).toBe(emit)
406
+ })
407
+
408
+ test('maskAllText can be disabled for total privacy', () => {
409
+ const lib = new Lib(PROJECT_ID, { devMode: true })
410
+ const emit = jest.fn()
411
+
412
+ const options = (lib as any).getSessionReplayRecordOptions(
413
+ 'total',
414
+ { maskTextSelector: '.masked' },
415
+ emit,
416
+ false,
417
+ false,
418
+ )
419
+
420
+ expect(options.maskAllInputs).toBe(true)
421
+ expect(options.maskTextSelector).toBe('.masked')
422
+ expect(options.blockSelector).toContain('img')
423
+ expect(options.emit).toBe(emit)
424
+ })
425
+
426
+ test('invalid privacy values fall back to total privacy', async () => {
427
+ const { recordOptions } = usePackageRrweb()
428
+ const { init, startSessionReplay } = await loadTracker()
429
+
430
+ init(PROJECT_ID, { devMode: true })
431
+ const actions = await startSessionReplay({
432
+ privacy: 'totl' as any,
433
+ })
434
+ const startCall = fetchMock.mock.calls.find(([url]) =>
435
+ String(url).includes('/session-replay/start'),
436
+ )
437
+
438
+ expect(startCall).toBeTruthy()
439
+ expect(JSON.parse(startCall![1].body as string)).toEqual(
440
+ expect.objectContaining({ privacy: 'total' }),
441
+ )
442
+ expect(recordOptions().maskTextSelector).toBe('*')
443
+
444
+ await actions.stop()
445
+ })
446
+
447
+ test('oversized replay events are dropped before upload', async () => {
448
+ const { recordOptions } = usePackageRrweb()
449
+ const { init, startSessionReplay } = await loadTracker()
450
+
451
+ init(PROJECT_ID, { devMode: true })
452
+ const actions = await startSessionReplay({
453
+ flushIntervalMs: 60_000,
454
+ maxBytesPerEvent: 30,
455
+ })
456
+
457
+ recordOptions().emit({
458
+ type: 3,
459
+ timestamp: 400,
460
+ data: { text: 'x'.repeat(200) },
461
+ })
462
+ await actions.flush()
463
+
464
+ expect(
465
+ fetchMock.mock.calls.filter(([url]) =>
466
+ String(url).includes('/session-replay/chunk'),
467
+ ),
468
+ ).toHaveLength(0)
469
+
470
+ await actions.stop()
471
+ })
472
+
473
+ test('fractional byte limits below one fall back to defaults', async () => {
474
+ const { recordOptions } = usePackageRrweb()
475
+ const { init, startSessionReplay } = await loadTracker()
476
+
477
+ init(PROJECT_ID, { devMode: true })
478
+ const actions = await startSessionReplay({
479
+ flushIntervalMs: 60_000,
480
+ maxBytesPerChunk: 0.5,
481
+ maxBytesPerEvent: 0.5,
482
+ })
483
+
484
+ recordOptions().emit({ type: 3, timestamp: 500 })
485
+ await Promise.resolve()
486
+ await Promise.resolve()
487
+
488
+ expect(
489
+ fetchMock.mock.calls.filter(([url]) =>
490
+ String(url).includes('/session-replay/chunk'),
491
+ ),
492
+ ).toHaveLength(0)
493
+
494
+ await actions.flush()
495
+
496
+ expect(fetchMock).toHaveBeenCalledWith(
497
+ 'https://api.swetrix.com/log/session-replay/chunk',
498
+ expect.objectContaining({
499
+ body: expect.stringContaining('"timestamp":500'),
500
+ }),
501
+ )
502
+
503
+ await actions.stop()
504
+ })
505
+
506
+ test('script rrweb loader clears failed loads so startSessionReplay can retry', async () => {
507
+ const record = jest.fn(() => jest.fn())
508
+ const trackerScript = document.createElement('script')
509
+ trackerScript.src = 'https://example.com/swetrix.js'
510
+ document.head.appendChild(trackerScript)
511
+ const rrwebUrl = 'https://example.com/replaylibrary.min.js'
512
+ const { init, startSessionReplay } = await loadTracker()
513
+
514
+ init(PROJECT_ID, { devMode: true })
515
+ const failedStart = startSessionReplay()
516
+ const failedScript = document.querySelector<HTMLScriptElement>(
517
+ `script[src="${rrwebUrl}"]`,
518
+ )
519
+ expect(failedScript).toBeTruthy()
520
+ failedScript!.dispatchEvent(new Event('error'))
521
+
522
+ await failedStart
523
+ expect((window as any).__SWETRIX_RRWEB_LOADING__).toBeUndefined()
524
+
525
+ const retryStart = startSessionReplay()
526
+ const scripts = document.querySelectorAll<HTMLScriptElement>(
527
+ `script[src="${rrwebUrl}"]`,
528
+ )
529
+ expect(scripts[1]).toBeTruthy()
530
+ expect(scripts[1]).not.toBe(failedScript)
531
+ ;(window as any).rrweb = { record }
532
+ scripts[1].dispatchEvent(new Event('load'))
533
+
534
+ const actions = await retryStart
535
+ expect((window as any).rrweb.record).toBe(record)
536
+ expect(record).toHaveBeenCalled()
537
+
538
+ await actions.stop()
539
+ })
540
+
541
+ test('user rrweb emit is composed with Swetrix uploads', async () => {
542
+ const { recordOptions } = usePackageRrweb()
543
+ const userEmit = jest.fn()
544
+ const { init, startSessionReplay } = await loadTracker()
545
+
546
+ init(PROJECT_ID, { devMode: true })
547
+ const actions = await startSessionReplay({
548
+ rrweb: { emit: userEmit, maskAllInputs: true },
549
+ })
550
+ const event = { type: 2, timestamp: 300 }
551
+ recordOptions().emit(event)
552
+ await actions.flush()
553
+
554
+ expect(userEmit).toHaveBeenCalledWith(event)
555
+ expect(fetchMock).toHaveBeenCalledWith(
556
+ 'https://api.swetrix.com/log/session-replay/chunk',
557
+ expect.objectContaining({
558
+ body: expect.stringContaining('"timestamp":300'),
559
+ }),
560
+ )
561
+
562
+ await actions.stop()
563
+ })
564
+
565
+ test('DNT and disabled tracking return no-op controls without uploading', async () => {
566
+ Object.defineProperty(navigator, 'doNotTrack', {
567
+ value: '1',
568
+ writable: true,
569
+ configurable: true,
570
+ })
571
+
572
+ const { init, startSessionReplay } = await loadTracker()
573
+ init(PROJECT_ID, { devMode: true, respectDNT: true })
574
+
575
+ const actions = await startSessionReplay()
576
+ await actions.flush()
577
+ await actions.stop()
578
+
579
+ expect(fetchMock).not.toHaveBeenCalled()
580
+ expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
581
+ expect(mockRrwebRecord).not.toHaveBeenCalled()
582
+
583
+ resetReplayGlobals()
584
+ mockRrwebRecord.mockReset()
585
+ Object.defineProperty(navigator, 'doNotTrack', {
586
+ value: null,
587
+ writable: true,
588
+ configurable: true,
589
+ })
590
+
591
+ const disabledModule = await loadTracker()
592
+ disabledModule.init(PROJECT_ID, { devMode: true, disabled: true })
593
+ const disabledActions = await disabledModule.startSessionReplay()
594
+ await disabledActions.flush()
595
+
596
+ expect(fetchMock).not.toHaveBeenCalled()
597
+ expect(document.querySelector(`script[src="${RRWEB_URL}"]`)).toBeNull()
598
+ expect(mockRrwebRecord).not.toHaveBeenCalled()
599
+ })
600
+ })
@@ -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
  }
@@ -0,0 +1,7 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "types": ["jest", "node"]
5
+ },
6
+ "include": ["src", "tests"]
7
+ }