@capillarytech/cap-ui-dev-tools 0.0.4 → 1.2.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/CAPVISION_USAGE.md +488 -0
- package/README.md +217 -40
- package/package.json +52 -8
- package/src/LibraryWatcherPlugin.js +53 -0
- package/src/capvision-recorder/adapters/WebdriverIOAdapter.js +312 -0
- package/src/capvision-recorder/assets/capvision-player.css +1 -0
- package/src/capvision-recorder/assets/capvision-player.min.js +31 -0
- package/src/capvision-recorder/assets/capvision-plugins/console-record.min.js +93 -0
- package/src/capvision-recorder/assets/capvision-plugins/console-replay.min.js +85 -0
- package/src/capvision-recorder/assets/capvision-plugins/network-record.min.js +542 -0
- package/src/capvision-recorder/assets/capvision-plugins/network-replay.min.js +434 -0
- package/src/capvision-recorder/assets/capvision.min.js +19 -0
- package/src/capvision-recorder/core/CapVisionRecorder.js +1338 -0
- package/src/capvision-recorder/core/ReportEnhancer.js +506 -0
- package/src/capvision-recorder/index.js +58 -0
- package/src/index.js +32 -5
- package/architecture.png +0 -0
- package/capillarytech-cap-ui-dev-tools-0.0.4.tgz +0 -0
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RRWeb Network Replay Plugin
|
|
3
|
+
* Displays network activity in a floating panel during replay
|
|
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.rrwebPluginNetworkReplay = factory());
|
|
10
|
+
})(this, (function () {
|
|
11
|
+
'use strict';
|
|
12
|
+
|
|
13
|
+
function getReplayNetworkPlugin(options) {
|
|
14
|
+
const opts = options || {};
|
|
15
|
+
const showPanel = opts.showPanel !== false;
|
|
16
|
+
const maxEntries = opts.maxEntries || 100;
|
|
17
|
+
const position = opts.position || 'bottom-right';
|
|
18
|
+
|
|
19
|
+
// Network entries storage
|
|
20
|
+
const networkEntries = [];
|
|
21
|
+
let panelElement = null;
|
|
22
|
+
let tableBody = null;
|
|
23
|
+
let isMinimized = false;
|
|
24
|
+
|
|
25
|
+
// Helper: Format duration
|
|
26
|
+
function formatDuration(ms) {
|
|
27
|
+
if (typeof ms !== 'number' || isNaN(ms)) return '-';
|
|
28
|
+
if (ms < 1000) return ms + 'ms';
|
|
29
|
+
return (ms / 1000).toFixed(2) + 's';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Helper: Format size
|
|
33
|
+
function formatSize(bytes) {
|
|
34
|
+
if (typeof bytes !== 'number' || isNaN(bytes)) return '-';
|
|
35
|
+
if (bytes < 1024) return bytes + 'B';
|
|
36
|
+
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + 'KB';
|
|
37
|
+
return (bytes / (1024 * 1024)).toFixed(2) + 'MB';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Helper: Truncate URL for display
|
|
41
|
+
function truncateURL(url, maxLength) {
|
|
42
|
+
if (!url) return '-';
|
|
43
|
+
if (url.length <= maxLength) return url;
|
|
44
|
+
return url.substring(0, maxLength - 3) + '...';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Helper: Get status color
|
|
48
|
+
function getStatusColor(status) {
|
|
49
|
+
if (!status) return '#666';
|
|
50
|
+
if (status >= 200 && status < 300) return '#28a745';
|
|
51
|
+
if (status >= 300 && status < 400) return '#ffc107';
|
|
52
|
+
if (status >= 400 && status < 500) return '#dc3545';
|
|
53
|
+
if (status >= 500) return '#dc3545';
|
|
54
|
+
return '#666';
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Helper: Get method color
|
|
58
|
+
function getMethodColor(method) {
|
|
59
|
+
if (!method) return '#666';
|
|
60
|
+
const upperMethod = method.toUpperCase();
|
|
61
|
+
if (upperMethod === 'GET') return '#007bff';
|
|
62
|
+
if (upperMethod === 'POST') return '#28a745';
|
|
63
|
+
if (upperMethod === 'PUT') return '#ffc107';
|
|
64
|
+
if (upperMethod === 'DELETE') return '#dc3545';
|
|
65
|
+
if (upperMethod === 'PATCH') return '#17a2b8';
|
|
66
|
+
return '#666';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Create panel HTML
|
|
70
|
+
function createPanel() {
|
|
71
|
+
if (!showPanel) return null;
|
|
72
|
+
|
|
73
|
+
const panel = document.createElement('div');
|
|
74
|
+
panel.id = 'rrweb-network-panel';
|
|
75
|
+
panel.style.cssText = `
|
|
76
|
+
position: fixed;
|
|
77
|
+
${position.includes('right') ? 'right: 20px;' : 'left: 20px;'}
|
|
78
|
+
${position.includes('bottom') ? 'bottom: 20px;' : 'top: 20px;'}
|
|
79
|
+
width: 600px;
|
|
80
|
+
max-height: 500px;
|
|
81
|
+
background: white;
|
|
82
|
+
border: 1px solid #ddd;
|
|
83
|
+
border-radius: 8px;
|
|
84
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.15);
|
|
85
|
+
z-index: 10000;
|
|
86
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
87
|
+
font-size: 12px;
|
|
88
|
+
display: flex;
|
|
89
|
+
flex-direction: column;
|
|
90
|
+
overflow: hidden;
|
|
91
|
+
`;
|
|
92
|
+
|
|
93
|
+
// Header
|
|
94
|
+
const header = document.createElement('div');
|
|
95
|
+
header.style.cssText = `
|
|
96
|
+
padding: 12px 16px;
|
|
97
|
+
background: #f8f9fa;
|
|
98
|
+
border-bottom: 1px solid #ddd;
|
|
99
|
+
display: flex;
|
|
100
|
+
justify-content: space-between;
|
|
101
|
+
align-items: center;
|
|
102
|
+
cursor: move;
|
|
103
|
+
user-select: none;
|
|
104
|
+
`;
|
|
105
|
+
|
|
106
|
+
const title = document.createElement('div');
|
|
107
|
+
title.textContent = '🌐 Network Activity';
|
|
108
|
+
title.style.cssText = 'font-weight: 600; font-size: 14px; color: #333;';
|
|
109
|
+
|
|
110
|
+
const controls = document.createElement('div');
|
|
111
|
+
controls.style.cssText = 'display: flex; gap: 8px; align-items: center;';
|
|
112
|
+
|
|
113
|
+
const clearBtn = document.createElement('button');
|
|
114
|
+
clearBtn.textContent = 'Clear';
|
|
115
|
+
clearBtn.style.cssText = `
|
|
116
|
+
padding: 4px 8px;
|
|
117
|
+
font-size: 11px;
|
|
118
|
+
border: 1px solid #ddd;
|
|
119
|
+
background: white;
|
|
120
|
+
border-radius: 4px;
|
|
121
|
+
cursor: pointer;
|
|
122
|
+
`;
|
|
123
|
+
clearBtn.onclick = () => {
|
|
124
|
+
networkEntries.length = 0;
|
|
125
|
+
updateTable();
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const minimizeBtn = document.createElement('button');
|
|
129
|
+
minimizeBtn.textContent = isMinimized ? '▼' : '▲';
|
|
130
|
+
minimizeBtn.style.cssText = `
|
|
131
|
+
padding: 4px 8px;
|
|
132
|
+
font-size: 11px;
|
|
133
|
+
border: 1px solid #ddd;
|
|
134
|
+
background: white;
|
|
135
|
+
border-radius: 4px;
|
|
136
|
+
cursor: pointer;
|
|
137
|
+
min-width: 24px;
|
|
138
|
+
`;
|
|
139
|
+
minimizeBtn.onclick = () => {
|
|
140
|
+
isMinimized = !isMinimized;
|
|
141
|
+
minimizeBtn.textContent = isMinimized ? '▼' : '▲';
|
|
142
|
+
content.style.display = isMinimized ? 'none' : 'flex';
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
controls.appendChild(clearBtn);
|
|
146
|
+
controls.appendChild(minimizeBtn);
|
|
147
|
+
header.appendChild(title);
|
|
148
|
+
header.appendChild(controls);
|
|
149
|
+
|
|
150
|
+
// Content area
|
|
151
|
+
const content = document.createElement('div');
|
|
152
|
+
content.id = 'rrweb-network-content';
|
|
153
|
+
content.style.cssText = `
|
|
154
|
+
flex: 1;
|
|
155
|
+
overflow-y: auto;
|
|
156
|
+
display: flex;
|
|
157
|
+
flex-direction: column;
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
// Table
|
|
161
|
+
const table = document.createElement('table');
|
|
162
|
+
table.style.cssText = `
|
|
163
|
+
width: 100%;
|
|
164
|
+
border-collapse: collapse;
|
|
165
|
+
font-size: 11px;
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
// Table header
|
|
169
|
+
const thead = document.createElement('thead');
|
|
170
|
+
thead.style.cssText = 'background: #f8f9fa; position: sticky; top: 0; z-index: 10;';
|
|
171
|
+
const headerRow = document.createElement('tr');
|
|
172
|
+
['Method', 'URL', 'Status', 'Duration', 'Size'].forEach(text => {
|
|
173
|
+
const th = document.createElement('th');
|
|
174
|
+
th.textContent = text;
|
|
175
|
+
th.style.cssText = 'padding: 8px; text-align: left; font-weight: 600; border-bottom: 2px solid #ddd;';
|
|
176
|
+
headerRow.appendChild(th);
|
|
177
|
+
});
|
|
178
|
+
thead.appendChild(headerRow);
|
|
179
|
+
|
|
180
|
+
// Table body
|
|
181
|
+
tableBody = document.createElement('tbody');
|
|
182
|
+
table.appendChild(thead);
|
|
183
|
+
table.appendChild(tableBody);
|
|
184
|
+
content.appendChild(table);
|
|
185
|
+
|
|
186
|
+
panel.appendChild(header);
|
|
187
|
+
panel.appendChild(content);
|
|
188
|
+
|
|
189
|
+
// Make panel draggable
|
|
190
|
+
let isDragging = false;
|
|
191
|
+
let currentX, currentY, initialX, initialY;
|
|
192
|
+
|
|
193
|
+
header.addEventListener('mousedown', (e) => {
|
|
194
|
+
isDragging = true;
|
|
195
|
+
initialX = e.clientX - panel.offsetLeft;
|
|
196
|
+
initialY = e.clientY - panel.offsetTop;
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
document.addEventListener('mousemove', (e) => {
|
|
200
|
+
if (isDragging) {
|
|
201
|
+
currentX = e.clientX - initialX;
|
|
202
|
+
currentY = e.clientY - initialY;
|
|
203
|
+
panel.style.left = currentX + 'px';
|
|
204
|
+
panel.style.top = currentY + 'px';
|
|
205
|
+
panel.style.right = 'auto';
|
|
206
|
+
panel.style.bottom = 'auto';
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
document.addEventListener('mouseup', () => {
|
|
211
|
+
isDragging = false;
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
document.body.appendChild(panel);
|
|
215
|
+
return panel;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Update table with network entries
|
|
219
|
+
function updateTable() {
|
|
220
|
+
if (!tableBody) return;
|
|
221
|
+
|
|
222
|
+
// Clear existing rows
|
|
223
|
+
tableBody.innerHTML = '';
|
|
224
|
+
|
|
225
|
+
// Show latest entries (up to maxEntries)
|
|
226
|
+
const entriesToShow = networkEntries.slice(-maxEntries);
|
|
227
|
+
|
|
228
|
+
entriesToShow.forEach(entry => {
|
|
229
|
+
const row = document.createElement('tr');
|
|
230
|
+
row.style.cssText = 'border-bottom: 1px solid #eee;';
|
|
231
|
+
|
|
232
|
+
// Method cell
|
|
233
|
+
const methodCell = document.createElement('td');
|
|
234
|
+
methodCell.textContent = entry.method || '-';
|
|
235
|
+
methodCell.style.cssText = `padding: 6px 8px; color: ${getMethodColor(entry.method)}; font-weight: 600;`;
|
|
236
|
+
|
|
237
|
+
// URL cell
|
|
238
|
+
const urlCell = document.createElement('td');
|
|
239
|
+
urlCell.textContent = truncateURL(entry.url, 40);
|
|
240
|
+
urlCell.title = entry.url || '';
|
|
241
|
+
urlCell.style.cssText = 'padding: 6px 8px; max-width: 200px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;';
|
|
242
|
+
|
|
243
|
+
// Status cell
|
|
244
|
+
const statusCell = document.createElement('td');
|
|
245
|
+
statusCell.textContent = entry.status || '-';
|
|
246
|
+
statusCell.style.cssText = `padding: 6px 8px; color: ${getStatusColor(entry.status)}; font-weight: 600;`;
|
|
247
|
+
|
|
248
|
+
// Duration cell
|
|
249
|
+
const durationCell = document.createElement('td');
|
|
250
|
+
durationCell.textContent = formatDuration(entry.duration);
|
|
251
|
+
durationCell.style.cssText = 'padding: 6px 8px;';
|
|
252
|
+
|
|
253
|
+
// Size cell
|
|
254
|
+
const sizeCell = document.createElement('td');
|
|
255
|
+
sizeCell.textContent = formatSize(entry.transferSize || entry.encodedBodySize);
|
|
256
|
+
sizeCell.style.cssText = 'padding: 6px 8px;';
|
|
257
|
+
|
|
258
|
+
row.appendChild(methodCell);
|
|
259
|
+
row.appendChild(urlCell);
|
|
260
|
+
row.appendChild(statusCell);
|
|
261
|
+
row.appendChild(durationCell);
|
|
262
|
+
row.appendChild(sizeCell);
|
|
263
|
+
|
|
264
|
+
tableBody.appendChild(row);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
// Scroll to bottom
|
|
268
|
+
if (tableBody.parentElement) {
|
|
269
|
+
const content = tableBody.parentElement.parentElement;
|
|
270
|
+
if (content) {
|
|
271
|
+
content.scrollTop = content.scrollHeight;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Process network event
|
|
277
|
+
function processNetworkEvent(event) {
|
|
278
|
+
// Only handle plugin events for network
|
|
279
|
+
if (!event || event.type !== 6 || !event.data) return;
|
|
280
|
+
if (event.data?.plugin !== 'rrweb/network@1') return;
|
|
281
|
+
|
|
282
|
+
// Try to get payload - handle nested structure (double-wrapped during RRWeb serialization)
|
|
283
|
+
let payload = null;
|
|
284
|
+
|
|
285
|
+
// Check for nested structure first (double-wrapped during RRWeb serialization)
|
|
286
|
+
if (event.data?.payload?.data?.payload) {
|
|
287
|
+
const nestedPayload = event.data.payload.data.payload;
|
|
288
|
+
if (nestedPayload.type === 'request' || nestedPayload.type === 'response') {
|
|
289
|
+
payload = nestedPayload;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Fallback: Check for normal structure
|
|
294
|
+
if (!payload && event.data?.payload) {
|
|
295
|
+
const testPayload = event.data.payload;
|
|
296
|
+
if (testPayload.type === 'request' || testPayload.type === 'response') {
|
|
297
|
+
payload = testPayload;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (!payload) return;
|
|
302
|
+
|
|
303
|
+
// Verify payload has the expected structure
|
|
304
|
+
if (typeof payload.type !== 'string' || (payload.type !== 'request' && payload.type !== 'response')) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (payload.type === 'request') {
|
|
309
|
+
// Create panel if not exists and showPanel is enabled
|
|
310
|
+
if (showPanel && !panelElement) {
|
|
311
|
+
panelElement = createPanel();
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Store request entry
|
|
315
|
+
const entry = {
|
|
316
|
+
id: payload.id,
|
|
317
|
+
method: payload.method || 'GET',
|
|
318
|
+
url: payload.url || '',
|
|
319
|
+
timestamp: payload.timestamp || event.timestamp,
|
|
320
|
+
requestType: payload.requestType || 'unknown'
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
// Find existing entry or create new
|
|
324
|
+
const existingIndex = networkEntries.findIndex(e => e.id === entry.id);
|
|
325
|
+
if (existingIndex >= 0) {
|
|
326
|
+
Object.assign(networkEntries[existingIndex], entry);
|
|
327
|
+
} else {
|
|
328
|
+
networkEntries.push(entry);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Limit entries
|
|
332
|
+
if (networkEntries.length > maxEntries * 2) {
|
|
333
|
+
networkEntries.shift();
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// Update table if panel is shown
|
|
337
|
+
if (showPanel) {
|
|
338
|
+
updateTable();
|
|
339
|
+
}
|
|
340
|
+
} else if (payload.type === 'response') {
|
|
341
|
+
// Create panel if not exists and showPanel is enabled
|
|
342
|
+
if (showPanel && !panelElement) {
|
|
343
|
+
panelElement = createPanel();
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// Update existing entry with response data
|
|
347
|
+
const entryIndex = networkEntries.findIndex(e => e.id === payload.id);
|
|
348
|
+
if (entryIndex >= 0) {
|
|
349
|
+
const entry = networkEntries[entryIndex];
|
|
350
|
+
entry.status = payload.status;
|
|
351
|
+
entry.statusText = payload.statusText;
|
|
352
|
+
entry.duration = payload.duration;
|
|
353
|
+
entry.error = payload.error;
|
|
354
|
+
|
|
355
|
+
if (payload.performance) {
|
|
356
|
+
entry.transferSize = payload.performance.transferSize;
|
|
357
|
+
entry.encodedBodySize = payload.performance.encodedBodySize;
|
|
358
|
+
entry.decodedBodySize = payload.performance.decodedBodySize;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Update table if panel is shown
|
|
362
|
+
if (showPanel) {
|
|
363
|
+
updateTable();
|
|
364
|
+
}
|
|
365
|
+
} else {
|
|
366
|
+
// Response without request (shouldn't happen, but handle it)
|
|
367
|
+
// Create panel if not exists and showPanel is enabled
|
|
368
|
+
if (showPanel && !panelElement) {
|
|
369
|
+
panelElement = createPanel();
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const entry = {
|
|
373
|
+
id: payload.id,
|
|
374
|
+
method: 'UNKNOWN',
|
|
375
|
+
url: '',
|
|
376
|
+
status: payload.status,
|
|
377
|
+
statusText: payload.statusText,
|
|
378
|
+
duration: payload.duration,
|
|
379
|
+
timestamp: payload.timestamp || event.timestamp,
|
|
380
|
+
error: payload.error
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
if (payload.performance) {
|
|
384
|
+
entry.transferSize = payload.performance.transferSize;
|
|
385
|
+
entry.encodedBodySize = payload.performance.encodedBodySize;
|
|
386
|
+
entry.decodedBodySize = payload.performance.decodedBodySize;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
networkEntries.push(entry);
|
|
390
|
+
|
|
391
|
+
if (networkEntries.length > maxEntries * 2) {
|
|
392
|
+
networkEntries.shift();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Update table if panel is shown
|
|
396
|
+
if (showPanel) {
|
|
397
|
+
updateTable();
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
name: 'rrweb/network@1',
|
|
405
|
+
handler(event, isSync, context) {
|
|
406
|
+
// Only handle plugin events for network
|
|
407
|
+
if (event.type !== 6 || event.data?.plugin !== 'rrweb/network@1') {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Process the network event
|
|
412
|
+
processNetworkEvent(event);
|
|
413
|
+
},
|
|
414
|
+
onBuild() {
|
|
415
|
+
// Create panel when player is built
|
|
416
|
+
if (showPanel && !panelElement) {
|
|
417
|
+
panelElement = createPanel();
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
onDestroy() {
|
|
421
|
+
// Cleanup panel when player is destroyed
|
|
422
|
+
if (panelElement && panelElement.parentElement) {
|
|
423
|
+
panelElement.parentElement.removeChild(panelElement);
|
|
424
|
+
panelElement = null;
|
|
425
|
+
tableBody = null;
|
|
426
|
+
}
|
|
427
|
+
networkEntries.length = 0;
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Export function
|
|
433
|
+
return getReplayNetworkPlugin;
|
|
434
|
+
}));
|