diff --git a/packages/optimizely-sdk/lib/core/notification_center/notification_registry.tests.ts b/packages/optimizely-sdk/lib/core/notification_center/notification_registry.tests.ts new file mode 100644 index 000000000..3a99b052c --- /dev/null +++ b/packages/optimizely-sdk/lib/core/notification_center/notification_registry.tests.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2023, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { describe, it } from 'mocha'; +import { expect } from 'chai'; + +import { NotificationRegistry } from './notification_registry'; + +describe('Notification Registry', () => { + it('Returns null notification center when SDK Key is null', () => { + const notificationCenter = NotificationRegistry.getNotificationCenter(); + expect(notificationCenter).to.be.undefined; + }); + + it('Returns the same notification center when SDK Keys are the same and not null', () => { + const sdkKey = 'testSDKKey'; + const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey); + const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey); + expect(notificationCenterA).to.eql(notificationCenterB); + }); + + it('Returns different notification centers when SDK Keys are not the same', () => { + const sdkKeyA = 'testSDKKeyA'; + const sdkKeyB = 'testSDKKeyB'; + const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKeyA); + const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKeyB); + expect(notificationCenterA).to.not.eql(notificationCenterB); + }); + + it('Removes old notification centers from the registry when removeNotificationCenter is called on the registry', () => { + const sdkKey = 'testSDKKey'; + const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey); + NotificationRegistry.removeNotificationCenter(sdkKey); + + const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey); + + expect(notificationCenterA).to.not.eql(notificationCenterB); + }); + + it('Does not throw an error when calling removeNotificationCenter with a null SDK Key', () => { + const sdkKey = 'testSDKKey'; + const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey); + NotificationRegistry.removeNotificationCenter(); + + const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey); + + expect(notificationCenterA).to.eql(notificationCenterB); + }); +}); diff --git a/packages/optimizely-sdk/lib/core/notification_center/notification_registry.ts b/packages/optimizely-sdk/lib/core/notification_center/notification_registry.ts new file mode 100644 index 000000000..80f9eb9f6 --- /dev/null +++ b/packages/optimizely-sdk/lib/core/notification_center/notification_registry.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2023, Optimizely + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getLogger, LogHandler, LogLevel } from '../../modules/logging'; +import { NotificationCenter, createNotificationCenter } from '../../core/notification_center'; + +/** + * Internal notification center registry for managing multiple notification centers. + */ +export class NotificationRegistry { + private static _notificationCenters = new Map(); + + constructor() {} + + /** + * Retrieves an SDK Key's corresponding notification center in the registry if it exists, otherwise it creates one + * @param sdkKey SDK Key to be used for the notification center tied to the ODP Manager + * @param logger Logger to be used for the corresponding notification center + * @returns {NotificationCenter | undefined} a notification center instance for ODP Manager if a valid SDK Key is provided, otherwise undefined + */ + public static getNotificationCenter( + sdkKey?: string, + logger: LogHandler = getLogger() + ): NotificationCenter | undefined { + if (!sdkKey) { + logger.log(LogLevel.ERROR, 'No SDK key provided to getNotificationCenter.'); + return undefined; + } + + let notificationCenter; + if (this._notificationCenters.has(sdkKey)) { + notificationCenter = this._notificationCenters.get(sdkKey); + } else { + notificationCenter = createNotificationCenter({ + logger, + errorHandler: { handleError: () => {} }, + }); + this._notificationCenters.set(sdkKey, notificationCenter); + } + + return notificationCenter; + } + + public static removeNotificationCenter(sdkKey?: string): void { + if (!sdkKey) { + return; + } + + const notificationCenter = this._notificationCenters.get(sdkKey); + if (notificationCenter) { + notificationCenter.clearAllNotificationListeners(); + this._notificationCenters.delete(sdkKey); + } + } +} diff --git a/packages/optimizely-sdk/lib/core/odp/odp_config.ts b/packages/optimizely-sdk/lib/core/odp/odp_config.ts index ea8017b7d..0f1753faf 100644 --- a/packages/optimizely-sdk/lib/core/odp/odp_config.ts +++ b/packages/optimizely-sdk/lib/core/odp/odp_config.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +import { checkArrayEquality } from '../../../lib/utils/fns'; + export class OdpConfig { /** * Host of ODP audience segments API. @@ -98,7 +100,7 @@ export class OdpConfig { return ( this._apiHost == config._apiHost && this._apiKey == config._apiKey && - JSON.stringify(this.segmentsToCheck) == JSON.stringify(config._segmentsToCheck) + checkArrayEquality(this.segmentsToCheck, config._segmentsToCheck) ); } } diff --git a/packages/optimizely-sdk/lib/core/odp/odp_manager.ts b/packages/optimizely-sdk/lib/core/odp/odp_manager.ts index 874a52a5c..939f2bab0 100644 --- a/packages/optimizely-sdk/lib/core/odp/odp_manager.ts +++ b/packages/optimizely-sdk/lib/core/odp/odp_manager.ts @@ -29,7 +29,6 @@ import { OdpEventApiManager } from './odp_event_api_manager'; import { OptimizelySegmentOption } from './optimizely_segment_option'; import { areOdpDataTypesValid } from './odp_types'; import { OdpEvent } from './odp_event'; -import { VuidManager } from '../../plugins/vuid_manager'; // Orchestrates segments manager, event manager, and ODP configuration export class OdpManager { @@ -39,6 +38,8 @@ export class OdpManager { odpConfig: OdpConfig; logger: LogHandler; + // Note: VuidManager only utilized in Browser variation at /plugins/odp_manager/index.browser.ts + /** * ODP Segment Manager which provides an interface to the remote ODP server (GraphQL API) for audience segments mapping. * It fetches all qualified segments for the given user context and manages the segments cache for all user contexts. @@ -107,9 +108,8 @@ export class OdpManager { } // Set up Events Manager (Events REST API Interface) - if (eventManager) { - eventManager.updateSettings(this.odpConfig); - this._eventManager = eventManager; + if (this._eventManager) { + this._eventManager.updateSettings(this.odpConfig); } else { this._eventManager = new OdpEventManager({ odpConfig: this.odpConfig, @@ -154,9 +154,10 @@ export class OdpManager { /** * Attempts to fetch and return a list of a user's qualified segments from the local segments cache. * If no cached data exists for the target user, this fetches and caches data from the ODP server instead. - * @param userId Unique identifier of a target user. - * @param options An array of OptimizelySegmentOption used to ignore and/or reset the cache. - * @returns + * @param {ODP_USER_KEY} userKey - Identifies the user id type. + * @param {string} userId - Unique identifier of a target user. + * @param {Array} A promise holding either a list of qualified segments or null. */ public async fetchQualifiedSegments( userKey: ODP_USER_KEY, diff --git a/packages/optimizely-sdk/lib/core/odp/odp_segment_manager.ts b/packages/optimizely-sdk/lib/core/odp/odp_segment_manager.ts index b825c4413..6b1c21438 100644 --- a/packages/optimizely-sdk/lib/core/odp/odp_segment_manager.ts +++ b/packages/optimizely-sdk/lib/core/odp/odp_segment_manager.ts @@ -24,13 +24,13 @@ import { OptimizelySegmentOption } from './optimizely_segment_option'; // Schedules connections to ODP for audience segmentation and caches the results. export class OdpSegmentManager { odpConfig: OdpConfig; - segmentsCache: LRUCache>; + segmentsCache: LRUCache; odpSegmentApiManager: OdpSegmentApiManager; logger: LogHandler; constructor( odpConfig: OdpConfig, - segmentsCache: LRUCache>, + segmentsCache: LRUCache, odpSegmentApiManager: OdpSegmentApiManager, logger?: LogHandler ) { @@ -52,7 +52,7 @@ export class OdpSegmentManager { userKey: ODP_USER_KEY, userValue: string, options: Array - ): Promise | null> { + ): Promise { const { apiHost: odpApiHost, apiKey: odpApiKey } = this.odpConfig; if (!odpApiKey || !odpApiHost) { diff --git a/packages/optimizely-sdk/lib/core/project_config/index.tests.js b/packages/optimizely-sdk/lib/core/project_config/index.tests.js index 479d7c18c..fa4c03d16 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.tests.js +++ b/packages/optimizely-sdk/lib/core/project_config/index.tests.js @@ -1,11 +1,11 @@ /** - * Copyright 2016-2022, Optimizely + * Copyright 2016-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -836,8 +836,8 @@ describe('lib/core/project_config', function () { }) it('should contain all expected unique odp segments in allSegments', () => { - assert.equal(config.allSegments.size, 3) - assert.deepEqual(config.allSegments, new Set(['odp-segment-1', 'odp-segment-2', 'odp-segment-3'])) + assert.equal(config.allSegments.length, 3) + assert.deepEqual(config.allSegments, ['odp-segment-1', 'odp-segment-2', 'odp-segment-3']) }) }); @@ -863,7 +863,7 @@ describe('lib/core/project_config', function () { }) it('should contain all expected unique odp segments in all segments', () => { - assert.equal(config.allSegments.size, 0) + assert.equal(config.allSegments.length, 0) }) }); diff --git a/packages/optimizely-sdk/lib/core/project_config/index.ts b/packages/optimizely-sdk/lib/core/project_config/index.ts index aa89cc566..f97b1a05c 100644 --- a/packages/optimizely-sdk/lib/core/project_config/index.ts +++ b/packages/optimizely-sdk/lib/core/project_config/index.ts @@ -1,11 +1,11 @@ /** - * Copyright 2016-2022, Optimizely + * Copyright 2016-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -103,7 +103,7 @@ export interface ProjectConfig { integrationKeyMap?: { [key: string]: Integration }; publicKeyForOdp?: string; hostForOdp?: string; - allSegments: Set; + allSegments: string[]; } const EXPERIMENT_RUNNING_STATUS = 'Running'; @@ -167,13 +167,13 @@ export const createProjectConfig = function ( projectConfig.audiencesById = keyBy(projectConfig.audiences, 'id'); assign(projectConfig.audiencesById, keyBy(projectConfig.typedAudiences, 'id')); - projectConfig.allSegments = new Set([]) + projectConfig.allSegments = [] Object.keys(projectConfig.audiencesById) .map((audience) => getAudienceSegments(projectConfig.audiencesById[audience])) .forEach(audienceSegments => { audienceSegments.forEach(segment => { - projectConfig.allSegments.add(segment) + projectConfig.allSegments.push(segment) }) }) diff --git a/packages/optimizely-sdk/lib/index.browser.tests.js b/packages/optimizely-sdk/lib/index.browser.tests.js index 69822f46c..c2417b46b 100644 --- a/packages/optimizely-sdk/lib/index.browser.tests.js +++ b/packages/optimizely-sdk/lib/index.browser.tests.js @@ -1,5 +1,5 @@ /** - * Copyright 2016-2020, 2022 Optimizely + * Copyright 2016-2020, 2022-2023 Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts b/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts index 7d11a14cd..fcf2c0efd 100644 --- a/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts +++ b/packages/optimizely-sdk/lib/modules/datafile-manager/httpPollingDatafileManager.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,9 @@ import { DEFAULT_UPDATE_INTERVAL, MIN_UPDATE_INTERVAL, DEFAULT_URL_TEMPLATE } fr import BackoffController from './backoffController'; import PersistentKeyValueCache from './persistentKeyValueCache'; +import { NotificationRegistry } from './../../core/notification_center/notification_registry'; +import { NOTIFICATION_TYPES } from '../../../lib/utils/enums'; + const logger = getLogger('DatafileManager'); const UPDATE_EVT = 'update'; @@ -95,6 +98,8 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana private cache: PersistentKeyValueCache; + private sdkKey: string; + // When true, this means the update interval timeout fired before the current // sync completed. In that case, we should sync again immediately upon // completion of the current request, instead of waiting another update @@ -117,6 +122,7 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana this.cache = cache; this.cacheKey = 'opt-datafile-' + sdkKey; + this.sdkKey = sdkKey; this.isReadyPromiseSettled = false; this.readyPromiseResolver = (): void => {}; this.readyPromiseRejecter = (): void => {}; @@ -232,6 +238,9 @@ export default abstract class HttpPollingDatafileManager implements DatafileMana const datafileUpdate: DatafileUpdate = { datafile, }; + NotificationRegistry.getNotificationCenter(this.sdkKey, logger)?.sendNotifications( + NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE + ); this.emitter.emit(UPDATE_EVT, datafileUpdate); } } diff --git a/packages/optimizely-sdk/lib/optimizely/index.ts b/packages/optimizely-sdk/lib/optimizely/index.ts index d6fdd8beb..b005d3f6b 100644 --- a/packages/optimizely-sdk/lib/optimizely/index.ts +++ b/packages/optimizely-sdk/lib/optimizely/index.ts @@ -1,11 +1,11 @@ /**************************************************************************** - * Copyright 2020-2022, Optimizely, Inc. and contributors * + * Copyright 2020-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * * You may obtain a copy of the License at * * * - * http://www.apache.org/licenses/LICENSE-2.0 * + * https://www.apache.org/licenses/LICENSE-2.0 * * * * Unless required by applicable law or agreed to in writing, software * * distributed under the License is distributed on an "AS IS" BASIS, * @@ -17,6 +17,7 @@ import { LoggerFacade, ErrorHandler } from '../modules/logging'; import { sprintf, objectValues } from '../utils/fns'; import { NotificationCenter } from '../core/notification_center'; import { EventProcessor } from '../../lib/modules/event_processor'; +import { OdpManager } from './../core/odp/odp_manager'; import { UserAttributes, @@ -29,7 +30,8 @@ import { FeatureVariable, OptimizelyOptions, OptimizelyDecideOption, - OptimizelyDecision + OptimizelyDecision, + NotificationListener } from '../shared_types'; import { newErrorDecision } from '../optimizely_decision'; import OptimizelyUserContext from '../optimizely_user_context'; @@ -37,6 +39,7 @@ import { createProjectConfigManager, ProjectConfigManager } from '../core/projec import { createDecisionService, DecisionService, DecisionObj } from '../core/decision_service'; import { getImpressionEvent, getConversionEvent } from '../core/event_builder'; import { buildImpressionEvent, buildConversionEvent } from '../core/event_builder/event_helpers'; +import { NotificationRegistry } from '../core/notification_center/notification_registry'; import fns from '../utils/fns' import { validate } from '../utils/attributes_validator'; import * as enums from '../utils/enums'; @@ -81,6 +84,7 @@ export default class Optimizely { private decisionService: DecisionService; private eventProcessor: EventProcessor; private defaultDecideOptions: { [key: string]: boolean }; + private odpManager?: OdpManager; public notificationCenter: NotificationCenter; constructor(config: OptimizelyOptions) { @@ -168,13 +172,27 @@ export default class Optimizely { const eventProcessorStartedPromise = this.eventProcessor.start(); - this.readyPromise = Promise.all([projectConfigManagerReadyPromise, eventProcessorStartedPromise]).then(function(promiseResults) { + this.readyPromise = Promise.all([projectConfigManagerReadyPromise, eventProcessorStartedPromise]).then((promiseResults) => { + if (config.odpManager != null) { + this.odpManager = config.odpManager; + this.odpManager.eventManager?.start(); + this.updateODPSettings(); + const sdkKey = this.projectConfigManager.getConfig()?.sdkKey; + if (sdkKey != null) { + NotificationRegistry.getNotificationCenter(sdkKey, this.logger) + ?.addNotificationListener(enums.NOTIFICATION_TYPES.OPTIMIZELY_CONFIG_UPDATE, () => this.updateODPSettings()); + } else { + this.logger.log(LOG_LEVEL.ERROR, ERROR_MESSAGES.ODP_SDK_KEY_MISSING_NOTIFICATION_CENTER_FAILURE); + } + } + // Only return status from project config promise because event processor promise does not return any status. return promiseResults[0]; }) this.readyTimeouts = {}; this.nextReadyTimeoutId = 0; + } /** @@ -1315,6 +1333,12 @@ export default class Optimizely { */ close(): Promise<{ success: boolean; reason?: string }> { try { + this.notificationCenter.clearAllNotificationListeners(); + const sdkKey = this.projectConfigManager.getConfig()?.sdkKey; + if (sdkKey) { + NotificationRegistry.removeNotificationCenter(sdkKey); + } + const eventProcessorStoppedPromise = this.eventProcessor.stop(); if (this.disposeOnUpdate) { this.disposeOnUpdate(); @@ -1672,4 +1696,13 @@ export default class Optimizely { return this.decideForKeys(user, allFlagKeys, options); } + /** + * Updates ODP Config with most recent ODP key, host, and segments from the project config + */ + updateODPSettings(): void { + const projectConfig = this.projectConfigManager.getConfig(); + if (this.odpManager != null && projectConfig != null) { + this.odpManager.updateSettings(projectConfig.publicKeyForOdp, projectConfig.hostForOdp, projectConfig.allSegments); + } + } } diff --git a/packages/optimizely-sdk/lib/plugins/datafile_manager/no_op_datafile_manager.ts b/packages/optimizely-sdk/lib/plugins/datafile_manager/no_op_datafile_manager.ts index eb602c371..2f1926d4f 100644 --- a/packages/optimizely-sdk/lib/plugins/datafile_manager/no_op_datafile_manager.ts +++ b/packages/optimizely-sdk/lib/plugins/datafile_manager/no_op_datafile_manager.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021, Optimizely + * Copyright 2021, 2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,13 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DatafileManager, DatafileUpdateListener} from '../../shared_types'; +import { DatafileManager, DatafileUpdateListener } from '../../shared_types'; +/** + * No-operation Datafile Manager for Lite Bundle designed for Edge platforms + * https://github.com/optimizely/javascript-sdk/issues/699 + */ class NoOpDatafileManager implements DatafileManager { - /* eslint-disable @typescript-eslint/no-unused-vars */ on(_eventName: string, _listener: DatafileUpdateListener): () => void { - return (): void => {} + return (): void => {}; } get(): string { diff --git a/packages/optimizely-sdk/lib/shared_types.ts b/packages/optimizely-sdk/lib/shared_types.ts index f3ff5251b..29122436d 100644 --- a/packages/optimizely-sdk/lib/shared_types.ts +++ b/packages/optimizely-sdk/lib/shared_types.ts @@ -1,5 +1,5 @@ /** - * Copyright 2020-2022, Optimizely + * Copyright 2020-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,9 @@ */ import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from '../lib/modules/logging'; import { EventProcessor } from '../lib/modules/event_processor'; +import { OdpManager } from './core/odp/odp_manager'; -import { NotificationCenter as NotificationCenterImpl } from './core/notification_center' +import { NotificationCenter as NotificationCenterImpl } from './core/notification_center'; import { NOTIFICATION_TYPES } from './utils/enums'; export interface BucketerParams { @@ -41,11 +42,10 @@ export type UserAttributes = { // TODO[OASIS-6649]: Don't use any type // eslint-disable-next-line @typescript-eslint/no-explicit-any [name: string]: any; -} +}; export interface ExperimentBucketMap { - [experiment_id: string]: - { variation_id: string } + [experiment_id: string]: { variation_id: string }; } // Information about past bucketing decisions for a user. @@ -64,7 +64,7 @@ export interface UserProfileService { } export interface DatafileManagerConfig { - sdkKey: string, + sdkKey: string; datafile?: string; } @@ -114,7 +114,7 @@ export interface EventDispatcher { * After the event has at least been queued for dispatch, call this function to return * control back to the Client. */ - dispatchEvent: (event: Event, callback: (response: { statusCode: number; }) => void) => void; + dispatchEvent: (event: Event, callback: (response: { statusCode: number }) => void) => void; } export interface VariationVariable { @@ -164,9 +164,9 @@ export interface FeatureFlag { rolloutId: string; key: string; id: string; - experimentIds: string[], - variables: FeatureVariable[], - variableKeyMap: { [key: string]: FeatureVariable } + experimentIds: string[]; + variables: FeatureVariable[]; + variableKeyMap: { [key: string]: FeatureVariable }; groupId?: string; } @@ -175,7 +175,7 @@ export type Condition = { type: string; match?: string; value: string | number | boolean | null; -} +}; export interface Audience { id: string; @@ -214,7 +214,7 @@ export interface Group { } export interface FeatureKeyMap { - [key: string]: FeatureFlag + [key: string]: FeatureFlag; } export interface OnReadyResult { @@ -224,7 +224,7 @@ export interface OnReadyResult { export type ObjectWithUnknownProperties = { [key: string]: unknown; -} +}; export interface Rollout { id: string; @@ -237,7 +237,7 @@ export enum OptimizelyDecideOption { ENABLED_FLAGS_ONLY = 'ENABLED_FLAGS_ONLY', IGNORE_USER_PROFILE_SERVICE = 'IGNORE_USER_PROFILE_SERVICE', INCLUDE_REASONS = 'INCLUDE_REASONS', - EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES' + EXCLUDE_VARIABLES = 'EXCLUDE_VARIABLES', } /** @@ -255,12 +255,13 @@ export interface OptimizelyOptions { eventProcessor: EventProcessor; isValidInstance: boolean; jsonSchemaValidator?: { - validate(jsonObject: unknown): boolean, + validate(jsonObject: unknown): boolean; }; logger: LoggerFacade; sdkKey?: string; userProfileService?: UserProfileService | null; defaultDecideOptions?: OptimizelyDecideOption[]; + odpManager?: OdpManager; notificationCenter: NotificationCenterImpl; } @@ -285,43 +286,15 @@ export interface OptimizelyVariable { export interface Client { notificationCenter: NotificationCenter; - createUserContext( - userId: string, - attributes?: UserAttributes - ): OptimizelyUserContext | null; - activate( - experimentKey: string, - userId: string, - attributes?: UserAttributes - ): string | null; - track( - eventKey: string, - userId: string, - attributes?: UserAttributes, - eventTags?: EventTags - ): void; - getVariation( - experimentKey: string, - userId: string, - attributes?: UserAttributes - ): string | null; + createUserContext(userId: string, attributes?: UserAttributes): OptimizelyUserContext | null; + activate(experimentKey: string, userId: string, attributes?: UserAttributes): string | null; + track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void; + getVariation(experimentKey: string, userId: string, attributes?: UserAttributes): string | null; setForcedVariation(experimentKey: string, userId: string, variationKey: string | null): boolean; getForcedVariation(experimentKey: string, userId: string): string | null; - isFeatureEnabled( - featureKey: string, - userId: string, - attributes?: UserAttributes - ): boolean; - getEnabledFeatures( - userId: string, - attributes?: UserAttributes - ): string[]; - getFeatureVariable( - featureKey: string, - variableKey: string, - userId: string, - attributes?: UserAttributes - ): unknown; + isFeatureEnabled(featureKey: string, userId: string, attributes?: UserAttributes): boolean; + getEnabledFeatures(userId: string, attributes?: UserAttributes): string[]; + getFeatureVariable(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): unknown; getFeatureVariableBoolean( featureKey: string, variableKey: string, @@ -346,12 +319,7 @@ export interface Client { userId: string, attributes?: UserAttributes ): string | null; - getFeatureVariableJSON( - featureKey: string, - variableKey: string, - userId: string, - attributes?: UserAttributes - ): unknown; + getFeatureVariableJSON(featureKey: string, variableKey: string, userId: string, attributes?: UserAttributes): unknown; getAllFeatureVariables( featureKey: string, userId: string, @@ -379,16 +347,12 @@ export interface TrackListenerPayload extends ListenerPayload { * For compatibility with the previous declaration file */ export interface Config extends ConfigLite { - // options for Datafile Manager - datafileOptions?: DatafileOptions; - // limit of events to dispatch in a batch - eventBatchSize?: number; - // maximum time for an event to stay in the queue - eventFlushInterval?: number; - // maximum size for the event queue - eventMaxQueueSize?: number; - // sdk key + datafileOptions?: DatafileOptions; // Options for Datafile Manager + eventBatchSize?: number; // Maximum size of events to be dispatched in a batch + eventFlushInterval?: number; // Maximum time for an event to be enqueued + eventMaxQueueSize?: number; // Maximum size for the event queue sdkKey?: string; + odpManager?: OdpManager; } /** @@ -406,7 +370,7 @@ export interface ConfigLite { eventDispatcher?: EventDispatcher; // The object to validate against the schema jsonSchemaValidator?: { - validate(jsonObject: unknown): boolean, + validate(jsonObject: unknown): boolean; }; // level of logging i.e debug, info, error, warning etc logLevel?: LogLevel | string; @@ -422,15 +386,15 @@ export interface ConfigLite { export type OptimizelyExperimentsMap = { [experimentKey: string]: OptimizelyExperiment; -} +}; export type OptimizelyVariablesMap = { [variableKey: string]: OptimizelyVariable; -} +}; export type OptimizelyFeaturesMap = { [featureKey: string]: OptimizelyFeature; -} +}; export type OptimizelyAttribute = { id: string; @@ -493,17 +457,9 @@ export interface OptimizelyUserContext { getUserId(): string; getAttributes(): UserAttributes; setAttribute(key: string, value: unknown): void; - decide( - key: string, - options?: OptimizelyDecideOption[] - ): OptimizelyDecision; - decideForKeys( - keys: string[], - options?: OptimizelyDecideOption[], - ): { [key: string]: OptimizelyDecision }; - decideAll( - options?: OptimizelyDecideOption[], - ): { [key: string]: OptimizelyDecision }; + decide(key: string, options?: OptimizelyDecideOption[]): OptimizelyDecision; + decideForKeys(keys: string[], options?: OptimizelyDecideOption[]): { [key: string]: OptimizelyDecision }; + decideAll(options?: OptimizelyDecideOption[]): { [key: string]: OptimizelyDecision }; trackEvent(eventName: string, eventTags?: EventTags): void; setForcedDecision(context: OptimizelyDecisionContext, decision: OptimizelyForcedDecision): boolean; getForcedDecision(context: OptimizelyDecisionContext): OptimizelyForcedDecision | null; diff --git a/packages/optimizely-sdk/lib/utils/enums/index.ts b/packages/optimizely-sdk/lib/utils/enums/index.ts index 8c8a345fe..8ce209f09 100644 --- a/packages/optimizely-sdk/lib/utils/enums/index.ts +++ b/packages/optimizely-sdk/lib/utils/enums/index.ts @@ -1,5 +1,5 @@ /**************************************************************************** - * Copyright 2016-2022, Optimizely, Inc. and contributors * + * Copyright 2016-2023, Optimizely, Inc. and contributors * * * * Licensed under the Apache License, Version 2.0 (the "License"); * * you may not use this file except in compliance with the License. * @@ -57,6 +57,8 @@ export const ERROR_MESSAGES = { ODP_INVALID_DATA: '%s: ODP data is not valid', ODP_NOT_INTEGRATED: '%s: ODP is not integrated', ODP_NOT_ENABLED: '%s: ODP is not enabled', + ODP_SDK_KEY_MISSING_NOTIFICATION_CENTER_FAILURE: + '%s: You must provide an sdkKey. Cannot start Notification Center for ODP Integration.', UNDEFINED_ATTRIBUTE: '%s: Provided attribute: %s has an undefined value.', UNRECOGNIZED_ATTRIBUTE: '%s: Unrecognized attribute %s provided. Pruning before sending event to Optimizely.', UNABLE_TO_CAST_VALUE: '%s: Unable to cast value %s to type %s, returning null.', diff --git a/packages/optimizely-sdk/lib/utils/fns/index.ts b/packages/optimizely-sdk/lib/utils/fns/index.ts index c769f147b..6852e030d 100644 --- a/packages/optimizely-sdk/lib/utils/fns/index.ts +++ b/packages/optimizely-sdk/lib/utils/fns/index.ts @@ -153,8 +153,21 @@ export function sprintf(format: string, ...args: any[]): string { }) } + + +/** + * Checks two string arrays for equality. + * @param arrayA First Array to be compared against. + * @param arrayB Second Array to be compared against. + * @returns {boolean} True if both arrays are equal, otherwise returns false. + */ +export function checkArrayEquality(arrayA: string[], arrayB: string[]): boolean { + return arrayA.length === arrayB.length && arrayA.every((item, index) => item === arrayB[index]); +} + export default { assign, + checkArrayEquality, currentTimestamp, isSafeInteger, keyBy, diff --git a/packages/optimizely-sdk/tests/odpEventManager.spec.ts b/packages/optimizely-sdk/tests/odpEventManager.spec.ts index 13f70d1e9..ad9e308ed 100644 --- a/packages/optimizely-sdk/tests/odpEventManager.spec.ts +++ b/packages/optimizely-sdk/tests/odpEventManager.spec.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,6 +14,8 @@ * limitations under the License. */ +import { expect, beforeAll } from '@jest/globals'; + import { OdpConfig } from '../lib/core/odp/odp_config'; import { OdpEventManager, STATE } from '../lib/core/odp/odp_event_manager'; import { anything, capture, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; @@ -448,7 +450,7 @@ describe('OdpEventManager', () => { expect(eventManager['odpConfig'].apiKey).toEqual(apiKey); expect(eventManager['odpConfig'].apiHost).toEqual(apiHost); - expect(eventManager['odpConfig'].segmentsToCheck).toContain(segmentsToCheck[0]); - expect(eventManager['odpConfig'].segmentsToCheck).toContain(segmentsToCheck[1]); + expect(eventManager['odpConfig'].segmentsToCheck).toContain(Array.from(segmentsToCheck)[0]); + expect(eventManager['odpConfig'].segmentsToCheck).toContain(Array.from(segmentsToCheck)[1]); }); }); diff --git a/packages/optimizely-sdk/tests/odpManager.spec.ts b/packages/optimizely-sdk/tests/odpManager.spec.ts index 68ccdf790..e3f9a1577 100644 --- a/packages/optimizely-sdk/tests/odpManager.spec.ts +++ b/packages/optimizely-sdk/tests/odpManager.spec.ts @@ -15,6 +15,7 @@ */ /// +import { expect, describe, it, beforeAll, beforeEach } from '@jest/globals'; import { BrowserLRUCache } from './../lib/utils/lru_cache/browser_lru_cache'; import { OdpSegmentManager } from './../lib/core/odp/odp_segment_manager'; import { LOG_MESSAGES } from './../lib/utils/enums/index'; @@ -25,7 +26,6 @@ import { anything, capture, instance, mock, resetCalls, spy, verify, when } from import { LogHandler, LogLevel } from '../lib/modules/logging'; import { RequestHandler } from '../lib/utils/http_request_handler/http'; import { LRUCache } from '../lib/utils/lru_cache'; -import { expect } from 'chai'; import { OdpSegmentApiManager } from '../lib/core/odp/odp_segment_api_manager'; import { OdpEventManager, STATE } from '../lib/core/odp/odp_event_manager'; @@ -51,7 +51,10 @@ describe('OdpManager', () => { let mockEventManager: OdpEventManager; let mockSegmentManager: OdpSegmentManager; - const segmentsCache = new LRUCache>({ + let eventManagerInstance: OdpEventManager; + let segmentManagerInstance: OdpSegmentManager; + + const segmentsCache = new LRUCache({ maxSize: 1000, timeout: 1000, }); @@ -66,6 +69,9 @@ describe('OdpManager', () => { mockEventManager = mock(); mockSegmentManager = mock(); + + eventManagerInstance = instance(mockEventManager); + segmentManagerInstance = instance(mockSegmentManager); }); beforeEach(() => { @@ -79,8 +85,8 @@ describe('OdpManager', () => { const manager = new OdpManager(true, requestHandler, logger); manager.updateSettings('valid', 'host', []); - expect(manager.odpConfig.apiKey).to.equal(''); - expect(manager.odpConfig.apiHost).to.equal(''); + expect(manager.odpConfig.apiKey).toEqual(''); + expect(manager.odpConfig.apiHost).toEqual(''); await manager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, 'user1', []); verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_ENABLED)).twice(); @@ -88,16 +94,18 @@ describe('OdpManager', () => { manager.identifyUser('user1'); verify(mockLogger.log(LogLevel.DEBUG, LOG_MESSAGES.ODP_IDENTIFY_FAILED_ODP_DISABLED)).once(); - expect(manager.eventManager).to.be.null; - expect(manager.segmentManager).to.be.null; + expect(manager.eventManager).toBeNull; + expect(manager.segmentManager).toBeNull; }); - it('should start ODP Event manager when ODP Manager is initialized', async () => { - const manager = new OdpManager(false, requestHandler, logger, undefined, mockSegmentManager, mockEventManager); - expect(manager.eventManager?.state).to.equal(STATE.RUNNING); + it('should start ODP Event manager when ODP Manager is initialized', () => { + const manager = new OdpManager(false, requestHandler, logger, undefined); + + expect(manager.eventManager).not.toBeNull(); + expect(manager.eventManager?.state).toEqual(STATE.RUNNING); }); - it('should be able to fetch qualified segments with a valid OdpConfig and enabled OdpManager instance', async () => { + it('should be able to fetch qualified segments with a valid OdpConfig and enabled OdpManager instance', () => { // const segmentManager = new OdpSegmentManager( // new OdpConfig(API_KEY, API_HOST, []), // new BrowserLRUCache(), @@ -106,7 +114,7 @@ describe('OdpManager', () => { // ); const manager = new OdpManager(false, requestHandler, logger, undefined, mockSegmentManager); - expect(manager.segmentManager).to.exist; + expect(manager.segmentManager).not.toBeNull(); // await manager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, 'user1', []); // verify(manager.segmentManager?.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, 'user1', [])).once(); diff --git a/packages/optimizely-sdk/tests/odpSegmentApiManager.ts b/packages/optimizely-sdk/tests/odpSegmentApiManager.ts index 5056145a2..5af60aca7 100644 --- a/packages/optimizely-sdk/tests/odpSegmentApiManager.ts +++ b/packages/optimizely-sdk/tests/odpSegmentApiManager.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022-2023, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ /// +import { expect, beforeAll } from '@jest/globals'; import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; import { LogHandler, LogLevel } from '../lib/modules/logging'; import { OdpSegmentApiManager } from '../lib/core/odp/odp_segment_api_manager'; @@ -150,7 +151,7 @@ describe('OdpSegmentApiManager', () => { const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(2); + expect(segments?.length).toEqual(2); expect(segments).toContain('has_email'); expect(segments).toContain('has_email_opted_in'); verify(mockLogger.log(anything(), anyString())).never(); @@ -159,9 +160,15 @@ describe('OdpSegmentApiManager', () => { it('should handle a request to query no segments', async () => { const manager = managerInstance(); - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, ODP_USER_KEY.FS_USER_ID, USER_VALUE, []); + const segments = await manager.fetchSegments( + API_key, + GRAPHQL_ENDPOINT, + ODP_USER_KEY.FS_USER_ID, + USER_VALUE, + [] + ); - expect(segments).toHaveLength(0); + if (segments) expect(segments.length).toEqual(0); verify(mockLogger.log(anything(), anyString())).never(); }); @@ -174,7 +181,7 @@ describe('OdpSegmentApiManager', () => { const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - expect(segments).toHaveLength(0); + if (segments) expect(segments.length).toEqual(0); verify(mockLogger.log(anything(), anyString())).never(); }); diff --git a/packages/optimizely-sdk/tests/odpSegmentManager.spec.ts b/packages/optimizely-sdk/tests/odpSegmentManager.spec.ts index 91277c5ee..71450d7f3 100644 --- a/packages/optimizely-sdk/tests/odpSegmentManager.spec.ts +++ b/packages/optimizely-sdk/tests/odpSegmentManager.spec.ts @@ -61,7 +61,7 @@ describe('OdpSegmentManager', () => { const API_KEY = 'test-api-key'; const API_HOST = 'https://odp.example.com'; odpConfig = new OdpConfig(API_KEY, API_HOST, []); - const segmentsCache = new LRUCache>({ + const segmentsCache = new LRUCache({ maxSize: 1000, timeout: 1000, }); @@ -121,7 +121,7 @@ describe('OdpSegmentManager', () => { // Utility Functions - function setCache(userKey: string, userValue: string, value: Array) { + function setCache(userKey: string, userValue: string, value: string[]) { const cacheKey = manager.makeCacheKey(userKey, userValue); manager.segmentsCache.save({ key: cacheKey, @@ -129,7 +129,7 @@ describe('OdpSegmentManager', () => { }); } - function peekCache(userKey: string, userValue: string): Array | null { + function peekCache(userKey: string, userValue: string): string[] | null { const cacheKey = manager.makeCacheKey(userKey, userValue); return manager.segmentsCache.peek(cacheKey); }