@leanbase-giangnd/js 0.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.
Files changed (49) hide show
  1. package/README.md +143 -0
  2. package/dist/index.cjs +6012 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.ts +1484 -0
  5. package/dist/index.mjs +6010 -0
  6. package/dist/index.mjs.map +1 -0
  7. package/dist/leanbase.iife.js +13431 -0
  8. package/dist/leanbase.iife.js.map +1 -0
  9. package/package.json +48 -0
  10. package/src/autocapture-utils.ts +550 -0
  11. package/src/autocapture.ts +415 -0
  12. package/src/config.ts +8 -0
  13. package/src/constants.ts +108 -0
  14. package/src/extensions/rageclick.ts +34 -0
  15. package/src/extensions/replay/external/config.ts +278 -0
  16. package/src/extensions/replay/external/denylist.ts +32 -0
  17. package/src/extensions/replay/external/lazy-loaded-session-recorder.ts +1376 -0
  18. package/src/extensions/replay/external/mutation-throttler.ts +109 -0
  19. package/src/extensions/replay/external/network-plugin.ts +701 -0
  20. package/src/extensions/replay/external/sessionrecording-utils.ts +141 -0
  21. package/src/extensions/replay/external/triggerMatching.ts +422 -0
  22. package/src/extensions/replay/rrweb-plugins/patch.ts +39 -0
  23. package/src/extensions/replay/session-recording.ts +285 -0
  24. package/src/extensions/replay/types/rrweb-types.ts +575 -0
  25. package/src/extensions/replay/types/rrweb.ts +114 -0
  26. package/src/extensions/sampling.ts +26 -0
  27. package/src/iife.ts +87 -0
  28. package/src/index.ts +2 -0
  29. package/src/leanbase-logger.ts +26 -0
  30. package/src/leanbase-persistence.ts +374 -0
  31. package/src/leanbase.ts +457 -0
  32. package/src/page-view.ts +124 -0
  33. package/src/scroll-manager.ts +103 -0
  34. package/src/session-props.ts +114 -0
  35. package/src/sessionid.ts +330 -0
  36. package/src/storage.ts +410 -0
  37. package/src/types/fflate.d.ts +5 -0
  38. package/src/types/rrweb-record.d.ts +8 -0
  39. package/src/types.ts +807 -0
  40. package/src/utils/blocked-uas.ts +162 -0
  41. package/src/utils/element-utils.ts +50 -0
  42. package/src/utils/event-utils.ts +304 -0
  43. package/src/utils/index.ts +222 -0
  44. package/src/utils/logger.ts +26 -0
  45. package/src/utils/request-utils.ts +128 -0
  46. package/src/utils/simple-event-emitter.ts +27 -0
  47. package/src/utils/user-agent-utils.ts +357 -0
  48. package/src/uuidv7.ts +268 -0
  49. package/src/version.ts +1 -0
@@ -0,0 +1,701 @@
1
+ /// <reference lib="dom" />
2
+
3
+ // rrweb/network@1 code starts
4
+ // most of what is below here will be removed when rrweb release their code for this
5
+ // see https://github.com/rrweb-io/rrweb/pull/1105
6
+
7
+ // NB adopted from https://github.com/rrweb-io/rrweb/pull/1105 which looks like it will be accepted into rrweb
8
+ // however, in the PR, it throws when the performance observer data is not available
9
+ // and assumes it is running in a browser with the Request API (i.e. not IE11)
10
+ // copying here so that we can use it before rrweb adopt it
11
+
12
+ import type { IWindow, listenerHandler, RecordPlugin } from '../types/rrweb-types'
13
+ import { CapturedNetworkRequest, Headers, InitiatorType, NetworkRecordOptions } from '../../../types'
14
+ import { isArray, isBoolean, isFormData, isNull, isNullish, isString, isUndefined, isObject } from '@posthog/core'
15
+ import { createLogger } from '../../../utils/logger'
16
+ import { formDataToQuery } from '../../../utils/request-utils'
17
+ import { patch } from '../rrweb-plugins/patch'
18
+ import { isHostOnDenyList } from '../../../extensions/replay/external/denylist'
19
+ import { defaultNetworkOptions } from './config'
20
+
21
+ const logger = createLogger('[Recorder]')
22
+
23
+ export type NetworkData = {
24
+ requests: CapturedNetworkRequest[]
25
+ isInitial?: boolean
26
+ }
27
+
28
+ type networkCallback = (data: NetworkData) => void
29
+
30
+ const isNavigationTiming = (entry: PerformanceEntry): entry is PerformanceNavigationTiming =>
31
+ entry.entryType === 'navigation'
32
+ const isResourceTiming = (entry: PerformanceEntry): entry is PerformanceResourceTiming => entry.entryType === 'resource'
33
+
34
+ type ObservedPerformanceEntry = (PerformanceNavigationTiming | PerformanceResourceTiming) & {
35
+ responseStatus?: number
36
+ }
37
+
38
+ export function findLast<T>(array: Array<T>, predicate: (value: T) => boolean): T | undefined {
39
+ const length = array.length
40
+ for (let i = length - 1; i >= 0; i -= 1) {
41
+ if (predicate(array[i])) {
42
+ return array[i]
43
+ }
44
+ }
45
+ return undefined
46
+ }
47
+
48
+ function isDocument(value: any): value is Document {
49
+ return !!value && typeof value === 'object' && 'nodeType' in value && (value as any).nodeType === 9
50
+ }
51
+
52
+ function initPerformanceObserver(cb: networkCallback, win: IWindow, options: Required<NetworkRecordOptions>) {
53
+ // if we are only observing timings then we could have a single observer for all types, with buffer true,
54
+ // but we are going to filter by initiatorType _if we are wrapping fetch and xhr as the wrapped functions
55
+ // will deal with those.
56
+ // so we have a block which captures requests from before fetch/xhr is wrapped
57
+ // these are marked `isInitial` so playback can display them differently if needed
58
+ // they will never have method/status/headers/body because they are pre-wrapping that provides that
59
+ if (options.recordInitialRequests) {
60
+ const initialPerformanceEntries = win.performance
61
+ .getEntries()
62
+ .filter(
63
+ (entry): entry is ObservedPerformanceEntry =>
64
+ isNavigationTiming(entry) ||
65
+ (isResourceTiming(entry) && options.initiatorTypes.includes(entry.initiatorType as InitiatorType))
66
+ )
67
+ cb({
68
+ requests: initialPerformanceEntries.flatMap((entry) =>
69
+ prepareRequest({ entry, method: undefined, status: undefined, networkRequest: {}, isInitial: true })
70
+ ),
71
+ isInitial: true,
72
+ })
73
+ }
74
+ const observer = new win.PerformanceObserver((entries) => {
75
+ // if recordBody or recordHeaders is true then we don't want to record fetch or xhr here
76
+ // as the wrapped functions will do that. Otherwise, this filter becomes a noop
77
+ // because we do want to record them here
78
+ const wrappedInitiatorFilter = (entry: ObservedPerformanceEntry) =>
79
+ options.recordBody || options.recordHeaders
80
+ ? entry.initiatorType !== 'xmlhttprequest' && entry.initiatorType !== 'fetch'
81
+ : true
82
+
83
+ const performanceEntries = entries.getEntries().filter(
84
+ (entry): entry is ObservedPerformanceEntry =>
85
+ isNavigationTiming(entry) ||
86
+ (isResourceTiming(entry) &&
87
+ options.initiatorTypes.includes(entry.initiatorType as InitiatorType) &&
88
+ // TODO if we are _only_ capturing timing we don't want to filter initiator here
89
+ wrappedInitiatorFilter(entry))
90
+ )
91
+
92
+ cb({
93
+ requests: performanceEntries.flatMap((entry) =>
94
+ prepareRequest({ entry, method: undefined, status: undefined, networkRequest: {} })
95
+ ),
96
+ })
97
+ })
98
+ // compat checked earlier
99
+ // eslint-disable-next-line compat/compat
100
+ const entryTypes = PerformanceObserver.supportedEntryTypes.filter((x) =>
101
+ options.performanceEntryTypeToObserve.includes(x)
102
+ )
103
+ // initial records are gathered above, so we don't need to observe and buffer each type separately
104
+ observer.observe({ entryTypes })
105
+ return () => {
106
+ observer.disconnect()
107
+ }
108
+ }
109
+
110
+ function shouldRecordHeaders(type: 'request' | 'response', recordHeaders: NetworkRecordOptions['recordHeaders']) {
111
+ return !!recordHeaders && (isBoolean(recordHeaders) || recordHeaders[type])
112
+ }
113
+
114
+ export function shouldRecordBody({
115
+ type,
116
+ recordBody,
117
+ headers,
118
+ url,
119
+ }: {
120
+ type: 'request' | 'response'
121
+ headers: Headers
122
+ url: string | URL | RequestInfo
123
+ recordBody: NetworkRecordOptions['recordBody']
124
+ }) {
125
+ function matchesContentType(contentTypes: string[]) {
126
+ const contentTypeHeader = Object.keys(headers).find((key) => key.toLowerCase() === 'content-type')
127
+ const contentType = contentTypeHeader && headers[contentTypeHeader]
128
+ return contentTypes.some((ct) => contentType?.includes(ct))
129
+ }
130
+
131
+ /**
132
+ * particularly in canvas applications we see many requests to blob URLs
133
+ * e.g. blob:https://video_url
134
+ * these blob/object URLs are local to the browser, we can never capture that body
135
+ * so we can just return false here
136
+ */
137
+ function isBlobURL(url: string | URL | RequestInfo) {
138
+ try {
139
+ if (typeof url === 'string') {
140
+ return url.startsWith('blob:')
141
+ }
142
+ if (url instanceof URL) {
143
+ return url.protocol === 'blob:'
144
+ }
145
+ if (url instanceof Request) {
146
+ return isBlobURL(url.url)
147
+ }
148
+ return false
149
+ } catch {
150
+ return false
151
+ }
152
+ }
153
+ if (!recordBody) return false
154
+ if (isBlobURL(url)) return false
155
+ if (isBoolean(recordBody)) return true
156
+ if (isArray(recordBody)) return matchesContentType(recordBody)
157
+ const recordBodyType = recordBody[type]
158
+ if (isBoolean(recordBodyType)) return recordBodyType
159
+ return matchesContentType(recordBodyType)
160
+ }
161
+
162
+ async function getRequestPerformanceEntry(
163
+ win: IWindow,
164
+ initiatorType: string,
165
+ url: string,
166
+ start?: number,
167
+ end?: number,
168
+ attempt = 0
169
+ ): Promise<PerformanceResourceTiming | null> {
170
+ if (attempt > 10) {
171
+ logger.warn('Failed to get performance entry for request', { url, initiatorType })
172
+ return null
173
+ }
174
+ const urlPerformanceEntries = win.performance.getEntriesByName(url) as PerformanceResourceTiming[]
175
+ const performanceEntry = findLast(
176
+ urlPerformanceEntries,
177
+ (entry) =>
178
+ isResourceTiming(entry) &&
179
+ entry.initiatorType === initiatorType &&
180
+ (isUndefined(start) || entry.startTime >= start) &&
181
+ (isUndefined(end) || entry.startTime <= end)
182
+ )
183
+ if (!performanceEntry) {
184
+ await new Promise((resolve) => setTimeout(resolve, 50 * attempt))
185
+ return getRequestPerformanceEntry(win, initiatorType, url, start, end, attempt + 1)
186
+ }
187
+ return performanceEntry
188
+ }
189
+
190
+ /**
191
+ * According to MDN https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/response
192
+ * xhr response is typed as any but can be an ArrayBuffer, a Blob, a Document, a JavaScript object,
193
+ * or a string, depending on the value of XMLHttpRequest.responseType, that contains the response entity body.
194
+ *
195
+ * XHR request body is Document | XMLHttpRequestBodyInit | null | undefined
196
+ */
197
+ function _tryReadXHRBody({
198
+ body,
199
+ options,
200
+ url,
201
+ }: {
202
+ body: Document | XMLHttpRequestBodyInit | any | null | undefined
203
+ options: NetworkRecordOptions
204
+ url: string | URL | RequestInfo
205
+ }): string | null {
206
+ if (isNullish(body)) {
207
+ return null
208
+ }
209
+
210
+ const { hostname, isHostDenied } = isHostOnDenyList(url, options)
211
+ if (isHostDenied) {
212
+ return hostname + ' is in deny list'
213
+ }
214
+
215
+ if (isString(body)) {
216
+ return body
217
+ }
218
+
219
+ if (isDocument(body)) {
220
+ return body.textContent
221
+ }
222
+
223
+ if (isFormData(body)) {
224
+ return formDataToQuery(body)
225
+ }
226
+
227
+ if (isObject(body)) {
228
+ try {
229
+ return JSON.stringify(body)
230
+ } catch {
231
+ return '[SessionReplay] Failed to stringify response object'
232
+ }
233
+ }
234
+
235
+ return '[SessionReplay] Cannot read body of type ' + toString.call(body)
236
+ }
237
+
238
+ function initXhrObserver(cb: networkCallback, win: IWindow, options: Required<NetworkRecordOptions>): listenerHandler {
239
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
240
+ return () => {
241
+ //
242
+ }
243
+ }
244
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders)
245
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders)
246
+
247
+ const restorePatch = patch(
248
+ win.XMLHttpRequest.prototype,
249
+ 'open',
250
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
251
+ // @ts-ignore
252
+ (originalOpen: typeof XMLHttpRequest.prototype.open) => {
253
+ return function (
254
+ method: string,
255
+ url: string | URL,
256
+ async = true,
257
+ username?: string | null,
258
+ password?: string | null
259
+ ) {
260
+ // because this function is returned in its actual context `this` _is_ an XMLHttpRequest
261
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
262
+ // @ts-ignore
263
+ const xhr = this as XMLHttpRequest
264
+
265
+ // check IE earlier than this, we only initialize if Request is present
266
+ // eslint-disable-next-line compat/compat
267
+ const req = new Request(url)
268
+ const networkRequest: Partial<CapturedNetworkRequest> = {}
269
+ let start: number | undefined
270
+ let end: number | undefined
271
+
272
+ const requestHeaders: Headers = {}
273
+ const originalSetRequestHeader = xhr.setRequestHeader.bind(xhr)
274
+ xhr.setRequestHeader = (header: string, value: string) => {
275
+ requestHeaders[header] = value
276
+ return originalSetRequestHeader(header, value)
277
+ }
278
+ if (recordRequestHeaders) {
279
+ networkRequest.requestHeaders = requestHeaders
280
+ }
281
+
282
+ const originalSend = xhr.send.bind(xhr)
283
+ xhr.send = (body) => {
284
+ if (
285
+ shouldRecordBody({
286
+ type: 'request',
287
+ headers: requestHeaders,
288
+ url,
289
+ recordBody: options.recordBody,
290
+ })
291
+ ) {
292
+ networkRequest.requestBody = _tryReadXHRBody({ body, options, url })
293
+ }
294
+ start = win.performance.now()
295
+ return originalSend(body)
296
+ }
297
+
298
+ const readyStateListener = () => {
299
+ if (xhr.readyState !== xhr.DONE) {
300
+ return
301
+ }
302
+
303
+ // Clean up the listener immediately when done to prevent memory leaks
304
+ xhr.removeEventListener('readystatechange', readyStateListener)
305
+
306
+ end = win.performance.now()
307
+ const responseHeaders: Headers = {}
308
+ const rawHeaders = xhr.getAllResponseHeaders()
309
+ const headers = rawHeaders.trim().split(/[\r\n]+/)
310
+ headers.forEach((line) => {
311
+ const parts = line.split(': ')
312
+ const header = parts.shift()
313
+ const value = parts.join(': ')
314
+ if (header) {
315
+ responseHeaders[header] = value
316
+ }
317
+ })
318
+ if (recordResponseHeaders) {
319
+ networkRequest.responseHeaders = responseHeaders
320
+ }
321
+ if (
322
+ shouldRecordBody({
323
+ type: 'response',
324
+ headers: responseHeaders,
325
+ url,
326
+ recordBody: options.recordBody,
327
+ })
328
+ ) {
329
+ networkRequest.responseBody = _tryReadXHRBody({ body: xhr.response, options, url })
330
+ }
331
+ getRequestPerformanceEntry(win, 'xmlhttprequest', req.url, start, end)
332
+ .then((entry) => {
333
+ const requests = prepareRequest({
334
+ entry,
335
+ method: method,
336
+ status: xhr?.status,
337
+ networkRequest,
338
+ start,
339
+ end,
340
+ url: url.toString(),
341
+ initiatorType: 'xmlhttprequest',
342
+ })
343
+ cb({ requests })
344
+ })
345
+ .catch(() => {
346
+ //
347
+ })
348
+ }
349
+
350
+ // This is very tricky code, and making it passive won't bring many performance benefits,
351
+ // so let's ignore the rule here.
352
+ // eslint-disable-next-line posthog-js/no-add-event-listener
353
+ xhr.addEventListener('readystatechange', readyStateListener)
354
+
355
+ originalOpen.call(xhr, method, url, async, username, password)
356
+ }
357
+ }
358
+ )
359
+ return () => {
360
+ restorePatch()
361
+ }
362
+ }
363
+
364
+ /**
365
+ * Check if this PerformanceEntry is either a PerformanceResourceTiming or a PerformanceNavigationTiming
366
+ * NB PerformanceNavigationTiming extends PerformanceResourceTiming
367
+ * Here we don't care which interface it implements as both expose `serverTimings`
368
+ */
369
+ const exposesServerTiming = (event: PerformanceEntry | null): event is PerformanceResourceTiming =>
370
+ !isNull(event) && (event.entryType === 'navigation' || event.entryType === 'resource')
371
+
372
+ function prepareRequest({
373
+ entry,
374
+ method,
375
+ status,
376
+ networkRequest,
377
+ isInitial,
378
+ start,
379
+ end,
380
+ url,
381
+ initiatorType,
382
+ }: {
383
+ entry: PerformanceResourceTiming | null
384
+ method: string | undefined
385
+ status: number | undefined
386
+ networkRequest: Partial<CapturedNetworkRequest>
387
+ isInitial?: boolean
388
+ start?: number
389
+ end?: number
390
+ // if there is no performance observer entry, we still need to know the url
391
+ url?: string
392
+ // if there is no performance observer entry, we can provide the initiatorType
393
+ initiatorType?: string
394
+ }): CapturedNetworkRequest[] {
395
+ start = entry ? entry.startTime : start
396
+ end = entry ? entry.responseEnd : end
397
+
398
+ // kudos to sentry javascript sdk for excellent background on why to use Date.now() here
399
+ // https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L70
400
+ // can't start observer if performance.now() is not available
401
+ // eslint-disable-next-line compat/compat
402
+ const timeOrigin = Math.floor(Date.now() - performance.now())
403
+ // clickhouse can't ingest timestamps that are floats
404
+ // (in this case representing fractions of a millisecond we don't care about anyway)
405
+ // use timeOrigin if we really can't gather a start time
406
+ const timestamp = Math.floor(timeOrigin + (start || 0))
407
+
408
+ const entryJSON = entry ? entry.toJSON() : { name: url }
409
+
410
+ const requests: CapturedNetworkRequest[] = [
411
+ {
412
+ ...entryJSON,
413
+ startTime: isUndefined(start) ? undefined : Math.round(start),
414
+ endTime: isUndefined(end) ? undefined : Math.round(end),
415
+ timeOrigin,
416
+ timestamp,
417
+ method: method,
418
+ initiatorType: initiatorType ? initiatorType : entry ? (entry.initiatorType as InitiatorType) : undefined,
419
+ status,
420
+ requestHeaders: networkRequest.requestHeaders,
421
+ requestBody: networkRequest.requestBody,
422
+ responseHeaders: networkRequest.responseHeaders,
423
+ responseBody: networkRequest.responseBody,
424
+ isInitial,
425
+ },
426
+ ]
427
+
428
+ if (exposesServerTiming(entry)) {
429
+ for (const timing of entry.serverTiming || []) {
430
+ requests.push({
431
+ timeOrigin,
432
+ timestamp,
433
+ startTime: Math.round(entry.startTime),
434
+ name: timing.name,
435
+ duration: timing.duration,
436
+ // the spec has a closed list of possible types
437
+ // https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEntry/entryType
438
+ // but, we need to know this was a server timing so that we know to
439
+ // match it to the appropriate navigation or resource timing
440
+ // that matching will have to be on timestamp and $current_url
441
+ entryType: 'serverTiming',
442
+ })
443
+ }
444
+ }
445
+
446
+ return requests
447
+ }
448
+
449
+ const contentTypePrefixDenyList = ['video/', 'audio/']
450
+
451
+ function _checkForCannotReadResponseBody({
452
+ r,
453
+ options,
454
+ url,
455
+ }: {
456
+ r: Response
457
+ options: NetworkRecordOptions
458
+ url: string | URL | RequestInfo
459
+ }): string | null {
460
+ if (r.headers.get('Transfer-Encoding') === 'chunked') {
461
+ return 'Chunked Transfer-Encoding is not supported'
462
+ }
463
+
464
+ // `get` and `has` are case-insensitive
465
+ // but return the header value with the casing that was supplied
466
+ const contentType = r.headers.get('Content-Type')?.toLowerCase()
467
+ const contentTypeIsDenied = contentTypePrefixDenyList.some((prefix) => contentType?.startsWith(prefix))
468
+ if (contentType && contentTypeIsDenied) {
469
+ return `Content-Type ${contentType} is not supported`
470
+ }
471
+
472
+ const { hostname, isHostDenied } = isHostOnDenyList(url, options)
473
+ if (isHostDenied) {
474
+ return hostname + ' is in deny list'
475
+ }
476
+
477
+ return null
478
+ }
479
+
480
+ function _tryReadBody(r: Request | Response): Promise<string> {
481
+ // there are now already multiple places where we're using Promise...
482
+ // eslint-disable-next-line compat/compat
483
+ return new Promise((resolve, reject) => {
484
+ const timeout = setTimeout(() => resolve('[SessionReplay] Timeout while trying to read body'), 500)
485
+ try {
486
+ r.clone()
487
+ .text()
488
+ .then(
489
+ (txt) => resolve(txt),
490
+ (reason) => reject(reason)
491
+ )
492
+ .finally(() => clearTimeout(timeout))
493
+ } catch {
494
+ clearTimeout(timeout)
495
+ resolve('[SessionReplay] Failed to read body')
496
+ }
497
+ })
498
+ }
499
+
500
+ async function _tryReadRequestBody({
501
+ r,
502
+ options,
503
+ url,
504
+ }: {
505
+ r: Request
506
+ options: NetworkRecordOptions
507
+ url: string | URL | RequestInfo
508
+ }): Promise<string> {
509
+ const { hostname, isHostDenied } = isHostOnDenyList(url, options)
510
+ if (isHostDenied) {
511
+ return Promise.resolve(hostname + ' is in deny list')
512
+ }
513
+
514
+ return _tryReadBody(r)
515
+ }
516
+
517
+ async function _tryReadResponseBody({
518
+ r,
519
+ options,
520
+ url,
521
+ }: {
522
+ r: Response
523
+ options: NetworkRecordOptions
524
+ url: string | URL | RequestInfo
525
+ }): Promise<string> {
526
+ const cannotReadBodyReason: string | null = _checkForCannotReadResponseBody({ r, options, url })
527
+ if (!isNull(cannotReadBodyReason)) {
528
+ return Promise.resolve(cannotReadBodyReason)
529
+ }
530
+
531
+ return _tryReadBody(r)
532
+ }
533
+
534
+ function initFetchObserver(
535
+ cb: networkCallback,
536
+ win: IWindow,
537
+ options: Required<NetworkRecordOptions>
538
+ ): listenerHandler {
539
+ if (!options.initiatorTypes.includes('fetch')) {
540
+ return () => {
541
+ //
542
+ }
543
+ }
544
+ const recordRequestHeaders = shouldRecordHeaders('request', options.recordHeaders)
545
+ const recordResponseHeaders = shouldRecordHeaders('response', options.recordHeaders)
546
+
547
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
548
+ // @ts-ignore
549
+ const restorePatch = patch(win, 'fetch', (originalFetch: typeof fetch) => {
550
+ return async function (url: URL | RequestInfo, init?: RequestInit | undefined) {
551
+ // check IE earlier than this, we only initialize if Request is present
552
+ // eslint-disable-next-line compat/compat
553
+ const req = new Request(url, init)
554
+ let res: Response | undefined
555
+ const networkRequest: Partial<CapturedNetworkRequest> = {}
556
+ let start: number | undefined
557
+ let end: number | undefined
558
+
559
+ try {
560
+ const requestHeaders: Headers = {}
561
+ req.headers.forEach((value, header) => {
562
+ requestHeaders[header] = value
563
+ })
564
+ if (recordRequestHeaders) {
565
+ networkRequest.requestHeaders = requestHeaders
566
+ }
567
+ if (
568
+ shouldRecordBody({
569
+ type: 'request',
570
+ headers: requestHeaders,
571
+ url,
572
+ recordBody: options.recordBody,
573
+ })
574
+ ) {
575
+ networkRequest.requestBody = await _tryReadRequestBody({ r: req, options, url })
576
+ }
577
+
578
+ start = win.performance.now()
579
+ res = await originalFetch(req)
580
+ end = win.performance.now()
581
+
582
+ const responseHeaders: Headers = {}
583
+ res.headers.forEach((value, header) => {
584
+ responseHeaders[header] = value
585
+ })
586
+ if (recordResponseHeaders) {
587
+ networkRequest.responseHeaders = responseHeaders
588
+ }
589
+ if (
590
+ shouldRecordBody({
591
+ type: 'response',
592
+ headers: responseHeaders,
593
+ url,
594
+ recordBody: options.recordBody,
595
+ })
596
+ ) {
597
+ networkRequest.responseBody = await _tryReadResponseBody({ r: res, options, url })
598
+ }
599
+
600
+ return res
601
+ } finally {
602
+ getRequestPerformanceEntry(win, 'fetch', req.url, start, end)
603
+ .then((entry) => {
604
+ const requests = prepareRequest({
605
+ entry,
606
+ method: req.method,
607
+ status: res?.status,
608
+ networkRequest,
609
+ start,
610
+ end,
611
+ url: req.url,
612
+ initiatorType: 'fetch',
613
+ })
614
+ cb({ requests })
615
+ })
616
+ .catch(() => {
617
+ //
618
+ })
619
+ }
620
+ }
621
+ })
622
+ return () => {
623
+ restorePatch()
624
+ }
625
+ }
626
+
627
+ let initialisedHandler: listenerHandler | null = null
628
+
629
+ function initNetworkObserver(
630
+ callback: networkCallback,
631
+ win: IWindow, // top window or in an iframe
632
+ options: NetworkRecordOptions
633
+ ): listenerHandler {
634
+ if (!('performance' in win)) {
635
+ return () => {
636
+ //
637
+ }
638
+ }
639
+
640
+ if (initialisedHandler) {
641
+ logger.warn('Network observer already initialised, doing nothing')
642
+ return () => {
643
+ // the first caller should already have this handler and will be responsible for teardown
644
+ }
645
+ }
646
+
647
+ const networkOptions = (
648
+ options ? Object.assign({}, defaultNetworkOptions, options) : defaultNetworkOptions
649
+ ) as Required<NetworkRecordOptions>
650
+
651
+ const cb: networkCallback = (data) => {
652
+ const requests: CapturedNetworkRequest[] = []
653
+ data.requests.forEach((request) => {
654
+ const maskedRequest = networkOptions.maskRequestFn(request)
655
+ if (maskedRequest) {
656
+ requests.push(maskedRequest)
657
+ }
658
+ })
659
+
660
+ if (requests.length > 0) {
661
+ callback({ ...data, requests })
662
+ }
663
+ }
664
+ const performanceObserver = initPerformanceObserver(cb, win, networkOptions)
665
+
666
+ // only wrap fetch and xhr if headers or body are being recorded
667
+ let xhrObserver: listenerHandler = () => {}
668
+ let fetchObserver: listenerHandler = () => {}
669
+ if (networkOptions.recordHeaders || networkOptions.recordBody) {
670
+ xhrObserver = initXhrObserver(cb, win, networkOptions)
671
+ fetchObserver = initFetchObserver(cb, win, networkOptions)
672
+ }
673
+
674
+ const teardown: listenerHandler = () => {
675
+ performanceObserver()
676
+ xhrObserver()
677
+ fetchObserver()
678
+ // allow future observers to initialize after cleanup
679
+ initialisedHandler = null
680
+ }
681
+
682
+ initialisedHandler = teardown
683
+ return teardown
684
+ }
685
+
686
+ // use the plugin name so that when this functionality is adopted into rrweb
687
+ // we can remove this plugin and use the core functionality with the same data
688
+ export const NETWORK_PLUGIN_NAME = 'rrweb/network@1'
689
+
690
+ // TODO how should this be typed?
691
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
692
+ // @ts-ignore
693
+ export const getRecordNetworkPlugin: (options?: NetworkRecordOptions) => RecordPlugin = (options) => {
694
+ return {
695
+ name: NETWORK_PLUGIN_NAME,
696
+ observer: initNetworkObserver,
697
+ options: options,
698
+ }
699
+ }
700
+
701
+ // rrweb/networ@1 ends