shokupan 0.7.0 → 0.9.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/README.md +53 -0
- package/dist/context.d.ts +50 -15
- package/dist/{http-server-DFhwlK8e.cjs → http-server-BEMPIs33.cjs} +4 -2
- package/dist/http-server-BEMPIs33.cjs.map +1 -0
- package/dist/{http-server-0xH174zz.js → http-server-CCeagTyU.js} +4 -2
- package/dist/http-server-CCeagTyU.js.map +1 -0
- package/dist/index.cjs +998 -136
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +996 -135
- package/dist/index.js.map +1 -1
- package/dist/plugins/application/dashboard/metrics-collector.d.ts +12 -0
- package/dist/plugins/application/dashboard/plugin.d.ts +14 -8
- package/dist/plugins/application/dashboard/static/charts.js +328 -0
- package/dist/plugins/application/dashboard/static/failures.js +85 -0
- package/dist/plugins/application/dashboard/static/graph.mjs +523 -0
- package/dist/plugins/application/dashboard/static/poll.js +146 -0
- package/dist/plugins/application/dashboard/static/reactflow.css +18 -0
- package/dist/plugins/application/dashboard/static/registry.css +131 -0
- package/dist/plugins/application/dashboard/static/registry.js +269 -0
- package/dist/plugins/application/dashboard/static/requests.js +118 -0
- package/dist/plugins/application/dashboard/static/scrollbar.css +24 -0
- package/dist/plugins/application/dashboard/static/styles.css +175 -0
- package/dist/plugins/application/dashboard/static/tables.js +92 -0
- package/dist/plugins/application/dashboard/static/tabs.js +113 -0
- package/dist/plugins/application/dashboard/static/tabulator.css +66 -0
- package/dist/plugins/application/dashboard/template.eta +246 -0
- package/dist/plugins/application/socket-io.d.ts +14 -0
- package/dist/router.d.ts +12 -0
- package/dist/shokupan.d.ts +21 -1
- package/dist/util/datastore.d.ts +4 -3
- package/dist/util/decorators.d.ts +5 -0
- package/dist/util/http-error.d.ts +38 -0
- package/dist/util/http-status.d.ts +30 -0
- package/dist/util/request.d.ts +1 -1
- package/dist/util/symbol.d.ts +19 -0
- package/dist/util/types.d.ts +30 -1
- package/package.json +6 -3
- package/dist/http-server-0xH174zz.js.map +0 -1
- package/dist/http-server-DFhwlK8e.cjs.map +0 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export declare class MetricsCollector {
|
|
2
|
+
private currentIntervalStart;
|
|
3
|
+
private pendingDetails;
|
|
4
|
+
private eventLoopHistogram;
|
|
5
|
+
private timer;
|
|
6
|
+
constructor();
|
|
7
|
+
recordRequest(duration: number, isError: boolean): void;
|
|
8
|
+
private alignTimestamp;
|
|
9
|
+
private collect;
|
|
10
|
+
private flushInterval;
|
|
11
|
+
stop(): void;
|
|
12
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import { ShokupanHooks } from '../../../util/types';
|
|
1
|
+
import { HeadersInit } from 'bun';
|
|
2
|
+
import { ShokupanHooks, ShokupanPlugin } from '../../../util/types';
|
|
4
3
|
export interface RequestLog {
|
|
5
4
|
method: string;
|
|
6
5
|
url: string;
|
|
@@ -10,22 +9,28 @@ export interface RequestLog {
|
|
|
10
9
|
handlerStack?: any[];
|
|
11
10
|
}
|
|
12
11
|
export interface DashboardConfig {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
*/
|
|
16
|
-
getHeaders?: (ctx: ShokupanContext) => Record<string, string>;
|
|
12
|
+
getRequestHeaders?: () => HeadersInit;
|
|
13
|
+
path?: string;
|
|
17
14
|
/**
|
|
18
15
|
* Retention time in milliseconds
|
|
19
16
|
*/
|
|
20
17
|
retentionMs?: number;
|
|
21
18
|
}
|
|
22
|
-
export declare class Dashboard
|
|
19
|
+
export declare class Dashboard implements ShokupanPlugin {
|
|
23
20
|
private readonly dashboardConfig;
|
|
21
|
+
private static __dirname;
|
|
22
|
+
private static getBasePath;
|
|
23
|
+
private router;
|
|
24
24
|
private metrics;
|
|
25
25
|
private eta;
|
|
26
26
|
private startTime;
|
|
27
27
|
private instrumented;
|
|
28
|
+
private metricsCollector;
|
|
28
29
|
constructor(dashboardConfig?: DashboardConfig);
|
|
30
|
+
onInit(app: any, options?: {
|
|
31
|
+
path?: string;
|
|
32
|
+
}): void;
|
|
33
|
+
private setupRoutes;
|
|
29
34
|
private instrumentApp;
|
|
30
35
|
private assignIdsToRegistry;
|
|
31
36
|
recordNodeMetric(id: string, type: string, duration: number, isError: boolean): void;
|
|
@@ -34,3 +39,4 @@ export declare class Dashboard extends ShokupanRouter {
|
|
|
34
39
|
getHooks(): ShokupanHooks;
|
|
35
40
|
private updateTiming;
|
|
36
41
|
}
|
|
42
|
+
export default function DebugDashboard(config?: DashboardConfig): Dashboard;
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
|
|
2
|
+
// Common chart config
|
|
3
|
+
const commonOptions = {
|
|
4
|
+
responsive: true,
|
|
5
|
+
maintainAspectRatio: false,
|
|
6
|
+
plugins: {
|
|
7
|
+
legend: { labels: { color: '#94a3b8' } }
|
|
8
|
+
},
|
|
9
|
+
scales: {
|
|
10
|
+
x: {
|
|
11
|
+
ticks: { color: '#94a3b8' },
|
|
12
|
+
grid: { color: '#334155' }
|
|
13
|
+
},
|
|
14
|
+
y: {
|
|
15
|
+
ticks: { color: '#94a3b8' },
|
|
16
|
+
grid: { color: '#334155' },
|
|
17
|
+
beginAtZero: true
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
animation: { duration: 0 },
|
|
21
|
+
interaction: {
|
|
22
|
+
mode: 'index',
|
|
23
|
+
intersect: false,
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Get request headers from global function if available
|
|
28
|
+
const headers = typeof getRequestHeaders !== 'undefined' ? getRequestHeaders() : {};
|
|
29
|
+
|
|
30
|
+
// Determine base path for API requests
|
|
31
|
+
const basePath = window.location.pathname.endsWith('/') ? window.location.pathname.slice(0, -1) : window.location.pathname;
|
|
32
|
+
const url = basePath + '/';
|
|
33
|
+
|
|
34
|
+
// Chart instances
|
|
35
|
+
|
|
36
|
+
// --- Latency Chart ---
|
|
37
|
+
const latencyCtx = document.getElementById('latencyChart').getContext('2d');
|
|
38
|
+
const latencyChart = new Chart(latencyCtx, {
|
|
39
|
+
type: 'line',
|
|
40
|
+
data: {
|
|
41
|
+
labels: [],
|
|
42
|
+
datasets: [
|
|
43
|
+
{
|
|
44
|
+
label: 'Msg (Avg)',
|
|
45
|
+
data: [],
|
|
46
|
+
borderColor: '#3b82f6',
|
|
47
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
48
|
+
borderWidth: 2,
|
|
49
|
+
tension: 0.4,
|
|
50
|
+
fill: false
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
label: 'p95',
|
|
54
|
+
data: [],
|
|
55
|
+
borderColor: '#eab308',
|
|
56
|
+
backgroundColor: 'rgba(234, 179, 8, 0.1)',
|
|
57
|
+
borderWidth: 2,
|
|
58
|
+
tension: 0.4,
|
|
59
|
+
fill: false
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
label: 'p99',
|
|
63
|
+
data: [],
|
|
64
|
+
borderColor: '#ef4444',
|
|
65
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
66
|
+
borderWidth: 2,
|
|
67
|
+
tension: 0.4,
|
|
68
|
+
fill: false
|
|
69
|
+
}
|
|
70
|
+
]
|
|
71
|
+
},
|
|
72
|
+
options: commonOptions
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// --- RPS Chart ---
|
|
76
|
+
const rpsCtx = document.getElementById('rpsChart').getContext('2d');
|
|
77
|
+
const rpsChart = new Chart(rpsCtx, {
|
|
78
|
+
type: 'line',
|
|
79
|
+
data: {
|
|
80
|
+
labels: [],
|
|
81
|
+
datasets: [
|
|
82
|
+
{
|
|
83
|
+
label: 'Total Requests',
|
|
84
|
+
data: [],
|
|
85
|
+
borderColor: '#10b981',
|
|
86
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
87
|
+
borderWidth: 2,
|
|
88
|
+
tension: 0.4,
|
|
89
|
+
fill: true
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
label: 'Errors',
|
|
93
|
+
data: [],
|
|
94
|
+
borderColor: '#ef4444',
|
|
95
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
96
|
+
borderWidth: 2,
|
|
97
|
+
tension: 0.4,
|
|
98
|
+
fill: true
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
},
|
|
102
|
+
options: commonOptions
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// --- CPU Chart ---
|
|
106
|
+
const cpuCtx = document.getElementById('cpuChart').getContext('2d');
|
|
107
|
+
const cpuChart = new Chart(cpuCtx, {
|
|
108
|
+
type: 'line',
|
|
109
|
+
data: {
|
|
110
|
+
labels: [],
|
|
111
|
+
datasets: [
|
|
112
|
+
{
|
|
113
|
+
label: 'CPU Load (1m)',
|
|
114
|
+
data: [],
|
|
115
|
+
borderColor: '#8b5cf6',
|
|
116
|
+
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
|
117
|
+
borderWidth: 2,
|
|
118
|
+
tension: 0.4,
|
|
119
|
+
fill: true
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
},
|
|
123
|
+
options: commonOptions
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// --- Memory Chart ---
|
|
127
|
+
const memoryCtx = document.getElementById('memoryChart').getContext('2d');
|
|
128
|
+
const memoryChart = new Chart(memoryCtx, {
|
|
129
|
+
type: 'line',
|
|
130
|
+
data: {
|
|
131
|
+
labels: [],
|
|
132
|
+
datasets: [
|
|
133
|
+
{
|
|
134
|
+
label: 'Heap Used (MB)',
|
|
135
|
+
data: [],
|
|
136
|
+
borderColor: '#f97316',
|
|
137
|
+
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
|
138
|
+
borderWidth: 2,
|
|
139
|
+
tension: 0.4,
|
|
140
|
+
fill: true
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
label: 'RSS (MB)',
|
|
144
|
+
data: [],
|
|
145
|
+
borderColor: '#06b6d4',
|
|
146
|
+
backgroundColor: 'rgba(6, 182, 212, 0.1)',
|
|
147
|
+
borderWidth: 2,
|
|
148
|
+
tension: 0.4,
|
|
149
|
+
fill: true
|
|
150
|
+
}
|
|
151
|
+
]
|
|
152
|
+
},
|
|
153
|
+
options: commonOptions
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// --- Heap Chart ---
|
|
157
|
+
const heapCtx = document.getElementById('heapChart').getContext('2d');
|
|
158
|
+
const heapChart = new Chart(heapCtx, {
|
|
159
|
+
type: 'line',
|
|
160
|
+
data: {
|
|
161
|
+
labels: [],
|
|
162
|
+
datasets: [
|
|
163
|
+
{
|
|
164
|
+
label: 'Heap Used (MB)',
|
|
165
|
+
data: [],
|
|
166
|
+
borderColor: '#f97316',
|
|
167
|
+
backgroundColor: 'rgba(249, 115, 22, 0.1)',
|
|
168
|
+
borderWidth: 2,
|
|
169
|
+
tension: 0.4,
|
|
170
|
+
fill: true
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
label: 'Heap Total (MB)',
|
|
174
|
+
data: [],
|
|
175
|
+
borderColor: '#fb923c',
|
|
176
|
+
backgroundColor: 'rgba(251, 146, 60, 0.1)',
|
|
177
|
+
borderWidth: 2,
|
|
178
|
+
tension: 0.4,
|
|
179
|
+
fill: false
|
|
180
|
+
}
|
|
181
|
+
]
|
|
182
|
+
},
|
|
183
|
+
options: commonOptions
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// --- Event Loop Latency Chart ---
|
|
187
|
+
const eventLoopCtx = document.getElementById('eventLoopChart').getContext('2d');
|
|
188
|
+
const eventLoopChart = new Chart(eventLoopCtx, {
|
|
189
|
+
type: 'line',
|
|
190
|
+
data: {
|
|
191
|
+
labels: [],
|
|
192
|
+
datasets: [
|
|
193
|
+
{
|
|
194
|
+
label: 'Mean (ms)',
|
|
195
|
+
data: [],
|
|
196
|
+
borderColor: '#3b82f6',
|
|
197
|
+
backgroundColor: 'rgba(59, 130, 246, 0.1)',
|
|
198
|
+
borderWidth: 2,
|
|
199
|
+
tension: 0.4,
|
|
200
|
+
fill: false
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: 'p95 (ms)',
|
|
204
|
+
data: [],
|
|
205
|
+
borderColor: '#eab308',
|
|
206
|
+
backgroundColor: 'rgba(234, 179, 8, 0.1)',
|
|
207
|
+
borderWidth: 2,
|
|
208
|
+
tension: 0.4,
|
|
209
|
+
fill: false
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
label: 'p99 (ms)',
|
|
213
|
+
data: [],
|
|
214
|
+
borderColor: '#ef4444',
|
|
215
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
216
|
+
borderWidth: 2,
|
|
217
|
+
tension: 0.4,
|
|
218
|
+
fill: false
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
},
|
|
222
|
+
options: commonOptions
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// --- Error Rate Chart ---
|
|
226
|
+
const errorRateCtx = document.getElementById('errorRateChart').getContext('2d');
|
|
227
|
+
const errorRateChart = new Chart(errorRateCtx, {
|
|
228
|
+
type: 'line',
|
|
229
|
+
data: {
|
|
230
|
+
labels: [],
|
|
231
|
+
datasets: [
|
|
232
|
+
{
|
|
233
|
+
label: 'Success Rate (%)',
|
|
234
|
+
data: [],
|
|
235
|
+
borderColor: '#10b981',
|
|
236
|
+
backgroundColor: 'rgba(16, 185, 129, 0.1)',
|
|
237
|
+
borderWidth: 2,
|
|
238
|
+
tension: 0.4,
|
|
239
|
+
fill: true
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
label: 'Error Rate (%)',
|
|
243
|
+
data: [],
|
|
244
|
+
borderColor: '#ef4444',
|
|
245
|
+
backgroundColor: 'rgba(239, 68, 68, 0.1)',
|
|
246
|
+
borderWidth: 2,
|
|
247
|
+
tension: 0.4,
|
|
248
|
+
fill: true
|
|
249
|
+
}
|
|
250
|
+
]
|
|
251
|
+
},
|
|
252
|
+
options: commonOptions
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
async function updateCharts() {
|
|
256
|
+
const period = document.getElementById('time-range-selector').value || '1m';
|
|
257
|
+
try {
|
|
258
|
+
const res = await fetch(`${url}metrics/history?interval=${period}`, { headers });
|
|
259
|
+
const data = await res.json();
|
|
260
|
+
const metrics = data.metrics || [];
|
|
261
|
+
|
|
262
|
+
const labels = metrics.map(m => new Date(m.timestamp).toLocaleTimeString());
|
|
263
|
+
|
|
264
|
+
// Latency
|
|
265
|
+
latencyChart.data.labels = labels;
|
|
266
|
+
latencyChart.data.datasets[0].data = metrics.map(m => m.responseTime.avg);
|
|
267
|
+
latencyChart.data.datasets[1].data = metrics.map(m => m.responseTime.p95);
|
|
268
|
+
latencyChart.data.datasets[2].data = metrics.map(m => m.responseTime.p99);
|
|
269
|
+
latencyChart.update();
|
|
270
|
+
|
|
271
|
+
// RPS
|
|
272
|
+
rpsChart.data.labels = labels;
|
|
273
|
+
rpsChart.data.datasets[0].data = metrics.map(m => m.requests.total);
|
|
274
|
+
rpsChart.data.datasets[1].data = metrics.map(m => m.requests.error);
|
|
275
|
+
rpsChart.update();
|
|
276
|
+
|
|
277
|
+
// CPU
|
|
278
|
+
cpuChart.data.labels = labels;
|
|
279
|
+
cpuChart.data.datasets[0].data = metrics.map(m => m.cpu);
|
|
280
|
+
cpuChart.update();
|
|
281
|
+
|
|
282
|
+
// Memory
|
|
283
|
+
memoryChart.data.labels = labels;
|
|
284
|
+
memoryChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
|
|
285
|
+
memoryChart.data.datasets[1].data = metrics.map(m => (m.memory.used / 1024 / 1024).toFixed(2));
|
|
286
|
+
memoryChart.update();
|
|
287
|
+
|
|
288
|
+
// Heap
|
|
289
|
+
heapChart.data.labels = labels;
|
|
290
|
+
heapChart.data.datasets[0].data = metrics.map(m => (m.memory.heapUsed / 1024 / 1024).toFixed(2));
|
|
291
|
+
heapChart.data.datasets[1].data = metrics.map(m => (m.memory.heapTotal / 1024 / 1024).toFixed(2));
|
|
292
|
+
heapChart.update();
|
|
293
|
+
|
|
294
|
+
// Event Loop Latency
|
|
295
|
+
eventLoopChart.data.labels = labels;
|
|
296
|
+
eventLoopChart.data.datasets[0].data = metrics.map(m => m.eventLoopLatency.mean);
|
|
297
|
+
eventLoopChart.data.datasets[1].data = metrics.map(m => m.eventLoopLatency.p95);
|
|
298
|
+
eventLoopChart.data.datasets[2].data = metrics.map(m => m.eventLoopLatency.p99);
|
|
299
|
+
eventLoopChart.update();
|
|
300
|
+
|
|
301
|
+
// Error Rate
|
|
302
|
+
errorRateChart.data.labels = labels;
|
|
303
|
+
errorRateChart.data.datasets[0].data = metrics.map(m => {
|
|
304
|
+
const total = m.requests.success + m.requests.error;
|
|
305
|
+
return total > 0 ? ((m.requests.success / total) * 100).toFixed(2) : 100;
|
|
306
|
+
});
|
|
307
|
+
errorRateChart.data.datasets[1].data = metrics.map(m => {
|
|
308
|
+
const total = m.requests.success + m.requests.error;
|
|
309
|
+
return total > 0 ? ((m.requests.error / total) * 100).toFixed(2) : 0;
|
|
310
|
+
});
|
|
311
|
+
errorRateChart.update();
|
|
312
|
+
|
|
313
|
+
} catch (e) {
|
|
314
|
+
console.error("Failed to fetch metrics", e);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Initial load
|
|
319
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
320
|
+
updateCharts();
|
|
321
|
+
// Poll every 10s for short intervals
|
|
322
|
+
setInterval(() => {
|
|
323
|
+
const period = document.getElementById('time-range-selector').value;
|
|
324
|
+
if (['1m', '5m', '30m', '1h', '2h'].includes(period)) {
|
|
325
|
+
updateCharts();
|
|
326
|
+
}
|
|
327
|
+
}, 10000);
|
|
328
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
|
|
2
|
+
// --- Failures Tab Logic ---
|
|
3
|
+
const failuresTable = new Tabulator("#failures-table-container", {
|
|
4
|
+
layout: "fitColumns",
|
|
5
|
+
height: "500px",
|
|
6
|
+
placeholder: "No failed requests found",
|
|
7
|
+
data: [],
|
|
8
|
+
columns: [
|
|
9
|
+
{ title: "Time", field: "timestamp", width: 180, formatter: (cell) => new Date(cell.getValue()).toLocaleString() },
|
|
10
|
+
{ title: "Method", field: "method", width: 100 },
|
|
11
|
+
{ title: "URL", field: "url" },
|
|
12
|
+
{ title: "Status", field: "status", width: 90, formatter: (cell) => `<span style="color: #ef4444; font-weight: bold;">${cell.getValue()}</span>` },
|
|
13
|
+
{
|
|
14
|
+
title: "Actions", formatter: (cell) => {
|
|
15
|
+
return `
|
|
16
|
+
<button class="replay-btn" style="background:#3b82f6; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer; margin-right:4px;">Replay</button>
|
|
17
|
+
<button class="export-btn" style="background:#64748b; color:white; border:none; padding:4px 8px; border-radius:4px; cursor:pointer;">Export</button>
|
|
18
|
+
`;
|
|
19
|
+
}, cellClick: (e, cell) => {
|
|
20
|
+
if (e.target.classList.contains('replay-btn')) {
|
|
21
|
+
replayRequest(cell.getRow().getData());
|
|
22
|
+
} else if (e.target.classList.contains('export-btn')) {
|
|
23
|
+
exportFailure(cell.getRow().getData());
|
|
24
|
+
}
|
|
25
|
+
}, width: 140, headerSort: false
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
function exportFailure(data) {
|
|
32
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
33
|
+
const url = URL.createObjectURL(blob);
|
|
34
|
+
const a = document.createElement('a');
|
|
35
|
+
a.href = url;
|
|
36
|
+
a.download = `failure-${data.timestamp}.json`;
|
|
37
|
+
a.click();
|
|
38
|
+
URL.revokeObjectURL(url);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function importFailure() {
|
|
42
|
+
const input = document.createElement('input');
|
|
43
|
+
input.type = 'file';
|
|
44
|
+
input.accept = '.json';
|
|
45
|
+
input.onchange = e => {
|
|
46
|
+
const file = e.target.files[0];
|
|
47
|
+
if (!file) return;
|
|
48
|
+
const reader = new FileReader();
|
|
49
|
+
reader.onload = ev => {
|
|
50
|
+
try {
|
|
51
|
+
const data = JSON.parse(ev.target.result);
|
|
52
|
+
replayRequest(data);
|
|
53
|
+
} catch (err) { alert("Invalid JSON: " + err); }
|
|
54
|
+
};
|
|
55
|
+
reader.readAsText(file);
|
|
56
|
+
};
|
|
57
|
+
input.click();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function replayRequest(req) {
|
|
61
|
+
if (!confirm(`Replay ${req.method} ${req.url}?`)) return;
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const headers = getRequestHeaders ? getRequestHeaders() : {};
|
|
65
|
+
const basePath = window.location.pathname.endsWith('/') ? '' : window.location.pathname;
|
|
66
|
+
const url = basePath + (basePath.endsWith('/') ? 'replay' : '/replay');
|
|
67
|
+
|
|
68
|
+
const res = await fetch(url, {
|
|
69
|
+
method: 'POST',
|
|
70
|
+
headers: { ...headers, 'Content-Type': 'application/json' },
|
|
71
|
+
body: JSON.stringify({
|
|
72
|
+
method: req.method,
|
|
73
|
+
url: req.url,
|
|
74
|
+
headers: req.headers,
|
|
75
|
+
body: req.body
|
|
76
|
+
})
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const result = await res.json();
|
|
80
|
+
alert(`Replay Status: ${res.status}\n\nResponse:\n${JSON.stringify(result, null, 2)}`);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
alert("Replay Failed: " + e);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|