mirror of
https://github.com/Azure/cosmos-explorer.git
synced 2026-04-19 12:59:12 +01:00
Frontend performance metrics (#2439)
* Added enriched metrics * Add more traces for observability
This commit is contained in:
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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 ({
|
||||
|
||||
@@ -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> {
|
||||
|
||||
Reference in New Issue
Block a user