mixpanel-browser 2.74.0 → 2.76.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +3 -1
- package/.github/workflows/integration-tests.yml +2 -2
- package/.github/workflows/unit-tests.yml +3 -3
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/build.sh +10 -8
- package/dist/async-modules/mixpanel-recorder-bIS4LMGd.js +23595 -0
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js +2 -0
- package/dist/async-modules/mixpanel-recorder-hFoTniVR.min.js.map +1 -0
- package/dist/async-modules/mixpanel-targeting-BcAPS-Mz.js +2520 -0
- package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js +2 -0
- package/dist/async-modules/mixpanel-targeting-VOeN7RWY.min.js.map +1 -0
- package/dist/mixpanel-core.cjs.d.ts +68 -0
- package/dist/mixpanel-core.cjs.js +802 -337
- package/dist/mixpanel-recorder.js +828 -40
- package/dist/mixpanel-recorder.min.js +1 -1
- package/dist/mixpanel-recorder.min.js.map +1 -1
- package/dist/mixpanel-targeting.js +2520 -0
- package/dist/mixpanel-targeting.min.js +2 -0
- package/dist/mixpanel-targeting.min.js.map +1 -0
- package/dist/mixpanel-with-async-modules.cjs.d.ts +590 -0
- package/dist/mixpanel-with-async-modules.cjs.js +9867 -0
- package/dist/mixpanel-with-async-recorder.cjs.d.ts +68 -0
- package/dist/mixpanel-with-async-recorder.cjs.js +802 -337
- package/dist/mixpanel-with-recorder.d.ts +68 -0
- package/dist/mixpanel-with-recorder.js +1591 -343
- package/dist/mixpanel-with-recorder.min.d.ts +68 -0
- package/dist/mixpanel-with-recorder.min.js +1 -1
- package/dist/mixpanel.amd.d.ts +68 -0
- package/dist/mixpanel.amd.js +2124 -345
- package/dist/mixpanel.cjs.d.ts +68 -0
- package/dist/mixpanel.cjs.js +2124 -345
- package/dist/mixpanel.globals.js +802 -337
- package/dist/mixpanel.min.js +185 -175
- package/dist/mixpanel.module.d.ts +68 -0
- package/dist/mixpanel.module.js +2124 -345
- package/dist/mixpanel.umd.d.ts +68 -0
- package/dist/mixpanel.umd.js +2124 -345
- package/dist/rrweb-bundled.js +119 -5
- package/dist/rrweb-compiled.js +116 -5
- package/logo.svg +5 -0
- package/package.json +5 -3
- package/rollup.config.mjs +189 -40
- package/src/autocapture/index.js +10 -27
- package/src/config.js +9 -3
- package/src/flags/index.js +269 -9
- package/src/index.d.ts +68 -0
- package/src/loaders/loader-module.js +1 -0
- package/src/mixpanel-core.js +83 -109
- package/src/recorder/index.js +2 -1
- package/src/recorder/recorder.js +5 -1
- package/src/recorder/rrweb-network-plugin.js +649 -0
- package/src/recorder/session-recording.js +31 -11
- package/src/recorder-manager.js +216 -0
- package/src/request-batcher.js +1 -1
- package/src/targeting/event-matcher.js +42 -0
- package/src/targeting/index.js +11 -0
- package/src/targeting/loader.js +36 -0
- package/src/utils.js +14 -9
- package/testServer.js +55 -0
- /package/src/loaders/{loader-module-with-async-recorder.js → loader-module-with-async-modules.js} +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
|
+
};
|