@deepeloper/deeptracker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,74 @@
1
+ # @deepeloper/deeptracker
2
+
3
+ > Auto-instrumenting **deep request tracer** for Node.js. See exactly what your API is doing — every request, every step, every error.
4
+
5
+ It records the full journey of each HTTP request — middleware, database queries, outbound
6
+ calls, errors — as a nested trace, and ships it to your [deeptracker dashboard](https://github.com/deepeloper)
7
+ where you can view it as a waterfall.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm i @deepeloper/deeptracker
13
+ ```
14
+
15
+ ## Quick start
16
+
17
+ Call `init()` once, at the very top of your entry file — **before** you require your
18
+ framework — so the auto-instrumentation is in place:
19
+
20
+ ```ts
21
+ import tracker from '@deepeloper/deeptracker';
22
+
23
+ tracker.init({
24
+ apiKey: 'trk_live_xxx', // from your dashboard
25
+ appName: 'my-api',
26
+ });
27
+
28
+ // ...then start your app as usual. Express, http/https and pg are traced automatically.
29
+ ```
30
+
31
+ That's it. Every request now produces a trace with timing for each step, and any error is
32
+ captured with the span where it happened.
33
+
34
+ ## What's captured automatically
35
+
36
+ | Source | What you get |
37
+ | --------------------------------------------- | --------------------------------------------------------- |
38
+ | Inbound HTTP (any framework on `http.Server`) | a root trace per request (method, path, status, duration) |
39
+ | Express | parameterized route templates (`/users/:id`) |
40
+ | `http` / `https` outbound | child spans for axios / request-based clients |
41
+ | `pg` | database query spans (timing, row count) |
42
+
43
+ ## Manual spans & tags
44
+
45
+ ```ts
46
+ // Wrap your own logic so it shows up in the trace
47
+ const user = await tracker.span('db.getUser', () => db.users.find(id));
48
+
49
+ // Tag the current trace (searchable in the dashboard)
50
+ tracker.setTag('userId', id);
51
+
52
+ // Add a log event to the current span
53
+ tracker.log('cache miss, fetching from DB');
54
+ ```
55
+
56
+ ## Configuration
57
+
58
+ ```ts
59
+ tracker.init({
60
+ apiKey: 'trk_live_xxx',
61
+ appName: 'my-api',
62
+ environment: 'production', // defaults to NODE_ENV
63
+ endpoint: 'https://...', // custom ingest endpoint (defaults to the hosted platform)
64
+ sampleRate: 1.0, // 0.0–1.0, fraction of requests to trace
65
+ ignore: ['/health', '/favicon.ico'], // paths (or predicates) to skip
66
+ flushIntervalMs: 5000, // how often buffered traces are shipped
67
+ });
68
+ ```
69
+
70
+ Without an `endpoint`, traces print to the console as a waterfall — handy in development.
71
+
72
+ ## License
73
+
74
+ MIT
package/dist/index.cjs ADDED
@@ -0,0 +1,546 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ default: () => index_default,
34
+ flush: () => flush,
35
+ getConfig: () => getConfig,
36
+ init: () => init,
37
+ log: () => log,
38
+ setTag: () => setTag,
39
+ span: () => span
40
+ });
41
+ module.exports = __toCommonJS(index_exports);
42
+
43
+ // src/core/context.ts
44
+ var import_node_async_hooks = require("async_hooks");
45
+ var als = new import_node_async_hooks.AsyncLocalStorage();
46
+ function getStore() {
47
+ return als.getStore();
48
+ }
49
+ function runWith(store, fn) {
50
+ return als.run(store, fn);
51
+ }
52
+
53
+ // src/core/tracer.ts
54
+ var import_node_perf_hooks = require("perf_hooks");
55
+
56
+ // src/core/ids.ts
57
+ var import_node_crypto = require("crypto");
58
+ function newTraceId() {
59
+ return (0, import_node_crypto.randomBytes)(16).toString("hex");
60
+ }
61
+ function newSpanId() {
62
+ return (0, import_node_crypto.randomBytes)(8).toString("hex");
63
+ }
64
+
65
+ // src/core/tracer.ts
66
+ var state = {
67
+ appName: "unknown",
68
+ environment: "development",
69
+ sampleRate: 1,
70
+ ignore: [],
71
+ sink: null
72
+ };
73
+ function configure(patch) {
74
+ state = { ...state, ...patch };
75
+ }
76
+ function shouldTrace(req) {
77
+ if (state.sampleRate < 1 && Math.random() > state.sampleRate) return false;
78
+ const path = stripQuery(req.url ?? "");
79
+ for (const rule of state.ignore) {
80
+ if (typeof rule === "string" ? rule === path : safeRule(rule, req)) return false;
81
+ }
82
+ return true;
83
+ }
84
+ function createTrace() {
85
+ return {
86
+ traceId: newTraceId(),
87
+ appName: state.appName,
88
+ environment: state.environment,
89
+ rootSpanId: newSpanId(),
90
+ startWall: Date.now(),
91
+ startMono: import_node_perf_hooks.performance.now(),
92
+ tags: {},
93
+ spans: [],
94
+ http: {},
95
+ ended: false
96
+ };
97
+ }
98
+ function startSpan(name, type, metadata = {}) {
99
+ const store = getStore();
100
+ if (!store) return null;
101
+ const parentId = store.parent ? store.parent.spanId : store.trace.rootSpanId;
102
+ const span2 = {
103
+ spanId: newSpanId(),
104
+ parentSpanId: parentId,
105
+ traceId: store.trace.traceId,
106
+ name,
107
+ type,
108
+ startTime: Date.now(),
109
+ endTime: 0,
110
+ duration: 0,
111
+ status: "ok",
112
+ metadata,
113
+ events: [],
114
+ _startMono: import_node_perf_hooks.performance.now()
115
+ };
116
+ store.trace.spans.push(span2);
117
+ return span2;
118
+ }
119
+ function endSpan(span2, error) {
120
+ if (!span2 || span2.endTime !== 0) return;
121
+ span2.duration = import_node_perf_hooks.performance.now() - span2._startMono;
122
+ span2.endTime = span2.startTime + span2.duration;
123
+ if (error !== void 0) {
124
+ span2.status = "error";
125
+ span2.error = toSpanError(error);
126
+ }
127
+ }
128
+ async function runInSpan(name, type, fn) {
129
+ const store = getStore();
130
+ if (!store) return await fn();
131
+ const span2 = startSpan(name, type);
132
+ if (!span2) return await fn();
133
+ return await runWith({ trace: store.trace, parent: span2 }, async () => {
134
+ try {
135
+ const result = await fn();
136
+ endSpan(span2);
137
+ return result;
138
+ } catch (err) {
139
+ endSpan(span2, err);
140
+ throw err;
141
+ }
142
+ });
143
+ }
144
+ function endTrace(trace, statusCode, path) {
145
+ if (trace.ended) return;
146
+ trace.ended = true;
147
+ const duration = import_node_perf_hooks.performance.now() - trace.startMono;
148
+ const hasError = statusCode >= 500 || trace.spans.some((s) => s.status === "error");
149
+ const out = {
150
+ traceId: trace.traceId,
151
+ appName: trace.appName,
152
+ environment: trace.environment,
153
+ startTime: trace.startWall,
154
+ endTime: trace.startWall + duration,
155
+ duration,
156
+ status: hasError ? "error" : "ok",
157
+ http: {
158
+ method: trace.http.method ?? "GET",
159
+ path,
160
+ rawPath: trace.http.rawPath ?? path,
161
+ statusCode,
162
+ ...trace.http.userAgent !== void 0 ? { userAgent: trace.http.userAgent } : {}
163
+ },
164
+ tags: trace.tags,
165
+ rootSpanId: trace.rootSpanId,
166
+ spans: trace.spans.map(serializeSpan)
167
+ };
168
+ state.sink?.(out);
169
+ }
170
+ function serializeSpan(span2) {
171
+ const { _startMono: _ignored, ...rest } = span2;
172
+ void _ignored;
173
+ return rest;
174
+ }
175
+ function toSpanError(error) {
176
+ if (error instanceof Error) {
177
+ return {
178
+ message: error.message,
179
+ stack: error.stack ?? "",
180
+ type: error.name || "Error"
181
+ };
182
+ }
183
+ return { message: String(error), stack: "", type: "Error" };
184
+ }
185
+ function safeRule(rule, req) {
186
+ try {
187
+ return rule(req);
188
+ } catch {
189
+ return false;
190
+ }
191
+ }
192
+ function stripQuery(url) {
193
+ const q = url.indexOf("?");
194
+ return q === -1 ? url : url.slice(0, q);
195
+ }
196
+
197
+ // src/core/buffer.ts
198
+ var DEFAULTS = { maxSize: 100, flushIntervalMs: 5e3 };
199
+ var TraceBuffer = class {
200
+ constructor(exporter, opts = {}) {
201
+ this.exporter = exporter;
202
+ this.opts = { ...DEFAULTS, ...opts };
203
+ }
204
+ exporter;
205
+ items = [];
206
+ timer = null;
207
+ opts;
208
+ start() {
209
+ if (this.timer) return;
210
+ this.timer = setInterval(() => void this.flush(), this.opts.flushIntervalMs);
211
+ this.timer.unref?.();
212
+ }
213
+ add(trace) {
214
+ this.items.push(trace);
215
+ if (this.items.length >= this.opts.maxSize) void this.flush();
216
+ }
217
+ async flush() {
218
+ if (this.items.length === 0) return;
219
+ const batch = this.items;
220
+ this.items = [];
221
+ try {
222
+ await this.exporter.export(batch);
223
+ } catch {
224
+ }
225
+ }
226
+ async shutdown() {
227
+ if (this.timer) {
228
+ clearInterval(this.timer);
229
+ this.timer = null;
230
+ }
231
+ await this.flush();
232
+ }
233
+ };
234
+
235
+ // src/transport/console.ts
236
+ var ConsoleExporter = class {
237
+ export(traces) {
238
+ for (const trace of traces) {
239
+ this.print(trace);
240
+ }
241
+ }
242
+ print(trace) {
243
+ const flag = trace.status === "error" ? "\u2717" : "\u2713";
244
+ const head = `${flag} ${trace.http.method} ${trace.http.path} ${trace.http.statusCode} ${fmt(trace.duration)}`;
245
+ console.log(`
246
+ [tracker] ${head} (${trace.traceId})`);
247
+ const byParent = /* @__PURE__ */ new Map();
248
+ for (const span2 of trace.spans) {
249
+ const key = span2.parentSpanId ?? trace.rootSpanId;
250
+ const list = byParent.get(key) ?? [];
251
+ list.push(span2);
252
+ byParent.set(key, list);
253
+ }
254
+ const walk = (parentId, depth) => {
255
+ const children = (byParent.get(parentId) ?? []).sort((a, b) => a.startTime - b.startTime);
256
+ for (const span2 of children) {
257
+ const indent = " ".repeat(depth + 1);
258
+ const mark = span2.status === "error" ? " \u2717" : "";
259
+ console.log(`${indent}${span2.type}: ${span2.name} ${fmt(span2.duration)}${mark}`);
260
+ if (span2.error) {
261
+ console.log(`${indent} \u21B3 ${span2.error.type}: ${span2.error.message}`);
262
+ }
263
+ walk(span2.spanId, depth + 1);
264
+ }
265
+ };
266
+ walk(trace.rootSpanId, 0);
267
+ }
268
+ };
269
+ function fmt(ms) {
270
+ return ms < 1e3 ? `${Math.round(ms)}ms` : `${(ms / 1e3).toFixed(2)}s`;
271
+ }
272
+
273
+ // src/transport/http.ts
274
+ var HttpExporter = class {
275
+ constructor(endpoint, apiKey, timeoutMs = 3e3) {
276
+ this.endpoint = endpoint;
277
+ this.apiKey = apiKey;
278
+ this.timeoutMs = timeoutMs;
279
+ }
280
+ endpoint;
281
+ apiKey;
282
+ timeoutMs;
283
+ async export(traces) {
284
+ const controller = new AbortController();
285
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
286
+ try {
287
+ await fetch(this.endpoint, {
288
+ method: "POST",
289
+ headers: {
290
+ "content-type": "application/json",
291
+ authorization: `Bearer ${this.apiKey}`
292
+ },
293
+ body: JSON.stringify({ traces }),
294
+ signal: controller.signal
295
+ });
296
+ } catch {
297
+ } finally {
298
+ clearTimeout(timer);
299
+ }
300
+ }
301
+ };
302
+
303
+ // src/instruments/http.ts
304
+ var import_node_http = __toESM(require("http"), 1);
305
+ var import_node_https = __toESM(require("https"), 1);
306
+
307
+ // src/instruments/wrap.ts
308
+ var WRAPPED = /* @__PURE__ */ Symbol.for("deeptracker.wrapped");
309
+ function wrap(target, name, factory) {
310
+ const original = target[name];
311
+ if (typeof original !== "function") return;
312
+ if (original[WRAPPED]) return;
313
+ const wrapped = factory(original);
314
+ Object.defineProperty(wrapped, "name", {
315
+ value: original.name,
316
+ configurable: true
317
+ });
318
+ wrapped[WRAPPED] = true;
319
+ wrapped.__original = original;
320
+ target[name] = wrapped;
321
+ }
322
+
323
+ // src/instruments/http.ts
324
+ var HANDLED = /* @__PURE__ */ Symbol.for("deeptracker.http.handled");
325
+ function installHttpServer() {
326
+ const patchServer = (proto) => {
327
+ wrap(
328
+ proto,
329
+ "emit",
330
+ (emit) => function(event, ...args) {
331
+ if (event !== "request") return emit.call(this, event, ...args);
332
+ const req = args[0];
333
+ const res = args[1];
334
+ if (!req || !res || req[HANDLED]) return emit.call(this, event, ...args);
335
+ req[HANDLED] = true;
336
+ if (!shouldTrace(req)) return emit.call(this, event, ...args);
337
+ const trace = createTrace();
338
+ trace.http.method = req.method ?? "GET";
339
+ trace.http.rawPath = stripQuery(String(req.url ?? "/"));
340
+ const ua = req.headers?.["user-agent"];
341
+ if (typeof ua === "string") trace.http.userAgent = ua;
342
+ return runWith({ trace, parent: null }, () => {
343
+ res.on("finish", () => {
344
+ endTrace(trace, res.statusCode ?? 0, resolvePath(req, trace.http.rawPath ?? "/"));
345
+ });
346
+ return emit.call(this, event, ...args);
347
+ });
348
+ }
349
+ );
350
+ };
351
+ patchServer(import_node_http.default.Server.prototype);
352
+ if (import_node_https.default.Server && import_node_https.default.Server.prototype !== import_node_http.default.Server.prototype) {
353
+ patchServer(import_node_https.default.Server.prototype);
354
+ }
355
+ }
356
+ function resolvePath(req, fallback) {
357
+ const routePath = req.route?.path;
358
+ if (typeof routePath === "string") {
359
+ return (req.baseUrl ?? "") + routePath;
360
+ }
361
+ return fallback;
362
+ }
363
+ function installHttpClient() {
364
+ for (const [mod, scheme] of [
365
+ [import_node_http.default, "http"],
366
+ [import_node_https.default, "https"]
367
+ ]) {
368
+ for (const method of ["request", "get"]) {
369
+ wrap(
370
+ mod,
371
+ method,
372
+ (original) => function(...args) {
373
+ const desc = describeOutbound(args, scheme);
374
+ const span2 = startSpan(`${desc.method} ${desc.url}`, "http", {
375
+ method: desc.method,
376
+ url: desc.url
377
+ });
378
+ if (!span2) return original.apply(this, args);
379
+ const req = original.apply(this, args);
380
+ let settled = false;
381
+ const finish = (err) => {
382
+ if (settled) return;
383
+ settled = true;
384
+ endSpan(span2, err);
385
+ };
386
+ try {
387
+ req.on("response", (res) => {
388
+ span2.metadata.statusCode = res.statusCode;
389
+ });
390
+ req.on("error", (err) => finish(err));
391
+ req.on("close", () => finish());
392
+ } catch {
393
+ finish();
394
+ }
395
+ return req;
396
+ }
397
+ );
398
+ }
399
+ }
400
+ }
401
+ function describeOutbound(args, scheme) {
402
+ const first = args[0];
403
+ let method = "GET";
404
+ let url = "";
405
+ if (typeof first === "string") {
406
+ url = first;
407
+ } else if (first instanceof URL) {
408
+ url = first.toString();
409
+ } else if (first && typeof first === "object") {
410
+ const host = first.hostname ?? first.host ?? "localhost";
411
+ const path = first.path ?? "/";
412
+ method = first.method ?? "GET";
413
+ url = `${scheme}://${host}${path}`;
414
+ }
415
+ const second = args[1];
416
+ if (second && typeof second === "object" && typeof second.method === "string") {
417
+ method = second.method;
418
+ }
419
+ return { method, url };
420
+ }
421
+
422
+ // src/instruments/pg.ts
423
+ var import_node_module = require("module");
424
+ var import_meta = {};
425
+ function patchPgClient(ClientClass) {
426
+ if (!ClientClass?.prototype) return;
427
+ wrap(
428
+ ClientClass.prototype,
429
+ "query",
430
+ (original) => function(...args) {
431
+ const span2 = startSpan(describeQuery(args[0]), "db", { query: extractSql(args[0]) });
432
+ if (!span2) return original.apply(this, args);
433
+ const last = args[args.length - 1];
434
+ if (typeof last === "function") {
435
+ args[args.length - 1] = function(...cbArgs) {
436
+ const [err, result] = cbArgs;
437
+ if (result?.rowCount != null) span2.metadata.rowCount = result.rowCount;
438
+ endSpan(span2, err ?? void 0);
439
+ return last.apply(this, cbArgs);
440
+ };
441
+ return original.apply(this, args);
442
+ }
443
+ const promise = original.apply(this, args);
444
+ if (promise && typeof promise.then === "function") {
445
+ return promise.then(
446
+ (result) => {
447
+ if (result?.rowCount != null) span2.metadata.rowCount = result.rowCount;
448
+ endSpan(span2);
449
+ return result;
450
+ },
451
+ (err) => {
452
+ endSpan(span2, err);
453
+ throw err;
454
+ }
455
+ );
456
+ }
457
+ endSpan(span2);
458
+ return promise;
459
+ }
460
+ );
461
+ }
462
+ function installPg() {
463
+ try {
464
+ const require2 = (0, import_node_module.createRequire)(import_meta.url);
465
+ const pg = require2("pg");
466
+ patchPgClient(pg.Client);
467
+ } catch {
468
+ }
469
+ }
470
+ function extractSql(arg) {
471
+ const text = typeof arg === "string" ? arg : arg?.text ?? "";
472
+ return String(text).slice(0, 2e3);
473
+ }
474
+ function describeQuery(arg) {
475
+ const sql = extractSql(arg).trim().replace(/\s+/g, " ");
476
+ const verb = sql.split(" ")[0]?.toUpperCase() ?? "QUERY";
477
+ return `db.${verb.toLowerCase()}`;
478
+ }
479
+
480
+ // src/index.ts
481
+ var activeConfig = null;
482
+ var buffer = null;
483
+ function init(config) {
484
+ if (!config.apiKey) throw new Error("[tracker] `apiKey` is required");
485
+ if (!config.appName) throw new Error("[tracker] `appName` is required");
486
+ activeConfig = config;
487
+ const environment = config.environment ?? process.env["NODE_ENV"] ?? "development";
488
+ const exporter = config.endpoint !== void 0 ? new HttpExporter(config.endpoint, config.apiKey) : new ConsoleExporter();
489
+ buffer = new TraceBuffer(exporter, {
490
+ ...config.maxBatchSize !== void 0 ? { maxSize: config.maxBatchSize } : {},
491
+ ...config.flushIntervalMs !== void 0 ? { flushIntervalMs: config.flushIntervalMs } : {}
492
+ });
493
+ buffer.start();
494
+ configure({
495
+ appName: config.appName,
496
+ environment,
497
+ sampleRate: config.sampleRate ?? 1,
498
+ ignore: config.ignore ?? [],
499
+ sink: (trace) => buffer?.add(trace)
500
+ });
501
+ installHttpServer();
502
+ installHttpClient();
503
+ installPg();
504
+ registerShutdown();
505
+ }
506
+ var shutdownRegistered = false;
507
+ function registerShutdown() {
508
+ if (shutdownRegistered) return;
509
+ shutdownRegistered = true;
510
+ const stop = () => {
511
+ void buffer?.shutdown();
512
+ };
513
+ process.once("beforeExit", stop);
514
+ process.once("SIGTERM", stop);
515
+ process.once("SIGINT", stop);
516
+ }
517
+ async function span(name, fn) {
518
+ return await runInSpan(name, "custom", fn);
519
+ }
520
+ function setTag(key, value) {
521
+ const store = getStore();
522
+ if (store) store.trace.tags[key] = String(value);
523
+ }
524
+ function log(message) {
525
+ const store = getStore();
526
+ if (store?.parent) {
527
+ store.parent.events.push({ timestamp: Date.now(), message });
528
+ }
529
+ }
530
+ async function flush() {
531
+ await buffer?.flush();
532
+ }
533
+ function getConfig() {
534
+ return activeConfig;
535
+ }
536
+ var index_default = { init, span, setTag, log, flush, getConfig };
537
+ // Annotate the CommonJS export names for ESM import in node:
538
+ 0 && (module.exports = {
539
+ flush,
540
+ getConfig,
541
+ init,
542
+ log,
543
+ setTag,
544
+ span
545
+ });
546
+ //# sourceMappingURL=index.cjs.map