mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-02-25 11:55:27 +00:00
Compare commits
4 Commits
users/saks
...
metrics_fi
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddd757b3a0 | ||
|
|
1d0914e1f9 | ||
|
|
cc26e2800e | ||
|
|
39ac7cf3f2 |
@@ -6,7 +6,7 @@ import { MinimalQueryIterator, nextPage } from "../IteratorUtilities";
|
||||
|
||||
// Redact sensitive information from BadRequest errors with specific codes
|
||||
export const redactSyntaxErrorMessage = (error: unknown): unknown => {
|
||||
const codesToRedact = ["SC1001", "SC2001"];
|
||||
const codesToRedact = ["SC1001", "SC2001", "SC1010"];
|
||||
|
||||
try {
|
||||
// Handle error objects with a message property
|
||||
|
||||
@@ -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 <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) {
|
||||
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,
|
||||
@@ -94,6 +105,23 @@ class ScenarioMonitor {
|
||||
});
|
||||
|
||||
ctx.timeoutId = window.setTimeout(() => {
|
||||
const missingPhases = ctx.config.requiredPhases.filter((p) => !ctx.completed.has(p));
|
||||
|
||||
this.devLog(
|
||||
`timeout: ${scenario} | missing=[${missingPhases.join(", ")}] | completed=[${Array.from(ctx.completed).join(
|
||||
", ",
|
||||
)}] | documentHidden=${document.hidden} | hasExpectedFailure=${ctx.hasExpectedFailure}`,
|
||||
);
|
||||
|
||||
traceMark(Action.MetricsScenario, {
|
||||
event: "scenario_timeout",
|
||||
scenario,
|
||||
missingPhases: missingPhases.join(","),
|
||||
completedPhases: Array.from(ctx.completed).join(","),
|
||||
documentHidden: document.hidden,
|
||||
hasExpectedFailure: ctx.hasExpectedFailure,
|
||||
});
|
||||
|
||||
// If an expected failure occurred (auth, firewall, etc.), emit healthy instead of unhealthy
|
||||
const healthy = ctx.hasExpectedFailure;
|
||||
this.emit(ctx, healthy, true);
|
||||
@@ -136,6 +164,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 +189,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 +210,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 +224,7 @@ class ScenarioMonitor {
|
||||
completedPhases: Array.from(ctx.completed).join(","),
|
||||
});
|
||||
|
||||
// Emit unhealthy immediately
|
||||
// Emit unhealthy immediately for unexpected failures
|
||||
this.emit(ctx, false, false, failureSnapshot);
|
||||
}
|
||||
|
||||
@@ -258,6 +305,7 @@ class ScenarioMonitor {
|
||||
scenario: ctx.scenario,
|
||||
healthy,
|
||||
timedOut,
|
||||
documentHidden: document.hidden,
|
||||
platform,
|
||||
api,
|
||||
durationMs: finalSnapshot.durationMs,
|
||||
@@ -270,6 +318,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) {
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
import React from "react";
|
||||
import MetricScenario from "./MetricEvents";
|
||||
import { useMetricScenario } from "./MetricScenarioProvider";
|
||||
import { scenarioMonitor } from "./ScenarioMonitor";
|
||||
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.
|
||||
*
|
||||
* Calls scenarioMonitor directly (not via React context) so that the effect dependencies
|
||||
* are only [scenario, enabled] — both stable primitives. This prevents re-renders from
|
||||
* cancelling the pending rAF due to an unstable context function reference.
|
||||
*/
|
||||
export function useInteractive(scenario: MetricScenario) {
|
||||
const { completePhase } = useMetricScenario();
|
||||
|
||||
export function useInteractive(scenario: MetricScenario, enabled = true) {
|
||||
React.useEffect(() => {
|
||||
requestAnimationFrame(() => {
|
||||
completePhase(scenario, CommonMetricPhase.Interactive);
|
||||
if (!enabled) {
|
||||
return undefined;
|
||||
}
|
||||
const id = requestAnimationFrame(() => {
|
||||
scenarioMonitor.completePhase(scenario, CommonMetricPhase.Interactive);
|
||||
});
|
||||
}, [scenario, completePhase]);
|
||||
return () => cancelAnimationFrame(id);
|
||||
}, [scenario, enabled]);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -22,18 +28,20 @@ export function useInteractive(scenario: MetricScenario) {
|
||||
* Tracks tree rendering and completes Interactive phase.
|
||||
* Only completes DatabaseTreeRendered if the database fetch was successful.
|
||||
* Note: Scenario must be started before databases are fetched (in refreshExplorer).
|
||||
*
|
||||
* Calls scenarioMonitor directly (not via React context) for the same stability reason
|
||||
* as useInteractive — avoids effect re-runs from unstable context function references.
|
||||
*/
|
||||
export function useDatabaseLoadScenario(databaseTreeNodes: unknown[], fetchSucceeded: boolean) {
|
||||
const { completePhase } = useMetricScenario();
|
||||
const hasCompletedTreeRenderRef = React.useRef(false);
|
||||
|
||||
// Track DatabaseTreeRendered phase (only if fetch succeeded)
|
||||
React.useEffect(() => {
|
||||
if (!hasCompletedTreeRenderRef.current && fetchSucceeded) {
|
||||
hasCompletedTreeRenderRef.current = true;
|
||||
completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
|
||||
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
|
||||
}
|
||||
}, [databaseTreeNodes, fetchSucceeded, completePhase]);
|
||||
}, [databaseTreeNodes, fetchSucceeded]);
|
||||
|
||||
// Track Interactive phase
|
||||
useInteractive(MetricScenario.DatabaseLoad);
|
||||
|
||||
Reference in New Issue
Block a user