@deeptracer/core 0.2.0 → 0.3.1

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/internal.cjs CHANGED
@@ -52,118 +52,167 @@ var Batcher = class {
52
52
  startTimer() {
53
53
  this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
54
54
  }
55
- destroy() {
55
+ async destroy() {
56
56
  if (this.timer) clearInterval(this.timer);
57
+ this.timer = null;
57
58
  this.flush();
58
59
  }
59
60
  };
60
61
 
62
+ // src/version.ts
63
+ var SDK_VERSION = "0.3.1";
64
+ var SDK_NAME = "core";
65
+
61
66
  // src/transport.ts
62
67
  var Transport = class {
63
68
  constructor(config) {
64
69
  this.config = config;
65
70
  }
66
- async sendLogs(logs) {
67
- try {
68
- const res = await fetch(`${this.config.endpoint}/ingest/logs`, {
69
- method: "POST",
70
- headers: {
71
- "Content-Type": "application/json",
72
- Authorization: `Bearer ${this.config.apiKey}`
73
- },
74
- body: JSON.stringify({
75
- product: this.config.product,
76
- service: this.config.service,
77
- environment: this.config.environment,
78
- logs
79
- })
80
- });
81
- if (!res.ok) {
71
+ inFlightRequests = /* @__PURE__ */ new Set();
72
+ /**
73
+ * Send a request with automatic retry and exponential backoff.
74
+ * Retries up to `maxRetries` times on network errors and 5xx responses.
75
+ * Does NOT retry on 4xx (client errors — bad payload, auth failure, etc.).
76
+ */
77
+ async sendWithRetry(url, body, label, maxRetries = 3) {
78
+ const baseDelays = [1e3, 2e3, 4e3];
79
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
80
+ try {
81
+ const res = await fetch(url, {
82
+ method: "POST",
83
+ headers: {
84
+ "Content-Type": "application/json",
85
+ Authorization: `Bearer ${this.config.apiKey}`,
86
+ "x-deeptracer-sdk": `${SDK_NAME}/${SDK_VERSION}`
87
+ },
88
+ body: JSON.stringify(body)
89
+ });
90
+ if (res.ok) return;
91
+ if (res.status >= 400 && res.status < 500) {
92
+ console.warn(
93
+ `[@deeptracer/core] Failed to send ${label}: ${res.status} ${res.statusText}`
94
+ );
95
+ return;
96
+ }
97
+ if (attempt < maxRetries) {
98
+ await this.sleep(this.jitter(baseDelays[attempt]));
99
+ continue;
100
+ }
82
101
  console.warn(
83
- `[@deeptracer/core] Failed to send logs: ${res.status} ${res.statusText}`
102
+ `[@deeptracer/core] Failed to send ${label}: ${res.status} ${res.statusText} (exhausted ${maxRetries} retries)`
84
103
  );
85
- }
86
- } catch {
87
- console.warn(
88
- "[@deeptracer/core] Failed to send logs, falling back to console"
89
- );
90
- }
91
- }
92
- async sendError(error) {
93
- try {
94
- const res = await fetch(`${this.config.endpoint}/ingest/errors`, {
95
- method: "POST",
96
- headers: {
97
- "Content-Type": "application/json",
98
- Authorization: `Bearer ${this.config.apiKey}`
99
- },
100
- body: JSON.stringify({
101
- ...error,
102
- product: this.config.product,
103
- service: this.config.service,
104
- environment: this.config.environment
105
- })
106
- });
107
- if (!res.ok) {
104
+ } catch {
105
+ if (attempt < maxRetries) {
106
+ await this.sleep(this.jitter(baseDelays[attempt]));
107
+ continue;
108
+ }
108
109
  console.warn(
109
- `[@deeptracer/core] Failed to send error: ${res.status} ${res.statusText}`
110
+ `[@deeptracer/core] Failed to send ${label} (exhausted ${maxRetries} retries)`
110
111
  );
111
112
  }
112
- } catch {
113
- console.warn("[@deeptracer/core] Failed to send error report");
114
- console.error(error.error_message);
115
113
  }
116
114
  }
115
+ /** Add +/- 20% jitter to a delay to prevent thundering herd. */
116
+ jitter(ms) {
117
+ const factor = 0.8 + Math.random() * 0.4;
118
+ return Math.round(ms * factor);
119
+ }
120
+ sleep(ms) {
121
+ return new Promise((resolve) => setTimeout(resolve, ms));
122
+ }
123
+ /** Track an in-flight request and remove it when done. */
124
+ track(promise) {
125
+ this.inFlightRequests.add(promise);
126
+ promise.finally(() => this.inFlightRequests.delete(promise));
127
+ }
128
+ async sendLogs(logs) {
129
+ const p = this.sendWithRetry(
130
+ `${this.config.endpoint}/ingest/logs`,
131
+ {
132
+ product: this.config.product,
133
+ service: this.config.service,
134
+ environment: this.config.environment,
135
+ logs
136
+ },
137
+ "logs"
138
+ );
139
+ this.track(p);
140
+ return p;
141
+ }
142
+ async sendError(error) {
143
+ const p = this.sendWithRetry(
144
+ `${this.config.endpoint}/ingest/errors`,
145
+ {
146
+ ...error,
147
+ product: this.config.product,
148
+ service: this.config.service,
149
+ environment: this.config.environment
150
+ },
151
+ "error"
152
+ );
153
+ this.track(p);
154
+ return p;
155
+ }
117
156
  async sendTrace(span) {
118
- try {
119
- const res = await fetch(`${this.config.endpoint}/ingest/traces`, {
120
- method: "POST",
121
- headers: {
122
- "Content-Type": "application/json",
123
- Authorization: `Bearer ${this.config.apiKey}`
124
- },
125
- body: JSON.stringify({
126
- ...span,
127
- product: this.config.product,
128
- service: this.config.service,
129
- environment: this.config.environment
130
- })
131
- });
132
- if (!res.ok) {
133
- console.warn(
134
- `[@deeptracer/core] Failed to send trace: ${res.status} ${res.statusText}`
135
- );
136
- }
137
- } catch {
138
- console.warn("[@deeptracer/core] Failed to send trace span");
139
- }
157
+ const p = this.sendWithRetry(
158
+ `${this.config.endpoint}/ingest/traces`,
159
+ {
160
+ ...span,
161
+ product: this.config.product,
162
+ service: this.config.service,
163
+ environment: this.config.environment
164
+ },
165
+ "trace"
166
+ );
167
+ this.track(p);
168
+ return p;
140
169
  }
141
170
  async sendLLMUsage(report) {
142
- try {
143
- const res = await fetch(`${this.config.endpoint}/ingest/llm`, {
144
- method: "POST",
145
- headers: {
146
- "Content-Type": "application/json",
147
- Authorization: `Bearer ${this.config.apiKey}`
148
- },
149
- body: JSON.stringify({
150
- ...report,
151
- product: this.config.product,
152
- service: this.config.service,
153
- environment: this.config.environment
154
- })
155
- });
156
- if (!res.ok) {
157
- console.warn(
158
- `[@deeptracer/core] Failed to send LLM usage: ${res.status} ${res.statusText}`
159
- );
160
- }
161
- } catch {
162
- console.warn("[@deeptracer/core] Failed to send LLM usage report");
163
- }
171
+ const p = this.sendWithRetry(
172
+ `${this.config.endpoint}/ingest/llm`,
173
+ {
174
+ ...report,
175
+ product: this.config.product,
176
+ service: this.config.service,
177
+ environment: this.config.environment
178
+ },
179
+ "LLM usage"
180
+ );
181
+ this.track(p);
182
+ return p;
183
+ }
184
+ /**
185
+ * Wait for all in-flight requests to complete, with a timeout.
186
+ * Used by `logger.destroy()` to ensure data is sent before process exit.
187
+ *
188
+ * @param timeoutMs - Maximum time to wait (default: 2000ms)
189
+ */
190
+ async drain(timeoutMs = 2e3) {
191
+ if (this.inFlightRequests.size === 0) return;
192
+ const allDone = Promise.all(this.inFlightRequests).then(() => {
193
+ });
194
+ const timeout = new Promise((resolve) => setTimeout(resolve, timeoutMs));
195
+ await Promise.race([allDone, timeout]);
164
196
  }
165
197
  };
166
198
 
199
+ // src/state.ts
200
+ function createLoggerState(maxBreadcrumbs) {
201
+ return {
202
+ user: null,
203
+ tags: {},
204
+ contexts: {},
205
+ breadcrumbs: [],
206
+ maxBreadcrumbs
207
+ };
208
+ }
209
+ function addBreadcrumb(state, breadcrumb) {
210
+ state.breadcrumbs.push(breadcrumb);
211
+ if (state.breadcrumbs.length > state.maxBreadcrumbs) {
212
+ state.breadcrumbs.shift();
213
+ }
214
+ }
215
+
167
216
  // src/logger.ts
168
217
  function generateId() {
169
218
  const bytes = new Uint8Array(8);
@@ -182,11 +231,13 @@ var Logger = class _Logger {
182
231
  transport;
183
232
  contextName;
184
233
  config;
234
+ state;
185
235
  requestMeta;
186
- constructor(config, contextName, requestMeta) {
236
+ constructor(config, contextName, requestMeta, state) {
187
237
  this.config = config;
188
238
  this.contextName = contextName;
189
239
  this.requestMeta = requestMeta;
240
+ this.state = state ?? createLoggerState(config.maxBreadcrumbs ?? 20);
190
241
  this.transport = new Transport(config);
191
242
  this.batcher = new Batcher(
192
243
  { batchSize: config.batchSize, flushIntervalMs: config.flushIntervalMs },
@@ -195,6 +246,111 @@ var Logger = class _Logger {
195
246
  }
196
247
  );
197
248
  }
249
+ // ---------------------------------------------------------------------------
250
+ // User context
251
+ // ---------------------------------------------------------------------------
252
+ /**
253
+ * Set the current user context. Attached to all subsequent logs, errors, spans, and LLM reports.
254
+ * Shared across all child loggers (withContext, forRequest).
255
+ *
256
+ * @example
257
+ * ```ts
258
+ * logger.setUser({ id: "u_123", email: "user@example.com", plan: "pro" })
259
+ * ```
260
+ */
261
+ setUser(user) {
262
+ this.state.user = user;
263
+ }
264
+ /** Clear the current user context. */
265
+ clearUser() {
266
+ this.state.user = null;
267
+ }
268
+ // ---------------------------------------------------------------------------
269
+ // Tags & Context
270
+ // ---------------------------------------------------------------------------
271
+ /**
272
+ * Set global tags (flat string key-values). Merged into all events' metadata as `_tags`.
273
+ * Tags are indexed and searchable on the dashboard.
274
+ *
275
+ * @example
276
+ * ```ts
277
+ * logger.setTags({ release: "1.2.3", region: "us-east-1" })
278
+ * ```
279
+ */
280
+ setTags(tags) {
281
+ Object.assign(this.state.tags, tags);
282
+ }
283
+ /** Clear all global tags. */
284
+ clearTags() {
285
+ this.state.tags = {};
286
+ }
287
+ /**
288
+ * Set a named context block. Merged into metadata as `_contexts.{name}`.
289
+ * Contexts are structured objects attached for reference (not necessarily indexed).
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * logger.setContext("server", { hostname: "web-3", memory: "4gb" })
294
+ * ```
295
+ */
296
+ setContext(name, data) {
297
+ this.state.contexts[name] = data;
298
+ }
299
+ /** Clear a specific context block, or all contexts if no name is given. */
300
+ clearContext(name) {
301
+ if (name) {
302
+ delete this.state.contexts[name];
303
+ } else {
304
+ this.state.contexts = {};
305
+ }
306
+ }
307
+ // ---------------------------------------------------------------------------
308
+ // Breadcrumbs
309
+ // ---------------------------------------------------------------------------
310
+ /**
311
+ * Manually add a breadcrumb to the trail.
312
+ * Breadcrumbs are also recorded automatically for every log, span, and error.
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * logger.addBreadcrumb({ type: "http", message: "POST /api/checkout" })
317
+ * ```
318
+ */
319
+ addBreadcrumb(breadcrumb) {
320
+ addBreadcrumb(this.state, {
321
+ type: breadcrumb.type,
322
+ message: breadcrumb.message,
323
+ timestamp: breadcrumb.timestamp || (/* @__PURE__ */ new Date()).toISOString()
324
+ });
325
+ }
326
+ // ---------------------------------------------------------------------------
327
+ // Internal helpers
328
+ // ---------------------------------------------------------------------------
329
+ /** Merge user, tags, and contexts from shared state into event metadata. */
330
+ mergeStateMetadata(metadata) {
331
+ const { user, tags, contexts } = this.state;
332
+ const hasUser = user !== null;
333
+ const hasTags = Object.keys(tags).length > 0;
334
+ const hasContexts = Object.keys(contexts).length > 0;
335
+ if (!hasUser && !hasTags && !hasContexts && !metadata) return void 0;
336
+ const result = { ...metadata };
337
+ if (hasUser) result.user = user;
338
+ if (hasTags) result._tags = { ...tags };
339
+ if (hasContexts) result._contexts = { ...contexts };
340
+ return result;
341
+ }
342
+ /** Run the beforeSend hook. If the hook throws, pass the event through. */
343
+ applyBeforeSend(event) {
344
+ if (!this.config.beforeSend) return event;
345
+ try {
346
+ return this.config.beforeSend(event);
347
+ } catch {
348
+ return event;
349
+ }
350
+ }
351
+ // ---------------------------------------------------------------------------
352
+ // Logging
353
+ // ---------------------------------------------------------------------------
198
354
  log(level, message, dataOrError, maybeError) {
199
355
  let metadata;
200
356
  let error;
@@ -216,6 +372,7 @@ var Logger = class _Logger {
216
372
  }
217
373
  };
218
374
  }
375
+ metadata = this.mergeStateMetadata(metadata);
219
376
  const entry = {
220
377
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
221
378
  level,
@@ -227,17 +384,25 @@ var Logger = class _Logger {
227
384
  request_id: this.requestMeta?.request_id,
228
385
  vercel_id: this.requestMeta?.vercel_id
229
386
  };
387
+ const hookResult = this.applyBeforeSend({ type: "log", data: entry });
388
+ if (hookResult === null) return;
389
+ const finalEntry = hookResult.data;
230
390
  if (this.config.debug) {
231
391
  const prefix = this.contextName ? `[${this.contextName}]` : "";
232
392
  const lvl = level.toUpperCase().padEnd(5);
233
393
  const consoleFn = level === "error" ? _originalConsole.error : level === "warn" ? _originalConsole.warn : level === "debug" ? _originalConsole.debug : _originalConsole.log;
234
- if (metadata) {
235
- consoleFn(`${lvl} ${prefix} ${message}`, metadata);
394
+ if (finalEntry.metadata) {
395
+ consoleFn(`${lvl} ${prefix} ${message}`, finalEntry.metadata);
236
396
  } else {
237
397
  consoleFn(`${lvl} ${prefix} ${message}`);
238
398
  }
239
399
  }
240
- this.batcher.add(entry);
400
+ addBreadcrumb(this.state, {
401
+ type: "log",
402
+ message: `[${level}] ${message}`,
403
+ timestamp: entry.timestamp
404
+ });
405
+ this.batcher.add(finalEntry);
241
406
  }
242
407
  /** Log a debug message. */
243
408
  debug(message, dataOrError, error) {
@@ -255,11 +420,14 @@ var Logger = class _Logger {
255
420
  error(message, dataOrError, error) {
256
421
  this.log("error", message, dataOrError, error);
257
422
  }
258
- /** Create a context-scoped logger. All logs include the context name. */
423
+ // ---------------------------------------------------------------------------
424
+ // Child loggers
425
+ // ---------------------------------------------------------------------------
426
+ /** Create a context-scoped logger. All logs include the context name. Shares state with parent. */
259
427
  withContext(name) {
260
- return new _Logger(this.config, name, this.requestMeta);
428
+ return new _Logger(this.config, name, this.requestMeta, this.state);
261
429
  }
262
- /** Create a request-scoped logger that extracts trace context from headers. */
430
+ /** Create a request-scoped logger that extracts trace context from headers. Shares state with parent. */
263
431
  forRequest(request) {
264
432
  const vercelId = request.headers.get("x-vercel-id") || void 0;
265
433
  const requestId = request.headers.get("x-request-id") || void 0;
@@ -270,25 +438,46 @@ var Logger = class _Logger {
270
438
  span_id: spanId,
271
439
  request_id: requestId || (vercelId ? vercelId.split("::").pop() : void 0),
272
440
  vercel_id: vercelId
273
- });
441
+ }, this.state);
274
442
  }
275
- /** Capture and report an error immediately (not batched). */
443
+ // ---------------------------------------------------------------------------
444
+ // Error capture
445
+ // ---------------------------------------------------------------------------
446
+ /**
447
+ * Capture and report an error immediately (not batched).
448
+ * Automatically attaches breadcrumbs from the buffer and user context.
449
+ */
276
450
  captureError(error, context) {
277
451
  const err = error instanceof Error ? error : new Error(String(error));
452
+ addBreadcrumb(this.state, {
453
+ type: "error",
454
+ message: err.message,
455
+ timestamp: (/* @__PURE__ */ new Date()).toISOString()
456
+ });
457
+ const enrichedContext = { ...context?.context };
458
+ if (this.state.user) enrichedContext.user = this.state.user;
459
+ if (Object.keys(this.state.tags).length > 0) enrichedContext._tags = { ...this.state.tags };
460
+ if (Object.keys(this.state.contexts).length > 0) enrichedContext._contexts = { ...this.state.contexts };
278
461
  const report = {
279
462
  error_message: err.message,
280
463
  stack_trace: err.stack || "",
281
464
  severity: context?.severity || "medium",
282
- context: context?.context,
465
+ context: Object.keys(enrichedContext).length > 0 ? enrichedContext : void 0,
283
466
  trace_id: this.requestMeta?.trace_id,
284
- user_id: context?.userId,
285
- breadcrumbs: context?.breadcrumbs
467
+ user_id: context?.userId || this.state.user?.id,
468
+ breadcrumbs: context?.breadcrumbs || [...this.state.breadcrumbs]
286
469
  };
287
- this.transport.sendError(report);
470
+ const hookResult = this.applyBeforeSend({ type: "error", data: report });
471
+ if (hookResult === null) return;
472
+ this.transport.sendError(hookResult.data);
288
473
  }
474
+ // ---------------------------------------------------------------------------
475
+ // LLM usage
476
+ // ---------------------------------------------------------------------------
289
477
  /** Track LLM usage. Sends to /ingest/llm and logs for visibility. */
290
478
  llmUsage(report) {
291
- this.transport.sendLLMUsage({
479
+ const metadata = this.mergeStateMetadata(report.metadata);
480
+ const payload = {
292
481
  model: report.model,
293
482
  provider: report.provider,
294
483
  operation: report.operation,
@@ -296,8 +485,11 @@ var Logger = class _Logger {
296
485
  output_tokens: report.outputTokens,
297
486
  cost_usd: report.costUsd || 0,
298
487
  latency_ms: report.latencyMs,
299
- metadata: report.metadata
300
- });
488
+ metadata
489
+ };
490
+ const hookResult = this.applyBeforeSend({ type: "llm", data: report });
491
+ if (hookResult === null) return;
492
+ this.transport.sendLLMUsage(payload);
301
493
  this.log("info", `LLM call: ${report.model} (${report.operation})`, {
302
494
  llm_usage: {
303
495
  model: report.model,
@@ -309,6 +501,9 @@ var Logger = class _Logger {
309
501
  }
310
502
  });
311
503
  }
504
+ // ---------------------------------------------------------------------------
505
+ // Tracing
506
+ // ---------------------------------------------------------------------------
312
507
  /** Start a span with automatic lifecycle (callback-based, recommended). */
313
508
  startSpan(operation, fn) {
314
509
  const inactive = this.startInactiveSpan(operation);
@@ -349,6 +544,11 @@ var Logger = class _Logger {
349
544
  const startTime = (/* @__PURE__ */ new Date()).toISOString();
350
545
  const startMs = Date.now();
351
546
  const childMeta = { ...this.requestMeta, trace_id: traceId, span_id: spanId };
547
+ addBreadcrumb(this.state, {
548
+ type: "function",
549
+ message: operation,
550
+ timestamp: startTime
551
+ });
352
552
  const span = {
353
553
  traceId,
354
554
  spanId,
@@ -364,16 +564,18 @@ var Logger = class _Logger {
364
564
  start_time: startTime,
365
565
  duration_ms: durationMs,
366
566
  status: options?.status || "ok",
367
- metadata: options?.metadata
567
+ metadata: this.mergeStateMetadata(options?.metadata)
368
568
  };
369
- this.transport.sendTrace(spanData);
569
+ const hookResult = this.applyBeforeSend({ type: "trace", data: spanData });
570
+ if (hookResult === null) return;
571
+ this.transport.sendTrace(hookResult.data);
370
572
  },
371
573
  startSpan: (childOp, fn) => {
372
- const childLogger = new _Logger(this.config, this.contextName, childMeta);
574
+ const childLogger = new _Logger(this.config, this.contextName, childMeta, this.state);
373
575
  return childLogger.startSpan(childOp, fn);
374
576
  },
375
577
  startInactiveSpan: (childOp) => {
376
- const childLogger = new _Logger(this.config, this.contextName, childMeta);
578
+ const childLogger = new _Logger(this.config, this.contextName, childMeta, this.state);
377
579
  return childLogger.startInactiveSpan(childOp);
378
580
  },
379
581
  getHeaders: () => ({
@@ -389,13 +591,28 @@ var Logger = class _Logger {
389
591
  return this.startSpan(operation, () => fn(...args));
390
592
  };
391
593
  }
594
+ // ---------------------------------------------------------------------------
595
+ // Lifecycle
596
+ // ---------------------------------------------------------------------------
392
597
  /** Immediately flush all batched log entries. */
393
598
  flush() {
394
599
  this.batcher.flush();
395
600
  }
396
- /** Stop the batch timer and flush remaining logs. */
397
- destroy() {
398
- this.batcher.destroy();
601
+ /**
602
+ * Stop the batch timer, flush remaining logs, and wait for in-flight requests.
603
+ *
604
+ * @param timeoutMs - Max time to wait for in-flight requests (default: 2000ms)
605
+ * @returns Promise that resolves when all data is sent or timeout is reached
606
+ *
607
+ * @example
608
+ * ```ts
609
+ * await logger.destroy()
610
+ * process.exit(0) // safe — data is confirmed sent
611
+ * ```
612
+ */
613
+ async destroy(timeoutMs) {
614
+ await this.batcher.destroy();
615
+ await this.transport.drain(timeoutMs);
399
616
  }
400
617
  };
401
618
  // Annotate the CommonJS export names for ESM import in node:
@@ -1 +1 @@
1
- export { Logger, LoggerConfig, MiddlewareOptions, _ as _originalConsole } from './index.cjs';
1
+ export { d as Logger, e as LoggerConfig, h as LoggerState, M as MiddlewareOptions, _ as _originalConsole } from './internal-DdKQRgCs.cjs';
@@ -1 +1 @@
1
- export { Logger, LoggerConfig, MiddlewareOptions, _ as _originalConsole } from './index.js';
1
+ export { d as Logger, e as LoggerConfig, h as LoggerState, M as MiddlewareOptions, _ as _originalConsole } from './internal-DdKQRgCs.js';
package/dist/internal.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Logger,
3
3
  _originalConsole
4
- } from "./chunk-LSMMIS4A.js";
4
+ } from "./chunk-XVDM75HZ.js";
5
5
  export {
6
6
  Logger,
7
7
  _originalConsole
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@deeptracer/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.1",
4
4
  "description": "Core SDK for DeepTracer — Logger class, types, transport, batcher, tracing",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",