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 000000000..1a44967d0 Binary files /dev/null and b/packages/expvariantassignmentsdk-1.0.0.tgz differ diff --git a/src/app/middleware/localStorageMiddleware.ts b/src/app/middleware/localStorageMiddleware.ts index acdec5461..9bbfba6c1 100644 --- a/src/app/middleware/localStorageMiddleware.ts +++ b/src/app/middleware/localStorageMiddleware.ts @@ -1,20 +1,21 @@ import { collectionsCache } from '../../modules/cache/collections.cache'; import { resourcesCache } from '../../modules/cache/resources.cache'; import { samplesCache } from '../../modules/cache/samples.cache'; -import { saveTheme } from '../../themes/theme-utils'; import { AppAction } from '../../types/action'; import { ResourcePath } from '../../types/resources'; import { addResourcePaths } from '../services/actions/collections-action-creators'; +import { CURRENT_THEME } from '../services/graph-constants'; import { getUniquePaths } from '../services/reducers/collections-reducer.util'; import { CHANGE_THEME_SUCCESS, COLLECTION_CREATE_SUCCESS, FETCH_RESOURCES_ERROR, FETCH_RESOURCES_SUCCESS, RESOURCEPATHS_ADD_SUCCESS, RESOURCEPATHS_DELETE_SUCCESS, SAMPLES_FETCH_SUCCESS } from '../services/redux-constants'; +import { saveToLocalStorage } from '../utils/local-storage'; const localStorageMiddleware = (store: any) => (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; -}