Skip to content
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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<string, NotificationCenter>();

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);
}
}
}
37 changes: 25 additions & 12 deletions packages/optimizely-sdk/lib/core/odp/odp_config.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -14,6 +14,8 @@
* limitations under the License.
*/

import { checkArrayEquality } from '../../../lib/utils/fns';

export class OdpConfig {
/**
* Host of ODP audience segments API.
Expand Down Expand Up @@ -57,26 +59,24 @@ export class OdpConfig {
return this._segmentsToCheck;
}

constructor(apiKey: string, apiHost: string, segmentsToCheck?: string[]) {
this._apiKey = apiKey;
this._apiHost = apiHost;
constructor(apiKey?: string, apiHost?: string, segmentsToCheck?: string[]) {
this._apiKey = apiKey ?? '';
this._apiHost = apiHost ?? '';
this._segmentsToCheck = segmentsToCheck ?? [];
}

/**
* Update the ODP configuration details
* @param apiKey Public API key for the ODP account
* @param apiHost Host of ODP audience segments API
* @param segmentsToCheck Audience segments
* @param {OdpConfig} config New ODP Config to potentially update self with
* @returns true if configuration was updated successfully
*/
public update(apiKey: string, apiHost: string, segmentsToCheck: string[]): boolean {
if (this._apiKey === apiKey && this._apiHost === apiHost && this._segmentsToCheck === segmentsToCheck) {
public update(config: OdpConfig): boolean {
if (this.equals(config)) {
return false;
} else {
this._apiKey = apiKey;
this._apiHost = apiHost;
this._segmentsToCheck = segmentsToCheck;
if (config.apiKey) this._apiKey = config.apiKey;
if (config.apiHost) this._apiHost = config.apiHost;
if (config.segmentsToCheck) this._segmentsToCheck = config.segmentsToCheck;

return true;
}
Expand All @@ -88,4 +88,17 @@ export class OdpConfig {
public isReady(): boolean {
return !!this._apiKey && !!this._apiHost;
}

/**
* Detects if there are any changes between the current and incoming ODP Configs
* @param configToCompare ODP Configuration to check self against for equality
* @returns Boolean based on if the current ODP Config is equivalent to the incoming ODP Config
*/
public equals(configToCompare: OdpConfig): boolean {
return (
this._apiHost === configToCompare._apiHost &&
this._apiKey === configToCompare._apiKey &&
checkArrayEquality(this.segmentsToCheck, configToCompare._segmentsToCheck)
);
}
}
63 changes: 32 additions & 31 deletions packages/optimizely-sdk/lib/core/odp/odp_event_manager.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -15,11 +15,14 @@
*/

import { LogHandler, LogLevel } from '../../modules/logging';
import { OdpEvent } from './odp_event';

import { uuid } from '../../utils/fns';
import { ODP_USER_KEY } from '../../utils/enums';
import { ERROR_MESSAGES, ODP_USER_KEY, ODP_EVENT_TYPE } from '../../utils/enums';

import { OdpEvent } from './odp_event';
import { OdpConfig } from './odp_config';
import { OdpEventApiManager } from './odp_event_api_manager';
import { invalidOdpDataFound } from './odp_utils';

const MAX_RETRIES = 3;
const DEFAULT_BATCH_SIZE = 10;
Expand Down Expand Up @@ -145,11 +148,18 @@ export class OdpEventManager implements IOdpEventManager {
}

/**
* Update ODP configuration settings
* @param odpConfig New configuration to apply
* Update ODP configuration settings.
* @param newConfig New configuration to apply
*/
public updateSettings(odpConfig: OdpConfig): void {
this.odpConfig = odpConfig;
public updateSettings(newConfig: OdpConfig): void {
this.odpConfig = newConfig;
}

/**
* Cleans up all pending events; occurs every time the ODP Config is updated.
*/
public flush(): void {
this.processQueue(true);
}

/**
Expand Down Expand Up @@ -181,23 +191,31 @@ export class OdpEventManager implements IOdpEventManager {
const identifiers = new Map<string, string>();
identifiers.set(ODP_USER_KEY.VUID, vuid);

const event = new OdpEvent('fullstack', 'client_initialized', identifiers);
const event = new OdpEvent(ODP_EVENT_TYPE, 'client_initialized', identifiers);
this.sendEvent(event);
}

/**
* Associate a full-stack userid with an established VUID
* @param userId Full-stack User ID
* @param vuid Visitor User ID
* @param {string} userId (Optional) Full-stack User ID
* @param {string} vuid (Optional) Visitor User ID
*/
public identifyUser(userId: string, vuid?: string): void {
public identifyUser(userId?: string, vuid?: string): void {
const identifiers = new Map<string, string>();
if (!userId && !vuid) {
this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_SEND_EVENT_FAILED_UID_MISSING);
return;
}

if (vuid) {
identifiers.set(ODP_USER_KEY.VUID, vuid);
}
identifiers.set(ODP_USER_KEY.FS_USER_ID, userId);

const event = new OdpEvent('fullstack', 'identified', identifiers);
if (userId) {
identifiers.set(ODP_USER_KEY.FS_USER_ID, userId);
}

const event = new OdpEvent(ODP_EVENT_TYPE, 'identified', identifiers);
this.sendEvent(event);
}

Expand All @@ -206,7 +224,7 @@ export class OdpEventManager implements IOdpEventManager {
* @param event ODP Event to forward
*/
public sendEvent(event: OdpEvent): void {
if (this.invalidDataFound(event.data)) {
if (invalidOdpDataFound(event.data)) {
this.logger.log(LogLevel.ERROR, 'Event data found to be invalid.');
} else {
event.data = this.augmentCommonData(event.data);
Expand Down Expand Up @@ -374,23 +392,6 @@ export class OdpEventManager implements IOdpEventManager {
return false;
}

/**
* Validate event data value types
* @param data Event data to be validated
* @returns True if an invalid type was found in the data otherwise False
* @private
*/
private invalidDataFound(data: Map<string, unknown>): boolean {
const validTypes: string[] = ['string', 'number', 'boolean'];
let foundInvalidValue = false;
data.forEach(value => {
if (!validTypes.includes(typeof value) && value !== null) {
foundInvalidValue = true;
}
});
return foundInvalidValue;
}

/**
* Add additional common data including an idempotent ID and execution context to event data
* @param sourceData Existing event data to augment
Expand Down
Loading