@capillarytech/cap-ui-dev-tools 1.4.0 → 1.6.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.
@@ -1,542 +0,0 @@
1
- /**
2
- * RRWeb Network Record Plugin
3
- * Captures XHR and Fetch network requests during recording
4
- * Based on: https://github.com/rrweb-io/rrweb/pull/1689
5
- */
6
- (function (global, factory) {
7
- typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
8
- typeof define === 'function' && define.amd ? define(factory) :
9
- (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.rrwebPluginNetworkRecord = factory());
10
- })(this, (function () {
11
- 'use strict';
12
-
13
- function getRecordNetworkPlugin(options) {
14
- const opts = options || {};
15
- const recordBody = opts.recordBody !== false;
16
- const recordHeaders = opts.recordHeaders !== false;
17
- const recordInitiator = opts.recordInitiator !== false;
18
- const recordPerformance = opts.recordPerformance !== false;
19
- const maxBodyLength = opts.maxBodyLength || 10000;
20
- const ignoreRequestFn = opts.ignoreRequestFn || (() => false);
21
-
22
- return {
23
- name: 'rrweb/network@1',
24
- observer(emit, win) {
25
- const window = win || globalThis;
26
-
27
- // Verify emit is a function
28
- if (typeof emit !== 'function') {
29
- return () => {};
30
- }
31
-
32
- // Helper: Truncate body if too large
33
- function truncateBody(body, maxLength) {
34
- if (!body) return body;
35
- const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
36
- if (bodyStr.length > maxLength) {
37
- return bodyStr.substring(0, maxLength) + '... [truncated]';
38
- }
39
- return bodyStr;
40
- }
41
-
42
- // Helper: Get initiator stack trace
43
- function getInitiator() {
44
- if (!recordInitiator) return undefined;
45
- try {
46
- const stack = new Error().stack;
47
- if (stack) {
48
- const lines = stack.split('\n').slice(3, 6);
49
- return lines.join('\n');
50
- }
51
- } catch (e) {
52
- // Ignore
53
- }
54
- return undefined;
55
- }
56
-
57
- // Helper: Get performance entry
58
- function getPerformanceEntry(url, startTime) {
59
- if (!recordPerformance || !window.performance || !window.performance.getEntriesByType) {
60
- return undefined;
61
- }
62
- try {
63
- const entries = window.performance.getEntriesByType('resource');
64
- const entry = entries.find(e =>
65
- e.name === url &&
66
- Math.abs(e.startTime - startTime) < 100
67
- );
68
- if (entry) {
69
- return {
70
- duration: entry.duration,
71
- transferSize: entry.transferSize,
72
- encodedBodySize: entry.encodedBodySize,
73
- decodedBodySize: entry.decodedBodySize,
74
- startTime: entry.startTime,
75
- responseStart: entry.responseStart,
76
- responseEnd: entry.responseEnd
77
- };
78
- }
79
- } catch (e) {
80
- // Ignore
81
- }
82
- return undefined;
83
- }
84
-
85
- // Store original functions
86
- const originalFetch = window.fetch;
87
- const originalXHROpen = window.XMLHttpRequest.prototype.open;
88
- const originalXHRSend = window.XMLHttpRequest.prototype.send;
89
-
90
- // PerformanceObserver to catch ALL network resources (from PR #1105)
91
- // This catches requests that don't go through fetch/XHR (images, scripts, etc.)
92
- let performanceObserver = null;
93
- if (window.PerformanceObserver && window.performance) {
94
- try {
95
- performanceObserver = new PerformanceObserver((list) => {
96
- for (const entry of list.getEntries()) {
97
- // Only capture resource entries (not navigation)
98
- if (entry.entryType === 'resource') {
99
- const resourceEntry = entry;
100
-
101
- // Skip if filtered
102
- if (ignoreRequestFn(resourceEntry.name, resourceEntry.initiatorType || 'other')) {
103
- continue;
104
- }
105
-
106
- // Skip fetch/XHR as they're handled by patching
107
- if (resourceEntry.initiatorType === 'xmlhttprequest' || resourceEntry.initiatorType === 'fetch') {
108
- continue;
109
- }
110
-
111
- // Emit request event for resource
112
- try {
113
- const requestId = `resource_${resourceEntry.startTime}_${Math.random().toString(36).substr(2, 9)}`;
114
- const method = 'GET'; // Resources are typically GET requests
115
-
116
- emit({
117
- type: 6,
118
- data: {
119
- plugin: 'rrweb/network@1',
120
- payload: {
121
- type: 'request',
122
- id: requestId,
123
- method: method,
124
- url: resourceEntry.name,
125
- timestamp: resourceEntry.startTime,
126
- requestType: resourceEntry.initiatorType || 'other',
127
- initiator: recordInitiator ? resourceEntry.name : undefined
128
- }
129
- },
130
- timestamp: resourceEntry.startTime
131
- });
132
-
133
- // Emit response event
134
- emit({
135
- type: 6,
136
- data: {
137
- plugin: 'rrweb/network@1',
138
- payload: {
139
- type: 'response',
140
- id: requestId,
141
- status: resourceEntry.responseStatus || 200,
142
- statusText: 'OK',
143
- timestamp: resourceEntry.responseEnd || resourceEntry.startTime + resourceEntry.duration,
144
- duration: resourceEntry.duration,
145
- performance: {
146
- duration: resourceEntry.duration,
147
- transferSize: resourceEntry.transferSize,
148
- encodedBodySize: resourceEntry.encodedBodySize,
149
- decodedBodySize: resourceEntry.decodedBodySize,
150
- startTime: resourceEntry.startTime,
151
- responseStart: resourceEntry.responseStart,
152
- responseEnd: resourceEntry.responseEnd
153
- }
154
- }
155
- },
156
- timestamp: resourceEntry.responseEnd || resourceEntry.startTime + resourceEntry.duration
157
- });
158
- } catch (e) {
159
- // Ignore errors
160
- }
161
- }
162
- }
163
- });
164
-
165
- // Observe resource timing entries
166
- performanceObserver.observe({ entryTypes: ['resource'] });
167
- } catch (e) {
168
- // PerformanceObserver not supported or error
169
- }
170
- }
171
-
172
- // Capture navigation timing (from PR #1105)
173
- if (window.performance && window.performance.getEntriesByType) {
174
- try {
175
- const navEntries = window.performance.getEntriesByType('navigation');
176
- if (navEntries.length > 0) {
177
- const navEntry = navEntries[0];
178
- if (navEntry && !ignoreRequestFn(window.location.href, 'navigation')) {
179
- try {
180
- const requestId = `navigation_${navEntry.startTime}`;
181
-
182
- emit({
183
- type: 6,
184
- data: {
185
- plugin: 'rrweb/network@1',
186
- payload: {
187
- type: 'request',
188
- id: requestId,
189
- method: 'GET',
190
- url: window.location.href,
191
- timestamp: navEntry.startTime,
192
- requestType: 'navigation'
193
- }
194
- },
195
- timestamp: navEntry.startTime
196
- });
197
-
198
- emit({
199
- type: 6,
200
- data: {
201
- plugin: 'rrweb/network@1',
202
- payload: {
203
- type: 'response',
204
- id: requestId,
205
- status: 200,
206
- statusText: 'OK',
207
- timestamp: navEntry.loadEventEnd || navEntry.startTime + navEntry.duration,
208
- duration: navEntry.duration,
209
- performance: {
210
- duration: navEntry.duration,
211
- transferSize: navEntry.transferSize,
212
- encodedBodySize: navEntry.encodedBodySize,
213
- decodedBodySize: navEntry.decodedBodySize,
214
- startTime: navEntry.startTime,
215
- responseStart: navEntry.responseStart,
216
- responseEnd: navEntry.responseEnd
217
- }
218
- }
219
- },
220
- timestamp: navEntry.loadEventEnd || navEntry.startTime + navEntry.duration
221
- });
222
- } catch (e) {
223
- // Ignore errors
224
- }
225
- }
226
- }
227
- } catch (e) {
228
- // Ignore errors
229
- }
230
- }
231
-
232
- // Wrap fetch
233
- window.fetch = function(...args) {
234
- // Extract URL
235
- let url = null;
236
- if (typeof args[0] === 'string') {
237
- url = args[0];
238
- } else if (args[0] instanceof Request) {
239
- url = args[0].url;
240
- } else if (args[0] instanceof URL) {
241
- url = args[0].href;
242
- } else if (args[0]?.url) {
243
- url = args[0].url;
244
- } else if (args[0]?.href) {
245
- url = args[0].href;
246
- }
247
-
248
- const init = args[1] || {};
249
-
250
- if (!url) {
251
- return originalFetch.apply(this, args);
252
- }
253
-
254
- if (ignoreRequestFn(url, 'fetch')) {
255
- return originalFetch.apply(this, args);
256
- }
257
-
258
- const startTime = Date.now();
259
- const requestId = `fetch_${startTime}_${Math.random().toString(36).substr(2, 9)}`;
260
- const initiator = getInitiator();
261
- const method = init.method || 'GET';
262
-
263
- // Emit request event
264
- try {
265
- emit({
266
- type: 6,
267
- data: {
268
- plugin: 'rrweb/network@1',
269
- payload: {
270
- type: 'request',
271
- id: requestId,
272
- method: method,
273
- url: url,
274
- headers: recordHeaders ? (init.headers instanceof Headers ? Object.fromEntries(init.headers.entries()) : init.headers) : undefined,
275
- body: recordBody ? truncateBody(init.body, maxBodyLength) : undefined,
276
- timestamp: startTime,
277
- initiator: initiator,
278
- requestType: 'fetch'
279
- }
280
- },
281
- timestamp: startTime
282
- });
283
- } catch (e) {
284
- // Ignore
285
- }
286
-
287
- return originalFetch.apply(this, args).then(
288
- response => {
289
- const endTime = Date.now();
290
- const performanceEntry = getPerformanceEntry(url, startTime);
291
-
292
- // Clone response to read body
293
- const clonedResponse = response.clone();
294
-
295
- // Read response body if needed
296
- if (recordBody) {
297
- clonedResponse.text().then(body => {
298
- try {
299
- emit({
300
- type: 6,
301
- data: {
302
- plugin: 'rrweb/network@1',
303
- payload: {
304
- type: 'response',
305
- id: requestId,
306
- status: response.status,
307
- statusText: response.statusText,
308
- headers: recordHeaders ? Object.fromEntries(response.headers.entries()) : undefined,
309
- body: truncateBody(body, maxBodyLength),
310
- timestamp: endTime,
311
- duration: endTime - startTime,
312
- performance: performanceEntry
313
- }
314
- },
315
- timestamp: endTime
316
- });
317
- } catch (e) {
318
- // Ignore
319
- }
320
- }).catch(() => {
321
- // If body read fails, emit without body
322
- try {
323
- emit({
324
- type: 6,
325
- data: {
326
- plugin: 'rrweb/network@1',
327
- payload: {
328
- type: 'response',
329
- id: requestId,
330
- status: response.status,
331
- statusText: response.statusText,
332
- headers: recordHeaders ? Object.fromEntries(response.headers.entries()) : undefined,
333
- timestamp: endTime,
334
- duration: endTime - startTime,
335
- performance: performanceEntry
336
- }
337
- },
338
- timestamp: endTime
339
- });
340
- } catch (e) {
341
- // Ignore
342
- }
343
- });
344
- } else {
345
- try {
346
- emit({
347
- type: 6,
348
- data: {
349
- plugin: 'rrweb/network@1',
350
- payload: {
351
- type: 'response',
352
- id: requestId,
353
- status: response.status,
354
- statusText: response.statusText,
355
- headers: recordHeaders ? Object.fromEntries(response.headers.entries()) : undefined,
356
- timestamp: endTime,
357
- duration: endTime - startTime,
358
- performance: performanceEntry
359
- }
360
- },
361
- timestamp: endTime
362
- });
363
- } catch (e) {
364
- // Ignore
365
- }
366
- }
367
-
368
- return response;
369
- },
370
- error => {
371
- const endTime = Date.now();
372
- try {
373
- emit({
374
- type: 6,
375
- data: {
376
- plugin: 'rrweb/network@1',
377
- payload: {
378
- type: 'response',
379
- id: requestId,
380
- error: error.message || 'Network request failed',
381
- timestamp: endTime,
382
- duration: endTime - startTime
383
- }
384
- },
385
- timestamp: endTime
386
- });
387
- } catch (e) {
388
- // Ignore
389
- }
390
- throw error;
391
- }
392
- );
393
- };
394
-
395
- // Wrap XMLHttpRequest prototype methods
396
- window.XMLHttpRequest.prototype.open = function(method, url, ...rest) {
397
- // Handle relative URLs - resolve them
398
- let resolvedUrl = url;
399
- if (url && typeof url === 'string' && !url.startsWith('http://') && !url.startsWith('https://') && !url.startsWith('//')) {
400
- try {
401
- resolvedUrl = new URL(url, window.location.href).href;
402
- } catch (e) {
403
- resolvedUrl = url;
404
- }
405
- }
406
-
407
- // Store network data on the XHR instance
408
- this._rrwebNetworkData = {
409
- method: method,
410
- url: resolvedUrl || url,
411
- startTime: Date.now(),
412
- requestId: `xhr_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
413
- initiator: getInitiator()
414
- };
415
-
416
- return originalXHROpen.apply(this, [method, url, ...rest]);
417
- };
418
-
419
- window.XMLHttpRequest.prototype.send = function(body) {
420
- const xhr = this;
421
- const data = xhr._rrwebNetworkData;
422
-
423
- if (!data) {
424
- return originalXHRSend.apply(this, arguments);
425
- }
426
-
427
- if (ignoreRequestFn(data.url, 'xhr')) {
428
- return originalXHRSend.apply(this, arguments);
429
- }
430
-
431
- // Emit request event
432
- try {
433
- emit({
434
- type: 6,
435
- data: {
436
- plugin: 'rrweb/network@1',
437
- payload: {
438
- type: 'request',
439
- id: data.requestId,
440
- method: data.method,
441
- url: data.url,
442
- headers: recordHeaders ? this.getAllResponseHeaders() : undefined,
443
- body: recordBody ? truncateBody(body, maxBodyLength) : undefined,
444
- timestamp: data.startTime,
445
- initiator: data.initiator,
446
- requestType: 'xhr'
447
- }
448
- },
449
- timestamp: data.startTime
450
- });
451
- } catch (e) {
452
- // Ignore
453
- }
454
-
455
- // Listen for response
456
- const originalOnReadyStateChange = xhr.onreadystatechange;
457
- xhr.onreadystatechange = function(...args) {
458
- if (xhr.readyState === 4) {
459
- const endTime = Date.now();
460
- const performanceEntry = getPerformanceEntry(data.url, data.startTime);
461
-
462
- try {
463
- emit({
464
- type: 6,
465
- data: {
466
- plugin: 'rrweb/network@1',
467
- payload: {
468
- type: 'response',
469
- id: data.requestId,
470
- status: xhr.status,
471
- statusText: xhr.statusText,
472
- headers: recordHeaders ? xhr.getAllResponseHeaders() : undefined,
473
- body: recordBody ? truncateBody(xhr.responseText, maxBodyLength) : undefined,
474
- timestamp: endTime,
475
- duration: endTime - data.startTime,
476
- performance: performanceEntry
477
- }
478
- },
479
- timestamp: endTime
480
- });
481
- } catch (e) {
482
- // Ignore
483
- }
484
- }
485
- if (originalOnReadyStateChange) {
486
- return originalOnReadyStateChange.apply(this, args);
487
- }
488
- };
489
-
490
- // Listen for errors
491
- const originalOnError = xhr.onerror;
492
- xhr.onerror = function(...args) {
493
- const endTime = Date.now();
494
- try {
495
- emit({
496
- type: 6,
497
- data: {
498
- plugin: 'rrweb/network@1',
499
- payload: {
500
- type: 'response',
501
- id: data.requestId,
502
- error: 'Network request failed',
503
- timestamp: endTime,
504
- duration: endTime - data.startTime
505
- }
506
- },
507
- timestamp: endTime
508
- });
509
- } catch (e) {
510
- // Ignore
511
- }
512
- if (originalOnError) {
513
- return originalOnError.apply(this, args);
514
- }
515
- };
516
-
517
- return originalXHRSend.apply(this, arguments);
518
- };
519
-
520
- // Return cleanup function
521
- return () => {
522
- try {
523
- // Disconnect PerformanceObserver
524
- if (performanceObserver) {
525
- performanceObserver.disconnect();
526
- performanceObserver = null;
527
- }
528
-
529
- // Restore original functions
530
- window.fetch = originalFetch;
531
- window.XMLHttpRequest.prototype.open = originalXHROpen;
532
- window.XMLHttpRequest.prototype.send = originalXHRSend;
533
- } catch (e) {
534
- // Ignore
535
- }
536
- };
537
- }
538
- };
539
- }
540
-
541
- return getRecordNetworkPlugin;
542
- }));