Skip to content

Commit 0d114f6

Browse files
committed
WIP: ODPManager -tests; Base for NotificationCenter Issue
1 parent ce4224d commit 0d114f6

File tree

12 files changed

+534
-25
lines changed

12 files changed

+534
-25
lines changed

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

Lines changed: 23 additions & 10 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.
@@ -19,7 +19,7 @@ export class OdpConfig {
1919
* Host of ODP audience segments API.
2020
* @private
2121
*/
22-
private _apiHost: string;
22+
private _apiHost = '';
2323

2424
/**
2525
* Getter to retrieve the ODP server host
@@ -33,7 +33,7 @@ export class OdpConfig {
3333
* Public API key for the ODP account from which the audience segments will be fetched (optional).
3434
* @private
3535
*/
36-
private _apiKey: string;
36+
private _apiKey = '';
3737

3838
/**
3939
* Getter to retrieve the ODP API key
@@ -57,9 +57,9 @@ export class OdpConfig {
5757
return this._segmentsToCheck;
5858
}
5959

60-
constructor(apiKey: string, apiHost: string, segmentsToCheck?: string[]) {
61-
this._apiKey = apiKey;
62-
this._apiHost = apiHost;
60+
constructor(apiKey?: string, apiHost?: string, segmentsToCheck?: string[]) {
61+
if (apiKey) this._apiKey = apiKey;
62+
if (apiHost) this._apiHost = apiHost;
6363
this._segmentsToCheck = segmentsToCheck ?? [];
6464
}
6565

@@ -70,13 +70,13 @@ export class OdpConfig {
7070
* @param segmentsToCheck Audience segments
7171
* @returns true if configuration was updated successfully
7272
*/
73-
public update(apiKey: string, apiHost: string, segmentsToCheck: string[]): boolean {
73+
public update(apiKey?: string, apiHost?: string, segmentsToCheck?: string[]): boolean {
7474
if (this._apiKey === apiKey && this._apiHost === apiHost && this._segmentsToCheck === segmentsToCheck) {
7575
return false;
7676
} else {
77-
this._apiKey = apiKey;
78-
this._apiHost = apiHost;
79-
this._segmentsToCheck = segmentsToCheck;
77+
if (apiKey) this._apiKey = apiKey;
78+
if (apiHost) this._apiHost = apiHost;
79+
if (segmentsToCheck) this._segmentsToCheck = 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 config ODP Configuration
95+
* @returns Boolean based on if the current ODP Config is equivalent to the incoming ODP Config
96+
*/
97+
public equals(config: OdpConfig): boolean {
98+
return (
99+
this._apiHost == config._apiHost &&
100+
this._apiKey == config._apiKey &&
101+
JSON.stringify(this.segmentsToCheck) == JSON.stringify(config._segmentsToCheck)
102+
);
103+
}
91104
}
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
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 { LOG_MESSAGES } from './../../utils/enums/index';
18+
import { RequestHandler } from './../../utils/http_request_handler/http';
19+
import { BrowserLRUCacheConfig } from './../../utils/lru_cache/browser_lru_cache';
20+
import { LRUCache } from './../../utils/lru_cache/lru_cache';
21+
import { ERROR_MESSAGES, ODP_USER_KEY } from '../../utils/enums';
22+
23+
import { getLogger, LogHandler, LogLevel } from '../../modules/logging';
24+
import { OdpConfig } from './odp_config';
25+
import { OdpEventManager } from './odp_event_manager';
26+
import { OdpSegmentManager } from './odp_segment_manager';
27+
import { OdpSegmentApiManager } from './odp_segment_api_manager';
28+
import { OdpEventApiManager } from './odp_event_api_manager';
29+
import { OptimizelySegmentOption } from './optimizely_segment_option';
30+
import { areOdpDataTypesValid } from './odp_types';
31+
import { OdpEvent } from './odp_event';
32+
import { VuidManager } from '../../plugins/vuid_manager';
33+
34+
// Orchestrates segments manager, event manager, and ODP configuration
35+
export class OdpManager {
36+
static MODULE_NAME = 'OdpManager';
37+
38+
enabled: boolean;
39+
odpConfig: OdpConfig;
40+
logger: LogHandler;
41+
42+
/**
43+
* ODP Segment Manager which provides an interface to the remote ODP server (GraphQL API) for audience segments mapping.
44+
* It fetches all qualified segments for the given user context and manages the segments cache for all user contexts.
45+
* @private
46+
*/
47+
private _segmentManager: OdpSegmentManager | null;
48+
49+
/**
50+
* Getter to retrieve the ODP segment manager.
51+
* @public
52+
*/
53+
public get segmentManager(): OdpSegmentManager | null {
54+
return this._segmentManager;
55+
}
56+
57+
/**
58+
* ODP Event Manager which provides an interface to the remote ODP server (REST API) for events.
59+
* It will queue all pending events (persistent) and send them (in batches of up to 10 events) to the ODP server when possible.
60+
* @private
61+
*/
62+
private _eventManager: OdpEventManager | null;
63+
64+
/**
65+
* Getter to retrieve the ODP event manager.
66+
* @public
67+
*/
68+
public get eventManager(): OdpEventManager | null {
69+
return this._eventManager;
70+
}
71+
72+
constructor(
73+
disable: boolean,
74+
requestHandler: RequestHandler,
75+
logger?: LogHandler,
76+
segmentsCache?: LRUCache<string, string[]>,
77+
segmentManager?: OdpSegmentManager,
78+
eventManager?: OdpEventManager,
79+
clientEngine?: string,
80+
clientVersion?: string
81+
) {
82+
this.enabled = !disable;
83+
this.odpConfig = new OdpConfig();
84+
this.logger = logger || getLogger(OdpManager.MODULE_NAME);
85+
86+
this._segmentManager = segmentManager || null;
87+
this._eventManager = eventManager || null;
88+
89+
if (!this.enabled) {
90+
this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_ENABLED);
91+
return;
92+
}
93+
94+
// Set up Segment Manager (Audience Segments GraphQL API Interface)
95+
if (this._segmentManager) {
96+
this._segmentManager.odpConfig = this.odpConfig;
97+
} else {
98+
this._segmentManager = new OdpSegmentManager(
99+
this.odpConfig,
100+
segmentsCache ||
101+
new LRUCache({
102+
maxSize: BrowserLRUCacheConfig.DEFAULT_CAPACITY,
103+
timeout: BrowserLRUCacheConfig.DEFAULT_TIMEOUT_SECS,
104+
}),
105+
new OdpSegmentApiManager(requestHandler, this.logger)
106+
);
107+
}
108+
109+
// Set up Events Manager (Events REST API Interface)
110+
if (eventManager) {
111+
eventManager.updateSettings(this.odpConfig);
112+
this._eventManager = eventManager;
113+
} else {
114+
this._eventManager = new OdpEventManager({
115+
odpConfig: this.odpConfig,
116+
apiManager: new OdpEventApiManager(requestHandler, this.logger),
117+
logger: this.logger,
118+
clientEngine: clientEngine || 'javascript-sdk',
119+
clientVersion: clientVersion || '4.9.2',
120+
});
121+
}
122+
123+
this._eventManager.start();
124+
}
125+
126+
/**
127+
* Provides a method to update ODP Manager's ODP Config API Key, API Host, and Audience Segments
128+
* @param apiKey Public API key for the ODP account
129+
* @param apiHost Host of ODP APIs for Audience Segments and Events
130+
* @param segmentsToCheck List of audience segments included in the new ODP Config
131+
*/
132+
public updateSettings(apiKey?: string, apiHost?: string, segmentsToCheck?: string[]): boolean {
133+
if (!this.enabled) return false;
134+
135+
const configChanged = this.odpConfig.update(apiKey, apiHost, segmentsToCheck);
136+
137+
if (configChanged) {
138+
this._eventManager?.updateSettings(this.odpConfig);
139+
this._segmentManager?.reset();
140+
this._segmentManager?.updateSettings(this.odpConfig);
141+
return true;
142+
}
143+
144+
return false;
145+
}
146+
147+
/**
148+
* Attempts to stop the current instance of ODP Manager's event manager, if it exists and is running.
149+
*/
150+
public close(): void {
151+
this._eventManager?.stop();
152+
}
153+
154+
/**
155+
* Attempts to fetch and return a list of a user's qualified segments from the local segments cache.
156+
* If no cached data exists for the target user, this fetches and caches data from the ODP server instead.
157+
* @param userId Unique identifier of a target user.
158+
* @param options An array of OptimizelySegmentOption used to ignore and/or reset the cache.
159+
* @returns
160+
*/
161+
public async fetchQualifiedSegments(
162+
userKey: ODP_USER_KEY,
163+
userId: string,
164+
options: Array<OptimizelySegmentOption>
165+
): Promise<string[] | null> {
166+
if (!this.enabled || !this._segmentManager) {
167+
this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_ENABLED);
168+
return null;
169+
}
170+
171+
return this._segmentManager.fetchQualifiedSegments(userKey, userId, options);
172+
}
173+
174+
/**
175+
* Identifies a user via the ODP Event Manager
176+
* @param userId Unique identifier of a target user.
177+
* @param vuid Automatically generated unique user identifier (for client-sdks only)
178+
* @returns
179+
*/
180+
public identifyUser(userId: string, vuid?: string): void {
181+
if (!this.enabled || !this._eventManager) {
182+
this.logger.log(LogLevel.DEBUG, LOG_MESSAGES.ODP_IDENTIFY_FAILED_ODP_DISABLED);
183+
return;
184+
}
185+
186+
if (!this.odpConfig.isReady()) {
187+
this.logger.log(LogLevel.DEBUG, LOG_MESSAGES.ODP_IDENTIFY_FAILED_ODP_NOT_INTEGRATED);
188+
return;
189+
}
190+
191+
this._eventManager.identifyUser(userId, vuid);
192+
}
193+
194+
/**
195+
* Sends an event to the ODP Server via the ODP Events API
196+
* @param type The event type
197+
* @param action The event action name
198+
* @param identifiers A map of identifiers
199+
* @param data A map of associated data; default event data will be included here before sending to ODP
200+
*/
201+
public sendEvent(type: string, action: string, identifiers: Map<string, string>, data: Map<string, any>): void {
202+
if (!this.enabled || !this._eventManager) {
203+
throw new Error('ODP Not Enabled');
204+
}
205+
206+
if (!this.odpConfig.isReady()) {
207+
throw new Error('ODP Not Integrated');
208+
}
209+
210+
if (!areOdpDataTypesValid(data)) {
211+
throw new Error('ODP Data Invalid');
212+
}
213+
214+
this._eventManager.sendEvent(new OdpEvent(type, action, identifiers, data));
215+
}
216+
}

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

Lines changed: 9 additions & 1 deletion
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.
@@ -113,4 +113,12 @@ export class OdpSegmentManager {
113113
makeCacheKey(userKey: string, userValue: string): string {
114114
return `${userKey}-$-${userValue}`;
115115
}
116+
117+
/**
118+
* Updates the ODP Config settings of ODP Segment Manager
119+
* @param config New ODP Config that will overwrite the existing config
120+
*/
121+
public updateSettings(config: OdpConfig): void {
122+
this.odpConfig = config;
123+
}
116124
}

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

Lines changed: 19 additions & 1 deletion
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.
@@ -82,3 +82,21 @@ export interface Node {
8282
name: string;
8383
state: string;
8484
}
85+
86+
/**
87+
* Checks a map of data for type validity
88+
* @param data Arbitrary data map of string keys to any value
89+
* @returns Boolean based on if data consists solely of valid types or not.
90+
*/
91+
export function areOdpDataTypesValid(data: Map<string, any>): boolean {
92+
const validTypes = ['string', 'number', 'boolean', undefined];
93+
let isValid = true;
94+
95+
data.forEach(item => {
96+
if (!validTypes.includes(typeof item)) {
97+
isValid = false;
98+
}
99+
});
100+
101+
return isValid;
102+
}

0 commit comments

Comments
 (0)