diff --git a/src/Main.tsx b/src/Main.tsx index c279d6e0d..43e1a90f6 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -120,7 +120,7 @@ const App = (): JSX.Element => { }, [explorer]); // Track interactive phase for both ContainerCopyPanel and DivExplorer paths - useInteractive(MetricScenario.ApplicationLoad); + useInteractive(MetricScenario.ApplicationLoad, !!config); if (!explorer) { return ; diff --git a/src/Metrics/ScenarioMonitor.ts b/src/Metrics/ScenarioMonitor.ts index 4c4fa88c2..f4fc2849a 100644 --- a/src/Metrics/ScenarioMonitor.ts +++ b/src/Metrics/ScenarioMonitor.ts @@ -56,6 +56,13 @@ class ScenarioMonitor { }); } + private devLog(msg: string) { + if (process.env.NODE_ENV === "development") { + // eslint-disable-next-line no-console + console.log(`[Metrics] ${msg}`); + } + } + start(scenario: MetricScenario) { if (this.contexts.has(scenario)) { return; @@ -86,6 +93,10 @@ class ScenarioMonitor { ctx.phases.set(phase, { startMarkName: phaseStartMarkName }); }); + this.devLog( + `scenario_start: ${scenario} | phases=${config.requiredPhases.join(", ")} | timeout=${config.timeoutMs}ms`, + ); + traceMark(Action.MetricsScenario, { event: "scenario_start", scenario, @@ -136,6 +147,12 @@ class ScenarioMonitor { const endTimeISO = endEntry ? new Date(navigationStart + endEntry.startTime).toISOString() : undefined; const durationMs = startEntry && endEntry ? endEntry.startTime - startEntry.startTime : undefined; + this.devLog( + `phase_complete: ${scenario}.${phase} | ${ + durationMs !== null && durationMs !== undefined ? `${Math.round(durationMs)}ms` : "?" + } | ${ctx.completed.size}/${ctx.config.requiredPhases.length} phases`, + ); + traceSuccess(Action.MetricsScenario, { event: "phase_complete", scenario, @@ -155,6 +172,13 @@ class ScenarioMonitor { return; } + // If an expected failure was flagged (auth, firewall, etc.), treat as success. + if (ctx.hasExpectedFailure) { + this.devLog(`phase_fail: ${scenario}.${phase} — expected failure, completing as healthy`); + this.completePhase(scenario, phase); + return; + } + // Mark the explicitly failed phase performance.mark(`scenario_${scenario}_${phase}_failed`); ctx.failed.add(phase); @@ -169,6 +193,12 @@ class ScenarioMonitor { // Build a snapshot with failure info const failureSnapshot = this.buildSnapshot(ctx, { final: false, timedOut: false }); + this.devLog( + `phase_fail: ${scenario}.${phase} | failed=[${Array.from(ctx.failed).join(", ")}] | completed=[${Array.from( + ctx.completed, + ).join(", ")}]`, + ); + traceFailure(Action.MetricsScenario, { event: "phase_fail", scenario, @@ -177,7 +207,7 @@ class ScenarioMonitor { completedPhases: Array.from(ctx.completed).join(","), }); - // Emit unhealthy immediately + // Emit unhealthy immediately for unexpected failures this.emit(ctx, false, false, failureSnapshot); } @@ -270,6 +300,19 @@ class ScenarioMonitor { ttfb: finalSnapshot.vitals?.ttfb, }); + this.devLog( + `scenario_end: ${ctx.scenario} | ${healthy ? "healthy" : "unhealthy"} | ${ + timedOut ? "timed out" : `${Math.round(finalSnapshot.durationMs)}ms` + } | ${JSON.stringify({ + completedPhases: finalSnapshot.completed.join(", "), + failedPhases: finalSnapshot.failedPhases?.join(", ") || "none", + platform, + api, + phaseTimings: finalSnapshot.phaseTimings, + vitals: finalSnapshot.vitals, + })}`, + ); + // Call portal backend health metrics endpoint // If healthy is true (either completed successfully or timeout with expected failure), report healthy if (healthy) { diff --git a/src/Metrics/useMetricPhases.ts b/src/Metrics/useMetricPhases.ts index b244c2966..31c07a98d 100644 --- a/src/Metrics/useMetricPhases.ts +++ b/src/Metrics/useMetricPhases.ts @@ -7,14 +7,18 @@ import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig"; * Hook to automatically complete the Interactive phase when the component becomes interactive. * Uses requestAnimationFrame to complete after the browser has painted. */ -export function useInteractive(scenario: MetricScenario) { +export function useInteractive(scenario: MetricScenario, enabled = true) { const { completePhase } = useMetricScenario(); React.useEffect(() => { - requestAnimationFrame(() => { + if (!enabled) { + return undefined; + } + const id = requestAnimationFrame(() => { completePhase(scenario, CommonMetricPhase.Interactive); }); - }, [scenario, completePhase]); + return () => cancelAnimationFrame(id); + }, [scenario, completePhase, enabled]); } /**