autotel-tanstack 1.13.5 → 1.13.7

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.
@@ -6,7 +6,7 @@ export { createTracedServerFnFactory, traceServerFn } from '../chunk-CSFIPJC2.js
6
6
  export { createTracedRoute, traceBeforeLoad, traceLoader } from '../chunk-MNP65ZX7.js';
7
7
  export { createTracedHeaders, extractContextFromRequest, getActiveContext, getCurrentSpanId, getCurrentTraceId, getTraceParent, getTraceState, injectContextToHeaders, runInContext } from '../chunk-DTZCOB4W.js';
8
8
  export { wrapStartHandler } from '../chunk-V3RO5N2M.js';
9
- export { assertSpanCreated, assertSpanHasAttribute, createTestCollector } from '../chunk-ZJTHTWHJ.js';
9
+ export { assertSpanCreated, assertSpanHasAttribute, createTestCollector } from '../chunk-YQYYPJCK.js';
10
10
  export { debugHeadersMiddleware } from '../chunk-JXO7H6KO.js';
11
11
  //# sourceMappingURL=index.js.map
12
12
  //# sourceMappingURL=index.js.map
@@ -1,3 +1,3 @@
1
- export { assertSpanCreated, assertSpanHasAttribute, createTestCollector, createTestSpansHandlers } from '../chunk-ZJTHTWHJ.js';
1
+ export { assertSpanCreated, assertSpanHasAttribute, createTestCollector, createTestSpansHandlers, createTestSpansRoute } from '../chunk-YQYYPJCK.js';
2
2
  //# sourceMappingURL=testing.js.map
3
3
  //# sourceMappingURL=testing.js.map
@@ -12,15 +12,18 @@ function assertSpanCreated(collector, name) {
12
12
  }
13
13
  function assertSpanHasAttribute(collector, name, key, value) {
14
14
  }
15
+ function createTestSpansRoute(createFileRoute, path) {
16
+ throw new Error("createTestSpansRoute is server-only");
17
+ }
15
18
  function createTestSpansHandlers() {
16
19
  return {
17
- GET(_request) {
20
+ GET(input) {
18
21
  return Response.json(
19
22
  { error: "createTestSpansHandlers is server-only" },
20
23
  { status: 404 }
21
24
  );
22
25
  },
23
- DELETE(_request) {
26
+ DELETE(input) {
24
27
  return Response.json(
25
28
  { error: "createTestSpansHandlers is server-only" },
26
29
  { status: 404 }
@@ -29,6 +32,6 @@ function createTestSpansHandlers() {
29
32
  };
30
33
  }
31
34
 
32
- export { assertSpanCreated, assertSpanHasAttribute, createTestCollector, createTestSpansHandlers };
33
- //# sourceMappingURL=chunk-ZJTHTWHJ.js.map
34
- //# sourceMappingURL=chunk-ZJTHTWHJ.js.map
35
+ export { assertSpanCreated, assertSpanHasAttribute, createTestCollector, createTestSpansHandlers, createTestSpansRoute };
36
+ //# sourceMappingURL=chunk-YQYYPJCK.js.map
37
+ //# sourceMappingURL=chunk-YQYYPJCK.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/browser/testing.ts"],"names":[],"mappings":";AA+BO,SAAS,mBAAA,GAAqC;AACnD,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,MAAM,EAAC;AAAA,IACjB,cAAA,EAAgB,MAAM,EAAC;AAAA,IACvB,OAAO,MAAM;AAAA,IAAC,CAAA;AAAA,IACd,YAAA,EAAc,YAAY;AAAC,GAC7B;AACF;AAKO,SAAS,iBAAA,CACd,WACA,IAAA,EACM;AAIR;AAKO,SAAS,sBAAA,CACd,SAAA,EACA,IAAA,EACA,GAAA,EACA,KAAA,EACM;AAMR;AA8BO,SAAS,oBAAA,CACd,iBACA,IAAA,EACS;AAGT,EAAA,MAAM,IAAI,MAAM,qCAAqC,CAAA;AACvD;AAMO,SAAS,uBAAA,GAGd;AACA,EAAA,OAAO;AAAA,IACL,IAAI,KAAA,EAA+B;AAEjC,MAAA,OAAO,QAAA,CAAS,IAAA;AAAA,QACd,EAAE,OAAO,wCAAA,EAAyC;AAAA,QAClD,EAAE,QAAQ,GAAA;AAAI,OAChB;AAAA,IACF,CAAA;AAAA,IACA,OAAO,KAAA,EAA+B;AAEpC,MAAA,OAAO,QAAA,CAAS,IAAA;AAAA,QACd,EAAE,OAAO,wCAAA,EAAyC;AAAA,QAClD,EAAE,QAAQ,GAAA;AAAI,OAChB;AAAA,IACF;AAAA,GACF;AACF","file":"chunk-YQYYPJCK.js","sourcesContent":["/**\n * Browser stub for testing module\n *\n * Testing utilities are server-side only.\n * In browser, these return no-op implementations.\n */\n\n/**\n * Test span structure (stub)\n */\nexport interface TestSpan {\n name: string;\n attributes: Record<string, unknown>;\n status: { code: number; message?: string };\n events: Array<{ name: string; attributes?: Record<string, unknown> }>;\n duration: number;\n}\n\n/**\n * Test collector structure (stub)\n */\nexport interface TestCollector {\n getSpans(): TestSpan[];\n getSpansByName(name: string): TestSpan[];\n clear(): void;\n waitForSpans(count: number, timeout?: number): Promise<TestSpan[]>;\n}\n\n/**\n * Browser stub: Returns empty collector\n */\nexport function createTestCollector(): TestCollector {\n return {\n getSpans: () => [],\n getSpansByName: () => [],\n clear: () => {},\n waitForSpans: async () => [],\n };\n}\n\n/**\n * Browser stub: No-op\n */\nexport function assertSpanCreated(\n collector: TestCollector,\n name: string,\n): void {\n void collector;\n void name;\n // No-op in browser\n}\n\n/**\n * Browser stub: No-op\n */\nexport function assertSpanHasAttribute(\n collector: TestCollector,\n name: string,\n key: string,\n value?: unknown,\n): void {\n void collector;\n void name;\n void key;\n void value;\n // No-op in browser\n}\n\n/**\n * Serialized span type (browser stub - mirrors server SerializedSpan).\n *\n * Defined as a `type` (not `interface`) so it is assignable to\n * `Record<string, unknown>` in TypeScript 6+ strict mode.\n */\nexport type SerializedSpan = {\n name: string;\n spanId: string;\n traceId: string;\n parentSpanId?: string;\n attributes?: Record<string, unknown>;\n status: { code: number; message?: string };\n durationMs: number;\n};\n\n/**\n * Accepts either a raw `Request` (legacy) or a TanStack Router context\n * object containing `{ request: Request }` (Router 1.168+).\n */\ntype HandlerInput = Request | { request: Request };\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype CreateFileRoute = (path: string) => (options: any) => any;\n\n/**\n * Browser stub: createTestSpansRoute is server-only.\n */\nexport function createTestSpansRoute(\n createFileRoute: CreateFileRoute,\n path?: string,\n): unknown {\n void createFileRoute;\n void path;\n throw new Error('createTestSpansRoute is server-only');\n}\n\n/**\n * Browser stub: test-spans handlers are server-only.\n * Returns no-op handlers that always return 404.\n */\nexport function createTestSpansHandlers(): {\n GET: (input: HandlerInput) => Response;\n DELETE: (input: HandlerInput) => Response;\n} {\n return {\n GET(input: HandlerInput): Response {\n void input;\n return Response.json(\n { error: 'createTestSpansHandlers is server-only' },\n { status: 404 },\n );\n },\n DELETE(input: HandlerInput): Response {\n void input;\n return Response.json(\n { error: 'createTestSpansHandlers is server-only' },\n { status: 404 },\n );\n },\n };\n}\n"]}
package/dist/testing.d.ts CHANGED
@@ -127,8 +127,11 @@ declare function generateTraceparent(traceId?: string, spanId?: string): string;
127
127
  /**
128
128
  * Serialized span shape returned by the test-spans HTTP endpoint.
129
129
  * Mirrors the fields the Playwright side needs for assertions.
130
+ *
131
+ * Defined as a `type` (not `interface`) so it is assignable to
132
+ * `Record<string, unknown>` in TypeScript 6+ strict mode.
130
133
  */
131
- interface SerializedSpan {
134
+ type SerializedSpan = {
132
135
  name: string;
133
136
  spanId: string;
134
137
  traceId: string;
@@ -139,13 +142,25 @@ interface SerializedSpan {
139
142
  message?: string;
140
143
  };
141
144
  durationMs: number;
142
- }
145
+ };
146
+ /**
147
+ * Accepts either a raw `Request` (legacy) or a TanStack Router context
148
+ * object containing `{ request: Request }` (Router 1.168+).
149
+ */
150
+ type HandlerInput = Request | {
151
+ request: Request;
152
+ };
153
+ type CreateFileRoute = (path: string) => (options: any) => any;
143
154
  /**
144
155
  * Creates GET and DELETE handlers for a test-spans HTTP endpoint.
145
156
  *
146
157
  * Use in a TanStack Start route to expose in-memory spans for Playwright assertions.
147
158
  * Only works when E2E=1 (set in webServer command).
148
159
  *
160
+ * Handlers accept either a raw `Request` or a TanStack Router context
161
+ * object `{ request: Request }`, so they work with both Router < 1.168
162
+ * and Router >= 1.168.
163
+ *
149
164
  * @example
150
165
  * ```typescript
151
166
  * // src/routes/api/test-spans.ts
@@ -157,9 +172,28 @@ interface SerializedSpan {
157
172
  * });
158
173
  * ```
159
174
  */
175
+ /**
176
+ * Creates a pre-built TanStack Start route for the test-spans endpoint.
177
+ *
178
+ * Reduces E2E boilerplate to three lines. The handlers accept both
179
+ * `Request` and `{ request: Request }` so they work with any Router version.
180
+ *
181
+ * @param createFileRoute - Pass `createFileRoute` from `@tanstack/react-router`
182
+ * @param path - Route path (default: `/api/test-spans`)
183
+ *
184
+ * @example
185
+ * ```typescript
186
+ * // src/routes/api/test-spans.ts
187
+ * import { createFileRoute } from "@tanstack/react-router";
188
+ * import { createTestSpansRoute } from "autotel-tanstack/testing";
189
+ *
190
+ * export const Route = createTestSpansRoute(createFileRoute);
191
+ * ```
192
+ */
193
+ declare function createTestSpansRoute(createFileRoute: CreateFileRoute, path?: string): any;
160
194
  declare function createTestSpansHandlers(): {
161
- GET: (request: Request) => Response;
162
- DELETE: (request: Request) => Response;
195
+ GET: (input: HandlerInput) => Response;
196
+ DELETE: (input: HandlerInput) => Response;
163
197
  };
164
198
 
165
- export { type SerializedSpan, type TestHarness, createMockRequest, createTestHarness, createTestSpansHandlers, generateTraceparent };
199
+ export { type SerializedSpan, type TestHarness, createMockRequest, createTestHarness, createTestSpansHandlers, createTestSpansRoute, generateTraceparent };
package/dist/testing.js CHANGED
@@ -125,9 +125,17 @@ function exporterGuard() {
125
125
  }
126
126
  return null;
127
127
  }
128
+ function createTestSpansRoute(createFileRoute, path = "/api/test-spans") {
129
+ const { GET, DELETE } = createTestSpansHandlers();
130
+ return createFileRoute(path)({
131
+ server: {
132
+ handlers: { GET, DELETE }
133
+ }
134
+ });
135
+ }
128
136
  function createTestSpansHandlers() {
129
137
  return {
130
- GET(_request) {
138
+ GET(_input) {
131
139
  const guard = e2eGuard() ?? exporterGuard();
132
140
  if (guard) return guard;
133
141
  const spans = getExporter().getFinishedSpans().map((span) => {
@@ -147,7 +155,7 @@ function createTestSpansHandlers() {
147
155
  });
148
156
  return Response.json({ spans });
149
157
  },
150
- DELETE(_request) {
158
+ DELETE(_input) {
151
159
  const guard = e2eGuard() ?? exporterGuard();
152
160
  if (guard) return guard;
153
161
  getExporter().reset();
@@ -156,6 +164,6 @@ function createTestSpansHandlers() {
156
164
  };
157
165
  }
158
166
 
159
- export { createMockRequest, createTestHarness, createTestSpansHandlers, generateTraceparent };
167
+ export { createMockRequest, createTestHarness, createTestSpansHandlers, createTestSpansRoute, generateTraceparent };
160
168
  //# sourceMappingURL=testing.js.map
161
169
  //# sourceMappingURL=testing.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/testing.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["import { type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { InMemorySpanExporter } from 'autotel/exporters';\nimport { SimpleSpanProcessor } from 'autotel/processors';\nimport { init } from 'autotel';\n\n/**\n * Test harness for TanStack instrumentation testing\n *\n * Provides utilities for testing TanStack Start applications\n * with autotel-tanstack instrumentation.\n */\nexport interface TestHarness {\n /**\n * The in-memory span exporter\n */\n exporter: {\n getFinishedSpans(): ReadableSpan[];\n reset(): void;\n };\n\n /**\n * Get all finished spans\n */\n getSpans(): ReadableSpan[];\n\n /**\n * Get spans by name (exact match or regex)\n */\n getSpansByName(name: string | RegExp): ReadableSpan[];\n\n /**\n * Get spans by TanStack type\n */\n getSpansByType(\n type: 'request' | 'serverFn' | 'loader' | 'beforeLoad',\n ): ReadableSpan[];\n\n /**\n * Reset collected spans\n */\n reset(): void;\n\n /**\n * Assert a span exists\n */\n assertSpanExists(name: string | RegExp): void;\n\n /**\n * Assert a span has a specific attribute\n */\n assertSpanHasAttribute(\n name: string | RegExp,\n attr: string,\n value?: unknown,\n ): void;\n\n /**\n * Assert a server function was traced\n */\n assertServerFnTraced(name: string): void;\n\n /**\n * Assert a loader was traced\n */\n assertLoaderTraced(routeId: string): void;\n\n /**\n * Assert a beforeLoad was traced\n */\n assertBeforeLoadTraced(routeId: string): void;\n\n /**\n * Assert an HTTP request was traced\n */\n assertRequestTraced(method: string, path: string): void;\n}\n\n/**\n * Create a test harness for TanStack instrumentation testing\n *\n * This sets up autotel with an in-memory exporter for testing.\n * Call this in your test setup to capture and assert on spans.\n *\n * @returns Test harness with assertion helpers\n *\n * @example\n * ```typescript\n * import { describe, it, beforeEach } from 'vitest';\n * import { createTestHarness } from 'autotel-tanstack/testing';\n *\n * describe('MyServerFunction', () => {\n * let harness: ReturnType<typeof createTestHarness>;\n *\n * beforeEach(() => {\n * harness = createTestHarness();\n * });\n *\n * afterEach(() => {\n * harness.reset();\n * });\n *\n * it('should trace the server function', async () => {\n * await myServerFunction({ id: '123' });\n *\n * harness.assertServerFnTraced('myServerFunction');\n * harness.assertSpanHasAttribute(\n * /tanstack\\.serverFn/,\n * 'tanstack.server_function.name',\n * 'myServerFunction'\n * );\n * });\n * });\n * ```\n */\nexport function createTestHarness(): TestHarness {\n const exporter = new InMemorySpanExporter();\n\n init({\n service: 'test',\n spanProcessors: [new SimpleSpanProcessor(exporter)],\n });\n\n function getSpans(): ReadableSpan[] {\n return exporter.getFinishedSpans() as ReadableSpan[];\n }\n\n function getSpansByName(name: string | RegExp): ReadableSpan[] {\n const spans = getSpans();\n if (typeof name === 'string') {\n return spans.filter((s) => s.name === name);\n }\n return spans.filter((s) => name.test(s.name));\n }\n\n function getSpansByType(\n type: 'request' | 'serverFn' | 'loader' | 'beforeLoad',\n ): ReadableSpan[] {\n return getSpans().filter((s) => s.attributes['tanstack.type'] === type);\n }\n\n function reset(): void {\n exporter.reset();\n }\n\n function assertSpanExists(name: string | RegExp): void {\n const spans = getSpansByName(name);\n if (spans.length === 0) {\n const allSpanNames = getSpans().map((s) => s.name);\n throw new Error(\n `Expected span \"${name}\" to exist. Found spans: ${JSON.stringify(allSpanNames)}`,\n );\n }\n }\n\n function assertSpanHasAttribute(\n name: string | RegExp,\n attr: string,\n value?: unknown,\n ): void {\n const spans = getSpansByName(name);\n if (spans.length === 0) {\n throw new Error(`Span \"${name}\" not found`);\n }\n\n const span = spans[0];\n const attrValue = span.attributes[attr];\n\n if (attrValue === undefined) {\n throw new Error(\n `Attribute \"${attr}\" not found on span \"${span.name}\". ` +\n `Available attributes: ${JSON.stringify(Object.keys(span.attributes))}`,\n );\n }\n\n if (value !== undefined && attrValue !== value) {\n throw new Error(\n `Expected attribute \"${attr}\" to be \"${value}\", got \"${attrValue}\"`,\n );\n }\n }\n\n function assertServerFnTraced(name: string): void {\n assertSpanExists(`tanstack.serverFn.${name}`);\n }\n\n function assertLoaderTraced(routeId: string): void {\n assertSpanExists(`tanstack.loader.${routeId}`);\n }\n\n function assertBeforeLoadTraced(routeId: string): void {\n assertSpanExists(`tanstack.beforeLoad.${routeId}`);\n }\n\n function assertRequestTraced(method: string, path: string): void {\n assertSpanExists(`${method} ${path}`);\n }\n\n return {\n exporter,\n getSpans,\n getSpansByName,\n getSpansByType,\n reset,\n assertSpanExists,\n assertSpanHasAttribute,\n assertServerFnTraced,\n assertLoaderTraced,\n assertBeforeLoadTraced,\n assertRequestTraced,\n };\n}\n\n/**\n * Mock request factory for testing\n *\n * Creates mock Request objects for testing middleware and handlers.\n *\n * @example\n * ```typescript\n * const request = createMockRequest('GET', '/api/users', {\n * headers: { 'x-request-id': 'test-123' },\n * });\n * ```\n */\nexport function createMockRequest(\n method: string,\n path: string,\n options: {\n headers?: Record<string, string>;\n body?: string;\n traceparent?: string;\n } = {},\n): Request {\n const headers = new Headers(options.headers);\n\n if (options.traceparent) {\n headers.set('traceparent', options.traceparent);\n }\n\n return new Request(`http://localhost${path}`, {\n method,\n headers,\n body: options.body,\n });\n}\n\n/**\n * Generate a valid W3C traceparent header for testing\n *\n * @param traceId - Optional 32-char hex trace ID\n * @param spanId - Optional 16-char hex span ID\n * @returns Valid traceparent header string\n *\n * @example\n * ```typescript\n * const traceparent = generateTraceparent();\n * const request = createMockRequest('GET', '/api/users', { traceparent });\n * ```\n */\nexport function generateTraceparent(traceId?: string, spanId?: string): string {\n const version = '00';\n const trace = traceId || generateHex(32);\n const span = spanId || generateHex(16);\n const flags = '01'; // Sampled\n\n return `${version}-${trace}-${span}-${flags}`;\n}\n\nfunction generateHex(length: number): string {\n const chars = '0123456789abcdef';\n let result = '';\n for (let i = 0; i < length; i++) {\n result += chars[Math.floor(Math.random() * 16)];\n }\n return result;\n}\n\n/**\n * Serialized span shape returned by the test-spans HTTP endpoint.\n * Mirrors the fields the Playwright side needs for assertions.\n */\nexport interface SerializedSpan {\n name: string;\n spanId: string;\n traceId: string;\n parentSpanId?: string;\n attributes?: Record<string, unknown>;\n status: { code: number; message?: string };\n durationMs: number;\n}\n\ninterface TestSpanExporter {\n getFinishedSpans(): Array<{\n name: string;\n spanContext(): { spanId: string; traceId: string };\n parentSpanContext?: { spanId: string };\n attributes: Record<string, unknown>;\n status: { code: number; message?: string };\n duration: [number, number];\n }>;\n reset(): void;\n}\n\nfunction getExporter(): TestSpanExporter | undefined {\n return (globalThis as Record<string, unknown>).__testSpanExporter as\n | TestSpanExporter\n | undefined;\n}\n\nfunction e2eGuard(): Response | null {\n if (process.env.E2E !== '1') {\n return Response.json(\n { error: 'test-spans endpoint only available in E2E mode' },\n { status: 404 },\n );\n }\n return null;\n}\n\nfunction exporterGuard(): Response | null {\n if (!getExporter()) {\n return Response.json(\n { error: 'in-memory span exporter not initialized' },\n { status: 500 },\n );\n }\n return null;\n}\n\n/**\n * Creates GET and DELETE handlers for a test-spans HTTP endpoint.\n *\n * Use in a TanStack Start route to expose in-memory spans for Playwright assertions.\n * Only works when E2E=1 (set in webServer command).\n *\n * @example\n * ```typescript\n * // src/routes/api/test-spans.ts\n * import { createFileRoute } from \"@tanstack/react-router\";\n * import { createTestSpansHandlers } from 'autotel-tanstack/testing';\n * const { GET, DELETE } = createTestSpansHandlers();\n * export const Route = createFileRoute('/api/test-spans')({\n * server: { handlers: { GET, DELETE } },\n * });\n * ```\n */\nexport function createTestSpansHandlers(): {\n GET: (request: Request) => Response;\n DELETE: (request: Request) => Response;\n} {\n return {\n GET(_request: Request): Response {\n const guard = e2eGuard() ?? exporterGuard();\n if (guard) return guard;\n\n const spans: SerializedSpan[] = getExporter()!\n .getFinishedSpans()\n .map((span) => {\n const { spanId, traceId } = span.spanContext();\n const serialized: SerializedSpan = {\n name: span.name,\n spanId,\n traceId,\n attributes: span.attributes,\n status: span.status,\n durationMs: span.duration[0] * 1000 + span.duration[1] / 1_000_000,\n };\n if (span.parentSpanContext?.spanId) {\n serialized.parentSpanId = span.parentSpanContext.spanId;\n }\n return serialized;\n });\n\n return Response.json({ spans });\n },\n\n DELETE(_request: Request): Response {\n const guard = e2eGuard() ?? exporterGuard();\n if (guard) return guard;\n getExporter()!.reset();\n return Response.json({ ok: true });\n },\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/testing.ts"],"names":[],"mappings":"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","file":"testing.js","sourcesContent":["import { type ReadableSpan } from '@opentelemetry/sdk-trace-base';\nimport { InMemorySpanExporter } from 'autotel/exporters';\nimport { SimpleSpanProcessor } from 'autotel/processors';\nimport { init } from 'autotel';\n\n/**\n * Test harness for TanStack instrumentation testing\n *\n * Provides utilities for testing TanStack Start applications\n * with autotel-tanstack instrumentation.\n */\nexport interface TestHarness {\n /**\n * The in-memory span exporter\n */\n exporter: {\n getFinishedSpans(): ReadableSpan[];\n reset(): void;\n };\n\n /**\n * Get all finished spans\n */\n getSpans(): ReadableSpan[];\n\n /**\n * Get spans by name (exact match or regex)\n */\n getSpansByName(name: string | RegExp): ReadableSpan[];\n\n /**\n * Get spans by TanStack type\n */\n getSpansByType(\n type: 'request' | 'serverFn' | 'loader' | 'beforeLoad',\n ): ReadableSpan[];\n\n /**\n * Reset collected spans\n */\n reset(): void;\n\n /**\n * Assert a span exists\n */\n assertSpanExists(name: string | RegExp): void;\n\n /**\n * Assert a span has a specific attribute\n */\n assertSpanHasAttribute(\n name: string | RegExp,\n attr: string,\n value?: unknown,\n ): void;\n\n /**\n * Assert a server function was traced\n */\n assertServerFnTraced(name: string): void;\n\n /**\n * Assert a loader was traced\n */\n assertLoaderTraced(routeId: string): void;\n\n /**\n * Assert a beforeLoad was traced\n */\n assertBeforeLoadTraced(routeId: string): void;\n\n /**\n * Assert an HTTP request was traced\n */\n assertRequestTraced(method: string, path: string): void;\n}\n\n/**\n * Create a test harness for TanStack instrumentation testing\n *\n * This sets up autotel with an in-memory exporter for testing.\n * Call this in your test setup to capture and assert on spans.\n *\n * @returns Test harness with assertion helpers\n *\n * @example\n * ```typescript\n * import { describe, it, beforeEach } from 'vitest';\n * import { createTestHarness } from 'autotel-tanstack/testing';\n *\n * describe('MyServerFunction', () => {\n * let harness: ReturnType<typeof createTestHarness>;\n *\n * beforeEach(() => {\n * harness = createTestHarness();\n * });\n *\n * afterEach(() => {\n * harness.reset();\n * });\n *\n * it('should trace the server function', async () => {\n * await myServerFunction({ id: '123' });\n *\n * harness.assertServerFnTraced('myServerFunction');\n * harness.assertSpanHasAttribute(\n * /tanstack\\.serverFn/,\n * 'tanstack.server_function.name',\n * 'myServerFunction'\n * );\n * });\n * });\n * ```\n */\nexport function createTestHarness(): TestHarness {\n const exporter = new InMemorySpanExporter();\n\n init({\n service: 'test',\n spanProcessors: [new SimpleSpanProcessor(exporter)],\n });\n\n function getSpans(): ReadableSpan[] {\n return exporter.getFinishedSpans() as ReadableSpan[];\n }\n\n function getSpansByName(name: string | RegExp): ReadableSpan[] {\n const spans = getSpans();\n if (typeof name === 'string') {\n return spans.filter((s) => s.name === name);\n }\n return spans.filter((s) => name.test(s.name));\n }\n\n function getSpansByType(\n type: 'request' | 'serverFn' | 'loader' | 'beforeLoad',\n ): ReadableSpan[] {\n return getSpans().filter((s) => s.attributes['tanstack.type'] === type);\n }\n\n function reset(): void {\n exporter.reset();\n }\n\n function assertSpanExists(name: string | RegExp): void {\n const spans = getSpansByName(name);\n if (spans.length === 0) {\n const allSpanNames = getSpans().map((s) => s.name);\n throw new Error(\n `Expected span \"${name}\" to exist. Found spans: ${JSON.stringify(allSpanNames)}`,\n );\n }\n }\n\n function assertSpanHasAttribute(\n name: string | RegExp,\n attr: string,\n value?: unknown,\n ): void {\n const spans = getSpansByName(name);\n if (spans.length === 0) {\n throw new Error(`Span \"${name}\" not found`);\n }\n\n const span = spans[0];\n const attrValue = span.attributes[attr];\n\n if (attrValue === undefined) {\n throw new Error(\n `Attribute \"${attr}\" not found on span \"${span.name}\". ` +\n `Available attributes: ${JSON.stringify(Object.keys(span.attributes))}`,\n );\n }\n\n if (value !== undefined && attrValue !== value) {\n throw new Error(\n `Expected attribute \"${attr}\" to be \"${value}\", got \"${attrValue}\"`,\n );\n }\n }\n\n function assertServerFnTraced(name: string): void {\n assertSpanExists(`tanstack.serverFn.${name}`);\n }\n\n function assertLoaderTraced(routeId: string): void {\n assertSpanExists(`tanstack.loader.${routeId}`);\n }\n\n function assertBeforeLoadTraced(routeId: string): void {\n assertSpanExists(`tanstack.beforeLoad.${routeId}`);\n }\n\n function assertRequestTraced(method: string, path: string): void {\n assertSpanExists(`${method} ${path}`);\n }\n\n return {\n exporter,\n getSpans,\n getSpansByName,\n getSpansByType,\n reset,\n assertSpanExists,\n assertSpanHasAttribute,\n assertServerFnTraced,\n assertLoaderTraced,\n assertBeforeLoadTraced,\n assertRequestTraced,\n };\n}\n\n/**\n * Mock request factory for testing\n *\n * Creates mock Request objects for testing middleware and handlers.\n *\n * @example\n * ```typescript\n * const request = createMockRequest('GET', '/api/users', {\n * headers: { 'x-request-id': 'test-123' },\n * });\n * ```\n */\nexport function createMockRequest(\n method: string,\n path: string,\n options: {\n headers?: Record<string, string>;\n body?: string;\n traceparent?: string;\n } = {},\n): Request {\n const headers = new Headers(options.headers);\n\n if (options.traceparent) {\n headers.set('traceparent', options.traceparent);\n }\n\n return new Request(`http://localhost${path}`, {\n method,\n headers,\n body: options.body,\n });\n}\n\n/**\n * Generate a valid W3C traceparent header for testing\n *\n * @param traceId - Optional 32-char hex trace ID\n * @param spanId - Optional 16-char hex span ID\n * @returns Valid traceparent header string\n *\n * @example\n * ```typescript\n * const traceparent = generateTraceparent();\n * const request = createMockRequest('GET', '/api/users', { traceparent });\n * ```\n */\nexport function generateTraceparent(traceId?: string, spanId?: string): string {\n const version = '00';\n const trace = traceId || generateHex(32);\n const span = spanId || generateHex(16);\n const flags = '01'; // Sampled\n\n return `${version}-${trace}-${span}-${flags}`;\n}\n\nfunction generateHex(length: number): string {\n const chars = '0123456789abcdef';\n let result = '';\n for (let i = 0; i < length; i++) {\n result += chars[Math.floor(Math.random() * 16)];\n }\n return result;\n}\n\n/**\n * Serialized span shape returned by the test-spans HTTP endpoint.\n * Mirrors the fields the Playwright side needs for assertions.\n *\n * Defined as a `type` (not `interface`) so it is assignable to\n * `Record<string, unknown>` in TypeScript 6+ strict mode.\n */\nexport type SerializedSpan = {\n name: string;\n spanId: string;\n traceId: string;\n parentSpanId?: string;\n attributes?: Record<string, unknown>;\n status: { code: number; message?: string };\n durationMs: number;\n};\n\ninterface TestSpanExporter {\n getFinishedSpans(): Array<{\n name: string;\n spanContext(): { spanId: string; traceId: string };\n parentSpanContext?: { spanId: string };\n attributes: Record<string, unknown>;\n status: { code: number; message?: string };\n duration: [number, number];\n }>;\n reset(): void;\n}\n\nfunction getExporter(): TestSpanExporter | undefined {\n return (globalThis as Record<string, unknown>).__testSpanExporter as\n | TestSpanExporter\n | undefined;\n}\n\nfunction e2eGuard(): Response | null {\n if (process.env.E2E !== '1') {\n return Response.json(\n { error: 'test-spans endpoint only available in E2E mode' },\n { status: 404 },\n );\n }\n return null;\n}\n\nfunction exporterGuard(): Response | null {\n if (!getExporter()) {\n return Response.json(\n { error: 'in-memory span exporter not initialized' },\n { status: 500 },\n );\n }\n return null;\n}\n\n/**\n * Accepts either a raw `Request` (legacy) or a TanStack Router context\n * object containing `{ request: Request }` (Router 1.168+).\n */\ntype HandlerInput = Request | { request: Request };\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\ntype CreateFileRoute = (path: string) => (options: any) => any;\n\n/**\n * Creates GET and DELETE handlers for a test-spans HTTP endpoint.\n *\n * Use in a TanStack Start route to expose in-memory spans for Playwright assertions.\n * Only works when E2E=1 (set in webServer command).\n *\n * Handlers accept either a raw `Request` or a TanStack Router context\n * object `{ request: Request }`, so they work with both Router < 1.168\n * and Router >= 1.168.\n *\n * @example\n * ```typescript\n * // src/routes/api/test-spans.ts\n * import { createFileRoute } from \"@tanstack/react-router\";\n * import { createTestSpansHandlers } from 'autotel-tanstack/testing';\n * const { GET, DELETE } = createTestSpansHandlers();\n * export const Route = createFileRoute('/api/test-spans')({\n * server: { handlers: { GET, DELETE } },\n * });\n * ```\n */\n/**\n * Creates a pre-built TanStack Start route for the test-spans endpoint.\n *\n * Reduces E2E boilerplate to three lines. The handlers accept both\n * `Request` and `{ request: Request }` so they work with any Router version.\n *\n * @param createFileRoute - Pass `createFileRoute` from `@tanstack/react-router`\n * @param path - Route path (default: `/api/test-spans`)\n *\n * @example\n * ```typescript\n * // src/routes/api/test-spans.ts\n * import { createFileRoute } from \"@tanstack/react-router\";\n * import { createTestSpansRoute } from \"autotel-tanstack/testing\";\n *\n * export const Route = createTestSpansRoute(createFileRoute);\n * ```\n */\nexport function createTestSpansRoute(\n createFileRoute: CreateFileRoute,\n path = '/api/test-spans',\n) {\n const { GET, DELETE } = createTestSpansHandlers();\n return createFileRoute(path)({\n server: {\n handlers: { GET, DELETE },\n },\n });\n}\n\nexport function createTestSpansHandlers(): {\n GET: (input: HandlerInput) => Response;\n DELETE: (input: HandlerInput) => Response;\n} {\n return {\n GET(_input: HandlerInput): Response {\n const guard = e2eGuard() ?? exporterGuard();\n if (guard) return guard;\n\n const spans: SerializedSpan[] = getExporter()!\n .getFinishedSpans()\n .map((span) => {\n const { spanId, traceId } = span.spanContext();\n const serialized: SerializedSpan = {\n name: span.name,\n spanId,\n traceId,\n attributes: span.attributes,\n status: span.status,\n durationMs: span.duration[0] * 1000 + span.duration[1] / 1_000_000,\n };\n if (span.parentSpanContext?.spanId) {\n serialized.parentSpanId = span.parentSpanContext.spanId;\n }\n return serialized;\n });\n\n return Response.json({ spans });\n },\n\n DELETE(_input: HandlerInput): Response {\n const guard = e2eGuard() ?? exporterGuard();\n if (guard) return guard;\n getExporter()!.reset();\n return Response.json({ ok: true });\n },\n };\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-tanstack",
3
- "version": "1.13.5",
3
+ "version": "1.13.7",
4
4
  "description": "OpenTelemetry instrumentation for TanStack Start - automatic tracing for server functions, middleware, and route loaders",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -121,14 +121,14 @@
121
121
  "author": "Jag Reehal <jag@jagreehal.com> (https://jagreehal.com)",
122
122
  "license": "MIT",
123
123
  "dependencies": {
124
- "@opentelemetry/api": "^1.9.0",
125
- "@tanstack/intent": "^0.0.23",
126
- "autotel": "2.25.4",
127
- "autotel-adapters": "0.2.4"
124
+ "@opentelemetry/api": "^1.9.1",
125
+ "@tanstack/intent": "^0.0.29",
126
+ "autotel": "2.26.0",
127
+ "autotel-adapters": "0.2.6"
128
128
  },
129
129
  "peerDependencies": {
130
- "@tanstack/react-start": "^1.167.3",
131
- "@tanstack/solid-start": "^1.167.3"
130
+ "@tanstack/react-start": "^1.167.16",
131
+ "@tanstack/solid-start": "^1.167.15"
132
132
  },
133
133
  "peerDependenciesMeta": {
134
134
  "@tanstack/react-start": {
@@ -139,14 +139,14 @@
139
139
  }
140
140
  },
141
141
  "devDependencies": {
142
- "@opentelemetry/sdk-trace-base": "^2.6.0",
143
- "@tanstack/react-router": "^1.168.2",
144
- "@types/node": "^25.5.0",
142
+ "@opentelemetry/sdk-trace-base": "^2.6.1",
143
+ "@tanstack/react-router": "^1.168.10",
144
+ "@types/node": "^25.5.2",
145
145
  "rimraf": "^6.1.3",
146
146
  "tsup": "^8.5.1",
147
- "typescript": "^5.9.3",
148
- "vitest": "^4.1.0",
149
- "vitest-mock-extended": "^3.1.0"
147
+ "typescript": "^6.0.2",
148
+ "vitest": "^4.1.3",
149
+ "vitest-mock-extended": "^4.0.0"
150
150
  },
151
151
  "repository": {
152
152
  "type": "git",
@@ -67,9 +67,12 @@ export function assertSpanHasAttribute(
67
67
  }
68
68
 
69
69
  /**
70
- * Serialized span interface (browser stub - mirrors server SerializedSpan).
70
+ * Serialized span type (browser stub - mirrors server SerializedSpan).
71
+ *
72
+ * Defined as a `type` (not `interface`) so it is assignable to
73
+ * `Record<string, unknown>` in TypeScript 6+ strict mode.
71
74
  */
72
- export interface SerializedSpan {
75
+ export type SerializedSpan = {
73
76
  name: string;
74
77
  spanId: string;
75
78
  traceId: string;
@@ -77,6 +80,27 @@ export interface SerializedSpan {
77
80
  attributes?: Record<string, unknown>;
78
81
  status: { code: number; message?: string };
79
82
  durationMs: number;
83
+ };
84
+
85
+ /**
86
+ * Accepts either a raw `Request` (legacy) or a TanStack Router context
87
+ * object containing `{ request: Request }` (Router 1.168+).
88
+ */
89
+ type HandlerInput = Request | { request: Request };
90
+
91
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
92
+ type CreateFileRoute = (path: string) => (options: any) => any;
93
+
94
+ /**
95
+ * Browser stub: createTestSpansRoute is server-only.
96
+ */
97
+ export function createTestSpansRoute(
98
+ createFileRoute: CreateFileRoute,
99
+ path?: string,
100
+ ): unknown {
101
+ void createFileRoute;
102
+ void path;
103
+ throw new Error('createTestSpansRoute is server-only');
80
104
  }
81
105
 
82
106
  /**
@@ -84,21 +108,19 @@ export interface SerializedSpan {
84
108
  * Returns no-op handlers that always return 404.
85
109
  */
86
110
  export function createTestSpansHandlers(): {
87
- GET: (request: Request) => Response;
88
- DELETE: (request: Request) => Response;
111
+ GET: (input: HandlerInput) => Response;
112
+ DELETE: (input: HandlerInput) => Response;
89
113
  } {
90
114
  return {
91
- GET(_request: Request): Response {
92
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
93
- void _request;
115
+ GET(input: HandlerInput): Response {
116
+ void input;
94
117
  return Response.json(
95
118
  { error: 'createTestSpansHandlers is server-only' },
96
119
  { status: 404 },
97
120
  );
98
121
  },
99
- DELETE(_request: Request): Response {
100
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
101
- void _request;
122
+ DELETE(input: HandlerInput): Response {
123
+ void input;
102
124
  return Response.json(
103
125
  { error: 'createTestSpansHandlers is server-only' },
104
126
  { status: 404 },
@@ -87,12 +87,20 @@ describe('createTestSpansHandlers', () => {
87
87
  delete process.env.E2E;
88
88
  });
89
89
 
90
- it('GET returns 404 when not in E2E mode', async () => {
90
+ it('GET returns 404 when not in E2E mode (raw Request)', async () => {
91
91
  const { GET } = createTestSpansHandlers();
92
92
  const res = GET(new Request('http://localhost/api/test-spans'));
93
93
  expect(res.status).toBe(404);
94
94
  });
95
95
 
96
+ it('GET returns 404 when not in E2E mode (context object)', async () => {
97
+ const { GET } = createTestSpansHandlers();
98
+ const res = GET({
99
+ request: new Request('http://localhost/api/test-spans'),
100
+ });
101
+ expect(res.status).toBe(404);
102
+ });
103
+
96
104
  it('GET returns 500 when exporter not initialized', async () => {
97
105
  process.env.E2E = '1';
98
106
  const { GET } = createTestSpansHandlers();
@@ -150,7 +158,7 @@ describe('createTestSpansHandlers', () => {
150
158
  expect(body.spans[0].parentSpanId).toBeUndefined();
151
159
  });
152
160
 
153
- it('DELETE returns 404 when not in E2E mode', async () => {
161
+ it('DELETE returns 404 when not in E2E mode (raw Request)', async () => {
154
162
  const { DELETE } = createTestSpansHandlers();
155
163
  const res = DELETE(
156
164
  new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
@@ -158,6 +166,16 @@ describe('createTestSpansHandlers', () => {
158
166
  expect(res.status).toBe(404);
159
167
  });
160
168
 
169
+ it('DELETE returns 404 when not in E2E mode (context object)', async () => {
170
+ const { DELETE } = createTestSpansHandlers();
171
+ const res = DELETE({
172
+ request: new Request('http://localhost/api/test-spans', {
173
+ method: 'DELETE',
174
+ }),
175
+ });
176
+ expect(res.status).toBe(404);
177
+ });
178
+
161
179
  it('DELETE returns 500 when exporter not initialized', async () => {
162
180
  process.env.E2E = '1';
163
181
  const { DELETE } = createTestSpansHandlers();
package/src/testing.ts CHANGED
@@ -278,8 +278,11 @@ function generateHex(length: number): string {
278
278
  /**
279
279
  * Serialized span shape returned by the test-spans HTTP endpoint.
280
280
  * Mirrors the fields the Playwright side needs for assertions.
281
+ *
282
+ * Defined as a `type` (not `interface`) so it is assignable to
283
+ * `Record<string, unknown>` in TypeScript 6+ strict mode.
281
284
  */
282
- export interface SerializedSpan {
285
+ export type SerializedSpan = {
283
286
  name: string;
284
287
  spanId: string;
285
288
  traceId: string;
@@ -287,7 +290,7 @@ export interface SerializedSpan {
287
290
  attributes?: Record<string, unknown>;
288
291
  status: { code: number; message?: string };
289
292
  durationMs: number;
290
- }
293
+ };
291
294
 
292
295
  interface TestSpanExporter {
293
296
  getFinishedSpans(): Array<{
@@ -327,12 +330,25 @@ function exporterGuard(): Response | null {
327
330
  return null;
328
331
  }
329
332
 
333
+ /**
334
+ * Accepts either a raw `Request` (legacy) or a TanStack Router context
335
+ * object containing `{ request: Request }` (Router 1.168+).
336
+ */
337
+ type HandlerInput = Request | { request: Request };
338
+
339
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
340
+ type CreateFileRoute = (path: string) => (options: any) => any;
341
+
330
342
  /**
331
343
  * Creates GET and DELETE handlers for a test-spans HTTP endpoint.
332
344
  *
333
345
  * Use in a TanStack Start route to expose in-memory spans for Playwright assertions.
334
346
  * Only works when E2E=1 (set in webServer command).
335
347
  *
348
+ * Handlers accept either a raw `Request` or a TanStack Router context
349
+ * object `{ request: Request }`, so they work with both Router < 1.168
350
+ * and Router >= 1.168.
351
+ *
336
352
  * @example
337
353
  * ```typescript
338
354
  * // src/routes/api/test-spans.ts
@@ -344,12 +360,42 @@ function exporterGuard(): Response | null {
344
360
  * });
345
361
  * ```
346
362
  */
363
+ /**
364
+ * Creates a pre-built TanStack Start route for the test-spans endpoint.
365
+ *
366
+ * Reduces E2E boilerplate to three lines. The handlers accept both
367
+ * `Request` and `{ request: Request }` so they work with any Router version.
368
+ *
369
+ * @param createFileRoute - Pass `createFileRoute` from `@tanstack/react-router`
370
+ * @param path - Route path (default: `/api/test-spans`)
371
+ *
372
+ * @example
373
+ * ```typescript
374
+ * // src/routes/api/test-spans.ts
375
+ * import { createFileRoute } from "@tanstack/react-router";
376
+ * import { createTestSpansRoute } from "autotel-tanstack/testing";
377
+ *
378
+ * export const Route = createTestSpansRoute(createFileRoute);
379
+ * ```
380
+ */
381
+ export function createTestSpansRoute(
382
+ createFileRoute: CreateFileRoute,
383
+ path = '/api/test-spans',
384
+ ) {
385
+ const { GET, DELETE } = createTestSpansHandlers();
386
+ return createFileRoute(path)({
387
+ server: {
388
+ handlers: { GET, DELETE },
389
+ },
390
+ });
391
+ }
392
+
347
393
  export function createTestSpansHandlers(): {
348
- GET: (request: Request) => Response;
349
- DELETE: (request: Request) => Response;
394
+ GET: (input: HandlerInput) => Response;
395
+ DELETE: (input: HandlerInput) => Response;
350
396
  } {
351
397
  return {
352
- GET(_request: Request): Response {
398
+ GET(_input: HandlerInput): Response {
353
399
  const guard = e2eGuard() ?? exporterGuard();
354
400
  if (guard) return guard;
355
401
 
@@ -374,7 +420,7 @@ export function createTestSpansHandlers(): {
374
420
  return Response.json({ spans });
375
421
  },
376
422
 
377
- DELETE(_request: Request): Response {
423
+ DELETE(_input: HandlerInput): Response {
378
424
  const guard = e2eGuard() ?? exporterGuard();
379
425
  if (guard) return guard;
380
426
  getExporter()!.reset();
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/browser/testing.ts"],"names":[],"mappings":";AA+BO,SAAS,mBAAA,GAAqC;AACnD,EAAA,OAAO;AAAA,IACL,QAAA,EAAU,MAAM,EAAC;AAAA,IACjB,cAAA,EAAgB,MAAM,EAAC;AAAA,IACvB,OAAO,MAAM;AAAA,IAAC,CAAA;AAAA,IACd,YAAA,EAAc,YAAY;AAAC,GAC7B;AACF;AAKO,SAAS,iBAAA,CACd,WACA,IAAA,EACM;AAIR;AAKO,SAAS,sBAAA,CACd,SAAA,EACA,IAAA,EACA,GAAA,EACA,KAAA,EACM;AAMR;AAmBO,SAAS,uBAAA,GAGd;AACA,EAAA,OAAO;AAAA,IACL,IAAI,QAAA,EAA6B;AAG/B,MAAA,OAAO,QAAA,CAAS,IAAA;AAAA,QACd,EAAE,OAAO,wCAAA,EAAyC;AAAA,QAClD,EAAE,QAAQ,GAAA;AAAI,OAChB;AAAA,IACF,CAAA;AAAA,IACA,OAAO,QAAA,EAA6B;AAGlC,MAAA,OAAO,QAAA,CAAS,IAAA;AAAA,QACd,EAAE,OAAO,wCAAA,EAAyC;AAAA,QAClD,EAAE,QAAQ,GAAA;AAAI,OAChB;AAAA,IACF;AAAA,GACF;AACF","file":"chunk-ZJTHTWHJ.js","sourcesContent":["/**\n * Browser stub for testing module\n *\n * Testing utilities are server-side only.\n * In browser, these return no-op implementations.\n */\n\n/**\n * Test span structure (stub)\n */\nexport interface TestSpan {\n name: string;\n attributes: Record<string, unknown>;\n status: { code: number; message?: string };\n events: Array<{ name: string; attributes?: Record<string, unknown> }>;\n duration: number;\n}\n\n/**\n * Test collector structure (stub)\n */\nexport interface TestCollector {\n getSpans(): TestSpan[];\n getSpansByName(name: string): TestSpan[];\n clear(): void;\n waitForSpans(count: number, timeout?: number): Promise<TestSpan[]>;\n}\n\n/**\n * Browser stub: Returns empty collector\n */\nexport function createTestCollector(): TestCollector {\n return {\n getSpans: () => [],\n getSpansByName: () => [],\n clear: () => {},\n waitForSpans: async () => [],\n };\n}\n\n/**\n * Browser stub: No-op\n */\nexport function assertSpanCreated(\n collector: TestCollector,\n name: string,\n): void {\n void collector;\n void name;\n // No-op in browser\n}\n\n/**\n * Browser stub: No-op\n */\nexport function assertSpanHasAttribute(\n collector: TestCollector,\n name: string,\n key: string,\n value?: unknown,\n): void {\n void collector;\n void name;\n void key;\n void value;\n // No-op in browser\n}\n\n/**\n * Serialized span interface (browser stub - mirrors server SerializedSpan).\n */\nexport interface SerializedSpan {\n name: string;\n spanId: string;\n traceId: string;\n parentSpanId?: string;\n attributes?: Record<string, unknown>;\n status: { code: number; message?: string };\n durationMs: number;\n}\n\n/**\n * Browser stub: test-spans handlers are server-only.\n * Returns no-op handlers that always return 404.\n */\nexport function createTestSpansHandlers(): {\n GET: (request: Request) => Response;\n DELETE: (request: Request) => Response;\n} {\n return {\n GET(_request: Request): Response {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n void _request;\n return Response.json(\n { error: 'createTestSpansHandlers is server-only' },\n { status: 404 },\n );\n },\n DELETE(_request: Request): Response {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n void _request;\n return Response.json(\n { error: 'createTestSpansHandlers is server-only' },\n { status: 404 },\n );\n },\n };\n}\n"]}