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.
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +0 -1
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/reporter.cjs.map +1 -1
- package/dist/reporter.d.cts +0 -1
- package/dist/reporter.d.cts.map +1 -1
- package/dist/reporter.d.ts +0 -1
- package/dist/reporter.d.ts.map +1 -1
- package/dist/reporter.js.map +1 -1
- package/package.json +4 -4
- package/skills/autotel-playwright/SKILL.md +21 -12
package/dist/index.cjs.map
CHANGED
|
@@ -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";
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","names":[],"sources":["../src/index.ts"],"mappings":"
|
|
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";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/index.ts"],"mappings":"
|
|
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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"}
|
package/dist/reporter.cjs.map
CHANGED
|
@@ -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 {
|
|
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"}
|
package/dist/reporter.d.cts
CHANGED
package/dist/reporter.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.cts","names":[],"sources":["../src/reporter.ts"],"mappings":"
|
|
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"}
|
package/dist/reporter.d.ts
CHANGED
package/dist/reporter.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reporter.d.ts","names":[],"sources":["../src/reporter.ts"],"mappings":"
|
|
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"}
|
package/dist/reporter.js.map
CHANGED
|
@@ -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 {
|
|
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.
|
|
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": "
|
|
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.
|
|
41
|
-
"tsdown": "^0.22.
|
|
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`)
|
|
14
|
-
- **OTel Reporter** (`autotel-playwright/reporter`)
|
|
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
|
|
78
|
-
|
|
79
|
-
| `page`
|
|
80
|
-
| `requestWithTrace` | `APIRequestContext` | Wraps `request`; injects `traceparent` + `x-test-name` on all matching URLs
|
|
81
|
-
| `_otelTestSpan`
|
|
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
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|