mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-20 05:19:28 +01:00
Fix health metrics race condition and improve expected-failure handling (#2387)
* Fix health monitoring * fix compile error
This commit is contained in:
@@ -120,7 +120,7 @@ const App = (): JSX.Element => {
|
|||||||
}, [explorer]);
|
}, [explorer]);
|
||||||
|
|
||||||
// Track interactive phase for both ContainerCopyPanel and DivExplorer paths
|
// Track interactive phase for both ContainerCopyPanel and DivExplorer paths
|
||||||
useInteractive(MetricScenario.ApplicationLoad);
|
useInteractive(MetricScenario.ApplicationLoad, !!config);
|
||||||
|
|
||||||
if (!explorer) {
|
if (!explorer) {
|
||||||
return <LoadingExplorer />;
|
return <LoadingExplorer />;
|
||||||
|
|||||||
@@ -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) {
|
start(scenario: MetricScenario) {
|
||||||
if (this.contexts.has(scenario)) {
|
if (this.contexts.has(scenario)) {
|
||||||
return;
|
return;
|
||||||
@@ -86,6 +93,10 @@ class ScenarioMonitor {
|
|||||||
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
|
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.devLog(
|
||||||
|
`scenario_start: ${scenario} | phases=${config.requiredPhases.join(", ")} | timeout=${config.timeoutMs}ms`,
|
||||||
|
);
|
||||||
|
|
||||||
traceMark(Action.MetricsScenario, {
|
traceMark(Action.MetricsScenario, {
|
||||||
event: "scenario_start",
|
event: "scenario_start",
|
||||||
scenario,
|
scenario,
|
||||||
@@ -136,6 +147,12 @@ class ScenarioMonitor {
|
|||||||
const endTimeISO = endEntry ? new Date(navigationStart + endEntry.startTime).toISOString() : undefined;
|
const endTimeISO = endEntry ? new Date(navigationStart + endEntry.startTime).toISOString() : undefined;
|
||||||
const durationMs = startEntry && endEntry ? endEntry.startTime - startEntry.startTime : 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, {
|
traceSuccess(Action.MetricsScenario, {
|
||||||
event: "phase_complete",
|
event: "phase_complete",
|
||||||
scenario,
|
scenario,
|
||||||
@@ -155,6 +172,13 @@ class ScenarioMonitor {
|
|||||||
return;
|
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
|
// Mark the explicitly failed phase
|
||||||
performance.mark(`scenario_${scenario}_${phase}_failed`);
|
performance.mark(`scenario_${scenario}_${phase}_failed`);
|
||||||
ctx.failed.add(phase);
|
ctx.failed.add(phase);
|
||||||
@@ -169,6 +193,12 @@ class ScenarioMonitor {
|
|||||||
// Build a snapshot with failure info
|
// Build a snapshot with failure info
|
||||||
const failureSnapshot = this.buildSnapshot(ctx, { final: false, timedOut: false });
|
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, {
|
traceFailure(Action.MetricsScenario, {
|
||||||
event: "phase_fail",
|
event: "phase_fail",
|
||||||
scenario,
|
scenario,
|
||||||
@@ -177,7 +207,7 @@ class ScenarioMonitor {
|
|||||||
completedPhases: Array.from(ctx.completed).join(","),
|
completedPhases: Array.from(ctx.completed).join(","),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Emit unhealthy immediately
|
// Emit unhealthy immediately for unexpected failures
|
||||||
this.emit(ctx, false, false, failureSnapshot);
|
this.emit(ctx, false, false, failureSnapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -270,6 +300,19 @@ class ScenarioMonitor {
|
|||||||
ttfb: finalSnapshot.vitals?.ttfb,
|
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
|
// Call portal backend health metrics endpoint
|
||||||
// If healthy is true (either completed successfully or timeout with expected failure), report healthy
|
// If healthy is true (either completed successfully or timeout with expected failure), report healthy
|
||||||
if (healthy) {
|
if (healthy) {
|
||||||
|
|||||||
@@ -7,14 +7,18 @@ import { ApplicationMetricPhase, CommonMetricPhase } from "./ScenarioConfig";
|
|||||||
* Hook to automatically complete the Interactive phase when the component becomes interactive.
|
* Hook to automatically complete the Interactive phase when the component becomes interactive.
|
||||||
* Uses requestAnimationFrame to complete after the browser has painted.
|
* Uses requestAnimationFrame to complete after the browser has painted.
|
||||||
*/
|
*/
|
||||||
export function useInteractive(scenario: MetricScenario) {
|
export function useInteractive(scenario: MetricScenario, enabled = true) {
|
||||||
const { completePhase } = useMetricScenario();
|
const { completePhase } = useMetricScenario();
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
requestAnimationFrame(() => {
|
if (!enabled) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const id = requestAnimationFrame(() => {
|
||||||
completePhase(scenario, CommonMetricPhase.Interactive);
|
completePhase(scenario, CommonMetricPhase.Interactive);
|
||||||
});
|
});
|
||||||
}, [scenario, completePhase]);
|
return () => cancelAnimationFrame(id);
|
||||||
|
}, [scenario, completePhase, enabled]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user