autotel-playwright 0.4.45 → 0.4.47

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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["TestSpanCollector","SimpleSpanProcessor","base","otelTrace","otelContext","SpanStatusCode"],"sources":["../src/index.ts"],"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"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","names":["TestSpanCollector","SimpleSpanProcessor","base","otelTrace","otelContext","SpanStatusCode"],"sources":["../src/index.ts"],"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 {\n Page,\n APIRequestContext,\n Request as PlaywrightRequest,\n} 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(\n requestUrl: string,\n apiBaseUrls: string[],\n): 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 ||\n 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> & {\n headers?: Record<string, string>;\n};\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: {\n ...(opts.headers as Record<string, string>),\n ...carrier,\n 'x-test-name': testName,\n },\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) =>\n request.get(url, merge(url, options)),\n post: (url: string, options?: RequestOptions) =>\n request.post(url, merge(url, options)),\n put: (url: string, options?: RequestOptions) =>\n request.put(url, merge(url, options)),\n patch: (url: string, options?: RequestOptions) =>\n request.patch(url, merge(url, options)),\n delete: (url: string, options?: RequestOptions) =>\n request.delete(url, merge(url, options)),\n head: (url: string, options?: RequestOptions) =>\n request.head(url, merge(url, options)),\n fetch: (\n urlOrRequest: string | PlaywrightRequest,\n options?: RequestOptions,\n ) =>\n request.fetch(\n urlOrRequest,\n merge(\n typeof urlOrRequest === 'string' ? urlOrRequest : urlOrRequest.url(),\n options,\n ),\n ),\n storageState: (options?: { path?: string }) =>\n 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, () =>\n use({ carrier, apiBaseUrls, testInfo }),\n );\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\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(\n otelTrace.setSpan(otelContext.active(), span),\n fn,\n );\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\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(\n initOptions?: AutotelConfig,\n): () => 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"],"mappings":"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"}
package/dist/index.d.cts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { APIRequestContext, Page, TestInfo, expect } from "@playwright/test";
2
2
  import { AutotelConfig, OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from "autotel";
3
-
4
3
  //#region src/index.d.ts
5
4
  /** Annotation type for custom span attributes: description should be "key=value" or "key=value1;key2=value2". */
6
5
  declare const AUTOTEL_ATTRIBUTE_ANNOTATION = "autotel.attribute";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;cAqGa,4BAAA;AAAA,KA8DR,YAAA;EACH,OAAA,EAAS,MAAA;EACT,WAAA;EACA,QAAA,EAAU,QAAQ;AAAA;AAAA,cAGP,IAAA,6BAAI,QAAA,4BAAA,kBAAA,8BAAA,qBAAA;QACT,IAAA;oBACY,iBAAA;iBACH,YAAA;AAAA;;;;;AAuGjB;;;;;;;;;;iBAAsB,IAAA,IAAQ,IAAA,UAAc,EAAA,QAAU,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;;;;;iBAoB3D,iBAAA,CAAkB,WAAA,GAAc,aAAA,SAAsB,OAAO;;AApBD;AAoB5E;UAciB,cAAA;EACf,IAAA;EACA,MAAA;EACA,OAAA;EACA,YAAA;EACA,UAAA,GAAa,MAAM;EACnB,MAAA;IAAU,IAAA;IAAc,OAAA;EAAA;EACxB,UAAA;AAAA;;;;;;;;;;;;AAAU;AAwBZ;;;;;;;;;iBAAgB,qBAAA,CACd,OAAA,UACA,OAAA;EAAY,IAAA;AAAA;EAEZ,QAAA,CAAS,OAAA,EAAS,iBAAA,GAAoB,OAAA,CAAQ,cAAA;EAC9C,UAAA,CAAW,OAAA,EAAS,iBAAA,GAAoB,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":";;;;cA6Ga;KAoFR;EACH,SAAS;EACT;EACA,UAAU;;cAGC,iCAAI,oCAAA,gDAAA;EACT,MAAA;EACY,kBAAA;EACH,eAAA;8BAkFf,kDAAA;;;;;;;;;;;;;;;iBA4BoB,KAAK,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;iBA4B3D,kBACd,cAAc,sBACP;;;;UAcQ;EACf;EACA;EACA;EACA;EACA,aAAa;EACb;IAAU;IAAc;;EACxB;;;;;;;;;;;;;;;;;;;;;;;iBAwBc,sBACd,iBACA;EAAY;;EAEZ,SAAS,SAAS,oBAAoB,QAAQ;EAC9C,WAAW,SAAS,oBAAoB"}
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import { APIRequestContext, Page, TestInfo, expect } from "@playwright/test";
2
2
  import { AutotelConfig, OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from "autotel";
3
-
4
3
  //#region src/index.d.ts
5
4
  /** Annotation type for custom span attributes: description should be "key=value" or "key=value1;key2=value2". */
6
5
  declare const AUTOTEL_ATTRIBUTE_ANNOTATION = "autotel.attribute";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;cAqGa,4BAAA;AAAA,KA8DR,YAAA;EACH,OAAA,EAAS,MAAA;EACT,WAAA;EACA,QAAA,EAAU,QAAQ;AAAA;AAAA,cAGP,IAAA,6BAAI,QAAA,4BAAA,kBAAA,8BAAA,qBAAA;QACT,IAAA;oBACY,iBAAA;iBACH,YAAA;AAAA;;;;;AAuGjB;;;;;;;;;;iBAAsB,IAAA,IAAQ,IAAA,UAAc,EAAA,QAAU,OAAA,CAAQ,CAAA,IAAK,OAAA,CAAQ,CAAA;;;;;iBAoB3D,iBAAA,CAAkB,WAAA,GAAc,aAAA,SAAsB,OAAO;;AApBD;AAoB5E;UAciB,cAAA;EACf,IAAA;EACA,MAAA;EACA,OAAA;EACA,YAAA;EACA,UAAA,GAAa,MAAM;EACnB,MAAA;IAAU,IAAA;IAAc,OAAA;EAAA;EACxB,UAAA;AAAA;;;;;;;;;;;;AAAU;AAwBZ;;;;;;;;;iBAAgB,qBAAA,CACd,OAAA,UACA,OAAA;EAAY,IAAA;AAAA;EAEZ,QAAA,CAAS,OAAA,EAAS,iBAAA,GAAoB,OAAA,CAAQ,cAAA;EAC9C,UAAA,CAAW,OAAA,EAAS,iBAAA,GAAoB,OAAA;AAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":";;;;cA6Ga;KAoFR;EACH,SAAS;EACT;EACA,UAAU;;cAGC,iCAAI,oCAAA,gDAAA;EACT,MAAA;EACY,kBAAA;EACH,eAAA;8BAkFf,kDAAA;;;;;;;;;;;;;;;iBA4BoB,KAAK,GAAG,cAAc,UAAU,QAAQ,KAAK,QAAQ;;;;;iBA4B3D,kBACd,cAAc,sBACP;;;;UAcQ;EACf;EACA;EACA;EACA;EACA,aAAa;EACb;IAAU;IAAc;;EACxB;;;;;;;;;;;;;;;;;;;;;;;iBAwBc,sBACd,iBACA;EAAY;;EAEZ,SAAS,SAAS,oBAAoB,QAAQ;EAC9C,WAAW,SAAS,oBAAoB"}
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["base","otelContext"],"sources":["../src/index.ts"],"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"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["base","otelContext"],"sources":["../src/index.ts"],"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 {\n Page,\n APIRequestContext,\n Request as PlaywrightRequest,\n} 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(\n requestUrl: string,\n apiBaseUrls: string[],\n): 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 ||\n 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> & {\n headers?: Record<string, string>;\n};\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: {\n ...(opts.headers as Record<string, string>),\n ...carrier,\n 'x-test-name': testName,\n },\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) =>\n request.get(url, merge(url, options)),\n post: (url: string, options?: RequestOptions) =>\n request.post(url, merge(url, options)),\n put: (url: string, options?: RequestOptions) =>\n request.put(url, merge(url, options)),\n patch: (url: string, options?: RequestOptions) =>\n request.patch(url, merge(url, options)),\n delete: (url: string, options?: RequestOptions) =>\n request.delete(url, merge(url, options)),\n head: (url: string, options?: RequestOptions) =>\n request.head(url, merge(url, options)),\n fetch: (\n urlOrRequest: string | PlaywrightRequest,\n options?: RequestOptions,\n ) =>\n request.fetch(\n urlOrRequest,\n merge(\n typeof urlOrRequest === 'string' ? urlOrRequest : urlOrRequest.url(),\n options,\n ),\n ),\n storageState: (options?: { path?: string }) =>\n 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, () =>\n use({ carrier, apiBaseUrls, testInfo }),\n );\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\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(\n otelTrace.setSpan(otelContext.active(), span),\n fn,\n );\n } catch (error) {\n span.setStatus({\n code: SpanStatusCode.ERROR,\n message: error instanceof Error ? error.message : 'Unknown error',\n });\n span.recordException(\n error instanceof Error ? error : new Error(String(error)),\n );\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(\n initOptions?: AutotelConfig,\n): () => 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"],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.cjs","names":["SpanStatusCode","otelTrace","otelContext"],"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Optional Playwright reporter that creates OTel spans for each test and step.\n * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.\n *\n * Use when you want test/step timing and hierarchy in OTLP from the runner side.\n * For \"test → API\" in one trace (worker side), use the test fixture and requestWithTrace.\n *\n * @example\n * // playwright.config.ts\n * import { defineConfig } from '@playwright/test';\n *\n * export default defineConfig({\n * reporter: [['list'], ['autotel-playwright/reporter']],\n * globalSetup: './globalSetup.ts', // must call init()\n * });\n */\n\nimport type {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n TestStep,\n} from '@playwright/test/reporter';\nimport { getTracer, context as otelContext, otelTrace, SpanStatusCode } from 'autotel';\n\nconst TRACER_NAME = 'playwright-reporter';\nconst TRACER_VERSION = '0.1.0';\n\nfunction testKey(test: TestCase): string {\n return test.id;\n}\n\n/** Convert Playwright TestError (no `name` field) to a standard Error for OTel. */\nfunction toError(testError: { message?: string; stack?: string }): Error {\n const err = new Error(testError.message ?? 'Unknown error');\n if (testError.stack) err.stack = testError.stack;\n return err;\n}\n\n/**\n * Playwright Reporter that creates one span per test and one per step (as children).\n * Requires autotel.init() in globalSetup so spans are exported.\n */\nclass OtelReporter implements Reporter {\n private testSpans = new Map<string, ReturnType<ReturnType<typeof getTracer>['startSpan']>>();\n private stepSpans = new WeakMap<TestStep, ReturnType<ReturnType<typeof getTracer>['startSpan']>>();\n\n onTestBegin(test: TestCase, _result: TestResult): void {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`e2e:${test.title}`, {\n attributes: {\n 'test.title': test.title,\n 'test.file': test.location?.file ?? '',\n 'test.line': test.location?.line ?? 0,\n },\n });\n this.testSpans.set(testKey(test), span);\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n const key = testKey(test);\n const span = this.testSpans.get(key);\n if (span) {\n if (result.status !== 'passed' && result.status !== 'skipped') {\n span.setStatus({ code: SpanStatusCode.ERROR });\n if (result.error) span.recordException(toError(result.error));\n }\n span.end();\n this.testSpans.delete(key);\n }\n }\n\n onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {\n const testSpan = this.testSpans.get(testKey(test));\n if (!testSpan) return;\n otelContext.with(otelTrace.setSpan(otelContext.active(), testSpan), () => {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${step.title}`, {\n attributes: { 'step.name': step.title },\n });\n this.stepSpans.set(step, span);\n });\n }\n\n onStepEnd(_test: TestCase, result: TestResult, step: TestStep): void {\n const span = this.stepSpans.get(step);\n if (span) {\n if (step.error) {\n span.recordException(toError(step.error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n span.end();\n this.stepSpans.delete(step);\n }\n }\n\n onBegin?(_config: FullConfig, _suite: Suite): void {}\n onEnd?(_result: FullResult): void {}\n}\n\nexport { OtelReporter };\nexport default OtelReporter;\n"],"mappings":";;;;;;AA4BA,MAAM,cAAc;AACpB,MAAM,iBAAiB;AAEvB,SAAS,QAAQ,MAAwB;CACvC,OAAO,KAAK;AACd;;AAGA,SAAS,QAAQ,WAAwD;CACvE,MAAM,MAAM,IAAI,MAAM,UAAU,WAAW,eAAe;CAC1D,IAAI,UAAU,OAAO,IAAI,QAAQ,UAAU;CAC3C,OAAO;AACT;;;;;AAMA,IAAM,eAAN,MAAuC;CACrC,4BAAoB,IAAI,IAAmE;CAC3F,4BAAoB,IAAI,QAAyE;CAEjG,YAAY,MAAgB,SAA2B;EAErD,MAAM,QAAA,GAAA,QAAA,UAAA,CADmB,aAAa,cACpB,CAAC,CAAC,UAAU,OAAO,KAAK,SAAS,EACjD,YAAY;GACV,cAAc,KAAK;GACnB,aAAa,KAAK,UAAU,QAAQ;GACpC,aAAa,KAAK,UAAU,QAAQ;EACtC,EACF,CAAC;EACD,KAAK,UAAU,IAAI,QAAQ,IAAI,GAAG,IAAI;CACxC;CAEA,UAAU,MAAgB,QAA0B;EAClD,MAAM,MAAM,QAAQ,IAAI;EACxB,MAAM,OAAO,KAAK,UAAU,IAAI,GAAG;EACnC,IAAI,MAAM;GACR,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,WAAW;IAC7D,KAAK,UAAU,EAAE,MAAMA,QAAAA,eAAe,MAAM,CAAC;IAC7C,IAAI,OAAO,OAAO,KAAK,gBAAgB,QAAQ,OAAO,KAAK,CAAC;GAC9D;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,GAAG;EAC3B;CACF;CAEA,YAAY,MAAgB,SAAqB,MAAsB;EACrE,MAAM,WAAW,KAAK,UAAU,IAAI,QAAQ,IAAI,CAAC;EACjD,IAAI,CAAC,UAAU;EACf,QAAA,QAAY,KAAKC,QAAAA,UAAU,QAAQC,QAAAA,QAAY,OAAO,GAAG,QAAQ,SAAS;GAExE,MAAM,QAAA,GAAA,QAAA,UAAA,CADmB,aAAa,cACpB,CAAC,CAAC,UAAU,QAAQ,KAAK,SAAS,EAClD,YAAY,EAAE,aAAa,KAAK,MAAM,EACxC,CAAC;GACD,KAAK,UAAU,IAAI,MAAM,IAAI;EAC/B,CAAC;CACH;CAEA,UAAU,OAAiB,QAAoB,MAAsB;EACnE,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;EACpC,IAAI,MAAM;GACR,IAAI,KAAK,OAAO;IACd,KAAK,gBAAgB,QAAQ,KAAK,KAAK,CAAC;IACxC,KAAK,UAAU,EAAE,MAAMF,QAAAA,eAAe,MAAM,CAAC;GAC/C;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,IAAI;EAC5B;CACF;CAEA,QAAS,SAAqB,QAAqB,CAAC;CACpD,MAAO,SAA2B,CAAC;AACrC"}
1
+ {"version":3,"file":"reporter.cjs","names":["SpanStatusCode","otelTrace","otelContext"],"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Optional Playwright reporter that creates OTel spans for each test and step.\n * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.\n *\n * Use when you want test/step timing and hierarchy in OTLP from the runner side.\n * For \"test → API\" in one trace (worker side), use the test fixture and requestWithTrace.\n *\n * @example\n * // playwright.config.ts\n * import { defineConfig } from '@playwright/test';\n *\n * export default defineConfig({\n * reporter: [['list'], ['autotel-playwright/reporter']],\n * globalSetup: './globalSetup.ts', // must call init()\n * });\n */\n\nimport type {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n TestStep,\n} from '@playwright/test/reporter';\nimport {\n getTracer,\n context as otelContext,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\n\nconst TRACER_NAME = 'playwright-reporter';\nconst TRACER_VERSION = '0.1.0';\n\nfunction testKey(test: TestCase): string {\n return test.id;\n}\n\n/** Convert Playwright TestError (no `name` field) to a standard Error for OTel. */\nfunction toError(testError: { message?: string; stack?: string }): Error {\n const err = new Error(testError.message ?? 'Unknown error');\n if (testError.stack) err.stack = testError.stack;\n return err;\n}\n\n/**\n * Playwright Reporter that creates one span per test and one per step (as children).\n * Requires autotel.init() in globalSetup so spans are exported.\n */\nclass OtelReporter implements Reporter {\n private testSpans = new Map<\n string,\n ReturnType<ReturnType<typeof getTracer>['startSpan']>\n >();\n private stepSpans = new WeakMap<\n TestStep,\n ReturnType<ReturnType<typeof getTracer>['startSpan']>\n >();\n\n onTestBegin(test: TestCase, _result: TestResult): void {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`e2e:${test.title}`, {\n attributes: {\n 'test.title': test.title,\n 'test.file': test.location?.file ?? '',\n 'test.line': test.location?.line ?? 0,\n },\n });\n this.testSpans.set(testKey(test), span);\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n const key = testKey(test);\n const span = this.testSpans.get(key);\n if (span) {\n if (result.status !== 'passed' && result.status !== 'skipped') {\n span.setStatus({ code: SpanStatusCode.ERROR });\n if (result.error) span.recordException(toError(result.error));\n }\n span.end();\n this.testSpans.delete(key);\n }\n }\n\n onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {\n const testSpan = this.testSpans.get(testKey(test));\n if (!testSpan) return;\n otelContext.with(otelTrace.setSpan(otelContext.active(), testSpan), () => {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${step.title}`, {\n attributes: { 'step.name': step.title },\n });\n this.stepSpans.set(step, span);\n });\n }\n\n onStepEnd(_test: TestCase, result: TestResult, step: TestStep): void {\n const span = this.stepSpans.get(step);\n if (span) {\n if (step.error) {\n span.recordException(toError(step.error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n span.end();\n this.stepSpans.delete(step);\n }\n }\n\n onBegin?(_config: FullConfig, _suite: Suite): void {}\n onEnd?(_result: FullResult): void {}\n}\n\nexport { OtelReporter };\nexport default OtelReporter;\n"],"mappings":";;;;;;AAiCA,MAAM,cAAc;AACpB,MAAM,iBAAiB;AAEvB,SAAS,QAAQ,MAAwB;CACvC,OAAO,KAAK;AACd;;AAGA,SAAS,QAAQ,WAAwD;CACvE,MAAM,MAAM,IAAI,MAAM,UAAU,WAAW,eAAe;CAC1D,IAAI,UAAU,OAAO,IAAI,QAAQ,UAAU;CAC3C,OAAO;AACT;;;;;AAMA,IAAM,eAAN,MAAuC;CACrC,4BAAoB,IAAI,IAGtB;CACF,4BAAoB,IAAI,QAGtB;CAEF,YAAY,MAAgB,SAA2B;EAErD,MAAM,QAAA,GAAA,QAAA,UAAA,CADmB,aAAa,cACpB,CAAC,CAAC,UAAU,OAAO,KAAK,SAAS,EACjD,YAAY;GACV,cAAc,KAAK;GACnB,aAAa,KAAK,UAAU,QAAQ;GACpC,aAAa,KAAK,UAAU,QAAQ;EACtC,EACF,CAAC;EACD,KAAK,UAAU,IAAI,QAAQ,IAAI,GAAG,IAAI;CACxC;CAEA,UAAU,MAAgB,QAA0B;EAClD,MAAM,MAAM,QAAQ,IAAI;EACxB,MAAM,OAAO,KAAK,UAAU,IAAI,GAAG;EACnC,IAAI,MAAM;GACR,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,WAAW;IAC7D,KAAK,UAAU,EAAE,MAAMA,QAAAA,eAAe,MAAM,CAAC;IAC7C,IAAI,OAAO,OAAO,KAAK,gBAAgB,QAAQ,OAAO,KAAK,CAAC;GAC9D;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,GAAG;EAC3B;CACF;CAEA,YAAY,MAAgB,SAAqB,MAAsB;EACrE,MAAM,WAAW,KAAK,UAAU,IAAI,QAAQ,IAAI,CAAC;EACjD,IAAI,CAAC,UAAU;EACf,QAAA,QAAY,KAAKC,QAAAA,UAAU,QAAQC,QAAAA,QAAY,OAAO,GAAG,QAAQ,SAAS;GAExE,MAAM,QAAA,GAAA,QAAA,UAAA,CADmB,aAAa,cACpB,CAAC,CAAC,UAAU,QAAQ,KAAK,SAAS,EAClD,YAAY,EAAE,aAAa,KAAK,MAAM,EACxC,CAAC;GACD,KAAK,UAAU,IAAI,MAAM,IAAI;EAC/B,CAAC;CACH;CAEA,UAAU,OAAiB,QAAoB,MAAsB;EACnE,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;EACpC,IAAI,MAAM;GACR,IAAI,KAAK,OAAO;IACd,KAAK,gBAAgB,QAAQ,KAAK,KAAK,CAAC;IACxC,KAAK,UAAU,EAAE,MAAMF,QAAAA,eAAe,MAAM,CAAC;GAC/C;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,IAAI;EAC5B;CACF;CAEA,QAAS,SAAqB,QAAqB,CAAC;CACpD,MAAO,SAA2B,CAAC;AACrC"}
@@ -1,5 +1,4 @@
1
1
  import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult, TestStep } from "@playwright/test/reporter";
2
-
3
2
  //#region src/reporter.d.ts
4
3
  /**
5
4
  * Playwright Reporter that creates one span per test and one per step (as children).
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.d.cts","names":[],"sources":["../src/reporter.ts"],"mappings":";;;;;;;cA8CM,YAAA,YAAwB,QAAA;EAAA,QACpB,SAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAY,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,UAAA;EAYrC,SAAA,CAAU,IAAA,EAAM,QAAA,EAAU,MAAA,EAAQ,UAAA;EAalC,WAAA,CAAY,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,UAAA,EAAY,IAAA,EAAM,QAAA;EAYvD,SAAA,CAAU,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,UAAA,EAAY,IAAA,EAAM,QAAA;EAYrD,OAAA,EAAS,OAAA,EAAS,UAAA,EAAY,MAAA,EAAQ,KAAA;EACtC,KAAA,EAAO,OAAA,EAAS,UAAA;AAAA"}
1
+ {"version":3,"file":"reporter.d.cts","names":[],"sources":["../src/reporter.ts"],"mappings":";;;;;;cAmDM,wBAAwB;UACpB;UAIA;EAKR,YAAY,MAAM,UAAU,SAAS;EAYrC,UAAU,MAAM,UAAU,QAAQ;EAalC,YAAY,MAAM,UAAU,SAAS,YAAY,MAAM;EAYvD,UAAU,OAAO,UAAU,QAAQ,YAAY,MAAM;EAYrD,SAAS,SAAS,YAAY,QAAQ;EACtC,OAAO,SAAS"}
@@ -1,5 +1,4 @@
1
1
  import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult, TestStep } from "@playwright/test/reporter";
2
-
3
2
  //#region src/reporter.d.ts
4
3
  /**
5
4
  * Playwright Reporter that creates one span per test and one per step (as children).
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.d.ts","names":[],"sources":["../src/reporter.ts"],"mappings":";;;;;;;cA8CM,YAAA,YAAwB,QAAA;EAAA,QACpB,SAAA;EAAA,QACA,SAAA;EAER,WAAA,CAAY,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,UAAA;EAYrC,SAAA,CAAU,IAAA,EAAM,QAAA,EAAU,MAAA,EAAQ,UAAA;EAalC,WAAA,CAAY,IAAA,EAAM,QAAA,EAAU,OAAA,EAAS,UAAA,EAAY,IAAA,EAAM,QAAA;EAYvD,SAAA,CAAU,KAAA,EAAO,QAAA,EAAU,MAAA,EAAQ,UAAA,EAAY,IAAA,EAAM,QAAA;EAYrD,OAAA,EAAS,OAAA,EAAS,UAAA,EAAY,MAAA,EAAQ,KAAA;EACtC,KAAA,EAAO,OAAA,EAAS,UAAA;AAAA"}
1
+ {"version":3,"file":"reporter.d.ts","names":[],"sources":["../src/reporter.ts"],"mappings":";;;;;;cAmDM,wBAAwB;UACpB;UAIA;EAKR,YAAY,MAAM,UAAU,SAAS;EAYrC,UAAU,MAAM,UAAU,QAAQ;EAalC,YAAY,MAAM,UAAU,SAAS,YAAY,MAAM;EAYvD,UAAU,OAAO,UAAU,QAAQ,YAAY,MAAM;EAYrD,SAAS,SAAS,YAAY,QAAQ;EACtC,OAAO,SAAS"}
@@ -1 +1 @@
1
- {"version":3,"file":"reporter.js","names":["otelContext"],"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Optional Playwright reporter that creates OTel spans for each test and step.\n * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.\n *\n * Use when you want test/step timing and hierarchy in OTLP from the runner side.\n * For \"test → API\" in one trace (worker side), use the test fixture and requestWithTrace.\n *\n * @example\n * // playwright.config.ts\n * import { defineConfig } from '@playwright/test';\n *\n * export default defineConfig({\n * reporter: [['list'], ['autotel-playwright/reporter']],\n * globalSetup: './globalSetup.ts', // must call init()\n * });\n */\n\nimport type {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n TestStep,\n} from '@playwright/test/reporter';\nimport { getTracer, context as otelContext, otelTrace, SpanStatusCode } from 'autotel';\n\nconst TRACER_NAME = 'playwright-reporter';\nconst TRACER_VERSION = '0.1.0';\n\nfunction testKey(test: TestCase): string {\n return test.id;\n}\n\n/** Convert Playwright TestError (no `name` field) to a standard Error for OTel. */\nfunction toError(testError: { message?: string; stack?: string }): Error {\n const err = new Error(testError.message ?? 'Unknown error');\n if (testError.stack) err.stack = testError.stack;\n return err;\n}\n\n/**\n * Playwright Reporter that creates one span per test and one per step (as children).\n * Requires autotel.init() in globalSetup so spans are exported.\n */\nclass OtelReporter implements Reporter {\n private testSpans = new Map<string, ReturnType<ReturnType<typeof getTracer>['startSpan']>>();\n private stepSpans = new WeakMap<TestStep, ReturnType<ReturnType<typeof getTracer>['startSpan']>>();\n\n onTestBegin(test: TestCase, _result: TestResult): void {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`e2e:${test.title}`, {\n attributes: {\n 'test.title': test.title,\n 'test.file': test.location?.file ?? '',\n 'test.line': test.location?.line ?? 0,\n },\n });\n this.testSpans.set(testKey(test), span);\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n const key = testKey(test);\n const span = this.testSpans.get(key);\n if (span) {\n if (result.status !== 'passed' && result.status !== 'skipped') {\n span.setStatus({ code: SpanStatusCode.ERROR });\n if (result.error) span.recordException(toError(result.error));\n }\n span.end();\n this.testSpans.delete(key);\n }\n }\n\n onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {\n const testSpan = this.testSpans.get(testKey(test));\n if (!testSpan) return;\n otelContext.with(otelTrace.setSpan(otelContext.active(), testSpan), () => {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${step.title}`, {\n attributes: { 'step.name': step.title },\n });\n this.stepSpans.set(step, span);\n });\n }\n\n onStepEnd(_test: TestCase, result: TestResult, step: TestStep): void {\n const span = this.stepSpans.get(step);\n if (span) {\n if (step.error) {\n span.recordException(toError(step.error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n span.end();\n this.stepSpans.delete(step);\n }\n }\n\n onBegin?(_config: FullConfig, _suite: Suite): void {}\n onEnd?(_result: FullResult): void {}\n}\n\nexport { OtelReporter };\nexport default OtelReporter;\n"],"mappings":";;AA4BA,MAAM,cAAc;AACpB,MAAM,iBAAiB;AAEvB,SAAS,QAAQ,MAAwB;CACvC,OAAO,KAAK;AACd;;AAGA,SAAS,QAAQ,WAAwD;CACvE,MAAM,MAAM,IAAI,MAAM,UAAU,WAAW,eAAe;CAC1D,IAAI,UAAU,OAAO,IAAI,QAAQ,UAAU;CAC3C,OAAO;AACT;;;;;AAMA,IAAM,eAAN,MAAuC;CACrC,4BAAoB,IAAI,IAAmE;CAC3F,4BAAoB,IAAI,QAAyE;CAEjG,YAAY,MAAgB,SAA2B;EAErD,MAAM,OADS,UAAU,aAAa,cACpB,CAAC,CAAC,UAAU,OAAO,KAAK,SAAS,EACjD,YAAY;GACV,cAAc,KAAK;GACnB,aAAa,KAAK,UAAU,QAAQ;GACpC,aAAa,KAAK,UAAU,QAAQ;EACtC,EACF,CAAC;EACD,KAAK,UAAU,IAAI,QAAQ,IAAI,GAAG,IAAI;CACxC;CAEA,UAAU,MAAgB,QAA0B;EAClD,MAAM,MAAM,QAAQ,IAAI;EACxB,MAAM,OAAO,KAAK,UAAU,IAAI,GAAG;EACnC,IAAI,MAAM;GACR,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,WAAW;IAC7D,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;IAC7C,IAAI,OAAO,OAAO,KAAK,gBAAgB,QAAQ,OAAO,KAAK,CAAC;GAC9D;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,GAAG;EAC3B;CACF;CAEA,YAAY,MAAgB,SAAqB,MAAsB;EACrE,MAAM,WAAW,KAAK,UAAU,IAAI,QAAQ,IAAI,CAAC;EACjD,IAAI,CAAC,UAAU;EACf,QAAY,KAAK,UAAU,QAAQA,QAAY,OAAO,GAAG,QAAQ,SAAS;GAExE,MAAM,OADS,UAAU,aAAa,cACpB,CAAC,CAAC,UAAU,QAAQ,KAAK,SAAS,EAClD,YAAY,EAAE,aAAa,KAAK,MAAM,EACxC,CAAC;GACD,KAAK,UAAU,IAAI,MAAM,IAAI;EAC/B,CAAC;CACH;CAEA,UAAU,OAAiB,QAAoB,MAAsB;EACnE,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;EACpC,IAAI,MAAM;GACR,IAAI,KAAK,OAAO;IACd,KAAK,gBAAgB,QAAQ,KAAK,KAAK,CAAC;IACxC,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;GAC/C;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,IAAI;EAC5B;CACF;CAEA,QAAS,SAAqB,QAAqB,CAAC;CACpD,MAAO,SAA2B,CAAC;AACrC"}
1
+ {"version":3,"file":"reporter.js","names":["otelContext"],"sources":["../src/reporter.ts"],"sourcesContent":["/**\n * Optional Playwright reporter that creates OTel spans for each test and step.\n * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.\n *\n * Use when you want test/step timing and hierarchy in OTLP from the runner side.\n * For \"test → API\" in one trace (worker side), use the test fixture and requestWithTrace.\n *\n * @example\n * // playwright.config.ts\n * import { defineConfig } from '@playwright/test';\n *\n * export default defineConfig({\n * reporter: [['list'], ['autotel-playwright/reporter']],\n * globalSetup: './globalSetup.ts', // must call init()\n * });\n */\n\nimport type {\n FullConfig,\n FullResult,\n Reporter,\n Suite,\n TestCase,\n TestResult,\n TestStep,\n} from '@playwright/test/reporter';\nimport {\n getTracer,\n context as otelContext,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\n\nconst TRACER_NAME = 'playwright-reporter';\nconst TRACER_VERSION = '0.1.0';\n\nfunction testKey(test: TestCase): string {\n return test.id;\n}\n\n/** Convert Playwright TestError (no `name` field) to a standard Error for OTel. */\nfunction toError(testError: { message?: string; stack?: string }): Error {\n const err = new Error(testError.message ?? 'Unknown error');\n if (testError.stack) err.stack = testError.stack;\n return err;\n}\n\n/**\n * Playwright Reporter that creates one span per test and one per step (as children).\n * Requires autotel.init() in globalSetup so spans are exported.\n */\nclass OtelReporter implements Reporter {\n private testSpans = new Map<\n string,\n ReturnType<ReturnType<typeof getTracer>['startSpan']>\n >();\n private stepSpans = new WeakMap<\n TestStep,\n ReturnType<ReturnType<typeof getTracer>['startSpan']>\n >();\n\n onTestBegin(test: TestCase, _result: TestResult): void {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`e2e:${test.title}`, {\n attributes: {\n 'test.title': test.title,\n 'test.file': test.location?.file ?? '',\n 'test.line': test.location?.line ?? 0,\n },\n });\n this.testSpans.set(testKey(test), span);\n }\n\n onTestEnd(test: TestCase, result: TestResult): void {\n const key = testKey(test);\n const span = this.testSpans.get(key);\n if (span) {\n if (result.status !== 'passed' && result.status !== 'skipped') {\n span.setStatus({ code: SpanStatusCode.ERROR });\n if (result.error) span.recordException(toError(result.error));\n }\n span.end();\n this.testSpans.delete(key);\n }\n }\n\n onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void {\n const testSpan = this.testSpans.get(testKey(test));\n if (!testSpan) return;\n otelContext.with(otelTrace.setSpan(otelContext.active(), testSpan), () => {\n const tracer = getTracer(TRACER_NAME, TRACER_VERSION);\n const span = tracer.startSpan(`step:${step.title}`, {\n attributes: { 'step.name': step.title },\n });\n this.stepSpans.set(step, span);\n });\n }\n\n onStepEnd(_test: TestCase, result: TestResult, step: TestStep): void {\n const span = this.stepSpans.get(step);\n if (span) {\n if (step.error) {\n span.recordException(toError(step.error));\n span.setStatus({ code: SpanStatusCode.ERROR });\n }\n span.end();\n this.stepSpans.delete(step);\n }\n }\n\n onBegin?(_config: FullConfig, _suite: Suite): void {}\n onEnd?(_result: FullResult): void {}\n}\n\nexport { OtelReporter };\nexport default OtelReporter;\n"],"mappings":";;AAiCA,MAAM,cAAc;AACpB,MAAM,iBAAiB;AAEvB,SAAS,QAAQ,MAAwB;CACvC,OAAO,KAAK;AACd;;AAGA,SAAS,QAAQ,WAAwD;CACvE,MAAM,MAAM,IAAI,MAAM,UAAU,WAAW,eAAe;CAC1D,IAAI,UAAU,OAAO,IAAI,QAAQ,UAAU;CAC3C,OAAO;AACT;;;;;AAMA,IAAM,eAAN,MAAuC;CACrC,4BAAoB,IAAI,IAGtB;CACF,4BAAoB,IAAI,QAGtB;CAEF,YAAY,MAAgB,SAA2B;EAErD,MAAM,OADS,UAAU,aAAa,cACpB,CAAC,CAAC,UAAU,OAAO,KAAK,SAAS,EACjD,YAAY;GACV,cAAc,KAAK;GACnB,aAAa,KAAK,UAAU,QAAQ;GACpC,aAAa,KAAK,UAAU,QAAQ;EACtC,EACF,CAAC;EACD,KAAK,UAAU,IAAI,QAAQ,IAAI,GAAG,IAAI;CACxC;CAEA,UAAU,MAAgB,QAA0B;EAClD,MAAM,MAAM,QAAQ,IAAI;EACxB,MAAM,OAAO,KAAK,UAAU,IAAI,GAAG;EACnC,IAAI,MAAM;GACR,IAAI,OAAO,WAAW,YAAY,OAAO,WAAW,WAAW;IAC7D,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;IAC7C,IAAI,OAAO,OAAO,KAAK,gBAAgB,QAAQ,OAAO,KAAK,CAAC;GAC9D;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,GAAG;EAC3B;CACF;CAEA,YAAY,MAAgB,SAAqB,MAAsB;EACrE,MAAM,WAAW,KAAK,UAAU,IAAI,QAAQ,IAAI,CAAC;EACjD,IAAI,CAAC,UAAU;EACf,QAAY,KAAK,UAAU,QAAQA,QAAY,OAAO,GAAG,QAAQ,SAAS;GAExE,MAAM,OADS,UAAU,aAAa,cACpB,CAAC,CAAC,UAAU,QAAQ,KAAK,SAAS,EAClD,YAAY,EAAE,aAAa,KAAK,MAAM,EACxC,CAAC;GACD,KAAK,UAAU,IAAI,MAAM,IAAI;EAC/B,CAAC;CACH;CAEA,UAAU,OAAiB,QAAoB,MAAsB;EACnE,MAAM,OAAO,KAAK,UAAU,IAAI,IAAI;EACpC,IAAI,MAAM;GACR,IAAI,KAAK,OAAO;IACd,KAAK,gBAAgB,QAAQ,KAAK,KAAK,CAAC;IACxC,KAAK,UAAU,EAAE,MAAM,eAAe,MAAM,CAAC;GAC/C;GACA,KAAK,IAAI;GACT,KAAK,UAAU,OAAO,IAAI;EAC5B;CACF;CAEA,QAAS,SAAqB,QAAqB,CAAC;CACpD,MAAO,SAA2B,CAAC;AACrC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-playwright",
3
- "version": "0.4.45",
3
+ "version": "0.4.47",
4
4
  "description": "Playwright fixture for OpenTelemetry: one span per test and trace context injected into API requests",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -25,7 +25,7 @@
25
25
  "skills"
26
26
  ],
27
27
  "dependencies": {
28
- "autotel": "4.2.5"
28
+ "autotel": "5.0.0"
29
29
  },
30
30
  "peerDependencies": {
31
31
  "@playwright/test": ">=1.61.1"
@@ -37,8 +37,8 @@
37
37
  },
38
38
  "devDependencies": {
39
39
  "@playwright/test": "^1.61.1",
40
- "@types/node": "^26.1.0",
41
- "tsdown": "^0.22.3",
40
+ "@types/node": "^26.1.1",
41
+ "tsdown": "^0.22.12",
42
42
  "typescript": "^6.0.3",
43
43
  "vitest": "^4.1.10"
44
44
  },
@@ -10,8 +10,8 @@ Playwright fixture that creates one OTel span per e2e test and propagates W3C tr
10
10
 
11
11
  Two independent features ship in this package:
12
12
 
13
- - **Test fixture** (`autotel-playwright`) — worker-side; spans follow each test, headers are injected per request.
14
- - **OTel Reporter** (`autotel-playwright/reporter`) — runner-side; creates spans for tests and steps from the Playwright runner process.
13
+ - **Test fixture** (`autotel-playwright`): worker-side; spans follow each test, headers are injected per request.
14
+ - **OTel Reporter** (`autotel-playwright/reporter`): runner-side; creates spans for tests and steps from the Playwright runner process.
15
15
 
16
16
  ## Setup
17
17
 
@@ -74,11 +74,11 @@ test('api health', async ({ requestWithTrace }) => {
74
74
 
75
75
  ### Fixtures
76
76
 
77
- | Fixture | Type | Description |
78
- |---|---|---|
79
- | `page` | `Page` | Standard Playwright `Page`; auto-injects trace headers for routes matching `API_BASE_URL` |
80
- | `requestWithTrace` | `APIRequestContext` | Wraps `request`; injects `traceparent` + `x-test-name` on all matching URLs |
81
- | `_otelTestSpan` | internal | Creates and manages the root test span; do not use directly |
77
+ | Fixture | Type | Description |
78
+ | ------------------ | ------------------- | ----------------------------------------------------------------------------------------- |
79
+ | `page` | `Page` | Standard Playwright `Page`; auto-injects trace headers for routes matching `API_BASE_URL` |
80
+ | `requestWithTrace` | `APIRequestContext` | Wraps `request`; injects `traceparent` + `x-test-name` on all matching URLs |
81
+ | `_otelTestSpan` | internal | Creates and manages the root test span; do not use directly |
82
82
 
83
83
  ### Named steps as child spans
84
84
 
@@ -131,13 +131,13 @@ test('server function is traced', async ({ page, request }) => {
131
131
  await page.click('button#send');
132
132
 
133
133
  const spans = await spansClient.getSpans(request);
134
- expect(spans.find(s => s.name === 'sendMoney.handler')).toBeDefined();
134
+ expect(spans.find((s) => s.name === 'sendMoney.handler')).toBeDefined();
135
135
  });
136
136
  ```
137
137
 
138
138
  ### OTel Reporter (runner-side)
139
139
 
140
- The reporter at `autotel-playwright/reporter` creates spans in the runner process — useful when you want test hierarchy in OTLP from outside the worker:
140
+ The reporter at `autotel-playwright/reporter` creates spans in the runner process. Useful when you want test hierarchy in OTLP from outside the worker:
141
141
 
142
142
  ```ts
143
143
  // playwright.config.ts
@@ -149,7 +149,11 @@ This is separate from the fixture. Both can be used together: fixture spans flow
149
149
  ### Trace context helpers
150
150
 
151
151
  ```ts
152
- import { getTraceContext, resolveTraceUrl, isTracing } from 'autotel-playwright';
152
+ import {
153
+ getTraceContext,
154
+ resolveTraceUrl,
155
+ isTracing,
156
+ } from 'autotel-playwright';
153
157
 
154
158
  test('logs trace link', async ({ page }) => {
155
159
  if (isTracing()) {
@@ -163,15 +167,17 @@ test('logs trace link', async ({ page }) => {
163
167
 
164
168
  ### HIGH: Not calling `createGlobalSetup` (or `init()`) in globalSetup
165
169
 
166
- Without `init()`, no spans are exported — the fixture creates spans but they are never sent to the backend.
170
+ Without `init()`, no spans are exported. The fixture creates spans but they are never sent to the backend.
167
171
 
168
172
  Wrong:
173
+
169
174
  ```ts
170
175
  // playwright.config.ts — no globalSetup
171
176
  export default defineConfig({ ... });
172
177
  ```
173
178
 
174
179
  Correct:
180
+
175
181
  ```ts
176
182
  // globalSetup.ts
177
183
  import { createGlobalSetup } from 'autotel-playwright';
@@ -186,12 +192,14 @@ export default defineConfig({ globalSetup: './globalSetup.ts', ... });
186
192
  The fixtures (`requestWithTrace`, trace-aware `page`) only exist on the extended `test` object.
187
193
 
188
194
  Wrong:
195
+
189
196
  ```ts
190
197
  import { test, expect } from '@playwright/test';
191
198
  // requestWithTrace fixture is not available
192
199
  ```
193
200
 
194
201
  Correct:
202
+
195
203
  ```ts
196
204
  import { test, expect } from 'autotel-playwright';
197
205
  ```
@@ -201,13 +209,14 @@ import { test, expect } from 'autotel-playwright';
201
209
  The package strips trailing slashes internally, but path-prefix matching only works if the env var is set correctly. Setting `API_BASE_URL=http://localhost:3000/api/` is fine, but setting `API_BASE_URL=http://localhost:3000` will inject trace headers on ALL requests to that origin, including unrelated paths like `/static/`.
202
210
 
203
211
  Use a path-scoped URL when you only want a subset of routes to receive headers:
212
+
204
213
  ```bash
205
214
  API_BASE_URL=http://localhost:3000/api
206
215
  ```
207
216
 
208
217
  ### MEDIUM: Confusing the fixture `page` with the reporter
209
218
 
210
- The fixture injects headers in the worker process per-test. The reporter creates spans in the runner process. They do not share span context — they produce separate traces. Use the fixture for test-to-API tracing; use the reporter for standalone test timing in OTLP.
219
+ The fixture injects headers in the worker process per-test. The reporter creates spans in the runner process. They do not share span context. They produce separate traces. Use the fixture for test-to-API tracing; use the reporter for standalone test timing in OTLP.
211
220
 
212
221
  ### MEDIUM: Using `step()` outside a test span context
213
222