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

View File

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

View File

@@ -636,6 +636,7 @@ export default class Explorer {
dataExplorerArea: Constants.Areas.ResourceTree, dataExplorerArea: Constants.Areas.ResourceTree,
}); });
scenarioMonitor.startPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
try { try {
await Promise.all( await Promise.all(
databasesToLoad.map(async (database: ViewModels.Database) => { databasesToLoad.map(async (database: ViewModels.Database) => {
@@ -647,13 +648,16 @@ export default class Explorer {
useTabs useTabs
.getState() .getState()
.refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id()); .refreshActiveTab((tab) => tab.collection && tab.collection.getDatabase().id() === database.id());
}),
);
TelemetryProcessor.traceSuccess( TelemetryProcessor.traceSuccess(
Action.LoadCollections, Action.LoadCollections,
{ dataExplorerArea: Constants.Areas.ResourceTree }, { dataExplorerArea: Constants.Areas.ResourceTree },
startKey, 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) { } catch (error) {
TelemetryProcessor.traceFailure( TelemetryProcessor.traceFailure(
Action.LoadCollections, Action.LoadCollections,
@@ -664,6 +668,7 @@ export default class Explorer {
}, },
startKey, startKey,
); );
scenarioMonitor.failPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
} }
} }
@@ -1203,10 +1208,15 @@ export default class Explorer {
} }
if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") { if (userContext.apiType !== "Postgres" && userContext.apiType !== "VCoreMongo") {
this.databasesRefreshed = if (userContext.authType === AuthType.ResourceToken) {
userContext.authType === AuthType.ResourceToken scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.CollectionsLoaded);
? this.refreshDatabaseForResourceToken() scenarioMonitor.skipPhase(MetricScenario.DatabaseLoad, ApplicationMetricPhase.DatabaseTreeRendered);
: this.refreshAllDatabases(); 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 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; return;
} }
const startKey = TelemetryProcessor.traceStart(Action.RefreshSampleData, {
dataExplorerArea: Constants.Areas.ResourceTree,
databaseId,
});
readSampleCollection() readSampleCollection()
.then((collection: DataModels.Collection) => { .then((collection: DataModels.Collection) => {
if (!collection) { if (!collection) {
TelemetryProcessor.traceSuccess(Action.RefreshSampleData, { sampleCollectionFound: false }, startKey);
return; return;
} }
const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true); const sampleDataResourceTokenCollection = new ResourceTokenCollection(this, databaseId, collection, true);
useDatabases.setState({ sampleDataResourceTokenCollection }); useDatabases.setState({ sampleDataResourceTokenCollection });
TelemetryProcessor.traceSuccess(Action.RefreshSampleData, { sampleCollectionFound: true }, startKey);
}) })
.catch((error) => { .catch((error) => {
TelemetryProcessor.traceFailure(Action.RefreshSampleData, { error: getErrorMessage(error) }, startKey);
Logger.logError(getErrorMessage(error), "Explorer/refreshSampleData"); 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(); : databaseAccount?.properties?.writeLocations?.[0]?.locationName.toLowerCase();
const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`; const disallowedLocationsUri: string = `${configContext.PORTAL_BACKEND_ENDPOINT}/api/disallowedlocations`;
const authorizationHeader = getAuthorizationHeader(); const authorizationHeader = getAuthorizationHeader();
const startKey = TelemetryProcessor.traceStart(Action.RefreshNotebooksEnabled, {
dataExplorerArea: "Notebook",
});
try { try {
const response = await fetch(disallowedLocationsUri, { const response = await fetch(disallowedLocationsUri, {
method: "POST", method: "POST",
@@ -155,9 +158,11 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
// firstWriteLocation should not be disallowed // firstWriteLocation should not be disallowed
const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1; const isAccountInAllowedLocation = firstWriteLocation && disallowedLocations.indexOf(firstWriteLocation) === -1;
set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation }); set({ isNotebooksEnabledForAccount: isAccountInAllowedLocation });
TelemetryProcessor.traceSuccess(Action.RefreshNotebooksEnabled, { isAccountInAllowedLocation }, startKey);
} catch (error) { } catch (error) {
Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount"); Logger.logError(getErrorMessage(error), "Explorer/isNotebooksEnabledForAccount");
set({ isNotebooksEnabledForAccount: false }); set({ isNotebooksEnabledForAccount: false });
TelemetryProcessor.traceFailure(Action.RefreshNotebooksEnabled, { error: getErrorMessage(error) }, startKey);
} }
}, },
findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => { findItem: (root: NotebookContentItem, item: NotebookContentItem): NotebookContentItem => {
@@ -304,6 +309,9 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }), setContainerStatus: (containerStatus: ContainerInfo) => set({ containerStatus }),
getPhoenixStatus: async () => { getPhoenixStatus: async () => {
if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) { if (get().isPhoenixNotebooks === undefined || get().isPhoenixFeatures === undefined) {
const startKey = TelemetryProcessor.traceStart(Action.CheckPhoenixStatus, {
dataExplorerArea: "Notebook",
});
let isPhoenixNotebooks = false; let isPhoenixNotebooks = false;
let isPhoenixFeatures = false; let isPhoenixFeatures = false;
@@ -328,6 +336,7 @@ export const useNotebook: UseStore<NotebookState> = create((set, get) => ({
} }
set({ isPhoenixNotebooks: isPhoenixNotebooks }); set({ isPhoenixNotebooks: isPhoenixNotebooks });
set({ isPhoenixFeatures: isPhoenixFeatures }); set({ isPhoenixFeatures: isPhoenixFeatures });
TelemetryProcessor.traceSuccess(Action.CheckPhoenixStatus, { isPhoenixNotebooks, isPhoenixFeatures }, startKey);
} }
}, },
setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }), setIsPhoenixNotebooks: (isPhoenixNotebooks: boolean) => set({ isPhoenixNotebooks: isPhoenixNotebooks }),

View File

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

View File

@@ -158,9 +158,14 @@ export default class Database implements ViewModels.Database {
if (restart) { if (restart) {
this.collectionsContinuationToken = undefined; this.collectionsContinuationToken = undefined;
} }
const startKey = TelemetryProcessor.traceStart(Action.LoadCollectionsPerDatabase, {
dataExplorerArea: Constants.Areas.ResourceTree,
databaseId: this.id(),
});
const containerPaginationEnabled = const containerPaginationEnabled =
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.ContainerPaginationEnabled) === StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.ContainerPaginationEnabled) ===
"true"; "true";
try {
if (containerPaginationEnabled) { if (containerPaginationEnabled) {
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination( const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
this.id(), this.id(),
@@ -216,6 +221,19 @@ export default class Database implements ViewModels.Database {
} }
useDatabases.getState().updateDatabase(this); 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> { 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> = { export const scenarioConfigs: Record<MetricScenario, ScenarioConfig> = {
[MetricScenario.ApplicationLoad]: { [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, timeoutMs: 10000,
}, },
[MetricScenario.DatabaseLoad]: { [MetricScenario.DatabaseLoad]: {
requiredPhases: [ requiredPhases: [
ApplicationMetricPhase.DatabasesFetched, ApplicationMetricPhase.DatabasesFetched,
ApplicationMetricPhase.CollectionsLoaded,
ApplicationMetricPhase.DatabaseTreeRendered, ApplicationMetricPhase.DatabaseTreeRendered,
CommonMetricPhase.Interactive, CommonMetricPhase.Interactive,
], ],
deferredPhases: [ApplicationMetricPhase.CollectionsLoaded, ApplicationMetricPhase.DatabaseTreeRendered],
timeoutMs: 10000, timeoutMs: 10000,
}, },
}; };

View File

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

View File

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

View File

@@ -87,8 +87,12 @@ class ScenarioMonitor {
hasExpectedFailure: false, 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) => { config.requiredPhases.forEach((phase) => {
if (deferredSet.has(phase)) {
return; // Deferred phases are started explicitly via startPhase()
}
const phaseStartMarkName = `scenario_${scenario}_${phase}_start`; const phaseStartMarkName = `scenario_${scenario}_${phase}_start`;
performance.mark(phaseStartMarkName); performance.mark(phaseStartMarkName);
ctx.phases.set(phase, { startMarkName: 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)) { if (!ctx || ctx.emitted || !ctx.config.requiredPhases.includes(phase) || ctx.phases.has(phase)) {
return; 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`; const startMarkName = `scenario_${scenario}_${phase}_start`;
performance.mark(startMarkName); 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) { completePhase(scenario: MetricScenario, phase: MetricPhase) {
const ctx = this.contexts.get(scenario); const ctx = this.contexts.get(scenario);
const phaseCtx = ctx?.phases.get(phase); 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; return;
} }
@@ -356,8 +392,7 @@ class ScenarioMonitor {
private cleanupPerformanceEntries(ctx: InternalScenarioContext) { private cleanupPerformanceEntries(ctx: InternalScenarioContext) {
performance.clearMarks(ctx.startMarkName); performance.clearMarks(ctx.startMarkName);
ctx.config.requiredPhases.forEach((phase) => { ctx.phases.forEach((phaseCtx, phase) => {
const phaseCtx = ctx.phases.get(phase);
if (phaseCtx?.startMarkName) { if (phaseCtx?.startMarkName) {
performance.clearMarks(phaseCtx.startMarkName); performance.clearMarks(phaseCtx.startMarkName);
} }

View File

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

View File

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

View File

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

View File

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