@gettrace/cli 2.0.6 → 2.0.8
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/dist/index.js +74 -3414
- package/instrument/trace-instrument.cjs +474 -0
- package/package.json +9 -5
- package/scripts/postinstall.js +110 -20
- package/dist/ast.d.ts +0 -48
- package/dist/ast.js +0 -203
- package/dist/ast.js.map +0 -1
- package/dist/file-lock.d.ts +0 -23
- package/dist/file-lock.js +0 -47
- package/dist/file-lock.js.map +0 -1
- package/dist/format.d.ts +0 -20
- package/dist/format.js +0 -68
- package/dist/format.js.map +0 -1
- package/dist/index.d.ts +0 -2
- package/dist/index.js.map +0 -1
- package/dist/lsp.d.ts +0 -46
- package/dist/lsp.js +0 -267
- package/dist/lsp.js.map +0 -1
- package/dist/search.d.ts +0 -31
- package/dist/search.js +0 -169
- package/dist/search.js.map +0 -1
- package/native-host/.homedir +0 -1
- package/native-host/native-host-debug.log +0 -4
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/* ==========================================================================
|
|
3
|
+
* Trace Backend Instrumentation (in-process, zero-dependency)
|
|
4
|
+
*
|
|
5
|
+
* Loaded into the USER'S backend process by Trace's dev_server via:
|
|
6
|
+
* node --require <abs path to this file> <their server entry>
|
|
7
|
+
*
|
|
8
|
+
* It passively captures the execution flow of each incoming HTTP request —
|
|
9
|
+
* the route, any outbound HTTP/fetch calls, and (best-effort) database
|
|
10
|
+
* queries — as a tree of spans, and streams them to the Trace bridge for the
|
|
11
|
+
* Backend Flow Visualizer.
|
|
12
|
+
*
|
|
13
|
+
* ── DESIGN PRINCIPLES (non-negotiable) ───────────────────────────────────
|
|
14
|
+
* 1. NEVER throw into user code. Every hook is wrapped; on any internal
|
|
15
|
+
* error we fall back to the original, un-instrumented behavior.
|
|
16
|
+
* 2. INERT BY DEFAULT. If TRACE_INGEST_URL is not set, this module does
|
|
17
|
+
* nothing at all (returns immediately). So it is safe to --require even
|
|
18
|
+
* for frontend dev servers; it only activates for backends Trace targets.
|
|
19
|
+
* 3. NO new dependencies. Uses only Node built-ins so it can run inside any
|
|
20
|
+
* user project without installing anything.
|
|
21
|
+
* 4. NEVER instrument our own span-export requests (infinite-loop guard).
|
|
22
|
+
* 5. BOUNDED memory + network. Spans are batched, capped, and dropped under
|
|
23
|
+
* backpressure rather than growing without limit.
|
|
24
|
+
* ========================================================================== */
|
|
25
|
+
|
|
26
|
+
// ── Activation gate ────────────────────────────────────────────────────────
|
|
27
|
+
// The bridge ingest endpoint, e.g. "http://127.0.0.1:8767/__trace/ingest".
|
|
28
|
+
const INGEST_URL = process.env.TRACE_INGEST_URL || '';
|
|
29
|
+
if (!INGEST_URL) {
|
|
30
|
+
// Not targeted by Trace — stay completely inert.
|
|
31
|
+
module.exports = { active: false };
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Guard against double-injection (e.g. --require listed twice, or re-require).
|
|
36
|
+
if (globalThis.__TRACE_INSTRUMENT_ACTIVE__) {
|
|
37
|
+
module.exports = globalThis.__TRACE_INSTRUMENT_ACTIVE__;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let ingestTarget;
|
|
42
|
+
try {
|
|
43
|
+
ingestTarget = new URL(INGEST_URL);
|
|
44
|
+
} catch {
|
|
45
|
+
// Malformed ingest URL — refuse to activate rather than risk misbehaving.
|
|
46
|
+
module.exports = { active: false };
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const http = require('http');
|
|
51
|
+
const https = require('https');
|
|
52
|
+
|
|
53
|
+
let AsyncLocalStorage;
|
|
54
|
+
try {
|
|
55
|
+
({ AsyncLocalStorage } = require('async_hooks'));
|
|
56
|
+
} catch {
|
|
57
|
+
AsyncLocalStorage = null;
|
|
58
|
+
}
|
|
59
|
+
// Without AsyncLocalStorage we cannot correlate child operations to a request.
|
|
60
|
+
// Server spans still work; child spans simply won't be attached. Never fatal.
|
|
61
|
+
const als = AsyncLocalStorage ? new AsyncLocalStorage() : null;
|
|
62
|
+
|
|
63
|
+
// ── Config / limits ──────────────────────────────────────────────────────
|
|
64
|
+
const SERVICE_NAME = process.env.TRACE_SERVICE_NAME || 'backend';
|
|
65
|
+
const MAX_BUFFER = 2000; // max spans queued for export before dropping
|
|
66
|
+
const FLUSH_INTERVAL_MS = 250; // batch export cadence
|
|
67
|
+
const MAX_BATCH = 200; // spans per export request
|
|
68
|
+
const MAX_ATTR_STRING = 2048; // truncate long attribute strings (e.g. SQL)
|
|
69
|
+
const MAX_URL_LEN = 1024;
|
|
70
|
+
|
|
71
|
+
// ── Safety helpers ─────────────────────────────────────────────────────────
|
|
72
|
+
function safe(fn, fallback) {
|
|
73
|
+
try { return fn(); } catch { return fallback; }
|
|
74
|
+
}
|
|
75
|
+
function truncate(str, max) {
|
|
76
|
+
if (typeof str !== 'string') return str;
|
|
77
|
+
return str.length > max ? str.slice(0, max - 1) + '\u2026' : str;
|
|
78
|
+
}
|
|
79
|
+
function hrNowMs() {
|
|
80
|
+
// Wall-clock ms with sub-ms precision; aligned to Date.now for the UI.
|
|
81
|
+
return Date.now();
|
|
82
|
+
}
|
|
83
|
+
let _seq = 0;
|
|
84
|
+
function genId() {
|
|
85
|
+
// 16 hex chars; collision-safe enough for a dev session.
|
|
86
|
+
_seq = (_seq + 1) >>> 0;
|
|
87
|
+
return (
|
|
88
|
+
Date.now().toString(16) +
|
|
89
|
+
Math.floor(Math.random() * 0xffffff).toString(16).padStart(6, '0') +
|
|
90
|
+
_seq.toString(16).padStart(4, '0')
|
|
91
|
+
).slice(0, 16);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ── Span buffer + exporter ──────────────────────────────────────────────────
|
|
95
|
+
const buffer = [];
|
|
96
|
+
let dropped = 0;
|
|
97
|
+
let exporting = false; // true while our own export request is in flight
|
|
98
|
+
let flushTimer = null;
|
|
99
|
+
|
|
100
|
+
function enqueue(span) {
|
|
101
|
+
if (buffer.length >= MAX_BUFFER) { dropped++; return; }
|
|
102
|
+
buffer.push(span);
|
|
103
|
+
scheduleFlush();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function scheduleFlush() {
|
|
107
|
+
if (flushTimer) return;
|
|
108
|
+
flushTimer = setTimeout(() => {
|
|
109
|
+
flushTimer = null;
|
|
110
|
+
flush();
|
|
111
|
+
}, FLUSH_INTERVAL_MS);
|
|
112
|
+
// Do not keep the event loop alive solely for flushing.
|
|
113
|
+
if (flushTimer.unref) flushTimer.unref();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function flush() {
|
|
117
|
+
if (buffer.length === 0) return;
|
|
118
|
+
const batch = buffer.splice(0, MAX_BATCH);
|
|
119
|
+
const payload = safe(() => JSON.stringify({
|
|
120
|
+
service: SERVICE_NAME,
|
|
121
|
+
pid: process.pid,
|
|
122
|
+
dropped,
|
|
123
|
+
spans: batch,
|
|
124
|
+
}), null);
|
|
125
|
+
if (payload == null) return; // unserializable — drop silently
|
|
126
|
+
dropped = 0;
|
|
127
|
+
|
|
128
|
+
const isHttps = ingestTarget.protocol === 'https:';
|
|
129
|
+
const lib = isHttps ? https : http;
|
|
130
|
+
const opts = {
|
|
131
|
+
hostname: ingestTarget.hostname,
|
|
132
|
+
port: ingestTarget.port || (isHttps ? 443 : 80),
|
|
133
|
+
path: ingestTarget.pathname + (ingestTarget.search || ''),
|
|
134
|
+
method: 'POST',
|
|
135
|
+
headers: {
|
|
136
|
+
'Content-Type': 'application/json',
|
|
137
|
+
'Content-Length': Buffer.byteLength(payload),
|
|
138
|
+
// Marker so our OWN outbound hook ignores this request (loop guard).
|
|
139
|
+
'x-trace-ingest': '1',
|
|
140
|
+
},
|
|
141
|
+
// Never hold up the host process on a slow/absent bridge.
|
|
142
|
+
timeout: 2000,
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
exporting = true;
|
|
146
|
+
let req;
|
|
147
|
+
try {
|
|
148
|
+
req = lib.request(opts, (res) => {
|
|
149
|
+
// Drain and discard.
|
|
150
|
+
res.on('data', () => {});
|
|
151
|
+
res.on('end', () => {});
|
|
152
|
+
res.resume();
|
|
153
|
+
});
|
|
154
|
+
} catch {
|
|
155
|
+
exporting = false;
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
req.on('error', () => { /* bridge down — drop this batch */ });
|
|
159
|
+
req.on('timeout', () => { safe(() => req.destroy()); });
|
|
160
|
+
req.on('close', () => { exporting = false; if (buffer.length) scheduleFlush(); });
|
|
161
|
+
safe(() => { req.end(payload); });
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Best-effort flush on exit so the last requests aren't lost.
|
|
165
|
+
safe(() => {
|
|
166
|
+
process.once('beforeExit', () => { flush(); });
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// ── Span emission ───────────────────────────────────────────────────────────
|
|
170
|
+
function emitSpan(s) {
|
|
171
|
+
// Normalize + clamp before queueing.
|
|
172
|
+
if (s.endTime == null) s.endTime = hrNowMs();
|
|
173
|
+
if (s.duration == null) s.duration = Math.max(0, s.endTime - s.startTime);
|
|
174
|
+
enqueue(s);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function currentContext() {
|
|
178
|
+
return als ? als.getStore() : undefined;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ── HTTP server instrumentation (incoming requests) ─────────────────────────
|
|
182
|
+
// Patch http.Server.prototype.emit so we wrap the 'request' event. This covers
|
|
183
|
+
// http AND https servers (https.Server delegates to the same emit), and every
|
|
184
|
+
// framework built on Node's http server (Express, Fastify, Koa, Nest, raw http).
|
|
185
|
+
function instrumentHttpServer() {
|
|
186
|
+
const proto = http.Server && http.Server.prototype;
|
|
187
|
+
if (!proto || proto.__tracePatched) return;
|
|
188
|
+
const origEmit = proto.emit;
|
|
189
|
+
if (typeof origEmit !== 'function') return;
|
|
190
|
+
|
|
191
|
+
proto.emit = function patchedEmit(event, ...args) {
|
|
192
|
+
if (event !== 'request') {
|
|
193
|
+
return origEmit.apply(this, [event, ...args]);
|
|
194
|
+
}
|
|
195
|
+
const req = args[0];
|
|
196
|
+
const res = args[1];
|
|
197
|
+
// Bail out cleanly if shapes are unexpected.
|
|
198
|
+
if (!req || !res) return origEmit.apply(this, [event, ...args]);
|
|
199
|
+
|
|
200
|
+
// Skip our own ingest traffic (defensive; ingest is outbound, but guard anyway).
|
|
201
|
+
if (safe(() => req.headers && req.headers['x-trace-ingest'] === '1', false)) {
|
|
202
|
+
return origEmit.apply(this, [event, ...args]);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const startTime = hrNowMs();
|
|
206
|
+
const traceId = genId();
|
|
207
|
+
const spanId = genId();
|
|
208
|
+
const method = safe(() => (req.method || 'GET').toUpperCase(), 'GET');
|
|
209
|
+
const rawUrl = safe(() => truncate(req.url || '/', MAX_URL_LEN), '/');
|
|
210
|
+
|
|
211
|
+
const ctx = {
|
|
212
|
+
traceId,
|
|
213
|
+
rootSpanId: spanId,
|
|
214
|
+
// Stack of currently-open span ids so children attach to the nearest parent.
|
|
215
|
+
// For the MVP we attach children directly to the root request span.
|
|
216
|
+
children: 0,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
let finished = false;
|
|
220
|
+
const finalize = () => {
|
|
221
|
+
if (finished) return;
|
|
222
|
+
finished = true;
|
|
223
|
+
const statusCode = safe(() => res.statusCode, 0);
|
|
224
|
+
const route = safe(() => req.route && req.route.path, undefined) ||
|
|
225
|
+
safe(() => req.baseUrl, undefined) ||
|
|
226
|
+
undefined;
|
|
227
|
+
emitSpan({
|
|
228
|
+
traceId,
|
|
229
|
+
spanId,
|
|
230
|
+
parentSpanId: null,
|
|
231
|
+
kind: 'server',
|
|
232
|
+
stage: 'route',
|
|
233
|
+
name: method + ' ' + (route || pathOf(rawUrl)),
|
|
234
|
+
startTime,
|
|
235
|
+
endTime: hrNowMs(),
|
|
236
|
+
attributes: clean({
|
|
237
|
+
'http.method': method,
|
|
238
|
+
'http.target': rawUrl,
|
|
239
|
+
'http.route': route,
|
|
240
|
+
'http.status_code': statusCode,
|
|
241
|
+
'http.status_class': statusClass(statusCode),
|
|
242
|
+
}),
|
|
243
|
+
error: statusCode >= 500
|
|
244
|
+
? { message: 'HTTP ' + statusCode }
|
|
245
|
+
: null,
|
|
246
|
+
});
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
safe(() => {
|
|
250
|
+
res.once('finish', finalize);
|
|
251
|
+
res.once('close', finalize);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
if (!als) {
|
|
255
|
+
return origEmit.apply(this, [event, ...args]);
|
|
256
|
+
}
|
|
257
|
+
// Run the rest of request handling inside the ALS context so outbound
|
|
258
|
+
// calls / db queries can find their parent request.
|
|
259
|
+
return als.run(ctx, () => origEmit.apply(this, [event, ...args]));
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
proto.__tracePatched = true;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function pathOf(u) {
|
|
266
|
+
return safe(() => {
|
|
267
|
+
const q = u.indexOf('?');
|
|
268
|
+
return q === -1 ? u : u.slice(0, q);
|
|
269
|
+
}, u);
|
|
270
|
+
}
|
|
271
|
+
function statusClass(code) {
|
|
272
|
+
if (!code) return 'unknown';
|
|
273
|
+
if (code >= 500) return '5xx';
|
|
274
|
+
if (code >= 400) return '4xx';
|
|
275
|
+
if (code >= 300) return '3xx';
|
|
276
|
+
if (code >= 200) return '2xx';
|
|
277
|
+
return '1xx';
|
|
278
|
+
}
|
|
279
|
+
function clean(obj) {
|
|
280
|
+
const out = {};
|
|
281
|
+
for (const k in obj) {
|
|
282
|
+
const v = obj[k];
|
|
283
|
+
if (v === undefined || v === null) continue;
|
|
284
|
+
out[k] = typeof v === 'string' ? truncate(v, MAX_ATTR_STRING) : v;
|
|
285
|
+
}
|
|
286
|
+
return out;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// ── Outbound HTTP/HTTPS instrumentation (external API calls) ─────────────────
|
|
290
|
+
function instrumentOutbound(lib, scheme) {
|
|
291
|
+
if (!lib || lib.__traceOutboundPatched) return;
|
|
292
|
+
const origRequest = lib.request;
|
|
293
|
+
const origGet = lib.get;
|
|
294
|
+
if (typeof origRequest !== 'function') return;
|
|
295
|
+
|
|
296
|
+
function wrap(orig) {
|
|
297
|
+
return function patched(...args) {
|
|
298
|
+
// Determine if this call originates inside a tracked request AND is not
|
|
299
|
+
// our own ingest export. If either fails, call through untouched.
|
|
300
|
+
const ctx = currentContext();
|
|
301
|
+
let isIngest = false;
|
|
302
|
+
safe(() => {
|
|
303
|
+
for (const a of args) {
|
|
304
|
+
if (a && typeof a === 'object' && a.headers && a.headers['x-trace-ingest'] === '1') {
|
|
305
|
+
isIngest = true; break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
if (exporting || isIngest || !ctx) {
|
|
310
|
+
return orig.apply(this, args);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const startTime = hrNowMs();
|
|
314
|
+
const spanId = genId();
|
|
315
|
+
const meta = safe(() => describeOutbound(scheme, args), {});
|
|
316
|
+
let clientReq;
|
|
317
|
+
try {
|
|
318
|
+
clientReq = orig.apply(this, args);
|
|
319
|
+
} catch (e) {
|
|
320
|
+
// Record the synchronous failure, then rethrow to preserve behavior.
|
|
321
|
+
emitSpan({
|
|
322
|
+
traceId: ctx.traceId, spanId, parentSpanId: ctx.rootSpanId,
|
|
323
|
+
kind: 'client', stage: 'external',
|
|
324
|
+
name: (meta.method || 'GET') + ' ' + (meta.host || 'external'),
|
|
325
|
+
startTime, endTime: hrNowMs(),
|
|
326
|
+
attributes: clean(meta.attributes), error: { message: String(e && e.message || e) },
|
|
327
|
+
});
|
|
328
|
+
throw e;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
safe(() => {
|
|
332
|
+
let done = false;
|
|
333
|
+
const finishClient = (statusCode, errMsg) => {
|
|
334
|
+
if (done) return; done = true;
|
|
335
|
+
emitSpan({
|
|
336
|
+
traceId: ctx.traceId, spanId, parentSpanId: ctx.rootSpanId,
|
|
337
|
+
kind: 'client', stage: 'external',
|
|
338
|
+
name: (meta.method || 'GET') + ' ' + (meta.host || 'external'),
|
|
339
|
+
startTime, endTime: hrNowMs(),
|
|
340
|
+
attributes: clean(Object.assign({}, meta.attributes, {
|
|
341
|
+
'http.status_code': statusCode,
|
|
342
|
+
'http.status_class': statusClass(statusCode),
|
|
343
|
+
})),
|
|
344
|
+
error: errMsg ? { message: errMsg } : (statusCode >= 500 ? { message: 'HTTP ' + statusCode } : null),
|
|
345
|
+
});
|
|
346
|
+
};
|
|
347
|
+
clientReq.once('response', (res) => {
|
|
348
|
+
const code = safe(() => res.statusCode, 0);
|
|
349
|
+
res.once('end', () => finishClient(code));
|
|
350
|
+
// Some responses never emit 'end' if not consumed; also finish on close.
|
|
351
|
+
res.once('close', () => finishClient(code));
|
|
352
|
+
});
|
|
353
|
+
clientReq.once('error', (e) => finishClient(0, String(e && e.message || e)));
|
|
354
|
+
clientReq.once('close', () => finishClient(safe(() => clientReq.res && clientReq.res.statusCode, 0)));
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
return clientReq;
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
safe(() => { lib.request = wrap(origRequest); });
|
|
362
|
+
if (typeof origGet === 'function') {
|
|
363
|
+
// http.get calls http.request internally in most Node versions, but patch
|
|
364
|
+
// it explicitly to be safe across versions.
|
|
365
|
+
safe(() => { lib.get = wrap(origGet); });
|
|
366
|
+
}
|
|
367
|
+
lib.__traceOutboundPatched = true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function describeOutbound(scheme, args) {
|
|
371
|
+
let method = 'GET';
|
|
372
|
+
let host = 'external';
|
|
373
|
+
let pathName = '';
|
|
374
|
+
let url;
|
|
375
|
+
const a0 = args[0];
|
|
376
|
+
if (typeof a0 === 'string') {
|
|
377
|
+
url = safe(() => new URL(a0), null);
|
|
378
|
+
} else if (a0 instanceof URL) {
|
|
379
|
+
url = a0;
|
|
380
|
+
} else if (a0 && typeof a0 === 'object') {
|
|
381
|
+
method = (a0.method || 'GET').toUpperCase();
|
|
382
|
+
host = a0.hostname || a0.host || 'external';
|
|
383
|
+
pathName = a0.path || '';
|
|
384
|
+
}
|
|
385
|
+
// Options object can also be the 2nd arg when 1st is a URL/string.
|
|
386
|
+
const optsArg = (a0 instanceof URL || typeof a0 === 'string') && args[1] && typeof args[1] === 'object' ? args[1] : null;
|
|
387
|
+
if (optsArg && optsArg.method) method = String(optsArg.method).toUpperCase();
|
|
388
|
+
if (url) {
|
|
389
|
+
host = url.host;
|
|
390
|
+
pathName = url.pathname + (url.search || '');
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
method, host,
|
|
394
|
+
attributes: {
|
|
395
|
+
'http.method': method,
|
|
396
|
+
'http.url': truncate((scheme ? scheme + '://' : '') + host + pathName, MAX_URL_LEN),
|
|
397
|
+
'net.peer.name': host,
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// ── fetch() / undici instrumentation (external API calls) ────────────────────
|
|
403
|
+
function instrumentFetch() {
|
|
404
|
+
if (typeof globalThis.fetch !== 'function' || globalThis.fetch.__tracePatched) return;
|
|
405
|
+
const origFetch = globalThis.fetch;
|
|
406
|
+
const patched = function tracedFetch(input, init) {
|
|
407
|
+
const ctx = currentContext();
|
|
408
|
+
if (!ctx || exporting) return origFetch.call(this, input, init);
|
|
409
|
+
|
|
410
|
+
const startTime = hrNowMs();
|
|
411
|
+
const spanId = genId();
|
|
412
|
+
const method = safe(() => ((init && init.method) || (input && input.method) || 'GET').toUpperCase(), 'GET');
|
|
413
|
+
const urlStr = safe(() => {
|
|
414
|
+
if (typeof input === 'string') return input;
|
|
415
|
+
if (input instanceof URL) return input.href;
|
|
416
|
+
if (input && input.url) return input.url;
|
|
417
|
+
return 'external';
|
|
418
|
+
}, 'external');
|
|
419
|
+
// Skip our own ingest export if it ever routed through fetch.
|
|
420
|
+
if (urlStr.indexOf(ingestTarget.host) !== -1 && urlStr.indexOf('__trace') !== -1) {
|
|
421
|
+
return origFetch.call(this, input, init);
|
|
422
|
+
}
|
|
423
|
+
const host = safe(() => new URL(urlStr).host, 'external');
|
|
424
|
+
|
|
425
|
+
let p;
|
|
426
|
+
try {
|
|
427
|
+
p = origFetch.call(this, input, init);
|
|
428
|
+
} catch (e) {
|
|
429
|
+
emitSpan({
|
|
430
|
+
traceId: ctx.traceId, spanId, parentSpanId: ctx.rootSpanId,
|
|
431
|
+
kind: 'client', stage: 'external', name: method + ' ' + host,
|
|
432
|
+
startTime, endTime: hrNowMs(),
|
|
433
|
+
attributes: clean({ 'http.method': method, 'http.url': truncate(urlStr, MAX_URL_LEN) }),
|
|
434
|
+
error: { message: String(e && e.message || e) },
|
|
435
|
+
});
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
return p.then(
|
|
439
|
+
(resp) => {
|
|
440
|
+
const code = safe(() => resp.status, 0);
|
|
441
|
+
emitSpan({
|
|
442
|
+
traceId: ctx.traceId, spanId, parentSpanId: ctx.rootSpanId,
|
|
443
|
+
kind: 'client', stage: 'external', name: method + ' ' + host,
|
|
444
|
+
startTime, endTime: hrNowMs(),
|
|
445
|
+
attributes: clean({ 'http.method': method, 'http.url': truncate(urlStr, MAX_URL_LEN), 'http.status_code': code, 'http.status_class': statusClass(code) }),
|
|
446
|
+
error: code >= 500 ? { message: 'HTTP ' + code } : null,
|
|
447
|
+
});
|
|
448
|
+
return resp;
|
|
449
|
+
},
|
|
450
|
+
(err) => {
|
|
451
|
+
emitSpan({
|
|
452
|
+
traceId: ctx.traceId, spanId, parentSpanId: ctx.rootSpanId,
|
|
453
|
+
kind: 'client', stage: 'external', name: method + ' ' + host,
|
|
454
|
+
startTime, endTime: hrNowMs(),
|
|
455
|
+
attributes: clean({ 'http.method': method, 'http.url': truncate(urlStr, MAX_URL_LEN) }),
|
|
456
|
+
error: { message: String(err && err.message || err) },
|
|
457
|
+
});
|
|
458
|
+
throw err;
|
|
459
|
+
}
|
|
460
|
+
);
|
|
461
|
+
};
|
|
462
|
+
patched.__tracePatched = true;
|
|
463
|
+
safe(() => { globalThis.fetch = patched; });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Activate (each step independently guarded) ───────────────────────────────
|
|
467
|
+
safe(instrumentHttpServer);
|
|
468
|
+
safe(() => instrumentOutbound(http, 'http'));
|
|
469
|
+
safe(() => instrumentOutbound(https, 'https'));
|
|
470
|
+
safe(instrumentFetch);
|
|
471
|
+
|
|
472
|
+
const api = { active: true, version: 1, service: SERVICE_NAME };
|
|
473
|
+
globalThis.__TRACE_INSTRUMENT_ACTIVE__ = api;
|
|
474
|
+
module.exports = api;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gettrace/cli",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"description": "Trace IDE Bridge for connecting local filesystem to Trace browser extensions.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"trace-connect": "./dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"scripts": {
|
|
12
|
-
"build": "esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/index.js",
|
|
12
|
+
"build": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --minify --legal-comments=none --outfile=dist/index.js",
|
|
13
|
+
"build:dev": "rm -rf dist && esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --sourcemap --outfile=dist/index.js",
|
|
13
14
|
"dev": "esbuild src/index.ts --bundle --platform=node --format=esm --packages=external --outfile=dist/index.js --watch",
|
|
14
15
|
"start": "node dist/index.js",
|
|
15
16
|
"prepublishOnly": "npm run build",
|
|
@@ -41,7 +42,7 @@
|
|
|
41
42
|
"@babel/traverse": "^7.24.0",
|
|
42
43
|
"@babel/generator": "^7.24.0",
|
|
43
44
|
"@babel/types": "^7.24.0",
|
|
44
|
-
"@gettrace/agent": "^2.0.
|
|
45
|
+
"@gettrace/agent": "^2.0.6"
|
|
45
46
|
},
|
|
46
47
|
"devDependencies": {
|
|
47
48
|
"@types/node": "^20.0.0",
|
|
@@ -52,9 +53,12 @@
|
|
|
52
53
|
"typescript": "^5.3.0"
|
|
53
54
|
},
|
|
54
55
|
"files": [
|
|
55
|
-
"dist",
|
|
56
|
-
"native-host",
|
|
56
|
+
"dist/index.js",
|
|
57
|
+
"native-host/host.cjs",
|
|
58
|
+
"native-host/host-entry",
|
|
59
|
+
"native-host/package.json",
|
|
57
60
|
"scripts",
|
|
61
|
+
"instrument",
|
|
58
62
|
"README.md"
|
|
59
63
|
]
|
|
60
64
|
}
|