@testrelic/maestro-analytics 1.1.0 → 1.2.0-next.52

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/cli.cjs CHANGED
@@ -190,6 +190,37 @@ function mergeCloudConfig(fileConfig, reporterOptions) {
190
190
  }
191
191
 
192
192
  // src/config.ts
193
+ var DEFAULT_REDACT_HEADERS = [
194
+ "authorization",
195
+ "cookie",
196
+ "set-cookie",
197
+ "x-api-key"
198
+ ];
199
+ var DEFAULT_REDACT_BODY_FIELDS = [
200
+ "password",
201
+ "secret",
202
+ "token",
203
+ "apiKey",
204
+ "api_key"
205
+ ];
206
+ var DEFAULT_MAX_BODY_BYTES = 1024 * 1024;
207
+ function resolveNetworkCapture(options) {
208
+ const o = options ?? {};
209
+ const envEnabled = process.env.TESTRELIC_CAPTURE_NETWORK === "1" || process.env.TESTRELIC_CAPTURE_NETWORK?.toLowerCase() === "true";
210
+ return Object.freeze({
211
+ enabled: o.enabled ?? envEnabled ?? false,
212
+ proxyPort: o.proxyPort ?? (process.env.TESTRELIC_PROXY_PORT ? Number.parseInt(process.env.TESTRELIC_PROXY_PORT, 10) : 8080),
213
+ proxyHost: o.proxyHost ?? process.env.TESTRELIC_PROXY_HOST ?? "127.0.0.1",
214
+ harPath: o.harPath ?? process.env.TESTRELIC_HAR_PATH ?? null,
215
+ outputDir: o.outputDir ?? null,
216
+ skipCertInstall: o.skipCertInstall ?? false,
217
+ includeUrls: Object.freeze(o.includeUrls ? [...o.includeUrls] : []),
218
+ excludeUrls: Object.freeze(o.excludeUrls ? [...o.excludeUrls] : []),
219
+ redactHeaders: Object.freeze(o.redactHeaders ? [...o.redactHeaders] : [...DEFAULT_REDACT_HEADERS]),
220
+ redactBodyFields: Object.freeze(o.redactBodyFields ? [...o.redactBodyFields] : [...DEFAULT_REDACT_BODY_FIELDS]),
221
+ maxBodyBytes: o.maxBodyBytes ?? DEFAULT_MAX_BODY_BYTES
222
+ });
223
+ }
193
224
  var DANGEROUS_KEYS2 = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
194
225
  function hasPrototypePollution(obj) {
195
226
  if (typeof obj !== "object" || obj === null) return false;
@@ -237,6 +268,7 @@ function resolveConfig(options) {
237
268
  target.quiet = options?.quiet ?? false;
238
269
  target.reportMode = options?.reportMode ?? "batch";
239
270
  target.cloud = resolveCloudFromMerge(options?.cloud ?? null);
271
+ target.network = resolveNetworkCapture(options?.network);
240
272
  return Object.freeze(target);
241
273
  }
242
274
  function resolveCloudFromMerge(reporterOptions) {
@@ -251,14 +283,323 @@ function resolveCloudFromMerge(reporterOptions) {
251
283
  }
252
284
 
253
285
  // src/maestro-runner.ts
286
+ var import_node_child_process3 = require("child_process");
287
+ var import_node_fs4 = require("fs");
288
+ var import_node_path4 = require("path");
289
+ var import_node_os3 = require("os");
290
+ var import_node_crypto2 = require("crypto");
291
+
292
+ // src/network-proxy.ts
254
293
  var import_node_child_process = require("child_process");
255
294
  var import_node_fs2 = require("fs");
256
295
  var import_node_path2 = require("path");
296
+ var import_node_url = require("url");
257
297
  var import_node_os = require("os");
258
298
  var import_node_crypto = require("crypto");
299
+ var import_meta = {};
300
+ function resolveAddonPath() {
301
+ const candidates = [];
302
+ try {
303
+ const here = (0, import_node_path2.dirname)((0, import_node_url.fileURLToPath)(import_meta.url));
304
+ candidates.push((0, import_node_path2.join)(here, "proxy-addon", "testrelic_capture.py"));
305
+ candidates.push((0, import_node_path2.join)(here, "..", "src", "proxy-addon", "testrelic_capture.py"));
306
+ } catch {
307
+ }
308
+ const maybeDirname = globalThis.__dirname;
309
+ if (maybeDirname) {
310
+ candidates.push((0, import_node_path2.join)(maybeDirname, "proxy-addon", "testrelic_capture.py"));
311
+ }
312
+ for (const c of candidates) {
313
+ if ((0, import_node_fs2.existsSync)(c)) return (0, import_node_path2.resolve)(c);
314
+ }
315
+ return null;
316
+ }
317
+ function generateRunDir(base) {
318
+ if (base) {
319
+ (0, import_node_fs2.mkdirSync)(base, { recursive: true });
320
+ return base;
321
+ }
322
+ const id = (0, import_node_crypto.randomBytes)(6).toString("hex");
323
+ const dir = (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `testrelic-maestro-network-${id}`);
324
+ (0, import_node_fs2.mkdirSync)(dir, { recursive: true });
325
+ return dir;
326
+ }
327
+ async function isMitmdumpAvailable() {
328
+ return new Promise((resolvePromise) => {
329
+ const child = (0, import_node_child_process.spawn)("mitmdump", ["--version"], { shell: true });
330
+ let resolved = false;
331
+ const settle = (ok) => {
332
+ if (resolved) return;
333
+ resolved = true;
334
+ resolvePromise(ok);
335
+ };
336
+ child.on("error", () => settle(false));
337
+ child.on("close", (code) => settle(code === 0));
338
+ setTimeout(() => {
339
+ try {
340
+ child.kill();
341
+ } catch {
342
+ }
343
+ settle(false);
344
+ }, 5e3);
345
+ });
346
+ }
347
+ async function startNetworkProxy(options) {
348
+ const { config, verbose = true } = options;
349
+ if (!config.enabled) return null;
350
+ if (!await isMitmdumpAvailable()) {
351
+ if (verbose) {
352
+ process.stderr.write(
353
+ "\u26A0 TestRelic: --capture-network requested but `mitmdump` is not on PATH.\n Install mitmproxy (`brew install mitmproxy` / `pipx install mitmproxy`)\n or supply --har-path to import a HAR file instead.\n"
354
+ );
355
+ }
356
+ return null;
357
+ }
358
+ const addon = resolveAddonPath();
359
+ if (!addon) {
360
+ if (verbose) {
361
+ process.stderr.write("\u26A0 TestRelic: network-capture addon not found in package; skipping capture.\n");
362
+ }
363
+ return null;
364
+ }
365
+ const outputDir = generateRunDir(options.outputDir ?? config.outputDir ?? void 0);
366
+ const outputPath = (0, import_node_path2.join)(outputDir, "network.jsonl");
367
+ const args = [
368
+ "--listen-host",
369
+ config.proxyHost,
370
+ "--listen-port",
371
+ String(config.proxyPort),
372
+ "-s",
373
+ addon,
374
+ "--set",
375
+ `testrelic_output=${outputPath}`,
376
+ "--set",
377
+ `testrelic_redact_headers=${config.redactHeaders.join(",")}`,
378
+ "--set",
379
+ `testrelic_redact_body_fields=${config.redactBodyFields.join(",")}`,
380
+ "--set",
381
+ `testrelic_max_body_bytes=${config.maxBodyBytes}`,
382
+ // Reduce noise; the addon writes its own log lines on warn.
383
+ "--set",
384
+ "console_eventlog_verbosity=warn",
385
+ "--set",
386
+ "termlog_verbosity=warn"
387
+ ];
388
+ let child;
389
+ try {
390
+ child = (0, import_node_child_process.spawn)("mitmdump", args, {
391
+ stdio: ["ignore", "pipe", "pipe"],
392
+ shell: true
393
+ });
394
+ } catch (err) {
395
+ if (verbose) {
396
+ process.stderr.write(`\u26A0 TestRelic: failed to spawn mitmdump: ${err.message}
397
+ `);
398
+ }
399
+ return null;
400
+ }
401
+ child.stdout?.on("data", (chunk) => {
402
+ if (verbose) process.stderr.write(`[mitmdump] ${chunk.toString("utf-8")}`);
403
+ });
404
+ child.stderr?.on("data", (chunk) => {
405
+ if (verbose) process.stderr.write(`[mitmdump] ${chunk.toString("utf-8")}`);
406
+ });
407
+ await new Promise((r) => setTimeout(r, 750));
408
+ const stop = () => new Promise((resolvePromise) => {
409
+ if (child.killed || child.exitCode !== null) {
410
+ resolvePromise();
411
+ return;
412
+ }
413
+ let settled = false;
414
+ const settle = () => {
415
+ if (settled) return;
416
+ settled = true;
417
+ resolvePromise();
418
+ };
419
+ child.once("close", settle);
420
+ child.once("exit", settle);
421
+ try {
422
+ child.kill("SIGINT");
423
+ } catch {
424
+ try {
425
+ child.kill();
426
+ } catch {
427
+ }
428
+ }
429
+ setTimeout(() => {
430
+ if (!settled) {
431
+ try {
432
+ child.kill("SIGKILL");
433
+ } catch {
434
+ }
435
+ settle();
436
+ }
437
+ }, 4e3);
438
+ });
439
+ return {
440
+ outputPath,
441
+ outputDir,
442
+ host: config.proxyHost,
443
+ port: config.proxyPort,
444
+ stop
445
+ };
446
+ }
447
+
448
+ // src/device-proxy-setup.ts
449
+ var import_node_child_process2 = require("child_process");
450
+ var import_node_path3 = require("path");
451
+ var import_node_os2 = require("os");
452
+ var import_node_fs3 = require("fs");
453
+ var NOOP_HANDLE = { teardown: async () => {
454
+ } };
455
+ function commandExists(cmd) {
456
+ const result = (0, import_node_child_process2.spawnSync)(cmd, ["--version"], { shell: true, stdio: "ignore" });
457
+ return result.error === void 0 || result.error.code !== "ENOENT";
458
+ }
459
+ function logHint(quiet, msg) {
460
+ if (!quiet) process.stderr.write(`${msg}
461
+ `);
462
+ }
463
+ function logManualInstructions(quiet, platform, proxyHost, proxyPort) {
464
+ if (quiet) return;
465
+ if (platform === "android") {
466
+ process.stderr.write(
467
+ `\u2139 TestRelic: real-device Android capture \u2014 set the device proxy manually:
468
+ Settings \u2192 Wi-Fi \u2192 (long-press your network) \u2192 Modify \u2192 Proxy: Manual
469
+ Host: ${proxyHost} Port: ${proxyPort}
470
+ Then install mitmproxy CA: open http://mitm.it from the device after the proxy is reachable.
471
+ `
472
+ );
473
+ } else if (platform === "ios") {
474
+ process.stderr.write(
475
+ `\u2139 TestRelic: real-device iOS capture \u2014 set the device proxy manually:
476
+ Settings \u2192 Wi-Fi \u2192 (i) on your network \u2192 Configure Proxy \u2192 Manual
477
+ Server: ${proxyHost} Port: ${proxyPort}
478
+ Then install + TRUST the mitmproxy CA:
479
+ 1. Open http://mitm.it from Safari on the device \u2192 install profile.
480
+ 2. Settings \u2192 General \u2192 About \u2192 Certificate Trust Settings \u2192 enable mitmproxy.
481
+ `
482
+ );
483
+ }
484
+ }
485
+ async function adb(args, deviceId) {
486
+ return new Promise((resolvePromise) => {
487
+ const full = deviceId ? ["-s", deviceId, ...args] : args;
488
+ const child = (0, import_node_child_process2.spawn)("adb", full, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
489
+ let out = "";
490
+ let err = "";
491
+ child.stdout?.on("data", (c) => {
492
+ out += c.toString();
493
+ });
494
+ child.stderr?.on("data", (c) => {
495
+ err += c.toString();
496
+ });
497
+ child.on("error", () => resolvePromise({ code: 127, stdout: out, stderr: err || "adb not on PATH" }));
498
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: out, stderr: err }));
499
+ });
500
+ }
501
+ async function setUpAndroidEmulator(opts) {
502
+ const { deviceId, proxyHost, proxyPort, quiet, skipCertInstall } = opts;
503
+ if (!commandExists("adb")) {
504
+ logHint(quiet, "\u26A0 TestRelic: `adb` not on PATH; skipping automatic Android proxy setup. See manual instructions:");
505
+ logManualInstructions(quiet, "android", proxyHost, proxyPort);
506
+ return NOOP_HANDLE;
507
+ }
508
+ const setResult = await adb(["shell", "settings", "put", "global", "http_proxy", `${proxyHost}:${proxyPort}`], deviceId);
509
+ if (setResult.code !== 0) {
510
+ logHint(quiet, `\u26A0 TestRelic: adb set proxy failed: ${setResult.stderr.trim() || "unknown error"}`);
511
+ logManualInstructions(quiet, "android", proxyHost, proxyPort);
512
+ return NOOP_HANDLE;
513
+ }
514
+ logHint(quiet, `\u2139 TestRelic: Android proxy set to ${proxyHost}:${proxyPort} on ${deviceId ?? "default device"}.`);
515
+ if (!skipCertInstall) {
516
+ logHint(
517
+ quiet,
518
+ "\u2139 TestRelic: if HTTPS calls fail, install the mitmproxy CA on the emulator:\n open http://mitm.it from Chrome on the emulator and follow prompts,\n OR for a system-level install:\n adb root && adb remount && adb push <mitm-ca>.0 /system/etc/security/cacerts/ && adb reboot\n Set --skip-cert-install to silence this hint."
519
+ );
520
+ }
521
+ const teardown = async () => {
522
+ await adb(["shell", "settings", "put", "global", "http_proxy", ":0"], deviceId);
523
+ };
524
+ return { teardown };
525
+ }
526
+ async function xcrun(args) {
527
+ return new Promise((resolvePromise) => {
528
+ const child = (0, import_node_child_process2.spawn)("xcrun", args, { shell: true, stdio: ["ignore", "pipe", "pipe"] });
529
+ let out = "";
530
+ let err = "";
531
+ child.stdout?.on("data", (c) => {
532
+ out += c.toString();
533
+ });
534
+ child.stderr?.on("data", (c) => {
535
+ err += c.toString();
536
+ });
537
+ child.on("error", () => resolvePromise({ code: 127, stdout: out, stderr: err || "xcrun not on PATH" }));
538
+ child.on("close", (code) => resolvePromise({ code: code ?? 1, stdout: out, stderr: err }));
539
+ });
540
+ }
541
+ async function setUpIosSimulator(opts) {
542
+ const { proxyHost, proxyPort, quiet, skipCertInstall } = opts;
543
+ if (process.platform !== "darwin") {
544
+ logHint(quiet, "\u26A0 TestRelic: iOS simulator capture requires macOS; the host is not Darwin. Skipping setup.");
545
+ logManualInstructions(quiet, "ios", proxyHost, proxyPort);
546
+ return NOOP_HANDLE;
547
+ }
548
+ if (!commandExists("xcrun")) {
549
+ logHint(quiet, "\u26A0 TestRelic: `xcrun` not on PATH; skipping automatic iOS simulator setup.");
550
+ logManualInstructions(quiet, "ios", proxyHost, proxyPort);
551
+ return NOOP_HANDLE;
552
+ }
553
+ if (!skipCertInstall) {
554
+ const caCandidate = (0, import_node_path3.join)(process.env.HOME ?? "", ".mitmproxy", "mitmproxy-ca-cert.pem");
555
+ if ((0, import_node_fs3.existsSync)(caCandidate)) {
556
+ const udid = opts.deviceId ?? "booted";
557
+ const res = await xcrun(["simctl", "keychain", udid, "add-root-cert", caCandidate]);
558
+ if (res.code === 0) {
559
+ logHint(quiet, `\u2139 TestRelic: installed mitmproxy CA on iOS simulator (${udid}).`);
560
+ } else {
561
+ logHint(quiet, `\u26A0 TestRelic: failed to add-root-cert via simctl: ${res.stderr.trim()}`);
562
+ logManualInstructions(quiet, "ios", proxyHost, proxyPort);
563
+ }
564
+ } else {
565
+ logHint(
566
+ quiet,
567
+ `\u26A0 TestRelic: mitmproxy CA not found at ${caCandidate}. Run mitmdump once to generate it,
568
+ then re-run with --capture-network. Falling back to manual instructions:`
569
+ );
570
+ logManualInstructions(quiet, "ios", proxyHost, proxyPort);
571
+ }
572
+ }
573
+ logHint(
574
+ quiet,
575
+ `\u2139 TestRelic: in the iOS simulator, configure Wi-Fi \u2192 (i) \u2192 Manual proxy:
576
+ Server: ${proxyHost} Port: ${proxyPort}
577
+ (We do not set host-wide \`networksetup\` proxy automatically; see docs.)`
578
+ );
579
+ return NOOP_HANDLE;
580
+ }
581
+ async function setUpDeviceProxy(options) {
582
+ switch (options.platform) {
583
+ case "android":
584
+ return setUpAndroidEmulator(options);
585
+ case "ios":
586
+ return setUpIosSimulator(options);
587
+ case "web":
588
+ logHint(options.quiet, "\u2139 TestRelic: --capture-network ignored for Maestro web flows (browser already handles HTTP).");
589
+ return NOOP_HANDLE;
590
+ case "unknown":
591
+ default:
592
+ logHint(options.quiet, "\u26A0 TestRelic: target platform unknown; printing manual proxy setup instructions:");
593
+ logManualInstructions(options.quiet, "android", options.proxyHost, options.proxyPort);
594
+ logManualInstructions(options.quiet, "ios", options.proxyHost, options.proxyPort);
595
+ return NOOP_HANDLE;
596
+ }
597
+ }
598
+
599
+ // src/maestro-runner.ts
259
600
  function generateTempDir() {
260
- const id = (0, import_node_crypto.randomBytes)(8).toString("hex");
261
- return (0, import_node_path2.join)((0, import_node_os.tmpdir)(), `testrelic-maestro-${id}`);
601
+ const id = (0, import_node_crypto2.randomBytes)(8).toString("hex");
602
+ return (0, import_node_path4.join)((0, import_node_os3.tmpdir)(), `testrelic-maestro-${id}`);
262
603
  }
263
604
  function buildMaestroArgs(options, tempDir) {
264
605
  const args = [];
@@ -269,9 +610,9 @@ function buildMaestroArgs(options, tempDir) {
269
610
  args.push("--device", options.device);
270
611
  }
271
612
  args.push("test");
272
- const junitPath = (0, import_node_path2.join)(tempDir, "report.xml");
273
- const testOutputDir = (0, import_node_path2.join)(tempDir, "artifacts");
274
- const debugOutputDir = (0, import_node_path2.join)(tempDir, "debug");
613
+ const junitPath = (0, import_node_path4.join)(tempDir, "report.xml");
614
+ const testOutputDir = (0, import_node_path4.join)(tempDir, "artifacts");
615
+ const debugOutputDir = (0, import_node_path4.join)(tempDir, "debug");
275
616
  args.push("--format", "junit");
276
617
  args.push("--output", junitPath);
277
618
  args.push("--test-output-dir", testOutputDir);
@@ -302,60 +643,124 @@ function buildMaestroArgs(options, tempDir) {
302
643
  args.push(...options.flowPaths);
303
644
  return args;
304
645
  }
305
- async function runMaestro(options) {
306
- const tempDir = options.outputDir ? (0, import_node_path2.resolve)(options.outputDir) : generateTempDir();
307
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(tempDir, "artifacts"), { recursive: true });
308
- (0, import_node_fs2.mkdirSync)((0, import_node_path2.join)(tempDir, "debug"), { recursive: true });
309
- const args = buildMaestroArgs(options, tempDir);
310
- const junitPath = (0, import_node_path2.join)(tempDir, "report.xml");
311
- const testOutputDir = (0, import_node_path2.join)(tempDir, "artifacts");
312
- const debugOutputDir = (0, import_node_path2.join)(tempDir, "debug");
646
+ function normalisePlatform(p) {
647
+ switch ((p ?? "").toLowerCase()) {
648
+ case "android":
649
+ return "android";
650
+ case "ios":
651
+ return "ios";
652
+ case "web":
653
+ return "web";
654
+ default:
655
+ return "unknown";
656
+ }
657
+ }
658
+ function runMaestroProc(args, junitPath, testOutputDir, debugOutputDir, networkJsonlDir, quiet, onClose) {
313
659
  return new Promise((resolvePromise) => {
314
660
  const stdoutChunks = [];
315
661
  const stderrChunks = [];
316
- const proc = (0, import_node_child_process.spawn)("maestro", args, {
662
+ const proc = (0, import_node_child_process3.spawn)("maestro", args, {
317
663
  stdio: ["inherit", "pipe", "pipe"],
318
664
  shell: true
319
665
  });
320
666
  proc.stdout.on("data", (chunk) => {
321
667
  stdoutChunks.push(chunk);
322
- if (!options.quiet) process.stdout.write(chunk);
668
+ if (!quiet) process.stdout.write(chunk);
323
669
  });
324
670
  proc.stderr.on("data", (chunk) => {
325
671
  stderrChunks.push(chunk);
326
- if (!options.quiet) process.stderr.write(chunk);
672
+ if (!quiet) process.stderr.write(chunk);
327
673
  });
328
- proc.on("close", (code) => {
674
+ proc.on("close", async (code) => {
675
+ try {
676
+ await onClose();
677
+ } catch {
678
+ }
329
679
  resolvePromise({
330
680
  exitCode: code ?? 1,
331
681
  junitPath,
332
682
  testOutputDir,
333
683
  debugOutputDir,
334
684
  stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
335
- stderr: Buffer.concat(stderrChunks).toString("utf-8")
685
+ stderr: Buffer.concat(stderrChunks).toString("utf-8"),
686
+ networkJsonlDir
336
687
  });
337
688
  });
338
- proc.on("error", (err) => {
689
+ proc.on("error", async (err) => {
690
+ try {
691
+ await onClose();
692
+ } catch {
693
+ }
339
694
  resolvePromise({
340
695
  exitCode: 127,
341
696
  junitPath,
342
697
  testOutputDir,
343
698
  debugOutputDir,
344
699
  stdout: "",
345
- stderr: `Failed to start maestro CLI: ${err.message}`
700
+ stderr: `Failed to start maestro CLI: ${err.message}`,
701
+ networkJsonlDir
346
702
  });
347
703
  });
348
704
  });
349
705
  }
706
+ async function runMaestro(options, extras = {}) {
707
+ const tempDir = options.outputDir ? (0, import_node_path4.resolve)(options.outputDir) : generateTempDir();
708
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(tempDir, "artifacts"), { recursive: true });
709
+ (0, import_node_fs4.mkdirSync)((0, import_node_path4.join)(tempDir, "debug"), { recursive: true });
710
+ const args = buildMaestroArgs(options, tempDir);
711
+ const junitPath = (0, import_node_path4.join)(tempDir, "report.xml");
712
+ const testOutputDir = (0, import_node_path4.join)(tempDir, "artifacts");
713
+ const debugOutputDir = (0, import_node_path4.join)(tempDir, "debug");
714
+ let proxyHandle = null;
715
+ let deviceHandle = null;
716
+ const networkConfig = extras.network;
717
+ if (networkConfig?.enabled) {
718
+ const networkDir = (0, import_node_path4.join)(tempDir, "network");
719
+ proxyHandle = await startNetworkProxy({
720
+ config: networkConfig,
721
+ outputDir: networkDir,
722
+ verbose: !options.quiet
723
+ });
724
+ if (proxyHandle) {
725
+ deviceHandle = await setUpDeviceProxy({
726
+ platform: normalisePlatform(options.platform),
727
+ deviceId: options.device,
728
+ proxyHost: proxyHandle.host,
729
+ proxyPort: proxyHandle.port,
730
+ skipCertInstall: networkConfig.skipCertInstall,
731
+ quiet: options.quiet
732
+ });
733
+ }
734
+ }
735
+ const networkJsonlDir = proxyHandle?.outputDir ?? null;
736
+ const cleanup = async () => {
737
+ if (proxyHandle) {
738
+ await new Promise((r) => setTimeout(r, 500));
739
+ }
740
+ if (deviceHandle) {
741
+ try {
742
+ await deviceHandle.teardown();
743
+ } catch {
744
+ }
745
+ }
746
+ if (proxyHandle) {
747
+ try {
748
+ await proxyHandle.stop();
749
+ } catch {
750
+ }
751
+ }
752
+ };
753
+ return runMaestroProc(args, junitPath, testOutputDir, debugOutputDir, networkJsonlDir, options.quiet, cleanup);
754
+ }
350
755
 
351
756
  // src/report-orchestrator.ts
352
- var import_node_fs15 = require("fs");
353
- var import_node_path13 = require("path");
354
- var import_node_crypto3 = require("crypto");
757
+ var import_node_fs18 = require("fs");
758
+ var import_node_path16 = require("path");
759
+ var import_node_crypto4 = require("crypto");
355
760
 
356
761
  // src/parsers/junit-parser.ts
357
762
  var import_fast_xml_parser = require("fast-xml-parser");
358
- var import_node_fs3 = require("fs");
763
+ var import_node_fs5 = require("fs");
359
764
  var parser = new import_fast_xml_parser.XMLParser({
360
765
  ignoreAttributes: false,
361
766
  attributeNamePrefix: "@_",
@@ -453,13 +858,13 @@ function parseJUnitXml(xmlContent) {
453
858
  return { testSuites: suites, totalTests, totalFailures, totalErrors, totalSkipped, totalTime };
454
859
  }
455
860
  function parseJUnitFile(filePath) {
456
- const content = (0, import_node_fs3.readFileSync)(filePath, "utf-8");
861
+ const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
457
862
  return parseJUnitXml(content);
458
863
  }
459
864
 
460
865
  // src/parsers/command-parser.ts
461
- var import_node_fs4 = require("fs");
462
- var import_node_path3 = require("path");
866
+ var import_node_fs6 = require("fs");
867
+ var import_node_path5 = require("path");
463
868
  var INTERACTION_COMMANDS = /* @__PURE__ */ new Set([
464
869
  "tapOn",
465
870
  "doubleTapOn",
@@ -524,6 +929,9 @@ var AI_COMMANDS = /* @__PURE__ */ new Set([
524
929
  "assertNoDefectsWithAI",
525
930
  "extractTextWithAI"
526
931
  ]);
932
+ var REDACTED_DETAIL_COMMANDS = /* @__PURE__ */ new Set(["inputText", "pasteText"]);
933
+ var MAX_SCRIPT_DETAIL_LENGTH = 200;
934
+ var MAX_AI_PROMPT_LENGTH = 400;
527
935
  function categorizeCommand(command) {
528
936
  if (AI_COMMANDS.has(command)) return "ai";
529
937
  if (ASSERTION_COMMANDS.has(command)) return "assertion";
@@ -564,22 +972,165 @@ function extractMaestroSelector(params) {
564
972
  if (typeof params.path === "string") return params.path;
565
973
  return void 0;
566
974
  }
975
+ function truncate(value, max) {
976
+ if (value.length <= max) return value;
977
+ return `${value.slice(0, max)}\u2026`;
978
+ }
979
+ function redactText(value) {
980
+ if (value.length === 0) return "";
981
+ return `[REDACTED ${value.length} chars]`;
982
+ }
983
+ function stringifyPoint(point) {
984
+ if (!point || typeof point !== "object") return void 0;
985
+ const p = point;
986
+ if (typeof p.x === "number" && typeof p.y === "number") return `${p.x},${p.y}`;
987
+ return void 0;
988
+ }
989
+ function extractCommandDetails(commandName, params) {
990
+ const details = {};
991
+ switch (commandName) {
992
+ case "inputText":
993
+ case "pasteText": {
994
+ const text = params.text ?? params.value;
995
+ if (typeof text === "string") {
996
+ details.text = REDACTED_DETAIL_COMMANDS.has(commandName) ? redactText(text) : text;
997
+ }
998
+ break;
999
+ }
1000
+ case "swipe":
1001
+ case "scroll":
1002
+ case "scrollUntilVisible": {
1003
+ if (typeof params.direction === "string") details.direction = params.direction;
1004
+ const startPoint = stringifyPoint(params.start ?? params.startRelative);
1005
+ const endPoint = stringifyPoint(params.end ?? params.endRelative);
1006
+ if (startPoint) details.start = startPoint;
1007
+ if (endPoint) details.end = endPoint;
1008
+ if (typeof params.duration === "number") details.swipeDurationMs = params.duration;
1009
+ if (typeof params.timeout === "number") details.timeoutMs = params.timeout;
1010
+ break;
1011
+ }
1012
+ case "setLocation": {
1013
+ if (typeof params.latitude === "number") details.latitude = params.latitude;
1014
+ if (typeof params.longitude === "number") details.longitude = params.longitude;
1015
+ break;
1016
+ }
1017
+ case "pressKey":
1018
+ case "back": {
1019
+ if (typeof params.key === "string") details.key = params.key;
1020
+ else if (typeof params.code === "string") details.key = params.code;
1021
+ break;
1022
+ }
1023
+ case "setOrientation": {
1024
+ if (typeof params.orientation === "string") details.orientation = params.orientation;
1025
+ break;
1026
+ }
1027
+ case "setAirplaneMode":
1028
+ case "toggleAirplaneMode": {
1029
+ if (typeof params.value === "string" || typeof params.value === "boolean") {
1030
+ details.value = params.value;
1031
+ }
1032
+ break;
1033
+ }
1034
+ case "setPermissions": {
1035
+ if (params.permissions && typeof params.permissions === "object") {
1036
+ details.permissions = params.permissions;
1037
+ }
1038
+ break;
1039
+ }
1040
+ case "addMedia": {
1041
+ if (Array.isArray(params.mediaPaths)) details.mediaPaths = params.mediaPaths;
1042
+ else if (typeof params.path === "string") details.mediaPaths = [params.path];
1043
+ break;
1044
+ }
1045
+ case "launchApp":
1046
+ case "killApp":
1047
+ case "stopApp":
1048
+ case "clearState": {
1049
+ const appId = params.appId ?? params.packageName ?? params.bundleId;
1050
+ if (typeof appId === "string") details.appId = appId;
1051
+ if (params.arguments) details.arguments = params.arguments;
1052
+ break;
1053
+ }
1054
+ case "openLink": {
1055
+ if (typeof params.url === "string") details.url = params.url;
1056
+ else if (typeof params.link === "string") details.url = params.link;
1057
+ break;
1058
+ }
1059
+ case "runScript":
1060
+ case "evalScript": {
1061
+ if (typeof params.path === "string") details.path = params.path;
1062
+ if (typeof params.script === "string") details.script = truncate(params.script, MAX_SCRIPT_DETAIL_LENGTH);
1063
+ if (typeof params.sourceDescription === "string") details.sourceDescription = params.sourceDescription;
1064
+ break;
1065
+ }
1066
+ case "assertWithAI":
1067
+ case "assertNoDefectsWithAI":
1068
+ case "extractTextWithAI": {
1069
+ const prompt = params.assertion ?? params.prompt ?? params.question;
1070
+ if (typeof prompt === "string") details.prompt = truncate(prompt, MAX_AI_PROMPT_LENGTH);
1071
+ break;
1072
+ }
1073
+ case "extendedWaitUntil":
1074
+ case "waitForAnimationToEnd": {
1075
+ if (typeof params.timeout === "number") details.timeoutMs = params.timeout;
1076
+ break;
1077
+ }
1078
+ case "repeat":
1079
+ case "retry": {
1080
+ if (typeof params.times === "number") details.times = params.times;
1081
+ if (typeof params.maxRetries === "number") details.maxRetries = params.maxRetries;
1082
+ if (typeof params.condition === "object" && params.condition) details.condition = params.condition;
1083
+ break;
1084
+ }
1085
+ case "runFlow": {
1086
+ if (typeof params.file === "string") details.file = params.file;
1087
+ if (typeof params.flowId === "string") details.flowId = params.flowId;
1088
+ break;
1089
+ }
1090
+ default:
1091
+ for (const key of ["text", "value", "url"]) {
1092
+ const v = params[key];
1093
+ if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
1094
+ details[key] = v;
1095
+ break;
1096
+ }
1097
+ }
1098
+ }
1099
+ return Object.keys(details).length > 0 ? details : void 0;
1100
+ }
1101
+ function extractChildCommands(params, raw, baseTimestamp) {
1102
+ const candidates = [params.commands, raw.commands, raw.subSteps, raw.children];
1103
+ for (const c of candidates) {
1104
+ if (Array.isArray(c) && c.length > 0) {
1105
+ return c.map(
1106
+ (entry, i) => parseRawCommand(entry, i, baseTimestamp)
1107
+ );
1108
+ }
1109
+ }
1110
+ return [];
1111
+ }
567
1112
  function parseRawCommand(raw, index, baseTimestamp) {
568
1113
  let command;
569
1114
  let selector;
1115
+ let details;
1116
+ let children = [];
570
1117
  if (typeof raw.command === "object" && raw.command !== null && !Array.isArray(raw.command)) {
571
1118
  const commandObj = raw.command;
572
1119
  const rawKey = Object.keys(commandObj)[0] ?? `step-${index}`;
573
1120
  command = normalizeMaestroCommandName(rawKey);
574
1121
  const params = commandObj[rawKey] ?? {};
575
1122
  selector = extractMaestroSelector(params);
1123
+ details = extractCommandDetails(command, params);
1124
+ children = extractChildCommands(params, raw, baseTimestamp);
576
1125
  } else {
577
1126
  command = raw.command ?? raw.commandName ?? raw.name ?? `step-${index}`;
578
1127
  selector = raw.selector ?? void 0;
1128
+ children = extractChildCommands({}, raw, baseTimestamp);
579
1129
  }
580
1130
  const meta = raw.metadata ?? {};
581
1131
  const status = mapStatus(meta.status ?? raw.status);
582
1132
  const duration = meta.duration ?? raw.duration ?? raw.durationMs ?? 0;
1133
+ const sequenceNumber = typeof meta.sequenceNumber === "number" ? meta.sequenceNumber : void 0;
583
1134
  let timestamp;
584
1135
  if (typeof meta.timestamp === "number") {
585
1136
  timestamp = new Date(meta.timestamp).toISOString();
@@ -594,7 +1145,10 @@ function parseRawCommand(raw, index, baseTimestamp) {
594
1145
  duration,
595
1146
  timestamp,
596
1147
  ...selector ? { selector } : {},
597
- ...error ? { error } : {}
1148
+ ...error ? { error } : {},
1149
+ ...details ? { details } : {},
1150
+ ...sequenceNumber !== void 0 ? { sequenceNumber } : {},
1151
+ ...children.length > 0 ? { children } : {}
598
1152
  };
599
1153
  }
600
1154
  function mapStatus(status) {
@@ -625,24 +1179,24 @@ function parseCommandsJson(jsonContent, baseTimestamp) {
625
1179
  }
626
1180
  function parseCommandsFile(filePath, baseTimestamp) {
627
1181
  try {
628
- const content = (0, import_node_fs4.readFileSync)(filePath, "utf-8");
1182
+ const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
629
1183
  return parseCommandsJson(content, baseTimestamp);
630
1184
  } catch {
631
1185
  return [];
632
1186
  }
633
1187
  }
634
1188
  function discoverCommandFiles(artifactsDir) {
635
- if (!(0, import_node_fs4.existsSync)(artifactsDir)) return [];
1189
+ if (!(0, import_node_fs6.existsSync)(artifactsDir)) return [];
636
1190
  try {
637
- return (0, import_node_fs4.readdirSync)(artifactsDir, { recursive: true }).map(String).filter((f) => (0, import_node_path3.basename)(f).startsWith("commands") && f.endsWith(".json")).map((f) => (0, import_node_path3.join)(artifactsDir, f));
1191
+ return (0, import_node_fs6.readdirSync)(artifactsDir, { recursive: true }).map(String).filter((f) => (0, import_node_path5.basename)(f).startsWith("commands") && f.endsWith(".json")).map((f) => (0, import_node_path5.join)(artifactsDir, f));
638
1192
  } catch {
639
1193
  return [];
640
1194
  }
641
1195
  }
642
1196
 
643
1197
  // src/parsers/flow-parser.ts
644
- var import_node_fs5 = require("fs");
645
- var import_node_path4 = require("path");
1198
+ var import_node_fs7 = require("fs");
1199
+ var import_node_path6 = require("path");
646
1200
  var import_yaml = require("yaml");
647
1201
  function extractSubflowRefs(body) {
648
1202
  const refs = [];
@@ -736,21 +1290,21 @@ function parseFlowYaml(content, filePath) {
736
1290
  };
737
1291
  }
738
1292
  function parseFlowFile(filePath) {
739
- const content = (0, import_node_fs5.readFileSync)(filePath, "utf-8");
1293
+ const content = (0, import_node_fs7.readFileSync)(filePath, "utf-8");
740
1294
  return parseFlowYaml(content, filePath);
741
1295
  }
742
1296
  function discoverFlowFiles(dir) {
743
- if (!(0, import_node_fs5.existsSync)(dir)) return [];
1297
+ if (!(0, import_node_fs7.existsSync)(dir)) return [];
744
1298
  const results = [];
745
1299
  function walk(currentDir) {
746
1300
  try {
747
- const entries = (0, import_node_fs5.readdirSync)(currentDir, { withFileTypes: true });
1301
+ const entries = (0, import_node_fs7.readdirSync)(currentDir, { withFileTypes: true });
748
1302
  for (const entry of entries) {
749
- const fullPath = (0, import_node_path4.join)(currentDir, entry.name);
1303
+ const fullPath = (0, import_node_path6.join)(currentDir, entry.name);
750
1304
  if (entry.isDirectory()) {
751
1305
  walk(fullPath);
752
1306
  } else if (entry.isFile()) {
753
- const ext = (0, import_node_path4.extname)(entry.name).toLowerCase();
1307
+ const ext = (0, import_node_path6.extname)(entry.name).toLowerCase();
754
1308
  if (ext === ".yaml" || ext === ".yml") {
755
1309
  results.push(fullPath);
756
1310
  }
@@ -764,9 +1318,9 @@ function discoverFlowFiles(dir) {
764
1318
  }
765
1319
 
766
1320
  // src/parsers/log-parser.ts
767
- var import_node_fs6 = require("fs");
768
- var import_node_path5 = require("path");
769
- var import_node_fs7 = require("fs");
1321
+ var import_node_fs8 = require("fs");
1322
+ var import_node_path7 = require("path");
1323
+ var import_node_fs9 = require("fs");
770
1324
  var LOG_LINE_REGEX = /^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?)\s+(\w+)\s+(.+)$/;
771
1325
  var TIMESTAMP_ONLY_REGEX = /^\[?(\d{2}:\d{2}:\d{2}(?:\.\d+)?)\]?\s+(\w+)\s+(.+)$/;
772
1326
  function parseLevel(raw) {
@@ -816,7 +1370,7 @@ function parseLogContent(content) {
816
1370
  }
817
1371
  function parseLogFile(filePath) {
818
1372
  try {
819
- const content = (0, import_node_fs6.readFileSync)(filePath, "utf-8");
1373
+ const content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
820
1374
  return parseLogContent(content);
821
1375
  } catch {
822
1376
  return [];
@@ -838,17 +1392,17 @@ function detectPlatformFromLogs(entries) {
838
1392
  return "unknown";
839
1393
  }
840
1394
  function discoverLogFiles(dir) {
841
- if (!(0, import_node_fs6.existsSync)(dir)) return [];
1395
+ if (!(0, import_node_fs8.existsSync)(dir)) return [];
842
1396
  try {
843
- return (0, import_node_fs7.readdirSync)(dir, { recursive: true }).map(String).filter((f) => (0, import_node_path5.basename)(f) === "maestro.log" || f.endsWith(".log")).map((f) => (0, import_node_path5.join)(dir, f));
1397
+ return (0, import_node_fs9.readdirSync)(dir, { recursive: true }).map(String).filter((f) => (0, import_node_path7.basename)(f) === "maestro.log" || f.endsWith(".log")).map((f) => (0, import_node_path7.join)(dir, f));
844
1398
  } catch {
845
1399
  return [];
846
1400
  }
847
1401
  }
848
1402
 
849
1403
  // src/parsers/ai-report-parser.ts
850
- var import_node_fs8 = require("fs");
851
- var import_node_path6 = require("path");
1404
+ var import_node_fs10 = require("fs");
1405
+ var import_node_path8 = require("path");
852
1406
  var DEFECT_SECTION_REGEX = /(?:defect|issue|bug|error|warning|problem)/i;
853
1407
  var SEVERITY_CRITICAL_REGEX = /(?:critical|severe|major|blocker)/i;
854
1408
  var SEVERITY_WARNING_REGEX = /(?:warning|moderate|minor)/i;
@@ -935,38 +1489,250 @@ function parseAiReportHtml(htmlContent) {
935
1489
  }
936
1490
  function parseAiReportFile(filePath) {
937
1491
  try {
938
- const content = (0, import_node_fs8.readFileSync)(filePath, "utf-8");
1492
+ const content = (0, import_node_fs10.readFileSync)(filePath, "utf-8");
939
1493
  return parseAiReportHtml(content);
940
1494
  } catch {
941
1495
  return { defects: [], totalDefects: 0, hasIssues: false, rawHtml: null };
942
1496
  }
943
1497
  }
944
1498
  function discoverAiReports(dir) {
945
- if (!(0, import_node_fs8.existsSync)(dir)) return [];
1499
+ if (!(0, import_node_fs10.existsSync)(dir)) return [];
946
1500
  try {
947
- return (0, import_node_fs8.readdirSync)(dir, { recursive: true }).map(String).filter((f) => {
948
- const name = (0, import_node_path6.basename)(f).toLowerCase();
949
- const ext = (0, import_node_path6.extname)(f).toLowerCase();
1501
+ return (0, import_node_fs10.readdirSync)(dir, { recursive: true }).map(String).filter((f) => {
1502
+ const name = (0, import_node_path8.basename)(f).toLowerCase();
1503
+ const ext = (0, import_node_path8.extname)(f).toLowerCase();
950
1504
  return ext === ".html" && (name.includes("insight") || name.includes("ai") || name.includes("analysis"));
951
- }).map((f) => (0, import_node_path6.join)(dir, f));
1505
+ }).map((f) => (0, import_node_path8.join)(dir, f));
1506
+ } catch {
1507
+ return [];
1508
+ }
1509
+ }
1510
+
1511
+ // src/parsers/network-parser.ts
1512
+ var import_node_fs11 = require("fs");
1513
+ var import_node_path9 = require("path");
1514
+
1515
+ // src/api-redactor.ts
1516
+ var REDACTED = "[REDACTED]";
1517
+ function redactHeaders(headers, redactList) {
1518
+ if (!headers) return null;
1519
+ const lower = new Set(redactList.map((h) => h.toLowerCase()));
1520
+ const out = {};
1521
+ for (const [k, v] of Object.entries(headers)) {
1522
+ out[k] = lower.has(k.toLowerCase()) ? REDACTED : v;
1523
+ }
1524
+ return out;
1525
+ }
1526
+ function redactJsonBody(value, fields) {
1527
+ if (value === null || typeof value !== "object") return value;
1528
+ if (Array.isArray(value)) return value.map((v) => redactJsonBody(v, fields));
1529
+ const out = {};
1530
+ for (const [k, v] of Object.entries(value)) {
1531
+ out[k] = fields.has(k) ? REDACTED : redactJsonBody(v, fields);
1532
+ }
1533
+ return out;
1534
+ }
1535
+ function redactBody(body, fields) {
1536
+ if (!body || fields.length === 0) return body;
1537
+ try {
1538
+ const parsed = JSON.parse(body);
1539
+ const redacted = redactJsonBody(parsed, new Set(fields));
1540
+ return JSON.stringify(redacted);
1541
+ } catch {
1542
+ return body;
1543
+ }
1544
+ }
1545
+ function redactApiCall(call, redactHeadersList, redactBodyFields) {
1546
+ return {
1547
+ ...call,
1548
+ requestHeaders: redactHeaders(call.requestHeaders, redactHeadersList),
1549
+ responseHeaders: redactHeaders(call.responseHeaders, redactHeadersList),
1550
+ requestBody: redactBody(call.requestBody, redactBodyFields),
1551
+ responseBody: call.isBinary ? call.responseBody : redactBody(call.responseBody, redactBodyFields)
1552
+ };
1553
+ }
1554
+ function matchesUrlFilter(url, include, exclude) {
1555
+ const matches = (pat) => typeof pat === "string" ? url.includes(pat) : pat.test(url);
1556
+ if (exclude.some(matches)) return false;
1557
+ if (include.length === 0) return true;
1558
+ return include.some(matches);
1559
+ }
1560
+
1561
+ // src/parsers/network-parser.ts
1562
+ function parseJsonlLine(line2) {
1563
+ const trimmed = line2.trim();
1564
+ if (!trimmed) return null;
1565
+ try {
1566
+ const obj = JSON.parse(trimmed);
1567
+ if (typeof obj.url !== "string" || typeof obj.method !== "string") return null;
1568
+ return {
1569
+ id: obj.id ?? `api-call-${Date.now()}`,
1570
+ timestamp: obj.timestamp ?? (/* @__PURE__ */ new Date()).toISOString(),
1571
+ method: obj.method,
1572
+ url: obj.url,
1573
+ requestHeaders: obj.requestHeaders ?? null,
1574
+ requestBody: obj.requestBody ?? null,
1575
+ responseStatusCode: obj.responseStatusCode ?? null,
1576
+ responseStatusText: obj.responseStatusText ?? null,
1577
+ responseHeaders: obj.responseHeaders ?? null,
1578
+ responseBody: obj.responseBody ?? null,
1579
+ responseTimeMs: typeof obj.responseTimeMs === "number" ? obj.responseTimeMs : 0,
1580
+ isBinary: obj.isBinary ?? false,
1581
+ error: obj.error ?? null
1582
+ };
1583
+ } catch {
1584
+ return null;
1585
+ }
1586
+ }
1587
+ function parseNetworkJsonl(content, config) {
1588
+ const records = [];
1589
+ for (const line2 of content.split(/\r?\n/)) {
1590
+ const rec = parseJsonlLine(line2);
1591
+ if (!rec) continue;
1592
+ if (!matchesUrlFilter(rec.url, config.includeUrls, config.excludeUrls)) continue;
1593
+ records.push(redactApiCall(rec, config.redactHeaders, config.redactBodyFields));
1594
+ }
1595
+ return records;
1596
+ }
1597
+ function parseNetworkJsonlFile(filePath, config) {
1598
+ try {
1599
+ return parseNetworkJsonl((0, import_node_fs11.readFileSync)(filePath, "utf-8"), config);
1600
+ } catch {
1601
+ return [];
1602
+ }
1603
+ }
1604
+ function discoverNetworkJsonlFiles(dir) {
1605
+ if (!(0, import_node_fs11.existsSync)(dir)) return [];
1606
+ try {
1607
+ return (0, import_node_fs11.readdirSync)(dir, { recursive: true }).map(String).filter((f) => f.endsWith(".jsonl")).map((f) => (0, import_node_path9.join)(dir, f));
1608
+ } catch {
1609
+ return [];
1610
+ }
1611
+ }
1612
+ function harHeadersToRecord(headers) {
1613
+ if (!headers || headers.length === 0) return null;
1614
+ const out = {};
1615
+ for (const h of headers) {
1616
+ if (typeof h.name === "string" && typeof h.value === "string") out[h.name] = h.value;
1617
+ }
1618
+ return Object.keys(out).length > 0 ? out : null;
1619
+ }
1620
+ function harBodyToString(postData) {
1621
+ if (!postData) return null;
1622
+ if (typeof postData.text === "string") return postData.text;
1623
+ if (Array.isArray(postData.params)) {
1624
+ return postData.params.map((p) => `${encodeURIComponent(p.name)}=${encodeURIComponent(p.value ?? "")}`).join("&");
1625
+ }
1626
+ return null;
1627
+ }
1628
+ function isBinaryMime(mime) {
1629
+ if (!mime) return false;
1630
+ return /^(image|audio|video|application\/(octet-stream|pdf|zip|protobuf))/.test(mime);
1631
+ }
1632
+ function harEntryToRecord(entry, index) {
1633
+ const req = entry.request;
1634
+ const res = entry.response;
1635
+ if (!req?.method || !req.url) return null;
1636
+ const content = res?.content;
1637
+ const isBinary = isBinaryMime(content?.mimeType);
1638
+ return {
1639
+ id: `har-call-${index}`,
1640
+ timestamp: entry.startedDateTime ?? (/* @__PURE__ */ new Date()).toISOString(),
1641
+ method: req.method,
1642
+ url: req.url,
1643
+ requestHeaders: harHeadersToRecord(req.headers),
1644
+ requestBody: harBodyToString(req.postData),
1645
+ responseStatusCode: typeof res?.status === "number" && res.status > 0 ? res.status : null,
1646
+ responseStatusText: res?.statusText ?? null,
1647
+ responseHeaders: harHeadersToRecord(res?.headers),
1648
+ responseBody: content?.text ?? null,
1649
+ responseTimeMs: typeof entry.time === "number" ? entry.time : 0,
1650
+ isBinary,
1651
+ error: null
1652
+ };
1653
+ }
1654
+ function parseHar(content, config) {
1655
+ let parsed;
1656
+ try {
1657
+ parsed = JSON.parse(content);
1658
+ } catch {
1659
+ return [];
1660
+ }
1661
+ const entries = parsed.log?.entries ?? [];
1662
+ const records = [];
1663
+ entries.forEach((entry, i) => {
1664
+ const rec = harEntryToRecord(entry, i);
1665
+ if (!rec) return;
1666
+ if (!matchesUrlFilter(rec.url, config.includeUrls, config.excludeUrls)) return;
1667
+ records.push(redactApiCall(rec, config.redactHeaders, config.redactBodyFields));
1668
+ });
1669
+ return records;
1670
+ }
1671
+ function parseHarFile(filePath, config) {
1672
+ try {
1673
+ return parseHar((0, import_node_fs11.readFileSync)(filePath, "utf-8"), config);
952
1674
  } catch {
953
1675
  return [];
954
1676
  }
955
1677
  }
1678
+ function bindApiCallsToFlows(calls, flows) {
1679
+ const buckets = /* @__PURE__ */ new Map();
1680
+ for (const f of flows) buckets.set(f.testId, []);
1681
+ if (calls.length === 0 || flows.length === 0) return buckets;
1682
+ const windows = flows.map((f) => ({
1683
+ testId: f.testId,
1684
+ startMs: Date.parse(f.startedAt),
1685
+ endMs: Date.parse(f.completedAt)
1686
+ }));
1687
+ for (const call of calls) {
1688
+ const t = Date.parse(call.timestamp);
1689
+ if (!Number.isFinite(t)) continue;
1690
+ const hit = windows.find((w) => Number.isFinite(w.startMs) && Number.isFinite(w.endMs) && t >= w.startMs && t <= w.endMs);
1691
+ if (hit) {
1692
+ buckets.get(hit.testId).push(call);
1693
+ continue;
1694
+ }
1695
+ let best = null;
1696
+ for (const w of windows) {
1697
+ if (!Number.isFinite(w.startMs) || !Number.isFinite(w.endMs)) continue;
1698
+ const dist = t < w.startMs ? w.startMs - t : t - w.endMs;
1699
+ if (!best || dist < best.dist) best = { testId: w.testId, dist };
1700
+ }
1701
+ if (best) buckets.get(best.testId).push(call);
1702
+ }
1703
+ return buckets;
1704
+ }
1705
+ function collectNetworkByFlow(jsonlDir, config, flows) {
1706
+ if (!config.enabled && !config.harPath) return /* @__PURE__ */ new Map();
1707
+ const all = [];
1708
+ if (jsonlDir) {
1709
+ for (const file of discoverNetworkJsonlFiles(jsonlDir)) {
1710
+ all.push(...parseNetworkJsonlFile(file, config));
1711
+ }
1712
+ }
1713
+ if (config.harPath && (0, import_node_fs11.existsSync)(config.harPath)) {
1714
+ all.push(...parseHarFile(config.harPath, config));
1715
+ }
1716
+ if (all.length === 0) return /* @__PURE__ */ new Map();
1717
+ return bindApiCallsToFlows(all, flows);
1718
+ }
1719
+ function reindexFlowCalls(calls) {
1720
+ return calls.map((c, i) => ({ ...c, id: `api-call-${i}` }));
1721
+ }
956
1722
 
957
1723
  // src/artifact-collector.ts
958
- var import_node_fs9 = require("fs");
959
- var import_node_path7 = require("path");
1724
+ var import_node_fs12 = require("fs");
1725
+ var import_node_path10 = require("path");
960
1726
  var IMAGE_EXTENSIONS = /* @__PURE__ */ new Set([".png", ".jpg", ".jpeg", ".webp"]);
961
1727
  var VIDEO_EXTENSIONS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov"]);
962
1728
  function walkDir(dir) {
963
1729
  const results = [];
964
- if (!(0, import_node_fs9.existsSync)(dir)) return results;
1730
+ if (!(0, import_node_fs12.existsSync)(dir)) return results;
965
1731
  function recurse(current) {
966
1732
  try {
967
- const entries = (0, import_node_fs9.readdirSync)(current, { withFileTypes: true });
1733
+ const entries = (0, import_node_fs12.readdirSync)(current, { withFileTypes: true });
968
1734
  for (const entry of entries) {
969
- const fullPath = (0, import_node_path7.join)(current, entry.name);
1735
+ const fullPath = (0, import_node_path10.join)(current, entry.name);
970
1736
  if (entry.isDirectory()) {
971
1737
  recurse(fullPath);
972
1738
  } else if (entry.isFile()) {
@@ -993,8 +1759,8 @@ function collectArtifacts(testOutputDir, debugOutputDir) {
993
1759
  for (const filePath of allFiles) {
994
1760
  if (seen.has(filePath)) continue;
995
1761
  seen.add(filePath);
996
- const name = (0, import_node_path7.basename)(filePath).toLowerCase();
997
- const ext = (0, import_node_path7.extname)(filePath).toLowerCase();
1762
+ const name = (0, import_node_path10.basename)(filePath).toLowerCase();
1763
+ const ext = (0, import_node_path10.extname)(filePath).toLowerCase();
998
1764
  if (IMAGE_EXTENSIONS.has(ext)) {
999
1765
  screenshotPaths.push(filePath);
1000
1766
  continue;
@@ -1055,24 +1821,106 @@ function mapCommandCategory(category) {
1055
1821
  return "custom_step";
1056
1822
  }
1057
1823
  }
1058
- function commandToAction(cmd) {
1824
+ function buildStepTitle(cmd) {
1825
+ const d = cmd.details;
1826
+ if (d) {
1827
+ switch (cmd.command) {
1828
+ case "inputText":
1829
+ case "pasteText":
1830
+ if (typeof d.text === "string") return `${cmd.command} \u2192 ${d.text}`;
1831
+ break;
1832
+ case "swipe":
1833
+ case "scroll":
1834
+ case "scrollUntilVisible":
1835
+ if (typeof d.direction === "string") return `${cmd.command} ${d.direction}`;
1836
+ break;
1837
+ case "setLocation":
1838
+ if (typeof d.latitude === "number" && typeof d.longitude === "number") {
1839
+ return `setLocation ${d.latitude},${d.longitude}`;
1840
+ }
1841
+ break;
1842
+ case "pressKey":
1843
+ if (typeof d.key === "string") return `pressKey ${d.key}`;
1844
+ break;
1845
+ case "setOrientation":
1846
+ if (typeof d.orientation === "string") return `setOrientation ${d.orientation}`;
1847
+ break;
1848
+ case "launchApp":
1849
+ case "killApp":
1850
+ case "stopApp":
1851
+ case "clearState":
1852
+ if (typeof d.appId === "string") return `${cmd.command} ${d.appId}`;
1853
+ break;
1854
+ case "openLink":
1855
+ if (typeof d.url === "string") return `openLink ${d.url}`;
1856
+ break;
1857
+ case "runScript":
1858
+ case "evalScript":
1859
+ if (typeof d.path === "string") return `${cmd.command} ${d.path}`;
1860
+ if (typeof d.sourceDescription === "string") return `${cmd.command} ${d.sourceDescription}`;
1861
+ break;
1862
+ case "assertWithAI":
1863
+ case "assertNoDefectsWithAI":
1864
+ case "extractTextWithAI":
1865
+ if (typeof d.prompt === "string") return `${cmd.command} \u2192 ${d.prompt}`;
1866
+ break;
1867
+ case "retry":
1868
+ if (typeof d.maxRetries === "number") return `retry \xD7 ${d.maxRetries}`;
1869
+ break;
1870
+ case "repeat":
1871
+ if (typeof d.times === "number") return `repeat \xD7 ${d.times}`;
1872
+ break;
1873
+ case "runFlow":
1874
+ if (typeof d.file === "string") return `runFlow ${d.file}`;
1875
+ break;
1876
+ }
1877
+ }
1878
+ return cmd.selector ? `${cmd.command} \u2192 ${cmd.selector}` : cmd.command;
1879
+ }
1880
+ function commandToAction(cmd, flowStartedMs) {
1881
+ const cmdMs = Date.parse(cmd.timestamp);
1882
+ const videoOffset = Number.isFinite(cmdMs) && Number.isFinite(flowStartedMs) ? Math.max(0, (cmdMs - flowStartedMs) / 1e3) : null;
1883
+ const children = (cmd.children ?? []).map((c) => commandToAction(c, flowStartedMs));
1059
1884
  return {
1060
- title: cmd.selector ? `${cmd.command} \u2192 ${cmd.selector}` : cmd.command,
1885
+ title: buildStepTitle(cmd),
1061
1886
  category: mapCommandCategory(cmd.category),
1062
1887
  status: cmd.status === "failed" ? "failed" : "passed",
1063
1888
  duration: cmd.duration,
1064
1889
  timestamp: cmd.timestamp,
1065
- videoOffset: null,
1890
+ videoOffset,
1066
1891
  error: cmd.error ?? null,
1067
- children: []
1892
+ children
1068
1893
  };
1069
1894
  }
1070
- function buildTestResult(flow) {
1895
+ function sortActions(a, b) {
1896
+ const ta = Date.parse(a.timestamp);
1897
+ const tb = Date.parse(b.timestamp);
1898
+ if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
1899
+ return 0;
1900
+ }
1901
+ function buildTestResult(flow, apiCalls) {
1902
+ const flowStartedMs = Date.parse(flow.startedAt);
1903
+ const orderedCommands = [...flow.commands].sort((a, b) => {
1904
+ const ta = Date.parse(a.timestamp);
1905
+ const tb = Date.parse(b.timestamp);
1906
+ if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
1907
+ const sa = a.sequenceNumber ?? 0;
1908
+ const sb = b.sequenceNumber ?? 0;
1909
+ return sa - sb;
1910
+ });
1911
+ const orderedAssertions = [...flow.assertions].sort((a, b) => {
1912
+ const ta = Date.parse(a.timestamp);
1913
+ const tb = Date.parse(b.timestamp);
1914
+ if (Number.isFinite(ta) && Number.isFinite(tb) && ta !== tb) return ta - tb;
1915
+ const sa = a.sequenceNumber ?? 0;
1916
+ const sb = b.sequenceNumber ?? 0;
1917
+ return sa - sb;
1918
+ });
1071
1919
  const actions = [
1072
- ...flow.commands.map(commandToAction),
1073
- ...flow.assertions.map(commandToAction)
1920
+ ...orderedCommands.map((c) => commandToAction(c, flowStartedMs)),
1921
+ ...orderedAssertions.map((c) => commandToAction(c, flowStartedMs))
1074
1922
  ];
1075
- actions.sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
1923
+ actions.sort(sortActions);
1076
1924
  const artifacts = flow.screenshotPaths.length > 0 || flow.videoPath ? {
1077
1925
  screenshot: flow.screenshotPaths[0] ?? void 0,
1078
1926
  video: flow.videoPath ?? void 0
@@ -1100,14 +1948,16 @@ function buildTestResult(flow) {
1100
1948
  actualStatus: flow.status,
1101
1949
  artifacts,
1102
1950
  networkRequests: null,
1103
- apiCalls: null,
1951
+ apiCalls: apiCalls.length > 0 ? [...apiCalls] : null,
1104
1952
  apiAssertions: null,
1105
1953
  actions: actions.length > 0 ? actions : null,
1106
1954
  consoleLogs: null
1107
1955
  };
1108
1956
  }
1109
- function buildTimelineEntry(flow) {
1110
- const testResult = buildTestResult(flow);
1957
+ function buildTimelineEntry(flow, apiCallsByFlow) {
1958
+ const testId = `${flow.flowFile}::${flow.flowName}`;
1959
+ const apiCalls = apiCallsByFlow?.get(testId) ?? [];
1960
+ const testResult = buildTestResult(flow, apiCalls);
1111
1961
  return {
1112
1962
  url: flow.appId ?? "maestro-flow",
1113
1963
  navigationType: "page_load",
@@ -1120,8 +1970,8 @@ function buildTimelineEntry(flow) {
1120
1970
  tests: [testResult]
1121
1971
  };
1122
1972
  }
1123
- function buildTimeline(flows) {
1124
- return flows.map(buildTimelineEntry);
1973
+ function buildTimeline(flows, apiCallsByFlow) {
1974
+ return flows.map((f) => buildTimelineEntry(f, apiCallsByFlow));
1125
1975
  }
1126
1976
 
1127
1977
  // src/summary-builder.ts
@@ -1132,6 +1982,58 @@ var EMPTY_STATUS_RANGE = {
1132
1982
  "5xx": 0,
1133
1983
  error: 0
1134
1984
  };
1985
+ function statusBucket(code) {
1986
+ if (code === null) return "error";
1987
+ if (code >= 200 && code < 300) return "2xx";
1988
+ if (code >= 300 && code < 400) return "3xx";
1989
+ if (code >= 400 && code < 500) return "4xx";
1990
+ if (code >= 500 && code < 600) return "5xx";
1991
+ return "error";
1992
+ }
1993
+ function percentile(sorted, p) {
1994
+ if (sorted.length === 0) return null;
1995
+ const idx = Math.min(sorted.length - 1, Math.floor(p / 100 * sorted.length));
1996
+ return sorted[idx];
1997
+ }
1998
+ function buildApiStats(calls) {
1999
+ if (calls.length === 0) {
2000
+ return {
2001
+ totalApiCalls: 0,
2002
+ uniqueApiUrls: 0,
2003
+ apiCallsByMethod: {},
2004
+ apiCallsByStatusRange: { ...EMPTY_STATUS_RANGE },
2005
+ apiResponseTime: null
2006
+ };
2007
+ }
2008
+ const byMethod = {};
2009
+ const byStatus = { ...EMPTY_STATUS_RANGE };
2010
+ const urls = /* @__PURE__ */ new Set();
2011
+ const times = [];
2012
+ for (const c of calls) {
2013
+ byMethod[c.method] = (byMethod[c.method] ?? 0) + 1;
2014
+ byStatus[statusBucket(c.responseStatusCode)] += 1;
2015
+ urls.add(c.url);
2016
+ if (Number.isFinite(c.responseTimeMs) && c.responseTimeMs > 0) {
2017
+ times.push(c.responseTimeMs);
2018
+ }
2019
+ }
2020
+ times.sort((a, b) => a - b);
2021
+ const apiResponseTime = times.length > 0 ? {
2022
+ p50: percentile(times, 50) ?? 0,
2023
+ p95: percentile(times, 95) ?? 0,
2024
+ p99: percentile(times, 99) ?? 0,
2025
+ avg: times.reduce((s, n) => s + n, 0) / times.length,
2026
+ min: times[0],
2027
+ max: times[times.length - 1]
2028
+ } : null;
2029
+ return {
2030
+ totalApiCalls: calls.length,
2031
+ uniqueApiUrls: urls.size,
2032
+ apiCallsByMethod: byMethod,
2033
+ apiCallsByStatusRange: byStatus,
2034
+ apiResponseTime
2035
+ };
2036
+ }
1135
2037
  function buildSummary(timeline) {
1136
2038
  let total = 0;
1137
2039
  let passed = 0;
@@ -1146,6 +2048,7 @@ function buildSummary(timeline) {
1146
2048
  let totalActionSteps = 0;
1147
2049
  const actionCategoryCounts = {};
1148
2050
  const uniqueUrls = /* @__PURE__ */ new Set();
2051
+ const allApiCalls = [];
1149
2052
  for (const entry of timeline) {
1150
2053
  uniqueUrls.add(entry.url);
1151
2054
  for (const test of entry.tests) {
@@ -1180,8 +2083,12 @@ function buildSummary(timeline) {
1180
2083
  }
1181
2084
  }
1182
2085
  }
2086
+ if (test.apiCalls && test.apiCalls.length > 0) {
2087
+ allApiCalls.push(...test.apiCalls);
2088
+ }
1183
2089
  }
1184
2090
  }
2091
+ const apiStats = buildApiStats(allApiCalls);
1185
2092
  return {
1186
2093
  total,
1187
2094
  passed,
@@ -1189,11 +2096,7 @@ function buildSummary(timeline) {
1189
2096
  flaky,
1190
2097
  skipped,
1191
2098
  timedout,
1192
- totalApiCalls: 0,
1193
- uniqueApiUrls: 0,
1194
- apiCallsByMethod: {},
1195
- apiCallsByStatusRange: EMPTY_STATUS_RANGE,
1196
- apiResponseTime: null,
2099
+ ...apiStats,
1197
2100
  totalAssertions,
1198
2101
  passedAssertions,
1199
2102
  failedAssertions,
@@ -1206,8 +2109,8 @@ function buildSummary(timeline) {
1206
2109
  }
1207
2110
 
1208
2111
  // src/html-report.ts
1209
- var import_node_fs10 = require("fs");
1210
- var import_node_path8 = require("path");
2112
+ var import_node_fs13 = require("fs");
2113
+ var import_node_path11 = require("path");
1211
2114
 
1212
2115
  // src/html-css.ts
1213
2116
  var CSS = `
@@ -2469,13 +3372,13 @@ function renderHtmlDocument(reportJson) {
2469
3372
 
2470
3373
  // src/html-report.ts
2471
3374
  function generateHtmlReport(report, aiDefects, screenshotPaths, outputPath) {
2472
- const htmlPath = (0, import_node_path8.resolve)(outputPath);
2473
- const htmlDir = (0, import_node_path8.dirname)(htmlPath);
2474
- if (!(0, import_node_fs10.existsSync)(htmlDir)) (0, import_node_fs10.mkdirSync)(htmlDir, { recursive: true });
3375
+ const htmlPath = (0, import_node_path11.resolve)(outputPath);
3376
+ const htmlDir = (0, import_node_path11.dirname)(htmlPath);
3377
+ if (!(0, import_node_fs13.existsSync)(htmlDir)) (0, import_node_fs13.mkdirSync)(htmlDir, { recursive: true });
2475
3378
  const payload = { report, aiDefects, screenshotPaths };
2476
3379
  const reportJson = JSON.stringify(payload);
2477
3380
  const html = renderHtmlDocument(reportJson);
2478
- (0, import_node_fs10.writeFileSync)(htmlPath, html, "utf-8");
3381
+ (0, import_node_fs13.writeFileSync)(htmlPath, html, "utf-8");
2479
3382
  }
2480
3383
 
2481
3384
  // src/console-summary.ts
@@ -2538,7 +3441,7 @@ function printConsoleSummary(summary, outputPath, htmlReportPath, quiet, aiDefec
2538
3441
  }
2539
3442
 
2540
3443
  // src/browser-open.ts
2541
- var import_node_child_process2 = require("child_process");
3444
+ var import_node_child_process4 = require("child_process");
2542
3445
  function openInBrowser(filePath) {
2543
3446
  const platform = process.platform;
2544
3447
  let command;
@@ -2549,14 +3452,14 @@ function openInBrowser(filePath) {
2549
3452
  } else {
2550
3453
  command = `xdg-open "${filePath}"`;
2551
3454
  }
2552
- (0, import_node_child_process2.exec)(command, () => {
3455
+ (0, import_node_child_process4.exec)(command, () => {
2553
3456
  });
2554
3457
  }
2555
3458
 
2556
3459
  // src/cloud-client.ts
2557
- var import_node_fs13 = require("fs");
2558
- var import_node_path11 = require("path");
2559
- var import_node_crypto2 = require("crypto");
3460
+ var import_node_fs16 = require("fs");
3461
+ var import_node_path14 = require("path");
3462
+ var import_node_crypto3 = require("crypto");
2560
3463
 
2561
3464
  // src/cloud-auth.ts
2562
3465
  var import_core3 = require("@testrelic/core");
@@ -2587,7 +3490,7 @@ async function healthCheck(endpoint) {
2587
3490
  }
2588
3491
  }
2589
3492
  async function sleep(ms) {
2590
- return new Promise((resolve5) => setTimeout(resolve5, ms));
3493
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
2591
3494
  }
2592
3495
  async function parseCloudError(response) {
2593
3496
  try {
@@ -2737,13 +3640,13 @@ async function resolveRepo(endpoint, accessToken, gitId, displayName, timeout, b
2737
3640
  }
2738
3641
 
2739
3642
  // src/git-metadata.ts
2740
- var import_node_child_process3 = require("child_process");
2741
- var import_node_fs11 = require("fs");
2742
- var import_node_path9 = require("path");
3643
+ var import_node_child_process5 = require("child_process");
3644
+ var import_node_fs14 = require("fs");
3645
+ var import_node_path12 = require("path");
2743
3646
  var GIT_TIMEOUT_MS = 5e3;
2744
3647
  function execGit(command, cwd) {
2745
3648
  try {
2746
- const result = (0, import_node_child_process3.execSync)(command, {
3649
+ const result = (0, import_node_child_process5.execSync)(command, {
2747
3650
  cwd,
2748
3651
  timeout: GIT_TIMEOUT_MS,
2749
3652
  encoding: "utf-8",
@@ -2788,7 +3691,7 @@ function deriveRepoDisplayName(normalizedUrl) {
2788
3691
  }
2789
3692
  function readPackageJsonName(dirPath) {
2790
3693
  try {
2791
- const raw = (0, import_node_fs11.readFileSync)((0, import_node_path9.join)(dirPath, "package.json"), "utf-8");
3694
+ const raw = (0, import_node_fs14.readFileSync)((0, import_node_path12.join)(dirPath, "package.json"), "utf-8");
2792
3695
  const pkg = JSON.parse(raw);
2793
3696
  return typeof pkg.name === "string" && pkg.name.length > 0 ? pkg.name : null;
2794
3697
  } catch {
@@ -2798,25 +3701,25 @@ function readPackageJsonName(dirPath) {
2798
3701
  function deriveNonGitProjectId(dirPath) {
2799
3702
  const pkgName = readPackageJsonName(dirPath);
2800
3703
  if (pkgName) return pkgName;
2801
- return `local/${(0, import_node_path9.basename)(dirPath)}`;
3704
+ return `local/${(0, import_node_path12.basename)(dirPath)}`;
2802
3705
  }
2803
3706
 
2804
3707
  // src/cloud-queue.ts
2805
- var import_node_fs12 = require("fs");
2806
- var import_node_path10 = require("path");
3708
+ var import_node_fs15 = require("fs");
3709
+ var import_node_path13 = require("path");
2807
3710
  var import_core4 = require("@testrelic/core");
2808
3711
  var QUEUE_ENTRY_VERSION = "1.0.0";
2809
3712
  var FLUSH_BATCH_SIZE = 10;
2810
3713
  function writeToQueue(queueDirectory, runId, type, reason, targetEndpoint, method, payload, headers) {
2811
3714
  try {
2812
- (0, import_node_fs12.mkdirSync)(queueDirectory, { recursive: true });
3715
+ (0, import_node_fs15.mkdirSync)(queueDirectory, { recursive: true });
2813
3716
  const timestamp = Date.now();
2814
3717
  const filename = `${timestamp}-${runId}-${type}.json`;
2815
- const filePath = (0, import_node_path10.join)(queueDirectory, filename);
3718
+ const filePath = (0, import_node_path13.join)(queueDirectory, filename);
2816
3719
  const entry = { version: QUEUE_ENTRY_VERSION, queuedAt: new Date(timestamp).toISOString(), reason, retryCount: 0, targetEndpoint, method, payload, headers };
2817
3720
  const tmpPath = filePath + ".tmp";
2818
- (0, import_node_fs12.writeFileSync)(tmpPath, JSON.stringify(entry, null, 2), "utf-8");
2819
- (0, import_node_fs12.renameSync)(tmpPath, filePath);
3721
+ (0, import_node_fs15.writeFileSync)(tmpPath, JSON.stringify(entry, null, 2), "utf-8");
3722
+ (0, import_node_fs15.renameSync)(tmpPath, filePath);
2820
3723
  } catch (err) {
2821
3724
  const code = err.code;
2822
3725
  if (code === "ENOSPC") {
@@ -2829,7 +3732,7 @@ function writeToQueue(queueDirectory, runId, type, reason, targetEndpoint, metho
2829
3732
  async function flushQueue(queueDirectory, endpoint, accessToken) {
2830
3733
  let files;
2831
3734
  try {
2832
- files = (0, import_node_fs12.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).sort();
3735
+ files = (0, import_node_fs15.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp")).sort();
2833
3736
  } catch {
2834
3737
  return;
2835
3738
  }
@@ -2840,14 +3743,14 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
2840
3743
  }
2841
3744
  for (const batch of batches) {
2842
3745
  for (const file of batch) {
2843
- const filePath = (0, import_node_path10.join)(queueDirectory, file);
3746
+ const filePath = (0, import_node_path13.join)(queueDirectory, file);
2844
3747
  try {
2845
- const stat = (0, import_node_fs12.statSync)(filePath);
3748
+ const stat = (0, import_node_fs15.statSync)(filePath);
2846
3749
  if (!stat.isFile()) continue;
2847
- const raw = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
3750
+ const raw = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
2848
3751
  const entry = JSON.parse(raw);
2849
3752
  if (!(0, import_core4.isValidQueueEntry)(entry)) {
2850
- (0, import_node_fs12.unlinkSync)(filePath);
3753
+ (0, import_node_fs15.unlinkSync)(filePath);
2851
3754
  continue;
2852
3755
  }
2853
3756
  const queueEntry = entry;
@@ -2857,7 +3760,7 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
2857
3760
  body: JSON.stringify(queueEntry.payload)
2858
3761
  });
2859
3762
  if (response.ok) {
2860
- (0, import_node_fs12.unlinkSync)(filePath);
3763
+ (0, import_node_fs15.unlinkSync)(filePath);
2861
3764
  } else {
2862
3765
  return;
2863
3766
  }
@@ -2869,21 +3772,21 @@ async function flushQueue(queueDirectory, endpoint, accessToken) {
2869
3772
  }
2870
3773
  function cleanupExpiredQueue(queueDirectory, maxAge) {
2871
3774
  try {
2872
- const files = (0, import_node_fs12.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
3775
+ const files = (0, import_node_fs15.readdirSync)(queueDirectory).filter((f) => f.endsWith(".json") && !f.endsWith(".tmp"));
2873
3776
  const now = Date.now();
2874
3777
  for (const file of files) {
2875
- const filePath = (0, import_node_path10.join)(queueDirectory, file);
3778
+ const filePath = (0, import_node_path13.join)(queueDirectory, file);
2876
3779
  try {
2877
- const raw = (0, import_node_fs12.readFileSync)(filePath, "utf-8");
3780
+ const raw = (0, import_node_fs15.readFileSync)(filePath, "utf-8");
2878
3781
  const entry = JSON.parse(raw);
2879
3782
  const queuedAt = entry.queuedAt;
2880
3783
  if (typeof queuedAt === "string") {
2881
3784
  const queuedTime = new Date(queuedAt).getTime();
2882
- if (now - queuedTime > maxAge) (0, import_node_fs12.unlinkSync)(filePath);
3785
+ if (now - queuedTime > maxAge) (0, import_node_fs15.unlinkSync)(filePath);
2883
3786
  }
2884
3787
  } catch {
2885
3788
  try {
2886
- (0, import_node_fs12.unlinkSync)(filePath);
3789
+ (0, import_node_fs15.unlinkSync)(filePath);
2887
3790
  } catch {
2888
3791
  }
2889
3792
  }
@@ -3026,7 +3929,7 @@ var CloudClient = class {
3026
3929
  }
3027
3930
  if (this.flushPromise) {
3028
3931
  try {
3029
- await Promise.race([this.flushPromise, new Promise((resolve5) => setTimeout(resolve5, FLUSH_TIMEOUT_MS))]);
3932
+ await Promise.race([this.flushPromise, new Promise((resolve6) => setTimeout(resolve6, FLUSH_TIMEOUT_MS))]);
3030
3933
  } catch {
3031
3934
  }
3032
3935
  this.flushPromise = null;
@@ -3084,10 +3987,10 @@ var CloudClient = class {
3084
3987
  }
3085
3988
  readRepoCache(gitId) {
3086
3989
  if (!this.config) return null;
3087
- const cachePath = (0, import_node_path11.join)(this.config.queueDirectory, "..", "cache", "repo.json");
3990
+ const cachePath = (0, import_node_path14.join)(this.config.queueDirectory, "..", "cache", "repo.json");
3088
3991
  try {
3089
- if (!(0, import_node_fs13.existsSync)(cachePath)) return null;
3090
- const raw = (0, import_node_fs13.readFileSync)(cachePath, "utf-8");
3992
+ if (!(0, import_node_fs16.existsSync)(cachePath)) return null;
3993
+ const raw = (0, import_node_fs16.readFileSync)(cachePath, "utf-8");
3091
3994
  const cache = JSON.parse(raw);
3092
3995
  if (cache.gitId !== gitId) return null;
3093
3996
  if (Date.now() - cache.resolvedAt > PROJECT_CACHE_TTL_MS) return null;
@@ -3100,20 +4003,20 @@ var CloudClient = class {
3100
4003
  }
3101
4004
  writeRepoCache(gitId, repoId, displayName) {
3102
4005
  if (!this.config) return;
3103
- const cacheDir = (0, import_node_path11.join)(this.config.queueDirectory, "..", "cache");
3104
- const cachePath = (0, import_node_path11.join)(cacheDir, "repo.json");
4006
+ const cacheDir = (0, import_node_path14.join)(this.config.queueDirectory, "..", "cache");
4007
+ const cachePath = (0, import_node_path14.join)(cacheDir, "repo.json");
3105
4008
  try {
3106
- (0, import_node_fs13.mkdirSync)(cacheDir, { recursive: true });
4009
+ (0, import_node_fs16.mkdirSync)(cacheDir, { recursive: true });
3107
4010
  const cache = { repoId, gitId, displayName, resolvedAt: Date.now(), apiKeyHash: this.hashApiKey() ?? "" };
3108
4011
  const tmpPath = cachePath + ".tmp";
3109
- (0, import_node_fs13.writeFileSync)(tmpPath, JSON.stringify(cache, null, 2), "utf-8");
3110
- (0, import_node_fs13.renameSync)(tmpPath, cachePath);
4012
+ (0, import_node_fs16.writeFileSync)(tmpPath, JSON.stringify(cache, null, 2), "utf-8");
4013
+ (0, import_node_fs16.renameSync)(tmpPath, cachePath);
3111
4014
  } catch {
3112
4015
  }
3113
4016
  }
3114
4017
  hashApiKey() {
3115
4018
  if (!this.config?.apiKey) return null;
3116
- return (0, import_node_crypto2.createHash)("sha256").update(this.config.apiKey).digest("hex").substring(0, 16);
4019
+ return (0, import_node_crypto3.createHash)("sha256").update(this.config.apiKey).digest("hex").substring(0, 16);
3117
4020
  }
3118
4021
  startBackgroundFlush() {
3119
4022
  if (!this.config || !this.authState.accessToken) return;
@@ -3122,7 +4025,7 @@ var CloudClient = class {
3122
4025
  const endpoint = this.config.endpoint;
3123
4026
  this.flushPromise = Promise.race([
3124
4027
  flushQueue(queueDir, endpoint, accessToken),
3125
- new Promise((resolve5) => setTimeout(resolve5, FLUSH_TIMEOUT_MS))
4028
+ new Promise((resolve6) => setTimeout(resolve6, FLUSH_TIMEOUT_MS))
3126
4029
  ]).catch(() => {
3127
4030
  });
3128
4031
  }
@@ -3179,7 +4082,7 @@ function buildUploadPayload(report, repoGitId, git, ci, tests) {
3179
4082
  return payload;
3180
4083
  }
3181
4084
  async function sleep2(ms) {
3182
- return new Promise((resolve5) => setTimeout(resolve5, ms));
4085
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
3183
4086
  }
3184
4087
  async function retryWithBackoff(url, init, onTokenRefresh) {
3185
4088
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
@@ -3255,8 +4158,8 @@ async function uploadBatchRun(endpoint, accessToken, payload, onTokenRefresh) {
3255
4158
  }
3256
4159
 
3257
4160
  // src/cloud-artifact-upload.ts
3258
- var import_node_fs14 = require("fs");
3259
- var import_node_path12 = require("path");
4161
+ var import_node_fs17 = require("fs");
4162
+ var import_node_path15 = require("path");
3260
4163
  var import_node_stream = require("stream");
3261
4164
  var UPLOAD_CONCURRENCY = 5;
3262
4165
  var RETRY_DELAYS_MS2 = [1e3, 3e3, 9e3];
@@ -3275,7 +4178,7 @@ var activeUploads = 0;
3275
4178
  var pendingUploads = [];
3276
4179
  async function acquireSlot() {
3277
4180
  if (activeUploads >= UPLOAD_CONCURRENCY) {
3278
- await new Promise((resolve5) => pendingUploads.push(resolve5));
4181
+ await new Promise((resolve6) => pendingUploads.push(resolve6));
3279
4182
  }
3280
4183
  activeUploads++;
3281
4184
  }
@@ -3284,7 +4187,7 @@ function releaseSlot() {
3284
4187
  if (pendingUploads.length > 0) pendingUploads.shift()();
3285
4188
  }
3286
4189
  async function sleep3(ms) {
3287
- return new Promise((resolve5) => setTimeout(resolve5, ms));
4190
+ return new Promise((resolve6) => setTimeout(resolve6, ms));
3288
4191
  }
3289
4192
  function getContentType(filePath) {
3290
4193
  const ext = filePath.substring(filePath.lastIndexOf(".")).toLowerCase();
@@ -3292,14 +4195,14 @@ function getContentType(filePath) {
3292
4195
  }
3293
4196
  function getFileSize(filePath) {
3294
4197
  try {
3295
- return (0, import_node_fs14.statSync)(filePath).size;
4198
+ return (0, import_node_fs17.statSync)(filePath).size;
3296
4199
  } catch {
3297
4200
  return 0;
3298
4201
  }
3299
4202
  }
3300
4203
  async function requestUploadUrl(endpoint, accessToken, request, sizeBytes, contentType) {
3301
4204
  const url = `${endpoint}/artifacts/upload-url`;
3302
- const body = JSON.stringify({ runId: request.runId, testId: request.testId, fileName: (0, import_node_path12.basename)(request.filePath), contentType, type: request.type, sizeBytes });
4205
+ const body = JSON.stringify({ runId: request.runId, testId: request.testId, fileName: (0, import_node_path15.basename)(request.filePath), contentType, type: request.type, sizeBytes });
3303
4206
  for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
3304
4207
  try {
3305
4208
  const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": `Bearer ${accessToken}` }, body });
@@ -3322,7 +4225,7 @@ async function requestUploadUrl(endpoint, accessToken, request, sizeBytes, conte
3322
4225
  async function putFileToPresignedUrl(presignedUrl, filePath, contentType, sizeBytes) {
3323
4226
  for (let attempt = 0; attempt < MAX_RETRIES2; attempt++) {
3324
4227
  try {
3325
- const nodeStream = (0, import_node_fs14.createReadStream)(filePath);
4228
+ const nodeStream = (0, import_node_fs17.createReadStream)(filePath);
3326
4229
  const webStream = import_node_stream.Readable.toWeb(nodeStream);
3327
4230
  const response = await fetch(presignedUrl, {
3328
4231
  method: "PUT",
@@ -3615,35 +4518,35 @@ async function finalizeAndUpload(cloudClient, cloudConfig, testRunId, report, co
3615
4518
  // src/report-orchestrator.ts
3616
4519
  function matchVideoToFlow(flowFile, videoPaths, flowRecordingPath) {
3617
4520
  if (videoPaths.length === 0) return null;
3618
- const flowBase = (0, import_node_path13.basename)(flowFile, (0, import_node_path13.extname)(flowFile)).toLowerCase();
4521
+ const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
3619
4522
  const exact = videoPaths.find(
3620
- (v) => (0, import_node_path13.basename)(v, (0, import_node_path13.extname)(v)).toLowerCase() === flowBase
4523
+ (v) => (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase() === flowBase
3621
4524
  );
3622
4525
  if (exact) return exact;
3623
4526
  const forward = videoPaths.find(
3624
- (v) => (0, import_node_path13.basename)(v).toLowerCase().includes(flowBase)
4527
+ (v) => (0, import_node_path16.basename)(v).toLowerCase().includes(flowBase)
3625
4528
  );
3626
4529
  if (forward) return forward;
3627
4530
  const reverse = videoPaths.find((v) => {
3628
- const videoBase = (0, import_node_path13.basename)(v, (0, import_node_path13.extname)(v)).toLowerCase();
4531
+ const videoBase = (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase();
3629
4532
  return videoBase.length >= 3 && (flowBase.startsWith(videoBase) || flowBase.includes(videoBase));
3630
4533
  });
3631
4534
  if (reverse) return reverse;
3632
4535
  if (flowRecordingPath) {
3633
- const recBase = (0, import_node_path13.basename)(flowRecordingPath, (0, import_node_path13.extname)(flowRecordingPath)).toLowerCase();
3634
- const byRec = videoPaths.find((v) => (0, import_node_path13.basename)(v, (0, import_node_path13.extname)(v)).toLowerCase() === recBase);
4536
+ const recBase = (0, import_node_path16.basename)(flowRecordingPath, (0, import_node_path16.extname)(flowRecordingPath)).toLowerCase();
4537
+ const byRec = videoPaths.find((v) => (0, import_node_path16.basename)(v, (0, import_node_path16.extname)(v)).toLowerCase() === recBase);
3635
4538
  if (byRec) return byRec;
3636
- const byRecPartial = videoPaths.find((v) => (0, import_node_path13.basename)(v).toLowerCase().includes(recBase));
4539
+ const byRecPartial = videoPaths.find((v) => (0, import_node_path16.basename)(v).toLowerCase().includes(recBase));
3637
4540
  if (byRecPartial) return byRecPartial;
3638
4541
  }
3639
4542
  return null;
3640
4543
  }
3641
4544
  function matchScreenshotsToFlow(flowFile, screenshotPaths) {
3642
4545
  if (screenshotPaths.length === 0) return [];
3643
- const flowBase = (0, import_node_path13.basename)(flowFile, (0, import_node_path13.extname)(flowFile)).toLowerCase();
4546
+ const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
3644
4547
  const matched = screenshotPaths.filter((s) => {
3645
- const name = (0, import_node_path13.basename)(s).toLowerCase();
3646
- const nameBase = (0, import_node_path13.basename)(s, (0, import_node_path13.extname)(s)).toLowerCase();
4548
+ const name = (0, import_node_path16.basename)(s).toLowerCase();
4549
+ const nameBase = (0, import_node_path16.basename)(s, (0, import_node_path16.extname)(s)).toLowerCase();
3647
4550
  if (name.startsWith(flowBase) || name.includes(flowBase)) return true;
3648
4551
  return nameBase.length >= 3 && (flowBase.startsWith(nameBase) || flowBase.includes(nameBase));
3649
4552
  });
@@ -3663,18 +4566,18 @@ function extractRecordingPath(commands) {
3663
4566
  function buildFlowRecordingMap(commandSteps) {
3664
4567
  const map = /* @__PURE__ */ new Map();
3665
4568
  for (const [filePath, commands] of commandSteps) {
3666
- const name = (0, import_node_path13.basename)(filePath).replace(/^commands[-_]?/i, "").replace(/\.json$/i, "").toLowerCase();
4569
+ const name = (0, import_node_path16.basename)(filePath).replace(/^commands[-_]?/i, "").replace(/\.json$/i, "").toLowerCase();
3667
4570
  const recPath = extractRecordingPath(commands);
3668
4571
  if (recPath && name) map.set(name, recPath);
3669
4572
  }
3670
4573
  return map;
3671
4574
  }
3672
4575
  function commandsForFlow(commandSteps, flowFile) {
3673
- const flowBase = (0, import_node_path13.basename)(flowFile, (0, import_node_path13.extname)(flowFile)).toLowerCase();
4576
+ const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
3674
4577
  if (!flowBase || commandSteps.size === 0) {
3675
4578
  return { commands: [], matchedAnyFile: false };
3676
4579
  }
3677
- const stripCmdPrefix = (p) => (0, import_node_path13.basename)(p, (0, import_node_path13.extname)(p)).replace(/^commands[-_]?/i, "").toLowerCase();
4580
+ const stripCmdPrefix = (p) => (0, import_node_path16.basename)(p, (0, import_node_path16.extname)(p)).replace(/^commands[-_]?/i, "").toLowerCase();
3678
4581
  for (const [cmdFile, cmds] of commandSteps) {
3679
4582
  if (stripCmdPrefix(cmdFile) === flowBase) {
3680
4583
  return { commands: cmds, matchedAnyFile: true };
@@ -3706,7 +4609,7 @@ function platformToOs(platform) {
3706
4609
  return void 0;
3707
4610
  }
3708
4611
  }
3709
- function flowToTestResult(flow) {
4612
+ function flowToTestResult(flow, apiCalls) {
3710
4613
  const consoleLogs = flow.logEntries.length > 0 ? flow.logEntries.map((entry) => ({
3711
4614
  level: entry.level.toLowerCase(),
3712
4615
  message: entry.message,
@@ -3733,7 +4636,8 @@ function flowToTestResult(flow) {
3733
4636
  platform: flow.platform !== "unknown" ? flow.platform : void 0,
3734
4637
  os: platformToOs(flow.platform),
3735
4638
  deviceName: flow.deviceId,
3736
- ...consoleLogs ? { consoleLogs } : {}
4639
+ ...consoleLogs ? { consoleLogs } : {},
4640
+ ...apiCalls && apiCalls.length > 0 ? { apiCalls } : {}
3737
4641
  };
3738
4642
  }
3739
4643
  function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
@@ -3752,7 +4656,7 @@ function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
3752
4656
  async function orchestrateReport(input) {
3753
4657
  const { config } = input;
3754
4658
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3755
- const testRunId = config.testRunId ?? (0, import_node_crypto3.randomUUID)();
4659
+ const testRunId = config.testRunId ?? (0, import_node_crypto4.randomUUID)();
3756
4660
  const artifacts = collectArtifacts(input.testOutputDir, input.debugOutputDir);
3757
4661
  const junitPath = input.junitPath ?? artifacts.junitReportPath;
3758
4662
  const logFiles = input.debugOutputDir ? discoverLogFiles(input.debugOutputDir) : artifacts.logPaths;
@@ -3767,7 +4671,7 @@ async function orchestrateReport(input) {
3767
4671
  if (detected !== "unknown") platform = detected;
3768
4672
  }
3769
4673
  const flowMetadataMap = /* @__PURE__ */ new Map();
3770
- if (input.flowsDir && (0, import_node_fs15.existsSync)(input.flowsDir)) {
4674
+ if (input.flowsDir && (0, import_node_fs18.existsSync)(input.flowsDir)) {
3771
4675
  const flowFiles = discoverFlowFiles(input.flowsDir);
3772
4676
  for (const flowFile of flowFiles) {
3773
4677
  const meta = parseFlowFile(flowFile);
@@ -3788,7 +4692,7 @@ async function orchestrateReport(input) {
3788
4692
  }
3789
4693
  const flowRecordingMap = buildFlowRecordingMap(commandSteps);
3790
4694
  const flowResults = [];
3791
- if (junitPath && (0, import_node_fs15.existsSync)(junitPath)) {
4695
+ if (junitPath && (0, import_node_fs18.existsSync)(junitPath)) {
3792
4696
  const junit = parseJUnitFile(junitPath);
3793
4697
  const allCommands = Array.from(commandSteps.values()).flat();
3794
4698
  const allAssertions = allCommands.filter((c) => c.category === "assertion");
@@ -3802,7 +4706,7 @@ async function orchestrateReport(input) {
3802
4706
  const flowStartedAt = startedAt;
3803
4707
  const flowCompletedAt = new Date(new Date(flowStartedAt).getTime() + durationMs).toISOString();
3804
4708
  const flowFile = testCase.classname || name;
3805
- const flowBase = (0, import_node_path13.basename)(flowFile, (0, import_node_path13.extname)(flowFile)).toLowerCase();
4709
+ const flowBase = (0, import_node_path16.basename)(flowFile, (0, import_node_path16.extname)(flowFile)).toLowerCase();
3806
4710
  const recordingPath = flowRecordingMap.get(flowBase) ?? null;
3807
4711
  const { commands: flowCmds, matchedAnyFile } = commandsForFlow(commandSteps, flowFile);
3808
4712
  const perFlowCommands = matchedAnyFile ? flowCmds.filter((c) => c.category !== "assertion") : allNonAssertions;
@@ -3863,7 +4767,20 @@ async function orchestrateReport(input) {
3863
4767
  });
3864
4768
  }
3865
4769
  }
3866
- const timeline = buildTimeline(flowResults);
4770
+ const apiCallsByFlow = (() => {
4771
+ if (!config.network?.enabled && !config.network?.harPath) return void 0;
4772
+ const windows = flowResults.map((f) => ({
4773
+ testId: `${f.flowFile}::${f.flowName}`,
4774
+ startedAt: f.startedAt,
4775
+ completedAt: f.completedAt
4776
+ }));
4777
+ const bound = collectNetworkByFlow(input.networkJsonlDir ?? null, config.network, windows);
4778
+ if (bound.size === 0) return void 0;
4779
+ const remapped = /* @__PURE__ */ new Map();
4780
+ for (const [k, v] of bound) remapped.set(k, reindexFlowCalls(v));
4781
+ return remapped;
4782
+ })();
4783
+ const timeline = buildTimeline(flowResults, apiCallsByFlow);
3867
4784
  const summary = buildSummary(timeline);
3868
4785
  const completedAt = (/* @__PURE__ */ new Date()).toISOString();
3869
4786
  const totalDuration = Date.now() - new Date(startedAt).getTime();
@@ -3879,12 +4796,12 @@ async function orchestrateReport(input) {
3879
4796
  timeline,
3880
4797
  shardRunIds: null
3881
4798
  };
3882
- (0, import_node_fs15.mkdirSync)((0, import_node_path13.dirname)((0, import_node_path13.resolve)(config.outputPath)), { recursive: true });
3883
- (0, import_node_fs15.writeFileSync)((0, import_node_path13.resolve)(config.outputPath), JSON.stringify(report, null, 2), "utf-8");
3884
- generateHtmlReport(report, allAiDefects, artifacts.screenshotPaths, (0, import_node_path13.resolve)(config.htmlReportPath));
4799
+ (0, import_node_fs18.mkdirSync)((0, import_node_path16.dirname)((0, import_node_path16.resolve)(config.outputPath)), { recursive: true });
4800
+ (0, import_node_fs18.writeFileSync)((0, import_node_path16.resolve)(config.outputPath), JSON.stringify(report, null, 2), "utf-8");
4801
+ generateHtmlReport(report, allAiDefects, artifacts.screenshotPaths, (0, import_node_path16.resolve)(config.htmlReportPath));
3885
4802
  printConsoleSummary(summary, config.outputPath, config.htmlReportPath, config.quiet, allAiDefects);
3886
4803
  if (config.openReport) {
3887
- openInBrowser((0, import_node_path13.resolve)(config.htmlReportPath));
4804
+ openInBrowser((0, import_node_path16.resolve)(config.htmlReportPath));
3888
4805
  }
3889
4806
  if (config.cloud) {
3890
4807
  const cloudClient = new CloudClient(config.cloud);
@@ -3893,7 +4810,7 @@ async function orchestrateReport(input) {
3893
4810
  const videoArtifacts = [];
3894
4811
  if (videoPathsToUpload.length > 0) {
3895
4812
  for (const flow of flowResults) {
3896
- const flowBase = (0, import_node_path13.basename)(flow.flowFile, (0, import_node_path13.extname)(flow.flowFile)).toLowerCase();
4813
+ const flowBase = (0, import_node_path16.basename)(flow.flowFile, (0, import_node_path16.extname)(flow.flowFile)).toLowerCase();
3897
4814
  const recordingPath = flowRecordingMap.get(flowBase) ?? null;
3898
4815
  const matched = matchVideoToFlow(flow.flowFile, videoPathsToUpload, recordingPath);
3899
4816
  if (matched) {
@@ -3915,7 +4832,10 @@ async function orchestrateReport(input) {
3915
4832
  }
3916
4833
  const matchedPaths = new Set(videoArtifacts.map((va) => va.path));
3917
4834
  const unmatchedVideos = videoPathsToUpload.filter((v) => !matchedPaths.has(v));
3918
- const testsForUpload = flowResults.map(flowToTestResult);
4835
+ const testsForUpload = flowResults.map((f) => {
4836
+ const testId = `${f.flowFile}::${f.flowName}`;
4837
+ return flowToTestResult(f, apiCallsByFlow?.get(testId));
4838
+ });
3919
4839
  await finalizeAndUpload(
3920
4840
  cloudClient,
3921
4841
  config.cloud,
@@ -3962,6 +4882,16 @@ Test Options:
3962
4882
  -e, --env <KEY=VALUE> Environment variable
3963
4883
  --quiet Suppress output
3964
4884
 
4885
+ Network Capture Options (mobile API logs):
4886
+ --capture-network Spawn mitmproxy to capture HTTP traffic from the app
4887
+ (requires mitmdump on PATH; --har-path is an alternative)
4888
+ --proxy-port <n> Proxy listen port (default: 8080)
4889
+ --proxy-host <host> Proxy listen host (default: 127.0.0.1)
4890
+ --har-path <file> Import a pre-recorded HAR file instead of running a proxy
4891
+ --skip-cert-install Skip auto-installing the mitmproxy CA on the device
4892
+ --proxy-include <pat> Only capture URLs matching this substring or regex (repeatable)
4893
+ --proxy-exclude <pat> Drop URLs matching this substring or regex (repeatable)
4894
+
3965
4895
  Report Options:
3966
4896
  --junit <path> Path to JUnit XML report
3967
4897
  --artifacts <dir> Path to Maestro test output directory
@@ -4001,6 +4931,15 @@ function parseArgs(args) {
4001
4931
  } else {
4002
4932
  options["env"] = args[i];
4003
4933
  }
4934
+ } else if (key === "proxy-include" || key === "proxy-exclude") {
4935
+ const existing = options[key];
4936
+ if (Array.isArray(existing)) {
4937
+ existing.push(args[i]);
4938
+ } else if (typeof existing === "string") {
4939
+ options[key] = [existing, args[i]];
4940
+ } else {
4941
+ options[key] = args[i];
4942
+ }
4004
4943
  } else {
4005
4944
  options[key] = args[i];
4006
4945
  }
@@ -4036,11 +4975,30 @@ function parseEnvVars(raw) {
4036
4975
  }
4037
4976
  return env;
4038
4977
  }
4978
+ function asStringArray(v) {
4979
+ if (!v) return [];
4980
+ return Array.isArray(v) ? v : [v];
4981
+ }
4982
+ function buildNetworkOptions(options) {
4983
+ const enabled = options["capture-network"] === "true";
4984
+ const harPath = typeof options["har-path"] === "string" ? options["har-path"] : void 0;
4985
+ if (!enabled && !harPath) return void 0;
4986
+ return {
4987
+ enabled,
4988
+ proxyPort: typeof options["proxy-port"] === "string" ? Number.parseInt(options["proxy-port"], 10) : void 0,
4989
+ proxyHost: typeof options["proxy-host"] === "string" ? options["proxy-host"] : void 0,
4990
+ harPath,
4991
+ skipCertInstall: options["skip-cert-install"] === "true",
4992
+ includeUrls: asStringArray(options["proxy-include"]),
4993
+ excludeUrls: asStringArray(options["proxy-exclude"])
4994
+ };
4995
+ }
4039
4996
  async function handleTest(options, positional) {
4040
4997
  if (positional.length === 0) {
4041
4998
  process.stderr.write("Error: No flow paths specified. Usage: testrelic-maestro test <flow-paths...>\n");
4042
4999
  process.exit(1);
4043
5000
  }
5001
+ const network = buildNetworkOptions(options);
4044
5002
  const testOptions = {
4045
5003
  flowPaths: positional,
4046
5004
  apiKey: typeof options["api-key"] === "string" ? options["api-key"] : void 0,
@@ -4054,12 +5012,10 @@ async function handleTest(options, positional) {
4054
5012
  shards: typeof options["shards"] === "string" ? parseInt(options["shards"], 10) : void 0,
4055
5013
  config: typeof options["config"] === "string" ? options["config"] : void 0,
4056
5014
  env: parseEnvVars(options["env"]),
4057
- quiet: options["quiet"] === "true"
5015
+ quiet: options["quiet"] === "true",
5016
+ network
4058
5017
  };
4059
5018
  process.stderr.write("\u2139 TestRelic: Running Maestro tests...\n");
4060
- const runResult = await runMaestro(testOptions);
4061
- process.stderr.write(`\u2139 TestRelic: Maestro exited with code ${runResult.exitCode}. Parsing results...
4062
- `);
4063
5019
  const config = resolveConfig({
4064
5020
  outputPath: typeof options["output-dir"] === "string" ? `${options["output-dir"]}/testrelic-maestro.json` : void 0,
4065
5021
  htmlReportPath: typeof options["html-report"] === "string" ? options["html-report"] : void 0,
@@ -4069,18 +5025,26 @@ async function handleTest(options, positional) {
4069
5025
  endpoint: typeof options["endpoint"] === "string" ? options["endpoint"] : void 0
4070
5026
  },
4071
5027
  quiet: options["quiet"] === "true",
4072
- flowsDir: positional[0]
5028
+ flowsDir: positional[0],
5029
+ network
4073
5030
  });
5031
+ const runResult = await runMaestro(testOptions, { network: config.network });
5032
+ process.stderr.write(`\u2139 TestRelic: Maestro exited with code ${runResult.exitCode}. Parsing results...
5033
+ `);
4074
5034
  await orchestrateReport({
4075
5035
  junitPath: runResult.junitPath,
4076
5036
  testOutputDir: runResult.testOutputDir,
4077
5037
  debugOutputDir: runResult.debugOutputDir,
4078
5038
  flowsDir: positional[0],
4079
- config
5039
+ config,
5040
+ platform: testOptions.platform ?? "unknown",
5041
+ device: testOptions.device,
5042
+ networkJsonlDir: runResult.networkJsonlDir
4080
5043
  });
4081
5044
  process.exit(runResult.exitCode);
4082
5045
  }
4083
5046
  async function handleReport(options) {
5047
+ const network = buildNetworkOptions(options);
4084
5048
  const config = resolveConfig({
4085
5049
  outputPath: typeof options["output-dir"] === "string" ? `${options["output-dir"]}/testrelic-maestro.json` : void 0,
4086
5050
  htmlReportPath: typeof options["html-report"] === "string" ? options["html-report"] : void 0,
@@ -4090,14 +5054,16 @@ async function handleReport(options) {
4090
5054
  endpoint: typeof options["endpoint"] === "string" ? options["endpoint"] : void 0
4091
5055
  },
4092
5056
  quiet: options["quiet"] === "true",
4093
- flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0
5057
+ flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0,
5058
+ network
4094
5059
  });
4095
5060
  process.stderr.write("\u2139 TestRelic: Parsing Maestro artifacts...\n");
4096
5061
  await orchestrateReport({
4097
5062
  junitPath: typeof options["junit"] === "string" ? options["junit"] : void 0,
4098
5063
  testOutputDir: typeof options["artifacts"] === "string" ? options["artifacts"] : void 0,
4099
5064
  flowsDir: typeof options["flows-dir"] === "string" ? options["flows-dir"] : void 0,
4100
- config
5065
+ config,
5066
+ networkJsonlDir: typeof options["network-dir"] === "string" ? options["network-dir"] : null
4101
5067
  });
4102
5068
  }
4103
5069
  async function main() {