@checkly/playwright-reporter 0.1.8 → 0.1.10

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/dist/index.d.ts CHANGED
@@ -81,6 +81,13 @@ interface ChecklyReporterOptions {
81
81
  * sessionName: ({ directoryName, config }) => `E2E: ${directoryName} (${config.projects.length} projects)`
82
82
  */
83
83
  sessionName?: SessionNameOption;
84
+ /**
85
+ * Enable verbose logging for debugging
86
+ * Logs detailed information about each phase of report generation
87
+ * Can also be enabled via CHECKLY_REPORTER_VERBOSE=true environment variable
88
+ * @default false
89
+ */
90
+ verbose?: boolean;
84
91
  }
85
92
  /**
86
93
  * Warning types that can be attached to test results
@@ -99,6 +106,67 @@ interface ChecklyWarning {
99
106
  */
100
107
  message: string;
101
108
  }
109
+ /**
110
+ * Console message type matching the webapp's CheckRunNavigationTracePage.console type
111
+ */
112
+ type ConsoleMessageType = 'debug' | 'error' | 'info' | 'log' | 'warning';
113
+ /**
114
+ * Location of a console message in source code
115
+ */
116
+ interface ConsoleMessageLocation {
117
+ url: string;
118
+ columnNumber: number;
119
+ lineNumber: number;
120
+ }
121
+ /**
122
+ * Console message extracted from Playwright trace
123
+ * Matches the webapp's CheckRunNavigationTracePage.console array item type
124
+ */
125
+ interface ConsoleMessage {
126
+ /**
127
+ * Unique identifier for the console message
128
+ */
129
+ id: string;
130
+ /**
131
+ * Source location where the console message was triggered
132
+ */
133
+ location: ConsoleMessageLocation;
134
+ /**
135
+ * The text content of the console message
136
+ */
137
+ text: string;
138
+ /**
139
+ * Timestamp when the message was logged (milliseconds)
140
+ */
141
+ timestamp: number;
142
+ /**
143
+ * Type of console message
144
+ */
145
+ type: ConsoleMessageType;
146
+ }
147
+ /**
148
+ * Network request extracted from Playwright trace
149
+ * Matches the webapp's CheckRunNavigationTraceNetworkEntry type
150
+ */
151
+ interface NetworkRequest {
152
+ id: string;
153
+ url: string;
154
+ domain: string;
155
+ method: string;
156
+ resourceType: string;
157
+ statusCode: number;
158
+ statusText: string;
159
+ start: number;
160
+ startedAt: number;
161
+ finishedAt: number;
162
+ time: number;
163
+ hasFinished: boolean;
164
+ hasSucceeded: boolean;
165
+ requestHeaders: Record<string, string>;
166
+ responseHeaders: Record<string, string>;
167
+ transferBytes?: number;
168
+ resourceBytes?: number;
169
+ }
102
170
  /**
103
171
  * Checkly-specific extensions added to JSONReportTestResult
104
172
  */
@@ -107,6 +175,14 @@ interface ChecklyTestResultExtensions {
107
175
  * Warnings about the test result (e.g., missing traces)
108
176
  */
109
177
  warnings?: ChecklyWarning[];
178
+ /**
179
+ * Console messages extracted from the Playwright trace
180
+ */
181
+ console?: ConsoleMessage[];
182
+ /**
183
+ * Network requests extracted from the Playwright trace
184
+ */
185
+ network?: NetworkRequest[];
110
186
  }
111
187
  /**
112
188
  * Extended JSONReportTestResult with Checkly-specific data
@@ -168,6 +244,13 @@ declare class ChecklyReporter implements Reporter {
168
244
  private testCounts;
169
245
  private stepsMap;
170
246
  private warningsMap;
247
+ private tracePathsMap;
248
+ private consoleMessagesMap;
249
+ private networkRequestsMap;
250
+ /**
251
+ * Log a message if verbose mode is enabled
252
+ */
253
+ private log;
171
254
  constructor(options?: ChecklyReporterOptions);
172
255
  /**
173
256
  * Resolves the session name from options
@@ -176,6 +259,7 @@ declare class ChecklyReporter implements Reporter {
176
259
  private resolveSessionName;
177
260
  /**
178
261
  * Checks if test result has a trace attachment and adds context-aware warning if missing
262
+ * Also captures trace file path for later console message extraction
179
263
  * The warning type depends on the trace configuration and test result state
180
264
  */
181
265
  private checkTraceAttachment;
@@ -196,10 +280,21 @@ declare class ChecklyReporter implements Reporter {
196
280
  onEnd(): Promise<void>;
197
281
  private printSummary;
198
282
  /**
199
- * Injects captured steps and warnings into the JSON report
283
+ * Extracts console messages and network requests from all captured traces
284
+ * Called before injecting data into the report
285
+ */
286
+ private extractDataFromTraces;
287
+ /**
288
+ * Injects captured steps, warnings, console messages, and network requests into the JSON report
200
289
  * Traverses the report structure and matches by test ID + retry
201
290
  */
202
291
  private injectDataIntoReport;
292
+ /**
293
+ * Reconstructs config.projects and test.projectId from test data
294
+ * This is necessary for blob merge scenarios where Playwright's JSON reporter
295
+ * doesn't populate projects array or projectId fields
296
+ */
297
+ private reconstructProjectsFromTests;
203
298
  /**
204
299
  * Uploads test results to Checkly API
205
300
  */
@@ -210,4 +305,4 @@ declare class ChecklyReporter implements Reporter {
210
305
  onError(error: TestError): void;
211
306
  }
212
307
 
213
- export { ChecklyReporter, type ChecklyReporterOptions, type ChecklyTestResultExtensions, type ChecklyWarning, type ChecklyWarningType, type JSONReport, type JSONReportSpec, type JSONReportSuite, type JSONReportTest, type JSONReportTestResult, ChecklyReporter as default };
308
+ export { ChecklyReporter, type ChecklyReporterOptions, type ChecklyTestResultExtensions, type ChecklyWarning, type ChecklyWarningType, type ConsoleMessage, type ConsoleMessageLocation, type ConsoleMessageType, type JSONReport, type JSONReportSpec, type JSONReportSuite, type JSONReportTest, type JSONReportTestResult, type NetworkRequest, ChecklyReporter as default };
package/dist/index.js CHANGED
@@ -234,8 +234,261 @@ var AssetCollector = class {
234
234
  }
235
235
  };
236
236
 
237
- // ../utils/src/zipper.ts
237
+ // ../utils/src/console-adapter.ts
238
+ import { createHash } from "crypto";
239
+ function normalizeType(messageType) {
240
+ switch (messageType.toLowerCase()) {
241
+ case "debug":
242
+ return "debug";
243
+ case "error":
244
+ return "error";
245
+ case "info":
246
+ return "info";
247
+ case "warning":
248
+ case "warn":
249
+ return "warning";
250
+ default:
251
+ return "log";
252
+ }
253
+ }
254
+ function generateId(time, messageType, text, url) {
255
+ return createHash("sha256").update(`${time}-${messageType}-${text}-${url}`).digest("hex").substring(0, 16);
256
+ }
257
+ function toConsoleMessage(event) {
258
+ const url = event.location?.url || "";
259
+ return {
260
+ id: generateId(event.time, event.messageType, event.text, url),
261
+ location: {
262
+ url,
263
+ columnNumber: event.location?.columnNumber || 0,
264
+ lineNumber: event.location?.lineNumber || 0
265
+ },
266
+ text: event.text || "",
267
+ timestamp: event.time,
268
+ type: normalizeType(event.messageType)
269
+ };
270
+ }
271
+
272
+ // ../utils/src/network-adapter.ts
273
+ import { createHash as createHash2 } from "crypto";
274
+ function generateId2(url, method, startedAt) {
275
+ return createHash2("sha256").update(`${url}-${method}-${startedAt}`).digest("hex").substring(0, 16);
276
+ }
277
+ function extractDomain(url) {
278
+ try {
279
+ return new URL(url).hostname;
280
+ } catch {
281
+ return "";
282
+ }
283
+ }
284
+ function headersArrayToRecord(headers) {
285
+ const record = {};
286
+ for (const { name, value } of headers) {
287
+ record[name.toLowerCase()] = value;
288
+ }
289
+ return record;
290
+ }
291
+ function isSuccessStatus(status) {
292
+ return status >= 200 && status < 400;
293
+ }
294
+ function determineResourceType(snapshot) {
295
+ if (snapshot._resourceType) {
296
+ return snapshot._resourceType;
297
+ }
298
+ if (snapshot._apiRequest) {
299
+ return "fetch";
300
+ }
301
+ return "other";
302
+ }
303
+ function toNetworkRequest(event) {
304
+ const { snapshot } = event;
305
+ const startedAt = new Date(snapshot.startedDateTime).getTime();
306
+ const time = Math.round(snapshot.time);
307
+ const finishedAt = startedAt + time;
308
+ const statusCode = snapshot.response.status;
309
+ const url = snapshot.request.url;
310
+ const method = snapshot.request.method;
311
+ return {
312
+ id: generateId2(url, method, startedAt),
313
+ url,
314
+ domain: extractDomain(url),
315
+ method,
316
+ resourceType: determineResourceType(snapshot),
317
+ statusCode,
318
+ statusText: snapshot.response.statusText || "",
319
+ start: startedAt,
320
+ startedAt,
321
+ finishedAt,
322
+ time,
323
+ hasFinished: true,
324
+ hasSucceeded: isSuccessStatus(statusCode),
325
+ requestHeaders: headersArrayToRecord(snapshot.request.headers || []),
326
+ responseHeaders: headersArrayToRecord(snapshot.response.headers || []),
327
+ transferBytes: snapshot.response._transferSize,
328
+ resourceBytes: snapshot.response.content?.size
329
+ };
330
+ }
331
+
332
+ // ../utils/src/trace-reader.ts
238
333
  import * as fs2 from "fs";
334
+
335
+ // ../utils/src/zip-reader.ts
336
+ import { promisify } from "util";
337
+ import * as zlib from "zlib";
338
+ var gunzip2 = promisify(zlib.gunzip);
339
+ var inflateRaw2 = promisify(zlib.inflateRaw);
340
+ function parseZipEntries(zipBuffer) {
341
+ const EOCD_SIG = 101010256;
342
+ let eocdOffset = -1;
343
+ for (let i = zipBuffer.length - 22; i >= 0; i--) {
344
+ if (zipBuffer.readUInt32LE(i) === EOCD_SIG) {
345
+ eocdOffset = i;
346
+ break;
347
+ }
348
+ }
349
+ if (eocdOffset === -1) {
350
+ return [];
351
+ }
352
+ const cdOffset = zipBuffer.readUInt32LE(eocdOffset + 16);
353
+ const cdEntries = zipBuffer.readUInt16LE(eocdOffset + 10);
354
+ const entries = [];
355
+ const CD_SIG = 33639248;
356
+ let offset = cdOffset;
357
+ for (let i = 0; i < cdEntries; i++) {
358
+ if (zipBuffer.readUInt32LE(offset) !== CD_SIG) {
359
+ break;
360
+ }
361
+ const compressionMethod = zipBuffer.readUInt16LE(offset + 10);
362
+ const compressedSize = zipBuffer.readUInt32LE(offset + 20);
363
+ const fileNameLength = zipBuffer.readUInt16LE(offset + 28);
364
+ const extraFieldLength = zipBuffer.readUInt16LE(offset + 30);
365
+ const commentLength = zipBuffer.readUInt16LE(offset + 32);
366
+ const localHeaderOffset = zipBuffer.readUInt32LE(offset + 42);
367
+ const fileName = zipBuffer.subarray(offset + 46, offset + 46 + fileNameLength).toString("utf-8");
368
+ entries.push({
369
+ fileName,
370
+ compressionMethod,
371
+ compressedSize,
372
+ localHeaderOffset
373
+ });
374
+ offset += 46 + fileNameLength + extraFieldLength + commentLength;
375
+ }
376
+ return entries;
377
+ }
378
+ async function readZipEntryContent(zipBuffer, entry) {
379
+ const LOCAL_SIG = 67324752;
380
+ if (zipBuffer.readUInt32LE(entry.localHeaderOffset) !== LOCAL_SIG) {
381
+ return null;
382
+ }
383
+ const localFileNameLength = zipBuffer.readUInt16LE(entry.localHeaderOffset + 26);
384
+ const localExtraLength = zipBuffer.readUInt16LE(entry.localHeaderOffset + 28);
385
+ const dataOffset = entry.localHeaderOffset + 30 + localFileNameLength + localExtraLength;
386
+ const compressedData = zipBuffer.subarray(dataOffset, dataOffset + entry.compressedSize);
387
+ let buffer;
388
+ if (entry.compressionMethod === 0) {
389
+ buffer = compressedData;
390
+ } else if (entry.compressionMethod === 8) {
391
+ buffer = await inflateRaw2(compressedData);
392
+ } else {
393
+ return null;
394
+ }
395
+ if (buffer.length >= 2 && buffer[0] === 31 && buffer[1] === 139) {
396
+ const decompressed = await gunzip2(buffer);
397
+ return decompressed.toString("utf-8");
398
+ }
399
+ return buffer.toString("utf-8");
400
+ }
401
+
402
+ // ../utils/src/trace-reader.ts
403
+ var TraceReader = class {
404
+ constructor(tracePath) {
405
+ this.tracePath = tracePath;
406
+ }
407
+ zipBuffer = null;
408
+ traceEntries = [];
409
+ async open() {
410
+ if (!fs2.existsSync(this.tracePath)) {
411
+ return false;
412
+ }
413
+ try {
414
+ this.zipBuffer = fs2.readFileSync(this.tracePath);
415
+ const entries = parseZipEntries(this.zipBuffer);
416
+ this.traceEntries = entries.filter(
417
+ (e) => (/^\d+-trace\.trace$/.test(e.fileName) || /^\d+-trace\.network$/.test(e.fileName)) && !e.fileName.includes("/")
418
+ );
419
+ return this.traceEntries.length > 0;
420
+ } catch {
421
+ return false;
422
+ }
423
+ }
424
+ /**
425
+ * Extracts events matching a text filter.
426
+ *
427
+ * @param textFilter - Substring to match (e.g., '"type":"console"')
428
+ * @param adapter - Optional adapter to transform events
429
+ */
430
+ async extractEvents(textFilter, adapter) {
431
+ if (!this.zipBuffer) {
432
+ throw new Error("TraceReader not opened. Call open() first.");
433
+ }
434
+ const results = [];
435
+ for (const traceEntry of this.traceEntries) {
436
+ const content = await readZipEntryContent(this.zipBuffer, traceEntry);
437
+ if (!content) continue;
438
+ for (const line of content.split("\n")) {
439
+ if (line.indexOf(textFilter) !== -1) {
440
+ try {
441
+ const event = JSON.parse(line);
442
+ results.push(adapter ? adapter(event) : event);
443
+ } catch {
444
+ }
445
+ }
446
+ }
447
+ }
448
+ if (results.length > 0 && typeof results[0].time === "number") {
449
+ return results.sort((a, b) => a.time - b.time);
450
+ }
451
+ return results;
452
+ }
453
+ /**
454
+ * Extracts all events from the trace.
455
+ */
456
+ async extractAllEvents(adapter) {
457
+ if (!this.zipBuffer) {
458
+ throw new Error("TraceReader not opened. Call open() first.");
459
+ }
460
+ const results = [];
461
+ for (const traceEntry of this.traceEntries) {
462
+ const content = await readZipEntryContent(this.zipBuffer, traceEntry);
463
+ if (!content) continue;
464
+ for (const line of content.split("\n")) {
465
+ if (line.trim()) {
466
+ try {
467
+ const event = JSON.parse(line);
468
+ results.push(adapter ? adapter(event) : event);
469
+ } catch {
470
+ }
471
+ }
472
+ }
473
+ }
474
+ if (results.length > 0 && typeof results[0].time === "number") {
475
+ return results.sort((a, b) => a.time - b.time);
476
+ }
477
+ return results;
478
+ }
479
+ listFiles() {
480
+ if (!this.zipBuffer) {
481
+ return [];
482
+ }
483
+ return parseZipEntries(this.zipBuffer).map((e) => e.fileName);
484
+ }
485
+ isOpen() {
486
+ return this.zipBuffer !== null;
487
+ }
488
+ };
489
+
490
+ // ../utils/src/zipper.ts
491
+ import * as fs3 from "fs";
239
492
  import * as os from "os";
240
493
  import * as path2 from "path";
241
494
  import { ZipArchive } from "archiver";
@@ -254,7 +507,7 @@ var Zipper = class {
254
507
  const entries = [];
255
508
  return new Promise((resolve2, reject) => {
256
509
  try {
257
- const output = fs2.createWriteStream(this.outputPath);
510
+ const output = fs3.createWriteStream(this.outputPath);
258
511
  const archive = new ZipArchive({
259
512
  zlib: { level: 0 }
260
513
  });
@@ -284,14 +537,14 @@ var Zipper = class {
284
537
  reject(err);
285
538
  });
286
539
  archive.pipe(output);
287
- if (!fs2.existsSync(reportPath)) {
540
+ if (!fs3.existsSync(reportPath)) {
288
541
  reject(new Error(`Report file not found: ${reportPath}`));
289
542
  return;
290
543
  }
291
544
  const transformedReportPath = this.transformJsonReport(reportPath);
292
545
  archive.file(transformedReportPath, { name: "output/playwright-test-report.json" });
293
546
  for (const asset of assets) {
294
- if (!fs2.existsSync(asset.sourcePath)) {
547
+ if (!fs3.existsSync(asset.sourcePath)) {
295
548
  console.warn(`[Checkly Reporter] Skipping missing asset: ${asset.sourcePath}`);
296
549
  continue;
297
550
  }
@@ -310,11 +563,11 @@ var Zipper = class {
310
563
  * @returns Path to the transformed JSON report (in temp directory)
311
564
  */
312
565
  transformJsonReport(reportPath) {
313
- const reportContent = fs2.readFileSync(reportPath, "utf-8");
566
+ const reportContent = fs3.readFileSync(reportPath, "utf-8");
314
567
  const report = JSON.parse(reportContent);
315
568
  this.transformAttachmentPaths(report);
316
569
  const tempReportPath = path2.join(os.tmpdir(), `playwright-test-report-${Date.now()}.json`);
317
- fs2.writeFileSync(tempReportPath, JSON.stringify(report, null, 2));
570
+ fs3.writeFileSync(tempReportPath, JSON.stringify(report, null, 2));
318
571
  return tempReportPath;
319
572
  }
320
573
  /**
@@ -392,8 +645,8 @@ var Zipper = class {
392
645
  };
393
646
 
394
647
  // src/reporter.ts
395
- import * as fs3 from "fs";
396
- import { readFileSync as readFileSync3 } from "fs";
648
+ import * as fs4 from "fs";
649
+ import { readFileSync as readFileSync4 } from "fs";
397
650
  import * as path3 from "path";
398
651
  import { dirname, join as join3 } from "path";
399
652
  import { fileURLToPath } from "url";
@@ -658,7 +911,7 @@ var TestResults = class {
658
911
  // src/reporter.ts
659
912
  var __filename = fileURLToPath(import.meta.url);
660
913
  var __dirname = dirname(__filename);
661
- var packageJson = JSON.parse(readFileSync3(join3(__dirname, "..", "package.json"), "utf-8"));
914
+ var packageJson = JSON.parse(readFileSync4(join3(__dirname, "..", "package.json"), "utf-8"));
662
915
  var pkgVersion = packageJson.version;
663
916
  var pluralRules = new Intl.PluralRules("en-US");
664
917
  var projectForms = {
@@ -725,11 +978,30 @@ var ChecklyReporter = class {
725
978
  stepsMap = /* @__PURE__ */ new Map();
726
979
  // Store warnings per test result, keyed by "testId:retry"
727
980
  warningsMap = /* @__PURE__ */ new Map();
981
+ // Store trace file paths per test result, keyed by "testId:retry"
982
+ tracePathsMap = /* @__PURE__ */ new Map();
983
+ // Store console messages per test result, keyed by "testId:retry"
984
+ consoleMessagesMap = /* @__PURE__ */ new Map();
985
+ // Store network requests per test result, keyed by "testId:retry"
986
+ networkRequestsMap = /* @__PURE__ */ new Map();
987
+ /**
988
+ * Log a message if verbose mode is enabled
989
+ */
990
+ log(message, data) {
991
+ if (!this.options.verbose) return;
992
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
993
+ if (data) {
994
+ console.log(`[Checkly Reporter DEBUG ${timestamp}] ${message}`, JSON.stringify(data, null, 2));
995
+ } else {
996
+ console.log(`[Checkly Reporter DEBUG ${timestamp}] ${message}`);
997
+ }
998
+ }
728
999
  constructor(options = {}) {
729
1000
  const environment = getEnvironment(options);
730
1001
  const baseUrl = getApiUrl(environment);
731
1002
  const apiKey = process.env.CHECKLY_API_KEY || options.apiKey;
732
1003
  const accountId = process.env.CHECKLY_ACCOUNT_ID || options.accountId;
1004
+ const verbose = options.verbose ?? process.env.CHECKLY_REPORTER_VERBOSE === "true";
733
1005
  this.options = {
734
1006
  accountId,
735
1007
  apiKey,
@@ -737,7 +1009,8 @@ var ChecklyReporter = class {
737
1009
  jsonReportPath: options.jsonReportPath ?? "test-results/playwright-test-report.json",
738
1010
  testResultsDir: options.testResultsDir ?? "test-results",
739
1011
  dryRun: options.dryRun ?? false,
740
- sessionName: options.sessionName
1012
+ sessionName: options.sessionName,
1013
+ verbose
741
1014
  };
742
1015
  this.assetCollector = new AssetCollector(this.options.testResultsDir);
743
1016
  this.zipper = new Zipper({
@@ -751,6 +1024,20 @@ var ChecklyReporter = class {
751
1024
  });
752
1025
  this.testResults = new TestResults(client.getAxiosInstance());
753
1026
  }
1027
+ if (verbose) {
1028
+ console.log(`[Checkly Reporter DEBUG] Initialized with options:`, {
1029
+ environment,
1030
+ baseUrl,
1031
+ hasApiKey: !!apiKey,
1032
+ hasAccountId: !!accountId,
1033
+ outputPath: this.options.outputPath,
1034
+ jsonReportPath: this.options.jsonReportPath,
1035
+ testResultsDir: this.options.testResultsDir,
1036
+ dryRun: this.options.dryRun,
1037
+ verbose: this.options.verbose,
1038
+ hasTestResults: !!this.testResults
1039
+ });
1040
+ }
754
1041
  }
755
1042
  /**
756
1043
  * Resolves the session name from options
@@ -768,14 +1055,16 @@ var ChecklyReporter = class {
768
1055
  }
769
1056
  /**
770
1057
  * Checks if test result has a trace attachment and adds context-aware warning if missing
1058
+ * Also captures trace file path for later console message extraction
771
1059
  * The warning type depends on the trace configuration and test result state
772
1060
  */
773
1061
  checkTraceAttachment(test, result) {
774
- const warningsKey = `${test.id}:${result.retry}`;
775
- const hasTrace = result.attachments?.some(
1062
+ const key = `${test.id}:${result.retry}`;
1063
+ const traceAttachment = result.attachments?.find(
776
1064
  (attachment) => attachment.name === "trace" || attachment.contentType === "application/zip"
777
1065
  );
778
- if (hasTrace) {
1066
+ if (traceAttachment?.path) {
1067
+ this.tracePathsMap.set(key, traceAttachment.path);
779
1068
  return;
780
1069
  }
781
1070
  const traceConfig = test.parent?.project()?.use?.trace;
@@ -841,9 +1130,9 @@ var ChecklyReporter = class {
841
1130
  warningType = "trace-missing";
842
1131
  message = `No trace found. Trace mode "${traceMode}" may not be generating traces for this result.`;
843
1132
  }
844
- const warnings = this.warningsMap.get(warningsKey) || [];
1133
+ const warnings = this.warningsMap.get(key) || [];
845
1134
  warnings.push({ type: warningType, message });
846
- this.warningsMap.set(warningsKey, warnings);
1135
+ this.warningsMap.set(key, warnings);
847
1136
  }
848
1137
  /**
849
1138
  * Called once before running tests
@@ -919,9 +1208,16 @@ var ChecklyReporter = class {
919
1208
  * This is where we create the ZIP archive and upload results
920
1209
  */
921
1210
  async onEnd() {
1211
+ this.log("onEnd started", {
1212
+ testCounts: this.testCounts,
1213
+ stepsMapSize: this.stepsMap.size,
1214
+ warningsMapSize: this.warningsMap.size,
1215
+ hasTestSession: !!this.testSession,
1216
+ testSessionId: this.testSession?.testSessionId
1217
+ });
922
1218
  try {
923
1219
  const jsonReportPath = this.options.jsonReportPath;
924
- if (!fs3.existsSync(jsonReportPath)) {
1220
+ if (!fs4.existsSync(jsonReportPath)) {
925
1221
  console.error(`[Checkly Reporter] ERROR: JSON report not found at: ${jsonReportPath}`);
926
1222
  console.error("[Checkly Reporter] Make sure to configure the json reporter before the checkly reporter:");
927
1223
  console.error(
@@ -929,17 +1225,42 @@ var ChecklyReporter = class {
929
1225
  );
930
1226
  return;
931
1227
  }
932
- const reportContent = fs3.readFileSync(jsonReportPath, "utf-8");
1228
+ this.log("Reading JSON report", { path: jsonReportPath });
1229
+ const reportContent = fs4.readFileSync(jsonReportPath, "utf-8");
933
1230
  const report = JSON.parse(reportContent);
1231
+ this.log("JSON report parsed", {
1232
+ configVersion: report.config?.version,
1233
+ projectsCount: report.config?.projects?.length ?? 0,
1234
+ suitesCount: report.suites?.length ?? 0,
1235
+ rootDir: report.config?.rootDir
1236
+ });
1237
+ await this.extractDataFromTraces();
934
1238
  this.injectDataIntoReport(report);
935
- fs3.writeFileSync(jsonReportPath, JSON.stringify(report, null, 2), "utf-8");
1239
+ this.log("Data injected into report", {
1240
+ projectsCountAfterReconstruction: report.config?.projects?.length ?? 0
1241
+ });
1242
+ fs4.writeFileSync(jsonReportPath, JSON.stringify(report, null, 2), "utf-8");
1243
+ this.log("Enriched report written to disk");
1244
+ this.log("Collecting assets", { testResultsDir: this.options.testResultsDir });
936
1245
  const assets = await this.assetCollector.collectAssets(report);
1246
+ this.log("Assets collected", {
1247
+ count: assets.length,
1248
+ assets: assets.map((a) => ({ source: a.sourcePath, archive: a.archivePath, type: a.type }))
1249
+ });
1250
+ this.log("Creating ZIP archive", { outputPath: this.options.outputPath });
937
1251
  const result = await this.zipper.createZip(jsonReportPath, assets);
1252
+ this.log("ZIP created", {
1253
+ zipPath: result.zipPath,
1254
+ zipSize: result.size,
1255
+ entriesCount: result.entries.length,
1256
+ entries: result.entries.map((e) => ({ name: e.name, start: e.start, end: e.end }))
1257
+ });
938
1258
  if (this.testResults && this.testSession) {
1259
+ this.log("Uploading results", { testSessionId: this.testSession.testSessionId });
939
1260
  await this.uploadResults(report, result.zipPath, result.entries);
940
1261
  if (!this.options.dryRun) {
941
1262
  try {
942
- fs3.unlinkSync(result.zipPath);
1263
+ fs4.unlinkSync(result.zipPath);
943
1264
  } catch (cleanupError) {
944
1265
  console.warn(`[Checkly Reporter] Warning: Could not delete ZIP file: ${cleanupError}`);
945
1266
  }
@@ -962,7 +1283,33 @@ var ChecklyReporter = class {
962
1283
  console.log("\n======================================================");
963
1284
  }
964
1285
  /**
965
- * Injects captured steps and warnings into the JSON report
1286
+ * Extracts console messages and network requests from all captured traces
1287
+ * Called before injecting data into the report
1288
+ */
1289
+ async extractDataFromTraces() {
1290
+ const extractionPromises = [];
1291
+ for (const [key, tracePath] of this.tracePathsMap.entries()) {
1292
+ extractionPromises.push(
1293
+ (async () => {
1294
+ const reader = new TraceReader(tracePath);
1295
+ if (!await reader.open()) return;
1296
+ const messages = await reader.extractEvents('"type":"console"', toConsoleMessage);
1297
+ if (messages.length > 0) {
1298
+ this.consoleMessagesMap.set(key, messages);
1299
+ }
1300
+ const networkRequests = await reader.extractEvents('"type":"resource-snapshot"', toNetworkRequest);
1301
+ if (networkRequests.length > 0) {
1302
+ this.networkRequestsMap.set(key, networkRequests);
1303
+ }
1304
+ })().catch((error) => {
1305
+ console.error(`[Checkly Reporter] Failed to extract data from trace: ${error}`);
1306
+ })
1307
+ );
1308
+ }
1309
+ await Promise.all(extractionPromises);
1310
+ }
1311
+ /**
1312
+ * Injects captured steps, warnings, console messages, and network requests into the JSON report
966
1313
  * Traverses the report structure and matches by test ID + retry
967
1314
  */
968
1315
  injectDataIntoReport(report) {
@@ -976,8 +1323,15 @@ var ChecklyReporter = class {
976
1323
  result.steps = steps;
977
1324
  }
978
1325
  const warnings = this.warningsMap.get(key);
979
- if (warnings && warnings.length > 0) {
980
- result._checkly = { warnings };
1326
+ const consoleMessages = this.consoleMessagesMap.get(key);
1327
+ const networkRequests = this.networkRequestsMap.get(key);
1328
+ const hasData = warnings && warnings.length > 0 || consoleMessages && consoleMessages.length > 0 || networkRequests && networkRequests.length > 0;
1329
+ if (hasData) {
1330
+ result._checkly = {
1331
+ ...warnings && warnings.length > 0 ? { warnings } : {},
1332
+ ...consoleMessages && consoleMessages.length > 0 ? { console: consoleMessages } : {},
1333
+ ...networkRequests && networkRequests.length > 0 ? { network: networkRequests } : {}
1334
+ };
981
1335
  }
982
1336
  }
983
1337
  }
@@ -991,12 +1345,75 @@ var ChecklyReporter = class {
991
1345
  for (const suite of report.suites) {
992
1346
  processSuite(suite);
993
1347
  }
1348
+ this.reconstructProjectsFromTests(report);
1349
+ }
1350
+ /**
1351
+ * Reconstructs config.projects and test.projectId from test data
1352
+ * This is necessary for blob merge scenarios where Playwright's JSON reporter
1353
+ * doesn't populate projects array or projectId fields
1354
+ */
1355
+ reconstructProjectsFromTests(report) {
1356
+ const projectNames = /* @__PURE__ */ new Set();
1357
+ let testsWithMissingProjectId = 0;
1358
+ const configAny = report.config;
1359
+ const originalProjectsCount = configAny.projects?.length ?? 0;
1360
+ this.log("reconstructProjectsFromTests started", {
1361
+ originalProjectsCount,
1362
+ suitesCount: report.suites?.length ?? 0
1363
+ });
1364
+ const collectProjectNames = (suite) => {
1365
+ for (const spec of suite.specs) {
1366
+ for (const test of spec.tests) {
1367
+ const testAny = test;
1368
+ if (testAny.projectName) {
1369
+ projectNames.add(testAny.projectName);
1370
+ }
1371
+ if (testAny.projectName && !testAny.projectId) {
1372
+ testAny.projectId = testAny.projectName;
1373
+ testsWithMissingProjectId++;
1374
+ }
1375
+ }
1376
+ }
1377
+ if (suite.suites) {
1378
+ for (const nestedSuite of suite.suites) {
1379
+ collectProjectNames(nestedSuite);
1380
+ }
1381
+ }
1382
+ };
1383
+ for (const suite of report.suites) {
1384
+ collectProjectNames(suite);
1385
+ }
1386
+ this.log("Project names collected from tests", {
1387
+ uniqueProjectNames: Array.from(projectNames),
1388
+ testsWithMissingProjectId
1389
+ });
1390
+ if ((!configAny.projects || configAny.projects.length === 0) && projectNames.size > 0) {
1391
+ configAny.projects = Array.from(projectNames).map((name) => ({
1392
+ id: name,
1393
+ name
1394
+ }));
1395
+ this.log("Reconstructed config.projects", {
1396
+ reconstructedProjectsCount: configAny.projects.length,
1397
+ projects: configAny.projects
1398
+ });
1399
+ } else {
1400
+ this.log("No project reconstruction needed", {
1401
+ reason: configAny.projects?.length > 0 ? "projects already present" : "no project names found in tests"
1402
+ });
1403
+ }
994
1404
  }
995
1405
  /**
996
1406
  * Uploads test results to Checkly API
997
1407
  */
998
- async uploadResults(report, zipPath, entries) {
1408
+ async uploadResults(_report, zipPath, entries) {
1409
+ this.log("uploadResults started", {
1410
+ zipPath,
1411
+ entriesCount: entries.length,
1412
+ hasTestResults: !!this.testResults,
1413
+ hasTestSession: !!this.testSession
1414
+ });
999
1415
  if (!this.testResults || !this.testSession) {
1416
+ this.log("uploadResults skipped - missing testResults or testSession");
1000
1417
  return;
1001
1418
  }
1002
1419
  try {
@@ -1005,25 +1422,42 @@ var ChecklyReporter = class {
1005
1422
  const isDegraded = failedCount === 0 && flakyCount > 0;
1006
1423
  const endTime = /* @__PURE__ */ new Date();
1007
1424
  const responseTime = this.startTime ? Math.max(0, endTime.getTime() - this.startTime.getTime()) : 0;
1008
- const zipSizeBytes = (await fs3.promises.stat(zipPath)).size;
1425
+ const zipSizeBytes = (await fs4.promises.stat(zipPath)).size;
1426
+ this.log("Upload metadata calculated", {
1427
+ testCounts: this.testCounts,
1428
+ overallStatus,
1429
+ isDegraded,
1430
+ responseTime,
1431
+ zipSizeBytes,
1432
+ testSessionId: this.testSession.testSessionId,
1433
+ testResultsCount: this.testSession.testResults.length
1434
+ });
1009
1435
  if (this.testSession.testResults.length > 0) {
1010
1436
  const firstResult = this.testSession.testResults[0];
1437
+ this.log("Using first test result for upload", {
1438
+ testResultId: firstResult.testResultId
1439
+ });
1011
1440
  let assetId;
1012
1441
  if (zipSizeBytes > 0) {
1442
+ this.log("Starting S3 asset upload", { zipSizeBytes });
1013
1443
  try {
1014
- const assets = fs3.createReadStream(zipPath);
1444
+ const assets = fs4.createReadStream(zipPath);
1015
1445
  const uploadResponse = await this.testResults.uploadTestResultAsset(
1016
1446
  this.testSession.testSessionId,
1017
1447
  firstResult.testResultId,
1018
1448
  assets
1019
1449
  );
1020
1450
  assetId = uploadResponse.assetId;
1451
+ this.log("S3 asset upload completed", { assetId });
1021
1452
  } catch (error) {
1022
1453
  const errorMessage = error instanceof Error ? error.message : String(error);
1023
1454
  console.error("[Checkly Reporter] Asset upload failed:", errorMessage);
1455
+ this.log("S3 asset upload failed", { error: errorMessage });
1024
1456
  }
1457
+ } else {
1458
+ this.log("Skipping S3 upload - ZIP is empty");
1025
1459
  }
1026
- await this.testResults.updateTestResult(this.testSession.testSessionId, firstResult.testResultId, {
1460
+ const updatePayload = {
1027
1461
  status: overallStatus,
1028
1462
  assetEntries: assetId ? entries : void 0,
1029
1463
  isDegraded,
@@ -1035,11 +1469,21 @@ var ChecklyReporter = class {
1035
1469
  s3PostTotalBytes: zipSizeBytes
1036
1470
  }
1037
1471
  }
1472
+ };
1473
+ this.log("Updating test result", {
1474
+ testResultId: firstResult.testResultId,
1475
+ payload: updatePayload,
1476
+ assetEntriesCount: assetId ? entries.length : 0
1038
1477
  });
1478
+ await this.testResults.updateTestResult(this.testSession.testSessionId, firstResult.testResultId, updatePayload);
1479
+ this.log("Test result updated successfully");
1480
+ } else {
1481
+ this.log("No test results in session to update");
1039
1482
  }
1040
1483
  } catch (error) {
1041
1484
  const errorMessage = error instanceof Error ? error.message : String(error);
1042
1485
  console.error("[Checkly Reporter] Failed to upload results:", errorMessage);
1486
+ this.log("uploadResults failed", { error: errorMessage });
1043
1487
  }
1044
1488
  }
1045
1489
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@checkly/playwright-reporter",
3
- "version": "0.1.8",
3
+ "version": "0.1.10",
4
4
  "description": "Playwright reporter that generates ZIP archives containing JSON reports and test assets",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",