ddapm-test-agent 1.36.0__py3-none-any.whl → 1.37.0__py3-none-any.whl
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.
- ddapm_test_agent/agent.py +98 -13
- ddapm_test_agent/remoteconfig.py +2 -2
- ddapm_test_agent/static/style.css +1679 -0
- ddapm_test_agent/templates/base.html +31 -0
- ddapm_test_agent/templates/config.html +440 -0
- ddapm_test_agent/templates/dashboard.html +90 -0
- ddapm_test_agent/templates/macros.html +40 -0
- ddapm_test_agent/templates/requests.html +1925 -0
- ddapm_test_agent/templates/session_detail.html +37 -0
- ddapm_test_agent/templates/sessions.html +23 -0
- ddapm_test_agent/templates/snapshot_detail.html +410 -0
- ddapm_test_agent/templates/snapshots.html +86 -0
- ddapm_test_agent/templates/trace_detail.html +37 -0
- ddapm_test_agent/templates/tracer_flares.html +640 -0
- ddapm_test_agent/templates/traces.html +24 -0
- ddapm_test_agent/web.py +1523 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.37.0.dist-info}/METADATA +14 -4
- ddapm_test_agent-1.37.0.dist-info/RECORD +40 -0
- ddapm_test_agent-1.36.0.dist-info/RECORD +0 -26
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.37.0.dist-info}/WHEEL +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.37.0.dist-info}/entry_points.txt +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.37.0.dist-info}/licenses/LICENSE.BSD3 +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.37.0.dist-info}/licenses/LICENSE.apache2 +0 -0
- {ddapm_test_agent-1.36.0.dist-info → ddapm_test_agent-1.37.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1925 @@
|
|
|
1
|
+
{% extends "base.html" %}
|
|
2
|
+
|
|
3
|
+
{% block content %}
|
|
4
|
+
<div class="requests-page">
|
|
5
|
+
<div class="page-header">
|
|
6
|
+
<h2>Requests</h2>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<div class="requests-container">
|
|
10
|
+
<div class="requests-controls">
|
|
11
|
+
<div class="filter-row">
|
|
12
|
+
<input type="text" id="filter-query" placeholder="Filter: method:POST -path:/info headers.user-agent:curl status:200..." class="filter-query-input">
|
|
13
|
+
<div class="tooltip-wrapper">
|
|
14
|
+
<button class="action-button small" onclick="clearAllFilters()">Clear</button>
|
|
15
|
+
<span class="tooltip-text">Clear all filters</span>
|
|
16
|
+
</div>
|
|
17
|
+
<div class="tooltip-wrapper">
|
|
18
|
+
<button class="action-button small" onclick="showFilterHelp()">Help</button>
|
|
19
|
+
<span class="tooltip-text">Show filter syntax guide</span>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="stats-row">
|
|
23
|
+
<div class="request-stats">
|
|
24
|
+
<span class="stat-item">Total: <span id="total-count">{{ requests|length }}</span></span>
|
|
25
|
+
<span class="stat-item">Showing: <span id="visible-count">{{ requests|length }}</span></span>
|
|
26
|
+
</div>
|
|
27
|
+
<div class="action-controls">
|
|
28
|
+
<div class="tooltip-wrapper">
|
|
29
|
+
<button class="action-button small" onclick="refreshRequests()">Refresh</button>
|
|
30
|
+
<span class="tooltip-text">Reload all requests from server</span>
|
|
31
|
+
</div>
|
|
32
|
+
<div class="tooltip-wrapper">
|
|
33
|
+
<button id="pause-sse-btn" class="action-button small" onclick="toggleSSE()">Pause</button>
|
|
34
|
+
<span class="tooltip-text">Pause/resume live request streaming</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div class="tooltip-wrapper">
|
|
37
|
+
<button class="action-button small" onclick="downloadRequests()">Download</button>
|
|
38
|
+
<span class="tooltip-text">Download visible requests as JSON</span>
|
|
39
|
+
</div>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</div>
|
|
43
|
+
|
|
44
|
+
{% if requests %}
|
|
45
|
+
<div class="requests-table-container">
|
|
46
|
+
<table class="requests-table">
|
|
47
|
+
<thead>
|
|
48
|
+
<tr>
|
|
49
|
+
<th>Time</th>
|
|
50
|
+
<th>Method</th>
|
|
51
|
+
<th>Path</th>
|
|
52
|
+
<th>Size</th>
|
|
53
|
+
</tr>
|
|
54
|
+
</thead>
|
|
55
|
+
<tbody>
|
|
56
|
+
{% for req in requests %}
|
|
57
|
+
<tr class="request-row" onclick="toggleRequestDetails('{{ loop.index0 }}')" style="cursor: pointer;">
|
|
58
|
+
<td class="timestamp-cell">
|
|
59
|
+
{{ req.timestamp | timestamp_format if req.timestamp }}
|
|
60
|
+
</td>
|
|
61
|
+
<td class="method-cell">
|
|
62
|
+
<span class="method-badge method-{{ req.method.lower() }}">{{ req.method }}</span>
|
|
63
|
+
</td>
|
|
64
|
+
<td class="path-cell">
|
|
65
|
+
<div class="path-info">
|
|
66
|
+
<span class="path">{{ req.path }}</span>
|
|
67
|
+
{% if req.query_string %}
|
|
68
|
+
<span class="query-string">?{{ req.query_string }}</span>
|
|
69
|
+
{% endif %}
|
|
70
|
+
{% if req.session_token %}
|
|
71
|
+
<span class="session-token">session: {{ req.session_token }}</span>
|
|
72
|
+
{% endif %}
|
|
73
|
+
</div>
|
|
74
|
+
</td>
|
|
75
|
+
<td class="size-cell">
|
|
76
|
+
{{ req.content_length }} bytes
|
|
77
|
+
</td>
|
|
78
|
+
</tr>
|
|
79
|
+
<tr class="request-details" id="details-{{ loop.index0 }}" style="display: none;">
|
|
80
|
+
<td colspan="4">
|
|
81
|
+
<div class="request-details-content">
|
|
82
|
+
<div class="details-tabs">
|
|
83
|
+
<button class="tab-button active" onclick="switchTab('{{ loop.index0 }}', 'request')">Request</button>
|
|
84
|
+
<button class="tab-button" onclick="switchTab('{{ loop.index0 }}', 'response')">Response</button>
|
|
85
|
+
{% if req.trace_data %}
|
|
86
|
+
<button class="tab-button" onclick="switchTab('{{ loop.index0 }}', 'traces')">Traces</button>
|
|
87
|
+
{% endif %}
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
<div class="tab-content" id="request-tab-{{ loop.index0 }}">
|
|
91
|
+
<div class="details-section">
|
|
92
|
+
<h4>Headers</h4>
|
|
93
|
+
<div class="metadata-grid">
|
|
94
|
+
{% for header, value in req.headers.items() %}
|
|
95
|
+
<div class="metadata-item">
|
|
96
|
+
<span class="metadata-label">{{ header }}:</span>
|
|
97
|
+
<span class="metadata-value">{{ value }}</span>
|
|
98
|
+
</div>
|
|
99
|
+
{% endfor %}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
{% if req.query_params %}
|
|
103
|
+
<div class="details-section">
|
|
104
|
+
<h4>Query Parameters</h4>
|
|
105
|
+
<div class="metadata-grid">
|
|
106
|
+
{% for param_name, param_values in req.query_params.items() %}
|
|
107
|
+
<div class="metadata-item">
|
|
108
|
+
<span class="metadata-label">{{ param_name }}:</span>
|
|
109
|
+
<span class="metadata-value">{{ param_values | join(', ') }}</span>
|
|
110
|
+
</div>
|
|
111
|
+
{% endfor %}
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
{% endif %}
|
|
115
|
+
{% if req.body %}
|
|
116
|
+
<div class="details-section">
|
|
117
|
+
<h4>Body</h4>
|
|
118
|
+
<div class="body-container">
|
|
119
|
+
{% if req.multipart_data %}
|
|
120
|
+
<div class="body-section">
|
|
121
|
+
<h5>Multipart Form Data</h5>
|
|
122
|
+
<div class="multipart-data">
|
|
123
|
+
{% for key, value in req.multipart_data.items() %}
|
|
124
|
+
<div class="multipart-field">
|
|
125
|
+
<div class="field-header">
|
|
126
|
+
<strong>{{ key }}:</strong>
|
|
127
|
+
{% if key == 'flare_file' %}
|
|
128
|
+
<span class="field-type">[ZIP file, {{ value|length }} chars base64]</span>
|
|
129
|
+
{% else %}
|
|
130
|
+
<span class="field-type">[text]</span>
|
|
131
|
+
{% endif %}
|
|
132
|
+
</div>
|
|
133
|
+
<div class="field-value">
|
|
134
|
+
<pre><code>{% if key == 'flare_file' %}{{ value[:500] }}{% if value|length > 500 %}...{% endif %}{% else %}{{ value }}{% endif %}</code></pre>
|
|
135
|
+
</div>
|
|
136
|
+
</div>
|
|
137
|
+
{% endfor %}
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
{% else %}
|
|
141
|
+
{% if not (req.content_type and 'multipart' in req.content_type.lower()) %}
|
|
142
|
+
<div class="body-section">
|
|
143
|
+
<h5>Raw</h5>
|
|
144
|
+
<div class="request-body">
|
|
145
|
+
{% if req.content_type and 'msgpack' in req.content_type.lower() %}
|
|
146
|
+
<pre><code class="raw-bytes" data-body="{{ req.body }}">Loading hex view...</code></pre>
|
|
147
|
+
{% else %}
|
|
148
|
+
<pre><code>{{ req.body }}</code></pre>
|
|
149
|
+
{% endif %}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
{% endif %}
|
|
153
|
+
{% if req.content_type and ('json' in req.content_type.lower() or 'msgpack' in req.content_type.lower() or 'multipart' in req.content_type.lower()) %}
|
|
154
|
+
<div class="body-section">
|
|
155
|
+
<h5>Decoded</h5>
|
|
156
|
+
<div class="decoded-body" data-content-type="{{ req.content_type }}" data-body="{{ req.body }}">
|
|
157
|
+
<!-- Decoded content will be inserted here by JavaScript -->
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
{% endif %}
|
|
161
|
+
{% endif %}
|
|
162
|
+
</div>
|
|
163
|
+
</div>
|
|
164
|
+
{% endif %}
|
|
165
|
+
</div>
|
|
166
|
+
|
|
167
|
+
<div class="tab-content" id="response-tab-{{ loop.index0 }}" style="display: none;">
|
|
168
|
+
<div class="details-section">
|
|
169
|
+
<h4>Response Headers</h4>
|
|
170
|
+
<div class="metadata-grid">
|
|
171
|
+
<div class="metadata-item">
|
|
172
|
+
<span class="metadata-label">Status:</span>
|
|
173
|
+
<span class="metadata-value">{{ req.response.status }}</span>
|
|
174
|
+
</div>
|
|
175
|
+
<div class="metadata-item">
|
|
176
|
+
<span class="metadata-label">Content-Type:</span>
|
|
177
|
+
<span class="metadata-value">{{ req.response.content_type or 'text/plain' }}</span>
|
|
178
|
+
</div>
|
|
179
|
+
{% for header, value in req.response.headers.items() %}
|
|
180
|
+
<div class="metadata-item">
|
|
181
|
+
<span class="metadata-label">{{ header }}:</span>
|
|
182
|
+
<span class="metadata-value">{{ value }}</span>
|
|
183
|
+
</div>
|
|
184
|
+
{% endfor %}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
<div class="details-section">
|
|
188
|
+
<h4>Response Body</h4>
|
|
189
|
+
<div class="body-container">
|
|
190
|
+
{% if not (req.response.content_type and 'multipart' in req.response.content_type.lower()) %}
|
|
191
|
+
<div class="body-section">
|
|
192
|
+
<h5>Raw</h5>
|
|
193
|
+
<div class="response-body">
|
|
194
|
+
{% if req.response.body_is_binary %}
|
|
195
|
+
<pre><code class="raw-bytes" data-body="{{ req.response.body }}">Loading hex view...</code></pre>
|
|
196
|
+
{% else %}
|
|
197
|
+
<pre><code>{{ req.response.body or 'Empty response body' }}</code></pre>
|
|
198
|
+
{% endif %}
|
|
199
|
+
</div>
|
|
200
|
+
</div>
|
|
201
|
+
{% endif %}
|
|
202
|
+
{% if req.response.content_type and ('json' in req.response.content_type.lower() or 'msgpack' in req.response.content_type.lower() or 'multipart' in req.response.content_type.lower()) %}
|
|
203
|
+
<div class="body-section">
|
|
204
|
+
<h5>Decoded</h5>
|
|
205
|
+
{% if req.response.body_is_binary %}
|
|
206
|
+
<div class="decoded-body" data-content-type="{{ req.response.content_type }}" data-body="{{ req.response.body }}" data-is-binary="true">
|
|
207
|
+
{% else %}
|
|
208
|
+
<div class="decoded-body" data-content-type="{{ req.response.content_type }}" data-body="{{ req.response.body }}">
|
|
209
|
+
{% endif %}
|
|
210
|
+
<pre><code>Loading response...</code></pre>
|
|
211
|
+
</div>
|
|
212
|
+
</div>
|
|
213
|
+
{% endif %}
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
|
|
218
|
+
{% if req.trace_data %}
|
|
219
|
+
<div class="tab-content" id="traces-tab-{{ loop.index0 }}" style="display: none;">
|
|
220
|
+
<!-- Hidden element containing trace data for JavaScript access -->
|
|
221
|
+
<div class="trace-data-container" data-trace-data-b64="{{ req.trace_data.trace_data_b64 }}" style="display: none;"></div>
|
|
222
|
+
|
|
223
|
+
<div class="details-section">
|
|
224
|
+
<h4>Trace Information</h4>
|
|
225
|
+
<div class="metadata-grid">
|
|
226
|
+
<div class="metadata-item">
|
|
227
|
+
<span class="metadata-label">Traces:</span>
|
|
228
|
+
<span class="metadata-value">{{ req.trace_data.trace_count }}</span>
|
|
229
|
+
</div>
|
|
230
|
+
<div class="metadata-item">
|
|
231
|
+
<span class="metadata-label">Spans:</span>
|
|
232
|
+
<span class="metadata-value">{{ req.trace_data.span_count }}</span>
|
|
233
|
+
</div>
|
|
234
|
+
</div>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
<div class="details-section">
|
|
238
|
+
<h4>Trace Visualization</h4>
|
|
239
|
+
<div class="trace-container">
|
|
240
|
+
<div class="trace-controls">
|
|
241
|
+
<button class="action-button small" onclick="toggleTraceView('{{ loop.index0 }}')">JSON View</button>
|
|
242
|
+
</div>
|
|
243
|
+
|
|
244
|
+
<div id="waterfall-view-{{ loop.index0 }}" class="waterfall-container">
|
|
245
|
+
<!-- Waterfall visualization will be generated here -->
|
|
246
|
+
</div>
|
|
247
|
+
|
|
248
|
+
<div id="json-view-{{ loop.index0 }}" class="code-container" style="display: none;">
|
|
249
|
+
<pre class="code-block"><code>{{ req.trace_data.traces | tojson(indent=2) }}</code></pre>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
{% endif %}
|
|
255
|
+
|
|
256
|
+
</div>
|
|
257
|
+
</td>
|
|
258
|
+
</tr>
|
|
259
|
+
{% endfor %}
|
|
260
|
+
</tbody>
|
|
261
|
+
</table>
|
|
262
|
+
</div>
|
|
263
|
+
{% else %}
|
|
264
|
+
<div class="empty-state">
|
|
265
|
+
<p>No requests received yet</p>
|
|
266
|
+
<p class="empty-subtitle">Requests will appear here as the test agent receives them</p>
|
|
267
|
+
</div>
|
|
268
|
+
{% endif %}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
|
|
272
|
+
<script src="https://msgpack.org/js/msgpack.js"></script>
|
|
273
|
+
<script>
|
|
274
|
+
// SSE state management (global scope)
|
|
275
|
+
let sseEventSource = null;
|
|
276
|
+
let ssePaused = false;
|
|
277
|
+
let sseQueuedRequests = [];
|
|
278
|
+
|
|
279
|
+
// Real-time filtering
|
|
280
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
281
|
+
const filterQuery = document.getElementById('filter-query');
|
|
282
|
+
// Store full request objects for filtering
|
|
283
|
+
window.allRequests = [
|
|
284
|
+
{% for req in requests %}
|
|
285
|
+
{
|
|
286
|
+
method: {{ req.method | tojson }},
|
|
287
|
+
path: {{ req.path | tojson }},
|
|
288
|
+
session_token: {{ req.session_token | tojson }},
|
|
289
|
+
content_length: {{ req.content_length or 0 }},
|
|
290
|
+
timestamp: {{ req.timestamp or 0 }},
|
|
291
|
+
headers: {{ req.headers | tojson }},
|
|
292
|
+
query_string: {{ req.query_string | tojson }},
|
|
293
|
+
body: {{ req.body | tojson }},
|
|
294
|
+
response: {
|
|
295
|
+
status: {{ req.response.status if req.response else 'null' }},
|
|
296
|
+
headers: {{ req.response.headers | tojson if req.response else '{}' }},
|
|
297
|
+
body: {{ req.response.body | tojson if req.response else 'null' }}
|
|
298
|
+
}
|
|
299
|
+
}{% if not loop.last %},{% endif %}
|
|
300
|
+
{% endfor %}
|
|
301
|
+
];
|
|
302
|
+
const visibleCount = document.getElementById('visible-count');
|
|
303
|
+
|
|
304
|
+
// Parse URL parameters for filter persistence
|
|
305
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
306
|
+
const filterParam = urlParams.get('filter');
|
|
307
|
+
if (filterParam) {
|
|
308
|
+
filterQuery.value = decodeURIComponent(filterParam);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function parseFilterQuery(queryString) {
|
|
312
|
+
const filters = [];
|
|
313
|
+
if (!queryString.trim()) return filters;
|
|
314
|
+
|
|
315
|
+
// Split by space but handle quoted strings
|
|
316
|
+
const tokens = queryString.match(/(?:[^\s"]+|"[^"]*")+/g) || [];
|
|
317
|
+
|
|
318
|
+
tokens.forEach(token => {
|
|
319
|
+
const isExclude = token.startsWith('-');
|
|
320
|
+
const cleanToken = isExclude ? token.slice(1) : token;
|
|
321
|
+
const colonIndex = cleanToken.indexOf(':');
|
|
322
|
+
|
|
323
|
+
if (colonIndex > 0) {
|
|
324
|
+
const key = cleanToken.slice(0, colonIndex).trim();
|
|
325
|
+
let value = cleanToken.slice(colonIndex + 1).trim();
|
|
326
|
+
|
|
327
|
+
// Remove quotes if present
|
|
328
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
329
|
+
value = value.slice(1, -1);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
filters.push({ key, value, exclude: isExclude });
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
return filters;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function getNestedValue(obj, path) {
|
|
340
|
+
return path.split('.').reduce((current, key) => {
|
|
341
|
+
return current && current[key] !== undefined ? current[key] : undefined;
|
|
342
|
+
}, obj);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function matchesFilter(request, filter) {
|
|
346
|
+
const { key, value, exclude } = filter;
|
|
347
|
+
let actualValue;
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
// Handle different filter keys
|
|
351
|
+
switch (key.toLowerCase()) {
|
|
352
|
+
case 'method':
|
|
353
|
+
actualValue = request.method?.toLowerCase();
|
|
354
|
+
break;
|
|
355
|
+
case 'path':
|
|
356
|
+
actualValue = request.path?.toLowerCase();
|
|
357
|
+
break;
|
|
358
|
+
case 'status':
|
|
359
|
+
actualValue = request.response?.status?.toString();
|
|
360
|
+
break;
|
|
361
|
+
case 'session':
|
|
362
|
+
actualValue = request.session_token?.toLowerCase();
|
|
363
|
+
break;
|
|
364
|
+
case 'size':
|
|
365
|
+
return matchNumericFilter(request.content_length || 0, value);
|
|
366
|
+
case 'duration':
|
|
367
|
+
return matchNumericFilter(request.duration_ms || 0, value);
|
|
368
|
+
case 'body':
|
|
369
|
+
actualValue = request.body?.toLowerCase();
|
|
370
|
+
break;
|
|
371
|
+
default:
|
|
372
|
+
// Handle nested keys like headers.user-agent or response.headers.content-type
|
|
373
|
+
if (key.startsWith('headers.')) {
|
|
374
|
+
const headerName = key.slice(8).toLowerCase();
|
|
375
|
+
const headers = getNestedValue(request, 'headers');
|
|
376
|
+
// Case-insensitive header lookup
|
|
377
|
+
if (headers) {
|
|
378
|
+
const headerKey = Object.keys(headers).find(k => k.toLowerCase() === headerName);
|
|
379
|
+
actualValue = headerKey ? headers[headerKey]?.toLowerCase() : undefined;
|
|
380
|
+
}
|
|
381
|
+
} else if (key.startsWith('response.headers.')) {
|
|
382
|
+
const headerName = key.slice(17).toLowerCase();
|
|
383
|
+
const headers = getNestedValue(request, 'response.headers');
|
|
384
|
+
// Case-insensitive header lookup
|
|
385
|
+
if (headers) {
|
|
386
|
+
const headerKey = Object.keys(headers).find(k => k.toLowerCase() === headerName);
|
|
387
|
+
actualValue = headerKey ? headers[headerKey]?.toLowerCase() : undefined;
|
|
388
|
+
}
|
|
389
|
+
} else if (key.startsWith('response.body')) {
|
|
390
|
+
actualValue = getNestedValue(request, 'response.body')?.toLowerCase();
|
|
391
|
+
} else if (key.startsWith('response.')) {
|
|
392
|
+
const respKey = key.slice(9);
|
|
393
|
+
actualValue = getNestedValue(request, `response.${respKey}`)?.toString()?.toLowerCase();
|
|
394
|
+
} else {
|
|
395
|
+
actualValue = getNestedValue(request, key)?.toString()?.toLowerCase();
|
|
396
|
+
}
|
|
397
|
+
break;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (actualValue === undefined || actualValue === null) {
|
|
401
|
+
return exclude; // If field doesn't exist, exclude filters pass, include filters fail
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const searchValue = value.toLowerCase();
|
|
405
|
+
let matches = false;
|
|
406
|
+
|
|
407
|
+
if (searchValue === '*') {
|
|
408
|
+
matches = actualValue !== '';
|
|
409
|
+
} else if (searchValue.includes('*')) {
|
|
410
|
+
// Simple wildcard matching
|
|
411
|
+
const regex = new RegExp('^' + searchValue.replace(/\*/g, '.*') + '$');
|
|
412
|
+
matches = regex.test(actualValue);
|
|
413
|
+
} else {
|
|
414
|
+
matches = actualValue.includes(searchValue);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const result = exclude ? !matches : matches;
|
|
418
|
+
if (key === 'path' && exclude) {
|
|
419
|
+
console.log('Exclusion debug:', { key, value, exclude, actualValue, matches, result });
|
|
420
|
+
}
|
|
421
|
+
return result;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
function matchNumericFilter(actualValue, filterValue) {
|
|
425
|
+
const num = parseFloat(actualValue);
|
|
426
|
+
if (isNaN(num)) return false;
|
|
427
|
+
|
|
428
|
+
if (filterValue.startsWith('>')) {
|
|
429
|
+
return num > parseFloat(filterValue.slice(1));
|
|
430
|
+
} else if (filterValue.startsWith('<')) {
|
|
431
|
+
return num < parseFloat(filterValue.slice(1));
|
|
432
|
+
} else if (filterValue.startsWith('>=')) {
|
|
433
|
+
return num >= parseFloat(filterValue.slice(2));
|
|
434
|
+
} else if (filterValue.startsWith('<=')) {
|
|
435
|
+
return num <= parseFloat(filterValue.slice(2));
|
|
436
|
+
} else {
|
|
437
|
+
return num === parseFloat(filterValue);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function filterRequests() {
|
|
442
|
+
const queryString = filterQuery.value;
|
|
443
|
+
const filters = parseFilterQuery(queryString);
|
|
444
|
+
|
|
445
|
+
// Update URL with filter parameter
|
|
446
|
+
const url = new URL(window.location);
|
|
447
|
+
if (queryString.trim()) {
|
|
448
|
+
url.searchParams.set('filter', encodeURIComponent(queryString));
|
|
449
|
+
} else {
|
|
450
|
+
url.searchParams.delete('filter');
|
|
451
|
+
}
|
|
452
|
+
window.history.replaceState({}, '', url);
|
|
453
|
+
|
|
454
|
+
const rows = document.querySelectorAll('.request-row');
|
|
455
|
+
let visibleRows = 0;
|
|
456
|
+
|
|
457
|
+
rows.forEach((row, index) => {
|
|
458
|
+
// Get the full request object from our global array
|
|
459
|
+
const request = window.allRequests[index];
|
|
460
|
+
|
|
461
|
+
if (!request) {
|
|
462
|
+
// Fallback to showing the row if we don't have request data
|
|
463
|
+
row.style.display = '';
|
|
464
|
+
visibleRows++;
|
|
465
|
+
return;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Apply all filters (AND logic)
|
|
469
|
+
let shouldShow = true;
|
|
470
|
+
for (const filter of filters) {
|
|
471
|
+
const filterResult = matchesFilter(request, filter);
|
|
472
|
+
if (!filterResult) {
|
|
473
|
+
shouldShow = false;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// Debug for exclusion filters
|
|
479
|
+
if (filters.some(f => f.exclude && f.key === 'path')) {
|
|
480
|
+
console.log('Filter decision:', { path: request.path, shouldShow, filters });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (shouldShow) {
|
|
484
|
+
row.style.display = '';
|
|
485
|
+
const detailsRow = row.nextElementSibling;
|
|
486
|
+
if (detailsRow && detailsRow.classList.contains('request-details') && detailsRow.style.display !== 'none') {
|
|
487
|
+
detailsRow.style.display = '';
|
|
488
|
+
}
|
|
489
|
+
visibleRows++;
|
|
490
|
+
} else {
|
|
491
|
+
row.style.display = 'none';
|
|
492
|
+
const detailsRow = row.nextElementSibling;
|
|
493
|
+
if (detailsRow && detailsRow.classList.contains('request-details')) {
|
|
494
|
+
detailsRow.style.display = 'none';
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
visibleCount.textContent = visibleRows;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Set up Server-Sent Events for real-time updates
|
|
503
|
+
function setupSSE() {
|
|
504
|
+
sseEventSource = new EventSource('/requests/stream');
|
|
505
|
+
|
|
506
|
+
sseEventSource.onmessage = function(event) {
|
|
507
|
+
const data = JSON.parse(event.data);
|
|
508
|
+
|
|
509
|
+
if (data.type === 'new_request') {
|
|
510
|
+
if (ssePaused) {
|
|
511
|
+
// Queue the request for later processing
|
|
512
|
+
sseQueuedRequests.push(data);
|
|
513
|
+
updatePauseButton();
|
|
514
|
+
} else {
|
|
515
|
+
addNewRequestToTable(data.request);
|
|
516
|
+
updateRequestCount(data.total_count);
|
|
517
|
+
}
|
|
518
|
+
} else if (data.type === 'count') {
|
|
519
|
+
updateRequestCount(data.count);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
sseEventSource.onerror = function(event) {
|
|
524
|
+
console.log('SSE connection error:', event);
|
|
525
|
+
// Reconnect after 5 seconds
|
|
526
|
+
setTimeout(() => {
|
|
527
|
+
sseEventSource.close();
|
|
528
|
+
setupSSE();
|
|
529
|
+
}, 5000);
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
function addNewRequestToTable(request) {
|
|
535
|
+
// Add to beginning of array to match DOM insertion order (newest first)
|
|
536
|
+
window.allRequests.unshift(request);
|
|
537
|
+
|
|
538
|
+
let tbody = document.querySelector('.requests-table tbody');
|
|
539
|
+
|
|
540
|
+
// If no table exists (empty state), create the table structure
|
|
541
|
+
if (!tbody) {
|
|
542
|
+
// Remove empty state if it exists
|
|
543
|
+
const emptyState = document.querySelector('.empty-state');
|
|
544
|
+
if (emptyState) {
|
|
545
|
+
emptyState.remove();
|
|
546
|
+
console.log('Removed empty state');
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const requestsContainer = document.querySelector('.requests-table-container') ||
|
|
550
|
+
document.querySelector('.requests-container');
|
|
551
|
+
|
|
552
|
+
// Create the table structure
|
|
553
|
+
const tableHTML = `
|
|
554
|
+
<div class="requests-table-container">
|
|
555
|
+
<table class="requests-table">
|
|
556
|
+
<thead>
|
|
557
|
+
<tr>
|
|
558
|
+
<th>Time</th>
|
|
559
|
+
<th>Method</th>
|
|
560
|
+
<th>Path</th>
|
|
561
|
+
<th>Size</th>
|
|
562
|
+
</tr>
|
|
563
|
+
</thead>
|
|
564
|
+
<tbody>
|
|
565
|
+
</tbody>
|
|
566
|
+
</table>
|
|
567
|
+
</div>
|
|
568
|
+
`;
|
|
569
|
+
|
|
570
|
+
if (requestsContainer && requestsContainer.classList.contains('requests-container')) {
|
|
571
|
+
requestsContainer.insertAdjacentHTML('beforeend', tableHTML);
|
|
572
|
+
} else {
|
|
573
|
+
document.querySelector('.requests-container').insertAdjacentHTML('beforeend', tableHTML);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
tbody = document.querySelector('.requests-table tbody');
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Create new row HTML - new row will be at index 0 since we insert at top
|
|
580
|
+
const newRow = createRequestRow(request, 0);
|
|
581
|
+
|
|
582
|
+
// Insert at the top (most recent first)
|
|
583
|
+
tbody.insertAdjacentHTML('afterbegin', newRow);
|
|
584
|
+
|
|
585
|
+
// Update indices for all existing rows since we pushed them down
|
|
586
|
+
updateAllRowIndices();
|
|
587
|
+
|
|
588
|
+
// Process body decoding for the new request - get only the newly added elements
|
|
589
|
+
const lastAddedRow = tbody.querySelector('.request-row');
|
|
590
|
+
if (lastAddedRow) {
|
|
591
|
+
// The details row is the next sibling of the request row
|
|
592
|
+
const detailsRow = lastAddedRow.nextElementSibling;
|
|
593
|
+
if (detailsRow && detailsRow.classList.contains('request-details')) {
|
|
594
|
+
const newDecodedBodyElements = detailsRow.querySelectorAll('.decoded-body[data-body]');
|
|
595
|
+
|
|
596
|
+
console.log('Found decoded body elements:', newDecodedBodyElements.length);
|
|
597
|
+
newDecodedBodyElements.forEach(element => {
|
|
598
|
+
const contentType = element.getAttribute('data-content-type');
|
|
599
|
+
const bodyData = element.getAttribute('data-body');
|
|
600
|
+
console.log('Processing body:', {contentType, bodyDataLength: bodyData ? bodyData.length : 0});
|
|
601
|
+
|
|
602
|
+
if (bodyData && bodyData.trim()) {
|
|
603
|
+
try {
|
|
604
|
+
processBodyDecoding(element, contentType, bodyData);
|
|
605
|
+
} catch (error) {
|
|
606
|
+
console.error('Error processing body decoding:', error);
|
|
607
|
+
// Continue processing other elements even if one fails
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Process raw bytes display for msgpack in new requests - get only the newly added elements
|
|
615
|
+
if (lastAddedRow && lastAddedRow.nextElementSibling) {
|
|
616
|
+
const newRawBytesElements = lastAddedRow.nextElementSibling.querySelectorAll('.raw-bytes[data-body]');
|
|
617
|
+
newRawBytesElements.forEach(element => {
|
|
618
|
+
const bodyData = element.getAttribute('data-body');
|
|
619
|
+
if (bodyData) {
|
|
620
|
+
try {
|
|
621
|
+
// Decode base64 to get actual bytes
|
|
622
|
+
const binaryString = atob(bodyData);
|
|
623
|
+
const uint8Array = new Uint8Array(binaryString.length);
|
|
624
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
625
|
+
uint8Array[i] = binaryString.charCodeAt(i);
|
|
626
|
+
}
|
|
627
|
+
const hexString = Array.from(uint8Array)
|
|
628
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
629
|
+
.join(' ');
|
|
630
|
+
element.textContent = hexString;
|
|
631
|
+
} catch (e) {
|
|
632
|
+
element.textContent = 'Error decoding base64: ' + e.message;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
|
|
637
|
+
|
|
638
|
+
// Re-apply filters to include the new request
|
|
639
|
+
filterRequests();
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function updateAllRowIndices() {
|
|
644
|
+
// Update onclick handlers and IDs for all rows to match their new positions
|
|
645
|
+
const rows = document.querySelectorAll('.request-row');
|
|
646
|
+
rows.forEach((row, index) => {
|
|
647
|
+
// Update onclick handler
|
|
648
|
+
row.setAttribute('onclick', `toggleRequestDetails('${index}')`);
|
|
649
|
+
|
|
650
|
+
// Update the details row ID if it exists
|
|
651
|
+
const detailsRow = row.nextElementSibling;
|
|
652
|
+
if (detailsRow && detailsRow.classList.contains('request-details')) {
|
|
653
|
+
detailsRow.id = `details-${index}`;
|
|
654
|
+
|
|
655
|
+
// Update all tab IDs within the details row
|
|
656
|
+
const tabIds = ['info-tab', 'headers-tab', 'body-tab', 'response-tab', 'traces-tab'];
|
|
657
|
+
tabIds.forEach(tabId => {
|
|
658
|
+
const tab = detailsRow.querySelector(`[id^="${tabId}"]`);
|
|
659
|
+
if (tab) {
|
|
660
|
+
tab.id = `${tabId}-${index}`;
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function createRequestRow(req, index) {
|
|
668
|
+
console.log('Creating request row with data:', { timestamp: req.timestamp, method: req.method, path: req.path });
|
|
669
|
+
const sessionToken = req.session_token ? `<span class="session-token">session: ${req.session_token}</span>` : '';
|
|
670
|
+
const queryString = req.query_string ? `<span class="query-string">?${req.query_string}</span>` : '';
|
|
671
|
+
const body = req.body || '';
|
|
672
|
+
|
|
673
|
+
let headersHTML = '';
|
|
674
|
+
for (const [header, value] of Object.entries(req.headers)) {
|
|
675
|
+
headersHTML += `
|
|
676
|
+
<div class="metadata-item">
|
|
677
|
+
<span class="metadata-label">${header}:</span>
|
|
678
|
+
<span class="metadata-value">${value}</span>
|
|
679
|
+
</div>
|
|
680
|
+
`;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return `
|
|
684
|
+
<tr class="request-row" onclick="toggleRequestDetails('${index}')" style="cursor: pointer;">
|
|
685
|
+
<td class="timestamp-cell">
|
|
686
|
+
${req.timestamp ? formatTimestamp(req.timestamp) : ''}
|
|
687
|
+
</td>
|
|
688
|
+
<td class="method-cell">
|
|
689
|
+
<span class="method-badge method-${req.method.toLowerCase()}">${req.method}</span>
|
|
690
|
+
</td>
|
|
691
|
+
<td class="path-cell">
|
|
692
|
+
<div class="path-info">
|
|
693
|
+
<span class="path">${req.path}</span>
|
|
694
|
+
${queryString}
|
|
695
|
+
${sessionToken}
|
|
696
|
+
</div>
|
|
697
|
+
</td>
|
|
698
|
+
<td class="size-cell">
|
|
699
|
+
${req.content_length} bytes
|
|
700
|
+
</td>
|
|
701
|
+
</tr>
|
|
702
|
+
<tr class="request-details" id="details-${index}" style="display: none;">
|
|
703
|
+
<td colspan="4">
|
|
704
|
+
<div class="request-details-content">
|
|
705
|
+
<div class="details-tabs">
|
|
706
|
+
<button class="tab-button active" onclick="switchTab('${index}', 'request')">Request</button>
|
|
707
|
+
<button class="tab-button" onclick="switchTab('${index}', 'response')">Response</button>
|
|
708
|
+
${req.trace_data ? `<button class="tab-button" onclick="switchTab('${index}', 'traces')">Traces</button>` : ''}
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div class="tab-content" id="request-tab-${index}">
|
|
712
|
+
<div class="details-section">
|
|
713
|
+
<h4>Headers</h4>
|
|
714
|
+
<div class="metadata-grid">
|
|
715
|
+
${headersHTML}
|
|
716
|
+
</div>
|
|
717
|
+
</div>
|
|
718
|
+
${req.query_params && Object.keys(req.query_params).length > 0 ? `
|
|
719
|
+
<div class="details-section">
|
|
720
|
+
<h4>Query Parameters</h4>
|
|
721
|
+
<div class="metadata-grid">
|
|
722
|
+
${Object.entries(req.query_params).map(([name, values]) =>
|
|
723
|
+
`<div class="metadata-item">
|
|
724
|
+
<span class="metadata-label">${name}:</span>
|
|
725
|
+
<span class="metadata-value">${Array.isArray(values) ? values.join(', ') : values}</span>
|
|
726
|
+
</div>`
|
|
727
|
+
).join('')}
|
|
728
|
+
</div>
|
|
729
|
+
</div>
|
|
730
|
+
` : ''}
|
|
731
|
+
${body ? `
|
|
732
|
+
<div class="details-section">
|
|
733
|
+
<h4>Body</h4>
|
|
734
|
+
<div class="body-container">
|
|
735
|
+
${!(req.content_type && req.content_type.toLowerCase().includes('multipart')) ? `<div class="body-section">
|
|
736
|
+
<h5>Raw</h5>
|
|
737
|
+
<div class="request-body">
|
|
738
|
+
${(req.content_type && req.content_type.toLowerCase().includes('msgpack')) ?
|
|
739
|
+
`<pre><code class="raw-bytes" data-body="${body ? body.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>') : ''}">Loading hex view...</code></pre>` :
|
|
740
|
+
`<pre><code>${body}</code></pre>`
|
|
741
|
+
}
|
|
742
|
+
</div>
|
|
743
|
+
</div>` : ''}
|
|
744
|
+
${(req.content_type && (req.content_type.toLowerCase().includes('json') || req.content_type.toLowerCase().includes('msgpack') || req.content_type.toLowerCase().includes('multipart'))) ?
|
|
745
|
+
`<div class="body-section">
|
|
746
|
+
<h5>Decoded</h5>
|
|
747
|
+
<div class="decoded-body" data-content-type="${req.content_type || ''}" data-body="${body ? body.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>') : ''}">
|
|
748
|
+
<!-- Decoded content will be inserted here by JavaScript -->
|
|
749
|
+
</div>
|
|
750
|
+
</div>` : ''}
|
|
751
|
+
</div>
|
|
752
|
+
</div>
|
|
753
|
+
` : ''}
|
|
754
|
+
</div>
|
|
755
|
+
|
|
756
|
+
<div class="tab-content" id="response-tab-${index}" style="display: none;">
|
|
757
|
+
<div class="details-section">
|
|
758
|
+
<h4>Response Headers</h4>
|
|
759
|
+
<div class="metadata-grid">
|
|
760
|
+
<div class="metadata-item">
|
|
761
|
+
<span class="metadata-label">Status:</span>
|
|
762
|
+
<span class="metadata-value">${req.response ? req.response.status : '200'}</span>
|
|
763
|
+
</div>
|
|
764
|
+
<div class="metadata-item">
|
|
765
|
+
<span class="metadata-label">Content-Type:</span>
|
|
766
|
+
<span class="metadata-value">${req.response ? (req.response.content_type || 'text/plain') : 'text/plain'}</span>
|
|
767
|
+
</div>
|
|
768
|
+
${req.response && req.response.headers ? Object.entries(req.response.headers).map(([header, value]) => `
|
|
769
|
+
<div class="metadata-item">
|
|
770
|
+
<span class="metadata-label">${header}:</span>
|
|
771
|
+
<span class="metadata-value">${value}</span>
|
|
772
|
+
</div>
|
|
773
|
+
`).join('') : ''}
|
|
774
|
+
</div>
|
|
775
|
+
</div>
|
|
776
|
+
<div class="details-section">
|
|
777
|
+
<h4>Response Body</h4>
|
|
778
|
+
<div class="body-container">
|
|
779
|
+
${!(req.response && req.response.content_type && req.response.content_type.toLowerCase().includes('multipart')) ? `<div class="body-section">
|
|
780
|
+
<h5>Raw</h5>
|
|
781
|
+
<div class="response-body">
|
|
782
|
+
${req.response && req.response.body_is_binary ?
|
|
783
|
+
`<pre><code class="raw-bytes" data-body="${req.response.body ? req.response.body.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>') : ''}">Loading hex view...</code></pre>` :
|
|
784
|
+
`<pre><code>${req.response && req.response.body ? req.response.body : 'Empty response body'}</code></pre>`
|
|
785
|
+
}
|
|
786
|
+
</div>
|
|
787
|
+
</div>` : ''}
|
|
788
|
+
${(req.response && req.response.content_type && (req.response.content_type.toLowerCase().includes('json') || req.response.content_type.toLowerCase().includes('msgpack') || req.response.content_type.toLowerCase().includes('multipart'))) ?
|
|
789
|
+
`<div class="body-section">
|
|
790
|
+
<h5>Decoded</h5>
|
|
791
|
+
<div class="decoded-body" data-content-type="${req.response.content_type || ''}" data-body="${req.response.body ? req.response.body.replace(/&/g, '&').replace(/"/g, '"').replace(/'/g, ''').replace(/</g, '<').replace(/>/g, '>') : ''}" ${req.response.body_is_binary ? 'data-is-binary="true"' : ''}>
|
|
792
|
+
<pre><code>Loading response...</code></pre>
|
|
793
|
+
</div>
|
|
794
|
+
</div>` : ''}
|
|
795
|
+
</div>
|
|
796
|
+
</div>
|
|
797
|
+
</div>
|
|
798
|
+
|
|
799
|
+
${req.trace_data ? `
|
|
800
|
+
<div class="tab-content" id="traces-tab-${index}" style="display: none;">
|
|
801
|
+
<!-- Hidden element containing trace data for JavaScript access -->
|
|
802
|
+
<div class="trace-data-container" data-trace-data-b64="${req.trace_data.trace_data_b64 || ''}" style="display: none;"></div>
|
|
803
|
+
|
|
804
|
+
<div class="details-section">
|
|
805
|
+
<h4>Trace Information</h4>
|
|
806
|
+
<div class="metadata-grid">
|
|
807
|
+
<div class="metadata-item">
|
|
808
|
+
<span class="metadata-label">Traces:</span>
|
|
809
|
+
<span class="metadata-value">${req.trace_data.trace_count}</span>
|
|
810
|
+
</div>
|
|
811
|
+
<div class="metadata-item">
|
|
812
|
+
<span class="metadata-label">Spans:</span>
|
|
813
|
+
<span class="metadata-value">${req.trace_data.span_count}</span>
|
|
814
|
+
</div>
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
|
|
818
|
+
<div class="details-section">
|
|
819
|
+
<h4>Trace Visualization</h4>
|
|
820
|
+
<div class="trace-container">
|
|
821
|
+
<div class="trace-controls">
|
|
822
|
+
<button class="action-button small" onclick="toggleTraceView('${index}')">JSON View</button>
|
|
823
|
+
</div>
|
|
824
|
+
|
|
825
|
+
<div id="waterfall-view-${index}" class="waterfall-container">
|
|
826
|
+
<!-- Waterfall visualization will be generated here -->
|
|
827
|
+
</div>
|
|
828
|
+
|
|
829
|
+
<div id="json-view-${index}" class="code-container" style="display: none;">
|
|
830
|
+
<pre class="code-block"><code>${JSON.stringify(req.trace_data.traces, null, 2)}</code></pre>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
</div>
|
|
834
|
+
</div>
|
|
835
|
+
` : ''}
|
|
836
|
+
</div>
|
|
837
|
+
</td>
|
|
838
|
+
</tr>
|
|
839
|
+
`;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
function updateRequestCount(count) {
|
|
843
|
+
const totalCountSpan = document.getElementById('total-count');
|
|
844
|
+
if (totalCountSpan) {
|
|
845
|
+
totalCountSpan.textContent = count;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// Make functions globally accessible
|
|
850
|
+
window.addNewRequestToTable = addNewRequestToTable;
|
|
851
|
+
window.updateRequestCount = updateRequestCount;
|
|
852
|
+
|
|
853
|
+
// Debug utility to verify array-DOM sync
|
|
854
|
+
window.debugFilterSync = function() {
|
|
855
|
+
const rows = document.querySelectorAll('.request-row');
|
|
856
|
+
const requests = window.allRequests;
|
|
857
|
+
|
|
858
|
+
console.log(`Array length: ${requests.length}, DOM rows: ${rows.length}`);
|
|
859
|
+
|
|
860
|
+
// Check first few entries
|
|
861
|
+
for (let i = 0; i < Math.min(5, rows.length); i++) {
|
|
862
|
+
const row = rows[i];
|
|
863
|
+
const request = requests[i];
|
|
864
|
+
|
|
865
|
+
// Extract method and path from DOM
|
|
866
|
+
const domMethod = row.querySelector('.method-badge')?.textContent;
|
|
867
|
+
const domPath = row.querySelector('.path-cell')?.textContent?.trim();
|
|
868
|
+
|
|
869
|
+
console.log(`Index ${i}:`, {
|
|
870
|
+
array: request ? `${request.method} ${request.path}` : 'undefined',
|
|
871
|
+
dom: `${domMethod} ${domPath}`
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
};
|
|
875
|
+
|
|
876
|
+
// Start SSE connection
|
|
877
|
+
setupSSE();
|
|
878
|
+
|
|
879
|
+
// Add event listeners for filters
|
|
880
|
+
filterQuery.addEventListener('input', filterRequests);
|
|
881
|
+
|
|
882
|
+
// Apply initial filter if loaded from URL
|
|
883
|
+
if (filterQuery.value.trim()) {
|
|
884
|
+
filterRequests();
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Process existing request bodies for decoding
|
|
888
|
+
processAllRequestBodies();
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
// SSE control functions (global scope)
|
|
892
|
+
function pauseSSE() {
|
|
893
|
+
ssePaused = true;
|
|
894
|
+
updatePauseButton();
|
|
895
|
+
console.log('SSE paused');
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
function resumeSSE() {
|
|
899
|
+
ssePaused = false;
|
|
900
|
+
|
|
901
|
+
// Process queued requests in order
|
|
902
|
+
const processedCount = sseQueuedRequests.length;
|
|
903
|
+
while (sseQueuedRequests.length > 0) {
|
|
904
|
+
const data = sseQueuedRequests.shift();
|
|
905
|
+
addNewRequestToTable(data.request);
|
|
906
|
+
updateRequestCount(data.total_count);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
updatePauseButton();
|
|
910
|
+
console.log('SSE resumed, processed', processedCount, 'queued requests');
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
function toggleSSE() {
|
|
914
|
+
if (ssePaused) {
|
|
915
|
+
resumeSSE();
|
|
916
|
+
} else {
|
|
917
|
+
pauseSSE();
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function updatePauseButton() {
|
|
922
|
+
const pauseBtn = document.getElementById('pause-sse-btn');
|
|
923
|
+
if (pauseBtn) {
|
|
924
|
+
if (ssePaused) {
|
|
925
|
+
pauseBtn.textContent = `Resume (${sseQueuedRequests.length} queued)`;
|
|
926
|
+
pauseBtn.classList.add('paused');
|
|
927
|
+
} else {
|
|
928
|
+
pauseBtn.textContent = 'Pause';
|
|
929
|
+
pauseBtn.classList.remove('paused');
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Global helper functions for SSE
|
|
935
|
+
function addNewRequestToTable(request) {
|
|
936
|
+
if (window.addNewRequestToTable) {
|
|
937
|
+
return window.addNewRequestToTable(request);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
function updateRequestCount(count) {
|
|
942
|
+
if (window.updateRequestCount) {
|
|
943
|
+
return window.updateRequestCount(count);
|
|
944
|
+
}
|
|
945
|
+
const totalCountSpan = document.getElementById('total-count');
|
|
946
|
+
if (totalCountSpan) {
|
|
947
|
+
totalCountSpan.textContent = count;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Function to process all request bodies on page load
|
|
952
|
+
function processAllRequestBodies() {
|
|
953
|
+
// Process decoded bodies with size-based loading
|
|
954
|
+
const decodedBodyElements = document.querySelectorAll('.decoded-body');
|
|
955
|
+
decodedBodyElements.forEach((element, index) => {
|
|
956
|
+
const contentType = element.getAttribute('data-content-type');
|
|
957
|
+
const bodyData = element.getAttribute('data-body');
|
|
958
|
+
|
|
959
|
+
if (bodyData && bodyData.trim()) {
|
|
960
|
+
const isLargePayload = estimatePayloadSize(bodyData) > AUTO_DECODE_THRESHOLD_KB;
|
|
961
|
+
|
|
962
|
+
if (isLargePayload) {
|
|
963
|
+
// Show "Click to decode" button for large payloads
|
|
964
|
+
showClickToDecodeButton(element, contentType, bodyData, 'page', index);
|
|
965
|
+
} else {
|
|
966
|
+
// Process small payloads immediately with error handling
|
|
967
|
+
try {
|
|
968
|
+
processBodyDecoding(element, contentType, bodyData);
|
|
969
|
+
} catch (error) {
|
|
970
|
+
showDecodingError(element, error.message);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
});
|
|
975
|
+
|
|
976
|
+
// Process raw bytes display for msgpack
|
|
977
|
+
const rawBytesElements = document.querySelectorAll('.raw-bytes[data-body]');
|
|
978
|
+
rawBytesElements.forEach(element => {
|
|
979
|
+
const bodyData = element.getAttribute('data-body');
|
|
980
|
+
if (bodyData) {
|
|
981
|
+
try {
|
|
982
|
+
// Decode base64 to get actual bytes
|
|
983
|
+
const binaryString = atob(bodyData);
|
|
984
|
+
const uint8Array = new Uint8Array(binaryString.length);
|
|
985
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
986
|
+
uint8Array[i] = binaryString.charCodeAt(i);
|
|
987
|
+
}
|
|
988
|
+
const hexString = Array.from(uint8Array)
|
|
989
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
990
|
+
.join(' ');
|
|
991
|
+
element.textContent = hexString;
|
|
992
|
+
} catch (e) {
|
|
993
|
+
element.textContent = 'Error decoding base64: ' + e.message;
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Function to process body decoding based on content type
|
|
1000
|
+
function processBodyDecoding(element, contentType, bodyData) {
|
|
1001
|
+
console.log('processBodyDecoding called with:', {contentType, bodyData: bodyData ? bodyData.substring(0, 100) + '...' : 'null'});
|
|
1002
|
+
try {
|
|
1003
|
+
if (!bodyData || !bodyData.trim()) {
|
|
1004
|
+
element.innerHTML = `
|
|
1005
|
+
<div class="empty-body">
|
|
1006
|
+
<pre><code>Empty or no response body</code></pre>
|
|
1007
|
+
</div>
|
|
1008
|
+
`;
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
if (contentType && contentType.toLowerCase().includes('application/json')) {
|
|
1013
|
+
// Handle HTML entities that might be in the data
|
|
1014
|
+
let cleanBodyData = bodyData;
|
|
1015
|
+
if (bodyData.includes('"')) {
|
|
1016
|
+
cleanBodyData = bodyData.replace(/"/g, '"')
|
|
1017
|
+
.replace(/&/g, '&')
|
|
1018
|
+
.replace(/</g, '<')
|
|
1019
|
+
.replace(/>/g, '>')
|
|
1020
|
+
.replace(/'/g, "'");
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Try to parse and pretty-print JSON
|
|
1024
|
+
try {
|
|
1025
|
+
const jsonData = JSON.parse(cleanBodyData);
|
|
1026
|
+
const prettyJson = JSON.stringify(jsonData, null, 2);
|
|
1027
|
+
element.innerHTML = `
|
|
1028
|
+
<div class="json-body">
|
|
1029
|
+
<pre><code>${prettyJson}</code></pre>
|
|
1030
|
+
</div>
|
|
1031
|
+
`;
|
|
1032
|
+
} catch (jsonError) {
|
|
1033
|
+
console.log('JSON parse error for data:', bodyData.substring(0, 200));
|
|
1034
|
+
console.log('JSON parse error:', jsonError);
|
|
1035
|
+
console.log('Full bodyData length:', bodyData.length);
|
|
1036
|
+
console.log('CleanBodyData equals bodyData:', cleanBodyData === bodyData);
|
|
1037
|
+
|
|
1038
|
+
element.innerHTML = `
|
|
1039
|
+
<div class="error-body">
|
|
1040
|
+
<pre><code>Invalid JSON: ${jsonError.message}
|
|
1041
|
+
|
|
1042
|
+
Data length: ${bodyData.length}
|
|
1043
|
+
Clean data length: ${cleanBodyData.length}
|
|
1044
|
+
First 100 chars: ${cleanBodyData.substring(0, 100)}
|
|
1045
|
+
Last 50 chars: ${cleanBodyData.slice(-50)}</code></pre>
|
|
1046
|
+
</div>
|
|
1047
|
+
`;
|
|
1048
|
+
}
|
|
1049
|
+
} else if (contentType && contentType.toLowerCase().includes('application/msgpack')) {
|
|
1050
|
+
// Try to decode msgpack
|
|
1051
|
+
try {
|
|
1052
|
+
// Decode base64 to get actual bytes for msgpack
|
|
1053
|
+
const binaryString = atob(bodyData);
|
|
1054
|
+
const uint8Array = new Uint8Array(binaryString.length);
|
|
1055
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1056
|
+
uint8Array[i] = binaryString.charCodeAt(i);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// Decode using official msgpack.js library
|
|
1060
|
+
let decodedData;
|
|
1061
|
+
if (typeof msgpack !== 'undefined' && msgpack.unpack) {
|
|
1062
|
+
decodedData = msgpack.unpack(uint8Array);
|
|
1063
|
+
} else if (typeof msgpack !== 'undefined' && msgpack.decode) {
|
|
1064
|
+
decodedData = msgpack.decode(uint8Array);
|
|
1065
|
+
} else if (typeof MessagePack !== 'undefined') {
|
|
1066
|
+
decodedData = MessagePack.unpack(uint8Array);
|
|
1067
|
+
} else {
|
|
1068
|
+
throw new Error('MessagePack library not found');
|
|
1069
|
+
}
|
|
1070
|
+
const prettyData = JSON.stringify(decodedData, null, 2);
|
|
1071
|
+
|
|
1072
|
+
element.innerHTML = `
|
|
1073
|
+
<div class="msgpack-body">
|
|
1074
|
+
<pre><code>${prettyData}</code></pre>
|
|
1075
|
+
</div>
|
|
1076
|
+
`;
|
|
1077
|
+
} catch (msgpackError) {
|
|
1078
|
+
element.innerHTML = `
|
|
1079
|
+
<div class="msgpack-body">
|
|
1080
|
+
<pre><code>MessagePack Error: ${msgpackError.message}
|
|
1081
|
+
Base64 length: ${bodyData.length} chars
|
|
1082
|
+
Available libraries: ${Object.keys(window).filter(k => k.toLowerCase().includes('msgpack')).join(', ') || 'none'}</code></pre>
|
|
1083
|
+
</div>
|
|
1084
|
+
`;
|
|
1085
|
+
}
|
|
1086
|
+
} else if (contentType && contentType.toLowerCase().includes('multipart/form-data')) {
|
|
1087
|
+
// Handle multipart form data
|
|
1088
|
+
try {
|
|
1089
|
+
// Decode base64 to get raw multipart data
|
|
1090
|
+
const rawData = atob(bodyData);
|
|
1091
|
+
|
|
1092
|
+
// Extract boundary from content type
|
|
1093
|
+
const boundaryMatch = contentType.match(/boundary=([^;]+)/);
|
|
1094
|
+
if (!boundaryMatch) {
|
|
1095
|
+
throw new Error('No boundary found in multipart content type');
|
|
1096
|
+
}
|
|
1097
|
+
const boundary = '--' + boundaryMatch[1];
|
|
1098
|
+
|
|
1099
|
+
// Parse multipart data
|
|
1100
|
+
const parts = rawData.split(boundary);
|
|
1101
|
+
const formData = {};
|
|
1102
|
+
let fileCount = 0;
|
|
1103
|
+
|
|
1104
|
+
for (let i = 1; i < parts.length - 1; i++) { // Skip first (empty) and last (closing) parts
|
|
1105
|
+
const part = parts[i];
|
|
1106
|
+
if (!part.trim()) continue;
|
|
1107
|
+
|
|
1108
|
+
// Split headers and body
|
|
1109
|
+
const headerBodySplit = part.indexOf('\r\n\r\n');
|
|
1110
|
+
if (headerBodySplit === -1) continue;
|
|
1111
|
+
|
|
1112
|
+
const headers = part.substring(0, headerBodySplit);
|
|
1113
|
+
const body = part.substring(headerBodySplit + 4);
|
|
1114
|
+
|
|
1115
|
+
// Extract field name from Content-Disposition header
|
|
1116
|
+
const nameMatch = headers.match(/name="([^"]+)"/);
|
|
1117
|
+
if (!nameMatch) continue;
|
|
1118
|
+
|
|
1119
|
+
const fieldName = nameMatch[1];
|
|
1120
|
+
|
|
1121
|
+
// Check if it's a file field
|
|
1122
|
+
const filenameMatch = headers.match(/filename="([^"]*)"/);
|
|
1123
|
+
if (filenameMatch) {
|
|
1124
|
+
// It's a file upload
|
|
1125
|
+
const filename = filenameMatch[1];
|
|
1126
|
+
const cleanBody = body.replace(/\r\n$/, ''); // Remove trailing CRLF
|
|
1127
|
+
formData[fieldName] = {
|
|
1128
|
+
type: 'file',
|
|
1129
|
+
filename: filename,
|
|
1130
|
+
size: cleanBody.length,
|
|
1131
|
+
content: `[Binary file data: ${cleanBody.length} bytes]`
|
|
1132
|
+
};
|
|
1133
|
+
fileCount++;
|
|
1134
|
+
} else {
|
|
1135
|
+
// It's a regular form field
|
|
1136
|
+
const cleanBody = body.replace(/\r\n$/, ''); // Remove trailing CRLF
|
|
1137
|
+
formData[fieldName] = {
|
|
1138
|
+
type: 'text',
|
|
1139
|
+
value: cleanBody
|
|
1140
|
+
};
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
// Format for display
|
|
1145
|
+
let displayContent = `Form Data (${Object.keys(formData).length} fields, ${fileCount} files):\n\n`;
|
|
1146
|
+
for (const [key, data] of Object.entries(formData)) {
|
|
1147
|
+
if (data.type === 'file') {
|
|
1148
|
+
displayContent += `${key}: [FILE] ${data.filename} (${data.size} bytes)\n`;
|
|
1149
|
+
} else {
|
|
1150
|
+
displayContent += `${key}: ${data.value}\n`;
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
element.innerHTML = `
|
|
1155
|
+
<div class="multipart-body">
|
|
1156
|
+
<pre><code>${displayContent}</code></pre>
|
|
1157
|
+
</div>
|
|
1158
|
+
`;
|
|
1159
|
+
} catch (multipartError) {
|
|
1160
|
+
element.innerHTML = `
|
|
1161
|
+
<div class="multipart-body">
|
|
1162
|
+
<pre><code>Multipart parsing error: ${multipartError.message}
|
|
1163
|
+
|
|
1164
|
+
Content-Type: ${contentType}
|
|
1165
|
+
Data length: ${bodyData.length} chars</code></pre>
|
|
1166
|
+
</div>
|
|
1167
|
+
`;
|
|
1168
|
+
}
|
|
1169
|
+
} else {
|
|
1170
|
+
// For other content types, try to show as text if it looks reasonable
|
|
1171
|
+
try {
|
|
1172
|
+
// Decode base64 to see if it's readable text
|
|
1173
|
+
const rawData = atob(bodyData);
|
|
1174
|
+
if (rawData.length > 0 && rawData.length < 10000) { // Only show reasonable-sized text
|
|
1175
|
+
// Check if it contains mostly printable characters
|
|
1176
|
+
const printableRatio = (rawData.match(/[\x20-\x7E\r\n\t]/g) || []).length / rawData.length;
|
|
1177
|
+
if (printableRatio > 0.8) { // 80% printable characters
|
|
1178
|
+
element.innerHTML = `
|
|
1179
|
+
<div class="text-body">
|
|
1180
|
+
<pre><code>${rawData}</code></pre>
|
|
1181
|
+
</div>
|
|
1182
|
+
`;
|
|
1183
|
+
return;
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
} catch (e) {
|
|
1187
|
+
// Not valid base64 or not readable text, fall through to hide
|
|
1188
|
+
}
|
|
1189
|
+
// For binary or other unreadable content types, hide the decoded section
|
|
1190
|
+
element.style.display = 'none';
|
|
1191
|
+
}
|
|
1192
|
+
} catch (error) {
|
|
1193
|
+
console.error('Error processing body:', error);
|
|
1194
|
+
element.innerHTML = `
|
|
1195
|
+
<div class="error-body">
|
|
1196
|
+
<pre><code>Error processing response: ${error.message}</code></pre>
|
|
1197
|
+
</div>
|
|
1198
|
+
`;
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function switchTab(index, tab) {
|
|
1203
|
+
console.log('switchTab called:', index, tab);
|
|
1204
|
+
// Update tab buttons
|
|
1205
|
+
const tabButtons = document.querySelectorAll(`#details-${index} .tab-button`);
|
|
1206
|
+
tabButtons.forEach(button => button.classList.remove('active'));
|
|
1207
|
+
event.target.classList.add('active');
|
|
1208
|
+
|
|
1209
|
+
// Show/hide tab content
|
|
1210
|
+
const requestTab = document.getElementById(`request-tab-${index}`);
|
|
1211
|
+
const responseTab = document.getElementById(`response-tab-${index}`);
|
|
1212
|
+
const tracesTab = document.getElementById(`traces-tab-${index}`);
|
|
1213
|
+
|
|
1214
|
+
console.log('Found tabs:', {requestTab, responseTab, tracesTab});
|
|
1215
|
+
|
|
1216
|
+
// Hide all tabs first
|
|
1217
|
+
requestTab.style.display = 'none';
|
|
1218
|
+
responseTab.style.display = 'none';
|
|
1219
|
+
if (tracesTab) tracesTab.style.display = 'none';
|
|
1220
|
+
|
|
1221
|
+
// Show selected tab
|
|
1222
|
+
if (tab === 'request') {
|
|
1223
|
+
requestTab.style.display = 'block';
|
|
1224
|
+
} else if (tab === 'response') {
|
|
1225
|
+
responseTab.style.display = 'block';
|
|
1226
|
+
} else if (tab === 'traces' && tracesTab) {
|
|
1227
|
+
console.log('Showing traces tab and generating waterfall');
|
|
1228
|
+
tracesTab.style.display = 'block';
|
|
1229
|
+
// Generate waterfall view if not already generated
|
|
1230
|
+
try {
|
|
1231
|
+
generateWaterfallViewForRequest(index);
|
|
1232
|
+
} catch (error) {
|
|
1233
|
+
console.error('Error generating waterfall view:', error);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// Size threshold for automatic decoding (in KB)
|
|
1239
|
+
const AUTO_DECODE_THRESHOLD_KB = 50;
|
|
1240
|
+
|
|
1241
|
+
function toggleRequestDetails(index) {
|
|
1242
|
+
const detailsRow = document.getElementById(`details-${index}`);
|
|
1243
|
+
|
|
1244
|
+
if (detailsRow.style.display === 'none' || !detailsRow.style.display) {
|
|
1245
|
+
detailsRow.style.display = 'table-row';
|
|
1246
|
+
|
|
1247
|
+
// Trigger deferred decoding for this request
|
|
1248
|
+
deferredDecodeRequest(index);
|
|
1249
|
+
} else {
|
|
1250
|
+
detailsRow.style.display = 'none';
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
function deferredDecodeRequest(index) {
|
|
1255
|
+
// Use a small timeout to allow the UI to render the details row first
|
|
1256
|
+
setTimeout(() => {
|
|
1257
|
+
processRequestDecoding(index);
|
|
1258
|
+
}, 10);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
function processRequestDecoding(index) {
|
|
1262
|
+
console.log('Processing deferred decoding for request:', index);
|
|
1263
|
+
|
|
1264
|
+
// Find all decoded-body elements in this request's details
|
|
1265
|
+
const detailsRow = document.getElementById(`details-${index}`);
|
|
1266
|
+
if (!detailsRow) return;
|
|
1267
|
+
|
|
1268
|
+
const decodedBodyElements = detailsRow.querySelectorAll('.decoded-body[data-content-type]');
|
|
1269
|
+
|
|
1270
|
+
decodedBodyElements.forEach((element, elementIndex) => {
|
|
1271
|
+
const contentType = element.getAttribute('data-content-type');
|
|
1272
|
+
const bodyData = element.getAttribute('data-body');
|
|
1273
|
+
const isLargePayload = estimatePayloadSize(bodyData) > AUTO_DECODE_THRESHOLD_KB;
|
|
1274
|
+
|
|
1275
|
+
if (isLargePayload) {
|
|
1276
|
+
// Show "Click to decode" button for large payloads
|
|
1277
|
+
showClickToDecodeButton(element, contentType, bodyData, index, elementIndex);
|
|
1278
|
+
} else {
|
|
1279
|
+
// Show loading state then decode automatically for small payloads
|
|
1280
|
+
showLoadingState(element);
|
|
1281
|
+
setTimeout(() => {
|
|
1282
|
+
try {
|
|
1283
|
+
processBodyDecoding(element, contentType, bodyData);
|
|
1284
|
+
} catch (error) {
|
|
1285
|
+
showDecodingError(element, error.message);
|
|
1286
|
+
}
|
|
1287
|
+
}, 50);
|
|
1288
|
+
}
|
|
1289
|
+
});
|
|
1290
|
+
|
|
1291
|
+
// Also process raw bytes elements
|
|
1292
|
+
const rawBytesElements = detailsRow.querySelectorAll('.raw-bytes[data-body]');
|
|
1293
|
+
rawBytesElements.forEach(element => {
|
|
1294
|
+
const bodyData = element.getAttribute('data-body');
|
|
1295
|
+
if (bodyData) {
|
|
1296
|
+
setTimeout(() => {
|
|
1297
|
+
try {
|
|
1298
|
+
// Decode base64 to get actual bytes
|
|
1299
|
+
const binaryString = atob(bodyData);
|
|
1300
|
+
const uint8Array = new Uint8Array(binaryString.length);
|
|
1301
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1302
|
+
uint8Array[i] = binaryString.charCodeAt(i);
|
|
1303
|
+
}
|
|
1304
|
+
const hexString = Array.from(uint8Array)
|
|
1305
|
+
.map(b => b.toString(16).padStart(2, '0'))
|
|
1306
|
+
.join(' ');
|
|
1307
|
+
element.textContent = hexString;
|
|
1308
|
+
} catch (e) {
|
|
1309
|
+
element.textContent = 'Error decoding base64: ' + e.message;
|
|
1310
|
+
}
|
|
1311
|
+
}, 25);
|
|
1312
|
+
}
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function estimatePayloadSize(base64Data) {
|
|
1317
|
+
if (!base64Data) return 0;
|
|
1318
|
+
// Base64 encoding increases size by ~33%, so decode length gives rough byte size
|
|
1319
|
+
return (base64Data.length * 3 / 4) / 1024; // Convert to KB
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
function showLoadingState(element) {
|
|
1323
|
+
element.innerHTML = `
|
|
1324
|
+
<div class="loading-state">
|
|
1325
|
+
<div class="loading-spinner"></div>
|
|
1326
|
+
<span>Decoding payload...</span>
|
|
1327
|
+
</div>
|
|
1328
|
+
`;
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function showClickToDecodeButton(element, contentType, bodyData, requestIndex, elementIndex) {
|
|
1332
|
+
const payloadSizeKB = estimatePayloadSize(bodyData);
|
|
1333
|
+
const elementId = `decode-btn-${requestIndex}-${elementIndex}`;
|
|
1334
|
+
|
|
1335
|
+
element.innerHTML = `
|
|
1336
|
+
<div class="large-payload-notice">
|
|
1337
|
+
<div class="payload-info">
|
|
1338
|
+
<strong>Large Payload Detected</strong>
|
|
1339
|
+
<p>Size: ~${payloadSizeKB.toFixed(1)} KB</p>
|
|
1340
|
+
<p>Content-Type: ${contentType}</p>
|
|
1341
|
+
</div>
|
|
1342
|
+
<button id="${elementId}" class="decode-button" onclick="decodeOnDemand('${elementId}', '${requestIndex}', '${elementIndex}')">
|
|
1343
|
+
Click to Decode
|
|
1344
|
+
</button>
|
|
1345
|
+
</div>
|
|
1346
|
+
`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
function decodeOnDemand(buttonId, requestIndex, elementIndex) {
|
|
1350
|
+
const button = document.getElementById(buttonId);
|
|
1351
|
+
const element = button.closest('.decoded-body');
|
|
1352
|
+
const contentType = element.getAttribute('data-content-type');
|
|
1353
|
+
const bodyData = element.getAttribute('data-body');
|
|
1354
|
+
|
|
1355
|
+
// Show loading state
|
|
1356
|
+
showLoadingState(element);
|
|
1357
|
+
|
|
1358
|
+
// Decode asynchronously to prevent UI blocking
|
|
1359
|
+
setTimeout(() => {
|
|
1360
|
+
try {
|
|
1361
|
+
processBodyDecoding(element, contentType, bodyData);
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
showDecodingError(element, error.message);
|
|
1364
|
+
}
|
|
1365
|
+
}, 50);
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
function showDecodingError(element, errorMessage) {
|
|
1369
|
+
element.innerHTML = `
|
|
1370
|
+
<div class="decoding-error">
|
|
1371
|
+
<strong>Decoding Error:</strong>
|
|
1372
|
+
<pre>${errorMessage}</pre>
|
|
1373
|
+
</div>
|
|
1374
|
+
`;
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
function refreshRequests() {
|
|
1378
|
+
location.reload();
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
async function clearRequests() {
|
|
1382
|
+
if (!confirm('Are you sure you want to clear all requests? This cannot be undone.')) {
|
|
1383
|
+
return;
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
try {
|
|
1387
|
+
const response = await fetch('/requests/clear', {
|
|
1388
|
+
method: 'POST',
|
|
1389
|
+
headers: {
|
|
1390
|
+
'Content-Type': 'application/json',
|
|
1391
|
+
}
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
if (response.ok) {
|
|
1395
|
+
// Clear the global requests array
|
|
1396
|
+
window.allRequests = [];
|
|
1397
|
+
// Clear the UI
|
|
1398
|
+
const requestsTable = document.querySelector('.requests-table-container');
|
|
1399
|
+
const requestsContainer = document.querySelector('.requests-container');
|
|
1400
|
+
|
|
1401
|
+
if (requestsTable) {
|
|
1402
|
+
requestsTable.remove();
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1405
|
+
// Show empty state
|
|
1406
|
+
const emptyStateHTML = `
|
|
1407
|
+
<div class="empty-state">
|
|
1408
|
+
<p>No requests received yet</p>
|
|
1409
|
+
<p class="empty-subtitle">Requests will appear here as the test agent receives them</p>
|
|
1410
|
+
</div>
|
|
1411
|
+
`;
|
|
1412
|
+
if (requestsContainer) {
|
|
1413
|
+
requestsContainer.insertAdjacentHTML('beforeend', emptyStateHTML);
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// Update counter (if it exists)
|
|
1417
|
+
const statBadge = document.querySelector('.stat-badge');
|
|
1418
|
+
if (statBadge) {
|
|
1419
|
+
statBadge.textContent = '0 total requests';
|
|
1420
|
+
}
|
|
1421
|
+
} else {
|
|
1422
|
+
const errorText = await response.text();
|
|
1423
|
+
console.error('Clear requests failed:', response.status, errorText);
|
|
1424
|
+
alert(`Failed to clear requests: ${response.status}`);
|
|
1425
|
+
}
|
|
1426
|
+
} catch (error) {
|
|
1427
|
+
console.error('Error clearing requests:', error);
|
|
1428
|
+
alert('Error clearing requests');
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
function downloadRequests() {
|
|
1433
|
+
// Get currently visible requests (respecting filters)
|
|
1434
|
+
const visibleRequests = [];
|
|
1435
|
+
const rows = document.querySelectorAll('.request-row');
|
|
1436
|
+
|
|
1437
|
+
rows.forEach((row, index) => {
|
|
1438
|
+
// Check if row is visible (not filtered out)
|
|
1439
|
+
if (row.style.display !== 'none') {
|
|
1440
|
+
if (window.allRequests[index]) {
|
|
1441
|
+
visibleRequests.push(window.allRequests[index]);
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
});
|
|
1445
|
+
|
|
1446
|
+
// Create a blob with the filtered data (minified JSON)
|
|
1447
|
+
const jsonData = JSON.stringify(visibleRequests);
|
|
1448
|
+
const blob = new Blob([jsonData], { type: 'application/json' });
|
|
1449
|
+
|
|
1450
|
+
// Create download link
|
|
1451
|
+
const link = document.createElement('a');
|
|
1452
|
+
link.href = URL.createObjectURL(blob);
|
|
1453
|
+
link.download = `requests_filtered_${new Date().toISOString().slice(0,19).replace(/[:-]/g, '')}.json`;
|
|
1454
|
+
document.body.appendChild(link);
|
|
1455
|
+
link.click();
|
|
1456
|
+
document.body.removeChild(link);
|
|
1457
|
+
URL.revokeObjectURL(link.href);
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function formatTimestamp(timestamp) {
|
|
1461
|
+
if (!timestamp) return '';
|
|
1462
|
+
const date = new Date(timestamp * 1000); // Convert from seconds to milliseconds
|
|
1463
|
+
return date.getFullYear() + '-' +
|
|
1464
|
+
String(date.getMonth() + 1).padStart(2, '0') + '-' +
|
|
1465
|
+
String(date.getDate()).padStart(2, '0') + ' ' +
|
|
1466
|
+
String(date.getHours()).padStart(2, '0') + ':' +
|
|
1467
|
+
String(date.getMinutes()).padStart(2, '0') + ':' +
|
|
1468
|
+
String(date.getSeconds()).padStart(2, '0');
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
|
|
1472
|
+
function applyQuickFilter(filterType, value) {
|
|
1473
|
+
const filterQuery = document.getElementById('filter-query');
|
|
1474
|
+
|
|
1475
|
+
// Create filter string based on type
|
|
1476
|
+
let filterString = '';
|
|
1477
|
+
switch(filterType) {
|
|
1478
|
+
case 'method':
|
|
1479
|
+
filterString = `method:${value}`;
|
|
1480
|
+
break;
|
|
1481
|
+
case 'path':
|
|
1482
|
+
filterString = `path:${value}`;
|
|
1483
|
+
break;
|
|
1484
|
+
case 'session':
|
|
1485
|
+
filterString = `session:${value}`;
|
|
1486
|
+
break;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
// Add to existing filter or replace
|
|
1490
|
+
const currentFilter = filterQuery.value.trim();
|
|
1491
|
+
if (currentFilter) {
|
|
1492
|
+
filterQuery.value = `${currentFilter} ${filterString}`;
|
|
1493
|
+
} else {
|
|
1494
|
+
filterQuery.value = filterString;
|
|
1495
|
+
}
|
|
1496
|
+
|
|
1497
|
+
// Trigger the filter update
|
|
1498
|
+
const filterEvent = new Event('input', { bubbles: true });
|
|
1499
|
+
filterQuery.dispatchEvent(filterEvent);
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
function clearAllFilters() {
|
|
1503
|
+
const filterQuery = document.getElementById('filter-query');
|
|
1504
|
+
filterQuery.value = '';
|
|
1505
|
+
|
|
1506
|
+
// Trigger filter update
|
|
1507
|
+
const filterEvent = new Event('input', { bubbles: true });
|
|
1508
|
+
filterQuery.dispatchEvent(filterEvent);
|
|
1509
|
+
}
|
|
1510
|
+
|
|
1511
|
+
function showFilterHelp() {
|
|
1512
|
+
const helpText = `Filter Syntax:
|
|
1513
|
+
• key:value - Include requests matching key=value
|
|
1514
|
+
• -key:value - Exclude requests matching key=value
|
|
1515
|
+
• Multiple filters with spaces (AND logic)
|
|
1516
|
+
|
|
1517
|
+
Supported keys:
|
|
1518
|
+
• method:POST - HTTP method
|
|
1519
|
+
• path:/api/traces - Request path (partial match)
|
|
1520
|
+
• status:200 - Response status code
|
|
1521
|
+
• session:abc123 - Session token (partial match)
|
|
1522
|
+
• headers.user-agent:curl - Request header
|
|
1523
|
+
• response.headers.content-type:json - Response header
|
|
1524
|
+
• size:>1000 or size:<500 - Content size comparison
|
|
1525
|
+
• body:contains - Request body contains text
|
|
1526
|
+
• response.body:error - Response body contains text
|
|
1527
|
+
|
|
1528
|
+
Examples:
|
|
1529
|
+
• method:POST -path:/info
|
|
1530
|
+
• headers.user-agent:curl status:200
|
|
1531
|
+
• size:>1000 -headers.authorization:*
|
|
1532
|
+
• method:POST path:/traces response.status:200`;
|
|
1533
|
+
|
|
1534
|
+
alert(helpText);
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function downloadFlareFile(sessionToken, requestIndex) {
|
|
1538
|
+
// Find the multipart data for this request
|
|
1539
|
+
const requestRows = document.querySelectorAll('.request-row');
|
|
1540
|
+
if (requestIndex >= requestRows.length) {
|
|
1541
|
+
alert('Request not found');
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
// Get the flare_file data from the DOM
|
|
1546
|
+
const detailsRow = document.getElementById(`details-${requestIndex}`);
|
|
1547
|
+
if (!detailsRow) {
|
|
1548
|
+
alert('Request details not found');
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
const flareFields = detailsRow.querySelectorAll('.multipart-field');
|
|
1553
|
+
let flareFileData = null;
|
|
1554
|
+
|
|
1555
|
+
flareFields.forEach(field => {
|
|
1556
|
+
const headerText = field.querySelector('.field-header strong').textContent;
|
|
1557
|
+
if (headerText === 'flare_file:') {
|
|
1558
|
+
const codeElement = field.querySelector('pre code');
|
|
1559
|
+
if (codeElement) {
|
|
1560
|
+
// Get the full data from the data attribute, fallback to text content
|
|
1561
|
+
flareFileData = codeElement.getAttribute('data-full-data') || codeElement.textContent.replace(/\.\.\.$/, '');
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
if (!flareFileData) {
|
|
1567
|
+
alert('Flare file data not found');
|
|
1568
|
+
return;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
try {
|
|
1572
|
+
// Convert base64 to binary data
|
|
1573
|
+
const binaryString = atob(flareFileData);
|
|
1574
|
+
const uint8Array = new Uint8Array(binaryString.length);
|
|
1575
|
+
for (let i = 0; i < binaryString.length; i++) {
|
|
1576
|
+
uint8Array[i] = binaryString.charCodeAt(i);
|
|
1577
|
+
}
|
|
1578
|
+
|
|
1579
|
+
// Create blob and download
|
|
1580
|
+
const blob = new Blob([uint8Array], { type: 'application/zip' });
|
|
1581
|
+
const url = URL.createObjectURL(blob);
|
|
1582
|
+
const link = document.createElement('a');
|
|
1583
|
+
link.href = url;
|
|
1584
|
+
link.download = `tracer_flare_${sessionToken || 'default'}_${Date.now()}.zip`;
|
|
1585
|
+
document.body.appendChild(link);
|
|
1586
|
+
link.click();
|
|
1587
|
+
document.body.removeChild(link);
|
|
1588
|
+
URL.revokeObjectURL(url);
|
|
1589
|
+
} catch (e) {
|
|
1590
|
+
alert('Error downloading file: ' + e.message);
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
// Waterfall visualization functions for traces tab
|
|
1595
|
+
const requestTraceData = new Map(); // Cache trace data per request index
|
|
1596
|
+
const serviceColorMap = new Map();
|
|
1597
|
+
let nextColorIndex = 0;
|
|
1598
|
+
|
|
1599
|
+
function getServiceColor(serviceName) {
|
|
1600
|
+
if (!serviceColorMap.has(serviceName)) {
|
|
1601
|
+
serviceColorMap.set(serviceName, nextColorIndex % 8);
|
|
1602
|
+
nextColorIndex++;
|
|
1603
|
+
}
|
|
1604
|
+
return serviceColorMap.get(serviceName);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
function generateWaterfallViewForRequest(index) {
|
|
1608
|
+
console.log('generateWaterfallViewForRequest called with index:', index);
|
|
1609
|
+
try {
|
|
1610
|
+
const container = document.getElementById(`waterfall-view-${index}`);
|
|
1611
|
+
|
|
1612
|
+
if (!container) {
|
|
1613
|
+
console.error(`Waterfall container not found for request ${index}`);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
console.log('Found waterfall container:', container);
|
|
1618
|
+
|
|
1619
|
+
// Check if already generated (ignore HTML comments and whitespace)
|
|
1620
|
+
const hasActualContent = Array.from(container.childNodes).some(node =>
|
|
1621
|
+
node.nodeType === Node.ELEMENT_NODE ||
|
|
1622
|
+
(node.nodeType === Node.TEXT_NODE && node.textContent.trim())
|
|
1623
|
+
);
|
|
1624
|
+
if (hasActualContent) {
|
|
1625
|
+
console.log('Container already has actual content, skipping generation');
|
|
1626
|
+
return;
|
|
1627
|
+
}
|
|
1628
|
+
|
|
1629
|
+
console.log('About to get trace data for request:', index);
|
|
1630
|
+
// Get trace data from the request
|
|
1631
|
+
const traceData = getTraceDataForRequest(index);
|
|
1632
|
+
|
|
1633
|
+
console.log('Generating waterfall view for request', index, 'traceData:', traceData);
|
|
1634
|
+
|
|
1635
|
+
if (!traceData || !Array.isArray(traceData) || traceData.length === 0) {
|
|
1636
|
+
container.innerHTML = '<p class="empty-state">No trace data available for waterfall view</p>';
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
const traces = traceData;
|
|
1641
|
+
let html = '<div class="waterfall-traces">';
|
|
1642
|
+
|
|
1643
|
+
traces.forEach((trace, traceIndex) => {
|
|
1644
|
+
console.log('Processing trace:', trace);
|
|
1645
|
+
|
|
1646
|
+
let spans = [];
|
|
1647
|
+
|
|
1648
|
+
// Handle different trace formats
|
|
1649
|
+
if (Array.isArray(trace)) {
|
|
1650
|
+
if (trace.length === 0) return;
|
|
1651
|
+
spans = trace;
|
|
1652
|
+
} else if (typeof trace === 'object' && trace.span_id) {
|
|
1653
|
+
// Single span
|
|
1654
|
+
spans = [trace];
|
|
1655
|
+
} else {
|
|
1656
|
+
console.log('Skipping trace with unsupported format:', trace);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
// Find the earliest start time and calculate total duration for this trace
|
|
1661
|
+
const validSpans = spans.filter(span => span && typeof span === 'object');
|
|
1662
|
+
if (validSpans.length === 0) return;
|
|
1663
|
+
|
|
1664
|
+
let minStart = Math.min(...validSpans.map(span => span.start || 0));
|
|
1665
|
+
let maxEnd = Math.max(...validSpans.map(span => (span.start || 0) + (span.duration || 0)));
|
|
1666
|
+
let totalDuration = maxEnd - minStart;
|
|
1667
|
+
|
|
1668
|
+
// Handle case where all spans have 0 duration
|
|
1669
|
+
if (totalDuration === 0) {
|
|
1670
|
+
totalDuration = Math.max(...validSpans.map(span => span.duration || 0));
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const traceId = validSpans[0].trace_id !== undefined ? validSpans[0].trace_id : traceIndex;
|
|
1674
|
+
|
|
1675
|
+
html += `<div class="waterfall-trace">
|
|
1676
|
+
<div class="trace-header" onclick="toggleTraceForRequest(${index}, ${traceIndex})">
|
|
1677
|
+
<div class="trace-header-left">
|
|
1678
|
+
<span class="trace-toggle" id="trace-toggle-${index}-${traceIndex}">▼</span>
|
|
1679
|
+
<h4>Trace ${traceId}</h4>
|
|
1680
|
+
</div>
|
|
1681
|
+
<span class="trace-duration">${formatDuration(totalDuration)}</span>
|
|
1682
|
+
</div>
|
|
1683
|
+
<div class="spans-timeline" id="trace-spans-${index}-${traceIndex}">`;
|
|
1684
|
+
|
|
1685
|
+
// Sort spans by start time and build hierarchy
|
|
1686
|
+
const sortedSpans = [...validSpans].sort((a, b) => (a.start || 0) - (b.start || 0));
|
|
1687
|
+
const spanHierarchy = buildSpanHierarchy(sortedSpans);
|
|
1688
|
+
|
|
1689
|
+
spanHierarchy.forEach(spanInfo => {
|
|
1690
|
+
html += renderSpanForRequest(spanInfo, minStart, totalDuration, 0, index, traceIndex);
|
|
1691
|
+
});
|
|
1692
|
+
|
|
1693
|
+
html += '</div></div>';
|
|
1694
|
+
});
|
|
1695
|
+
|
|
1696
|
+
html += '</div>';
|
|
1697
|
+
container.innerHTML = html;
|
|
1698
|
+
} catch (error) {
|
|
1699
|
+
console.error('Error in generateWaterfallViewForRequest:', error);
|
|
1700
|
+
const container = document.getElementById(`waterfall-view-${index}`);
|
|
1701
|
+
if (container) {
|
|
1702
|
+
container.innerHTML = '<p class="error-message">Error generating waterfall view: ' + error.message + '</p>';
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
|
|
1707
|
+
function getTraceDataForRequest(index) {
|
|
1708
|
+
console.log('getTraceDataForRequest called with index:', index);
|
|
1709
|
+
// Get trace data from DOM or cache
|
|
1710
|
+
const tracesTab = document.getElementById(`traces-tab-${index}`);
|
|
1711
|
+
console.log('Found traces tab:', tracesTab);
|
|
1712
|
+
if (tracesTab) {
|
|
1713
|
+
// Try base64 encoded data first (for streaming requests)
|
|
1714
|
+
let dataElement = tracesTab.querySelector('.trace-data-container[data-trace-data-b64]');
|
|
1715
|
+
if (dataElement) {
|
|
1716
|
+
try {
|
|
1717
|
+
const traceDataB64 = dataElement.getAttribute('data-trace-data-b64');
|
|
1718
|
+
console.log('Found base64 trace data, length:', traceDataB64 ? traceDataB64.length : 'null');
|
|
1719
|
+
const traceDataStr = atob(traceDataB64);
|
|
1720
|
+
return JSON.parse(traceDataStr);
|
|
1721
|
+
} catch (e) {
|
|
1722
|
+
console.error('Failed to parse base64 trace data:', e);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1726
|
+
// Fallback to regular data attribute (for non-streaming requests)
|
|
1727
|
+
dataElement = tracesTab.querySelector('.trace-data-container[data-trace-data]');
|
|
1728
|
+
console.log('Found regular data element:', dataElement);
|
|
1729
|
+
if (dataElement) {
|
|
1730
|
+
try {
|
|
1731
|
+
const traceDataStr = dataElement.getAttribute('data-trace-data');
|
|
1732
|
+
console.log('Raw trace data attribute:', traceDataStr ? traceDataStr.substring(0, 200) + '...' : 'null');
|
|
1733
|
+
return JSON.parse(traceDataStr);
|
|
1734
|
+
} catch (e) {
|
|
1735
|
+
console.error('Failed to parse trace data:', e);
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
return null;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function buildSpanHierarchy(spans) {
|
|
1743
|
+
const spanMap = new Map();
|
|
1744
|
+
const rootSpans = [];
|
|
1745
|
+
|
|
1746
|
+
// Create span objects with children arrays
|
|
1747
|
+
spans.forEach(span => {
|
|
1748
|
+
spanMap.set(span.span_id, { ...span, children: [] });
|
|
1749
|
+
});
|
|
1750
|
+
|
|
1751
|
+
// Build parent-child relationships
|
|
1752
|
+
spans.forEach(span => {
|
|
1753
|
+
if (span.parent_id && span.parent_id !== 0 && spanMap.has(span.parent_id)) {
|
|
1754
|
+
spanMap.get(span.parent_id).children.push(spanMap.get(span.span_id));
|
|
1755
|
+
} else {
|
|
1756
|
+
rootSpans.push(spanMap.get(span.span_id));
|
|
1757
|
+
}
|
|
1758
|
+
});
|
|
1759
|
+
|
|
1760
|
+
return rootSpans;
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
function renderSpanForRequest(span, traceStart, totalDuration, depth, requestIndex, traceIndex) {
|
|
1764
|
+
const start = span.start || 0;
|
|
1765
|
+
const duration = span.duration || 0;
|
|
1766
|
+
const relativeStart = start - traceStart;
|
|
1767
|
+
const startPercent = totalDuration > 0 ? (relativeStart / totalDuration) * 100 : 0;
|
|
1768
|
+
const widthPercent = totalDuration > 0 ? (duration / totalDuration) * 100 : 0;
|
|
1769
|
+
|
|
1770
|
+
const service = span.service || 'unknown';
|
|
1771
|
+
const resource = span.resource || span.name || 'unknown';
|
|
1772
|
+
const spanClass = span.error ? 'span-error' : 'span-normal';
|
|
1773
|
+
const serviceColorClass = `service-color-${getServiceColor(service)}`;
|
|
1774
|
+
const indentStyle = `margin-left: ${depth * 20}px;`;
|
|
1775
|
+
|
|
1776
|
+
const spanId = `span-${requestIndex}-${traceIndex}-${span.span_id || Math.random()}`;
|
|
1777
|
+
|
|
1778
|
+
let html = `
|
|
1779
|
+
<div class="waterfall-span ${spanClass}" style="${indentStyle}">
|
|
1780
|
+
<div class="span-row">
|
|
1781
|
+
<div class="span-info" onclick="toggleSpanForRequest('${spanId}')">
|
|
1782
|
+
<div class="span-label">
|
|
1783
|
+
<span class="span-toggle" id="toggle-${spanId}">▶</span>
|
|
1784
|
+
<div class="span-names">
|
|
1785
|
+
<span class="service-name">${service}</span>
|
|
1786
|
+
<span class="operation-name">${resource}</span>
|
|
1787
|
+
</div>
|
|
1788
|
+
</div>
|
|
1789
|
+
<div class="span-timing">
|
|
1790
|
+
<span class="span-duration">${formatDuration(duration)}</span>
|
|
1791
|
+
</div>
|
|
1792
|
+
</div>
|
|
1793
|
+
<div class="span-bar-container">
|
|
1794
|
+
<div class="span-bar ${spanClass} ${serviceColorClass}"
|
|
1795
|
+
style="left: ${startPercent}%; width: ${Math.max(widthPercent, 0.1)}%;"
|
|
1796
|
+
title="${service}.${span.resource || span.name} - ${formatDuration(duration)}">
|
|
1797
|
+
</div>
|
|
1798
|
+
</div>
|
|
1799
|
+
</div>
|
|
1800
|
+
<div class="span-details" id="details-${spanId}" style="display: none;">
|
|
1801
|
+
${renderSpanMetadata(span)}
|
|
1802
|
+
</div>
|
|
1803
|
+
</div>`;
|
|
1804
|
+
|
|
1805
|
+
// Render children recursively
|
|
1806
|
+
span.children.forEach(child => {
|
|
1807
|
+
html += renderSpanForRequest(child, traceStart, totalDuration, depth + 1, requestIndex, traceIndex);
|
|
1808
|
+
});
|
|
1809
|
+
|
|
1810
|
+
return html;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
function formatDuration(nanoseconds) {
|
|
1814
|
+
if (nanoseconds < 1000) return `${nanoseconds}ns`;
|
|
1815
|
+
if (nanoseconds < 1000000) return `${(nanoseconds / 1000).toFixed(1)}μs`;
|
|
1816
|
+
if (nanoseconds < 1000000000) return `${(nanoseconds / 1000000).toFixed(1)}ms`;
|
|
1817
|
+
return `${(nanoseconds / 1000000000).toFixed(2)}s`;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
function toggleTraceForRequest(requestIndex, traceIndex) {
|
|
1821
|
+
const toggle = document.getElementById(`trace-toggle-${requestIndex}-${traceIndex}`);
|
|
1822
|
+
const spans = document.getElementById(`trace-spans-${requestIndex}-${traceIndex}`);
|
|
1823
|
+
|
|
1824
|
+
if (spans.style.display === 'none') {
|
|
1825
|
+
spans.style.display = 'block';
|
|
1826
|
+
toggle.textContent = '▼';
|
|
1827
|
+
} else {
|
|
1828
|
+
spans.style.display = 'none';
|
|
1829
|
+
toggle.textContent = '▶';
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function toggleSpanForRequest(spanId) {
|
|
1834
|
+
const toggle = document.getElementById(`toggle-${spanId}`);
|
|
1835
|
+
const details = document.getElementById(`details-${spanId}`);
|
|
1836
|
+
|
|
1837
|
+
if (details.style.display === 'none') {
|
|
1838
|
+
details.style.display = 'block';
|
|
1839
|
+
toggle.textContent = '▼';
|
|
1840
|
+
} else {
|
|
1841
|
+
details.style.display = 'none';
|
|
1842
|
+
toggle.textContent = '▶';
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
function renderSpanMetadata(span) {
|
|
1847
|
+
const fields = [
|
|
1848
|
+
{ label: 'Span ID', value: span.span_id },
|
|
1849
|
+
{ label: 'Trace ID', value: span.trace_id },
|
|
1850
|
+
{ label: 'Parent ID', value: span.parent_id || 'None' },
|
|
1851
|
+
{ label: 'Service', value: span.service || 'Not set' },
|
|
1852
|
+
{ label: 'Name', value: span.name },
|
|
1853
|
+
{ label: 'Resource', value: span.resource },
|
|
1854
|
+
{ label: 'Type', value: span.type || 'Not set' },
|
|
1855
|
+
{ label: 'Start', value: span.start ? new Date(span.start / 1000000).toISOString() : 'Not set' },
|
|
1856
|
+
{ label: 'Duration', value: span.duration ? formatDuration(span.duration) : 'Not set' },
|
|
1857
|
+
{ label: 'Error', value: span.error ? 'Yes' : 'No' }
|
|
1858
|
+
];
|
|
1859
|
+
|
|
1860
|
+
let html = '<div class="span-metadata">';
|
|
1861
|
+
html += '<h5>Span Details</h5>';
|
|
1862
|
+
html += '<div class="metadata-grid">';
|
|
1863
|
+
|
|
1864
|
+
fields.forEach(field => {
|
|
1865
|
+
if (field.value !== undefined && field.value !== null && field.value !== '') {
|
|
1866
|
+
html += `
|
|
1867
|
+
<div class="metadata-item">
|
|
1868
|
+
<span class="metadata-label">${field.label}:</span>
|
|
1869
|
+
<span class="metadata-value">${field.value}</span>
|
|
1870
|
+
</div>`;
|
|
1871
|
+
}
|
|
1872
|
+
});
|
|
1873
|
+
|
|
1874
|
+
// Add meta tags if present
|
|
1875
|
+
if (span.meta && Object.keys(span.meta).length > 0) {
|
|
1876
|
+
html += '<div class="metadata-section"><h6>Meta</h6>';
|
|
1877
|
+
for (const [key, value] of Object.entries(span.meta)) {
|
|
1878
|
+
html += `
|
|
1879
|
+
<div class="metadata-item">
|
|
1880
|
+
<span class="metadata-label">${key}:</span>
|
|
1881
|
+
<span class="metadata-value">${value}</span>
|
|
1882
|
+
</div>`;
|
|
1883
|
+
}
|
|
1884
|
+
html += '</div>';
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
// Add metrics if present
|
|
1888
|
+
if (span.metrics && Object.keys(span.metrics).length > 0) {
|
|
1889
|
+
html += '<div class="metadata-section"><h6>Metrics</h6>';
|
|
1890
|
+
for (const [key, value] of Object.entries(span.metrics)) {
|
|
1891
|
+
html += `
|
|
1892
|
+
<div class="metadata-item">
|
|
1893
|
+
<span class="metadata-label">${key}:</span>
|
|
1894
|
+
<span class="metadata-value">${value}</span>
|
|
1895
|
+
</div>`;
|
|
1896
|
+
}
|
|
1897
|
+
html += '</div>';
|
|
1898
|
+
}
|
|
1899
|
+
|
|
1900
|
+
html += '</div></div>';
|
|
1901
|
+
return html;
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
function toggleTraceView(index) {
|
|
1905
|
+
const toggleButton = event.target;
|
|
1906
|
+
const waterfallView = document.getElementById(`waterfall-view-${index}`);
|
|
1907
|
+
const jsonView = document.getElementById(`json-view-${index}`);
|
|
1908
|
+
|
|
1909
|
+
if (toggleButton.textContent === 'JSON View') {
|
|
1910
|
+
// Switch to JSON view
|
|
1911
|
+
waterfallView.style.display = 'none';
|
|
1912
|
+
jsonView.style.display = 'block';
|
|
1913
|
+
toggleButton.textContent = 'Waterfall View';
|
|
1914
|
+
} else {
|
|
1915
|
+
// Switch to waterfall view
|
|
1916
|
+
waterfallView.style.display = 'block';
|
|
1917
|
+
jsonView.style.display = 'none';
|
|
1918
|
+
toggleButton.textContent = 'JSON View';
|
|
1919
|
+
// Regenerate waterfall if needed
|
|
1920
|
+
generateWaterfallViewForRequest(index);
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
</script>
|
|
1925
|
+
{% endblock %}
|