agent-telemetry 0.2.0 → 0.3.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 +38 -8
- package/package.json +5 -1
- package/src/adapters/express.ts +23 -11
- package/src/adapters/fastify.ts +27 -11
- package/src/adapters/fetch.ts +52 -50
- package/src/adapters/hono.ts +17 -11
- package/src/adapters/prisma.ts +15 -9
- package/src/adapters/supabase.ts +29 -41
- package/src/browser.ts +184 -0
- package/src/fetch-utils.ts +67 -0
- package/src/trace-context.ts +41 -0
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Lightweight JSONL telemetry for easier AI agent consumption. Zero runtime dependencies.
|
|
4
4
|
|
|
5
|
-
Writes structured telemetry events to rotating JSONL files in development. Falls back to `console.log` in runtimes without filesystem access (Cloudflare Workers). Includes framework adapters for Hono, Inngest, Express, Fastify, Prisma, Supabase,
|
|
5
|
+
Writes structured telemetry events to rotating JSONL files in development. Falls back to `console.log` in runtimes without filesystem access (Cloudflare Workers). Includes framework adapters for Hono, Inngest, Express, Fastify, Prisma, Supabase, a generic traced fetch wrapper, and browser trace-context helpers.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
@@ -47,7 +47,7 @@ Inbound HTTP → Database Queries → External API Calls → Background Jo
|
|
|
47
47
|
Fastify storage/functions)
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
-
One `traceId` follows a request from the HTTP boundary through database queries, external API calls, and into background job execution. HTTP adapters use the [W3C `traceparent`](https://www.w3.org/TR/trace-context/) header for propagation, enabling interop with OpenTelemetry and other standards-compliant tools. Query your JSONL logs by `traceId` to see the full chain.
|
|
50
|
+
One `traceId` follows a request from the HTTP boundary through database queries, external API calls, and into background job execution. `spanId`/`parentSpanId` fields preserve parent-child relationships inside that trace. HTTP adapters use the [W3C `traceparent`](https://www.w3.org/TR/trace-context/) header for propagation, enabling interop with OpenTelemetry and other standards-compliant tools. Query your JSONL logs by `traceId` to see the full chain.
|
|
51
51
|
|
|
52
52
|
## Full-Stack Example
|
|
53
53
|
|
|
@@ -152,10 +152,10 @@ app.use('*', trace)
|
|
|
152
152
|
The middleware:
|
|
153
153
|
- Parses the incoming W3C `traceparent` header, or generates a fresh trace ID if absent/invalid
|
|
154
154
|
- Sets `traceparent` on the response for client-side correlation (format: `00-{traceId}-{spanId}-01`)
|
|
155
|
-
- Emits `http.request` events with method, path, status, duration, and
|
|
155
|
+
- Emits `http.request` events with method, path, status, duration, extracted entities, and span linkage (`spanId`, `parentSpanId`)
|
|
156
156
|
- Extracts entity IDs from URL paths — looks for a matching `segment`, then checks if the next segment is a UUID
|
|
157
157
|
|
|
158
|
-
`getTraceContext(c)` returns `{ _trace: { traceId, parentSpanId } }` for spreading into dispatch payloads. Returns `{}` if no trace middleware is active.
|
|
158
|
+
`getTraceContext(c)` returns `{ _trace: { traceId, parentSpanId, traceFlags } }` for spreading into dispatch payloads. Returns `{}` if no trace middleware is active.
|
|
159
159
|
|
|
160
160
|
## Inngest Adapter
|
|
161
161
|
|
|
@@ -188,17 +188,47 @@ const fetch = createTracedFetch({
|
|
|
188
188
|
telemetry,
|
|
189
189
|
baseFetch: globalThis.fetch, // Optional — default: globalThis.fetch
|
|
190
190
|
getTraceContext: () => ctx, // Optional — correlate with parent request
|
|
191
|
+
propagateTo: (url) => url.origin === 'https://api.my-app.com', // Optional header allowlist
|
|
192
|
+
onResponseTraceparent: (tp) => { // Optional response callback
|
|
193
|
+
console.log(tp)
|
|
194
|
+
},
|
|
191
195
|
isEnabled: () => true, // Optional guard
|
|
192
196
|
})
|
|
193
197
|
|
|
194
198
|
const res = await fetch('https://api.stripe.com/v1/charges', { method: 'POST' })
|
|
195
199
|
```
|
|
196
200
|
|
|
197
|
-
- Emits `external.call` events with `service` (hostname)
|
|
201
|
+
- Emits `external.call` events with `service` (hostname), `operation` (`METHOD /pathname`), and span linkage (`spanId`, optional `parentSpanId`)
|
|
198
202
|
- `duration_ms` measures time-to-headers (TTFB) — the Response body is returned untouched for streaming
|
|
199
203
|
- Handles all three fetch input types: `string`, `URL`, `Request`
|
|
204
|
+
- Can inject outbound `traceparent` headers using `propagateTo` (default: same-origin only in browser, disabled elsewhere)
|
|
200
205
|
- Non-2xx responses return normally (not thrown); network errors re-throw after emitting
|
|
201
206
|
|
|
207
|
+
## Browser Trace Context
|
|
208
|
+
|
|
209
|
+
Use the browser helpers to continue the same trace from UI requests into server adapters.
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { createBrowserTraceContext, createBrowserTracedFetch } from 'agent-telemetry/browser'
|
|
213
|
+
|
|
214
|
+
const trace = createBrowserTraceContext({
|
|
215
|
+
// Optional SSR bootstrap: <meta name="traceparent" content="00-...">
|
|
216
|
+
initialTraceparent: document.querySelector('meta[name="traceparent"]')?.getAttribute('content'),
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
const fetch = createBrowserTracedFetch({
|
|
220
|
+
trace,
|
|
221
|
+
// Default is same-origin only. Keep this allowlist strict.
|
|
222
|
+
propagateTo: (url) => url.origin === window.location.origin,
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
await fetch('/api/users')
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
- `createBrowserTraceContext()` bootstraps from `initialTraceparent`, then `<meta name="traceparent">`, then fresh IDs
|
|
229
|
+
- `createBrowserTracedFetch()` injects W3C `traceparent` on allowed requests and can adopt response `traceparent`
|
|
230
|
+
- `trace.withSpan(name, fn)` creates a child span for user actions and restores the previous parent span after completion
|
|
231
|
+
|
|
202
232
|
## Prisma Adapter
|
|
203
233
|
|
|
204
234
|
Traces all Prisma model operations via `$extends()`. No runtime `@prisma/client` import — the extension is structurally compatible.
|
|
@@ -213,7 +243,7 @@ const prisma = new PrismaClient().$extends(createPrismaTrace({
|
|
|
213
243
|
}))
|
|
214
244
|
```
|
|
215
245
|
|
|
216
|
-
- Emits `db.query` events with `provider: "prisma"`, `model` (e.g. `"User"`),
|
|
246
|
+
- Emits `db.query` events with `provider: "prisma"`, `model` (e.g. `"User"`), `operation` (e.g. `"findMany"`), and span linkage (`spanId`, optional `parentSpanId`)
|
|
217
247
|
- Requires Prisma 5.0.0+ (stable `$extends` API)
|
|
218
248
|
- No access to raw SQL at the query extension level — model and operation names only
|
|
219
249
|
|
|
@@ -239,7 +269,7 @@ app.post('/api/users/:id', (req, res) => {
|
|
|
239
269
|
})
|
|
240
270
|
```
|
|
241
271
|
|
|
242
|
-
- Emits `http.request` events with method, path (query string stripped), status, duration, entities
|
|
272
|
+
- Emits `http.request` events with method, path (query string stripped), status, duration, entities, and span linkage
|
|
243
273
|
- Parses/sets W3C `traceparent` header for propagation
|
|
244
274
|
- Uses `req.route.path` for parameterized patterns (e.g. `/users/:id`), falls back to `req.originalUrl`
|
|
245
275
|
- Handles both `res.on("finish")` and `res.on("close")` to capture aborted requests
|
|
@@ -260,7 +290,7 @@ app.register(createFastifyTrace({
|
|
|
260
290
|
}))
|
|
261
291
|
```
|
|
262
292
|
|
|
263
|
-
- Emits `http.request` events using `reply.elapsedTime` for high-resolution duration
|
|
293
|
+
- Emits `http.request` events using `reply.elapsedTime` for high-resolution duration, including span linkage
|
|
264
294
|
- Strips query strings from emitted `path` values
|
|
265
295
|
- Parses/sets W3C `traceparent` header for propagation
|
|
266
296
|
- Uses `request.routeOptions.url` for parameterized route patterns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-telemetry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Lightweight JSONL telemetry for AI agent backends. Zero deps, framework adapters included.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"./supabase": {
|
|
36
36
|
"import": "./src/adapters/supabase.ts",
|
|
37
37
|
"types": "./src/adapters/supabase.ts"
|
|
38
|
+
},
|
|
39
|
+
"./browser": {
|
|
40
|
+
"import": "./src/browser.ts",
|
|
41
|
+
"types": "./src/browser.ts"
|
|
38
42
|
}
|
|
39
43
|
},
|
|
40
44
|
"files": ["src"],
|
package/src/adapters/express.ts
CHANGED
|
@@ -17,8 +17,8 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { extractEntities } from "../entities.ts";
|
|
20
|
-
import {
|
|
21
|
-
import { formatTraceparent
|
|
20
|
+
import { startSpanFromTraceparent } from "../trace-context.ts";
|
|
21
|
+
import { formatTraceparent } from "../traceparent.ts";
|
|
22
22
|
import type { EntityPattern, HttpRequestEvent, Telemetry } from "../types.ts";
|
|
23
23
|
|
|
24
24
|
// ============================================================================
|
|
@@ -52,7 +52,7 @@ export type ExpressMiddleware = (
|
|
|
52
52
|
// Request-Scoped Trace Storage
|
|
53
53
|
// ============================================================================
|
|
54
54
|
|
|
55
|
-
const traceStore = new WeakMap<object, { traceId: string; spanId: string }>();
|
|
55
|
+
const traceStore = new WeakMap<object, { traceId: string; spanId: string; traceFlags: string }>();
|
|
56
56
|
|
|
57
57
|
function stripQueryAndFragment(url: string): string {
|
|
58
58
|
const queryIdx = url.indexOf("?");
|
|
@@ -104,12 +104,14 @@ export function createExpressTrace(options: ExpressTraceOptions): ExpressMiddlew
|
|
|
104
104
|
const incoming = Array.isArray(req.headers.traceparent)
|
|
105
105
|
? req.headers.traceparent[0]
|
|
106
106
|
: req.headers.traceparent;
|
|
107
|
-
const
|
|
108
|
-
const traceId = parsed?.traceId ?? generateTraceId();
|
|
109
|
-
const spanId = generateSpanId();
|
|
107
|
+
const span = startSpanFromTraceparent(incoming);
|
|
110
108
|
|
|
111
|
-
traceStore.set(req, {
|
|
112
|
-
|
|
109
|
+
traceStore.set(req, {
|
|
110
|
+
traceId: span.traceId,
|
|
111
|
+
spanId: span.spanId,
|
|
112
|
+
traceFlags: span.traceFlags,
|
|
113
|
+
});
|
|
114
|
+
res.setHeader("traceparent", formatTraceparent(span.traceId, span.spanId, span.traceFlags));
|
|
113
115
|
|
|
114
116
|
const start = performance.now();
|
|
115
117
|
let emitted = false;
|
|
@@ -124,7 +126,9 @@ export function createExpressTrace(options: ExpressTraceOptions): ExpressMiddlew
|
|
|
124
126
|
|
|
125
127
|
const event: HttpRequestEvent = {
|
|
126
128
|
kind: "http.request",
|
|
127
|
-
traceId,
|
|
129
|
+
traceId: span.traceId,
|
|
130
|
+
spanId: span.spanId,
|
|
131
|
+
parentSpanId: span.parentSpanId,
|
|
128
132
|
method: req.method,
|
|
129
133
|
path,
|
|
130
134
|
status: res.statusCode,
|
|
@@ -169,8 +173,16 @@ export function createExpressTrace(options: ExpressTraceOptions): ExpressMiddlew
|
|
|
169
173
|
*/
|
|
170
174
|
export function getTraceContext(
|
|
171
175
|
req: object,
|
|
172
|
-
):
|
|
176
|
+
):
|
|
177
|
+
| { _trace: { traceId: string; parentSpanId: string; traceFlags?: string } }
|
|
178
|
+
| Record<string, never> {
|
|
173
179
|
const stored = traceStore.get(req);
|
|
174
180
|
if (!stored) return {};
|
|
175
|
-
return {
|
|
181
|
+
return {
|
|
182
|
+
_trace: {
|
|
183
|
+
traceId: stored.traceId,
|
|
184
|
+
parentSpanId: stored.spanId,
|
|
185
|
+
traceFlags: stored.traceFlags,
|
|
186
|
+
},
|
|
187
|
+
};
|
|
176
188
|
}
|
package/src/adapters/fastify.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { extractEntities } from "../entities.ts";
|
|
21
|
-
import {
|
|
22
|
-
import { formatTraceparent
|
|
21
|
+
import { startSpanFromTraceparent } from "../trace-context.ts";
|
|
22
|
+
import { formatTraceparent } from "../traceparent.ts";
|
|
23
23
|
import type { EntityPattern, HttpRequestEvent, Telemetry } from "../types.ts";
|
|
24
24
|
|
|
25
25
|
// ---------------------------------------------------------------------------
|
|
@@ -62,7 +62,10 @@ type FastifyPluginCallback = ((
|
|
|
62
62
|
// Trace storage (keyed on Fastify request wrapper, not request.raw)
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
|
|
65
|
-
const traceStore = new WeakMap<
|
|
65
|
+
const traceStore = new WeakMap<
|
|
66
|
+
object,
|
|
67
|
+
{ traceId: string; spanId: string; parentSpanId?: string; traceFlags: string }
|
|
68
|
+
>();
|
|
66
69
|
|
|
67
70
|
function stripQueryAndFragment(url: string): string {
|
|
68
71
|
const queryIdx = url.indexOf("?");
|
|
@@ -113,12 +116,15 @@ export function createFastifyTrace(options: FastifyTraceOptions): FastifyPluginC
|
|
|
113
116
|
const incoming = Array.isArray(request.headers.traceparent)
|
|
114
117
|
? request.headers.traceparent[0]
|
|
115
118
|
: request.headers.traceparent;
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
119
|
+
const span = startSpanFromTraceparent(incoming);
|
|
120
|
+
|
|
121
|
+
traceStore.set(request, {
|
|
122
|
+
traceId: span.traceId,
|
|
123
|
+
spanId: span.spanId,
|
|
124
|
+
parentSpanId: span.parentSpanId,
|
|
125
|
+
traceFlags: span.traceFlags,
|
|
126
|
+
});
|
|
127
|
+
reply.header("traceparent", formatTraceparent(span.traceId, span.spanId, span.traceFlags));
|
|
122
128
|
hookDone();
|
|
123
129
|
});
|
|
124
130
|
|
|
@@ -136,6 +142,8 @@ export function createFastifyTrace(options: FastifyTraceOptions): FastifyPluginC
|
|
|
136
142
|
const event: HttpRequestEvent = {
|
|
137
143
|
kind: "http.request",
|
|
138
144
|
traceId: trace.traceId,
|
|
145
|
+
spanId: trace.spanId,
|
|
146
|
+
parentSpanId: trace.parentSpanId,
|
|
139
147
|
method: request.method,
|
|
140
148
|
path,
|
|
141
149
|
status: reply.statusCode,
|
|
@@ -183,9 +191,17 @@ export function createFastifyTrace(options: FastifyTraceOptions): FastifyPluginC
|
|
|
183
191
|
*/
|
|
184
192
|
export function getTraceContext(
|
|
185
193
|
request: unknown,
|
|
186
|
-
):
|
|
194
|
+
):
|
|
195
|
+
| { _trace: { traceId: string; parentSpanId: string; traceFlags?: string } }
|
|
196
|
+
| Record<string, never> {
|
|
187
197
|
if (!request || typeof request !== "object") return {};
|
|
188
198
|
const trace = traceStore.get(request);
|
|
189
199
|
if (!trace) return {};
|
|
190
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
_trace: {
|
|
202
|
+
traceId: trace.traceId,
|
|
203
|
+
parentSpanId: trace.spanId,
|
|
204
|
+
traceFlags: trace.traceFlags,
|
|
205
|
+
},
|
|
206
|
+
};
|
|
191
207
|
}
|
package/src/adapters/fetch.ts
CHANGED
|
@@ -19,11 +19,18 @@
|
|
|
19
19
|
* ```
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
import {
|
|
23
|
+
type FetchFn,
|
|
24
|
+
defaultPropagateTo,
|
|
25
|
+
injectTraceparent,
|
|
26
|
+
resolveInput,
|
|
27
|
+
resolveUrl,
|
|
28
|
+
} from "../fetch-utils.ts";
|
|
29
|
+
import { startSpan } from "../trace-context.ts";
|
|
30
|
+
import { formatTraceparent } from "../traceparent.ts";
|
|
31
|
+
import type { ExternalCallEvent, Telemetry, TraceContext } from "../types.ts";
|
|
32
|
+
|
|
33
|
+
export type { FetchFn } from "../fetch-utils.ts";
|
|
27
34
|
|
|
28
35
|
/** Options for the traced fetch adapter. */
|
|
29
36
|
export interface TracedFetchOptions {
|
|
@@ -32,46 +39,32 @@ export interface TracedFetchOptions {
|
|
|
32
39
|
/** Base fetch implementation. Default: globalThis.fetch. */
|
|
33
40
|
baseFetch?: FetchFn;
|
|
34
41
|
/** Provide trace context for correlating with a parent HTTP request. */
|
|
35
|
-
getTraceContext?: () =>
|
|
42
|
+
getTraceContext?: () => TraceContext | undefined;
|
|
43
|
+
/** Predicate controlling where to forward `traceparent` headers. */
|
|
44
|
+
propagateTo?: (url: URL) => boolean;
|
|
45
|
+
/** Optional callback invoked when responses include `traceparent`. */
|
|
46
|
+
onResponseTraceparent?: (traceparent: string) => void;
|
|
36
47
|
/** Guard function — return false to skip tracing. */
|
|
37
48
|
isEnabled?: () => boolean;
|
|
38
49
|
}
|
|
39
50
|
|
|
40
|
-
/**
|
|
41
|
-
* Extract URL metadata from the three fetch input types.
|
|
42
|
-
* This is metadata-only — the original input is never modified.
|
|
43
|
-
*/
|
|
44
|
-
function resolveInput(input: RequestInfo | URL): {
|
|
45
|
-
url: string;
|
|
46
|
-
method: string;
|
|
47
|
-
} {
|
|
48
|
-
if (input instanceof Request) {
|
|
49
|
-
return { url: input.url, method: input.method };
|
|
50
|
-
}
|
|
51
|
-
if (input instanceof URL) {
|
|
52
|
-
return { url: input.href, method: "GET" };
|
|
53
|
-
}
|
|
54
|
-
// string — try absolute first, then relative with localhost fallback
|
|
55
|
-
try {
|
|
56
|
-
return { url: new URL(input).href, method: "GET" };
|
|
57
|
-
} catch {
|
|
58
|
-
return {
|
|
59
|
-
url: new URL(input, "http://localhost").href,
|
|
60
|
-
method: "GET",
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
|
|
65
51
|
/**
|
|
66
52
|
* Create a traced fetch function that emits external.call telemetry events.
|
|
67
53
|
*
|
|
68
54
|
* The returned function has the same signature as globalThis.fetch.
|
|
69
|
-
*
|
|
55
|
+
* Request inputs are only cloned when header propagation is enabled.
|
|
70
56
|
* Non-2xx responses are returned normally (not thrown). Network errors
|
|
71
57
|
* are emitted as status "error" and re-thrown.
|
|
72
58
|
*/
|
|
73
59
|
export function createTracedFetch(options: TracedFetchOptions): FetchFn {
|
|
74
|
-
const {
|
|
60
|
+
const {
|
|
61
|
+
telemetry,
|
|
62
|
+
baseFetch = globalThis.fetch,
|
|
63
|
+
getTraceContext,
|
|
64
|
+
propagateTo,
|
|
65
|
+
onResponseTraceparent,
|
|
66
|
+
isEnabled,
|
|
67
|
+
} = options;
|
|
75
68
|
|
|
76
69
|
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
77
70
|
if (isEnabled && !isEnabled()) {
|
|
@@ -79,34 +72,42 @@ export function createTracedFetch(options: TracedFetchOptions): FetchFn {
|
|
|
79
72
|
}
|
|
80
73
|
|
|
81
74
|
const { url, method: resolvedMethod } = resolveInput(input);
|
|
82
|
-
const method = init?.method?.toUpperCase() ?? resolvedMethod;
|
|
75
|
+
const method = init?.method?.toUpperCase() ?? resolvedMethod.toUpperCase();
|
|
76
|
+
const parsedUrl = resolveUrl(url);
|
|
83
77
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
try {
|
|
87
|
-
const parsed = new URL(url);
|
|
88
|
-
service = parsed.hostname;
|
|
89
|
-
pathname = parsed.pathname;
|
|
90
|
-
} catch {
|
|
91
|
-
// keep defaults
|
|
92
|
-
}
|
|
78
|
+
const service = parsedUrl.hostname;
|
|
79
|
+
const pathname = parsedUrl.pathname;
|
|
93
80
|
|
|
94
81
|
const operation = `${method} ${pathname}`;
|
|
95
82
|
|
|
96
83
|
const ctx = getTraceContext?.();
|
|
97
|
-
const
|
|
98
|
-
|
|
84
|
+
const span = startSpan({
|
|
85
|
+
traceId: ctx?.traceId,
|
|
86
|
+
parentSpanId: ctx?.parentSpanId,
|
|
87
|
+
traceFlags: ctx?.traceFlags,
|
|
88
|
+
});
|
|
89
|
+
const traceparent = formatTraceparent(span.traceId, span.spanId, span.traceFlags);
|
|
90
|
+
|
|
91
|
+
const shouldPropagate = (propagateTo ?? defaultPropagateTo)(parsedUrl);
|
|
92
|
+
const outbound = shouldPropagate
|
|
93
|
+
? injectTraceparent(input, init, traceparent)
|
|
94
|
+
: { input, init };
|
|
99
95
|
|
|
100
96
|
const start = performance.now();
|
|
101
97
|
|
|
102
98
|
try {
|
|
103
|
-
const response = await baseFetch(input, init);
|
|
99
|
+
const response = await baseFetch(outbound.input, outbound.init);
|
|
104
100
|
const duration_ms = Math.round(performance.now() - start);
|
|
101
|
+
const responseTraceparent = response.headers.get("traceparent");
|
|
102
|
+
if (responseTraceparent) {
|
|
103
|
+
onResponseTraceparent?.(responseTraceparent);
|
|
104
|
+
}
|
|
105
105
|
|
|
106
106
|
telemetry.emit({
|
|
107
107
|
kind: "external.call",
|
|
108
|
-
traceId,
|
|
109
|
-
spanId,
|
|
108
|
+
traceId: span.traceId,
|
|
109
|
+
spanId: span.spanId,
|
|
110
|
+
parentSpanId: span.parentSpanId,
|
|
110
111
|
service,
|
|
111
112
|
operation,
|
|
112
113
|
duration_ms,
|
|
@@ -119,8 +120,9 @@ export function createTracedFetch(options: TracedFetchOptions): FetchFn {
|
|
|
119
120
|
|
|
120
121
|
telemetry.emit({
|
|
121
122
|
kind: "external.call",
|
|
122
|
-
traceId,
|
|
123
|
-
spanId,
|
|
123
|
+
traceId: span.traceId,
|
|
124
|
+
spanId: span.spanId,
|
|
125
|
+
parentSpanId: span.parentSpanId,
|
|
124
126
|
service,
|
|
125
127
|
operation,
|
|
126
128
|
duration_ms,
|
package/src/adapters/hono.ts
CHANGED
|
@@ -22,8 +22,9 @@
|
|
|
22
22
|
import type { Context, MiddlewareHandler } from "hono";
|
|
23
23
|
import { extractEntities } from "../entities.ts";
|
|
24
24
|
import { toSafeErrorLabel } from "../error.ts";
|
|
25
|
-
import { generateSpanId
|
|
26
|
-
import {
|
|
25
|
+
import { generateSpanId } from "../ids.ts";
|
|
26
|
+
import { startSpanFromTraceparent } from "../trace-context.ts";
|
|
27
|
+
import { formatTraceparent } from "../traceparent.ts";
|
|
27
28
|
import type { EntityPattern, HttpRequestEvent, Telemetry } from "../types.ts";
|
|
28
29
|
|
|
29
30
|
/** Options for Hono trace middleware. */
|
|
@@ -39,6 +40,7 @@ export interface HonoTraceOptions {
|
|
|
39
40
|
/** Hono variable keys for trace storage. */
|
|
40
41
|
const TRACE_ID_VAR = "traceId" as const;
|
|
41
42
|
const SPAN_ID_VAR = "spanId" as const;
|
|
43
|
+
const TRACE_FLAGS_VAR = "traceFlags" as const;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Create Hono middleware that traces HTTP requests.
|
|
@@ -55,12 +57,11 @@ export function createHonoTrace(options: HonoTraceOptions): MiddlewareHandler {
|
|
|
55
57
|
return next();
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
const
|
|
59
|
-
const traceId = parsed?.traceId ?? generateTraceId();
|
|
60
|
-
const spanId = generateSpanId();
|
|
60
|
+
const span = startSpanFromTraceparent(c.req.header("traceparent"));
|
|
61
61
|
|
|
62
|
-
c.set(TRACE_ID_VAR, traceId);
|
|
63
|
-
c.set(SPAN_ID_VAR, spanId);
|
|
62
|
+
c.set(TRACE_ID_VAR, span.traceId);
|
|
63
|
+
c.set(SPAN_ID_VAR, span.spanId);
|
|
64
|
+
c.set(TRACE_FLAGS_VAR, span.traceFlags);
|
|
64
65
|
|
|
65
66
|
const start = performance.now();
|
|
66
67
|
let error: string | undefined;
|
|
@@ -75,11 +76,13 @@ export function createHonoTrace(options: HonoTraceOptions): MiddlewareHandler {
|
|
|
75
76
|
const duration_ms = Math.round(performance.now() - start);
|
|
76
77
|
const path = c.req.path;
|
|
77
78
|
|
|
78
|
-
c.header("traceparent", formatTraceparent(traceId, spanId));
|
|
79
|
+
c.header("traceparent", formatTraceparent(span.traceId, span.spanId, span.traceFlags));
|
|
79
80
|
|
|
80
81
|
const event: HttpRequestEvent = {
|
|
81
82
|
kind: "http.request",
|
|
82
|
-
traceId,
|
|
83
|
+
traceId: span.traceId,
|
|
84
|
+
spanId: span.spanId,
|
|
85
|
+
parentSpanId: span.parentSpanId,
|
|
83
86
|
method: c.req.method,
|
|
84
87
|
path,
|
|
85
88
|
status,
|
|
@@ -117,9 +120,12 @@ export function createHonoTrace(options: HonoTraceOptions): MiddlewareHandler {
|
|
|
117
120
|
*/
|
|
118
121
|
export function getTraceContext(
|
|
119
122
|
c: Context,
|
|
120
|
-
):
|
|
123
|
+
):
|
|
124
|
+
| { _trace: { traceId: string; parentSpanId: string; traceFlags?: string } }
|
|
125
|
+
| Record<string, never> {
|
|
121
126
|
const traceId = c.get(TRACE_ID_VAR) as string | undefined;
|
|
122
127
|
const spanId = c.get(SPAN_ID_VAR) as string | undefined;
|
|
128
|
+
const traceFlags = c.get(TRACE_FLAGS_VAR) as string | undefined;
|
|
123
129
|
if (!traceId) return {};
|
|
124
|
-
return { _trace: { traceId, parentSpanId: spanId ?? generateSpanId() } };
|
|
130
|
+
return { _trace: { traceId, parentSpanId: spanId ?? generateSpanId(), traceFlags } };
|
|
125
131
|
}
|
package/src/adapters/prisma.ts
CHANGED
|
@@ -18,8 +18,8 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { toSafeErrorLabel } from "../error.ts";
|
|
21
|
-
import {
|
|
22
|
-
import type { DbQueryEvent, Telemetry } from "../types.ts";
|
|
21
|
+
import { startSpan } from "../trace-context.ts";
|
|
22
|
+
import type { DbQueryEvent, Telemetry, TraceContext } from "../types.ts";
|
|
23
23
|
|
|
24
24
|
/** Options for the Prisma trace extension. */
|
|
25
25
|
export interface PrismaTraceOptions {
|
|
@@ -28,7 +28,7 @@ export interface PrismaTraceOptions {
|
|
|
28
28
|
/** Guard function — return false to skip tracing. */
|
|
29
29
|
isEnabled?: () => boolean;
|
|
30
30
|
/** Provide parent trace context for correlating with an incoming request. */
|
|
31
|
-
getTraceContext?: () =>
|
|
31
|
+
getTraceContext?: () => TraceContext | undefined;
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/** Callback params passed by Prisma's $allOperations hook. */
|
|
@@ -67,8 +67,12 @@ export function createPrismaTrace(options: PrismaTraceOptions): PrismaTraceExten
|
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const start = performance.now();
|
|
70
|
-
const
|
|
71
|
-
const
|
|
70
|
+
const ctx = getTraceContext?.();
|
|
71
|
+
const span = startSpan({
|
|
72
|
+
traceId: ctx?.traceId,
|
|
73
|
+
parentSpanId: ctx?.parentSpanId,
|
|
74
|
+
traceFlags: ctx?.traceFlags,
|
|
75
|
+
});
|
|
72
76
|
|
|
73
77
|
try {
|
|
74
78
|
const result = await query(args);
|
|
@@ -76,8 +80,9 @@ export function createPrismaTrace(options: PrismaTraceOptions): PrismaTraceExten
|
|
|
76
80
|
|
|
77
81
|
const event: DbQueryEvent = {
|
|
78
82
|
kind: "db.query",
|
|
79
|
-
traceId,
|
|
80
|
-
spanId,
|
|
83
|
+
traceId: span.traceId,
|
|
84
|
+
spanId: span.spanId,
|
|
85
|
+
parentSpanId: span.parentSpanId,
|
|
81
86
|
provider: "prisma",
|
|
82
87
|
model,
|
|
83
88
|
operation,
|
|
@@ -92,8 +97,9 @@ export function createPrismaTrace(options: PrismaTraceOptions): PrismaTraceExten
|
|
|
92
97
|
|
|
93
98
|
const event: DbQueryEvent = {
|
|
94
99
|
kind: "db.query",
|
|
95
|
-
traceId,
|
|
96
|
-
spanId,
|
|
100
|
+
traceId: span.traceId,
|
|
101
|
+
spanId: span.spanId,
|
|
102
|
+
parentSpanId: span.parentSpanId,
|
|
97
103
|
provider: "prisma",
|
|
98
104
|
model,
|
|
99
105
|
operation,
|
package/src/adapters/supabase.ts
CHANGED
|
@@ -24,11 +24,17 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { toSafeErrorLabel } from "../error.ts";
|
|
27
|
-
import {
|
|
28
|
-
import
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
import { type FetchFn, resolveInput } from "../fetch-utils.ts";
|
|
28
|
+
import { startSpan } from "../trace-context.ts";
|
|
29
|
+
import type {
|
|
30
|
+
DbQueryEvent,
|
|
31
|
+
ExternalCallEvent,
|
|
32
|
+
SupabaseEvents,
|
|
33
|
+
Telemetry,
|
|
34
|
+
TraceContext,
|
|
35
|
+
} from "../types.ts";
|
|
36
|
+
|
|
37
|
+
export type { FetchFn } from "../fetch-utils.ts";
|
|
32
38
|
|
|
33
39
|
/** Options for the Supabase trace adapter. */
|
|
34
40
|
export interface SupabaseTraceOptions {
|
|
@@ -37,7 +43,7 @@ export interface SupabaseTraceOptions {
|
|
|
37
43
|
/** Base fetch implementation. Default: globalThis.fetch. */
|
|
38
44
|
baseFetch?: FetchFn;
|
|
39
45
|
/** Provide trace context for correlating with a parent HTTP request. */
|
|
40
|
-
getTraceContext?: () =>
|
|
46
|
+
getTraceContext?: () => TraceContext | undefined;
|
|
41
47
|
/** Guard function — return false to skip tracing. */
|
|
42
48
|
isEnabled?: () => boolean;
|
|
43
49
|
}
|
|
@@ -67,31 +73,6 @@ const AUTH_RE = /\/auth\/v\d+\/(.+)/;
|
|
|
67
73
|
const STORAGE_RE = /\/storage\/v\d+\/object\/([^/]+)/;
|
|
68
74
|
const FUNCTIONS_RE = /\/functions\/v\d+\/([^?/]+)/;
|
|
69
75
|
|
|
70
|
-
/**
|
|
71
|
-
* Extract URL metadata from the three fetch input types.
|
|
72
|
-
* This is metadata-only — the original input is never modified (C3/C4).
|
|
73
|
-
*/
|
|
74
|
-
function resolveInput(input: RequestInfo | URL): {
|
|
75
|
-
url: string;
|
|
76
|
-
method: string;
|
|
77
|
-
} {
|
|
78
|
-
if (input instanceof Request) {
|
|
79
|
-
return { url: input.url, method: input.method };
|
|
80
|
-
}
|
|
81
|
-
if (input instanceof URL) {
|
|
82
|
-
return { url: input.href, method: "GET" };
|
|
83
|
-
}
|
|
84
|
-
// string
|
|
85
|
-
try {
|
|
86
|
-
return { url: new URL(input).href, method: "GET" };
|
|
87
|
-
} catch {
|
|
88
|
-
return {
|
|
89
|
-
url: new URL(input, "http://localhost").href,
|
|
90
|
-
method: "GET",
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
76
|
/**
|
|
96
77
|
* Classify a Supabase request URL into the appropriate event type.
|
|
97
78
|
* Uses the URL pathname to determine if it's a PostgREST, Auth,
|
|
@@ -180,8 +161,11 @@ export function createSupabaseTrace(options: SupabaseTraceOptions): FetchFn {
|
|
|
180
161
|
const classification = classifyRequest(parsed, method);
|
|
181
162
|
|
|
182
163
|
const ctx = getTraceContext?.();
|
|
183
|
-
const
|
|
184
|
-
|
|
164
|
+
const span = startSpan({
|
|
165
|
+
traceId: ctx?.traceId,
|
|
166
|
+
parentSpanId: ctx?.parentSpanId,
|
|
167
|
+
traceFlags: ctx?.traceFlags,
|
|
168
|
+
});
|
|
185
169
|
|
|
186
170
|
const start = performance.now();
|
|
187
171
|
|
|
@@ -193,8 +177,9 @@ export function createSupabaseTrace(options: SupabaseTraceOptions): FetchFn {
|
|
|
193
177
|
if (classification.kind === "db.query") {
|
|
194
178
|
const event: DbQueryEvent = {
|
|
195
179
|
kind: "db.query",
|
|
196
|
-
traceId,
|
|
197
|
-
spanId,
|
|
180
|
+
traceId: span.traceId,
|
|
181
|
+
spanId: span.spanId,
|
|
182
|
+
parentSpanId: span.parentSpanId,
|
|
198
183
|
provider: classification.provider,
|
|
199
184
|
model: classification.model,
|
|
200
185
|
operation: classification.operation,
|
|
@@ -205,8 +190,9 @@ export function createSupabaseTrace(options: SupabaseTraceOptions): FetchFn {
|
|
|
205
190
|
} else {
|
|
206
191
|
const event: ExternalCallEvent = {
|
|
207
192
|
kind: "external.call",
|
|
208
|
-
traceId,
|
|
209
|
-
spanId,
|
|
193
|
+
traceId: span.traceId,
|
|
194
|
+
spanId: span.spanId,
|
|
195
|
+
parentSpanId: span.parentSpanId,
|
|
210
196
|
service: classification.service,
|
|
211
197
|
operation: classification.operation,
|
|
212
198
|
duration_ms,
|
|
@@ -222,8 +208,9 @@ export function createSupabaseTrace(options: SupabaseTraceOptions): FetchFn {
|
|
|
222
208
|
if (classification.kind === "db.query") {
|
|
223
209
|
const event: DbQueryEvent = {
|
|
224
210
|
kind: "db.query",
|
|
225
|
-
traceId,
|
|
226
|
-
spanId,
|
|
211
|
+
traceId: span.traceId,
|
|
212
|
+
spanId: span.spanId,
|
|
213
|
+
parentSpanId: span.parentSpanId,
|
|
227
214
|
provider: classification.provider,
|
|
228
215
|
model: classification.model,
|
|
229
216
|
operation: classification.operation,
|
|
@@ -235,8 +222,9 @@ export function createSupabaseTrace(options: SupabaseTraceOptions): FetchFn {
|
|
|
235
222
|
} else {
|
|
236
223
|
const event: ExternalCallEvent = {
|
|
237
224
|
kind: "external.call",
|
|
238
|
-
traceId,
|
|
239
|
-
spanId,
|
|
225
|
+
traceId: span.traceId,
|
|
226
|
+
spanId: span.spanId,
|
|
227
|
+
parentSpanId: span.parentSpanId,
|
|
240
228
|
service: classification.service,
|
|
241
229
|
operation: classification.operation,
|
|
242
230
|
duration_ms,
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type FetchFn,
|
|
3
|
+
defaultPropagateTo,
|
|
4
|
+
injectTraceparent,
|
|
5
|
+
resolveInput,
|
|
6
|
+
resolveUrl,
|
|
7
|
+
} from "./fetch-utils.ts";
|
|
8
|
+
import { generateSpanId, generateTraceId } from "./ids.ts";
|
|
9
|
+
import { normalizeTraceFlags, startSpan } from "./trace-context.ts";
|
|
10
|
+
import { formatTraceparent, parseTraceparent } from "./traceparent.ts";
|
|
11
|
+
import type { TraceContext } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export type { FetchFn } from "./fetch-utils.ts";
|
|
14
|
+
|
|
15
|
+
interface BrowserTraceState {
|
|
16
|
+
traceId: string;
|
|
17
|
+
parentSpanId: string;
|
|
18
|
+
traceFlags: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function readMetaTraceparent(metaName: string): string | undefined {
|
|
22
|
+
if (typeof document === "undefined") return undefined;
|
|
23
|
+
const value = document.querySelector(`meta[name="${metaName}"]`)?.getAttribute("content");
|
|
24
|
+
return value ?? undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function toTraceContext(state: BrowserTraceState): TraceContext {
|
|
28
|
+
return {
|
|
29
|
+
traceId: state.traceId,
|
|
30
|
+
parentSpanId: state.parentSpanId,
|
|
31
|
+
traceFlags: state.traceFlags,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Browser trace context manager for request propagation. */
|
|
36
|
+
export interface BrowserTraceContext {
|
|
37
|
+
/** Get the current trace context for child operations. */
|
|
38
|
+
getTraceContext(): TraceContext;
|
|
39
|
+
/** Get a serialized `traceparent` for the current context. */
|
|
40
|
+
getTraceparent(): string;
|
|
41
|
+
/** Replace the current trace context. */
|
|
42
|
+
setTraceContext(context: TraceContext): void;
|
|
43
|
+
/** Parse and adopt an incoming `traceparent` header value. */
|
|
44
|
+
updateFromTraceparent(traceparent: string | null | undefined): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Run work under a child span.
|
|
47
|
+
* The callback receives a context whose `parentSpanId` is the created span ID.
|
|
48
|
+
*/
|
|
49
|
+
withSpan<T>(
|
|
50
|
+
name: string,
|
|
51
|
+
run: (context: TraceContext & { spanId: string; name: string }) => Promise<T> | T,
|
|
52
|
+
): Promise<T>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface BrowserTraceContextOptions {
|
|
56
|
+
/** Optional bootstrap header value (e.g. from SSR). */
|
|
57
|
+
initialTraceparent?: string | null;
|
|
58
|
+
/** Meta tag name used for bootstrap lookup. Default: "traceparent". */
|
|
59
|
+
metaName?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Create browser trace context.
|
|
64
|
+
*
|
|
65
|
+
* Bootstrap order:
|
|
66
|
+
* 1) options.initialTraceparent
|
|
67
|
+
* 2) <meta name="traceparent" content="...">
|
|
68
|
+
* 3) fresh trace/span IDs
|
|
69
|
+
*/
|
|
70
|
+
export function createBrowserTraceContext(
|
|
71
|
+
options: BrowserTraceContextOptions = {},
|
|
72
|
+
): BrowserTraceContext {
|
|
73
|
+
const metaName = options.metaName ?? "traceparent";
|
|
74
|
+
const bootstrap = options.initialTraceparent ?? readMetaTraceparent(metaName);
|
|
75
|
+
const parsed = parseTraceparent(bootstrap);
|
|
76
|
+
|
|
77
|
+
const state: BrowserTraceState = {
|
|
78
|
+
traceId: parsed?.traceId ?? generateTraceId(),
|
|
79
|
+
parentSpanId: parsed?.parentId ?? generateSpanId(),
|
|
80
|
+
traceFlags: normalizeTraceFlags(parsed?.traceFlags),
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const api: BrowserTraceContext = {
|
|
84
|
+
getTraceContext() {
|
|
85
|
+
return toTraceContext(state);
|
|
86
|
+
},
|
|
87
|
+
getTraceparent() {
|
|
88
|
+
return formatTraceparent(state.traceId, state.parentSpanId, state.traceFlags);
|
|
89
|
+
},
|
|
90
|
+
setTraceContext(context) {
|
|
91
|
+
state.traceId = context.traceId;
|
|
92
|
+
state.parentSpanId = context.parentSpanId;
|
|
93
|
+
state.traceFlags = normalizeTraceFlags(context.traceFlags);
|
|
94
|
+
},
|
|
95
|
+
updateFromTraceparent(traceparent) {
|
|
96
|
+
const incoming = parseTraceparent(traceparent);
|
|
97
|
+
if (!incoming) return false;
|
|
98
|
+
state.traceId = incoming.traceId;
|
|
99
|
+
state.parentSpanId = incoming.parentId;
|
|
100
|
+
state.traceFlags = normalizeTraceFlags(incoming.traceFlags);
|
|
101
|
+
return true;
|
|
102
|
+
},
|
|
103
|
+
async withSpan(name, run) {
|
|
104
|
+
const currentParent = state.parentSpanId;
|
|
105
|
+
const span = startSpan({
|
|
106
|
+
traceId: state.traceId,
|
|
107
|
+
parentSpanId: currentParent,
|
|
108
|
+
traceFlags: state.traceFlags,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
state.parentSpanId = span.spanId;
|
|
112
|
+
try {
|
|
113
|
+
return await run({
|
|
114
|
+
traceId: span.traceId,
|
|
115
|
+
parentSpanId: span.spanId,
|
|
116
|
+
traceFlags: span.traceFlags,
|
|
117
|
+
spanId: span.spanId,
|
|
118
|
+
name,
|
|
119
|
+
});
|
|
120
|
+
} finally {
|
|
121
|
+
state.parentSpanId = currentParent;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
return api;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export interface BrowserTracedFetchOptions {
|
|
130
|
+
/** Base fetch implementation. Default: globalThis.fetch. */
|
|
131
|
+
baseFetch?: FetchFn;
|
|
132
|
+
/** Shared trace context manager. If omitted, a new one is created. */
|
|
133
|
+
trace?: BrowserTraceContext;
|
|
134
|
+
/** Predicate controlling where to forward `traceparent`. Default: same-origin only. */
|
|
135
|
+
propagateTo?: (url: URL) => boolean;
|
|
136
|
+
/** Whether to adopt response `traceparent` headers. Default: true. */
|
|
137
|
+
updateContextFromResponse?: boolean;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Create a browser fetch wrapper that injects W3C `traceparent`.
|
|
142
|
+
*
|
|
143
|
+
* By default it only propagates headers to same-origin URLs.
|
|
144
|
+
*/
|
|
145
|
+
export function createBrowserTracedFetch(options: BrowserTracedFetchOptions = {}): FetchFn {
|
|
146
|
+
const {
|
|
147
|
+
baseFetch = globalThis.fetch,
|
|
148
|
+
trace = createBrowserTraceContext(),
|
|
149
|
+
propagateTo = defaultPropagateTo,
|
|
150
|
+
updateContextFromResponse = true,
|
|
151
|
+
} = options;
|
|
152
|
+
|
|
153
|
+
return async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
|
|
154
|
+
const { url } = resolveInput(input);
|
|
155
|
+
const parsedUrl = resolveUrl(url);
|
|
156
|
+
|
|
157
|
+
const ctx = trace.getTraceContext();
|
|
158
|
+
const span = startSpan({
|
|
159
|
+
traceId: ctx.traceId,
|
|
160
|
+
parentSpanId: ctx.parentSpanId,
|
|
161
|
+
traceFlags: ctx.traceFlags,
|
|
162
|
+
});
|
|
163
|
+
const traceparent = formatTraceparent(span.traceId, span.spanId, span.traceFlags);
|
|
164
|
+
|
|
165
|
+
const outbound = propagateTo(parsedUrl)
|
|
166
|
+
? injectTraceparent(input, init, traceparent)
|
|
167
|
+
: { input, init };
|
|
168
|
+
|
|
169
|
+
const response = await baseFetch(outbound.input, outbound.init);
|
|
170
|
+
|
|
171
|
+
if (updateContextFromResponse) {
|
|
172
|
+
const responseTraceparent = response.headers.get("traceparent");
|
|
173
|
+
if (!responseTraceparent || !trace.updateFromTraceparent(responseTraceparent)) {
|
|
174
|
+
trace.setTraceContext({
|
|
175
|
+
traceId: span.traceId,
|
|
176
|
+
parentSpanId: span.spanId,
|
|
177
|
+
traceFlags: span.traceFlags,
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return response;
|
|
183
|
+
};
|
|
184
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared fetch utilities for adapters that wrap fetch.
|
|
3
|
+
*
|
|
4
|
+
* Used by the traced fetch adapter, Supabase adapter, and browser module
|
|
5
|
+
* to avoid duplicating URL resolution, traceparent injection, and
|
|
6
|
+
* origin detection logic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Callable fetch signature (without static properties like `preconnect`). */
|
|
10
|
+
export type FetchFn = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
11
|
+
|
|
12
|
+
export function getLocationOrigin(): string | undefined {
|
|
13
|
+
const globalWithLocation = globalThis as { location?: { origin?: string } };
|
|
14
|
+
return globalWithLocation.location?.origin;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function resolveUrl(url: string): URL {
|
|
18
|
+
const base = getLocationOrigin() ?? "http://localhost";
|
|
19
|
+
return new URL(url, base);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function defaultPropagateTo(url: URL): boolean {
|
|
23
|
+
const origin = getLocationOrigin();
|
|
24
|
+
return origin != null && url.origin === origin;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Extract URL metadata from the three fetch input types.
|
|
29
|
+
* This is metadata-only — the original input is never modified.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveInput(input: RequestInfo | URL): {
|
|
32
|
+
url: string;
|
|
33
|
+
method: string;
|
|
34
|
+
} {
|
|
35
|
+
if (input instanceof Request) {
|
|
36
|
+
return { url: input.url, method: input.method };
|
|
37
|
+
}
|
|
38
|
+
if (input instanceof URL) {
|
|
39
|
+
return { url: input.href, method: "GET" };
|
|
40
|
+
}
|
|
41
|
+
// string — try absolute first, then relative with location-aware fallback
|
|
42
|
+
try {
|
|
43
|
+
return { url: new URL(input).href, method: "GET" };
|
|
44
|
+
} catch {
|
|
45
|
+
return {
|
|
46
|
+
url: resolveUrl(input).href,
|
|
47
|
+
method: "GET",
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function injectTraceparent(
|
|
53
|
+
input: RequestInfo | URL,
|
|
54
|
+
init: RequestInit | undefined,
|
|
55
|
+
traceparent: string,
|
|
56
|
+
): { input: RequestInfo | URL; init: RequestInit | undefined } {
|
|
57
|
+
if (input instanceof Request) {
|
|
58
|
+
const request = new Request(input, init);
|
|
59
|
+
const headers = new Headers(request.headers);
|
|
60
|
+
headers.set("traceparent", traceparent);
|
|
61
|
+
return { input: new Request(request, { headers }), init: undefined };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const headers = new Headers(init?.headers);
|
|
65
|
+
headers.set("traceparent", traceparent);
|
|
66
|
+
return { input, init: { ...init, headers } };
|
|
67
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { generateSpanId, generateTraceId } from "./ids.ts";
|
|
2
|
+
import { parseTraceparent } from "./traceparent.ts";
|
|
3
|
+
|
|
4
|
+
const TRACE_FLAGS_RE = /^[\da-f]{2}$/;
|
|
5
|
+
|
|
6
|
+
export function normalizeTraceFlags(traceFlags: string | undefined): string {
|
|
7
|
+
if (!traceFlags) return "01";
|
|
8
|
+
const normalized = traceFlags.toLowerCase();
|
|
9
|
+
return TRACE_FLAGS_RE.test(normalized) ? normalized : "01";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface SpanStartOptions {
|
|
13
|
+
traceId?: string;
|
|
14
|
+
parentSpanId?: string;
|
|
15
|
+
traceFlags?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface SpanContext {
|
|
19
|
+
traceId: string;
|
|
20
|
+
spanId: string;
|
|
21
|
+
parentSpanId?: string;
|
|
22
|
+
traceFlags: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function startSpan(options: SpanStartOptions = {}): SpanContext {
|
|
26
|
+
return {
|
|
27
|
+
traceId: options.traceId ?? generateTraceId(),
|
|
28
|
+
spanId: generateSpanId(),
|
|
29
|
+
parentSpanId: options.parentSpanId,
|
|
30
|
+
traceFlags: normalizeTraceFlags(options.traceFlags),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function startSpanFromTraceparent(header: string | null | undefined): SpanContext {
|
|
35
|
+
const parsed = parseTraceparent(header);
|
|
36
|
+
return startSpan({
|
|
37
|
+
traceId: parsed?.traceId,
|
|
38
|
+
parentSpanId: parsed?.parentId,
|
|
39
|
+
traceFlags: parsed?.traceFlags,
|
|
40
|
+
});
|
|
41
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface BaseTelemetryEvent {
|
|
|
22
22
|
|
|
23
23
|
export interface HttpRequestEvent extends BaseTelemetryEvent {
|
|
24
24
|
kind: "http.request";
|
|
25
|
+
spanId?: string;
|
|
26
|
+
parentSpanId?: string;
|
|
25
27
|
method: string;
|
|
26
28
|
path: string;
|
|
27
29
|
status: number;
|
|
@@ -72,6 +74,7 @@ export type JobEvents = JobStartEvent | JobEndEvent | JobDispatchEvent | JobStep
|
|
|
72
74
|
export interface ExternalCallEvent extends BaseTelemetryEvent {
|
|
73
75
|
kind: "external.call";
|
|
74
76
|
spanId: string;
|
|
77
|
+
parentSpanId?: string;
|
|
75
78
|
service: string;
|
|
76
79
|
operation: string;
|
|
77
80
|
duration_ms: number;
|
|
@@ -84,6 +87,7 @@ export type ExternalEvents = ExternalCallEvent;
|
|
|
84
87
|
export interface DbQueryEvent extends BaseTelemetryEvent {
|
|
85
88
|
kind: "db.query";
|
|
86
89
|
spanId: string;
|
|
90
|
+
parentSpanId?: string;
|
|
87
91
|
/** Provider identifier (e.g. "prisma", "supabase", "drizzle"). */
|
|
88
92
|
provider: string;
|
|
89
93
|
/** The data entity being operated on — ORM model name or database table. */
|