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.
- package/README.md +154 -0
- package/dist/index.cjs +208 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +41 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +185 -0
- package/dist/index.js.map +1 -0
- package/dist/reporter.cjs +76 -0
- package/dist/reporter.cjs.map +1 -0
- package/dist/reporter.d.cts +35 -0
- package/dist/reporter.d.ts +35 -0
- package/dist/reporter.js +71 -0
- package/dist/reporter.js.map +1 -0
- package/package.json +53 -0
- package/src/index.test.ts +291 -0
- package/src/index.ts +293 -0
- package/src/reporter.test.ts +81 -0
- package/src/reporter.ts +105 -0
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# autotel-playwright
|
|
2
|
+
|
|
3
|
+
Playwright fixture that gives each test an OpenTelemetry span and injects W3C trace context into HTTP requests to your API so **test → API** appears as one trace in your OTLP backend.
|
|
4
|
+
|
|
5
|
+
## How test and server appear as one trace
|
|
6
|
+
|
|
7
|
+
The test runs in the **Playwright worker process**; your API runs in another process (or on another host). The fixture injects a `traceparent` header into every request from `page` or `requestWithTrace` to your API. Your server uses `extractTraceContext()` (e.g. from `autotel/http`) and creates its spans in that context, so **the same trace ID** is used on both sides. In your OTLP backend, look up the test’s trace ID to see the full trace: test span, step spans, and all server spans (HTTP, handlers, `db.userId`, etc.).
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
sequenceDiagram
|
|
11
|
+
participant Test as Test process
|
|
12
|
+
participant Server as Server process
|
|
13
|
+
participant OTLP as OTLP backend
|
|
14
|
+
|
|
15
|
+
Note over Test: One span per test (trace ID = X)
|
|
16
|
+
Test->>Server: Request with traceparent header
|
|
17
|
+
Note over Server: extractTraceContext()<br/>HTTP span + child spans
|
|
18
|
+
Test->>OTLP: Export spans (trace X)
|
|
19
|
+
Server->>OTLP: Export spans (trace X)
|
|
20
|
+
Note over OTLP: One trace = test + API
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pnpm add autotel autotel-playwright
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Peer dependency: `@playwright/test`.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
### 1. Init autotel (globalSetup)
|
|
34
|
+
|
|
35
|
+
Call `autotel.init()` before tests run. In `playwright.config.ts`:
|
|
36
|
+
|
|
37
|
+
```ts
|
|
38
|
+
import { defineConfig } from '@playwright/test';
|
|
39
|
+
import { createGlobalSetup } from 'autotel-playwright';
|
|
40
|
+
|
|
41
|
+
export default defineConfig({
|
|
42
|
+
globalSetup: createGlobalSetup({
|
|
43
|
+
service: 'e2e-tests',
|
|
44
|
+
debug: true,
|
|
45
|
+
endpoint: process.env.OTLP_ENDPOINT,
|
|
46
|
+
}),
|
|
47
|
+
// ...
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Or in a globalSetup file:
|
|
52
|
+
|
|
53
|
+
```ts
|
|
54
|
+
// globalSetup.ts
|
|
55
|
+
import { init } from 'autotel';
|
|
56
|
+
|
|
57
|
+
export default function globalSetup() {
|
|
58
|
+
init({ service: 'e2e-tests', debug: true });
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. API base URL
|
|
63
|
+
|
|
64
|
+
Set one or both env vars so the fixture injects trace headers only into requests to your API:
|
|
65
|
+
|
|
66
|
+
- `API_BASE_URL` - e.g. `http://localhost:3000`
|
|
67
|
+
- `AUTOTEL_PLAYWRIGHT_API_ORIGIN` - same idea
|
|
68
|
+
|
|
69
|
+
If neither is set, tests still run; no route is added and no headers are injected.
|
|
70
|
+
|
|
71
|
+
### 3. Use the fixture in specs
|
|
72
|
+
|
|
73
|
+
**Browser (page):** requests made by the page to your API get trace context via the `page` fixture.
|
|
74
|
+
|
|
75
|
+
```ts
|
|
76
|
+
import { test, expect } from 'autotel-playwright';
|
|
77
|
+
|
|
78
|
+
test('health check', async ({ page }) => {
|
|
79
|
+
await page.goto(process.env.API_BASE_URL + '/health');
|
|
80
|
+
// Request to API_BASE_URL will include traceparent/tracestate and x-test-name
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**Node (request):** use the `requestWithTrace` fixture for API calls from the test process (e.g. `request.get()`, `request.post()`). Same trace context is injected so test → API stays one trace.
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
test('api health from Node', async ({ requestWithTrace }) => {
|
|
88
|
+
const res = await requestWithTrace.get(process.env.API_BASE_URL + '/health');
|
|
89
|
+
expect(res.ok()).toBeTruthy();
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**Step spans:** use the `step()` helper inside a test to create a child span for a logical step. Steps appear under the test span in the same trace.
|
|
94
|
+
|
|
95
|
+
```ts
|
|
96
|
+
import { test, expect, step } from 'autotel-playwright';
|
|
97
|
+
|
|
98
|
+
test('user flow', async ({ page }) => {
|
|
99
|
+
await step('login', async () => {
|
|
100
|
+
await page.fill('#email', 'user@example.com');
|
|
101
|
+
await page.click('button[type=submit]');
|
|
102
|
+
});
|
|
103
|
+
await step('open dashboard', async () => {
|
|
104
|
+
await page.goto('/dashboard');
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Behaviour
|
|
110
|
+
|
|
111
|
+
- One span per test: name `e2e:${testInfo.title}`, attributes `test.title`, `test.file`, `test.line`, `test.project`.
|
|
112
|
+
- **page:** For every request whose URL matches the configured API base origin, the fixture adds W3C trace context and `x-test-name` to the request headers (via `page.route`).
|
|
113
|
+
- **requestWithTrace:** Same headers are merged into requests made with the wrapped `request` fixture (get/post/put/patch/delete/head/fetch) when the URL matches the API base.
|
|
114
|
+
- **step(name, fn):** Runs `fn` as a child span `step:${name}` under the test span for finer-grained traces. If `fn` throws, the span is marked as error with `recordException` before re-throwing.
|
|
115
|
+
- Optional per-test attributes: use annotations with type `autotel.attribute` and description `key=value` or semicolon-delimited `key1=value1;key2=value2` (see `AUTOTEL_ATTRIBUTE_ANNOTATION`).
|
|
116
|
+
|
|
117
|
+
## API
|
|
118
|
+
|
|
119
|
+
- **`test`** - Extended Playwright `test` with `page` (browser, trace context on matching requests) and `requestWithTrace` (Node APIRequestContext with trace context on matching URLs).
|
|
120
|
+
- **`step(name, fn)`** - Runs the async function as a named step; creates a child span so the trace shows test → step → … .
|
|
121
|
+
- **`expect`** - Re-exported from `@playwright/test`.
|
|
122
|
+
- **`createGlobalSetup(opts?)`** - Returns an async function that calls `autotel.init(opts)` for use as `globalSetup`.
|
|
123
|
+
- **`AUTOTEL_ATTRIBUTE_ANNOTATION`** - Annotation type string for custom span attributes (`key=value` in description).
|
|
124
|
+
|
|
125
|
+
### Optional: reporter (test + step spans from runner)
|
|
126
|
+
|
|
127
|
+
For test/step timing and hierarchy in OTLP from the **runner process** (separate from the worker's fixture spans), add the OTel reporter. You still need `init()` in globalSetup so reporter spans are exported.
|
|
128
|
+
|
|
129
|
+
```ts
|
|
130
|
+
// playwright.config.ts
|
|
131
|
+
import { defineConfig } from '@playwright/test';
|
|
132
|
+
|
|
133
|
+
export default defineConfig({
|
|
134
|
+
reporter: [['list'], ['autotel-playwright/reporter']],
|
|
135
|
+
globalSetup: './globalSetup.ts', // must call init()
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
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
|
+
|
|
141
|
+
## Configuration and troubleshooting
|
|
142
|
+
|
|
143
|
+
### API base URL with a path
|
|
144
|
+
|
|
145
|
+
When `API_BASE_URL` or `AUTOTEL_PLAYWRIGHT_API_ORIGIN` includes a path (e.g. `http://localhost:3000/api`), only requests whose path **starts with** that path receive trace headers. Same-origin requests to other paths (e.g. `http://localhost:3000/health`) do **not** get headers, so trace context is not leaked to unrelated endpoints.
|
|
146
|
+
|
|
147
|
+
### globalSetup must be a string
|
|
148
|
+
|
|
149
|
+
Playwright expects `globalSetup` to be a **path string** (e.g. `'./globalSetup.ts'`), not a function. Use a file that exports a default function which calls `init()`.
|
|
150
|
+
|
|
151
|
+
### Fixture spans vs reporter spans
|
|
152
|
+
|
|
153
|
+
- **Fixture** (worker): test span and `step()` spans run in the worker; requests to your API get trace context, so one trace = test → API. Call `init()` in a globalSetup **or** at the top of your spec so the worker process exports these spans.
|
|
154
|
+
- **Reporter** (runner): test and step spans from `onTestBegin` / `onStepBegin` run in the runner. Call `init()` in globalSetup so the runner exports reporter spans. Use Playwright’s `test.step()` to get step spans from the reporter.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var test$1 = require('@playwright/test');
|
|
4
|
+
var autotel = require('autotel');
|
|
5
|
+
var testSpanCollector = require('autotel/test-span-collector');
|
|
6
|
+
var processors = require('autotel/processors');
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
var TRACER_NAME = "playwright-tests";
|
|
10
|
+
var TRACER_VERSION = "0.1.0";
|
|
11
|
+
var collector = null;
|
|
12
|
+
function ensureCollector() {
|
|
13
|
+
if (!collector) {
|
|
14
|
+
collector = new testSpanCollector.TestSpanCollector();
|
|
15
|
+
const provider = autotel.getAutotelTracerProvider();
|
|
16
|
+
if ("addSpanProcessor" in provider) {
|
|
17
|
+
provider.addSpanProcessor(new processors.SimpleSpanProcessor(collector));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return collector;
|
|
21
|
+
}
|
|
22
|
+
var ENV_API_BASE_URL = "API_BASE_URL";
|
|
23
|
+
var ENV_API_ORIGIN = "AUTOTEL_PLAYWRIGHT_API_ORIGIN";
|
|
24
|
+
function getApiBaseUrls() {
|
|
25
|
+
const a = process.env[ENV_API_BASE_URL];
|
|
26
|
+
const b = process.env[ENV_API_ORIGIN];
|
|
27
|
+
const urls = [];
|
|
28
|
+
if (a) urls.push(a.replace(/\/$/, ""));
|
|
29
|
+
if (b) urls.push(b.replace(/\/$/, ""));
|
|
30
|
+
return [...new Set(urls)];
|
|
31
|
+
}
|
|
32
|
+
function urlMatchesApiOrigin(requestUrl, apiBaseUrls) {
|
|
33
|
+
if (apiBaseUrls.length === 0) return false;
|
|
34
|
+
try {
|
|
35
|
+
const u = new URL(requestUrl);
|
|
36
|
+
const requestOrigin = u.origin;
|
|
37
|
+
const requestPathname = u.pathname;
|
|
38
|
+
return apiBaseUrls.some((base2) => {
|
|
39
|
+
try {
|
|
40
|
+
const b = new URL(base2);
|
|
41
|
+
if (requestOrigin !== b.origin) return false;
|
|
42
|
+
const basePathname = b.pathname.replace(/\/$/, "") || "/";
|
|
43
|
+
if (basePathname === "/") return true;
|
|
44
|
+
return requestPathname === basePathname || requestPathname.startsWith(basePathname + "/");
|
|
45
|
+
} catch {
|
|
46
|
+
return requestUrl.startsWith(base2);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return apiBaseUrls.some((base2) => requestUrl.startsWith(base2));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var AUTOTEL_ATTRIBUTE_ANNOTATION = "autotel.attribute";
|
|
54
|
+
function setAttributesFromAnnotations(span, testInfo) {
|
|
55
|
+
for (const a of testInfo.annotations) {
|
|
56
|
+
if (a.type !== AUTOTEL_ATTRIBUTE_ANNOTATION || !a.description) continue;
|
|
57
|
+
const entries = a.description.split(";");
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const parts = entry.split("=");
|
|
60
|
+
if (parts.length >= 2) {
|
|
61
|
+
const key = parts[0].trim();
|
|
62
|
+
const value = parts.slice(1).join("=").trim();
|
|
63
|
+
span.setAttribute(key, value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function mergeTraceHeaders(url, options, apiBaseUrls, carrier, testName) {
|
|
69
|
+
const opts = options ?? {};
|
|
70
|
+
if (!urlMatchesApiOrigin(url, apiBaseUrls)) return opts;
|
|
71
|
+
return {
|
|
72
|
+
...opts,
|
|
73
|
+
headers: { ...opts.headers, ...carrier, "x-test-name": testName }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function createRequestWithTrace(request, apiBaseUrls, carrier, testInfo) {
|
|
77
|
+
const merge = (url, options) => mergeTraceHeaders(url, options, apiBaseUrls, carrier, testInfo.title);
|
|
78
|
+
return {
|
|
79
|
+
get: (url, options) => request.get(url, merge(url, options)),
|
|
80
|
+
post: (url, options) => request.post(url, merge(url, options)),
|
|
81
|
+
put: (url, options) => request.put(url, merge(url, options)),
|
|
82
|
+
patch: (url, options) => request.patch(url, merge(url, options)),
|
|
83
|
+
delete: (url, options) => request.delete(url, merge(url, options)),
|
|
84
|
+
head: (url, options) => request.head(url, merge(url, options)),
|
|
85
|
+
fetch: (urlOrRequest, options) => request.fetch(urlOrRequest, merge(typeof urlOrRequest === "string" ? urlOrRequest : urlOrRequest.url(), options)),
|
|
86
|
+
storageState: (options) => request.storageState(options),
|
|
87
|
+
dispose: () => request.dispose()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
var test = test$1.test.extend({
|
|
91
|
+
_otelTestSpan: [
|
|
92
|
+
// eslint-disable-next-line no-empty-pattern
|
|
93
|
+
async ({}, use, testInfo) => {
|
|
94
|
+
ensureCollector();
|
|
95
|
+
const apiBaseUrls = getApiBaseUrls();
|
|
96
|
+
const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
97
|
+
const spanName = `e2e:${testInfo.title}`;
|
|
98
|
+
const span = tracer.startSpan(spanName, {
|
|
99
|
+
attributes: {
|
|
100
|
+
"test.title": testInfo.title,
|
|
101
|
+
"test.project": testInfo.project.name,
|
|
102
|
+
"test.file": testInfo.file ?? "",
|
|
103
|
+
"test.line": testInfo.line ?? 0
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
setAttributesFromAnnotations(span, testInfo);
|
|
107
|
+
const ctx = autotel.otelTrace.setSpan(autotel.context.active(), span);
|
|
108
|
+
const carrier = {};
|
|
109
|
+
autotel.propagation.inject(ctx, carrier);
|
|
110
|
+
try {
|
|
111
|
+
await use({ carrier, apiBaseUrls, testInfo });
|
|
112
|
+
} finally {
|
|
113
|
+
span.end();
|
|
114
|
+
const traceId = span.spanContext().traceId;
|
|
115
|
+
const rootSpanId = span.spanContext().spanId;
|
|
116
|
+
const spans = collector.drainTrace(traceId, rootSpanId);
|
|
117
|
+
if (spans.length > 0) {
|
|
118
|
+
testInfo.annotations.push({
|
|
119
|
+
type: "otel-spans",
|
|
120
|
+
description: JSON.stringify(spans)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{ scope: "test" }
|
|
126
|
+
],
|
|
127
|
+
page: async ({ page, _otelTestSpan }, use) => {
|
|
128
|
+
const { carrier, apiBaseUrls, testInfo } = _otelTestSpan;
|
|
129
|
+
if (apiBaseUrls.length > 0) {
|
|
130
|
+
await page.route("**/*", async (route) => {
|
|
131
|
+
const request = route.request();
|
|
132
|
+
const url = request.url();
|
|
133
|
+
if (urlMatchesApiOrigin(url, apiBaseUrls)) {
|
|
134
|
+
const headers = {
|
|
135
|
+
...request.headers(),
|
|
136
|
+
...carrier,
|
|
137
|
+
"x-test-name": testInfo.title
|
|
138
|
+
};
|
|
139
|
+
await route.continue({ headers });
|
|
140
|
+
} else {
|
|
141
|
+
await route.continue();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await use(page);
|
|
146
|
+
},
|
|
147
|
+
requestWithTrace: async ({ request, _otelTestSpan }, use) => {
|
|
148
|
+
const wrapped = createRequestWithTrace(
|
|
149
|
+
request,
|
|
150
|
+
_otelTestSpan.apiBaseUrls,
|
|
151
|
+
_otelTestSpan.carrier,
|
|
152
|
+
_otelTestSpan.testInfo
|
|
153
|
+
);
|
|
154
|
+
await use(wrapped);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
async function step(name, fn) {
|
|
158
|
+
const tracer = autotel.getTracer(TRACER_NAME, TRACER_VERSION);
|
|
159
|
+
const span = tracer.startSpan(`step:${name}`, {
|
|
160
|
+
attributes: { "step.name": name }
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
return await autotel.context.with(autotel.otelTrace.setSpan(autotel.context.active(), span), fn);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
span.setStatus({ code: autotel.SpanStatusCode.ERROR, message: error instanceof Error ? error.message : "Unknown error" });
|
|
166
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
167
|
+
throw error;
|
|
168
|
+
} finally {
|
|
169
|
+
span.end();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function createGlobalSetup(initOptions) {
|
|
173
|
+
return async () => {
|
|
174
|
+
const { init } = await import('autotel');
|
|
175
|
+
init({
|
|
176
|
+
service: "e2e-tests",
|
|
177
|
+
debug: true,
|
|
178
|
+
...initOptions
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
Object.defineProperty(exports, "expect", {
|
|
184
|
+
enumerable: true,
|
|
185
|
+
get: function () { return test$1.expect; }
|
|
186
|
+
});
|
|
187
|
+
Object.defineProperty(exports, "enrichWithTraceContext", {
|
|
188
|
+
enumerable: true,
|
|
189
|
+
get: function () { return autotel.enrichWithTraceContext; }
|
|
190
|
+
});
|
|
191
|
+
Object.defineProperty(exports, "getTraceContext", {
|
|
192
|
+
enumerable: true,
|
|
193
|
+
get: function () { return autotel.getTraceContext; }
|
|
194
|
+
});
|
|
195
|
+
Object.defineProperty(exports, "isTracing", {
|
|
196
|
+
enumerable: true,
|
|
197
|
+
get: function () { return autotel.isTracing; }
|
|
198
|
+
});
|
|
199
|
+
Object.defineProperty(exports, "resolveTraceUrl", {
|
|
200
|
+
enumerable: true,
|
|
201
|
+
get: function () { return autotel.resolveTraceUrl; }
|
|
202
|
+
});
|
|
203
|
+
exports.AUTOTEL_ATTRIBUTE_ANNOTATION = AUTOTEL_ATTRIBUTE_ANNOTATION;
|
|
204
|
+
exports.createGlobalSetup = createGlobalSetup;
|
|
205
|
+
exports.step = step;
|
|
206
|
+
exports.test = test;
|
|
207
|
+
//# sourceMappingURL=index.cjs.map
|
|
208
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts"],"names":["TestSpanCollector","getAutotelTracerProvider","SimpleSpanProcessor","base","getTracer","otelTrace","otelContext","propagation","SpanStatusCode"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * autotel-playwright\n *\n * Playwright fixture that creates one OTel span per test and injects W3C trace\n * context into requests to your API so \"test → API\" appears as one trace.\n *\n * @example\n * // globalSetup.ts: init({ service: 'e2e-tests' });\n * // In spec:\n * import { test, expect } from 'autotel-playwright';\n * test('checks health', async ({ page }) => {\n * await page.goto(API_BASE_URL + '/health'); // request gets traceparent\n * });\n * // Node-side API calls with trace context:\n * test('api health', async ({ requestWithTrace }) => {\n * const res = await requestWithTrace.get(API_BASE_URL + '/health');\n * expect(res.ok()).toBeTruthy();\n * });\n */\n\nimport { test as base } from '@playwright/test';\nimport type { Page, APIRequestContext, Request as PlaywrightRequest } from '@playwright/test';\nimport type { TestInfo } from '@playwright/test';\nimport type { AutotelConfig } from 'autotel';\nimport {\n getTracer,\n getAutotelTracerProvider,\n context as otelContext,\n propagation,\n otelTrace,\n SpanStatusCode,\n} from 'autotel';\nimport { TestSpanCollector } from 'autotel/test-span-collector';\nimport { SimpleSpanProcessor } from 'autotel/processors';\n\nconst TRACER_NAME = 'playwright-tests';\nconst TRACER_VERSION = '0.1.0';\n\nlet collector: TestSpanCollector | null = null;\n\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"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as _playwright_test from '@playwright/test';
|
|
2
|
+
import { Page, APIRequestContext, TestInfo } from '@playwright/test';
|
|
3
|
+
export { expect } from '@playwright/test';
|
|
4
|
+
import { AutotelConfig } from 'autotel';
|
|
5
|
+
export { OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
6
|
+
|
|
7
|
+
/** Annotation type for custom span attributes: description should be "key=value" or "key=value1;key2=value2". */
|
|
8
|
+
declare const AUTOTEL_ATTRIBUTE_ANNOTATION = "autotel.attribute";
|
|
9
|
+
type OtelTestSpan = {
|
|
10
|
+
carrier: Record<string, string>;
|
|
11
|
+
apiBaseUrls: string[];
|
|
12
|
+
testInfo: TestInfo;
|
|
13
|
+
};
|
|
14
|
+
declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & {
|
|
15
|
+
page: Page;
|
|
16
|
+
requestWithTrace: APIRequestContext;
|
|
17
|
+
_otelTestSpan: OtelTestSpan;
|
|
18
|
+
}, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs a named step as a child span of the current test span. Use inside a test to get
|
|
22
|
+
* step-level spans (e.g. "step:login", "step:navigate") under the test span in the same trace.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* test('user flow', async ({ page }) => {
|
|
26
|
+
* await step('login', async () => {
|
|
27
|
+
* await page.click('button[type=submit]');
|
|
28
|
+
* });
|
|
29
|
+
* await step('open profile', async () => {
|
|
30
|
+
* await page.goto('/profile');
|
|
31
|
+
* });
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
declare function step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Returns a function suitable for Playwright globalSetup that inits autotel.
|
|
37
|
+
* Call autotel.init() with the given options (or defaults) so test spans are exported.
|
|
38
|
+
*/
|
|
39
|
+
declare function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void>;
|
|
40
|
+
|
|
41
|
+
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, step, test };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import * as _playwright_test from '@playwright/test';
|
|
2
|
+
import { Page, APIRequestContext, TestInfo } from '@playwright/test';
|
|
3
|
+
export { expect } from '@playwright/test';
|
|
4
|
+
import { AutotelConfig } from 'autotel';
|
|
5
|
+
export { OtelTraceContext, enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
6
|
+
|
|
7
|
+
/** Annotation type for custom span attributes: description should be "key=value" or "key=value1;key2=value2". */
|
|
8
|
+
declare const AUTOTEL_ATTRIBUTE_ANNOTATION = "autotel.attribute";
|
|
9
|
+
type OtelTestSpan = {
|
|
10
|
+
carrier: Record<string, string>;
|
|
11
|
+
apiBaseUrls: string[];
|
|
12
|
+
testInfo: TestInfo;
|
|
13
|
+
};
|
|
14
|
+
declare const test: _playwright_test.TestType<_playwright_test.PlaywrightTestArgs & _playwright_test.PlaywrightTestOptions & {
|
|
15
|
+
page: Page;
|
|
16
|
+
requestWithTrace: APIRequestContext;
|
|
17
|
+
_otelTestSpan: OtelTestSpan;
|
|
18
|
+
}, _playwright_test.PlaywrightWorkerArgs & _playwright_test.PlaywrightWorkerOptions>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Runs a named step as a child span of the current test span. Use inside a test to get
|
|
22
|
+
* step-level spans (e.g. "step:login", "step:navigate") under the test span in the same trace.
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* test('user flow', async ({ page }) => {
|
|
26
|
+
* await step('login', async () => {
|
|
27
|
+
* await page.click('button[type=submit]');
|
|
28
|
+
* });
|
|
29
|
+
* await step('open profile', async () => {
|
|
30
|
+
* await page.goto('/profile');
|
|
31
|
+
* });
|
|
32
|
+
* });
|
|
33
|
+
*/
|
|
34
|
+
declare function step<T>(name: string, fn: () => Promise<T>): Promise<T>;
|
|
35
|
+
/**
|
|
36
|
+
* Returns a function suitable for Playwright globalSetup that inits autotel.
|
|
37
|
+
* Call autotel.init() with the given options (or defaults) so test spans are exported.
|
|
38
|
+
*/
|
|
39
|
+
declare function createGlobalSetup(initOptions?: AutotelConfig): () => Promise<void>;
|
|
40
|
+
|
|
41
|
+
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, step, test };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { test as test$1 } from '@playwright/test';
|
|
2
|
+
export { expect } from '@playwright/test';
|
|
3
|
+
import { getTracer, otelTrace, context, propagation, getAutotelTracerProvider, SpanStatusCode } from 'autotel';
|
|
4
|
+
export { enrichWithTraceContext, getTraceContext, isTracing, resolveTraceUrl } from 'autotel';
|
|
5
|
+
import { TestSpanCollector } from 'autotel/test-span-collector';
|
|
6
|
+
import { SimpleSpanProcessor } from 'autotel/processors';
|
|
7
|
+
|
|
8
|
+
// src/index.ts
|
|
9
|
+
var TRACER_NAME = "playwright-tests";
|
|
10
|
+
var TRACER_VERSION = "0.1.0";
|
|
11
|
+
var collector = null;
|
|
12
|
+
function ensureCollector() {
|
|
13
|
+
if (!collector) {
|
|
14
|
+
collector = new TestSpanCollector();
|
|
15
|
+
const provider = getAutotelTracerProvider();
|
|
16
|
+
if ("addSpanProcessor" in provider) {
|
|
17
|
+
provider.addSpanProcessor(new SimpleSpanProcessor(collector));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return collector;
|
|
21
|
+
}
|
|
22
|
+
var ENV_API_BASE_URL = "API_BASE_URL";
|
|
23
|
+
var ENV_API_ORIGIN = "AUTOTEL_PLAYWRIGHT_API_ORIGIN";
|
|
24
|
+
function getApiBaseUrls() {
|
|
25
|
+
const a = process.env[ENV_API_BASE_URL];
|
|
26
|
+
const b = process.env[ENV_API_ORIGIN];
|
|
27
|
+
const urls = [];
|
|
28
|
+
if (a) urls.push(a.replace(/\/$/, ""));
|
|
29
|
+
if (b) urls.push(b.replace(/\/$/, ""));
|
|
30
|
+
return [...new Set(urls)];
|
|
31
|
+
}
|
|
32
|
+
function urlMatchesApiOrigin(requestUrl, apiBaseUrls) {
|
|
33
|
+
if (apiBaseUrls.length === 0) return false;
|
|
34
|
+
try {
|
|
35
|
+
const u = new URL(requestUrl);
|
|
36
|
+
const requestOrigin = u.origin;
|
|
37
|
+
const requestPathname = u.pathname;
|
|
38
|
+
return apiBaseUrls.some((base2) => {
|
|
39
|
+
try {
|
|
40
|
+
const b = new URL(base2);
|
|
41
|
+
if (requestOrigin !== b.origin) return false;
|
|
42
|
+
const basePathname = b.pathname.replace(/\/$/, "") || "/";
|
|
43
|
+
if (basePathname === "/") return true;
|
|
44
|
+
return requestPathname === basePathname || requestPathname.startsWith(basePathname + "/");
|
|
45
|
+
} catch {
|
|
46
|
+
return requestUrl.startsWith(base2);
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return apiBaseUrls.some((base2) => requestUrl.startsWith(base2));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
var AUTOTEL_ATTRIBUTE_ANNOTATION = "autotel.attribute";
|
|
54
|
+
function setAttributesFromAnnotations(span, testInfo) {
|
|
55
|
+
for (const a of testInfo.annotations) {
|
|
56
|
+
if (a.type !== AUTOTEL_ATTRIBUTE_ANNOTATION || !a.description) continue;
|
|
57
|
+
const entries = a.description.split(";");
|
|
58
|
+
for (const entry of entries) {
|
|
59
|
+
const parts = entry.split("=");
|
|
60
|
+
if (parts.length >= 2) {
|
|
61
|
+
const key = parts[0].trim();
|
|
62
|
+
const value = parts.slice(1).join("=").trim();
|
|
63
|
+
span.setAttribute(key, value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function mergeTraceHeaders(url, options, apiBaseUrls, carrier, testName) {
|
|
69
|
+
const opts = options ?? {};
|
|
70
|
+
if (!urlMatchesApiOrigin(url, apiBaseUrls)) return opts;
|
|
71
|
+
return {
|
|
72
|
+
...opts,
|
|
73
|
+
headers: { ...opts.headers, ...carrier, "x-test-name": testName }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
function createRequestWithTrace(request, apiBaseUrls, carrier, testInfo) {
|
|
77
|
+
const merge = (url, options) => mergeTraceHeaders(url, options, apiBaseUrls, carrier, testInfo.title);
|
|
78
|
+
return {
|
|
79
|
+
get: (url, options) => request.get(url, merge(url, options)),
|
|
80
|
+
post: (url, options) => request.post(url, merge(url, options)),
|
|
81
|
+
put: (url, options) => request.put(url, merge(url, options)),
|
|
82
|
+
patch: (url, options) => request.patch(url, merge(url, options)),
|
|
83
|
+
delete: (url, options) => request.delete(url, merge(url, options)),
|
|
84
|
+
head: (url, options) => request.head(url, merge(url, options)),
|
|
85
|
+
fetch: (urlOrRequest, options) => request.fetch(urlOrRequest, merge(typeof urlOrRequest === "string" ? urlOrRequest : urlOrRequest.url(), options)),
|
|
86
|
+
storageState: (options) => request.storageState(options),
|
|
87
|
+
dispose: () => request.dispose()
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
var test = test$1.extend({
|
|
91
|
+
_otelTestSpan: [
|
|
92
|
+
// eslint-disable-next-line no-empty-pattern
|
|
93
|
+
async ({}, use, testInfo) => {
|
|
94
|
+
ensureCollector();
|
|
95
|
+
const apiBaseUrls = getApiBaseUrls();
|
|
96
|
+
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
97
|
+
const spanName = `e2e:${testInfo.title}`;
|
|
98
|
+
const span = tracer.startSpan(spanName, {
|
|
99
|
+
attributes: {
|
|
100
|
+
"test.title": testInfo.title,
|
|
101
|
+
"test.project": testInfo.project.name,
|
|
102
|
+
"test.file": testInfo.file ?? "",
|
|
103
|
+
"test.line": testInfo.line ?? 0
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
setAttributesFromAnnotations(span, testInfo);
|
|
107
|
+
const ctx = otelTrace.setSpan(context.active(), span);
|
|
108
|
+
const carrier = {};
|
|
109
|
+
propagation.inject(ctx, carrier);
|
|
110
|
+
try {
|
|
111
|
+
await use({ carrier, apiBaseUrls, testInfo });
|
|
112
|
+
} finally {
|
|
113
|
+
span.end();
|
|
114
|
+
const traceId = span.spanContext().traceId;
|
|
115
|
+
const rootSpanId = span.spanContext().spanId;
|
|
116
|
+
const spans = collector.drainTrace(traceId, rootSpanId);
|
|
117
|
+
if (spans.length > 0) {
|
|
118
|
+
testInfo.annotations.push({
|
|
119
|
+
type: "otel-spans",
|
|
120
|
+
description: JSON.stringify(spans)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
{ scope: "test" }
|
|
126
|
+
],
|
|
127
|
+
page: async ({ page, _otelTestSpan }, use) => {
|
|
128
|
+
const { carrier, apiBaseUrls, testInfo } = _otelTestSpan;
|
|
129
|
+
if (apiBaseUrls.length > 0) {
|
|
130
|
+
await page.route("**/*", async (route) => {
|
|
131
|
+
const request = route.request();
|
|
132
|
+
const url = request.url();
|
|
133
|
+
if (urlMatchesApiOrigin(url, apiBaseUrls)) {
|
|
134
|
+
const headers = {
|
|
135
|
+
...request.headers(),
|
|
136
|
+
...carrier,
|
|
137
|
+
"x-test-name": testInfo.title
|
|
138
|
+
};
|
|
139
|
+
await route.continue({ headers });
|
|
140
|
+
} else {
|
|
141
|
+
await route.continue();
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
await use(page);
|
|
146
|
+
},
|
|
147
|
+
requestWithTrace: async ({ request, _otelTestSpan }, use) => {
|
|
148
|
+
const wrapped = createRequestWithTrace(
|
|
149
|
+
request,
|
|
150
|
+
_otelTestSpan.apiBaseUrls,
|
|
151
|
+
_otelTestSpan.carrier,
|
|
152
|
+
_otelTestSpan.testInfo
|
|
153
|
+
);
|
|
154
|
+
await use(wrapped);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
async function step(name, fn) {
|
|
158
|
+
const tracer = getTracer(TRACER_NAME, TRACER_VERSION);
|
|
159
|
+
const span = tracer.startSpan(`step:${name}`, {
|
|
160
|
+
attributes: { "step.name": name }
|
|
161
|
+
});
|
|
162
|
+
try {
|
|
163
|
+
return await context.with(otelTrace.setSpan(context.active(), span), fn);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
span.setStatus({ code: SpanStatusCode.ERROR, message: error instanceof Error ? error.message : "Unknown error" });
|
|
166
|
+
span.recordException(error instanceof Error ? error : new Error(String(error)));
|
|
167
|
+
throw error;
|
|
168
|
+
} finally {
|
|
169
|
+
span.end();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
function createGlobalSetup(initOptions) {
|
|
173
|
+
return async () => {
|
|
174
|
+
const { init } = await import('autotel');
|
|
175
|
+
init({
|
|
176
|
+
service: "e2e-tests",
|
|
177
|
+
debug: true,
|
|
178
|
+
...initOptions
|
|
179
|
+
});
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export { AUTOTEL_ATTRIBUTE_ANNOTATION, createGlobalSetup, step, test };
|
|
184
|
+
//# sourceMappingURL=index.js.map
|
|
185
|
+
//# sourceMappingURL=index.js.map
|