From 2ec0bbce49d91330c2b30a361ad3e30d56eefda7 Mon Sep 17 00:00:00 2001 From: tyaginidhi Date: Tue, 7 May 2024 15:37:29 +0530 Subject: [PATCH] Users/nityagi/update feature branch (#933) * Moving auth create flow to utils (#926) Co-authored-by: amitjoshi * Making auth provider a common module flow (#928) * refactor: Update QuickPickProvider to handle tab input changes (#930) The code changes in the QuickPickProvider class handle tab input changes by checking if the active tab is an instance of vscode.TabInputCustom or vscode.TabInputText. If it is, the QuickPickProvider updates the quick pick items based on the file path of the tab input. Note: This commit message follows the convention used in recent repository commits. * Updates to Artemis service & telemetry endpoints for gov clouds (#906) * Updates to artemis for gov clouds * change * changes --------- Co-authored-by: Shivika Gupta --------- Co-authored-by: amitjoshi438 <54068463+amitjoshi438@users.noreply.github.com> Co-authored-by: amitjoshi Co-authored-by: Ritik Ramuka <56073559+ritikramuka@users.noreply.github.com> Co-authored-by: gshivi <123095952+gshivi@users.noreply.github.com> Co-authored-by: Shivika Gupta --- l10n/bundle.l10n.json | 2 + .../vscode-powerplatform.xlf | 6 + src/client/extension.ts | 4 +- src/common/ArtemisService.ts | 14 +- .../AuthenticationProvider.ts} | 85 +++++----- src/common/ErrorConstants.ts | 55 ++++++ .../OneDSLoggerTelemetry/oneDSLogger.ts | 53 ++++-- .../oneDSLoggerWrapper.ts | 8 +- src/common/TelemetryConstants.ts | 15 ++ src/common/Utils.ts | 22 ++- src/common/copilot/PowerPagesCopilot.ts | 22 +-- src/common/copilot/telemetry/ITelemetry.ts | 4 +- .../copilot/telemetry/telemetryConstants.ts | 1 + src/common/copilot/user-feedback/CESSurvey.ts | 17 +- src/web/client/WebExtensionContext.ts | 12 +- src/web/client/common/errorHandler.ts | 49 ------ src/web/client/dal/concurrencyHandler.ts | 4 +- src/web/client/dal/fileSystemProvider.ts | 2 +- src/web/client/dal/remoteFetchProvider.ts | 5 +- src/web/client/dal/remoteSaveProvider.ts | 4 +- src/web/client/extension.ts | 6 + src/web/client/services/NPSService.ts | 157 +++++++++--------- src/web/client/services/etagHandlerService.ts | 2 +- src/web/client/services/graphClientService.ts | 17 +- src/web/client/telemetry/constants.ts | 14 +- .../AuthenticationProvider.test.ts | 54 +++--- .../integration/WebExtensionContext.test.ts | 10 +- .../integration/remoteFetchProvider.test.ts | 8 +- src/web/client/webViews/NPSWebView.ts | 4 +- src/web/client/webViews/QuickPickProvider.ts | 20 ++- 30 files changed, 404 insertions(+), 272 deletions(-) rename src/{web/client/common/authenticationProvider.ts => common/AuthenticationProvider.ts} (75%) create mode 100644 src/common/ErrorConstants.ts create mode 100644 src/common/TelemetryConstants.ts diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index c08f1ed2..6aba81ae 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -7,7 +7,9 @@ "You are editing a live, public site ": "You are editing a live, public site ", "Preview site": "Preview site", "Open in Power Pages studio": "Open in Power Pages studio", + "Preview site URL is not available": "Preview site URL is not available", "Opening preview site...": "Opening preview site...", + "Power Pages studio URL is not available": "Power Pages studio URL is not available", "Microsoft wants your feeback": "Microsoft wants your feeback", "Check the URL and verify the parameters are correct": "Check the URL and verify the parameters are correct", "Unable to complete the request": "Unable to complete the request", diff --git a/loc/translations-export/vscode-powerplatform.xlf b/loc/translations-export/vscode-powerplatform.xlf index 79905571..8ff6b43c 100644 --- a/loc/translations-export/vscode-powerplatform.xlf +++ b/loc/translations-export/vscode-powerplatform.xlf @@ -263,6 +263,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID) Power Pages Copilot is now connected to the environment: {0} : {1} {0} represents the environment name + + Power Pages studio URL is not available + Preparing pac CLI (v{0})... {0} represents the version number @@ -270,6 +273,9 @@ The {3} represents Dataverse Environment's Organization ID (GUID) Preview site + + Preview site URL is not available + Profile Kind: {0} The {0} represents the profile type (Admin vs Dataverse) diff --git a/src/client/extension.ts b/src/client/extension.ts index 12def86a..4020da1b 100644 --- a/src/client/extension.ts +++ b/src/client/extension.ts @@ -193,8 +193,8 @@ export async function activate( const orgID = orgDetails.OrgId; const artemisResponse = await fetchArtemisResponse(orgID, _telemetry); if (artemisResponse) { - const { geoName } = artemisResponse[0]; - oneDSLoggerWrapper.instantiate(geoName); + const { geoName, geoLongName } = artemisResponse[0]; + oneDSLoggerWrapper.instantiate(geoName, geoLongName); oneDSLoggerWrapper.getLogger().traceInfo(telemetryEventNames.DESKTOP_EXTENSION_INIT_CONTEXT, {...orgDetails, orgGeo: geoName}); } }) diff --git a/src/common/ArtemisService.ts b/src/common/ArtemisService.ts index c4656a6a..777f3c35 100644 --- a/src/common/ArtemisService.ts +++ b/src/common/ArtemisService.ts @@ -32,8 +32,8 @@ export async function getIntelligenceEndpoint(orgId: string, telemetry: ITelemet // Function to fetch Artemis response export async function fetchArtemisResponse(orgId: string, telemetry: ITelemetry, sessionID = '') { - const { tstUrl, preprodUrl, prodUrl } = convertGuidToUrls(orgId); - const endpoints = [tstUrl, preprodUrl, prodUrl]; + const { tstUrl, preprodUrl, prodUrl, gccUrl, highUrl, mooncakeUrl, dodUrl } = convertGuidToUrls(orgId); + const endpoints = [tstUrl, preprodUrl, prodUrl, gccUrl, highUrl, mooncakeUrl, dodUrl]; const artemisResponse = await fetchIslandInfo(endpoints, telemetry, sessionID); @@ -87,10 +87,18 @@ export function convertGuidToUrls(orgId: string) { const tstUrl = `https://${domain}.${nonProdSegment}.organization.api.test.powerplatform.com/gateway/cluster?api-version=1`; const preprodUrl = `https://${domain}.${nonProdSegment}.organization.api.preprod.powerplatform.com/gateway/cluster?api-version=1`; const prodUrl = `https://${domainProd}.${prodSegment}.organization.api.powerplatform.com/gateway/cluster?api-version=1`; + const gccUrl = `https://${domain}.${nonProdSegment}.organization.api.gov.powerplatform.microsoft.us/gateway/cluster?api-version=1`; + const highUrl = `https://${domain}.${nonProdSegment}.organization.api.high.powerplatform.microsoft.us/gateway/cluster?api-version=1`; + const mooncakeUrl = `https://${domain}.${nonProdSegment}.organization.api.powerplatform.partner.microsoftonline.cn/gateway/cluster?app-version=1`; + const dodUrl = `https://${domain}.${nonProdSegment}.organization.api.appsplatform.us/gateway/cluster?app-version=1`; return { tstUrl, preprodUrl, - prodUrl + prodUrl, + gccUrl, + highUrl, + mooncakeUrl, + dodUrl }; } diff --git a/src/web/client/common/authenticationProvider.ts b/src/common/AuthenticationProvider.ts similarity index 75% rename from src/web/client/common/authenticationProvider.ts rename to src/common/AuthenticationProvider.ts index 24837e98..8a1ff646 100644 --- a/src/web/client/common/authenticationProvider.ts +++ b/src/common/AuthenticationProvider.ts @@ -4,8 +4,6 @@ */ import * as vscode from "vscode"; -import WebExtensionContext from "../WebExtensionContext"; -import { telemetryEventNames } from "../telemetry/constants"; import { INTELLIGENCE_SCOPE_DEFAULT, PROVIDER_ID, @@ -13,12 +11,22 @@ import { SCOPE_OPTION_DEFAULT, SCOPE_OPTION_OFFLINE_ACCESS, SCOPE_OPTION_USERS_READ_BASIC_ALL, -} from "./constants"; -import { ERRORS, showErrorDialog } from "./errorHandler"; -import { ITelemetry } from "../../../client/telemetry/ITelemetry"; -import { sendTelemetryEvent } from "../../../common/copilot/telemetry/copilotTelemetry"; -import { CopilotLoginFailureEvent, CopilotLoginSuccessEvent } from "../../../common/copilot/telemetry/telemetryConstants"; -import { getUserAgent } from "../../../common/Utils"; +} from "../web/client/common/constants"; +import { showErrorDialog } from "../web/client/common/errorHandler"; +import { ITelemetry } from "../client/telemetry/ITelemetry"; +import { sendTelemetryEvent } from "./copilot/telemetry/copilotTelemetry"; +import { CopilotLoginFailureEvent, CopilotLoginSuccessEvent } from "./copilot/telemetry/telemetryConstants"; +import { getUserAgent } from "./Utils"; +import { + VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_COMPLETED, + VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED, + VSCODE_EXTENSION_NPS_AUTHENTICATION_COMPLETED, + VSCODE_EXTENSION_NPS_AUTHENTICATION_FAILED, + VSCODE_EXTENSION_NPS_AUTHENTICATION_STARTED, + VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_FAILED, + VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED +} from "./TelemetryConstants"; +import { ERRORS } from "./ErrorConstants"; export function getCommonHeadersForDataverse( @@ -85,6 +93,7 @@ export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessi } export async function dataverseAuthentication( + telemetry: ITelemetry, dataverseOrgURL: string, firstTimeAuth = false ): Promise<{ accessToken: string, userId: string }> { @@ -119,25 +128,25 @@ export async function dataverseAuthentication( } if (firstTimeAuth) { - WebExtensionContext.telemetry.sendInfoTelemetry( - telemetryEventNames.WEB_EXTENSION_DATAVERSE_AUTHENTICATION_COMPLETED, + sendTelemetryEvent(telemetry, { + eventName: VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_COMPLETED, userId: userId } ); } } catch (error) { - const authError = (error as Error)?.message; showErrorDialog( vscode.l10n.t( "Authorization Failed. Please run again to authorize it" ), vscode.l10n.t("There was a permissions problem with the server") ); - WebExtensionContext.telemetry.sendErrorTelemetry( - telemetryEventNames.WEB_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED, - dataverseAuthentication.name, - authError + sendTelemetryEvent( + telemetry, { + eventName: VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED, + error: error as Error + } ); } @@ -145,11 +154,12 @@ export async function dataverseAuthentication( } export async function npsAuthentication( + telemetry: ITelemetry, cesSurveyAuthorizationEndpoint: string ): Promise { let accessToken = ""; - WebExtensionContext.telemetry.sendInfoTelemetry( - telemetryEventNames.NPS_AUTHENTICATION_STARTED + sendTelemetryEvent(telemetry, + { eventName: VSCODE_EXTENSION_NPS_AUTHENTICATION_STARTED } ); try { const session = await vscode.authentication.getSession( @@ -161,21 +171,22 @@ export async function npsAuthentication( if (!accessToken) { throw new Error(ERRORS.NO_ACCESS_TOKEN); } - WebExtensionContext.telemetry.sendInfoTelemetry( - telemetryEventNames.NPS_AUTHENTICATION_COMPLETED + sendTelemetryEvent(telemetry, + { eventName: VSCODE_EXTENSION_NPS_AUTHENTICATION_COMPLETED } ); } catch (error) { - const authError = (error as Error)?.message; showErrorDialog( vscode.l10n.t( "Authorization Failed. Please run again to authorize it" ), vscode.l10n.t("There was a permissions problem with the server") ); - WebExtensionContext.telemetry.sendErrorTelemetry( - telemetryEventNames.NPS_AUTHENTICATION_FAILED, - npsAuthentication.name, - authError + sendTelemetryEvent( + telemetry, + { + eventName: VSCODE_EXTENSION_NPS_AUTHENTICATION_FAILED, + error: error as Error + } ); } @@ -183,6 +194,7 @@ export async function npsAuthentication( } export async function graphClientAuthentication( + telemetry: ITelemetry, firstTimeAuth = false ): Promise { let accessToken = ""; @@ -213,29 +225,24 @@ export async function graphClientAuthentication( } if (firstTimeAuth) { - WebExtensionContext.telemetry.sendInfoTelemetry( - telemetryEventNames.WEB_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED, - { - userId: - session?.account.id.split("/").pop() ?? - session?.account.id ?? - "", - } - ); + sendTelemetryEvent(telemetry, { + eventName: VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED, + userId: + session?.account.id.split("/").pop() ?? + session?.account.id ?? + "", + }); } } catch (error) { - const authError = (error as Error)?.message; showErrorDialog( vscode.l10n.t( "Authorization Failed. Please run again to authorize it" ), vscode.l10n.t("There was a permissions problem with the server") ); - WebExtensionContext.telemetry.sendErrorTelemetry( - telemetryEventNames.WEB_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_FAILED, - graphClientAuthentication.name, - authError - ); + sendTelemetryEvent(telemetry, + { eventName: VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_FAILED, error: error as Error } + ) } return accessToken; diff --git a/src/common/ErrorConstants.ts b/src/common/ErrorConstants.ts new file mode 100644 index 00000000..92bace75 --- /dev/null +++ b/src/common/ErrorConstants.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + + +export const ERRORS = { + SUBURI_EMPTY: "SubURI value for entity file is empty", + NO_ACCESS_TOKEN: "No access token was created", + PORTAL_FOLDER_NAME_EMPTY: "portalFolderName value for entity file is empty", + ATTRIBUTES_EMPTY: "Entity file attribute or extension field empty", + WORKSPACE_INITIAL_LOAD: "There was a problem opening the workspace", + WORKSPACE_INITIAL_LOAD_DESC: "Try refreshing the browser", + UNKNOWN_APP: "Unable to find that app", + AUTHORIZATION_FAILED: + "Authorization Failed. Please run again to authorize it", + FILE_NOT_FOUND: "The file was not found", + RETRY_LIMIT_EXCEEDED: "Unable to complete that operation", + RETRY_LIMIT_EXCEEDED_DESC: "You've exceeded the retry limit ", + PRECONDITION_CHECK_FAILED: "The precondition check did not work", + PRECONDITION_CHECK_FAILED_DESC: "Try again", + SERVER_ERROR_RETRY_LATER: "There was a problem with the server", + SERVER_ERROR_RETRY_LATER_DESC: "Please try again in a minute or two", + SERVER_ERROR_PERMISSION_DENIED: + "There was a permissions problem with the server", + SERVER_ERROR_PERMISSION_DENIED_DESC: "Please try again in a minute or two", + EMPTY_RESPONSE: "There was no response", + EMPTY_RESPONSE_DESC: "Try again", + THRESHOLD_LIMIT_EXCEEDED: "Threshold for dataverse api", + THRESHOLD_LIMIT_EXCEEDED_DESC: + "You’ve exceeded the threshold rate limit for the Dataverse API", + BAD_REQUEST: "Unable to complete the request", + BAD_REQUEST_DESC: + "One or more attribute names have been changed or removed. Contact your admin.", + BACKEND_ERROR: "There’s a problem on the back end", + SERVICE_UNAVAILABLE: "There’s a problem connecting to Dataverse", + SERVICE_ERROR: "There’s a problem connecting to Dataverse", + INVALID_ARGUMENT: "One or more commands are invalid or malformed", + BACKEND_ERROR_DESC: "Try again", + SERVICE_UNAVAILABLE_DESC: "Try again", + SERVICE_ERROR_DESC: "Try again", + INVALID_ARGUMENT_DESC: "Check the parameters and try again", + MANDATORY_PARAMETERS_NULL: "The workspace is not available ", + MANDATORY_PARAMETERS_NULL_DESC: + "Check the URL and verify the parameters are correct", + FILE_NAME_NOT_SET: "That file is not available", + FILE_NAME_NOT_SET_DESC: + "The metadata may have changed in the Dataverse side. Contact your admin. {message_attribute}", + FILE_NAME_EMPTY: "File name is empty", + FILE_ID_EMPTY: "File ID is empty", + LANGUAGE_CODE_ID_VALUE_NULL: "Language code ID is empty", + LANGUAGE_CODE_EMPTY: "Language code is empty", + BULKHEAD_LIMITS_EXCEEDED: "Bulkhead queue limits exceeded", + NPS_FAILED_AUTH: "Failed to authenticate with NPS" +}; diff --git a/src/common/OneDSLoggerTelemetry/oneDSLogger.ts b/src/common/OneDSLoggerTelemetry/oneDSLogger.ts index b35d2d04..6ede8564 100644 --- a/src/common/OneDSLoggerTelemetry/oneDSLogger.ts +++ b/src/common/OneDSLoggerTelemetry/oneDSLogger.ts @@ -83,7 +83,7 @@ export class OneDSLogger implements ITelemetryLogger { }, }; - public constructor(geo?: string) { + public constructor(geo?:string, geoLongName?:string ) { this.appInsightsCore = new AppInsightsCore(); this.postChannel = new PostChannel(); @@ -93,7 +93,7 @@ export class OneDSLogger implements ITelemetryLogger { httpXHROverride: this.fetchHttpXHROverride, }; - const instrumentationSetting: IInstrumentationSettings = OneDSLogger.getInstrumentationSettings(geo); // Need to replace with actual data + const instrumentationSetting : IInstrumentationSettings= OneDSLogger.getInstrumentationSettings(geo, geoLongName); // Need to replace with actual data // Configure App insights core to send to collector const coreConfig: IExtendedConfiguration = { @@ -136,12 +136,26 @@ export class OneDSLogger implements ITelemetryLogger { } } - private static getInstrumentationSettings(geo?: string): IInstrumentationSettings { - const buildRegion: string = region; - const instrumentationSettings: IInstrumentationSettings = { + private static getInstrumentationSettings(geo?:string, geoLongName?: string): IInstrumentationSettings { + const buildRegion:string = region; + const instrumentationSettings:IInstrumentationSettings = { endpointURL: 'https://self.pipe.aria.int.microsoft.com/OneCollector/1.0/', instrumentationKey: 'ffdb4c99ca3a4ad5b8e9ffb08bf7da0d-65357ff3-efcd-47fc-b2fd-ad95a52373f4-7402' }; + switch(geoLongName){ + case 'usgov': + geo = 'gov'; + break; + case 'usgovhigh': + geo = 'high'; + break; + case 'usdod': + geo = 'dod'; + break; + case 'china': + geo = 'mooncake'; + break; + } switch (buildRegion) { case 'tie': case 'test': @@ -161,7 +175,7 @@ export class OneDSLogger implements ITelemetryLogger { case 'ae': case 'kr': instrumentationSettings.endpointURL = 'https://us-mobile.events.data.microsoft.com/OneCollector/1.0/', - instrumentationSettings.instrumentationKey = '197418c5cb8c4426b201f9db2e87b914-87887378-2790-49b0-9295-51f43b6204b1-7172' + instrumentationSettings.instrumentationKey = '197418c5cb8c4426b201f9db2e87b914-87887378-2790-49b0-9295-51f43b6204b1-7172' break; case 'eu': case 'uk': @@ -170,21 +184,30 @@ export class OneDSLogger implements ITelemetryLogger { case 'no': case 'ch': instrumentationSettings.endpointURL = 'https://eu-mobile.events.data.microsoft.com/OneCollector/1.0/', - instrumentationSettings.instrumentationKey = '197418c5cb8c4426b201f9db2e87b914-87887378-2790-49b0-9295-51f43b6204b1-7172' + instrumentationSettings.instrumentationKey = '197418c5cb8c4426b201f9db2e87b914-87887378-2790-49b0-9295-51f43b6204b1-7172' + break; + case 'gov': + instrumentationSettings.endpointURL = 'https://tb.events.data.microsoft.com/OneCollector/1.0/', + instrumentationSettings.instrumentationKey = '2f217cb8f40440eeb8b0aa80a2be2f7e-e0ec7b51-d1bb-4d8c-83b1-cc77aaba9009-7472' + break; + case 'high': + instrumentationSettings.endpointURL = 'https://tb.events.data.microsoft.com/OneCollector/1.0/', + instrumentationSettings.instrumentationKey = '4a07e143372c46aabf3841dc4f0ef795-a753031e-2005-4282-9451-a086fea4234a-6942' + break; + case 'dod': + instrumentationSettings.endpointURL = 'https://pf.events.data.microsoft.com/OneCollector/1.0/', + instrumentationSettings.instrumentationKey = 'af47f3d608774379a53fa07cf36362ea-69701588-1aad-43ee-8b52-f71125849774-6656' + break; + case 'mooncake': + instrumentationSettings.endpointURL = 'https://collector.azure.cn/OneCollector/1.0/', + instrumentationSettings.instrumentationKey = 'f9b6e63b5e394453ba8f58f7a7b9aea7-f38fcfa2-eb34-48bc-9ae2-61fba4abbd39-7390' //prod key; break; default: instrumentationSettings.endpointURL = 'https://us-mobile.events.data.microsoft.com/OneCollector/1.0/', - instrumentationSettings.instrumentationKey = '197418c5cb8c4426b201f9db2e87b914-87887378-2790-49b0-9295-51f43b6204b1-7172' + instrumentationSettings.instrumentationKey = '197418c5cb8c4426b201f9db2e87b914-87887378-2790-49b0-9295-51f43b6204b1-7172' break; } break; - case 'gov': - case 'high': - case 'dod': - case 'mooncake': - instrumentationSettings.endpointURL = '', - instrumentationSettings.instrumentationKey = '' //prod key; - break; case 'ex': case 'rx': default: diff --git a/src/common/OneDSLoggerTelemetry/oneDSLoggerWrapper.ts b/src/common/OneDSLoggerTelemetry/oneDSLoggerWrapper.ts index 6a85c269..47c034e9 100644 --- a/src/common/OneDSLoggerTelemetry/oneDSLoggerWrapper.ts +++ b/src/common/OneDSLoggerTelemetry/oneDSLoggerWrapper.ts @@ -14,8 +14,8 @@ export class oneDSLoggerWrapper { private static instance: oneDSLoggerWrapper; private static oneDSLoggerIntance: OneDSLogger; - private constructor(geo?: string) { - oneDSLoggerWrapper.oneDSLoggerIntance = new OneDSLogger(geo); + private constructor(geo?: string, geoLongName?: string) { + oneDSLoggerWrapper.oneDSLoggerIntance = new OneDSLogger(geo, geoLongName); } @@ -23,8 +23,8 @@ export class oneDSLoggerWrapper { return this.instance; } - static instantiate(geo?: string) { - oneDSLoggerWrapper.instance = new oneDSLoggerWrapper(geo); + static instantiate(geo?:string, geoLongName?: string){ + oneDSLoggerWrapper.instance = new oneDSLoggerWrapper(geo, geoLongName); } /// Trace info log diff --git a/src/common/TelemetryConstants.ts b/src/common/TelemetryConstants.ts new file mode 100644 index 00000000..6cc6353f --- /dev/null +++ b/src/common/TelemetryConstants.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +// Telemetry Event Names +export const VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_STARTED = "VSCodeExtensionDataVerseAuthenticationStarted"; +export const VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED = "VSCodeExtensionDataVerseAuthenticationFailed"; +export const VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_MISSING = "VSCodeExtensionDataVerseAuthenticationMissing"; +export const VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_COMPLETED = "VSCodeExtensionDataVerseAuthenticationCompleted"; +export const VSCODE_EXTENSION_NPS_AUTHENTICATION_STARTED = "VSCodeExtensionNPSAuthenticationStarted"; +export const VSCODE_EXTENSION_NPS_AUTHENTICATION_COMPLETED = "VSCodeExtensionNPSAuthenticationCompleted"; +export const VSCODE_EXTENSION_NPS_AUTHENTICATION_FAILED = "VSCodeExtensionNPSAuthenticationFailed"; +export const VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_FAILED = "VSCodeExtensionGraphClientAuthenticationFailed"; +export const VSCODE_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED = "VSCodeExtensionGraphClientAuthenticationCompleted"; diff --git a/src/common/Utils.ts b/src/common/Utils.ts index 8ed0aa12..0eddc9e2 100644 --- a/src/common/Utils.ts +++ b/src/common/Utils.ts @@ -7,6 +7,8 @@ import * as vscode from "vscode"; import { EXTENSION_ID, EXTENSION_NAME, SETTINGS_EXPERIMENTAL_STORE_NAME } from "../client/constants"; import { CUSTOM_TELEMETRY_FOR_POWER_PAGES_SETTING_NAME } from "./OneDSLoggerTelemetry/telemetryConstants"; +import { PacWrapper } from "../client/pac/PacWrapper"; +import { AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, PAC_SUCCESS } from "./copilot/constants"; export function getSelectedCode(editor: vscode.TextEditor): string { if (!editor) { @@ -131,4 +133,22 @@ export function getUserAgent(): string { .replace("{product}", EXTENSION_NAME) .replace("{product-version}", getExtensionVersion()) .replace("{comment}", "(" + getExtensionType()+'; )'); -} \ No newline at end of file +} + +export async function createAuthProfileExp(pacWrapper: PacWrapper | undefined) { + const userOrgUrl = await showInputBoxAndGetOrgUrl(); + if (!userOrgUrl) { + return; + } + + if(!pacWrapper){ + vscode.window.showErrorMessage(AUTH_CREATE_FAILED); + return; + } + + const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); + if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== PAC_SUCCESS) { + vscode.window.showErrorMessage(AUTH_CREATE_FAILED); + return; + } +} diff --git a/src/common/copilot/PowerPagesCopilot.ts b/src/common/copilot/PowerPagesCopilot.ts index 6893cbcd..e5049451 100644 --- a/src/common/copilot/PowerPagesCopilot.ts +++ b/src/common/copilot/PowerPagesCopilot.ts @@ -6,13 +6,13 @@ import * as vscode from "vscode"; import { sendApiRequest } from "./IntelligenceApiService"; -import { dataverseAuthentication, intelligenceAPIAuthentication } from "../../web/client/common/authenticationProvider"; +import { dataverseAuthentication, intelligenceAPIAuthentication } from "../AuthenticationProvider"; import { v4 as uuidv4 } from 'uuid' import { PacWrapper } from "../../client/pac/PacWrapper"; import { ITelemetry } from "../../client/telemetry/ITelemetry"; import { ADX_ENTITYFORM, ADX_ENTITYLIST, AUTH_CREATE_FAILED, AUTH_CREATE_MESSAGE, AuthProfileNotFound, COPILOT_UNAVAILABLE, CopilotDisclaimer, CopilotStylePathSegments, DataverseEntityNameMap, EXPLAIN_CODE, EntityFieldMap, FieldTypeMap, PAC_SUCCESS, SELECTED_CODE_INFO, SELECTED_CODE_INFO_ENABLED, THUMBS_DOWN, THUMBS_UP, UserPrompt, WebViewMessage, sendIconSvg } from "./constants"; import { IActiveFileParams, IActiveFileData, IOrgInfo } from './model'; -import { escapeDollarSign, getLastThreePartsOfFileName, getNonce, getSelectedCode, getSelectedCodeLineRange, getUserName, openWalkthrough, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../Utils"; +import { createAuthProfileExp, escapeDollarSign, getLastThreePartsOfFileName, getNonce, getSelectedCode, getSelectedCodeLineRange, getUserName, openWalkthrough, showConnectedOrgMessage, showInputBoxAndGetOrgUrl, showProgressWithNotification } from "../Utils"; import { CESUserFeedback } from "./user-feedback/CESSurvey"; import { ActiveOrgOutput } from "../../client/pac/PacTypes"; import { CopilotWalkthroughEvent, CopilotCopyCodeToClipboardEvent, CopilotInsertCodeToEditorEvent, CopilotLoadedEvent, CopilotOrgChangedEvent, CopilotUserFeedbackThumbsDownEvent, CopilotUserFeedbackThumbsUpEvent, CopilotUserPromptedEvent, CopilotCodeLineCountEvent, CopilotClearChatEvent, CopilotNotAvailable, CopilotExplainCode, CopilotExplainCodeSize, CopilotNotAvailableECSConfig } from "./telemetry/telemetryConstants"; @@ -109,7 +109,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { ); this._disposables.push( - orgChangeErrorEvent(async () => await this.createAuthProfileExp()) + orgChangeErrorEvent(async () => await createAuthProfileExp(this._pacWrapper)) ); if (orgInfo) { @@ -131,19 +131,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { if (pacOutput && pacOutput.Status === PAC_SUCCESS) { this.handleOrgChangeSuccess(pacOutput.Results); } else if (this._view?.visible) { - await this.createAuthProfileExp(); - } - } - - private async createAuthProfileExp() { - const userOrgUrl = await showInputBoxAndGetOrgUrl(); - if (!userOrgUrl) { - return; - } - const pacAuthCreateOutput = await showProgressWithNotification(vscode.l10n.t(AUTH_CREATE_MESSAGE), async () => { return await this._pacWrapper?.authCreateNewAuthProfileForOrg(userOrgUrl) }); - if (pacAuthCreateOutput && pacAuthCreateOutput.Status !== PAC_SUCCESS) { - vscode.window.showErrorMessage(AUTH_CREATE_FAILED); - return; + await createAuthProfileExp(this._pacWrapper) } } @@ -373,7 +361,7 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider { if (activeFileParams.dataverseEntity == ADX_ENTITYFORM || activeFileParams.dataverseEntity == ADX_ENTITYLIST) { metadataInfo = await getEntityName(telemetry, sessionID, activeFileParams.dataverseEntity); - const dataverseToken = (await dataverseAuthentication(activeOrgUrl, true)).accessToken; + const dataverseToken = (await dataverseAuthentication(telemetry, activeOrgUrl, true)).accessToken; if (activeFileParams.dataverseEntity == ADX_ENTITYFORM) { const formColumns = await getFormXml(metadataInfo.entityName, metadataInfo.formName, activeOrgUrl, dataverseToken, telemetry, sessionID); diff --git a/src/common/copilot/telemetry/ITelemetry.ts b/src/common/copilot/telemetry/ITelemetry.ts index 8702df1e..ee5a327e 100644 --- a/src/common/copilot/telemetry/ITelemetry.ts +++ b/src/common/copilot/telemetry/ITelemetry.ts @@ -7,7 +7,7 @@ export interface IProDevCopilotTelemetryData { eventName: string, durationInMills?: number, exception?: Error, - copilotSessionId: string, + copilotSessionId?: string, orgId?: string, FeedbackId?: string error?: Error, @@ -21,4 +21,6 @@ export interface IProDevCopilotTelemetryData { tokenSize?: string isSuggestedPrompt?: string; subScenario?: string; + userId?: string; + errorMsg?: string; } diff --git a/src/common/copilot/telemetry/telemetryConstants.ts b/src/common/copilot/telemetry/telemetryConstants.ts index c9723806..9baddb0d 100644 --- a/src/common/copilot/telemetry/telemetryConstants.ts +++ b/src/common/copilot/telemetry/telemetryConstants.ts @@ -36,3 +36,4 @@ export const CopilotNotAvailable = 'CopilotNotAvailable'; export const CopilotNotAvailableECSConfig = 'CopilotNotAvailableECSConfig'; export const CopilotExplainCode = 'CopilotExplainCode'; export const CopilotExplainCodeSize = 'CopilotExplainCodeSize'; +export const CopilotNpsAuthenticationCompleted = "CopilotNpsAuthenticationCompleted"; diff --git a/src/common/copilot/user-feedback/CESSurvey.ts b/src/common/copilot/user-feedback/CESSurvey.ts index 2b2a4dd9..025315ed 100644 --- a/src/common/copilot/user-feedback/CESSurvey.ts +++ b/src/common/copilot/user-feedback/CESSurvey.ts @@ -4,20 +4,21 @@ */ import * as vscode from "vscode"; -import { npsAuthentication } from "../../../web/client/common/authenticationProvider"; +import { npsAuthentication } from "../../AuthenticationProvider"; import { SurveyConstants } from "../../../web/client/common/constants"; import fetch from "node-fetch"; import { getNonce } from "../../Utils"; import { ITelemetry } from "../../../client/telemetry/ITelemetry"; -import { CopilotUserFeedbackFailureEvent, CopilotUserFeedbackSuccessEvent } from "../telemetry/telemetryConstants"; +import { CopilotNpsAuthenticationCompleted, CopilotUserFeedbackFailureEvent, CopilotUserFeedbackSuccessEvent } from "../telemetry/telemetryConstants"; import { sendTelemetryEvent } from "../telemetry/copilotTelemetry"; import { IFeedbackData } from "../model"; import { EUROPE_GEO, UK_GEO } from "../constants"; +import { ERRORS } from "../../ErrorConstants"; let feedbackPanel: vscode.WebviewPanel | undefined; -export async function CESUserFeedback(context: vscode.ExtensionContext, sessionId: string, userID: string, thumbType: string, telemetry: ITelemetry, geoName: string, messageScenario: string, tenantId?: string) { +export async function CESUserFeedback(context: vscode.ExtensionContext, sessionId: string, userID: string, thumbType: string, telemetry: ITelemetry, geoName: string, messageScenario: string, tenantId?: string) { if (feedbackPanel) { feedbackPanel.dispose(); @@ -35,7 +36,13 @@ export async function CESUserFeedback(context: vscode.ExtensionContext, sessionI const feedbackData = initializeFeedbackData(sessionId, vscode.env.uiKind === vscode.UIKind.Web, geoName, messageScenario, tenantId); - const apiToken: string = await npsAuthentication(SurveyConstants.AUTHORIZATION_ENDPOINT); + const apiToken: string = await npsAuthentication(telemetry, SurveyConstants.AUTHORIZATION_ENDPOINT); + + if (apiToken) { + sendTelemetryEvent(telemetry, { eventName: CopilotNpsAuthenticationCompleted, feedbackType: thumbType, copilotSessionId: sessionId }); + } else { + sendTelemetryEvent(telemetry, { eventName: CopilotUserFeedbackFailureEvent, feedbackType: thumbType, copilotSessionId: sessionId, error: new Error(ERRORS.NPS_FAILED_AUTH) }); + } const endpointUrl = useEUEndpoint(geoName) ? `https://europe.ces.microsoftcloud.com/api/v1/portalsdesigner/Surveys/powerpageschatgpt/Feedbacks?userId=${userID}` : `https://world.ces.microsoftcloud.com/api/v1/portalsdesigner/Surveys/powerpageschatgpt/Feedbacks?userId=${userID}`; @@ -79,7 +86,7 @@ function getWebviewURIs(context: vscode.ExtensionContext, feedbackPanel: vscode. return { feedbackCssUri, feedbackJsUri }; } -function initializeFeedbackData(sessionId: string, isWebExtension: boolean, geoName: string, messageScenario: string, tenantId?: string): IFeedbackData { +function initializeFeedbackData(sessionId: string, isWebExtension: boolean, geoName: string, messageScenario: string, tenantId?: string): IFeedbackData { const feedbackData: IFeedbackData = { TenantId: tenantId ? tenantId : '', Geo: geoName, diff --git a/src/web/client/WebExtensionContext.ts b/src/web/client/WebExtensionContext.ts index 13778f90..b3166d48 100644 --- a/src/web/client/WebExtensionContext.ts +++ b/src/web/client/WebExtensionContext.ts @@ -7,7 +7,7 @@ import * as vscode from "vscode"; import { dataverseAuthentication, getCommonHeadersForDataverse, -} from "./common/authenticationProvider"; +} from "../../common/AuthenticationProvider"; import * as Constants from "./common/constants"; import { getDataSourcePropertiesMap, @@ -371,6 +371,7 @@ class WebExtensionContext implements IWebExtensionContext { Constants.queryParameters.ORG_URL ) as string; const { accessToken, userId } = await dataverseAuthentication( + this._telemetry.getTelemetryReporter(), dataverseOrgUrl, firstTimeAuth ); @@ -396,6 +397,15 @@ class WebExtensionContext implements IWebExtensionContext { this._dataverseAccessToken = accessToken; this._userId = userId; + + if (firstTimeAuth) { + this._telemetry.sendInfoTelemetry( + telemetryEventNames.WEB_EXTENSION_DATAVERSE_AUTHENTICATION_COMPLETED, + { + userId: userId + } + ); + } } public async updateFileDetailsInContext( diff --git a/src/web/client/common/errorHandler.ts b/src/web/client/common/errorHandler.ts index 5e2658a9..b209d2d1 100644 --- a/src/web/client/common/errorHandler.ts +++ b/src/web/client/common/errorHandler.ts @@ -10,55 +10,6 @@ import { telemetryEventNames } from "../telemetry/constants"; import { PORTALS_FOLDER_NAME_DEFAULT, queryParameters } from "./constants"; import { isMultifileEnabled } from "../utilities/commonUtil"; -export const ERRORS = { - SUBURI_EMPTY: "SubURI value for entity file is empty", - NO_ACCESS_TOKEN: "No access token was created", - PORTAL_FOLDER_NAME_EMPTY: "portalFolderName value for entity file is empty", - ATTRIBUTES_EMPTY: "Entity file attribute or extension field empty", - WORKSPACE_INITIAL_LOAD: "There was a problem opening the workspace", - WORKSPACE_INITIAL_LOAD_DESC: "Try refreshing the browser", - UNKNOWN_APP: "Unable to find that app", - AUTHORIZATION_FAILED: - "Authorization Failed. Please run again to authorize it", - FILE_NOT_FOUND: "The file was not found", - RETRY_LIMIT_EXCEEDED: "Unable to complete that operation", - RETRY_LIMIT_EXCEEDED_DESC: "You've exceeded the retry limit ", - PRECONDITION_CHECK_FAILED: "The precondition check did not work", - PRECONDITION_CHECK_FAILED_DESC: "Try again", - SERVER_ERROR_RETRY_LATER: "There was a problem with the server", - SERVER_ERROR_RETRY_LATER_DESC: "Please try again in a minute or two", - SERVER_ERROR_PERMISSION_DENIED: - "There was a permissions problem with the server", - SERVER_ERROR_PERMISSION_DENIED_DESC: "Please try again in a minute or two", - EMPTY_RESPONSE: "There was no response", - EMPTY_RESPONSE_DESC: "Try again", - THRESHOLD_LIMIT_EXCEEDED: "Threshold for dataverse api", - THRESHOLD_LIMIT_EXCEEDED_DESC: - "You’ve exceeded the threshold rate limit for the Dataverse API", - BAD_REQUEST: "Unable to complete the request", - BAD_REQUEST_DESC: - "One or more attribute names have been changed or removed. Contact your admin.", - BACKEND_ERROR: "There’s a problem on the back end", - SERVICE_UNAVAILABLE: "There’s a problem connecting to Dataverse", - SERVICE_ERROR: "There’s a problem connecting to Dataverse", - INVALID_ARGUMENT: "One or more commands are invalid or malformed", - BACKEND_ERROR_DESC: "Try again", - SERVICE_UNAVAILABLE_DESC: "Try again", - SERVICE_ERROR_DESC: "Try again", - INVALID_ARGUMENT_DESC: "Check the parameters and try again", - MANDATORY_PARAMETERS_NULL: "The workspace is not available ", - MANDATORY_PARAMETERS_NULL_DESC: - "Check the URL and verify the parameters are correct", - FILE_NAME_NOT_SET: "That file is not available", - FILE_NAME_NOT_SET_DESC: - "The metadata may have changed in the Dataverse side. Contact your admin. {message_attribute}", - FILE_NAME_EMPTY: "File name is empty", - FILE_ID_EMPTY: "File ID is empty", - LANGUAGE_CODE_ID_VALUE_NULL: "Language code ID is empty", - LANGUAGE_CODE_EMPTY: "Language code is empty", - BULKHEAD_LIMITS_EXCEEDED: "Bulkhead queue limits exceeded", -}; - export function showErrorDialog(errorString: string, detailMessage?: string) { const options = { detail: detailMessage, modal: true }; vscode.window.showErrorMessage(errorString, options); diff --git a/src/web/client/dal/concurrencyHandler.ts b/src/web/client/dal/concurrencyHandler.ts index 3874b299..4bb218fc 100644 --- a/src/web/client/dal/concurrencyHandler.ts +++ b/src/web/client/dal/concurrencyHandler.ts @@ -6,9 +6,9 @@ import { BulkheadRejectedError, bulkhead } from 'cockatiel'; import fetch, { RequestInfo, RequestInit } from "node-fetch"; import { MAX_CONCURRENT_REQUEST_COUNT, MAX_CONCURRENT_REQUEST_QUEUE_COUNT } from '../common/constants'; -import { ERRORS } from '../common/errorHandler'; import WebExtensionContext from "../WebExtensionContext"; import { telemetryEventNames } from '../telemetry/constants'; +import { ERRORS } from '../../../common/ErrorConstants'; export class ConcurrencyHandler { private _bulkhead = bulkhead(MAX_CONCURRENT_REQUEST_COUNT, MAX_CONCURRENT_REQUEST_QUEUE_COUNT); @@ -32,4 +32,4 @@ export class ConcurrencyHandler { } } } -} \ No newline at end of file +} diff --git a/src/web/client/dal/fileSystemProvider.ts b/src/web/client/dal/fileSystemProvider.ts index fa96e42d..dacc62f6 100644 --- a/src/web/client/dal/fileSystemProvider.ts +++ b/src/web/client/dal/fileSystemProvider.ts @@ -13,7 +13,6 @@ import { import WebExtensionContext from "../WebExtensionContext"; import { fetchDataFromDataverseAndUpdateVFS } from "./remoteFetchProvider"; import { saveData } from "./remoteSaveProvider"; -import { ERRORS } from "../common/errorHandler"; import { telemetryEventNames } from "../telemetry/constants"; import { getFolderSubUris } from "../utilities/folderHelperUtility"; import { EtagHandlerService } from "../services/etagHandlerService"; @@ -32,6 +31,7 @@ import { } from "../utilities/fileAndEntityUtil"; import { getImageFileContent, getRangeForMultilineMatch, isImageFileSupportedForEdit, isVersionControlEnabled, updateFileContentInFileDataMap } from "../utilities/commonUtil"; import { IFileInfo, ISearchQueryMatch, ISearchQueryResults } from "../common/interfaces"; +import { ERRORS } from "../../../common/ErrorConstants"; export class File implements vscode.FileStat { type: vscode.FileType; diff --git a/src/web/client/dal/remoteFetchProvider.ts b/src/web/client/dal/remoteFetchProvider.ts index 4cdbc957..99168707 100644 --- a/src/web/client/dal/remoteFetchProvider.ts +++ b/src/web/client/dal/remoteFetchProvider.ts @@ -15,9 +15,9 @@ import { setFileContent, } from "../utilities/commonUtil"; import { getCustomRequestURL, getMappingEntityContent, getMetadataInfo, getMappingEntityId, getMimeType, getRequestURL } from "../utilities/urlBuilderUtil"; -import { getCommonHeadersForDataverse } from "../common/authenticationProvider"; +import { getCommonHeadersForDataverse } from "../../../common/AuthenticationProvider"; import * as Constants from "../common/constants"; -import { ERRORS, showErrorDialog } from "../common/errorHandler"; +import { showErrorDialog } from "../common/errorHandler"; import { PortalsFS } from "./fileSystemProvider"; import { encodeAsBase64, @@ -32,6 +32,7 @@ import { EntityMetadataKeyCore, SchemaEntityMetadata, folderExportType, schemaEn import { getEntityNameForExpandedEntityContent, getRequestUrlForEntities } from "../utilities/folderHelperUtility"; import { IAttributePath, IFileInfo } from "../common/interfaces"; import { portal_schema_V2 } from "../schema/portalSchema"; +import { ERRORS } from "../../../common/ErrorConstants"; export async function fetchDataFromDataverseAndUpdateVFS( portalFs: PortalsFS, diff --git a/src/web/client/dal/remoteSaveProvider.ts b/src/web/client/dal/remoteSaveProvider.ts index ae108f1b..dc7db03a 100644 --- a/src/web/client/dal/remoteSaveProvider.ts +++ b/src/web/client/dal/remoteSaveProvider.ts @@ -5,7 +5,7 @@ import { RequestInit } from "node-fetch"; import * as vscode from "vscode"; -import { getCommonHeadersForDataverse } from "../common/authenticationProvider"; +import { getCommonHeadersForDataverse } from "../../../common/AuthenticationProvider"; import { BAD_REQUEST, MIMETYPE, queryParameters } from "../common/constants"; import { showErrorDialog } from "../common/errorHandler"; import { FileData } from "../context/fileData"; @@ -231,4 +231,4 @@ async function saveDataToDataverse( throw error; } } -} \ No newline at end of file +} diff --git a/src/web/client/extension.ts b/src/web/client/extension.ts index 2250b2c8..168d6ac4 100644 --- a/src/web/client/extension.ts +++ b/src/web/client/extension.ts @@ -308,6 +308,12 @@ export function processWorkspaceStateChanges(context: vscode.ExtensionContext) { if (entityInfo.entityId && entityInfo.entityName) { context.workspaceState.update(document.uri.fsPath, entityInfo); WebExtensionContext.updateVscodeWorkspaceState(document.uri.fsPath, entityInfo); + + if (isCoPresenceEnabled() && tab.input instanceof vscode.TabInputCustom) { + // sending message to webworker event listener for Co-Presence feature + sendingMessageToWebWorkerForCoPresence(entityInfo); + WebExtensionContext.quickPickProvider.refresh(); + } } } }); diff --git a/src/web/client/services/NPSService.ts b/src/web/client/services/NPSService.ts index b4a2a928..5ee290c4 100644 --- a/src/web/client/services/NPSService.ts +++ b/src/web/client/services/NPSService.ts @@ -4,7 +4,7 @@ */ import jwt_decode from 'jwt-decode'; -import { npsAuthentication } from "../common/authenticationProvider"; +import { npsAuthentication } from "../../../common/AuthenticationProvider"; import { SurveyConstants, httpMethod, queryParameters } from '../common/constants'; import { RequestInit } from 'node-fetch' import WebExtensionContext from '../WebExtensionContext'; @@ -12,85 +12,90 @@ import { telemetryEventNames } from '../telemetry/constants'; import { getCurrentDataBoundary } from '../utilities/dataBoundary'; export class NPSService { - public static getCesHeader(accessToken: string) { - return { - authorization: "Bearer " + accessToken, - Accept: 'application/json', - 'Content-Type': 'application/json', - }; - } + public static getCesHeader(accessToken: string) { + return { + authorization: "Bearer " + accessToken, + Accept: 'application/json', + 'Content-Type': 'application/json', + }; + } - public static getNpsSurveyEndpoint(): string { - const region = WebExtensionContext.urlParametersMap?.get(queryParameters.REGION)?.toLowerCase(); - const dataBoundary = getCurrentDataBoundary(); - let npsSurveyEndpoint = ''; - switch (region) { - case 'tie': - case 'test': - case 'preprod': - switch (dataBoundary) { - case 'eu': - npsSurveyEndpoint = 'https://europe.tip1.ces.microsoftcloud.com'; - break; - default: - npsSurveyEndpoint = 'https://world.tip1.ces.microsoftcloud.com'; - } - break; - case 'prod': - case 'preview': - switch (dataBoundary) { - case 'eu': - npsSurveyEndpoint = 'https://europe.ces.microsoftcloud.com'; - break; - default: - npsSurveyEndpoint = 'https://world.ces.microsoftcloud.com'; + public static getNpsSurveyEndpoint(): string { + const region = WebExtensionContext.urlParametersMap?.get(queryParameters.REGION)?.toLowerCase(); + const dataBoundary = getCurrentDataBoundary(); + let npsSurveyEndpoint = ''; + switch (region) { + case 'tie': + case 'test': + case 'preprod': + switch (dataBoundary) { + case 'eu': + npsSurveyEndpoint = 'https://europe.tip1.ces.microsoftcloud.com'; + break; + default: + npsSurveyEndpoint = 'https://world.tip1.ces.microsoftcloud.com'; + } + break; + case 'prod': + case 'preview': + switch (dataBoundary) { + case 'eu': + npsSurveyEndpoint = 'https://europe.ces.microsoftcloud.com'; + break; + default: + npsSurveyEndpoint = 'https://world.ces.microsoftcloud.com'; + } + break; + case 'gov': + case 'high': + case 'dod': + case 'mooncake': + npsSurveyEndpoint = 'https://world.ces.microsoftcloud.com'; + break; + case 'ex': + case 'rx': + default: + break; } - break; - case 'gov': - case 'high': - case 'dod': - case 'mooncake': - npsSurveyEndpoint = 'https://world.ces.microsoftcloud.com'; - break; - case 'ex': - case 'rx': - default: - break; + + return npsSurveyEndpoint; } - return npsSurveyEndpoint; - } + public static async setEligibility() { + try { - public static async setEligibility() { - try { + const baseApiUrl = this.getNpsSurveyEndpoint(); + const accessToken: string = await npsAuthentication(WebExtensionContext.telemetry.getTelemetryReporter(), SurveyConstants.AUTHORIZATION_ENDPOINT); - const baseApiUrl = this.getNpsSurveyEndpoint(); - const accessToken: string = await npsAuthentication(SurveyConstants.AUTHORIZATION_ENDPOINT); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const parsedToken = jwt_decode(accessToken) as any; - WebExtensionContext.setUserId(parsedToken?.oid) - const apiEndpoint = `${baseApiUrl}/api/v1/${SurveyConstants.TEAM_NAME}/Eligibilities/${SurveyConstants.SURVEY_NAME}?userId=${parsedToken?.oid}&eventName=${SurveyConstants.EVENT_NAME}&tenantId=${parsedToken.tid}`; - const requestInitPost: RequestInit = { - method: httpMethod.POST, - body: '{}', - headers: NPSService.getCesHeader(accessToken) - }; - const requestSentAtTime = new Date().getTime(); - const response = await WebExtensionContext.concurrencyHandler.handleRequest(apiEndpoint, requestInitPost); - const result = await response?.json(); - if (result?.Eligibility) { - WebExtensionContext.telemetry.sendAPISuccessTelemetry( - telemetryEventNames.NPS_USER_ELIGIBLE, - "NPS Api", - httpMethod.POST, - new Date().getTime() - requestSentAtTime, - this.setEligibility.name - ); - WebExtensionContext.setNPSEligibility(true); - WebExtensionContext.setFormsProEligibilityId(result?.FormsProEligibilityId); - } - } catch (error) { - WebExtensionContext.telemetry.sendErrorTelemetry(telemetryEventNames.NPS_API_FAILED, this.setEligibility.name,(error as Error)?.message, error as Error); + if (accessToken) { + WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_NPS_AUTHENTICATION_COMPLETED); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const parsedToken = jwt_decode(accessToken) as any; + WebExtensionContext.setUserId(parsedToken?.oid) + const apiEndpoint = `${baseApiUrl}/api/v1/${SurveyConstants.TEAM_NAME}/Eligibilities/${SurveyConstants.SURVEY_NAME}?userId=${parsedToken?.oid}&eventName=${SurveyConstants.EVENT_NAME}&tenantId=${parsedToken.tid}`; + const requestInitPost: RequestInit = { + method: httpMethod.POST, + body: '{}', + headers: NPSService.getCesHeader(accessToken) + }; + const requestSentAtTime = new Date().getTime(); + const response = await WebExtensionContext.concurrencyHandler.handleRequest(apiEndpoint, requestInitPost); + const result = await response?.json(); + if (result?.Eligibility) { + WebExtensionContext.telemetry.sendAPISuccessTelemetry( + telemetryEventNames.WEB_EXTENSION_NPS_USER_ELIGIBLE, + "NPS Api", + httpMethod.POST, + new Date().getTime() - requestSentAtTime, + this.setEligibility.name + ); + WebExtensionContext.setNPSEligibility(true); + WebExtensionContext.setFormsProEligibilityId(result?.FormsProEligibilityId); + } + } catch (error) { + WebExtensionContext.telemetry.sendErrorTelemetry(telemetryEventNames.WEB_EXTENSION_NPS_API_FAILED, this.setEligibility.name, (error as Error)?.message, error as Error); + } } - } -} \ No newline at end of file +} diff --git a/src/web/client/services/etagHandlerService.ts b/src/web/client/services/etagHandlerService.ts index 5b3d298d..75178787 100644 --- a/src/web/client/services/etagHandlerService.ts +++ b/src/web/client/services/etagHandlerService.ts @@ -5,7 +5,7 @@ import * as vscode from "vscode"; import { RequestInit } from "node-fetch"; -import { getCommonHeadersForDataverse } from "../common/authenticationProvider"; +import { getCommonHeadersForDataverse } from "../../../common/AuthenticationProvider"; import { httpMethod, ODATA_ETAG, queryParameters } from "../common/constants"; import { IAttributePath } from "../common/interfaces"; import { PortalsFS } from "../dal/fileSystemProvider"; diff --git a/src/web/client/services/graphClientService.ts b/src/web/client/services/graphClientService.ts index ce6c4945..bb97a6f5 100644 --- a/src/web/client/services/graphClientService.ts +++ b/src/web/client/services/graphClientService.ts @@ -5,7 +5,7 @@ import path from "path"; import WebExtensionContext from "../WebExtensionContext"; -import { getCommonHeaders, graphClientAuthentication } from "../common/authenticationProvider"; +import { getCommonHeaders, graphClientAuthentication } from "../../../common/AuthenticationProvider"; import * as Constants from "../common/constants"; import { telemetryEventNames } from "../telemetry/constants"; @@ -21,7 +21,20 @@ export class GraphClientService { } public async graphClientAuthentication(firstTimeAuth = false) { - const accessToken = await graphClientAuthentication(firstTimeAuth); + const accessToken = await graphClientAuthentication(WebExtensionContext.telemetry.getTelemetryReporter(), firstTimeAuth); + if (!accessToken) { + WebExtensionContext.telemetry.sendErrorTelemetry( + telemetryEventNames.WEB_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_FAILED, + graphClientAuthentication.name + ); + } + + if (firstTimeAuth && accessToken) { + WebExtensionContext.telemetry.sendInfoTelemetry( + telemetryEventNames.WEB_EXTENSION_GRAPH_CLIENT_AUTHENTICATION_COMPLETED + ); + } + this._graphToken = accessToken; } diff --git a/src/web/client/telemetry/constants.ts b/src/web/client/telemetry/constants.ts index 730612f6..76399dd5 100644 --- a/src/web/client/telemetry/constants.ts +++ b/src/web/client/telemetry/constants.ts @@ -35,13 +35,13 @@ export enum telemetryEventNames { WEB_EXTENSION_ENTITY_CONTENT_CHANGED = "WebExtensionEntityConentChanged", WEB_EXTENSION_ENTITY_CONTENT_SAME = "WebExtensionEntityContentSame", WEB_EXTENSION_ENTITY_CONTENT_UNEXPECTED_RESPONSE = "WebExtensionEntityContentUnexpectedResponse", - NPS_AUTHENTICATION_STARTED = "WebExtensionNPSAuthenticationStarted", - NPS_AUTHENTICATION_COMPLETED = "WebExtensionNPSAuthenticationCompleted", - NPS_AUTHENTICATION_FAILED = "WebExtensionNPSAuthenticationFailed", - NPS_USER_ELIGIBLE = "WebExtensionUserIsEligible", - NPS_API_FAILED = "WebExtensionNPSApiFailed", - RENDER_NPS = "WebExtensionNPSRenderSurveyForm", - RENDER_NPS_FAILED = "WebExtensionNPSRenderSurveyFormFailed", + WEB_EXTENSION_NPS_AUTHENTICATION_STARTED = "WebExtensionNPSAuthenticationStarted", + WEB_EXTENSION_NPS_AUTHENTICATION_COMPLETED = "WebExtensionNPSAuthenticationCompleted", + WEB_EXTENSION_NPS_AUTHENTICATION_FAILED = "WebExtensionNPSAuthenticationFailed", + WEB_EXTENSION_NPS_USER_ELIGIBLE = "WebExtensionUserIsEligible", + WEB_EXTENSION_NPS_API_FAILED = "WebExtensionNPSApiFailed", + WEB_EXTENSION_RENDER_NPS = "WebExtensionNPSRenderSurveyForm", + WEB_EXTENSION_RENDER_NPS_FAILED = "WebExtensionNPSRenderSurveyFormFailed", WEB_EXTENSION_CREATE_ENTITY_FOLDER = "WebExtensionCreateEntityFolder", WEB_EXTENSION_FILE_HAS_DIRTY_CHANGES = "WebExtensionFileHasDirtyChanges", WEB_EXTENSION_DIFF_VIEW_TRIGGERED = "WebExtensionDiffViewTriggered", diff --git a/src/web/client/test/integration/AuthenticationProvider.test.ts b/src/web/client/test/integration/AuthenticationProvider.test.ts index 04ec0c01..76fec74d 100644 --- a/src/web/client/test/integration/AuthenticationProvider.test.ts +++ b/src/web/client/test/integration/AuthenticationProvider.test.ts @@ -8,12 +8,14 @@ import { expect } from "chai"; import { dataverseAuthentication, getCommonHeaders, -} from "../../common/authenticationProvider"; +} from "../../../../common/AuthenticationProvider"; import vscode from "vscode"; -import WebExtensionContext from "../../WebExtensionContext"; -import { telemetryEventNames } from "../../telemetry/constants"; import * as errorHandler from "../../common/errorHandler"; import { oneDSLoggerWrapper } from "../../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import * as copilotTelemetry from "../../../../common/copilot/telemetry/copilotTelemetry"; +import { WebExtensionTelemetry } from "../../telemetry/webExtensionTelemetry"; +import { vscodeExtAppInsightsResourceProvider } from "../../../../common/telemetry-generated/telemetryConfiguration"; +import { VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED } from "../../../../common/TelemetryConstants"; // eslint-disable-next-line @typescript-eslint/no-explicit-any let traceError: any @@ -32,6 +34,16 @@ describe("Authentication Provider", () => { // Restore the default sandbox here sinon.restore(); }); + + const webExtensionTelemetry = new WebExtensionTelemetry(); + const appInsightsResource = + vscodeExtAppInsightsResourceProvider.GetAppInsightsResourceForDataBoundary( + undefined + ); + webExtensionTelemetry.setTelemetryReporter("", "", appInsightsResource); + + const telemetry = webExtensionTelemetry.getTelemetryReporter(); + it("getHeader", () => { const accessToken = "f068ee9f-a010-47b9-b1e1-7e6353730e7d"; const result = getCommonHeaders(accessToken); @@ -54,7 +66,7 @@ describe("Authentication Provider", () => { scopes: [], }); - const result = await dataverseAuthentication(dataverseOrgURL); + const result = await dataverseAuthentication(telemetry, dataverseOrgURL); sinon.assert.calledOnce(_mockgetSession); expect(result.accessToken).eq("f068ee9f-a010-47b9-b1e1-7e6353730e7d"); expect(result.userId).empty; @@ -76,26 +88,21 @@ describe("Authentication Provider", () => { const showErrorDialog = sinon.spy(errorHandler, "showErrorDialog"); - const sendErrorTelemetry = sinon.spy( - WebExtensionContext.telemetry, - "sendErrorTelemetry" + const sendTelemetryEvent = sinon.spy( + copilotTelemetry, + "sendTelemetryEvent" ); //Act - await dataverseAuthentication(dataverseOrgURL); + await dataverseAuthentication(telemetry, dataverseOrgURL); sinon.assert.calledWith( showErrorDialog, "Authorization Failed. Please run again to authorize it" ); - sinon.assert.calledWith( - sendErrorTelemetry, - telemetryEventNames.WEB_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED - ); - + sinon.assert.calledOnce(sendTelemetryEvent); sinon.assert.calledOnce(showErrorDialog); - sinon.assert.calledOnce(sendErrorTelemetry); sinon.assert.calledOnce(_mockgetSession); }); @@ -107,21 +114,22 @@ describe("Authentication Provider", () => { .stub(await vscode.authentication, "getSession") .throws({ message: errorMessage }); - const sendError = sinon.spy( - WebExtensionContext.telemetry, - "sendErrorTelemetry" + const sendTelemetryEvent = sinon.spy( + copilotTelemetry, + "sendTelemetryEvent" ); // Act - const result = await dataverseAuthentication(dataverseOrgURL); + const result = await dataverseAuthentication(telemetry, dataverseOrgURL); //Assert - sinon.assert.calledOnce(sendError); + sinon.assert.calledOnce(sendTelemetryEvent); sinon.assert.calledWith( - sendError, - telemetryEventNames.WEB_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED, - dataverseAuthentication.name, - errorMessage + sendTelemetryEvent, + telemetry, { + eventName: VSCODE_EXTENSION_DATAVERSE_AUTHENTICATION_FAILED, + error: { message: errorMessage } as Error + } ); sinon.assert.calledOnce(_mockgetSession); expect(result.accessToken).empty; diff --git a/src/web/client/test/integration/WebExtensionContext.test.ts b/src/web/client/test/integration/WebExtensionContext.test.ts index 9274379d..05b25fcb 100644 --- a/src/web/client/test/integration/WebExtensionContext.test.ts +++ b/src/web/client/test/integration/WebExtensionContext.test.ts @@ -11,11 +11,11 @@ import WebExtensionContext from "../../WebExtensionContext"; import { schemaKey, schemaEntityKey } from "../../schema/constants"; import * as portalSchemaReader from "../../schema/portalSchemaReader"; import * as Constants from "../../common/constants"; -import * as authenticationProvider from "../../common/authenticationProvider"; +import * as authenticationProvider from "../../../../common/AuthenticationProvider"; import { telemetryEventNames } from "../../telemetry/constants"; import * as schemaHelperUtil from "../../utilities/schemaHelperUtil"; import * as urlBuilderUtil from "../../utilities/urlBuilderUtil"; -import { getCommonHeadersForDataverse } from "../../common/authenticationProvider"; +import { getCommonHeadersForDataverse } from "../../../../common/AuthenticationProvider"; import { IAttributePath } from "../../common/interfaces"; describe("WebExtensionContext", () => { @@ -463,7 +463,7 @@ describe("WebExtensionContext", () => { ); expect(WebExtensionContext.dataverseAccessToken).eq(accessToken); - assert.calledOnceWithExactly(dataverseAuthentication, ORG_URL, true); + assert.calledOnceWithExactly(dataverseAuthentication, WebExtensionContext.telemetry.getTelemetryReporter(), ORG_URL, true); assert.callCount(sendAPISuccessTelemetry, 3); assert.calledOnceWithExactly( getLcidCodeMap, @@ -641,7 +641,7 @@ describe("WebExtensionContext", () => { ); expect(WebExtensionContext.dataverseAccessToken).eq(accessToken); - assert.calledOnceWithExactly(dataverseAuthentication, ORG_URL, true); + assert.calledOnceWithExactly(dataverseAuthentication, WebExtensionContext.telemetry.getTelemetryReporter(), ORG_URL, true); assert.notCalled(getLcidCodeMap); assert.notCalled(getWebsiteIdToLcidMap); assert.notCalled(getPortalLanguageIdToLcidMap); @@ -757,7 +757,7 @@ describe("WebExtensionContext", () => { ); expect(WebExtensionContext.dataverseAccessToken).eq(accessToken); - assert.calledOnceWithExactly(dataverseAuthentication, ORG_URL, true); + assert.calledOnceWithExactly(dataverseAuthentication, WebExtensionContext.telemetry.getTelemetryReporter(), ORG_URL, true); //#region Fetch const header = getCommonHeadersForDataverse(accessToken); assert.calledThrice(_mockFetch); diff --git a/src/web/client/test/integration/remoteFetchProvider.test.ts b/src/web/client/test/integration/remoteFetchProvider.test.ts index ac66efd1..79d84bc6 100644 --- a/src/web/client/test/integration/remoteFetchProvider.test.ts +++ b/src/web/client/test/integration/remoteFetchProvider.test.ts @@ -15,7 +15,7 @@ import { schemaEntityKey, schemaKey } from "../../schema/constants"; import * as urlBuilderUtil from "../../utilities/urlBuilderUtil"; import * as commonUtil from "../../utilities/commonUtil"; import { expect } from "chai"; -import * as authenticationProvider from "../../common/authenticationProvider"; +import * as authenticationProvider from "../../../../common/AuthenticationProvider"; import { telemetryEventNames } from "../../telemetry/constants"; describe("remoteFetchProvider", () => { @@ -209,7 +209,7 @@ describe("remoteFetchProvider", () => { assert.callCount(writeFile, 3); assert.calledOnce(updateSingleFileUrisInContext); - assert.callCount(sendInfoTelemetry, 4); + assert.callCount(sendInfoTelemetry, 5); assert.callCount(sendAPISuccessTelemetry, 4); }); @@ -399,7 +399,7 @@ describe("remoteFetchProvider", () => { expect(updateFileDetailsInContextCalls[1].args[7], "false"); assert.callCount(writeFile, 3); - assert.callCount(sendInfoTelemetry, 6); + assert.callCount(sendInfoTelemetry, 7); assert.calledOnce(executeCommand); assert.callCount(sendAPISuccessTelemetry, 4); }); @@ -1145,7 +1145,7 @@ describe("remoteFetchProvider", () => { assert.callCount(writeFile, 1); assert.calledOnce(updateSingleFileUrisInContext); - assert.callCount(sendInfoTelemetry, 4); + assert.callCount(sendInfoTelemetry, 5); assert.callCount(sendAPISuccessTelemetry, 5); }); }); diff --git a/src/web/client/webViews/NPSWebView.ts b/src/web/client/webViews/NPSWebView.ts index 35fcdb40..455af31c 100644 --- a/src/web/client/webViews/NPSWebView.ts +++ b/src/web/client/webViews/NPSWebView.ts @@ -46,7 +46,7 @@ export class NPSWebView { const formsProEligibilityId = WebExtensionContext.formsProEligibilityId; WebExtensionContext.telemetry.sendInfoTelemetry( - telemetryEventNames.RENDER_NPS + telemetryEventNames.WEB_EXTENSION_RENDER_NPS ); return ` @@ -64,7 +64,7 @@ export class NPSWebView { `; } catch (error) { WebExtensionContext.telemetry.sendErrorTelemetry( - telemetryEventNames.RENDER_NPS_FAILED, + telemetryEventNames.WEB_EXTENSION_RENDER_NPS_FAILED, this._getHtml.name, (error as Error)?.message ); diff --git a/src/web/client/webViews/QuickPickProvider.ts b/src/web/client/webViews/QuickPickProvider.ts index cf2e6507..a2ea7ed7 100644 --- a/src/web/client/webViews/QuickPickProvider.ts +++ b/src/web/client/webViews/QuickPickProvider.ts @@ -23,14 +23,18 @@ export class QuickPickProvider { } public refresh() { - if (vscode.window.activeTextEditor) { - const fileFsPath = vscode.window.activeTextEditor.document.uri.fsPath; - const entityInfo: IEntityInfo = { - entityId: getFileEntityId(fileFsPath), - entityName: getFileEntityName(fileFsPath), - rootWebPageId: getFileRootWebPageId(fileFsPath), - }; - this.updateQuickPickItems(entityInfo); + const tabGroup = vscode.window.tabGroups; + if (tabGroup.activeTabGroup && tabGroup.activeTabGroup.activeTab) { + const tab = tabGroup.activeTabGroup.activeTab; + if (tab.input instanceof vscode.TabInputCustom || tab.input instanceof vscode.TabInputText) { + const fileFsPath = tab.input.uri.fsPath; + const entityInfo: IEntityInfo = { + entityId: getFileEntityId(fileFsPath), + entityName: getFileEntityName(fileFsPath), + rootWebPageId: getFileRootWebPageId(fileFsPath), + }; + this.updateQuickPickItems(entityInfo); + } } }