@testrelic/playwright-analytics 1.0.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 ADDED
@@ -0,0 +1,304 @@
1
+ # @testrelic/playwright-analytics
2
+
3
+ A Playwright custom reporter and navigation-tracking fixture that produces a structured JSON timeline of every page visit, network request, and test result in your test suite.
4
+
5
+ ## What It Does
6
+
7
+ When you run your Playwright tests with this SDK, it generates a single JSON report that captures:
8
+
9
+ - **Navigation timeline** — Every URL visited during each test, in chronological order
10
+ - **Network statistics** — Total requests, bytes transferred, and breakdowns by resource type (scripts, images, stylesheets, fonts, XHR, etc.)
11
+ - **Test results** — Pass/fail status, duration, retry count, and tags for every test
12
+ - **Failure diagnostics** — Error messages, stack traces, and source code snippets pointing to the exact line that failed
13
+ - **CI metadata** — Automatic detection of GitHub Actions, GitLab CI, Jenkins, and CircleCI with build ID, commit SHA, and branch
14
+ - **SPA detection** — Tracks `pushState`, `replaceState`, `popstate`, and `hashchange` navigations in single-page applications
15
+ - **Sensitive data redaction** — Automatically scrubs AWS keys, Bearer tokens, private keys, and credential URLs from the report
16
+
17
+ ## Prerequisites
18
+
19
+ - **Node.js** >= 18
20
+ - **Playwright** >= 1.35.0
21
+ - **Package manager**: npm, pnpm, or yarn
22
+
23
+ ## Installation
24
+
25
+ ```bash
26
+ npm install @testrelic/playwright-analytics
27
+ ```
28
+
29
+ ```bash
30
+ pnpm add @testrelic/playwright-analytics
31
+ ```
32
+
33
+ ```bash
34
+ yarn add @testrelic/playwright-analytics
35
+ ```
36
+
37
+ ## Quick Start
38
+
39
+ ### 1. Add the reporter to your Playwright config
40
+
41
+ ```typescript
42
+ // playwright.config.ts
43
+ import { defineConfig } from '@playwright/test';
44
+
45
+ export default defineConfig({
46
+ reporter: [
47
+ ['list'],
48
+ ['@testrelic/playwright-analytics', {
49
+ outputPath: './test-results/analytics-timeline.json',
50
+ includeStackTrace: true,
51
+ includeCodeSnippets: true,
52
+ includeNetworkStats: true,
53
+ }],
54
+ ],
55
+ });
56
+ ```
57
+
58
+ ### 2. Use the fixture in your tests
59
+
60
+ Replace your Playwright `test` import with the TestRelic fixture to enable automatic navigation tracking:
61
+
62
+ ```typescript
63
+ // my-test.spec.ts
64
+ import { test, expect } from '@testrelic/playwright-analytics/fixture';
65
+
66
+ test('homepage loads correctly', async ({ page }) => {
67
+ await page.goto('https://example.com');
68
+ await expect(page.locator('h1')).toBeVisible();
69
+ });
70
+ ```
71
+
72
+ That's it. Run your tests as usual:
73
+
74
+ ```bash
75
+ npx playwright test
76
+ ```
77
+
78
+ The JSON report will be written to `./test-results/analytics-timeline.json` (or wherever you set `outputPath`).
79
+
80
+ ## Configuration Options
81
+
82
+ All options are passed as the second element of the reporter tuple in your Playwright config:
83
+
84
+ | Option | Type | Default | Description |
85
+ |---|---|---|---|
86
+ | `outputPath` | `string` | `./test-results/analytics-timeline.json` | Where to write the JSON report |
87
+ | `includeStackTrace` | `boolean` | `false` | Include full stack traces in failure diagnostics |
88
+ | `includeCodeSnippets` | `boolean` | `true` | Include source code snippets around the failure line |
89
+ | `codeContextLines` | `number` | `3` | Number of lines above/below the failure line to include |
90
+ | `includeNetworkStats` | `boolean` | `true` | Track network requests and byte sizes per navigation |
91
+ | `navigationTypes` | `NavigationType[] \| null` | `null` (all) | Filter timeline to specific navigation types only |
92
+ | `redactPatterns` | `(string \| RegExp)[]` | Built-in patterns | Additional patterns to redact from error messages and stacks |
93
+ | `testRunId` | `string \| null` | `null` (auto UUID) | Override the test run ID |
94
+ | `metadata` | `Record<string, unknown> \| null` | `null` | Attach custom metadata to the report |
95
+
96
+ ### Configuration Example
97
+
98
+ ```typescript
99
+ ['@testrelic/playwright-analytics', {
100
+ outputPath: './reports/timeline.json',
101
+ includeStackTrace: true,
102
+ includeCodeSnippets: true,
103
+ codeContextLines: 5,
104
+ includeNetworkStats: true,
105
+ navigationTypes: ['goto', 'navigation', 'back', 'forward'],
106
+ metadata: {
107
+ team: 'frontend',
108
+ environment: 'staging',
109
+ },
110
+ redactPatterns: [
111
+ /my-secret-pattern/g,
112
+ 'literal-string-to-redact',
113
+ ],
114
+ }]
115
+ ```
116
+
117
+ ## Output Format
118
+
119
+ The report is a single JSON file with this structure:
120
+
121
+ ```jsonc
122
+ {
123
+ "schemaVersion": "1.0.0",
124
+ "testRunId": "797128f5-c86d-466c-8d6d-8ec62dfc70b6",
125
+ "startedAt": "2026-02-07T10:41:28.759Z",
126
+ "completedAt": "2026-02-07T10:41:36.794Z",
127
+ "totalDuration": 8035,
128
+ "summary": {
129
+ "total": 6,
130
+ "passed": 5,
131
+ "failed": 1,
132
+ "flaky": 0,
133
+ "skipped": 0
134
+ },
135
+ "ci": null, // Auto-populated in CI environments
136
+ "metadata": null, // Custom metadata from config
137
+ "timeline": [
138
+ {
139
+ "url": "https://en.wikipedia.org/wiki/Main_Page",
140
+ "navigationType": "goto",
141
+ "visitedAt": "2026-02-07T10:41:29.844Z",
142
+ "duration": 216,
143
+ "specFile": "sdk-validation.spec.ts",
144
+ "domContentLoadedAt": "2026-02-07T10:41:30.200Z",
145
+ "networkIdleAt": null,
146
+ "networkStats": {
147
+ "totalRequests": 40,
148
+ "failedRequests": 0,
149
+ "totalBytes": 1289736,
150
+ "byType": {
151
+ "xhr": 0,
152
+ "document": 1,
153
+ "script": 9,
154
+ "stylesheet": 2,
155
+ "image": 27,
156
+ "font": 2,
157
+ "other": 0
158
+ }
159
+ },
160
+ "tests": [
161
+ {
162
+ "title": "sdk-validation.spec.ts > SDK E2E Validation > homepage visit",
163
+ "status": "passed",
164
+ "duration": 1028,
165
+ "startedAt": "2026-02-07T10:41:29.133Z",
166
+ "completedAt": "2026-02-07T10:41:30.161Z",
167
+ "retryCount": 0,
168
+ "tags": [],
169
+ "failure": null
170
+ }
171
+ ]
172
+ }
173
+ // ... more timeline entries
174
+ ],
175
+ "shardRunIds": null // Populated when merging shard reports
176
+ }
177
+ ```
178
+
179
+ ### Timeline Entry Fields
180
+
181
+ | Field | Type | Description |
182
+ |---|---|---|
183
+ | `url` | `string` | The URL that was navigated to |
184
+ | `navigationType` | `string` | How the navigation happened (see navigation types below) |
185
+ | `visitedAt` | `string` | ISO-8601 timestamp of the navigation |
186
+ | `duration` | `number` | Milliseconds until the next navigation or test end |
187
+ | `specFile` | `string` | Relative path to the spec file |
188
+ | `domContentLoadedAt` | `string \| null` | When DOMContentLoaded fired |
189
+ | `networkIdleAt` | `string \| null` | When network became idle |
190
+ | `networkStats` | `object \| null` | Network request statistics for this navigation |
191
+ | `tests` | `array` | Test results associated with this navigation |
192
+
193
+ ### Navigation Types
194
+
195
+ | Type | Description |
196
+ |---|---|
197
+ | `goto` | `page.goto()` call |
198
+ | `navigation` | Browser frame navigation (link click, form submit) |
199
+ | `back` | `page.goBack()` call |
200
+ | `forward` | `page.goForward()` call |
201
+ | `refresh` | `page.reload()` call |
202
+ | `spa_route` | `history.pushState()` detected |
203
+ | `spa_replace` | `history.replaceState()` detected |
204
+ | `hash_change` | URL hash change |
205
+ | `popstate` | Browser back/forward via popstate event |
206
+
207
+ ### Network Stats
208
+
209
+ When `includeNetworkStats` is enabled, each timeline entry includes:
210
+
211
+ ```json
212
+ {
213
+ "totalRequests": 40,
214
+ "failedRequests": 0,
215
+ "totalBytes": 1289736,
216
+ "byType": {
217
+ "xhr": 0,
218
+ "document": 1,
219
+ "script": 9,
220
+ "stylesheet": 2,
221
+ "image": 27,
222
+ "font": 2,
223
+ "other": 0
224
+ }
225
+ }
226
+ ```
227
+
228
+ ### Failure Diagnostics
229
+
230
+ When a test fails, the report includes diagnostic details:
231
+
232
+ ```json
233
+ {
234
+ "message": "expect(received).toBe(expected) // Object.is equality\n\nExpected: 3\nReceived: 2",
235
+ "line": 70,
236
+ "code": " 67 | test('deliberate failure', async ({ page }) => {\n 68 | await page.goto('https://example.com');\n 69 | // This assertion will fail\n> 70 | expect(1 + 1).toBe(3);\n 71 | });",
237
+ "stack": "Error: expect(received).toBe(expected)\n at /path/to/spec.ts:70:19"
238
+ }
239
+ ```
240
+
241
+ ### CI Metadata
242
+
243
+ When running in a supported CI environment, the `ci` field is auto-populated:
244
+
245
+ ```json
246
+ {
247
+ "provider": "github-actions",
248
+ "buildId": "12345678",
249
+ "commitSha": "abc123def456",
250
+ "branch": "main"
251
+ }
252
+ ```
253
+
254
+ Supported providers: **GitHub Actions**, **GitLab CI**, **Jenkins**, **CircleCI**.
255
+
256
+ ## Merging Shard Reports
257
+
258
+ When running Playwright with sharding, each shard produces its own report. Use the CLI to merge them:
259
+
260
+ ```bash
261
+ npx testrelic merge shard-1.json shard-2.json shard-3.json -o merged-report.json
262
+ ```
263
+
264
+ Or programmatically:
265
+
266
+ ```typescript
267
+ import { mergeReports } from '@testrelic/playwright-analytics/merge';
268
+
269
+ const merged = await mergeReports(
270
+ ['shard-1.json', 'shard-2.json'],
271
+ { output: 'merged-report.json' }
272
+ );
273
+ ```
274
+
275
+ The merged report combines all timelines chronologically, recalculates the summary, and records the original shard run IDs.
276
+
277
+ ## Security
278
+
279
+ The reporter automatically redacts sensitive data from error messages, stack traces, and code snippets before writing the report:
280
+
281
+ - AWS access key IDs (`AKIA...`)
282
+ - Bearer tokens (`Bearer eyJ...`)
283
+ - Private keys (`-----BEGIN PRIVATE KEY-----`)
284
+ - Credential URLs (`//user:password@host`)
285
+
286
+ You can add custom redaction patterns via the `redactPatterns` config option.
287
+
288
+ ## How It Works
289
+
290
+ The SDK has two components that work together:
291
+
292
+ 1. **Fixture** (`@testrelic/playwright-analytics/fixture`) — Wraps the Playwright `page` object with a `NavigationTracker` that intercepts `goto`, `goBack`, `goForward`, and `reload` calls, listens for `framenavigated` events, injects a script to detect SPA navigations, and optionally tracks network responses. All tracked data is written as test annotations.
293
+
294
+ 2. **Reporter** (`@testrelic/playwright-analytics`) — A Playwright custom reporter that collects test results and navigation annotations in `onTestEnd`, then on `onEnd` builds the final JSON timeline, detects CI metadata, applies redaction, and writes the report atomically (write to temp file, then rename).
295
+
296
+ The reporter never throws during test execution — all hooks are wrapped in try/catch to ensure it never crashes your test run.
297
+
298
+ ## Using Without the Fixture
299
+
300
+ The reporter works without the fixture, but navigation tracking will not be available. Tests will still be captured with their pass/fail status, duration, and failure diagnostics. The timeline entries will show `about:blank` as the URL with `dummy` navigation type.
301
+
302
+ ## License
303
+
304
+ MIT
package/dist/cli.cjs ADDED
@@ -0,0 +1,146 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // src/merge.ts
5
+ var import_node_crypto = require("crypto");
6
+ var import_node_fs = require("fs");
7
+ var import_node_path = require("path");
8
+ var import_core = require("@testrelic/core");
9
+ async function mergeReports(files, options) {
10
+ const reports = [];
11
+ for (const file of files) {
12
+ let raw;
13
+ try {
14
+ raw = (0, import_node_fs.readFileSync)(file, "utf-8");
15
+ } catch (err) {
16
+ throw (0, import_core.createError)(
17
+ import_core.ErrorCode.MERGE_READ_FAILED,
18
+ `Failed to read file: ${file}`,
19
+ err
20
+ );
21
+ }
22
+ let parsed;
23
+ try {
24
+ parsed = JSON.parse(raw);
25
+ } catch (err) {
26
+ throw (0, import_core.createError)(
27
+ import_core.ErrorCode.MERGE_INVALID_SCHEMA,
28
+ `Invalid JSON in file: ${file}`,
29
+ err
30
+ );
31
+ }
32
+ if (!(0, import_core.isValidTestRunReport)(parsed)) {
33
+ throw (0, import_core.createError)(
34
+ import_core.ErrorCode.MERGE_INVALID_SCHEMA,
35
+ `Invalid report schema in file: ${file}`
36
+ );
37
+ }
38
+ reports.push(parsed);
39
+ }
40
+ const shardRunIds = reports.map((r) => r.testRunId);
41
+ const allTimelines = [];
42
+ for (const report of reports) {
43
+ allTimelines.push(...report.timeline);
44
+ }
45
+ allTimelines.sort(
46
+ (a, b) => new Date(a.visitedAt).getTime() - new Date(b.visitedAt).getTime()
47
+ );
48
+ const summary = recalculateSummary(reports);
49
+ const startedAt = reports.reduce(
50
+ (earliest, r) => r.startedAt < earliest ? r.startedAt : earliest,
51
+ reports[0]?.startedAt ?? (/* @__PURE__ */ new Date()).toISOString()
52
+ );
53
+ const completedAt = reports.reduce(
54
+ (latest, r) => r.completedAt > latest ? r.completedAt : latest,
55
+ reports[0]?.completedAt ?? (/* @__PURE__ */ new Date()).toISOString()
56
+ );
57
+ const totalDuration = new Date(completedAt).getTime() - new Date(startedAt).getTime();
58
+ const merged = {
59
+ schemaVersion: reports[0]?.schemaVersion ?? "1.0.0",
60
+ testRunId: options.testRunId ?? (0, import_node_crypto.randomUUID)(),
61
+ startedAt,
62
+ completedAt,
63
+ totalDuration,
64
+ summary,
65
+ ci: reports.find((r) => r.ci !== null)?.ci ?? null,
66
+ metadata: reports.find((r) => r.metadata !== null)?.metadata ?? null,
67
+ timeline: allTimelines,
68
+ shardRunIds
69
+ };
70
+ const dir = (0, import_node_path.dirname)(options.output);
71
+ (0, import_node_fs.mkdirSync)(dir, { recursive: true });
72
+ (0, import_node_fs.writeFileSync)(options.output, JSON.stringify(merged, null, 2), "utf-8");
73
+ return merged;
74
+ }
75
+ function recalculateSummary(reports) {
76
+ let total = 0;
77
+ let passed = 0;
78
+ let failed = 0;
79
+ let flaky = 0;
80
+ let skipped = 0;
81
+ for (const report of reports) {
82
+ total += report.summary.total;
83
+ passed += report.summary.passed;
84
+ failed += report.summary.failed;
85
+ flaky += report.summary.flaky;
86
+ skipped += report.summary.skipped;
87
+ }
88
+ return { total, passed, failed, flaky, skipped };
89
+ }
90
+
91
+ // src/cli.ts
92
+ async function main() {
93
+ const args = process.argv.slice(2);
94
+ if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
95
+ printUsage();
96
+ process.exit(0);
97
+ }
98
+ if (args[0] !== "merge") {
99
+ process.stderr.write(`Unknown command: ${args[0]}
100
+ `);
101
+ printUsage();
102
+ process.exit(1);
103
+ }
104
+ const mergeArgs = args.slice(1);
105
+ const outputIndex = mergeArgs.findIndex((a) => a === "-o" || a === "--output");
106
+ if (outputIndex === -1 || outputIndex === mergeArgs.length - 1) {
107
+ process.stderr.write("Error: -o <output> is required\n");
108
+ printUsage();
109
+ process.exit(1);
110
+ }
111
+ const output = mergeArgs[outputIndex + 1];
112
+ const files = [
113
+ ...mergeArgs.slice(0, outputIndex),
114
+ ...mergeArgs.slice(outputIndex + 2)
115
+ ];
116
+ if (files.length === 0) {
117
+ process.stderr.write("Error: No input files specified\n");
118
+ printUsage();
119
+ process.exit(1);
120
+ }
121
+ try {
122
+ const merged = await mergeReports(files, { output });
123
+ process.stderr.write(
124
+ `Merged ${files.length} reports (${merged.summary.total} tests) \u2192 ${output}
125
+ `
126
+ );
127
+ } catch (err) {
128
+ process.stderr.write(
129
+ `Error: ${err instanceof Error ? err.message : String(err)}
130
+ `
131
+ );
132
+ process.exit(1);
133
+ }
134
+ }
135
+ function printUsage() {
136
+ process.stderr.write(
137
+ `Usage: testrelic merge <files...> -o <output>
138
+
139
+ Merge multiple shard report JSON files into one.
140
+
141
+ Example:
142
+ npx testrelic merge shard-1.json shard-2.json -o merged.json
143
+ `
144
+ );
145
+ }
146
+ main();