Skip to content

Commit 0116d75

Browse files
authored
[FSSDK-8452] feat: Added ODP Manager Implementation (#797)
1 parent ce4224d commit 0116d75

30 files changed

+1467
-375
lines changed
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/**
2+
* Copyright 2023, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { describe, it } from 'mocha';
18+
import { expect } from 'chai';
19+
20+
import { NotificationRegistry } from './notification_registry';
21+
22+
describe('Notification Registry', () => {
23+
it('Returns null notification center when SDK Key is null', () => {
24+
const notificationCenter = NotificationRegistry.getNotificationCenter();
25+
expect(notificationCenter).to.be.undefined;
26+
});
27+
28+
it('Returns the same notification center when SDK Keys are the same and not null', () => {
29+
const sdkKey = 'testSDKKey';
30+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey);
31+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey);
32+
expect(notificationCenterA).to.eql(notificationCenterB);
33+
});
34+
35+
it('Returns different notification centers when SDK Keys are not the same', () => {
36+
const sdkKeyA = 'testSDKKeyA';
37+
const sdkKeyB = 'testSDKKeyB';
38+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKeyA);
39+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKeyB);
40+
expect(notificationCenterA).to.not.eql(notificationCenterB);
41+
});
42+
43+
it('Removes old notification centers from the registry when removeNotificationCenter is called on the registry', () => {
44+
const sdkKey = 'testSDKKey';
45+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey);
46+
NotificationRegistry.removeNotificationCenter(sdkKey);
47+
48+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey);
49+
50+
expect(notificationCenterA).to.not.eql(notificationCenterB);
51+
});
52+
53+
it('Does not throw an error when calling removeNotificationCenter with a null SDK Key', () => {
54+
const sdkKey = 'testSDKKey';
55+
const notificationCenterA = NotificationRegistry.getNotificationCenter(sdkKey);
56+
NotificationRegistry.removeNotificationCenter();
57+
58+
const notificationCenterB = NotificationRegistry.getNotificationCenter(sdkKey);
59+
60+
expect(notificationCenterA).to.eql(notificationCenterB);
61+
});
62+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* Copyright 2023, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { getLogger, LogHandler, LogLevel } from '../../modules/logging';
18+
import { NotificationCenter, createNotificationCenter } from '../../core/notification_center';
19+
20+
/**
21+
* Internal notification center registry for managing multiple notification centers.
22+
*/
23+
export class NotificationRegistry {
24+
private static _notificationCenters = new Map<string, NotificationCenter>();
25+
26+
constructor() {}
27+
28+
/**
29+
* Retrieves an SDK Key's corresponding notification center in the registry if it exists, otherwise it creates one
30+
* @param sdkKey SDK Key to be used for the notification center tied to the ODP Manager
31+
* @param logger Logger to be used for the corresponding notification center
32+
* @returns {NotificationCenter | undefined} a notification center instance for ODP Manager if a valid SDK Key is provided, otherwise undefined
33+
*/
34+
public static getNotificationCenter(
35+
sdkKey?: string,
36+
logger: LogHandler = getLogger()
37+
): NotificationCenter | undefined {
38+
if (!sdkKey) {
39+
logger.log(LogLevel.ERROR, 'No SDK key provided to getNotificationCenter.');
40+
return undefined;
41+
}
42+
43+
let notificationCenter;
44+
if (this._notificationCenters.has(sdkKey)) {
45+
notificationCenter = this._notificationCenters.get(sdkKey);
46+
} else {
47+
notificationCenter = createNotificationCenter({
48+
logger,
49+
errorHandler: { handleError: () => {} },
50+
});
51+
this._notificationCenters.set(sdkKey, notificationCenter);
52+
}
53+
54+
return notificationCenter;
55+
}
56+
57+
public static removeNotificationCenter(sdkKey?: string): void {
58+
if (!sdkKey) {
59+
return;
60+
}
61+
62+
const notificationCenter = this._notificationCenters.get(sdkKey);
63+
if (notificationCenter) {
64+
notificationCenter.clearAllNotificationListeners();
65+
this._notificationCenters.delete(sdkKey);
66+
}
67+
}
68+
}

packages/optimizely-sdk/lib/core/odp/odp_config.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022, Optimizely
2+
* Copyright 2022-2023, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -14,6 +14,8 @@
1414
* limitations under the License.
1515
*/
1616

17+
import { checkArrayEquality } from '../../../lib/utils/fns';
18+
1719
export class OdpConfig {
1820
/**
1921
* Host of ODP audience segments API.
@@ -57,26 +59,24 @@ export class OdpConfig {
5759
return this._segmentsToCheck;
5860
}
5961

60-
constructor(apiKey: string, apiHost: string, segmentsToCheck?: string[]) {
61-
this._apiKey = apiKey;
62-
this._apiHost = apiHost;
62+
constructor(apiKey?: string, apiHost?: string, segmentsToCheck?: string[]) {
63+
this._apiKey = apiKey ?? '';
64+
this._apiHost = apiHost ?? '';
6365
this._segmentsToCheck = segmentsToCheck ?? [];
6466
}
6567

6668
/**
6769
* Update the ODP configuration details
68-
* @param apiKey Public API key for the ODP account
69-
* @param apiHost Host of ODP audience segments API
70-
* @param segmentsToCheck Audience segments
70+
* @param {OdpConfig} config New ODP Config to potentially update self with
7171
* @returns true if configuration was updated successfully
7272
*/
73-
public update(apiKey: string, apiHost: string, segmentsToCheck: string[]): boolean {
74-
if (this._apiKey === apiKey && this._apiHost === apiHost && this._segmentsToCheck === segmentsToCheck) {
73+
public update(config: OdpConfig): boolean {
74+
if (this.equals(config)) {
7575
return false;
7676
} else {
77-
this._apiKey = apiKey;
78-
this._apiHost = apiHost;
79-
this._segmentsToCheck = segmentsToCheck;
77+
if (config.apiKey) this._apiKey = config.apiKey;
78+
if (config.apiHost) this._apiHost = config.apiHost;
79+
if (config.segmentsToCheck) this._segmentsToCheck = config.segmentsToCheck;
8080

8181
return true;
8282
}
@@ -88,4 +88,17 @@ export class OdpConfig {
8888
public isReady(): boolean {
8989
return !!this._apiKey && !!this._apiHost;
9090
}
91+
92+
/**
93+
* Detects if there are any changes between the current and incoming ODP Configs
94+
* @param configToCompare ODP Configuration to check self against for equality
95+
* @returns Boolean based on if the current ODP Config is equivalent to the incoming ODP Config
96+
*/
97+
public equals(configToCompare: OdpConfig): boolean {
98+
return (
99+
this._apiHost === configToCompare._apiHost &&
100+
this._apiKey === configToCompare._apiKey &&
101+
checkArrayEquality(this.segmentsToCheck, configToCompare._segmentsToCheck)
102+
);
103+
}
91104
}

packages/optimizely-sdk/lib/core/odp/odp_event_manager.ts

Lines changed: 32 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Copyright 2022, Optimizely
2+
* Copyright 2022-2023, Optimizely
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -15,11 +15,14 @@
1515
*/
1616

1717
import { LogHandler, LogLevel } from '../../modules/logging';
18-
import { OdpEvent } from './odp_event';
18+
1919
import { uuid } from '../../utils/fns';
20-
import { ODP_USER_KEY } from '../../utils/enums';
20+
import { ERROR_MESSAGES, ODP_USER_KEY, ODP_EVENT_TYPE } from '../../utils/enums';
21+
22+
import { OdpEvent } from './odp_event';
2123
import { OdpConfig } from './odp_config';
2224
import { OdpEventApiManager } from './odp_event_api_manager';
25+
import { invalidOdpDataFound } from './odp_utils';
2326

2427
const MAX_RETRIES = 3;
2528
const DEFAULT_BATCH_SIZE = 10;
@@ -145,11 +148,18 @@ export class OdpEventManager implements IOdpEventManager {
145148
}
146149

147150
/**
148-
* Update ODP configuration settings
149-
* @param odpConfig New configuration to apply
151+
* Update ODP configuration settings.
152+
* @param newConfig New configuration to apply
150153
*/
151-
public updateSettings(odpConfig: OdpConfig): void {
152-
this.odpConfig = odpConfig;
154+
public updateSettings(newConfig: OdpConfig): void {
155+
this.odpConfig = newConfig;
156+
}
157+
158+
/**
159+
* Cleans up all pending events; occurs every time the ODP Config is updated.
160+
*/
161+
public flush(): void {
162+
this.processQueue(true);
153163
}
154164

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

184-
const event = new OdpEvent('fullstack', 'client_initialized', identifiers);
194+
const event = new OdpEvent(ODP_EVENT_TYPE, 'client_initialized', identifiers);
185195
this.sendEvent(event);
186196
}
187197

188198
/**
189199
* Associate a full-stack userid with an established VUID
190-
* @param userId Full-stack User ID
191-
* @param vuid Visitor User ID
200+
* @param {string} userId (Optional) Full-stack User ID
201+
* @param {string} vuid (Optional) Visitor User ID
192202
*/
193-
public identifyUser(userId: string, vuid?: string): void {
203+
public identifyUser(userId?: string, vuid?: string): void {
194204
const identifiers = new Map<string, string>();
205+
if (!userId && !vuid) {
206+
this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_SEND_EVENT_FAILED_UID_MISSING);
207+
return;
208+
}
209+
195210
if (vuid) {
196211
identifiers.set(ODP_USER_KEY.VUID, vuid);
197212
}
198-
identifiers.set(ODP_USER_KEY.FS_USER_ID, userId);
199213

200-
const event = new OdpEvent('fullstack', 'identified', identifiers);
214+
if (userId) {
215+
identifiers.set(ODP_USER_KEY.FS_USER_ID, userId);
216+
}
217+
218+
const event = new OdpEvent(ODP_EVENT_TYPE, 'identified', identifiers);
201219
this.sendEvent(event);
202220
}
203221

@@ -206,7 +224,7 @@ export class OdpEventManager implements IOdpEventManager {
206224
* @param event ODP Event to forward
207225
*/
208226
public sendEvent(event: OdpEvent): void {
209-
if (this.invalidDataFound(event.data)) {
227+
if (invalidOdpDataFound(event.data)) {
210228
this.logger.log(LogLevel.ERROR, 'Event data found to be invalid.');
211229
} else {
212230
event.data = this.augmentCommonData(event.data);
@@ -374,23 +392,6 @@ export class OdpEventManager implements IOdpEventManager {
374392
return false;
375393
}
376394

377-
/**
378-
* Validate event data value types
379-
* @param data Event data to be validated
380-
* @returns True if an invalid type was found in the data otherwise False
381-
* @private
382-
*/
383-
private invalidDataFound(data: Map<string, unknown>): boolean {
384-
const validTypes: string[] = ['string', 'number', 'boolean'];
385-
let foundInvalidValue = false;
386-
data.forEach(value => {
387-
if (!validTypes.includes(typeof value) && value !== null) {
388-
foundInvalidValue = true;
389-
}
390-
});
391-
return foundInvalidValue;
392-
}
393-
394395
/**
395396
* Add additional common data including an idempotent ID and execution context to event data
396397
* @param sourceData Existing event data to augment

0 commit comments

Comments
 (0)