@testrelic/maestro-analytics 1.0.1 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.cjs CHANGED
@@ -1040,7 +1040,7 @@ function mapCommandCategory(category) {
1040
1040
  case "ai":
1041
1041
  return "assertion";
1042
1042
  case "navigation":
1043
- return "custom_step";
1043
+ return "navigation";
1044
1044
  case "device":
1045
1045
  return "custom_step";
1046
1046
  case "media":
@@ -3669,6 +3669,31 @@ function buildFlowRecordingMap(commandSteps) {
3669
3669
  }
3670
3670
  return map;
3671
3671
  }
3672
+ function commandsForFlow(commandSteps, flowFile) {
3673
+ const flowBase = (0, import_node_path13.basename)(flowFile, (0, import_node_path13.extname)(flowFile)).toLowerCase();
3674
+ if (!flowBase || commandSteps.size === 0) {
3675
+ return { commands: [], matchedAnyFile: false };
3676
+ }
3677
+ const stripCmdPrefix = (p) => (0, import_node_path13.basename)(p, (0, import_node_path13.extname)(p)).replace(/^commands[-_]?/i, "").toLowerCase();
3678
+ for (const [cmdFile, cmds] of commandSteps) {
3679
+ if (stripCmdPrefix(cmdFile) === flowBase) {
3680
+ return { commands: cmds, matchedAnyFile: true };
3681
+ }
3682
+ }
3683
+ for (const [cmdFile, cmds] of commandSteps) {
3684
+ const stripped = stripCmdPrefix(cmdFile);
3685
+ if (stripped.length >= 3 && stripped.includes(flowBase)) {
3686
+ return { commands: cmds, matchedAnyFile: true };
3687
+ }
3688
+ }
3689
+ for (const [cmdFile, cmds] of commandSteps) {
3690
+ const stripped = stripCmdPrefix(cmdFile);
3691
+ if (stripped.length >= 3 && flowBase.includes(stripped)) {
3692
+ return { commands: cmds, matchedAnyFile: true };
3693
+ }
3694
+ }
3695
+ return { commands: [], matchedAnyFile: false };
3696
+ }
3672
3697
  function platformToOs(platform) {
3673
3698
  switch (platform) {
3674
3699
  case "android":
@@ -3682,6 +3707,12 @@ function platformToOs(platform) {
3682
3707
  }
3683
3708
  }
3684
3709
  function flowToTestResult(flow) {
3710
+ const consoleLogs = flow.logEntries.length > 0 ? flow.logEntries.map((entry) => ({
3711
+ level: entry.level.toLowerCase(),
3712
+ message: entry.message,
3713
+ timestamp: entry.timestamp,
3714
+ source: entry.source
3715
+ })) : void 0;
3685
3716
  return {
3686
3717
  testId: `${flow.flowFile}::${flow.flowName}`,
3687
3718
  title: flow.flowName,
@@ -3701,26 +3732,39 @@ function flowToTestResult(flow) {
3701
3732
  source: "maestro",
3702
3733
  platform: flow.platform !== "unknown" ? flow.platform : void 0,
3703
3734
  os: platformToOs(flow.platform),
3704
- deviceName: flow.deviceId
3735
+ deviceName: flow.deviceId,
3736
+ ...consoleLogs ? { consoleLogs } : {}
3705
3737
  };
3706
3738
  }
3739
+ function logEntriesForFlow(allEntries, flowStartedAt, flowCompletedAt) {
3740
+ if (allEntries.length === 0) return [];
3741
+ const startMs = Date.parse(flowStartedAt);
3742
+ const endMs = Date.parse(flowCompletedAt);
3743
+ if (Number.isNaN(startMs) || Number.isNaN(endMs)) {
3744
+ return allEntries.slice();
3745
+ }
3746
+ return allEntries.filter((entry) => {
3747
+ const t = Date.parse(entry.timestamp);
3748
+ if (Number.isNaN(t)) return true;
3749
+ return t >= startMs && t <= endMs;
3750
+ });
3751
+ }
3707
3752
  async function orchestrateReport(input) {
3708
3753
  const { config } = input;
3709
3754
  const startedAt = (/* @__PURE__ */ new Date()).toISOString();
3710
3755
  const testRunId = config.testRunId ?? (0, import_node_crypto3.randomUUID)();
3711
3756
  const artifacts = collectArtifacts(input.testOutputDir, input.debugOutputDir);
3712
3757
  const junitPath = input.junitPath ?? artifacts.junitReportPath;
3758
+ const logFiles = input.debugOutputDir ? discoverLogFiles(input.debugOutputDir) : artifacts.logPaths;
3759
+ const allLogEntries = [];
3760
+ for (const logFile of logFiles) {
3761
+ const entries = parseLogFile(logFile);
3762
+ if (entries.length > 0) allLogEntries.push(...entries);
3763
+ }
3713
3764
  let platform = input.platform ?? "unknown";
3714
- if (platform === "unknown") {
3715
- const logFiles = input.debugOutputDir ? discoverLogFiles(input.debugOutputDir) : artifacts.logPaths;
3716
- for (const logFile of logFiles) {
3717
- const logEntries = parseLogFile(logFile);
3718
- const detected = detectPlatformFromLogs(logEntries);
3719
- if (detected !== "unknown") {
3720
- platform = detected;
3721
- break;
3722
- }
3723
- }
3765
+ if (platform === "unknown" && allLogEntries.length > 0) {
3766
+ const detected = detectPlatformFromLogs(allLogEntries);
3767
+ if (detected !== "unknown") platform = detected;
3724
3768
  }
3725
3769
  const flowMetadataMap = /* @__PURE__ */ new Map();
3726
3770
  if (input.flowsDir && (0, import_node_fs15.existsSync)(input.flowsDir)) {
@@ -3747,8 +3791,8 @@ async function orchestrateReport(input) {
3747
3791
  if (junitPath && (0, import_node_fs15.existsSync)(junitPath)) {
3748
3792
  const junit = parseJUnitFile(junitPath);
3749
3793
  const allCommands = Array.from(commandSteps.values()).flat();
3750
- const assertionCommands = allCommands.filter((c) => c.category === "assertion");
3751
- const nonAssertionCommands = allCommands.filter((c) => c.category !== "assertion");
3794
+ const allAssertions = allCommands.filter((c) => c.category === "assertion");
3795
+ const allNonAssertions = allCommands.filter((c) => c.category !== "assertion");
3752
3796
  for (const suite of junit.testSuites) {
3753
3797
  for (const testCase of suite.testCases) {
3754
3798
  const name = testCase.name;
@@ -3760,6 +3804,9 @@ async function orchestrateReport(input) {
3760
3804
  const flowFile = testCase.classname || name;
3761
3805
  const flowBase = (0, import_node_path13.basename)(flowFile, (0, import_node_path13.extname)(flowFile)).toLowerCase();
3762
3806
  const recordingPath = flowRecordingMap.get(flowBase) ?? null;
3807
+ const { commands: flowCmds, matchedAnyFile } = commandsForFlow(commandSteps, flowFile);
3808
+ const perFlowCommands = matchedAnyFile ? flowCmds.filter((c) => c.category !== "assertion") : allNonAssertions;
3809
+ const perFlowAssertions = matchedAnyFile ? flowCmds.filter((c) => c.category === "assertion") : allAssertions;
3763
3810
  flowResults.push({
3764
3811
  flowName: name,
3765
3812
  flowFile,
@@ -3772,12 +3819,12 @@ async function orchestrateReport(input) {
3772
3819
  completedAt: flowCompletedAt,
3773
3820
  tags: meta?.tags ?? [],
3774
3821
  properties: meta?.properties ?? {},
3775
- commands: nonAssertionCommands,
3776
- assertions: assertionCommands,
3822
+ commands: perFlowCommands,
3823
+ assertions: perFlowAssertions,
3777
3824
  screenshotPaths: matchScreenshotsToFlow(flowFile, artifacts.screenshotPaths),
3778
3825
  videoPath: matchVideoToFlow(flowFile, artifacts.videoPaths, recordingPath),
3779
3826
  aiDefects: allAiDefects,
3780
- logEntries: [],
3827
+ logEntries: logEntriesForFlow(allLogEntries, flowStartedAt, flowCompletedAt),
3781
3828
  failureMessage: testCase.failureMessage ?? testCase.errorMessage ?? null,
3782
3829
  failureType: testCase.failureType ?? testCase.errorType ?? null,
3783
3830
  subflowRefs: meta?.subflowRefs ?? [],
@@ -3805,7 +3852,10 @@ async function orchestrateReport(input) {
3805
3852
  screenshotPaths: [],
3806
3853
  videoPath: null,
3807
3854
  aiDefects: [],
3808
- logEntries: [],
3855
+ // No JUnit window to bound by; if maestro.log was parsed, surface
3856
+ // every entry so the platform's Logs tab is still useful for
3857
+ // pre-test setup / runtime failures.
3858
+ logEntries: allLogEntries.slice(),
3809
3859
  failureMessage: null,
3810
3860
  failureType: null,
3811
3861
  subflowRefs: meta.subflowRefs,