Frontend performance metrics (#2439)

* Added enriched metrics

* Add more traces for observability
This commit is contained in:
sunghyunkang1111
2026-04-01 09:42:32 -05:00
committed by GitHub
parent eac5842176
commit 2ba58cd1a5
14 changed files with 363 additions and 103 deletions

View File

@@ -5,6 +5,8 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraTables } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
@@ -17,6 +19,11 @@ import { handleError } from "../ErrorHandlingUtils";
export async function readCollections(databaseId: string): Promise<DataModels.Collection[]> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
const startKey = traceStart(Action.ReadCollections, {
dataExplorerArea: "ResourceTree",
databaseId,
apiType: userContext.apiType,
});
if (isFabricMirroredKey() && userContext.fabricContext?.databaseName === databaseId) {
const collections: DataModels.Collection[] = [];
@@ -43,8 +50,10 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
// Sort collections by id before returning
collections.sort((a, b) => a.id.localeCompare(b.id));
traceSuccess(Action.ReadCollections, { databaseId, collectionCount: collections.length }, startKey);
return collections;
} catch (error) {
traceFailure(Action.ReadCollections, { databaseId, error: error?.message }, startKey);
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error;
} finally {
@@ -59,7 +68,9 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
userContext.apiType !== "Tables" &&
!isFabric()
) {
return await readCollectionsWithARM(databaseId);
const result = await readCollectionsWithARM(databaseId);
traceSuccess(Action.ReadCollections, { databaseId, collectionCount: result?.length, path: "ARM" }, startKey);
return result;
}
Logger.logInfo(`readCollections: calling fetchAll for database ${databaseId}`, "readCollections");
@@ -70,8 +81,14 @@ export async function readCollections(databaseId: string): Promise<DataModels.Co
?.length}, durationMs=${Date.now() - fetchAllStart}`,
"readCollections",
);
traceSuccess(
Action.ReadCollections,
{ databaseId, collectionCount: sdkResponse.resources?.length, path: "SDK" },
startKey,
);
return sdkResponse.resources as DataModels.Collection[];
} catch (error) {
traceFailure(Action.ReadCollections, { databaseId, error: error?.message }, startKey);
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error;
} finally {
@@ -84,6 +101,11 @@ export async function readCollectionsWithPagination(
continuationToken?: string,
): Promise<DataModels.CollectionsWithPagination> {
const clearMessage = logConsoleProgress(`Querying containers for database ${databaseId}`);
const startKey = traceStart(Action.ReadCollections, {
dataExplorerArea: "ResourceTree",
databaseId,
paginated: true,
});
try {
const sdkResponse = await client()
.database(databaseId)
@@ -99,8 +121,14 @@ export async function readCollectionsWithPagination(
collections: sdkResponse.resources as DataModels.Collection[],
continuationToken: sdkResponse.continuationToken,
};
traceSuccess(
Action.ReadCollections,
{ databaseId, collectionCount: collectionsWithPagination.collections?.length, paginated: true },
startKey,
);
return collectionsWithPagination;
} catch (error) {
traceFailure(Action.ReadCollections, { databaseId, error: error?.message, paginated: true }, startKey);
handleError(error, "ReadCollections", `Error while querying containers for database ${databaseId}`);
throw error;
} finally {

View File

@@ -2,6 +2,8 @@ import { CosmosDbArtifactType } from "Contracts/FabricMessagesContract";
import { isFabric, isFabricMirroredKey, isFabricNative } from "Platform/Fabric/FabricUtil";
import { AuthType } from "../../AuthType";
import * as DataModels from "../../Contracts/DataModels";
import { Action } from "../../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../../Shared/Telemetry/TelemetryProcessor";
import { FabricArtifactInfo, userContext } from "../../UserContext";
import { logConsoleProgress } from "../../Utils/NotificationConsoleUtils";
import { listCassandraKeyspaces } from "../../Utils/arm/generatedClients/cosmos/cassandraResources";
@@ -14,6 +16,10 @@ import { handleError } from "../ErrorHandlingUtils";
export async function readDatabases(): Promise<DataModels.Database[]> {
let databases: DataModels.Database[];
const clearMessage = logConsoleProgress(`Querying databases`);
const startKey = traceStart(Action.ReadDatabases, {
dataExplorerArea: "ResourceTree",
apiType: userContext.apiType,
});
if (
isFabricMirroredKey() &&
@@ -81,9 +87,11 @@ export async function readDatabases(): Promise<DataModels.Database[]> {
databases = sdkResponse.resources as DataModels.Database[];
}
} catch (error) {
traceFailure(Action.ReadDatabases, { error: error?.message }, startKey);
handleError(error, "ReadDatabases", `Error while querying databases`);
throw error;
}
traceSuccess(Action.ReadDatabases, { databaseCount: databases?.length }, startKey);
clearMessage();
return databases;
}

View File

@@ -636,6 +636,7 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree,
});
scenarioMonitor.startPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
try {
await Promise.all(
databasesToLoad.map(async (database: ViewModels.Database) => {
@@ -647,13 +648,16 @@ export default class Explorer {
useTabs
.getState()
.refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id());
}),
);
TelemetryProcessor.traceSuccess(
Action.LoadCollections,
{ dataExplorerArea: Constants.Areas.ResourceTree },
startKey,
);
}),
);
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
// Start DatabaseTreeRendered — React render cycle will complete it in ResourceTree
scenarioMonitor.startPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.LoadCollections,
@@ -664,6 +668,7 @@ export default class Explorer {
},
startKey,
);
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
}
}
@@ -1203,10 +1208,15 @@ export default class Explorer {
}
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
this.databasesRefreshed =
userContext.authType === AuthType.ResourceToken
? this.refreshDatabaseForResourceToken()
: this.refreshAllDatabases();
if (userContext.authType === AuthType.ResourceToken) {
scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
this.databasesRefreshed = this.refreshDatabaseForResourceToken().then(() => {
scenarioMonitor.completePhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabasesFetched);
});
} else {
this.databasesRefreshed = this.refreshAllDatabases();
}
await this.databasesRefreshed; // await: we rely on the databases to be loaded before restoring the tabs further in the flow
}
@@ -1274,16 +1284,23 @@ export default class Explorer {
return;
}
const startKey = TelemetryProcessor.traceStart(Action.RefreshSampleData, {
dataExplorerArea: Constants.Areas.ResourceTree,
databaseId,
});
readSampleCollection()
.then((collection: DataModels.Collection) => {
if (!collection) {
TelemetryProcessor.traceSuccess(Action.RefreshSampleData, { sampleCollectionFound: false }, startKey);
return;
}
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection });
TelemetryProcessor.traceSuccess(Action.RefreshSampleData, { sampleCollectionFound: true }, startKey);
})
.catch((error) => {
TelemetryProcessor.traceFailure(Action.RefreshSampleData, { error: getErrorMessage(error) }, startKey);
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData");
});
}

View File

@@ -129,6 +129,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
: databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
const authorizationHeader = getAuthorizationHeader();
const startKey = TelemetryProcessor.traceStart(Action.RefreshNotebooksEnabled, {
dataExplorerArea: "Notebook",
});
try {
const response = await fetch(disallowedLocationsUri, {
method: "POST",
@@ -155,9 +158,11 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
// firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
TelemetryProcessor.traceSuccess(Action.RefreshNotebooksEnabled, { isAccountInAllowedLocation }, startKey);
} catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false });
TelemetryProcessor.traceFailure(Action.RefreshNotebooksEnabled, { error: getErrorMessage(error) }, startKey);
}
},
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
@@ -304,6 +309,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => {
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
const startKey = TelemetryProcessor.traceStart(Action.CheckPhoenixStatus, {
dataExplorerArea: "Notebook",
});
let isPhoenixNotebooks = false;
let isPhoenixFeatures = false;
@@ -328,6 +336,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
}
set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures });
TelemetryProcessor.traceSuccess(Action.CheckPhoenixStatus, { isPhoenixNotebooks, isPhoenixFeatures }, startKey);
}
},
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),

View File

@@ -63,20 +63,27 @@ export const isCopilotFeatureRegistered = async (subscriptionId: string): Promis
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const startKey = traceStart(Action.CheckCopilotFeatureRegistration, {
dataExplorerArea: Areas.Copilot,
});
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
traceFailure(Action.CheckCopilotFeatureRegistration, { error: String(error) }, startKey);
return false;
}
if (!response?.ok) {
traceFailure(Action.CheckCopilotFeatureRegistration, { status: response?.status }, startKey);
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
const registered = featureRegistration?.properties?.state === "Registered";
traceSuccess(Action.CheckCopilotFeatureRegistration, { registered }, startKey);
return registered;
};
export const getCopilotEnabled = async (): Promise<boolean> => {
@@ -86,20 +93,27 @@ export const getCopilotEnabled = async (): Promise<boolean> => {
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const startKey = traceStart(Action.GetCopilotEnabled, {
dataExplorerArea: Areas.Copilot,
});
let response;
try {
response = await fetchWithTimeout(url, headers);
} catch (error) {
traceFailure(Action.GetCopilotEnabled, { error: String(error) }, startKey);
return false;
}
if (!response?.ok) {
traceFailure(Action.GetCopilotEnabled, { status: response?.status }, startKey);
return false;
}
const copilotPortalConfiguration = (await response?.json()) as CopilotEnabledConfiguration;
return copilotPortalConfiguration?.isEnabled;
const isEnabled = copilotPortalConfiguration?.isEnabled;
traceSuccess(Action.GetCopilotEnabled, { isEnabled }, startKey);
return isEnabled;
};
export const allocatePhoenixContainer = async ({

View File

@@ -158,9 +158,14 @@ export default class Database implements ViewModels.Database {
if (restart) {
this.collectionsContinuationToken = undefined;
}
const startKey = TelemetryProcessor.traceStart(Action.LoadCollectionsPerDatabase, {
dataExplorerArea: Constants.Areas.ResourceTree,
databaseId: this.id(),
});
const containerPaginationEnabled =
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.ContainerPaginationEnabled) ===
"true";
try {
if (containerPaginationEnabled) {
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
this.id(),
@@ -216,6 +221,19 @@ export default class Database implements ViewModels.Database {
}
useDatabases.getState().updateDatabase(this);
TelemetryProcessor.traceSuccess(
Action.LoadCollectionsPerDatabase,
{ dataExplorerArea: Constants.Areas.ResourceTree, databaseId: this.id(), collectionCount: collections?.length },
startKey,
);
} catch (error) {
TelemetryProcessor.traceFailure(
Action.LoadCollectionsPerDatabase,
{ dataExplorerArea: Constants.Areas.ResourceTree, databaseId: this.id(), error: error?.message },
startKey,
);
throw error;
}
}
public async openAddCollection(database: Database): Promise<void> {

View File

@@ -3,15 +3,28 @@ import { ApplicationMetricPhase, CommonMetricPhase, ScenarioConfig } from "./Sce
export const scenarioConfigs: Record<MetricScenario, ScenarioConfig> = {
[MetricScenario.ApplicationLoad]: {
requiredPhases: [ApplicationMetricPhase.ExplorerInitialized, CommonMetricPhase.Interactive],
requiredPhases: [
ApplicationMetricPhase.PlatformConfigured,
ApplicationMetricPhase.CopilotConfigured,
ApplicationMetricPhase.SampleDataLoaded,
ApplicationMetricPhase.ExplorerInitialized,
CommonMetricPhase.Interactive,
],
deferredPhases: [
ApplicationMetricPhase.CopilotConfigured,
ApplicationMetricPhase.SampleDataLoaded,
ApplicationMetricPhase.ExplorerInitialized,
],
timeoutMs: 10000,
},
[MetricScenario.DatabaseLoad]: {
requiredPhases: [
ApplicationMetricPhase.DatabasesFetched,
ApplicationMetricPhase.CollectionsLoaded,
ApplicationMetricPhase.DatabaseTreeRendered,
CommonMetricPhase.Interactive,
],
deferredPhases: [ApplicationMetricPhase.CollectionsLoaded, ApplicationMetricPhase.DatabaseTreeRendered],
timeoutMs: 10000,
},
};

View File

@@ -9,7 +9,11 @@ export enum CommonMetricPhase {
// Application-specific phases
export enum ApplicationMetricPhase {
ExplorerInitialized = "ExplorerInitialized",
PlatformConfigured = "PlatformConfigured",
CopilotConfigured = "CopilotConfigured",
SampleDataLoaded = "SampleDataLoaded",
DatabasesFetched = "DatabasesFetched",
CollectionsLoaded = "CollectionsLoaded",
DatabaseTreeRendered = "DatabaseTreeRendered",
}
@@ -18,6 +22,7 @@ export type MetricPhase = CommonMetricPhase | ApplicationMetricPhase;
export interface ScenarioConfig<TPhase extends string = MetricPhase> {
requiredPhases: TPhase[];
deferredPhases?: TPhase[]; // Phases not auto-started at scenario start; started explicitly via startPhase()
timeoutMs: number;
validate?: (ctx: ScenarioContextSnapshot<TPhase>) => boolean; // Optional custom validation
}

View File

@@ -110,7 +110,13 @@ describe("ScenarioMonitor", () => {
// Start scenario
scenarioMonitor.start(MetricScenario.ApplicationLoad);
// Complete all phases to emit
// Complete all phases to emit (PlatformConfigured auto-started, deferred phases need start+complete)
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
@@ -161,13 +167,13 @@ describe("ScenarioMonitor", () => {
it("emits healthy even with partial phase completion and expected failure", () => {
scenarioMonitor.start(MetricScenario.ApplicationLoad);
// Complete one phase
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
// Complete one non-deferred phase
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
// Mark expected failure
scenarioMonitor.markExpectedFailure();
// Let timeout fire (Interactive phase not completed)
// Let timeout fire (deferred phases and Interactive not completed)
jest.advanceTimersByTime(10000);
expect(reportMetric).toHaveBeenCalledWith(
@@ -175,7 +181,7 @@ describe("ScenarioMonitor", () => {
healthy: true,
timedOut: true,
hasExpectedFailure: true,
completedPhases: expect.arrayContaining(["ExplorerInitialized"]),
completedPhases: expect.arrayContaining(["PlatformConfigured"]),
}),
);
});
@@ -216,7 +222,13 @@ describe("ScenarioMonitor", () => {
it("emits healthy when all phases complete", () => {
scenarioMonitor.start(MetricScenario.ApplicationLoad);
// Complete all required phases
// Complete all required phases (PlatformConfigured + Interactive auto-started, deferred need start)
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);
@@ -225,7 +237,13 @@ describe("ScenarioMonitor", () => {
scenario: MetricScenario.ApplicationLoad,
healthy: true,
timedOut: false,
completedPhases: expect.arrayContaining(["ExplorerInitialized", "Interactive"]),
completedPhases: expect.arrayContaining([
"PlatformConfigured",
"CopilotConfigured",
"SampleDataLoaded",
"ExplorerInitialized",
"Interactive",
]),
}),
);
});
@@ -233,8 +251,8 @@ describe("ScenarioMonitor", () => {
it("does not emit until all phases complete", () => {
scenarioMonitor.start(MetricScenario.ApplicationLoad);
// Complete only one phase
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
// Complete only one non-deferred phase
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
expect(reportMetric).not.toHaveBeenCalled();
});
@@ -246,7 +264,13 @@ describe("ScenarioMonitor", () => {
scenarioMonitor.start(MetricScenario.ApplicationLoad);
scenarioMonitor.start(MetricScenario.DatabaseLoad);
// Complete ApplicationLoad
// Complete ApplicationLoad (all phases including deferred)
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, CommonMetricPhase.Interactive);

View File

@@ -87,8 +87,12 @@ class ScenarioMonitor {
hasExpectedFailure: false,
};
// Start all required phases at scenario start time
// Start all required phases at scenario start time, except deferred ones
const deferredSet = new Set(config.deferredPhases ?? []);
config.requiredPhases.forEach((phase) => {
if (deferredSet.has(phase)) {
return; // Deferred phases are started explicitly via startPhase()
}
const phaseStartMarkName = `scenario_${scenario}_${phase}_start`;
performance.mark(phaseStartMarkName);
ctx.phases.set(phase, { startMarkName: phaseStartMarkName });
@@ -135,6 +139,11 @@ class ScenarioMonitor {
if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || ctx.phases.has(phase)) {
return;
}
// Only deferred phases can be started via startPhase(); non-deferred are auto-started in start()
const isDeferredPhase = ctx.config.deferredPhases?.includes(phase) ?? false;
if (!isDeferredPhase) {
return;
}
const startMarkName = `scenario_${scenario}_${phase}_start`;
performance.mark(startMarkName);
@@ -147,10 +156,37 @@ class ScenarioMonitor {
});
}
/**
* Marks a phase as skipped (e.g. copilot disabled). Removes the phase from
* requiredPhases so it no longer blocks scenario completion.
*/
skipPhase(scenario: MetricScenario, phase: MetricPhase) {
const ctx = this.contexts.get(scenario);
if (!ctx || ctx.emitted) {
return;
}
// Remove from requiredPhases so it doesn't block completion
ctx.config = {
...ctx.config,
requiredPhases: ctx.config.requiredPhases.filter((p) => p !== phase),
deferredPhases: ctx.config.deferredPhases?.filter((p) => p !== phase),
};
this.devLog(`phase_skip: ${scenario}.${phase}`);
traceMark(Action.MetricsScenario, {
event: "phase_skip",
scenario,
phase,
});
this.tryEmitIfReady(ctx);
}
completePhase(scenario: MetricScenario, phase: MetricPhase) {
const ctx = this.contexts.get(scenario);
const phaseCtx = ctx?.phases.get(phase);
if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || !phaseCtx) {
if (!ctx || ctx.emitted || ctx.completed.has(phase) || !ctx.config.requiredPhases.includes(phase) || !phaseCtx) {
return;
}
@@ -356,8 +392,7 @@ class ScenarioMonitor {
private cleanupPerformanceEntries(ctx: InternalScenarioContext) {
performance.clearMarks(ctx.startMarkName);
ctx.config.requiredPhases.forEach((phase) => {
const phaseCtx = ctx.phases.get(phase);
ctx.phases.forEach((phaseCtx, phase) => {
if (phaseCtx?.startMarkName) {
performance.clearMarks(phaseCtx.startMarkName);
}

View File

@@ -152,6 +152,22 @@ export enum Action {
CloudShellTerminalSession,
OpenVSCode,
ImportSampleData,
// Tracing for ApplicationLoad & DatabaseLoad scenarios
ConfigurePortal,
FetchAccountKeys,
AcquireMsalToken,
UpdateCopilotContext,
GetCopilotEnabled,
CheckCopilotFeatureRegistration,
UpdateSampleDataContext,
RefreshSampleData,
ReadDatabases,
ReadCollections,
LoadCollectionsPerDatabase,
RefreshNotebooksEnabled,
CheckPhoenixStatus,
CheckFeatureRegistration,
}
export const ActionModifiers = {

View File

@@ -10,7 +10,7 @@ import { DatabaseAccount } from "../Contracts/DataModels";
import * as ViewModels from "../Contracts/ViewModels";
import { isExpectedError } from "../Metrics/ErrorClassification";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { trace, traceFailure } from "../Shared/Telemetry/TelemetryProcessor";
import { trace, traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
import { UserContext, userContext } from "../UserContext";
export function getAuthorizationHeader(): ViewModels.AuthorizationTokenHeaderMetadata {
@@ -74,7 +74,11 @@ export async function acquireMsalTokenForAccount(
silent: boolean = false,
user_hint?: string,
) {
const msalStartKey = traceStart(Action.AcquireMsalToken, {
acquireTokenType: silent ? "silent" : "interactive",
});
if (userContext.databaseAccount.properties?.documentEndpoint === undefined) {
traceFailure(Action.AcquireMsalToken, { error: "No document endpoint" }, msalStartKey);
throw new Error("Database account has no document endpoint defined");
}
let hrefEndpoint = "";
@@ -107,6 +111,7 @@ export async function acquireMsalTokenForAccount(
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-sso#sso-between-different-apps
try {
const loginResponse = await msalInstance.ssoSilent(loginRequest);
traceSuccess(Action.AcquireMsalToken, { method: "ssoSilent" }, msalStartKey);
return loginResponse.accessToken;
} catch (silentError) {
trace(Action.SignInAad, ActionModifiers.Mark, {
@@ -122,6 +127,7 @@ export async function acquireMsalTokenForAccount(
// See https://learn.microsoft.com/en-us/entra/identity-platform/msal-js-prompt-behavior#interactive-requests-with-promptnone
// The hint will be used to pre-fill the username field in the popup if silent is false.
const loginResponse = await msalInstance.loginPopup({ prompt: silent ? "none" : "login", ...loginRequest });
traceSuccess(Action.AcquireMsalToken, { method: "loginPopup" }, msalStartKey);
return loginResponse.accessToken;
} catch (error) {
traceFailure(Action.SignInAad, {
@@ -129,6 +135,7 @@ export async function acquireMsalTokenForAccount(
acquireTokenType: silent ? "silent" : "interactive",
errorMessage: JSON.stringify(error),
});
traceFailure(Action.AcquireMsalToken, { error: JSON.stringify(error) }, msalStartKey);
// Mark expected failure for health metrics so timeout emits healthy
if (isExpectedError(error)) {
scenarioMonitor.markExpectedFailure();
@@ -161,7 +168,8 @@ export async function acquireTokenWithMsal(
try {
// attempt silent acquisition first
return (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
const token = (await msalInstance.acquireTokenSilent(tokenRequest)).accessToken;
return token;
} catch (silentError) {
if (silentError instanceof msal.InteractionRequiredAuthError && silent === false) {
try {

View File

@@ -1,6 +1,8 @@
import { configContext } from "ConfigContext";
import { FeatureRegistration } from "Contracts/DataModels";
import { AuthorizationTokenHeaderMetadata } from "Contracts/ViewModels";
import { Action } from "Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "Shared/Telemetry/TelemetryProcessor";
import { getAuthorizationHeader } from "Utils/AuthorizationUtils";
export const featureRegistered = async (subscriptionId: string, feature: string) => {
@@ -9,20 +11,27 @@ export const featureRegistered = async (subscriptionId: string, feature: string)
const authorizationHeader: AuthorizationTokenHeaderMetadata = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
const startKey = traceStart(Action.CheckFeatureRegistration, {
feature,
});
let response;
try {
response = await _fetchWithTimeout(url, headers);
} catch (error) {
traceFailure(Action.CheckFeatureRegistration, { feature, error: String(error) }, startKey);
return false;
}
if (!response?.ok) {
traceFailure(Action.CheckFeatureRegistration, { feature, status: response?.status }, startKey);
return false;
}
const featureRegistration = (await response?.json()) as FeatureRegistration;
return featureRegistration?.properties?.state === "Registered";
const registered = featureRegistration?.properties?.state === "Registered";
traceSuccess(Action.CheckFeatureRegistration, { feature, registered }, startKey);
return registered;
};
async function _fetchWithTimeout(

View File

@@ -49,6 +49,9 @@ import {
HostedExplorerChildFrame,
ResourceToken,
} from "../HostedExplorerChildFrame";
import MetricScenario from "../Metrics/MetricEvents";
import { ApplicationMetricPhase } from "../Metrics/ScenarioConfig";
import { scenarioMonitor } from "../Metrics/ScenarioMonitor";
import { emulatorAccount } from "../Platform/Emulator/emulatorAccount";
import { parseResourceTokenConnectionString } from "../Platform/Hosted/Helpers/ResourceTokenUtils";
import {
@@ -57,6 +60,8 @@ import {
} from "../Platform/Hosted/HostedUtils";
import { extractFeatures } from "../Platform/Hosted/extractFeatures";
import { DefaultExperienceUtility } from "../Shared/DefaultExperienceUtility";
import { Action } from "../Shared/Telemetry/TelemetryConstants";
import { traceFailure, traceStart, traceSuccess } from "../Shared/Telemetry/TelemetryProcessor";
import { FabricArtifactInfo, Node, PortalEnv, updateUserContext, userContext } from "../UserContext";
import {
acquireMsalTokenForAccount,
@@ -88,6 +93,7 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
}
let explorer: Explorer;
try {
if (platform === Platform.Hosted) {
explorer = await configureHosted();
} else if (platform === Platform.Emulator) {
@@ -97,14 +103,27 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
} else if (platform === Platform.Fabric) {
explorer = await configureFabric();
}
} catch (error) {
scenarioMonitor.failPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
throw error;
}
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.PlatformConfigured);
if (explorer && userContext.features.enableCopilot) {
await updateContextForCopilot(explorer);
await updateContextForSampleData(explorer);
} else {
// Explorer falsy or copilot disabled — skip deferred copilot/sampleData phases
scenarioMonitor.skipPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
scenarioMonitor.skipPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
}
restoreOpenTabs();
// Complete ExplorerInitialized — React state update that unblocks ResourceTree
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
setExplorer(explorer);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.ExplorerInitialized);
}
};
effect();
@@ -657,6 +676,11 @@ function configureEmulator(): Explorer {
export async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup: string, account: string) {
Logger.logInfo(`Fetching keys for ${userContext.apiType} account ${account}`, "Explorer/fetchAndUpdateKeys");
const startKey = traceStart(Action.FetchAccountKeys, {
dataExplorerArea: "ResourceTree",
apiType: userContext.apiType,
accountName: account,
});
let keys;
try {
keys = await listKeys(subscriptionId, resourceGroup, account);
@@ -664,6 +688,7 @@ export async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup:
updateUserContext({
masterKey: keys.primaryMasterKey,
});
traceSuccess(Action.FetchAccountKeys, { accountName: account }, startKey);
} catch (error) {
if (error.code === "AuthorizationFailed") {
keys = await getReadOnlyKeys(subscriptionId, resourceGroup, account);
@@ -674,18 +699,23 @@ export async function fetchAndUpdateKeys(subscriptionId: string, resourceGroup:
updateUserContext({
masterKey: keys.primaryReadonlyMasterKey,
});
traceSuccess(Action.FetchAccountKeys, { accountName: account, fallbackToReadOnly: true }, startKey);
} else {
logConsoleError(`Error occurred fetching keys for the account." ${error.message}`);
Logger.logError(
`Error during fetching keys or updating user context: ${error} for ${userContext.apiType} account ${account}`,
"Explorer/fetchAndUpdateKeys",
);
traceFailure(Action.FetchAccountKeys, { accountName: account, error: error.message }, startKey);
throw error;
}
}
}
async function configurePortal(): Promise<Explorer> {
const configureStartKey = traceStart(Action.ConfigurePortal, {
dataExplorerArea: "ResourceTree",
});
updateUserContext({
authType: AuthType.AAD,
});
@@ -800,6 +830,7 @@ async function configurePortal(): Promise<Explorer> {
}
explorer = new Explorer();
traceSuccess(Action.ConfigurePortal, {}, configureStartKey);
resolve(explorer);
if (openAction) {
@@ -1018,17 +1049,34 @@ interface PortalMessage {
}
async function updateContextForCopilot(explorer: Explorer): Promise<void> {
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
const startKey = traceStart(Action.UpdateCopilotContext, {
dataExplorerArea: "ResourceTree",
});
try {
await explorer.configureCopilot();
traceSuccess(Action.UpdateCopilotContext, {}, startKey);
} catch (error) {
traceFailure(Action.UpdateCopilotContext, { error: error?.message }, startKey);
} finally {
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.CopilotConfigured);
}
}
async function updateContextForSampleData(explorer: Explorer): Promise<void> {
scenarioMonitor.startPhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
const copilotEnabled =
userContext.apiType === "SQL" && userContext.features.enableCopilot && useQueryCopilot.getState().copilotEnabled;
if (!copilotEnabled) {
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
return;
}
const startKey = traceStart(Action.UpdateSampleDataContext, {
dataExplorerArea: "ResourceTree",
});
try {
const url: string = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
const authorizationHeader = getAuthorizationHeader();
const headers = { [authorizationHeader.header]: authorizationHeader.token };
@@ -1038,6 +1086,8 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
});
if (!response.ok) {
traceSuccess(Action.UpdateSampleDataContext, { sampleDataAvailable: false }, startKey);
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
return undefined;
}
@@ -1046,6 +1096,12 @@ async function updateContextForSampleData(explorer: Explorer): Promise<void> {
updateUserContext({ sampleDataConnectionInfo });
explorer.refreshSampleData();
traceSuccess(Action.UpdateSampleDataContext, { sampleDataAvailable: true }, startKey);
} catch (error) {
traceFailure(Action.UpdateSampleDataContext, { error: error?.message }, startKey);
} finally {
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
}
}
interface SampledataconnectionResponse {