From b2add409278ae5241c25bf16ded263edcb2ff911 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 2 Dec 2024 20:30:57 +0600 Subject: [PATCH 01/28] init refac --- lib/index.browser.ts | 4 +- lib/odp/constant.ts | 12 + .../event_api_manager.browser.ts | 69 --- .../event_manager/event_api_manager.node.ts | 51 -- .../event_manager/event_manager.browser.ts | 4 +- lib/odp/event_manager/event_manager.node.ts | 4 +- .../odp_event_api_manager.spec.ts | 209 +++++++ .../event_manager/odp_event_api_manager.ts | 149 +++-- lib/odp/event_manager/odp_event_manager.ts | 555 +++++++----------- lib/odp/odp_manager.browser.ts | 16 +- lib/odp/odp_manager.node.ts | 16 +- lib/odp/odp_manager.ts | 382 ++++++------ lib/odp/odp_utils.ts | 32 - .../odp_segment_api_manager.ts | 11 +- .../segment_manager/odp_segment_manager.ts | 124 ++-- lib/odp/ua_parser/ua_parser.browser.ts | 6 +- lib/odp/ua_parser/user_agent_parser.ts | 2 +- lib/optimizely/index.ts | 8 +- lib/plugins/vuid_manager/index.ts | 30 - lib/shared_types.ts | 30 +- lib/utils/cache/in_memory_lru_cache.ts | 83 +++ lib/utils/enums/index.ts | 21 - lib/utils/fns/index.ts | 5 - lib/utils/lru_cache/browser_lru_cache.ts | 2 +- lib/utils/lru_cache/index.ts | 2 +- lib/utils/lru_cache/lru_cache.ts | 132 ----- lib/utils/lru_cache/server_lru_cache.ts | 2 +- lib/utils/repeater/repeater.ts | 13 +- lib/vuid/vuid.spec.ts | 15 + lib/vuid/vuid.ts | 15 + tests/odpEventApiManager.spec.ts | 139 ----- vitest.config.mts | 2 +- 32 files changed, 863 insertions(+), 1282 deletions(-) create mode 100644 lib/odp/constant.ts delete mode 100644 lib/odp/event_manager/event_api_manager.browser.ts delete mode 100644 lib/odp/event_manager/event_api_manager.node.ts create mode 100644 lib/odp/event_manager/odp_event_api_manager.spec.ts delete mode 100644 lib/odp/odp_utils.ts create mode 100644 lib/utils/cache/in_memory_lru_cache.ts delete mode 100644 lib/utils/lru_cache/lru_cache.ts create mode 100644 lib/vuid/vuid.spec.ts create mode 100644 lib/vuid/vuid.ts delete mode 100644 tests/odpEventApiManager.spec.ts diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 05cc88075..537a1ffae 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -26,7 +26,7 @@ import { createNotificationCenter } from './notification_center'; import { OptimizelyDecideOption, Client, Config, OptimizelyOptions } from './shared_types'; import { BrowserOdpManager } from './odp/odp_manager.browser'; import Optimizely from './optimizely'; -import { IUserAgentParser } from './odp/ua_parser/user_agent_parser'; +import { UserAgentParser } from './odp/ua_parser/user_agent_parser'; import { getUserAgentParser } from './odp/ua_parser/ua_parser.browser'; import * as commonExports from './common_exports'; import { PollingConfigManagerConfig } from './project_config/config_manager_factory'; @@ -192,7 +192,7 @@ export { createInstance, __internalResetRetryState, OptimizelyDecideOption, - IUserAgentParser, + UserAgentParser as IUserAgentParser, getUserAgentParser, createPollingProjectConfigManager, createForwardingEventProcessor, diff --git a/lib/odp/constant.ts b/lib/odp/constant.ts new file mode 100644 index 000000000..4499fb1d8 --- /dev/null +++ b/lib/odp/constant.ts @@ -0,0 +1,12 @@ +export enum ODP_USER_KEY { + VUID = 'vuid', + FS_USER_ID = 'fs_user_id', + FS_USER_ID_ALIAS = 'fs-user-id', +} + +export enum ODP_EVENT_ACTION { + IDENTIFIED = 'identified', + INITIALIZED = 'client_initialized', +} + +export const ODP_DEFAULT_EVENT_TYPE = 'fullstack'; diff --git a/lib/odp/event_manager/event_api_manager.browser.ts b/lib/odp/event_manager/event_api_manager.browser.ts deleted file mode 100644 index 26ed98136..000000000 --- a/lib/odp/event_manager/event_api_manager.browser.ts +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright 2024, 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 { OdpEvent } from './odp_event'; -import { OdpEventApiManager } from './odp_event_api_manager'; -import { LogLevel } from '../../modules/logging'; -import { OdpConfig } from '../odp_config'; -import { HttpMethod } from '../../utils/http_request_handler/http'; - -const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; - -const pixelApiPath = 'v2/zaius.gif'; - -export class BrowserOdpEventApiManager extends OdpEventApiManager { - protected shouldSendEvents(events: OdpEvent[]): boolean { - if (events.length <= 1) { - return true; - } - this.getLogger().log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (browser only supports batch size 1)`); - return false; - } - - private getPixelApiEndpoint(odpConfig: OdpConfig): string { - const pixelUrl = odpConfig.pixelUrl; - const pixelApiEndpoint = new URL(pixelApiPath, pixelUrl).href; - return pixelApiEndpoint; - } - - protected generateRequestData( - odpConfig: OdpConfig, - events: OdpEvent[] - ): { method: HttpMethod; endpoint: string; headers: { [key: string]: string }; data: string } { - const pixelApiEndpoint = this.getPixelApiEndpoint(odpConfig); - - const apiKey = odpConfig.apiKey; - const method = 'GET'; - const event = events[0]; - const url = new URL(pixelApiEndpoint); - event.identifiers.forEach((v, k) => { - url.searchParams.append(k, v); - }); - event.data.forEach((v, k) => { - url.searchParams.append(k, v as string); - }); - url.searchParams.append('tracker_id', apiKey); - url.searchParams.append('event_type', event.type); - url.searchParams.append('vdl_action', event.action); - const endpoint = url.toString(); - return { - method, - endpoint, - headers: {}, - data: '', - }; - } -} diff --git a/lib/odp/event_manager/event_api_manager.node.ts b/lib/odp/event_manager/event_api_manager.node.ts deleted file mode 100644 index 3bf1f2ad4..000000000 --- a/lib/odp/event_manager/event_api_manager.node.ts +++ /dev/null @@ -1,51 +0,0 @@ -/** - * Copyright 2024, 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 { OdpConfig } from '../odp_config'; -import { OdpEvent } from './odp_event' -import { OdpEventApiManager } from './odp_event_api_manager'; -import { HttpMethod } from '../../utils/http_request_handler/http'; - -export class NodeOdpEventApiManager extends OdpEventApiManager { - protected shouldSendEvents(events: OdpEvent[]): boolean { - return true; - } - - protected generateRequestData( - odpConfig: OdpConfig, - events: OdpEvent[] - ): { method: HttpMethod; endpoint: string; headers: { [key: string]: string }; data: string } { - - const { apiHost, apiKey } = odpConfig; - - return { - method: 'POST', - endpoint: `${apiHost}/v3/events`, - headers: { - 'Content-Type': 'application/json', - 'x-api-key': apiKey, - }, - data: JSON.stringify(events, this.replacer), - }; - } - - private replacer(_: unknown, value: unknown) { - if (value instanceof Map) { - return Object.fromEntries(value); - } else { - return value; - } - } -} diff --git a/lib/odp/event_manager/event_manager.browser.ts b/lib/odp/event_manager/event_manager.browser.ts index 4151c9b68..b2ac15f82 100644 --- a/lib/odp/event_manager/event_manager.browser.ts +++ b/lib/odp/event_manager/event_manager.browser.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { IOdpEventManager, OdpEventManager } from './odp_event_manager'; +import { OdpEventManager, DefaultOdpEventManager } from './odp_event_manager'; import { LogLevel } from '../../modules/logging'; import { OdpEvent } from './odp_event'; const DEFAULT_BROWSER_QUEUE_SIZE = 100; -export class BrowserOdpEventManager extends OdpEventManager implements IOdpEventManager { +export class BrowserOdpEventManager extends DefaultOdpEventManager implements OdpEventManager { protected initParams( batchSize: number | undefined, queueSize: number | undefined, diff --git a/lib/odp/event_manager/event_manager.node.ts b/lib/odp/event_manager/event_manager.node.ts index e057755a9..4d88744e9 100644 --- a/lib/odp/event_manager/event_manager.node.ts +++ b/lib/odp/event_manager/event_manager.node.ts @@ -15,14 +15,14 @@ */ import { OdpEvent } from './odp_event'; -import { IOdpEventManager, OdpEventManager } from './odp_event_manager'; +import { OdpEventManager, DefaultOdpEventManager } from './odp_event_manager'; import { LogLevel } from '../../modules/logging'; const DEFAULT_BATCH_SIZE = 10; const DEFAULT_FLUSH_INTERVAL_MSECS = 1000; const DEFAULT_SERVER_QUEUE_SIZE = 10000; -export class NodeOdpEventManager extends OdpEventManager implements IOdpEventManager { +export class NodeOdpEventManager extends DefaultOdpEventManager implements OdpEventManager { protected initParams( batchSize: number | undefined, queueSize: number | undefined, diff --git a/lib/odp/event_manager/odp_event_api_manager.spec.ts b/lib/odp/event_manager/odp_event_api_manager.spec.ts new file mode 100644 index 000000000..c5fe60e87 --- /dev/null +++ b/lib/odp/event_manager/odp_event_api_manager.spec.ts @@ -0,0 +1,209 @@ +/** + * Copyright 2022-2024, 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, beforeEach, beforeAll, it, expect, vi } from 'vitest'; + +import { LogHandler, LogLevel } from '../../modules/logging'; +import { DefaultOdpEventApiManager, eventApiRequestGenerator, pixelApiRequestGenerator } from './odp_event_api_manager'; +import { OdpEvent } from './odp_event'; +import { RequestHandler } from '../../utils/http_request_handler/http'; +import { OdpConfig } from '../odp_config'; +import { get } from 'http'; + +const data1 = new Map(); +data1.set('key11', 'value-1'); +data1.set('key12', true); +data1.set('key13', 3.5); +data1.set('key14', null); + +const data2 = new Map(); + +data2.set('key2', 'value-2'); + +const ODP_EVENTS = [ + new OdpEvent('t1', 'a1', new Map([['id-key-1', 'id-value-1']]), data1), + new OdpEvent('t2', 'a2', new Map([['id-key-2', 'id-value-2']]), data2), +]; + +const API_KEY = 'test-api-key'; +const API_HOST = 'https://odp.example.com'; +const PIXEL_URL = 'https://odp.pixel.com'; + +const odpConfig = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, []); + +import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; + +describe('DefaultOdpEventApiManager', () => { + it('should generate the event request using the correct odp config and event', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.resolve({ + statusCode: 200, + body: '', + headers: {}, + }), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + manager.sendEvents(odpConfig, ODP_EVENTS); + + expect(requestGenerator.mock.calls[0][0]).toEqual(odpConfig); + expect(requestGenerator.mock.calls[0][1]).toEqual(ODP_EVENTS); + }); + + it('should send the correct request using the request handler', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.resolve({ + statusCode: 200, + body: '', + headers: {}, + }), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + manager.sendEvents(odpConfig, ODP_EVENTS); + + expect(mockRequestHandler.makeRequest.mock.calls[0][0]).toEqual('https://odp.example.com/v3/events'); + expect(mockRequestHandler.makeRequest.mock.calls[0][1]).toEqual({ + 'x-api-key': 'test-api', + }); + expect(mockRequestHandler.makeRequest.mock.calls[0][2]).toEqual('PATCH'); + expect(mockRequestHandler.makeRequest.mock.calls[0][3]).toEqual('event-data'); + }); + + it('should return a promise that fails if the requestHandler response promise fails', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.reject(new Error('Request failed')), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + const response = manager.sendEvents(odpConfig, ODP_EVENTS); + + await expect(response).rejects.toThrow('Request failed'); + }); + + it('should return a promise that resolves with correct response code from the requestHandler', async () => { + const mockRequestHandler = getMockRequestHandler(); + mockRequestHandler.makeRequest.mockReturnValue({ + responsePromise: Promise.resolve({ + statusCode: 226, + body: '', + headers: {}, + }), + }); + const requestGenerator = vi.fn().mockReturnValue({ + method: 'PATCH', + endpoint: 'https://odp.example.com/v3/events', + headers: { + 'x-api-key': 'test-api', + }, + data: 'event-data', + }); + + const manager = new DefaultOdpEventApiManager(mockRequestHandler, requestGenerator); + const response = manager.sendEvents(odpConfig, ODP_EVENTS); + + await expect(response).resolves.not.toThrow(); + const statusCode = await response.then((r) => r.statusCode); + expect(statusCode).toBe(226); + }); +}); + +describe('pixelApiRequestGenerator', () => { + it('should generate the correct request for the pixel API using only the first event', () => { + const request = pixelApiRequestGenerator(odpConfig, ODP_EVENTS); + expect(request.method).toBe('GET'); + const endpoint = new URL(request.endpoint); + expect(endpoint.origin).toBe(PIXEL_URL); + expect(endpoint.pathname).toBe('/v2/zaius.gif'); + expect(endpoint.searchParams.get('id-key-1')).toBe('id-value-1'); + expect(endpoint.searchParams.get('key11')).toBe('value-1'); + expect(endpoint.searchParams.get('key12')).toBe('true'); + expect(endpoint.searchParams.get('key13')).toBe('3.5'); + expect(endpoint.searchParams.get('key14')).toBe('null'); + expect(endpoint.searchParams.get('tracker_id')).toBe(API_KEY); + expect(endpoint.searchParams.get('event_type')).toBe('t1'); + expect(endpoint.searchParams.get('vdl_action')).toBe('a1'); + + expect(request.headers).toEqual({}); + expect(request.data).toBe(''); + }); +}); + +describe('eventApiRequestGenerator', () => { + it('should generate the correct request for the event API using all events', () => { + const request = eventApiRequestGenerator(odpConfig, ODP_EVENTS); + expect(request.method).toBe('POST'); + expect(request.endpoint).toBe('https://odp.example.com/v3/events'); + expect(request.headers).toEqual({ + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + }); + + const data = JSON.parse(request.data); + expect(data).toEqual([ + { + type: 't1', + action: 'a1', + identifiers: { + 'id-key-1': 'id-value-1', + }, + data: { + key11: 'value-1', + key12: true, + key13: 3.5, + key14: null, + }, + }, + { + type: 't2', + action: 'a2', + identifiers: { + 'id-key-2': 'id-value-2', + }, + data: { + key2: 'value-2', + }, + }, + ]); + }); +}); diff --git a/lib/odp/event_manager/odp_event_api_manager.ts b/lib/odp/event_manager/odp_event_api_manager.ts index 2a5249a28..3ec22be34 100644 --- a/lib/odp/event_manager/odp_event_api_manager.ts +++ b/lib/odp/event_manager/odp_event_api_manager.ts @@ -14,103 +14,92 @@ * limitations under the License. */ -import { LogHandler, LogLevel } from '../../modules/logging'; +import { LoggerFacade } from '../../modules/logging'; import { OdpEvent } from './odp_event'; import { HttpMethod, RequestHandler } from '../../utils/http_request_handler/http'; import { OdpConfig } from '../odp_config'; -const EVENT_SENDING_FAILURE_MESSAGE = 'ODP event send failed'; - -/** - * Manager for communicating with the Optimizely Data Platform REST API - */ -export interface IOdpEventApiManager { - sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise; +export type EventDispatchResponse = { + statusCode?: number; +}; +export interface OdpEventApiManager { + sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise; } -/** - * Concrete implementation for accessing the ODP REST API - */ -export abstract class OdpEventApiManager implements IOdpEventApiManager { - /** - * Handler for recording execution logs - * @private - */ - private readonly logger: LogHandler; - - /** - * Handler for making external HTTP/S requests - * @private - */ - private readonly requestHandler: RequestHandler; +export type EventRequest = { + method: HttpMethod; + endpoint: string; + headers: Record; + data: string; +} - /** - * Creates instance to access Optimizely Data Platform (ODP) REST API - * @param requestHandler Desired request handler for testing - * @param logger Collect and record events/errors for this GraphQL implementation - */ - constructor(requestHandler: RequestHandler, logger: LogHandler) { +export type EventRequestGenerator = (odpConfig: OdpConfig, events: OdpEvent[]) => EventRequest; +export class DefaultOdpEventApiManager implements OdpEventApiManager { + private logger?: LoggerFacade; + private requestHandler: RequestHandler; + private requestGenerator: EventRequestGenerator; + + constructor( + requestHandler: RequestHandler, + requestDataGenerator: EventRequestGenerator, + logger?: LoggerFacade + ) { this.requestHandler = requestHandler; + this.requestGenerator = requestDataGenerator; this.logger = logger; } - getLogger(): LogHandler { - return this.logger; - } - - /** - * Service for sending ODP events to REST API - * @param events ODP events to send - * @returns Retry is true - if network or server error (5xx), otherwise false - */ - async sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise { - let shouldRetry = false; - + async sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise { if (events.length === 0) { - this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (no events)`); - return shouldRetry; + return Promise.resolve({}); } - if (!this.shouldSendEvents(events)) { - return shouldRetry; - } + const { method, endpoint, headers, data } = this.requestGenerator(odpConfig, events); - const { method, endpoint, headers, data } = this.generateRequestData(odpConfig, events); - - let statusCode = 0; - try { - const request = this.requestHandler.makeRequest(endpoint, headers, method, data); - const response = await request.responsePromise; - statusCode = response.statusCode ?? statusCode; - } catch (err) { - let message = 'network error'; - if (err instanceof Error) { - message = (err as Error).message; - } - this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${message})`); - shouldRetry = true; - } - - if (statusCode >= 400) { - this.logger.log(LogLevel.ERROR, `${EVENT_SENDING_FAILURE_MESSAGE} (${statusCode})`); - } - - if (statusCode >= 500) { - shouldRetry = true; - } - - return shouldRetry; + const request = this.requestHandler.makeRequest(endpoint, headers, method, data); + return request.responsePromise; } +} - protected abstract shouldSendEvents(events: OdpEvent[]): boolean; +export const pixelApiRequestGenerator: EventRequestGenerator = (odpConfig: OdpConfig, events: OdpEvent[]): EventRequest => { + const pixelApiPath = 'v2/zaius.gif'; + const pixelApiEndpoint = new URL(pixelApiPath, odpConfig.pixelUrl); + + const apiKey = odpConfig.apiKey; + const method = 'GET'; + const event = events[0]; + + event.identifiers.forEach((v, k) => { + pixelApiEndpoint.searchParams.append(k, v); + }); + event.data.forEach((v, k) => { + pixelApiEndpoint.searchParams.append(k, v as string); + }); + pixelApiEndpoint.searchParams.append('tracker_id', apiKey); + pixelApiEndpoint.searchParams.append('event_type', event.type); + pixelApiEndpoint.searchParams.append('vdl_action', event.action); + const endpoint = pixelApiEndpoint.toString(); + + return { + method, + endpoint, + headers: {}, + data: '', + }; +} - protected abstract generateRequestData( - odpConfig: OdpConfig, - events: OdpEvent[] - ): { - method: HttpMethod; - endpoint: string; - headers: { [key: string]: string }; - data: string; +export const eventApiRequestGenerator: EventRequestGenerator = (odpConfig: OdpConfig, events: OdpEvent[]): EventRequest => { + const { apiHost, apiKey } = odpConfig; + + return { + method: 'POST', + endpoint: `${apiHost}/v3/events`, + headers: { + 'Content-Type': 'application/json', + 'x-api-key': apiKey, + }, + data: JSON.stringify(events, (_: unknown, value: unknown) => { + return value instanceof Map ? Object.fromEntries(value) : value; + }), }; } diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index 2b4d69e57..8829fe51a 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -14,440 +14,279 @@ * limitations under the License. */ -import { LogHandler, LogLevel } from '../../modules/logging'; - -import { uuid } from '../../utils/fns'; -import { ERROR_MESSAGES, ODP_USER_KEY, ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION } from '../../utils/enums'; +import { LogLevel } from '../../modules/logging'; import { OdpEvent } from './odp_event'; -import { OdpConfig } from '../odp_config'; -import { IOdpEventApiManager } from './odp_event_api_manager'; -import { invalidOdpDataFound } from '../odp_utils'; -import { IUserAgentParser } from '../ua_parser/user_agent_parser'; -import { scheduleMicrotask } from '../../utils/microtask'; - -const MAX_RETRIES = 3; - -/** - * Event dispatcher's execution states - */ -export enum Status { - Stopped, - Running, -} - -/** - * Manager for persisting events to the Optimizely Data Platform (ODP) - */ -export interface IOdpEventManager { - updateSettings(odpConfig: OdpConfig): void; - - start(): void; - - stop(): Promise; - - registerVuid(vuid: string): void; - - identifyUser(userId?: string, vuid?: string): void; - +import { OdpConfig, OdpIntegrationConfig } from '../odp_config'; +import { OdpEventApiManager } from './odp_event_api_manager'; +import { BaseService, Service, ServiceState, StartupLog } from '../../service'; +import { BackoffController, Repeater } from '../../utils/repeater/repeater'; +import { Producer } from '../../utils/type'; +import { runWithRetry } from '../../utils/executor/backoff_retry_runner'; +import { isSuccessStatusCode } from '../../utils/http_request_handler/http_util'; +import { ERROR_MESSAGES } from '../../utils/enums'; +import { ODP_DEFAULT_EVENT_TYPE, ODP_USER_KEY } from '../constant'; + +export interface OdpEventManager extends Service { + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void; sendEvent(event: OdpEvent): void; - - flush(retry?: boolean): void; } -/** - * Concrete implementation of a manager for persisting events to the Optimizely Data Platform - */ -export abstract class OdpEventManager implements IOdpEventManager { - /** - * Current state of the event processor - */ - status: Status = Status.Stopped; +export type RetryConfig = { + maxRetries: number; + backoffProvider: Producer; +} - /** - * Queue for holding all events to be eventually dispatched - * @protected - */ - protected queue = new Array(); +export type OdpEventManagerConfig = { + repeater: Repeater, + apiManager: OdpEventApiManager, + batchSize: number, + startUpLogs?: StartupLog[], + retryConfig: RetryConfig, +}; + +export class DefaultOdpEventManager extends BaseService implements OdpEventManager { + private queue: OdpEvent[] = []; + private repeater: Repeater; + private odpIntegrationConfig?: OdpIntegrationConfig; + private apiManager: OdpEventApiManager; + private batchSize: number; + + private retryConfig: RetryConfig; + + // private readonly userAgentData?: Map; + + constructor(config: OdpEventManagerConfig) { + super(config.startUpLogs); + + this.apiManager = config.apiManager; + this.batchSize = config.batchSize; + this.retryConfig = config.retryConfig; + + this.repeater = config.repeater; + this.repeater.setTask(() => this.flush()); + + // if (config.userAgentParser) { + // const { os, device } = config.userAgentParser.parseUserAgentInfo(); + + // const userAgentInfo: Record = { + // 'os': os.name, + // 'os_version': os.version, + // 'device_type': device.type, + // 'model': device.model, + // }; + + // this.userAgentData = new Map( + // Object.entries(userAgentInfo).filter(([_, value]) => value != null && value != undefined) + // ); + // } + } - /** - * Identifier of the currently running timeout so clearCurrentTimeout() can be called - * @private - */ - private timeoutId?: NodeJS.Timeout | number; + // setClientInfo(clientEngine: string, clientVersion: string): void { + // this.clientEngine = clientEngine; + // this.clientVersion = clientVersion; + // } - /** - * ODP configuration settings for identifying the target API and segments - * @private - */ - private odpConfig?: OdpConfig; + private async executeDispatch(odpConfig: OdpConfig, batch: OdpEvent[]): Promise { + const res = await this.apiManager.sendEvents(odpConfig, batch); + if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { + // TODO: replace message with imported constants + return Promise.reject(new Error(`Failed to dispatch events: ${res.statusCode}`)); + } + return await Promise.resolve(res); + } - /** - * REST API Manager used to send the events - * @private - */ - private readonly apiManager: IOdpEventApiManager; + private async flush(): Promise { + if (!this.odpIntegrationConfig || !this.odpIntegrationConfig.integrated) { + return; + } - /** - * Handler for recording execution logs - * @private - */ - private readonly logger: LogHandler; + const odpConfig = this.odpIntegrationConfig.odpConfig; - /** - * Maximum queue size - * @protected - */ - protected queueSize!: number; + const batch = this.queue; + this.queue = []; - /** - * Maximum number of events to process at once. Ignored in browser context - * @protected - */ - protected batchSize!: number; + // as the current queue has been emptied, stop repeating flus + // until more events becomes availabe + this.repeater.reset(); - /** - * Milliseconds between setTimeout() to process new batches. Ignored in browser context - * @protected - */ - protected flushInterval!: number; + return runWithRetry( + () => this.executeDispatch(odpConfig, batch), this.retryConfig.backoffProvider(), this.retryConfig.maxRetries + ).result.catch((err) => { + // TODO: replace with imported constants + this.logger?.error('failed to send odp events', err); + }); + } - /** - * Type of execution context eg node, js, react - * @private - */ - private readonly clientEngine: string; + start(): void { + if (!this.isNew) { + return; + } - /** - * Version of the client being used - * @private - */ - private readonly clientVersion: string; + super.start(); + if (this.odpIntegrationConfig) { + this.goToRunningState(); + } else { + this.state = ServiceState.Starting; + } - /** - * Version of the client being used - * @private - */ - private readonly userAgentParser?: IUserAgentParser; + // if (!this.odpIntegrationConfig) { + // this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); + // return; + // } - private retries: number; + // this.status = Status.Running; + // // no need of periodic flush if batchSize is 1 + // if (this.batchSize > 1) { + // this.setNewTimeout(); + // } + } - /** - * Information about the user agent - * @private - */ - private readonly userAgentData?: Map; - - constructor({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - queueSize, - batchSize, - flushInterval, - userAgentParser, - retries, - }: { - odpConfig?: OdpConfig; - apiManager: IOdpEventApiManager; - logger: LogHandler; - clientEngine: string; - clientVersion: string; - queueSize?: number; - batchSize?: number; - flushInterval?: number; - userAgentParser?: IUserAgentParser; - retries?: number; - }) { - this.apiManager = apiManager; - this.logger = logger; - this.clientEngine = clientEngine; - this.clientVersion = clientVersion; - this.initParams(batchSize, queueSize, flushInterval); - this.status = Status.Stopped; - this.userAgentParser = userAgentParser; - this.retries = retries || MAX_RETRIES; - - if (userAgentParser) { - const { os, device } = userAgentParser.parseUserAgentInfo(); - - const userAgentInfo: Record = { - 'os': os.name, - 'os_version': os.version, - 'device_type': device.type, - 'model': device.model, - }; - - this.userAgentData = new Map( - Object.entries(userAgentInfo).filter(([key, value]) => value != null && value != undefined) - ); + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void { + if (this.isDone()) { + return; } - if (odpConfig) { - this.updateSettings(odpConfig); + if (this.isNew()) { + this.odpIntegrationConfig = odpIntegrationConfig; + return; } - } - - protected abstract initParams( - batchSize: number | undefined, - queueSize: number | undefined, - flushInterval: number | undefined - ): void; - /** - * Update ODP configuration settings. - * @param newConfig New configuration to apply - */ - updateSettings(odpConfig: OdpConfig): void { - // do nothing if config did not change - if (this.odpConfig && this.odpConfig.equals(odpConfig)) { + if (this.isStarting()) { + this.odpIntegrationConfig = odpIntegrationConfig; + this.goToRunningState(); return; } + // already running, flush the queue using the previous config first before updating the config this.flush(); - this.odpConfig = odpConfig; + this.odpIntegrationConfig = odpIntegrationConfig; } - /** - * Cleans up all pending events; - */ - flush(): void { - this.processQueue(true); + private goToRunningState() { + this.state = ServiceState.Running; + this.startPromise.resolve(); } - /** - * Start the event manager - */ - start(): void { - if (!this.odpConfig) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); + stop(): void { + if (this.isDone()) { return; } - this.status = Status.Running; - - // no need of periodic flush if batchSize is 1 - if (this.batchSize > 1) { - this.setNewTimeout(); + if (this.isNew()) { + this.startPromise.reject(new Error('odp event manager stopped before it could start')); } - } - - /** - * Drain the queue sending all remaining events in batches then stop processing - */ - async stop(): Promise { - this.logger.log(LogLevel.DEBUG, 'Stop requested.'); this.flush(); - this.clearCurrentTimeout(); - this.status = Status.Stopped; - this.logger.log(LogLevel.DEBUG, 'Stopped. Queue Count: %s', this.queue.length); - } - - /** - * Register a new visitor user id (VUID) in ODP - * @param vuid Visitor User ID to send - */ - registerVuid(vuid: string): void { - const identifiers = new Map(); - identifiers.set(ODP_USER_KEY.VUID, vuid); - - const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.INITIALIZED, identifiers); - this.sendEvent(event); + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); } + // TODO: move this to ODP manager /** * Associate a full-stack userid with an established VUID * @param {string} userId (Optional) Full-stack User ID * @param {string} vuid (Optional) Visitor User ID */ - identifyUser(userId?: string, vuid?: string): void { - const identifiers = new Map(); - if (!userId && !vuid) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_SEND_EVENT_FAILED_UID_MISSING); - return; - } + // identifyUser(userId?: string, vuid?: string): void { + // const identifiers = new Map(); + // 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); - } + // if (vuid) { + // identifiers.set(ODP_USER_KEY.VUID, vuid); + // } - if (userId) { - identifiers.set(ODP_USER_KEY.FS_USER_ID, userId); - } + // if (userId) { + // identifiers.set(ODP_USER_KEY.FS_USER_ID, userId); + // } - const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); - this.sendEvent(event); - } + // const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); + // this.sendEvent(event); + // } - /** - * Send an event to ODP via dispatch queue - * @param event ODP Event to forward - */ sendEvent(event: OdpEvent): void { - if (invalidOdpDataFound(event.data)) { - this.logger.log(LogLevel.ERROR, 'Event data found to be invalid.'); - } else { - event.data = this.augmentCommonData(event.data); - this.enqueue(event); - } - } - - /** - * Add a new event to the main queue - * @param event ODP Event to be queued - * @private - */ - private enqueue(event: OdpEvent): void { - if (this.status === Status.Stopped) { - this.logger.log(LogLevel.WARNING, 'Failed to Process ODP Event. ODPEventManager is not running.'); + if (!this.isRunning()) { + this.logger?.error('ODP event manager is not running.'); return; } - if (!this.hasNecessaryIdentifiers(event)) { - this.logger.log(LogLevel.ERROR, 'ODP events should have at least one key-value pair in identifiers.'); - return; + if (!this.odpIntegrationConfig?.integrated) { + this.logger?.error(ERROR_MESSAGES.ODP_NOT_INTEGRATED); + return; } - if (this.queue.length >= this.queueSize) { - this.logger.log( - LogLevel.WARNING, - 'Failed to Process ODP Event. Event Queue full. queueSize = %s.', - this.queue.length - ); + if (event.identifiers.size === 0) { + this.logger?.log(LogLevel.ERROR, 'ODP events should have at least one key-value pair in identifiers.'); return; } - this.queue.push(event); - this.processQueue(); - } - - protected abstract hasNecessaryIdentifiers(event: OdpEvent): boolean; - - /** - * Process events in the main queue - * @param shouldFlush Flush all events regardless of available queue event count - * @private - */ - private processQueue(shouldFlush = false): void { - if (this.status !== Status.Running) { + if (!this.isDataValid(event.data)) { + this.logger?.error('Event data found to be invalid.'); return; - } - - if (shouldFlush) { - // clear the queue completely - this.clearCurrentTimeout(); - - while (this.queueContainsItems()) { - this.makeAndSend1Batch(); - } - } else if (this.queueHasBatches()) { - // Check if queue has a full batch available - this.clearCurrentTimeout(); + } - while (this.queueHasBatches()) { - this.makeAndSend1Batch(); - } + if (!event.action ) { + this.logger?.error('Event action invalid.'); + return; } - // no need for periodic flush if batchSize is 1 - if (this.batchSize > 1) { - this.setNewTimeout(); + if (event.type === '') { + event.action = ODP_DEFAULT_EVENT_TYPE; } - } - /** - * Clear the currently running timout - * @private - */ - private clearCurrentTimeout(): void { - clearTimeout(this.timeoutId); - this.timeoutId = undefined; + event.identifiers.forEach((key, value) => { + // Catch for fs-user-id, FS-USER-ID, and FS_USER_ID and assign value to fs_user_id identifier. + if ( + ODP_USER_KEY.FS_USER_ID_ALIAS === key.toLowerCase() || + ODP_USER_KEY.FS_USER_ID === key.toLowerCase() + ) { + event.identifiers.delete(key); + event.identifiers.set(ODP_USER_KEY.FS_USER_ID, value); + } + }); + + this.processEvent(event); } - /** - * Start a new timeout - * @private - */ - private setNewTimeout(): void { - if (this.timeoutId !== undefined) { - return; - } - this.timeoutId = setTimeout(() => this.processQueue(true), this.flushInterval); + private isDataValid(data: Map): boolean { + const validTypes: string[] = ['string', 'number', 'boolean']; + return Array.from(data.values()).reduce( + (valid, value) => valid && (value === null || validTypes.includes(typeof value)), + true, + ); } - /** - * Make a batch and send it to ODP - * @private - */ - private makeAndSend1Batch(): void { - if (!this.odpConfig) { - return; - } - - const batch = this.queue.splice(0, this.batchSize); - - const odpConfig = this.odpConfig; + private processEvent(event: OdpEvent): void { + this.queue.push(event); - if (batch.length > 0) { - // put sending the event on another event loop - scheduleMicrotask(async () => { - let shouldRetry: boolean; - let attemptNumber = 0; - do { - shouldRetry = await this.apiManager.sendEvents(odpConfig, batch); - attemptNumber += 1; - } while (shouldRetry && attemptNumber < this.retries); - }) + if (this.queue.length === this.batchSize) { + this.flush(); + } else if (!this.repeater.isRunning() && this.batchSize > 1) { + // no need to repeatedly flush if batchSize is 1 + this.repeater.start(); } } - /** - * Check if main queue has any full/even batches available - * @returns True if there are event batches available in the queue otherwise False - * @private - */ - private queueHasBatches(): boolean { - return this.queueContainsItems() && this.queue.length % this.batchSize === 0; - } - - /** - * Check if main queue has any items - * @returns True if there are any events in the queue otherwise False - * @private - */ - private queueContainsItems(): boolean { - return this.queue.length > 0; - } - - protected abstract discardEventsIfNeeded(): void; - + // TODO: move to ODP maanger /** * Add additional common data including an idempotent ID and execution context to event data * @param sourceData Existing event data to augment * @returns Augmented event data * @private */ - private augmentCommonData(sourceData: Map): Map { - const data = new Map(this.userAgentData); + // private augmentCommonData(sourceData: Map): Map { + // const data = new Map(this.userAgentData); - data.set('idempotence_id', uuid()); - data.set('data_source_type', 'sdk'); - data.set('data_source', this.clientEngine); - data.set('data_source_version', this.clientVersion); - - sourceData.forEach((value, key) => data.set(key, value)); - return data; - } - - protected getLogger(): LogHandler { - return this.logger; - } - - getQueue(): OdpEvent[] { - return this.queue; - } + // data.set('idempotence_id', uuidV4()); + // data.set('data_source_type', 'sdk'); + // data.set('data_source', this.clientEngine); + // data.set('data_source_version', this.clientVersion); + + // sourceData.forEach((value, key) => data.set(key, value)); + // return data; + // } } diff --git a/lib/odp/odp_manager.browser.ts b/lib/odp/odp_manager.browser.ts index 7168b5822..6c6e8ffaf 100644 --- a/lib/odp/odp_manager.browser.ts +++ b/lib/odp/odp_manager.browser.ts @@ -32,13 +32,13 @@ import { BrowserLRUCache } from '../utils/lru_cache'; import { VuidManager } from '../plugins/vuid_manager/index'; -import { OdpManager } from './odp_manager'; +import { DefaultOdpManager } from './odp_manager'; import { OdpEvent } from './event_manager/odp_event'; import { IOdpEventManager, OdpOptions } from '../shared_types'; import { BrowserOdpEventApiManager } from './event_manager/event_api_manager.browser'; import { BrowserOdpEventManager } from './event_manager/event_manager.browser'; -import { IOdpSegmentManager, OdpSegmentManager } from './segment_manager/odp_segment_manager'; -import { OdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; +import { OdpSegmentManager, DefaultSegmentManager } from './segment_manager/odp_segment_manager'; +import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; import { OdpConfig, OdpIntegrationConfig } from './odp_config'; interface BrowserOdpManagerConfig { @@ -50,14 +50,14 @@ interface BrowserOdpManagerConfig { } // Client-side Browser Plugin for ODP Manager -export class BrowserOdpManager extends OdpManager { +export class BrowserOdpManager extends DefaultOdpManager { static cache = new BrowserAsyncStorageCache(); vuidManager?: VuidManager; vuid?: string; constructor(options: { odpIntegrationConfig?: OdpIntegrationConfig; - segmentManager: IOdpSegmentManager; + segmentManager: OdpSegmentManager; eventManager: IOdpEventManager; logger: LogHandler; }) { @@ -88,18 +88,18 @@ export class BrowserOdpManager extends OdpManager { }); } - let segmentManager: IOdpSegmentManager; + let segmentManager: OdpSegmentManager; if (odpOptions?.segmentManager) { segmentManager = odpOptions.segmentManager; } else { - segmentManager = new OdpSegmentManager( + segmentManager = new DefaultSegmentManager( odpOptions?.segmentsCache || new BrowserLRUCache({ maxSize: odpOptions?.segmentsCacheSize, timeout: odpOptions?.segmentsCacheTimeout, }), - new OdpSegmentApiManager(customSegmentRequestHandler, logger), + new DefaultOdpSegmentApiManager(customSegmentRequestHandler, logger), logger, odpConfig ); diff --git a/lib/odp/odp_manager.node.ts b/lib/odp/odp_manager.node.ts index 648e27751..e5839b4bf 100644 --- a/lib/odp/odp_manager.node.ts +++ b/lib/odp/odp_manager.node.ts @@ -26,12 +26,12 @@ import { REQUEST_TIMEOUT_ODP_SEGMENTS_MS, } from '../utils/enums'; -import { OdpManager } from './odp_manager'; +import { DefaultOdpManager } from './odp_manager'; import { IOdpEventManager, OdpOptions } from '../shared_types'; import { NodeOdpEventApiManager } from './event_manager/event_api_manager.node'; import { NodeOdpEventManager } from './event_manager/event_manager.node'; -import { IOdpSegmentManager, OdpSegmentManager } from './segment_manager/odp_segment_manager'; -import { OdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; +import { OdpSegmentManager, DefaultSegmentManager } from './segment_manager/odp_segment_manager'; +import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; import { OdpConfig, OdpIntegrationConfig } from './odp_config'; interface NodeOdpManagerConfig { @@ -46,10 +46,10 @@ interface NodeOdpManagerConfig { * Server-side Node Plugin for ODP Manager. * Note: As this is still a work-in-progress. Please avoid using the Node ODP Manager. */ -export class NodeOdpManager extends OdpManager { +export class NodeOdpManager extends DefaultOdpManager { constructor(options: { odpIntegrationConfig?: OdpIntegrationConfig; - segmentManager: IOdpSegmentManager; + segmentManager: OdpSegmentManager; eventManager: IOdpEventManager; logger: LogHandler; }) { @@ -80,18 +80,18 @@ export class NodeOdpManager extends OdpManager { }); } - let segmentManager: IOdpSegmentManager; + let segmentManager: OdpSegmentManager; if (odpOptions?.segmentManager) { segmentManager = odpOptions.segmentManager; } else { - segmentManager = new OdpSegmentManager( + segmentManager = new DefaultSegmentManager( odpOptions?.segmentsCache || new ServerLRUCache({ maxSize: odpOptions?.segmentsCacheSize, timeout: odpOptions?.segmentsCacheTimeout, }), - new OdpSegmentApiManager(customSegmentRequestHandler, logger), + new DefaultOdpSegmentApiManager(customSegmentRequestHandler, logger), logger, odpConfig ); diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index df2bbc394..254546513 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -14,190 +14,186 @@ * limitations under the License. */ -import { LogHandler, LogLevel } from '../modules/logging'; -import { ERROR_MESSAGES, ODP_USER_KEY } from '../utils/enums'; - -import { VuidManager } from '../plugins/vuid_manager'; +import { v4 as uuidV4} from 'uuid'; +import { LoggerFacade } from '../modules/logging'; import { OdpIntegrationConfig, odpIntegrationsAreEqual } from './odp_config'; -import { IOdpEventManager } from './event_manager/odp_event_manager'; -import { IOdpSegmentManager } from './segment_manager/odp_segment_manager'; +import { OdpEventManager } from './event_manager/odp_event_manager'; +import { OdpSegmentManager } from './segment_manager/odp_segment_manager'; import { OptimizelySegmentOption } from './segment_manager/optimizely_segment_option'; -import { invalidOdpDataFound } from './odp_utils'; import { OdpEvent } from './event_manager/odp_event'; import { resolvablePromise, ResolvablePromise } from '../utils/promise/resolvablePromise'; +import { BaseService, Service, ServiceState } from '../service'; +import { UserAgentParser } from './ua_parser/user_agent_parser'; +import { ERROR_MESSAGES } from '../utils/enums'; +import { ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION, ODP_USER_KEY } from './constant'; +import { isVuid } from '../vuid/vuid'; -/** - * Manager for handling internal all business logic related to - * Optimizely Data Platform (ODP) / Advanced Audience Targeting (AAT) - */ -export interface IOdpManager { - onReady(): Promise; - - isReady(): boolean; - +export interface OdpManager extends Service { updateSettings(odpIntegrationConfig: OdpIntegrationConfig): boolean; - - stop(): void; - fetchQualifiedSegments(userId: string, options?: Array): Promise; - identifyUser(userId?: string, vuid?: string): void; - - sendEvent({ type, action, identifiers, data }: OdpEvent): void; - - isVuidEnabled(): boolean; - - getVuid(): string | undefined; + sendEvent(event: OdpEvent): void; + setClientInfo(clientEngine: string, clientVersion: string): void; } -export enum Status { - Running, - Stopped, -} - -/** - * Orchestrates segments manager, event manager, and ODP configuration - */ -export abstract class OdpManager implements IOdpManager { - /** - * Promise that returns when the OdpManager is finished initializing - */ - private initPromise: Promise; - private ready = false; +export type OdpManagerConfig = { + segmentManager: OdpSegmentManager; + eventManager: OdpEventManager; + logger?: LoggerFacade; + userAgentParser?: UserAgentParser; +}; - /** - * Promise that resolves when odpConfig becomes available - */ +export class DefaultOdpManager extends BaseService implements OdpManager { private configPromise: ResolvablePromise; - - status: Status = Status.Stopped; - - /** - * 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. - */ - private segmentManager: IOdpSegmentManager; - - /** - * ODP Event Manager which provides an interface to the remote ODP server (REST API) for events. - * It will queue all pending events (persistent) and send them (in batches of up to 10 events) to the ODP server when possible. - */ - private eventManager: IOdpEventManager; - - /** - * Handler for recording execution logs - * @protected - */ - protected logger: LogHandler; - - /** - * ODP configuration settings for identifying the target API and segments - */ - odpIntegrationConfig?: OdpIntegrationConfig; - - // TODO: Consider accepting logger as a parameter and initializing it in constructor instead - constructor({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - }: { - odpIntegrationConfig?: OdpIntegrationConfig; - segmentManager: IOdpSegmentManager; - eventManager: IOdpEventManager; - logger: LogHandler; - }) { - this.segmentManager = segmentManager; - this.eventManager = eventManager; - this.logger = logger; + private segmentManager: OdpSegmentManager; + private eventManager: OdpEventManager; + private odpIntegrationConfig?: OdpIntegrationConfig; + private vuid?: string; + private clientEngine?: string; + private clientVersion?: string; + private userAgentData?: Map; + + constructor(config: OdpManagerConfig) { + super(); + this.segmentManager = config.segmentManager; + this.eventManager = config.eventManager; + this.logger = config.logger; this.configPromise = resolvablePromise(); - const readinessDependencies: PromiseLike[] = [this.configPromise]; + if (config.userAgentParser) { + const { os, device } = config.userAgentParser.parseUserAgentInfo(); - if (this.isVuidEnabled()) { - readinessDependencies.push(this.initializeVuid()); + const userAgentInfo: Record = { + 'os': os.name, + 'os_version': os.version, + 'device_type': device.type, + 'model': device.model, + }; + + this.userAgentData = new Map( + Object.entries(userAgentInfo).filter(([_, value]) => value != null && value != undefined) + ); } - this.initPromise = Promise.all(readinessDependencies); + // const readinessDependencies: PromiseLike[] = [this.configPromise, this.on]; - this.onReady().then(() => { - this.ready = true; - if (this.isVuidEnabled() && this.status === Status.Running) { - this.registerVuid(); - } - }); + // if (this.isVuidEnabled()) { + // readinessDependencies.push(this.initializeVuid()); + // } + // this.initPromise = Promise.all(readinessDependencies); - if (odpIntegrationConfig) { - this.updateSettings(odpIntegrationConfig); - } + // this.onReady().then(() => { + // this.ready = true; + // if (this.isVuidEnabled() && this.status === Status.Running) { + // this.registerVuid(); + // } + // }); + + // if (odpIntegrationConfig) { + // this.updateSettings(odpIntegrationConfig); + // } } - public getStatus(): Status { - return this.status; + // private async activate(): Promise { + // if (!this.odpIntegrationConfig) { + // return; + // } + + // if (!this.odpIntegrationConfig.integrated) { + // return; + // } + + // this.activityStatus = ActivityStatus.Activating; + + // this.segmentManager.updateSettings(this.odpIntegrationConfig.odpConfig); + // this.eventManager.updateSettings(this.odpIntegrationConfig.odpConfig); + // this.eventManager.start(); + // return Promise.resolve(); + // } + + setClientInfo(clientEngine: string, clientVersion: string): void { + this.clientEngine = clientEngine; + this.clientVersion = clientVersion; } - async start(): Promise { - if (this.status === Status.Running) { + start(): void { + if (!this.isNew()) { return; } - if (!this.odpIntegrationConfig) { - return Promise.reject(new Error('cannot start without ODP config')); - } - - if (!this.odpIntegrationConfig.integrated) { - return Promise.reject(new Error('start() called when ODP is not integrated')); - } + this.state = ServiceState.Starting; - this.status = Status.Running; - this.segmentManager.updateSettings(this.odpIntegrationConfig.odpConfig); - this.eventManager.updateSettings(this.odpIntegrationConfig.odpConfig); + this.segmentManager.start(); this.eventManager.start(); - return Promise.resolve(); + + const startDependencies = [ + this.configPromise, + this.segmentManager.onRunning(), + this.eventManager.onRunning(), + ]; + + Promise.all(startDependencies) + .then(() => { + this.handleStartSuccess(); + }).catch((err) => { + this.handleStartFailure(err); + }); + // this.segmentManager.updateSettings(this.odpIntegrationConfig.odpConfig); + // this.eventManager.updateSettings(this.odpIntegrationConfig.odpConfig); + // this.eventManager.start(); + // return Promise.resolve(); } - async stop(): Promise { - if (this.status === Status.Stopped) { + private handleStartSuccess() { + if (this.isDone()) { return; } - this.status = Status.Stopped; - await this.eventManager.stop(); + this.state = ServiceState.Running; + this.startPromise.resolve(); } - onReady(): Promise { - return this.initPromise; + private handleStartFailure(error: Error) { + if (this.isDone()) { + return; + } + + this.state = ServiceState.Failed; + this.startPromise.reject(error); + this.stopPromise.reject(error); } - isReady(): boolean { - return this.ready; + stop(): void { + // if (this.status === Status.Stopped) { + // return; + // } + // this.status = Status.Stopped; + // await this.eventManager.stop(); } /** * Provides a method to update ODP Manager's ODP Config */ updateSettings(odpIntegrationConfig: OdpIntegrationConfig): boolean { - this.configPromise.resolve(); - // do nothing if config did not change if (this.odpIntegrationConfig && odpIntegrationsAreEqual(this.odpIntegrationConfig, odpIntegrationConfig)) { return false; } + if (this.isDone()) { + return false; + } + this.odpIntegrationConfig = odpIntegrationConfig; - if (odpIntegrationConfig.integrated) { - // already running, just propagate updated config to children; - if (this.status === Status.Running) { - this.segmentManager.updateSettings(odpIntegrationConfig.odpConfig); - this.eventManager.updateSettings(odpIntegrationConfig.odpConfig); - } else { - this.start(); - } - } else { - this.stop(); + if (this.isStarting()) { + this.configPromise.resolve(); } + + this.segmentManager.updateConfig(odpIntegrationConfig) + this.eventManager.updateConfig(odpIntegrationConfig); + return true; } @@ -209,114 +205,68 @@ export abstract class OdpManager implements IOdpManager { * @returns {Promise} A promise holding either a list of qualified segments or null. */ async fetchQualifiedSegments(userId: string, options: Array = []): Promise { - if (!this.odpIntegrationConfig) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); - return null; - } + // if (!this.odpIntegrationConfig) { + // this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); + // return null; + // } - if (!this.odpIntegrationConfig.integrated) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED); - return null; - } + // if (!this.odpIntegrationConfig.integrated) { + // this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED); + // return null; + // } - if (VuidManager.isVuid(userId)) { + if (isVuid(userId)) { return this.segmentManager.fetchQualifiedSegments(ODP_USER_KEY.VUID, userId, options); } return this.segmentManager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, userId, options); } - /** - * Identifies a user via the ODP Event Manager - * @param {string} userId (Optional) Custom unique identifier of a target user. - * @param {string} vuid (Optional) Secondary unique identifier of a target user, primarily used by client SDKs. - * @returns - */ identifyUser(userId?: string, vuid?: string): void { - if (!this.odpIntegrationConfig) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); + const identifiers = new Map(); + if (!userId && !vuid) { + this.logger?.error(ERROR_MESSAGES.ODP_SEND_EVENT_FAILED_UID_MISSING); return; } - if (!this.odpIntegrationConfig.integrated) { - this.logger.log(LogLevel.INFO, ERROR_MESSAGES.ODP_NOT_INTEGRATED); - return; + if (vuid) { + identifiers.set(ODP_USER_KEY.VUID, vuid); } - if (userId && VuidManager.isVuid(userId)) { - this.eventManager.identifyUser(undefined, userId); - return; + if (userId) { + identifiers.set(ODP_USER_KEY.FS_USER_ID, userId); } - this.eventManager.identifyUser(userId, vuid); + const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); + this.sendEvent(event); } - /** - * Sends an event to the ODP Server via the ODP Events API - * @param {OdpEvent} > ODP Event to send to event manager - */ - sendEvent({ type, action, identifiers, data }: OdpEvent): void { - let mType = type; - - if (typeof mType !== 'string' || mType === '') { - mType = 'fullstack'; - } - - if (!this.odpIntegrationConfig) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); - return; - } - - if (!this.odpIntegrationConfig.integrated) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED); - return; - } - - if (invalidOdpDataFound(data)) { - throw new Error(ERROR_MESSAGES.ODP_INVALID_DATA); + sendEvent(event: OdpEvent): void { + if (!event.identifiers.has(ODP_USER_KEY.VUID) && this.vuid) { + event.identifiers.set(ODP_USER_KEY.VUID, this.vuid); } - if (typeof action !== 'string' || action === '') { - throw new Error('ODP action is not valid (cannot be empty).'); - } - - this.eventManager.sendEvent(new OdpEvent(mType, action, identifiers, data)); + event.data = this.augmentCommonData(event.data); + this.eventManager.sendEvent(event); } - /** - * Identifies if the VUID feature is enabled - */ - abstract isVuidEnabled(): boolean; + private augmentCommonData(sourceData: Map): Map { + const data = new Map(this.userAgentData); + + data.set('idempotence_id', uuidV4()); + data.set('data_source_type', 'sdk'); + data.set('data_source', this.clientEngine || ''); + data.set('data_source_version', this.clientVersion || ''); - /** - * Returns VUID value if it exists - */ - abstract getVuid(): string | undefined; - - protected initializeVuid(): Promise { - return Promise.resolve(); + sourceData.forEach((value, key) => data.set(key, value)); + return data; } - private registerVuid() { - if (!this.odpIntegrationConfig) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); - return; - } - - if (!this.odpIntegrationConfig.integrated) { - this.logger.log(LogLevel.INFO, ERROR_MESSAGES.ODP_NOT_INTEGRATED); - return; - } - - const vuid = this.getVuid(); - if (!vuid) { - return; - } - - try { - this.eventManager.registerVuid(vuid); - } catch (e) { - this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_VUID_REGISTRATION_FAILED); - } + setVuid(vuid: string): void { + this.vuid = vuid; + this.onRunning().then(() => { + const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.INITIALIZED); + this.sendEvent(event); + }); } } diff --git a/lib/odp/odp_utils.ts b/lib/odp/odp_utils.ts deleted file mode 100644 index 875b7e091..000000000 --- a/lib/odp/odp_utils.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 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. - */ - -/** - * 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 - */ -export function invalidOdpDataFound(data: Map): boolean { - const validTypes: string[] = ['string', 'number', 'boolean']; - let foundInvalidValue = false; - data.forEach(value => { - if (!validTypes.includes(typeof value) && value !== null) { - foundInvalidValue = true; - } - }); - return foundInvalidValue; -} diff --git a/lib/odp/segment_manager/odp_segment_api_manager.ts b/lib/odp/segment_manager/odp_segment_api_manager.ts index afe20ae2a..a688bd2df 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -17,7 +17,7 @@ import { LogHandler, LogLevel } from '../../modules/logging'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from '../odp_response_schema'; -import { ODP_USER_KEY } from '../../utils/enums'; +import { ODP_USER_KEY } from '../constant'; import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http'; import { Response as GraphQLResponse } from '../odp_types'; @@ -41,7 +41,7 @@ const AUDIENCE_FETCH_FAILURE_MESSAGE = 'Audience segments fetch failed'; /** * Manager for communicating with the Optimizely Data Platform GraphQL endpoint */ -export interface IOdpSegmentApiManager { +export interface OdpSegmentApiManager { fetchSegments( apiKey: string, apiHost: string, @@ -54,15 +54,10 @@ export interface IOdpSegmentApiManager { /** * Concrete implementation for communicating with the ODP GraphQL endpoint */ -export class OdpSegmentApiManager implements IOdpSegmentApiManager { +export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { private readonly logger: LogHandler; private readonly requestHandler: RequestHandler; - /** - * Communicates with Optimizely Data Platform's GraphQL endpoint - * @param requestHandler Desired request handler for testing - * @param logger Collect and record events/errors for this GraphQL implementation - */ constructor(requestHandler: RequestHandler, logger: LogHandler) { this.requestHandler = requestHandler; this.logger = logger; diff --git a/lib/odp/segment_manager/odp_segment_manager.ts b/lib/odp/segment_manager/odp_segment_manager.ts index 4aaa47dc3..64a5367ca 100644 --- a/lib/odp/segment_manager/odp_segment_manager.ts +++ b/lib/odp/segment_manager/odp_segment_manager.ts @@ -14,70 +14,37 @@ * limitations under the License. */ -import { getLogger, LogHandler, LogLevel } from '../../modules/logging'; -import { ERROR_MESSAGES, ODP_USER_KEY } from '../../utils/enums'; -import { ICache } from '../../utils/lru_cache'; -import { IOdpSegmentApiManager } from './odp_segment_api_manager'; -import { OdpConfig } from '../odp_config'; +import { ERROR_MESSAGES } from '../../utils/enums'; +import { Cache } from '../../utils/cache/cache'; +import { OdpSegmentApiManager } from './odp_segment_api_manager'; +import { OdpIntegrationConfig } from '../odp_config'; import { OptimizelySegmentOption } from './optimizely_segment_option'; +import { ODP_USER_KEY } from '../constant'; +import { LoggerFacade } from '../../modules/logging'; -export interface IOdpSegmentManager { +export interface OdpSegmentManager { fetchQualifiedSegments( userKey: ODP_USER_KEY, - userValue: string, + user: string, options: Array ): Promise; - reset(): void; - makeCacheKey(userKey: string, userValue: string): string; - updateSettings(config: OdpConfig): void; + updateConfig(config: OdpIntegrationConfig): void; } -/** - * Schedules connections to ODP for audience segmentation and caches the results. - */ -export class OdpSegmentManager implements IOdpSegmentManager { - /** - * ODP configuration settings in used - * @private - */ - private odpConfig?: OdpConfig; - - /** - * Holds cached audience segments - * @private - */ - private _segmentsCache: ICache; - - /** - * Getter for private segments cache - * @public - */ - get segmentsCache(): ICache { - return this._segmentsCache; - } - - /** - * GraphQL API Manager used to fetch segments - * @private - */ - private odpSegmentApiManager: IOdpSegmentApiManager; - - /** - * Handler for recording execution logs - * @private - */ - private readonly logger: LogHandler; +export class DefaultSegmentManager implements OdpSegmentManager { + private odpIntegrationConfig?: OdpIntegrationConfig; + private segmentsCache: Cache; + private odpSegmentApiManager: OdpSegmentApiManager + private logger?: LoggerFacade; constructor( - segmentsCache: ICache, - odpSegmentApiManager: IOdpSegmentApiManager, - logger?: LogHandler, - odpConfig?: OdpConfig, + segmentsCache: Cache, + odpSegmentApiManager: OdpSegmentApiManager, + logger?: LoggerFacade, ) { - this.odpConfig = odpConfig; - this._segmentsCache = segmentsCache; + this.segmentsCache = segmentsCache; this.odpSegmentApiManager = odpSegmentApiManager; - this.logger = logger || getLogger('OdpSegmentManager'); + this.logger = logger; } /** @@ -93,14 +60,20 @@ export class OdpSegmentManager implements IOdpSegmentManager { userValue: string, options: Array ): Promise { - if (!this.odpConfig) { - this.logger.log(LogLevel.WARNING, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); + if (!this.odpIntegrationConfig) { + this.logger?.warn(ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); return null; } - const segmentsToCheck = this.odpConfig.segmentsToCheck; + if (!this.odpIntegrationConfig.integrated) { + this.logger?.warn(ERROR_MESSAGES.ODP_NOT_INTEGRATED); + return null; + } + + const odpConfig = this.odpIntegrationConfig.odpConfig; + + const segmentsToCheck = odpConfig.segmentsToCheck; if (!segmentsToCheck || segmentsToCheck.length <= 0) { - this.logger.log(LogLevel.DEBUG, 'No segments are used in the project. Returning an empty list.'); return []; } @@ -110,58 +83,37 @@ export class OdpSegmentManager implements IOdpSegmentManager { const resetCache = options.includes(OptimizelySegmentOption.RESET_CACHE); if (resetCache) { - this.reset(); + this.segmentsCache.clear(); } - if (!ignoreCache && !resetCache) { - const cachedSegments = this._segmentsCache.lookup(cacheKey); + if (!ignoreCache) { + const cachedSegments = await this.segmentsCache.get(cacheKey); if (cachedSegments) { - this.logger.log(LogLevel.DEBUG, 'ODP cache hit. Returning segments from cache "%s".', cacheKey); return cachedSegments; } - this.logger.log(LogLevel.DEBUG, `ODP cache miss.`); } - this.logger.log(LogLevel.DEBUG, `Making a call to ODP server.`); - const segments = await this.odpSegmentApiManager.fetchSegments( - this.odpConfig.apiKey, - this.odpConfig.apiHost, + odpConfig.apiKey, + odpConfig.apiHost, userKey, userValue, segmentsToCheck ); if (segments && !ignoreCache) { - this._segmentsCache.save({ key: cacheKey, value: segments }); + this.segmentsCache.set(cacheKey, segments); } return segments; } - /** - * Clears the segments cache - */ - reset(): void { - this._segmentsCache.reset(); - } - - /** - * Creates a key used to identify which user fetchQualifiedSegments should lookup and save to in the segments cache - * @param userKey User type based on ODP_USER_KEY, such as "vuid" or "fs_user_id" - * @param userValue Arbitrary string, such as "test-user" - * @returns Concatenates inputs and returns the string "{userKey}-$-{userValue}" - */ makeCacheKey(userKey: string, userValue: string): string { return `${userKey}-$-${userValue}`; } - /** - * Updates the ODP Config settings of ODP Segment Manager - * @param config New ODP Config that will overwrite the existing config - */ - updateSettings(config: OdpConfig): void { - this.odpConfig = config; - this.reset(); + updateConfig(config: OdpIntegrationConfig): void { + this.odpIntegrationConfig = config; + this.segmentsCache.clear(); } } diff --git a/lib/odp/ua_parser/ua_parser.browser.ts b/lib/odp/ua_parser/ua_parser.browser.ts index e6cc27dc8..522c538be 100644 --- a/lib/odp/ua_parser/ua_parser.browser.ts +++ b/lib/odp/ua_parser/ua_parser.browser.ts @@ -16,9 +16,9 @@ import { UAParser } from 'ua-parser-js'; import { UserAgentInfo } from './user_agent_info'; -import { IUserAgentParser } from './user_agent_parser'; +import { UserAgentParser } from './user_agent_parser'; -const userAgentParser: IUserAgentParser = { +const userAgentParser: UserAgentParser = { parseUserAgentInfo(): UserAgentInfo { const parser = new UAParser(); const agentInfo = parser.getResult(); @@ -27,7 +27,7 @@ const userAgentParser: IUserAgentParser = { } } -export function getUserAgentParser(): IUserAgentParser { +export function getUserAgentParser(): UserAgentParser { return userAgentParser; } diff --git a/lib/odp/ua_parser/user_agent_parser.ts b/lib/odp/ua_parser/user_agent_parser.ts index 227065fb7..9ca30c141 100644 --- a/lib/odp/ua_parser/user_agent_parser.ts +++ b/lib/odp/ua_parser/user_agent_parser.ts @@ -16,6 +16,6 @@ import { UserAgentInfo } from "./user_agent_info"; -export interface IUserAgentParser { +export interface UserAgentParser { parseUserAgentInfo(): UserAgentInfo, } diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 7628a0a17..d8a2157a0 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -19,7 +19,7 @@ import { sprintf, objectValues } from '../utils/fns'; import { DefaultNotificationCenter, NotificationCenter } from '../notification_center'; import { EventProcessor } from '../event_processor/event_processor'; -import { IOdpManager } from '../odp/odp_manager'; +import { OdpManager } from '../odp/odp_manager'; import { OdpEvent } from '../odp/event_manager/odp_event'; import { OptimizelySegmentOption } from '../odp/segment_manager/optimizely_segment_option'; @@ -41,7 +41,6 @@ import { newErrorDecision } from '../optimizely_decision'; import OptimizelyUserContext from '../optimizely_user_context'; import { ProjectConfigManager } from '../project_config/project_config_manager'; import { createDecisionService, DecisionService, DecisionObj } from '../core/decision_service'; -// import { getImpressionEvent, getConversionEvent } from '../event_processor/event_builder'; import { buildLogEvent } from '../event_processor/event_builder/log_event'; import { buildImpressionEvent, buildConversionEvent, ImpressionEvent } from '../event_processor/event_builder/user_event'; import fns from '../utils/fns'; @@ -63,9 +62,6 @@ import { // NOTIFICATION_TYPES, NODE_CLIENT_ENGINE, CLIENT_VERSION, - ODP_DEFAULT_EVENT_TYPE, - FS_USER_ID_ALIAS, - ODP_USER_KEY, } from '../utils/enums'; import { Fn } from '../utils/type'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; @@ -99,7 +95,7 @@ export default class Optimizely implements Client { private decisionService: DecisionService; private eventProcessor?: EventProcessor; private defaultDecideOptions: { [key: string]: boolean }; - protected odpManager?: IOdpManager; + protected odpManager?: OdpManager; public notificationCenter: DefaultNotificationCenter; constructor(config: OptimizelyOptions) { diff --git a/lib/plugins/vuid_manager/index.ts b/lib/plugins/vuid_manager/index.ts index 8587724d6..10b3f48f5 100644 --- a/lib/plugins/vuid_manager/index.ts +++ b/lib/plugins/vuid_manager/index.ts @@ -25,13 +25,6 @@ export interface IVuidManager { * Manager for creating, persisting, and retrieving a Visitor Unique Identifier */ export class VuidManager implements IVuidManager { - /** - * Prefix used as part of the VUID format - * @public - * @readonly - */ - static readonly vuid_prefix: string = `vuid_`; - /** * Unique key used within the persistent value cache against which to * store the VUID @@ -97,22 +90,6 @@ export class VuidManager implements IVuidManager { return this._vuid; } - /** - * Creates a new VUID - * @returns A new visitor unique identifier - * @private - */ - private makeVuid(): string { - const maxLength = 32; // required by ODP server - - // make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details. - const uuidV4 = uuid(); - const formatted = uuidV4.replace(/-/g, '').toLowerCase(); - const vuidFull = `${VuidManager.vuid_prefix}${formatted}`; - - return vuidFull.length <= maxLength ? vuidFull : vuidFull.substring(0, maxLength); - } - /** * Saves a VUID to a persistent cache * @param vuid VUID to be stored @@ -123,13 +100,6 @@ export class VuidManager implements IVuidManager { await cache.set(this._keyForVuid, vuid); } - /** - * Validates the format of a Visitor Unique Identifier - * @param vuid VistorId to check - * @returns *true* if the VisitorId is valid otherwise *false* for invalid - */ - static isVuid = (vuid: string): boolean => vuid?.startsWith(VuidManager.vuid_prefix) || false; - /** * Function used in unit testing to reset the VuidManager * **Important**: This should not to be used in production code diff --git a/lib/shared_types.ts b/lib/shared_types.ts index 2cab1c052..568f2a9ba 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -28,12 +28,12 @@ import { IOptimizelyUserContext as OptimizelyUserContext } from './optimizely_us import { ICache } from './utils/lru_cache'; import { RequestHandler } from './utils/http_request_handler/http'; import { OptimizelySegmentOption } from './odp/segment_manager/optimizely_segment_option'; -import { IOdpSegmentApiManager } from './odp/segment_manager/odp_segment_api_manager'; -import { IOdpSegmentManager } from './odp/segment_manager/odp_segment_manager'; -import { IOdpEventApiManager } from './odp/event_manager/odp_event_api_manager'; -import { IOdpEventManager } from './odp/event_manager/odp_event_manager'; -import { IOdpManager } from './odp/odp_manager'; -import { IUserAgentParser } from './odp/ua_parser/user_agent_parser'; +import { OdpSegmentApiManager } from './odp/segment_manager/odp_segment_api_manager'; +import { OdpSegmentManager } from './odp/segment_manager/odp_segment_manager'; +import { DefaultOdpEventApiManager } from './odp/event_manager/odp_event_api_manager'; +import { OdpEventManager } from './odp/event_manager/odp_event_manager'; +import { OdpManager } from './odp/odp_manager'; +import { UserAgentParser } from './odp/ua_parser/user_agent_parser'; import PersistentCache from './plugins/key_value_cache/persistentKeyValueCache'; import { ProjectConfig } from './project_config/project_config'; import { ProjectConfigManager } from './project_config/project_config_manager'; @@ -106,14 +106,14 @@ export interface OdpOptions { segmentsCacheTimeout?: number; segmentsApiTimeout?: number; segmentsRequestHandler?: RequestHandler; - segmentManager?: IOdpSegmentManager; + segmentManager?: OdpSegmentManager; eventFlushInterval?: number; eventBatchSize?: number; eventQueueSize?: number; eventApiTimeout?: number; eventRequestHandler?: RequestHandler; - eventManager?: IOdpEventManager; - userAgentParser?: IUserAgentParser; + eventManager?: OdpEventManager; + userAgentParser?: UserAgentParser; } export interface ListenerPayload { @@ -282,7 +282,7 @@ export interface OptimizelyOptions { userProfileService?: UserProfileService | null; defaultDecideOptions?: OptimizelyDecideOption[]; isSsr?:boolean; - odpManager?: IOdpManager; + odpManager?: OdpManager; notificationCenter: DefaultNotificationCenter; } @@ -542,9 +542,9 @@ export { ICache, RequestHandler, OptimizelySegmentOption, - IOdpSegmentApiManager, - IOdpSegmentManager, - IOdpEventApiManager, - IOdpEventManager, - IOdpManager, + OdpSegmentApiManager as IOdpSegmentApiManager, + OdpSegmentManager as IOdpSegmentManager, + DefaultOdpEventApiManager as IOdpEventApiManager, + OdpEventManager as IOdpEventManager, + OdpManager as IOdpManager, }; diff --git a/lib/utils/cache/in_memory_lru_cache.ts b/lib/utils/cache/in_memory_lru_cache.ts new file mode 100644 index 000000000..ca917ca2a --- /dev/null +++ b/lib/utils/cache/in_memory_lru_cache.ts @@ -0,0 +1,83 @@ +/** + * 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. + * 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 { Maybe } from "../type"; +import { SyncCache } from "./cache"; + +type CacheElement = { + value: V; + expiresAt?: number; +}; + +export class InMemoryLruCache implements SyncCache { + public operation = 'sync' as const; + private data: Map> = new Map(); + private maxSize: number; + private ttl?: number; + + constructor(maxSize: number, ttl?: number) { + this.maxSize = maxSize; + this.ttl = ttl; + } + + get(key: string): Maybe { + const element = this.data.get(key); + + if (!element) return undefined; + this.data.delete(key); + + if (element.expiresAt && element.expiresAt <= Date.now()) { + return undefined; + } + + this.data.set(key, element); + return element.value; + } + + set(key: string, value: V): void { + this.data.delete(key); + + if (this.data.size === this.maxSize) { + const firstMapEntryKey = this.data.keys().next().value; + this.data.delete(firstMapEntryKey!); + } + + this.data.set(key, { + value, + expiresAt: this.ttl ? Date.now() + this.ttl : undefined, + }); + } + + remove(key: string): void { + this.data.delete(key); + } + + clear(): void { + this.data.clear(); + } + + getKeys(): string[] { + return Array.from(this.data.keys()); + } + + getBatched(keys: string[]): Maybe[] { + return keys.map((key) => this.get(key)); + } + + peek(key: string): Maybe { + return this.data.get(key)?.value; + } +} diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts index 10a5deb3f..6878b7bfe 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -294,25 +294,4 @@ export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute export const REQUEST_TIMEOUT_ODP_SEGMENTS_MS = 10 * 1000; // 10 secs export const REQUEST_TIMEOUT_ODP_EVENTS_MS = 10 * 1000; // 10 secs -/** - * ODP User Key Options - */ -export enum ODP_USER_KEY { - VUID = 'vuid', - FS_USER_ID = 'fs_user_id', -} - -/** - * Alias for fs_user_id to catch for and automatically convert to fs_user_id - */ -export const FS_USER_ID_ALIAS = 'fs-user-id'; - -export const ODP_DEFAULT_EVENT_TYPE = 'fullstack'; -/** - * ODP Event Action Options - */ -export enum ODP_EVENT_ACTION { - IDENTIFIED = 'identified', - INITIALIZED = 'client_initialized', -} diff --git a/lib/utils/fns/index.ts b/lib/utils/fns/index.ts index 98606a77a..f66d9d56a 100644 --- a/lib/utils/fns/index.ts +++ b/lib/utils/fns/index.ts @@ -57,10 +57,6 @@ export function keyBy(arr: K[], key: string): { [key: string]: K } { }); } -function isNumber(value: unknown): boolean { - return typeof value === 'number'; -} - export function uuid(): string { return v4(); } @@ -170,7 +166,6 @@ export default { isSafeInteger, keyBy, uuid, - isNumber, getTimestamp, isValidEnum, groupBy, diff --git a/lib/utils/lru_cache/browser_lru_cache.ts b/lib/utils/lru_cache/browser_lru_cache.ts index ca5d4cb92..c9e6cc6ff 100644 --- a/lib/utils/lru_cache/browser_lru_cache.ts +++ b/lib/utils/lru_cache/browser_lru_cache.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import LRUCache, { ISegmentsCacheConfig } from './lru_cache'; +import LRUCache, { ISegmentsCacheConfig } from '../cache/in_memory_lru_cache'; export interface BrowserLRUCacheConfig { maxSize?: number; diff --git a/lib/utils/lru_cache/index.ts b/lib/utils/lru_cache/index.ts index fb7ada423..681120d17 100644 --- a/lib/utils/lru_cache/index.ts +++ b/lib/utils/lru_cache/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { ICache, LRUCache } from './lru_cache'; +import { ICache, LRUCache } from '../cache/in_memory_lru_cache'; import { BrowserLRUCache } from './browser_lru_cache'; import { ServerLRUCache } from './server_lru_cache'; diff --git a/lib/utils/lru_cache/lru_cache.ts b/lib/utils/lru_cache/lru_cache.ts deleted file mode 100644 index 0e8be1d8c..000000000 --- a/lib/utils/lru_cache/lru_cache.ts +++ /dev/null @@ -1,132 +0,0 @@ -/** - * 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. - * 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 } from '../../modules/logging'; -import CacheElement from './cache_element'; - -export interface LRUCacheConfig { - maxSize: number; - timeout: number; -} - -export interface ICache { - lookup(key: K): V | null; - save({ key, value }: { key: K; value: V }): void; - reset(): void; -} - -/** - * Least-Recently Used Cache (LRU Cache) Implementation with Generic Key-Value Pairs - * Analogous to a Map that has a specified max size and a timeout per element. - * - Removes the least-recently used element from the cache if max size exceeded. - * - Removes stale elements (entries older than their timeout) from the cache. - */ -export class LRUCache implements ICache { - private _map: Map> = new Map(); - private _maxSize; // Defines maximum size of _map - private _timeout; // Milliseconds each entry has before it becomes stale - - get map(): Map> { - return this._map; - } - - get maxSize(): number { - return this._maxSize; - } - - get timeout(): number { - return this._timeout; - } - - constructor({ maxSize, timeout }: LRUCacheConfig) { - const logger = getLogger(); - - logger.debug(`Provisioning cache with maxSize of ${maxSize}`); - logger.debug(`Provisioning cache with timeout of ${timeout}`); - - this._maxSize = maxSize; - this._timeout = timeout; - } - - /** - * Returns a valid, non-stale value from LRU Cache based on an input key. - * Additionally moves the element to the end of the cache and removes from cache if stale. - */ - lookup(key: K): V | null { - if (this._maxSize <= 0) { - return null; - } - - const element: CacheElement | undefined = this._map.get(key); - - if (!element) return null; - - if (element.is_stale(this._timeout)) { - this._map.delete(key); - return null; - } - - this._map.delete(key); - this._map.set(key, element); - - return element.value; - } - - /** - * Inserts/moves an input key-value pair to the end of the LRU Cache. - * Removes the least-recently used element if the cache exceeds it's maxSize. - */ - save({ key, value }: { key: K; value: V }): void { - if (this._maxSize <= 0) return; - - const element: CacheElement | undefined = this._map.get(key); - if (element) this._map.delete(key); - this._map.set(key, new CacheElement(value)); - - if (this._map.size > this._maxSize) { - const firstMapEntryKey = this._map.keys().next().value; - this._map.delete(firstMapEntryKey); - } - } - - /** - * Clears the LRU Cache - */ - reset(): void { - if (this._maxSize <= 0) return; - - this._map.clear(); - } - - /** - * Reads value from specified key without moving elements in the LRU Cache. - * @param {K} key - */ - peek(key: K): V | null { - if (this._maxSize <= 0) return null; - - const element: CacheElement | undefined = this._map.get(key); - - return element?.value ?? null; - } -} - -export interface ISegmentsCacheConfig { - DEFAULT_CAPACITY: number; - DEFAULT_TIMEOUT_SECS: number; -} - -export default LRUCache; diff --git a/lib/utils/lru_cache/server_lru_cache.ts b/lib/utils/lru_cache/server_lru_cache.ts index 110d9b28e..3956c4f9c 100644 --- a/lib/utils/lru_cache/server_lru_cache.ts +++ b/lib/utils/lru_cache/server_lru_cache.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import LRUCache, { ISegmentsCacheConfig } from './lru_cache'; +import LRUCache, { ISegmentsCacheConfig } from '../cache/in_memory_lru_cache'; export interface ServerLRUCacheConfig { maxSize?: number; diff --git a/lib/utils/repeater/repeater.ts b/lib/utils/repeater/repeater.ts index 1425db431..9f307ab95 100644 --- a/lib/utils/repeater/repeater.ts +++ b/lib/utils/repeater/repeater.ts @@ -31,6 +31,7 @@ export interface Repeater { stop(): void; reset(): void; setTask(task: AsyncTransformer): void; + isRunning(): boolean; } export interface BackoffController { @@ -74,13 +75,17 @@ export class IntervalRepeater implements Repeater { private interval: number; private failureCount = 0; private backoffController?: BackoffController; - private isRunning = false; + private running = false; constructor(interval: number, backoffController?: BackoffController) { this.interval = interval; this.backoffController = backoffController; } + isRunning(): boolean { + return this.running; + } + private handleSuccess() { this.failureCount = 0; this.backoffController?.reset(); @@ -94,7 +99,7 @@ export class IntervalRepeater implements Repeater { } private setTimer(timeout: number) { - if (!this.isRunning){ + if (!this.running){ return; } this.timeoutId = setTimeout(this.executeTask.bind(this), timeout); @@ -111,7 +116,7 @@ export class IntervalRepeater implements Repeater { } start(immediateExecution?: boolean): void { - this.isRunning = true; + this.running = true; if(immediateExecution) { scheduleMicrotask(this.executeTask.bind(this)); } else { @@ -120,7 +125,7 @@ export class IntervalRepeater implements Repeater { } stop(): void { - this.isRunning = false; + this.running = false; clearInterval(this.timeoutId); } diff --git a/lib/vuid/vuid.spec.ts b/lib/vuid/vuid.spec.ts new file mode 100644 index 000000000..6427796cb --- /dev/null +++ b/lib/vuid/vuid.spec.ts @@ -0,0 +1,15 @@ +import { vi, describe, expect, it } from 'vitest'; + +import { VUID_PREFIX, VUID_MAX_LENGTH, isVuid, makeVuid } from './vuid'; + +describe('isVuid', () => { + it('should return true if and only if the value strats with the VUID_PREFIX and is longer than vuid_prefix', () => { + expect(isVuid('vuid_a')).toBe(true); + expect(isVuid('vuid_123')).toBe(true); + expect(isVuid('vuid_')).toBe(false); + expect(isVuid('vuid')).toBe(false); + expect(isVuid('vui')).toBe(false); + expect(isVuid('vu_123')).toBe(false); + expect(isVuid('123')).toBe(false); + }) +}); diff --git a/lib/vuid/vuid.ts b/lib/vuid/vuid.ts new file mode 100644 index 000000000..4c49204c8 --- /dev/null +++ b/lib/vuid/vuid.ts @@ -0,0 +1,15 @@ +import { v4 as uuidV4 } from 'uuid'; + +export const VUID_PREFIX: string = `vuid_`; +export const VUID_MAX_LENGTH = 32; + +export const isVuid = (vuid: string): boolean => vuid.startsWith(VUID_PREFIX) && vuid.length > VUID_PREFIX.length; + +export const makeVuid = (): string => { + // make sure UUIDv4 is used (not UUIDv1 or UUIDv6) since the trailing 5 chars will be truncated. See TDD for details. + const uuid = uuidV4(); + const formatted = uuid.replace(/-/g, ''); + const vuidFull = `${VUID_PREFIX}${formatted}`; + + return vuidFull.length <= VUID_MAX_LENGTH ? vuidFull : vuidFull.substring(0, VUID_MAX_LENGTH); +}; diff --git a/tests/odpEventApiManager.spec.ts b/tests/odpEventApiManager.spec.ts deleted file mode 100644 index 07632c72a..000000000 --- a/tests/odpEventApiManager.spec.ts +++ /dev/null @@ -1,139 +0,0 @@ -/** - * Copyright 2022-2024, 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, beforeEach, beforeAll, it, expect } from 'vitest'; - -import { anyString, anything, capture, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { NodeOdpEventApiManager } from '../lib/odp/event_manager/event_api_manager.node'; -import { OdpEvent } from '../lib/odp/event_manager/odp_event'; -import { RequestHandler } from '../lib/utils/http_request_handler/http'; -import { OdpConfig } from '../lib/odp/odp_config'; - -const data1 = new Map(); -data1.set('key11', 'value-1'); -data1.set('key12', true); -data1.set('key12', 3.5); -data1.set('key14', null); -const data2 = new Map(); -data2.set('key2', 'value-2'); -const ODP_EVENTS = [ - new OdpEvent('t1', 'a1', new Map([['id-key-1', 'id-value-1']]), data1), - new OdpEvent('t2', 'a2', new Map([['id-key-2', 'id-value-2']]), data2), -]; - -const API_KEY = 'test-api-key'; -const API_HOST = 'https://odp.example.com'; -const PIXEL_URL = 'https://odp.pixel.com'; - -const odpConfig = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, []); - -describe('NodeOdpEventApiManager', () => { - let mockLogger: LogHandler; - let mockRequestHandler: RequestHandler; - - beforeAll(() => { - mockLogger = mock(); - mockRequestHandler = mock(); - }); - - beforeEach(() => { - resetCalls(mockLogger); - resetCalls(mockRequestHandler); - }); - - const managerInstance = () => { - const manager = new NodeOdpEventApiManager(instance(mockRequestHandler), instance(mockLogger)); - return manager; - } - - const abortableRequest = (statusCode: number, body: string) => { - return { - abort: () => {}, - responsePromise: Promise.resolve({ - statusCode, - body, - headers: {}, - }), - }; - }; - - it('should should send events successfully and not suggest retry', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, '') - ); - const manager = managerInstance(); - - const shouldRetry = await manager.sendEvents(odpConfig, ODP_EVENTS); - - expect(shouldRetry).toBe(false); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('should not suggest a retry for 400 HTTP response', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(400, '') - ); - const manager = managerInstance(); - - const shouldRetry = await manager.sendEvents(odpConfig, ODP_EVENTS); - - expect(shouldRetry).toBe(false); - verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (400)')).once(); - }); - - it('should suggest a retry for 500 HTTP response', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(500, '') - ); - const manager = managerInstance(); - - const shouldRetry = await manager.sendEvents(odpConfig, ODP_EVENTS); - - expect(shouldRetry).toBe(true); - verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (500)')).once(); - }); - - it('should suggest a retry for network timeout', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => {}, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - const manager = managerInstance(); - - const shouldRetry = await manager.sendEvents(odpConfig, ODP_EVENTS); - - expect(shouldRetry).toBe(true); - verify(mockLogger.log(LogLevel.ERROR, 'ODP event send failed (Request timed out)')).once(); - }); - - it('should send events to the correct host using correct api key', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => {}, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - - const manager = managerInstance(); - - await manager.sendEvents(odpConfig, ODP_EVENTS); - - verify(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).once(); - - const [initUrl, headers] = capture(mockRequestHandler.makeRequest).first(); - expect(initUrl).toEqual(`${API_HOST}/v3/events`); - expect(headers['x-api-key']).toEqual(odpConfig.apiKey); - }); -}); diff --git a/vitest.config.mts b/vitest.config.mts index 673f7d1c6..1b02bf791 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/*.spec.ts'], + include: ['**/odp_event_api_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 10d134911dbd0a9a888e52c8c652a5d7d77bcd5e Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 2 Dec 2024 22:21:56 +0600 Subject: [PATCH 02/28] upd --- .../event_manager}/odpEventManager.spec.ts | 24 ++--- .../event_manager/odp_event_manager.spec.ts | 15 ++++ lib/odp/event_manager/odp_event_manager.ts | 88 +------------------ 3 files changed, 31 insertions(+), 96 deletions(-) rename {tests => lib/odp/event_manager}/odpEventManager.spec.ts (96%) create mode 100644 lib/odp/event_manager/odp_event_manager.spec.ts diff --git a/tests/odpEventManager.spec.ts b/lib/odp/event_manager/odpEventManager.spec.ts similarity index 96% rename from tests/odpEventManager.spec.ts rename to lib/odp/event_manager/odpEventManager.spec.ts index 38cf9d379..74c974b67 100644 --- a/tests/odpEventManager.spec.ts +++ b/lib/odp/event_manager/odpEventManager.spec.ts @@ -15,20 +15,20 @@ */ import { describe, beforeEach, afterEach, beforeAll, it, vi, expect } from 'vitest'; -import { ODP_EVENT_ACTION, ODP_DEFAULT_EVENT_TYPE, ERROR_MESSAGES } from '../lib/utils/enums'; -import { OdpConfig } from '../lib/odp/odp_config'; -import { Status } from '../lib/odp/event_manager/odp_event_manager'; -import { BrowserOdpEventManager } from '../lib/odp/event_manager/event_manager.browser'; -import { NodeOdpEventManager } from '../lib/odp/event_manager/event_manager.node'; -import { OdpEventManager } from '../lib/odp/event_manager/odp_event_manager'; +import { ODP_EVENT_ACTION, ODP_DEFAULT_EVENT_TYPE, ERROR_MESSAGES } from '../../utils/enums'; +import { OdpConfig } from '../odp_config'; +import { Status } from './odp_event_manager'; +import { BrowserOdpEventManager } from './event_manager.browser'; +import { NodeOdpEventManager } from './event_manager.node'; +import { OdpEventManager } from './odp_event_manager'; import { anything, capture, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; -import { IOdpEventApiManager } from '../lib/odp/event_manager/odp_event_api_manager'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpEvent } from '../lib/odp/event_manager/odp_event'; -import { IUserAgentParser } from '../lib/odp/ua_parser/user_agent_parser'; -import { UserAgentInfo } from '../lib/odp/ua_parser/user_agent_info'; +import { IOdpEventApiManager } from './odp_event_api_manager'; +import { LogHandler, LogLevel } from '../../modules/logging'; +import { OdpEvent } from './odp_event'; +import { IUserAgentParser } from '../ua_parser/user_agent_parser'; +import { UserAgentInfo } from '../ua_parser/user_agent_info'; import { resolve } from 'path'; -import { advanceTimersByTime } from './testUtils'; +import { advanceTimersByTime } from '../../../tests/testUtils'; const API_KEY = 'test-api-key'; const API_HOST = 'https://odp.example.com'; diff --git a/lib/odp/event_manager/odp_event_manager.spec.ts b/lib/odp/event_manager/odp_event_manager.spec.ts new file mode 100644 index 000000000..76dd7be54 --- /dev/null +++ b/lib/odp/event_manager/odp_event_manager.spec.ts @@ -0,0 +1,15 @@ +/** + * Copyright 2024, 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. + */ diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index 8829fe51a..6122d98f7 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import { LogLevel } from '../../modules/logging'; - import { OdpEvent } from './odp_event'; import { OdpConfig, OdpIntegrationConfig } from '../odp_config'; import { OdpEventApiManager } from './odp_event_api_manager'; @@ -54,8 +52,6 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag private retryConfig: RetryConfig; - // private readonly userAgentData?: Map; - constructor(config: OdpEventManagerConfig) { super(config.startUpLogs); @@ -65,28 +61,8 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag this.repeater = config.repeater; this.repeater.setTask(() => this.flush()); - - // if (config.userAgentParser) { - // const { os, device } = config.userAgentParser.parseUserAgentInfo(); - - // const userAgentInfo: Record = { - // 'os': os.name, - // 'os_version': os.version, - // 'device_type': device.type, - // 'model': device.model, - // }; - - // this.userAgentData = new Map( - // Object.entries(userAgentInfo).filter(([_, value]) => value != null && value != undefined) - // ); - // } } - // setClientInfo(clientEngine: string, clientVersion: string): void { - // this.clientEngine = clientEngine; - // this.clientVersion = clientVersion; - // } - private async executeDispatch(odpConfig: OdpConfig, batch: OdpEvent[]): Promise { const res = await this.apiManager.sendEvents(odpConfig, batch); if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { @@ -106,8 +82,8 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag const batch = this.queue; this.queue = []; - // as the current queue has been emptied, stop repeating flus - // until more events becomes availabe + // as the queue has been emptied, stop repeating flush + // until more events become available this.repeater.reset(); return runWithRetry( @@ -129,18 +105,6 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag } else { this.state = ServiceState.Starting; } - - // if (!this.odpIntegrationConfig) { - // this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); - // return; - // } - - // this.status = Status.Running; - - // // no need of periodic flush if batchSize is 1 - // if (this.batchSize > 1) { - // this.setNewTimeout(); - // } } updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void { @@ -183,31 +147,6 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag this.stopPromise.resolve(); } - // TODO: move this to ODP manager - /** - * Associate a full-stack userid with an established VUID - * @param {string} userId (Optional) Full-stack User ID - * @param {string} vuid (Optional) Visitor User ID - */ - // identifyUser(userId?: string, vuid?: string): void { - // const identifiers = new Map(); - // 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); - // } - - // if (userId) { - // identifiers.set(ODP_USER_KEY.FS_USER_ID, userId); - // } - - // const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); - // this.sendEvent(event); - // } - sendEvent(event: OdpEvent): void { if (!this.isRunning()) { this.logger?.error('ODP event manager is not running.'); @@ -220,7 +159,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag } if (event.identifiers.size === 0) { - this.logger?.log(LogLevel.ERROR, 'ODP events should have at least one key-value pair in identifiers.'); + this.logger?.error('ODP events should have at least one key-value pair in identifiers.'); return; } @@ -235,7 +174,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag } if (event.type === '') { - event.action = ODP_DEFAULT_EVENT_TYPE; + event.type = ODP_DEFAULT_EVENT_TYPE; } event.identifiers.forEach((key, value) => { @@ -270,23 +209,4 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag this.repeater.start(); } } - - // TODO: move to ODP maanger - /** - * Add additional common data including an idempotent ID and execution context to event data - * @param sourceData Existing event data to augment - * @returns Augmented event data - * @private - */ - // private augmentCommonData(sourceData: Map): Map { - // const data = new Map(this.userAgentData); - - // data.set('idempotence_id', uuidV4()); - // data.set('data_source_type', 'sdk'); - // data.set('data_source', this.clientEngine); - // data.set('data_source_version', this.clientVersion); - - // sourceData.forEach((value, key) => data.set(key, value)); - // return data; - // } } From bab1a705f33e157a4f65f8706084aeeef80b44aa Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 3 Dec 2024 05:47:45 +0600 Subject: [PATCH 03/28] tests --- .../event_manager/odp_event_manager.spec.ts | 925 ++++++++++++++++++ lib/odp/event_manager/odp_event_manager.ts | 2 +- lib/tests/mock/mock_repeater.ts | 7 +- lib/tests/testUtils.ts | 5 + vitest.config.mts | 2 +- 5 files changed, 936 insertions(+), 5 deletions(-) create mode 100644 lib/tests/testUtils.ts diff --git a/lib/odp/event_manager/odp_event_manager.spec.ts b/lib/odp/event_manager/odp_event_manager.spec.ts index 76dd7be54..7a51d3065 100644 --- a/lib/odp/event_manager/odp_event_manager.spec.ts +++ b/lib/odp/event_manager/odp_event_manager.spec.ts @@ -13,3 +13,928 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import { DefaultOdpEventManager } from './odp_event_manager'; +import { getMockRepeater } from '../../tests/mock/mock_repeater'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { ServiceState } from '../../service'; +import { exhaustMicrotasks } from '../../tests/testUtils'; +import { OdpEvent } from './odp_event'; +import { OdpConfig } from '../odp_config'; +import { EventDispatchResponse } from './odp_event_api_manager'; +import { advanceTimersByTime } from '../../../tests/testUtils'; + +const API_KEY = 'test-api-key'; +const API_HOST = 'https://odp.example.com'; +const PIXEL_URL = 'https://odp.pixel.com'; +const SEGMENTS_TO_CHECK = ['segment1', 'segment2']; + +const config = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, SEGMENTS_TO_CHECK); + +const makeEvent = (id: number) => { + const identifiers = new Map(); + identifiers.set('identifier1', 'value1-' + id); + identifiers.set('identifier2', 'value2-' + id); + + const data = new Map(); + data.set('data1', 'data-value1-' + id); + data.set('data2', id); + + return new OdpEvent('test-type-' + id, 'test-action-' + id, identifiers, data); +}; + +const getMockApiManager = () => { + return { + sendEvents: vi.fn(), + }; +}; + +describe('DefaultOdpEventManager', () => { + beforeEach(() => { + vi.useFakeTimers(); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('should be in new state after construction', () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + expect(odpEventManager.getState()).toBe(ServiceState.New); + }); + + it('should stay in starting state if started with a odpIntegationConfig and not resolve or reject onRunning', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + const onRunningHandler = vi.fn(); + odpEventManager.onRunning().then(onRunningHandler, onRunningHandler); + + odpEventManager.start(); + expect(odpEventManager.getState()).toBe(ServiceState.Starting); + + await exhaustMicrotasks(); + + expect(odpEventManager.getState()).toBe(ServiceState.Starting); + expect(onRunningHandler).not.toHaveBeenCalled(); + }); + + it('should move to running state and resolve onRunning() is start() is called after updateConfig()', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: false, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + }); + + it('should move to running state and resolve onRunning() is updateConfig() is called after start()', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.start(); + + odpEventManager.updateConfig({ + integrated: false, + }); + + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + }); + + it('should queue events until batchSize is reached', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for (let i = 0; i < 9; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + events.push(makeEvent(9)); + odpEventManager.sendEvent(events[9]); + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + }); + + it('should should send events immediately asynchronously if batchSize is 1', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + for (let i = 0; i < 10; i++) { + const event = makeEvent(i); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i + 1, config, [event]); + } + }); + + it('drops events and logs if the state is not running', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + expect(odpEventManager.getState()).toBe(ServiceState.New); + + const event = makeEvent(0); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('drops events and logs if odpIntegrationConfig is not integrated', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: false, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = makeEvent(0); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('drops event and logs if there is no identifier', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = new OdpEvent('test-type', 'test-action', new Map(), new Map()); + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('accepts string, number, boolean, and null values for data', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const data = new Map(); + data.set('string', 'string-value'); + data.set('number', 123); + data.set('boolean', true); + data.set('null', null); + + const event = new OdpEvent('test-type', 'test-action', new Map([['k', 'v']]), data); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, [event]); + }); + + it('should drop event and log if data contains values other than string, number, boolean, or null', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const data = new Map(); + data.set('string', 'string-value'); + data.set('number', 123); + data.set('boolean', true); + data.set('null', null); + data.set('invalid', new Date()); + + const event = new OdpEvent('test-type', 'test-action', new Map([['k', 'v']]), data); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('should drop event and log if action is empty', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = new OdpEvent('test-type', '', new Map([['k', 'v']]), new Map([['k', 'v']])); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('should use fullstack as type if type is empty', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 1, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event = new OdpEvent('', 'test-action', new Map([['k', 'v']]), new Map([['k', 'v']])); + + odpEventManager.sendEvent(event); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents.mock.calls[0][1][0].type).toBe('fullstack'); + }); + + it('should transform identifiers with keys FS-USER-ID, fs-user-id and FS_USER_ID to fs_user_id', async () => { + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: apiManager, + batchSize: 3, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.setLogger(logger); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Running); + + const event1 = new OdpEvent('test-type', 'test-action', new Map([['FS-USER-ID', 'value1']]), new Map([['k', 'v']])); + const event2 = new OdpEvent('test-type', 'test-action', new Map([['fs-user-id', 'value2']]), new Map([['k', 'v']])); + const event3 = new OdpEvent('test-type', 'test-action', new Map([['FS_USER_ID', 'value3']]), new Map([['k', 'v']])); + + odpEventManager.sendEvent(event1); + odpEventManager.sendEvent(event2); + odpEventManager.sendEvent(event3); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents.mock.calls[0][1][0].identifiers.get('fs_user_id')).toBe('value1'); + expect(apiManager.sendEvents.mock.calls[0][1][1].identifiers.get('fs_user_id')).toBe('value2'); + expect(apiManager.sendEvents.mock.calls[0][1][2].identifiers.get('fs_user_id')).toBe('value3'); + }); + + it('should start the repeater when the first event is sent', async () => { + const repeater = getMockRepeater(); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: getMockApiManager(), + batchSize: 300, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + expect(repeater.start).not.toHaveBeenCalled(); + + for(let i = 0; i < 10; i++) { + odpEventManager.sendEvent(makeEvent(i)); + await exhaustMicrotasks(); + expect(repeater.start).toHaveBeenCalledTimes(1); + } + }); + + it('should flush the queue when the repeater triggers', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + await repeater.execute(0); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + }); + + it('should reset the repeater after flush', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + expect(repeater.reset).not.toHaveBeenCalled(); + + await repeater.execute(0); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + expect(repeater.reset).toHaveBeenCalledTimes(1); + }); + + it('should retry specified number of times with backoff if apiManager.sendEvents returns a rejecting promise', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockReturnValue(Promise.reject(new Error('Failed to dispatch events'))); + + const backoffController = { + backoff: vi.fn().mockReturnValue(666), + reset: vi.fn(), + }; + + const maxRetries = 5; + const retryConfig = { + maxRetries, + backoffProvider: () => backoffController, + }; + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: retryConfig, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + repeater.execute(0); + for(let i = 1; i <= maxRetries; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(666); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i, config, events); + expect(backoffController.backoff).toHaveBeenCalledTimes(i); + } + }); + + it('should retry specified number of times with backoff if apiManager returns 5xx', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockReturnValue(Promise.resolve({ statusCode: 500 })); + + const backoffController = { + backoff: vi.fn().mockReturnValue(666), + reset: vi.fn(), + }; + + const maxRetries = 5; + const retryConfig = { + maxRetries, + backoffProvider: () => backoffController, + }; + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: retryConfig, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + repeater.execute(0); + for(let i = 1; i <= maxRetries; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(666); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i, config, events); + expect(backoffController.backoff).toHaveBeenCalledTimes(i); + } + }); + + it('should log error if event sends fails even after retry', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockReturnValue(Promise.reject(new Error('Failed to dispatch events'))); + + const backoffController = { + backoff: vi.fn().mockReturnValue(666), + reset: vi.fn(), + }; + + const maxRetries = 5; + const retryConfig = { + maxRetries, + backoffProvider: () => backoffController, + }; + + const logger = getMockLogger(); + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: retryConfig, + }); + + odpEventManager.setLogger(logger); + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + repeater.execute(0); + for(let i = 1; i <= maxRetries; i++) { + await exhaustMicrotasks(); + await advanceTimersByTime(666); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(i + 1); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(i, config, events); + expect(backoffController.backoff).toHaveBeenCalledTimes(i); + } + + await exhaustMicrotasks(); + expect(logger.error).toHaveBeenCalledTimes(1); + }); + + it('flushes the queue with old config if updateConfig is called with a new config', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + const newConfig = new OdpConfig('new-api-key', 'https://new-odp.example.com', 'https://new-odp.pixel.com', ['new-segment']); + odpEventManager.updateConfig({ + integrated: true, + odpConfig: newConfig, + }); + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledOnce(); + expect(apiManager.sendEvents).toHaveBeenCalledWith(config, events); + }); + + it('uses the new config after updateConfig is called', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + const newConfig = new OdpConfig('new-api-key', 'https://new-odp.example.com', 'https://new-odp.pixel.com', ['new-segment']); + odpEventManager.updateConfig({ + integrated: true, + odpConfig: newConfig, + }); + + const newEvents: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + newEvents.push(makeEvent(i + 10)); + odpEventManager.sendEvent(newEvents[i]); + } + + repeater.execute(0); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(2); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); + expect(apiManager.sendEvents).toHaveBeenNthCalledWith(2, newConfig, newEvents); + }); + + it('should reject onRunning() if stop() is called in new state', async () => { + const odpEventManager = new DefaultOdpEventManager({ + repeater: getMockRepeater(), + apiManager: getMockApiManager(), + batchSize: 10, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.stop(); + await expect(odpEventManager.onRunning()).rejects.toThrow(); + }); + + it('should flush the queue and reset the repeater if stop() is called in running state', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + odpEventManager.stop(); + await exhaustMicrotasks(); + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenCalledWith(config, events); + expect(repeater.reset).toHaveBeenCalledTimes(1); + }); + + it('resolve onTerminated() and go to Terminated state if stop() is called in running state', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + odpEventManager.stop(); + await expect(odpEventManager.onTerminated()).resolves.not.toThrow(); + expect(odpEventManager.getState()).toBe(ServiceState.Terminated); + }); +}); diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index 6122d98f7..0d61870d3 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -177,7 +177,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag event.type = ODP_DEFAULT_EVENT_TYPE; } - event.identifiers.forEach((key, value) => { + Array.from(event.identifiers.entries()).forEach(([key, value]) => { // Catch for fs-user-id, FS-USER-ID, and FS_USER_ID and assign value to fs_user_id identifier. if ( ODP_USER_KEY.FS_USER_ID_ALIAS === key.toLowerCase() || diff --git a/lib/tests/mock/mock_repeater.ts b/lib/tests/mock/mock_repeater.ts index a93cbfa87..adf6baf83 100644 --- a/lib/tests/mock/mock_repeater.ts +++ b/lib/tests/mock/mock_repeater.ts @@ -20,7 +20,7 @@ import { AsyncTransformer } from '../../utils/type'; // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const getMockRepeater = () => { const mock = { - isRunning: false, + running: false, handler: undefined as any, start: vi.fn(), stop: vi.fn(), @@ -36,8 +36,9 @@ export const getMockRepeater = () => { ret?.catch(() => {}); return ret; }, + isRunning: () => mock.running, }; - mock.start.mockImplementation(() => mock.isRunning = true); - mock.stop.mockImplementation(() => mock.isRunning = false); + mock.start.mockImplementation(() => mock.running = true); + mock.stop.mockImplementation(() => mock.running = false); return mock; } diff --git a/lib/tests/testUtils.ts b/lib/tests/testUtils.ts new file mode 100644 index 000000000..b24f10517 --- /dev/null +++ b/lib/tests/testUtils.ts @@ -0,0 +1,5 @@ +export const exhaustMicrotasks = async (loop = 100) => { + for(let i = 0; i < loop; i++) { + await Promise.resolve(); + } +}; diff --git a/vitest.config.mts b/vitest.config.mts index 1b02bf791..55dbcbc02 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_event_api_manager.spec.ts'], + include: ['**/odp_event_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 64de7975875dc546f185fe076d5ade4283889e8b Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 3 Dec 2024 20:25:33 +0600 Subject: [PATCH 04/28] segmen test --- .../event_manager/event_manager.browser.ts | 50 -- lib/odp/event_manager/event_manager.node.ts | 50 -- lib/odp/event_manager/odpEventManager.spec.ts | 733 ------------------ lib/odp/odp_manager.browser.ts | 4 +- lib/odp/odp_manager.node.ts | 4 +- .../odpSegmentApiManager.spec.ts | 8 +- .../odp_segment_manager.spec.ts | 180 +++++ .../segment_manager/odp_segment_manager.ts | 12 +- tests/odpSegmentManager.spec.ts | 179 ----- vitest.config.mts | 2 +- 10 files changed, 195 insertions(+), 1027 deletions(-) delete mode 100644 lib/odp/event_manager/event_manager.browser.ts delete mode 100644 lib/odp/event_manager/event_manager.node.ts delete mode 100644 lib/odp/event_manager/odpEventManager.spec.ts rename {tests => lib/odp/segment_manager}/odpSegmentApiManager.spec.ts (97%) create mode 100644 lib/odp/segment_manager/odp_segment_manager.spec.ts delete mode 100644 tests/odpSegmentManager.spec.ts diff --git a/lib/odp/event_manager/event_manager.browser.ts b/lib/odp/event_manager/event_manager.browser.ts deleted file mode 100644 index b2ac15f82..000000000 --- a/lib/odp/event_manager/event_manager.browser.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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 { OdpEventManager, DefaultOdpEventManager } from './odp_event_manager'; -import { LogLevel } from '../../modules/logging'; -import { OdpEvent } from './odp_event'; - -const DEFAULT_BROWSER_QUEUE_SIZE = 100; - -export class BrowserOdpEventManager extends DefaultOdpEventManager implements OdpEventManager { - protected initParams( - batchSize: number | undefined, - queueSize: number | undefined, - flushInterval: number | undefined - ): void { - this.queueSize = queueSize || DEFAULT_BROWSER_QUEUE_SIZE; - - // disable event batching for browser - this.batchSize = 1; - this.flushInterval = 0; - - if (typeof batchSize !== 'undefined' && batchSize !== 1) { - this.getLogger().log(LogLevel.WARNING, 'ODP event batch size must be 1 in the browser.'); - } - - if (typeof flushInterval !== 'undefined' && flushInterval !== 0) { - this.getLogger().log(LogLevel.WARNING, 'ODP event flush interval must be 0 in the browser.'); - } - } - - protected discardEventsIfNeeded(): void { - // in Browser/client-side context, give debug message but leave events in queue - this.getLogger().log(LogLevel.DEBUG, 'ODPConfig not ready. Leaving events in queue.'); - } - - protected hasNecessaryIdentifiers = (event: OdpEvent): boolean => event.identifiers.size >= 0; -} diff --git a/lib/odp/event_manager/event_manager.node.ts b/lib/odp/event_manager/event_manager.node.ts deleted file mode 100644 index 4d88744e9..000000000 --- a/lib/odp/event_manager/event_manager.node.ts +++ /dev/null @@ -1,50 +0,0 @@ -/** - * 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 { OdpEvent } from './odp_event'; -import { OdpEventManager, DefaultOdpEventManager } from './odp_event_manager'; -import { LogLevel } from '../../modules/logging'; - -const DEFAULT_BATCH_SIZE = 10; -const DEFAULT_FLUSH_INTERVAL_MSECS = 1000; -const DEFAULT_SERVER_QUEUE_SIZE = 10000; - -export class NodeOdpEventManager extends DefaultOdpEventManager implements OdpEventManager { - protected initParams( - batchSize: number | undefined, - queueSize: number | undefined, - flushInterval: number | undefined - ): void { - this.queueSize = queueSize || DEFAULT_SERVER_QUEUE_SIZE; - this.batchSize = batchSize || DEFAULT_BATCH_SIZE; - - if (flushInterval === 0) { - // disable event batching - this.batchSize = 1; - this.flushInterval = 0; - } else { - this.flushInterval = flushInterval || DEFAULT_FLUSH_INTERVAL_MSECS; - } - } - - protected discardEventsIfNeeded(): void { - // if Node/server-side context, empty queue items before ready state - this.getLogger().log(LogLevel.WARNING, 'ODPConfig not ready. Discarding events in queue.'); - this.queue = new Array(); - } - - protected hasNecessaryIdentifiers = (event: OdpEvent): boolean => event.identifiers.size >= 1; -} diff --git a/lib/odp/event_manager/odpEventManager.spec.ts b/lib/odp/event_manager/odpEventManager.spec.ts deleted file mode 100644 index 74c974b67..000000000 --- a/lib/odp/event_manager/odpEventManager.spec.ts +++ /dev/null @@ -1,733 +0,0 @@ -/** - * Copyright 2022-2024, 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, beforeEach, afterEach, beforeAll, it, vi, expect } from 'vitest'; - -import { ODP_EVENT_ACTION, ODP_DEFAULT_EVENT_TYPE, ERROR_MESSAGES } from '../../utils/enums'; -import { OdpConfig } from '../odp_config'; -import { Status } from './odp_event_manager'; -import { BrowserOdpEventManager } from './event_manager.browser'; -import { NodeOdpEventManager } from './event_manager.node'; -import { OdpEventManager } from './odp_event_manager'; -import { anything, capture, instance, mock, resetCalls, spy, verify, when } from 'ts-mockito'; -import { IOdpEventApiManager } from './odp_event_api_manager'; -import { LogHandler, LogLevel } from '../../modules/logging'; -import { OdpEvent } from './odp_event'; -import { IUserAgentParser } from '../ua_parser/user_agent_parser'; -import { UserAgentInfo } from '../ua_parser/user_agent_info'; -import { resolve } from 'path'; -import { advanceTimersByTime } from '../../../tests/testUtils'; - -const API_KEY = 'test-api-key'; -const API_HOST = 'https://odp.example.com'; -const PIXEL_URL = 'https://odp.pixel.com'; -const MOCK_IDEMPOTENCE_ID = 'c1dc758e-f095-4f09-9b49-172d74c53880'; -const EVENTS: OdpEvent[] = [ - new OdpEvent( - 't1', - 'a1', - new Map([['id-key-1', 'id-value-1']]), - new Map([ - ['key-1', 'value1'], - ['key-2', null], - ['key-3', 3.3], - ['key-4', true], - ]), - ), - new OdpEvent( - 't2', - 'a2', - new Map([['id-key-2', 'id-value-2']]), - new Map( - Object.entries({ - 'key-2': 'value2', - data_source: 'my-source', - }) - ) - ), -]; -// naming for object destructuring -const clientEngine = 'javascript-sdk'; -const clientVersion = '4.9.3'; -const PROCESSED_EVENTS: OdpEvent[] = [ - new OdpEvent( - 't1', - 'a1', - new Map([['id-key-1', 'id-value-1']]), - new Map( - Object.entries({ - idempotence_id: MOCK_IDEMPOTENCE_ID, - data_source_type: 'sdk', - data_source: clientEngine, - data_source_version: clientVersion, - 'key-1': 'value1', - 'key-2': null, - 'key-3': 3.3, - 'key-4': true, - }) - ) - ), - new OdpEvent( - 't2', - 'a2', - new Map([['id-key-2', 'id-value-2']]), - new Map( - Object.entries({ - idempotence_id: MOCK_IDEMPOTENCE_ID, - data_source_type: 'sdk', - data_source: clientEngine, - data_source_version: clientVersion, - 'key-2': 'value2', - }) - ) - ), -]; -const EVENT_WITH_EMPTY_IDENTIFIER = new OdpEvent( - 't4', - 'a4', - new Map(), - new Map([ - ['key-53f3', true], - ['key-a04a', 123], - ['key-2ab4', 'Linus Torvalds'], - ]), -); -const EVENT_WITH_UNDEFINED_IDENTIFIER = new OdpEvent( - 't4', - 'a4', - undefined, - new Map([ - ['key-53f3', false], - ['key-a04a', 456], - ['key-2ab4', 'Bill Gates'] - ]), -); -const makeEvent = (id: number) => { - const identifiers = new Map(); - identifiers.set('identifier1', 'value1-' + id); - identifiers.set('identifier2', 'value2-' + id); - - const data = new Map(); - data.set('data1', 'data-value1-' + id); - data.set('data2', id); - - return new OdpEvent('test-type-' + id, 'test-action-' + id, identifiers, data); -}; -const pause = (timeoutMilliseconds: number): Promise => { - return new Promise(resolve => setTimeout(resolve, timeoutMilliseconds)); -}; -const abortableRequest = (statusCode: number, body: string) => { - return { - abort: () => {}, - responsePromise: Promise.resolve({ - statusCode, - body, - headers: {}, - }), - }; -}; - -class TestOdpEventManager extends OdpEventManager { - constructor(options: any) { - super(options); - } - protected initParams(batchSize: number, queueSize: number, flushInterval: number): void { - this.queueSize = queueSize; - this.batchSize = batchSize; - this.flushInterval = flushInterval; - } - protected discardEventsIfNeeded(): void { - } - protected hasNecessaryIdentifiers = (event: OdpEvent): boolean => event.identifiers.size >= 0; -} - -describe('OdpEventManager', () => { - let mockLogger: LogHandler; - let mockApiManager: IOdpEventApiManager; - - let odpConfig: OdpConfig; - let logger: LogHandler; - let apiManager: IOdpEventApiManager; - - beforeAll(() => { - mockLogger = mock(); - mockApiManager = mock(); - odpConfig = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, []); - logger = instance(mockLogger); - apiManager = instance(mockApiManager); - }); - - beforeEach(() => { - vi.useFakeTimers(); - resetCalls(mockLogger); - resetCalls(mockApiManager); - }); - - afterEach(() => { - vi.clearAllTimers(); - }); - - it('should log an error and not start if start() is called without a config', () => { - const eventManager = new TestOdpEventManager({ - odpConfig: undefined, - apiManager, - logger, - clientEngine, - clientVersion, - }); - - eventManager.start(); - verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).once(); - expect(eventManager.status).toEqual(Status.Stopped); - }); - - it('should start() correctly after odpConfig is provided', () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - }); - - expect(eventManager.status).toEqual(Status.Stopped); - eventManager.updateSettings(odpConfig); - eventManager.start(); - expect(eventManager.status).toEqual(Status.Running); - }); - - it('should log and discard events when event manager is not running', () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - }); - - expect(eventManager.status).toEqual(Status.Stopped); - eventManager.sendEvent(EVENTS[0]); - verify(mockLogger.log(LogLevel.WARNING, 'Failed to Process ODP Event. ODPEventManager is not running.')).once(); - expect(eventManager.getQueue().length).toEqual(0); - }); - - it('should discard events with invalid data', () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - }); - eventManager.start(); - - expect(eventManager.status).toEqual(Status.Running); - - // make an event with invalid data key-value entry - const badEvent = new OdpEvent( - 't3', - 'a3', - new Map([['id-key-3', 'id-value-3']]), - new Map([ - ['key-1', false], - ['key-2', { random: 'object', whichShouldFail: true }], - ]), - ); - eventManager.sendEvent(badEvent); - - verify(mockLogger.log(LogLevel.ERROR, 'Event data found to be invalid.')).once(); - expect(eventManager.getQueue().length).toEqual(0); - }); - - it('should log a max queue hit and discard ', () => { - // set queue to maximum of 1 - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - queueSize: 1, // With max queue size set to 1... - }); - - eventManager.start(); - - eventManager['queue'].push(EVENTS[0]); // simulate 1 event already in the queue then... - // ...try adding the second event - eventManager.sendEvent(EVENTS[1]); - - verify( - mockLogger.log(LogLevel.WARNING, 'Failed to Process ODP Event. Event Queue full. queueSize = %s.', 1) - ).once(); - }); - - it('should add additional information to each event', () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - }); - eventManager.start(); - - const processedEventData = PROCESSED_EVENTS[0].data; - - const eventData = eventManager['augmentCommonData'](EVENTS[0].data); - - expect((eventData.get('idempotence_id') as string).length).toEqual( - (processedEventData.get('idempotence_id') as string).length - ); - expect(eventData.get('data_source_type')).toEqual(processedEventData.get('data_source_type')); - expect(eventData.get('data_source')).toEqual(processedEventData.get('data_source')); - expect(eventData.get('data_source_version')).toEqual(processedEventData.get('data_source_version')); - expect(eventData.get('key-1')).toEqual(processedEventData.get('key-1')); - expect(eventData.get('key-2')).toEqual(processedEventData.get('key-2')); - expect(eventData.get('key-3')).toEqual(processedEventData.get('key-3')); - expect(eventData.get('key-4')).toEqual(processedEventData.get('key-4')); - }); - - it('should attempt to flush an empty queue at flush intervals if batchSize is greater than 1', async () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, - flushInterval: 100, - }); - - //@ts-ignore - const processQueueSpy = vi.spyOn(eventManager, 'processQueue'); - - eventManager.start(); - // do not add events to the queue, but allow for... - vi.advanceTimersByTime(350); // 3 flush intervals executions (giving a little longer) - - expect(processQueueSpy).toHaveBeenCalledTimes(3); - }); - - - it('should not flush periodically if batch size is 1', async () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 1, - flushInterval: 100, - }); - - //@ts-ignore - const processQueueSpy = vi.spyOn(eventManager, 'processQueue'); - - eventManager.start(); - eventManager.sendEvent(EVENTS[0]); - eventManager.sendEvent(EVENTS[1]); - - vi.advanceTimersByTime(350); // 3 flush intervals executions (giving a little longer) - - expect(processQueueSpy).toHaveBeenCalledTimes(2); - }); - - it('should dispatch events in correct batch sizes', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - - const apiManager = instance(mockApiManager); - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, // with batch size of 10... - flushInterval: 250, - }); - - eventManager.start(); - - for (let i = 0; i < 25; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - await Promise.resolve(); - - // as we are not advancing the vi fake timers, no flush should occur - // ...there should be 3 batches: - // batch #1 with 10, batch #2 with 10, and batch #3 (after flushInterval lapsed) with 5 = 25 events - verify(mockApiManager.sendEvents(anything(), anything())).twice(); - - // rest of the events should now be flushed - await advanceTimersByTime(250); - verify(mockApiManager.sendEvents(anything(), anything())).thrice(); - }); - - it('should dispatch events with correct payload', async () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, - flushInterval: 100, - }); - - eventManager.start(); - EVENTS.forEach(event => eventManager.sendEvent(event)); - - await advanceTimersByTime(100); - // sending 1 batch of 2 events after flushInterval since batchSize is 10 - verify(mockApiManager.sendEvents(anything(), anything())).once(); - const [_, events] = capture(mockApiManager.sendEvents).last(); - expect(events.length).toEqual(2); - expect(events[0].identifiers.size).toEqual(PROCESSED_EVENTS[0].identifiers.size); - expect(events[0].data.size).toEqual(PROCESSED_EVENTS[0].data.size); - expect(events[1].identifiers.size).toEqual(PROCESSED_EVENTS[1].identifiers.size); - expect(events[1].data.size).toEqual(PROCESSED_EVENTS[1].data.size); - }); - - it('should dispatch events with correct odpConfig', async () => { - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, - flushInterval: 100, - }); - - eventManager.start(); - EVENTS.forEach(event => eventManager.sendEvent(event)); - - await advanceTimersByTime(100); - - // sending 1 batch of 2 events after flushInterval since batchSize is 10 - verify(mockApiManager.sendEvents(anything(), anything())).once(); - const [usedOdpConfig] = capture(mockApiManager.sendEvents).last(); - expect(usedOdpConfig.equals(odpConfig)).toBeTruthy(); - }); - - it('should augment events with data from user agent parser', async () => { - const userAgentParser : IUserAgentParser = { - parseUserAgentInfo: function (): UserAgentInfo { - return { - os: { 'name': 'windows', 'version': '11' }, - device: { 'type': 'laptop', 'model': 'thinkpad' }, - } - } - } - - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, - flushInterval: 100, - userAgentParser, - }); - - eventManager.start(); - EVENTS.forEach(event => eventManager.sendEvent(event)); - await advanceTimersByTime(100); - - verify(mockApiManager.sendEvents(anything(), anything())).called(); - const [_, events] = capture(mockApiManager.sendEvents).last(); - const event = events[0]; - - expect(event.data.get('os')).toEqual('windows'); - expect(event.data.get('os_version')).toEqual('11'); - expect(event.data.get('device_type')).toEqual('laptop'); - expect(event.data.get('model')).toEqual('thinkpad'); - }); - - it('should retry failed events', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(true) - - const retries = 3; - const apiManager = instance(mockApiManager); - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 2, - flushInterval: 100, - retries, - }); - - eventManager.start(); - for (let i = 0; i < 4; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - vi.runAllTicks(); - vi.useRealTimers(); - await pause(100); - - // retry 3x for 2 batches or 6 calls to attempt to process - verify(mockApiManager.sendEvents(anything(), anything())).times(6); - }); - - it('should flush all queued events when flush() is called', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - - const apiManager = instance(mockApiManager); - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 200, - flushInterval: 100, - }); - - eventManager.start(); - for (let i = 0; i < 25; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - expect(eventManager.getQueue().length).toEqual(25); - - eventManager.flush(); - - await Promise.resolve(); - - verify(mockApiManager.sendEvents(anything(), anything())).once(); - expect(eventManager.getQueue().length).toEqual(0); - }); - - it('should flush all queued events before stopping', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - const apiManager = instance(mockApiManager); - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 200, - flushInterval: 100, - }); - - eventManager.start(); - for (let i = 0; i < 25; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - expect(eventManager.getQueue().length).toEqual(25); - - eventManager.flush(); - - await Promise.resolve(); - - verify(mockApiManager.sendEvents(anything(), anything())).once(); - expect(eventManager.getQueue().length).toEqual(0); - }); - - it('should flush all queued events using the old odpConfig when updateSettings is called()', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - - const odpConfig = new OdpConfig('old-key', 'old-host', 'https://new-odp.pixel.com', []); - const updatedConfig = new OdpConfig('new-key', 'new-host', 'https://new-odp.pixel.com', []); - - const apiManager = instance(mockApiManager); - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 200, - flushInterval: 100, - }); - - eventManager.start(); - for (let i = 0; i < 25; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - expect(eventManager.getQueue().length).toEqual(25); - - eventManager.updateSettings(updatedConfig); - - await Promise.resolve(); - - verify(mockApiManager.sendEvents(anything(), anything())).once(); - expect(eventManager.getQueue().length).toEqual(0); - const [usedOdpConfig] = capture(mockApiManager.sendEvents).last(); - expect(usedOdpConfig.equals(odpConfig)).toBeTruthy(); - }); - - it('should use updated odpConfig to send events', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - - const odpConfig = new OdpConfig('old-key', 'old-host', 'https://new-odp.pixel.com', []); - const updatedConfig = new OdpConfig('new-key', 'new-host', 'https://new-odp.pixel.com', []); - - const apiManager = instance(mockApiManager); - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 200, - flushInterval: 100, - }); - - eventManager.start(); - for (let i = 0; i < 25; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - expect(eventManager.getQueue().length).toEqual(25); - - await advanceTimersByTime(100); - - expect(eventManager.getQueue().length).toEqual(0); - let [usedOdpConfig] = capture(mockApiManager.sendEvents).first(); - expect(usedOdpConfig.equals(odpConfig)).toBeTruthy(); - - eventManager.updateSettings(updatedConfig); - - for (let i = 0; i < 25; i += 1) { - eventManager.sendEvent(makeEvent(i)); - } - - await advanceTimersByTime(100); - - expect(eventManager.getQueue().length).toEqual(0); - ([usedOdpConfig] = capture(mockApiManager.sendEvents).last()); - expect(usedOdpConfig.equals(updatedConfig)).toBeTruthy(); - }); - - it('should prepare correct payload for register VUID', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - - const apiManager = instance(mockApiManager); - - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, - flushInterval: 250, - }); - - const vuid = 'vuid_330e05cad15746d9af8a75b8d10'; - const fsUserId = 'test-fs-user-id'; - - eventManager.start(); - eventManager.registerVuid(vuid); - - await advanceTimersByTime(250); - - const [_, events] = capture(mockApiManager.sendEvents).last(); - expect(events.length).toBe(1); - - const [event] = events; - expect(event.type).toEqual('fullstack'); - expect(event.action).toEqual(ODP_EVENT_ACTION.INITIALIZED); - expect(event.identifiers).toEqual(new Map([['vuid', vuid]])); - expect((event.data.get("idempotence_id") as string).length).toBe(36); // uuid length - expect((event.data.get("data_source_type") as string)).toEqual('sdk'); - expect((event.data.get("data_source") as string)).toEqual('javascript-sdk'); - expect(event.data.get("data_source_version") as string).not.toBeNull(); - }); - - it('should send correct event payload for identify user', async () => { - when(mockApiManager.sendEvents(anything(), anything())).thenResolve(false); - - const apiManager = instance(mockApiManager); - - const eventManager = new TestOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - batchSize: 10, - flushInterval: 250, - }); - - const vuid = 'vuid_330e05cad15746d9af8a75b8d10'; - const fsUserId = 'test-fs-user-id'; - - eventManager.start(); - eventManager.identifyUser(fsUserId, vuid); - - await advanceTimersByTime(260); - - const [_, events] = capture(mockApiManager.sendEvents).last(); - expect(events.length).toBe(1); - - const [event] = events; - expect(event.type).toEqual(ODP_DEFAULT_EVENT_TYPE); - expect(event.action).toEqual(ODP_EVENT_ACTION.IDENTIFIED); - expect(event.identifiers).toEqual(new Map([['vuid', vuid], ['fs_user_id', fsUserId]])); - expect((event.data.get("idempotence_id") as string).length).toBe(36); // uuid length - expect((event.data.get("data_source_type") as string)).toEqual('sdk'); - expect((event.data.get("data_source") as string)).toEqual('javascript-sdk'); - expect(event.data.get("data_source_version") as string).not.toBeNull(); - }); - - it('should error when no identifiers are provided in Node', () => { - const eventManager = new NodeOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - }); - - eventManager.start(); - eventManager.sendEvent(EVENT_WITH_EMPTY_IDENTIFIER); - eventManager.sendEvent(EVENT_WITH_UNDEFINED_IDENTIFIER); - eventManager.stop(); - - vi.runAllTicks(); - - verify(mockLogger.log(LogLevel.ERROR, 'ODP events should have at least one key-value pair in identifiers.')).twice(); - }); - - it('should never error when no identifiers are provided in Browser', () => { - const eventManager = new BrowserOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine, - clientVersion, - }); - - eventManager.start(); - eventManager.sendEvent(EVENT_WITH_EMPTY_IDENTIFIER); - eventManager.sendEvent(EVENT_WITH_UNDEFINED_IDENTIFIER); - eventManager.stop(); - - vi.runAllTicks(); - - verify(mockLogger.log(LogLevel.ERROR, 'ODP events should have at least one key-value pair in identifiers.')).never(); - }); -}); diff --git a/lib/odp/odp_manager.browser.ts b/lib/odp/odp_manager.browser.ts index 6c6e8ffaf..89eeacf71 100644 --- a/lib/odp/odp_manager.browser.ts +++ b/lib/odp/odp_manager.browser.ts @@ -37,7 +37,7 @@ import { OdpEvent } from './event_manager/odp_event'; import { IOdpEventManager, OdpOptions } from '../shared_types'; import { BrowserOdpEventApiManager } from './event_manager/event_api_manager.browser'; import { BrowserOdpEventManager } from './event_manager/event_manager.browser'; -import { OdpSegmentManager, DefaultSegmentManager } from './segment_manager/odp_segment_manager'; +import { OdpSegmentManager, DefaultOdpSegmentManager } from './segment_manager/odp_segment_manager'; import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; import { OdpConfig, OdpIntegrationConfig } from './odp_config'; @@ -93,7 +93,7 @@ export class BrowserOdpManager extends DefaultOdpManager { if (odpOptions?.segmentManager) { segmentManager = odpOptions.segmentManager; } else { - segmentManager = new DefaultSegmentManager( + segmentManager = new DefaultOdpSegmentManager( odpOptions?.segmentsCache || new BrowserLRUCache({ maxSize: odpOptions?.segmentsCacheSize, diff --git a/lib/odp/odp_manager.node.ts b/lib/odp/odp_manager.node.ts index e5839b4bf..e4490e4d5 100644 --- a/lib/odp/odp_manager.node.ts +++ b/lib/odp/odp_manager.node.ts @@ -30,7 +30,7 @@ import { DefaultOdpManager } from './odp_manager'; import { IOdpEventManager, OdpOptions } from '../shared_types'; import { NodeOdpEventApiManager } from './event_manager/event_api_manager.node'; import { NodeOdpEventManager } from './event_manager/event_manager.node'; -import { OdpSegmentManager, DefaultSegmentManager } from './segment_manager/odp_segment_manager'; +import { OdpSegmentManager, DefaultOdpSegmentManager } from './segment_manager/odp_segment_manager'; import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; import { OdpConfig, OdpIntegrationConfig } from './odp_config'; @@ -85,7 +85,7 @@ export class NodeOdpManager extends DefaultOdpManager { if (odpOptions?.segmentManager) { segmentManager = odpOptions.segmentManager; } else { - segmentManager = new DefaultSegmentManager( + segmentManager = new DefaultOdpSegmentManager( odpOptions?.segmentsCache || new ServerLRUCache({ maxSize: odpOptions?.segmentsCacheSize, diff --git a/tests/odpSegmentApiManager.spec.ts b/lib/odp/segment_manager/odpSegmentApiManager.spec.ts similarity index 97% rename from tests/odpSegmentApiManager.spec.ts rename to lib/odp/segment_manager/odpSegmentApiManager.spec.ts index ee8ebc482..455890ff7 100644 --- a/tests/odpSegmentApiManager.spec.ts +++ b/lib/odp/segment_manager/odpSegmentApiManager.spec.ts @@ -17,10 +17,10 @@ import { describe, beforeEach, beforeAll, it, expect } from 'vitest'; import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { OdpSegmentApiManager } from '../lib/odp/segment_manager/odp_segment_api_manager'; -import { RequestHandler } from '../lib/utils/http_request_handler/http'; -import { ODP_USER_KEY } from '../lib/utils/enums'; +import { LogHandler, LogLevel } from '../../modules/logging'; +import { OdpSegmentApiManager } from './odp_segment_api_manager'; +import { RequestHandler } from '../../utils/http_request_handler/http'; +import { ODP_USER_KEY } from '../../utils/enums'; const API_key = 'not-real-api-key'; const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; diff --git a/lib/odp/segment_manager/odp_segment_manager.spec.ts b/lib/odp/segment_manager/odp_segment_manager.spec.ts new file mode 100644 index 000000000..0c4b5168f --- /dev/null +++ b/lib/odp/segment_manager/odp_segment_manager.spec.ts @@ -0,0 +1,180 @@ +/** + * Copyright 2022-2024, 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, beforeEach, it, expect, vi } from 'vitest'; + + +import { ODP_USER_KEY } from '../constant'; +import { DefaultOdpSegmentManager, OdpSegmentManager } from './odp_segment_manager'; +import { OdpConfig } from '../odp_config'; +import { OptimizelySegmentOption } from './optimizely_segment_option'; +import { OdpSegmentApiManager } from './odp_segment_api_manager'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { getMockSyncCache } from '../../tests/mock/mock_cache'; +import exp from 'constants'; + +const API_KEY = 'test-api-key'; +const API_HOST = 'https://odp.example.com'; +const PIXEL_URL = 'https://odp.pixel.com'; +const SEGMENTS_TO_CHECK = ['segment1', 'segment2']; + +const config = new OdpConfig(API_KEY, API_HOST, PIXEL_URL, SEGMENTS_TO_CHECK); + +const getMockApiManager = () => { + return { + fetchSegments: vi.fn(), + }; +}; + +const userKey: ODP_USER_KEY = ODP_USER_KEY.FS_USER_ID; +const userValue = 'test-user'; + +describe('DefaultOdpSegmentManager', () => { + it('should return null and log error if the ODP config is not available.', async () => { + const logger = getMockLogger(); + const cache = getMockSyncCache(); + const manager = new DefaultOdpSegmentManager(cache, getMockApiManager(), logger); + expect(await manager.fetchQualifiedSegments(userKey, userValue)).toBeNull(); + expect(logger.warn).toHaveBeenCalledOnce(); + }); + + it('should return null and log error if ODP is not integrated.', async () => { + const logger = getMockLogger(); + const cache = getMockSyncCache(); + const manager = new DefaultOdpSegmentManager(cache, getMockApiManager(), logger); + manager.updateConfig({ integrated: false }); + expect(await manager.fetchQualifiedSegments(userKey, userValue)).toBeNull(); + expect(logger.warn).toHaveBeenCalledOnce(); + }); + + it('should fetch segments from apiManager using correct config on cache miss and save to cache.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue); + expect(segments).toEqual(['k', 'l']); + expect(apiManager.fetchSegments).toHaveBeenCalledWith(API_KEY, API_HOST, userKey, userValue, SEGMENTS_TO_CHECK); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); + }); + + it('should return sement from cache and not call apiManager on cache hit.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue); + expect(segments).toEqual(['x']); + + expect(apiManager.fetchSegments).not.toHaveBeenCalled(); + }); + + it('should return null when apiManager returns null.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(null); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue); + expect(segments).toBeNull(); + }); + + it('should ignore the cache if the option enum is included in the options array.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue, [OptimizelySegmentOption.IGNORE_CACHE]); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['x']); + expect(apiManager.fetchSegments).toHaveBeenCalledWith(API_KEY, API_HOST, userKey, userValue, SEGMENTS_TO_CHECK); + }); + + it('should ignore the cache if the option string is included in the options array.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + + // @ts-ignore + const segments = await manager.fetchQualifiedSegments(userKey, userValue, ['IGNORE_CACHE']); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['x']); + expect(apiManager.fetchSegments).toHaveBeenCalledWith(API_KEY, API_HOST, userKey, userValue, SEGMENTS_TO_CHECK); + }); + + it('should reset the cache if the option enum is included in the options array.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + cache.set(manager.makeCacheKey(userKey, '123'), ['a']); + cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + + const segments = await manager.fetchQualifiedSegments(userKey, userValue, [OptimizelySegmentOption.RESET_CACHE]); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); + expect(cache.size()).toBe(1); + }); + + it('should reset the cache if the option string is included in the options array.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + manager.updateConfig({ integrated: true, odpConfig: config }); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + cache.set(manager.makeCacheKey(userKey, '123'), ['a']); + cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + + // @ts-ignore + const segments = await manager.fetchQualifiedSegments(userKey, userValue, ['RESET_CACHE']); + expect(segments).toEqual(['k', 'l']); + expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); + expect(cache.size()).toBe(1); + }); + + it('should reset the cache on config update.', async () => { + const cache = getMockSyncCache(); + const apiManager = getMockApiManager(); + apiManager.fetchSegments.mockResolvedValue(['k', 'l']); + + const manager = new DefaultOdpSegmentManager(cache, apiManager); + cache.set(manager.makeCacheKey(userKey, userValue), ['x']); + cache.set(manager.makeCacheKey(userKey, '123'), ['a']); + cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + + expect(cache.size()).toBe(3); + manager.updateConfig({ integrated: true, odpConfig: config }); + expect(cache.size()).toBe(0); + }); +}); diff --git a/lib/odp/segment_manager/odp_segment_manager.ts b/lib/odp/segment_manager/odp_segment_manager.ts index 64a5367ca..dbf83a12f 100644 --- a/lib/odp/segment_manager/odp_segment_manager.ts +++ b/lib/odp/segment_manager/odp_segment_manager.ts @@ -25,13 +25,13 @@ import { LoggerFacade } from '../../modules/logging'; export interface OdpSegmentManager { fetchQualifiedSegments( userKey: ODP_USER_KEY, - user: string, - options: Array + userValue: string, + options?: Array ): Promise; updateConfig(config: OdpIntegrationConfig): void; } -export class DefaultSegmentManager implements OdpSegmentManager { +export class DefaultOdpSegmentManager implements OdpSegmentManager { private odpIntegrationConfig?: OdpIntegrationConfig; private segmentsCache: Cache; private odpSegmentApiManager: OdpSegmentApiManager @@ -58,7 +58,7 @@ export class DefaultSegmentManager implements OdpSegmentManager { async fetchQualifiedSegments( userKey: ODP_USER_KEY, userValue: string, - options: Array + options?: Array ): Promise { if (!this.odpIntegrationConfig) { this.logger?.warn(ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); @@ -79,8 +79,8 @@ export class DefaultSegmentManager implements OdpSegmentManager { const cacheKey = this.makeCacheKey(userKey, userValue); - const ignoreCache = options.includes(OptimizelySegmentOption.IGNORE_CACHE); - const resetCache = options.includes(OptimizelySegmentOption.RESET_CACHE); + const ignoreCache = options?.includes(OptimizelySegmentOption.IGNORE_CACHE); + const resetCache = options?.includes(OptimizelySegmentOption.RESET_CACHE); if (resetCache) { this.segmentsCache.clear(); diff --git a/tests/odpSegmentManager.spec.ts b/tests/odpSegmentManager.spec.ts deleted file mode 100644 index f10dbc353..000000000 --- a/tests/odpSegmentManager.spec.ts +++ /dev/null @@ -1,179 +0,0 @@ -/** - * Copyright 2022-2024, 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, beforeEach, it, expect } from 'vitest'; - -import { mock, resetCalls, instance } from 'ts-mockito'; - -import { LogHandler } from '../lib/modules/logging'; -import { ODP_USER_KEY } from '../lib/utils/enums'; -import { RequestHandler } from '../lib/utils/http_request_handler/http'; - -import { OdpSegmentManager } from '../lib/odp/segment_manager/odp_segment_manager'; -import { OdpConfig } from '../lib/odp/odp_config'; -import { LRUCache } from '../lib/utils/lru_cache'; -import { OptimizelySegmentOption } from './../lib/odp/segment_manager/optimizely_segment_option'; -import { OdpSegmentApiManager } from '../lib/odp/segment_manager/odp_segment_api_manager'; - -describe('OdpSegmentManager', () => { - class MockOdpSegmentApiManager extends OdpSegmentApiManager { - async fetchSegments( - apiKey: string, - apiHost: string, - userKey: ODP_USER_KEY, - userValue: string, - segmentsToCheck: string[] - ): Promise { - if (apiKey == 'invalid-key') return null; - return segmentsToCheck; - } - } - - const mockLogHandler = mock(); - const mockRequestHandler = mock(); - - const apiManager = new MockOdpSegmentApiManager(instance(mockRequestHandler), instance(mockLogHandler)); - - let options: Array = []; - - const userKey: ODP_USER_KEY = ODP_USER_KEY.VUID; - const userValue = 'test-user'; - - const validTestOdpConfig = new OdpConfig('valid-key', 'host', 'pixel-url', ['new-customer']); - const invalidTestOdpConfig = new OdpConfig('invalid-key', 'host', 'pixel-url', ['new-customer']); - - const getSegmentsCache = () => { - return new LRUCache({ - maxSize: 1000, - timeout: 1000, - }); - } - - beforeEach(() => { - resetCalls(mockLogHandler); - resetCalls(mockRequestHandler); - - const API_KEY = 'test-api-key'; - const API_HOST = 'https://odp.example.com'; - const PIXEL_URL = 'https://odp.pixel.com'; - }); - - it('should fetch segments successfully on cache miss.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - setCache(manager, userKey, '123', ['a']); - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, options); - expect(segments).toEqual(['new-customer']); - }); - - it('should fetch segments successfully on cache hit.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - setCache(manager, userKey, userValue, ['a']); - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, options); - expect(segments).toEqual(['a']); - }); - - it('should return null when fetching segments returns an error.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, invalidTestOdpConfig); - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, []); - expect(segments).toBeNull; - }); - - it('should ignore the cache if the option enum is included in the options array.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - setCache(manager, userKey, userValue, ['a']); - options = [OptimizelySegmentOption.IGNORE_CACHE]; - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, options); - expect(segments).toEqual(['new-customer']); - expect(cacheCount(manager)).toBe(1); - }); - - it('should ignore the cache if the option string is included in the options array.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - setCache(manager,userKey, userValue, ['a']); - // @ts-ignore - options = ['IGNORE_CACHE']; - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, options); - expect(segments).toEqual(['new-customer']); - expect(cacheCount(manager)).toBe(1); - }); - - it('should reset the cache if the option enum is included in the options array.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - setCache(manager, userKey, userValue, ['a']); - setCache(manager, userKey, '123', ['a']); - setCache(manager, userKey, '456', ['a']); - options = [OptimizelySegmentOption.RESET_CACHE]; - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, options); - expect(segments).toEqual(['new-customer']); - expect(peekCache(manager, userKey, userValue)).toEqual(segments); - expect(cacheCount(manager)).toBe(1); - }); - - it('should reset the cache on settings update.', async () => { - const oldConfig = new OdpConfig('old-key', 'old-host', 'pixel-url', ['new-customer']); - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - - setCache(manager, userKey, userValue, ['a']); - expect(cacheCount(manager)).toBe(1); - - const newConfig = new OdpConfig('new-key', 'new-host', 'pixel-url', ['new-customer']); - manager.updateSettings(newConfig); - - expect(cacheCount(manager)).toBe(0); - }); - - it('should reset the cache if the option string is included in the options array.', async () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - setCache(manager, userKey, userValue, ['a']); - setCache(manager, userKey, '123', ['a']); - setCache(manager, userKey, '456', ['a']); - // @ts-ignore - options = ['RESET_CACHE']; - - const segments = await manager.fetchQualifiedSegments(userKey, userValue, options); - expect(segments).toEqual(['new-customer']); - expect(peekCache(manager, userKey, userValue)).toEqual(segments); - expect(cacheCount(manager)).toBe(1); - }); - - it('should make a valid cache key.', () => { - const manager = new OdpSegmentManager(getSegmentsCache(), apiManager, mockLogHandler, validTestOdpConfig); - expect('vuid-$-test-user').toBe(manager.makeCacheKey(userKey, userValue)); - }); - - // Utility Functions - - function setCache(manager: OdpSegmentManager, userKey: string, userValue: string, value: string[]) { - const cacheKey = manager.makeCacheKey(userKey, userValue); - manager.segmentsCache.save({ - key: cacheKey, - value, - }); - } - - function peekCache(manager: OdpSegmentManager, userKey: string, userValue: string): string[] | null { - const cacheKey = manager.makeCacheKey(userKey, userValue); - return (manager.segmentsCache as LRUCache).peek(cacheKey); - } - - const cacheCount = (manager: OdpSegmentManager) => (manager.segmentsCache as LRUCache).map.size; -}); diff --git a/vitest.config.mts b/vitest.config.mts index 55dbcbc02..9732becfb 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_event_manager.spec.ts'], + include: ['**/odp_segment_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 811fd4867c68fe12741052a12c33185dcbf365b1 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 3 Dec 2024 21:53:25 +0600 Subject: [PATCH 05/28] seg api man --- .../odpSegmentApiManager.spec.ts | 300 ------------------ .../odp_segment_api_manager.spec.ts | 248 +++++++++++++++ .../odp_segment_api_manager.ts | 37 +-- .../odp_segment_manager.spec.ts | 6 +- vitest.config.mts | 2 +- 5 files changed, 265 insertions(+), 328 deletions(-) delete mode 100644 lib/odp/segment_manager/odpSegmentApiManager.spec.ts create mode 100644 lib/odp/segment_manager/odp_segment_api_manager.spec.ts diff --git a/lib/odp/segment_manager/odpSegmentApiManager.spec.ts b/lib/odp/segment_manager/odpSegmentApiManager.spec.ts deleted file mode 100644 index 455890ff7..000000000 --- a/lib/odp/segment_manager/odpSegmentApiManager.spec.ts +++ /dev/null @@ -1,300 +0,0 @@ -/** - * Copyright 2022-2024 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, beforeEach, beforeAll, it, expect } from 'vitest'; - -import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../../modules/logging'; -import { OdpSegmentApiManager } from './odp_segment_api_manager'; -import { RequestHandler } from '../../utils/http_request_handler/http'; -import { ODP_USER_KEY } from '../../utils/enums'; - -const API_key = 'not-real-api-key'; -const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; -const USER_KEY = ODP_USER_KEY.FS_USER_ID; -const USER_VALUE = 'tester-101'; -const SEGMENTS_TO_CHECK = ['has_email', 'has_email_opted_in', 'push_on_sale']; - -describe('OdpSegmentApiManager', () => { - let mockLogger: LogHandler; - let mockRequestHandler: RequestHandler; - - beforeAll(() => { - mockLogger = mock(); - mockRequestHandler = mock(); - }); - - beforeEach(() => { - resetCalls(mockLogger); - resetCalls(mockRequestHandler); - }); - - const managerInstance = () => new OdpSegmentApiManager(instance(mockRequestHandler), instance(mockLogger)); - - const abortableRequest = (statusCode: number, body: string) => { - return { - abort: () => {}, - responsePromise: Promise.resolve({ - statusCode, - body, - headers: {}, - }), - }; - }; - - it('should parse a successful response', () => { - const validJsonResponse = `{ - "data": { - "customer": { - "audiences": { - "edges": [ - { - "node": { - "name": "has_email", - "state": "qualified" - } - }, - { - "node": { - "name": "has_email_opted_in", - "state": "not-qualified" - } - } - ] - } - } - } - }`; - const manager = managerInstance(); - - const response = manager['parseSegmentsResponseJson'](validJsonResponse); - - expect(response).not.toBeUndefined(); - expect(response?.errors).toHaveLength(0); - expect(response?.data?.customer?.audiences?.edges).not.toBeNull(); - expect(response?.data.customer.audiences.edges).toHaveLength(2); - let node = response?.data.customer.audiences.edges[0].node; - expect(node?.name).toEqual('has_email'); - expect(node?.state).toEqual('qualified'); - node = response?.data.customer.audiences.edges[1].node; - expect(node?.name).toEqual('has_email_opted_in'); - expect(node?.state).not.toEqual('qualified'); - }); - - it('should parse an error response', () => { - const errorJsonResponse = `{ - "errors": [ - { - "message": "Exception while fetching data (/customer) : Exception: could not resolve _fs_user_id = mock_user_id", - "locations": [ - { - "line": 2, - "column": 3 - } - ], - "path": [ - "customer" - ], - "extensions": { - "classification": "InvalidIdentifierException" - } - } - ], - "data": { - "customer": null - } -}`; - const manager = managerInstance(); - - const response = manager['parseSegmentsResponseJson'](errorJsonResponse); - - expect(response).not.toBeUndefined(); - expect(response?.data.customer).toBeNull(); - expect(response?.errors).not.toBeNull(); - expect(response?.errors[0].extensions.classification).toEqual('InvalidIdentifierException'); - }); - - it('should construct a valid GraphQL query string', () => { - const manager = managerInstance(); - - const response = manager['toGraphQLJson'](USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(response).toBe( - `{"query" : "query {customer(${USER_KEY} : \\"${USER_VALUE}\\") {audiences(subset: [\\"has_email\\",\\"has_email_opted_in\\",\\"push_on_sale\\"]) {edges {node {name state}}}}}"}` - ); - }); - - it('should fetch valid qualified segments', async () => { - const responseJsonWithQualifiedSegments = - '{"data":{"customer":{"audiences":' + - '{"edges":[{"node":{"name":"has_email",' + - '"state":"qualified"}},{"node":{"name":' + - '"has_email_opted_in","state":"qualified"}}]}}}}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, responseJsonWithQualifiedSegments) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments?.length).toEqual(2); - expect(segments).toContain('has_email'); - expect(segments).toContain('has_email_opted_in'); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - 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, []); - - expect(segments?.length).toEqual(0); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('should handle empty qualified segments', async () => { - const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, responseJsonWithNoQualifiedSegments) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments?.length).toEqual(0); - verify(mockLogger.log(anything(), anyString())).never(); - }); - - it('should handle error with invalid identifier', async () => { - const INVALID_USER_ID = 'invalid-user'; - const errorJsonResponse = - '{"errors":[{"message":' + - '"Exception while fetching data (/customer) : ' + - `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + - '"locations":[{"line":1,"column":8}],"path":["customer"],' + - '"extensions":{"code": "INVALID_IDENTIFIER_EXCEPTION","classification":"DataFetchingException"}}],' + - '"data":{"customer":null}}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, errorJsonResponse) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments( - API_key, - GRAPHQL_ENDPOINT, - USER_KEY, - INVALID_USER_ID, - SEGMENTS_TO_CHECK - ); - - expect(segments).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (invalid identifier)')).once(); - }); - - it('should handle other fetch error responses', async () => { - const INVALID_USER_ID = 'invalid-user'; - const errorJsonResponse = - '{"errors":[{"message":' + - '"Exception while fetching data (/customer) : ' + - `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + - '"locations":[{"line":1,"column":8}],"path":["customer"],' + - '"extensions":{"classification":"DataFetchingException"}}],' + - '"data":{"customer":null}}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, errorJsonResponse) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments( - API_key, - GRAPHQL_ENDPOINT, - USER_KEY, - INVALID_USER_ID, - SEGMENTS_TO_CHECK - ); - - expect(segments).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (DataFetchingException)')).once(); - }); - - it('should handle unrecognized JSON responses', async () => { - const unrecognizedJson = '{"unExpectedObject":{ "withSome": "value", "thatIsNotParseable": "true" }}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, unrecognizedJson) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); - }); - - it('should handle other exception types', async () => { - const errorJsonResponse = - '{"errors":[{"message":"Validation error of type ' + - 'UnknownArgument: Unknown field argument not_real_userKey @ ' + - '\'customer\'","locations":[{"line":1,"column":17}],' + - '"extensions":{"classification":"ValidationError"}}]}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, errorJsonResponse) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments).toBeNull(); - verify(mockLogger.log(anything(), anyString())).once(); - }); - - it('should handle bad responses', async () => { - const badResponse = '{"data":{ }}'; - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(200, badResponse) - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (decode error)')).once(); - }); - - it('should handle non 200 HTTP status code response', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn( - abortableRequest(400, '') - ); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); - }); - - it('should handle a timeout', async () => { - when(mockRequestHandler.makeRequest(anything(), anything(), anything(), anything())).thenReturn({ - abort: () => {}, - responsePromise: Promise.reject(new Error('Request timed out')), - }); - const manager = managerInstance(); - - const segments = await manager.fetchSegments(API_key, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); - - expect(segments).toBeNull(); - verify(mockLogger.log(LogLevel.ERROR, 'Audience segments fetch failed (network error)')).once(); - }); -}); diff --git a/lib/odp/segment_manager/odp_segment_api_manager.spec.ts b/lib/odp/segment_manager/odp_segment_api_manager.spec.ts new file mode 100644 index 000000000..97e0262c1 --- /dev/null +++ b/lib/odp/segment_manager/odp_segment_api_manager.spec.ts @@ -0,0 +1,248 @@ +/** + * Copyright 2022-2024 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, beforeEach, beforeAll, it, expect } from 'vitest'; + +import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; +import { LogHandler, LogLevel } from '../../modules/logging'; +import { ODP_USER_KEY } from '../constant'; +import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; +import { getMockLogger } from '../../tests/mock/mock_logger'; +import { DefaultOdpSegmentApiManager } from './odp_segment_api_manager'; +import exp from 'constants'; + +const API_KEY = 'not-real-api-key'; +const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; +const USER_KEY = ODP_USER_KEY.FS_USER_ID; +const USER_VALUE = 'tester-101'; +const SEGMENTS_TO_CHECK = ['has_email', 'has_email_opted_in', 'push_on_sale']; + +describe('DefaultOdpSegmentApiManager', () => { + it('should return empty list without calling api when segmentsToCheck is empty', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: '' }), + }); + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, []); + + expect(segments).toEqual([]); + expect(requestHandler.makeRequest).not.toHaveBeenCalled(); + }); + + it('should return null and log error if requestHandler promise rejects', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.reject(new Error('Request timed out')), + }); + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should log error and return null in case of non 200 HTTP status code response', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 500, body: '' }), + }); + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should return null and log error if response body is invalid JSON', async () => { + const invalidJsonResponse = 'not-a-valid-json-response'; + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: invalidJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should return null and log error if response body is unrecognized JSON', async () => { + const invalidJsonResponse = '{"a":1}'; + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: invalidJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should log error and return null in case of invalid identifier error response', async () => { + const INVALID_USER_ID = 'invalid-user'; + const errorJsonResponse = + '{"errors":[{"message":' + + '"Exception while fetching data (/customer) : ' + + `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + + '"locations":[{"line":1,"column":8}],"path":["customer"],' + + '"extensions":{"code": "INVALID_IDENTIFIER_EXCEPTION","classification":"DataFetchingException"}}],' + + '"data":{"customer":null}}'; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: errorJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, 'mock_user_id', SEGMENTS_TO_CHECK); + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledWith('Audience segments fetch failed (invalid identifier)'); + }); + + it('should log error and return null in case of errors other than invalid identifier error', async () => { + const INVALID_USER_ID = 'invalid-user'; + const errorJsonResponse = + '{"errors":[{"message":' + + '"Exception while fetching data (/customer) : ' + + `Exception: could not resolve _fs_user_id = ${INVALID_USER_ID}",` + + '"locations":[{"line":1,"column":8}],"path":["customer"],' + + '"extensions":{"classification":"DataFetchingException"}}],' + + '"data":{"customer":null}}'; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: errorJsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, 'mock_user_id', SEGMENTS_TO_CHECK); + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledWith('Audience segments fetch failed (DataFetchingException)'); + }); + + it('should log error and return null in case of response with invalid falsy edges field', async () => { + const jsonResponse = `{ + "data": { + "customer": { + "audiences": { + } + } + } + }`; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: jsonResponse }), + }); + + const logger = getMockLogger(); + const manager = new DefaultOdpSegmentApiManager(requestHandler, logger); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toBeNull(); + expect(logger.error).toHaveBeenCalledOnce(); + }); + + it('should parse a success response and return qualified segments', async () => { + const validJsonResponse = `{ + "data": { + "customer": { + "audiences": { + "edges": [ + { + "node": { + "name": "has_email", + "state": "qualified" + } + }, + { + "node": { + "name": "has_email_opted_in", + "state": "not-qualified" + } + } + ] + } + } + } + }`; + + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: validJsonResponse }), + }); + + const manager = new DefaultOdpSegmentApiManager(requestHandler); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toEqual(['has_email']); + }); + + it('should handle empty qualified segments', async () => { + const responseJsonWithNoQualifiedSegments = '{"data":{"customer":{"audiences":' + '{"edges":[ ]}}}}'; + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: responseJsonWithNoQualifiedSegments }), + }); + + const manager = new DefaultOdpSegmentApiManager(requestHandler); + const segments = await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(segments).toEqual([]); + }); + + it('should construct a valid GraphQL query request', async () => { + const requestHandler = getMockRequestHandler(); + requestHandler.makeRequest.mockReturnValue({ + abort: () => {}, + responsePromise: Promise.resolve({ statusCode: 200, body: '' }), + }); + + const manager = new DefaultOdpSegmentApiManager(requestHandler); + await manager.fetchSegments(API_KEY, GRAPHQL_ENDPOINT, USER_KEY, USER_VALUE, SEGMENTS_TO_CHECK); + + expect(requestHandler.makeRequest).toHaveBeenCalledWith( + `${GRAPHQL_ENDPOINT}/v3/graphql`, + { + 'Content-Type': 'application/json', + 'x-api-key': API_KEY, + }, + 'POST', + `{"query" : "query {customer(${USER_KEY} : \\"${USER_VALUE}\\") {audiences(subset: [\\"has_email\\",\\"has_email_opted_in\\",\\"push_on_sale\\"]) {edges {node {name state}}}}}"}` + ); + }); +}); diff --git a/lib/odp/segment_manager/odp_segment_api_manager.ts b/lib/odp/segment_manager/odp_segment_api_manager.ts index a688bd2df..680a4d7b0 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LogHandler, LogLevel } from '../../modules/logging'; +import { LoggerFacade, LogLevel } from '../../modules/logging'; import { validate } from '../../utils/json_schema_validator'; import { OdpResponseSchema } from '../odp_response_schema'; import { ODP_USER_KEY } from '../constant'; @@ -51,14 +51,11 @@ export interface OdpSegmentApiManager { ): Promise; } -/** - * Concrete implementation for communicating with the ODP GraphQL endpoint - */ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { - private readonly logger: LogHandler; + private readonly logger?: LoggerFacade; private readonly requestHandler: RequestHandler; - constructor(requestHandler: RequestHandler, logger: LogHandler) { + constructor(requestHandler: RequestHandler, logger?: LoggerFacade) { this.requestHandler = requestHandler; this.logger = logger; } @@ -78,11 +75,6 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { userValue: string, segmentsToCheck: string[] ): Promise { - if (!apiKey || !apiHost) { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (Parameters apiKey or apiHost invalid)`); - return null; - } - if (segmentsToCheck?.length === 0) { return EMPTY_SEGMENTS_COLLECTION; } @@ -90,15 +82,15 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { const endpoint = `${apiHost}/v3/graphql`; const query = this.toGraphQLJson(userKey, userValue, segmentsToCheck); - const segmentsResponse = await this.querySegments(apiKey, endpoint, userKey, userValue, query); + const segmentsResponse = await this.querySegments(apiKey, endpoint, query); if (!segmentsResponse) { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (network error)`); return null; } const parsedSegments = this.parseSegmentsResponseJson(segmentsResponse); if (!parsedSegments) { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); return null; } @@ -106,9 +98,9 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { const { code, classification } = parsedSegments.errors[0].extensions; if (code == 'INVALID_IDENTIFIER_EXCEPTION') { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (invalid identifier)`); + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (invalid identifier)`); } else { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (${classification})`); + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (${classification})`); } return null; @@ -116,7 +108,7 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { const edges = parsedSegments?.data?.customer?.audiences?.edges; if (!edges) { - this.logger.log(LogLevel.ERROR, `${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); + this.logger?.error(`${AUDIENCE_FETCH_FAILURE_MESSAGE} (decode error)`); return null; } @@ -151,8 +143,6 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { private async querySegments( apiKey: string, endpoint: string, - userKey: string, - userValue: string, query: string ): Promise { const method = 'POST'; @@ -162,15 +152,16 @@ export class DefaultOdpSegmentApiManager implements OdpSegmentApiManager { 'x-api-key': apiKey, }; - let response: HttpResponse; try { const request = this.requestHandler.makeRequest(url, headers, method, query); - response = await request.responsePromise; + const { statusCode, body} = await request.responsePromise; + if (!(statusCode >= 200 && statusCode < 300)) { + return null; + } + return body; } catch { return null; } - - return response.body; } /** diff --git a/lib/odp/segment_manager/odp_segment_manager.spec.ts b/lib/odp/segment_manager/odp_segment_manager.spec.ts index 0c4b5168f..757aab52d 100644 --- a/lib/odp/segment_manager/odp_segment_manager.spec.ts +++ b/lib/odp/segment_manager/odp_segment_manager.spec.ts @@ -14,17 +14,15 @@ * limitations under the License. */ -import { describe, beforeEach, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; import { ODP_USER_KEY } from '../constant'; -import { DefaultOdpSegmentManager, OdpSegmentManager } from './odp_segment_manager'; +import { DefaultOdpSegmentManager } from './odp_segment_manager'; import { OdpConfig } from '../odp_config'; import { OptimizelySegmentOption } from './optimizely_segment_option'; -import { OdpSegmentApiManager } from './odp_segment_api_manager'; import { getMockLogger } from '../../tests/mock/mock_logger'; import { getMockSyncCache } from '../../tests/mock/mock_cache'; -import exp from 'constants'; const API_KEY = 'test-api-key'; const API_HOST = 'https://odp.example.com'; diff --git a/vitest.config.mts b/vitest.config.mts index 9732becfb..9b571c578 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_segment_manager.spec.ts'], + include: ['**/odp_segment_api_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 1b76ae71aec98f473b3f7d8a8af7990fdda0322d Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 3 Dec 2024 23:53:14 +0600 Subject: [PATCH 06/28] up --- lib/odp/odp_manager.spec.ts | 677 +++++++++++++++++ lib/odp/odp_manager.ts | 2 - .../odp_response_schema.ts | 0 .../odp_segment_api_manager.spec.ts | 5 +- .../odp_segment_api_manager.ts | 5 +- tests/odpManager.spec.ts | 698 ------------------ vitest.config.mts | 2 +- 7 files changed, 681 insertions(+), 708 deletions(-) create mode 100644 lib/odp/odp_manager.spec.ts rename lib/odp/{ => segment_manager}/odp_response_schema.ts (100%) delete mode 100644 tests/odpManager.spec.ts diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts new file mode 100644 index 000000000..d5690677e --- /dev/null +++ b/lib/odp/odp_manager.spec.ts @@ -0,0 +1,677 @@ +/** + * Copyright 2023-2024, 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, beforeEach, beforeAll, it, vi, expect } from 'vitest'; + +import { ERROR_MESSAGES } from '../utils/enums/index'; + +import { LogHandler, LogLevel } from '../modules/logging'; +import { RequestHandler } from '../utils/http_request_handler/http'; + +import { DefaultOdpManager, OdpManager } from './odp_manager'; +import { OdpConfig, OdpIntegratedConfig, OdpIntegrationConfig, OdpNotIntegratedConfig } from './odp_config'; +import { NodeOdpEventApiManager as OdpEventApiManager } from '../lib/odp/event_manager/event_api_manager.node'; +import { NodeOdpEventManager as OdpEventManager } from '../lib/odp/event_manager/event_manager.node'; +import { IOdpSegmentManager, OdpSegmentManager } from './segment_manager/odp_segment_manager'; +import { OdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; +import { IOdpEventManager } from '../shared_types'; +import { wait } from '../../tests/testUtils'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { on } from 'events'; +import { ServiceState } from '../service'; + +const keyA = 'key-a'; +const hostA = 'host-a'; +const pixelA = 'pixel-a'; +const segmentsA = ['a']; +const userA = 'fs-user-a'; + +const keyB = 'key-b'; +const hostB = 'host-b'; +const pixelB = 'pixel-b'; +const segmentsB = ['b']; +const userB = 'fs-user-b'; + +const getMockOdpEventManager = () => { + return { + start: vi.fn(), + stop: vi.fn(), + onRunning: vi.fn(), + onTerminated: vi.fn(), + getState: vi.fn(), + updateConfig: vi.fn(), + sendEvent: vi.fn(), + }; +}; + +const getMockOdpSegmentManager = () => { + return { + fetchQualifiedSegments: vi.fn(), + updateConfig: vi.fn(), + }; +}; + +describe('DefaultOdpManager', () => { + // let mockLogger: LogHandler; + // let mockRequestHandler: RequestHandler; + + // let odpConfig: OdpConfig; + // let logger: LogHandler; + // let defaultRequestHandler: RequestHandler; + + // let mockEventApiManager: OdpEventApiManager; + // let mockEventManager: OdpEventManager; + // let mockSegmentApiManager: OdpSegmentApiManager; + // let mockSegmentManager: OdpSegmentManager; + + // let eventApiManager: OdpEventApiManager; + // let eventManager: OdpEventManager; + // let segmentApiManager: OdpSegmentApiManager; + // let segmentManager: OdpSegmentManager; + + // beforeAll(() => { + // mockLogger = mock(); + // mockRequestHandler = mock(); + + // logger = instance(mockLogger); + // defaultRequestHandler = instance(mockRequestHandler); + + // mockEventApiManager = mock(); + // mockEventManager = mock(); + // mockSegmentApiManager = mock(); + // mockSegmentManager = mock(); + + // eventApiManager = instance(mockEventApiManager); + // eventManager = instance(mockEventManager); + // segmentApiManager = instance(mockSegmentApiManager); + // segmentManager = instance(mockSegmentManager); + // }); + + // beforeEach(() => { + // resetCalls(mockLogger); + // resetCalls(mockRequestHandler); + // resetCalls(mockEventApiManager); + // resetCalls(mockEventManager); + // resetCalls(mockSegmentManager); + // }); + + it('should be in new state on construction', () => { + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager: getMockOdpEventManager(), + }); + + expect(odpManager.getState()).toEqual(ServiceState.New); + }); + + // it('should be in stopped status and not ready if constructed without odpIntegrationConfig', () => { + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // }); + + // expect(odpManager.isReady()).toBe(false); + // expect(odpManager.getStatus()).toEqual(Status.Stopped); + // }); + + // it('should become ready only after odpIntegrationConfig is provided if vuid is not enabled', async () => { + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: false, + // }); + + // // should not be ready untill odpIntegrationConfig is provided + // await wait(500); + // expect(odpManager.isReady()).toBe(false); + + // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; + // odpManager.updateSettings(odpIntegrationConfig); + + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // }); + + // it('should become ready if odpIntegrationConfig is provided in constructor and then initialzeVuid', async () => { + // const vuidPromise = resolvablePromise(); + // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; + + // const vuidInitializer = () => { + // return vuidPromise.promise; + // } + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // vuidInitializer, + // }); + + // await wait(500); + // expect(odpManager.isReady()).toBe(false); + + // vuidPromise.resolve(); + + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // }); + + // it('should become ready after odpIntegrationConfig is provided using updateSettings() and then initialzeVuid finishes', async () => { + // const vuidPromise = resolvablePromise(); + + // const vuidInitializer = () => { + // return vuidPromise.promise; + // } + + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // vuidInitializer, + // }); + + + // expect(odpManager.isReady()).toBe(false); + + // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; + // odpManager.updateSettings(odpIntegrationConfig); + + // await wait(500); + // expect(odpManager.isReady()).toBe(false); + + // vuidPromise.resolve(); + + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // }); + + // it('should become ready after initialzeVuid finishes and then odpIntegrationConfig is provided using updateSettings()', async () => { + // const vuidPromise = resolvablePromise(); + + // const vuidInitializer = () => { + // return vuidPromise.promise; + // } + + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // vuidInitializer, + // }); + + // expect(odpManager.isReady()).toBe(false); + // vuidPromise.resolve(); + + // await wait(500); + // expect(odpManager.isReady()).toBe(false); + + // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; + // odpManager.updateSettings(odpIntegrationConfig); + + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // }); + + // it('should become ready and stay in stopped state and not start eventManager if OdpNotIntegrated config is provided', async () => { + // const vuidPromise = resolvablePromise(); + + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; + // odpManager.updateSettings(odpIntegrationConfig); + + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // expect(odpManager.getStatus()).toEqual(Status.Stopped); + // verify(mockEventManager.start()).never(); + // }); + + // it('should pass the integrated odp config given in constructor to eventManger and segmentManager', async () => { + // when(mockEventManager.updateSettings(anything())).thenReturn(undefined); + // when(mockSegmentManager.updateSettings(anything())).thenReturn(undefined); + + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // verify(mockEventManager.updateSettings(anything())).once(); + // const [eventOdpConfig] = capture(mockEventManager.updateSettings).first(); + // expect(eventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + + // verify(mockSegmentManager.updateSettings(anything())).once(); + // const [segmentOdpConfig] = capture(mockEventManager.updateSettings).first(); + // expect(segmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + // }); + + // it('should pass the integrated odp config given in updateSettings() to eventManger and segmentManager', async () => { + // when(mockEventManager.updateSettings(anything())).thenReturn(undefined); + // when(mockSegmentManager.updateSettings(anything())).thenReturn(undefined); + + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // odpManager.updateSettings(odpIntegrationConfig); + + // verify(mockEventManager.updateSettings(anything())).once(); + // const [eventOdpConfig] = capture(mockEventManager.updateSettings).first(); + // expect(eventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + + // verify(mockSegmentManager.updateSettings(anything())).once(); + // const [segmentOdpConfig] = capture(mockEventManager.updateSettings).first(); + // expect(segmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + // }); + + // it('should start if odp is integrated and start odpEventManger', async () => { + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // odpManager.updateSettings(odpIntegrationConfig); + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // expect(odpManager.getStatus()).toEqual(Status.Running); + // }); + + // it('should just update config when updateSettings is called in running state', async () => { + // const odpManager = testOdpManager({ + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // odpManager.updateSettings(odpIntegrationConfig); + + // await odpManager.onReady(); + // expect(odpManager.isReady()).toBe(true); + // expect(odpManager.getStatus()).toEqual(Status.Running); + + // const newOdpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyB, hostB, pixelB, segmentsB) + // }; + + // odpManager.updateSettings(newOdpIntegrationConfig); + + // verify(mockEventManager.start()).once(); + // verify(mockEventManager.stop()).never(); + // verify(mockEventManager.updateSettings(anything())).twice(); + // const [firstEventOdpConfig] = capture(mockEventManager.updateSettings).first(); + // expect(firstEventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + // const [secondEventOdpConfig] = capture(mockEventManager.updateSettings).second(); + // expect(secondEventOdpConfig.equals(newOdpIntegrationConfig.odpConfig)).toBe(true); + + // verify(mockSegmentManager.updateSettings(anything())).twice(); + // const [firstSegmentOdpConfig] = capture(mockEventManager.updateSettings).first(); + // expect(firstSegmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + // const [secondSegmentOdpConfig] = capture(mockEventManager.updateSettings).second(); + // expect(secondSegmentOdpConfig.equals(newOdpIntegrationConfig.odpConfig)).toBe(true); + // }); + + // it('should stop and stop eventManager if OdpNotIntegrated config is updated in running state', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // expect(odpManager.isReady()).toBe(true); + // expect(odpManager.getStatus()).toEqual(Status.Running); + + // const newOdpIntegrationConfig: OdpNotIntegratedConfig = { + // integrated: false, + // }; + + // odpManager.updateSettings(newOdpIntegrationConfig); + + // expect(odpManager.getStatus()).toEqual(Status.Stopped); + // verify(mockEventManager.stop()).once(); + // }); + + // it('should register vuid after becoming ready if odp is integrated', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // verify(mockEventManager.registerVuid(anything())).once(); + // }); + + // it('should call eventManager.identifyUser with correct parameters when identifyUser is called', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // const userId = 'user123'; + // const vuid = 'vuid_123'; + + // odpManager.identifyUser(userId, vuid); + // const [userIdArg, vuidArg] = capture(mockEventManager.identifyUser).byCallIndex(0); + // expect(userIdArg).toEqual(userId); + // expect(vuidArg).toEqual(vuid); + + // odpManager.identifyUser(userId); + // const [userIdArg2, vuidArg2] = capture(mockEventManager.identifyUser).byCallIndex(1); + // expect(userIdArg2).toEqual(userId); + // expect(vuidArg2).toEqual(undefined); + + // odpManager.identifyUser(vuid); + // const [userIdArg3, vuidArg3] = capture(mockEventManager.identifyUser).byCallIndex(2); + // expect(userIdArg3).toEqual(undefined); + // expect(vuidArg3).toEqual(vuid); + // }); + + // it('should send event with correct parameters', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // const identifiers = new Map([['email', 'a@b.com']]); + // const data = new Map([['key1', 'value1'], ['key2', 'value2']]); + + // odpManager.sendEvent({ + // action: 'action', + // type: 'type', + // identifiers, + // data, + // }); + + // const [event] = capture(mockEventManager.sendEvent).byCallIndex(0); + // expect(event.action).toEqual('action'); + // expect(event.type).toEqual('type'); + // expect(event.identifiers).toEqual(identifiers); + // expect(event.data).toEqual(data); + + // // should use `fullstack` as type if empty string is provided + // odpManager.sendEvent({ + // type: '', + // action: 'action', + // identifiers, + // data, + // }); + + // const [event2] = capture(mockEventManager.sendEvent).byCallIndex(1); + // expect(event2.action).toEqual('action'); + // expect(event2.type).toEqual('fullstack'); + // expect(event2.identifiers).toEqual(identifiers); + // }); + + + // it('should throw an error if event action is empty string and not call eventManager', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // const identifiers = new Map([['email', 'a@b.com']]); + // const data = new Map([['key1', 'value1'], ['key2', 'value2']]); + + // const sendEvent = () => odpManager.sendEvent({ + // action: '', + // type: 'type', + // identifiers, + // data, + // }); + + // expect(sendEvent).toThrow('ODP action is not valid'); + // verify(mockEventManager.sendEvent(anything())).never(); + // }); + + // it('should throw an error if event data is invalid', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // const identifiers = new Map([['email', 'a@b.com']]); + // const data = new Map([['key1', {}]]); + + // const sendEvent = () => odpManager.sendEvent({ + // action: 'action', + // type: 'type', + // identifiers, + // data, + // }); + + // expect(sendEvent).toThrow(ERROR_MESSAGES.ODP_INVALID_DATA); + // verify(mockEventManager.sendEvent(anything())).never(); + // }); + + // it('should fetch qualified segments correctly for both fs_user_id and vuid', async () => { + // const userId = 'user123'; + // const vuid = 'vuid_123'; + + // when(mockSegmentManager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, userId, anything())) + // .thenResolve(['fs1', 'fs2']); + + // when(mockSegmentManager.fetchQualifiedSegments(ODP_USER_KEY.VUID, vuid, anything())) + // .thenResolve(['vuid1', 'vuid2']); + + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager: instance(mockSegmentManager), + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // const fsSegments = await odpManager.fetchQualifiedSegments(userId); + // expect(fsSegments).toEqual(['fs1', 'fs2']); + + // const vuidSegments = await odpManager.fetchQualifiedSegments(vuid); + // expect(vuidSegments).toEqual(['vuid1', 'vuid2']); + // }); + + + // it('should stop itself and eventManager if stop is called', async () => { + // const odpIntegrationConfig: OdpIntegratedConfig = { + // integrated: true, + // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) + // }; + + // const odpManager = testOdpManager({ + // odpIntegrationConfig, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // odpManager.stop(); + + // expect(odpManager.getStatus()).toEqual(Status.Stopped); + // verify(mockEventManager.stop()).once(); + // }); + + + + // it('should drop relevant calls and log error when odpIntegrationConfig is not available', async () => { + // const odpManager = testOdpManager({ + // odpIntegrationConfig: undefined, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // const segments = await odpManager.fetchQualifiedSegments('vuid_user1', []); + // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).once(); + // expect(segments).toBeNull(); + + // odpManager.identifyUser('vuid_user1'); + // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).twice(); + // verify(mockEventManager.identifyUser(anything(), anything())).never(); + + // const identifiers = new Map([['email', 'a@b.com']]); + // const data = new Map([['key1', {}]]); + + // odpManager.sendEvent({ + // action: 'action', + // type: 'type', + // identifiers, + // data, + // }); + + // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).thrice(); + // verify(mockEventManager.sendEvent(anything())).never(); + + // }); + + // it('should drop relevant calls and log error when odp is not integrated', async () => { + // const odpManager = testOdpManager({ + // odpIntegrationConfig: { integrated: false }, + // segmentManager, + // eventManager, + // logger, + // vuidEnabled: true, + // }); + + // await odpManager.onReady(); + + // const segments = await odpManager.fetchQualifiedSegments('vuid_user1', []); + // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).once(); + // expect(segments).toBeNull(); + + // odpManager.identifyUser('vuid_user1'); + // verify(mockLogger.log(LogLevel.INFO, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).once(); + // verify(mockEventManager.identifyUser(anything(), anything())).never(); + + // const identifiers = new Map([['email', 'a@b.com']]); + // const data = new Map([['key1', {}]]); + + // odpManager.sendEvent({ + // action: 'action', + // type: 'type', + // identifiers, + // data, + // }); + + // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).twice(); + // verify(mockEventManager.sendEvent(anything())).never(); + // }); +}); + diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 254546513..2ae42f18c 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -125,12 +125,10 @@ export class DefaultOdpManager extends BaseService implements OdpManager { this.state = ServiceState.Starting; - this.segmentManager.start(); this.eventManager.start(); const startDependencies = [ this.configPromise, - this.segmentManager.onRunning(), this.eventManager.onRunning(), ]; diff --git a/lib/odp/odp_response_schema.ts b/lib/odp/segment_manager/odp_response_schema.ts similarity index 100% rename from lib/odp/odp_response_schema.ts rename to lib/odp/segment_manager/odp_response_schema.ts diff --git a/lib/odp/segment_manager/odp_segment_api_manager.spec.ts b/lib/odp/segment_manager/odp_segment_api_manager.spec.ts index 97e0262c1..52237add9 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.spec.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.spec.ts @@ -14,15 +14,12 @@ * limitations under the License. */ -import { describe, beforeEach, beforeAll, it, expect } from 'vitest'; +import { describe, it, expect } from 'vitest'; -import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; -import { LogHandler, LogLevel } from '../../modules/logging'; import { ODP_USER_KEY } from '../constant'; import { getMockRequestHandler } from '../../tests/mock/mock_request_handler'; import { getMockLogger } from '../../tests/mock/mock_logger'; import { DefaultOdpSegmentApiManager } from './odp_segment_api_manager'; -import exp from 'constants'; const API_KEY = 'not-real-api-key'; const GRAPHQL_ENDPOINT = 'https://some.example.com/graphql/endpoint'; diff --git a/lib/odp/segment_manager/odp_segment_api_manager.ts b/lib/odp/segment_manager/odp_segment_api_manager.ts index 680a4d7b0..af316ea75 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -16,11 +16,10 @@ import { LoggerFacade, LogLevel } from '../../modules/logging'; import { validate } from '../../utils/json_schema_validator'; -import { OdpResponseSchema } from '../odp_response_schema'; +import { OdpResponseSchema } from './odp_response_schema'; import { ODP_USER_KEY } from '../constant'; -import { RequestHandler, Response as HttpResponse } from '../../utils/http_request_handler/http'; +import { RequestHandler } from '../../utils/http_request_handler/http'; import { Response as GraphQLResponse } from '../odp_types'; - /** * Expected value for a qualified/valid segment */ diff --git a/tests/odpManager.spec.ts b/tests/odpManager.spec.ts deleted file mode 100644 index 96f69b353..000000000 --- a/tests/odpManager.spec.ts +++ /dev/null @@ -1,698 +0,0 @@ -/** - * Copyright 2023-2024, 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, beforeEach, beforeAll, it, vi, expect } from 'vitest'; - -import { anything, capture, instance, mock, resetCalls, verify, when } from 'ts-mockito'; - -import { ERROR_MESSAGES, ODP_USER_KEY } from './../lib/utils/enums/index'; - -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { RequestHandler } from '../lib/utils/http_request_handler/http'; - -import { OdpManager, Status } from '../lib/odp/odp_manager'; -import { OdpConfig, OdpIntegratedConfig, OdpIntegrationConfig, OdpNotIntegratedConfig } from '../lib/odp/odp_config'; -import { NodeOdpEventApiManager as OdpEventApiManager } from '../lib/odp/event_manager/event_api_manager.node'; -import { NodeOdpEventManager as OdpEventManager } from '../lib/odp/event_manager/event_manager.node'; -import { IOdpSegmentManager, OdpSegmentManager } from './../lib/odp/segment_manager/odp_segment_manager'; -import { OdpSegmentApiManager } from '../lib/odp/segment_manager/odp_segment_api_manager'; -import { IOdpEventManager } from '../lib/shared_types'; -import { wait } from './testUtils'; -import { resolvablePromise } from '../lib/utils/promise/resolvablePromise'; - -const keyA = 'key-a'; -const hostA = 'host-a'; -const pixelA = 'pixel-a'; -const segmentsA = ['a']; -const userA = 'fs-user-a'; - -const keyB = 'key-b'; -const hostB = 'host-b'; -const pixelB = 'pixel-b'; -const segmentsB = ['b']; -const userB = 'fs-user-b'; - -const testOdpManager = ({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled, - vuid, - vuidInitializer, -}: { - odpIntegrationConfig?: OdpIntegrationConfig; - segmentManager: IOdpSegmentManager; - eventManager: IOdpEventManager; - logger: LogHandler; - vuidEnabled?: boolean; - vuid?: string; - vuidInitializer?: () => Promise; -}): OdpManager => { - class TestOdpManager extends OdpManager{ - constructor() { - super({ odpIntegrationConfig, segmentManager, eventManager, logger }); - } - isVuidEnabled(): boolean { - return vuidEnabled ?? false; - } - getVuid(): string { - return vuid ?? 'vuid_123'; - } - protected initializeVuid(): Promise { - return vuidInitializer?.() ?? Promise.resolve(); - } - } - return new TestOdpManager(); -} - -describe('OdpManager', () => { - let mockLogger: LogHandler; - let mockRequestHandler: RequestHandler; - - let odpConfig: OdpConfig; - let logger: LogHandler; - let defaultRequestHandler: RequestHandler; - - let mockEventApiManager: OdpEventApiManager; - let mockEventManager: OdpEventManager; - let mockSegmentApiManager: OdpSegmentApiManager; - let mockSegmentManager: OdpSegmentManager; - - let eventApiManager: OdpEventApiManager; - let eventManager: OdpEventManager; - let segmentApiManager: OdpSegmentApiManager; - let segmentManager: OdpSegmentManager; - - beforeAll(() => { - mockLogger = mock(); - mockRequestHandler = mock(); - - logger = instance(mockLogger); - defaultRequestHandler = instance(mockRequestHandler); - - mockEventApiManager = mock(); - mockEventManager = mock(); - mockSegmentApiManager = mock(); - mockSegmentManager = mock(); - - eventApiManager = instance(mockEventApiManager); - eventManager = instance(mockEventManager); - segmentApiManager = instance(mockSegmentApiManager); - segmentManager = instance(mockSegmentManager); - }); - - beforeEach(() => { - resetCalls(mockLogger); - resetCalls(mockRequestHandler); - resetCalls(mockEventApiManager); - resetCalls(mockEventManager); - resetCalls(mockSegmentManager); - }); - - - it('should be in stopped status and not ready if constructed without odpIntegrationConfig', () => { - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - }); - - expect(odpManager.isReady()).toBe(false); - expect(odpManager.getStatus()).toEqual(Status.Stopped); - }); - - it('should call initialzeVuid on construction if vuid is enabled', () => { - const vuidInitializer = vi.fn(); - - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - vuidInitializer: vuidInitializer, - }); - - expect(vuidInitializer).toHaveBeenCalledTimes(1); - }); - - it('should become ready only after odpIntegrationConfig is provided if vuid is not enabled', async () => { - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: false, - }); - - // should not be ready untill odpIntegrationConfig is provided - await wait(500); - expect(odpManager.isReady()).toBe(false); - - const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - odpManager.updateSettings(odpIntegrationConfig); - - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - }); - - it('should become ready if odpIntegrationConfig is provided in constructor and then initialzeVuid', async () => { - const vuidPromise = resolvablePromise(); - const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - - const vuidInitializer = () => { - return vuidPromise.promise; - } - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - vuidInitializer, - }); - - await wait(500); - expect(odpManager.isReady()).toBe(false); - - vuidPromise.resolve(); - - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - }); - - it('should become ready after odpIntegrationConfig is provided using updateSettings() and then initialzeVuid finishes', async () => { - const vuidPromise = resolvablePromise(); - - const vuidInitializer = () => { - return vuidPromise.promise; - } - - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - vuidInitializer, - }); - - - expect(odpManager.isReady()).toBe(false); - - const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - odpManager.updateSettings(odpIntegrationConfig); - - await wait(500); - expect(odpManager.isReady()).toBe(false); - - vuidPromise.resolve(); - - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - }); - - it('should become ready after initialzeVuid finishes and then odpIntegrationConfig is provided using updateSettings()', async () => { - const vuidPromise = resolvablePromise(); - - const vuidInitializer = () => { - return vuidPromise.promise; - } - - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - vuidInitializer, - }); - - expect(odpManager.isReady()).toBe(false); - vuidPromise.resolve(); - - await wait(500); - expect(odpManager.isReady()).toBe(false); - - const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - odpManager.updateSettings(odpIntegrationConfig); - - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - }); - - it('should become ready and stay in stopped state and not start eventManager if OdpNotIntegrated config is provided', async () => { - const vuidPromise = resolvablePromise(); - - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - odpManager.updateSettings(odpIntegrationConfig); - - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - expect(odpManager.getStatus()).toEqual(Status.Stopped); - verify(mockEventManager.start()).never(); - }); - - it('should pass the integrated odp config given in constructor to eventManger and segmentManager', async () => { - when(mockEventManager.updateSettings(anything())).thenReturn(undefined); - when(mockSegmentManager.updateSettings(anything())).thenReturn(undefined); - - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - verify(mockEventManager.updateSettings(anything())).once(); - const [eventOdpConfig] = capture(mockEventManager.updateSettings).first(); - expect(eventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - - verify(mockSegmentManager.updateSettings(anything())).once(); - const [segmentOdpConfig] = capture(mockEventManager.updateSettings).first(); - expect(segmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - }); - - it('should pass the integrated odp config given in updateSettings() to eventManger and segmentManager', async () => { - when(mockEventManager.updateSettings(anything())).thenReturn(undefined); - when(mockSegmentManager.updateSettings(anything())).thenReturn(undefined); - - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - odpManager.updateSettings(odpIntegrationConfig); - - verify(mockEventManager.updateSettings(anything())).once(); - const [eventOdpConfig] = capture(mockEventManager.updateSettings).first(); - expect(eventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - - verify(mockSegmentManager.updateSettings(anything())).once(); - const [segmentOdpConfig] = capture(mockEventManager.updateSettings).first(); - expect(segmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - }); - - it('should start if odp is integrated and start odpEventManger', async () => { - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - odpManager.updateSettings(odpIntegrationConfig); - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - expect(odpManager.getStatus()).toEqual(Status.Running); - }); - - it('should just update config when updateSettings is called in running state', async () => { - const odpManager = testOdpManager({ - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - odpManager.updateSettings(odpIntegrationConfig); - - await odpManager.onReady(); - expect(odpManager.isReady()).toBe(true); - expect(odpManager.getStatus()).toEqual(Status.Running); - - const newOdpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyB, hostB, pixelB, segmentsB) - }; - - odpManager.updateSettings(newOdpIntegrationConfig); - - verify(mockEventManager.start()).once(); - verify(mockEventManager.stop()).never(); - verify(mockEventManager.updateSettings(anything())).twice(); - const [firstEventOdpConfig] = capture(mockEventManager.updateSettings).first(); - expect(firstEventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - const [secondEventOdpConfig] = capture(mockEventManager.updateSettings).second(); - expect(secondEventOdpConfig.equals(newOdpIntegrationConfig.odpConfig)).toBe(true); - - verify(mockSegmentManager.updateSettings(anything())).twice(); - const [firstSegmentOdpConfig] = capture(mockEventManager.updateSettings).first(); - expect(firstSegmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - const [secondSegmentOdpConfig] = capture(mockEventManager.updateSettings).second(); - expect(secondSegmentOdpConfig.equals(newOdpIntegrationConfig.odpConfig)).toBe(true); - }); - - it('should stop and stop eventManager if OdpNotIntegrated config is updated in running state', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - expect(odpManager.isReady()).toBe(true); - expect(odpManager.getStatus()).toEqual(Status.Running); - - const newOdpIntegrationConfig: OdpNotIntegratedConfig = { - integrated: false, - }; - - odpManager.updateSettings(newOdpIntegrationConfig); - - expect(odpManager.getStatus()).toEqual(Status.Stopped); - verify(mockEventManager.stop()).once(); - }); - - it('should register vuid after becoming ready if odp is integrated', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - verify(mockEventManager.registerVuid(anything())).once(); - }); - - it('should call eventManager.identifyUser with correct parameters when identifyUser is called', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - const userId = 'user123'; - const vuid = 'vuid_123'; - - odpManager.identifyUser(userId, vuid); - const [userIdArg, vuidArg] = capture(mockEventManager.identifyUser).byCallIndex(0); - expect(userIdArg).toEqual(userId); - expect(vuidArg).toEqual(vuid); - - odpManager.identifyUser(userId); - const [userIdArg2, vuidArg2] = capture(mockEventManager.identifyUser).byCallIndex(1); - expect(userIdArg2).toEqual(userId); - expect(vuidArg2).toEqual(undefined); - - odpManager.identifyUser(vuid); - const [userIdArg3, vuidArg3] = capture(mockEventManager.identifyUser).byCallIndex(2); - expect(userIdArg3).toEqual(undefined); - expect(vuidArg3).toEqual(vuid); - }); - - it('should send event with correct parameters', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - const identifiers = new Map([['email', 'a@b.com']]); - const data = new Map([['key1', 'value1'], ['key2', 'value2']]); - - odpManager.sendEvent({ - action: 'action', - type: 'type', - identifiers, - data, - }); - - const [event] = capture(mockEventManager.sendEvent).byCallIndex(0); - expect(event.action).toEqual('action'); - expect(event.type).toEqual('type'); - expect(event.identifiers).toEqual(identifiers); - expect(event.data).toEqual(data); - - // should use `fullstack` as type if empty string is provided - odpManager.sendEvent({ - type: '', - action: 'action', - identifiers, - data, - }); - - const [event2] = capture(mockEventManager.sendEvent).byCallIndex(1); - expect(event2.action).toEqual('action'); - expect(event2.type).toEqual('fullstack'); - expect(event2.identifiers).toEqual(identifiers); - }); - - - it('should throw an error if event action is empty string and not call eventManager', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - const identifiers = new Map([['email', 'a@b.com']]); - const data = new Map([['key1', 'value1'], ['key2', 'value2']]); - - const sendEvent = () => odpManager.sendEvent({ - action: '', - type: 'type', - identifiers, - data, - }); - - expect(sendEvent).toThrow('ODP action is not valid'); - verify(mockEventManager.sendEvent(anything())).never(); - }); - - it('should throw an error if event data is invalid', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - const identifiers = new Map([['email', 'a@b.com']]); - const data = new Map([['key1', {}]]); - - const sendEvent = () => odpManager.sendEvent({ - action: 'action', - type: 'type', - identifiers, - data, - }); - - expect(sendEvent).toThrow(ERROR_MESSAGES.ODP_INVALID_DATA); - verify(mockEventManager.sendEvent(anything())).never(); - }); - - it('should fetch qualified segments correctly for both fs_user_id and vuid', async () => { - const userId = 'user123'; - const vuid = 'vuid_123'; - - when(mockSegmentManager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, userId, anything())) - .thenResolve(['fs1', 'fs2']); - - when(mockSegmentManager.fetchQualifiedSegments(ODP_USER_KEY.VUID, vuid, anything())) - .thenResolve(['vuid1', 'vuid2']); - - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager: instance(mockSegmentManager), - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - const fsSegments = await odpManager.fetchQualifiedSegments(userId); - expect(fsSegments).toEqual(['fs1', 'fs2']); - - const vuidSegments = await odpManager.fetchQualifiedSegments(vuid); - expect(vuidSegments).toEqual(['vuid1', 'vuid2']); - }); - - - it('should stop itself and eventManager if stop is called', async () => { - const odpIntegrationConfig: OdpIntegratedConfig = { - integrated: true, - odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - }; - - const odpManager = testOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - odpManager.stop(); - - expect(odpManager.getStatus()).toEqual(Status.Stopped); - verify(mockEventManager.stop()).once(); - }); - - - - it('should drop relevant calls and log error when odpIntegrationConfig is not available', async () => { - const odpManager = testOdpManager({ - odpIntegrationConfig: undefined, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - const segments = await odpManager.fetchQualifiedSegments('vuid_user1', []); - verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).once(); - expect(segments).toBeNull(); - - odpManager.identifyUser('vuid_user1'); - verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).twice(); - verify(mockEventManager.identifyUser(anything(), anything())).never(); - - const identifiers = new Map([['email', 'a@b.com']]); - const data = new Map([['key1', {}]]); - - odpManager.sendEvent({ - action: 'action', - type: 'type', - identifiers, - data, - }); - - verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).thrice(); - verify(mockEventManager.sendEvent(anything())).never(); - - }); - - it('should drop relevant calls and log error when odp is not integrated', async () => { - const odpManager = testOdpManager({ - odpIntegrationConfig: { integrated: false }, - segmentManager, - eventManager, - logger, - vuidEnabled: true, - }); - - await odpManager.onReady(); - - const segments = await odpManager.fetchQualifiedSegments('vuid_user1', []); - verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).once(); - expect(segments).toBeNull(); - - odpManager.identifyUser('vuid_user1'); - verify(mockLogger.log(LogLevel.INFO, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).once(); - verify(mockEventManager.identifyUser(anything(), anything())).never(); - - const identifiers = new Map([['email', 'a@b.com']]); - const data = new Map([['key1', {}]]); - - odpManager.sendEvent({ - action: 'action', - type: 'type', - identifiers, - data, - }); - - verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).twice(); - verify(mockEventManager.sendEvent(anything())).never(); - }); -}); - diff --git a/vitest.config.mts b/vitest.config.mts index 9b571c578..f9b770b1e 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_segment_api_manager.spec.ts'], + include: ['**/odp_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 4a5f1edd42223d38b4dacb278db06bf9879161de Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 4 Dec 2024 13:00:11 +0600 Subject: [PATCH 07/28] odpman --- lib/odp/odp_manager.browser.ts | 1 - lib/odp/odp_manager.spec.ts | 662 ++++++++++++++++++++------------- lib/odp/odp_manager.ts | 37 +- lib/optimizely/index.ts | 2 +- 4 files changed, 422 insertions(+), 280 deletions(-) diff --git a/lib/odp/odp_manager.browser.ts b/lib/odp/odp_manager.browser.ts index 89eeacf71..ad9048ca3 100644 --- a/lib/odp/odp_manager.browser.ts +++ b/lib/odp/odp_manager.browser.ts @@ -18,7 +18,6 @@ import { CLIENT_VERSION, ERROR_MESSAGES, JAVASCRIPT_CLIENT_ENGINE, - ODP_USER_KEY, REQUEST_TIMEOUT_ODP_SEGMENTS_MS, REQUEST_TIMEOUT_ODP_EVENTS_MS, LOG_MESSAGES, diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts index d5690677e..1a9d63b8c 100644 --- a/lib/odp/odp_manager.spec.ts +++ b/lib/odp/odp_manager.spec.ts @@ -13,24 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, beforeEach, beforeAll, it, vi, expect } from 'vitest'; +import { describe, it, vi, expect } from 'vitest'; -import { ERROR_MESSAGES } from '../utils/enums/index'; -import { LogHandler, LogLevel } from '../modules/logging'; -import { RequestHandler } from '../utils/http_request_handler/http'; - -import { DefaultOdpManager, OdpManager } from './odp_manager'; -import { OdpConfig, OdpIntegratedConfig, OdpIntegrationConfig, OdpNotIntegratedConfig } from './odp_config'; -import { NodeOdpEventApiManager as OdpEventApiManager } from '../lib/odp/event_manager/event_api_manager.node'; -import { NodeOdpEventManager as OdpEventManager } from '../lib/odp/event_manager/event_manager.node'; -import { IOdpSegmentManager, OdpSegmentManager } from './segment_manager/odp_segment_manager'; -import { OdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; -import { IOdpEventManager } from '../shared_types'; -import { wait } from '../../tests/testUtils'; -import { resolvablePromise } from '../utils/promise/resolvablePromise'; -import { on } from 'events'; +import { DefaultOdpManager } from './odp_manager'; import { ServiceState } from '../service'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { OdpConfig } from './odp_config'; +import { exhaustMicrotasks } from '../tests/testUtils'; +import { ODP_USER_KEY } from './constant'; +import { OptimizelySegmentOption } from './segment_manager/optimizely_segment_option'; +import { OdpEventManager } from './event_manager/odp_event_manager'; +import exp from 'constants'; +import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; const keyA = 'key-a'; const hostA = 'host-a'; @@ -44,6 +39,9 @@ const pixelB = 'pixel-b'; const segmentsB = ['b']; const userB = 'fs-user-b'; +const config = new OdpConfig(keyA, hostA, pixelA, segmentsA); +const updatedConfig = new OdpConfig(keyB, hostB, pixelB, segmentsB); + const getMockOdpEventManager = () => { return { start: vi.fn(), @@ -116,300 +114,460 @@ describe('DefaultOdpManager', () => { expect(odpManager.getState()).toEqual(ServiceState.New); }); - // it('should be in stopped status and not ready if constructed without odpIntegrationConfig', () => { - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // }); + it('should be in starting state after start is called', () => { + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager: getMockOdpEventManager(), + }); - // expect(odpManager.isReady()).toBe(false); - // expect(odpManager.getStatus()).toEqual(Status.Stopped); - // }); + odpManager.start(); - // it('should become ready only after odpIntegrationConfig is provided if vuid is not enabled', async () => { - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: false, - // }); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + }); + + it('should start eventManager after start is called', () => { + const eventManager = getMockOdpEventManager(); - // // should not be ready untill odpIntegrationConfig is provided - // await wait(500); - // expect(odpManager.isReady()).toBe(false); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - // odpManager.updateSettings(odpIntegrationConfig); + odpManager.start(); + expect(eventManager.start).toHaveBeenCalled(); + }); - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // }); + it('should stay in starting state if updateConfig is called but eventManager is still not running', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(resolvablePromise().promise); - // it('should become ready if odpIntegrationConfig is provided in constructor and then initialzeVuid', async () => { - // const vuidPromise = resolvablePromise(); - // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // const vuidInitializer = () => { - // return vuidPromise.promise; - // } + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // vuidInitializer, - // }); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + }); - // await wait(500); - // expect(odpManager.isReady()).toBe(false); + it('should stay in starting state if eventManager is running but config is not yet available', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); - // vuidPromise.resolve(); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // }); + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); - // it('should become ready after odpIntegrationConfig is provided using updateSettings() and then initialzeVuid finishes', async () => { - // const vuidPromise = resolvablePromise(); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + }); - // const vuidInitializer = () => { - // return vuidPromise.promise; - // } + it('should go to running state and resolve onRunning() if updateConfig is called and eventManager is running', async () => { + const eventManager = getMockOdpEventManager(); + const eventManagerPromise = resolvablePromise(); + eventManager.onRunning.mockReturnValue(eventManagerPromise.promise); - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // vuidInitializer, - // }); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); - // expect(odpManager.isReady()).toBe(false); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await exhaustMicrotasks(); - // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - // odpManager.updateSettings(odpIntegrationConfig); - - // await wait(500); - // expect(odpManager.isReady()).toBe(false); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + eventManagerPromise.resolve(); - // vuidPromise.resolve(); + await expect(odpManager.onRunning()).resolves.not.toThrow(); + expect(odpManager.getState()).toEqual(ServiceState.Running); + }); - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // }); + it('should go to failed state and reject onRunning(), onTerminated() if updateConfig is called and eventManager fails to start', async () => { + const eventManager = getMockOdpEventManager(); + const eventManagerPromise = resolvablePromise(); + eventManager.onRunning.mockReturnValue(eventManagerPromise.promise); - // it('should become ready after initialzeVuid finishes and then odpIntegrationConfig is provided using updateSettings()', async () => { - // const vuidPromise = resolvablePromise(); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // const vuidInitializer = () => { - // return vuidPromise.promise; - // } + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // vuidInitializer, - // }); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await exhaustMicrotasks(); - // expect(odpManager.isReady()).toBe(false); - // vuidPromise.resolve(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); + eventManagerPromise.reject(new Error('Failed to start')); - // await wait(500); - // expect(odpManager.isReady()).toBe(false); + await expect(odpManager.onRunning()).rejects.toThrow(); + await expect(odpManager.onTerminated()).rejects.toThrow(); + expect(odpManager.getState()).toEqual(ServiceState.Failed); + }); - // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - // odpManager.updateSettings(odpIntegrationConfig); - - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // }); + it('should go to failed state and reject onRunning(), onTerminated() if eventManager fails to start before updateSettings()', async () => { + const eventManager = getMockOdpEventManager(); + const eventManagerPromise = resolvablePromise(); + eventManager.onRunning.mockReturnValue(eventManagerPromise.promise); - // it('should become ready and stay in stopped state and not start eventManager if OdpNotIntegrated config is provided', async () => { - // const vuidPromise = resolvablePromise(); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); - // const odpIntegrationConfig: OdpNotIntegratedConfig = { integrated: false }; - // odpManager.updateSettings(odpIntegrationConfig); - - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // expect(odpManager.getStatus()).toEqual(Status.Stopped); - // verify(mockEventManager.start()).never(); - // }); + eventManagerPromise.reject(new Error('Failed to start')); - // it('should pass the integrated odp config given in constructor to eventManger and segmentManager', async () => { - // when(mockEventManager.updateSettings(anything())).thenReturn(undefined); - // when(mockSegmentManager.updateSettings(anything())).thenReturn(undefined); + await expect(odpManager.onRunning()).rejects.toThrow(); + await expect(odpManager.onTerminated()).rejects.toThrow(); + expect(odpManager.getState()).toEqual(ServiceState.Failed); + }); - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + it('should pass the changed config to eventManager and segmentManager', async () => { + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); - - // verify(mockEventManager.updateSettings(anything())).once(); - // const [eventOdpConfig] = capture(mockEventManager.updateSettings).first(); - // expect(eventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + eventManager.onRunning.mockReturnValue(Promise.resolve()); - // verify(mockSegmentManager.updateSettings(anything())).once(); - // const [segmentOdpConfig] = capture(mockEventManager.updateSettings).first(); - // expect(segmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - // }); + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); - // it('should pass the integrated odp config given in updateSettings() to eventManger and segmentManager', async () => { - // when(mockEventManager.updateSettings(anything())).thenReturn(undefined); - // when(mockSegmentManager.updateSettings(anything())).thenReturn(undefined); + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + expect(eventManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); + expect(segmentManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + odpManager.updateConfig({ integrated: true, odpConfig: updatedConfig }); - // odpManager.updateSettings(odpIntegrationConfig); - - // verify(mockEventManager.updateSettings(anything())).once(); - // const [eventOdpConfig] = capture(mockEventManager.updateSettings).first(); - // expect(eventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); + expect(eventManager.updateConfig).toHaveBeenNthCalledWith(2, { integrated: true, odpConfig: updatedConfig }); + expect(segmentManager.updateConfig).toHaveBeenNthCalledWith(2, { integrated: true, odpConfig: updatedConfig }); + expect(eventManager.updateConfig).toHaveBeenCalledTimes(2); + expect(segmentManager.updateConfig).toHaveBeenCalledTimes(2); + }); - // verify(mockSegmentManager.updateSettings(anything())).once(); - // const [segmentOdpConfig] = capture(mockEventManager.updateSettings).first(); - // expect(segmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - // }); + it('should not call eventManager and segmentManager updateConfig if config does not change', async () => { + const eventManager = getMockOdpEventManager(); + const segmentManager = getMockOdpSegmentManager(); - // it('should start if odp is integrated and start odpEventManger', async () => { - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + eventManager.onRunning.mockReturnValue(Promise.resolve()); - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); - // odpManager.updateSettings(odpIntegrationConfig); - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // expect(odpManager.getStatus()).toEqual(Status.Running); - // }); + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // it('should just update config when updateSettings is called in running state', async () => { - // const odpManager = testOdpManager({ - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + expect(eventManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); + expect(segmentManager.updateConfig).toHaveBeenNthCalledWith(1, { integrated: true, odpConfig: config }); - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + odpManager.updateConfig({ integrated: true, odpConfig: JSON.parse(JSON.stringify(config)) }); - // odpManager.updateSettings(odpIntegrationConfig); + expect(eventManager.updateConfig).toHaveBeenCalledTimes(1); + expect(segmentManager.updateConfig).toHaveBeenCalledTimes(1); + }); - // await odpManager.onReady(); - // expect(odpManager.isReady()).toBe(true); - // expect(odpManager.getStatus()).toEqual(Status.Running); + it('fetches qualified segments correctly for both fs_user_id and vuid from segmentManager', async () => { + const segmentManager = getMockOdpSegmentManager(); + segmentManager.fetchQualifiedSegments.mockImplementation((key: ODP_USER_KEY, ...arg) => { + if (key === ODP_USER_KEY.FS_USER_ID) { + return Promise.resolve(['fs1', 'fs2']); + } + return Promise.resolve(['vuid1', 'vuid2']); + }); - // const newOdpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyB, hostB, pixelB, segmentsB) - // }; + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager: getMockOdpEventManager(), + }); - // odpManager.updateSettings(newOdpIntegrationConfig); - - // verify(mockEventManager.start()).once(); - // verify(mockEventManager.stop()).never(); - // verify(mockEventManager.updateSettings(anything())).twice(); - // const [firstEventOdpConfig] = capture(mockEventManager.updateSettings).first(); - // expect(firstEventOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - // const [secondEventOdpConfig] = capture(mockEventManager.updateSettings).second(); - // expect(secondEventOdpConfig.equals(newOdpIntegrationConfig.odpConfig)).toBe(true); - - // verify(mockSegmentManager.updateSettings(anything())).twice(); - // const [firstSegmentOdpConfig] = capture(mockEventManager.updateSettings).first(); - // expect(firstSegmentOdpConfig.equals(odpIntegrationConfig.odpConfig)).toBe(true); - // const [secondSegmentOdpConfig] = capture(mockEventManager.updateSettings).second(); - // expect(secondSegmentOdpConfig.equals(newOdpIntegrationConfig.odpConfig)).toBe(true); - // }); + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // it('should stop and stop eventManager if OdpNotIntegrated config is updated in running state', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + const fsSegments = await odpManager.fetchQualifiedSegments(userA); + expect(fsSegments).toEqual(['fs1', 'fs2']); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(1, ODP_USER_KEY.FS_USER_ID, userA, []); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + const vuidSegments = await odpManager.fetchQualifiedSegments('vuid_abcd'); + expect(vuidSegments).toEqual(['vuid1', 'vuid2']); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(2, ODP_USER_KEY.VUID, 'vuid_abcd', []); + }); - // await odpManager.onReady(); + it('returns null from fetchQualifiedSegments if segmentManger returns null', async () => { + const segmentManager = getMockOdpSegmentManager(); + segmentManager.fetchQualifiedSegments.mockResolvedValue(null); - // expect(odpManager.isReady()).toBe(true); - // expect(odpManager.getStatus()).toEqual(Status.Running); + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager: getMockOdpEventManager(), + }); - // const newOdpIntegrationConfig: OdpNotIntegratedConfig = { - // integrated: false, - // }; + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // odpManager.updateSettings(newOdpIntegrationConfig); + const fsSegments = await odpManager.fetchQualifiedSegments(userA); + expect(fsSegments).toBeNull(); - // expect(odpManager.getStatus()).toEqual(Status.Stopped); - // verify(mockEventManager.stop()).once(); - // }); + const vuidSegments = await odpManager.fetchQualifiedSegments('vuid_abcd'); + expect(vuidSegments).toBeNull(); + }); - // it('should register vuid after becoming ready if odp is integrated', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + it('passes options to segmentManager correctly', async () => { + const segmentManager = getMockOdpSegmentManager(); + segmentManager.fetchQualifiedSegments.mockResolvedValue(null); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager: getMockOdpEventManager(), + }); - // await odpManager.onReady(); - - // verify(mockEventManager.registerVuid(anything())).once(); - // }); + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const options = [OptimizelySegmentOption.IGNORE_CACHE, OptimizelySegmentOption.RESET_CACHE]; + await odpManager.fetchQualifiedSegments(userA, options); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(1, ODP_USER_KEY.FS_USER_ID, userA, options); + + await odpManager.fetchQualifiedSegments('vuid_abcd', options); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(2, ODP_USER_KEY.VUID, 'vuid_abcd', options); + + await odpManager.fetchQualifiedSegments(userA, [OptimizelySegmentOption.IGNORE_CACHE]); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith( + 3, ODP_USER_KEY.FS_USER_ID, userA, [OptimizelySegmentOption.IGNORE_CACHE]); + + await odpManager.fetchQualifiedSegments('vuid_abcd', []); + expect(segmentManager.fetchQualifiedSegments).toHaveBeenNthCalledWith(4, ODP_USER_KEY.VUID, 'vuid_abcd', []); + }); + + it('sends a client_intialized event with the vuid after becoming ready if setVuid is called and odp is integrated', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.setVuid('vuid_123'); + await exhaustMicrotasks(); + expect(eventManager.sendEvent).not.toHaveBeenCalled(); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + expect(mockSendEvents).toHaveBeenCalledOnce(); + + const { type, action, identifiers } = mockSendEvents.mock.calls[0][0]; + expect(type).toEqual('fullstack'); + expect(action).toEqual('client_initialized'); + expect(identifiers).toEqual(new Map([['vuid', 'vuid_123']])); + }); + + it('does not send a client_intialized event with the vuid after becoming ready if setVuid is called and odp is not integrated', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.setVuid('vuid_123'); + + await exhaustMicrotasks(); + expect(eventManager.sendEvent).not.toHaveBeenCalled(); + + odpManager.updateConfig({ integrated: false }); + await odpManager.onRunning(); + + await exhaustMicrotasks(); + expect(mockSendEvents).not.toHaveBeenCalled(); + }); + + it('includes the available vuid in events sent via sendEvent', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.setVuid('vuid_123'); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['email', 'a@b.com'], ['vuid', 'vuid_123']])); + }); + + it('does not override the vuid in events sent via sendEvent', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.setVuid('vuid_123'); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com'], ['vuid', 'vuid_456']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['email', 'a@b.com'], ['vuid', 'vuid_456']])); + }); + + it('augments the data with common data before sending the event', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { data } = mockSendEvents.mock.calls[0][0]; + expect(data.get('idempotence_id')).toBeDefined(); + expect(data.get('data_source_type')).toEqual('sdk'); + expect(data.get('data_source')).toEqual(JAVASCRIPT_CLIENT_ENGINE); + expect(data.get('data_source_version')).toEqual(CLIENT_VERSION); + expect(data.get('key1')).toEqual('value1'); + expect(data.get('key2')).toEqual('value2'); + }); + + it('uses the clientInfo provided by setClientInfo() when augmenting the data', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + odpManager.setClientInfo('client', 'version'); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { data } = mockSendEvents.mock.calls[0][0]; + expect(data.get('data_source')).toEqual('client'); + expect(data.get('data_source_version')).toEqual('version'); + }); + + it('augments the data with user agent data before sending the event if userAgentParser is provided ', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + userAgentParser: { + parseUserAgentInfo: () => ({ + os: { name: 'os', version: '1.0' }, + device: { type: 'phone', model: 'model' }, + }), + }, + }); + + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); + + const event = { + type: 'type', + action: 'action', + identifiers: new Map([['email', 'a@b.com']]), + data: new Map([['key1', 'value1'], ['key2', 'value2']]), + }; + + odpManager.sendEvent(event); + const { data } = mockSendEvents.mock.calls[0][0]; + expect(data.get('os')).toEqual('os'); + expect(data.get('os_version')).toEqual('1.0'); + expect(data.get('device_type')).toEqual('phone'); + expect(data.get('model')).toEqual('model'); + }); + // it('should call eventManager.identifyUser with correct parameters when identifyUser is called', async () => { // const odpIntegrationConfig: OdpIntegratedConfig = { // integrated: true, diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 2ae42f18c..1a8f6ea7a 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -25,12 +25,12 @@ import { OdpEvent } from './event_manager/odp_event'; import { resolvablePromise, ResolvablePromise } from '../utils/promise/resolvablePromise'; import { BaseService, Service, ServiceState } from '../service'; import { UserAgentParser } from './ua_parser/user_agent_parser'; -import { ERROR_MESSAGES } from '../utils/enums'; +import { CLIENT_VERSION, ERROR_MESSAGES, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; import { ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION, ODP_USER_KEY } from './constant'; import { isVuid } from '../vuid/vuid'; export interface OdpManager extends Service { - updateSettings(odpIntegrationConfig: OdpIntegrationConfig): boolean; + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean; fetchQualifiedSegments(userId: string, options?: Array): Promise; identifyUser(userId?: string, vuid?: string): void; sendEvent(event: OdpEvent): void; @@ -50,8 +50,8 @@ export class DefaultOdpManager extends BaseService implements OdpManager { private eventManager: OdpEventManager; private odpIntegrationConfig?: OdpIntegrationConfig; private vuid?: string; - private clientEngine?: string; - private clientVersion?: string; + private clientEngine = JAVASCRIPT_CLIENT_ENGINE; + private clientVersion = CLIENT_VERSION; private userAgentData?: Map; constructor(config: OdpManagerConfig) { @@ -138,10 +138,6 @@ export class DefaultOdpManager extends BaseService implements OdpManager { }).catch((err) => { this.handleStartFailure(err); }); - // this.segmentManager.updateSettings(this.odpIntegrationConfig.odpConfig); - // this.eventManager.updateSettings(this.odpIntegrationConfig.odpConfig); - // this.eventManager.start(); - // return Promise.resolve(); } private handleStartSuccess() { @@ -170,10 +166,7 @@ export class DefaultOdpManager extends BaseService implements OdpManager { // await this.eventManager.stop(); } - /** - * Provides a method to update ODP Manager's ODP Config - */ - updateSettings(odpIntegrationConfig: OdpIntegrationConfig): boolean { + updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean { // do nothing if config did not change if (this.odpIntegrationConfig && odpIntegrationsAreEqual(this.odpIntegrationConfig, odpIntegrationConfig)) { return false; @@ -203,16 +196,6 @@ export class DefaultOdpManager extends BaseService implements OdpManager { * @returns {Promise} A promise holding either a list of qualified segments or null. */ async fetchQualifiedSegments(userId: string, options: Array = []): Promise { - // if (!this.odpIntegrationConfig) { - // this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE); - // return null; - // } - - // if (!this.odpIntegrationConfig.integrated) { - // this.logger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED); - // return null; - // } - if (isVuid(userId)) { return this.segmentManager.fetchQualifiedSegments(ODP_USER_KEY.VUID, userId, options); } @@ -253,8 +236,8 @@ export class DefaultOdpManager extends BaseService implements OdpManager { data.set('idempotence_id', uuidV4()); data.set('data_source_type', 'sdk'); - data.set('data_source', this.clientEngine || ''); - data.set('data_source_version', this.clientVersion || ''); + data.set('data_source', this.clientEngine); + data.set('data_source_version', this.clientVersion); sourceData.forEach((value, key) => data.set(key, value)); return data; @@ -263,8 +246,10 @@ export class DefaultOdpManager extends BaseService implements OdpManager { setVuid(vuid: string): void { this.vuid = vuid; this.onRunning().then(() => { - const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.INITIALIZED); - this.sendEvent(event); + if (this.odpIntegrationConfig?.integrated) { + const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.INITIALIZED); + this.sendEvent(event); + } }); } } diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index d8a2157a0..a03f43728 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -1628,7 +1628,7 @@ export default class Optimizely implements Client { } if (this.odpManager) { - this.odpManager.updateSettings(projectConfig.odpIntegrationConfig); + this.odpManager.updateConfig(projectConfig.odpIntegrationConfig); } } From 9abba358099d4ef26f4713f832291f632077c7af Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 4 Dec 2024 21:52:05 +0600 Subject: [PATCH 08/28] odpman test --- lib/odp/odp_manager.spec.ts | 273 +++++++----------------------------- lib/odp/odp_manager.ts | 58 ++------ 2 files changed, 63 insertions(+), 268 deletions(-) diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts index 1a9d63b8c..6a4d60d9d 100644 --- a/lib/odp/odp_manager.spec.ts +++ b/lib/odp/odp_manager.spec.ts @@ -289,7 +289,7 @@ describe('DefaultOdpManager', () => { it('fetches qualified segments correctly for both fs_user_id and vuid from segmentManager', async () => { const segmentManager = getMockOdpSegmentManager(); - segmentManager.fetchQualifiedSegments.mockImplementation((key: ODP_USER_KEY, ...arg) => { + segmentManager.fetchQualifiedSegments.mockImplementation((key: ODP_USER_KEY) => { if (key === ODP_USER_KEY.FS_USER_ID) { return Promise.resolve(['fs1', 'fs2']); } @@ -567,180 +567,70 @@ describe('DefaultOdpManager', () => { expect(data.get('device_type')).toEqual('phone'); expect(data.get('model')).toEqual('model'); }); - - // it('should call eventManager.identifyUser with correct parameters when identifyUser is called', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; - - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); - - // await odpManager.onReady(); - - // const userId = 'user123'; - // const vuid = 'vuid_123'; - - // odpManager.identifyUser(userId, vuid); - // const [userIdArg, vuidArg] = capture(mockEventManager.identifyUser).byCallIndex(0); - // expect(userIdArg).toEqual(userId); - // expect(vuidArg).toEqual(vuid); - - // odpManager.identifyUser(userId); - // const [userIdArg2, vuidArg2] = capture(mockEventManager.identifyUser).byCallIndex(1); - // expect(userIdArg2).toEqual(userId); - // expect(vuidArg2).toEqual(undefined); - - // odpManager.identifyUser(vuid); - // const [userIdArg3, vuidArg3] = capture(mockEventManager.identifyUser).byCallIndex(2); - // expect(userIdArg3).toEqual(undefined); - // expect(vuidArg3).toEqual(vuid); - // }); - - // it('should send event with correct parameters', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; - - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); - - // await odpManager.onReady(); - - // const identifiers = new Map([['email', 'a@b.com']]); - // const data = new Map([['key1', 'value1'], ['key2', 'value2']]); - - // odpManager.sendEvent({ - // action: 'action', - // type: 'type', - // identifiers, - // data, - // }); - - // const [event] = capture(mockEventManager.sendEvent).byCallIndex(0); - // expect(event.action).toEqual('action'); - // expect(event.type).toEqual('type'); - // expect(event.identifiers).toEqual(identifiers); - // expect(event.data).toEqual(data); - - // // should use `fullstack` as type if empty string is provided - // odpManager.sendEvent({ - // type: '', - // action: 'action', - // identifiers, - // data, - // }); - // const [event2] = capture(mockEventManager.sendEvent).byCallIndex(1); - // expect(event2.action).toEqual('action'); - // expect(event2.type).toEqual('fullstack'); - // expect(event2.identifiers).toEqual(identifiers); - // }); - - - // it('should throw an error if event action is empty string and not call eventManager', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; - - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); - - // await odpManager.onReady(); - - // const identifiers = new Map([['email', 'a@b.com']]); - // const data = new Map([['key1', 'value1'], ['key2', 'value2']]); - - // const sendEvent = () => odpManager.sendEvent({ - // action: '', - // type: 'type', - // identifiers, - // data, - // }); + it('sends identified event with both fs_user_id and vuid if both parameters are provided', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); - // expect(sendEvent).toThrow('ODP action is not valid'); - // verify(mockEventManager.sendEvent(anything())).never(); - // }); + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); - // it('should throw an error if event data is invalid', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // await odpManager.onReady(); + odpManager.identifyUser('user', 'vuid_a'); + expect(mockSendEvents).toHaveBeenCalledOnce(); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['fs_user_id', 'user'], ['vuid', 'vuid_a']])); + }); - // const identifiers = new Map([['email', 'a@b.com']]); - // const data = new Map([['key1', {}]]); + it('sends identified event when called with just fs_user_id in first parameter', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); - // const sendEvent = () => odpManager.sendEvent({ - // action: 'action', - // type: 'type', - // identifiers, - // data, - // }); + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); - // expect(sendEvent).toThrow(ERROR_MESSAGES.ODP_INVALID_DATA); - // verify(mockEventManager.sendEvent(anything())).never(); - // }); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // it('should fetch qualified segments correctly for both fs_user_id and vuid', async () => { - // const userId = 'user123'; - // const vuid = 'vuid_123'; + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // when(mockSegmentManager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, userId, anything())) - // .thenResolve(['fs1', 'fs2']); + odpManager.identifyUser('user'); + expect(mockSendEvents).toHaveBeenCalledOnce(); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['fs_user_id', 'user']])); + }); - // when(mockSegmentManager.fetchQualifiedSegments(ODP_USER_KEY.VUID, vuid, anything())) - // .thenResolve(['vuid1', 'vuid2']); + it('sends identified event when called with just vuid in first parameter', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + const mockSendEvents = vi.mocked(eventManager.sendEvent as OdpEventManager['sendEvent']); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager: instance(mockSegmentManager), - // eventManager, - // logger, - // vuidEnabled: true, - // }); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // await odpManager.onReady(); + odpManager.start(); + odpManager.updateConfig({ integrated: true, odpConfig: config }); + await odpManager.onRunning(); - // const fsSegments = await odpManager.fetchQualifiedSegments(userId); - // expect(fsSegments).toEqual(['fs1', 'fs2']); + odpManager.identifyUser('vuid_a'); + expect(mockSendEvents).toHaveBeenCalledOnce(); + const { identifiers } = mockSendEvents.mock.calls[0][0]; + expect(identifiers).toEqual(new Map([['vuid', 'vuid_a']])); + }); - // const vuidSegments = await odpManager.fetchQualifiedSegments(vuid); - // expect(vuidSegments).toEqual(['vuid1', 'vuid2']); - // }); // it('should stop itself and eventManager if stop is called', async () => { @@ -766,70 +656,5 @@ describe('DefaultOdpManager', () => { // }); - - // it('should drop relevant calls and log error when odpIntegrationConfig is not available', async () => { - // const odpManager = testOdpManager({ - // odpIntegrationConfig: undefined, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); - - // const segments = await odpManager.fetchQualifiedSegments('vuid_user1', []); - // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).once(); - // expect(segments).toBeNull(); - - // odpManager.identifyUser('vuid_user1'); - // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).twice(); - // verify(mockEventManager.identifyUser(anything(), anything())).never(); - - // const identifiers = new Map([['email', 'a@b.com']]); - // const data = new Map([['key1', {}]]); - - // odpManager.sendEvent({ - // action: 'action', - // type: 'type', - // identifiers, - // data, - // }); - - // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_CONFIG_NOT_AVAILABLE)).thrice(); - // verify(mockEventManager.sendEvent(anything())).never(); - - // }); - - // it('should drop relevant calls and log error when odp is not integrated', async () => { - // const odpManager = testOdpManager({ - // odpIntegrationConfig: { integrated: false }, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); - - // await odpManager.onReady(); - - // const segments = await odpManager.fetchQualifiedSegments('vuid_user1', []); - // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).once(); - // expect(segments).toBeNull(); - - // odpManager.identifyUser('vuid_user1'); - // verify(mockLogger.log(LogLevel.INFO, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).once(); - // verify(mockEventManager.identifyUser(anything(), anything())).never(); - - // const identifiers = new Map([['email', 'a@b.com']]); - // const data = new Map([['key1', {}]]); - - // odpManager.sendEvent({ - // action: 'action', - // type: 'type', - // identifiers, - // data, - // }); - - // verify(mockLogger.log(LogLevel.ERROR, ERROR_MESSAGES.ODP_NOT_INTEGRATED)).twice(); - // verify(mockEventManager.sendEvent(anything())).never(); - // }); }); diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 1a8f6ea7a..62c1c4d6a 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -28,11 +28,12 @@ import { UserAgentParser } from './ua_parser/user_agent_parser'; import { CLIENT_VERSION, ERROR_MESSAGES, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; import { ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION, ODP_USER_KEY } from './constant'; import { isVuid } from '../vuid/vuid'; +import { Maybe } from '../utils/type'; export interface OdpManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean; fetchQualifiedSegments(userId: string, options?: Array): Promise; - identifyUser(userId?: string, vuid?: string): void; + identifyUser(userId: string, vuid?: string): void; sendEvent(event: OdpEvent): void; setClientInfo(clientEngine: string, clientVersion: string): void; } @@ -76,43 +77,8 @@ export class DefaultOdpManager extends BaseService implements OdpManager { Object.entries(userAgentInfo).filter(([_, value]) => value != null && value != undefined) ); } - - // const readinessDependencies: PromiseLike[] = [this.configPromise, this.on]; - - // if (this.isVuidEnabled()) { - // readinessDependencies.push(this.initializeVuid()); - // } - // this.initPromise = Promise.all(readinessDependencies); - - // this.onReady().then(() => { - // this.ready = true; - // if (this.isVuidEnabled() && this.status === Status.Running) { - // this.registerVuid(); - // } - // }); - - // if (odpIntegrationConfig) { - // this.updateSettings(odpIntegrationConfig); - // } } - // private async activate(): Promise { - // if (!this.odpIntegrationConfig) { - // return; - // } - - // if (!this.odpIntegrationConfig.integrated) { - // return; - // } - - // this.activityStatus = ActivityStatus.Activating; - - // this.segmentManager.updateSettings(this.odpIntegrationConfig.odpConfig); - // this.eventManager.updateSettings(this.odpIntegrationConfig.odpConfig); - // this.eventManager.start(); - // return Promise.resolve(); - // } - setClientInfo(clientEngine: string, clientVersion: string): void { this.clientEngine = clientEngine; this.clientVersion = clientVersion; @@ -203,19 +169,23 @@ export class DefaultOdpManager extends BaseService implements OdpManager { return this.segmentManager.fetchQualifiedSegments(ODP_USER_KEY.FS_USER_ID, userId, options); } - identifyUser(userId?: string, vuid?: string): void { + identifyUser(userId: string, vuid?: string): void { const identifiers = new Map(); - if (!userId && !vuid) { - this.logger?.error(ERROR_MESSAGES.ODP_SEND_EVENT_FAILED_UID_MISSING); - return; + + let finalUserId: Maybe = userId; + let finalVuid: Maybe = vuid; + + if (!vuid && isVuid(userId)) { + finalVuid = userId; + finalUserId = undefined; } - if (vuid) { - identifiers.set(ODP_USER_KEY.VUID, vuid); + if (finalVuid) { + identifiers.set(ODP_USER_KEY.VUID, finalVuid); } - if (userId) { - identifiers.set(ODP_USER_KEY.FS_USER_ID, userId); + if (finalUserId) { + identifiers.set(ODP_USER_KEY.FS_USER_ID, finalUserId); } const event = new OdpEvent(ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION.IDENTIFIED, identifiers); From bc4f408dfeb1a8f6c173441115502f1f34f4e711 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 5 Dec 2024 00:39:27 +0600 Subject: [PATCH 09/28] odp man stopped --- lib/odp/odp_manager.ts | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 62c1c4d6a..203529279 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -125,11 +125,26 @@ export class DefaultOdpManager extends BaseService implements OdpManager { } stop(): void { - // if (this.status === Status.Stopped) { - // return; - // } - // this.status = Status.Stopped; - // await this.eventManager.stop(); + if (this.isDone()) { + return; + } + + if (this.isNew()) { + this.startPromise.reject(new Error('odp manager stopped before starting')); + this.stopPromise.resolve(); + return; + } + + this.state = ServiceState.Stopping; + this.eventManager.stop(); + + this.eventManager.onTerminated().then(() => { + this.state = ServiceState.Terminated; + this.stopPromise.resolve(); + }).catch((err) => { + this.state = ServiceState.Failed; + this.stopPromise.reject(err); + }); } updateConfig(odpIntegrationConfig: OdpIntegrationConfig): boolean { From 3129c0640ddc245ad06523d4b2c95c2c05bcd88a Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 5 Dec 2024 16:59:56 +0600 Subject: [PATCH 10/28] odpman tests done --- lib/odp/odp_manager.spec.ts | 161 ++++++++++++++++++++++-------------- lib/odp/odp_manager.ts | 6 +- 2 files changed, 102 insertions(+), 65 deletions(-) diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts index 6a4d60d9d..2464bc28b 100644 --- a/lib/odp/odp_manager.spec.ts +++ b/lib/odp/odp_manager.spec.ts @@ -24,7 +24,6 @@ import { exhaustMicrotasks } from '../tests/testUtils'; import { ODP_USER_KEY } from './constant'; import { OptimizelySegmentOption } from './segment_manager/optimizely_segment_option'; import { OdpEventManager } from './event_manager/odp_event_manager'; -import exp from 'constants'; import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; const keyA = 'key-a'; @@ -62,49 +61,6 @@ const getMockOdpSegmentManager = () => { }; describe('DefaultOdpManager', () => { - // let mockLogger: LogHandler; - // let mockRequestHandler: RequestHandler; - - // let odpConfig: OdpConfig; - // let logger: LogHandler; - // let defaultRequestHandler: RequestHandler; - - // let mockEventApiManager: OdpEventApiManager; - // let mockEventManager: OdpEventManager; - // let mockSegmentApiManager: OdpSegmentApiManager; - // let mockSegmentManager: OdpSegmentManager; - - // let eventApiManager: OdpEventApiManager; - // let eventManager: OdpEventManager; - // let segmentApiManager: OdpSegmentApiManager; - // let segmentManager: OdpSegmentManager; - - // beforeAll(() => { - // mockLogger = mock(); - // mockRequestHandler = mock(); - - // logger = instance(mockLogger); - // defaultRequestHandler = instance(mockRequestHandler); - - // mockEventApiManager = mock(); - // mockEventManager = mock(); - // mockSegmentApiManager = mock(); - // mockSegmentManager = mock(); - - // eventApiManager = instance(mockEventApiManager); - // eventManager = instance(mockEventManager); - // segmentApiManager = instance(mockSegmentApiManager); - // segmentManager = instance(mockSegmentManager); - // }); - - // beforeEach(() => { - // resetCalls(mockLogger); - // resetCalls(mockRequestHandler); - // resetCalls(mockEventApiManager); - // resetCalls(mockEventManager); - // resetCalls(mockSegmentManager); - // }); - it('should be in new state on construction', () => { const odpManager = new DefaultOdpManager({ segmentManager: getMockOdpSegmentManager(), @@ -631,30 +587,113 @@ describe('DefaultOdpManager', () => { expect(identifiers).toEqual(new Map([['vuid', 'vuid_a']])); }); + it('should reject onRunning() if stopped in new state', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.stop(); + + await expect(odpManager.onRunning()).rejects.toThrow(); + }); + + it('should reject onRunning() if stopped in starting state', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(Promise.resolve()); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + odpManager.start(); + expect(odpManager.getState()).toEqual(ServiceState.Starting); - // it('should stop itself and eventManager if stop is called', async () => { - // const odpIntegrationConfig: OdpIntegratedConfig = { - // integrated: true, - // odpConfig: new OdpConfig(keyA, hostA, pixelA, segmentsA) - // }; + odpManager.stop(); + await expect(odpManager.onRunning()).rejects.toThrow(); + }); + + it('should go to stopping state and wait for eventManager to stop if stop is called', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(resolvablePromise().promise); - // const odpManager = testOdpManager({ - // odpIntegrationConfig, - // segmentManager, - // eventManager, - // logger, - // vuidEnabled: true, - // }); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); - // await odpManager.onReady(); + odpManager.start(); + odpManager.stop(); - // odpManager.stop(); + const terminatedHandler = vi.fn(); + odpManager.onTerminated().then(terminatedHandler); - // expect(odpManager.getStatus()).toEqual(Status.Stopped); - // verify(mockEventManager.stop()).once(); - // }); + expect(odpManager.getState()).toEqual(ServiceState.Stopping); + await exhaustMicrotasks(); + expect(terminatedHandler).not.toHaveBeenCalled(); + }); + it('should stop eventManager if stop is called', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + eventManager.onTerminated.mockReturnValue(Promise.resolve()); + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + + odpManager.stop(); + expect(eventManager.stop).toHaveBeenCalled(); + }); + + it('should resolve onTerminated after eventManager stops successfully', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + const eventManagerTerminatedPromise = resolvablePromise(); + eventManager.onTerminated.mockReturnValue(eventManagerTerminatedPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.stop(); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Stopping); + + eventManagerTerminatedPromise.resolve(); + await expect(odpManager.onTerminated()).resolves.not.toThrow(); + }); + + it('should reject onTerminated after eventManager fails to stop correctly', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockReturnValue(Promise.resolve()); + const eventManagerTerminatedPromise = resolvablePromise(); + eventManager.onTerminated.mockReturnValue(eventManagerTerminatedPromise.promise); + + const odpManager = new DefaultOdpManager({ + segmentManager: getMockOdpSegmentManager(), + eventManager, + }); + + odpManager.start(); + odpManager.stop(); + await exhaustMicrotasks(); + expect(odpManager.getState()).toEqual(ServiceState.Stopping); + + eventManagerTerminatedPromise.reject(new Error('Failed to stop')); + await expect(odpManager.onTerminated()).rejects.toThrow(); + }); }); diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 203529279..9388cd8eb 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -129,10 +129,8 @@ export class DefaultOdpManager extends BaseService implements OdpManager { return; } - if (this.isNew()) { - this.startPromise.reject(new Error('odp manager stopped before starting')); - this.stopPromise.resolve(); - return; + if (!this.isRunning()) { + this.startPromise.reject(new Error('odp manager stopped before running')); } this.state = ServiceState.Stopping; From 51e78858e52e1d03241f08a97723458e26b23626 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Thu, 5 Dec 2024 20:00:11 +0600 Subject: [PATCH 11/28] lru cache tests --- lib/tests/testUtils.ts | 2 + lib/utils/cache/in_memory_lru_cache.spec.ts | 131 +++++++++ lib/utils/cache/in_memory_lru_cache.ts | 5 - lib/utils/lru_cache/lru_cache.tests.ts | 309 -------------------- vitest.config.mts | 2 +- 5 files changed, 134 insertions(+), 315 deletions(-) create mode 100644 lib/utils/cache/in_memory_lru_cache.spec.ts delete mode 100644 lib/utils/lru_cache/lru_cache.tests.ts diff --git a/lib/tests/testUtils.ts b/lib/tests/testUtils.ts index b24f10517..7d6e86dc4 100644 --- a/lib/tests/testUtils.ts +++ b/lib/tests/testUtils.ts @@ -3,3 +3,5 @@ export const exhaustMicrotasks = async (loop = 100) => { await Promise.resolve(); } }; + +export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file diff --git a/lib/utils/cache/in_memory_lru_cache.spec.ts b/lib/utils/cache/in_memory_lru_cache.spec.ts new file mode 100644 index 000000000..2820dc438 --- /dev/null +++ b/lib/utils/cache/in_memory_lru_cache.spec.ts @@ -0,0 +1,131 @@ +/** + * Copyright 2024, 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 { expect, describe, it } from 'vitest'; +import { InMemoryLruCache } from './in_memory_lru_cache'; +import { wait } from '../../tests/testUtils'; + +describe('InMemoryLruCache', () => { + it('should save and get values correctly', () => { + const cache = new InMemoryLruCache(2); + cache.set('a', 1); + cache.set('b', 2); + expect(cache.get('a')).toBe(1); + expect(cache.get('b')).toBe(2); + }); + + it('should return undefined for non-existent keys', () => { + const cache = new InMemoryLruCache(2); + expect(cache.get('a')).toBe(undefined); + }); + + it('should return all keys in cache when getKeys is called', () => { + const cache = new InMemoryLruCache(20); + cache.set('a', 1); + cache.set('b', 2); + cache.set('c', 3); + cache.set('d', 4); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['d', 'c', 'b', 'a'])); + }); + + it('should evict least recently used keys when full', () => { + const cache = new InMemoryLruCache(3); + cache.set('a', 1); + cache.set('b', 2); + cache.set('c', 3); + + expect(cache.get('b')).toBe(2); + expect(cache.get('c')).toBe(3); + expect(cache.get('a')).toBe(1); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['a', 'c', 'b'])); + + // key use order is now a c b. next insert should evict b + cache.set('d', 4); + expect(cache.get('b')).toBe(undefined); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['d', 'a', 'c'])); + + // key use order is now d a c. setting c should put it at the front + cache.set('c', 5); + + // key use order is now c d a. next insert should evict a + cache.set('e', 6); + expect(cache.get('a')).toBe(undefined); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['e', 'c', 'd'])); + + // key use order is now e c d. reading d should put it at the front + expect(cache.get('d')).toBe(4); + + // key use order is now d e c. next insert should evict c + cache.set('f', 7); + expect(cache.get('c')).toBe(undefined); + expect(cache.getKeys()).toEqual(expect.arrayContaining(['f', 'd', 'e'])); + }); + + it('should not return expired values when get is called', async () => { + const cache = new InMemoryLruCache(2, 100); + cache.set('a', 1); + cache.set('b', 2); + expect(cache.get('a')).toBe(1); + expect(cache.get('b')).toBe(2); + + await wait(150); + expect(cache.get('a')).toBe(undefined); + expect(cache.get('b')).toBe(undefined); + }); + + it('should remove values correctly', () => { + const cache = new InMemoryLruCache(2); + cache.set('a', 1); + cache.set('b', 2); + cache.set('c', 3); + cache.remove('a'); + expect(cache.get('a')).toBe(undefined); + expect(cache.get('b')).toBe(2); + expect(cache.get('c')).toBe(3); + }); + + it('should clear all values correctly', () => { + const cache = new InMemoryLruCache(2); + cache.set('a', 1); + cache.set('b', 2); + cache.clear(); + expect(cache.get('a')).toBe(undefined); + expect(cache.get('b')).toBe(undefined); + }); + + it('should return correct values when getBatched is called', () => { + const cache = new InMemoryLruCache(2); + cache.set('a', 1); + cache.set('b', 2); + expect(cache.getBatched(['a', 'b', 'c'])).toEqual([1, 2, undefined]); + }); + + it('should return correct values when getBatched is called', () => { + const cache = new InMemoryLruCache(2); + cache.set('a', 1); + cache.set('b', 2); + expect(cache.getBatched(['a', 'b', 'c'])).toEqual([1, 2, undefined]); + }); + + it('should not return expired values when getBatched is called', async () => { + const cache = new InMemoryLruCache(2, 100); + cache.set('a', 1); + cache.set('b', 2); + expect(cache.getBatched(['a', 'b'])).toEqual([1, 2]); + + await wait(150); + expect(cache.getBatched(['a', 'b'])).toEqual([undefined, undefined]); + }); +}); diff --git a/lib/utils/cache/in_memory_lru_cache.ts b/lib/utils/cache/in_memory_lru_cache.ts index ca917ca2a..82cefbc78 100644 --- a/lib/utils/cache/in_memory_lru_cache.ts +++ b/lib/utils/cache/in_memory_lru_cache.ts @@ -35,7 +35,6 @@ export class InMemoryLruCache implements SyncCache { get(key: string): Maybe { const element = this.data.get(key); - if (!element) return undefined; this.data.delete(key); @@ -76,8 +75,4 @@ export class InMemoryLruCache implements SyncCache { getBatched(keys: string[]): Maybe[] { return keys.map((key) => this.get(key)); } - - peek(key: string): Maybe { - return this.data.get(key)?.value; - } } diff --git a/lib/utils/lru_cache/lru_cache.tests.ts b/lib/utils/lru_cache/lru_cache.tests.ts deleted file mode 100644 index 4c9de8d1a..000000000 --- a/lib/utils/lru_cache/lru_cache.tests.ts +++ /dev/null @@ -1,309 +0,0 @@ -/** - * Copyright 2022, 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 { assert } from 'chai'; -import { LRUCache } from './lru_cache'; -import { BrowserLRUCache } from './browser_lru_cache'; -import { ServerLRUCache } from './server_lru_cache'; - -const sleep = async (ms: number) => { - return await new Promise(r => setTimeout(r, ms)); -}; - -describe('/lib/core/odp/lru_cache (Default)', () => { - let cache: LRUCache; - - describe('LRU Cache > Initialization', () => { - it('should successfully create a new cache with maxSize > 0 and timeout > 0', () => { - cache = new LRUCache({ - maxSize: 1000, - timeout: 2000, - }); - - assert.exists(cache); - - assert.equal(cache.maxSize, 1000); - assert.equal(cache.timeout, 2000); - }); - - it('should successfully create a new cache with maxSize == 0 and timeout == 0', () => { - cache = new LRUCache({ - maxSize: 0, - timeout: 0, - }); - - assert.exists(cache); - - assert.equal(cache.maxSize, 0); - assert.equal(cache.timeout, 0); - }); - }); - - describe('LRU Cache > Save & Lookup', () => { - const maxCacheSize = 2; - - beforeEach(() => { - cache = new LRUCache({ - maxSize: maxCacheSize, - timeout: 1000, - }); - }); - - it('should have no values in the cache upon initialization', () => { - assert.isNull(cache.peek(1)); - }); - - it('should save keys and values of any valid type', () => { - cache.save({ key: 'a', value: 1 }); // { a: 1 } - assert.equal(cache.peek('a'), 1); - - cache.save({ key: 2, value: 'b' }); // { a: 1, 2: 'b' } - assert.equal(cache.peek(2), 'b'); - - const foo = Symbol('foo'); - const bar = {}; - cache.save({ key: foo, value: bar }); // { 2: 'b', Symbol('foo'): {} } - assert.deepEqual({}, cache.peek(foo)); - }); - - it('should save values up to its maxSize', () => { - cache.save({ key: 'a', value: 1 }); // { a: 1 } - assert.equal(cache.peek('a'), 1); - - cache.save({ key: 'b', value: 2 }); // { a: 1, b: 2 } - assert.equal(cache.peek('a'), 1); - assert.equal(cache.peek('b'), 2); - - cache.save({ key: 'c', value: 3 }); // { b: 2, c: 3 } - assert.equal(cache.peek('a'), null); - assert.equal(cache.peek('b'), 2); - assert.equal(cache.peek('c'), 3); - }); - - it('should override values of matching keys when saving', () => { - cache.save({ key: 'a', value: 1 }); // { a: 1 } - assert.equal(cache.peek('a'), 1); - - cache.save({ key: 'a', value: 2 }); // { a: 2 } - assert.equal(cache.peek('a'), 2); - - cache.save({ key: 'a', value: 3 }); // { a: 3 } - assert.equal(cache.peek('a'), 3); - }); - - it('should update cache accordingly when using lookup/peek', () => { - assert.isNull(cache.lookup(3)); - - cache.save({ key: 'b', value: 201 }); // { b: 201 } - cache.save({ key: 'a', value: 101 }); // { b: 201, a: 101 } - - assert.equal(cache.lookup('b'), 201); // { a: 101, b: 201 } - - cache.save({ key: 'c', value: 302 }); // { b: 201, c: 302 } - - assert.isNull(cache.peek(1)); - assert.equal(cache.peek('b'), 201); - assert.equal(cache.peek('c'), 302); - assert.equal(cache.lookup('c'), 302); // { b: 201, c: 302 } - - cache.save({ key: 'a', value: 103 }); // { c: 302, a: 103 } - assert.equal(cache.peek('a'), 103); - assert.isNull(cache.peek('b')); - assert.equal(cache.peek('c'), 302); - }); - }); - - describe('LRU Cache > Size', () => { - it('should keep LRU Cache map size capped at cache.capacity', () => { - const maxCacheSize = 2; - - cache = new LRUCache({ - maxSize: maxCacheSize, - timeout: 1000, - }); - - cache.save({ key: 'a', value: 1 }); // { a: 1 } - cache.save({ key: 'b', value: 2 }); // { a: 1, b: 2 } - - assert.equal(cache.map.size, maxCacheSize); - assert.equal(cache.map.size, cache.maxSize); - }); - - it('should not save to cache if maxSize is 0', () => { - cache = new LRUCache({ - maxSize: 0, - timeout: 1000, - }); - - assert.isNull(cache.lookup('a')); - cache.save({ key: 'a', value: 100 }); - assert.isNull(cache.lookup('a')); - }); - - it('should not save to cache if maxSize is negative', () => { - cache = new LRUCache({ - maxSize: -500, - timeout: 1000, - }); - - assert.isNull(cache.lookup('a')); - cache.save({ key: 'a', value: 100 }); - assert.isNull(cache.lookup('a')); - }); - }); - - describe('LRU Cache > Timeout', () => { - it('should discard stale entries in the cache on peek/lookup when timeout is greater than 0', async () => { - const maxTimeout = 100; - - cache = new LRUCache({ - maxSize: 1000, - timeout: maxTimeout, - }); - - cache.save({ key: 'a', value: 100 }); // { a: 100 } - cache.save({ key: 'b', value: 200 }); // { a: 100, b: 200 } - cache.save({ key: 'c', value: 300 }); // { a: 100, b: 200, c: 300 } - - assert.equal(cache.peek('a'), 100); - assert.equal(cache.peek('b'), 200); - assert.equal(cache.peek('c'), 300); - - await sleep(150); - - assert.isNull(cache.lookup('a')); - assert.isNull(cache.lookup('b')); - assert.isNull(cache.lookup('c')); - - cache.save({ key: 'd', value: 400 }); // { d: 400 } - cache.save({ key: 'a', value: 101 }); // { d: 400, a: 101 } - - assert.equal(cache.lookup('a'), 101); // { d: 400, a: 101 } - assert.equal(cache.lookup('d'), 400); // { a: 101, d: 400 } - }); - - it('should never have stale entries if timeout is 0', async () => { - const maxTimeout = 0; - - cache = new LRUCache({ - maxSize: 1000, - timeout: maxTimeout, - }); - - cache.save({ key: 'a', value: 100 }); // { a: 100 } - cache.save({ key: 'b', value: 200 }); // { a: 100, b: 200 } - - await sleep(100); - assert.equal(cache.lookup('a'), 100); - assert.equal(cache.lookup('b'), 200); - }); - - it('should never have stale entries if timeout is less than 0', async () => { - const maxTimeout = -500; - - cache = new LRUCache({ - maxSize: 1000, - timeout: maxTimeout, - }); - - cache.save({ key: 'a', value: 100 }); // { a: 100 } - cache.save({ key: 'b', value: 200 }); // { a: 100, b: 200 } - - await sleep(100); - assert.equal(cache.lookup('a'), 100); - assert.equal(cache.lookup('b'), 200); - }); - }); - - describe('LRU Cache > Reset', () => { - it('should be able to reset the cache', async () => { - cache = new LRUCache({ maxSize: 2, timeout: 100 }); - cache.save({ key: 'a', value: 100 }); // { a: 100 } - cache.save({ key: 'b', value: 200 }); // { a: 100, b: 200 } - - await sleep(0); - - assert.equal(cache.map.size, 2); - cache.reset(); // { } - - await sleep(150); - - assert.equal(cache.map.size, 0); - - it('should be fully functional after resetting the cache', () => { - cache.save({ key: 'c', value: 300 }); // { c: 300 } - cache.save({ key: 'd', value: 400 }); // { c: 300, d: 400 } - assert.isNull(cache.peek('b')); - assert.equal(cache.peek('c'), 300); - assert.equal(cache.peek('d'), 400); - - cache.save({ key: 'a', value: 500 }); // { d: 400, a: 500 } - cache.save({ key: 'b', value: 600 }); // { a: 500, b: 600 } - assert.isNull(cache.peek('c')); - assert.equal(cache.peek('a'), 500); - assert.equal(cache.peek('b'), 600); - - const _ = cache.lookup('a'); // { b: 600, a: 500 } - assert.equal(500, _); - - cache.save({ key: 'c', value: 700 }); // { a: 500, c: 700 } - assert.isNull(cache.peek('b')); - assert.equal(cache.peek('a'), 500); - assert.equal(cache.peek('c'), 700); - }); - }); - }); -}); - -describe('/lib/core/odp/lru_cache (Client)', () => { - let cache: BrowserLRUCache; - - it('should create and test the default client LRU Cache', () => { - cache = new BrowserLRUCache(); - assert.exists(cache); - assert.isNull(cache.lookup('a')); - assert.equal(cache.maxSize, 100); - assert.equal(cache.timeout, 600 * 1000); - - cache.save({ key: 'a', value: 100 }); - cache.save({ key: 'b', value: 200 }); - cache.save({ key: 'c', value: 300 }); - assert.equal(cache.map.size, 3); - assert.equal(cache.peek('a'), 100); - assert.equal(cache.lookup('b'), 200); - assert.deepEqual(cache.map.keys().next().value, 'a'); - }); -}); - -describe('/lib/core/odp/lru_cache (Server)', () => { - let cache: ServerLRUCache; - - it('should create and test the default server LRU Cache', () => { - cache = new ServerLRUCache(); - assert.exists(cache); - assert.isNull(cache.lookup('a')); - assert.equal(cache.maxSize, 10000); - assert.equal(cache.timeout, 600 * 1000); - - cache.save({ key: 'a', value: 100 }); - cache.save({ key: 'b', value: 200 }); - cache.save({ key: 'c', value: 300 }); - assert.equal(cache.map.size, 3); - assert.equal(cache.peek('a'), 100); - assert.equal(cache.lookup('b'), 200); - assert.deepEqual(cache.map.keys().next().value, 'a'); - }); -}); diff --git a/vitest.config.mts b/vitest.config.mts index f9b770b1e..f64e00c20 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_manager.spec.ts'], + include: ['**/in_memory_lru_cache.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 898686fad1958a91d17134eb1e48bbf0dd274032 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 6 Dec 2024 00:52:47 +0600 Subject: [PATCH 12/28] factory test --- lib/odp/event_manager/odp_event_manager.ts | 3 +- lib/odp/odp_manager_factory.browser.ts | 26 ++ lib/odp/odp_manager_factory.node.ts | 29 ++ lib/odp/odp_manager_factory.react_native.ts | 29 ++ lib/odp/odp_manager_factory.spec.ts | 374 ++++++++++++++++++++ lib/odp/odp_manager_factory.ts | 79 +++++ lib/utils/enums/index.ts | 3 +- vitest.config.mts | 2 +- 8 files changed, 540 insertions(+), 5 deletions(-) create mode 100644 lib/odp/odp_manager_factory.browser.ts create mode 100644 lib/odp/odp_manager_factory.node.ts create mode 100644 lib/odp/odp_manager_factory.react_native.ts create mode 100644 lib/odp/odp_manager_factory.spec.ts create mode 100644 lib/odp/odp_manager_factory.ts diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index 0d61870d3..9db9086a4 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -204,8 +204,7 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag if (this.queue.length === this.batchSize) { this.flush(); - } else if (!this.repeater.isRunning() && this.batchSize > 1) { - // no need to repeatedly flush if batchSize is 1 + } else if (!this.repeater.isRunning()) { this.repeater.start(); } } diff --git a/lib/odp/odp_manager_factory.browser.ts b/lib/odp/odp_manager_factory.browser.ts new file mode 100644 index 000000000..4ad1516b4 --- /dev/null +++ b/lib/odp/odp_manager_factory.browser.ts @@ -0,0 +1,26 @@ +import { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; +import { pixelApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { OdpManager } from './odp_manager'; +import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; + +export const BROWSER_DEFAULT_API_TIMEOUT = 10_000; + +export const createOdpManager = (options: OdpManagerOptions): OdpManager => { + let defaultRequestHandler = new BrowserRequestHandler({ timeout: BROWSER_DEFAULT_API_TIMEOUT }); + + const segmentRequestHandler = options.segmentsApiTimeout !== undefined ? + new BrowserRequestHandler({ timeout: options.segmentsApiTimeout }) : + defaultRequestHandler; + + const eventRequestHandler = options.eventApiTimeout !== undefined ? + new BrowserRequestHandler({ timeout: options.eventApiTimeout }) : + defaultRequestHandler; + + return getOdpManager({ + ...options, + eventBatchSize: 1, + segmentRequestHandler, + eventRequestHandler, + eventRequestGenerator: pixelApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_manager_factory.node.ts b/lib/odp/odp_manager_factory.node.ts new file mode 100644 index 000000000..599f9cd2f --- /dev/null +++ b/lib/odp/odp_manager_factory.node.ts @@ -0,0 +1,29 @@ +import { NodeRequestHandler } from '../utils/http_request_handler/node_request_handler'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { OdpManager } from './odp_manager'; +import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; + +export const NODE_DEFAULT_API_TIMEOUT = 10_000; +export const NODE_DEFAULT_BATCH_SIZE = 10; +export const NODE_DEFAULT_FLUSH_INTERVAL = 1000; + +export const createOdpManager = (options: OdpManagerOptions): OdpManager => { + let defaultRequestHandler = new NodeRequestHandler({ timeout: NODE_DEFAULT_API_TIMEOUT }); + + const segmentRequestHandler = options.segmentsApiTimeout !== undefined ? + new NodeRequestHandler({ timeout: options.segmentsApiTimeout }) : + defaultRequestHandler; + + const eventRequestHandler = options.eventApiTimeout !== undefined ? + new NodeRequestHandler({ timeout: options.eventApiTimeout }) : + defaultRequestHandler; + + return getOdpManager({ + ...options, + segmentRequestHandler, + eventRequestHandler, + eventBatchSize: options.eventBatchSize || NODE_DEFAULT_BATCH_SIZE, + eventFlushInterval: options.eventFlushInterval || NODE_DEFAULT_FLUSH_INTERVAL, + eventRequestGenerator: eventApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_manager_factory.react_native.ts b/lib/odp/odp_manager_factory.react_native.ts new file mode 100644 index 000000000..f1439690f --- /dev/null +++ b/lib/odp/odp_manager_factory.react_native.ts @@ -0,0 +1,29 @@ +import { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; +import { OdpManager } from './odp_manager'; +import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; + +export const RN_DEFAULT_API_TIMEOUT = 10_000; +export const RN_DEFAULT_BATCH_SIZE = 10; +export const RN_DEFAULT_FLUSH_INTERVAL = 1000; + +export const createOdpManager = (options: OdpManagerOptions): OdpManager => { + let defaultRequestHandler = new BrowserRequestHandler({ timeout: RN_DEFAULT_API_TIMEOUT }); + + const segmentRequestHandler = options.segmentsApiTimeout !== undefined ? + new BrowserRequestHandler({ timeout: options.segmentsApiTimeout }) : + defaultRequestHandler; + + const eventRequestHandler = options.eventApiTimeout !== undefined ? + new BrowserRequestHandler({ timeout: options.eventApiTimeout }) : + defaultRequestHandler; + + return getOdpManager({ + ...options, + segmentRequestHandler, + eventRequestHandler, + eventBatchSize: options.eventBatchSize || RN_DEFAULT_BATCH_SIZE, + eventFlushInterval: options.eventFlushInterval || RN_DEFAULT_FLUSH_INTERVAL, + eventRequestGenerator: eventApiRequestGenerator, + }); +}; diff --git a/lib/odp/odp_manager_factory.spec.ts b/lib/odp/odp_manager_factory.spec.ts new file mode 100644 index 000000000..f01f94c83 --- /dev/null +++ b/lib/odp/odp_manager_factory.spec.ts @@ -0,0 +1,374 @@ +vi.mock('./odp_manager', () => { + return { + DefaultOdpManager: vi.fn(), + }; +}); + +vi.mock('./segment_manager/odp_segment_manager', () => { + return { + DefaultOdpSegmentManager: vi.fn(), + }; +}); + +vi.mock('./segment_manager/odp_segment_api_manager', () => { + return { + DefaultOdpSegmentApiManager: vi.fn(), + }; +}); + +vi.mock('../utils/cache/in_memory_lru_cache', () => { + return { + InMemoryLruCache: vi.fn(), + }; +}); + +vi.mock('./event_manager/odp_event_manager', () => { + return { + DefaultOdpEventManager: vi.fn(), + }; +}); + +vi.mock('./event_manager/odp_event_api_manager', () => { + return { + DefaultOdpEventApiManager: vi.fn(), + }; +}); + +vi.mock( '../utils/repeater/repeater', () => { + return { + IntervalRepeater: vi.fn(), + ExponentialBackoff: vi.fn(), + }; +}); + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { DefaultOdpManager } from './odp_manager'; +import { DEFAULT_CACHE_SIZE, DEFAULT_CACHE_TIMEOUT, DEFAULT_EVENT_BATCH_SIZE, DEFAULT_EVENT_MAX_BACKOFF, DEFAULT_EVENT_MAX_RETRIES, DEFAULT_EVENT_MIN_BACKOFF, getOdpManager } from './odp_manager_factory'; +import { getMockRequestHandler } from '../tests/mock/mock_request_handler'; +import { DefaultOdpSegmentManager } from './segment_manager/odp_segment_manager'; +import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; +import { InMemoryLruCache } from '../utils/cache/in_memory_lru_cache'; +import { DefaultOdpEventManager } from './event_manager/odp_event_manager'; +import { DefaultOdpEventApiManager } from './event_manager/odp_event_api_manager'; +import { IntervalRepeater } from '../utils/repeater/repeater'; +import { ExponentialBackoff } from '../utils/repeater/repeater'; + +describe('getOdpManager', () => { + const MockDefaultOdpManager = vi.mocked(DefaultOdpManager); + const MockDefaultOdpSegmentManager = vi.mocked(DefaultOdpSegmentManager); + const MockDefaultOdpSegmentApiManager = vi.mocked(DefaultOdpSegmentApiManager); + const MockInMemoryLruCache = vi.mocked(InMemoryLruCache); + const MockDefaultOdpEventManager = vi.mocked(DefaultOdpEventManager); + const MockDefaultOdpEventApiManager = vi.mocked(DefaultOdpEventApiManager); + const MockIntervalRepeater = vi.mocked(IntervalRepeater); + const MockExponentialBackoff = vi.mocked(ExponentialBackoff); + + beforeEach(() => { + MockDefaultOdpManager.mockClear(); + MockDefaultOdpSegmentManager.mockClear(); + MockDefaultOdpSegmentApiManager.mockClear(); + MockInMemoryLruCache.mockClear(); + MockDefaultOdpEventManager.mockClear(); + MockDefaultOdpEventApiManager.mockClear(); + MockIntervalRepeater.mockClear(); + MockExponentialBackoff.mockClear(); + }); + + it('should use provided segment manager', () => { + const segmentManager = {} as any; + + const odpManager = getOdpManager({ + segmentManager, + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedSegmentManager).toBe(segmentManager); + }); + + describe('when no segment manager is provided', () => { + it('should create a default segment manager with default api manager using the passed eventRequestHandler', () => { + const segmentRequestHandler = getMockRequestHandler(); + const odpManager = getOdpManager({ + segmentRequestHandler, + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const apiManager = MockDefaultOdpSegmentManager.mock.calls[0][1]; + expect(Object.is(apiManager, MockDefaultOdpSegmentApiManager.mock.instances[0])).toBe(true); + const usedRequestHandler = MockDefaultOdpSegmentApiManager.mock.calls[0][0]; + expect(Object.is(usedRequestHandler, segmentRequestHandler)).toBe(true); + }); + + it('should create a default segment manager with the provided segment cache', () => { + const segmentsCache = {} as any; + + const odpManager = getOdpManager({ + segmentsCache, + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(segmentsCache); + }); + + describe('when no segment cache is provided', () => { + it('should use a InMemoryLruCache with the provided size', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCacheSize: 3141, + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][0]).toBe(3141); + }); + + it('should use a InMemoryLruCache with default size if no segmentCacheSize is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][0]).toBe(DEFAULT_CACHE_SIZE); + }); + + it('should use a InMemoryLruCache with the provided timeout', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + segmentsCacheTimeout: 123456, + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][1]).toBe(123456); + }); + + it('should use a InMemoryLruCache with default timeout if no segmentsCacheTimeout is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(Object.is(odpManager, MockDefaultOdpManager.mock.instances[0])).toBe(true); + const { segmentManager: usedSegmentManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(Object.is(usedSegmentManager, MockDefaultOdpSegmentManager.mock.instances[0])).toBe(true); + const usedCache = MockDefaultOdpSegmentManager.mock.calls[0][0]; + expect(usedCache).toBe(MockInMemoryLruCache.mock.instances[0]); + expect(MockInMemoryLruCache.mock.calls[0][1]).toBe(DEFAULT_CACHE_TIMEOUT); + }); + }); + }); + + it('uses provided event manager', () => { + const eventManager = {} as any; + + const odpManager = getOdpManager({ + eventManager, + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(eventManager); + }); + + describe('when no event manager is provided', () => { + it('should use a default event manager with default api manager using the passed eventRequestHandler and eventRequestGenerator', () => { + const eventRequestHandler = getMockRequestHandler(); + const eventRequestGenerator = vi.fn(); + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler, + eventRequestGenerator, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const apiManager = MockDefaultOdpEventManager.mock.calls[0][0].apiManager; + expect(apiManager).toBe(MockDefaultOdpEventApiManager.mock.instances[0]); + const usedRequestHandler = MockDefaultOdpEventApiManager.mock.calls[0][0]; + expect(usedRequestHandler).toBe(eventRequestHandler); + const usedRequestGenerator = MockDefaultOdpEventApiManager.mock.calls[0][1]; + expect(usedRequestGenerator).toBe(eventRequestGenerator); + }); + + it('should use a default event manager with the provided event batch size', () => { + const eventBatchSize = 1234; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventBatchSize, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBatchSize = MockDefaultOdpEventManager.mock.calls[0][0].batchSize; + expect(usedBatchSize).toBe(eventBatchSize); + }); + + it('should use a default event manager with the default batch size if no eventBatchSize is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBatchSize = MockDefaultOdpEventManager.mock.calls[0][0].batchSize; + expect(usedBatchSize).toBe(DEFAULT_EVENT_BATCH_SIZE); + }); + + it('should use a default event manager with an interval repeater with the provided flush interval', () => { + const eventFlushInterval = 1234; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventFlushInterval, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedRepeater = MockDefaultOdpEventManager.mock.calls[0][0].repeater; + expect(usedRepeater).toBe(MockIntervalRepeater.mock.instances[0]); + const usedInterval = MockIntervalRepeater.mock.calls[0][0]; + expect(usedInterval).toBe(eventFlushInterval); + }); + + it('should use a default event manager with the provided max retries', () => { + const eventMaxRetries = 7; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventMaxRetries, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedMaxRetries = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.maxRetries; + expect(usedMaxRetries).toBe(eventMaxRetries); + }); + + it('should use a default event manager with the default max retries if no eventMaxRetries is provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedMaxRetries = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.maxRetries; + expect(usedMaxRetries).toBe(DEFAULT_EVENT_MAX_RETRIES); + }); + + it('should use a default event manager with ExponentialBackoff with provided minBackoff', () => { + const eventMinBackoff = 1234; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventMinBackoff, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][0]).toBe(eventMinBackoff); + }); + + it('should use a default event manager with ExponentialBackoff with default min backoff if none provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][0]).toBe(DEFAULT_EVENT_MIN_BACKOFF); + }); + + it('should use a default event manager with ExponentialBackoff with provided maxBackoff', () => { + const eventMaxBackoff = 9999; + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + eventMaxBackoff: eventMaxBackoff, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][1]).toBe(eventMaxBackoff); + }); + + it('should use a default event manager with ExponentialBackoff with default max backoff if none provided', () => { + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { eventManager: usedEventManager } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedEventManager).toBe(MockDefaultOdpEventManager.mock.instances[0]); + const usedBackoffProvider = MockDefaultOdpEventManager.mock.calls[0][0].retryConfig.backoffProvider; + const backoff = usedBackoffProvider(); + expect(Object.is(backoff, MockExponentialBackoff.mock.instances[0])).toBe(true); + expect(MockExponentialBackoff.mock.calls[0][1]).toBe(DEFAULT_EVENT_MAX_BACKOFF); + }); + }); +}); diff --git a/lib/odp/odp_manager_factory.ts b/lib/odp/odp_manager_factory.ts new file mode 100644 index 000000000..a1378e92d --- /dev/null +++ b/lib/odp/odp_manager_factory.ts @@ -0,0 +1,79 @@ +import { RequestHandler } from "../shared_types"; +import { Cache } from "../utils/cache/cache"; +import { InMemoryLruCache } from "../utils/cache/in_memory_lru_cache"; +import { ExponentialBackoff, IntervalRepeater } from "../utils/repeater/repeater"; +import { DefaultOdpEventApiManager, EventRequestGenerator } from "./event_manager/odp_event_api_manager"; +import { DefaultOdpEventManager, OdpEventManager } from "./event_manager/odp_event_manager"; +import { DefaultOdpManager, OdpManager } from "./odp_manager"; +import { DefaultOdpSegmentApiManager } from "./segment_manager/odp_segment_api_manager"; +import { DefaultOdpSegmentManager, OdpSegmentManager } from "./segment_manager/odp_segment_manager"; +import { UserAgentParser } from "./ua_parser/user_agent_parser"; + +export const DEFAULT_CACHE_SIZE = 1000; +export const DEFAULT_CACHE_TIMEOUT = 600_000; + +export const DEFAULT_EVENT_BATCH_SIZE = 100; +export const DEFAULT_EVENT_FLUSH_INTERVAL = 10_000; +export const DEFAULT_EVENT_MAX_RETRIES = 5; +export const DEFAULT_EVENT_MIN_BACKOFF = 1000; +export const DEFAULT_EVENT_MAX_BACKOFF = 32_000; + +export type OdpManagerOptions = { + segmentsCache?: Cache; + segmentsCacheSize?: number; + segmentsCacheTimeout?: number; + segmentsApiTimeout?: number; + segmentManager?: OdpSegmentManager; + eventFlushInterval?: number; + eventBatchSize?: number; + eventApiTimeout?: number; + eventManager?: OdpEventManager; + userAgentParser?: UserAgentParser; +}; + +export type OdpManagerFactoryOptions = Omit & { + segmentRequestHandler: RequestHandler; + eventRequestHandler: RequestHandler; + eventRequestGenerator: EventRequestGenerator; + eventMaxRetries?: number; + eventMinBackoff?: number; + eventMaxBackoff?: number; +} + +const getDefaultSegmentsCache = (cacheSize?: number, cacheTimeout?: number) => { + return new InMemoryLruCache(cacheSize || DEFAULT_CACHE_SIZE, cacheTimeout || DEFAULT_CACHE_TIMEOUT); +} + +const getDefaultSegmentManager = (options: OdpManagerFactoryOptions) => { + return new DefaultOdpSegmentManager( + options.segmentsCache || getDefaultSegmentsCache(options.segmentsCacheSize, options.segmentsCacheTimeout), + new DefaultOdpSegmentApiManager(options.segmentRequestHandler), + ); +}; + +const getDefaultEventManager = (options: OdpManagerFactoryOptions) => { + return new DefaultOdpEventManager({ + apiManager: new DefaultOdpEventApiManager(options.eventRequestHandler, options.eventRequestGenerator), + batchSize: options.eventBatchSize || DEFAULT_EVENT_BATCH_SIZE, + repeater: new IntervalRepeater(options.eventFlushInterval || DEFAULT_EVENT_FLUSH_INTERVAL), + retryConfig: { + maxRetries: options.eventMaxRetries || DEFAULT_EVENT_MAX_RETRIES, + backoffProvider: () => new ExponentialBackoff( + options.eventMinBackoff || DEFAULT_EVENT_MIN_BACKOFF, + options.eventMaxBackoff || DEFAULT_EVENT_MAX_BACKOFF, + 500, + ), + }, + }); +} + +export const getOdpManager = (options: OdpManagerFactoryOptions): OdpManager => { + const segmentManager = options.segmentManager || getDefaultSegmentManager(options); + const eventManager = options.eventManager || getDefaultEventManager(options); + + return new DefaultOdpManager({ + segmentManager, + eventManager, + userAgentParser: options.userAgentParser, + }); +}; diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts index 6878b7bfe..551fa6b98 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -291,7 +291,6 @@ export { NOTIFICATION_TYPES } from '../../notification_center/type'; * Default milliseconds before request timeout */ export const REQUEST_TIMEOUT_MS = 60 * 1000; // 1 minute -export const REQUEST_TIMEOUT_ODP_SEGMENTS_MS = 10 * 1000; // 10 secs -export const REQUEST_TIMEOUT_ODP_EVENTS_MS = 10 * 1000; // 10 secs + diff --git a/vitest.config.mts b/vitest.config.mts index f64e00c20..05eca6e41 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/in_memory_lru_cache.spec.ts'], + include: ['**/odp_manager_factory.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 7c5427cae909b663c97b1b1c14224ed336948d60 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 6 Dec 2024 00:55:43 +0600 Subject: [PATCH 13/28] saving --- lib/odp/odp_manager_factory.spec.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/lib/odp/odp_manager_factory.spec.ts b/lib/odp/odp_manager_factory.spec.ts index f01f94c83..860e26ec7 100644 --- a/lib/odp/odp_manager_factory.spec.ts +++ b/lib/odp/odp_manager_factory.spec.ts @@ -371,4 +371,19 @@ describe('getOdpManager', () => { expect(MockExponentialBackoff.mock.calls[0][1]).toBe(DEFAULT_EVENT_MAX_BACKOFF); }); }); + + it('should use the provided userAgentParser', () => { + const userAgentParser = {} as any; + + const odpManager = getOdpManager({ + segmentRequestHandler: getMockRequestHandler(), + eventRequestHandler: getMockRequestHandler(), + eventRequestGenerator: vi.fn(), + userAgentParser, + }); + + expect(odpManager).toBe(MockDefaultOdpManager.mock.instances[0]); + const { userAgentParser: usedUserAgentParser } = MockDefaultOdpManager.mock.calls[0][0]; + expect(usedUserAgentParser).toBe(userAgentParser); + }); }); From cf73c11fbc7e6fbe85b1623466b7dd48d71ea1c7 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 6 Dec 2024 01:08:09 +0600 Subject: [PATCH 14/28] up --- lib/odp/odp_manager.browser.ts | 201 -------------------- lib/odp/odp_manager.node.ts | 144 -------------- lib/odp/odp_manager_factory.browser.spec.ts | 11 ++ lib/odp/odp_manager_factory.browser.ts | 14 +- 4 files changed, 17 insertions(+), 353 deletions(-) delete mode 100644 lib/odp/odp_manager.browser.ts delete mode 100644 lib/odp/odp_manager.node.ts create mode 100644 lib/odp/odp_manager_factory.browser.spec.ts diff --git a/lib/odp/odp_manager.browser.ts b/lib/odp/odp_manager.browser.ts deleted file mode 100644 index ad9048ca3..000000000 --- a/lib/odp/odp_manager.browser.ts +++ /dev/null @@ -1,201 +0,0 @@ -/** - * Copyright 2023-2024, 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 { - CLIENT_VERSION, - ERROR_MESSAGES, - JAVASCRIPT_CLIENT_ENGINE, - REQUEST_TIMEOUT_ODP_SEGMENTS_MS, - REQUEST_TIMEOUT_ODP_EVENTS_MS, - LOG_MESSAGES, -} from '../utils/enums'; -import { getLogger, LogHandler, LogLevel } from '../modules/logging'; - -import { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; - -import BrowserAsyncStorageCache from '../plugins/key_value_cache/browserAsyncStorageCache'; -import { BrowserLRUCache } from '../utils/lru_cache'; - -import { VuidManager } from '../plugins/vuid_manager/index'; - -import { DefaultOdpManager } from './odp_manager'; -import { OdpEvent } from './event_manager/odp_event'; -import { IOdpEventManager, OdpOptions } from '../shared_types'; -import { BrowserOdpEventApiManager } from './event_manager/event_api_manager.browser'; -import { BrowserOdpEventManager } from './event_manager/event_manager.browser'; -import { OdpSegmentManager, DefaultOdpSegmentManager } from './segment_manager/odp_segment_manager'; -import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; -import { OdpConfig, OdpIntegrationConfig } from './odp_config'; - -interface BrowserOdpManagerConfig { - clientEngine?: string, - clientVersion?: string, - logger?: LogHandler; - odpOptions?: OdpOptions; - odpIntegrationConfig?: OdpIntegrationConfig; -} - -// Client-side Browser Plugin for ODP Manager -export class BrowserOdpManager extends DefaultOdpManager { - static cache = new BrowserAsyncStorageCache(); - vuidManager?: VuidManager; - vuid?: string; - - constructor(options: { - odpIntegrationConfig?: OdpIntegrationConfig; - segmentManager: OdpSegmentManager; - eventManager: IOdpEventManager; - logger: LogHandler; - }) { - super(options); - } - - static createInstance({ - logger, odpOptions, odpIntegrationConfig, clientEngine, clientVersion - }: BrowserOdpManagerConfig): BrowserOdpManager { - logger = logger || getLogger(); - - clientEngine = clientEngine || JAVASCRIPT_CLIENT_ENGINE; - clientVersion = clientVersion || CLIENT_VERSION; - - let odpConfig : OdpConfig | undefined = undefined; - if (odpIntegrationConfig?.integrated) { - odpConfig = odpIntegrationConfig.odpConfig; - } - - let customSegmentRequestHandler; - - if (odpOptions?.segmentsRequestHandler) { - customSegmentRequestHandler = odpOptions.segmentsRequestHandler; - } else { - customSegmentRequestHandler = new BrowserRequestHandler({ - logger, - timeout: odpOptions?.segmentsApiTimeout || REQUEST_TIMEOUT_ODP_SEGMENTS_MS - }); - } - - let segmentManager: OdpSegmentManager; - - if (odpOptions?.segmentManager) { - segmentManager = odpOptions.segmentManager; - } else { - segmentManager = new DefaultOdpSegmentManager( - odpOptions?.segmentsCache || - new BrowserLRUCache({ - maxSize: odpOptions?.segmentsCacheSize, - timeout: odpOptions?.segmentsCacheTimeout, - }), - new DefaultOdpSegmentApiManager(customSegmentRequestHandler, logger), - logger, - odpConfig - ); - } - - let customEventRequestHandler; - - if (odpOptions?.eventRequestHandler) { - customEventRequestHandler = odpOptions.eventRequestHandler; - } else { - customEventRequestHandler = new BrowserRequestHandler({ - logger, - timeout:odpOptions?.eventApiTimeout || REQUEST_TIMEOUT_ODP_EVENTS_MS - }); - } - - let eventManager: IOdpEventManager; - - if (odpOptions?.eventManager) { - eventManager = odpOptions.eventManager; - } else { - eventManager = new BrowserOdpEventManager({ - odpConfig, - apiManager: new BrowserOdpEventApiManager(customEventRequestHandler, logger), - logger: logger, - clientEngine, - clientVersion, - flushInterval: odpOptions?.eventFlushInterval, - batchSize: odpOptions?.eventBatchSize, - queueSize: odpOptions?.eventQueueSize, - userAgentParser: odpOptions?.userAgentParser, - }); - } - - return new BrowserOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - }); - } - - /** - * @override - * accesses or creates new VUID from Browser cache - */ - protected async initializeVuid(): Promise { - const vuidManager = await VuidManager.instance(BrowserOdpManager.cache); - this.vuid = vuidManager.vuid; - } - - /** - * @override - * - Still identifies a user via the ODP Event Manager - * - Additionally, also passes VUID to help identify client-side users - * @param fsUserId Unique identifier of a target user. - */ - identifyUser(fsUserId?: string, vuid?: string): void { - if (fsUserId && VuidManager.isVuid(fsUserId)) { - super.identifyUser(undefined, fsUserId); - return; - } - - if (fsUserId && vuid && VuidManager.isVuid(vuid)) { - super.identifyUser(fsUserId, vuid); - return; - } - - super.identifyUser(fsUserId, vuid || this.vuid); - } - - /** - * @override - * - Sends an event to the ODP Server via the ODP Events API - * - Intercepts identifiers and injects VUID before sending event - * - Identifiers must contain at least one key-value pair - * @param {OdpEvent} odpEvent > ODP Event to send to event manager - */ - sendEvent({ type, action, identifiers, data }: OdpEvent): void { - const identifiersWithVuid = new Map(identifiers); - - if (!identifiers.has(ODP_USER_KEY.VUID)) { - if (this.vuid) { - identifiersWithVuid.set(ODP_USER_KEY.VUID, this.vuid); - } else { - throw new Error(ERROR_MESSAGES.ODP_SEND_EVENT_FAILED_VUID_MISSING); - } - } - - super.sendEvent({ type, action, identifiers: identifiersWithVuid, data }); - } - - isVuidEnabled(): boolean { - return true; - } - - getVuid(): string | undefined { - return this.vuid; - } -} diff --git a/lib/odp/odp_manager.node.ts b/lib/odp/odp_manager.node.ts deleted file mode 100644 index e4490e4d5..000000000 --- a/lib/odp/odp_manager.node.ts +++ /dev/null @@ -1,144 +0,0 @@ -/** - * Copyright 2023-2024, 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 { NodeRequestHandler } from '../utils/http_request_handler/node_request_handler'; - -import { ServerLRUCache } from '../utils/lru_cache/server_lru_cache'; - -import { getLogger, LogHandler, LogLevel } from '../modules/logging'; -import { - NODE_CLIENT_ENGINE, - CLIENT_VERSION, - REQUEST_TIMEOUT_ODP_EVENTS_MS, - REQUEST_TIMEOUT_ODP_SEGMENTS_MS, -} from '../utils/enums'; - -import { DefaultOdpManager } from './odp_manager'; -import { IOdpEventManager, OdpOptions } from '../shared_types'; -import { NodeOdpEventApiManager } from './event_manager/event_api_manager.node'; -import { NodeOdpEventManager } from './event_manager/event_manager.node'; -import { OdpSegmentManager, DefaultOdpSegmentManager } from './segment_manager/odp_segment_manager'; -import { DefaultOdpSegmentApiManager } from './segment_manager/odp_segment_api_manager'; -import { OdpConfig, OdpIntegrationConfig } from './odp_config'; - -interface NodeOdpManagerConfig { - clientEngine?: string, - clientVersion?: string, - logger?: LogHandler; - odpOptions?: OdpOptions; - odpIntegrationConfig?: OdpIntegrationConfig; -} - -/** - * Server-side Node Plugin for ODP Manager. - * Note: As this is still a work-in-progress. Please avoid using the Node ODP Manager. - */ -export class NodeOdpManager extends DefaultOdpManager { - constructor(options: { - odpIntegrationConfig?: OdpIntegrationConfig; - segmentManager: OdpSegmentManager; - eventManager: IOdpEventManager; - logger: LogHandler; - }) { - super(options); - } - - static createInstance({ - logger, odpOptions, odpIntegrationConfig, clientEngine, clientVersion - }: NodeOdpManagerConfig): NodeOdpManager { - logger = logger || getLogger(); - - clientEngine = clientEngine || NODE_CLIENT_ENGINE; - clientVersion = clientVersion || CLIENT_VERSION; - - let odpConfig : OdpConfig | undefined = undefined; - if (odpIntegrationConfig?.integrated) { - odpConfig = odpIntegrationConfig.odpConfig; - } - - let customSegmentRequestHandler; - - if (odpOptions?.segmentsRequestHandler) { - customSegmentRequestHandler = odpOptions.segmentsRequestHandler; - } else { - customSegmentRequestHandler = new NodeRequestHandler({ - logger, - timeout: odpOptions?.segmentsApiTimeout || REQUEST_TIMEOUT_ODP_SEGMENTS_MS - }); - } - - let segmentManager: OdpSegmentManager; - - if (odpOptions?.segmentManager) { - segmentManager = odpOptions.segmentManager; - } else { - segmentManager = new DefaultOdpSegmentManager( - odpOptions?.segmentsCache || - new ServerLRUCache({ - maxSize: odpOptions?.segmentsCacheSize, - timeout: odpOptions?.segmentsCacheTimeout, - }), - new DefaultOdpSegmentApiManager(customSegmentRequestHandler, logger), - logger, - odpConfig - ); - } - - let customEventRequestHandler; - - if (odpOptions?.eventRequestHandler) { - customEventRequestHandler = odpOptions.eventRequestHandler; - } else { - customEventRequestHandler = new NodeRequestHandler({ - logger, - timeout: odpOptions?.eventApiTimeout || REQUEST_TIMEOUT_ODP_EVENTS_MS - }); - } - - let eventManager: IOdpEventManager; - - if (odpOptions?.eventManager) { - eventManager = odpOptions.eventManager; - } else { - eventManager = new NodeOdpEventManager({ - odpConfig, - apiManager: new NodeOdpEventApiManager(customEventRequestHandler, logger), - logger: logger, - clientEngine, - clientVersion, - flushInterval: odpOptions?.eventFlushInterval, - batchSize: odpOptions?.eventBatchSize, - queueSize: odpOptions?.eventQueueSize, - userAgentParser: odpOptions?.userAgentParser, - }); - } - - return new NodeOdpManager({ - odpIntegrationConfig, - segmentManager, - eventManager, - logger, - }); - } - - public isVuidEnabled(): boolean { - return false; - } - - public getVuid(): string | undefined { - return undefined; - } -} diff --git a/lib/odp/odp_manager_factory.browser.spec.ts b/lib/odp/odp_manager_factory.browser.spec.ts new file mode 100644 index 000000000..ac8ca4900 --- /dev/null +++ b/lib/odp/odp_manager_factory.browser.spec.ts @@ -0,0 +1,11 @@ +import { describe, it, expect, beforeEach, vi } from 'vitest'; + +import { createOdpManager } from './odp_manager_factory.browser'; +import { getMockRequestHandler } from '../tests/mock/mock_request_handler'; + +describe('createOdpManager', () => { + it('should use BrowserRequestHandler with the provided timeout as the segment request handler', () => { + const odpManager = createOdpManager({ segmentsApiTimeout: 1000 }); + expect((odpManager as any).segmentRequestHandler.timeout).toBe(1000); + }); +}); diff --git a/lib/odp/odp_manager_factory.browser.ts b/lib/odp/odp_manager_factory.browser.ts index 4ad1516b4..97c320aee 100644 --- a/lib/odp/odp_manager_factory.browser.ts +++ b/lib/odp/odp_manager_factory.browser.ts @@ -6,15 +6,13 @@ import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; export const BROWSER_DEFAULT_API_TIMEOUT = 10_000; export const createOdpManager = (options: OdpManagerOptions): OdpManager => { - let defaultRequestHandler = new BrowserRequestHandler({ timeout: BROWSER_DEFAULT_API_TIMEOUT }); - - const segmentRequestHandler = options.segmentsApiTimeout !== undefined ? - new BrowserRequestHandler({ timeout: options.segmentsApiTimeout }) : - defaultRequestHandler; + const segmentRequestHandler = new BrowserRequestHandler({ + timeout: options.segmentsApiTimeout || BROWSER_DEFAULT_API_TIMEOUT, + }); - const eventRequestHandler = options.eventApiTimeout !== undefined ? - new BrowserRequestHandler({ timeout: options.eventApiTimeout }) : - defaultRequestHandler; + const eventRequestHandler = new BrowserRequestHandler({ + timeout: options.eventApiTimeout || BROWSER_DEFAULT_API_TIMEOUT, + }); return getOdpManager({ ...options, From 6bb2d85d0afbe4d8533d8a8f77bead9594162e3d Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 6 Dec 2024 19:43:59 +0600 Subject: [PATCH 15/28] factory tests --- lib/odp/odp_manager_factory.browser.spec.ts | 95 ++++++++++++++- lib/odp/odp_manager_factory.node.spec.ts | 109 ++++++++++++++++++ lib/odp/odp_manager_factory.node.ts | 14 +-- .../odp_manager_factory.react_native.spec.ts | 109 ++++++++++++++++++ lib/odp/odp_manager_factory.react_native.ts | 14 +-- vitest.config.mts | 2 +- 6 files changed, 321 insertions(+), 22 deletions(-) create mode 100644 lib/odp/odp_manager_factory.node.spec.ts create mode 100644 lib/odp/odp_manager_factory.react_native.spec.ts diff --git a/lib/odp/odp_manager_factory.browser.spec.ts b/lib/odp/odp_manager_factory.browser.spec.ts index ac8ca4900..4268a860a 100644 --- a/lib/odp/odp_manager_factory.browser.spec.ts +++ b/lib/odp/odp_manager_factory.browser.spec.ts @@ -1,11 +1,96 @@ -import { describe, it, expect, beforeEach, vi } from 'vitest'; +vi.mock('../utils/http_request_handler/browser_request_handler', () => { + return { BrowserRequestHandler: vi.fn() }; +}); + +vi.mock('./odp_manager_factory', () => { + return { getOdpManager: vi.fn().mockImplementation(() => ({})) }; +}); + -import { createOdpManager } from './odp_manager_factory.browser'; -import { getMockRequestHandler } from '../tests/mock/mock_request_handler'; +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; +import { BROWSER_DEFAULT_API_TIMEOUT, createOdpManager } from './odp_manager_factory.browser'; +import { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; +import { pixelApiRequestGenerator } from './event_manager/odp_event_api_manager'; describe('createOdpManager', () => { + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const mockGetOdpManager = vi.mocked(getOdpManager); + + beforeEach(() => { + MockBrowserRequestHandler.mockClear(); + mockGetOdpManager.mockClear(); + }); + it('should use BrowserRequestHandler with the provided timeout as the segment request handler', () => { - const odpManager = createOdpManager({ segmentsApiTimeout: 1000 }); - expect((odpManager as any).segmentRequestHandler.timeout).toBe(1000); + const odpManager = createOdpManager({ segmentsApiTimeout: 3456 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(3456); + }); + + it('should use BrowserRequestHandler with the browser default timeout as the segment request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(BROWSER_DEFAULT_API_TIMEOUT); + }); + + it('should use BrowserRequestHandler with the provided timeout as the event request handler', () => { + const odpManager = createOdpManager({ eventApiTimeout: 2345 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(2345); + }); + + it('should use BrowserRequestHandler with the browser default timeout as the event request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(BROWSER_DEFAULT_API_TIMEOUT); + }); + + it('should use batchSize 1 if batchSize is not provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(1); + }); + + it('should use batchSize 1 event if some other batchSize value is provided', () => { + const odpManager = createOdpManager({ eventBatchSize: 99 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(1); + }); + + it('uses the pixel api request generator', () => { + const odpManager = createOdpManager({ }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestGenerator } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestGenerator).toBe(pixelApiRequestGenerator); + }); + + it('uses the passed options for relevant fields', () => { + const options: OdpManagerOptions = { + segmentsCache: {} as any, + segmentsCacheSize: 11, + segmentsCacheTimeout: 2025, + segmentManager: {} as any, + eventFlushInterval: 2222, + eventManager: {} as any, + userAgentParser: {} as any, + }; + const odpManager = createOdpManager(options); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + expect(mockGetOdpManager).toHaveBeenNthCalledWith(1, expect.objectContaining(options)); }); }); diff --git a/lib/odp/odp_manager_factory.node.spec.ts b/lib/odp/odp_manager_factory.node.spec.ts new file mode 100644 index 000000000..4a3f2b0b0 --- /dev/null +++ b/lib/odp/odp_manager_factory.node.spec.ts @@ -0,0 +1,109 @@ +vi.mock('../utils/http_request_handler/node_request_handler', () => { + return { NodeRequestHandler: vi.fn() }; +}); + +vi.mock('./odp_manager_factory', () => { + return { getOdpManager: vi.fn().mockImplementation(() => ({})) }; +}); + + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; +import { NODE_DEFAULT_API_TIMEOUT, NODE_DEFAULT_BATCH_SIZE, NODE_DEFAULT_FLUSH_INTERVAL, createOdpManager } from './odp_manager_factory.node'; +import { NodeRequestHandler } from '../utils/http_request_handler/node_request_handler'; +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; + +describe('createOdpManager', () => { + const MockNodeRequestHandler = vi.mocked(NodeRequestHandler); + const mockGetOdpManager = vi.mocked(getOdpManager); + + beforeEach(() => { + MockNodeRequestHandler.mockClear(); + mockGetOdpManager.mockClear(); + }); + + it('should use NodeRequestHandler with the provided timeout as the segment request handler', () => { + const odpManager = createOdpManager({ segmentsApiTimeout: 3456 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockNodeRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(3456); + }); + + it('should use NodeRequestHandler with the node default timeout as the segment request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockNodeRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(NODE_DEFAULT_API_TIMEOUT); + }); + + it('should use NodeRequestHandler with the provided timeout as the event request handler', () => { + const odpManager = createOdpManager({ eventApiTimeout: 2345 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockNodeRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(2345); + }); + + it('should use NodeRequestHandler with the node default timeout as the event request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockNodeRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockNodeRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(NODE_DEFAULT_API_TIMEOUT); + }); + + it('uses the event api request generator', () => { + const odpManager = createOdpManager({ }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestGenerator } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestGenerator).toBe(eventApiRequestGenerator); + }); + + it('should use the provided eventBatchSize', () => { + const odpManager = createOdpManager({ eventBatchSize: 99 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(99); + }); + + it('should use the node default eventBatchSize if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(NODE_DEFAULT_BATCH_SIZE); + }); + + it('should use the provided eventFlushInterval', () => { + const odpManager = createOdpManager({ eventFlushInterval: 9999 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(9999); + }); + + it('should use the node default eventFlushInterval if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(NODE_DEFAULT_FLUSH_INTERVAL); + }); + + it('uses the passed options for relevant fields', () => { + const options: OdpManagerOptions = { + segmentsCache: {} as any, + segmentsCacheSize: 11, + segmentsCacheTimeout: 2025, + segmentManager: {} as any, + eventManager: {} as any, + userAgentParser: {} as any, + }; + const odpManager = createOdpManager(options); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + expect(mockGetOdpManager).toHaveBeenNthCalledWith(1, expect.objectContaining(options)); + }); +}); diff --git a/lib/odp/odp_manager_factory.node.ts b/lib/odp/odp_manager_factory.node.ts index 599f9cd2f..e0fbd7734 100644 --- a/lib/odp/odp_manager_factory.node.ts +++ b/lib/odp/odp_manager_factory.node.ts @@ -8,15 +8,13 @@ export const NODE_DEFAULT_BATCH_SIZE = 10; export const NODE_DEFAULT_FLUSH_INTERVAL = 1000; export const createOdpManager = (options: OdpManagerOptions): OdpManager => { - let defaultRequestHandler = new NodeRequestHandler({ timeout: NODE_DEFAULT_API_TIMEOUT }); - - const segmentRequestHandler = options.segmentsApiTimeout !== undefined ? - new NodeRequestHandler({ timeout: options.segmentsApiTimeout }) : - defaultRequestHandler; + const segmentRequestHandler = new NodeRequestHandler({ + timeout: options.segmentsApiTimeout || NODE_DEFAULT_API_TIMEOUT, + }); - const eventRequestHandler = options.eventApiTimeout !== undefined ? - new NodeRequestHandler({ timeout: options.eventApiTimeout }) : - defaultRequestHandler; + const eventRequestHandler = new NodeRequestHandler({ + timeout: options.eventApiTimeout || NODE_DEFAULT_API_TIMEOUT, + }); return getOdpManager({ ...options, diff --git a/lib/odp/odp_manager_factory.react_native.spec.ts b/lib/odp/odp_manager_factory.react_native.spec.ts new file mode 100644 index 000000000..e979d3e88 --- /dev/null +++ b/lib/odp/odp_manager_factory.react_native.spec.ts @@ -0,0 +1,109 @@ +vi.mock('../utils/http_request_handler/browser_request_handler', () => { + return { BrowserRequestHandler: vi.fn() }; +}); + +vi.mock('./odp_manager_factory', () => { + return { getOdpManager: vi.fn().mockImplementation(() => ({})) }; +}); + + +import { describe, it, expect, beforeEach, vi } from 'vitest'; +import { getOdpManager, OdpManagerOptions } from './odp_manager_factory'; +import { RN_DEFAULT_API_TIMEOUT, RN_DEFAULT_BATCH_SIZE, RN_DEFAULT_FLUSH_INTERVAL, createOdpManager } from './odp_manager_factory.react_native'; +import { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler' +import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; + +describe('createOdpManager', () => { + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const mockGetOdpManager = vi.mocked(getOdpManager); + + beforeEach(() => { + MockBrowserRequestHandler.mockClear(); + mockGetOdpManager.mockClear(); + }); + + it('should use BrowserRequestHandler with the provided timeout as the segment request handler', () => { + const odpManager = createOdpManager({ segmentsApiTimeout: 3456 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(3456); + }); + + it('should use BrowserRequestHandler with the node default timeout as the segment request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { segmentRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(segmentRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[0]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[0][0]; + expect(requestHandlerOptions?.timeout).toBe(RN_DEFAULT_API_TIMEOUT); + }); + + it('should use BrowserRequestHandler with the provided timeout as the event request handler', () => { + const odpManager = createOdpManager({ eventApiTimeout: 2345 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(2345); + }); + + it('should use BrowserRequestHandler with the node default timeout as the event request handler', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestHandler } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestHandler).toBe(MockBrowserRequestHandler.mock.instances[1]); + const requestHandlerOptions = MockBrowserRequestHandler.mock.calls[1][0]; + expect(requestHandlerOptions?.timeout).toBe(RN_DEFAULT_API_TIMEOUT); + }); + + it('uses the event api request generator', () => { + const odpManager = createOdpManager({ }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventRequestGenerator } = mockGetOdpManager.mock.calls[0][0]; + expect(eventRequestGenerator).toBe(eventApiRequestGenerator); + }); + + it('should use the provided eventBatchSize', () => { + const odpManager = createOdpManager({ eventBatchSize: 99 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(99); + }); + + it('should use the react_native default eventBatchSize if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventBatchSize } = mockGetOdpManager.mock.calls[0][0]; + expect(eventBatchSize).toBe(RN_DEFAULT_BATCH_SIZE); + }); + + it('should use the provided eventFlushInterval', () => { + const odpManager = createOdpManager({ eventFlushInterval: 9999 }); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(9999); + }); + + it('should use the react_native default eventFlushInterval if none provided', () => { + const odpManager = createOdpManager({}); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + const { eventFlushInterval } = mockGetOdpManager.mock.calls[0][0]; + expect(eventFlushInterval).toBe(RN_DEFAULT_FLUSH_INTERVAL); + }); + + it('uses the passed options for relevant fields', () => { + const options: OdpManagerOptions = { + segmentsCache: {} as any, + segmentsCacheSize: 11, + segmentsCacheTimeout: 2025, + segmentManager: {} as any, + eventManager: {} as any, + userAgentParser: {} as any, + }; + const odpManager = createOdpManager(options); + expect(odpManager).toBe(mockGetOdpManager.mock.results[0].value); + expect(mockGetOdpManager).toHaveBeenNthCalledWith(1, expect.objectContaining(options)); + }); +}); diff --git a/lib/odp/odp_manager_factory.react_native.ts b/lib/odp/odp_manager_factory.react_native.ts index f1439690f..1126647e8 100644 --- a/lib/odp/odp_manager_factory.react_native.ts +++ b/lib/odp/odp_manager_factory.react_native.ts @@ -8,15 +8,13 @@ export const RN_DEFAULT_BATCH_SIZE = 10; export const RN_DEFAULT_FLUSH_INTERVAL = 1000; export const createOdpManager = (options: OdpManagerOptions): OdpManager => { - let defaultRequestHandler = new BrowserRequestHandler({ timeout: RN_DEFAULT_API_TIMEOUT }); - - const segmentRequestHandler = options.segmentsApiTimeout !== undefined ? - new BrowserRequestHandler({ timeout: options.segmentsApiTimeout }) : - defaultRequestHandler; + const segmentRequestHandler = new BrowserRequestHandler({ + timeout: options.segmentsApiTimeout || RN_DEFAULT_API_TIMEOUT, + }); - const eventRequestHandler = options.eventApiTimeout !== undefined ? - new BrowserRequestHandler({ timeout: options.eventApiTimeout }) : - defaultRequestHandler; + const eventRequestHandler = new BrowserRequestHandler({ + timeout: options.eventApiTimeout || RN_DEFAULT_API_TIMEOUT, + }); return getOdpManager({ ...options, diff --git a/vitest.config.mts b/vitest.config.mts index 05eca6e41..37d2eff3f 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_manager_factory.spec.ts'], + include: ['**/odp_manager_factory.react_native.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 25eaa78d0680c5e8b316776a07f2e9c6333b7b84 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Sat, 7 Dec 2024 01:36:19 +0600 Subject: [PATCH 16/28] vuidman test --- lib/odp/odp_manager.ts | 2 +- lib/optimizely/index.ts | 38 +----- lib/plugins/vuid_manager/index.ts | 111 ----------------- lib/shared_types.ts | 23 +--- lib/utils/fns/index.ts | 6 + lib/utils/lru_cache/browser_lru_cache.ts | 36 ------ lib/utils/lru_cache/cache_element.tests.ts | 53 -------- lib/utils/lru_cache/cache_element.ts | 42 ------- lib/utils/lru_cache/index.ts | 21 ---- lib/utils/lru_cache/server_lru_cache.ts | 36 ------ lib/vuid/vuid_manager.spec.ts | 133 +++++++++++++++++++++ lib/vuid/vuid_manager.ts | 92 ++++++++++++++ tests/vuidManager.spec.ts | 102 ---------------- vitest.config.mts | 2 +- 14 files changed, 241 insertions(+), 456 deletions(-) delete mode 100644 lib/plugins/vuid_manager/index.ts delete mode 100644 lib/utils/lru_cache/browser_lru_cache.ts delete mode 100644 lib/utils/lru_cache/cache_element.tests.ts delete mode 100644 lib/utils/lru_cache/cache_element.ts delete mode 100644 lib/utils/lru_cache/index.ts delete mode 100644 lib/utils/lru_cache/server_lru_cache.ts create mode 100644 lib/vuid/vuid_manager.spec.ts create mode 100644 lib/vuid/vuid_manager.ts delete mode 100644 tests/vuidManager.spec.ts diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 9388cd8eb..bb66cb201 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -25,7 +25,7 @@ import { OdpEvent } from './event_manager/odp_event'; import { resolvablePromise, ResolvablePromise } from '../utils/promise/resolvablePromise'; import { BaseService, Service, ServiceState } from '../service'; import { UserAgentParser } from './ua_parser/user_agent_parser'; -import { CLIENT_VERSION, ERROR_MESSAGES, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; +import { CLIENT_VERSION, JAVASCRIPT_CLIENT_ENGINE } from '../utils/enums'; import { ODP_DEFAULT_EVENT_TYPE, ODP_EVENT_ACTION, ODP_USER_KEY } from './constant'; import { isVuid } from '../vuid/vuid'; import { Maybe } from '../utils/type'; diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index a03f43728..2612bbd0c 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -181,7 +181,7 @@ export default class Optimizely implements Client { this.readyPromise = Promise.all([ projectConfigManagerRunningPromise, eventProcessorRunningPromise, - config.odpManager ? config.odpManager.onReady() : Promise.resolve(), + config.odpManager ? config.odpManager.onRunning() : Promise.resolve(), ]); this.readyTimeouts = {}; @@ -1226,13 +1226,10 @@ export default class Optimizely implements Client { */ close(): Promise<{ success: boolean; reason?: string }> { try { - if (this.odpManager) { - this.odpManager.stop(); - } - - this.notificationCenter.clearAllNotificationListeners(); - + this.projectConfigManager.stop(); this.eventProcessor?.stop(); + this.odpManager?.stop(); + this.notificationCenter.clearAllNotificationListeners(); const eventProcessorStoppedPromise = this.eventProcessor ? this.eventProcessor.onTerminated() : Promise.resolve(); @@ -1241,9 +1238,7 @@ export default class Optimizely implements Client { this.disposeOnUpdate(); this.disposeOnUpdate = undefined; } - if (this.projectConfigManager) { - this.projectConfigManager.stop(); - } + Object.keys(this.readyTimeouts).forEach((readyTimeoutId: string) => { const readyTimeoutRecord = this.readyTimeouts[readyTimeoutId]; clearTimeout(readyTimeoutRecord.readyTimeout); @@ -1651,29 +1646,8 @@ export default class Optimizely implements Client { return; } - const odpEventType = type ?? ODP_DEFAULT_EVENT_TYPE; - - const odpIdentifiers = new Map(identifiers); - - if (identifiers && identifiers.size > 0) { - try { - identifiers.forEach((identifier_value, identifier_key) => { - // Catch for fs-user-id, FS-USER-ID, and FS_USER_ID and assign value to fs_user_id identifier. - if ( - FS_USER_ID_ALIAS === identifier_key.toLowerCase() || - ODP_USER_KEY.FS_USER_ID === identifier_key.toLowerCase() - ) { - odpIdentifiers.delete(identifier_key); - odpIdentifiers.set(ODP_USER_KEY.FS_USER_ID, identifier_value); - } - }); - } catch (e) { - this.logger.warn(LOG_MESSAGES.ODP_SEND_EVENT_IDENTIFIER_CONVERSION_FAILED); - } - } - try { - const odpEvent = new OdpEvent(odpEventType, action, odpIdentifiers, data); + const odpEvent = new OdpEvent(type || '', action, identifiers, data); this.odpManager.sendEvent(odpEvent); } catch (e) { this.logger.error(ERROR_MESSAGES.ODP_EVENT_FAILED, e); diff --git a/lib/plugins/vuid_manager/index.ts b/lib/plugins/vuid_manager/index.ts deleted file mode 100644 index 10b3f48f5..000000000 --- a/lib/plugins/vuid_manager/index.ts +++ /dev/null @@ -1,111 +0,0 @@ -/** - * 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. - * 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 { uuid } from '../../utils/fns'; -import PersistentKeyValueCache from '../key_value_cache/persistentKeyValueCache'; - -export interface IVuidManager { - readonly vuid: string; -} - -/** - * Manager for creating, persisting, and retrieving a Visitor Unique Identifier - */ -export class VuidManager implements IVuidManager { - /** - * Unique key used within the persistent value cache against which to - * store the VUID - * @private - */ - private _keyForVuid = 'optimizely-vuid'; - - /** - * Current VUID value being used - * @private - */ - private _vuid: string; - - /** - * Get the current VUID value being used - */ - get vuid(): string { - return this._vuid; - } - - private constructor() { - this._vuid = ''; - } - - /** - * Instance of the VUID Manager - * @private - */ - private static _instance: VuidManager; - - /** - * Gets the current instance of the VUID Manager, initializing if needed - * @param cache Caching mechanism to use for persisting the VUID outside working memory * - * @returns An instance of VuidManager - */ - static async instance(cache: PersistentKeyValueCache): Promise { - if (!this._instance) { - this._instance = new VuidManager(); - } - - if (!this._instance._vuid) { - await this._instance.load(cache); - } - - return this._instance; - } - - /** - * Attempts to load a VUID from persistent cache or generates a new VUID - * @param cache Caching mechanism to use for persisting the VUID outside working memory - * @returns Current VUID stored in the VuidManager - * @private - */ - private async load(cache: PersistentKeyValueCache): Promise { - const cachedValue = await cache.get(this._keyForVuid); - if (cachedValue && VuidManager.isVuid(cachedValue)) { - this._vuid = cachedValue; - } else { - this._vuid = this.makeVuid(); - await this.save(this._vuid, cache); - } - - return this._vuid; - } - - /** - * Saves a VUID to a persistent cache - * @param vuid VUID to be stored - * @param cache Caching mechanism to use for persisting the VUID outside working memory - * @private - */ - private async save(vuid: string, cache: PersistentKeyValueCache): Promise { - await cache.set(this._keyForVuid, vuid); - } - - /** - * Function used in unit testing to reset the VuidManager - * **Important**: This should not to be used in production code - * @private - */ - private static _reset(): void { - this._instance._vuid = ''; - } -} diff --git a/lib/shared_types.ts b/lib/shared_types.ts index 568f2a9ba..ae287dc22 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -25,7 +25,6 @@ import { NotificationCenter, DefaultNotificationCenter } from './notification_ce import { IOptimizelyUserContext as OptimizelyUserContext } from './optimizely_user_context'; -import { ICache } from './utils/lru_cache'; import { RequestHandler } from './utils/http_request_handler/http'; import { OptimizelySegmentOption } from './odp/segment_manager/optimizely_segment_option'; import { OdpSegmentApiManager } from './odp/segment_manager/odp_segment_api_manager'; @@ -33,7 +32,6 @@ import { OdpSegmentManager } from './odp/segment_manager/odp_segment_manager'; import { DefaultOdpEventApiManager } from './odp/event_manager/odp_event_api_manager'; import { OdpEventManager } from './odp/event_manager/odp_event_manager'; import { OdpManager } from './odp/odp_manager'; -import { UserAgentParser } from './odp/ua_parser/user_agent_parser'; import PersistentCache from './plugins/key_value_cache/persistentKeyValueCache'; import { ProjectConfig } from './project_config/project_config'; import { ProjectConfigManager } from './project_config/project_config_manager'; @@ -44,6 +42,7 @@ export { EventDispatcher } from './event_processor/event_dispatcher/event_dispat export { EventProcessor } from './event_processor/event_processor'; export { NotificationCenter } from './notification_center'; +export { OdpManager } from './odp/odp_manager'; export interface BucketerParams { experimentId: string; experimentKey: string; @@ -99,23 +98,6 @@ export interface DatafileOptions { datafileAccessToken?: string; } -export interface OdpOptions { - disabled?: boolean; - segmentsCache?: ICache; - segmentsCacheSize?: number; - segmentsCacheTimeout?: number; - segmentsApiTimeout?: number; - segmentsRequestHandler?: RequestHandler; - segmentManager?: OdpSegmentManager; - eventFlushInterval?: number; - eventBatchSize?: number; - eventQueueSize?: number; - eventApiTimeout?: number; - eventRequestHandler?: RequestHandler; - eventManager?: OdpEventManager; - userAgentParser?: UserAgentParser; -} - export interface ListenerPayload { userId: string; attributes?: UserAttributes; @@ -386,7 +368,6 @@ export interface Config extends ConfigLite { // eventFlushInterval?: number; // Maximum time for an event to be enqueued // eventMaxQueueSize?: number; // Maximum size for the event queue sdkKey?: string; - odpOptions?: OdpOptions; persistentCacheProvider?: PersistentCacheProvider; } @@ -417,6 +398,7 @@ export interface ConfigLite { clientEngine?: string; clientVersion?: string; isSsr?: boolean; + odpManager?: OdpManager; } export type OptimizelyExperimentsMap = { @@ -539,7 +521,6 @@ export interface OptimizelyForcedDecision { // ODP Exports export { - ICache, RequestHandler, OptimizelySegmentOption, OdpSegmentApiManager as IOdpSegmentApiManager, diff --git a/lib/utils/fns/index.ts b/lib/utils/fns/index.ts index f66d9d56a..e53402a22 100644 --- a/lib/utils/fns/index.ts +++ b/lib/utils/fns/index.ts @@ -57,6 +57,11 @@ export function keyBy(arr: K[], key: string): { [key: string]: K } { }); } + +function isNumber(value: unknown): boolean { + return typeof value === 'number'; +} + export function uuid(): string { return v4(); } @@ -166,6 +171,7 @@ export default { isSafeInteger, keyBy, uuid, + isNumber, getTimestamp, isValidEnum, groupBy, diff --git a/lib/utils/lru_cache/browser_lru_cache.ts b/lib/utils/lru_cache/browser_lru_cache.ts deleted file mode 100644 index c9e6cc6ff..000000000 --- a/lib/utils/lru_cache/browser_lru_cache.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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. - * 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 LRUCache, { ISegmentsCacheConfig } from '../cache/in_memory_lru_cache'; - -export interface BrowserLRUCacheConfig { - maxSize?: number; - timeout?: number; -} - -export const BrowserLRUCacheConfig: ISegmentsCacheConfig = { - DEFAULT_CAPACITY: 100, - DEFAULT_TIMEOUT_SECS: 600, -}; - -export class BrowserLRUCache extends LRUCache { - constructor(config?: BrowserLRUCacheConfig) { - super({ - maxSize: config?.maxSize?? BrowserLRUCacheConfig.DEFAULT_CAPACITY, - timeout: config?.timeout?? BrowserLRUCacheConfig.DEFAULT_TIMEOUT_SECS * 1000, - }); - } -} diff --git a/lib/utils/lru_cache/cache_element.tests.ts b/lib/utils/lru_cache/cache_element.tests.ts deleted file mode 100644 index dfba16fa7..000000000 --- a/lib/utils/lru_cache/cache_element.tests.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright 2022, 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 { assert } from 'chai'; -import { CacheElement } from './cache_element'; - -const sleep = async (ms: number) => { - return await new Promise(r => setTimeout(r, ms)); -}; - -describe('/odp/lru_cache/CacheElement', () => { - let element: CacheElement; - - beforeEach(() => { - element = new CacheElement('foo'); - }); - - it('should initialize a valid CacheElement', () => { - assert.exists(element); - assert.equal(element.value, 'foo'); - assert.isNotNull(element.time); - assert.doesNotThrow(() => element.is_stale(0)); - }); - - it('should return false if not stale based on timeout', () => { - const timeoutLong = 1000; - assert.equal(element.is_stale(timeoutLong), false); - }); - - it('should return false if not stale because timeout is less than or equal to 0', () => { - const timeoutNone = 0; - assert.equal(element.is_stale(timeoutNone), false); - }); - - it('should return true if stale based on timeout', async () => { - await sleep(100); - const timeoutShort = 1; - assert.equal(element.is_stale(timeoutShort), true); - }); -}); diff --git a/lib/utils/lru_cache/cache_element.ts b/lib/utils/lru_cache/cache_element.ts deleted file mode 100644 index c286aab7a..000000000 --- a/lib/utils/lru_cache/cache_element.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Copyright 2022, 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. - */ - -/** - * CacheElement represents an individual generic item within the LRUCache - */ -export class CacheElement { - private _value: V | null; - private _time: number; - - get value(): V | null { - return this._value; - } - get time(): number { - return this._time; - } - - constructor(value: V | null = null) { - this._value = value; - this._time = Date.now(); - } - - public is_stale(timeout: number): boolean { - if (timeout <= 0) return false; - return Date.now() - this._time >= timeout; - } -} - -export default CacheElement; diff --git a/lib/utils/lru_cache/index.ts b/lib/utils/lru_cache/index.ts deleted file mode 100644 index 681120d17..000000000 --- a/lib/utils/lru_cache/index.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * Copyright 2022, 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 { ICache, LRUCache } from '../cache/in_memory_lru_cache'; -import { BrowserLRUCache } from './browser_lru_cache'; -import { ServerLRUCache } from './server_lru_cache'; - -export { ICache, LRUCache, BrowserLRUCache, ServerLRUCache }; diff --git a/lib/utils/lru_cache/server_lru_cache.ts b/lib/utils/lru_cache/server_lru_cache.ts deleted file mode 100644 index 3956c4f9c..000000000 --- a/lib/utils/lru_cache/server_lru_cache.ts +++ /dev/null @@ -1,36 +0,0 @@ -/** - * 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. - * 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 LRUCache, { ISegmentsCacheConfig } from '../cache/in_memory_lru_cache'; - -export interface ServerLRUCacheConfig { - maxSize?: number; - timeout?: number; -} - -export const ServerLRUCacheConfig: ISegmentsCacheConfig = { - DEFAULT_CAPACITY: 10000, - DEFAULT_TIMEOUT_SECS: 600, -}; - -export class ServerLRUCache extends LRUCache { - constructor(config?: ServerLRUCacheConfig) { - super({ - maxSize: config?.maxSize?? ServerLRUCacheConfig.DEFAULT_CAPACITY, - timeout: config?.timeout?? ServerLRUCacheConfig.DEFAULT_TIMEOUT_SECS * 1000, - }); - } -} diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts new file mode 100644 index 000000000..25b5d4000 --- /dev/null +++ b/lib/vuid/vuid_manager.spec.ts @@ -0,0 +1,133 @@ +/** + * Copyright 2022, 2024, 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, expect, vi } from 'vitest'; + +import { VuidManager } from './vuid_manager' +import { getMockAsyncCache, getMockSyncCache } from '../tests/mock/mock_cache'; +import { isVuid } from './vuid'; +import { resolvablePromise } from '../utils/promise/resolvablePromise'; +import { exhaustMicrotasks } from '../tests/testUtils'; + +const vuidCacheKey = 'optimizely-vuid'; + +describe('VuidManager', () => {; + describe('when configured with enableVuid = true', () => { + it('should create and save a new vuid if there is no vuid in cache', async () => { + const cache = getMockSyncCache(); + const manager = new VuidManager(cache); + + await manager.configure({ enableVuid: true }); + + const savedVuid = cache.get(vuidCacheKey); + expect(isVuid(manager.getVuid()!)).toBe(true); + expect(savedVuid).toBe(manager.getVuid()); + }); + + it('should create and save a new vuid if old VUID from cache is not valid', async () => { + const cache = getMockSyncCache(); + cache.set(vuidCacheKey, 'invalid-vuid'); + + const manager = new VuidManager(cache); + await manager.configure({ enableVuid: true }); + + const savedVuid = cache.get(vuidCacheKey); + expect(isVuid(manager.getVuid()!)).toBe(true); + expect(savedVuid).toBe(manager.getVuid()); + }); + + it('should use the vuid in cache if available', async () => { + const cache = getMockSyncCache(); + cache.set(vuidCacheKey, 'vuid_valid'); + + const manager = new VuidManager(cache); + await manager.configure({ enableVuid: true }); + + const savedVuid = cache.get(vuidCacheKey); + expect(isVuid(manager.getVuid()!)).toBe(true); + expect(savedVuid).toBe(manager.getVuid()); + expect(savedVuid).toBe('vuid_valid'); + }); + }); + + describe('when configured with enableVuid = false', () => { + it('should remove existing vuid form memory and cache', async () => { + const cache = getMockSyncCache(); + const manager = new VuidManager(cache); + + await manager.configure({ enableVuid: true }); + + const savedVuid = cache.get(vuidCacheKey); + expect(isVuid(manager.getVuid()!)).toBe(true); + expect(savedVuid).toBe(manager.getVuid()); + + await manager.configure({ enableVuid: false }); + expect(manager.getVuid()).toBeUndefined(); + expect(cache.get(vuidCacheKey)).toBeUndefined(); + }); + }); + + it('should sequence configure calls', async() => { + const cache = getMockAsyncCache(); + const removeSpy = vi.spyOn(cache, 'remove'); + const getSpy = vi.spyOn(cache, 'get'); + const setSpy = vi.spyOn(cache, 'set'); + + const removePromise = resolvablePromise(); + removeSpy.mockReturnValueOnce(removePromise.promise); + + const getPromise = resolvablePromise(); + getSpy.mockReturnValueOnce(getPromise.promise); + + const setPromise = resolvablePromise(); + setSpy.mockReturnValueOnce(setPromise.promise); + + const manager = new VuidManager(cache); + + // this should try to remove vuid, which should stay pending + const configure1 = manager.configure({ enableVuid: false }); + + // this should try to get the vuid from store + const configure2 = manager.configure({ enableVuid: true }); + + // this should again try to remove vuid + const configure3 = manager.configure({ enableVuid: false }); + + await exhaustMicrotasks(); + + expect(removeSpy).toHaveBeenCalledTimes(1); // from the first configure call + expect(getSpy).not.toHaveBeenCalled(); + + // this will resolve the first configure call + removePromise.resolve(true); + await exhaustMicrotasks(); + await expect(configure1).resolves.not.toThrow(); + + // this get call is from the second configure call + expect(getSpy).toHaveBeenCalledTimes(1); + await exhaustMicrotasks(); + + // as the get call is pending, remove call from the third configure call should not yet happen + expect(removeSpy).toHaveBeenCalledTimes(1); + + // this should fail the second configure call, allowing the third configure call to proceed + getPromise.reject(new Error('get failed')); + await exhaustMicrotasks(); + await expect(configure2).rejects.toThrow(); + + expect(removeSpy).toHaveBeenCalledTimes(2); + }); +}); diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts new file mode 100644 index 000000000..4c83bbae1 --- /dev/null +++ b/lib/vuid/vuid_manager.ts @@ -0,0 +1,92 @@ +/** + * Copyright 2022-2024, 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 { LogHandler } from '../modules/logging'; +import { Cache } from '../utils/cache/cache'; +import { isVuid, makeVuid } from './vuid'; + +export type VuidManagerOptions = { + enableVuid: boolean; +} + +export class VuidManager { + private logger?: LogHandler; + private vuidCacheKey = 'optimizely-vuid'; + private vuid?: string; + private vuidEnabled = false; + private cache: Cache; + private waitPromise: Promise = Promise.resolve(); + + getVuid(): string | undefined { + return this.vuid; + } + + isVuidEnabled(): boolean { + return this.vuidEnabled; + } + + constructor(cache: Cache, logger?: LogHandler) { + this.cache = cache; + this.logger = logger; + } + + setLogger(logger: LogHandler): void { + this.logger = logger; + } + + /** + * Configures the VuidManager + * @returns Promise that resolves when the VuidManager is configured + */ + async configure(options: VuidManagerOptions): Promise { + const configureFn = async () => { + this.vuidEnabled = options.enableVuid; + + if (!this.vuidEnabled) { + await this.cache.remove(this.vuidCacheKey); + this.vuid = undefined; + return; + } + + if (!this.vuid) { + await this.initializeVuid(); + } + } + + this.waitPromise = this.waitPromise.then(configureFn, configureFn); + this.waitPromise.catch(() => {}); + return this.waitPromise; + } + + /** + * Attempts to load a VUID from persistent cache or generates a new VUID + * and saves it in the cache + * @private + */ + private async initializeVuid(): Promise { + const cachedValue = await this.cache.get(this.vuidCacheKey); + if (cachedValue && isVuid(cachedValue)) { + this.vuid = cachedValue; + } else { + await this.save(makeVuid()); + } + } + + private async save(vuid: string): Promise { + await this.cache.set(this.vuidCacheKey, vuid); + this.vuid = vuid; + } +} diff --git a/tests/vuidManager.spec.ts b/tests/vuidManager.spec.ts deleted file mode 100644 index 2f412fe02..000000000 --- a/tests/vuidManager.spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright 2022, 2024, 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, beforeEach, beforeAll, it, expect } from 'vitest'; - -import { VuidManager } from '../lib/plugins/vuid_manager'; -import PersistentKeyValueCache from '../lib/plugins/key_value_cache/persistentKeyValueCache'; -import { anyString, anything, instance, mock, resetCalls, verify, when } from 'ts-mockito'; - -describe('VuidManager', () => { - let mockCache: PersistentKeyValueCache; - - beforeAll(() => { - mockCache = mock(); - when(mockCache.contains(anyString())).thenResolve(true); - when(mockCache.get(anyString())).thenResolve(''); - when(mockCache.remove(anyString())).thenResolve(true); - when(mockCache.set(anyString(), anything())).thenResolve(); - VuidManager.instance(instance(mockCache)); - }); - - beforeEach(() => { - resetCalls(mockCache); - VuidManager['_reset'](); - }); - - it('should make a VUID', async () => { - const manager = await VuidManager.instance(instance(mockCache)); - - const vuid = manager['makeVuid'](); - - expect(vuid.startsWith('vuid_')).toBe(true); - expect(vuid.length).toEqual(32); - expect(vuid).not.toContain('-'); - }); - - it('should test if a VUID is valid', async () => { - const manager = await VuidManager.instance(instance(mockCache)); - - expect(VuidManager.isVuid('vuid_123')).toBe(true); - expect(VuidManager.isVuid('vuid-123')).toBe(false); - expect(VuidManager.isVuid('123')).toBe(false); - }); - - it('should auto-save and auto-load', async () => { - const cache = instance(mockCache); - - await cache.remove('optimizely-odp'); - - const manager1 = await VuidManager.instance(cache); - const vuid1 = manager1.vuid; - - const manager2 = await VuidManager.instance(cache); - const vuid2 = manager2.vuid; - - expect(vuid1).toStrictEqual(vuid2); - expect(VuidManager.isVuid(vuid1)).toBe(true); - expect(VuidManager.isVuid(vuid2)).toBe(true); - - await cache.remove('optimizely-odp'); - - // should end up being a new instance since we just removed it above - await manager2['load'](cache); - const vuid3 = manager2.vuid; - - expect(vuid3).not.toStrictEqual(vuid1); - expect(VuidManager.isVuid(vuid3)).toBe(true); - }); - - it('should handle no valid optimizely-vuid in the cache', async () => { - when(mockCache.get(anyString())).thenResolve(undefined); - - const manager = await VuidManager.instance(instance(mockCache)); // load() called initially - - verify(mockCache.get(anyString())).once(); - verify(mockCache.set(anyString(), anything())).once(); - expect(VuidManager.isVuid(manager.vuid)).toBe(true); - }); - - it('should create a new vuid if old VUID from cache is not valid', async () => { - when(mockCache.get(anyString())).thenResolve('vuid-not-valid'); - - const manager = await VuidManager.instance(instance(mockCache)); - - verify(mockCache.get(anyString())).once(); - verify(mockCache.set(anyString(), anything())).once(); - expect(VuidManager.isVuid(manager.vuid)).toBe(true); - }); -}); diff --git a/vitest.config.mts b/vitest.config.mts index 37d2eff3f..7163ecc27 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/odp_manager_factory.react_native.spec.ts'], + include: ['**/vuid_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From e808f8785799ea35174c0b85efe33d00a2e5e633 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Sat, 7 Dec 2024 02:40:23 +0600 Subject: [PATCH 17/28] vuidman --- lib/vuid/vuid_manager.spec.ts | 14 +-- lib/vuid/vuid_manager.ts | 119 ++++++++++++++--------- lib/vuid/vuid_manager_factory.browser.ts | 27 +++++ lib/vuid/vuid_manager_factory.ts | 3 + 4 files changed, 111 insertions(+), 52 deletions(-) create mode 100644 lib/vuid/vuid_manager_factory.browser.ts create mode 100644 lib/vuid/vuid_manager_factory.ts diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts index 25b5d4000..d60be3338 100644 --- a/lib/vuid/vuid_manager.spec.ts +++ b/lib/vuid/vuid_manager.spec.ts @@ -16,7 +16,7 @@ import { describe, it, expect, vi } from 'vitest'; -import { VuidManager } from './vuid_manager' +import { DefaultVuidManager } from './vuid_manager' import { getMockAsyncCache, getMockSyncCache } from '../tests/mock/mock_cache'; import { isVuid } from './vuid'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; @@ -24,11 +24,11 @@ import { exhaustMicrotasks } from '../tests/testUtils'; const vuidCacheKey = 'optimizely-vuid'; -describe('VuidManager', () => {; +describe('DefaultVuidManager', () => {; describe('when configured with enableVuid = true', () => { it('should create and save a new vuid if there is no vuid in cache', async () => { const cache = getMockSyncCache(); - const manager = new VuidManager(cache); + const manager = new DefaultVuidManager(cache); await manager.configure({ enableVuid: true }); @@ -41,7 +41,7 @@ describe('VuidManager', () => {; const cache = getMockSyncCache(); cache.set(vuidCacheKey, 'invalid-vuid'); - const manager = new VuidManager(cache); + const manager = new DefaultVuidManager(cache); await manager.configure({ enableVuid: true }); const savedVuid = cache.get(vuidCacheKey); @@ -53,7 +53,7 @@ describe('VuidManager', () => {; const cache = getMockSyncCache(); cache.set(vuidCacheKey, 'vuid_valid'); - const manager = new VuidManager(cache); + const manager = new DefaultVuidManager(cache); await manager.configure({ enableVuid: true }); const savedVuid = cache.get(vuidCacheKey); @@ -66,7 +66,7 @@ describe('VuidManager', () => {; describe('when configured with enableVuid = false', () => { it('should remove existing vuid form memory and cache', async () => { const cache = getMockSyncCache(); - const manager = new VuidManager(cache); + const manager = new DefaultVuidManager(cache); await manager.configure({ enableVuid: true }); @@ -95,7 +95,7 @@ describe('VuidManager', () => {; const setPromise = resolvablePromise(); setSpy.mockReturnValueOnce(setPromise.promise); - const manager = new VuidManager(cache); + const manager = new DefaultVuidManager(cache); // this should try to remove vuid, which should stay pending const configure1 = manager.configure({ enableVuid: false }); diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts index 4c83bbae1..a273fe092 100644 --- a/lib/vuid/vuid_manager.ts +++ b/lib/vuid/vuid_manager.ts @@ -13,31 +13,27 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { LogHandler } from '../modules/logging'; import { Cache } from '../utils/cache/cache'; +import { AsyncProducer, Maybe } from '../utils/type'; import { isVuid, makeVuid } from './vuid'; -export type VuidManagerOptions = { - enableVuid: boolean; +export interface VuidManager { + getVuid(): Maybe; + isVuidEnabled(): boolean; + initialize(): Promise; } -export class VuidManager { +export class VuidCacheManager { private logger?: LogHandler; private vuidCacheKey = 'optimizely-vuid'; - private vuid?: string; - private vuidEnabled = false; private cache: Cache; + // if this value is not undefined, this means the same value is in the cache + // if this is undefined, it could either mean that there is no value in the cache + // or that there is a value in the cache but it has not been loaded yet + private vuid?: string; private waitPromise: Promise = Promise.resolve(); - getVuid(): string | undefined { - return this.vuid; - } - - isVuidEnabled(): boolean { - return this.vuidEnabled; - } - constructor(cache: Cache, logger?: LogHandler) { this.cache = cache; this.logger = logger; @@ -47,46 +43,79 @@ export class VuidManager { this.logger = logger; } - /** - * Configures the VuidManager - * @returns Promise that resolves when the VuidManager is configured - */ - async configure(options: VuidManagerOptions): Promise { - const configureFn = async () => { - this.vuidEnabled = options.enableVuid; - - if (!this.vuidEnabled) { - await this.cache.remove(this.vuidCacheKey); - this.vuid = undefined; - return; - } - - if (!this.vuid) { - await this.initializeVuid(); - } + async serialize(fn: AsyncProducer): Promise { + const resultPromise = this.waitPromise.then(fn, fn); + this.waitPromise = resultPromise.catch(() => {}); + return resultPromise; + } + + async remove(): Promise { + const removeFn = async () => { + this.vuid = undefined; + await this.cache.remove(this.vuidCacheKey); } - this.waitPromise = this.waitPromise.then(configureFn, configureFn); - this.waitPromise.catch(() => {}); - return this.waitPromise; + return this.serialize(removeFn); } - /** - * Attempts to load a VUID from persistent cache or generates a new VUID - * and saves it in the cache - * @private - */ - private async initializeVuid(): Promise { + async load(): Promise { + if (this.vuid) { + return this.vuid; + } + const cachedValue = await this.cache.get(this.vuidCacheKey); if (cachedValue && isVuid(cachedValue)) { this.vuid = cachedValue; - } else { - await this.save(makeVuid()); + return this.vuid; + } + + const saveFn = async () => { + const newVuid = makeVuid(); + await this.cache.set(this.vuidCacheKey, newVuid); + this.vuid = newVuid; + return newVuid; } + return this.serialize(saveFn); } +} + +export type VuidManagerConfig = { + enableVuid?: boolean; + vuidCacheManager: VuidCacheManager; +} + +export class DefaultVuidManger implements VuidManager { + private vuidCacheManager: VuidCacheManager; + private logger?: LogHandler; + private vuid?: string; + private vuidEnabled = false; + private initialized = false; + + constructor(config: VuidManagerConfig) { + this.vuidCacheManager = config.vuidCacheManager; + this.vuidEnabled = config.enableVuid || false; + } + + getVuid(): Maybe { + return this.vuid; + } + + isVuidEnabled(): boolean { + return this.vuidEnabled; + } + + /** + * initializes the VuidManager + * @returns Promise that resolves when the VuidManager is initialized + */ + async initialize(): Promise { + if (!this.vuidEnabled) { + await this.vuidCacheManager.remove(); + this.initialized = true; + return; + } - private async save(vuid: string): Promise { - await this.cache.set(this.vuidCacheKey, vuid); - this.vuid = vuid; + this.vuid = await this.vuidCacheManager.load(); + this.initialized = true; } } diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts new file mode 100644 index 000000000..366dff0a3 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -0,0 +1,27 @@ +/** +* Copyright 2024, 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 { DefaultVuidManger, VuidCacheManager, VuidManager } from './vuid_manager'; +import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; +import { VuidManagerOptions } from './vuid_manager_factory'; + +export const vuidCacheManager = new VuidCacheManager(new LocalStorageCache()); + +export const createVuidManager = (options: VuidManagerOptions): VuidManager => { + return new DefaultVuidManger({ + vuidCacheManager, + enableVuid: options.enableVuid + }); +} diff --git a/lib/vuid/vuid_manager_factory.ts b/lib/vuid/vuid_manager_factory.ts new file mode 100644 index 000000000..27c5ae948 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.ts @@ -0,0 +1,3 @@ +export type VuidManagerOptions = { + enableVuid: boolean; +} From c2a2c07e47ec334496de5f19afefe47c8e11dce3 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 9 Dec 2024 19:57:08 +0600 Subject: [PATCH 18/28] vuidman test --- lib/vuid/vuid_manager.spec.ts | 181 ++++++++++++++++------- lib/vuid/vuid_manager.ts | 36 ++--- lib/vuid/vuid_manager_factory.browser.ts | 4 +- 3 files changed, 142 insertions(+), 79 deletions(-) diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts index d60be3338..b579dffd2 100644 --- a/lib/vuid/vuid_manager.spec.ts +++ b/lib/vuid/vuid_manager.spec.ts @@ -16,71 +16,72 @@ import { describe, it, expect, vi } from 'vitest'; -import { DefaultVuidManager } from './vuid_manager' -import { getMockAsyncCache, getMockSyncCache } from '../tests/mock/mock_cache'; +import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; + +import { getMockAsyncCache } from '../tests/mock/mock_cache'; import { isVuid } from './vuid'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { exhaustMicrotasks } from '../tests/testUtils'; const vuidCacheKey = 'optimizely-vuid'; -describe('DefaultVuidManager', () => {; - describe('when configured with enableVuid = true', () => { - it('should create and save a new vuid if there is no vuid in cache', async () => { - const cache = getMockSyncCache(); - const manager = new DefaultVuidManager(cache); - - await manager.configure({ enableVuid: true }); - - const savedVuid = cache.get(vuidCacheKey); - expect(isVuid(manager.getVuid()!)).toBe(true); - expect(savedVuid).toBe(manager.getVuid()); - }); - - it('should create and save a new vuid if old VUID from cache is not valid', async () => { - const cache = getMockSyncCache(); - cache.set(vuidCacheKey, 'invalid-vuid'); +describe('VuidCacheManager', () => { + it('should remove vuid from cache', async () => { + const cache = getMockAsyncCache(); + await cache.set(vuidCacheKey, 'vuid_valid'); - const manager = new DefaultVuidManager(cache); - await manager.configure({ enableVuid: true }); - - const savedVuid = cache.get(vuidCacheKey); - expect(isVuid(manager.getVuid()!)).toBe(true); - expect(savedVuid).toBe(manager.getVuid()); - }); + const manager = new VuidCacheManager(cache); + await manager.remove(); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBeUndefined(); + }); - it('should use the vuid in cache if available', async () => { - const cache = getMockSyncCache(); - cache.set(vuidCacheKey, 'vuid_valid'); + it('should create and save a new vuid if there is no vuid in cache', async () => { + const cache = getMockAsyncCache(); - const manager = new DefaultVuidManager(cache); - await manager.configure({ enableVuid: true }); + const manager = new VuidCacheManager(cache); + const vuid = await manager.load(); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe(vuid); + expect(isVuid(vuid)).toBe(true); + }); - const savedVuid = cache.get(vuidCacheKey); - expect(isVuid(manager.getVuid()!)).toBe(true); - expect(savedVuid).toBe(manager.getVuid()); - expect(savedVuid).toBe('vuid_valid'); - }); + it('should create and save a new vuid if old VUID from cache is not valid', async () => { + const cache = getMockAsyncCache(); + await cache.set(vuidCacheKey, 'invalid-vuid'); + + const manager = new VuidCacheManager(cache); + const vuid = await manager.load(); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe(vuid); + expect(isVuid(vuid)).toBe(true); }); - describe('when configured with enableVuid = false', () => { - it('should remove existing vuid form memory and cache', async () => { - const cache = getMockSyncCache(); - const manager = new DefaultVuidManager(cache); + it('should return the same vuid without modifying the cache after creating a new vuid', async () => { + const cache = getMockAsyncCache(); - await manager.configure({ enableVuid: true }); - - const savedVuid = cache.get(vuidCacheKey); - expect(isVuid(manager.getVuid()!)).toBe(true); - expect(savedVuid).toBe(manager.getVuid()); + const manager = new VuidCacheManager(cache); + const vuid1 = await manager.load(); + const vuid2 = await manager.load(); + expect(vuid1).toBe(vuid2); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe(vuid1); + }); - await manager.configure({ enableVuid: false }); - expect(manager.getVuid()).toBeUndefined(); - expect(cache.get(vuidCacheKey)).toBeUndefined(); - }); + it('should use the vuid in cache if available', async () => { + const cache = getMockAsyncCache(); + cache.set(vuidCacheKey, 'vuid_valid'); + + const manager = new VuidCacheManager(cache); + const vuid1 = await manager.load(); + const vuid2 = await manager.load(); + expect(vuid1).toBe('vuid_valid'); + expect(vuid1).toBe('vuid_valid'); + const vuidInCache = await cache.get(vuidCacheKey); + expect(vuidInCache).toBe('vuid_valid'); }); - it('should sequence configure calls', async() => { + it('should sequence remove and load calls', async() => { const cache = getMockAsyncCache(); const removeSpy = vi.spyOn(cache, 'remove'); const getSpy = vi.spyOn(cache, 'get'); @@ -95,16 +96,16 @@ describe('DefaultVuidManager', () => {; const setPromise = resolvablePromise(); setSpy.mockReturnValueOnce(setPromise.promise); - const manager = new DefaultVuidManager(cache); + const manager = new VuidCacheManager(cache); - // this should try to remove vuid, which should stay pending - const configure1 = manager.configure({ enableVuid: false }); + // this should try to remove from cached, which should stay pending + const call1 = manager.remove(); // this should try to get the vuid from store - const configure2 = manager.configure({ enableVuid: true }); + const call2 = manager.load(); // this should again try to remove vuid - const configure3 = manager.configure({ enableVuid: false }); + const call3 = manager.remove(); await exhaustMicrotasks(); @@ -114,7 +115,7 @@ describe('DefaultVuidManager', () => {; // this will resolve the first configure call removePromise.resolve(true); await exhaustMicrotasks(); - await expect(configure1).resolves.not.toThrow(); + await expect(call1).resolves.not.toThrow(); // this get call is from the second configure call expect(getSpy).toHaveBeenCalledTimes(1); @@ -123,11 +124,77 @@ describe('DefaultVuidManager', () => {; // as the get call is pending, remove call from the third configure call should not yet happen expect(removeSpy).toHaveBeenCalledTimes(1); - // this should fail the second configure call, allowing the third configure call to proceed + // this should fail the load call, allowing the second remnove call to proceed getPromise.reject(new Error('get failed')); await exhaustMicrotasks(); - await expect(configure2).rejects.toThrow(); + await expect(call2).rejects.toThrow(); expect(removeSpy).toHaveBeenCalledTimes(2); }); }); + +describe('DefaultVuidManager', () => { + it('should return undefined for getVuid() before initialization', async () => { + const vuidCacheManager ={ + remove: vi.fn(), + load: vi.fn(), + } as unknown as VuidCacheManager; + + const manager = new DefaultVuidManager({ + vuidCacheManager, + enableVuid: true + }); + + expect(manager.getVuid()).toBeUndefined(); + }); + + it('should call remove on VuidCacheManager if enableVuid is false', async () => { + const vuidCacheManager ={ + remove: vi.fn(), + load: vi.fn(), + } as unknown as VuidCacheManager; + + const manager = new DefaultVuidManager({ + vuidCacheManager, + enableVuid: false + }); + + await manager.initialize(); + expect(vuidCacheManager.remove).toHaveBeenCalled(); + }); + + it('should return undefined for getVuid() after initialization if enableVuid is false', async () => { + const vuidCacheManager ={ + remove: vi.fn(), + load: vi.fn(), + } as unknown as VuidCacheManager; + + const manager = new DefaultVuidManager({ + vuidCacheManager, + enableVuid: false + }); + + await manager.initialize(); + expect(manager.getVuid()).toBeUndefined(); + }); + + it('should load vuid using VuidCacheManger if enableVuid=true', async () => { + const load = vi.fn(); + + const vuidCacheManager ={ + remove: vi.fn(), + load, + } as unknown as VuidCacheManager; + + load.mockResolvedValue('vuid_valid'); + + const manager = new DefaultVuidManager({ + vuidCacheManager, + enableVuid: true + }); + + await manager.initialize(); + expect(vuidCacheManager.load).toHaveBeenCalled(); + expect(manager.getVuid()).toBe('vuid_valid'); + }); +}); diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts index a273fe092..d49eeb600 100644 --- a/lib/vuid/vuid_manager.ts +++ b/lib/vuid/vuid_manager.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LogHandler } from '../modules/logging'; +import { LoggerFacade } from '../modules/logging'; import { Cache } from '../utils/cache/cache'; import { AsyncProducer, Maybe } from '../utils/type'; import { isVuid, makeVuid } from './vuid'; @@ -25,25 +25,26 @@ export interface VuidManager { } export class VuidCacheManager { - private logger?: LogHandler; + private logger?: LoggerFacade; private vuidCacheKey = 'optimizely-vuid'; private cache: Cache; - // if this value is not undefined, this means the same value is in the cache + // if this value is not undefined, this means the same value is in the cache. // if this is undefined, it could either mean that there is no value in the cache - // or that there is a value in the cache but it has not been loaded yet + // or that there is a value in the cache but it has not been loaded yet or failed + // to load. private vuid?: string; private waitPromise: Promise = Promise.resolve(); - constructor(cache: Cache, logger?: LogHandler) { + constructor(cache: Cache, logger?: LoggerFacade) { this.cache = cache; this.logger = logger; } - setLogger(logger: LogHandler): void { + setLogger(logger: LoggerFacade): void { this.logger = logger; } - async serialize(fn: AsyncProducer): Promise { + private async serialize(fn: AsyncProducer): Promise { const resultPromise = this.waitPromise.then(fn, fn); this.waitPromise = resultPromise.catch(() => {}); return resultPromise; @@ -63,19 +64,18 @@ export class VuidCacheManager { return this.vuid; } - const cachedValue = await this.cache.get(this.vuidCacheKey); - if (cachedValue && isVuid(cachedValue)) { - this.vuid = cachedValue; - return this.vuid; - } - - const saveFn = async () => { + const loadFn = async () => { + const cachedValue = await this.cache.get(this.vuidCacheKey); + if (cachedValue && isVuid(cachedValue)) { + this.vuid = cachedValue; + return this.vuid; + } const newVuid = makeVuid(); await this.cache.set(this.vuidCacheKey, newVuid); this.vuid = newVuid; return newVuid; } - return this.serialize(saveFn); + return this.serialize(loadFn); } } @@ -84,12 +84,10 @@ export type VuidManagerConfig = { vuidCacheManager: VuidCacheManager; } -export class DefaultVuidManger implements VuidManager { +export class DefaultVuidManager implements VuidManager { private vuidCacheManager: VuidCacheManager; - private logger?: LogHandler; private vuid?: string; private vuidEnabled = false; - private initialized = false; constructor(config: VuidManagerConfig) { this.vuidCacheManager = config.vuidCacheManager; @@ -111,11 +109,9 @@ export class DefaultVuidManger implements VuidManager { async initialize(): Promise { if (!this.vuidEnabled) { await this.vuidCacheManager.remove(); - this.initialized = true; return; } this.vuid = await this.vuidCacheManager.load(); - this.initialized = true; } } diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts index 366dff0a3..89e6cbf08 100644 --- a/lib/vuid/vuid_manager_factory.browser.ts +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -13,14 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DefaultVuidManger, VuidCacheManager, VuidManager } from './vuid_manager'; +import { DefaultVuidMaanger, VuidCacheManager, VuidManager } from './vuid_manager'; import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { VuidManagerOptions } from './vuid_manager_factory'; export const vuidCacheManager = new VuidCacheManager(new LocalStorageCache()); export const createVuidManager = (options: VuidManagerOptions): VuidManager => { - return new DefaultVuidManger({ + return new DefaultVuidMaanger({ vuidCacheManager, enableVuid: options.enableVuid }); From c752285b9a0a58736b71cd8eb36b3eed843e2340 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 9 Dec 2024 21:47:49 +0600 Subject: [PATCH 19/28] more refactor --- lib/vuid/vuid_manager.spec.ts | 21 ++++++- lib/vuid/vuid_manager.ts | 17 ++++- lib/vuid/vuid_manager_factory.browser.spec.ts | 63 +++++++++++++++++++ lib/vuid/vuid_manager_factory.browser.ts | 8 ++- lib/vuid/vuid_manager_factory.node.spec.ts | 10 +++ lib/vuid/vuid_manager_factory.node.ts | 22 +++++++ .../vuid_manager_factory.react_native.spec.ts | 63 +++++++++++++++++++ lib/vuid/vuid_manager_factory.react_native.ts | 27 ++++++++ lib/vuid/vuid_manager_factory.ts | 5 +- vitest.config.mts | 2 +- 10 files changed, 229 insertions(+), 9 deletions(-) create mode 100644 lib/vuid/vuid_manager_factory.browser.spec.ts create mode 100644 lib/vuid/vuid_manager_factory.node.spec.ts create mode 100644 lib/vuid/vuid_manager_factory.node.ts create mode 100644 lib/vuid/vuid_manager_factory.react_native.spec.ts create mode 100644 lib/vuid/vuid_manager_factory.react_native.ts diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts index b579dffd2..2dbd31cff 100644 --- a/lib/vuid/vuid_manager.spec.ts +++ b/lib/vuid/vuid_manager.spec.ts @@ -43,7 +43,7 @@ describe('VuidCacheManager', () => { const vuid = await manager.load(); const vuidInCache = await cache.get(vuidCacheKey); expect(vuidInCache).toBe(vuid); - expect(isVuid(vuid)).toBe(true); + expect(isVuid(vuid!)).toBe(true); }); it('should create and save a new vuid if old VUID from cache is not valid', async () => { @@ -54,7 +54,7 @@ describe('VuidCacheManager', () => { const vuid = await manager.load(); const vuidInCache = await cache.get(vuidCacheKey); expect(vuidInCache).toBe(vuid); - expect(isVuid(vuid)).toBe(true); + expect(isVuid(vuid!)).toBe(true); }); it('should return the same vuid without modifying the cache after creating a new vuid', async () => { @@ -81,6 +81,23 @@ describe('VuidCacheManager', () => { expect(vuidInCache).toBe('vuid_valid'); }); + it('should use the new cache after setCache is called', async () => { + const cache1 = getMockAsyncCache(); + const cache2 = getMockAsyncCache(); + + await cache1.set(vuidCacheKey, 'vuid_123'); + await cache2.set(vuidCacheKey, 'vuid_456'); + + const manager = new VuidCacheManager(cache1); + const vuid1 = await manager.load(); + expect(vuid1).toBe('vuid_123'); + + manager.setCache(cache2); + await manager.load(); + const vuid2 = await cache2.get(vuidCacheKey); + expect(vuid2).toBe('vuid_456'); + }); + it('should sequence remove and load calls', async() => { const cache = getMockAsyncCache(); const removeSpy = vi.spyOn(cache, 'remove'); diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts index d49eeb600..64429a400 100644 --- a/lib/vuid/vuid_manager.ts +++ b/lib/vuid/vuid_manager.ts @@ -27,7 +27,7 @@ export interface VuidManager { export class VuidCacheManager { private logger?: LoggerFacade; private vuidCacheKey = 'optimizely-vuid'; - private cache: Cache; + private cache?: Cache; // if this value is not undefined, this means the same value is in the cache. // if this is undefined, it could either mean that there is no value in the cache // or that there is a value in the cache but it has not been loaded yet or failed @@ -35,11 +35,16 @@ export class VuidCacheManager { private vuid?: string; private waitPromise: Promise = Promise.resolve(); - constructor(cache: Cache, logger?: LoggerFacade) { + constructor(cache?: Cache, logger?: LoggerFacade) { this.cache = cache; this.logger = logger; } + setCache(cache: Cache): void { + this.cache = cache; + this.vuid = undefined; + } + setLogger(logger: LoggerFacade): void { this.logger = logger; } @@ -52,6 +57,9 @@ export class VuidCacheManager { async remove(): Promise { const removeFn = async () => { + if (!this.cache) { + return; + } this.vuid = undefined; await this.cache.remove(this.vuidCacheKey); } @@ -59,12 +67,15 @@ export class VuidCacheManager { return this.serialize(removeFn); } - async load(): Promise { + async load(): Promise> { if (this.vuid) { return this.vuid; } const loadFn = async () => { + if (!this.cache) { + return; + } const cachedValue = await this.cache.get(this.vuidCacheKey); if (cachedValue && isVuid(cachedValue)) { this.vuid = cachedValue; diff --git a/lib/vuid/vuid_manager_factory.browser.spec.ts b/lib/vuid/vuid_manager_factory.browser.spec.ts new file mode 100644 index 000000000..7967faee0 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.browser.spec.ts @@ -0,0 +1,63 @@ +import { vi, describe, expect, it, beforeEach } from 'vitest'; + +vi.mock('../utils/cache/local_storage_cache.browser', () => { + return { + LocalStorageCache: vi.fn(), + }; +}); + +vi.mock('./vuid_manager', () => { + return { + DefaultVuidManager: vi.fn(), + VuidCacheManager: vi.fn(), + }; +}); + +import { createVuidManager } from './vuid_manager_factory.browser'; +import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; +import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; + +describe('createVuidManager', () => { + const MockVuidCacheManager = vi.mocked(VuidCacheManager); + const MockLocalStorageCache = vi.mocked(LocalStorageCache); + const MockDefaultVuidManager = vi.mocked(DefaultVuidManager); + + beforeEach(() => { + MockDefaultVuidManager.mockClear(); + }); + + it('should pass the enableVuid option to the DefaultVuidManager', () => { + const manager = createVuidManager({ enableVuid: true }); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].enableVuid).toBe(true); + + const manager2 = createVuidManager({ enableVuid: false }); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockDefaultVuidManager.mock.calls[1][0].enableVuid).toBe(false); + }); + + it('should use a VuidCacheManager with a LocalStorageCache', () => { + const manager = createVuidManager({ enableVuid: true }); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + + + const usedCacheManager = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; + expect(usedCacheManager).toBe(MockVuidCacheManager.mock.instances[0]); + + const usedCache = MockVuidCacheManager.mock.calls[0][0]; + expect(usedCache).toBe(MockLocalStorageCache.mock.instances[0]); + }); + + it('should use a single VuidCacheManager instance for all VuidManager instances', () => { + const manager1 = createVuidManager({ enableVuid: true }); + const manager2 = createVuidManager({ enableVuid: true }); + expect(manager1).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockVuidCacheManager.mock.instances.length).toBe(1); + + const usedCacheManager1 = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; + const usedCacheManager2 = MockDefaultVuidManager.mock.calls[1][0].vuidCacheManager; + expect(usedCacheManager1).toBe(usedCacheManager2); + expect(usedCacheManager1).toBe(MockVuidCacheManager.mock.instances[0]); + }); +}); diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts index 89e6cbf08..20683d575 100644 --- a/lib/vuid/vuid_manager_factory.browser.ts +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -13,14 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DefaultVuidMaanger, VuidCacheManager, VuidManager } from './vuid_manager'; +import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { VuidManagerOptions } from './vuid_manager_factory'; export const vuidCacheManager = new VuidCacheManager(new LocalStorageCache()); export const createVuidManager = (options: VuidManagerOptions): VuidManager => { - return new DefaultVuidMaanger({ + if (options.vuidCache) { + vuidCacheManager.setCache(options.vuidCache); + } + + return new DefaultVuidManager({ vuidCacheManager, enableVuid: options.enableVuid }); diff --git a/lib/vuid/vuid_manager_factory.node.spec.ts b/lib/vuid/vuid_manager_factory.node.spec.ts new file mode 100644 index 000000000..6108da404 --- /dev/null +++ b/lib/vuid/vuid_manager_factory.node.spec.ts @@ -0,0 +1,10 @@ +import { vi, describe, expect, it } from 'vitest'; + +import { createVuidManager } from './vuid_manager_factory.node'; + +describe('createVuidManager', () => { + it('should throw an error', () => { + expect(() => createVuidManager({ enableVuid: true })) + .toThrowError('VUID is not supported in Node.js environment'); + }); +}); diff --git a/lib/vuid/vuid_manager_factory.node.ts b/lib/vuid/vuid_manager_factory.node.ts new file mode 100644 index 000000000..993fbb60a --- /dev/null +++ b/lib/vuid/vuid_manager_factory.node.ts @@ -0,0 +1,22 @@ +/** +* Copyright 2024, 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 { VuidManager } from './vuid_manager'; +import { VuidManagerOptions } from './vuid_manager_factory'; + +export const createVuidManager = (options: VuidManagerOptions): VuidManager => { + throw new Error('VUID is not supported in Node.js environment'); +}; + diff --git a/lib/vuid/vuid_manager_factory.react_native.spec.ts b/lib/vuid/vuid_manager_factory.react_native.spec.ts new file mode 100644 index 000000000..e65123ccc --- /dev/null +++ b/lib/vuid/vuid_manager_factory.react_native.spec.ts @@ -0,0 +1,63 @@ +import { vi, describe, expect, it, beforeEach } from 'vitest'; + +vi.mock('../utils/cache/local_storage_cache.react_native', () => { + return { + LocalStorageCache: vi.fn(), + }; +}); + +vi.mock('./vuid_manager', () => { + return { + DefaultVuidManager: vi.fn(), + VuidCacheManager: vi.fn(), + }; +}); + +import { createVuidManager } from './vuid_manager_factory.browser'; +import { Async} +import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; + +describe('createVuidManager', () => { + const MockVuidCacheManager = vi.mocked(VuidCacheManager); + const MockLocalStorageCache = vi.mocked(LocalStorageCache); + const MockDefaultVuidManager = vi.mocked(DefaultVuidManager); + + beforeEach(() => { + MockDefaultVuidManager.mockClear(); + }); + + it('should pass the enableVuid option to the DefaultVuidManager', () => { + const manager = createVuidManager({ enableVuid: true }); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].enableVuid).toBe(true); + + const manager2 = createVuidManager({ enableVuid: false }); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockDefaultVuidManager.mock.calls[1][0].enableVuid).toBe(false); + }); + + it('should use a VuidCacheManager with a LocalStorageCache', () => { + const manager = createVuidManager({ enableVuid: true }); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + + + const usedCacheManager = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; + expect(usedCacheManager).toBe(MockVuidCacheManager.mock.instances[0]); + + const usedCache = MockVuidCacheManager.mock.calls[0][0]; + expect(usedCache).toBe(MockLocalStorageCache.mock.instances[0]); + }); + + it('should use a single VuidCacheManager instance for all VuidManager instances', () => { + const manager1 = createVuidManager({ enableVuid: true }); + const manager2 = createVuidManager({ enableVuid: true }); + expect(manager1).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(manager2).toBe(MockDefaultVuidManager.mock.instances[1]); + expect(MockVuidCacheManager.mock.instances.length).toBe(1); + + const usedCacheManager1 = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; + const usedCacheManager2 = MockDefaultVuidManager.mock.calls[1][0].vuidCacheManager; + expect(usedCacheManager1).toBe(usedCacheManager2); + expect(usedCacheManager1).toBe(MockVuidCacheManager.mock.instances[0]); + }); +}); diff --git a/lib/vuid/vuid_manager_factory.react_native.ts b/lib/vuid/vuid_manager_factory.react_native.ts new file mode 100644 index 000000000..d985ea67e --- /dev/null +++ b/lib/vuid/vuid_manager_factory.react_native.ts @@ -0,0 +1,27 @@ +/** +* Copyright 2024, 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 { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manager'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; +import { VuidManagerOptions } from './vuid_manager_factory'; + +export const vuidCacheManager = new VuidCacheManager(new AsyncStorageCache()); + +export const createVuidManager = (options: VuidManagerOptions): VuidManager => { + return new DefaultVuidManager({ + vuidCacheManager, + enableVuid: options.enableVuid + }); +} diff --git a/lib/vuid/vuid_manager_factory.ts b/lib/vuid/vuid_manager_factory.ts index 27c5ae948..f4234b06e 100644 --- a/lib/vuid/vuid_manager_factory.ts +++ b/lib/vuid/vuid_manager_factory.ts @@ -1,3 +1,6 @@ +import { Cache } from '../utils/cache/cache'; + export type VuidManagerOptions = { - enableVuid: boolean; + vuidCache?: Cache; + enableVuid?: boolean; } diff --git a/vitest.config.mts b/vitest.config.mts index 7163ecc27..4d7d375a6 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/vuid_manager.spec.ts'], + include: ['**/vuid_manager_factory.browser.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 258d2f0be278f9e80d15bab1bbf36cad0b711933 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 9 Dec 2024 22:10:17 +0600 Subject: [PATCH 20/28] test fix --- lib/vuid/vuid_manager.ts | 4 +++ lib/vuid/vuid_manager_factory.browser.spec.ts | 17 +++++++---- lib/vuid/vuid_manager_factory.browser.ts | 7 ++--- .../vuid_manager_factory.react_native.spec.ts | 30 +++++++++++-------- lib/vuid/vuid_manager_factory.react_native.ts | 3 +- vitest.config.mts | 2 +- 6 files changed, 38 insertions(+), 25 deletions(-) diff --git a/lib/vuid/vuid_manager.ts b/lib/vuid/vuid_manager.ts index 64429a400..8de680609 100644 --- a/lib/vuid/vuid_manager.ts +++ b/lib/vuid/vuid_manager.ts @@ -92,17 +92,20 @@ export class VuidCacheManager { export type VuidManagerConfig = { enableVuid?: boolean; + vuidCache: Cache; vuidCacheManager: VuidCacheManager; } export class DefaultVuidManager implements VuidManager { private vuidCacheManager: VuidCacheManager; private vuid?: string; + private vuidCache: Cache; private vuidEnabled = false; constructor(config: VuidManagerConfig) { this.vuidCacheManager = config.vuidCacheManager; this.vuidEnabled = config.enableVuid || false; + this.vuidCache = config.vuidCache; } getVuid(): Maybe { @@ -118,6 +121,7 @@ export class DefaultVuidManager implements VuidManager { * @returns Promise that resolves when the VuidManager is initialized */ async initialize(): Promise { + this.vuidCacheManager.setCache(this.vuidCache); if (!this.vuidEnabled) { await this.vuidCacheManager.remove(); return; diff --git a/lib/vuid/vuid_manager_factory.browser.spec.ts b/lib/vuid/vuid_manager_factory.browser.spec.ts index 7967faee0..c6ca2d39d 100644 --- a/lib/vuid/vuid_manager_factory.browser.spec.ts +++ b/lib/vuid/vuid_manager_factory.browser.spec.ts @@ -13,6 +13,7 @@ vi.mock('./vuid_manager', () => { }; }); +import { getMockSyncCache } from '../tests/mock/mock_cache'; import { createVuidManager } from './vuid_manager_factory.browser'; import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; @@ -23,6 +24,7 @@ describe('createVuidManager', () => { const MockDefaultVuidManager = vi.mocked(DefaultVuidManager); beforeEach(() => { + MockLocalStorageCache.mockClear(); MockDefaultVuidManager.mockClear(); }); @@ -36,15 +38,18 @@ describe('createVuidManager', () => { expect(MockDefaultVuidManager.mock.calls[1][0].enableVuid).toBe(false); }); - it('should use a VuidCacheManager with a LocalStorageCache', () => { - const manager = createVuidManager({ enableVuid: true }); + it('should use the provided cache', () => { + const cache = getMockSyncCache(); + const manager = createVuidManager({ enableVuid: true, vuidCache: cache }); expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].vuidCache).toBe(cache); + }); + it('should use a LocalStorageCache if no cache is provided', () => { + const manager = createVuidManager({ enableVuid: true }); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); - const usedCacheManager = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; - expect(usedCacheManager).toBe(MockVuidCacheManager.mock.instances[0]); - - const usedCache = MockVuidCacheManager.mock.calls[0][0]; + const usedCache = MockDefaultVuidManager.mock.calls[0][0].vuidCache; expect(usedCache).toBe(MockLocalStorageCache.mock.instances[0]); }); diff --git a/lib/vuid/vuid_manager_factory.browser.ts b/lib/vuid/vuid_manager_factory.browser.ts index 20683d575..cf8df6a44 100644 --- a/lib/vuid/vuid_manager_factory.browser.ts +++ b/lib/vuid/vuid_manager_factory.browser.ts @@ -17,15 +17,12 @@ import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manage import { LocalStorageCache } from '../utils/cache/local_storage_cache.browser'; import { VuidManagerOptions } from './vuid_manager_factory'; -export const vuidCacheManager = new VuidCacheManager(new LocalStorageCache()); +export const vuidCacheManager = new VuidCacheManager(); export const createVuidManager = (options: VuidManagerOptions): VuidManager => { - if (options.vuidCache) { - vuidCacheManager.setCache(options.vuidCache); - } - return new DefaultVuidManager({ vuidCacheManager, + vuidCache: options.vuidCache || new LocalStorageCache(), enableVuid: options.enableVuid }); } diff --git a/lib/vuid/vuid_manager_factory.react_native.spec.ts b/lib/vuid/vuid_manager_factory.react_native.spec.ts index e65123ccc..67dc86a92 100644 --- a/lib/vuid/vuid_manager_factory.react_native.spec.ts +++ b/lib/vuid/vuid_manager_factory.react_native.spec.ts @@ -1,8 +1,8 @@ import { vi, describe, expect, it, beforeEach } from 'vitest'; -vi.mock('../utils/cache/local_storage_cache.react_native', () => { +vi.mock('../utils/cache/async_storage_cache.react_native', () => { return { - LocalStorageCache: vi.fn(), + AsyncStorageCache: vi.fn(), }; }); @@ -13,16 +13,19 @@ vi.mock('./vuid_manager', () => { }; }); -import { createVuidManager } from './vuid_manager_factory.browser'; -import { Async} +import { getMockAsyncCache } from '../tests/mock/mock_cache'; +import { createVuidManager } from './vuid_manager_factory.react_native'; +import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; + import { DefaultVuidManager, VuidCacheManager } from './vuid_manager'; describe('createVuidManager', () => { const MockVuidCacheManager = vi.mocked(VuidCacheManager); - const MockLocalStorageCache = vi.mocked(LocalStorageCache); + const MockAsyncStorageCache = vi.mocked(AsyncStorageCache); const MockDefaultVuidManager = vi.mocked(DefaultVuidManager); beforeEach(() => { + MockAsyncStorageCache.mockClear(); MockDefaultVuidManager.mockClear(); }); @@ -36,16 +39,19 @@ describe('createVuidManager', () => { expect(MockDefaultVuidManager.mock.calls[1][0].enableVuid).toBe(false); }); - it('should use a VuidCacheManager with a LocalStorageCache', () => { - const manager = createVuidManager({ enableVuid: true }); + it('should use the provided cache', () => { + const cache = getMockAsyncCache(); + const manager = createVuidManager({ enableVuid: true, vuidCache: cache }); expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); + expect(MockDefaultVuidManager.mock.calls[0][0].vuidCache).toBe(cache); + }); + it('should use a AsyncStorageCache if no cache is provided', () => { + const manager = createVuidManager({ enableVuid: true }); + expect(manager).toBe(MockDefaultVuidManager.mock.instances[0]); - const usedCacheManager = MockDefaultVuidManager.mock.calls[0][0].vuidCacheManager; - expect(usedCacheManager).toBe(MockVuidCacheManager.mock.instances[0]); - - const usedCache = MockVuidCacheManager.mock.calls[0][0]; - expect(usedCache).toBe(MockLocalStorageCache.mock.instances[0]); + const usedCache = MockDefaultVuidManager.mock.calls[0][0].vuidCache; + expect(usedCache).toBe(MockAsyncStorageCache.mock.instances[0]); }); it('should use a single VuidCacheManager instance for all VuidManager instances', () => { diff --git a/lib/vuid/vuid_manager_factory.react_native.ts b/lib/vuid/vuid_manager_factory.react_native.ts index d985ea67e..6eba4c9f2 100644 --- a/lib/vuid/vuid_manager_factory.react_native.ts +++ b/lib/vuid/vuid_manager_factory.react_native.ts @@ -17,11 +17,12 @@ import { DefaultVuidManager, VuidCacheManager, VuidManager } from './vuid_manage import { AsyncStorageCache } from '../utils/cache/async_storage_cache.react_native'; import { VuidManagerOptions } from './vuid_manager_factory'; -export const vuidCacheManager = new VuidCacheManager(new AsyncStorageCache()); +export const vuidCacheManager = new VuidCacheManager(); export const createVuidManager = (options: VuidManagerOptions): VuidManager => { return new DefaultVuidManager({ vuidCacheManager, + vuidCache: options.vuidCache || new AsyncStorageCache(), enableVuid: options.enableVuid }); } diff --git a/vitest.config.mts b/vitest.config.mts index 4d7d375a6..82a632718 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/vuid_manager_factory.browser.spec.ts'], + include: ['**/vuid_manager_factory.react_native.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 0d5593e912d69dc4fcb08e09d14b1c016725f77e Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 9 Dec 2024 22:22:14 +0600 Subject: [PATCH 21/28] more fix --- lib/vuid/vuid_manager.spec.ts | 65 +++++++++++++++++++++-------------- vitest.config.mts | 2 +- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts index 2dbd31cff..6e3709c98 100644 --- a/lib/vuid/vuid_manager.spec.ts +++ b/lib/vuid/vuid_manager.spec.ts @@ -22,6 +22,7 @@ import { getMockAsyncCache } from '../tests/mock/mock_cache'; import { isVuid } from './vuid'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { exhaustMicrotasks } from '../tests/testUtils'; +import { get } from 'http'; const vuidCacheKey = 'optimizely-vuid'; @@ -76,7 +77,7 @@ describe('VuidCacheManager', () => { const vuid1 = await manager.load(); const vuid2 = await manager.load(); expect(vuid1).toBe('vuid_valid'); - expect(vuid1).toBe('vuid_valid'); + expect(vuid2).toBe('vuid_valid'); const vuidInCache = await cache.get(vuidCacheKey); expect(vuidInCache).toBe('vuid_valid'); }); @@ -96,6 +97,10 @@ describe('VuidCacheManager', () => { await manager.load(); const vuid2 = await cache2.get(vuidCacheKey); expect(vuid2).toBe('vuid_456'); + + await manager.remove(); + const vuidInCache = await cache2.get(vuidCacheKey); + expect(vuidInCache).toBeUndefined(); }); it('should sequence remove and load calls', async() => { @@ -151,28 +156,43 @@ describe('VuidCacheManager', () => { }); describe('DefaultVuidManager', () => { - it('should return undefined for getVuid() before initialization', async () => { - const vuidCacheManager ={ - remove: vi.fn(), - load: vi.fn(), - } as unknown as VuidCacheManager; + const getMockCacheManager = () => ({ + remove: vi.fn(), + load: vi.fn(), + setCache: vi.fn(), + }); + it('should return undefined for getVuid() before initialization', async () => { const manager = new DefaultVuidManager({ - vuidCacheManager, + vuidCache: getMockAsyncCache(), + vuidCacheManager: getMockCacheManager() as unknown as VuidCacheManager, enableVuid: true }); expect(manager.getVuid()).toBeUndefined(); }); + it('should set the cache on vuidCacheManager', async () => { + const vuidCacheManager = getMockCacheManager(); + + const cache = getMockAsyncCache(); + + const manager = new DefaultVuidManager({ + vuidCache: cache, + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, + enableVuid: false + }); + + await manager.initialize(); + expect(vuidCacheManager.setCache).toHaveBeenCalledWith(cache); + }); + it('should call remove on VuidCacheManager if enableVuid is false', async () => { - const vuidCacheManager ={ - remove: vi.fn(), - load: vi.fn(), - } as unknown as VuidCacheManager; + const vuidCacheManager = getMockCacheManager(); const manager = new DefaultVuidManager({ - vuidCacheManager, + vuidCache: getMockAsyncCache(), + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, enableVuid: false }); @@ -181,13 +201,11 @@ describe('DefaultVuidManager', () => { }); it('should return undefined for getVuid() after initialization if enableVuid is false', async () => { - const vuidCacheManager ={ - remove: vi.fn(), - load: vi.fn(), - } as unknown as VuidCacheManager; + const vuidCacheManager = getMockCacheManager(); const manager = new DefaultVuidManager({ - vuidCacheManager, + vuidCache: getMockAsyncCache(), + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, enableVuid: false }); @@ -196,17 +214,12 @@ describe('DefaultVuidManager', () => { }); it('should load vuid using VuidCacheManger if enableVuid=true', async () => { - const load = vi.fn(); - - const vuidCacheManager ={ - remove: vi.fn(), - load, - } as unknown as VuidCacheManager; - - load.mockResolvedValue('vuid_valid'); + const vuidCacheManager = getMockCacheManager(); + vuidCacheManager.load.mockResolvedValue('vuid_valid'); const manager = new DefaultVuidManager({ - vuidCacheManager, + vuidCache: getMockAsyncCache(), + vuidCacheManager: vuidCacheManager as unknown as VuidCacheManager, enableVuid: true }); diff --git a/vitest.config.mts b/vitest.config.mts index 82a632718..7163ecc27 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/vuid_manager_factory.react_native.spec.ts'], + include: ['**/vuid_manager.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From c94d32f53386f3e8164c0038cbc50a667ca79971 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 9 Dec 2024 22:43:17 +0600 Subject: [PATCH 22/28] fix --- lib/export_types.ts | 1 - lib/index.browser.tests.js | 31 +++++++++--------- lib/index.browser.ts | 66 ++++++-------------------------------- lib/index.node.ts | 44 +++++-------------------- lib/index.react_native.ts | 45 +++++--------------------- lib/odp/odp_manager.ts | 1 + lib/optimizely/index.ts | 24 ++++++++------ lib/shared_types.ts | 4 +++ vitest.config.mts | 2 +- 9 files changed, 61 insertions(+), 157 deletions(-) diff --git a/lib/export_types.ts b/lib/export_types.ts index df11a89a8..a55f56f27 100644 --- a/lib/export_types.ts +++ b/lib/export_types.ts @@ -45,5 +45,4 @@ export { TrackListenerPayload, NotificationCenter, OptimizelySegmentOption, - ICache, } from './shared_types'; diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js index 15145c7a6..fbe95fa5e 100644 --- a/lib/index.browser.tests.js +++ b/lib/index.browser.tests.js @@ -26,7 +26,6 @@ import configValidator from './utils/config_validator'; import OptimizelyUserContext from './optimizely_user_context'; import { LOG_MESSAGES, ODP_EVENT_ACTION } from './utils/enums'; -import { BrowserOdpManager } from './odp/odp_manager.browser'; import { OdpConfig } from './odp/odp_config'; import { BrowserOdpEventManager } from './odp/event_manager/event_manager.browser'; import { BrowserOdpEventApiManager } from './odp/event_manager/event_api_manager.browser'; @@ -637,21 +636,21 @@ describe('javascript-sdk (Browser)', function() { sinon.assert.calledWith(fakeOptimizely.identifyUser, testFsUserId); }); - it('should log info when odp is disabled', () => { - const disabledClient = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { disabled: true }, - odpManager: BrowserOdpManager.createInstance({ - logger, - odpOptions: { - disabled: true, - }, - }), - }); + // it('should log info when odp is disabled', () => { + // const disabledClient = optimizelyFactory.createInstance({ + // datafile: testData.getTestProjectConfigWithFeatures(), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // eventBatchSize: null, + // logger, + // odpOptions: { disabled: true }, + // odpManager: BrowserOdpManager.createInstance({ + // logger, + // odpOptions: { + // disabled: true, + // }, + // }), + // }); sinon.assert.calledWith(logger.log, optimizelyFactory.enums.LOG_LEVEL.INFO, LOG_MESSAGES.ODP_DISABLED); }); diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 537a1ffae..1e72cbd54 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -24,7 +24,6 @@ import * as enums from './utils/enums'; import * as loggerPlugin from './plugins/logger'; import { createNotificationCenter } from './notification_center'; import { OptimizelyDecideOption, Client, Config, OptimizelyOptions } from './shared_types'; -import { BrowserOdpManager } from './odp/odp_manager.browser'; import Optimizely from './optimizely'; import { UserAgentParser } from './odp/ua_parser/user_agent_parser'; import { getUserAgentParser } from './odp/ua_parser/ua_parser.browser'; @@ -32,6 +31,9 @@ import * as commonExports from './common_exports'; import { PollingConfigManagerConfig } from './project_config/config_manager_factory'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.browser'; import { createBatchEventProcessor, createForwardingEventProcessor } from './event_processor/event_processor_factory.browser'; +import { createVuidManager } from './vuid/vuid_manager_factory.browser'; +import { createOdpManager } from './odp/odp_manager_factory.browser'; + const logger = getLogger(); logHelper.setLogHandler(loggerPlugin.createLogger()); @@ -75,73 +77,19 @@ const createInstance = function(config: Config): Client | null { logger.error(ex); } - // let eventDispatcher; - // // prettier-ignore - // if (config.eventDispatcher == null) { // eslint-disable-line eqeqeq - // // only wrap the event dispatcher with pending events retry if the user didnt override - // eventDispatcher = new LocalStoragePendingEventsDispatcher({ - // eventDispatcher: defaultEventDispatcher, - // }); - - // if (!hasRetriedEvents) { - // eventDispatcher.sendPendingEvents(); - // hasRetriedEvents = true; - // } - // } else { - // eventDispatcher = config.eventDispatcher; - // } - - // let closingDispatcher = config.closingEventDispatcher; - - // if (!config.eventDispatcher && !closingDispatcher && window.navigator && 'sendBeacon' in window.navigator) { - // closingDispatcher = sendBeaconEventDispatcher; - // } - - // let eventBatchSize = config.eventBatchSize; - // let eventFlushInterval = config.eventFlushInterval; - - // if (!eventProcessorConfigValidator.validateEventBatchSize(config.eventBatchSize)) { - // logger.warn('Invalid eventBatchSize %s, defaulting to %s', config.eventBatchSize, DEFAULT_EVENT_BATCH_SIZE); - // eventBatchSize = DEFAULT_EVENT_BATCH_SIZE; - // } - // if (!eventProcessorConfigValidator.validateEventFlushInterval(config.eventFlushInterval)) { - // logger.warn( - // 'Invalid eventFlushInterval %s, defaulting to %s', - // config.eventFlushInterval, - // DEFAULT_EVENT_FLUSH_INTERVAL - // ); - // eventFlushInterval = DEFAULT_EVENT_FLUSH_INTERVAL; - // } - const errorHandler = getErrorHandler(); const notificationCenter = createNotificationCenter({ logger: logger, errorHandler: errorHandler }); - // const eventProcessorConfig = { - // dispatcher: eventDispatcher, - // closingDispatcher, - // flushInterval: eventFlushInterval, - // batchSize: eventBatchSize, - // maxQueueSize: config.eventMaxQueueSize || DEFAULT_EVENT_MAX_QUEUE_SIZE, - // notificationCenter, - // }; - - const odpExplicitlyOff = config.odpOptions?.disabled === true; - if (odpExplicitlyOff) { - logger.info(enums.LOG_MESSAGES.ODP_DISABLED); - } - const { clientEngine, clientVersion } = config; const optimizelyOptions: OptimizelyOptions = { - clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, ...config, - // eventProcessor: eventProcessor.createEventProcessor(eventProcessorConfig), + clientEngine: clientEngine || enums.NODE_CLIENT_ENGINE, + clientVersion: clientVersion || enums.CLIENT_VERSION, logger, errorHandler, notificationCenter, isValidInstance, - odpManager: odpExplicitlyOff ? undefined - : BrowserOdpManager.createInstance({ logger, odpOptions: config.odpOptions, clientEngine, clientVersion }), }; const optimizely = new Optimizely(optimizelyOptions); @@ -197,6 +145,8 @@ export { createPollingProjectConfigManager, createForwardingEventProcessor, createBatchEventProcessor, + createOdpManager, + createVuidManager, }; export * from './common_exports'; @@ -217,6 +167,8 @@ export default { createPollingProjectConfigManager, createForwardingEventProcessor, createBatchEventProcessor, + createOdpManager, + createVuidManager, }; export * from './export_types'; diff --git a/lib/index.node.ts b/lib/index.node.ts index a5a3b2968..3f28538d9 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -23,10 +23,11 @@ import defaultErrorHandler from './plugins/error_handler'; import defaultEventDispatcher from './event_processor/event_dispatcher/default_dispatcher.node'; import { createNotificationCenter } from './notification_center'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; -import { NodeOdpManager } from './odp/odp_manager.node'; import * as commonExports from './common_exports'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.node'; import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor/event_processor_factory.node'; +import { createVuidManager } from './vuid/vuid_manager_factory.browser'; +import { createOdpManager } from './odp/odp_manager_factory.browser'; const logger = getLogger(); setLogLevel(LogLevel.ERROR); @@ -72,53 +73,20 @@ const createInstance = function(config: Config): Client | null { } } - // let eventBatchSize = config.eventBatchSize; - // let eventFlushInterval = config.eventFlushInterval; - - // if (!eventProcessorConfigValidator.validateEventBatchSize(config.eventBatchSize)) { - // logger.warn('Invalid eventBatchSize %s, defaulting to %s', config.eventBatchSize, DEFAULT_EVENT_BATCH_SIZE); - // eventBatchSize = DEFAULT_EVENT_BATCH_SIZE; - // } - // if (!eventProcessorConfigValidator.validateEventFlushInterval(config.eventFlushInterval)) { - // logger.warn( - // 'Invalid eventFlushInterval %s, defaulting to %s', - // config.eventFlushInterval, - // DEFAULT_EVENT_FLUSH_INTERVAL - // ); - // eventFlushInterval = DEFAULT_EVENT_FLUSH_INTERVAL; - // } const errorHandler = getErrorHandler(); const notificationCenter = createNotificationCenter({ logger: logger, errorHandler: errorHandler }); - // const eventProcessorConfig = { - // dispatcher: config.eventDispatcher || defaultEventDispatcher, - // flushInterval: eventFlushInterval, - // batchSize: eventBatchSize, - // maxQueueSize: config.eventMaxQueueSize || DEFAULT_EVENT_MAX_QUEUE_SIZE, - // notificationCenter, - // }; - - // const eventProcessor = createEventProcessor(eventProcessorConfig); - // const eventProcessor = config.eventProcessor; - - const odpExplicitlyOff = config.odpOptions?.disabled === true; - if (odpExplicitlyOff) { - logger.info(enums.LOG_MESSAGES.ODP_DISABLED); - } - const { clientEngine, clientVersion } = config; const optimizelyOptions = { - clientEngine: enums.NODE_CLIENT_ENGINE, ...config, - // eventProcessor, + clientEngine: clientEngine || enums.NODE_CLIENT_ENGINE, + clientVersion: clientVersion || enums.CLIENT_VERSION, logger, errorHandler, notificationCenter, isValidInstance, - odpManager: odpExplicitlyOff ? undefined - : NodeOdpManager.createInstance({ logger, odpOptions: config.odpOptions, clientEngine, clientVersion }), }; return new Optimizely(optimizelyOptions); @@ -144,6 +112,8 @@ export { createPollingProjectConfigManager, createForwardingEventProcessor, createBatchEventProcessor, + createOdpManager, + createVuidManager, }; export * from './common_exports'; @@ -161,6 +131,8 @@ export default { createPollingProjectConfigManager, createForwardingEventProcessor, createBatchEventProcessor, + createOdpManager, + createVuidManager, }; export * from './export_types'; diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index c0417d588..6172ce4d3 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -23,10 +23,11 @@ import * as loggerPlugin from './plugins/logger/index.react_native'; import defaultEventDispatcher from './event_processor/event_dispatcher/default_dispatcher.browser'; import { createNotificationCenter } from './notification_center'; import { OptimizelyDecideOption, Client, Config } from './shared_types'; -import { BrowserOdpManager } from './odp/odp_manager.browser'; import * as commonExports from './common_exports'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.react_native'; import { createBatchEventProcessor, createForwardingEventProcessor } from './event_processor/event_processor_factory.react_native'; +import { createOdpManager } from './odp/odp_manager_factory.react_native'; +import { createVuidManager } from './vuid/vuid_manager_factory.react_native'; import 'fast-text-encoding'; import 'react-native-get-random-values'; @@ -70,53 +71,19 @@ const createInstance = function(config: Config): Client | null { logger.error(ex); } - // let eventBatchSize = config.eventBatchSize; - // let eventFlushInterval = config.eventFlushInterval; - - // if (!eventProcessorConfigValidator.validateEventBatchSize(config.eventBatchSize)) { - // logger.warn('Invalid eventBatchSize %s, defaulting to %s', config.eventBatchSize, DEFAULT_EVENT_BATCH_SIZE); - // eventBatchSize = DEFAULT_EVENT_BATCH_SIZE; - // } - // if (!eventProcessorConfigValidator.validateEventFlushInterval(config.eventFlushInterval)) { - // logger.warn( - // 'Invalid eventFlushInterval %s, defaulting to %s', - // config.eventFlushInterval, - // DEFAULT_EVENT_FLUSH_INTERVAL - // ); - // eventFlushInterval = DEFAULT_EVENT_FLUSH_INTERVAL; - // } - const errorHandler = getErrorHandler(); const notificationCenter = createNotificationCenter({ logger: logger, errorHandler: errorHandler }); - // const eventProcessorConfig = { - // dispatcher: config.eventDispatcher || defaultEventDispatcher, - // flushInterval: eventFlushInterval, - // batchSize: eventBatchSize, - // maxQueueSize: config.eventMaxQueueSize || DEFAULT_EVENT_MAX_QUEUE_SIZE, - // notificationCenter, - // peristentCacheProvider: config.persistentCacheProvider, - // }; - - // const eventProcessor = createEventProcessor(eventProcessorConfig); - - const odpExplicitlyOff = config.odpOptions?.disabled === true; - if (odpExplicitlyOff) { - logger.info(enums.LOG_MESSAGES.ODP_DISABLED); - } - const { clientEngine, clientVersion } = config; const optimizelyOptions = { - clientEngine: enums.REACT_NATIVE_JS_CLIENT_ENGINE, ...config, - // eventProcessor, + clientEngine: clientEngine || enums.NODE_CLIENT_ENGINE, + clientVersion: clientVersion || enums.CLIENT_VERSION, logger, errorHandler, notificationCenter, isValidInstance: isValidInstance, - odpManager: odpExplicitlyOff ? undefined - :BrowserOdpManager.createInstance({ logger, odpOptions: config.odpOptions, clientEngine, clientVersion }), }; // If client engine is react, convert it to react native. @@ -147,6 +114,8 @@ export { createPollingProjectConfigManager, createForwardingEventProcessor, createBatchEventProcessor, + createOdpManager, + createVuidManager, }; export * from './common_exports'; @@ -164,6 +133,8 @@ export default { createPollingProjectConfigManager, createForwardingEventProcessor, createBatchEventProcessor, + createOdpManager, + createVuidManager, }; export * from './export_types'; diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index bb66cb201..560e445a4 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -36,6 +36,7 @@ export interface OdpManager extends Service { identifyUser(userId: string, vuid?: string): void; sendEvent(event: OdpEvent): void; setClientInfo(clientEngine: string, clientVersion: string): void; + setVuid(vuid: string): void; } export type OdpManagerConfig = { diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 2612bbd0c..91ec1503e 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -20,6 +20,7 @@ import { DefaultNotificationCenter, NotificationCenter } from '../notification_c import { EventProcessor } from '../event_processor/event_processor'; import { OdpManager } from '../odp/odp_manager'; +import { VuidManager } from '../vuid/vuid_manager'; import { OdpEvent } from '../odp/event_manager/odp_event'; import { OptimizelySegmentOption } from '../odp/segment_manager/optimizely_segment_option'; @@ -97,6 +98,7 @@ export default class Optimizely implements Client { private defaultDecideOptions: { [key: string]: boolean }; protected odpManager?: OdpManager; public notificationCenter: DefaultNotificationCenter; + private vuidManager?: VuidManager; constructor(config: OptimizelyOptions) { let clientEngine = config.clientEngine; @@ -111,6 +113,7 @@ export default class Optimizely implements Client { this.isOptimizelyConfigValid = config.isValidInstance; this.logger = config.logger; this.odpManager = config.odpManager; + this.vuidManager = config.vuidManager; let decideOptionsArray = config.defaultDecideOptions ?? []; if (!Array.isArray(decideOptionsArray)) { @@ -182,8 +185,16 @@ export default class Optimizely implements Client { projectConfigManagerRunningPromise, eventProcessorRunningPromise, config.odpManager ? config.odpManager.onRunning() : Promise.resolve(), + config.vuidManager ? config.vuidManager.initialize() : Promise.resolve(), ]); + this.readyPromise.then(() => { + const vuid = this.vuidManager?.getVuid(); + if (vuid) { + this.odpManager?.setVuid(vuid); + } + }); + this.readyTimeouts = {}; this.nextReadyTimeoutId = 0; } @@ -1349,7 +1360,7 @@ export default class Optimizely implements Client { * null if provided inputs are invalid */ createUserContext(userId?: string, attributes?: UserAttributes): OptimizelyUserContext | null { - const userIdentifier = userId ?? this.odpManager?.getVuid(); + const userIdentifier = userId ?? this.vuidManager?.getVuid(); if (userIdentifier === undefined || !this.validateInputs({ user_id: userIdentifier }, attributes)) { return null; @@ -1694,16 +1705,11 @@ export default class Optimizely implements Client { * ODP Manager has not been instantiated yet for any reason. */ public getVuid(): string | undefined { - if (!this.odpManager) { - this.logger?.error('Unable to get VUID - ODP Manager is not instantiated yet.'); - return undefined; - } - - if (!this.odpManager.isVuidEnabled()) { - this.logger.log(LOG_LEVEL.WARNING, 'getVuid() unavailable for this platform', MODULE_NAME); + if (!this.vuidManager) { + this.logger?.error('Unable to get VUID - VuidManager is not available'); return undefined; } - return this.odpManager.getVuid(); + return this.vuidManager.getVuid(); } } diff --git a/lib/shared_types.ts b/lib/shared_types.ts index ae287dc22..e5138b191 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -37,12 +37,14 @@ import { ProjectConfig } from './project_config/project_config'; import { ProjectConfigManager } from './project_config/project_config_manager'; import { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; import { EventProcessor } from './event_processor/event_processor'; +import { VuidManager } from './vuid/vuid_manager'; export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; export { EventProcessor } from './event_processor/event_processor'; export { NotificationCenter } from './notification_center'; export { OdpManager } from './odp/odp_manager'; +export { VuidManager } from './vuid/vuid_manager'; export interface BucketerParams { experimentId: string; experimentKey: string; @@ -266,6 +268,7 @@ export interface OptimizelyOptions { isSsr?:boolean; odpManager?: OdpManager; notificationCenter: DefaultNotificationCenter; + vuidManager?: VuidManager } /** @@ -399,6 +402,7 @@ export interface ConfigLite { clientVersion?: string; isSsr?: boolean; odpManager?: OdpManager; + vuidManager?: VuidManager; } export type OptimizelyExperimentsMap = { diff --git a/vitest.config.mts b/vitest.config.mts index 7163ecc27..673f7d1c6 100644 --- a/vitest.config.mts +++ b/vitest.config.mts @@ -20,7 +20,7 @@ export default defineConfig({ test: { onConsoleLog: () => true, environment: 'happy-dom', - include: ['**/vuid_manager.spec.ts'], + include: ['**/*.spec.ts'], typecheck: { tsconfig: 'tsconfig.spec.json', }, From 5694d72c3417a77c6512b97077ef7cfb73c0981a Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Mon, 9 Dec 2024 23:36:10 +0600 Subject: [PATCH 23/28] up --- lib/index.browser.tests.js | 856 +++--------------- lib/index.browser.ts | 2 +- lib/index.react_native.ts | 2 +- .../odp_segment_manager.spec.ts | 4 +- lib/tests/testUtils.ts | 4 +- lib/vuid/vuid.ts | 2 +- tests/odpManager.browser.spec.ts | 513 ----------- 7 files changed, 121 insertions(+), 1262 deletions(-) delete mode 100644 tests/odpManager.browser.spec.ts diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js index fbe95fa5e..3405beea4 100644 --- a/lib/index.browser.tests.js +++ b/lib/index.browser.tests.js @@ -23,13 +23,6 @@ import testData from './tests/test_data'; import packageJSON from '../package.json'; import optimizelyFactory from './index.browser'; import configValidator from './utils/config_validator'; -import OptimizelyUserContext from './optimizely_user_context'; - -import { LOG_MESSAGES, ODP_EVENT_ACTION } from './utils/enums'; -import { OdpConfig } from './odp/odp_config'; -import { BrowserOdpEventManager } from './odp/event_manager/event_manager.browser'; -import { BrowserOdpEventApiManager } from './odp/event_manager/event_api_manager.browser'; -import { OdpEvent } from './odp/event_manager/odp_event'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; import { createProjectConfig } from './project_config/project_config'; @@ -431,152 +424,6 @@ describe('javascript-sdk (Browser)', function() { sinon.assert.calledWithExactly(logging.setLogHandler, fakeLogger); }); }); - - // TODO: user will create and inject an event processor - // these tests will be refactored accordingly - // describe('event processor configuration', function() { - // beforeEach(function() { - // sinon.stub(eventProcessor, 'createEventProcessor'); - // }); - - // afterEach(function() { - // eventProcessor.createEventProcessor.restore(); - // }); - - // it('should use default event flush interval when none is provided', function() { - // optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // logger: silentLogger, - // }); - // sinon.assert.calledWithExactly( - // eventProcessor.createEventProcessor, - // sinon.match({ - // flushInterval: 1000, - // }) - // ); - // }); - - // describe('with an invalid flush interval', function() { - // beforeEach(function() { - // sinon.stub(eventProcessorConfigValidator, 'validateEventFlushInterval').returns(false); - // }); - - // afterEach(function() { - // eventProcessorConfigValidator.validateEventFlushInterval.restore(); - // }); - - // it('should ignore the event flush interval and use the default instead', function() { - // optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // logger: silentLogger, - // eventFlushInterval: ['invalid', 'flush', 'interval'], - // }); - // sinon.assert.calledWithExactly( - // eventProcessor.createEventProcessor, - // sinon.match({ - // flushInterval: 1000, - // }) - // ); - // }); - // }); - - // describe('with a valid flush interval', function() { - // beforeEach(function() { - // sinon.stub(eventProcessorConfigValidator, 'validateEventFlushInterval').returns(true); - // }); - - // afterEach(function() { - // eventProcessorConfigValidator.validateEventFlushInterval.restore(); - // }); - - // it('should use the provided event flush interval', function() { - // optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // logger: silentLogger, - // eventFlushInterval: 9000, - // }); - // sinon.assert.calledWithExactly( - // eventProcessor.createEventProcessor, - // sinon.match({ - // flushInterval: 9000, - // }) - // ); - // }); - // }); - - // it('should use default event batch size when none is provided', function() { - // optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // logger: silentLogger, - // }); - // sinon.assert.calledWithExactly( - // eventProcessor.createEventProcessor, - // sinon.match({ - // batchSize: 10, - // }) - // ); - // }); - - // describe('with an invalid event batch size', function() { - // beforeEach(function() { - // sinon.stub(eventProcessorConfigValidator, 'validateEventBatchSize').returns(false); - // }); - - // afterEach(function() { - // eventProcessorConfigValidator.validateEventBatchSize.restore(); - // }); - - // it('should ignore the event batch size and use the default instead', function() { - // optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // logger: silentLogger, - // eventBatchSize: null, - // }); - // sinon.assert.calledWithExactly( - // eventProcessor.createEventProcessor, - // sinon.match({ - // batchSize: 10, - // }) - // ); - // }); - // }); - - // describe('with a valid event batch size', function() { - // beforeEach(function() { - // sinon.stub(eventProcessorConfigValidator, 'validateEventBatchSize').returns(true); - // }); - - // afterEach(function() { - // eventProcessorConfigValidator.validateEventBatchSize.restore(); - // }); - - // it('should use the provided event batch size', function() { - // optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // logger: silentLogger, - // eventBatchSize: 300, - // }); - // sinon.assert.calledWithExactly( - // eventProcessor.createEventProcessor, - // sinon.match({ - // batchSize: 300, - // }) - // ); - // }); - // }); - // }); }); describe('ODP/ATS', () => { @@ -623,627 +470,150 @@ describe('javascript-sdk (Browser)', function() { requestParams.clear(); }); - it('should send identify event by default when initialized', async () => { - new OptimizelyUserContext({ - optimizely: fakeOptimizely, - userId: testFsUserId, - }); - - await fakeOptimizely.onReady(); - sinon.assert.calledOnce(fakeOptimizely.identifyUser); + // TODO: these tests should be elsewhere + // it('should send an odp event when calling sendOdpEvent with valid parameters', async () => { + // const fakeEventManager = { + // updateSettings: sinon.spy(), + // start: sinon.spy(), + // stop: sinon.spy(), + // registerVuid: sinon.spy(), + // identifyUser: sinon.spy(), + // sendEvent: sinon.spy(), + // flush: sinon.spy(), + // }; - sinon.assert.calledWith(fakeOptimizely.identifyUser, testFsUserId); - }); + // const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); + // const projectConfigManager = getMockProjectConfigManager({ + // initConfig: config, + // onRunning: Promise.resolve(), + // }); - // it('should log info when odp is disabled', () => { - // const disabledClient = optimizelyFactory.createInstance({ - // datafile: testData.getTestProjectConfigWithFeatures(), + // const client = optimizelyFactory.createInstance({ + // projectConfigManager, // errorHandler: fakeErrorHandler, // eventDispatcher: fakeEventDispatcher, // eventBatchSize: null, // logger, - // odpOptions: { disabled: true }, - // odpManager: BrowserOdpManager.createInstance({ - // logger, - // odpOptions: { - // disabled: true, - // }, - // }), + // odpOptions: { + // eventManager: fakeEventManager, + // }, // }); - sinon.assert.calledWith(logger.log, optimizelyFactory.enums.LOG_LEVEL.INFO, LOG_MESSAGES.ODP_DISABLED); - }); - - it('should include the VUID instantation promise of Browser ODP Manager in the Optimizely client onReady promise dependency array', () => { - const client = optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - onRunning: Promise.resolve(), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpManager: BrowserOdpManager.createInstance({ - logger, - }), - }); - - client - .onReady() - .then(() => { - assert.isDefined(client.odpManager.initPromise); - client.odpManager.initPromise - .then(() => { - assert.isTrue(true); - }) - .catch(() => { - assert.isTrue(false); - }); - assert.isDefined(client.odpManager.getVuid()); - }) - .catch(() => { - assert.isTrue(false); - }); - - sinon.assert.neverCalledWith(logger.log, optimizelyFactory.enums.LOG_LEVEL.ERROR); - }); - - it('should accept a valid custom cache size', () => { - const client = optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - onRunning: Promise.resolve(), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpManager: BrowserOdpManager.createInstance({ - logger, - odpOptions: { - segmentsCacheSize: 10, - }, - }), - }); - - sinon.assert.calledWith( - logger.log, - optimizelyFactory.enums.LOG_LEVEL.DEBUG, - 'Provisioning cache with maxSize of 10' - ); - }); - - it('should accept a custom cache timeout', () => { - const client = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpManager: BrowserOdpManager.createInstance({ - logger, - odpOptions: { - segmentsCacheTimeout: 10, - }, - }), - }); - - sinon.assert.calledWith( - logger.log, - optimizelyFactory.enums.LOG_LEVEL.DEBUG, - 'Provisioning cache with timeout of 10' - ); - }); - - it('should accept both a custom cache size and timeout', () => { - const client = optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - segmentsCacheSize: 10, - segmentsCacheTimeout: 10, - }, - }); - - sinon.assert.calledWith( - logger.log, - optimizelyFactory.enums.LOG_LEVEL.DEBUG, - 'Provisioning cache with maxSize of 10' - ); - - sinon.assert.calledWith( - logger.log, - optimizelyFactory.enums.LOG_LEVEL.DEBUG, - 'Provisioning cache with timeout of 10' - ); - }); - - it('should accept a valid custom odp segment manager', async () => { - const fakeSegmentManager = { - fetchQualifiedSegments: sinon.stub().returns(['a']), - updateSettings: sinon.spy(), - }; - - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - segmentManager: fakeSegmentManager, - }, - }); - - projectConfigManager.pushUpdate(config); - - const readyData = await client.onReady(); - - sinon.assert.called(fakeSegmentManager.updateSettings); - - const segments = await client.fetchQualifiedSegments(testVuid); - assert.deepEqual(segments, ['a']); - - sinon.assert.notCalled(logger.error); - sinon.assert.called(fakeSegmentManager.fetchQualifiedSegments); - }); - - it('should accept a valid custom odp event manager', async () => { - const fakeEventManager = { - start: sinon.spy(), - updateSettings: sinon.spy(), - flush: sinon.spy(), - stop: sinon.spy(), - registerVuid: sinon.spy(), - identifyUser: sinon.spy(), - sendEvent: sinon.spy(), - }; - - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - disabled: false, - eventManager: fakeEventManager, - }, - }); - projectConfigManager.pushUpdate(config); - - await client.onReady(); - - sinon.assert.called(fakeEventManager.start); - }); - - it('should send an odp event when calling sendOdpEvent with valid parameters', async () => { - const fakeEventManager = { - updateSettings: sinon.spy(), - start: sinon.spy(), - stop: sinon.spy(), - registerVuid: sinon.spy(), - identifyUser: sinon.spy(), - sendEvent: sinon.spy(), - flush: sinon.spy(), - }; - - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - eventManager: fakeEventManager, - }, - }); - - projectConfigManager.pushUpdate(config); - await client.onReady(); + // projectConfigManager.pushUpdate(config); + // await client.onReady(); - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); + // client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); - sinon.assert.notCalled(logger.error); - sinon.assert.called(fakeEventManager.sendEvent); - }); - - it('should augment odp events with user agent data if userAgentParser is provided', async () => { - const userAgentParser = { - parseUserAgentInfo() { - return { - os: { name: 'windows', version: '11' }, - device: { type: 'laptop', model: 'thinkpad' }, - }; - }, - }; - - const fakeRequestHandler = { - makeRequest: sinon.spy(function(requestUrl, headers, method, data) { - return { - abort: () => {}, - responsePromise: Promise.resolve({ statusCode: 200 }), - }; - }), - }; - - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - userAgentParser, - eventRequestHandler: fakeRequestHandler, - }, - }); - projectConfigManager.pushUpdate(config); - await client.onReady(); - - client.sendOdpEvent('test', '', new Map([['eamil', 'test@test.test']]), new Map([['key', 'value']])); - clock.tick(10000); - - const eventRequestUrl = new URL(fakeRequestHandler.makeRequest.lastCall.args[0]); - const searchParams = eventRequestUrl.searchParams; - - assert.equal(searchParams.get('os'), 'windows'); - assert.equal(searchParams.get('os_version'), '11'); - assert.equal(searchParams.get('device_type'), 'laptop'); - assert.equal(searchParams.get('model'), 'thinkpad'); - }); - - it('should convert fs-user-id, FS-USER-ID, and FS_USER_ID to fs_user_id identifier when calling sendOdpEvent', async () => { - const fakeEventManager = { - updateSettings: sinon.spy(), - start: sinon.spy(), - stop: sinon.spy(), - registerVuid: sinon.spy(), - identifyUser: sinon.spy(), - sendEvent: sinon.spy(), - flush: sinon.spy(), - }; - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - eventManager: fakeEventManager, - }, - }); - - projectConfigManager.pushUpdate(config); - await client.onReady(); - - // fs-user-id - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED, undefined, new Map([['fs-user-id', 'fsUserA']])); - sinon.assert.notCalled(logger.error); - sinon.assert.neverCalledWith(logger.warn, LOG_MESSAGES.ODP_SEND_EVENT_IDENTIFIER_CONVERSION_FAILED); - - const sendEventArgs1 = fakeEventManager.sendEvent.args; - assert.deepEqual( - sendEventArgs1[0].toString(), - new OdpEvent('fullstack', 'client_initialized', new Map([['fs_user_id', 'fsUserA']]), new Map()).toString() - ); - - // FS-USER-ID - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED, undefined, new Map([['FS-USER-ID', 'fsUserA']])); - sinon.assert.notCalled(logger.error); - sinon.assert.neverCalledWith(logger.warn, LOG_MESSAGES.ODP_SEND_EVENT_IDENTIFIER_CONVERSION_FAILED); - - const sendEventArgs2 = fakeEventManager.sendEvent.args; - assert.deepEqual( - sendEventArgs2[0].toString(), - new OdpEvent('fullstack', 'client_initialized', new Map([['fs_user_id', 'fsUserA']]), new Map()).toString() - ); - - // FS_USER_ID - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED, undefined, new Map([['FS_USER_ID', 'fsUserA']])); - sinon.assert.notCalled(logger.error); - sinon.assert.neverCalledWith(logger.warn, LOG_MESSAGES.ODP_SEND_EVENT_IDENTIFIER_CONVERSION_FAILED); - - const sendEventArgs3 = fakeEventManager.sendEvent.args; - assert.deepEqual( - sendEventArgs3[0].toString(), - new OdpEvent('fullstack', 'client_initialized', new Map([['fs_user_id', 'fsUserA']]), new Map()).toString() - ); - - // fs_user_id - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED, undefined, new Map([['fs_user_id', 'fsUserA']])); - sinon.assert.notCalled(logger.error); - sinon.assert.neverCalledWith(logger.warn, LOG_MESSAGES.ODP_SEND_EVENT_IDENTIFIER_CONVERSION_FAILED); - - const sendEventArgs4 = fakeEventManager.sendEvent.args; - assert.deepEqual( - sendEventArgs4[0].toString(), - new OdpEvent('fullstack', 'client_initialized', new Map([['fs_user_id', 'fsUserA']]), new Map()).toString() - ); - }); - - it('should throw an error and not send an odp event when calling sendOdpEvent with an invalid action input', async () => { - const fakeEventManager = { - updateSettings: sinon.spy(), - start: sinon.spy(), - stop: sinon.spy(), - registerVuid: sinon.spy(), - identifyUser: sinon.spy(), - sendEvent: sinon.spy(), - flush: sinon.spy(), - }; - - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - eventManager: fakeEventManager, - }, - }); - - projectConfigManager.pushUpdate(config); - await client.onReady(); - - client.sendOdpEvent(''); - sinon.assert.called(logger.error); - - client.sendOdpEvent(null); - sinon.assert.calledTwice(logger.error); - - client.sendOdpEvent(undefined); - sinon.assert.calledThrice(logger.error); - - sinon.assert.notCalled(fakeEventManager.sendEvent); - }); - - it('should use fullstack as a fallback value for the odp event when calling sendOdpEvent with an empty type input', async () => { - const fakeEventManager = { - updateSettings: sinon.spy(), - start: sinon.spy(), - stop: sinon.spy(), - registerVuid: sinon.spy(), - identifyUser: sinon.spy(), - sendEvent: sinon.spy(), - flush: sinon.spy(), - }; - - const config = createProjectConfig(testData.getOdpIntegratedConfigWithoutSegments()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - eventManager: fakeEventManager, - }, - }); - projectConfigManager.pushUpdate(config); - await client.onReady(); - - client.sendOdpEvent('dummy-action', ''); - - const sendEventArgs = fakeEventManager.sendEvent.args; - - const expectedEventArgs = new OdpEvent('fullstack', 'dummy-action', new Map(), new Map()); - assert.deepEqual(JSON.stringify(sendEventArgs[0][0]), JSON.stringify(expectedEventArgs)); - }); - - it('should log an error when attempting to send an odp event when odp is disabled', async () => { - const config = createProjectConfig(testData.getTestProjectConfigWithFeatures()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - disabled: true, - }, - }); - - projectConfigManager.pushUpdate(config); - - await client.onReady(); - - assert.isUndefined(client.odpManager); - sinon.assert.calledWith(logger.log, optimizelyFactory.enums.LOG_LEVEL.INFO, 'ODP Disabled.'); - - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); - - sinon.assert.calledWith( - logger.error, - optimizelyFactory.enums.ERROR_MESSAGES.ODP_EVENT_FAILED_ODP_MANAGER_MISSING - ); - }); - - it('should log a warning when attempting to use an event batch size other than 1', async () => { - const config = createProjectConfig(testData.getTestProjectConfigWithFeatures()); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); - - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - eventBatchSize: 5, - }, - }); + // sinon.assert.notCalled(logger.error); + // sinon.assert.called(fakeEventManager.sendEvent); + // }); - projectConfigManager.pushUpdate(config); - await client.onReady(); + // it('should log an error when attempting to send an odp event when odp is disabled', async () => { + // const config = createProjectConfig(testData.getTestProjectConfigWithFeatures()); + // const projectConfigManager = getMockProjectConfigManager({ + // initConfig: config, + // onRunning: Promise.resolve(), + // }); - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); + // const client = optimizelyFactory.createInstance({ + // projectConfigManager, + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // eventBatchSize: null, + // logger, + // odpOptions: { + // disabled: true, + // }, + // }); - sinon.assert.calledWith( - logger.log, - optimizelyFactory.enums.LOG_LEVEL.WARNING, - 'ODP event batch size must be 1 in the browser.' - ); - assert(client.odpManager.eventManager.batchSize, 1); - }); + // projectConfigManager.pushUpdate(config); - it('should send an odp event to the browser endpoint', async () => { - const odpConfig = new OdpConfig(); + // await client.onReady(); - const apiManager = new BrowserOdpEventApiManager(mockRequestHandler, logger); - const eventManager = new BrowserOdpEventManager({ - odpConfig, - apiManager, - logger, - clientEngine: 'javascript-sdk', - clientVersion: 'great', - }); + // assert.isUndefined(client.odpManager); + // sinon.assert.calledWith(logger.log, optimizelyFactory.enums.LOG_LEVEL.INFO, 'ODP Disabled.'); - let datafile = testData.getOdpIntegratedConfigWithSegments(); - const config = createProjectConfig(datafile); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); + // client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - odpConfig, - eventManager, - }, - }); - - projectConfigManager.pushUpdate(config); - await client.onReady(); + // sinon.assert.calledWith( + // logger.error, + // optimizelyFactory.enums.ERROR_MESSAGES.ODP_EVENT_FAILED_ODP_MANAGER_MISSING + // ); + // }); - client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); + // it('should log a warning when attempting to use an event batch size other than 1', async () => { + // const config = createProjectConfig(testData.getTestProjectConfigWithFeatures()); + // const projectConfigManager = getMockProjectConfigManager({ + // initConfig: config, + // onRunning: Promise.resolve(), + // }); - // wait for request to be sent - clock.tick(100); + // const client = optimizelyFactory.createInstance({ + // projectConfigManager, + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // eventBatchSize: null, + // logger, + // odpOptions: { + // eventBatchSize: 5, + // }, + // }); - let publicKey = datafile.integrations[0].publicKey; - let pixelUrl = datafile.integrations[0].pixelUrl; + // projectConfigManager.pushUpdate(config); - const pixelApiEndpoint = `${pixelUrl}/v2/zaius.gif`; - let requestEndpoint = new URL(requestParams.get('endpoint')); - assert.equal(requestEndpoint.origin + requestEndpoint.pathname, pixelApiEndpoint); - assert.equal(requestParams.get('method'), 'GET'); + // await client.onReady(); - let searchParams = requestEndpoint.searchParams; - assert.lengthOf(searchParams.get('idempotence_id'), 36); - assert.equal(searchParams.get('data_source'), 'javascript-sdk'); - assert.equal(searchParams.get('data_source_type'), 'sdk'); - assert.equal(searchParams.get('data_source_version'), 'great'); - assert.equal(searchParams.get('tracker_id'), publicKey); - assert.equal(searchParams.get('event_type'), 'fullstack'); - assert.equal(searchParams.get('vdl_action'), ODP_EVENT_ACTION.INITIALIZED); - assert.isTrue(searchParams.get('vuid').startsWith('vuid_')); - assert.isNotNull(searchParams.get('data_source_version')); + // client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); - sinon.assert.notCalled(logger.error); - }); + // sinon.assert.calledWith( + // logger.log, + // optimizelyFactory.enums.LOG_LEVEL.WARNING, + // 'ODP event batch size must be 1 in the browser.' + // ); + // assert(client.odpManager.eventManager.batchSize, 1); + // }); - it('should send odp client_initialized on client instantiation', async () => { - const odpConfig = new OdpConfig('key', 'host', 'pixel', []); - const apiManager = new BrowserOdpEventApiManager(mockRequestHandler, logger); - sinon.spy(apiManager, 'sendEvents'); - const eventManager = new BrowserOdpEventManager({ - odpConfig, - apiManager, - logger, - }); - const datafile = testData.getOdpIntegratedConfigWithSegments(); - const config = createProjectConfig(datafile); - const projectConfigManager = getMockProjectConfigManager({ - initConfig: config, - onRunning: Promise.resolve(), - }); + // it('should send odp client_initialized on client instantiation', async () => { + // const odpConfig = new OdpConfig('key', 'host', 'pixel', []); + // const apiManager = new BrowserOdpEventApiManager(mockRequestHandler, logger); + // sinon.spy(apiManager, 'sendEvents'); + // const eventManager = new BrowserOdpEventManager({ + // odpConfig, + // apiManager, + // logger, + // }); + // const datafile = testData.getOdpIntegratedConfigWithSegments(); + // const config = createProjectConfig(datafile); + // const projectConfigManager = getMockProjectConfigManager({ + // initConfig: config, + // onRunning: Promise.resolve(), + // }); - const client = optimizelyFactory.createInstance({ - projectConfigManager, - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - eventBatchSize: null, - logger, - odpOptions: { - odpConfig, - eventManager, - }, - }); + // const client = optimizelyFactory.createInstance({ + // projectConfigManager, + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // eventBatchSize: null, + // logger, + // odpOptions: { + // odpConfig, + // eventManager, + // }, + // }); - projectConfigManager.pushUpdate(config); - await client.onReady(); + // projectConfigManager.pushUpdate(config); + // await client.onReady(); - clock.tick(100); + // clock.tick(100); - const [_, events] = apiManager.sendEvents.getCall(0).args; + // const [_, events] = apiManager.sendEvents.getCall(0).args; - const [firstEvent] = events; - assert.equal(firstEvent.action, 'client_initialized'); - assert.equal(firstEvent.type, 'fullstack'); - }); + // const [firstEvent] = events; + // assert.equal(firstEvent.action, 'client_initialized'); + // assert.equal(firstEvent.type, 'fullstack'); + // }); }); }); }); diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 1e72cbd54..7317540db 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -84,7 +84,7 @@ const createInstance = function(config: Config): Client | null { const optimizelyOptions: OptimizelyOptions = { ...config, - clientEngine: clientEngine || enums.NODE_CLIENT_ENGINE, + clientEngine: clientEngine || enums.JAVASCRIPT_CLIENT_ENGINE, clientVersion: clientVersion || enums.CLIENT_VERSION, logger, errorHandler, diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index 6172ce4d3..8cedf06d5 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -78,7 +78,7 @@ const createInstance = function(config: Config): Client | null { const optimizelyOptions = { ...config, - clientEngine: clientEngine || enums.NODE_CLIENT_ENGINE, + clientEngine: clientEngine || enums.REACT_NATIVE_JS_CLIENT_ENGINE, clientVersion: clientVersion || enums.CLIENT_VERSION, logger, errorHandler, diff --git a/lib/odp/segment_manager/odp_segment_manager.spec.ts b/lib/odp/segment_manager/odp_segment_manager.spec.ts index 757aab52d..5d6fea58a 100644 --- a/lib/odp/segment_manager/odp_segment_manager.spec.ts +++ b/lib/odp/segment_manager/odp_segment_manager.spec.ts @@ -119,7 +119,8 @@ describe('DefaultOdpSegmentManager', () => { manager.updateConfig({ integrated: true, odpConfig: config }); cache.set(manager.makeCacheKey(userKey, userValue), ['x']); - // @ts-ignore + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore const segments = await manager.fetchQualifiedSegments(userKey, userValue, ['IGNORE_CACHE']); expect(segments).toEqual(['k', 'l']); expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['x']); @@ -154,6 +155,7 @@ describe('DefaultOdpSegmentManager', () => { cache.set(manager.makeCacheKey(userKey, '123'), ['a']); cache.set(manager.makeCacheKey(userKey, '456'), ['b']); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore const segments = await manager.fetchQualifiedSegments(userKey, userValue, ['RESET_CACHE']); expect(segments).toEqual(['k', 'l']); diff --git a/lib/tests/testUtils.ts b/lib/tests/testUtils.ts index 7d6e86dc4..cee93ca2c 100644 --- a/lib/tests/testUtils.ts +++ b/lib/tests/testUtils.ts @@ -1,7 +1,7 @@ -export const exhaustMicrotasks = async (loop = 100) => { +export const exhaustMicrotasks = async (loop = 100): Promise => { for(let i = 0; i < loop; i++) { await Promise.resolve(); } }; -export const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file +export const wait = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file diff --git a/lib/vuid/vuid.ts b/lib/vuid/vuid.ts index 4c49204c8..53cbaf805 100644 --- a/lib/vuid/vuid.ts +++ b/lib/vuid/vuid.ts @@ -1,6 +1,6 @@ import { v4 as uuidV4 } from 'uuid'; -export const VUID_PREFIX: string = `vuid_`; +export const VUID_PREFIX = `vuid_`; export const VUID_MAX_LENGTH = 32; export const isVuid = (vuid: string): boolean => vuid.startsWith(VUID_PREFIX) && vuid.length > VUID_PREFIX.length; diff --git a/tests/odpManager.browser.spec.ts b/tests/odpManager.browser.spec.ts deleted file mode 100644 index ee9415a78..000000000 --- a/tests/odpManager.browser.spec.ts +++ /dev/null @@ -1,513 +0,0 @@ -/** - * Copyright 2023-2024, 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, beforeEach, beforeAll, it, expect } from 'vitest'; - -import { instance, mock, resetCalls } from 'ts-mockito'; - -import { LogHandler, LogLevel } from '../lib/modules/logging'; -import { RequestHandler } from '../lib/utils/http_request_handler/http'; -import { BrowserLRUCache } from './../lib/utils/lru_cache/browser_lru_cache'; - -import { BrowserOdpManager } from './../lib/odp/odp_manager.browser'; - -import { OdpConfig } from '../lib/odp/odp_config'; -import { BrowserOdpEventApiManager } from '../lib/odp/event_manager/event_api_manager.browser'; -import { OdpSegmentManager } from './../lib/odp/segment_manager/odp_segment_manager'; -import { OdpSegmentApiManager } from '../lib/odp/segment_manager/odp_segment_api_manager'; -import { VuidManager } from '../lib/plugins/vuid_manager'; -import { BrowserRequestHandler } from '../lib/utils/http_request_handler/browser_request_handler'; -import { BrowserOdpEventManager } from '../lib/odp/event_manager/event_manager.browser'; -import { OdpOptions } from '../lib/shared_types'; - - -const keyA = 'key-a'; -const hostA = 'host-a'; -const pixelA = 'pixel-a'; -const segmentsA = ['a']; -const userA = 'fs-user-a'; -const vuidA = 'vuid_a'; -const odpConfigA = new OdpConfig(keyA, hostA, pixelA, segmentsA); - -const keyB = 'key-b'; -const hostB = 'host-b'; -const pixelB = 'pixel-b'; -const segmentsB = ['b']; -const userB = 'fs-user-b'; -const vuidB = 'vuid_b'; -const odpConfigB = new OdpConfig(keyB, hostB, pixelB, segmentsB); - -describe('OdpManager', () => { - let odpConfig: OdpConfig; - - let mockLogger: LogHandler; - let fakeLogger: LogHandler; - - let mockRequestHandler: RequestHandler; - let fakeRequestHandler: RequestHandler; - - let mockEventApiManager: BrowserOdpEventApiManager; - let fakeEventApiManager: BrowserOdpEventApiManager; - - let mockEventManager: BrowserOdpEventManager; - let fakeEventManager: BrowserOdpEventManager; - - let mockSegmentApiManager: OdpSegmentApiManager; - let fakeSegmentApiManager: OdpSegmentApiManager; - - let mockSegmentManager: OdpSegmentManager; - let fakeSegmentManager: OdpSegmentManager; - - let mockBrowserOdpManager: BrowserOdpManager; - let fakeBrowserOdpManager: BrowserOdpManager; - - beforeAll(() => { - mockLogger = mock(); - mockRequestHandler = mock(); - - odpConfig = new OdpConfig(keyA, hostA, pixelA, segmentsA); - fakeLogger = instance(mockLogger); - fakeRequestHandler = instance(mockRequestHandler); - - mockEventApiManager = mock(); - mockEventManager = mock(); - mockSegmentApiManager = mock(); - mockSegmentManager = mock(); - mockBrowserOdpManager = mock(); - - fakeEventApiManager = instance(mockEventApiManager); - fakeEventManager = instance(mockEventManager); - fakeSegmentApiManager = instance(mockSegmentApiManager); - fakeSegmentManager = instance(mockSegmentManager); - fakeBrowserOdpManager = instance(mockBrowserOdpManager); - }); - - beforeEach(() => { - resetCalls(mockLogger); - resetCalls(mockRequestHandler); - resetCalls(mockEventApiManager); - resetCalls(mockEventManager); - resetCalls(mockSegmentManager); - }); - - const browserOdpManagerInstance = () => - BrowserOdpManager.createInstance({ - odpOptions: { - eventManager: fakeEventManager, - segmentManager: fakeSegmentManager, - }, - }); - - it('should create VUID automatically on BrowserOdpManager initialization', async () => { - const browserOdpManager = browserOdpManagerInstance(); - const vuidManager = await VuidManager.instance(BrowserOdpManager.cache); - expect(browserOdpManager.vuid).toBe(vuidManager.vuid); - }); - - describe('Populates BrowserOdpManager correctly with all odpOptions', () => { - beforeAll(() => { - - }); - - it('Custom odpOptions.segmentsCache overrides default LRUCache', () => { - const odpOptions: OdpOptions = { - segmentsCache: new BrowserLRUCache({ - maxSize: 2, - timeout: 4000, - }), - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - const segmentManager = browserOdpManager['segmentManager'] as OdpSegmentManager; - - // @ts-ignore - expect(browserOdpManager.segmentManager._segmentsCache.maxSize).toBe(2); - - // @ts-ignore - expect(browserOdpManager.segmentManager._segmentsCache.timeout).toBe(4000); - }); - - it('Custom odpOptions.segmentsCacheSize overrides default LRUCache size', () => { - const odpOptions: OdpOptions = { - segmentsCacheSize: 2, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager._segmentsCache.maxSize).toBe(2); - }); - - it('Custom odpOptions.segmentsCacheTimeout overrides default LRUCache timeout', () => { - const odpOptions: OdpOptions = { - segmentsCacheTimeout: 4000, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager._segmentsCache.timeout).toBe(4000); - }); - - it('Custom odpOptions.segmentsApiTimeout overrides default Segment API Request Handler timeout', () => { - const odpOptions: OdpOptions = { - segmentsApiTimeout: 4000, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager.odpSegmentApiManager.requestHandler.timeout).toBe(4000); - }); - - it('Browser default Segments API Request Handler timeout should be used when odpOptions does not include segmentsApiTimeout', () => { - const browserOdpManager = BrowserOdpManager.createInstance({}); - - // @ts-ignore - expect(browserOdpManager.segmentManager.odpSegmentApiManager.requestHandler.timeout).toBe(10000); - }); - - it('Custom odpOptions.segmentsRequestHandler overrides default Segment API Request Handler', () => { - const odpOptions: OdpOptions = { - segmentsRequestHandler: new BrowserRequestHandler({ logger: fakeLogger, timeout: 4000 }), - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager.odpSegmentApiManager.requestHandler.timeout).toBe(4000); - }); - - it('Custom odpOptions.segmentRequestHandler override takes precedence over odpOptions.eventApiTimeout', () => { - const odpOptions: OdpOptions = { - segmentsApiTimeout: 2, - segmentsRequestHandler: new BrowserRequestHandler({ logger: fakeLogger, timeout: 1 }), - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager.odpSegmentApiManager.requestHandler.timeout).toBe(1); - }); - - it('Custom odpOptions.segmentManager overrides default Segment Manager', () => { - const customSegmentManager = new OdpSegmentManager( - new BrowserLRUCache(), - fakeSegmentApiManager, - fakeLogger, - odpConfig, - ); - - const odpOptions: OdpOptions = { - segmentManager: customSegmentManager, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager).toBe(customSegmentManager); - }); - - it('Custom odpOptions.segmentManager override takes precedence over all other segments-related odpOptions', () => { - const customSegmentManager = new OdpSegmentManager( - new BrowserLRUCache({ - maxSize: 1, - timeout: 1, - }), - new OdpSegmentApiManager(new BrowserRequestHandler({ logger: fakeLogger, timeout: 1 }), fakeLogger), - fakeLogger, - odpConfig, - ); - - const odpOptions: OdpOptions = { - segmentsCacheSize: 2, - segmentsCacheTimeout: 2, - segmentsCache: new BrowserLRUCache({ maxSize: 2, timeout: 2 }), - segmentsApiTimeout: 2, - segmentsRequestHandler: new BrowserRequestHandler({ logger: fakeLogger, timeout: 2 }), - segmentManager: customSegmentManager, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager?._segmentsCache.maxSize).toBe(1); - - // @ts-ignore - expect(browserOdpManager.segmentManager?._segmentsCache.timeout).toBe(1); - - // @ts-ignore - expect(browserOdpManager.segmentManager.odpSegmentApiManager.requestHandler.timeout).toBe(1); - - // @ts-ignore - expect(browserOdpManager.segmentManager).toBe(customSegmentManager); - }); - - it('Custom odpOptions.eventApiTimeout overrides default Event API Request Handler timeout', () => { - const odpOptions: OdpOptions = { - eventApiTimeout: 4000, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.apiManager.requestHandler.timeout).toBe(4000); - }); - - it('Browser default Events API Request Handler timeout should be used when odpOptions does not include eventsApiTimeout', () => { - const odpOptions: OdpOptions = {}; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.apiManager.requestHandler.timeout).toBe(10000); - }); - - it('Custom odpOptions.eventFlushInterval cannot override the default Event Manager flush interval', () => { - const odpOptions: OdpOptions = { - eventFlushInterval: 4000, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.flushInterval).toBe(0); // Note: Browser flush interval is always 0 due to use of Pixel API - }); - - it('Default ODP event flush interval is used when odpOptions does not include eventFlushInterval', () => { - const odpOptions: OdpOptions = {}; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.flushInterval).toBe(0); - }); - - it('ODP event batch size set to one when odpOptions.eventFlushInterval set to 0', () => { - const odpOptions: OdpOptions = { - eventFlushInterval: 0, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.flushInterval).toBe(0); - - // @ts-ignore - expect(browserOdpManager.eventManager.batchSize).toBe(1); - }); - - it('Custom odpOptions.eventBatchSize does not override default Event Manager batch size', () => { - const odpOptions: OdpOptions = { - eventBatchSize: 2, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.batchSize).toBe(1); // Note: Browser event batch size is always 1 due to use of Pixel API - }); - - it('Custom odpOptions.eventQueueSize overrides default Event Manager queue size', () => { - const odpOptions: OdpOptions = { - eventQueueSize: 2, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.queueSize).toBe(2); - }); - - it('Custom odpOptions.eventRequestHandler overrides default Event Manager request handler', () => { - const odpOptions: OdpOptions = { - eventRequestHandler: new BrowserRequestHandler({ logger: fakeLogger, timeout: 4000 }), - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.apiManager.requestHandler.timeout).toBe(4000); - }); - - it('Custom odpOptions.eventRequestHandler override takes precedence over odpOptions.eventApiTimeout', () => { - const odpOptions: OdpOptions = { - eventApiTimeout: 2, - eventBatchSize: 2, - eventFlushInterval: 2, - eventQueueSize: 2, - eventRequestHandler: new BrowserRequestHandler({ logger: fakeLogger, timeout: 1 }), - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager.apiManager.requestHandler.timeout).toBe(1); - }); - - it('Custom odpOptions.eventManager overrides default Event Manager', () => { - const fakeClientEngine = 'test-javascript-sdk'; - const fakeClientVersion = '1.2.3'; - - const customEventManager = new BrowserOdpEventManager({ - odpConfig, - apiManager: fakeEventApiManager, - logger: fakeLogger, - clientEngine: fakeClientEngine, - clientVersion: fakeClientVersion, - }); - - const odpOptions: OdpOptions = { - eventManager: customEventManager, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager).toBe(customEventManager); - - // @ts-ignore - expect(browserOdpManager.eventManager.clientEngine).toBe(fakeClientEngine); - - // @ts-ignore - expect(browserOdpManager.eventManager.clientVersion).toBe(fakeClientVersion); - }); - - it('Custom odpOptions.eventManager override takes precedence over all other event-related odpOptions', () => { - const fakeClientEngine = 'test-javascript-sdk'; - const fakeClientVersion = '1.2.3'; - - const customEventManager = new BrowserOdpEventManager({ - odpConfig, - apiManager: new BrowserOdpEventApiManager(new BrowserRequestHandler({ logger: fakeLogger, timeout: 1 }), fakeLogger), - logger: fakeLogger, - clientEngine: fakeClientEngine, - clientVersion: fakeClientVersion, - queueSize: 1, - batchSize: 1, - flushInterval: 1, - }); - - const odpOptions: OdpOptions = { - eventApiTimeout: 2, - eventBatchSize: 2, - eventFlushInterval: 2, - eventQueueSize: 2, - eventRequestHandler: new BrowserRequestHandler({ logger: fakeLogger, timeout: 3 }), - eventManager: customEventManager, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.eventManager).toBe(customEventManager); - - // @ts-ignore - expect(browserOdpManager.eventManager.clientEngine).toBe(fakeClientEngine); - - // @ts-ignore - expect(browserOdpManager.eventManager.clientVersion).toBe(fakeClientVersion); - - // @ts-ignore - expect(browserOdpManager.eventManager.batchSize).toBe(1); - - // @ts-ignore - expect(browserOdpManager.eventManager.flushInterval).toBe(0); // Note: Browser event flush interval will always be 0 due to use of Pixel API - - // @ts-ignore - expect(browserOdpManager.eventManager.queueSize).toBe(1); - - // @ts-ignore - expect(browserOdpManager.eventManager.apiManager.requestHandler.timeout).toBe(1); - }); - - it('Custom odpOptions micro values (non-request/manager) override all expected fields for both segments and event managers', () => { - const odpOptions: OdpOptions = { - segmentsCacheSize: 4, - segmentsCacheTimeout: 4, - segmentsCache: new BrowserLRUCache({ maxSize: 4, timeout: 4 }), - segmentsApiTimeout: 4, - eventApiTimeout: 4, - eventBatchSize: 4, - eventFlushInterval: 4, - eventQueueSize: 4, - }; - - const browserOdpManager = BrowserOdpManager.createInstance({ - odpOptions, - }); - - // @ts-ignore - expect(browserOdpManager.segmentManager?._segmentsCache.maxSize).toBe(4); - - // @ts-ignore - expect(browserOdpManager.segmentManager?._segmentsCache.timeout).toBe(4); - - // @ts-ignore - expect(browserOdpManager.segmentManager.odpSegmentApiManager.requestHandler.timeout).toBe(4); - - // @ts-ignore - expect(browserOdpManager.eventManager.batchSize).toBe(1); // Note: Browser batch size will always be 1 due to use of Pixel API - - // @ts-ignore - expect(browserOdpManager.eventManager.flushInterval).toBe(0); // Note: Browser event flush interval will always be 0 due to use of Pixel API - - // @ts-ignore - expect(browserOdpManager.eventManager.queueSize).toBe(4); - - // @ts-ignore - expect(browserOdpManager.eventManager.apiManager.requestHandler.timeout).toBe(4); - }); - }); -}); From 9ba83d86018e9b7e010e0ff1b6124ee08caf59bf Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 10 Dec 2024 00:50:56 +0600 Subject: [PATCH 24/28] upd --- lib/index.browser.tests.js | 32 -------------------------------- lib/optimizely/index.spec.ts | 18 +++++------------- 2 files changed, 5 insertions(+), 45 deletions(-) diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js index 3405beea4..0a7859353 100644 --- a/lib/index.browser.tests.js +++ b/lib/index.browser.tests.js @@ -543,38 +543,6 @@ describe('javascript-sdk (Browser)', function() { // ); // }); - // it('should log a warning when attempting to use an event batch size other than 1', async () => { - // const config = createProjectConfig(testData.getTestProjectConfigWithFeatures()); - // const projectConfigManager = getMockProjectConfigManager({ - // initConfig: config, - // onRunning: Promise.resolve(), - // }); - - // const client = optimizelyFactory.createInstance({ - // projectConfigManager, - // errorHandler: fakeErrorHandler, - // eventDispatcher: fakeEventDispatcher, - // eventBatchSize: null, - // logger, - // odpOptions: { - // eventBatchSize: 5, - // }, - // }); - - // projectConfigManager.pushUpdate(config); - - // await client.onReady(); - - // client.sendOdpEvent(ODP_EVENT_ACTION.INITIALIZED); - - // sinon.assert.calledWith( - // logger.log, - // optimizelyFactory.enums.LOG_LEVEL.WARNING, - // 'ODP event batch size must be 1 in the browser.' - // ); - // assert(client.odpManager.eventManager.batchSize, 1); - // }); - // it('should send odp client_initialized on client instantiation', async () => { // const odpConfig = new OdpConfig('key', 'host', 'pixel', []); // const apiManager = new BrowserOdpEventApiManager(mockRequestHandler, logger); diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts index ee1525e2d..364c05658 100644 --- a/lib/optimizely/index.spec.ts +++ b/lib/optimizely/index.spec.ts @@ -25,8 +25,9 @@ import testData from '../tests/test_data'; import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; import { LoggerFacade } from '../modules/logging'; import { createProjectConfig } from '../project_config/project_config'; +import { getMockLogger } from '../tests/mock/mock_logger'; -describe('lib/optimizely', () => { +describe('Optimizely', () => { const errorHandler = { handleError: function() {} }; const eventDispatcher = { @@ -35,18 +36,9 @@ describe('lib/optimizely', () => { const eventProcessor = getForwardingEventProcessor(eventDispatcher); - const createdLogger: LoggerFacade = { - ...logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - }), - info: () => {}, - debug: () => {}, - warn: () => {}, - error: () => {}, - log: () => {}, - }; + const logger = getMockLogger(); - const notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler }); + const notificationCenter = createNotificationCenter({ logger, errorHandler }); it('should pass ssr to the project config manager', () => { const projectConfigManager = getMockProjectConfigManager({ @@ -60,7 +52,7 @@ describe('lib/optimizely', () => { projectConfigManager, errorHandler, jsonSchemaValidator, - logger: createdLogger, + logger, notificationCenter, eventProcessor, isSsr: true, From c280d5ddfae717720fc61afb6da0427aaa2fda21 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Wed, 11 Dec 2024 18:34:05 +0600 Subject: [PATCH 25/28] rem --- .../browserAsyncStorageCache.ts | 75 --------------- tests/browserAsyncStorageCache.spec.ts | 92 ------------------- 2 files changed, 167 deletions(-) delete mode 100644 lib/plugins/key_value_cache/browserAsyncStorageCache.ts delete mode 100644 tests/browserAsyncStorageCache.spec.ts diff --git a/lib/plugins/key_value_cache/browserAsyncStorageCache.ts b/lib/plugins/key_value_cache/browserAsyncStorageCache.ts deleted file mode 100644 index 508a9e5f4..000000000 --- a/lib/plugins/key_value_cache/browserAsyncStorageCache.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright 2022-2024, 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 { tryWithLocalStorage } from '../../utils/local_storage/tryLocalStorage'; -import PersistentKeyValueCache from './persistentKeyValueCache'; -import { getLogger } from '../../modules/logging'; -import { ERROR_MESSAGES } from './../../utils/enums/index'; - -export default class BrowserAsyncStorageCache implements PersistentKeyValueCache { - logger = getLogger(); - - async contains(key: string): Promise { - return tryWithLocalStorage({ - browserCallback: (localStorage?: Storage) => { - return localStorage?.getItem(key) !== null; - }, - nonBrowserCallback: () => { - this.logger.error(ERROR_MESSAGES.LOCAL_STORAGE_DOES_NOT_EXIST); - return false; - }, - }); - } - - async get(key: string): Promise { - return tryWithLocalStorage({ - browserCallback: (localStorage?: Storage) => { - return (localStorage?.getItem(key) || undefined); - }, - nonBrowserCallback: () => { - this.logger.error(ERROR_MESSAGES.LOCAL_STORAGE_DOES_NOT_EXIST); - return undefined; - }, - }); - } - - async remove(key: string): Promise { - if (await this.contains(key)) { - tryWithLocalStorage({ - browserCallback: (localStorage?: Storage) => { - localStorage?.removeItem(key); - }, - nonBrowserCallback: () => { - this.logger.error(ERROR_MESSAGES.LOCAL_STORAGE_DOES_NOT_EXIST); - }, - }); - return true; - } else { - return false; - } - } - - async set(key: string, val: string): Promise { - return tryWithLocalStorage({ - browserCallback: (localStorage?: Storage) => { - localStorage?.setItem(key, val); - }, - nonBrowserCallback: () => { - this.logger.error(ERROR_MESSAGES.LOCAL_STORAGE_DOES_NOT_EXIST); - }, - }); - } -} diff --git a/tests/browserAsyncStorageCache.spec.ts b/tests/browserAsyncStorageCache.spec.ts deleted file mode 100644 index c30b675bc..000000000 --- a/tests/browserAsyncStorageCache.spec.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2022, 2024, 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, beforeEach, it, expect, vi } from 'vitest'; - -import BrowserAsyncStorageCache from '../lib/plugins/key_value_cache/browserAsyncStorageCache'; - -describe('BrowserAsyncStorageCache', () => { - const KEY_THAT_EXISTS = 'keyThatExists'; - const VALUE_FOR_KEY_THAT_EXISTS = 'some really super value that exists for keyThatExists'; - const NONEXISTENT_KEY = 'someKeyThatDoesNotExist'; - - let cacheInstance: BrowserAsyncStorageCache; - - beforeEach(() => { - const stubData = new Map(); - stubData.set(KEY_THAT_EXISTS, VALUE_FOR_KEY_THAT_EXISTS); - - cacheInstance = new BrowserAsyncStorageCache(); - - vi - .spyOn(localStorage, 'getItem') - .mockImplementation((key) => key == KEY_THAT_EXISTS ? VALUE_FOR_KEY_THAT_EXISTS : null); - vi - .spyOn(localStorage, 'setItem') - .mockImplementation(() => 1); - vi - .spyOn(localStorage, 'removeItem') - .mockImplementation((key) => key == KEY_THAT_EXISTS); - }); - - describe('contains', () => { - it('should return true if value with key exists', async () => { - const keyWasFound = await cacheInstance.contains(KEY_THAT_EXISTS); - - expect(keyWasFound).toBe(true); - }); - - it('should return false if value with key does not exist', async () => { - const keyWasFound = await cacheInstance.contains(NONEXISTENT_KEY); - - expect(keyWasFound).toBe(false); - }); - }); - - describe('get', () => { - it('should return correct string when item is found in cache', async () => { - const foundValue = await cacheInstance.get(KEY_THAT_EXISTS); - - expect(foundValue).toEqual(VALUE_FOR_KEY_THAT_EXISTS); - }); - - it('should return undefined if item is not found in cache', async () => { - const json = await cacheInstance.get(NONEXISTENT_KEY); - - expect(json).toBeUndefined(); - }); - }); - - describe('remove', () => { - it('should return true after removing a found entry', async () => { - const wasSuccessful = await cacheInstance.remove(KEY_THAT_EXISTS); - - expect(wasSuccessful).toBe(true); - }); - - it('should return false after trying to remove an entry that is not found ', async () => { - const wasSuccessful = await cacheInstance.remove(NONEXISTENT_KEY); - - expect(wasSuccessful).toBe(false); - }); - }); - - describe('set', () => { - it('should resolve promise if item was successfully set in the cache', async () => { - await cacheInstance.set('newTestKey', 'a value for this newTestKey'); - }); - }); -}); From 9bb07f7b92668eb2940f5e273803c1b1c3affe85 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 13 Dec 2024 01:25:49 +0600 Subject: [PATCH 26/28] review --- lib/odp/constant.ts | 16 +++++++++++ lib/odp/event_manager/odp_event.ts | 2 +- .../odp_event_api_manager.spec.ts | 7 ++--- .../event_manager/odp_event_api_manager.ts | 2 +- .../event_manager/odp_event_manager.spec.ts | 2 +- lib/odp/odp_manager_factory.browser.spec.ts | 16 +++++++++++ lib/odp/odp_manager_factory.browser.ts | 16 +++++++++++ lib/odp/odp_manager_factory.node.spec.ts | 16 +++++++++++ lib/odp/odp_manager_factory.node.ts | 16 +++++++++++ .../odp_manager_factory.react_native.spec.ts | 16 +++++++++++ lib/odp/odp_manager_factory.react_native.ts | 16 +++++++++++ lib/odp/odp_manager_factory.spec.ts | 16 +++++++++++ lib/odp/odp_manager_factory.ts | 16 +++++++++++ lib/odp/odp_types.ts | 2 +- .../segment_manager/odp_response_schema.ts | 2 +- .../odp_segment_api_manager.ts | 2 +- .../odp_segment_manager.spec.ts | 2 +- .../optimizely_segment_option.ts | 2 +- lib/optimizely/index.ts | 4 +-- lib/tests/testUtils.ts | 18 +++++++++++- lib/utils/cache/in_memory_lru_cache.spec.ts | 7 ----- lib/utils/cache/in_memory_lru_cache.ts | 2 +- lib/vuid/vuid.spec.ts | 28 +++++++++++++++++-- lib/vuid/vuid.ts | 16 +++++++++++ lib/vuid/vuid_manager.spec.ts | 14 +++++----- lib/vuid/vuid_manager_factory.browser.spec.ts | 16 +++++++++++ lib/vuid/vuid_manager_factory.node.spec.ts | 16 +++++++++++ .../vuid_manager_factory.react_native.spec.ts | 16 +++++++++++ lib/vuid/vuid_manager_factory.ts | 16 +++++++++++ 29 files changed, 287 insertions(+), 33 deletions(-) diff --git a/lib/odp/constant.ts b/lib/odp/constant.ts index 4499fb1d8..c33f3f0c9 100644 --- a/lib/odp/constant.ts +++ b/lib/odp/constant.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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. + */ + export enum ODP_USER_KEY { VUID = 'vuid', FS_USER_ID = 'fs_user_id', diff --git a/lib/odp/event_manager/odp_event.ts b/lib/odp/event_manager/odp_event.ts index e777789bc..062798d1b 100644 --- a/lib/odp/event_manager/odp_event.ts +++ b/lib/odp/event_manager/odp_event.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, 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/lib/odp/event_manager/odp_event_api_manager.spec.ts b/lib/odp/event_manager/odp_event_api_manager.spec.ts index c5fe60e87..ca9bd81b1 100644 --- a/lib/odp/event_manager/odp_event_api_manager.spec.ts +++ b/lib/odp/event_manager/odp_event_api_manager.spec.ts @@ -14,14 +14,11 @@ * limitations under the License. */ -import { describe, beforeEach, beforeAll, it, expect, vi } from 'vitest'; +import { describe, it, expect, vi } from 'vitest'; -import { LogHandler, LogLevel } from '../../modules/logging'; import { DefaultOdpEventApiManager, eventApiRequestGenerator, pixelApiRequestGenerator } from './odp_event_api_manager'; -import { OdpEvent } from './odp_event'; -import { RequestHandler } from '../../utils/http_request_handler/http'; +import { OdpEvent } from './odp_event';; import { OdpConfig } from '../odp_config'; -import { get } from 'http'; const data1 = new Map(); data1.set('key11', 'value-1'); diff --git a/lib/odp/event_manager/odp_event_api_manager.ts b/lib/odp/event_manager/odp_event_api_manager.ts index 3ec22be34..8ea4f7060 100644 --- a/lib/odp/event_manager/odp_event_api_manager.ts +++ b/lib/odp/event_manager/odp_event_api_manager.ts @@ -51,7 +51,7 @@ export class DefaultOdpEventApiManager implements OdpEventApiManager { async sendEvents(odpConfig: OdpConfig, events: OdpEvent[]): Promise { if (events.length === 0) { - return Promise.resolve({}); + return {}; } const { method, endpoint, headers, data } = this.requestGenerator(odpConfig, events); diff --git a/lib/odp/event_manager/odp_event_manager.spec.ts b/lib/odp/event_manager/odp_event_manager.spec.ts index 7a51d3065..dfe8d496a 100644 --- a/lib/odp/event_manager/odp_event_manager.spec.ts +++ b/lib/odp/event_manager/odp_event_manager.spec.ts @@ -175,7 +175,7 @@ describe('DefaultOdpEventManager', () => { expect(apiManager.sendEvents).toHaveBeenNthCalledWith(1, config, events); }); - it('should should send events immediately asynchronously if batchSize is 1', async () => { + it('should send events immediately asynchronously if batchSize is 1', async () => { const apiManager = getMockApiManager(); apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); diff --git a/lib/odp/odp_manager_factory.browser.spec.ts b/lib/odp/odp_manager_factory.browser.spec.ts index 4268a860a..333856743 100644 --- a/lib/odp/odp_manager_factory.browser.spec.ts +++ b/lib/odp/odp_manager_factory.browser.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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. + */ + vi.mock('../utils/http_request_handler/browser_request_handler', () => { return { BrowserRequestHandler: vi.fn() }; }); diff --git a/lib/odp/odp_manager_factory.browser.ts b/lib/odp/odp_manager_factory.browser.ts index 97c320aee..481252278 100644 --- a/lib/odp/odp_manager_factory.browser.ts +++ b/lib/odp/odp_manager_factory.browser.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; import { pixelApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { OdpManager } from './odp_manager'; diff --git a/lib/odp/odp_manager_factory.node.spec.ts b/lib/odp/odp_manager_factory.node.spec.ts index 4a3f2b0b0..b63850180 100644 --- a/lib/odp/odp_manager_factory.node.spec.ts +++ b/lib/odp/odp_manager_factory.node.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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. + */ + vi.mock('../utils/http_request_handler/node_request_handler', () => { return { NodeRequestHandler: vi.fn() }; }); diff --git a/lib/odp/odp_manager_factory.node.ts b/lib/odp/odp_manager_factory.node.ts index e0fbd7734..3d449fd3b 100644 --- a/lib/odp/odp_manager_factory.node.ts +++ b/lib/odp/odp_manager_factory.node.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { NodeRequestHandler } from '../utils/http_request_handler/node_request_handler'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { OdpManager } from './odp_manager'; diff --git a/lib/odp/odp_manager_factory.react_native.spec.ts b/lib/odp/odp_manager_factory.react_native.spec.ts index e979d3e88..604a71bc7 100644 --- a/lib/odp/odp_manager_factory.react_native.spec.ts +++ b/lib/odp/odp_manager_factory.react_native.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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. + */ + vi.mock('../utils/http_request_handler/browser_request_handler', () => { return { BrowserRequestHandler: vi.fn() }; }); diff --git a/lib/odp/odp_manager_factory.react_native.ts b/lib/odp/odp_manager_factory.react_native.ts index 1126647e8..c63982430 100644 --- a/lib/odp/odp_manager_factory.react_native.ts +++ b/lib/odp/odp_manager_factory.react_native.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; import { eventApiRequestGenerator } from './event_manager/odp_event_api_manager'; import { OdpManager } from './odp_manager'; diff --git a/lib/odp/odp_manager_factory.spec.ts b/lib/odp/odp_manager_factory.spec.ts index 860e26ec7..94aa565e5 100644 --- a/lib/odp/odp_manager_factory.spec.ts +++ b/lib/odp/odp_manager_factory.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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. + */ + vi.mock('./odp_manager', () => { return { DefaultOdpManager: vi.fn(), diff --git a/lib/odp/odp_manager_factory.ts b/lib/odp/odp_manager_factory.ts index a1378e92d..31d908df1 100644 --- a/lib/odp/odp_manager_factory.ts +++ b/lib/odp/odp_manager_factory.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { RequestHandler } from "../shared_types"; import { Cache } from "../utils/cache/cache"; import { InMemoryLruCache } from "../utils/cache/in_memory_lru_cache"; diff --git a/lib/odp/odp_types.ts b/lib/odp/odp_types.ts index bd3e8217e..abe47b245 100644 --- a/lib/odp/odp_types.ts +++ b/lib/odp/odp_types.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, 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/lib/odp/segment_manager/odp_response_schema.ts b/lib/odp/segment_manager/odp_response_schema.ts index 9aad4ac35..4221178af 100644 --- a/lib/odp/segment_manager/odp_response_schema.ts +++ b/lib/odp/segment_manager/odp_response_schema.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022, 2024, 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/lib/odp/segment_manager/odp_segment_api_manager.ts b/lib/odp/segment_manager/odp_segment_api_manager.ts index af316ea75..6b609a8a3 100644 --- a/lib/odp/segment_manager/odp_segment_api_manager.ts +++ b/lib/odp/segment_manager/odp_segment_api_manager.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, 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/lib/odp/segment_manager/odp_segment_manager.spec.ts b/lib/odp/segment_manager/odp_segment_manager.spec.ts index 5d6fea58a..31598dd71 100644 --- a/lib/odp/segment_manager/odp_segment_manager.spec.ts +++ b/lib/odp/segment_manager/odp_segment_manager.spec.ts @@ -71,7 +71,7 @@ describe('DefaultOdpSegmentManager', () => { expect(cache.get(manager.makeCacheKey(userKey, userValue))).toEqual(['k', 'l']); }); - it('should return sement from cache and not call apiManager on cache hit.', async () => { + it('should return segment from cache and not call apiManager on cache hit.', async () => { const cache = getMockSyncCache(); const apiManager = getMockApiManager(); diff --git a/lib/odp/segment_manager/optimizely_segment_option.ts b/lib/odp/segment_manager/optimizely_segment_option.ts index 112cd39cc..cf7c801ef 100644 --- a/lib/odp/segment_manager/optimizely_segment_option.ts +++ b/lib/odp/segment_manager/optimizely_segment_option.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022, Optimizely + * Copyright 2022, 2024, 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/lib/optimizely/index.ts b/lib/optimizely/index.ts index 91ec1503e..4c4898c91 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -91,12 +91,12 @@ export default class Optimizely implements Client { private clientEngine: string; private clientVersion: string; private errorHandler: ErrorHandler; - protected logger: LoggerFacade; + private logger: LoggerFacade; private projectConfigManager: ProjectConfigManager; private decisionService: DecisionService; private eventProcessor?: EventProcessor; private defaultDecideOptions: { [key: string]: boolean }; - protected odpManager?: OdpManager; + private odpManager?: OdpManager; public notificationCenter: DefaultNotificationCenter; private vuidManager?: VuidManager; diff --git a/lib/tests/testUtils.ts b/lib/tests/testUtils.ts index cee93ca2c..8bcd093f8 100644 --- a/lib/tests/testUtils.ts +++ b/lib/tests/testUtils.ts @@ -1,7 +1,23 @@ +/** + * Copyright 2024, 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. + */ + export const exhaustMicrotasks = async (loop = 100): Promise => { for(let i = 0; i < loop; i++) { await Promise.resolve(); } }; -export const wait = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); \ No newline at end of file +export const wait = (ms: number): Promise => new Promise(resolve => setTimeout(resolve, ms)); diff --git a/lib/utils/cache/in_memory_lru_cache.spec.ts b/lib/utils/cache/in_memory_lru_cache.spec.ts index 2820dc438..c6ab08780 100644 --- a/lib/utils/cache/in_memory_lru_cache.spec.ts +++ b/lib/utils/cache/in_memory_lru_cache.spec.ts @@ -112,13 +112,6 @@ describe('InMemoryLruCache', () => { expect(cache.getBatched(['a', 'b', 'c'])).toEqual([1, 2, undefined]); }); - it('should return correct values when getBatched is called', () => { - const cache = new InMemoryLruCache(2); - cache.set('a', 1); - cache.set('b', 2); - expect(cache.getBatched(['a', 'b', 'c'])).toEqual([1, 2, undefined]); - }); - it('should not return expired values when getBatched is called', async () => { const cache = new InMemoryLruCache(2, 100); cache.set('a', 1); diff --git a/lib/utils/cache/in_memory_lru_cache.ts b/lib/utils/cache/in_memory_lru_cache.ts index 82cefbc78..1b4d3a7bd 100644 --- a/lib/utils/cache/in_memory_lru_cache.ts +++ b/lib/utils/cache/in_memory_lru_cache.ts @@ -1,5 +1,5 @@ /** - * Copyright 2022-2023, Optimizely + * Copyright 2022-2024, 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/lib/vuid/vuid.spec.ts b/lib/vuid/vuid.spec.ts index 6427796cb..0a0790b59 100644 --- a/lib/vuid/vuid.spec.ts +++ b/lib/vuid/vuid.spec.ts @@ -1,6 +1,22 @@ -import { vi, describe, expect, it } from 'vitest'; +/** + * Copyright 2024, 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 { VUID_PREFIX, VUID_MAX_LENGTH, isVuid, makeVuid } from './vuid'; +import { describe, expect, it } from 'vitest'; + +import { isVuid, makeVuid, VUID_MAX_LENGTH } from './vuid'; describe('isVuid', () => { it('should return true if and only if the value strats with the VUID_PREFIX and is longer than vuid_prefix', () => { @@ -13,3 +29,11 @@ describe('isVuid', () => { expect(isVuid('123')).toBe(false); }) }); + +describe('makeVuid', () => { + it('should return a string that is a valid vuid and whose length is within VUID_MAX_LENGTH', () => { + const vuid = makeVuid(); + expect(isVuid(vuid)).toBe(true); + expect(vuid.length).toBeLessThanOrEqual(VUID_MAX_LENGTH); + }); +}); diff --git a/lib/vuid/vuid.ts b/lib/vuid/vuid.ts index 53cbaf805..d335c329d 100644 --- a/lib/vuid/vuid.ts +++ b/lib/vuid/vuid.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { v4 as uuidV4 } from 'uuid'; export const VUID_PREFIX = `vuid_`; diff --git a/lib/vuid/vuid_manager.spec.ts b/lib/vuid/vuid_manager.spec.ts index 6e3709c98..5a4713d68 100644 --- a/lib/vuid/vuid_manager.spec.ts +++ b/lib/vuid/vuid_manager.spec.ts @@ -71,7 +71,7 @@ describe('VuidCacheManager', () => { it('should use the vuid in cache if available', async () => { const cache = getMockAsyncCache(); - cache.set(vuidCacheKey, 'vuid_valid'); + await cache.set(vuidCacheKey, 'vuid_valid'); const manager = new VuidCacheManager(cache); const vuid1 = await manager.load(); @@ -120,10 +120,10 @@ describe('VuidCacheManager', () => { const manager = new VuidCacheManager(cache); - // this should try to remove from cached, which should stay pending + // this should try to remove from cache, which should stay pending const call1 = manager.remove(); - // this should try to get the vuid from store + // this should try to get the vuid from cache const call2 = manager.load(); // this should again try to remove vuid @@ -131,19 +131,19 @@ describe('VuidCacheManager', () => { await exhaustMicrotasks(); - expect(removeSpy).toHaveBeenCalledTimes(1); // from the first configure call + expect(removeSpy).toHaveBeenCalledTimes(1); // from the first manager.remove call expect(getSpy).not.toHaveBeenCalled(); - // this will resolve the first configure call + // this will resolve the first manager.remove call removePromise.resolve(true); await exhaustMicrotasks(); await expect(call1).resolves.not.toThrow(); - // this get call is from the second configure call + // this get call is from the load call expect(getSpy).toHaveBeenCalledTimes(1); await exhaustMicrotasks(); - // as the get call is pending, remove call from the third configure call should not yet happen + // as the get call is pending, remove call from the second manager.remove call should not yet happen expect(removeSpy).toHaveBeenCalledTimes(1); // this should fail the load call, allowing the second remnove call to proceed diff --git a/lib/vuid/vuid_manager_factory.browser.spec.ts b/lib/vuid/vuid_manager_factory.browser.spec.ts index c6ca2d39d..d4a7c2c72 100644 --- a/lib/vuid/vuid_manager_factory.browser.spec.ts +++ b/lib/vuid/vuid_manager_factory.browser.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { vi, describe, expect, it, beforeEach } from 'vitest'; vi.mock('../utils/cache/local_storage_cache.browser', () => { diff --git a/lib/vuid/vuid_manager_factory.node.spec.ts b/lib/vuid/vuid_manager_factory.node.spec.ts index 6108da404..2a81f9a8a 100644 --- a/lib/vuid/vuid_manager_factory.node.spec.ts +++ b/lib/vuid/vuid_manager_factory.node.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { vi, describe, expect, it } from 'vitest'; import { createVuidManager } from './vuid_manager_factory.node'; diff --git a/lib/vuid/vuid_manager_factory.react_native.spec.ts b/lib/vuid/vuid_manager_factory.react_native.spec.ts index 67dc86a92..22920c099 100644 --- a/lib/vuid/vuid_manager_factory.react_native.spec.ts +++ b/lib/vuid/vuid_manager_factory.react_native.spec.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { vi, describe, expect, it, beforeEach } from 'vitest'; vi.mock('../utils/cache/async_storage_cache.react_native', () => { diff --git a/lib/vuid/vuid_manager_factory.ts b/lib/vuid/vuid_manager_factory.ts index f4234b06e..ab2264242 100644 --- a/lib/vuid/vuid_manager_factory.ts +++ b/lib/vuid/vuid_manager_factory.ts @@ -1,3 +1,19 @@ +/** + * Copyright 2024, 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 { Cache } from '../utils/cache/cache'; export type VuidManagerOptions = { From 83e1b19f23fa966f7bb97f19726094ff6e343571 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 13 Dec 2024 01:31:06 +0600 Subject: [PATCH 27/28] node --- lib/index.node.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/index.node.ts b/lib/index.node.ts index 3f28538d9..63f7e16e5 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -26,8 +26,8 @@ import { OptimizelyDecideOption, Client, Config } from './shared_types'; import * as commonExports from './common_exports'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.node'; import { createForwardingEventProcessor, createBatchEventProcessor } from './event_processor/event_processor_factory.node'; -import { createVuidManager } from './vuid/vuid_manager_factory.browser'; -import { createOdpManager } from './odp/odp_manager_factory.browser'; +import { createVuidManager } from './vuid/vuid_manager_factory.node'; +import { createOdpManager } from './odp/odp_manager_factory.node'; const logger = getLogger(); setLogLevel(LogLevel.ERROR); From 57a74f0af6a4e68f3b62ae9de4ce6508031ada13 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Fri, 13 Dec 2024 01:44:50 +0600 Subject: [PATCH 28/28] rem --- lib/odp/event_manager/odp_event_api_manager.spec.ts | 2 +- lib/shared_types.ts | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/lib/odp/event_manager/odp_event_api_manager.spec.ts b/lib/odp/event_manager/odp_event_api_manager.spec.ts index ca9bd81b1..8f6a07fd2 100644 --- a/lib/odp/event_manager/odp_event_api_manager.spec.ts +++ b/lib/odp/event_manager/odp_event_api_manager.spec.ts @@ -17,7 +17,7 @@ import { describe, it, expect, vi } from 'vitest'; import { DefaultOdpEventApiManager, eventApiRequestGenerator, pixelApiRequestGenerator } from './odp_event_api_manager'; -import { OdpEvent } from './odp_event';; +import { OdpEvent } from './odp_event'; import { OdpConfig } from '../odp_config'; const data1 = new Map(); diff --git a/lib/shared_types.ts b/lib/shared_types.ts index e5138b191..fa3579e69 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -42,9 +42,8 @@ import { VuidManager } from './vuid/vuid_manager'; export { EventDispatcher } from './event_processor/event_dispatcher/event_dispatcher'; export { EventProcessor } from './event_processor/event_processor'; export { NotificationCenter } from './notification_center'; - -export { OdpManager } from './odp/odp_manager'; export { VuidManager } from './vuid/vuid_manager'; + export interface BucketerParams { experimentId: string; experimentKey: string; @@ -527,9 +526,9 @@ export interface OptimizelyForcedDecision { export { RequestHandler, OptimizelySegmentOption, - OdpSegmentApiManager as IOdpSegmentApiManager, - OdpSegmentManager as IOdpSegmentManager, - DefaultOdpEventApiManager as IOdpEventApiManager, - OdpEventManager as IOdpEventManager, - OdpManager as IOdpManager, + OdpSegmentApiManager, + OdpSegmentManager, + DefaultOdpEventApiManager, + OdpEventManager, + OdpManager, };