Compare commits

..

3 Commits

Author SHA1 Message Date
Laurent Nguyen
5832170b2b Fix extractHeaderStatus to handle undefined and non-string messages (#2393)
* Fix extractHeaderStatus to handle undefined and non-string messages

* Fix unsafe casts

---------

Co-authored-by: Laurent Nguyen <languye@microsoft.com>
2026-02-20 16:42:17 +01:00
sunghyunkang1111
cc26e2800e Fix health metrics race condition and improve expected-failure handling (#2387)
* Fix health monitoring

* fix compile error
2026-02-17 12:54:04 -06:00
Laurent Nguyen
39ac7cf3f2 Add SC1010 to codes redacted in syntax error messages (#2388) 2026-02-16 18:34:54 +01:00
11 changed files with 70 additions and 86 deletions

View File

@@ -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

View File

@@ -113,7 +113,7 @@ export class ContainerSampleGenerator {
? await createMongoDocument(collection.databaseId, collection, shardKey, doc)
: await createDocument(collection, doc);
} catch (error) {
NotificationConsoleUtils.logConsoleError(error);
NotificationConsoleUtils.logConsoleError(error instanceof Error ? error.message : String(error));
}
}),
);

View File

@@ -17,18 +17,12 @@ import SynapseIcon from "../../../../images/synapse-link.svg";
import VSCodeIcon from "../../../../images/vscode.svg";
import { AuthType } from "../../../AuthType";
import * as Constants from "../../../Common/Constants";
import { configContext, Platform } from "../../../ConfigContext";
import { Platform, configContext } from "../../../ConfigContext";
import * as ViewModels from "../../../Contracts/ViewModels";
import {
isVCoreMongoNativeAuthDisabled,
userContext,
VCoreMongoNativeAuthDisabledMessage,
VCoreMongoNativeAuthLearnMoreUrl,
} from "../../../UserContext";
import { userContext } from "../../../UserContext";
import { isRunningOnNationalCloud } from "../../../Utils/CloudUtils";
import { useSidePanel } from "../../../hooks/useSidePanel";
import { CommandButtonComponentProps } from "../../Controls/CommandButton/CommandButtonComponent";
import { useDialog } from "../../Controls/Dialog";
import Explorer from "../../Explorer";
import { useNotebook } from "../../Notebook/useNotebook";
import { BrowseQueriesPane } from "../../Panes/BrowseQueriesPane/BrowseQueriesPane";
@@ -514,21 +508,12 @@ function createOpenTerminalButtonByKind(
const label = `Open ${terminalFriendlyName()} shell`;
const tooltip =
"This feature is not yet available in your account's region. View supported regions here: https://aka.ms/cosmos-enable-notebooks.";
const isNativeAuthDisabled = terminalKind === ViewModels.TerminalKind.VCoreMongo && isVCoreMongoNativeAuthDisabled();
const disableButton =
(!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled) ||
isNativeAuthDisabled;
!useNotebook.getState().isNotebooksEnabledForAccount && !useNotebook.getState().isNotebookEnabled;
return {
iconSrc: HostedTerminalIcon,
iconAlt: label,
onCommandClick: () => {
if (isNativeAuthDisabled) {
useDialog.getState().showOkModalDialog("Native Authentication Disabled", VCoreMongoNativeAuthDisabledMessage, {
linkText: "Learn more",
linkUrl: VCoreMongoNativeAuthLearnMoreUrl,
});
return;
}
if (useNotebook.getState().isNotebookEnabled || userContext.features.enableCloudShell) {
container.openNotebookTerminal(terminalKind);
}
@@ -537,7 +522,7 @@ function createOpenTerminalButtonByKind(
hasPopup: false,
disabled: disableButton,
ariaLabel: label,
tooltipText: isNativeAuthDisabled ? VCoreMongoNativeAuthDisabledMessage : !disableButton ? "" : tooltip,
tooltipText: !disableButton ? "" : tooltip,
};
}

View File

@@ -329,7 +329,10 @@ export class NotificationConsoleComponent extends React.Component<
}
private static extractHeaderStatus(consoleData: ConsoleData) {
return consoleData?.message.split(":\n")[0];
if (!consoleData?.message || typeof consoleData.message !== "string") {
return undefined;
}
return consoleData.message.split(":\n")[0];
}
private onConsoleWasExpanded = (): void => {

View File

@@ -34,14 +34,8 @@ import VisualStudioIcon from "../../../images/VisualStudio.svg";
import NotebookIcon from "../../../images/notebook/Notebook-resource.svg";
import CollectionIcon from "../../../images/tree-collection.svg";
import * as Constants from "../../Common/Constants";
import {
isVCoreMongoNativeAuthDisabled,
userContext,
VCoreMongoNativeAuthDisabledMessage,
VCoreMongoNativeAuthLearnMoreUrl,
} from "../../UserContext";
import { userContext } from "../../UserContext";
import { getCollectionName } from "../../Utils/APITypeUtils";
import { useDialog } from "../Controls/Dialog";
import Explorer from "../Explorer";
import * as MostRecentActivity from "../MostRecentActivity/MostRecentActivity";
import { useNotebook } from "../Notebook/useNotebook";
@@ -429,23 +423,11 @@ export const SplashScreen: React.FC<SplashScreenProps> = ({ explorer }) => {
}
if (userContext.apiType === "VCoreMongo") {
const isNativeAuthDisabled = isVCoreMongoNativeAuthDisabled();
return {
iconSrc: PowerShellIcon,
title: "Mongo Shell",
description: "Create a collection and interact with data using MongoDB's shell interface",
onClick: () => {
if (isNativeAuthDisabled) {
useDialog
.getState()
.showOkModalDialog("Native Authentication Disabled", VCoreMongoNativeAuthDisabledMessage, {
linkText: "Learn more",
linkUrl: VCoreMongoNativeAuthLearnMoreUrl,
});
} else {
container.openNotebookTerminal(TerminalKind.VCoreMongo);
}
},
onClick: () => container.openNotebookTerminal(TerminalKind.VCoreMongo),
};
}

View File

@@ -1,4 +1,4 @@
import { Link, MessageBar, MessageBarType, Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { Spinner, SpinnerSize, Stack, Text } from "@fluentui/react";
import { PoolIdType } from "Common/Constants";
import { NotebookWorkspaceConnectionInfo } from "Contracts/DataModels";
import { MessageTypes } from "Contracts/ExplorerContracts";
@@ -8,12 +8,7 @@ import { useNotebook } from "Explorer/Notebook/useNotebook";
import { QuickstartFirewallNotification } from "Explorer/Quickstart/QuickstartFirewallNotification";
import { VcoreMongoQuickstartGuide } from "Explorer/Quickstart/VCoreMongoQuickstartGuide";
import { checkFirewallRules } from "Explorer/Tabs/Shared/CheckFirewallRules";
import {
isVCoreMongoNativeAuthDisabled,
userContext,
VCoreMongoNativeAuthDisabledMessage,
VCoreMongoNativeAuthLearnMoreUrl,
} from "UserContext";
import { userContext } from "UserContext";
import React, { useEffect, useState } from "react";
import FirewallRuleScreenshot from "../../../images/vcoreMongoFirewallRule.png";
@@ -26,7 +21,6 @@ export const VcoreMongoQuickstartTab: React.FC<VCoreMongoQuickstartTabProps> = (
}: VCoreMongoQuickstartTabProps): JSX.Element => {
const notebookServerInfo = useNotebook((state) => state.notebookServerInfo);
const [isAllPublicIPAddressEnabled, setIsAllPublicIPAddressEnabled] = useState<boolean>(true);
const isNativeAuthDisabled = isVCoreMongoNativeAuthDisabled();
const getNotebookServerInfo = (): NotebookWorkspaceConnectionInfo => ({
authToken: notebookServerInfo.authToken,
@@ -54,26 +48,14 @@ export const VcoreMongoQuickstartTab: React.FC<VCoreMongoQuickstartTabProps> = (
<VcoreMongoQuickstartGuide />
</Stack>
<Stack style={{ width: "50%", borderLeft: "black solid 1px" }}>
{isNativeAuthDisabled && (
<Stack style={{ margin: "auto", padding: 20 }}>
<MessageBar messageBarType={MessageBarType.warning} isMultiline={true}>
<Text>
{VCoreMongoNativeAuthDisabledMessage}{" "}
<Link href={VCoreMongoNativeAuthLearnMoreUrl} target="_blank">
Learn more
</Link>
</Text>
</MessageBar>
</Stack>
)}
{!isNativeAuthDisabled && !isAllPublicIPAddressEnabled && (
{!isAllPublicIPAddressEnabled && (
<QuickstartFirewallNotification
messageType={MessageTypes.OpenVCoreMongoNetworkingBlade}
screenshot={FirewallRuleScreenshot}
shellName="MongoDB"
/>
)}
{!isNativeAuthDisabled && isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
{isAllPublicIPAddressEnabled && notebookServerInfo?.notebookServerEndpoint && (
<NotebookTerminalComponent
notebookServerInfo={getNotebookServerInfo()}
databaseAccount={userContext.databaseAccount}
@@ -81,7 +63,7 @@ export const VcoreMongoQuickstartTab: React.FC<VCoreMongoQuickstartTabProps> = (
username={userContext.vcoreMongoConnectionParams.adminLogin}
/>
)}
{!isNativeAuthDisabled && isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
{isAllPublicIPAddressEnabled && !notebookServerInfo?.notebookServerEndpoint && (
<Stack style={{ margin: "auto 0" }}>
<Text block style={{ margin: "auto" }}>
Connecting to the Mongo shell.

View File

@@ -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 />;

View File

@@ -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) {

View File

@@ -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]);
}
/**

View File

@@ -37,7 +37,7 @@ const requestFabricToken = async (): Promise<void> => {
scheduleRefreshFabricToken();
} catch (error) {
logConsoleError(error as string);
logConsoleError(error instanceof Error ? error.message : String(error));
throw error;
} finally {
lastRequestTimestamp = undefined;

View File

@@ -42,25 +42,10 @@ export interface PostgresConnectionStrParams {
isMarlinServerGroup: boolean;
isFreeTier: boolean;
}
export type VCoreMongoAuthMode = "NativeAuth" | "MicrosoftEntraID";
export interface VCoreMongoAuthConfig {
allowedModes?: VCoreMongoAuthMode[];
}
export interface VCoreMongoConnectionParams {
adminLogin: string;
connectionString: string;
authConfig?: VCoreMongoAuthConfig;
}
export const VCoreMongoNativeAuthLearnMoreUrl = "https://go.microsoft.com/fwlink/?linkid=2340100";
export const VCoreMongoNativeAuthDisabledMessage =
"Native DocumentDB authentication is disabled on this cluster. You can use MongoDB Shell with Entra ID authentication outside of the Azure portal.";
export function isVCoreMongoNativeAuthDisabled(): boolean {
const allowedModes = userContext.vcoreMongoConnectionParams?.authConfig?.allowedModes || [];
return allowedModes.length > 0 && !allowedModes.includes("NativeAuth");
}
export interface FabricArtifactInfo {