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

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

View File

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

View File

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

View File

@@ -158,64 +158,82 @@ export default class Database implements ViewModels.Database {
if (restart) {
this.collectionsContinuationToken = undefined;
}
const startKey = TelemetryProcessor.traceStart(Action.LoadCollectionsPerDatabase, {
dataExplorerArea: Constants.Areas.ResourceTree,
databaseId: this.id(),
});
const containerPaginationEnabled =
StorageUtility.LocalStorageUtility.getEntryString(StorageUtility.StorageKey.ContainerPaginationEnabled) ===
"true";
if (containerPaginationEnabled) {
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
this.id(),
this.collectionsContinuationToken,
);
try {
if (containerPaginationEnabled) {
const collectionsWithPagination: DataModels.CollectionsWithPagination = await readCollectionsWithPagination(
this.id(),
this.collectionsContinuationToken,
);
if (collectionsWithPagination.collections?.length === Constants.Queries.containersPerPage) {
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"`],
};
if (collectionsWithPagination.collections?.length === Constants.Queries.containersPerPage) {
this.collectionsContinuationToken = collectionsWithPagination.continuationToken;
} else {
collection.partitionKey = {
paths: ["/'$v'/'_partitionKey'/'$v'"],
kind: "Hash",
version: 2,
systemKey: true,
};
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 {
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> {