From 0aed99f3b0d4323a986df46b46b684c59bc18257 Mon Sep 17 00:00:00 2001 From: Elinor Date: Thu, 29 Jun 2023 14:10:59 +0300 Subject: [PATCH] Feature: Add flighting support (#2621) --- package-lock.json | 17 +++++ package.json | 1 + packages/expvariantassignmentsdk-1.0.0.tgz | Bin 0 -> 5521 bytes src/app/middleware/localStorageMiddleware.ts | 5 +- src/app/services/graph-constants.ts | 2 + src/app/services/variant-constants.ts | 1 + src/app/services/variant-service.ts | 68 +++++++++++++++++++ src/app/utils/local-storage.ts | 17 +++++ src/index.tsx | 9 ++- src/telemetry/ITelemetry.ts | 1 + src/telemetry/telemetry.ts | 16 ++++- src/themes/theme-utils.spec.ts | 7 +- src/themes/theme-utils.ts | 10 --- 13 files changed, 134 insertions(+), 20 deletions(-) create mode 100644 packages/expvariantassignmentsdk-1.0.0.tgz create mode 100644 src/app/services/variant-constants.ts create mode 100644 src/app/services/variant-service.ts create mode 100644 src/app/utils/local-storage.ts delete mode 100644 src/themes/theme-utils.ts diff --git a/package-lock.json b/package-lock.json index 662c6b2a6..950696064 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9432,6 +9432,23 @@ } } }, + "expvariantassignmentsdk": { + "version": "file:packages/expvariantassignmentsdk-1.0.0.tgz", + "integrity": "sha512-Fwk0rryTZk1vlWTi4WCuTTi5xV7S7LcbmQk2Lmr/c9aO0XJw3h10GhhBxMlpJQk0xFcUJ9XJl91vysYd4NEMpw==", + "requires": { + "axios": "^0.21.1" + }, + "dependencies": { + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.0" + } + } + } + }, "extract-zip": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", diff --git a/package.json b/package.json index 9e3a02978..48f7633bf 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "eslint-plugin-react": "7.32.2", "eslint-webpack-plugin": "4.0.1", "express": "4.18.2", + "expvariantassignmentsdk": "file:packages/expvariantassignmentsdk-1.0.0.tgz", "file-loader": "6.2.0", "fork-ts-checker-webpack-plugin": "8.0.0", "fs-extra": "11.1.1", diff --git a/packages/expvariantassignmentsdk-1.0.0.tgz b/packages/expvariantassignmentsdk-1.0.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..1a44967d017a0d4bd49c7ed5c8fc55eca28b99b8 GIT binary patch literal 5521 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 (next: any) => async (action: AppAction) => { switch (action.type) { case CHANGE_THEME_SUCCESS: - saveTheme(action.response); + saveToLocalStorage(CURRENT_THEME,action.response); break; case SAMPLES_FETCH_SUCCESS: diff --git a/src/app/services/graph-constants.ts b/src/app/services/graph-constants.ts index 842b846d8..c1ff4ad76 100644 --- a/src/app/services/graph-constants.ts +++ b/src/app/services/graph-constants.ts @@ -32,3 +32,5 @@ export const REVOKING_PERMISSIONS_REQUIRED_SCOPES = 'DelegatedPermissionGrant.Re export const ADMIN_CONSENT_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/security-authorization#:~:text=If%20you%27re%20calling%20the%20Microsoft%20Graph%20Security%20API%20from%20Graph%20Explorer' // eslint-disable-next-line max-len export const CONSENT_TYPE_DOC_LINK = 'https://learn.microsoft.com/en-us/graph/api/resources/oauth2permissiongrant?view=graph-rest-1.0#:~:text=(eq%20only).-,consentType,-String' +export const CURRENT_THEME='CURRENT_THEME'; +export const EXP_URL='https://default.exp-tas.com/exptas76/9b835cbf-9742-40db-84a7-7a323a77f3eb-gedev/api/v1/tas' \ No newline at end of file diff --git a/src/app/services/variant-constants.ts b/src/app/services/variant-constants.ts new file mode 100644 index 000000000..1300b0cbc --- /dev/null +++ b/src/app/services/variant-constants.ts @@ -0,0 +1 @@ +export const ALWAYSSHOWBUTTONS = 'alwaysShowButtons'; \ No newline at end of file diff --git a/src/app/services/variant-service.ts b/src/app/services/variant-service.ts new file mode 100644 index 000000000..4adf0de47 --- /dev/null +++ b/src/app/services/variant-service.ts @@ -0,0 +1,68 @@ +/* eslint-disable max-len */ +import { VariantAssignmentRequest } from 'expvariantassignmentsdk/src/interfaces/VariantAssignmentRequest'; +import {VariantAssignmentServiceClient} from 'expvariantassignmentsdk/src/contracts/VariantAssignmentServiceClient'; +import { VariantAssignmentClientSettings } from 'expvariantassignmentsdk/src/contracts/VariantAssignmentClientSettings'; +import { errorTypes, telemetry } from '../../telemetry'; +import { readFromLocalStorage, saveToLocalStorage } from '../utils/local-storage'; +import { EXP_URL } from './graph-constants'; +import { SeverityLevel } from '@microsoft/applicationinsights-web'; + + +interface TasResponse { + Id: string; + Parameters: Parameters; +} +interface Parameters { + [key: string]: string | boolean | number; +} +class VariantService { + + private endpoint = EXP_URL; + private expResponse: TasResponse[] | null = []; + private assignmentContext: string = ''; + + public async initialize() { + const settings: VariantAssignmentClientSettings = { endpoint: this.endpoint }; + this.createUser(); + const request: VariantAssignmentRequest = + { + parameters: this.getParameters() + }; + + const client = new VariantAssignmentServiceClient(settings); + const response = await client.getVariantAssignments(request); + Promise.resolve(response).then((r) => { + if (r){ + this.expResponse = r.featureVariables as TasResponse[] | null; + this.assignmentContext = r.assignmentContext; + } + }) + .catch((error) => { + telemetry.trackException(new Error(errorTypes.UNHANDLED_ERROR), SeverityLevel.Error, error); + }); + } + + public createUser() { + const userid = telemetry.getUserId(); + saveToLocalStorage('userid', userid.toString()); + } + + public getAssignmentContext() { + return this.assignmentContext; + } + + public async getFeatureVariables(namespace: string, flagname: string) { + const defaultConfig = this.expResponse?.find(c => c.Id === namespace); + return defaultConfig?.Parameters[flagname]; + } + + // Parameters will include randomization units (you can have more than one in a single call!) + // and audience filters like market/region, browser, ismsft etc., + private getParameters(): Map { + const map: Map = new Map(); + map.set('userid', [readFromLocalStorage('userid')]); + return map; + } +} + +export default new VariantService(); \ No newline at end of file diff --git a/src/app/utils/local-storage.ts b/src/app/utils/local-storage.ts new file mode 100644 index 000000000..a56d57ae4 --- /dev/null +++ b/src/app/utils/local-storage.ts @@ -0,0 +1,17 @@ +export const saveToLocalStorage = (key: string, value: Object|string) => { + if (typeof value === 'string') { + localStorage.setItem(key, value); + } else { + localStorage.setItem(key, JSON.stringify(value)); + } +}; + +export const readFromLocalStorage = (key: string) => { + const value = localStorage.getItem(key); + + if (value && typeof value === 'object') { + return JSON.parse(value); + } else{ + return value; + } +}; \ No newline at end of file diff --git a/src/index.tsx b/src/index.tsx index bc2a01cfd..5164897e9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -26,16 +26,18 @@ import './styles/index.scss'; import { telemetry } from './telemetry'; import ITelemetry from './telemetry/ITelemetry'; import { loadGETheme } from './themes'; -import { readTheme } from './themes/theme-utils'; +import { readFromLocalStorage } from './app/utils/local-storage'; import { IDevxAPI } from './types/devx-api'; import { Mode } from './types/enums'; import { Collection } from './types/resources'; +import variantService from './app/services/variant-service'; +import { CURRENT_THEME } from './app/services/graph-constants'; const appRoot: HTMLElement = document.getElementById('root')!; initializeIcons(); -let currentTheme = readTheme() || 'light'; +let currentTheme = readFromLocalStorage(CURRENT_THEME) || 'light'; export function removeSpinners() { // removes the loading spinner from GE html after the app is loaded const spinner = document.getElementById('spinner'); @@ -54,7 +56,7 @@ export function removeSpinners() { } function setCurrentSystemTheme(): void { - const themeFromLocalStorage = readTheme(); + const themeFromLocalStorage = readFromLocalStorage(CURRENT_THEME); if (themeFromLocalStorage) { currentTheme = themeFromLocalStorage; @@ -161,6 +163,7 @@ function loadResources() { } loadResources(); +variantService.initialize(); const telemetryProvider: ITelemetry = telemetry; telemetryProvider.initialize(); diff --git a/src/telemetry/ITelemetry.ts b/src/telemetry/ITelemetry.ts index 4079406d7..468c27eeb 100644 --- a/src/telemetry/ITelemetry.ts +++ b/src/telemetry/ITelemetry.ts @@ -16,4 +16,5 @@ export default interface ITelemetry { severityLevel: SeverityLevel, properties: {} ): void; + getUserId():string } diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 8ba22413a..454bb615f 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -32,6 +32,7 @@ import { getBrowserScreenSize, getDeviceScreenScale } from '../app/utils/device-characteristics-telemetry'; +import variantService from '../app/services/variant-service'; class Telemetry implements ITelemetry { private appInsights: ApplicationInsights; @@ -66,8 +67,10 @@ class Telemetry implements ITelemetry { this.appInsights.context.application.ver = getVersion().toString(); } - public trackEvent(name: string, properties: {}) { - this.appInsights.trackEvent({ name, properties }); + public trackEvent(name: string, properties:{ AssignmentContext?: string, [key: string]: any } = {}) { + const defaultProperties = { AssignmentContext: variantService.getAssignmentContext() }; + const mergedProperties = { ...defaultProperties, ...properties }; + this.appInsights.trackEvent({ name, properties: mergedProperties }); } public trackException( @@ -140,6 +143,15 @@ class Telemetry implements ITelemetry { '' ); } + + public getUserId(){ + try { + const userId = document.cookie.split(';').filter((item) => item.trim().startsWith('ai_user')).map((item) => item.split('=')[1])[0]; + return userId.split('|')[0]; + } catch (error) { + return ''; + } + } } export const telemetry = new Telemetry(); diff --git a/src/themes/theme-utils.spec.ts b/src/themes/theme-utils.spec.ts index 77727f0da..b6adc0fae 100644 --- a/src/themes/theme-utils.spec.ts +++ b/src/themes/theme-utils.spec.ts @@ -1,9 +1,10 @@ -import { saveTheme, readTheme } from './theme-utils'; +import { CURRENT_THEME } from '../app/services/graph-constants'; +import { readFromLocalStorage, saveToLocalStorage } from '../app/utils/local-storage'; describe('Tests theme utils should', () => { it('save theme to local storage then retrieve the saved theme', () => { const theme = 'dark'; - saveTheme(theme); - expect(readTheme()).toEqual(theme); + saveToLocalStorage(CURRENT_THEME,theme); + expect(readFromLocalStorage(CURRENT_THEME)).toEqual(theme); }) }) \ No newline at end of file diff --git a/src/themes/theme-utils.ts b/src/themes/theme-utils.ts deleted file mode 100644 index a66a397c3..000000000 --- a/src/themes/theme-utils.ts +++ /dev/null @@ -1,10 +0,0 @@ -const key = 'CURRENT_THEME'; - -export function saveTheme(theme: string) { - localStorage.setItem(key, theme); -} - -export function readTheme() { - const theme = localStorage.getItem(key); - return theme; -}