autotel-playwright 0.4.9 → 0.4.11
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/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -2
- package/dist/index.d.ts +44 -2
- package/dist/index.js +22 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/index.test.ts +83 -0
- package/src/index.ts +64 -0
package/dist/index.cjs
CHANGED
|
@@ -187,6 +187,27 @@ function createGlobalSetup(initOptions) {
|
|
|
187
187
|
});
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
|
+
function createTestSpansClient(baseUrl, options) {
|
|
191
|
+
const base2 = baseUrl.replace(/\/$/, "");
|
|
192
|
+
const path = options?.path ?? "/api/test-spans";
|
|
193
|
+
const url = `${base2}${path}`;
|
|
194
|
+
return {
|
|
195
|
+
async getSpans(request) {
|
|
196
|
+
const res = await request.get(url);
|
|
197
|
+
if (!res.ok()) {
|
|
198
|
+
throw new Error(`GET ${path} failed: ${res.status()}`);
|
|
199
|
+
}
|
|
200
|
+
const body = await res.json();
|
|
201
|
+
return body.spans;
|
|
202
|
+
},
|
|
203
|
+
async clearSpans(request) {
|
|
204
|
+
const res = await request.delete(url);
|
|
205
|
+
if (!res.ok()) {
|
|
206
|
+
throw new Error(`DELETE ${path} failed: ${res.status()}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
190
211
|
|
|
191
212
|
Object.defineProperty(exports, "expect", {
|
|
192
213
|
enumerable: true,
|
|
@@ -210,6 +231,7 @@ Object.defineProperty(exports, "resolveTraceUrl", {
|
|
|
210
231
|
});
|
|
211
232
|
exports.AUTOTEL_ATTRIBUTE_ANNOTATION = AUTOTEL_ATTRIBUTE_ANNOTATION;
|
|
212
233
|
exports.createGlobalSetup = createGlobalSetup;
|
|
234
|
+
exports.createTestSpansClient = createTestSpansClient;
|
|
213
235
|
exports.step = step;
|
|
214
236
|
exports.test = test;
|
|
215
237
|
//# sourceMappingURL=index.cjs.map
|
package/dist/index.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["TestSpanCollector","getAutotelTracerProvider","SimpleSpanProcessor","base","getTracer","otelTrace","otelContext","propagation","SpanStatusCode"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * autotel-playwright\n *\n * Playwright fixture that creates one OTel span per test and injects W3C trace\n * context into requests to your API so \"test → API\" appears as one trace.\n *\n * @example\n * // globalSetup.ts: init({ service: 'e2e-tests' });\n * // In spec:\n * import { test, expect } from 'autotel-playwright';\n * test('checks health', async ({ page }) => {\n * await page.goto(API_BASE_URL + '/health'); // request gets traceparent\n * });\n * // Node-side API calls with trace context:\n * test('api health', async ({ requestWithTrace }) => {\n * const res = await requestWithTrace.get(API_BASE_URL + '/health');\n * expect(res.ok()).toBeTruthy();\n * });\n */\n\nimport { test as base } from '@playwright/test';\nimport type { Page, APIRequestContext, Request as PlaywrightRequest } from '@playwright/test';\nimport type { TestInfo } from '@playwright/test';\nimport type { AutotelConfig } from 'autotel';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n propagation,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'playwright-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\ninterface TracerProviderWithProcessor {\n addSpanProcessor(processor: unknown): void;\n}\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as TracerProviderWithProcessor).addSpanProcessor(\n new SimpleSpanProcessor(collector),\n );\n }\n }\n return collector;\n}\n\n/** Env keys for API base URL (requests to this origin get trace context injected). */\nconst ENV_API_BASE_URL = 'API_BASE_URL';\nconst ENV_API_ORIGIN = 'AUTOTEL_PLAYWRIGHT_API_ORIGIN';\n\nfunction getApiBaseUrls(): string[] {\n const a = process.env[ENV_API_BASE_URL];\n const b = process.env[ENV_API_ORIGIN];\n const urls: string[] = [];\n if (a) urls.push(a.replace(/\\/$/, ''));\n if (b) urls.push(b.replace(/\\/$/, ''));\n return [...new Set(urls)];\n}\n\n/**\n * Returns true if requestUrl should receive trace headers for the given apiBaseUrls.\n * When a base URL includes a path (e.g. http://localhost:3000/api), only requests\n * whose path starts with that path segment match; same-origin but different path\n * (e.g. /health) must not match to avoid leaking trace context to unrelated endpoints.\n */\nfunction urlMatchesApiOrigin(requestUrl: string, apiBaseUrls: string[]): boolean {\n if (apiBaseUrls.length === 0) return false;\n try {\n const u = new URL(requestUrl);\n const requestOrigin = u.origin;\n const requestPathname = u.pathname;\n return apiBaseUrls.some((base) => {\n try {\n const b = new URL(base);\n if (requestOrigin !== b.origin) return false;\n const basePathname = b.pathname.replace(/\\/$/, '') || '/';\n if (basePathname === '/') return true;\n return (\n requestPathname === basePathname || requestPathname.startsWith(basePathname + '/')\n );\n } catch {\n return requestUrl.startsWith(base);\n }\n });\n } catch {\n return apiBaseUrls.some((base) => requestUrl.startsWith(base));\n }\n}\n\n/** Annotation type for custom span attributes: description should be \"key=value\" or \"key=value1;key2=value2\". */\nexport const AUTOTEL_ATTRIBUTE_ANNOTATION = 'autotel.attribute';\n\nfunction setAttributesFromAnnotations(\n span: { setAttribute: (k: string, v: string | number | boolean) => void },\n testInfo: { annotations: Array<{ type: string; description?: string }> },\n): void {\n for (const a of testInfo.annotations) {\n if (a.type !== AUTOTEL_ATTRIBUTE_ANNOTATION || !a.description) continue;\n const entries = a.description.split(';');\n for (const entry of entries) {\n const parts = entry.split('=');\n if (parts.length >= 2) {\n const key = parts[0].trim();\n const value = parts.slice(1).join('=').trim();\n span.setAttribute(key, value);\n }\n }\n }\n}\n\n/** Internal: options for get/post/put/patch/delete/head/fetch that may include headers. */\ntype RequestOptions = Record<string, unknown> & { headers?: Record<string, string> };\n\nfunction mergeTraceHeaders(\n url: string,\n options: RequestOptions | undefined,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testName: string,\n): RequestOptions {\n const opts = options ?? {};\n if (!urlMatchesApiOrigin(url, apiBaseUrls)) return opts;\n return {\n ...opts,\n headers: { ...(opts.headers as Record<string, string>), ...carrier, 'x-test-name': testName },\n };\n}\n\n/** Wraps APIRequestContext so requests to API_BASE_URL get trace context injected. */\nfunction createRequestWithTrace(\n request: APIRequestContext,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testInfo: TestInfo,\n): APIRequestContext {\n const merge = (url: string, options?: RequestOptions) =>\n mergeTraceHeaders(url, options, apiBaseUrls, carrier, testInfo.title);\n\n return {\n get: (url: string, options?: RequestOptions) => request.get(url, merge(url, options)),\n post: (url: string, options?: RequestOptions) => request.post(url, merge(url, options)),\n put: (url: string, options?: RequestOptions) => request.put(url, merge(url, options)),\n patch: (url: string, options?: RequestOptions) => request.patch(url, merge(url, options)),\n delete: (url: string, options?: RequestOptions) => request.delete(url, merge(url, options)),\n head: (url: string, options?: RequestOptions) => request.head(url, merge(url, options)),\n fetch: (urlOrRequest: string | PlaywrightRequest, options?: RequestOptions) =>\n request.fetch(urlOrRequest, merge(typeof urlOrRequest === 'string' ? urlOrRequest : urlOrRequest.url(), options)),\n storageState: (options?: { path?: string }) => request.storageState(options),\n dispose: () => request.dispose(),\n } as APIRequestContext;\n}\n\ntype OtelTestSpan = {\n carrier: Record<string, string>;\n apiBaseUrls: string[];\n testInfo: TestInfo;\n};\n\nexport const test = base.extend<{\n page: Page;\n requestWithTrace: APIRequestContext;\n _otelTestSpan: OtelTestSpan;\n}>({\n _otelTestSpan: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n ensureCollector();\n const apiBaseUrls = getApiBaseUrls();\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const spanName = `e2e:${testInfo.title}`;\n const span = tracer.startSpan(spanName, {\n attributes: {\n 'test.title': testInfo.title,\n 'test.project': testInfo.project.name,\n 'test.file': testInfo.file ?? '',\n 'test.line': testInfo.line ?? 0,\n },\n });\n setAttributesFromAnnotations(span, testInfo);\n const ctx = otelTrace.setSpan(otelContext.active(), span);\n const carrier: Record<string, string> = {};\n otelContext.with(ctx, () => {\n propagation.inject(otelContext.active(), carrier);\n });\n try {\n await otelContext.with(ctx, () => use({ carrier, apiBaseUrls, testInfo }));\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n const traceId = span.spanContext().traceId;\n const rootSpanId = span.spanContext().spanId;\n const spans = collector!.drainTrace(traceId, rootSpanId);\n if (spans.length > 0) {\n testInfo.annotations.push({\n type: 'otel-spans',\n description: JSON.stringify(spans),\n });\n }\n }\n },\n { scope: 'test' },\n ],\n\n page: async ({ page, _otelTestSpan }, use) => {\n const { carrier, apiBaseUrls, testInfo } = _otelTestSpan;\n if (apiBaseUrls.length > 0) {\n await page.route('**/*', async (route) => {\n const request = route.request();\n const url = request.url();\n if (urlMatchesApiOrigin(url, apiBaseUrls)) {\n const headers = {\n ...request.headers(),\n ...carrier,\n 'x-test-name': testInfo.title,\n };\n await route.continue({ headers });\n } else {\n await route.continue();\n }\n });\n }\n await use(page);\n },\n\n requestWithTrace: async ({ request, _otelTestSpan }, use) => {\n const wrapped = createRequestWithTrace(\n request,\n _otelTestSpan.apiBaseUrls,\n _otelTestSpan.carrier,\n _otelTestSpan.testInfo,\n );\n await use(wrapped);\n },\n});\n\nexport { expect } from '@playwright/test';\n\n// Re-export trace context helpers for DX convenience\nexport {\n getTraceContext,\n resolveTraceUrl,\n isTracing,\n enrichWithTraceContext,\n} from 'autotel';\n\nexport type { OtelTraceContext } from 'autotel';\n\n/**\n * Runs a named step as a child span of the current test span. Use inside a test to get\n * step-level spans (e.g. \"step:login\", \"step:navigate\") under the test span in the same trace.\n *\n * @example\n * test('user flow', async ({ page }) => {\n * await step('login', async () => {\n * await page.click('button[type=submit]');\n * });\n * await step('open profile', async () => {\n * await page.goto('/profile');\n * });\n * });\n */\nexport async function step<T>(name: string, fn: () => Promise<T>): Promise<T> {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${name}`, {\n attributes: { 'step.name': name },\n });\n try {\n return await otelContext.with(otelTrace.setSpan(otelContext.active(), span), fn);\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n/**\n * Returns a function suitable for Playwright globalSetup that inits autotel.\n * Call autotel.init() with the given options (or defaults) so test spans are exported.\n */\nexport function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void> {\n return async () => {\n const { init } = await import('autotel');\n init({\n service: 'e2e-tests',\n debug: true,\n ...initOptions,\n });\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["TestSpanCollector","getAutotelTracerProvider","SimpleSpanProcessor","base","getTracer","otelTrace","otelContext","propagation","SpanStatusCode"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * autotel-playwright\n *\n * Playwright fixture that creates one OTel span per test and injects W3C trace\n * context into requests to your API so \"test → API\" appears as one trace.\n *\n * @example\n * // globalSetup.ts: init({ service: 'e2e-tests' });\n * // In spec:\n * import { test, expect } from 'autotel-playwright';\n * test('checks health', async ({ page }) => {\n * await page.goto(API_BASE_URL + '/health'); // request gets traceparent\n * });\n * // Node-side API calls with trace context:\n * test('api health', async ({ requestWithTrace }) => {\n * const res = await requestWithTrace.get(API_BASE_URL + '/health');\n * expect(res.ok()).toBeTruthy();\n * });\n */\n\nimport { test as base } from '@playwright/test';\nimport type { Page, APIRequestContext, Request as PlaywrightRequest } from '@playwright/test';\nimport type { TestInfo } from '@playwright/test';\nimport type { AutotelConfig } from 'autotel';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n propagation,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'playwright-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\ninterface TracerProviderWithProcessor {\n addSpanProcessor(processor: unknown): void;\n}\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as TracerProviderWithProcessor).addSpanProcessor(\n new SimpleSpanProcessor(collector),\n );\n }\n }\n return collector;\n}\n\n/** Env keys for API base URL (requests to this origin get trace context injected). */\nconst ENV_API_BASE_URL = 'API_BASE_URL';\nconst ENV_API_ORIGIN = 'AUTOTEL_PLAYWRIGHT_API_ORIGIN';\n\nfunction getApiBaseUrls(): string[] {\n const a = process.env[ENV_API_BASE_URL];\n const b = process.env[ENV_API_ORIGIN];\n const urls: string[] = [];\n if (a) urls.push(a.replace(/\\/$/, ''));\n if (b) urls.push(b.replace(/\\/$/, ''));\n return [...new Set(urls)];\n}\n\n/**\n * Returns true if requestUrl should receive trace headers for the given apiBaseUrls.\n * When a base URL includes a path (e.g. http://localhost:3000/api), only requests\n * whose path starts with that path segment match; same-origin but different path\n * (e.g. /health) must not match to avoid leaking trace context to unrelated endpoints.\n */\nfunction urlMatchesApiOrigin(requestUrl: string, apiBaseUrls: string[]): boolean {\n if (apiBaseUrls.length === 0) return false;\n try {\n const u = new URL(requestUrl);\n const requestOrigin = u.origin;\n const requestPathname = u.pathname;\n return apiBaseUrls.some((base) => {\n try {\n const b = new URL(base);\n if (requestOrigin !== b.origin) return false;\n const basePathname = b.pathname.replace(/\\/$/, '') || '/';\n if (basePathname === '/') return true;\n return (\n requestPathname === basePathname || requestPathname.startsWith(basePathname + '/')\n );\n } catch {\n return requestUrl.startsWith(base);\n }\n });\n } catch {\n return apiBaseUrls.some((base) => requestUrl.startsWith(base));\n }\n}\n\n/** Annotation type for custom span attributes: description should be \"key=value\" or \"key=value1;key2=value2\". */\nexport const AUTOTEL_ATTRIBUTE_ANNOTATION = 'autotel.attribute';\n\nfunction setAttributesFromAnnotations(\n span: { setAttribute: (k: string, v: string | number | boolean) => void },\n testInfo: { annotations: Array<{ type: string; description?: string }> },\n): void {\n for (const a of testInfo.annotations) {\n if (a.type !== AUTOTEL_ATTRIBUTE_ANNOTATION || !a.description) continue;\n const entries = a.description.split(';');\n for (const entry of entries) {\n const parts = entry.split('=');\n if (parts.length >= 2) {\n const key = parts[0].trim();\n const value = parts.slice(1).join('=').trim();\n span.setAttribute(key, value);\n }\n }\n }\n}\n\n/** Internal: options for get/post/put/patch/delete/head/fetch that may include headers. */\ntype RequestOptions = Record<string, unknown> & { headers?: Record<string, string> };\n\nfunction mergeTraceHeaders(\n url: string,\n options: RequestOptions | undefined,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testName: string,\n): RequestOptions {\n const opts = options ?? {};\n if (!urlMatchesApiOrigin(url, apiBaseUrls)) return opts;\n return {\n ...opts,\n headers: { ...(opts.headers as Record<string, string>), ...carrier, 'x-test-name': testName },\n };\n}\n\n/** Wraps APIRequestContext so requests to API_BASE_URL get trace context injected. */\nfunction createRequestWithTrace(\n request: APIRequestContext,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testInfo: TestInfo,\n): APIRequestContext {\n const merge = (url: string, options?: RequestOptions) =>\n mergeTraceHeaders(url, options, apiBaseUrls, carrier, testInfo.title);\n\n return {\n get: (url: string, options?: RequestOptions) => request.get(url, merge(url, options)),\n post: (url: string, options?: RequestOptions) => request.post(url, merge(url, options)),\n put: (url: string, options?: RequestOptions) => request.put(url, merge(url, options)),\n patch: (url: string, options?: RequestOptions) => request.patch(url, merge(url, options)),\n delete: (url: string, options?: RequestOptions) => request.delete(url, merge(url, options)),\n head: (url: string, options?: RequestOptions) => request.head(url, merge(url, options)),\n fetch: (urlOrRequest: string | PlaywrightRequest, options?: RequestOptions) =>\n request.fetch(urlOrRequest, merge(typeof urlOrRequest === 'string' ? urlOrRequest : urlOrRequest.url(), options)),\n storageState: (options?: { path?: string }) => request.storageState(options),\n dispose: () => request.dispose(),\n } as APIRequestContext;\n}\n\ntype OtelTestSpan = {\n carrier: Record<string, string>;\n apiBaseUrls: string[];\n testInfo: TestInfo;\n};\n\nexport const test = base.extend<{\n page: Page;\n requestWithTrace: APIRequestContext;\n _otelTestSpan: OtelTestSpan;\n}>({\n _otelTestSpan: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n ensureCollector();\n const apiBaseUrls = getApiBaseUrls();\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const spanName = `e2e:${testInfo.title}`;\n const span = tracer.startSpan(spanName, {\n attributes: {\n 'test.title': testInfo.title,\n 'test.project': testInfo.project.name,\n 'test.file': testInfo.file ?? '',\n 'test.line': testInfo.line ?? 0,\n },\n });\n setAttributesFromAnnotations(span, testInfo);\n const ctx = otelTrace.setSpan(otelContext.active(), span);\n const carrier: Record<string, string> = {};\n otelContext.with(ctx, () => {\n propagation.inject(otelContext.active(), carrier);\n });\n try {\n await otelContext.with(ctx, () => use({ carrier, apiBaseUrls, testInfo }));\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n const traceId = span.spanContext().traceId;\n const rootSpanId = span.spanContext().spanId;\n const spans = collector!.drainTrace(traceId, rootSpanId);\n if (spans.length > 0) {\n testInfo.annotations.push({\n type: 'otel-spans',\n description: JSON.stringify(spans),\n });\n }\n }\n },\n { scope: 'test' },\n ],\n\n page: async ({ page, _otelTestSpan }, use) => {\n const { carrier, apiBaseUrls, testInfo } = _otelTestSpan;\n if (apiBaseUrls.length > 0) {\n await page.route('**/*', async (route) => {\n const request = route.request();\n const url = request.url();\n if (urlMatchesApiOrigin(url, apiBaseUrls)) {\n const headers = {\n ...request.headers(),\n ...carrier,\n 'x-test-name': testInfo.title,\n };\n await route.continue({ headers });\n } else {\n await route.continue();\n }\n });\n }\n await use(page);\n },\n\n requestWithTrace: async ({ request, _otelTestSpan }, use) => {\n const wrapped = createRequestWithTrace(\n request,\n _otelTestSpan.apiBaseUrls,\n _otelTestSpan.carrier,\n _otelTestSpan.testInfo,\n );\n await use(wrapped);\n },\n});\n\nexport { expect } from '@playwright/test';\n\n// Re-export trace context helpers for DX convenience\nexport {\n getTraceContext,\n resolveTraceUrl,\n isTracing,\n enrichWithTraceContext,\n} from 'autotel';\n\nexport type { OtelTraceContext } from 'autotel';\n\n/**\n * Runs a named step as a child span of the current test span. Use inside a test to get\n * step-level spans (e.g. \"step:login\", \"step:navigate\") under the test span in the same trace.\n *\n * @example\n * test('user flow', async ({ page }) => {\n * await step('login', async () => {\n * await page.click('button[type=submit]');\n * });\n * await step('open profile', async () => {\n * await page.goto('/profile');\n * });\n * });\n */\nexport async function step<T>(name: string, fn: () => Promise<T>): Promise<T> {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${name}`, {\n attributes: { 'step.name': name },\n });\n try {\n return await otelContext.with(otelTrace.setSpan(otelContext.active(), span), fn);\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n/**\n * Returns a function suitable for Playwright globalSetup that inits autotel.\n * Call autotel.init() with the given options (or defaults) so test spans are exported.\n */\nexport function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void> {\n return async () => {\n const { init } = await import('autotel');\n init({\n service: 'e2e-tests',\n debug: true,\n ...initOptions,\n });\n };\n}\n\n/**\n * Serialized span returned by the test-spans endpoint (matches autotel-tanstack/testing 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 * Creates a typed client for the test-spans HTTP endpoint.\n *\n * Pairs with `createTestSpansHandlers()` from `autotel-tanstack/testing`.\n *\n * @param baseUrl - Base URL of the app under test (e.g. 'http://localhost:3100')\n * @param options.path - Path of the test-spans endpoint (default: '/api/test-spans')\n *\n * @example\n * ```typescript\n * const spansClient = createTestSpansClient('http://localhost:3100');\n *\n * test('server function is traced', async ({ request }) => {\n * await spansClient.clearSpans(request);\n * await page.goto('/');\n * // ... trigger action ...\n * const spans = await spansClient.getSpans(request);\n * expect(spans.find(s => s.name === 'sendMoney.handler')).toBeDefined();\n * });\n * ```\n */\nexport function createTestSpansClient(\n baseUrl: string,\n options?: { path?: string },\n): {\n getSpans(request: APIRequestContext): Promise<SerializedSpan[]>;\n clearSpans(request: APIRequestContext): Promise<void>;\n} {\n const base = baseUrl.replace(/\\/$/, '');\n const path = options?.path ?? '/api/test-spans';\n const url = `${base}${path}`;\n\n return {\n async getSpans(request: APIRequestContext): Promise<SerializedSpan[]> {\n const res = await request.get(url);\n if (!res.ok()) {\n throw new Error(`GET ${path} failed: ${res.status()}`);\n }\n const body = await res.json() as { spans: SerializedSpan[] };\n return body.spans;\n },\n\n async clearSpans(request: APIRequestContext): Promise<void> {\n const res = await request.delete(url);\n if (!res.ok()) {\n throw new Error(`DELETE ${path} failed: ${res.status()}`);\n }\n },\n };\n}\n"]}
|
package/dist/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _playwright_test from '@playwright/test';
|
|
2
|
-
import {
|
|
2
|
+
import { APIRequestContext, Page, TestInfo } from '@playwright/test';
|
|
3
3
|
export { expect } from '@playwright/test';
|
|
4
4
|
import { AutotelConfig } from 'autotel';
|
|
5
5
|
export { OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
@@ -37,5 +37,47 @@ declare function step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
|
37
37
|
* Call autotel.init() with the given options (or defaults) so test spans are exported.
|
|
38
38
|
*/
|
|
39
39
|
declare function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Serialized span returned by the test-spans endpoint (matches autotel-tanstack/testing SerializedSpan).
|
|
42
|
+
*/
|
|
43
|
+
interface SerializedSpan {
|
|
44
|
+
name: string;
|
|
45
|
+
spanId: string;
|
|
46
|
+
traceId: string;
|
|
47
|
+
parentSpanId?: string;
|
|
48
|
+
attributes?: Record<string, unknown>;
|
|
49
|
+
status: {
|
|
50
|
+
code: number;
|
|
51
|
+
message?: string;
|
|
52
|
+
};
|
|
53
|
+
durationMs: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a typed client for the test-spans HTTP endpoint.
|
|
57
|
+
*
|
|
58
|
+
* Pairs with `createTestSpansHandlers()` from `autotel-tanstack/testing`.
|
|
59
|
+
*
|
|
60
|
+
* @param baseUrl - Base URL of the app under test (e.g. 'http://localhost:3100')
|
|
61
|
+
* @param options.path - Path of the test-spans endpoint (default: '/api/test-spans')
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const spansClient = createTestSpansClient('http://localhost:3100');
|
|
66
|
+
*
|
|
67
|
+
* test('server function is traced', async ({ request }) => {
|
|
68
|
+
* await spansClient.clearSpans(request);
|
|
69
|
+
* await page.goto('/');
|
|
70
|
+
* // ... trigger action ...
|
|
71
|
+
* const spans = await spansClient.getSpans(request);
|
|
72
|
+
* expect(spans.find(s => s.name === 'sendMoney.handler')).toBeDefined();
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function createTestSpansClient(baseUrl: string, options?: {
|
|
77
|
+
path?: string;
|
|
78
|
+
}): {
|
|
79
|
+
getSpans(request: APIRequestContext): Promise<SerializedSpan[]>;
|
|
80
|
+
clearSpans(request: APIRequestContext): Promise<void>;
|
|
81
|
+
};
|
|
40
82
|
|
|
41
|
-
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, step, test };
|
|
83
|
+
export { AUTOTEL_ATTRIBUTE_ANNOTATION, type SerializedSpan, createGlobalSetup, createTestSpansClient, step, test };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as _playwright_test from '@playwright/test';
|
|
2
|
-
import {
|
|
2
|
+
import { APIRequestContext, Page, TestInfo } from '@playwright/test';
|
|
3
3
|
export { expect } from '@playwright/test';
|
|
4
4
|
import { AutotelConfig } from 'autotel';
|
|
5
5
|
export { OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
@@ -37,5 +37,47 @@ declare function step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
|
37
37
|
* Call autotel.init() with the given options (or defaults) so test spans are exported.
|
|
38
38
|
*/
|
|
39
39
|
declare function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void>;
|
|
40
|
+
/**
|
|
41
|
+
* Serialized span returned by the test-spans endpoint (matches autotel-tanstack/testing SerializedSpan).
|
|
42
|
+
*/
|
|
43
|
+
interface SerializedSpan {
|
|
44
|
+
name: string;
|
|
45
|
+
spanId: string;
|
|
46
|
+
traceId: string;
|
|
47
|
+
parentSpanId?: string;
|
|
48
|
+
attributes?: Record<string, unknown>;
|
|
49
|
+
status: {
|
|
50
|
+
code: number;
|
|
51
|
+
message?: string;
|
|
52
|
+
};
|
|
53
|
+
durationMs: number;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Creates a typed client for the test-spans HTTP endpoint.
|
|
57
|
+
*
|
|
58
|
+
* Pairs with `createTestSpansHandlers()` from `autotel-tanstack/testing`.
|
|
59
|
+
*
|
|
60
|
+
* @param baseUrl - Base URL of the app under test (e.g. 'http://localhost:3100')
|
|
61
|
+
* @param options.path - Path of the test-spans endpoint (default: '/api/test-spans')
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* ```typescript
|
|
65
|
+
* const spansClient = createTestSpansClient('http://localhost:3100');
|
|
66
|
+
*
|
|
67
|
+
* test('server function is traced', async ({ request }) => {
|
|
68
|
+
* await spansClient.clearSpans(request);
|
|
69
|
+
* await page.goto('/');
|
|
70
|
+
* // ... trigger action ...
|
|
71
|
+
* const spans = await spansClient.getSpans(request);
|
|
72
|
+
* expect(spans.find(s => s.name === 'sendMoney.handler')).toBeDefined();
|
|
73
|
+
* });
|
|
74
|
+
* ```
|
|
75
|
+
*/
|
|
76
|
+
declare function createTestSpansClient(baseUrl: string, options?: {
|
|
77
|
+
path?: string;
|
|
78
|
+
}): {
|
|
79
|
+
getSpans(request: APIRequestContext): Promise<SerializedSpan[]>;
|
|
80
|
+
clearSpans(request: APIRequestContext): Promise<void>;
|
|
81
|
+
};
|
|
40
82
|
|
|
41
|
-
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, step, test };
|
|
83
|
+
export { AUTOTEL_ATTRIBUTE_ANNOTATION, type SerializedSpan, createGlobalSetup, createTestSpansClient, step, test };
|
package/dist/index.js
CHANGED
|
@@ -187,7 +187,28 @@ function createGlobalSetup(initOptions) {
|
|
|
187
187
|
});
|
|
188
188
|
};
|
|
189
189
|
}
|
|
190
|
+
function createTestSpansClient(baseUrl, options) {
|
|
191
|
+
const base2 = baseUrl.replace(/\/$/, "");
|
|
192
|
+
const path = options?.path ?? "/api/test-spans";
|
|
193
|
+
const url = `${base2}${path}`;
|
|
194
|
+
return {
|
|
195
|
+
async getSpans(request) {
|
|
196
|
+
const res = await request.get(url);
|
|
197
|
+
if (!res.ok()) {
|
|
198
|
+
throw new Error(`GET ${path} failed: ${res.status()}`);
|
|
199
|
+
}
|
|
200
|
+
const body = await res.json();
|
|
201
|
+
return body.spans;
|
|
202
|
+
},
|
|
203
|
+
async clearSpans(request) {
|
|
204
|
+
const res = await request.delete(url);
|
|
205
|
+
if (!res.ok()) {
|
|
206
|
+
throw new Error(`DELETE ${path} failed: ${res.status()}`);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
}
|
|
190
211
|
|
|
191
|
-
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, step, test };
|
|
212
|
+
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, createTestSpansClient, step, test };
|
|
192
213
|
//# sourceMappingURL=index.js.map
|
|
193
214
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts"],"names":["base","otelContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * autotel-playwright\n *\n * Playwright fixture that creates one OTel span per test and injects W3C trace\n * context into requests to your API so \"test → API\" appears as one trace.\n *\n * @example\n * // globalSetup.ts: init({ service: 'e2e-tests' });\n * // In spec:\n * import { test, expect } from 'autotel-playwright';\n * test('checks health', async ({ page }) => {\n * await page.goto(API_BASE_URL + '/health'); // request gets traceparent\n * });\n * // Node-side API calls with trace context:\n * test('api health', async ({ requestWithTrace }) => {\n * const res = await requestWithTrace.get(API_BASE_URL + '/health');\n * expect(res.ok()).toBeTruthy();\n * });\n */\n\nimport { test as base } from '@playwright/test';\nimport type { Page, APIRequestContext, Request as PlaywrightRequest } from '@playwright/test';\nimport type { TestInfo } from '@playwright/test';\nimport type { AutotelConfig } from 'autotel';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n propagation,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'playwright-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\ninterface TracerProviderWithProcessor {\n addSpanProcessor(processor: unknown): void;\n}\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as TracerProviderWithProcessor).addSpanProcessor(\n new SimpleSpanProcessor(collector),\n );\n }\n }\n return collector;\n}\n\n/** Env keys for API base URL (requests to this origin get trace context injected). */\nconst ENV_API_BASE_URL = 'API_BASE_URL';\nconst ENV_API_ORIGIN = 'AUTOTEL_PLAYWRIGHT_API_ORIGIN';\n\nfunction getApiBaseUrls(): string[] {\n const a = process.env[ENV_API_BASE_URL];\n const b = process.env[ENV_API_ORIGIN];\n const urls: string[] = [];\n if (a) urls.push(a.replace(/\\/$/, ''));\n if (b) urls.push(b.replace(/\\/$/, ''));\n return [...new Set(urls)];\n}\n\n/**\n * Returns true if requestUrl should receive trace headers for the given apiBaseUrls.\n * When a base URL includes a path (e.g. http://localhost:3000/api), only requests\n * whose path starts with that path segment match; same-origin but different path\n * (e.g. /health) must not match to avoid leaking trace context to unrelated endpoints.\n */\nfunction urlMatchesApiOrigin(requestUrl: string, apiBaseUrls: string[]): boolean {\n if (apiBaseUrls.length === 0) return false;\n try {\n const u = new URL(requestUrl);\n const requestOrigin = u.origin;\n const requestPathname = u.pathname;\n return apiBaseUrls.some((base) => {\n try {\n const b = new URL(base);\n if (requestOrigin !== b.origin) return false;\n const basePathname = b.pathname.replace(/\\/$/, '') || '/';\n if (basePathname === '/') return true;\n return (\n requestPathname === basePathname || requestPathname.startsWith(basePathname + '/')\n );\n } catch {\n return requestUrl.startsWith(base);\n }\n });\n } catch {\n return apiBaseUrls.some((base) => requestUrl.startsWith(base));\n }\n}\n\n/** Annotation type for custom span attributes: description should be \"key=value\" or \"key=value1;key2=value2\". */\nexport const AUTOTEL_ATTRIBUTE_ANNOTATION = 'autotel.attribute';\n\nfunction setAttributesFromAnnotations(\n span: { setAttribute: (k: string, v: string | number | boolean) => void },\n testInfo: { annotations: Array<{ type: string; description?: string }> },\n): void {\n for (const a of testInfo.annotations) {\n if (a.type !== AUTOTEL_ATTRIBUTE_ANNOTATION || !a.description) continue;\n const entries = a.description.split(';');\n for (const entry of entries) {\n const parts = entry.split('=');\n if (parts.length >= 2) {\n const key = parts[0].trim();\n const value = parts.slice(1).join('=').trim();\n span.setAttribute(key, value);\n }\n }\n }\n}\n\n/** Internal: options for get/post/put/patch/delete/head/fetch that may include headers. */\ntype RequestOptions = Record<string, unknown> & { headers?: Record<string, string> };\n\nfunction mergeTraceHeaders(\n url: string,\n options: RequestOptions | undefined,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testName: string,\n): RequestOptions {\n const opts = options ?? {};\n if (!urlMatchesApiOrigin(url, apiBaseUrls)) return opts;\n return {\n ...opts,\n headers: { ...(opts.headers as Record<string, string>), ...carrier, 'x-test-name': testName },\n };\n}\n\n/** Wraps APIRequestContext so requests to API_BASE_URL get trace context injected. */\nfunction createRequestWithTrace(\n request: APIRequestContext,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testInfo: TestInfo,\n): APIRequestContext {\n const merge = (url: string, options?: RequestOptions) =>\n mergeTraceHeaders(url, options, apiBaseUrls, carrier, testInfo.title);\n\n return {\n get: (url: string, options?: RequestOptions) => request.get(url, merge(url, options)),\n post: (url: string, options?: RequestOptions) => request.post(url, merge(url, options)),\n put: (url: string, options?: RequestOptions) => request.put(url, merge(url, options)),\n patch: (url: string, options?: RequestOptions) => request.patch(url, merge(url, options)),\n delete: (url: string, options?: RequestOptions) => request.delete(url, merge(url, options)),\n head: (url: string, options?: RequestOptions) => request.head(url, merge(url, options)),\n fetch: (urlOrRequest: string | PlaywrightRequest, options?: RequestOptions) =>\n request.fetch(urlOrRequest, merge(typeof urlOrRequest === 'string' ? urlOrRequest : urlOrRequest.url(), options)),\n storageState: (options?: { path?: string }) => request.storageState(options),\n dispose: () => request.dispose(),\n } as APIRequestContext;\n}\n\ntype OtelTestSpan = {\n carrier: Record<string, string>;\n apiBaseUrls: string[];\n testInfo: TestInfo;\n};\n\nexport const test = base.extend<{\n page: Page;\n requestWithTrace: APIRequestContext;\n _otelTestSpan: OtelTestSpan;\n}>({\n _otelTestSpan: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n ensureCollector();\n const apiBaseUrls = getApiBaseUrls();\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const spanName = `e2e:${testInfo.title}`;\n const span = tracer.startSpan(spanName, {\n attributes: {\n 'test.title': testInfo.title,\n 'test.project': testInfo.project.name,\n 'test.file': testInfo.file ?? '',\n 'test.line': testInfo.line ?? 0,\n },\n });\n setAttributesFromAnnotations(span, testInfo);\n const ctx = otelTrace.setSpan(otelContext.active(), span);\n const carrier: Record<string, string> = {};\n otelContext.with(ctx, () => {\n propagation.inject(otelContext.active(), carrier);\n });\n try {\n await otelContext.with(ctx, () => use({ carrier, apiBaseUrls, testInfo }));\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n const traceId = span.spanContext().traceId;\n const rootSpanId = span.spanContext().spanId;\n const spans = collector!.drainTrace(traceId, rootSpanId);\n if (spans.length > 0) {\n testInfo.annotations.push({\n type: 'otel-spans',\n description: JSON.stringify(spans),\n });\n }\n }\n },\n { scope: 'test' },\n ],\n\n page: async ({ page, _otelTestSpan }, use) => {\n const { carrier, apiBaseUrls, testInfo } = _otelTestSpan;\n if (apiBaseUrls.length > 0) {\n await page.route('**/*', async (route) => {\n const request = route.request();\n const url = request.url();\n if (urlMatchesApiOrigin(url, apiBaseUrls)) {\n const headers = {\n ...request.headers(),\n ...carrier,\n 'x-test-name': testInfo.title,\n };\n await route.continue({ headers });\n } else {\n await route.continue();\n }\n });\n }\n await use(page);\n },\n\n requestWithTrace: async ({ request, _otelTestSpan }, use) => {\n const wrapped = createRequestWithTrace(\n request,\n _otelTestSpan.apiBaseUrls,\n _otelTestSpan.carrier,\n _otelTestSpan.testInfo,\n );\n await use(wrapped);\n },\n});\n\nexport { expect } from '@playwright/test';\n\n// Re-export trace context helpers for DX convenience\nexport {\n getTraceContext,\n resolveTraceUrl,\n isTracing,\n enrichWithTraceContext,\n} from 'autotel';\n\nexport type { OtelTraceContext } from 'autotel';\n\n/**\n * Runs a named step as a child span of the current test span. Use inside a test to get\n * step-level spans (e.g. \"step:login\", \"step:navigate\") under the test span in the same trace.\n *\n * @example\n * test('user flow', async ({ page }) => {\n * await step('login', async () => {\n * await page.click('button[type=submit]');\n * });\n * await step('open profile', async () => {\n * await page.goto('/profile');\n * });\n * });\n */\nexport async function step<T>(name: string, fn: () => Promise<T>): Promise<T> {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${name}`, {\n attributes: { 'step.name': name },\n });\n try {\n return await otelContext.with(otelTrace.setSpan(otelContext.active(), span), fn);\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n/**\n * Returns a function suitable for Playwright globalSetup that inits autotel.\n * Call autotel.init() with the given options (or defaults) so test spans are exported.\n */\nexport function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void> {\n return async () => {\n const { init } = await import('autotel');\n init({\n service: 'e2e-tests',\n debug: true,\n ...initOptions,\n });\n };\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["base","otelContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * autotel-playwright\n *\n * Playwright fixture that creates one OTel span per test and injects W3C trace\n * context into requests to your API so \"test → API\" appears as one trace.\n *\n * @example\n * // globalSetup.ts: init({ service: 'e2e-tests' });\n * // In spec:\n * import { test, expect } from 'autotel-playwright';\n * test('checks health', async ({ page }) => {\n * await page.goto(API_BASE_URL + '/health'); // request gets traceparent\n * });\n * // Node-side API calls with trace context:\n * test('api health', async ({ requestWithTrace }) => {\n * const res = await requestWithTrace.get(API_BASE_URL + '/health');\n * expect(res.ok()).toBeTruthy();\n * });\n */\n\nimport { test as base } from '@playwright/test';\nimport type { Page, APIRequestContext, Request as PlaywrightRequest } from '@playwright/test';\nimport type { TestInfo } from '@playwright/test';\nimport type { AutotelConfig } from 'autotel';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n propagation,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'playwright-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\ninterface TracerProviderWithProcessor {\n addSpanProcessor(processor: unknown): void;\n}\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as TracerProviderWithProcessor).addSpanProcessor(\n new SimpleSpanProcessor(collector),\n );\n }\n }\n return collector;\n}\n\n/** Env keys for API base URL (requests to this origin get trace context injected). */\nconst ENV_API_BASE_URL = 'API_BASE_URL';\nconst ENV_API_ORIGIN = 'AUTOTEL_PLAYWRIGHT_API_ORIGIN';\n\nfunction getApiBaseUrls(): string[] {\n const a = process.env[ENV_API_BASE_URL];\n const b = process.env[ENV_API_ORIGIN];\n const urls: string[] = [];\n if (a) urls.push(a.replace(/\\/$/, ''));\n if (b) urls.push(b.replace(/\\/$/, ''));\n return [...new Set(urls)];\n}\n\n/**\n * Returns true if requestUrl should receive trace headers for the given apiBaseUrls.\n * When a base URL includes a path (e.g. http://localhost:3000/api), only requests\n * whose path starts with that path segment match; same-origin but different path\n * (e.g. /health) must not match to avoid leaking trace context to unrelated endpoints.\n */\nfunction urlMatchesApiOrigin(requestUrl: string, apiBaseUrls: string[]): boolean {\n if (apiBaseUrls.length === 0) return false;\n try {\n const u = new URL(requestUrl);\n const requestOrigin = u.origin;\n const requestPathname = u.pathname;\n return apiBaseUrls.some((base) => {\n try {\n const b = new URL(base);\n if (requestOrigin !== b.origin) return false;\n const basePathname = b.pathname.replace(/\\/$/, '') || '/';\n if (basePathname === '/') return true;\n return (\n requestPathname === basePathname || requestPathname.startsWith(basePathname + '/')\n );\n } catch {\n return requestUrl.startsWith(base);\n }\n });\n } catch {\n return apiBaseUrls.some((base) => requestUrl.startsWith(base));\n }\n}\n\n/** Annotation type for custom span attributes: description should be \"key=value\" or \"key=value1;key2=value2\". */\nexport const AUTOTEL_ATTRIBUTE_ANNOTATION = 'autotel.attribute';\n\nfunction setAttributesFromAnnotations(\n span: { setAttribute: (k: string, v: string | number | boolean) => void },\n testInfo: { annotations: Array<{ type: string; description?: string }> },\n): void {\n for (const a of testInfo.annotations) {\n if (a.type !== AUTOTEL_ATTRIBUTE_ANNOTATION || !a.description) continue;\n const entries = a.description.split(';');\n for (const entry of entries) {\n const parts = entry.split('=');\n if (parts.length >= 2) {\n const key = parts[0].trim();\n const value = parts.slice(1).join('=').trim();\n span.setAttribute(key, value);\n }\n }\n }\n}\n\n/** Internal: options for get/post/put/patch/delete/head/fetch that may include headers. */\ntype RequestOptions = Record<string, unknown> & { headers?: Record<string, string> };\n\nfunction mergeTraceHeaders(\n url: string,\n options: RequestOptions | undefined,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testName: string,\n): RequestOptions {\n const opts = options ?? {};\n if (!urlMatchesApiOrigin(url, apiBaseUrls)) return opts;\n return {\n ...opts,\n headers: { ...(opts.headers as Record<string, string>), ...carrier, 'x-test-name': testName },\n };\n}\n\n/** Wraps APIRequestContext so requests to API_BASE_URL get trace context injected. */\nfunction createRequestWithTrace(\n request: APIRequestContext,\n apiBaseUrls: string[],\n carrier: Record<string, string>,\n testInfo: TestInfo,\n): APIRequestContext {\n const merge = (url: string, options?: RequestOptions) =>\n mergeTraceHeaders(url, options, apiBaseUrls, carrier, testInfo.title);\n\n return {\n get: (url: string, options?: RequestOptions) => request.get(url, merge(url, options)),\n post: (url: string, options?: RequestOptions) => request.post(url, merge(url, options)),\n put: (url: string, options?: RequestOptions) => request.put(url, merge(url, options)),\n patch: (url: string, options?: RequestOptions) => request.patch(url, merge(url, options)),\n delete: (url: string, options?: RequestOptions) => request.delete(url, merge(url, options)),\n head: (url: string, options?: RequestOptions) => request.head(url, merge(url, options)),\n fetch: (urlOrRequest: string | PlaywrightRequest, options?: RequestOptions) =>\n request.fetch(urlOrRequest, merge(typeof urlOrRequest === 'string' ? urlOrRequest : urlOrRequest.url(), options)),\n storageState: (options?: { path?: string }) => request.storageState(options),\n dispose: () => request.dispose(),\n } as APIRequestContext;\n}\n\ntype OtelTestSpan = {\n carrier: Record<string, string>;\n apiBaseUrls: string[];\n testInfo: TestInfo;\n};\n\nexport const test = base.extend<{\n page: Page;\n requestWithTrace: APIRequestContext;\n _otelTestSpan: OtelTestSpan;\n}>({\n _otelTestSpan: [\n // eslint-disable-next-line no-empty-pattern\n async ({}, use, testInfo) => {\n ensureCollector();\n const apiBaseUrls = getApiBaseUrls();\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const spanName = `e2e:${testInfo.title}`;\n const span = tracer.startSpan(spanName, {\n attributes: {\n 'test.title': testInfo.title,\n 'test.project': testInfo.project.name,\n 'test.file': testInfo.file ?? '',\n 'test.line': testInfo.line ?? 0,\n },\n });\n setAttributesFromAnnotations(span, testInfo);\n const ctx = otelTrace.setSpan(otelContext.active(), span);\n const carrier: Record<string, string> = {};\n otelContext.with(ctx, () => {\n propagation.inject(otelContext.active(), carrier);\n });\n try {\n await otelContext.with(ctx, () => use({ carrier, apiBaseUrls, testInfo }));\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n const traceId = span.spanContext().traceId;\n const rootSpanId = span.spanContext().spanId;\n const spans = collector!.drainTrace(traceId, rootSpanId);\n if (spans.length > 0) {\n testInfo.annotations.push({\n type: 'otel-spans',\n description: JSON.stringify(spans),\n });\n }\n }\n },\n { scope: 'test' },\n ],\n\n page: async ({ page, _otelTestSpan }, use) => {\n const { carrier, apiBaseUrls, testInfo } = _otelTestSpan;\n if (apiBaseUrls.length > 0) {\n await page.route('**/*', async (route) => {\n const request = route.request();\n const url = request.url();\n if (urlMatchesApiOrigin(url, apiBaseUrls)) {\n const headers = {\n ...request.headers(),\n ...carrier,\n 'x-test-name': testInfo.title,\n };\n await route.continue({ headers });\n } else {\n await route.continue();\n }\n });\n }\n await use(page);\n },\n\n requestWithTrace: async ({ request, _otelTestSpan }, use) => {\n const wrapped = createRequestWithTrace(\n request,\n _otelTestSpan.apiBaseUrls,\n _otelTestSpan.carrier,\n _otelTestSpan.testInfo,\n );\n await use(wrapped);\n },\n});\n\nexport { expect } from '@playwright/test';\n\n// Re-export trace context helpers for DX convenience\nexport {\n getTraceContext,\n resolveTraceUrl,\n isTracing,\n enrichWithTraceContext,\n} from 'autotel';\n\nexport type { OtelTraceContext } from 'autotel';\n\n/**\n * Runs a named step as a child span of the current test span. Use inside a test to get\n * step-level spans (e.g. \"step:login\", \"step:navigate\") under the test span in the same trace.\n *\n * @example\n * test('user flow', async ({ page }) => {\n * await step('login', async () => {\n * await page.click('button[type=submit]');\n * });\n * await step('open profile', async () => {\n * await page.goto('/profile');\n * });\n * });\n */\nexport async function step<T>(name: string, fn: () => Promise<T>): Promise<T> {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${name}`, {\n attributes: { 'step.name': name },\n });\n try {\n return await otelContext.with(otelTrace.setSpan(otelContext.active(), span), fn);\n } catch (error) {\n span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : 'Unknown error' });\n span.recordException(error instanceof Error ? error : new Error(String(error)));\n throw error;\n } finally {\n span.end();\n }\n}\n\n/**\n * Returns a function suitable for Playwright globalSetup that inits autotel.\n * Call autotel.init() with the given options (or defaults) so test spans are exported.\n */\nexport function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void> {\n return async () => {\n const { init } = await import('autotel');\n init({\n service: 'e2e-tests',\n debug: true,\n ...initOptions,\n });\n };\n}\n\n/**\n * Serialized span returned by the test-spans endpoint (matches autotel-tanstack/testing 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 * Creates a typed client for the test-spans HTTP endpoint.\n *\n * Pairs with `createTestSpansHandlers()` from `autotel-tanstack/testing`.\n *\n * @param baseUrl - Base URL of the app under test (e.g. 'http://localhost:3100')\n * @param options.path - Path of the test-spans endpoint (default: '/api/test-spans')\n *\n * @example\n * ```typescript\n * const spansClient = createTestSpansClient('http://localhost:3100');\n *\n * test('server function is traced', async ({ request }) => {\n * await spansClient.clearSpans(request);\n * await page.goto('/');\n * // ... trigger action ...\n * const spans = await spansClient.getSpans(request);\n * expect(spans.find(s => s.name === 'sendMoney.handler')).toBeDefined();\n * });\n * ```\n */\nexport function createTestSpansClient(\n baseUrl: string,\n options?: { path?: string },\n): {\n getSpans(request: APIRequestContext): Promise<SerializedSpan[]>;\n clearSpans(request: APIRequestContext): Promise<void>;\n} {\n const base = baseUrl.replace(/\\/$/, '');\n const path = options?.path ?? '/api/test-spans';\n const url = `${base}${path}`;\n\n return {\n async getSpans(request: APIRequestContext): Promise<SerializedSpan[]> {\n const res = await request.get(url);\n if (!res.ok()) {\n throw new Error(`GET ${path} failed: ${res.status()}`);\n }\n const body = await res.json() as { spans: SerializedSpan[] };\n return body.spans;\n },\n\n async clearSpans(request: APIRequestContext): Promise<void> {\n const res = await request.delete(url);\n if (!res.ok()) {\n throw new Error(`DELETE ${path} failed: ${res.status()}`);\n }\n },\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "autotel-playwright",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "Playwright fixture for OpenTelemetry: one span per test and trace context injected into API requests",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"README.md"
|
|
25
25
|
],
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"autotel": "2.25.
|
|
27
|
+
"autotel": "2.25.5"
|
|
28
28
|
},
|
|
29
29
|
"peerDependencies": {
|
|
30
30
|
"@playwright/test": ">=1.58.2"
|
package/src/index.test.ts
CHANGED
|
@@ -341,3 +341,86 @@ describe('trace context helper re-exports', () => {
|
|
|
341
341
|
expect(mod.enrichWithTraceContext).toBeTypeOf('function');
|
|
342
342
|
});
|
|
343
343
|
});
|
|
344
|
+
|
|
345
|
+
// Minimal APIRequestContext mock for createTestSpansClient tests
|
|
346
|
+
function makeRequest(responseBody: unknown, status = 200) {
|
|
347
|
+
const mockResponse = {
|
|
348
|
+
ok: () => status >= 200 && status < 300,
|
|
349
|
+
status: () => status,
|
|
350
|
+
json: async () => responseBody,
|
|
351
|
+
};
|
|
352
|
+
return {
|
|
353
|
+
get: vi.fn().mockResolvedValue(mockResponse),
|
|
354
|
+
delete: vi.fn().mockResolvedValue(mockResponse),
|
|
355
|
+
};
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
describe('createTestSpansClient', () => {
|
|
359
|
+
afterEach(() => {
|
|
360
|
+
vi.resetModules();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it('getSpans calls GET /api/test-spans', async () => {
|
|
364
|
+
const { createTestSpansClient } = await import('./index');
|
|
365
|
+
const mockSpan = {
|
|
366
|
+
name: 'sendMoney.handler',
|
|
367
|
+
spanId: 'abc',
|
|
368
|
+
traceId: 'trace',
|
|
369
|
+
attributes: {},
|
|
370
|
+
status: { code: 0 },
|
|
371
|
+
durationMs: 100,
|
|
372
|
+
};
|
|
373
|
+
const req = makeRequest({ spans: [mockSpan] });
|
|
374
|
+
const client = createTestSpansClient('http://localhost:3100');
|
|
375
|
+
const spans = await client.getSpans(req as any);
|
|
376
|
+
expect(req.get).toHaveBeenCalledWith('http://localhost:3100/api/test-spans');
|
|
377
|
+
expect(spans).toHaveLength(1);
|
|
378
|
+
expect(spans[0].name).toBe('sendMoney.handler');
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('getSpans uses custom path when provided', async () => {
|
|
382
|
+
const { createTestSpansClient } = await import('./index');
|
|
383
|
+
const req = makeRequest({ spans: [] });
|
|
384
|
+
const client = createTestSpansClient('http://localhost:3100', { path: '/custom/spans' });
|
|
385
|
+
await client.getSpans(req as any);
|
|
386
|
+
expect(req.get).toHaveBeenCalledWith('http://localhost:3100/custom/spans');
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it('getSpans throws when response not ok', async () => {
|
|
390
|
+
const { createTestSpansClient } = await import('./index');
|
|
391
|
+
const req = makeRequest({ error: 'not found' }, 404);
|
|
392
|
+
const client = createTestSpansClient('http://localhost:3100');
|
|
393
|
+
await expect(client.getSpans(req as any)).rejects.toThrow('GET /api/test-spans failed: 404');
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
it('clearSpans calls DELETE /api/test-spans', async () => {
|
|
397
|
+
const { createTestSpansClient } = await import('./index');
|
|
398
|
+
const req = makeRequest({ ok: true });
|
|
399
|
+
const client = createTestSpansClient('http://localhost:3100');
|
|
400
|
+
await client.clearSpans(req as any);
|
|
401
|
+
expect(req.delete).toHaveBeenCalledWith('http://localhost:3100/api/test-spans');
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('clearSpans uses custom path when provided', async () => {
|
|
405
|
+
const { createTestSpansClient } = await import('./index');
|
|
406
|
+
const req = makeRequest({ ok: true });
|
|
407
|
+
const client = createTestSpansClient('http://localhost:3100', { path: '/custom/spans' });
|
|
408
|
+
await client.clearSpans(req as any);
|
|
409
|
+
expect(req.delete).toHaveBeenCalledWith('http://localhost:3100/custom/spans');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('clearSpans throws when response not ok', async () => {
|
|
413
|
+
const { createTestSpansClient } = await import('./index');
|
|
414
|
+
const req = makeRequest({ error: 'not found' }, 404);
|
|
415
|
+
const client = createTestSpansClient('http://localhost:3100');
|
|
416
|
+
await expect(client.clearSpans(req as any)).rejects.toThrow('DELETE /api/test-spans failed: 404');
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('strips trailing slash from baseUrl', async () => {
|
|
420
|
+
const { createTestSpansClient } = await import('./index');
|
|
421
|
+
const req = makeRequest({ spans: [] });
|
|
422
|
+
const client = createTestSpansClient('http://localhost:3100/');
|
|
423
|
+
await client.getSpans(req as any);
|
|
424
|
+
expect(req.get).toHaveBeenCalledWith('http://localhost:3100/api/test-spans');
|
|
425
|
+
});
|
|
426
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -303,3 +303,67 @@ export function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<vo
|
|
|
303
303
|
});
|
|
304
304
|
};
|
|
305
305
|
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Serialized span returned by the test-spans endpoint (matches autotel-tanstack/testing SerializedSpan).
|
|
309
|
+
*/
|
|
310
|
+
export interface SerializedSpan {
|
|
311
|
+
name: string;
|
|
312
|
+
spanId: string;
|
|
313
|
+
traceId: string;
|
|
314
|
+
parentSpanId?: string;
|
|
315
|
+
attributes?: Record<string, unknown>;
|
|
316
|
+
status: { code: number; message?: string };
|
|
317
|
+
durationMs: number;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Creates a typed client for the test-spans HTTP endpoint.
|
|
322
|
+
*
|
|
323
|
+
* Pairs with `createTestSpansHandlers()` from `autotel-tanstack/testing`.
|
|
324
|
+
*
|
|
325
|
+
* @param baseUrl - Base URL of the app under test (e.g. 'http://localhost:3100')
|
|
326
|
+
* @param options.path - Path of the test-spans endpoint (default: '/api/test-spans')
|
|
327
|
+
*
|
|
328
|
+
* @example
|
|
329
|
+
* ```typescript
|
|
330
|
+
* const spansClient = createTestSpansClient('http://localhost:3100');
|
|
331
|
+
*
|
|
332
|
+
* test('server function is traced', async ({ request }) => {
|
|
333
|
+
* await spansClient.clearSpans(request);
|
|
334
|
+
* await page.goto('/');
|
|
335
|
+
* // ... trigger action ...
|
|
336
|
+
* const spans = await spansClient.getSpans(request);
|
|
337
|
+
* expect(spans.find(s => s.name === 'sendMoney.handler')).toBeDefined();
|
|
338
|
+
* });
|
|
339
|
+
* ```
|
|
340
|
+
*/
|
|
341
|
+
export function createTestSpansClient(
|
|
342
|
+
baseUrl: string,
|
|
343
|
+
options?: { path?: string },
|
|
344
|
+
): {
|
|
345
|
+
getSpans(request: APIRequestContext): Promise<SerializedSpan[]>;
|
|
346
|
+
clearSpans(request: APIRequestContext): Promise<void>;
|
|
347
|
+
} {
|
|
348
|
+
const base = baseUrl.replace(/\/$/, '');
|
|
349
|
+
const path = options?.path ?? '/api/test-spans';
|
|
350
|
+
const url = `${base}${path}`;
|
|
351
|
+
|
|
352
|
+
return {
|
|
353
|
+
async getSpans(request: APIRequestContext): Promise<SerializedSpan[]> {
|
|
354
|
+
const res = await request.get(url);
|
|
355
|
+
if (!res.ok()) {
|
|
356
|
+
throw new Error(`GET ${path} failed: ${res.status()}`);
|
|
357
|
+
}
|
|
358
|
+
const body = await res.json() as { spans: SerializedSpan[] };
|
|
359
|
+
return body.spans;
|
|
360
|
+
},
|
|
361
|
+
|
|
362
|
+
async clearSpans(request: APIRequestContext): Promise<void> {
|
|
363
|
+
const res = await request.delete(url);
|
|
364
|
+
if (!res.ok()) {
|
|
365
|
+
throw new Error(`DELETE ${path} failed: ${res.status()}`);
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|