@deeptracer/core 0.2.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,611 @@
1
+ # @deeptracer/core
2
+
3
+ Zero-dependency shared core for the [DeepTracer JavaScript SDK](https://github.com/getdeeptracer/deeptracer-js). Provides the `Logger` class with structured logging, error tracking, distributed tracing, and LLM usage monitoring.
4
+
5
+ This package is the foundation that all other `@deeptracer/*` packages build on. **Most users should install `@deeptracer/node` instead**, which re-exports everything from core and adds Node.js/Bun-specific features. Use `@deeptracer/core` directly only if you are building a custom integration or working in a runtime that is not Node.js, Bun, or a browser.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Quick Start](#quick-start)
11
+ - [Configuration](#configuration)
12
+ - [API Reference](#api-reference)
13
+ - [createLogger(config)](#createloggerconfig)
14
+ - [Logging Methods](#logging-methods)
15
+ - [Error Tracking](#error-tracking)
16
+ - [Distributed Tracing](#distributed-tracing)
17
+ - [LLM Usage Tracking](#llm-usage-tracking)
18
+ - [Request Context](#request-context)
19
+ - [Context Scoping](#context-scoping)
20
+ - [Lifecycle](#lifecycle)
21
+ - [Type Reference](#type-reference)
22
+ - [LoggerConfig](#loggerconfig)
23
+ - [LogLevel](#loglevel)
24
+ - [LogEntry](#logentry)
25
+ - [ErrorReport](#errorreport)
26
+ - [LLMUsageReport](#llmusagereport)
27
+ - [Span](#span)
28
+ - [InactiveSpan](#inactivespan)
29
+ - [SpanData](#spandata)
30
+ - [MiddlewareOptions](#middlewareoptions)
31
+ - [Batching Behavior](#batching-behavior)
32
+ - [Transport](#transport)
33
+ - [Monorepo](#monorepo)
34
+ - [License](#license)
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ npm install @deeptracer/core
40
+ ```
41
+
42
+ The package ships as both ESM and CJS with full TypeScript declarations. Zero runtime dependencies.
43
+
44
+ ## Quick Start
45
+
46
+ ```ts
47
+ import { createLogger } from "@deeptracer/core"
48
+
49
+ const logger = createLogger({
50
+ product: "my-app",
51
+ service: "api",
52
+ environment: "production",
53
+ endpoint: "https://your-deeptracer.example.com",
54
+ apiKey: "dt_live_xxx",
55
+ })
56
+
57
+ // Structured logging (batched -- sent in groups of 50 or every 5 seconds)
58
+ logger.info("Server started", { port: 3000 })
59
+ logger.warn("Slow query", { duration_ms: 1200, query: "SELECT ..." })
60
+ logger.error("Request failed", { path: "/api/users" }, new Error("timeout"))
61
+
62
+ // Error tracking (sent immediately)
63
+ try {
64
+ await riskyOperation()
65
+ } catch (err) {
66
+ logger.captureError(err, { severity: "high" })
67
+ }
68
+
69
+ // Distributed tracing
70
+ const result = await logger.startSpan("fetch-user", async (span) => {
71
+ const res = await fetch("https://api.example.com/user/1", {
72
+ headers: span.getHeaders(), // propagate trace context
73
+ })
74
+ return res.json()
75
+ })
76
+
77
+ // Flush before shutdown
78
+ logger.destroy()
79
+ ```
80
+
81
+ ## Configuration
82
+
83
+ Pass a `LoggerConfig` object to `createLogger()`:
84
+
85
+ ```ts
86
+ const logger = createLogger({
87
+ // Required
88
+ product: "spotbeam", // Product name for grouping in the dashboard
89
+ service: "api", // Service name within the product
90
+ environment: "production", // "production" or "staging"
91
+ endpoint: "https://dt.co", // DeepTracer ingestion endpoint URL
92
+ apiKey: "dt_live_xxx", // API key for authentication
93
+
94
+ // Optional
95
+ batchSize: 50, // Logs to buffer before sending (default: 50)
96
+ flushIntervalMs: 5000, // Max ms between flushes (default: 5000)
97
+ debug: false, // Mirror logs to console (default: false)
98
+ })
99
+ ```
100
+
101
+ | Field | Type | Required | Default | Description |
102
+ |-------|------|----------|---------|-------------|
103
+ | `product` | `string` | Yes | -- | Product name (e.g., `"spotbeam"`, `"macro"`) |
104
+ | `service` | `string` | Yes | -- | Service name (e.g., `"api"`, `"worker"`, `"web"`) |
105
+ | `environment` | `"production" \| "staging"` | Yes | -- | Deployment environment |
106
+ | `endpoint` | `string` | Yes | -- | DeepTracer ingestion endpoint URL |
107
+ | `apiKey` | `string` | Yes | -- | DeepTracer API key |
108
+ | `batchSize` | `number` | No | `50` | Number of log entries to buffer before flushing |
109
+ | `flushIntervalMs` | `number` | No | `5000` | Milliseconds between automatic flushes |
110
+ | `debug` | `boolean` | No | `false` | When `true`, all log calls also print to the console |
111
+
112
+ ## API Reference
113
+
114
+ ### createLogger(config)
115
+
116
+ Create a new `Logger` instance. This is the main entry point.
117
+
118
+ ```ts
119
+ import { createLogger } from "@deeptracer/core"
120
+
121
+ const logger = createLogger({
122
+ product: "my-app",
123
+ service: "api",
124
+ environment: "production",
125
+ endpoint: "https://your-deeptracer.example.com",
126
+ apiKey: "dt_live_xxx",
127
+ })
128
+ ```
129
+
130
+ **Parameters:**
131
+ - `config: LoggerConfig` -- See [Configuration](#configuration).
132
+
133
+ **Returns:** `Logger`
134
+
135
+ ---
136
+
137
+ ### Logging Methods
138
+
139
+ All logging methods accept the same signature:
140
+
141
+ ```ts
142
+ logger.debug(message: string, metadata?: Record<string, unknown>, error?: unknown): void
143
+ logger.info(message: string, metadata?: Record<string, unknown>, error?: unknown): void
144
+ logger.warn(message: string, metadata?: Record<string, unknown>, error?: unknown): void
145
+ logger.error(message: string, metadata?: Record<string, unknown>, error?: unknown): void
146
+ ```
147
+
148
+ Logs are **batched** -- they accumulate in an internal buffer and are sent to the DeepTracer backend in bulk (every 50 entries or every 5 seconds by default). This minimizes network overhead.
149
+
150
+ **Flexible argument handling:** The second argument can be either a metadata object or an Error. If it is an Error, the error details (message, name, stack) are automatically extracted into the metadata.
151
+
152
+ ```ts
153
+ // Just a message
154
+ logger.info("User signed in")
155
+
156
+ // Message with structured metadata
157
+ logger.info("User signed in", { userId: "u_123", method: "oauth" })
158
+
159
+ // Message with an error (error is auto-extracted into metadata)
160
+ logger.error("Payment failed", new Error("Card declined"))
161
+
162
+ // Message with both metadata and an error
163
+ logger.error("Payment failed", { orderId: "ord_456" }, new Error("Card declined"))
164
+ ```
165
+
166
+ When `debug: true` is set in the config, all log calls also print to the local console using the appropriate console method (`console.debug`, `console.log`, `console.warn`, `console.error`).
167
+
168
+ ---
169
+
170
+ ### Error Tracking
171
+
172
+ #### `captureError(error, options?)`
173
+
174
+ Capture and report an error **immediately** (not batched). Errors are sent to the `/ingest/errors` endpoint as soon as they occur.
175
+
176
+ ```ts
177
+ logger.captureError(error: Error | unknown, options?: {
178
+ severity?: "low" | "medium" | "high" | "critical", // default: "medium"
179
+ userId?: string,
180
+ context?: Record<string, unknown>,
181
+ breadcrumbs?: Array<{ type: string, message: string, timestamp: string }>,
182
+ }): void
183
+ ```
184
+
185
+ **Examples:**
186
+
187
+ ```ts
188
+ // Basic error capture
189
+ try {
190
+ await processPayment(order)
191
+ } catch (err) {
192
+ logger.captureError(err)
193
+ }
194
+
195
+ // With severity and context
196
+ logger.captureError(err, {
197
+ severity: "critical",
198
+ userId: "u_123",
199
+ context: { orderId: "ord_456", amount: 99.99 },
200
+ })
201
+
202
+ // With breadcrumbs for debugging
203
+ logger.captureError(err, {
204
+ severity: "high",
205
+ breadcrumbs: [
206
+ { type: "navigation", message: "Visited /checkout", timestamp: new Date().toISOString() },
207
+ { type: "action", message: "Clicked 'Pay Now'", timestamp: new Date().toISOString() },
208
+ ],
209
+ })
210
+
211
+ // Non-Error values are automatically wrapped
212
+ logger.captureError("something went wrong") // converted to Error internally
213
+ ```
214
+
215
+ If the logger was created with `forRequest()`, the `trace_id` from the request context is automatically attached to the error report.
216
+
217
+ ---
218
+
219
+ ### Distributed Tracing
220
+
221
+ Three ways to create spans, from simplest to most flexible:
222
+
223
+ #### `startSpan(operation, fn)` -- Callback-based (recommended)
224
+
225
+ Creates a span, runs your function inside it, and automatically ends the span when the function returns or throws. Works with both sync and async functions.
226
+
227
+ ```ts
228
+ const result = await logger.startSpan("fetch-user", async (span) => {
229
+ // span is automatically ended when this function returns
230
+ const res = await fetch("https://api.example.com/user/1", {
231
+ headers: span.getHeaders(), // { "x-trace-id": "...", "x-span-id": "..." }
232
+ })
233
+ return res.json()
234
+ })
235
+ ```
236
+
237
+ If the callback throws, the span is ended with `status: "error"` and the error re-throws. If it succeeds, the span ends with `status: "ok"`.
238
+
239
+ ```ts
240
+ // Sync usage
241
+ const value = logger.startSpan("compute", (span) => {
242
+ return heavyComputation()
243
+ })
244
+
245
+ // Async usage
246
+ const data = await logger.startSpan("db-query", async (span) => {
247
+ return db.query("SELECT * FROM users")
248
+ })
249
+ ```
250
+
251
+ #### `startInactiveSpan(operation)` -- Manual lifecycle
252
+
253
+ Creates a span that you must manually end by calling `span.end()`. Useful when the span lifetime does not fit a single function scope.
254
+
255
+ ```ts
256
+ const span = logger.startInactiveSpan("process-job")
257
+
258
+ try {
259
+ await step1()
260
+ await step2()
261
+ span.end({ status: "ok", metadata: { steps: 2 } })
262
+ } catch (err) {
263
+ span.end({ status: "error", metadata: { error: err.message } })
264
+ throw err
265
+ }
266
+ ```
267
+
268
+ **`InactiveSpan` methods:**
269
+
270
+ | Method | Description |
271
+ |--------|-------------|
272
+ | `end(options?)` | End the span. Options: `{ status?: "ok" \| "error", metadata?: Record<string, unknown> }` |
273
+ | `startSpan(op, fn)` | Create a child span with callback-based lifecycle |
274
+ | `startInactiveSpan(op)` | Create a child span with manual lifecycle |
275
+ | `getHeaders()` | Returns `{ "x-trace-id": "...", "x-span-id": "..." }` for propagation |
276
+
277
+ **Nested spans:**
278
+
279
+ ```ts
280
+ const parent = logger.startInactiveSpan("handle-request")
281
+
282
+ // Child spans automatically inherit the parent's trace_id
283
+ await parent.startSpan("validate-input", async (span) => {
284
+ // ...
285
+ })
286
+
287
+ const dbSpan = parent.startInactiveSpan("db-query")
288
+ await db.query("SELECT ...")
289
+ dbSpan.end()
290
+
291
+ parent.end()
292
+ ```
293
+
294
+ #### `wrap(operation, fn)` -- Function decorator
295
+
296
+ Wraps an existing function so that every invocation is automatically traced. Returns a new function with the same signature.
297
+
298
+ ```ts
299
+ const fetchUser = logger.wrap("fetch-user", async (id: string) => {
300
+ const res = await fetch(`https://api.example.com/users/${id}`)
301
+ return res.json()
302
+ })
303
+
304
+ // Every call is now traced
305
+ const user = await fetchUser("u_123")
306
+ ```
307
+
308
+ #### Span Properties
309
+
310
+ Every span (both `Span` and `InactiveSpan`) has these read-only properties:
311
+
312
+ | Property | Type | Description |
313
+ |----------|------|-------------|
314
+ | `traceId` | `string` | 16-char hex ID linking all spans in a trace |
315
+ | `spanId` | `string` | 16-char hex ID unique to this span |
316
+ | `parentSpanId` | `string` | Parent span ID (empty string for root spans) |
317
+ | `operation` | `string` | Operation name passed to `startSpan()` |
318
+
319
+ #### Trace Context Propagation
320
+
321
+ Use `span.getHeaders()` to propagate trace context to downstream services:
322
+
323
+ ```ts
324
+ await logger.startSpan("call-downstream", async (span) => {
325
+ const res = await fetch("https://other-service.com/api", {
326
+ headers: {
327
+ ...span.getHeaders(), // { "x-trace-id": "abc123", "x-span-id": "def456" }
328
+ "Content-Type": "application/json",
329
+ },
330
+ body: JSON.stringify(data),
331
+ })
332
+ })
333
+ ```
334
+
335
+ The downstream service can then pick up the trace context via `logger.forRequest(request)`.
336
+
337
+ ---
338
+
339
+ ### LLM Usage Tracking
340
+
341
+ #### `llmUsage(report)`
342
+
343
+ Manually report LLM usage data. Sends to the `/ingest/llm` endpoint and also emits an info-level log for visibility.
344
+
345
+ ```ts
346
+ logger.llmUsage(report: LLMUsageReport): void
347
+ ```
348
+
349
+ ```ts
350
+ logger.llmUsage({
351
+ model: "gpt-4o",
352
+ provider: "openai",
353
+ operation: "chat.completions.create",
354
+ inputTokens: 150,
355
+ outputTokens: 320,
356
+ latencyMs: 1200,
357
+ costUsd: 0.0045, // optional
358
+ metadata: { userId: "u_1" }, // optional
359
+ })
360
+ ```
361
+
362
+ > **Tip:** For automatic LLM tracking, use [`@deeptracer/ai`](https://github.com/getdeeptracer/deeptracer-js/tree/main/packages/ai) which wraps Vercel AI SDK, OpenAI, and Anthropic clients.
363
+
364
+ ---
365
+
366
+ ### Request Context
367
+
368
+ #### `forRequest(request)`
369
+
370
+ Create a request-scoped logger that extracts distributed trace context from incoming HTTP headers. The returned logger attaches `trace_id`, `span_id`, `request_id`, and `vercel_id` to all subsequent logs, errors, and spans.
371
+
372
+ ```ts
373
+ const reqLogger = logger.forRequest(request: Request): Logger
374
+ ```
375
+
376
+ Headers read:
377
+ - `x-trace-id` -- distributed trace ID
378
+ - `x-span-id` -- parent span ID
379
+ - `x-request-id` -- request ID
380
+ - `x-vercel-id` -- Vercel deployment ID (also used to derive request ID)
381
+
382
+ ```ts
383
+ // In a Hono route handler
384
+ app.get("/api/users", async (c) => {
385
+ const reqLogger = logger.forRequest(c.req.raw)
386
+ reqLogger.info("Fetching users") // automatically includes trace_id, request_id, etc.
387
+
388
+ const users = await reqLogger.startSpan("db-query", async () => {
389
+ return db.query("SELECT * FROM users")
390
+ })
391
+
392
+ return c.json(users)
393
+ })
394
+ ```
395
+
396
+ ---
397
+
398
+ ### Context Scoping
399
+
400
+ #### `withContext(name)`
401
+
402
+ Create a new logger that includes a context name in every log entry. Useful for distinguishing logs from different modules or subsystems.
403
+
404
+ ```ts
405
+ const dbLogger = logger.withContext("database")
406
+ dbLogger.info("Connection pool initialized") // context: "database"
407
+ dbLogger.warn("Slow query detected") // context: "database"
408
+
409
+ const authLogger = logger.withContext("auth")
410
+ authLogger.info("Token refreshed") // context: "auth"
411
+ ```
412
+
413
+ ---
414
+
415
+ ### Lifecycle
416
+
417
+ #### `flush()`
418
+
419
+ Immediately send all buffered log entries. Call this before your process exits or when you want to ensure logs are delivered.
420
+
421
+ ```ts
422
+ logger.flush()
423
+ ```
424
+
425
+ #### `destroy()`
426
+
427
+ Stop the internal batch timer and flush any remaining log entries. Call this during graceful shutdown.
428
+
429
+ ```ts
430
+ process.on("SIGTERM", () => {
431
+ logger.destroy()
432
+ process.exit(0)
433
+ })
434
+ ```
435
+
436
+ ## Type Reference
437
+
438
+ ### LoggerConfig
439
+
440
+ ```ts
441
+ interface LoggerConfig {
442
+ product: string
443
+ service: string
444
+ environment: "production" | "staging"
445
+ endpoint: string
446
+ apiKey: string
447
+ batchSize?: number // default: 50
448
+ flushIntervalMs?: number // default: 5000
449
+ debug?: boolean // default: false
450
+ }
451
+ ```
452
+
453
+ ### LogLevel
454
+
455
+ ```ts
456
+ type LogLevel = "debug" | "info" | "warn" | "error"
457
+ ```
458
+
459
+ ### LogEntry
460
+
461
+ Internal type representing a single log entry sent to the backend. You do not construct these manually.
462
+
463
+ ```ts
464
+ interface LogEntry {
465
+ timestamp: string // ISO 8601
466
+ level: LogLevel
467
+ message: string
468
+ metadata?: Record<string, unknown>
469
+ trace_id?: string
470
+ span_id?: string
471
+ request_id?: string
472
+ vercel_id?: string
473
+ context?: string
474
+ }
475
+ ```
476
+
477
+ ### ErrorReport
478
+
479
+ ```ts
480
+ interface ErrorReport {
481
+ error_message: string
482
+ stack_trace: string
483
+ severity: "low" | "medium" | "high" | "critical"
484
+ context?: Record<string, unknown>
485
+ trace_id?: string
486
+ user_id?: string
487
+ breadcrumbs?: Array<{
488
+ type: string
489
+ message: string
490
+ timestamp: string
491
+ }>
492
+ }
493
+ ```
494
+
495
+ ### LLMUsageReport
496
+
497
+ ```ts
498
+ interface LLMUsageReport {
499
+ model: string
500
+ provider: string
501
+ operation: string
502
+ inputTokens: number
503
+ outputTokens: number
504
+ latencyMs: number
505
+ costUsd?: number
506
+ metadata?: Record<string, unknown>
507
+ }
508
+ ```
509
+
510
+ ### Span
511
+
512
+ Returned by `startSpan()`. Lifecycle is managed automatically by the callback.
513
+
514
+ ```ts
515
+ interface Span {
516
+ traceId: string
517
+ spanId: string
518
+ parentSpanId: string
519
+ operation: string
520
+ getHeaders(): Record<string, string>
521
+ }
522
+ ```
523
+
524
+ ### InactiveSpan
525
+
526
+ Returned by `startInactiveSpan()`. You must call `.end()` manually.
527
+
528
+ ```ts
529
+ interface InactiveSpan extends Span {
530
+ end(options?: { status?: "ok" | "error"; metadata?: Record<string, unknown> }): void
531
+ startSpan<T>(operation: string, fn: (span: Span) => T): T
532
+ startInactiveSpan(operation: string): InactiveSpan
533
+ }
534
+ ```
535
+
536
+ ### SpanData
537
+
538
+ Internal type representing the raw span payload sent to the backend.
539
+
540
+ ```ts
541
+ interface SpanData {
542
+ trace_id: string
543
+ span_id: string
544
+ parent_span_id: string
545
+ operation: string
546
+ start_time: string // ISO 8601
547
+ duration_ms: number
548
+ status: "ok" | "error"
549
+ metadata?: Record<string, unknown>
550
+ }
551
+ ```
552
+
553
+ ### MiddlewareOptions
554
+
555
+ Used by `honoMiddleware()` and `expressMiddleware()` in `@deeptracer/node`.
556
+
557
+ ```ts
558
+ interface MiddlewareOptions {
559
+ operationName?: (method: string, path: string) => string
560
+ ignorePaths?: string[]
561
+ }
562
+ ```
563
+
564
+ ## Batching Behavior
565
+
566
+ Log entries are buffered and sent in batches to reduce network overhead:
567
+
568
+ 1. Entries accumulate in an internal buffer.
569
+ 2. The buffer is flushed when **either** condition is met:
570
+ - The buffer reaches `batchSize` (default: 50 entries).
571
+ - The flush interval timer fires (default: every 5000ms).
572
+ 3. On flush, all buffered entries are sent as a single POST to `/ingest/logs`.
573
+ 4. Calling `flush()` triggers an immediate flush regardless of buffer size.
574
+ 5. Calling `destroy()` clears the interval timer and performs a final flush.
575
+
576
+ Error reports (`captureError`) and span data (`startSpan`, `startInactiveSpan`) are **not batched** -- they are sent immediately.
577
+
578
+ ## Transport
579
+
580
+ The transport layer sends data to four DeepTracer ingestion endpoints:
581
+
582
+ | Endpoint | Method | Data |
583
+ |----------|--------|------|
584
+ | `POST /ingest/logs` | Batched | Log entries |
585
+ | `POST /ingest/errors` | Immediate | Error reports |
586
+ | `POST /ingest/traces` | Immediate | Span data |
587
+ | `POST /ingest/llm` | Immediate | LLM usage reports |
588
+
589
+ All requests include:
590
+ - `Authorization: Bearer <apiKey>` header
591
+ - `Content-Type: application/json` header
592
+ - `product`, `service`, and `environment` fields in the JSON body
593
+
594
+ 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
+
596
+ ## Monorepo
597
+
598
+ This package is part of the [DeepTracer JavaScript SDK](https://github.com/getdeeptracer/deeptracer-js) monorepo:
599
+
600
+ | Package | Description |
601
+ |---------|-------------|
602
+ | **`@deeptracer/core`** | Zero-dependency shared core (this package) |
603
+ | [`@deeptracer/node`](https://github.com/getdeeptracer/deeptracer-js/tree/main/packages/node) | Node.js/Bun SDK -- global errors, console capture, Hono & Express middleware |
604
+ | [`@deeptracer/ai`](https://github.com/getdeeptracer/deeptracer-js/tree/main/packages/ai) | AI SDK wrappers -- Vercel AI, OpenAI, Anthropic |
605
+ | [`@deeptracer/browser`](https://github.com/getdeeptracer/deeptracer-js/tree/main/packages/browser) | Browser SDK (preview) |
606
+ | [`@deeptracer/react`](https://github.com/getdeeptracer/deeptracer-js/tree/main/packages/react) | React integration (coming soon) |
607
+ | [`@deeptracer/nextjs`](https://github.com/getdeeptracer/deeptracer-js/tree/main/packages/nextjs) | Next.js integration (coming soon) |
608
+
609
+ ## License
610
+
611
+ MIT