@deeptracer/core 0.3.1 → 0.4.2

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 CHANGED
@@ -47,11 +47,10 @@ The package ships as both ESM and CJS with full TypeScript declarations. Zero ru
47
47
  import { createLogger } from "@deeptracer/core"
48
48
 
49
49
  const logger = createLogger({
50
- product: "my-app",
51
50
  service: "api",
52
51
  environment: "production",
53
52
  endpoint: "https://your-deeptracer.example.com",
54
- apiKey: "dt_live_xxx",
53
+ secretKey: "dt_secret_xxx",
55
54
  })
56
55
 
57
56
  // Structured logging (batched -- sent in groups of 50 or every 5 seconds)
@@ -85,11 +84,10 @@ Pass a `LoggerConfig` object to `createLogger()`:
85
84
  ```ts
86
85
  const logger = createLogger({
87
86
  // Required
88
- product: "spotbeam", // Product name for grouping in the dashboard
89
87
  service: "api", // Service name within the product
90
88
  environment: "production", // "production" or "staging"
91
89
  endpoint: "https://dt.co", // DeepTracer ingestion endpoint URL
92
- apiKey: "dt_live_xxx", // API key for authentication
90
+ secretKey: "dt_secret_xxx", // Server-side API key for authentication
93
91
 
94
92
  // Optional
95
93
  batchSize: 50, // Logs to buffer before sending (default: 50)
@@ -100,11 +98,11 @@ const logger = createLogger({
100
98
 
101
99
  | Field | Type | Required | Default | Description |
102
100
  |-------|------|----------|---------|-------------|
103
- | `product` | `string` | Yes | -- | Product name (e.g., `"spotbeam"`, `"macro"`) |
104
101
  | `service` | `string` | Yes | -- | Service name (e.g., `"api"`, `"worker"`, `"web"`) |
105
102
  | `environment` | `"production" \| "staging"` | Yes | -- | Deployment environment |
106
103
  | `endpoint` | `string` | Yes | -- | DeepTracer ingestion endpoint URL |
107
- | `apiKey` | `string` | Yes | -- | DeepTracer API key |
104
+ | `secretKey` | `string` | Yes | -- | Server-side API key (prefix: `dt_secret_`) |
105
+ | `publicKey` | `string` | No | -- | Client-side API key (prefix: `dt_public_`) |
108
106
  | `batchSize` | `number` | No | `50` | Number of log entries to buffer before flushing |
109
107
  | `flushIntervalMs` | `number` | No | `5000` | Milliseconds between automatic flushes |
110
108
  | `debug` | `boolean` | No | `false` | When `true`, all log calls also print to the console |
@@ -119,11 +117,10 @@ Create a new `Logger` instance. This is the main entry point.
119
117
  import { createLogger } from "@deeptracer/core"
120
118
 
121
119
  const logger = createLogger({
122
- product: "my-app",
123
120
  service: "api",
124
121
  environment: "production",
125
122
  endpoint: "https://your-deeptracer.example.com",
126
- apiKey: "dt_live_xxx",
123
+ secretKey: "dt_secret_xxx",
127
124
  })
128
125
  ```
129
126
 
@@ -439,11 +436,11 @@ process.on("SIGTERM", () => {
439
436
 
440
437
  ```ts
441
438
  interface LoggerConfig {
442
- product: string
443
439
  service: string
444
440
  environment: "production" | "staging"
445
441
  endpoint: string
446
- apiKey: string
442
+ secretKey: string
443
+ publicKey?: string
447
444
  batchSize?: number // default: 50
448
445
  flushIntervalMs?: number // default: 5000
449
446
  debug?: boolean // default: false
@@ -587,9 +584,9 @@ The transport layer sends data to four DeepTracer ingestion endpoints:
587
584
  | `POST /ingest/llm` | Immediate | LLM usage reports |
588
585
 
589
586
  All requests include:
590
- - `Authorization: Bearer <apiKey>` header
587
+ - `Authorization: Bearer <secretKey>` header
591
588
  - `Content-Type: application/json` header
592
- - `product`, `service`, and `environment` fields in the JSON body
589
+ - `service` and `environment` fields in the JSON body
593
590
 
594
591
  If a request fails, a warning is logged to the console. The SDK does not retry failed requests -- it is designed to be non-blocking and never crash your application.
595
592
 
@@ -1,53 +1,49 @@
1
1
  // src/version.ts
2
- var SDK_VERSION = "0.3.1";
2
+ var SDK_VERSION = "0.4.2";
3
3
  var SDK_NAME = "core";
4
4
 
5
- // src/batcher.ts
6
- var Batcher = class {
7
- constructor(config, onFlush) {
8
- this.onFlush = onFlush;
9
- this.batchSize = config.batchSize ?? 50;
10
- this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
11
- this.startTimer();
12
- }
13
- buffer = [];
14
- timer = null;
15
- batchSize;
16
- flushIntervalMs;
17
- add(entry) {
18
- this.buffer.push(entry);
19
- if (this.buffer.length >= this.batchSize) {
20
- this.flush();
21
- }
22
- }
23
- flush() {
24
- if (this.buffer.length === 0) return;
25
- const entries = [...this.buffer];
26
- this.buffer = [];
27
- this.onFlush(entries);
28
- }
29
- startTimer() {
30
- this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
31
- }
32
- async destroy() {
33
- if (this.timer) clearInterval(this.timer);
34
- this.timer = null;
35
- this.flush();
36
- }
37
- };
38
-
39
5
  // src/transport.ts
40
6
  var Transport = class {
41
7
  constructor(config) {
42
8
  this.config = config;
9
+ const hasKey = !!(config.secretKey || config.publicKey);
10
+ const hasEndpoint = !!config.endpoint;
11
+ this.disabled = !hasKey || !hasEndpoint;
12
+ if (config.secretKey?.startsWith("dt_secret_") && typeof globalThis.window !== "undefined") {
13
+ console.error(
14
+ "[@deeptracer/core] WARNING: `secretKey` (dt_secret_...) detected in a browser bundle. This exposes your server key to end users. Use `publicKey` (dt_public_...) for client-side code."
15
+ );
16
+ }
43
17
  }
44
18
  inFlightRequests = /* @__PURE__ */ new Set();
19
+ /**
20
+ * When true, all send methods become silent no-ops.
21
+ * Set automatically when no auth key or no endpoint is configured.
22
+ * This prevents pointless network requests and console noise during
23
+ * local development without API keys.
24
+ */
25
+ disabled;
26
+ /**
27
+ * Tracks which send types (logs, error, trace, LLM usage) have already
28
+ * logged a failure warning. After the first failure for a given type,
29
+ * subsequent failures are silently dropped to prevent console spam
30
+ * (e.g., when the ingestion endpoint is unreachable during development).
31
+ */
32
+ warnedLabels = /* @__PURE__ */ new Set();
33
+ /** Resolve the auth key: prefer secretKey (server), fall back to publicKey (client). */
34
+ get authKey() {
35
+ return this.config.secretKey ?? this.config.publicKey ?? "";
36
+ }
45
37
  /**
46
38
  * Send a request with automatic retry and exponential backoff.
47
39
  * Retries up to `maxRetries` times on network errors and 5xx responses.
48
40
  * Does NOT retry on 4xx (client errors — bad payload, auth failure, etc.).
41
+ *
42
+ * After the first total failure for a given label, subsequent failures
43
+ * are silently dropped (no more console warnings).
49
44
  */
50
45
  async sendWithRetry(url, body, label, maxRetries = 3) {
46
+ if (this.disabled) return;
51
47
  const baseDelays = [1e3, 2e3, 4e3];
52
48
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
53
49
  try {
@@ -55,33 +51,45 @@ var Transport = class {
55
51
  method: "POST",
56
52
  headers: {
57
53
  "Content-Type": "application/json",
58
- Authorization: `Bearer ${this.config.apiKey}`,
54
+ Authorization: `Bearer ${this.authKey}`,
59
55
  "x-deeptracer-sdk": `${SDK_NAME}/${SDK_VERSION}`
60
56
  },
61
57
  body: JSON.stringify(body)
62
58
  });
63
- if (res.ok) return;
59
+ if (res.ok) {
60
+ this.warnedLabels.delete(label);
61
+ return;
62
+ }
64
63
  if (res.status >= 400 && res.status < 500) {
65
- console.warn(
66
- `[@deeptracer/core] Failed to send ${label}: ${res.status} ${res.statusText}`
67
- );
64
+ if (!this.warnedLabels.has(label)) {
65
+ this.warnedLabels.add(label);
66
+ console.warn(
67
+ `[@deeptracer/core] Failed to send ${label}: ${res.status} ${res.statusText}`
68
+ );
69
+ }
68
70
  return;
69
71
  }
70
72
  if (attempt < maxRetries) {
71
73
  await this.sleep(this.jitter(baseDelays[attempt]));
72
74
  continue;
73
75
  }
74
- console.warn(
75
- `[@deeptracer/core] Failed to send ${label}: ${res.status} ${res.statusText} (exhausted ${maxRetries} retries)`
76
- );
76
+ if (!this.warnedLabels.has(label)) {
77
+ this.warnedLabels.add(label);
78
+ console.warn(
79
+ `[@deeptracer/core] Failed to send ${label}: ${res.status} ${res.statusText} (exhausted ${maxRetries} retries). Suppressing further warnings.`
80
+ );
81
+ }
77
82
  } catch {
78
83
  if (attempt < maxRetries) {
79
84
  await this.sleep(this.jitter(baseDelays[attempt]));
80
85
  continue;
81
86
  }
82
- console.warn(
83
- `[@deeptracer/core] Failed to send ${label} (exhausted ${maxRetries} retries)`
84
- );
87
+ if (!this.warnedLabels.has(label)) {
88
+ this.warnedLabels.add(label);
89
+ console.warn(
90
+ `[@deeptracer/core] Failed to send ${label} (exhausted ${maxRetries} retries). Suppressing further warnings.`
91
+ );
92
+ }
85
93
  }
86
94
  }
87
95
  }
@@ -102,7 +110,6 @@ var Transport = class {
102
110
  const p = this.sendWithRetry(
103
111
  `${this.config.endpoint}/ingest/logs`,
104
112
  {
105
- product: this.config.product,
106
113
  service: this.config.service,
107
114
  environment: this.config.environment,
108
115
  logs
@@ -117,7 +124,6 @@ var Transport = class {
117
124
  `${this.config.endpoint}/ingest/errors`,
118
125
  {
119
126
  ...error,
120
- product: this.config.product,
121
127
  service: this.config.service,
122
128
  environment: this.config.environment
123
129
  },
@@ -131,7 +137,6 @@ var Transport = class {
131
137
  `${this.config.endpoint}/ingest/traces`,
132
138
  {
133
139
  ...span,
134
- product: this.config.product,
135
140
  service: this.config.service,
136
141
  environment: this.config.environment
137
142
  },
@@ -145,7 +150,6 @@ var Transport = class {
145
150
  `${this.config.endpoint}/ingest/llm`,
146
151
  {
147
152
  ...report,
148
- product: this.config.product,
149
153
  service: this.config.service,
150
154
  environment: this.config.environment
151
155
  },
@@ -169,6 +173,40 @@ var Transport = class {
169
173
  }
170
174
  };
171
175
 
176
+ // src/batcher.ts
177
+ var Batcher = class {
178
+ constructor(config, onFlush) {
179
+ this.onFlush = onFlush;
180
+ this.batchSize = config.batchSize ?? 50;
181
+ this.flushIntervalMs = config.flushIntervalMs ?? 5e3;
182
+ this.startTimer();
183
+ }
184
+ buffer = [];
185
+ timer = null;
186
+ batchSize;
187
+ flushIntervalMs;
188
+ add(entry) {
189
+ this.buffer.push(entry);
190
+ if (this.buffer.length >= this.batchSize) {
191
+ this.flush();
192
+ }
193
+ }
194
+ flush() {
195
+ if (this.buffer.length === 0) return;
196
+ const entries = [...this.buffer];
197
+ this.buffer = [];
198
+ this.onFlush(entries);
199
+ }
200
+ startTimer() {
201
+ this.timer = setInterval(() => this.flush(), this.flushIntervalMs);
202
+ }
203
+ async destroy() {
204
+ if (this.timer) clearInterval(this.timer);
205
+ this.timer = null;
206
+ this.flush();
207
+ }
208
+ };
209
+
172
210
  // src/state.ts
173
211
  function createLoggerState(maxBreadcrumbs) {
174
212
  return {
@@ -179,6 +217,15 @@ function createLoggerState(maxBreadcrumbs) {
179
217
  maxBreadcrumbs
180
218
  };
181
219
  }
220
+ function cloneState(state) {
221
+ return {
222
+ user: state.user ? { ...state.user } : null,
223
+ tags: { ...state.tags },
224
+ contexts: Object.fromEntries(Object.entries(state.contexts).map(([k, v]) => [k, { ...v }])),
225
+ breadcrumbs: [...state.breadcrumbs],
226
+ maxBreadcrumbs: state.maxBreadcrumbs
227
+ };
228
+ }
182
229
  function addBreadcrumb(state, breadcrumb) {
183
230
  state.breadcrumbs.push(breadcrumb);
184
231
  if (state.breadcrumbs.length > state.maxBreadcrumbs) {
@@ -187,11 +234,33 @@ function addBreadcrumb(state, breadcrumb) {
187
234
  }
188
235
 
189
236
  // src/logger.ts
237
+ var LOG_LEVEL_VALUES = {
238
+ debug: 0,
239
+ info: 1,
240
+ warn: 2,
241
+ error: 3
242
+ };
190
243
  function generateId() {
191
244
  const bytes = new Uint8Array(8);
192
245
  crypto.getRandomValues(bytes);
193
246
  return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
194
247
  }
248
+ function generateTraceId() {
249
+ const bytes = new Uint8Array(16);
250
+ crypto.getRandomValues(bytes);
251
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
252
+ }
253
+ function parseTraceparent(header) {
254
+ const parts = header.trim().split("-");
255
+ if (parts.length !== 4) return null;
256
+ const [version, traceId, parentId, flags] = parts;
257
+ if (version !== "00") return null;
258
+ if (traceId.length !== 32 || !/^[0-9a-f]{32}$/.test(traceId)) return null;
259
+ if (parentId.length !== 16 || !/^[0-9a-f]{16}$/.test(parentId)) return null;
260
+ if (flags.length !== 2 || !/^[0-9a-f]{2}$/.test(flags)) return null;
261
+ if (/^0+$/.test(traceId) || /^0+$/.test(parentId)) return null;
262
+ return { traceId, parentId, flags };
263
+ }
195
264
  var _originalConsole = {
196
265
  log: console.log,
197
266
  info: console.info,
@@ -202,6 +271,7 @@ var _originalConsole = {
202
271
  var Logger = class _Logger {
203
272
  batcher;
204
273
  transport;
274
+ effectiveLevel;
205
275
  contextName;
206
276
  config;
207
277
  state;
@@ -211,6 +281,20 @@ var Logger = class _Logger {
211
281
  this.contextName = contextName;
212
282
  this.requestMeta = requestMeta;
213
283
  this.state = state ?? createLoggerState(config.maxBreadcrumbs ?? 20);
284
+ const hasKey = !!(config.secretKey || config.publicKey);
285
+ const hasEndpoint = !!config.endpoint;
286
+ if (!hasKey && !hasEndpoint) {
287
+ _originalConsole.warn(
288
+ "[@deeptracer/core] No API key or endpoint configured. Running in local-only mode (logging methods work, but events are not sent). Set DEEPTRACER_SECRET_KEY and DEEPTRACER_ENDPOINT to enable."
289
+ );
290
+ } else if (!hasKey) {
291
+ _originalConsole.warn(
292
+ "[@deeptracer/core] No `secretKey` or `publicKey` provided. Events will not be sent."
293
+ );
294
+ } else if (!hasEndpoint) {
295
+ _originalConsole.warn("[@deeptracer/core] No `endpoint` provided. Events will not be sent.");
296
+ }
297
+ this.effectiveLevel = LOG_LEVEL_VALUES[config.level ?? (config.environment === "production" ? "info" : "debug")];
214
298
  this.transport = new Transport(config);
215
299
  this.batcher = new Batcher(
216
300
  { batchSize: config.batchSize, flushIntervalMs: config.flushIntervalMs },
@@ -224,7 +308,8 @@ var Logger = class _Logger {
224
308
  // ---------------------------------------------------------------------------
225
309
  /**
226
310
  * Set the current user context. Attached to all subsequent logs, errors, spans, and LLM reports.
227
- * Shared across all child loggers (withContext, forRequest).
311
+ * Only affects this logger instance — child loggers created via `withContext()` or `forRequest()`
312
+ * have their own independent state.
228
313
  *
229
314
  * @example
230
315
  * ```ts
@@ -346,26 +431,13 @@ var Logger = class _Logger {
346
431
  };
347
432
  }
348
433
  metadata = this.mergeStateMetadata(metadata);
349
- const entry = {
350
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
351
- level,
352
- message,
353
- metadata,
354
- context: this.contextName,
355
- trace_id: this.requestMeta?.trace_id,
356
- span_id: this.requestMeta?.span_id,
357
- request_id: this.requestMeta?.request_id,
358
- vercel_id: this.requestMeta?.vercel_id
359
- };
360
- const hookResult = this.applyBeforeSend({ type: "log", data: entry });
361
- if (hookResult === null) return;
362
- const finalEntry = hookResult.data;
434
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
363
435
  if (this.config.debug) {
364
436
  const prefix = this.contextName ? `[${this.contextName}]` : "";
365
437
  const lvl = level.toUpperCase().padEnd(5);
366
438
  const consoleFn = level === "error" ? _originalConsole.error : level === "warn" ? _originalConsole.warn : level === "debug" ? _originalConsole.debug : _originalConsole.log;
367
- if (finalEntry.metadata) {
368
- consoleFn(`${lvl} ${prefix} ${message}`, finalEntry.metadata);
439
+ if (metadata) {
440
+ consoleFn(`${lvl} ${prefix} ${message}`, metadata);
369
441
  } else {
370
442
  consoleFn(`${lvl} ${prefix} ${message}`);
371
443
  }
@@ -373,9 +445,23 @@ var Logger = class _Logger {
373
445
  addBreadcrumb(this.state, {
374
446
  type: "log",
375
447
  message: `[${level}] ${message}`,
376
- timestamp: entry.timestamp
448
+ timestamp
377
449
  });
378
- this.batcher.add(finalEntry);
450
+ if (LOG_LEVEL_VALUES[level] < this.effectiveLevel) return;
451
+ const entry = {
452
+ timestamp,
453
+ level,
454
+ message,
455
+ metadata,
456
+ context: this.contextName,
457
+ trace_id: this.requestMeta?.trace_id,
458
+ span_id: this.requestMeta?.span_id,
459
+ request_id: this.requestMeta?.request_id,
460
+ vercel_id: this.requestMeta?.vercel_id
461
+ };
462
+ const hookResult = this.applyBeforeSend({ type: "log", data: entry });
463
+ if (hookResult === null) return;
464
+ this.batcher.add(hookResult.data);
379
465
  }
380
466
  /** Log a debug message. */
381
467
  debug(message, dataOrError, error) {
@@ -396,22 +482,37 @@ var Logger = class _Logger {
396
482
  // ---------------------------------------------------------------------------
397
483
  // Child loggers
398
484
  // ---------------------------------------------------------------------------
399
- /** Create a context-scoped logger. All logs include the context name. Shares state with parent. */
485
+ /** Create a context-scoped logger. All logs include the context name. Gets an independent copy of state. */
400
486
  withContext(name) {
401
- return new _Logger(this.config, name, this.requestMeta, this.state);
487
+ return new _Logger(this.config, name, this.requestMeta, cloneState(this.state));
402
488
  }
403
- /** Create a request-scoped logger that extracts trace context from headers. Shares state with parent. */
489
+ /** Create a request-scoped logger that extracts trace context from headers. Gets an independent copy of state. */
404
490
  forRequest(request) {
405
- const vercelId = request.headers.get("x-vercel-id") || void 0;
491
+ let traceId;
492
+ let spanId;
493
+ const traceparent = request.headers.get("traceparent");
494
+ if (traceparent) {
495
+ const parsed = parseTraceparent(traceparent);
496
+ if (parsed) {
497
+ traceId = parsed.traceId;
498
+ spanId = parsed.parentId;
499
+ }
500
+ }
501
+ traceId = traceId || request.headers.get("x-trace-id") || void 0;
502
+ spanId = spanId || request.headers.get("x-span-id") || void 0;
406
503
  const requestId = request.headers.get("x-request-id") || void 0;
407
- const traceId = request.headers.get("x-trace-id") || void 0;
408
- const spanId = request.headers.get("x-span-id") || void 0;
409
- return new _Logger(this.config, this.contextName, {
410
- trace_id: traceId,
411
- span_id: spanId,
412
- request_id: requestId || (vercelId ? vercelId.split("::").pop() : void 0),
413
- vercel_id: vercelId
414
- }, this.state);
504
+ const vercelId = request.headers.get("x-vercel-id") || void 0;
505
+ return new _Logger(
506
+ this.config,
507
+ this.contextName,
508
+ {
509
+ trace_id: traceId,
510
+ span_id: spanId,
511
+ request_id: requestId || (vercelId ? vercelId.split("::").pop() : void 0),
512
+ vercel_id: vercelId
513
+ },
514
+ cloneState(this.state)
515
+ );
415
516
  }
416
517
  // ---------------------------------------------------------------------------
417
518
  // Error capture
@@ -430,7 +531,8 @@ var Logger = class _Logger {
430
531
  const enrichedContext = { ...context?.context };
431
532
  if (this.state.user) enrichedContext.user = this.state.user;
432
533
  if (Object.keys(this.state.tags).length > 0) enrichedContext._tags = { ...this.state.tags };
433
- if (Object.keys(this.state.contexts).length > 0) enrichedContext._contexts = { ...this.state.contexts };
534
+ if (Object.keys(this.state.contexts).length > 0)
535
+ enrichedContext._contexts = { ...this.state.contexts };
434
536
  const report = {
435
537
  error_message: err.message,
436
538
  stack_trace: err.stack || "",
@@ -511,7 +613,7 @@ var Logger = class _Logger {
511
613
  }
512
614
  /** Start a span with manual lifecycle. You must call span.end(). */
513
615
  startInactiveSpan(operation) {
514
- const traceId = this.requestMeta?.trace_id || generateId();
616
+ const traceId = this.requestMeta?.trace_id || generateTraceId();
515
617
  const parentSpanId = this.requestMeta?.span_id || "";
516
618
  const spanId = generateId();
517
619
  const startTime = (/* @__PURE__ */ new Date()).toISOString();
@@ -551,10 +653,16 @@ var Logger = class _Logger {
551
653
  const childLogger = new _Logger(this.config, this.contextName, childMeta, this.state);
552
654
  return childLogger.startInactiveSpan(childOp);
553
655
  },
554
- getHeaders: () => ({
555
- "x-trace-id": traceId,
556
- "x-span-id": spanId
557
- })
656
+ getHeaders: () => {
657
+ const headers = {
658
+ "x-trace-id": traceId,
659
+ "x-span-id": spanId
660
+ };
661
+ if (/^[0-9a-f]{32}$/.test(traceId)) {
662
+ headers.traceparent = `00-${traceId}-${spanId}-01`;
663
+ }
664
+ return headers;
665
+ }
558
666
  };
559
667
  return span;
560
668
  }
@@ -595,6 +703,8 @@ function createLogger(config) {
595
703
  export {
596
704
  SDK_VERSION,
597
705
  SDK_NAME,
706
+ Transport,
707
+ parseTraceparent,
598
708
  _originalConsole,
599
709
  Logger,
600
710
  createLogger