autotel-playwright 0.1.0

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.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"names":["base","otelContext"],"mappings":"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","file":"index.js","sourcesContent":["/**\n * autotel-playwright\n *\n * Playwright fixture that creates one OTel span per test and injects W3C trace\n * context into requests to your API so \"test → API\" appears as one trace.\n *\n * @example\n * // globalSetup.ts: init({ service: 'e2e-tests' });\n * // In spec:\n * import { test, expect } from 'autotel-playwright';\n * test('checks health', async ({ page }) => {\n * await page.goto(API_BASE_URL + '/health'); // request gets traceparent\n * });\n * // Node-side API calls with trace context:\n * test('api health', async ({ requestWithTrace }) => {\n * const res = await requestWithTrace.get(API_BASE_URL + '/health');\n * expect(res.ok()).toBeTruthy();\n * });\n */\n\nimport { test as base } from '@playwright/test';\nimport type { Page, APIRequestContext, Request as PlaywrightRequest } from '@playwright/test';\nimport type { TestInfo } from '@playwright/test';\nimport type { AutotelConfig } from 'autotel';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n propagation,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'playwright-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\nfunction ensureCollector(): TestSpanCollector {\n if (!collector) {\n collector = new TestSpanCollector();\n const provider = getAutotelTracerProvider();\n if ('addSpanProcessor' in provider) {\n (provider as any).addSpanProcessor(new SimpleSpanProcessor(collector));\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 propagation.inject(ctx, carrier);\n try {\n await use({ carrier, apiBaseUrls, testInfo });\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"]}
@@ -0,0 +1,76 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var autotel = require('autotel');
6
+
7
+ // src/reporter.ts
8
+ var TRACER_NAME = "playwright-reporter";
9
+ var TRACER_VERSION = "0.1.0";
10
+ function testKey(test) {
11
+ return test.id;
12
+ }
13
+ function toError(testError) {
14
+ const err = new Error(testError.message ?? "Unknown error");
15
+ if (testError.stack) err.stack = testError.stack;
16
+ return err;
17
+ }
18
+ var OtelReporter = class {
19
+ testSpans = /* @__PURE__ */ new Map();
20
+ stepSpans = /* @__PURE__ */ new WeakMap();
21
+ onTestBegin(test, _result) {
22
+ const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
23
+ const span = tracer.startSpan(`e2e:${test.title}`, {
24
+ attributes: {
25
+ "test.title": test.title,
26
+ "test.file": test.location?.file ?? "",
27
+ "test.line": test.location?.line ?? 0
28
+ }
29
+ });
30
+ this.testSpans.set(testKey(test), span);
31
+ }
32
+ onTestEnd(test, result) {
33
+ const key = testKey(test);
34
+ const span = this.testSpans.get(key);
35
+ if (span) {
36
+ if (result.status !== "passed" && result.status !== "skipped") {
37
+ span.setStatus({ code: autotel.SpanStatusCode.ERROR });
38
+ if (result.error) span.recordException(toError(result.error));
39
+ }
40
+ span.end();
41
+ this.testSpans.delete(key);
42
+ }
43
+ }
44
+ onStepBegin(test, _result, step) {
45
+ const testSpan = this.testSpans.get(testKey(test));
46
+ if (!testSpan) return;
47
+ autotel.context.with(autotel.otelTrace.setSpan(autotel.context.active(), testSpan), () => {
48
+ const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
49
+ const span = tracer.startSpan(`step:${step.title}`, {
50
+ attributes: { "step.name": step.title }
51
+ });
52
+ this.stepSpans.set(step, span);
53
+ });
54
+ }
55
+ onStepEnd(_test, result, step) {
56
+ const span = this.stepSpans.get(step);
57
+ if (span) {
58
+ if (step.error) {
59
+ span.recordException(toError(step.error));
60
+ span.setStatus({ code: autotel.SpanStatusCode.ERROR });
61
+ }
62
+ span.end();
63
+ this.stepSpans.delete(step);
64
+ }
65
+ }
66
+ onBegin(_config, _suite) {
67
+ }
68
+ onEnd(_result) {
69
+ }
70
+ };
71
+ var reporter_default = OtelReporter;
72
+
73
+ exports.OtelReporter = OtelReporter;
74
+ exports.default = reporter_default;
75
+ //# sourceMappingURL=reporter.cjs.map
76
+ //# sourceMappingURL=reporter.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporter.ts"],"names":["getTracer","SpanStatusCode","otelContext","otelTrace"],"mappings":"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","file":"reporter.cjs","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"]}
@@ -0,0 +1,35 @@
1
+ import { Reporter, TestCase, TestResult, TestStep, FullConfig, Suite, FullResult } from '@playwright/test/reporter';
2
+
3
+ /**
4
+ * Optional Playwright reporter that creates OTel spans for each test and step.
5
+ * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.
6
+ *
7
+ * Use when you want test/step timing and hierarchy in OTLP from the runner side.
8
+ * For "test → API" in one trace (worker side), use the test fixture and requestWithTrace.
9
+ *
10
+ * @example
11
+ * // playwright.config.ts
12
+ * import { defineConfig } from '@playwright/test';
13
+ *
14
+ * export default defineConfig({
15
+ * reporter: [['list'], ['autotel-playwright/reporter']],
16
+ * globalSetup: './globalSetup.ts', // must call init()
17
+ * });
18
+ */
19
+
20
+ /**
21
+ * Playwright Reporter that creates one span per test and one per step (as children).
22
+ * Requires autotel.init() in globalSetup so spans are exported.
23
+ */
24
+ declare class OtelReporter implements Reporter {
25
+ private testSpans;
26
+ private stepSpans;
27
+ onTestBegin(test: TestCase, _result: TestResult): void;
28
+ onTestEnd(test: TestCase, result: TestResult): void;
29
+ onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void;
30
+ onStepEnd(_test: TestCase, result: TestResult, step: TestStep): void;
31
+ onBegin?(_config: FullConfig, _suite: Suite): void;
32
+ onEnd?(_result: FullResult): void;
33
+ }
34
+
35
+ export { OtelReporter, OtelReporter as default };
@@ -0,0 +1,35 @@
1
+ import { Reporter, TestCase, TestResult, TestStep, FullConfig, Suite, FullResult } from '@playwright/test/reporter';
2
+
3
+ /**
4
+ * Optional Playwright reporter that creates OTel spans for each test and step.
5
+ * Runs in the runner process; ensure autotel.init() is called in globalSetup so spans are exported.
6
+ *
7
+ * Use when you want test/step timing and hierarchy in OTLP from the runner side.
8
+ * For "test → API" in one trace (worker side), use the test fixture and requestWithTrace.
9
+ *
10
+ * @example
11
+ * // playwright.config.ts
12
+ * import { defineConfig } from '@playwright/test';
13
+ *
14
+ * export default defineConfig({
15
+ * reporter: [['list'], ['autotel-playwright/reporter']],
16
+ * globalSetup: './globalSetup.ts', // must call init()
17
+ * });
18
+ */
19
+
20
+ /**
21
+ * Playwright Reporter that creates one span per test and one per step (as children).
22
+ * Requires autotel.init() in globalSetup so spans are exported.
23
+ */
24
+ declare class OtelReporter implements Reporter {
25
+ private testSpans;
26
+ private stepSpans;
27
+ onTestBegin(test: TestCase, _result: TestResult): void;
28
+ onTestEnd(test: TestCase, result: TestResult): void;
29
+ onStepBegin(test: TestCase, _result: TestResult, step: TestStep): void;
30
+ onStepEnd(_test: TestCase, result: TestResult, step: TestStep): void;
31
+ onBegin?(_config: FullConfig, _suite: Suite): void;
32
+ onEnd?(_result: FullResult): void;
33
+ }
34
+
35
+ export { OtelReporter, OtelReporter as default };
@@ -0,0 +1,71 @@
1
+ import { getTracer, SpanStatusCode, context, otelTrace } from 'autotel';
2
+
3
+ // src/reporter.ts
4
+ var TRACER_NAME = "playwright-reporter";
5
+ var TRACER_VERSION = "0.1.0";
6
+ function testKey(test) {
7
+ return test.id;
8
+ }
9
+ function toError(testError) {
10
+ const err = new Error(testError.message ?? "Unknown error");
11
+ if (testError.stack) err.stack = testError.stack;
12
+ return err;
13
+ }
14
+ var OtelReporter = class {
15
+ testSpans = /* @__PURE__ */ new Map();
16
+ stepSpans = /* @__PURE__ */ new WeakMap();
17
+ onTestBegin(test, _result) {
18
+ const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
19
+ const span = tracer.startSpan(`e2e:${test.title}`, {
20
+ attributes: {
21
+ "test.title": test.title,
22
+ "test.file": test.location?.file ?? "",
23
+ "test.line": test.location?.line ?? 0
24
+ }
25
+ });
26
+ this.testSpans.set(testKey(test), span);
27
+ }
28
+ onTestEnd(test, result) {
29
+ const key = testKey(test);
30
+ const span = this.testSpans.get(key);
31
+ if (span) {
32
+ if (result.status !== "passed" && result.status !== "skipped") {
33
+ span.setStatus({ code: SpanStatusCode.ERROR });
34
+ if (result.error) span.recordException(toError(result.error));
35
+ }
36
+ span.end();
37
+ this.testSpans.delete(key);
38
+ }
39
+ }
40
+ onStepBegin(test, _result, step) {
41
+ const testSpan = this.testSpans.get(testKey(test));
42
+ if (!testSpan) return;
43
+ context.with(otelTrace.setSpan(context.active(), testSpan), () => {
44
+ const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
45
+ const span = tracer.startSpan(`step:${step.title}`, {
46
+ attributes: { "step.name": step.title }
47
+ });
48
+ this.stepSpans.set(step, span);
49
+ });
50
+ }
51
+ onStepEnd(_test, result, step) {
52
+ const span = this.stepSpans.get(step);
53
+ if (span) {
54
+ if (step.error) {
55
+ span.recordException(toError(step.error));
56
+ span.setStatus({ code: SpanStatusCode.ERROR });
57
+ }
58
+ span.end();
59
+ this.stepSpans.delete(step);
60
+ }
61
+ }
62
+ onBegin(_config, _suite) {
63
+ }
64
+ onEnd(_result) {
65
+ }
66
+ };
67
+ var reporter_default = OtelReporter;
68
+
69
+ export { OtelReporter, reporter_default as default };
70
+ //# sourceMappingURL=reporter.js.map
71
+ //# sourceMappingURL=reporter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/reporter.ts"],"names":["otelContext"],"mappings":"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","file":"reporter.js","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"]}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "autotel-playwright",
3
+ "version": "0.1.0",
4
+ "description": "Playwright fixture for OpenTelemetry: one span per test and trace context injected into API requests",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "sideEffects": false,
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ },
15
+ "./reporter": {
16
+ "types": "./dist/reporter.d.ts",
17
+ "import": "./dist/reporter.js",
18
+ "require": "./dist/reporter.cjs"
19
+ }
20
+ },
21
+ "files": ["dist", "src", "README.md"],
22
+ "scripts": {
23
+ "build": "tsup",
24
+ "dev": "tsup --watch",
25
+ "type-check": "tsc --noEmit",
26
+ "test": "vitest run",
27
+ "test:watch": "vitest",
28
+ "lint": "eslint src",
29
+ "clean": "rimraf dist"
30
+ },
31
+ "dependencies": {
32
+ "autotel": "workspace:*"
33
+ },
34
+ "peerDependencies": {
35
+ "@playwright/test": ">=1.40.0"
36
+ },
37
+ "peerDependenciesMeta": {
38
+ "@playwright/test": {
39
+ "optional": false
40
+ }
41
+ },
42
+ "devDependencies": {
43
+ "@playwright/test": "^1.49.0",
44
+ "tsup": "^8.5.1",
45
+ "typescript": "^5.9.3",
46
+ "vitest": "^4.0.18"
47
+ },
48
+ "repository": {
49
+ "type": "git",
50
+ "url": "https://github.com/jagreehal/autotel",
51
+ "directory": "packages/autotel-playwright"
52
+ }
53
+ }
@@ -0,0 +1,291 @@
1
+ import { afterEach, describe, expect, it, vi } from 'vitest';
2
+
3
+ type Fixtures = {
4
+ _otelTestSpan?: TestSpanFixtureFn | [TestSpanFixtureFn, { scope: 'test' }];
5
+ requestWithTrace?: (
6
+ args: { request: unknown; _otelTestSpan: unknown },
7
+ use: (wrapped: any) => Promise<void>,
8
+ ) => Promise<void>;
9
+ };
10
+
11
+ type TestSpanFixtureFn = (
12
+ args: Record<string, never>,
13
+ use: (spanData: unknown) => Promise<void>,
14
+ testInfo: {
15
+ annotations: Array<{ type: string; description?: string }>;
16
+ file?: string;
17
+ line?: number;
18
+ project: { name: string };
19
+ title: string;
20
+ },
21
+ ) => Promise<void>;
22
+
23
+ const state: { fixtures?: Fixtures } = {};
24
+ let spanIdCounter = 0;
25
+ let mockDrainResult: unknown[] = [];
26
+ const createdSpans: Array<{
27
+ end: ReturnType<typeof vi.fn>;
28
+ recordException: ReturnType<typeof vi.fn>;
29
+ setAttribute: ReturnType<typeof vi.fn>;
30
+ setStatus: ReturnType<typeof vi.fn>;
31
+ spanContext: () => { traceId: string; spanId: string };
32
+ }> = [];
33
+
34
+ vi.mock('@playwright/test', () => ({
35
+ expect,
36
+ test: {
37
+ extend: (fixtures: Fixtures) => {
38
+ state.fixtures = fixtures;
39
+ return fixtures;
40
+ },
41
+ },
42
+ }));
43
+
44
+ vi.mock('autotel', () => ({
45
+ SpanStatusCode: { UNSET: 0, OK: 1, ERROR: 2 },
46
+ context: {
47
+ active: () => ({}),
48
+ with: (_ctx: unknown, fn: () => Promise<unknown>) => fn(),
49
+ },
50
+ getTracer: () => ({
51
+ startSpan: () => {
52
+ const id = String(++spanIdCounter);
53
+ const span = {
54
+ end: vi.fn(),
55
+ recordException: vi.fn(),
56
+ setAttribute: vi.fn(),
57
+ setStatus: vi.fn(),
58
+ spanContext: () => ({ traceId: `trace-${id}`, spanId: `span-${id}` }),
59
+ };
60
+ createdSpans.push(span);
61
+ return span;
62
+ },
63
+ }),
64
+ propagation: {
65
+ inject: (_ctx: unknown, carrier: Record<string, string>) => {
66
+ carrier.traceparent = '00-testtrace-testspan-01';
67
+ },
68
+ },
69
+ otelTrace: {
70
+ setSpan: () => ({}),
71
+ },
72
+ getAutotelTracerProvider: vi.fn(() => ({})),
73
+ getTraceContext: vi.fn(() => null),
74
+ resolveTraceUrl: vi.fn(() => undefined),
75
+ isTracing: vi.fn(() => false),
76
+ enrichWithTraceContext: vi.fn((obj: unknown) => obj),
77
+ }));
78
+
79
+ vi.mock('autotel/test-span-collector', () => ({
80
+ TestSpanCollector: class {
81
+ export = vi.fn();
82
+ drainTrace = vi.fn(() => mockDrainResult);
83
+ shutdown = vi.fn(() => Promise.resolve());
84
+ forceFlush = vi.fn(() => Promise.resolve());
85
+ },
86
+ }));
87
+
88
+ vi.mock('autotel/processors', () => ({
89
+ SimpleSpanProcessor: class {
90
+ constructor() {}
91
+ },
92
+ }));
93
+
94
+ describe('autotel-playwright requestWithTrace.fetch', () => {
95
+ afterEach(() => {
96
+ delete process.env.API_BASE_URL;
97
+ delete process.env.AUTOTEL_PLAYWRIGHT_API_ORIGIN;
98
+ state.fixtures = undefined;
99
+ createdSpans.length = 0;
100
+ spanIdCounter = 0;
101
+ mockDrainResult = [];
102
+ vi.resetModules();
103
+ });
104
+
105
+ it('injects trace headers for string URLs', async () => {
106
+ await import('./index');
107
+
108
+ const requestWithTraceFixture = state.fixtures?.requestWithTrace;
109
+ expect(requestWithTraceFixture).toBeTypeOf('function');
110
+
111
+ const fetchSpy = vi.fn(async () => ({ ok: true }));
112
+ const request = { fetch: fetchSpy } as any;
113
+ let wrapped: any;
114
+
115
+ await requestWithTraceFixture?.(
116
+ {
117
+ request,
118
+ _otelTestSpan: {
119
+ apiBaseUrls: ['http://localhost:3000'],
120
+ carrier: { traceparent: '00-testtrace-testspan-01' },
121
+ testInfo: { title: 'fetch string url' },
122
+ },
123
+ },
124
+ async (value) => {
125
+ wrapped = value;
126
+ },
127
+ );
128
+
129
+ await wrapped.fetch('http://localhost:3000/health');
130
+
131
+ expect(fetchSpy).toHaveBeenCalledWith(
132
+ 'http://localhost:3000/health',
133
+ expect.objectContaining({
134
+ headers: expect.objectContaining({
135
+ traceparent: '00-testtrace-testspan-01',
136
+ 'x-test-name': 'fetch string url',
137
+ }),
138
+ }),
139
+ );
140
+ });
141
+
142
+ it('injects trace headers for Playwright Request objects', async () => {
143
+ await import('./index');
144
+
145
+ const requestWithTraceFixture = state.fixtures?.requestWithTrace;
146
+ expect(requestWithTraceFixture).toBeTypeOf('function');
147
+
148
+ const fetchSpy = vi.fn(async () => ({ ok: true }));
149
+ const request = { fetch: fetchSpy } as any;
150
+ let wrapped: any;
151
+
152
+ await requestWithTraceFixture?.(
153
+ {
154
+ request,
155
+ _otelTestSpan: {
156
+ apiBaseUrls: ['http://localhost:3000'],
157
+ carrier: { traceparent: '00-testtrace-testspan-01' },
158
+ testInfo: { title: 'fetch request object' },
159
+ },
160
+ },
161
+ async (value) => {
162
+ wrapped = value;
163
+ },
164
+ );
165
+
166
+ const playwrightRequestLike = {
167
+ url: () => 'http://localhost:3000/health',
168
+ };
169
+
170
+ await wrapped.fetch(playwrightRequestLike);
171
+
172
+ expect(fetchSpy).toHaveBeenCalledWith(
173
+ playwrightRequestLike,
174
+ expect.objectContaining({
175
+ headers: expect.objectContaining({
176
+ traceparent: '00-testtrace-testspan-01',
177
+ 'x-test-name': 'fetch request object',
178
+ }),
179
+ }),
180
+ );
181
+ });
182
+ });
183
+
184
+ describe('autotel-playwright annotations', () => {
185
+ afterEach(() => {
186
+ delete process.env.API_BASE_URL;
187
+ delete process.env.AUTOTEL_PLAYWRIGHT_API_ORIGIN;
188
+ state.fixtures = undefined;
189
+ createdSpans.length = 0;
190
+ spanIdCounter = 0;
191
+ mockDrainResult = [];
192
+ vi.resetModules();
193
+ });
194
+
195
+ it('supports semicolon-delimited autotel.attribute key-value pairs', async () => {
196
+ await import('./index');
197
+
198
+ const spanFixture = state.fixtures?._otelTestSpan;
199
+ const spanFixtureFn = Array.isArray(spanFixture) ? spanFixture[0] : spanFixture;
200
+ expect(spanFixtureFn).toBeTypeOf('function');
201
+
202
+ await spanFixtureFn?.(
203
+ {},
204
+ async () => {},
205
+ {
206
+ annotations: [
207
+ {
208
+ type: 'autotel.attribute',
209
+ description: 'team=checkout;flow=signup',
210
+ },
211
+ ],
212
+ project: { name: 'chromium' },
213
+ title: 'annotation parsing',
214
+ },
215
+ );
216
+
217
+ const span = createdSpans[0];
218
+ expect(span).toBeDefined();
219
+ expect(span.setAttribute).toHaveBeenCalledWith('team', 'checkout');
220
+ expect(span.setAttribute).toHaveBeenCalledWith('flow', 'signup');
221
+ });
222
+
223
+ it('attaches otel-spans annotation to testInfo when collector returns spans', async () => {
224
+ mockDrainResult = [
225
+ { spanId: 's1', name: 'e2e:test', startTimeMs: 1000, durationMs: 100, status: 'ok' },
226
+ ];
227
+
228
+ await import('./index');
229
+
230
+ const spanFixture = state.fixtures?._otelTestSpan;
231
+ const spanFixtureFn = Array.isArray(spanFixture) ? spanFixture[0] : spanFixture;
232
+
233
+ const annotations: Array<{ type: string; description?: string }> = [];
234
+ await spanFixtureFn?.(
235
+ {},
236
+ async () => {},
237
+ {
238
+ annotations,
239
+ project: { name: 'chromium' },
240
+ title: 'otel-spans test',
241
+ },
242
+ );
243
+
244
+ const spansAnnotation = annotations.find((a) => a.type === 'otel-spans');
245
+ expect(spansAnnotation).toBeDefined();
246
+ expect(JSON.parse(spansAnnotation!.description!)).toEqual([
247
+ { spanId: 's1', name: 'e2e:test', startTimeMs: 1000, durationMs: 100, status: 'ok' },
248
+ ]);
249
+ });
250
+ });
251
+
252
+ describe('autotel-playwright step', () => {
253
+ afterEach(() => {
254
+ createdSpans.length = 0;
255
+ vi.resetModules();
256
+ });
257
+
258
+ it('marks step span as error and records exception when the step throws', async () => {
259
+ const { step } = await import('./index');
260
+ const err = new Error('step failed');
261
+
262
+ await expect(step('boom', async () => Promise.reject(err))).rejects.toThrow('step failed');
263
+
264
+ const stepSpan = createdSpans.at(-1);
265
+ expect(stepSpan).toBeDefined();
266
+ expect(stepSpan?.setStatus).toHaveBeenCalled();
267
+ expect(stepSpan?.recordException).toHaveBeenCalledWith(err);
268
+ });
269
+ });
270
+
271
+ describe('trace context helper re-exports', () => {
272
+ it('re-exports getTraceContext', async () => {
273
+ const mod = await import('./index');
274
+ expect(mod.getTraceContext).toBeTypeOf('function');
275
+ });
276
+
277
+ it('re-exports resolveTraceUrl', async () => {
278
+ const mod = await import('./index');
279
+ expect(mod.resolveTraceUrl).toBeTypeOf('function');
280
+ });
281
+
282
+ it('re-exports isTracing', async () => {
283
+ const mod = await import('./index');
284
+ expect(mod.isTracing).toBeTypeOf('function');
285
+ });
286
+
287
+ it('re-exports enrichWithTraceContext', async () => {
288
+ const mod = await import('./index');
289
+ expect(mod.enrichWithTraceContext).toBeTypeOf('function');
290
+ });
291
+ });