autotel-tanstack 1.13.4 → 1.13.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/auto.js +19 -7
- package/dist/auto.js.map +1 -1
- package/dist/browser/index.js +1 -1
- package/dist/browser/testing.js +1 -1
- package/dist/chunk-ZJTHTWHJ.js +34 -0
- package/dist/chunk-ZJTHTWHJ.js.map +1 -0
- package/dist/testing.d.ts +38 -1
- package/dist/testing.js +52 -1
- package/dist/testing.js.map +1 -1
- package/package.json +4 -2
- package/src/auto.test.ts +107 -0
- package/src/auto.ts +28 -8
- package/src/browser/testing.ts +41 -0
- package/src/testing.test.ts +117 -2
- package/src/testing.ts +108 -0
- package/dist/chunk-Z3MJ3GZ6.js +0 -18
- package/dist/chunk-Z3MJ3GZ6.js.map +0 -1
package/dist/auto.js
CHANGED
|
@@ -6,6 +6,8 @@ import './chunk-I4LX3LOG.js';
|
|
|
6
6
|
import './chunk-NTY64BKS.js';
|
|
7
7
|
import './chunk-EGRHWZRV.js';
|
|
8
8
|
import { init } from 'autotel';
|
|
9
|
+
import { InMemorySpanExporter } from 'autotel/exporters';
|
|
10
|
+
import { SimpleSpanProcessor } from 'autotel/processors';
|
|
9
11
|
|
|
10
12
|
var service = process.env.OTEL_SERVICE_NAME || "tanstack-start";
|
|
11
13
|
var endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
@@ -28,16 +30,26 @@ function resolveDebug() {
|
|
|
28
30
|
if (!endpoint && process.env.NODE_ENV === "development") return "pretty";
|
|
29
31
|
return false;
|
|
30
32
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
33
|
+
if (process.env.E2E === "1") {
|
|
34
|
+
const e2eExporter = new InMemorySpanExporter();
|
|
35
|
+
const e2eProcessor = new SimpleSpanProcessor(e2eExporter);
|
|
36
|
+
globalThis.__testSpanExporter = e2eExporter;
|
|
37
|
+
init({
|
|
38
|
+
service,
|
|
39
|
+
spanProcessors: [e2eProcessor]
|
|
40
|
+
});
|
|
41
|
+
} else {
|
|
42
|
+
init({
|
|
43
|
+
service,
|
|
44
|
+
endpoint,
|
|
45
|
+
headers,
|
|
46
|
+
debug: resolveDebug()
|
|
47
|
+
});
|
|
48
|
+
}
|
|
37
49
|
if (process.env.NODE_ENV === "development" || process.env.AUTOTEL_DEBUG) {
|
|
38
50
|
console.log("[autotel-tanstack] Auto-initialized with:", {
|
|
39
51
|
service,
|
|
40
|
-
endpoint: endpoint || "(not configured)",
|
|
52
|
+
endpoint: process.env.E2E === "1" ? "(E2E: in-memory)" : endpoint || "(not configured)",
|
|
41
53
|
hasHeaders: !!headers
|
|
42
54
|
});
|
|
43
55
|
}
|
package/dist/auto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/auto.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../src/auto.ts"],"names":[],"mappings":";;;;;;;;;;;AA8BA,IAAM,OAAA,GAAU,OAAA,CAAQ,GAAA,CAAI,iBAAA,IAAqB,gBAAA;AAGjD,IAAM,QAAA,GAAW,QAAQ,GAAA,CAAI,2BAAA;AAG7B,IAAI,OAAA;AACJ,IAAI,OAAA,CAAQ,IAAI,0BAAA,EAA4B;AAC1C,EAAA,OAAA,GAAU,EAAC;AACX,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,GAAA,CAAI,0BAAA,CAA2B,MAAM,GAAG,CAAA;AAC9D,EAAA,KAAA,MAAW,QAAQ,KAAA,EAAO;AACxB,IAAA,MAAM,CAAC,GAAA,EAAK,KAAK,CAAA,GAAI,IAAA,CAAK,MAAM,GAAG,CAAA;AACnC,IAAA,IAAI,OAAO,KAAA,EAAO;AAChB,MAAA,OAAA,CAAQ,GAAA,CAAI,IAAA,EAAM,CAAA,GAAI,MAAM,IAAA,EAAK;AAAA,IACnC;AAAA,EACF;AACF;AAIA,SAAS,YAAA,GAAmC;AAC1C,EAAA,MAAM,GAAA,GAAM,QAAQ,GAAA,CAAI,aAAA;AACxB,EAAA,IAAI,GAAA,KAAQ,UAAU,OAAO,QAAA;AAC7B,EAAA,IAAI,GAAA,KAAQ,MAAA,IAAU,GAAA,KAAQ,GAAA,EAAK,OAAO,IAAA;AAC1C,EAAA,IAAI,GAAA,KAAQ,OAAA,IAAW,GAAA,KAAQ,GAAA,EAAK,OAAO,KAAA;AAC3C,EAAA,IAAI,CAAC,QAAA,IAAY,OAAA,CAAQ,GAAA,CAAI,QAAA,KAAa,eAAe,OAAO,QAAA;AAChE,EAAA,OAAO,KAAA;AACT;AAMA,IAAI,OAAA,CAAQ,GAAA,CAAI,GAAA,KAAQ,GAAA,EAAK;AAC3B,EAAA,MAAM,WAAA,GAAc,IAAI,oBAAA,EAAqB;AAC7C,EAAA,MAAM,YAAA,GAAe,IAAI,mBAAA,CAAoB,WAAW,CAAA;AACxD,EAAC,WAAuC,kBAAA,GAAqB,WAAA;AAE7D,EAAA,IAAA,CAAK;AAAA,IACH,OAAA;AAAA,IACA,cAAA,EAAgB,CAAC,YAAY;AAAA,GAC9B,CAAA;AACH,CAAA,MAAO;AAEL,EAAA,IAAA,CAAK;AAAA,IACH,OAAA;AAAA,IACA,QAAA;AAAA,IACA,OAAA;AAAA,IACA,OAAO,YAAA;AAAa,GACrB,CAAA;AACH;AAGA,IAAI,QAAQ,GAAA,CAAI,QAAA,KAAa,aAAA,IAAiB,OAAA,CAAQ,IAAI,aAAA,EAAe;AACvE,EAAA,OAAA,CAAQ,IAAI,2CAAA,EAA6C;AAAA,IACvD,OAAA;AAAA,IACA,UACE,OAAA,CAAQ,GAAA,CAAI,GAAA,KAAQ,GAAA,GAChB,qBACA,QAAA,IAAY,kBAAA;AAAA,IAClB,UAAA,EAAY,CAAC,CAAC;AAAA,GACf,CAAA;AACH;AAUO,SAAS,2BAAA,GAAuC;AACrD,EAAA,OAAO,IAAA;AACT;AAKO,SAAS,cAAA,GAAyB;AACvC,EAAA,OAAO,OAAA;AACT;AAKO,SAAS,WAAA,GAAkC;AAChD,EAAA,OAAO,QAAA;AACT","file":"auto.js","sourcesContent":["/**\n * Zero-config auto-instrumentation for TanStack Start\n *\n * Import this module to automatically instrument TanStack Start applications\n * with OpenTelemetry tracing. Configuration is read from environment variables.\n *\n * Environment Variables:\n * - OTEL_SERVICE_NAME: Service name (default: 'tanstack-start')\n * - OTEL_EXPORTER_OTLP_ENDPOINT: OTLP collector URL\n * - OTEL_EXPORTER_OTLP_HEADERS: Authentication headers (key=value,key=value)\n * - AUTOTEL_DEBUG: Set to 'true' or 'pretty' to log spans to the server console\n *\n * @example\n * ```typescript\n * // app/start.ts\n * import 'autotel-tanstack/auto';\n * import { createStart } from '@tanstack/react-start';\n *\n * // Tracing is automatically configured!\n * export const startInstance = createStart(() => ({}));\n * ```\n *\n * @module\n */\n\nimport { init } from 'autotel';\nimport { InMemorySpanExporter } from 'autotel/exporters';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\n// Parse service name\nconst service = process.env.OTEL_SERVICE_NAME || 'tanstack-start';\n\n// Parse endpoint\nconst endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;\n\n// Parse headers\nlet headers: Record<string, string> | undefined;\nif (process.env.OTEL_EXPORTER_OTLP_HEADERS) {\n headers = {};\n const pairs = process.env.OTEL_EXPORTER_OTLP_HEADERS.split(',');\n for (const pair of pairs) {\n const [key, value] = pair.split('=');\n if (key && value) {\n headers[key.trim()] = value.trim();\n }\n }\n}\n\n// Debug: span output to server console. AUTOTEL_DEBUG=pretty | true, or default\n// to pretty in dev when no OTLP endpoint is set so you see spans immediately.\nfunction resolveDebug(): boolean | 'pretty' {\n const env = process.env.AUTOTEL_DEBUG;\n if (env === 'pretty') return 'pretty';\n if (env === 'true' || env === '1') return true;\n if (env === 'false' || env === '0') return false;\n if (!endpoint && process.env.NODE_ENV === 'development') return 'pretty';\n return false;\n}\n\n// E2E mode: use InMemorySpanExporter so tests can capture and assert on spans.\n// When E2E=1, skip the normal OTLP path and use in-memory storage instead.\n// Note: combined E2E + OTLP (two processors) is handled at integration level\n// because constructing an OTLP processor requires deps not available here.\nif (process.env.E2E === '1') {\n const e2eExporter = new InMemorySpanExporter();\n const e2eProcessor = new SimpleSpanProcessor(e2eExporter);\n (globalThis as Record<string, unknown>).__testSpanExporter = e2eExporter;\n\n init({\n service,\n spanProcessors: [e2eProcessor],\n });\n} else {\n // Initialize autotel (production path — unchanged)\n init({\n service,\n endpoint,\n headers,\n debug: resolveDebug(),\n });\n}\n\n// Log initialization (only in development)\nif (process.env.NODE_ENV === 'development' || process.env.AUTOTEL_DEBUG) {\n console.log('[autotel-tanstack] Auto-initialized with:', {\n service,\n endpoint:\n process.env.E2E === '1'\n ? '(E2E: in-memory)'\n : endpoint || '(not configured)',\n hasHeaders: !!headers,\n });\n}\n\n// Re-export middleware for convenience\nexport { tracingMiddleware, functionTracingMiddleware } from './middleware';\nexport { traceServerFn } from './server-functions';\nexport { traceLoader, traceBeforeLoad } from './loaders';\n\n/**\n * Check if auto-instrumentation is active\n */\nexport function isAutoInstrumentationActive(): boolean {\n return true;\n}\n\n/**\n * Get the configured service name\n */\nexport function getServiceName(): string {\n return service;\n}\n\n/**\n * Get the configured endpoint\n */\nexport function getEndpoint(): string | undefined {\n return endpoint;\n}\n"]}
|
package/dist/browser/index.js
CHANGED
|
@@ -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-
|
|
9
|
+
export { assertSpanCreated, assertSpanHasAttribute, createTestCollector } from '../chunk-ZJTHTWHJ.js';
|
|
10
10
|
export { debugHeadersMiddleware } from '../chunk-JXO7H6KO.js';
|
|
11
11
|
//# sourceMappingURL=index.js.map
|
|
12
12
|
//# sourceMappingURL=index.js.map
|
package/dist/browser/testing.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { assertSpanCreated, assertSpanHasAttribute, createTestCollector } from '../chunk-
|
|
1
|
+
export { assertSpanCreated, assertSpanHasAttribute, createTestCollector, createTestSpansHandlers } from '../chunk-ZJTHTWHJ.js';
|
|
2
2
|
//# sourceMappingURL=testing.js.map
|
|
3
3
|
//# sourceMappingURL=testing.js.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/browser/testing.ts
|
|
2
|
+
function createTestCollector() {
|
|
3
|
+
return {
|
|
4
|
+
getSpans: () => [],
|
|
5
|
+
getSpansByName: () => [],
|
|
6
|
+
clear: () => {
|
|
7
|
+
},
|
|
8
|
+
waitForSpans: async () => []
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
function assertSpanCreated(collector, name) {
|
|
12
|
+
}
|
|
13
|
+
function assertSpanHasAttribute(collector, name, key, value) {
|
|
14
|
+
}
|
|
15
|
+
function createTestSpansHandlers() {
|
|
16
|
+
return {
|
|
17
|
+
GET(_request) {
|
|
18
|
+
return Response.json(
|
|
19
|
+
{ error: "createTestSpansHandlers is server-only" },
|
|
20
|
+
{ status: 404 }
|
|
21
|
+
);
|
|
22
|
+
},
|
|
23
|
+
DELETE(_request) {
|
|
24
|
+
return Response.json(
|
|
25
|
+
{ error: "createTestSpansHandlers is server-only" },
|
|
26
|
+
{ status: 404 }
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export { assertSpanCreated, assertSpanHasAttribute, createTestCollector, createTestSpansHandlers };
|
|
33
|
+
//# sourceMappingURL=chunk-ZJTHTWHJ.js.map
|
|
34
|
+
//# sourceMappingURL=chunk-ZJTHTWHJ.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;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"]}
|
package/dist/testing.d.ts
CHANGED
|
@@ -124,5 +124,42 @@ declare function createMockRequest(method: string, path: string, options?: {
|
|
|
124
124
|
* ```
|
|
125
125
|
*/
|
|
126
126
|
declare function generateTraceparent(traceId?: string, spanId?: string): string;
|
|
127
|
+
/**
|
|
128
|
+
* Serialized span shape returned by the test-spans HTTP endpoint.
|
|
129
|
+
* Mirrors the fields the Playwright side needs for assertions.
|
|
130
|
+
*/
|
|
131
|
+
interface SerializedSpan {
|
|
132
|
+
name: string;
|
|
133
|
+
spanId: string;
|
|
134
|
+
traceId: string;
|
|
135
|
+
parentSpanId?: string;
|
|
136
|
+
attributes?: Record<string, unknown>;
|
|
137
|
+
status: {
|
|
138
|
+
code: number;
|
|
139
|
+
message?: string;
|
|
140
|
+
};
|
|
141
|
+
durationMs: number;
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Creates GET and DELETE handlers for a test-spans HTTP endpoint.
|
|
145
|
+
*
|
|
146
|
+
* Use in a TanStack Start route to expose in-memory spans for Playwright assertions.
|
|
147
|
+
* Only works when E2E=1 (set in webServer command).
|
|
148
|
+
*
|
|
149
|
+
* @example
|
|
150
|
+
* ```typescript
|
|
151
|
+
* // src/routes/api/test-spans.ts
|
|
152
|
+
* import { createFileRoute } from "@tanstack/react-router";
|
|
153
|
+
* import { createTestSpansHandlers } from 'autotel-tanstack/testing';
|
|
154
|
+
* const { GET, DELETE } = createTestSpansHandlers();
|
|
155
|
+
* export const Route = createFileRoute('/api/test-spans')({
|
|
156
|
+
* server: { handlers: { GET, DELETE } },
|
|
157
|
+
* });
|
|
158
|
+
* ```
|
|
159
|
+
*/
|
|
160
|
+
declare function createTestSpansHandlers(): {
|
|
161
|
+
GET: (request: Request) => Response;
|
|
162
|
+
DELETE: (request: Request) => Response;
|
|
163
|
+
};
|
|
127
164
|
|
|
128
|
-
export { type TestHarness, createMockRequest, createTestHarness, generateTraceparent };
|
|
165
|
+
export { type SerializedSpan, type TestHarness, createMockRequest, createTestHarness, createTestSpansHandlers, generateTraceparent };
|
package/dist/testing.js
CHANGED
|
@@ -104,7 +104,58 @@ function generateHex(length) {
|
|
|
104
104
|
}
|
|
105
105
|
return result;
|
|
106
106
|
}
|
|
107
|
+
function getExporter() {
|
|
108
|
+
return globalThis.__testSpanExporter;
|
|
109
|
+
}
|
|
110
|
+
function e2eGuard() {
|
|
111
|
+
if (process.env.E2E !== "1") {
|
|
112
|
+
return Response.json(
|
|
113
|
+
{ error: "test-spans endpoint only available in E2E mode" },
|
|
114
|
+
{ status: 404 }
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
function exporterGuard() {
|
|
120
|
+
if (!getExporter()) {
|
|
121
|
+
return Response.json(
|
|
122
|
+
{ error: "in-memory span exporter not initialized" },
|
|
123
|
+
{ status: 500 }
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function createTestSpansHandlers() {
|
|
129
|
+
return {
|
|
130
|
+
GET(_request) {
|
|
131
|
+
const guard = e2eGuard() ?? exporterGuard();
|
|
132
|
+
if (guard) return guard;
|
|
133
|
+
const spans = getExporter().getFinishedSpans().map((span) => {
|
|
134
|
+
const { spanId, traceId } = span.spanContext();
|
|
135
|
+
const serialized = {
|
|
136
|
+
name: span.name,
|
|
137
|
+
spanId,
|
|
138
|
+
traceId,
|
|
139
|
+
attributes: span.attributes,
|
|
140
|
+
status: span.status,
|
|
141
|
+
durationMs: span.duration[0] * 1e3 + span.duration[1] / 1e6
|
|
142
|
+
};
|
|
143
|
+
if (span.parentSpanContext?.spanId) {
|
|
144
|
+
serialized.parentSpanId = span.parentSpanContext.spanId;
|
|
145
|
+
}
|
|
146
|
+
return serialized;
|
|
147
|
+
});
|
|
148
|
+
return Response.json({ spans });
|
|
149
|
+
},
|
|
150
|
+
DELETE(_request) {
|
|
151
|
+
const guard = e2eGuard() ?? exporterGuard();
|
|
152
|
+
if (guard) return guard;
|
|
153
|
+
getExporter().reset();
|
|
154
|
+
return Response.json({ ok: true });
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
}
|
|
107
158
|
|
|
108
|
-
export { createMockRequest, createTestHarness, generateTraceparent };
|
|
159
|
+
export { createMockRequest, createTestHarness, createTestSpansHandlers, generateTraceparent };
|
|
109
160
|
//# sourceMappingURL=testing.js.map
|
|
110
161
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -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"]}
|
|
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"]}
|
package/package.json
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-tanstack",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.5",
|
|
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",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
8
|
-
"sideEffects":
|
|
8
|
+
"sideEffects": [
|
|
9
|
+
"./dist/auto.js"
|
|
10
|
+
],
|
|
9
11
|
"exports": {
|
|
10
12
|
".": {
|
|
11
13
|
"browser": {
|
package/src/auto.test.ts
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Must mock before importing auto.ts (which runs at module level)
|
|
4
|
+
const mockInit = vi.fn();
|
|
5
|
+
const mockReset = vi.fn();
|
|
6
|
+
const mockGetFinishedSpans = vi.fn(() => []);
|
|
7
|
+
|
|
8
|
+
vi.mock('autotel', () => ({
|
|
9
|
+
init: mockInit,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const mockExporterInstance = {
|
|
13
|
+
reset: mockReset,
|
|
14
|
+
getFinishedSpans: mockGetFinishedSpans,
|
|
15
|
+
};
|
|
16
|
+
// Use regular function (not arrow) so vi.fn() can be called with `new` in vitest 4.x
|
|
17
|
+
const MockInMemorySpanExporter = vi.fn(function () {
|
|
18
|
+
return mockExporterInstance;
|
|
19
|
+
});
|
|
20
|
+
const MockSimpleSpanProcessor = vi.fn(function (exp: unknown) {
|
|
21
|
+
return { exporter: exp };
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
vi.mock('autotel/exporters', () => ({
|
|
25
|
+
InMemorySpanExporter: MockInMemorySpanExporter,
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
vi.mock('autotel/processors', () => ({
|
|
29
|
+
SimpleSpanProcessor: MockSimpleSpanProcessor,
|
|
30
|
+
}));
|
|
31
|
+
|
|
32
|
+
describe('auto.ts E2E mode', () => {
|
|
33
|
+
const originalEnv = { ...process.env };
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.resetModules();
|
|
37
|
+
mockInit.mockReset();
|
|
38
|
+
MockInMemorySpanExporter.mockClear();
|
|
39
|
+
MockSimpleSpanProcessor.mockClear();
|
|
40
|
+
delete (globalThis as Record<string, unknown>).__testSpanExporter;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
process.env = { ...originalEnv };
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('uses InMemorySpanExporter when E2E=1', async () => {
|
|
48
|
+
process.env.E2E = '1';
|
|
49
|
+
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
50
|
+
await import('./auto');
|
|
51
|
+
expect(MockInMemorySpanExporter).toHaveBeenCalledOnce();
|
|
52
|
+
expect(MockSimpleSpanProcessor).toHaveBeenCalledOnce();
|
|
53
|
+
expect(mockInit).toHaveBeenCalledWith(
|
|
54
|
+
expect.objectContaining({ spanProcessors: expect.any(Array) }),
|
|
55
|
+
);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('sets globalThis.__testSpanExporter when E2E=1', async () => {
|
|
59
|
+
process.env.E2E = '1';
|
|
60
|
+
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
61
|
+
await import('./auto');
|
|
62
|
+
expect((globalThis as Record<string, unknown>).__testSpanExporter).toBe(
|
|
63
|
+
mockExporterInstance,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not set __testSpanExporter in production mode', async () => {
|
|
68
|
+
delete process.env.E2E;
|
|
69
|
+
delete process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
70
|
+
await import('./auto');
|
|
71
|
+
expect(
|
|
72
|
+
(globalThis as Record<string, unknown>).__testSpanExporter,
|
|
73
|
+
).toBeUndefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// Skipped: constructing a second OTLP BatchSpanProcessor requires
|
|
77
|
+
// @opentelemetry/exporter-trace-otlp-http which is not available in
|
|
78
|
+
// autotel-tanstack's direct dependencies. The combined E2E+OTLP path
|
|
79
|
+
// is tested at the integration level instead.
|
|
80
|
+
it.skip('adds both InMemory and OTLP processors when E2E=1 and endpoint set', async () => {
|
|
81
|
+
process.env.E2E = '1';
|
|
82
|
+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:4318';
|
|
83
|
+
await import('./auto');
|
|
84
|
+
const call = mockInit.mock.calls[0][0] as { spanProcessors?: unknown[] };
|
|
85
|
+
expect(call.spanProcessors).toHaveLength(2);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('passes endpoint and debug to init in production mode', async () => {
|
|
89
|
+
delete process.env.E2E;
|
|
90
|
+
process.env.OTEL_EXPORTER_OTLP_ENDPOINT = 'http://localhost:4318';
|
|
91
|
+
await import('./auto');
|
|
92
|
+
expect(mockInit).toHaveBeenCalledWith(
|
|
93
|
+
expect.objectContaining({
|
|
94
|
+
endpoint: 'http://localhost:4318',
|
|
95
|
+
}),
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('uses OTEL_SERVICE_NAME env var', async () => {
|
|
100
|
+
process.env.E2E = '1';
|
|
101
|
+
process.env.OTEL_SERVICE_NAME = 'my-e2e-app';
|
|
102
|
+
await import('./auto');
|
|
103
|
+
expect(mockInit).toHaveBeenCalledWith(
|
|
104
|
+
expect.objectContaining({ service: 'my-e2e-app' }),
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
});
|
package/src/auto.ts
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { init } from 'autotel';
|
|
27
|
+
import { InMemorySpanExporter } from 'autotel/exporters';
|
|
28
|
+
import { SimpleSpanProcessor } from 'autotel/processors';
|
|
27
29
|
|
|
28
30
|
// Parse service name
|
|
29
31
|
const service = process.env.OTEL_SERVICE_NAME || 'tanstack-start';
|
|
@@ -55,19 +57,37 @@ function resolveDebug(): boolean | 'pretty' {
|
|
|
55
57
|
return false;
|
|
56
58
|
}
|
|
57
59
|
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
60
|
+
// E2E mode: use InMemorySpanExporter so tests can capture and assert on spans.
|
|
61
|
+
// When E2E=1, skip the normal OTLP path and use in-memory storage instead.
|
|
62
|
+
// Note: combined E2E + OTLP (two processors) is handled at integration level
|
|
63
|
+
// because constructing an OTLP processor requires deps not available here.
|
|
64
|
+
if (process.env.E2E === '1') {
|
|
65
|
+
const e2eExporter = new InMemorySpanExporter();
|
|
66
|
+
const e2eProcessor = new SimpleSpanProcessor(e2eExporter);
|
|
67
|
+
(globalThis as Record<string, unknown>).__testSpanExporter = e2eExporter;
|
|
68
|
+
|
|
69
|
+
init({
|
|
70
|
+
service,
|
|
71
|
+
spanProcessors: [e2eProcessor],
|
|
72
|
+
});
|
|
73
|
+
} else {
|
|
74
|
+
// Initialize autotel (production path — unchanged)
|
|
75
|
+
init({
|
|
76
|
+
service,
|
|
77
|
+
endpoint,
|
|
78
|
+
headers,
|
|
79
|
+
debug: resolveDebug(),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
65
82
|
|
|
66
83
|
// Log initialization (only in development)
|
|
67
84
|
if (process.env.NODE_ENV === 'development' || process.env.AUTOTEL_DEBUG) {
|
|
68
85
|
console.log('[autotel-tanstack] Auto-initialized with:', {
|
|
69
86
|
service,
|
|
70
|
-
endpoint:
|
|
87
|
+
endpoint:
|
|
88
|
+
process.env.E2E === '1'
|
|
89
|
+
? '(E2E: in-memory)'
|
|
90
|
+
: endpoint || '(not configured)',
|
|
71
91
|
hasHeaders: !!headers,
|
|
72
92
|
});
|
|
73
93
|
}
|
package/src/browser/testing.ts
CHANGED
|
@@ -65,3 +65,44 @@ export function assertSpanHasAttribute(
|
|
|
65
65
|
void value;
|
|
66
66
|
// No-op in browser
|
|
67
67
|
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Serialized span interface (browser stub - mirrors server SerializedSpan).
|
|
71
|
+
*/
|
|
72
|
+
export interface SerializedSpan {
|
|
73
|
+
name: string;
|
|
74
|
+
spanId: string;
|
|
75
|
+
traceId: string;
|
|
76
|
+
parentSpanId?: string;
|
|
77
|
+
attributes?: Record<string, unknown>;
|
|
78
|
+
status: { code: number; message?: string };
|
|
79
|
+
durationMs: number;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Browser stub: test-spans handlers are server-only.
|
|
84
|
+
* Returns no-op handlers that always return 404.
|
|
85
|
+
*/
|
|
86
|
+
export function createTestSpansHandlers(): {
|
|
87
|
+
GET: (request: Request) => Response;
|
|
88
|
+
DELETE: (request: Request) => Response;
|
|
89
|
+
} {
|
|
90
|
+
return {
|
|
91
|
+
GET(_request: Request): Response {
|
|
92
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
93
|
+
void _request;
|
|
94
|
+
return Response.json(
|
|
95
|
+
{ error: 'createTestSpansHandlers is server-only' },
|
|
96
|
+
{ status: 404 },
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
DELETE(_request: Request): Response {
|
|
100
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
101
|
+
void _request;
|
|
102
|
+
return Response.json(
|
|
103
|
+
{ error: 'createTestSpansHandlers is server-only' },
|
|
104
|
+
{ status: 404 },
|
|
105
|
+
);
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/testing.test.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import {
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
createMockRequest,
|
|
4
|
+
generateTraceparent,
|
|
5
|
+
createTestSpansHandlers,
|
|
6
|
+
type SerializedSpan,
|
|
7
|
+
} from './testing';
|
|
3
8
|
|
|
4
9
|
describe('testing utilities', () => {
|
|
5
10
|
describe('createMockRequest', () => {
|
|
@@ -70,3 +75,113 @@ describe('testing utilities', () => {
|
|
|
70
75
|
});
|
|
71
76
|
});
|
|
72
77
|
});
|
|
78
|
+
|
|
79
|
+
// Helper to read Response JSON
|
|
80
|
+
async function json(res: Response): Promise<unknown> {
|
|
81
|
+
return res.json();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
describe('createTestSpansHandlers', () => {
|
|
85
|
+
beforeEach(() => {
|
|
86
|
+
delete (globalThis as Record<string, unknown>).__testSpanExporter;
|
|
87
|
+
delete process.env.E2E;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('GET returns 404 when not in E2E mode', async () => {
|
|
91
|
+
const { GET } = createTestSpansHandlers();
|
|
92
|
+
const res = GET(new Request('http://localhost/api/test-spans'));
|
|
93
|
+
expect(res.status).toBe(404);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('GET returns 500 when exporter not initialized', async () => {
|
|
97
|
+
process.env.E2E = '1';
|
|
98
|
+
const { GET } = createTestSpansHandlers();
|
|
99
|
+
const res = GET(new Request('http://localhost/api/test-spans'));
|
|
100
|
+
expect(res.status).toBe(500);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('GET returns serialized spans', async () => {
|
|
104
|
+
process.env.E2E = '1';
|
|
105
|
+
const mockSpan = {
|
|
106
|
+
name: 'sendMoney.handler',
|
|
107
|
+
spanContext: () => ({ spanId: 'abc123', traceId: 'trace456' }),
|
|
108
|
+
parentSpanContext: { spanId: 'parent789' },
|
|
109
|
+
attributes: { 'transfer.amount': 100 },
|
|
110
|
+
status: { code: 0 },
|
|
111
|
+
duration: [0, 500_000_000], // 500ms in [seconds, nanoseconds]
|
|
112
|
+
};
|
|
113
|
+
(globalThis as Record<string, unknown>).__testSpanExporter = {
|
|
114
|
+
getFinishedSpans: () => [mockSpan],
|
|
115
|
+
reset: () => {},
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
const { GET } = createTestSpansHandlers();
|
|
119
|
+
const res = GET(new Request('http://localhost/api/test-spans'));
|
|
120
|
+
expect(res.status).toBe(200);
|
|
121
|
+
const body = (await json(res)) as { spans: SerializedSpan[] };
|
|
122
|
+
expect(body.spans).toHaveLength(1);
|
|
123
|
+
expect(body.spans[0].name).toBe('sendMoney.handler');
|
|
124
|
+
expect(body.spans[0].spanId).toBe('abc123');
|
|
125
|
+
expect(body.spans[0].traceId).toBe('trace456');
|
|
126
|
+
expect(body.spans[0].parentSpanId).toBe('parent789');
|
|
127
|
+
expect(body.spans[0].attributes?.['transfer.amount']).toBe(100);
|
|
128
|
+
expect(body.spans[0].durationMs).toBeCloseTo(500, 0);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('GET omits parentSpanId when no parent', async () => {
|
|
132
|
+
process.env.E2E = '1';
|
|
133
|
+
const mockSpan = {
|
|
134
|
+
name: 'root',
|
|
135
|
+
spanContext: () => ({ spanId: 'abc123', traceId: 'trace456' }),
|
|
136
|
+
parentSpanContext: undefined,
|
|
137
|
+
attributes: {},
|
|
138
|
+
status: { code: 0 },
|
|
139
|
+
duration: [0, 0],
|
|
140
|
+
};
|
|
141
|
+
(globalThis as Record<string, unknown>).__testSpanExporter = {
|
|
142
|
+
getFinishedSpans: () => [mockSpan],
|
|
143
|
+
reset: () => {},
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const { GET } = createTestSpansHandlers();
|
|
147
|
+
const body = (await json(
|
|
148
|
+
GET(new Request('http://localhost/api/test-spans')),
|
|
149
|
+
)) as { spans: SerializedSpan[] };
|
|
150
|
+
expect(body.spans[0].parentSpanId).toBeUndefined();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('DELETE returns 404 when not in E2E mode', async () => {
|
|
154
|
+
const { DELETE } = createTestSpansHandlers();
|
|
155
|
+
const res = DELETE(
|
|
156
|
+
new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
|
|
157
|
+
);
|
|
158
|
+
expect(res.status).toBe(404);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('DELETE returns 500 when exporter not initialized', async () => {
|
|
162
|
+
process.env.E2E = '1';
|
|
163
|
+
const { DELETE } = createTestSpansHandlers();
|
|
164
|
+
const res = DELETE(
|
|
165
|
+
new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
|
|
166
|
+
);
|
|
167
|
+
expect(res.status).toBe(500);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('DELETE resets exporter and returns ok', async () => {
|
|
171
|
+
process.env.E2E = '1';
|
|
172
|
+
const reset = vi.fn();
|
|
173
|
+
(globalThis as Record<string, unknown>).__testSpanExporter = {
|
|
174
|
+
getFinishedSpans: () => [],
|
|
175
|
+
reset,
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const { DELETE } = createTestSpansHandlers();
|
|
179
|
+
const res = DELETE(
|
|
180
|
+
new Request('http://localhost/api/test-spans', { method: 'DELETE' }),
|
|
181
|
+
);
|
|
182
|
+
expect(res.status).toBe(200);
|
|
183
|
+
expect(reset).toHaveBeenCalledOnce();
|
|
184
|
+
const body = (await json(res)) as { ok: boolean };
|
|
185
|
+
expect(body.ok).toBe(true);
|
|
186
|
+
});
|
|
187
|
+
});
|
package/src/testing.ts
CHANGED
|
@@ -274,3 +274,111 @@ function generateHex(length: number): string {
|
|
|
274
274
|
}
|
|
275
275
|
return result;
|
|
276
276
|
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Serialized span shape returned by the test-spans HTTP endpoint.
|
|
280
|
+
* Mirrors the fields the Playwright side needs for assertions.
|
|
281
|
+
*/
|
|
282
|
+
export interface SerializedSpan {
|
|
283
|
+
name: string;
|
|
284
|
+
spanId: string;
|
|
285
|
+
traceId: string;
|
|
286
|
+
parentSpanId?: string;
|
|
287
|
+
attributes?: Record<string, unknown>;
|
|
288
|
+
status: { code: number; message?: string };
|
|
289
|
+
durationMs: number;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
interface TestSpanExporter {
|
|
293
|
+
getFinishedSpans(): Array<{
|
|
294
|
+
name: string;
|
|
295
|
+
spanContext(): { spanId: string; traceId: string };
|
|
296
|
+
parentSpanContext?: { spanId: string };
|
|
297
|
+
attributes: Record<string, unknown>;
|
|
298
|
+
status: { code: number; message?: string };
|
|
299
|
+
duration: [number, number];
|
|
300
|
+
}>;
|
|
301
|
+
reset(): void;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function getExporter(): TestSpanExporter | undefined {
|
|
305
|
+
return (globalThis as Record<string, unknown>).__testSpanExporter as
|
|
306
|
+
| TestSpanExporter
|
|
307
|
+
| undefined;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function e2eGuard(): Response | null {
|
|
311
|
+
if (process.env.E2E !== '1') {
|
|
312
|
+
return Response.json(
|
|
313
|
+
{ error: 'test-spans endpoint only available in E2E mode' },
|
|
314
|
+
{ status: 404 },
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function exporterGuard(): Response | null {
|
|
321
|
+
if (!getExporter()) {
|
|
322
|
+
return Response.json(
|
|
323
|
+
{ error: 'in-memory span exporter not initialized' },
|
|
324
|
+
{ status: 500 },
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Creates GET and DELETE handlers for a test-spans HTTP endpoint.
|
|
332
|
+
*
|
|
333
|
+
* Use in a TanStack Start route to expose in-memory spans for Playwright assertions.
|
|
334
|
+
* Only works when E2E=1 (set in webServer command).
|
|
335
|
+
*
|
|
336
|
+
* @example
|
|
337
|
+
* ```typescript
|
|
338
|
+
* // src/routes/api/test-spans.ts
|
|
339
|
+
* import { createFileRoute } from "@tanstack/react-router";
|
|
340
|
+
* import { createTestSpansHandlers } from 'autotel-tanstack/testing';
|
|
341
|
+
* const { GET, DELETE } = createTestSpansHandlers();
|
|
342
|
+
* export const Route = createFileRoute('/api/test-spans')({
|
|
343
|
+
* server: { handlers: { GET, DELETE } },
|
|
344
|
+
* });
|
|
345
|
+
* ```
|
|
346
|
+
*/
|
|
347
|
+
export function createTestSpansHandlers(): {
|
|
348
|
+
GET: (request: Request) => Response;
|
|
349
|
+
DELETE: (request: Request) => Response;
|
|
350
|
+
} {
|
|
351
|
+
return {
|
|
352
|
+
GET(_request: Request): Response {
|
|
353
|
+
const guard = e2eGuard() ?? exporterGuard();
|
|
354
|
+
if (guard) return guard;
|
|
355
|
+
|
|
356
|
+
const spans: SerializedSpan[] = getExporter()!
|
|
357
|
+
.getFinishedSpans()
|
|
358
|
+
.map((span) => {
|
|
359
|
+
const { spanId, traceId } = span.spanContext();
|
|
360
|
+
const serialized: SerializedSpan = {
|
|
361
|
+
name: span.name,
|
|
362
|
+
spanId,
|
|
363
|
+
traceId,
|
|
364
|
+
attributes: span.attributes,
|
|
365
|
+
status: span.status,
|
|
366
|
+
durationMs: span.duration[0] * 1000 + span.duration[1] / 1_000_000,
|
|
367
|
+
};
|
|
368
|
+
if (span.parentSpanContext?.spanId) {
|
|
369
|
+
serialized.parentSpanId = span.parentSpanContext.spanId;
|
|
370
|
+
}
|
|
371
|
+
return serialized;
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
return Response.json({ spans });
|
|
375
|
+
},
|
|
376
|
+
|
|
377
|
+
DELETE(_request: Request): Response {
|
|
378
|
+
const guard = e2eGuard() ?? exporterGuard();
|
|
379
|
+
if (guard) return guard;
|
|
380
|
+
getExporter()!.reset();
|
|
381
|
+
return Response.json({ ok: true });
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
package/dist/chunk-Z3MJ3GZ6.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
// src/browser/testing.ts
|
|
2
|
-
function createTestCollector() {
|
|
3
|
-
return {
|
|
4
|
-
getSpans: () => [],
|
|
5
|
-
getSpansByName: () => [],
|
|
6
|
-
clear: () => {
|
|
7
|
-
},
|
|
8
|
-
waitForSpans: async () => []
|
|
9
|
-
};
|
|
10
|
-
}
|
|
11
|
-
function assertSpanCreated(collector, name) {
|
|
12
|
-
}
|
|
13
|
-
function assertSpanHasAttribute(collector, name, key, value) {
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export { assertSpanCreated, assertSpanHasAttribute, createTestCollector };
|
|
17
|
-
//# sourceMappingURL=chunk-Z3MJ3GZ6.js.map
|
|
18
|
-
//# sourceMappingURL=chunk-Z3MJ3GZ6.js.map
|
|
@@ -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","file":"chunk-Z3MJ3GZ6.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"]}
|