mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-16 19:39:19 +01:00
Frontend performance metrics (#2439)
* Added enriched metrics * Add more traces for observability
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
|
||||||
Action.LoadCollections,
|
|
||||||
{ dataExplorerArea: Constants.Areas.ResourceTree },
|
|
||||||
startKey,
|
|
||||||
);
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
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) {
|
} 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");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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 ({
|
||||||
|
|||||||
@@ -158,64 +158,82 @@ 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";
|
||||||
if (containerPaginationEnabled) {
|
try {
|
||||||
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
|
if (containerPaginationEnabled) {
|
||||||
this.id(),
|
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
|
||||||
this.collectionsContinuationToken,
|
this.id(),
|
||||||
);
|
this.collectionsContinuationToken,
|
||||||
|
);
|
||||||
|
|
||||||
if (collectionsWithPagination.collections?.length === Constants.Queries.containersPerPage) {
|
if (collectionsWithPagination.collections?.length === Constants.Queries.containersPerPage) {
|
||||||
this.collectionsContinuationToken = collectionsWithPagination.continuationToken;
|
this.collectionsContinuationToken = collectionsWithPagination.continuationToken;
|
||||||
} else {
|
|
||||||
this.collectionsContinuationToken = undefined;
|
|
||||||
}
|
|
||||||
collections = collectionsWithPagination.collections;
|
|
||||||
} else {
|
|
||||||
collections = await readCollections(this.id());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO Remove
|
|
||||||
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
|
|
||||||
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
|
|
||||||
collections.map((collection) => {
|
|
||||||
if (collection.shardKey) {
|
|
||||||
const shardKey = Object.keys(collection.shardKey)[0];
|
|
||||||
collection.partitionKey = {
|
|
||||||
version: undefined,
|
|
||||||
kind: "Hash",
|
|
||||||
paths: [`/"$v"/"${shardKey.split(".").join(`"/"$v"/"`)}"/"$v"`],
|
|
||||||
};
|
|
||||||
} else {
|
} else {
|
||||||
collection.partitionKey = {
|
this.collectionsContinuationToken = undefined;
|
||||||
paths: ["/'$v'/'_partitionKey'/'$v'"],
|
|
||||||
kind: "Hash",
|
|
||||||
version: 2,
|
|
||||||
systemKey: true,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
collections = collectionsWithPagination.collections;
|
||||||
|
} else {
|
||||||
|
collections = await readCollections(this.id());
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO Remove
|
||||||
|
// This is a hack to make Mongo collections read via ARM have a SQL-ish partitionKey property
|
||||||
|
if (userContext.apiType === "Mongo" && userContext.authType === AuthType.AAD) {
|
||||||
|
collections.map((collection) => {
|
||||||
|
if (collection.shardKey) {
|
||||||
|
const shardKey = Object.keys(collection.shardKey)[0];
|
||||||
|
collection.partitionKey = {
|
||||||
|
version: undefined,
|
||||||
|
kind: "Hash",
|
||||||
|
paths: [`/"$v"/"${shardKey.split(".").join(`"/"$v"/"`)}"/"$v"`],
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
collection.partitionKey = {
|
||||||
|
paths: ["/'$v'/'_partitionKey'/'$v'"],
|
||||||
|
kind: "Hash",
|
||||||
|
version: 2,
|
||||||
|
systemKey: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const deltaCollections = this.getDeltaCollections(collections);
|
||||||
|
|
||||||
|
collections.forEach((collection: DataModels.Collection) => {
|
||||||
|
this.addSchema(collection);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
||||||
|
const collectionVM: Collection = new Collection(this.container, this.id(), collection);
|
||||||
|
collectionVMs.push(collectionVM);
|
||||||
|
});
|
||||||
|
|
||||||
|
//merge collections
|
||||||
|
this.addCollectionsToList(collectionVMs);
|
||||||
|
if (!containerPaginationEnabled || restart) {
|
||||||
|
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
const deltaCollections = this.getDeltaCollections(collections);
|
|
||||||
|
|
||||||
collections.forEach((collection: DataModels.Collection) => {
|
|
||||||
this.addSchema(collection);
|
|
||||||
});
|
|
||||||
|
|
||||||
deltaCollections.toAdd.forEach((collection: DataModels.Collection) => {
|
|
||||||
const collectionVM: Collection = new Collection(this.container, this.id(), collection);
|
|
||||||
collectionVMs.push(collectionVM);
|
|
||||||
});
|
|
||||||
|
|
||||||
//merge collections
|
|
||||||
this.addCollectionsToList(collectionVMs);
|
|
||||||
if (!containerPaginationEnabled || restart) {
|
|
||||||
this.deleteCollectionsFromList(deltaCollections.toDelete);
|
|
||||||
}
|
|
||||||
|
|
||||||
useDatabases.getState().updateDatabase(this);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openAddCollection(database: Database): Promise<void> {
|
public async openAddCollection(database: Database): Promise<void> {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 = {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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,23 +93,37 @@ export function useKnockoutExplorer(platform: Platform): Explorer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let explorer: Explorer;
|
let explorer: Explorer;
|
||||||
if (platform === Platform.Hosted) {
|
try {
|
||||||
explorer = await configureHosted();
|
if (platform === Platform.Hosted) {
|
||||||
} else if (platform === Platform.Emulator) {
|
explorer = await configureHosted();
|
||||||
explorer = configureEmulator();
|
} else if (platform === Platform.Emulator) {
|
||||||
} else if (platform === Platform.Portal) {
|
explorer = configureEmulator();
|
||||||
explorer = await configurePortal();
|
} else if (platform === Platform.Portal) {
|
||||||
} else if (platform === Platform.Fabric) {
|
explorer = await configurePortal();
|
||||||
explorer = await configureFabric();
|
} 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) {
|
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,34 +1049,59 @@ interface PortalMessage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function updateContextForCopilot(explorer: Explorer): Promise<void> {
|
async function updateContextForCopilot(explorer: Explorer): Promise<void> {
|
||||||
await explorer.configureCopilot();
|
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> {
|
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 url: string = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
const startKey = traceStart(Action.UpdateSampleDataContext, {
|
||||||
const authorizationHeader = getAuthorizationHeader();
|
dataExplorerArea: "ResourceTree",
|
||||||
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
|
||||||
|
|
||||||
const response = await window.fetch(url, {
|
|
||||||
headers,
|
|
||||||
});
|
});
|
||||||
|
try {
|
||||||
|
const url: string = createUri(configContext.PORTAL_BACKEND_ENDPOINT, "/api/sampledata");
|
||||||
|
const authorizationHeader = getAuthorizationHeader();
|
||||||
|
const headers = { [authorizationHeader.header]: authorizationHeader.token };
|
||||||
|
|
||||||
if (!response.ok) {
|
const response = await window.fetch(url, {
|
||||||
return undefined;
|
headers,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
traceSuccess(Action.UpdateSampleDataContext, { sampleDataAvailable: false }, startKey);
|
||||||
|
scenarioMonitor.completePhase(MetricScenario.ApplicationLoad, ApplicationMetricPhase.SampleDataLoaded);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: SampledataconnectionResponse = await response.json();
|
||||||
|
const sampleDataConnectionInfo = parseResourceTokenConnectionString(data.connectionString);
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data: SampledataconnectionResponse = await response.json();
|
|
||||||
const sampleDataConnectionInfo = parseResourceTokenConnectionString(data.connectionString);
|
|
||||||
updateUserContext({ sampleDataConnectionInfo });
|
|
||||||
|
|
||||||
explorer.refreshSampleData();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SampledataconnectionResponse {
|
interface SampledataconnectionResponse {
|
||||||
|
|||||||
Reference in New Issue
Block a user