mixpanel-browser 2.75.0 → 2.77.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 (62) hide show
  1. package/.claude/settings.local.json +14 -0
  2. package/.github/dependabot.yml +8 -0
  3. package/.github/workflows/integration-tests.yml +4 -4
  4. package/.github/workflows/unit-tests.yml +4 -4
  5. package/CHANGELOG.md +14 -0
  6. package/build.sh +10 -8
  7. package/dist/async-modules/mixpanel-recorder-DLKbUIEE.js +23669 -0
  8. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js +2 -0
  9. package/dist/async-modules/mixpanel-recorder-wIWnMDLA.min.js.map +1 -0
  10. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js +2 -0
  11. package/dist/async-modules/mixpanel-targeting-CTcftSJC.min.js.map +1 -0
  12. package/dist/async-modules/mixpanel-targeting-CmVvUyFM.js +2520 -0
  13. package/dist/mixpanel-core.cjs.d.ts +70 -1
  14. package/dist/mixpanel-core.cjs.js +724 -426
  15. package/dist/mixpanel-recorder.js +791 -41
  16. package/dist/mixpanel-recorder.min.js +1 -1
  17. package/dist/mixpanel-recorder.min.js.map +1 -1
  18. package/dist/mixpanel-targeting.js +6 -62
  19. package/dist/mixpanel-targeting.min.js +1 -1
  20. package/dist/mixpanel-targeting.min.js.map +1 -1
  21. package/dist/mixpanel-with-async-modules.cjs.d.ts +70 -1
  22. package/dist/mixpanel-with-async-modules.cjs.js +724 -426
  23. package/dist/mixpanel-with-async-recorder.cjs.d.ts +70 -1
  24. package/dist/mixpanel-with-async-recorder.cjs.js +724 -426
  25. package/dist/mixpanel-with-recorder.d.ts +70 -1
  26. package/dist/mixpanel-with-recorder.js +1471 -450
  27. package/dist/mixpanel-with-recorder.min.d.ts +70 -1
  28. package/dist/mixpanel-with-recorder.min.js +1 -1
  29. package/dist/mixpanel.amd.d.ts +70 -1
  30. package/dist/mixpanel.amd.js +1473 -504
  31. package/dist/mixpanel.cjs.d.ts +70 -1
  32. package/dist/mixpanel.cjs.js +1473 -504
  33. package/dist/mixpanel.globals.js +724 -426
  34. package/dist/mixpanel.min.js +189 -182
  35. package/dist/mixpanel.module.d.ts +70 -1
  36. package/dist/mixpanel.module.js +1473 -504
  37. package/dist/mixpanel.umd.d.ts +70 -1
  38. package/dist/mixpanel.umd.js +1473 -504
  39. package/dist/rrweb-bundled.js +61 -9
  40. package/dist/rrweb-compiled.js +56 -9
  41. package/logo.svg +5 -0
  42. package/package.json +6 -4
  43. package/rollup.config.mjs +163 -46
  44. package/src/autocapture/index.js +10 -27
  45. package/src/config.js +9 -3
  46. package/src/flags/index.js +1 -2
  47. package/src/index.d.ts +70 -1
  48. package/src/mixpanel-core.js +77 -112
  49. package/src/recorder/index.js +1 -1
  50. package/src/recorder/recorder.js +5 -1
  51. package/src/recorder/rrweb-network-plugin.js +649 -0
  52. package/src/recorder/session-recording.js +36 -12
  53. package/src/recorder/utils.js +27 -1
  54. package/src/recorder-manager.js +324 -0
  55. package/src/request-batcher.js +1 -1
  56. package/src/targeting/event-matcher.js +2 -57
  57. package/src/targeting/index.js +1 -1
  58. package/src/targeting/loader.js +1 -1
  59. package/src/utils.js +13 -1
  60. package/testServer.js +69 -1
  61. package/src/globals.js +0 -14
  62. /package/src/loaders/{loader-module-with-async-recorder.d.ts → loader-module-with-async-modules.d.ts} +0 -0
@@ -0,0 +1,649 @@
1
+ /**
2
+ * This is a port of the open rrweb network plugin in this PR https://github.com/rrweb-io/rrweb/pull/1105
3
+ * the hope is that eventually this can be replaced with the official plugin once it's published (and we sync the mixpanel rrweb fork)
4
+ *
5
+ * This plugin incorporates some important fixes for fetch/XHR body recording that are not yet in the main rrweb repo, as well as makes
6
+ * header and body recording more restrictive by requiring an allowlist instead of content type / blocklist.
7
+ *
8
+ */
9
+ import { urlMatchesRegexList, console_with_prefix, _ } from '../utils'; // eslint-disable-line camelcase
10
+
11
+ var logger = console_with_prefix('network-plugin');
12
+
13
+ /**
14
+ * Get the time origin for converting performance timestamps to absolute timestamps.
15
+ * Uses Date.now() - performance.now() instead of performance.timeOrigin because
16
+ * browsers can report timeOrigin values that are skewed from actual time, and some
17
+ * browsers (notably older Safari versions) don't implement timeOrigin at all.
18
+ * See: https://github.com/getsentry/sentry-javascript/blob/e856e40b6e71a73252e788cd42b5260f81c9c88e/packages/utils/src/time.ts#L49-L70
19
+ * @param {Window} win
20
+ * @returns {number}
21
+ */
22
+ function getTimeOrigin(win) {
23
+ return Math.round(Date.now() - win.performance.now());
24
+ }
25
+
26
+ /**
27
+ * @typedef {import('../index.d.ts').InitiatorType} InitiatorType
28
+ * @typedef {import('../index.d.ts').NetworkRequest} NetworkRequest
29
+ * @typedef {import('../index.d.ts').NetworkRecordOptions} NetworkRecordOptions
30
+ * @typedef {import('../index.d.ts').NetworkData} NetworkData
31
+ */
32
+
33
+ /**
34
+ * @typedef {Record<string, string>} Headers
35
+ */
36
+
37
+ /**
38
+ * @typedef {string | Document | Blob | ArrayBufferView | ArrayBuffer | FormData | URLSearchParams | ReadableStream<Uint8Array> | null} Body
39
+ */
40
+
41
+ /**
42
+ * @callback networkCallback
43
+ * @param {NetworkData} data
44
+ * @returns {void}
45
+ */
46
+
47
+ /**
48
+ * @callback listenerHandler
49
+ * @returns {void}
50
+ */
51
+
52
+ /**
53
+ * @typedef {(PerformanceNavigationTiming | PerformanceResourceTiming) & { responseStatus?: number }} ObservedPerformanceEntry
54
+ */
55
+
56
+ /**
57
+ * @typedef {Object} RecordPlugin
58
+ * @property {string} name
59
+ * @property {(callback: networkCallback, win: Window, options: NetworkRecordOptions) => listenerHandler} observer
60
+ * @property {NetworkRecordOptions} [options]
61
+ */
62
+
63
+ /** @type {Required<NetworkRecordOptions>} */
64
+ var defaultNetworkOptions = {
65
+ initiatorTypes: [
66
+ 'audio',
67
+ 'beacon',
68
+ 'body',
69
+ 'css',
70
+ 'early-hint',
71
+ 'embed',
72
+ 'fetch',
73
+ 'frame',
74
+ 'iframe',
75
+ 'icon',
76
+ 'image',
77
+ 'img',
78
+ 'input',
79
+ 'link',
80
+ 'navigation',
81
+ 'object',
82
+ 'ping',
83
+ 'script',
84
+ 'track',
85
+ 'video',
86
+ 'xmlhttprequest',
87
+ ],
88
+ ignoreRequestFn: function() { return false; },
89
+ recordHeaders: {
90
+ request: [],
91
+ response: [],
92
+ },
93
+ recordBodyUrls: {
94
+ request: [],
95
+ response: [],
96
+ },
97
+ recordInitialRequests: false,
98
+ };
99
+
100
+ /**
101
+ * @param {PerformanceEntry} entry
102
+ * @returns {entry is PerformanceNavigationTiming}
103
+ */
104
+ function isNavigationTiming(entry) {
105
+ return entry.entryType === 'navigation';
106
+ }
107
+
108
+ /**
109
+ * @param {PerformanceEntry} entry
110
+ * @returns {entry is PerformanceResourceTiming}
111
+ */
112
+ function isResourceTiming (entry) {
113
+ return entry.entryType === 'resource';
114
+ }
115
+
116
+ export function findLast(array, predicate) {
117
+ var length = array.length;
118
+ for (var i = length - 1; i >= 0; i -= 1) {
119
+ if (predicate(array[i])) {
120
+ return array[i];
121
+ }
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Monkey-patches a method on an object with a wrapped version, returning a function that restores the original.
127
+ * Adapted from Sentry's `fill` utility:
128
+ * https://github.com/getsentry/sentry-javascript/blob/de5c5cbe177b4334386e747857225eec36a91ea1/packages/core/src/utils/object.ts#L67-L95
129
+ *
130
+ * @param {object} source - The object containing the method to patch
131
+ * @param {string} name - The method name to patch
132
+ * @param {function} replacementFactory - A function that receives the original method and returns the replacement
133
+ * @returns {function} A function that restores the original method
134
+ */
135
+ export function patch(source, name, replacementFactory) {
136
+ if (!(name in source) || typeof source[name] !== 'function') {
137
+ return function() {};
138
+ }
139
+ var original = source[name];
140
+ var wrapped = replacementFactory(original);
141
+ source[name] = wrapped;
142
+ return function() {
143
+ source[name] = original;
144
+ };
145
+ }
146
+
147
+
148
+ /**
149
+ * Maximum body size to record (1MB)
150
+ */
151
+ var MAX_BODY_SIZE = 1024 * 1024;
152
+
153
+ /**
154
+ * Truncate string if it exceeds max size
155
+ * @param {string} str
156
+ * @returns {string}
157
+ */
158
+ export function truncateBody(str) {
159
+ if (!str || typeof str !== 'string') {
160
+ return str;
161
+ }
162
+ if (str.length > MAX_BODY_SIZE) {
163
+ logger.error('Body truncated from ' + str.length + ' to ' + MAX_BODY_SIZE + ' characters');
164
+ return str.substring(0, MAX_BODY_SIZE) + '... [truncated]';
165
+ }
166
+ return str;
167
+ }
168
+
169
+ /**
170
+ * @param {networkCallback} cb
171
+ * @param {Window} win
172
+ * @param {Required<NetworkRecordOptions>} options
173
+ * @returns {listenerHandler}
174
+ */
175
+ function initPerformanceObserver(cb, win, options) {
176
+ if (!win.PerformanceObserver) {
177
+ logger.error('PerformanceObserver not supported');
178
+ return function() {
179
+ //
180
+ };
181
+ }
182
+ if (options.recordInitialRequests) {
183
+ var initialPerformanceEntries = win.performance
184
+ .getEntries()
185
+ .filter(function(entry) {
186
+ return isNavigationTiming(entry) ||
187
+ (isResourceTiming(entry) &&
188
+ options.initiatorTypes.includes(entry.initiatorType));
189
+ });
190
+ cb({
191
+ requests: initialPerformanceEntries.map(function(entry) {
192
+ return {
193
+ url: entry.name,
194
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
195
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
196
+ startTime: Math.round(entry.startTime),
197
+ endTime: Math.round(entry.responseEnd),
198
+ timeOrigin: getTimeOrigin(win),
199
+ };
200
+ }),
201
+ isInitial: true,
202
+ });
203
+ }
204
+ var observer = new win.PerformanceObserver(function(entries) {
205
+ var performanceEntries = entries
206
+ .getEntries()
207
+ .filter(function(entry) {
208
+ return isResourceTiming(entry) &&
209
+ options.initiatorTypes.includes(entry.initiatorType) &&
210
+ entry.initiatorType !== 'xmlhttprequest' &&
211
+ entry.initiatorType !== 'fetch';
212
+ });
213
+ cb({
214
+ requests: performanceEntries.map(function(entry) {
215
+ return {
216
+ url: entry.name,
217
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
218
+ status: 'responseStatus' in entry ? entry.responseStatus : undefined,
219
+ startTime: Math.round(entry.startTime),
220
+ endTime: Math.round(entry.responseEnd),
221
+ timeOrigin: getTimeOrigin(win),
222
+ };
223
+ }),
224
+ });
225
+ });
226
+ observer.observe({ entryTypes: ['navigation', 'resource'] });
227
+ return function() {
228
+ observer.disconnect();
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Variation of the original rrweb function that requires an allowlist for headers instead of supporting boolean options
234
+ * @param {'request' | 'response'} type
235
+ * @param {NetworkRecordOptions['recordHeaders']} recordHeaders
236
+ * @param {string} headerName
237
+ * @returns {boolean}
238
+ */
239
+ export function shouldRecordHeader(type, recordHeaders, headerName) {
240
+ if (!recordHeaders[type] || recordHeaders[type].length === 0) {
241
+ return false;
242
+ }
243
+
244
+ return recordHeaders[type].includes(headerName.toLowerCase());
245
+ }
246
+
247
+ /**
248
+ * Variation of the original rrweb function that requires an allowlist for URLs instead of supporting boolean options or by content type
249
+ * @param {'request' | 'response'} type
250
+ * @param {NetworkRecordOptions['recordBodyUrls']} recordBodyUrls
251
+ * @param {string} url
252
+ * @returns {boolean}
253
+ */
254
+ export function shouldRecordBody(type, recordBodyUrls, url) {
255
+ if (!recordBodyUrls[type] || recordBodyUrls[type].length === 0) {
256
+ return false;
257
+ }
258
+
259
+ return urlMatchesRegexList(url, recordBodyUrls[type]);
260
+ }
261
+
262
+ function tryReadXHRBody(body) {
263
+ if (body === null || body === undefined) {
264
+ return null;
265
+ }
266
+
267
+ var result;
268
+ if (typeof body === 'string') {
269
+ result = body;
270
+ } else if (body instanceof Document) {
271
+ result = body.textContent;
272
+ } else if (body instanceof FormData) {
273
+ result = _.HTTPBuildQuery(body);
274
+ } else if (_.isObject(body)) {
275
+ try {
276
+ result = JSON.stringify(body);
277
+ } catch (e) {
278
+ return 'Failed to stringify response object';
279
+ }
280
+ } else {
281
+ return 'Cannot read body of type ' + typeof body;
282
+ }
283
+
284
+ return truncateBody(result);
285
+ }
286
+
287
+ /**
288
+ * @param {Request | Response} r
289
+ * @returns {Promise<string>}
290
+ */
291
+ function tryReadFetchBody(r) {
292
+ return new Promise(function(resolve) {
293
+ var timeout = setTimeout(function() {
294
+ resolve('Timeout while trying to read body');
295
+ }, 500);
296
+ try {
297
+ r.clone()
298
+ .text()
299
+ .then(
300
+ function(txt) {
301
+ clearTimeout(timeout);
302
+ resolve(truncateBody(txt));
303
+ },
304
+ function(reason) {
305
+ clearTimeout(timeout);
306
+ resolve('Failed to read body: ' + String(reason));
307
+ }
308
+ );
309
+ } catch (e) {
310
+ clearTimeout(timeout);
311
+ resolve('Failed to read body: ' + String(e));
312
+ }
313
+ });
314
+ }
315
+
316
+ /**
317
+ * @param {Window} win
318
+ * @param {string} initiatorType
319
+ * @param {string} url
320
+ * @param {number} [after]
321
+ * @param {number} [before]
322
+ * @param {number} [attempt]
323
+ * @returns {Promise<PerformanceResourceTiming>}
324
+ */
325
+ function getRequestPerformanceEntry(win, initiatorType, url, after, before, attempt) {
326
+ if (attempt === undefined) {
327
+ attempt = 0;
328
+ }
329
+ if (attempt > 10) {
330
+ logger.error('Cannot find performance entry');
331
+ return Promise.resolve(null);
332
+ }
333
+ var urlPerformanceEntries = /** @type {PerformanceResourceTiming[]} */ (
334
+ win.performance.getEntriesByName(url)
335
+ );
336
+ var performanceEntry = findLast(
337
+ urlPerformanceEntries,
338
+ function(entry) {
339
+ return isResourceTiming(entry) &&
340
+ entry.initiatorType === initiatorType &&
341
+ (!after || entry.startTime >= after) &&
342
+ (!before || entry.startTime <= before);
343
+ }
344
+ );
345
+ if (!performanceEntry) {
346
+ return new Promise(function(resolve) {
347
+ setTimeout(resolve, 50 * attempt);
348
+ }).then(function() {
349
+ return getRequestPerformanceEntry(
350
+ win,
351
+ initiatorType,
352
+ url,
353
+ after,
354
+ before,
355
+ attempt + 1
356
+ );
357
+ });
358
+ }
359
+ return Promise.resolve(performanceEntry);
360
+ }
361
+
362
+ /**
363
+ * @param {networkCallback} cb
364
+ * @param {Window} win
365
+ * @param {Required<NetworkRecordOptions>} options
366
+ * @returns {listenerHandler}
367
+ */
368
+ function initXhrObserver(cb, win, options) {
369
+ if (!options.initiatorTypes.includes('xmlhttprequest')) {
370
+ return function() {
371
+ //
372
+ };
373
+ }
374
+ var restorePatch = patch(
375
+ win.XMLHttpRequest.prototype,
376
+ 'open',
377
+ function(/** @type {typeof XMLHttpRequest.prototype.open} */ originalOpen) {
378
+ return function(
379
+ /** @type {string} */ method,
380
+ /** @type {string | URL} */ url,
381
+ /** @type {boolean} */ async,
382
+ username, password
383
+ ) {
384
+ if (async === undefined) {
385
+ async = true;
386
+ }
387
+ var xhr = /** @type {XMLHttpRequest} */ (this);
388
+ var req = new Request(url, { method: method });
389
+ /** @type {Partial<NetworkRequest>} */
390
+ var networkRequest = {};
391
+ /** @type {number | undefined} */
392
+ var after;
393
+ /** @type {number | undefined} */
394
+ var before;
395
+
396
+ /** @type {Headers} */
397
+ var requestHeaders = {};
398
+ var originalSetRequestHeader = xhr.setRequestHeader.bind(xhr);
399
+ xhr.setRequestHeader = function(/** @type {string} */ header, /** @type {string} */ value) {
400
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
401
+ requestHeaders[header] = value;
402
+ }
403
+ return originalSetRequestHeader(header, value);
404
+ };
405
+ networkRequest.requestHeaders = requestHeaders;
406
+
407
+ var originalSend = xhr.send.bind(xhr);
408
+ xhr.send = function(/** @type {Body} */ body) {
409
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
410
+ networkRequest.requestBody = tryReadXHRBody(body);
411
+ }
412
+ after = win.performance.now();
413
+ return originalSend(body);
414
+ };
415
+ xhr.addEventListener('readystatechange', function() {
416
+ if (xhr.readyState !== xhr.DONE) {
417
+ return;
418
+ }
419
+ before = win.performance.now();
420
+ /** @type {Headers} */
421
+ var responseHeaders = {};
422
+ var rawHeaders = xhr.getAllResponseHeaders();
423
+ if (rawHeaders) {
424
+ var headers = rawHeaders.trim().split(/[\r\n]+/);
425
+ headers.forEach(function(line) {
426
+ if (!line) return;
427
+ var colonIndex = line.indexOf(': ');
428
+ if (colonIndex === -1) return;
429
+ var header = line.substring(0, colonIndex);
430
+ var value = line.substring(colonIndex + 2);
431
+ if (header && shouldRecordHeader('response', options.recordHeaders, header)) {
432
+ responseHeaders[header] = value;
433
+ }
434
+ });
435
+ }
436
+ networkRequest.responseHeaders = responseHeaders;
437
+ if (
438
+ shouldRecordBody('response', options.recordBodyUrls, req.url)
439
+ ) {
440
+ networkRequest.responseBody = tryReadXHRBody(xhr.response);
441
+ }
442
+ getRequestPerformanceEntry(
443
+ win,
444
+ 'xmlhttprequest',
445
+ req.url,
446
+ after,
447
+ before
448
+ )
449
+ .then(function(entry) {
450
+ if (!entry) {
451
+ logger.error('Failed to get performance entry for XHR request to ' + req.url);
452
+ return;
453
+ }
454
+ /** @type {NetworkRequest} */
455
+ var request = {
456
+ url: entry.name,
457
+ method: req.method,
458
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
459
+ status: xhr.status,
460
+ startTime: Math.round(entry.startTime),
461
+ endTime: Math.round(entry.responseEnd),
462
+ timeOrigin: getTimeOrigin(win),
463
+ requestHeaders: networkRequest.requestHeaders,
464
+ requestBody: networkRequest.requestBody,
465
+ responseHeaders: networkRequest.responseHeaders,
466
+ responseBody: networkRequest.responseBody,
467
+ };
468
+ cb({ requests: [request] });
469
+ })
470
+ .catch(function(e) {
471
+ logger.error('Error recording XHR request to ' + req.url + ': ' + String(e));
472
+ });
473
+ });
474
+
475
+ originalOpen.call(xhr, method, url, async, username, password);
476
+ };
477
+ }
478
+ );
479
+ return function() {
480
+ restorePatch();
481
+ };
482
+ }
483
+
484
+ /**
485
+ * @param {networkCallback} cb
486
+ * @param {Window} win
487
+ * @param {Required<NetworkRecordOptions>} options
488
+ * @returns {listenerHandler}
489
+ */
490
+ function initFetchObserver(cb, win, options) {
491
+ if (!options.initiatorTypes.includes('fetch')) {
492
+ return function() {
493
+ //
494
+ };
495
+ }
496
+
497
+ var restorePatch = patch(win, 'fetch', function(/** @type {typeof fetch} */ originalFetch) {
498
+ return function() {
499
+ var req = new Request(arguments[0], arguments[1]);
500
+ /** @type {Response | undefined} */
501
+ var res;
502
+ /** @type {Partial<NetworkRequest>} */
503
+ var networkRequest = {};
504
+ /** @type {number | undefined} */
505
+ var after;
506
+ /** @type {number | undefined} */
507
+ var before;
508
+
509
+ var originalFetchPromise;
510
+ var requestBodyPromise = Promise.resolve(undefined);
511
+ var responseBodyPromise = Promise.resolve(undefined);
512
+ try {
513
+ /** @type {Headers} */
514
+ var requestHeaders = {};
515
+ req.headers.forEach(function(value, header) {
516
+ if (shouldRecordHeader('request', options.recordHeaders, header)) {
517
+ requestHeaders[header] = value;
518
+ }
519
+ });
520
+ networkRequest.requestHeaders = requestHeaders;
521
+
522
+ if (shouldRecordBody('request', options.recordBodyUrls, req.url)) {
523
+ requestBodyPromise = tryReadFetchBody(req)
524
+ .then(function(body) {
525
+ networkRequest.requestBody = body;
526
+ });
527
+ }
528
+
529
+ after = win.performance.now();
530
+ originalFetchPromise = originalFetch.apply(win, arguments).then(function(response) {
531
+ res = response;
532
+ before = win.performance.now();
533
+
534
+ /** @type {Headers} */
535
+ var responseHeaders = {};
536
+ res.headers.forEach(function(value, header) {
537
+ if (shouldRecordHeader('response', options.recordHeaders, header)) {
538
+ responseHeaders[header] = value;
539
+ }
540
+ });
541
+ networkRequest.responseHeaders = responseHeaders;
542
+
543
+ if (shouldRecordBody('response', options.recordBodyUrls, req.url)) {
544
+ responseBodyPromise = tryReadFetchBody(res)
545
+ .then(function(body) {
546
+ networkRequest.responseBody = body;
547
+ });
548
+ }
549
+
550
+ return res;
551
+ });
552
+ } catch (e) {
553
+ originalFetchPromise = Promise.reject(e);
554
+ }
555
+
556
+ // await concurrently so we don't delay the fetch response
557
+ Promise.all([requestBodyPromise, responseBodyPromise, originalFetchPromise])
558
+ .then(function () {
559
+ return getRequestPerformanceEntry(win, 'fetch', req.url, after, before);
560
+ })
561
+ .then(function(entry) {
562
+ if (!entry) {
563
+ logger.error('Failed to get performance entry for fetch request to ' + req.url);
564
+ return;
565
+ }
566
+ /** @type {NetworkRequest} */
567
+ var request = {
568
+ url: entry.name,
569
+ method: req.method,
570
+ initiatorType: /** @type {InitiatorType} */ (entry.initiatorType),
571
+ status: res ? res.status : undefined,
572
+ startTime: Math.round(entry.startTime),
573
+ endTime: Math.round(entry.responseEnd),
574
+ timeOrigin: getTimeOrigin(win),
575
+ requestHeaders: networkRequest.requestHeaders,
576
+ requestBody: networkRequest.requestBody,
577
+ responseHeaders: networkRequest.responseHeaders,
578
+ responseBody: networkRequest.responseBody,
579
+ };
580
+ cb({ requests: [request] });
581
+ })
582
+ .catch(function (e) {
583
+ logger.error('Error recording fetch request to ' + req.url + ': ' + String(e));
584
+ });
585
+
586
+ return originalFetchPromise;
587
+ };
588
+ });
589
+ return function() {
590
+ restorePatch();
591
+ };
592
+ }
593
+
594
+ /**
595
+ * @param {networkCallback} callback
596
+ * @param {Window} win
597
+ * @param {NetworkRecordOptions} options
598
+ * @returns {listenerHandler}
599
+ */
600
+ function initNetworkObserver(callback, win, options) {
601
+ if (!('performance' in win)) {
602
+ return function() {
603
+ //
604
+ };
605
+ }
606
+
607
+ var recordHeaders = Object.assign({}, defaultNetworkOptions.recordHeaders, options.recordHeaders || {});
608
+ var recordBodyUrls = Object.assign({}, defaultNetworkOptions.recordBodyUrls, options.recordBodyUrls || {});
609
+ options = Object.assign({}, options, {
610
+ recordHeaders: recordHeaders,
611
+ recordBodyUrls: recordBodyUrls,
612
+ });
613
+ var networkOptions = /** @type {Required<NetworkRecordOptions>} */ Object.assign({}, defaultNetworkOptions, options);
614
+
615
+ /** @type {networkCallback} */
616
+ var cb = function(data) {
617
+ var requests = data.requests.filter(function(request) {
618
+ var shouldIgnoreUrl = urlMatchesRegexList(request.url, networkOptions.ignoreRequestUrls || []);
619
+ return !shouldIgnoreUrl && !networkOptions.ignoreRequestFn(request);
620
+ });
621
+ if (requests.length > 0 || data.isInitial) {
622
+ callback(Object.assign({}, data, { requests: requests }));
623
+ }
624
+ };
625
+ var performanceObserver = initPerformanceObserver(cb, win, networkOptions);
626
+ var xhrObserver = initXhrObserver(cb, win, networkOptions);
627
+ var fetchObserver = initFetchObserver(cb, win, networkOptions);
628
+ return function() {
629
+ performanceObserver();
630
+ xhrObserver();
631
+ fetchObserver();
632
+ };
633
+ }
634
+
635
+ // arbitrary .mp suffix in case rrweb does publish this plugin later and we use it but need to handle
636
+ // a changed format in the mixpanel product.
637
+ export var NETWORK_PLUGIN_NAME = 'rrweb/network@1.mp';
638
+
639
+ /**
640
+ * @param {NetworkRecordOptions} [options]
641
+ * @returns {RecordPlugin}
642
+ */
643
+ export var getRecordNetworkPlugin = function(options) {
644
+ return {
645
+ name: NETWORK_PLUGIN_NAME,
646
+ observer: initNetworkObserver,
647
+ options: options,
648
+ };
649
+ };