@testpulse.run/playwright-jsonl-reporter 0.2.3
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 +72 -0
- package/dist/index.cjs +568 -0
- package/dist/index.d.cts +166 -0
- package/dist/index.d.ts +166 -0
- package/dist/index.js +545 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://testpulse.run">
|
|
3
|
+
<img src="https://testpulse.run/images/logo-testpulse.svg" alt="TestPulse.run" height="48">
|
|
4
|
+
</a>
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<a href="https://testpulse.run">testpulse.run</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
# @testpulse.run/playwright-jsonl-reporter
|
|
12
|
+
|
|
13
|
+
Append-only JSONL reporter for Playwright TestPulse events.
|
|
14
|
+
|
|
15
|
+
Use this package when you want Playwright run events as newline-delimited JSON for local files, custom ingestion pipelines, debugging, or tests around reporter behavior.
|
|
16
|
+
|
|
17
|
+
Most TestPulse users should install `@testpulse.run/reporter`, which uses this package with an HTTP sink preconfigured for the TestPulse API.
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
npm install -D @testpulse.run/playwright-jsonl-reporter @playwright/test
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Playwright Configuration
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { defineConfig } from "@playwright/test";
|
|
29
|
+
|
|
30
|
+
export default defineConfig({
|
|
31
|
+
reporter: [
|
|
32
|
+
["@testpulse.run/playwright-jsonl-reporter", { outputDir: "testpulse-events" }]
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
By default the reporter writes JSONL files to `testpulse-events`.
|
|
38
|
+
|
|
39
|
+
## Options
|
|
40
|
+
|
|
41
|
+
- `outputDir`: directory for filesystem JSONL output.
|
|
42
|
+
- `runId`: explicit run identifier. A process/time based id is generated by default.
|
|
43
|
+
- `includeCoreEvents`: include normalized Playwright lifecycle events; defaults to `true`.
|
|
44
|
+
- `includeDerivedEvents`: include derived attempt and outcome events; defaults to `true`.
|
|
45
|
+
- `failOnHandlerError`: cause Playwright to fail when reporter handlers throw.
|
|
46
|
+
- `sink`: custom event sink for filesystem, HTTP, or in-memory integrations.
|
|
47
|
+
|
|
48
|
+
## Custom Sink
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
import { JsonlReporter, type EventSink, type JsonlEvent } from "@testpulse.run/playwright-jsonl-reporter";
|
|
52
|
+
|
|
53
|
+
class MemorySink implements EventSink {
|
|
54
|
+
readonly metadata = { writerId: "memory", sinkType: "memory" };
|
|
55
|
+
readonly events: JsonlEvent[] = [];
|
|
56
|
+
|
|
57
|
+
write(event: JsonlEvent) {
|
|
58
|
+
this.events.push(event);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async flush() {}
|
|
62
|
+
async close() {}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export default new JsonlReporter({ sink: new MemorySink() });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Related Packages
|
|
69
|
+
|
|
70
|
+
- `@testpulse.run/reporter`: preconfigured HTTP reporter for TestPulse.
|
|
71
|
+
- `@testpulse.run/playwright-events`: derived test attempt and outcome events.
|
|
72
|
+
- `@testpulse.run/playwright-core`: normalized Playwright reporter lifecycle events.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
FileSystemEventSink: () => FileSystemEventSink,
|
|
24
|
+
HttpEventSink: () => HttpEventSink,
|
|
25
|
+
JsonlReporter: () => JsonlReporter,
|
|
26
|
+
default: () => reporter_default
|
|
27
|
+
});
|
|
28
|
+
module.exports = __toCommonJS(index_exports);
|
|
29
|
+
|
|
30
|
+
// src/reporter.ts
|
|
31
|
+
var import_playwright_core = require("@testpulse.run/playwright-core");
|
|
32
|
+
var import_playwright_events = require("@testpulse.run/playwright-events");
|
|
33
|
+
|
|
34
|
+
// src/serialize.ts
|
|
35
|
+
function serializeCoreEvent(event, context) {
|
|
36
|
+
return serializeEvent(event, context, "core");
|
|
37
|
+
}
|
|
38
|
+
function serializeDerivedEvent(event, context) {
|
|
39
|
+
return serializeEvent(event, context, "derived");
|
|
40
|
+
}
|
|
41
|
+
function serializeEvent(event, context, stream) {
|
|
42
|
+
const payload = createPayload(event);
|
|
43
|
+
const worker = readWorker(event);
|
|
44
|
+
const test = readTest(event);
|
|
45
|
+
const step = readStep(event);
|
|
46
|
+
const terminal = readTerminalFacts(event);
|
|
47
|
+
const run = readRunFacts(event);
|
|
48
|
+
const eventName = event.eventName;
|
|
49
|
+
return omitUndefined({
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
eventId: context.eventId,
|
|
52
|
+
sequence: context.sequence,
|
|
53
|
+
type: eventName,
|
|
54
|
+
stream,
|
|
55
|
+
eventName,
|
|
56
|
+
timestamp: event.timestamp,
|
|
57
|
+
source: event.source,
|
|
58
|
+
runId: context.runId,
|
|
59
|
+
writerId: context.writerId,
|
|
60
|
+
shardId: context.shard?.shardId,
|
|
61
|
+
shardIndex: context.shard?.shardIndex,
|
|
62
|
+
shardCount: context.shard?.shardCount,
|
|
63
|
+
workerId: worker ? `worker-${worker.workerIndex}` : void 0,
|
|
64
|
+
workerIndex: worker?.workerIndex,
|
|
65
|
+
parallelIndex: worker?.parallelIndex,
|
|
66
|
+
testId: test?.testId,
|
|
67
|
+
testTitle: test?.title,
|
|
68
|
+
file: test?.location?.file,
|
|
69
|
+
line: test?.location?.line,
|
|
70
|
+
column: test?.location?.column,
|
|
71
|
+
projectName: test?.projectName,
|
|
72
|
+
retry: readRetry(event),
|
|
73
|
+
repeatEachIndex: test?.repeatEachIndex,
|
|
74
|
+
expectedStatus: readExpectedStatus(event, test),
|
|
75
|
+
stepId: step?.stepId,
|
|
76
|
+
parentStepId: step?.parentStepId,
|
|
77
|
+
stepTitle: step?.title,
|
|
78
|
+
stepCategory: step?.category,
|
|
79
|
+
stepFile: step?.location?.file,
|
|
80
|
+
stepLine: step?.location?.line,
|
|
81
|
+
stepColumn: step?.location?.column,
|
|
82
|
+
status: terminal.status ?? run.status,
|
|
83
|
+
durationMs: terminal.durationMs ?? run.durationMs,
|
|
84
|
+
startTime: run.startTime,
|
|
85
|
+
totalTests: run.totalTests,
|
|
86
|
+
configuredWorkers: run.configuredWorkers,
|
|
87
|
+
errors: normalizeErrors(terminal.errors),
|
|
88
|
+
attachments: terminal.attachments,
|
|
89
|
+
outcome: "outcome" in event ? event.outcome : void 0,
|
|
90
|
+
payload: Object.keys(payload).length > 0 ? payload : void 0
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function createPayload(event) {
|
|
94
|
+
const payload = {};
|
|
95
|
+
for (const [key, value] of Object.entries(event)) {
|
|
96
|
+
if (value === void 0 || TOP_LEVEL_OR_STRUCTURAL_KEYS.has(key)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
payload[key] = value;
|
|
100
|
+
}
|
|
101
|
+
return payload;
|
|
102
|
+
}
|
|
103
|
+
function readWorker(event) {
|
|
104
|
+
return "worker" in event ? event.worker : void 0;
|
|
105
|
+
}
|
|
106
|
+
function readTest(event) {
|
|
107
|
+
return "test" in event ? event.test : void 0;
|
|
108
|
+
}
|
|
109
|
+
function readStep(event) {
|
|
110
|
+
return "step" in event ? event.step : void 0;
|
|
111
|
+
}
|
|
112
|
+
function readRetry(event) {
|
|
113
|
+
if ("retry" in event) {
|
|
114
|
+
return event.retry;
|
|
115
|
+
}
|
|
116
|
+
if ("attempt" in event) {
|
|
117
|
+
return event.attempt.retry;
|
|
118
|
+
}
|
|
119
|
+
if ("finalAttempt" in event) {
|
|
120
|
+
return event.finalAttempt.retry;
|
|
121
|
+
}
|
|
122
|
+
return void 0;
|
|
123
|
+
}
|
|
124
|
+
function readExpectedStatus(event, test) {
|
|
125
|
+
return "expectedStatus" in event ? event.expectedStatus : test?.expectedStatus;
|
|
126
|
+
}
|
|
127
|
+
function readTerminalFacts(event) {
|
|
128
|
+
if ("attempt" in event) {
|
|
129
|
+
return readAttemptFacts(event.attempt);
|
|
130
|
+
}
|
|
131
|
+
if ("finalAttempt" in event) {
|
|
132
|
+
return readAttemptFacts(event.finalAttempt);
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
status: "status" in event ? event.status : void 0,
|
|
136
|
+
durationMs: "durationMs" in event ? event.durationMs : void 0,
|
|
137
|
+
errors: readErrors(event),
|
|
138
|
+
attachments: "attachments" in event ? event.attachments : void 0
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function readAttemptFacts(attempt) {
|
|
142
|
+
return {
|
|
143
|
+
status: attempt.status,
|
|
144
|
+
durationMs: attempt.durationMs,
|
|
145
|
+
errors: attempt.errors,
|
|
146
|
+
attachments: attempt.attachments
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function normalizeErrors(errors) {
|
|
150
|
+
return errors?.map((error) => ({
|
|
151
|
+
message: error.message,
|
|
152
|
+
stack: error.stack,
|
|
153
|
+
value: error.value,
|
|
154
|
+
file: error.location?.file,
|
|
155
|
+
line: error.location?.line,
|
|
156
|
+
column: error.location?.column
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
function readRunFacts(event) {
|
|
160
|
+
const result = "result" in event ? event.result : void 0;
|
|
161
|
+
return {
|
|
162
|
+
startTime: result?.startTime,
|
|
163
|
+
status: result?.status,
|
|
164
|
+
durationMs: result?.durationMs,
|
|
165
|
+
totalTests: "totalTests" in event ? event.totalTests : "suite" in event ? event.suite.totalTests : void 0,
|
|
166
|
+
configuredWorkers: "configuredWorkers" in event ? event.configuredWorkers : "config" in event ? event.config.configuredWorkers : void 0
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function readErrors(event) {
|
|
170
|
+
if ("errors" in event) {
|
|
171
|
+
return event.errors;
|
|
172
|
+
}
|
|
173
|
+
if ("error" in event) {
|
|
174
|
+
return event.error ? [event.error] : void 0;
|
|
175
|
+
}
|
|
176
|
+
return void 0;
|
|
177
|
+
}
|
|
178
|
+
var TOP_LEVEL_OR_STRUCTURAL_KEYS = /* @__PURE__ */ new Set([
|
|
179
|
+
"eventName",
|
|
180
|
+
"timestamp",
|
|
181
|
+
"source",
|
|
182
|
+
"test",
|
|
183
|
+
"step",
|
|
184
|
+
"result",
|
|
185
|
+
"config",
|
|
186
|
+
"suite",
|
|
187
|
+
"shard",
|
|
188
|
+
"worker",
|
|
189
|
+
"testId",
|
|
190
|
+
"title",
|
|
191
|
+
"titlePath",
|
|
192
|
+
"totalTests",
|
|
193
|
+
"configuredWorkers",
|
|
194
|
+
"location",
|
|
195
|
+
"projectName",
|
|
196
|
+
"retryCount",
|
|
197
|
+
"repeatEachIndex",
|
|
198
|
+
"retry",
|
|
199
|
+
"attempt",
|
|
200
|
+
"attemptIndex",
|
|
201
|
+
"status",
|
|
202
|
+
"durationMs",
|
|
203
|
+
"expectedStatus",
|
|
204
|
+
"errors",
|
|
205
|
+
"error",
|
|
206
|
+
"attachments",
|
|
207
|
+
"outcome",
|
|
208
|
+
"finalAttempt",
|
|
209
|
+
"attempts",
|
|
210
|
+
"coreEvent"
|
|
211
|
+
]);
|
|
212
|
+
function omitUndefined(event) {
|
|
213
|
+
return Object.fromEntries(Object.entries(event).filter(([, value]) => value !== void 0));
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// src/sink.ts
|
|
217
|
+
var import_node_crypto = require("crypto");
|
|
218
|
+
var import_node_fs = require("fs");
|
|
219
|
+
var import_node_path = require("path");
|
|
220
|
+
var FileSystemEventSink = class {
|
|
221
|
+
filePath;
|
|
222
|
+
metadata;
|
|
223
|
+
stream;
|
|
224
|
+
closed = false;
|
|
225
|
+
streamError;
|
|
226
|
+
constructor(options = {}) {
|
|
227
|
+
const outputDir = (0, import_node_path.resolve)(options.outputDir ?? "testpulse-events");
|
|
228
|
+
(0, import_node_fs.mkdirSync)(outputDir, { recursive: true });
|
|
229
|
+
this.metadata = {
|
|
230
|
+
writerId: options.writerId ?? createWriterId(),
|
|
231
|
+
sinkType: "filesystem"
|
|
232
|
+
};
|
|
233
|
+
this.filePath = (0, import_node_path.resolve)(outputDir, options.fileName ?? createDefaultFileName());
|
|
234
|
+
this.stream = (0, import_node_fs.createWriteStream)(this.filePath, { flags: "a" });
|
|
235
|
+
this.stream.on("error", (error) => {
|
|
236
|
+
this.streamError = error;
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
write(event) {
|
|
240
|
+
if (this.closed) {
|
|
241
|
+
throw new Error("Cannot write to a closed FileSystemEventSink.");
|
|
242
|
+
}
|
|
243
|
+
if (this.streamError) {
|
|
244
|
+
throw this.streamError;
|
|
245
|
+
}
|
|
246
|
+
this.stream.write(`${JSON.stringify(event)}
|
|
247
|
+
`, (error) => {
|
|
248
|
+
if (error) {
|
|
249
|
+
this.streamError = error;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
async flush() {
|
|
254
|
+
if (this.closed) {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (this.streamError) {
|
|
258
|
+
throw this.streamError;
|
|
259
|
+
}
|
|
260
|
+
await new Promise((resolvePromise, reject) => {
|
|
261
|
+
this.stream.write("", (error) => {
|
|
262
|
+
if (error) {
|
|
263
|
+
this.streamError = error;
|
|
264
|
+
reject(error);
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (this.streamError) {
|
|
268
|
+
reject(this.streamError);
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
resolvePromise();
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
async close() {
|
|
276
|
+
if (this.closed) {
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
this.closed = true;
|
|
280
|
+
await new Promise((resolvePromise, reject) => {
|
|
281
|
+
this.stream.end((error) => {
|
|
282
|
+
if (error) {
|
|
283
|
+
this.streamError = error;
|
|
284
|
+
reject(error);
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (this.streamError) {
|
|
288
|
+
reject(this.streamError);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
resolvePromise();
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
function createDefaultFileName() {
|
|
297
|
+
return `events-pid-${process.pid}-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.jsonl`;
|
|
298
|
+
}
|
|
299
|
+
function createWriterId() {
|
|
300
|
+
return `filesystem-${process.pid}-${(0, import_node_crypto.randomUUID)()}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/reporter.ts
|
|
304
|
+
var JsonlReporter = class {
|
|
305
|
+
core;
|
|
306
|
+
events;
|
|
307
|
+
sink;
|
|
308
|
+
configuredRunId;
|
|
309
|
+
nextEventId = 1;
|
|
310
|
+
runId = createRunId();
|
|
311
|
+
shard;
|
|
312
|
+
constructor(options = {}) {
|
|
313
|
+
this.sink = options.sink ?? new FileSystemEventSink({ outputDir: options.outputDir });
|
|
314
|
+
this.configuredRunId = options.runId;
|
|
315
|
+
const includeCoreEvents = options.includeCoreEvents ?? true;
|
|
316
|
+
const includeDerivedEvents = options.includeDerivedEvents ?? true;
|
|
317
|
+
const corePlugins = [];
|
|
318
|
+
const derivedPlugins = [];
|
|
319
|
+
if (includeDerivedEvents) {
|
|
320
|
+
derivedPlugins.push(this.createDerivedPlugin());
|
|
321
|
+
}
|
|
322
|
+
this.events = new import_playwright_events.PlaywrightEvents({ plugins: derivedPlugins });
|
|
323
|
+
if (includeCoreEvents) {
|
|
324
|
+
corePlugins.push(this.createCorePlugin());
|
|
325
|
+
}
|
|
326
|
+
corePlugins.push(this.events.asCorePlugin());
|
|
327
|
+
if (!includeCoreEvents) {
|
|
328
|
+
corePlugins.push(this.createSinkFinalizerPlugin());
|
|
329
|
+
}
|
|
330
|
+
this.core = new import_playwright_core.TestPulseReporterCore({
|
|
331
|
+
plugins: corePlugins,
|
|
332
|
+
failOnHandlerError: options.failOnHandlerError
|
|
333
|
+
});
|
|
334
|
+
this.events.setErrorSink(this.core);
|
|
335
|
+
}
|
|
336
|
+
onBegin(config, suite) {
|
|
337
|
+
this.nextEventId = 1;
|
|
338
|
+
this.runId = this.configuredRunId ?? createRunId();
|
|
339
|
+
this.shard = void 0;
|
|
340
|
+
this.core.onBegin(config, suite);
|
|
341
|
+
}
|
|
342
|
+
onEnd(result) {
|
|
343
|
+
return this.core.onEnd(result);
|
|
344
|
+
}
|
|
345
|
+
onError(...args) {
|
|
346
|
+
this.core.onError(...args);
|
|
347
|
+
}
|
|
348
|
+
onTestBegin(...args) {
|
|
349
|
+
this.core.onTestBegin(...args);
|
|
350
|
+
}
|
|
351
|
+
onTestEnd(...args) {
|
|
352
|
+
this.core.onTestEnd(...args);
|
|
353
|
+
}
|
|
354
|
+
onStepBegin(...args) {
|
|
355
|
+
this.core.onStepBegin(...args);
|
|
356
|
+
}
|
|
357
|
+
onStepEnd(...args) {
|
|
358
|
+
this.core.onStepEnd(...args);
|
|
359
|
+
}
|
|
360
|
+
onStdOut(...args) {
|
|
361
|
+
this.core.onStdOut(...args);
|
|
362
|
+
}
|
|
363
|
+
onStdErr(...args) {
|
|
364
|
+
this.core.onStdErr(...args);
|
|
365
|
+
}
|
|
366
|
+
createCorePlugin() {
|
|
367
|
+
const plugin = { name: "@testpulse.run/playwright-jsonl-reporter/core" };
|
|
368
|
+
for (const eventName of Object.values(import_playwright_core.TestPulseReporterEventName)) {
|
|
369
|
+
plugin[eventName] = ((event) => {
|
|
370
|
+
this.writeCoreEvent(event);
|
|
371
|
+
if (event.eventName === "RunFinished") {
|
|
372
|
+
return this.sink.close();
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
return plugin;
|
|
377
|
+
}
|
|
378
|
+
createDerivedPlugin() {
|
|
379
|
+
const plugin = { name: "@testpulse.run/playwright-jsonl-reporter/derived" };
|
|
380
|
+
for (const eventName of Object.values(import_playwright_events.TestPulseDerivedEventName)) {
|
|
381
|
+
plugin[eventName] = ((event) => {
|
|
382
|
+
this.writeDerivedEvent(event);
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return plugin;
|
|
386
|
+
}
|
|
387
|
+
createSinkFinalizerPlugin() {
|
|
388
|
+
return {
|
|
389
|
+
name: "@testpulse.run/playwright-jsonl-reporter/finalizer",
|
|
390
|
+
RunFinished: () => this.sink.close()
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
writeCoreEvent(event) {
|
|
394
|
+
if (event.eventName === "ShardStarted") {
|
|
395
|
+
this.shard = event.shard;
|
|
396
|
+
}
|
|
397
|
+
this.sink.write(serializeCoreEvent(event, this.createSerializationContext()));
|
|
398
|
+
}
|
|
399
|
+
writeDerivedEvent(event) {
|
|
400
|
+
this.sink.write(serializeDerivedEvent(event, this.createSerializationContext()));
|
|
401
|
+
}
|
|
402
|
+
createSerializationContext() {
|
|
403
|
+
const sequence = this.nextEventId;
|
|
404
|
+
this.nextEventId += 1;
|
|
405
|
+
return {
|
|
406
|
+
eventId: `${this.runId}:${this.sink.metadata.writerId}:${sequence}`,
|
|
407
|
+
sequence,
|
|
408
|
+
runId: this.runId,
|
|
409
|
+
writerId: this.sink.metadata.writerId,
|
|
410
|
+
shard: this.shard ? {
|
|
411
|
+
shardId: `${this.runId}:${this.sink.metadata.writerId}:shard-${this.shard.current}-of-${this.shard.total}`,
|
|
412
|
+
shardIndex: this.shard.current,
|
|
413
|
+
shardCount: this.shard.total
|
|
414
|
+
} : void 0
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
};
|
|
418
|
+
function createRunId() {
|
|
419
|
+
return `run-${process.pid}-${Date.now()}`;
|
|
420
|
+
}
|
|
421
|
+
var reporter_default = JsonlReporter;
|
|
422
|
+
|
|
423
|
+
// src/http-sink.ts
|
|
424
|
+
var import_node_crypto2 = require("crypto");
|
|
425
|
+
var HttpEventSink = class {
|
|
426
|
+
metadata;
|
|
427
|
+
baseUrl;
|
|
428
|
+
apiKey;
|
|
429
|
+
storageStreamId;
|
|
430
|
+
flushIntervalMs;
|
|
431
|
+
batchSize;
|
|
432
|
+
fetchImpl;
|
|
433
|
+
pending = [];
|
|
434
|
+
closed = false;
|
|
435
|
+
inFlight;
|
|
436
|
+
flushTimer;
|
|
437
|
+
lastBackgroundError;
|
|
438
|
+
constructor(options) {
|
|
439
|
+
if (!options.baseUrl) {
|
|
440
|
+
throw new Error("HttpEventSink requires a baseUrl.");
|
|
441
|
+
}
|
|
442
|
+
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
443
|
+
this.apiKey = options.apiKey;
|
|
444
|
+
this.storageStreamId = options.storageStreamId ?? "events";
|
|
445
|
+
this.flushIntervalMs = readPositiveNumber(options.flushIntervalMs, 250, "flushIntervalMs");
|
|
446
|
+
this.batchSize = readPositiveNumber(options.batchSize, 100, "batchSize");
|
|
447
|
+
this.fetchImpl = options.fetch ?? fetch;
|
|
448
|
+
this.metadata = {
|
|
449
|
+
writerId: options.writerId ?? createWriterId2(),
|
|
450
|
+
sinkType: "http"
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
write(event) {
|
|
454
|
+
if (this.closed) {
|
|
455
|
+
throw new Error("Cannot write to a closed HttpEventSink.");
|
|
456
|
+
}
|
|
457
|
+
this.pending.push(event);
|
|
458
|
+
this.ensureFlushTimer();
|
|
459
|
+
if (this.pending.length >= this.batchSize) {
|
|
460
|
+
this.triggerBackgroundFlush();
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
async flush() {
|
|
464
|
+
if (this.inFlight) {
|
|
465
|
+
try {
|
|
466
|
+
await this.inFlight;
|
|
467
|
+
} catch (error) {
|
|
468
|
+
this.lastBackgroundError = error;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
if (this.pending.length === 0) {
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
const events = this.pending.slice();
|
|
475
|
+
this.inFlight = this.sendEvents(events);
|
|
476
|
+
try {
|
|
477
|
+
await this.inFlight;
|
|
478
|
+
this.pending.splice(0, events.length);
|
|
479
|
+
this.lastBackgroundError = void 0;
|
|
480
|
+
} finally {
|
|
481
|
+
this.inFlight = void 0;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
async close() {
|
|
485
|
+
if (this.closed) {
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
this.clearFlushTimer();
|
|
489
|
+
await this.flush();
|
|
490
|
+
this.closed = true;
|
|
491
|
+
}
|
|
492
|
+
ensureFlushTimer() {
|
|
493
|
+
if (this.flushTimer) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
this.flushTimer = setInterval(() => this.triggerBackgroundFlush(), this.flushIntervalMs);
|
|
497
|
+
this.flushTimer.unref?.();
|
|
498
|
+
}
|
|
499
|
+
clearFlushTimer() {
|
|
500
|
+
if (!this.flushTimer) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
clearInterval(this.flushTimer);
|
|
504
|
+
this.flushTimer = void 0;
|
|
505
|
+
}
|
|
506
|
+
triggerBackgroundFlush() {
|
|
507
|
+
if (this.inFlight) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
void this.flush().catch((error) => {
|
|
511
|
+
this.lastBackgroundError = error;
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
async sendEvents(events) {
|
|
515
|
+
for (const group of groupByRun(events)) {
|
|
516
|
+
const response = await this.fetchImpl(this.createEndpoint(group.runId), {
|
|
517
|
+
method: "POST",
|
|
518
|
+
headers: this.createHeaders(),
|
|
519
|
+
body: JSON.stringify({ events: group.events })
|
|
520
|
+
});
|
|
521
|
+
if (!response.ok) {
|
|
522
|
+
throw new Error(`TestPulse HTTP ingest failed with ${response.status} ${response.statusText}: ${await response.text()}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
createEndpoint(runId) {
|
|
527
|
+
return `${this.baseUrl}/api/v1/runs/${encodeURIComponent(runId)}/streams/${encodeURIComponent(this.storageStreamId)}/events`;
|
|
528
|
+
}
|
|
529
|
+
createHeaders() {
|
|
530
|
+
const headers = {
|
|
531
|
+
"content-type": "application/json"
|
|
532
|
+
};
|
|
533
|
+
if (this.apiKey) {
|
|
534
|
+
headers["x-api-key"] = this.apiKey;
|
|
535
|
+
}
|
|
536
|
+
return headers;
|
|
537
|
+
}
|
|
538
|
+
};
|
|
539
|
+
function groupByRun(events) {
|
|
540
|
+
const groups = /* @__PURE__ */ new Map();
|
|
541
|
+
for (const event of events) {
|
|
542
|
+
const group = groups.get(event.runId);
|
|
543
|
+
if (group) {
|
|
544
|
+
group.events.push(event);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
groups.set(event.runId, { runId: event.runId, events: [event] });
|
|
548
|
+
}
|
|
549
|
+
return [...groups.values()];
|
|
550
|
+
}
|
|
551
|
+
function createWriterId2() {
|
|
552
|
+
return `http-${process.pid}-${(0, import_node_crypto2.randomUUID)()}`;
|
|
553
|
+
}
|
|
554
|
+
function readPositiveNumber(value, defaultValue, optionName) {
|
|
555
|
+
if (value === void 0) {
|
|
556
|
+
return defaultValue;
|
|
557
|
+
}
|
|
558
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
559
|
+
throw new Error(`HttpEventSink ${optionName} must be a positive number.`);
|
|
560
|
+
}
|
|
561
|
+
return value;
|
|
562
|
+
}
|
|
563
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
564
|
+
0 && (module.exports = {
|
|
565
|
+
FileSystemEventSink,
|
|
566
|
+
HttpEventSink,
|
|
567
|
+
JsonlReporter
|
|
568
|
+
});
|