autotel-playwright 0.4.61 → 0.5.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.
package/README.md CHANGED
@@ -121,6 +121,8 @@ test('user flow', async ({ page }) => {
121
121
  - **`expect`** - Re-exported from `@playwright/test`.
122
122
  - **`createGlobalSetup(opts?)`** - Returns an async function that calls `autotel.init(opts)` for use as `globalSetup`.
123
123
  - **`AUTOTEL_ATTRIBUTE_ANNOTATION`** - Annotation type string for custom span attributes (`key=value` in description).
124
+ - **`withBrowserSession(context, run, options?)`** (`autotel-playwright/session`) - Runs `run` inside a `browser.session` span that records the session's CPU, heap, network bytes, pages and console errors. See [Browser sessions outside tests](#browser-sessions-outside-tests).
125
+ - **`BROWSER_SESSION_ATTRIBUTES`** (`autotel-playwright/session`) - The attribute names that span carries.
124
126
 
125
127
  ### Optional: reporter (test + step spans from runner)
126
128
 
@@ -138,6 +140,57 @@ export default defineConfig({
138
140
 
139
141
  Reporter creates one span per test (`e2e:${title}`) and one per step (`step:${title}`) as children. For a single trace that includes **test → API** (worker), use the fixture only; the reporter adds a parallel view from the runner.
140
142
 
143
+ ## Browser sessions outside tests
144
+
145
+ `withBrowserSession()` puts a span around a whole browser session and records
146
+ what the session cost. It takes a Playwright `BrowserContext`, so it works for
147
+ agents and scrapers driving a browser in production, not only for tests.
148
+
149
+ ```ts
150
+ import { chromium } from 'playwright';
151
+ import { init } from 'autotel';
152
+ import { withBrowserSession } from 'autotel-playwright/session';
153
+
154
+ init({ service: 'browser-agent' });
155
+
156
+ const context = await (await chromium.launch()).newContext();
157
+
158
+ await withBrowserSession(context, async ({ sessionId }) => {
159
+ const page = await context.newPage();
160
+ await page.goto('https://example.com');
161
+ // Anything started in here joins the session's trace, including work whose
162
+ // RPC or HTTP calls carry W3C trace context.
163
+ });
164
+ ```
165
+
166
+ One `browser.session` span per session, carrying:
167
+
168
+ | attribute | what it answers |
169
+ | -------------------------------- | ------------------------------------------------------- |
170
+ | `session.id` | your id, or the span id when you pass none |
171
+ | `browser.session.cpu.time` | seconds of browser CPU, summed over the session's pages |
172
+ | `browser.session.memory.usage` | peak JS heap in bytes |
173
+ | `browser.session.network.io` | bytes on the wire, headers and bodies, both directions |
174
+ | `browser.session.pages` | pages the session opened |
175
+ | `browser.session.console.errors` | console errors plus uncaught page exceptions |
176
+
177
+ Console output arrives as `browser.console` span events (`error` and `warning`
178
+ by default, `consoleLevels` to widen), and every uncaught page exception is
179
+ recorded on the span. CPU and heap come from CDP and so are Chromium-only;
180
+ network, console and timing work on every browser Playwright drives.
181
+
182
+ ```ts
183
+ await withBrowserSession(context, run, {
184
+ sessionId: myWorkflowId,
185
+ consoleLevels: ['error', 'warning', 'info'],
186
+ attributes: { 'workflow.name': 'checkout-audit' },
187
+ });
188
+ ```
189
+
190
+ Pair it with `context.tracing.start({ screenshots: true, snapshots: true })` and
191
+ pass the trace path in `attributes` when you want a replay to open from the span
192
+ at [trace.playwright.dev](https://trace.playwright.dev).
193
+
141
194
  ## Configuration and troubleshooting
142
195
 
143
196
  ### API base URL with a path
@@ -0,0 +1,120 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let autotel = require("autotel");
3
+ //#region src/session.ts
4
+ const TRACER_NAME = "autotel-playwright";
5
+ const TRACER_VERSION = "0.1.0";
6
+ /** Console levels recorded as span events when the caller names none. */
7
+ const DEFAULT_CONSOLE_LEVELS = ["error", "warning"];
8
+ /**
9
+ * Session totals. `session.id` is canonical; the rest extend `browser.*`,
10
+ * which OpenTelemetry defines for the browser but not for the driver.
11
+ */
12
+ const BROWSER_SESSION_ATTRIBUTES = {
13
+ SESSION_ID: "session.id",
14
+ /** Seconds of browser CPU, summed over the session's pages. */
15
+ CPU_TIME: "browser.session.cpu.time",
16
+ /** Peak JS heap in bytes across the session's pages. */
17
+ MEMORY_USAGE: "browser.session.memory.usage",
18
+ /** Bytes on the wire, request and response, headers and bodies. */
19
+ NETWORK_IO: "browser.session.network.io",
20
+ PAGES: "browser.session.pages",
21
+ CONSOLE_ERRORS: "browser.session.console.errors",
22
+ CONSOLE_LEVEL: "browser.console.level",
23
+ CONSOLE_MESSAGE: "browser.console.message"
24
+ };
25
+ /**
26
+ * Runs `run` inside a `browser.session` span and records what the session cost.
27
+ * The span is active for the callback, so any instrumented work started inside
28
+ * it - including an agent whose RPC propagates trace context - joins the trace.
29
+ */
30
+ async function withBrowserSession(browserContext, run, options = {}) {
31
+ const levels = new Set(options.consoleLevels ?? DEFAULT_CONSOLE_LEVELS);
32
+ const span = (0, autotel.getTracer)(TRACER_NAME, TRACER_VERSION).startSpan("browser.session", { attributes: { ...options.attributes } });
33
+ const sessionId = options.sessionId ?? span.spanContext().spanId;
34
+ span.setAttribute(BROWSER_SESSION_ATTRIBUTES.SESSION_ID, sessionId);
35
+ const totals = {
36
+ consoleErrors: 0,
37
+ cpuTime: 0,
38
+ memoryUsage: 0,
39
+ networkIo: 0,
40
+ pages: 0
41
+ };
42
+ const pending = /* @__PURE__ */ new Set();
43
+ const cdpSessions = /* @__PURE__ */ new Map();
44
+ const track = (work) => {
45
+ pending.add(work);
46
+ work.finally(() => pending.delete(work));
47
+ };
48
+ /**
49
+ * CPU is cumulative per target, so one read at the end is the whole story;
50
+ * the heap read is a point in time, which is why closing pages are sampled
51
+ * too rather than only the ones still open when the session ends.
52
+ */
53
+ const sample = async (page) => {
54
+ const session = cdpSessions.get(page);
55
+ cdpSessions.delete(page);
56
+ try {
57
+ const cdp = await session;
58
+ if (!cdp) return;
59
+ const { metrics } = await cdp.send("Performance.getMetrics");
60
+ const read = (name) => metrics.find((metric) => metric.name === name)?.value ?? 0;
61
+ totals.cpuTime += read("TaskDuration");
62
+ totals.memoryUsage = Math.max(totals.memoryUsage, read("JSHeapUsedSize"));
63
+ } catch {}
64
+ };
65
+ const addBytes = async (request) => {
66
+ try {
67
+ const sizes = await request.sizes();
68
+ totals.networkIo += sizes.requestBodySize + sizes.requestHeadersSize + sizes.responseBodySize + sizes.responseHeadersSize;
69
+ } catch {}
70
+ };
71
+ const attach = (page) => {
72
+ totals.pages += 1;
73
+ cdpSessions.set(page, browserContext.newCDPSession(page).then(async (cdp) => {
74
+ await cdp.send("Performance.enable");
75
+ return cdp;
76
+ }).catch(() => void 0));
77
+ page.on("console", (message) => {
78
+ const level = message.type();
79
+ if (level === "error") totals.consoleErrors += 1;
80
+ if (!levels.has(level)) return;
81
+ span.addEvent("browser.console", {
82
+ [BROWSER_SESSION_ATTRIBUTES.CONSOLE_LEVEL]: level,
83
+ [BROWSER_SESSION_ATTRIBUTES.CONSOLE_MESSAGE]: message.text()
84
+ });
85
+ });
86
+ page.on("pageerror", (error) => {
87
+ totals.consoleErrors += 1;
88
+ span.recordException(error);
89
+ });
90
+ page.on("requestfinished", (request) => track(addBytes(request)));
91
+ page.on("close", () => track(sample(page)));
92
+ };
93
+ browserContext.on("page", attach);
94
+ for (const page of browserContext.pages()) attach(page);
95
+ try {
96
+ return await autotel.context.with(autotel.otelTrace.setSpan(autotel.context.active(), span), () => run({ sessionId }));
97
+ } catch (error) {
98
+ span.recordException(error);
99
+ span.setStatus({
100
+ code: autotel.SpanStatusCode.ERROR,
101
+ message: error instanceof Error ? error.message : String(error)
102
+ });
103
+ throw error;
104
+ } finally {
105
+ browserContext.off("page", attach);
106
+ for (const page of cdpSessions.keys()) track(sample(page));
107
+ await Promise.allSettled([...pending]);
108
+ span.setAttributes({
109
+ [BROWSER_SESSION_ATTRIBUTES.CONSOLE_ERRORS]: totals.consoleErrors,
110
+ [BROWSER_SESSION_ATTRIBUTES.CPU_TIME]: totals.cpuTime,
111
+ [BROWSER_SESSION_ATTRIBUTES.MEMORY_USAGE]: totals.memoryUsage,
112
+ [BROWSER_SESSION_ATTRIBUTES.NETWORK_IO]: totals.networkIo,
113
+ [BROWSER_SESSION_ATTRIBUTES.PAGES]: totals.pages
114
+ });
115
+ span.end();
116
+ }
117
+ }
118
+ //#endregion
119
+ exports.BROWSER_SESSION_ATTRIBUTES = BROWSER_SESSION_ATTRIBUTES;
120
+ exports.withBrowserSession = withBrowserSession;
@@ -0,0 +1,37 @@
1
+ import { BrowserContext } from "@playwright/test";
2
+ //#region src/session.d.ts
3
+ /**
4
+ * Session totals. `session.id` is canonical; the rest extend `browser.*`,
5
+ * which OpenTelemetry defines for the browser but not for the driver.
6
+ */
7
+ declare const BROWSER_SESSION_ATTRIBUTES: {
8
+ readonly SESSION_ID: "session.id";
9
+ /** Seconds of browser CPU, summed over the session's pages. */
10
+ readonly CPU_TIME: "browser.session.cpu.time";
11
+ /** Peak JS heap in bytes across the session's pages. */
12
+ readonly MEMORY_USAGE: "browser.session.memory.usage";
13
+ /** Bytes on the wire, request and response, headers and bodies. */
14
+ readonly NETWORK_IO: "browser.session.network.io";
15
+ readonly PAGES: "browser.session.pages";
16
+ readonly CONSOLE_ERRORS: "browser.session.console.errors";
17
+ readonly CONSOLE_LEVEL: "browser.console.level";
18
+ readonly CONSOLE_MESSAGE: "browser.console.message";
19
+ };
20
+ interface BrowserSessionOptions {
21
+ /** Your own session identifier. Defaults to the session span's id. */
22
+ sessionId?: string;
23
+ /** Playwright console levels kept as span events. Default: error, warning. */
24
+ consoleLevels?: readonly string[];
25
+ /** Extra session attributes, e.g. a workflow or customer id. */
26
+ attributes?: Record<string, string | number | boolean>;
27
+ }
28
+ /**
29
+ * Runs `run` inside a `browser.session` span and records what the session cost.
30
+ * The span is active for the callback, so any instrumented work started inside
31
+ * it - including an agent whose RPC propagates trace context - joins the trace.
32
+ */
33
+ declare function withBrowserSession<T>(browserContext: BrowserContext, run: (session: {
34
+ sessionId: string;
35
+ }) => Promise<T>, options?: BrowserSessionOptions): Promise<T>;
36
+ //#endregion
37
+ export { BROWSER_SESSION_ATTRIBUTES, BrowserSessionOptions, withBrowserSession };
@@ -0,0 +1,37 @@
1
+ import { BrowserContext } from "@playwright/test";
2
+ //#region src/session.d.ts
3
+ /**
4
+ * Session totals. `session.id` is canonical; the rest extend `browser.*`,
5
+ * which OpenTelemetry defines for the browser but not for the driver.
6
+ */
7
+ declare const BROWSER_SESSION_ATTRIBUTES: {
8
+ readonly SESSION_ID: "session.id";
9
+ /** Seconds of browser CPU, summed over the session's pages. */
10
+ readonly CPU_TIME: "browser.session.cpu.time";
11
+ /** Peak JS heap in bytes across the session's pages. */
12
+ readonly MEMORY_USAGE: "browser.session.memory.usage";
13
+ /** Bytes on the wire, request and response, headers and bodies. */
14
+ readonly NETWORK_IO: "browser.session.network.io";
15
+ readonly PAGES: "browser.session.pages";
16
+ readonly CONSOLE_ERRORS: "browser.session.console.errors";
17
+ readonly CONSOLE_LEVEL: "browser.console.level";
18
+ readonly CONSOLE_MESSAGE: "browser.console.message";
19
+ };
20
+ interface BrowserSessionOptions {
21
+ /** Your own session identifier. Defaults to the session span's id. */
22
+ sessionId?: string;
23
+ /** Playwright console levels kept as span events. Default: error, warning. */
24
+ consoleLevels?: readonly string[];
25
+ /** Extra session attributes, e.g. a workflow or customer id. */
26
+ attributes?: Record<string, string | number | boolean>;
27
+ }
28
+ /**
29
+ * Runs `run` inside a `browser.session` span and records what the session cost.
30
+ * The span is active for the callback, so any instrumented work started inside
31
+ * it - including an agent whose RPC propagates trace context - joins the trace.
32
+ */
33
+ declare function withBrowserSession<T>(browserContext: BrowserContext, run: (session: {
34
+ sessionId: string;
35
+ }) => Promise<T>, options?: BrowserSessionOptions): Promise<T>;
36
+ //#endregion
37
+ export { BROWSER_SESSION_ATTRIBUTES, BrowserSessionOptions, withBrowserSession };
@@ -0,0 +1,118 @@
1
+ import { SpanStatusCode, context, getTracer, otelTrace } from "autotel";
2
+ //#region src/session.ts
3
+ const TRACER_NAME = "autotel-playwright";
4
+ const TRACER_VERSION = "0.1.0";
5
+ /** Console levels recorded as span events when the caller names none. */
6
+ const DEFAULT_CONSOLE_LEVELS = ["error", "warning"];
7
+ /**
8
+ * Session totals. `session.id` is canonical; the rest extend `browser.*`,
9
+ * which OpenTelemetry defines for the browser but not for the driver.
10
+ */
11
+ const BROWSER_SESSION_ATTRIBUTES = {
12
+ SESSION_ID: "session.id",
13
+ /** Seconds of browser CPU, summed over the session's pages. */
14
+ CPU_TIME: "browser.session.cpu.time",
15
+ /** Peak JS heap in bytes across the session's pages. */
16
+ MEMORY_USAGE: "browser.session.memory.usage",
17
+ /** Bytes on the wire, request and response, headers and bodies. */
18
+ NETWORK_IO: "browser.session.network.io",
19
+ PAGES: "browser.session.pages",
20
+ CONSOLE_ERRORS: "browser.session.console.errors",
21
+ CONSOLE_LEVEL: "browser.console.level",
22
+ CONSOLE_MESSAGE: "browser.console.message"
23
+ };
24
+ /**
25
+ * Runs `run` inside a `browser.session` span and records what the session cost.
26
+ * The span is active for the callback, so any instrumented work started inside
27
+ * it - including an agent whose RPC propagates trace context - joins the trace.
28
+ */
29
+ async function withBrowserSession(browserContext, run, options = {}) {
30
+ const levels = new Set(options.consoleLevels ?? DEFAULT_CONSOLE_LEVELS);
31
+ const span = getTracer(TRACER_NAME, TRACER_VERSION).startSpan("browser.session", { attributes: { ...options.attributes } });
32
+ const sessionId = options.sessionId ?? span.spanContext().spanId;
33
+ span.setAttribute(BROWSER_SESSION_ATTRIBUTES.SESSION_ID, sessionId);
34
+ const totals = {
35
+ consoleErrors: 0,
36
+ cpuTime: 0,
37
+ memoryUsage: 0,
38
+ networkIo: 0,
39
+ pages: 0
40
+ };
41
+ const pending = /* @__PURE__ */ new Set();
42
+ const cdpSessions = /* @__PURE__ */ new Map();
43
+ const track = (work) => {
44
+ pending.add(work);
45
+ work.finally(() => pending.delete(work));
46
+ };
47
+ /**
48
+ * CPU is cumulative per target, so one read at the end is the whole story;
49
+ * the heap read is a point in time, which is why closing pages are sampled
50
+ * too rather than only the ones still open when the session ends.
51
+ */
52
+ const sample = async (page) => {
53
+ const session = cdpSessions.get(page);
54
+ cdpSessions.delete(page);
55
+ try {
56
+ const cdp = await session;
57
+ if (!cdp) return;
58
+ const { metrics } = await cdp.send("Performance.getMetrics");
59
+ const read = (name) => metrics.find((metric) => metric.name === name)?.value ?? 0;
60
+ totals.cpuTime += read("TaskDuration");
61
+ totals.memoryUsage = Math.max(totals.memoryUsage, read("JSHeapUsedSize"));
62
+ } catch {}
63
+ };
64
+ const addBytes = async (request) => {
65
+ try {
66
+ const sizes = await request.sizes();
67
+ totals.networkIo += sizes.requestBodySize + sizes.requestHeadersSize + sizes.responseBodySize + sizes.responseHeadersSize;
68
+ } catch {}
69
+ };
70
+ const attach = (page) => {
71
+ totals.pages += 1;
72
+ cdpSessions.set(page, browserContext.newCDPSession(page).then(async (cdp) => {
73
+ await cdp.send("Performance.enable");
74
+ return cdp;
75
+ }).catch(() => void 0));
76
+ page.on("console", (message) => {
77
+ const level = message.type();
78
+ if (level === "error") totals.consoleErrors += 1;
79
+ if (!levels.has(level)) return;
80
+ span.addEvent("browser.console", {
81
+ [BROWSER_SESSION_ATTRIBUTES.CONSOLE_LEVEL]: level,
82
+ [BROWSER_SESSION_ATTRIBUTES.CONSOLE_MESSAGE]: message.text()
83
+ });
84
+ });
85
+ page.on("pageerror", (error) => {
86
+ totals.consoleErrors += 1;
87
+ span.recordException(error);
88
+ });
89
+ page.on("requestfinished", (request) => track(addBytes(request)));
90
+ page.on("close", () => track(sample(page)));
91
+ };
92
+ browserContext.on("page", attach);
93
+ for (const page of browserContext.pages()) attach(page);
94
+ try {
95
+ return await context.with(otelTrace.setSpan(context.active(), span), () => run({ sessionId }));
96
+ } catch (error) {
97
+ span.recordException(error);
98
+ span.setStatus({
99
+ code: SpanStatusCode.ERROR,
100
+ message: error instanceof Error ? error.message : String(error)
101
+ });
102
+ throw error;
103
+ } finally {
104
+ browserContext.off("page", attach);
105
+ for (const page of cdpSessions.keys()) track(sample(page));
106
+ await Promise.allSettled([...pending]);
107
+ span.setAttributes({
108
+ [BROWSER_SESSION_ATTRIBUTES.CONSOLE_ERRORS]: totals.consoleErrors,
109
+ [BROWSER_SESSION_ATTRIBUTES.CPU_TIME]: totals.cpuTime,
110
+ [BROWSER_SESSION_ATTRIBUTES.MEMORY_USAGE]: totals.memoryUsage,
111
+ [BROWSER_SESSION_ATTRIBUTES.NETWORK_IO]: totals.networkIo,
112
+ [BROWSER_SESSION_ATTRIBUTES.PAGES]: totals.pages
113
+ });
114
+ span.end();
115
+ }
116
+ }
117
+ //#endregion
118
+ export { BROWSER_SESSION_ATTRIBUTES, withBrowserSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "autotel-playwright",
3
- "version": "0.4.61",
3
+ "version": "0.5.0",
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",
@@ -17,6 +17,11 @@
17
17
  "types": "./dist/reporter.d.ts",
18
18
  "import": "./dist/reporter.js",
19
19
  "require": "./dist/reporter.cjs"
20
+ },
21
+ "./session": {
22
+ "types": "./dist/session.d.ts",
23
+ "import": "./dist/session.js",
24
+ "require": "./dist/session.cjs"
20
25
  }
21
26
  },
22
27
  "files": [
@@ -24,7 +29,7 @@
24
29
  "README.md"
25
30
  ],
26
31
  "dependencies": {
27
- "autotel": "7.4.0"
32
+ "autotel": "7.6.0"
28
33
  },
29
34
  "peerDependencies": {
30
35
  "@playwright/test": ">=1.62.0"