Skip to content

Commit

Permalink
ECS support infra - Introducing ECS feature flag read call via a comm…
Browse files Browse the repository at this point in the history
…on module (#778)

* ECS skeleton folder

* ignore eslint for now

* Update client logic and setup way to introduce new feature gates config read

* Address nits

* Update ECS API call to support more client configs

* Make  appname generic as well

* take the telemetry changes later

* update the constant app name for power pages

* Update interface class for ecs feature flag filters and request feature config definition parameters

* Update interface definitions

* remove unused code

* cleanup

* minor renaming changes

* Update src/common/ecs-features/ecsFeatureClient.ts

Co-authored-by: Abdelmoumen Bouabdallah <[email protected]>

* Update src/common/ecs-features/ecsFeatureClient.ts

Co-authored-by: Abdelmoumen Bouabdallah <[email protected]>

* Update src/common/ecs-features/constants.ts

Co-authored-by: Abdelmoumen Bouabdallah <[email protected]>

* Address comments and update functions for extensibility

* Call ecs client and make call for loading config

* updated path names

* update calling and comments

* fix test cases

* fix auth test cases

* fix test

* fix test

* update for test case

---------

Co-authored-by: Abdelmoumen Bouabdallah <[email protected]>
  • Loading branch information
tyaginidhi and AbdouMoumen authored Apr 29, 2024
1 parent f8bb0a0 commit bd923c6
Show file tree
Hide file tree
Showing 14 changed files with 278 additions and 43 deletions.
8 changes: 4 additions & 4 deletions src/common/copilot/PowerPagesCopilot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,11 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider {

if (feedbackValue === THUMBS_UP) {

sendTelemetryEvent(this.telemetry, { eventName: CopilotUserFeedbackThumbsUpEvent, copilotSessionId: sessionID, orgId: orgID, subScenario: String(messageScenario)});
sendTelemetryEvent(this.telemetry, { eventName: CopilotUserFeedbackThumbsUpEvent, copilotSessionId: sessionID, orgId: orgID, subScenario: String(messageScenario) });
CESUserFeedback(this._extensionContext, sessionID, userID, THUMBS_UP, this.telemetry, this.geoName as string, messageScenario, tenantId)
} else if (feedbackValue === THUMBS_DOWN) {

sendTelemetryEvent(this.telemetry, { eventName: CopilotUserFeedbackThumbsDownEvent, copilotSessionId: sessionID, orgId: orgID, subScenario: String(messageScenario)});
sendTelemetryEvent(this.telemetry, { eventName: CopilotUserFeedbackThumbsDownEvent, copilotSessionId: sessionID, orgId: orgID, subScenario: String(messageScenario) });
CESUserFeedback(this._extensionContext, sessionID, userID, THUMBS_DOWN, this.telemetry, this.geoName as string, messageScenario, tenantId)
}
break;
Expand Down Expand Up @@ -363,12 +363,12 @@ export class PowerPagesCopilot implements vscode.WebviewViewProvider {
this.sendMessageToWebview({ type: 'userName', value: userName });

let metadataInfo = { entityName: '', formName: '' };
let componentInfo : string[] = [];
let componentInfo: string[] = [];

if (activeFileParams.dataverseEntity == ADX_ENTITYFORM || activeFileParams.dataverseEntity == ADX_ENTITYLIST) {
metadataInfo = await getEntityName(telemetry, sessionID, activeFileParams.dataverseEntity);

const dataverseToken = await dataverseAuthentication(activeOrgUrl, true);
const dataverseToken = (await dataverseAuthentication(activeOrgUrl, true)).accessToken;

if (activeFileParams.dataverseEntity == ADX_ENTITYFORM) {
const formColumns = await getFormXml(metadataInfo.entityName, metadataInfo.formName, activeOrgUrl, dataverseToken, telemetry, sessionID);
Expand Down
14 changes: 14 additions & 0 deletions src/common/ecs-features/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

export const ECS_REQUEST_URL_TEMPLATE = "https://ecs.office.com/config/v1";

export const PowerPagesClientName = 'PortalsMakerExperiences'; // Project name in ECS Portal, Do not change this
export type ProjectTeam = typeof PowerPagesClientName;
export const PowerPagesAppName = 'powerpages-microsoft-com'; // Project name in ECS Portal, Do not change this
export type ProjectName = typeof PowerPagesAppName;

export const VisualStudioCodeDevInsidersUrl = 'https://insiders.vscode.dev/power/portal'; // VScode dev insiders/pre-prod env
export const VisualStudioCodeDevUrl = 'https://vscode.dev/power/portal'; // VScode dev GA/prod env
50 changes: 50 additions & 0 deletions src/common/ecs-features/ecsFeatureClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import TelemetryReporter from "@vscode/extension-telemetry";
import { ITelemetry } from "../../client/telemetry/ITelemetry";
import { createECSRequestURL } from "./ecsFeatureUtil";
import { ECSFeatureDefinition as ECSFeatureProperties } from "./ecsFeatureProperties";
import { ECSAPIFeatureFlagFilters } from "./ecsFeatureFlagFilters";

export abstract class ECSFeaturesClient {
private static _ecsConfig: Record<string, string | boolean>;
private static _featuresConfig = {};

// Initialize ECSFeatureClient - any client config can be fetched with utility function like below
// EnableMultifileVscodeWeb.getConfig().enableMultifileVscodeWeb
public static async init(telemetry: ITelemetry | TelemetryReporter, filters: ECSAPIFeatureFlagFilters, clientName: string) {
if (this._ecsConfig) return;

const requestURL = createECSRequestURL(filters, clientName);
try {
const response = await fetch(requestURL, {
method: 'GET'
});
if (!response.ok) {
throw new Error('Request failed');
}
const result = await response.json();
// Update telemetry in other PR
// telemetry.sendTelemetryEvent('ECSConfig', {});

// Initialize ECS config
this._ecsConfig = result[clientName];
} catch (error) {
// Log error
// telemetry.sendTelemetryEvent('ECSConfigError', {});
}
}

public static getConfig<TConfig extends Record<string, boolean | string>, TeamName extends string>(
feature: ECSFeatureProperties<TConfig, TeamName>
) {
if (Object.keys(this._featuresConfig).length === 0) {
this._featuresConfig = this._ecsConfig && feature.extractECSFeatureFlagConfig?.(this._ecsConfig as TConfig);
}

return Object.keys(this._featuresConfig).length === 0 ? feature.fallback : this._featuresConfig;
}
}
29 changes: 29 additions & 0 deletions src/common/ecs-features/ecsFeatureFlagFilters.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

export interface ECSAPIFeatureFlagFilters {
/**
* Application AppName
* @example powerpages-microsoft-com
*/
AppName: string;

/** The AAD user object ID or MSA. */
UserID: string;

/** The AAD tenant object ID. */
TenantID: string;

/** Unique Dataverse Environment ID */
EnvID: string;

/**
* Deployment region
* @example test, preview, prod
*/
Region: string;

// TBD - more API call filters to be added later
}
17 changes: 17 additions & 0 deletions src/common/ecs-features/ecsFeatureGates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { PowerPagesClientName } from './constants';
import { getFeatureConfigs } from './ecsFeatureUtil';

export const {
feature: EnableMultifileVscodeWeb
} = getFeatureConfigs({
teamName: PowerPagesClientName,
description: 'Enable multiple file view in Visual Studio Code Web',
fallback: {
enableMultifileVscodeWeb: false,
},
});
68 changes: 68 additions & 0 deletions src/common/ecs-features/ecsFeatureProperties.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

/**
* Interface representing a Feature config in ECS.
*/
export interface ECSFeatureDefinition<TConfig extends Record<string, string | boolean>, TeamName extends string> {
/**
* Name of the Team owning the feature. Must match the "clientTeam" name configured in ECS.
*/
teamName: TeamName;

/**
* Fallback config to be used while loading or if configs are unavailable.
*/
fallback: TConfig;

/**
* Brief description of the feature (used for tracking/cleanup purposes)
*/
description?: string;

/**
* Callback to extract the Feature-specific config from the Team config.
* @param config overall config for the Team.
*/
extractECSFeatureFlagConfig<TTeamConfig extends TConfig>(config: TTeamConfig): Partial<TConfig>;
}

export type ECSFeatureInfo<TConfig extends Record<string, boolean | string>, TeamName extends string> = Omit<
ECSFeatureDefinition<TConfig, TeamName>,
'extractECSFeatureFlagConfig'
>;

/**
* Creates a Feature object based on the feature definition that includes fallback flags and the team owning the feature.
* @param featureInfo Feature definition specifying the fallback flags and the team owning the feature.
*/
export function createECSFeatureDefinition<TConfig extends Record<string, boolean | string>, TeamName extends string>(
featureInfo: ECSFeatureInfo<TConfig, TeamName>
): ECSFeatureDefinition<TConfig, TeamName> {
return {
...featureInfo,
extractECSFeatureFlagConfig: (config) => extractECSFeatureFlagConfig(config, Object.keys(featureInfo.fallback)),
};
}

/**
* Extracts the feature-specific config, based on the provided keys, from the overall Project Team config
* @param teamConfig the overall config object for the Project Team
* @param keys property names to extract from the Project Team config
*/
function extractECSFeatureFlagConfig<TConfig extends Record<string, boolean | string>, TeamConfig extends TConfig>(
teamConfig: TeamConfig,
keys: (keyof TConfig)[]
): Partial<TConfig> {
const config: Partial<TConfig> = {};

for (const key of keys) {
if (typeof teamConfig[key] !== 'undefined') {
config[key] = teamConfig[key];
}
}

return config;
}
38 changes: 38 additions & 0 deletions src/common/ecs-features/ecsFeatureUtil.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*/

import { PowerPagesClientName, ECS_REQUEST_URL_TEMPLATE } from "./constants";
import { ECSFeaturesClient } from "./ecsFeatureClient";
import { ECSAPIFeatureFlagFilters } from "./ecsFeatureFlagFilters";
import { ECSFeatureDefinition, ECSFeatureInfo, createECSFeatureDefinition } from "./ecsFeatureProperties";

export function createECSRequestURL(filters: ECSAPIFeatureFlagFilters, clientName = PowerPagesClientName): string {
const queryParams: { [key: string]: string } = {
AppName: filters.AppName,
EnvironmentID: filters.EnvID,
UserID: filters.UserID,
TenantID: filters.TenantID,
region: filters.Region
};

const queryString = Object.keys(queryParams)
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');

return `${ECS_REQUEST_URL_TEMPLATE}/${clientName}/1.0.0.0?${queryString}`;
}

export function getFeatureConfigs<TConfig extends Record<string, string | boolean>, TeamName extends string>(featureInfo: ECSFeatureInfo<TConfig, TeamName>) {
type EnhancedFeature = ECSFeatureDefinition<TConfig, TeamName> & {
getConfig: () => Partial<TConfig>;
};

const feature = createECSFeatureDefinition(featureInfo) as EnhancedFeature;
feature.getConfig = () => ECSFeaturesClient.getConfig(feature);

return {
feature: feature,
};
}
4 changes: 3 additions & 1 deletion src/web/client/WebExtensionContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,14 +370,15 @@ class WebExtensionContext implements IWebExtensionContext {
const dataverseOrgUrl = this.urlParametersMap.get(
Constants.queryParameters.ORG_URL
) as string;
const accessToken: string = await dataverseAuthentication(
const { accessToken, userId } = await dataverseAuthentication(
dataverseOrgUrl,
firstTimeAuth
);

if (accessToken.length === 0) {
// re-set all properties to default values
this._dataverseAccessToken = "";
this._userId = "";
this._websiteIdToLanguage = new Map<string, string>();
this._websiteLanguageIdToPortalLanguageMap = new Map<
string,
Expand All @@ -394,6 +395,7 @@ class WebExtensionContext implements IWebExtensionContext {
}

this._dataverseAccessToken = accessToken;
this._userId = userId;
}

public async updateFileDetailsInContext(
Expand Down
13 changes: 7 additions & 6 deletions src/web/client/common/authenticationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ export async function intelligenceAPIAuthentication(telemetry: ITelemetry, sessi
export async function dataverseAuthentication(
dataverseOrgURL: string,
firstTimeAuth = false
): Promise<string> {
): Promise<{ accessToken: string, userId: string }> {
let accessToken = "";
let userId = "";
try {
let session = await vscode.authentication.getSession(
PROVIDER_ID,
Expand All @@ -110,6 +111,9 @@ export async function dataverseAuthentication(
}

accessToken = session?.accessToken ?? "";
userId = session?.account.id.split("/").pop() ??
session?.account.id ??
"";
if (!accessToken) {
throw new Error(ERRORS.NO_ACCESS_TOKEN);
}
Expand All @@ -118,10 +122,7 @@ export async function dataverseAuthentication(
WebExtensionContext.telemetry.sendInfoTelemetry(
telemetryEventNames.WEB_EXTENSION_DATAVERSE_AUTHENTICATION_COMPLETED,
{
userId:
session?.account.id.split("/").pop() ??
session?.account.id ??
"",
userId: userId
}
);
}
Expand All @@ -140,7 +141,7 @@ export async function dataverseAuthentication(
);
}

return accessToken;
return { accessToken, userId };
}

export async function npsAuthentication(
Expand Down
36 changes: 25 additions & 11 deletions src/web/client/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,9 @@ import { fetchArtemisResponse } from "../../common/ArtemisService";
import { oneDSLoggerWrapper } from "../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper";
import { GeoNames } from "../../common/OneDSLoggerTelemetry/telemetryConstants";
import { sendingMessageToWebWorkerForCoPresence } from "./utilities/collaborationUtils";
import {IPortalWebExtensionInitQueryParametersTelemetryData} from "./telemetry/webExtensionTelemetryInterface";
import { ECSFeaturesClient } from "../../common/ecs-features/ecsFeatureClient";
import { PowerPagesAppName, PowerPagesClientName } from "../../common/ecs-features/constants";
import { IPortalWebExtensionInitQueryParametersTelemetryData } from "./telemetry/webExtensionTelemetryInterface";

export function activate(context: vscode.ExtensionContext): void {
// setup telemetry
Expand Down Expand Up @@ -114,7 +116,7 @@ export function activate(context: vscode.ExtensionContext): void {
logOneDSLogger(queryParamsMap);
const orgId = queryParamsMap.get(queryParameters.ORG_ID) as string;
const orgGeo = await fetchArtemisData(orgId);
WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_ORG_GEO,{orgGeo: orgGeo});
WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_ORG_GEO, { orgGeo: orgGeo });
oneDSLoggerWrapper.instantiate(orgGeo);

WebExtensionContext.telemetry.sendExtensionInitPathParametersTelemetry(
Expand Down Expand Up @@ -146,6 +148,18 @@ export function activate(context: vscode.ExtensionContext): void {
},
async () => {
await portalsFS.readDirectory(WebExtensionContext.rootDirectory, true);

// Initialize ECS config in webExtensionContext
await ECSFeaturesClient.init(WebExtensionContext.telemetry.getTelemetryReporter(),
{
AppName: PowerPagesAppName,
EnvID: queryParamsMap.get(queryParameters.ENV_ID) as string,
UserID: WebExtensionContext.userId,
TenantID: queryParamsMap.get(queryParameters.TENANT_ID) as string,
Region: queryParamsMap.get(queryParameters.REGION) as string
},
PowerPagesClientName);

registerCopilot(context);
processWillStartCollaboration(context);
}
Expand Down Expand Up @@ -629,14 +643,14 @@ function isActiveDocument(fileFsPath: string): boolean {
);
}

async function fetchArtemisData(orgId: string) : Promise<string> {
const artemisResponse = await fetchArtemisResponse(orgId, WebExtensionContext.telemetry.getTelemetryReporter());
if (!artemisResponse) {
// Todo: Log in error telemetry. Runtime maintains another table for this kind of failure. We should do the same.
return '';
}
async function fetchArtemisData(orgId: string): Promise<string> {
const artemisResponse = await fetchArtemisResponse(orgId, WebExtensionContext.telemetry.getTelemetryReporter());
if (!artemisResponse) {
// Todo: Log in error telemetry. Runtime maintains another table for this kind of failure. We should do the same.
return '';
}

return artemisResponse[0].geoName as string;
return artemisResponse[0].geoName as string;
}

async function logArtemisTelemetry() {
Expand All @@ -646,7 +660,7 @@ async function logArtemisTelemetry() {
queryParameters.ORG_ID
) as string

const geoName= fetchArtemisData(orgId);
const geoName = fetchArtemisData(orgId);
WebExtensionContext.telemetry.sendInfoTelemetry(telemetryEventNames.WEB_EXTENSION_ARTEMIS_RESPONSE,
{ orgId: orgId, geoName: String(geoName) });
} catch (error) {
Expand All @@ -657,7 +671,7 @@ async function logArtemisTelemetry() {
}
}

function logOneDSLogger (queryParamsMap: Map<string, string>) {
function logOneDSLogger(queryParamsMap: Map<string, string>) {
const telemetryData: IPortalWebExtensionInitQueryParametersTelemetryData = {
eventName: telemetryEventNames.WEB_EXTENSION_INIT_QUERY_PARAMETERS,
properties: {
Expand Down
Loading

0 comments on commit bd923c6

Please sign in to comment.