@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 +304 -0
- package/dist/cli.cjs +146 -0
- package/dist/fixture.cjs +291 -0
- package/dist/fixture.cjs.map +1 -0
- package/dist/fixture.d.cts +6 -0
- package/dist/fixture.d.ts +6 -0
- package/dist/fixture.js +265 -0
- package/dist/fixture.js.map +1 -0
- package/dist/index.cjs +773 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +100 -0
- package/dist/index.d.ts +100 -0
- package/dist/index.js +745 -0
- package/dist/index.js.map +1 -0
- package/dist/merge.cjs +115 -0
- package/dist/merge.cjs.map +1 -0
- package/dist/merge.d.cts +12 -0
- package/dist/merge.d.ts +12 -0
- package/dist/merge.js +90 -0
- package/dist/merge.js.map +1 -0
- package/package.json +80 -0
- package/timeline-schema.json +190 -0
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();
|