diff --git a/lib/core/decision_service/index.tests.js b/lib/core/decision_service/index.tests.js index b9197ebab..025a11d69 100644 --- a/lib/core/decision_service/index.tests.js +++ b/lib/core/decision_service/index.tests.js @@ -25,14 +25,14 @@ import { DECISION_SOURCES, } from '../../utils/enums'; import { createLogger } from '../../plugins/logger'; -import { createForwardingEventProcessor } from '../../plugins/event_processor/forwarding_event_processor'; +import { getForwardingEventProcessor } from '../../event_processor/forwarding_event_processor'; import { createNotificationCenter } from '../notification_center'; import Optimizely from '../../optimizely'; import OptimizelyUserContext from '../../optimizely_user_context'; import projectConfig, { createProjectConfig } from '../../project_config/project_config'; import AudienceEvaluator from '../audience_evaluator'; import errorHandler from '../../plugins/error_handler'; -import eventDispatcher from '../../plugins/event_dispatcher/index.node'; +import eventDispatcher from '../../event_processor/default_dispatcher.browser'; import * as jsonSchemaValidator from '../../utils/json_schema_validator'; import { getMockProjectConfigManager } from '../../tests/mock/mock_project_config_manager'; @@ -1075,7 +1075,7 @@ describe('lib/core/decision_service', function() { jsonSchemaValidator: jsonSchemaValidator, isValidInstance: true, logger: createdLogger, - eventProcessor: createForwardingEventProcessor(eventDispatcher), + eventProcessor: getForwardingEventProcessor(eventDispatcher), notificationCenter: createNotificationCenter(createdLogger, errorHandler), errorHandler: errorHandler, }); diff --git a/lib/core/event_builder/build_event_v1.ts b/lib/core/event_builder/build_event_v1.ts index b1f5b271d..1ca9c63ea 100644 --- a/lib/core/event_builder/build_event_v1.ts +++ b/lib/core/event_builder/build_event_v1.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021-2022, Optimizely + * Copyright 2021-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. @@ -17,7 +17,7 @@ import { EventTags, ConversionEvent, ImpressionEvent, -} from '../../modules/event_processor'; +} from '../../event_processor'; import { Event } from '../../shared_types'; diff --git a/lib/core/event_builder/index.ts b/lib/core/event_builder/index.ts index f896adbea..707cb178c 100644 --- a/lib/core/event_builder/index.ts +++ b/lib/core/event_builder/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { LoggerFacade } from '../../modules/logging'; -import { EventV1 as CommonEventParams } from '../../modules/event_processor'; +import { EventV1 as CommonEventParams } from '../../event_processor'; import fns from '../../utils/fns'; import { CONTROL_ATTRIBUTES, RESERVED_EVENT_KEYWORDS } from '../../utils/enums'; diff --git a/lib/event_processor/default_dispatcher.browser.spec.ts b/lib/event_processor/default_dispatcher.browser.spec.ts new file mode 100644 index 000000000..4c35e39a7 --- /dev/null +++ b/lib/event_processor/default_dispatcher.browser.spec.ts @@ -0,0 +1,49 @@ +/** + * 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 { vi, expect, it, describe, afterAll } from 'vitest'; + +vi.mock('./default_dispatcher', () => { + const DefaultEventDispatcher = vi.fn(); + return { DefaultEventDispatcher }; +}); + +vi.mock('../utils/http_request_handler/browser_request_handler', () => { + const BrowserRequestHandler = vi.fn(); + return { BrowserRequestHandler }; +}); + +import { DefaultEventDispatcher } from './default_dispatcher'; +import { BrowserRequestHandler } from '../utils/http_request_handler/browser_request_handler'; +import eventDispatcher from './default_dispatcher.browser'; + +describe('eventDispatcher', () => { + afterAll(() => { + MockDefaultEventDispatcher.mockReset(); + MockBrowserRequestHandler.mockReset(); + }); + const MockBrowserRequestHandler = vi.mocked(BrowserRequestHandler); + const MockDefaultEventDispatcher = vi.mocked(DefaultEventDispatcher); + + it('creates and returns the instance by calling DefaultEventDispatcher', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + }); + + it('uses a BrowserRequestHandler', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + expect(Object.is(MockDefaultEventDispatcher.mock.calls[0][0], MockBrowserRequestHandler.mock.instances[0])).toBe(true); + }); +}); diff --git a/lib/event_processor/default_dispatcher.browser.ts b/lib/event_processor/default_dispatcher.browser.ts new file mode 100644 index 000000000..12cdf5a3e --- /dev/null +++ b/lib/event_processor/default_dispatcher.browser.ts @@ -0,0 +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 + * + * 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 { BrowserRequestHandler } from "../utils/http_request_handler/browser_request_handler"; +import { EventDispatcher } from '../event_processor'; +import { DefaultEventDispatcher } from './default_dispatcher'; + +const eventDispatcher: EventDispatcher = new DefaultEventDispatcher(new BrowserRequestHandler()); + +export default eventDispatcher; diff --git a/lib/event_processor/default_dispatcher.node.spec.ts b/lib/event_processor/default_dispatcher.node.spec.ts new file mode 100644 index 000000000..ddfc0c763 --- /dev/null +++ b/lib/event_processor/default_dispatcher.node.spec.ts @@ -0,0 +1,49 @@ +/** + * 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 { vi, expect, it, describe, afterAll } from 'vitest'; + +vi.mock('./default_dispatcher', () => { + const DefaultEventDispatcher = vi.fn(); + return { DefaultEventDispatcher }; +}); + +vi.mock('../utils/http_request_handler/node_request_handler', () => { + const NodeRequestHandler = vi.fn(); + return { NodeRequestHandler }; +}); + +import { DefaultEventDispatcher } from './default_dispatcher'; +import { NodeRequestHandler } from '../utils/http_request_handler/node_request_handler'; +import eventDispatcher from './default_dispatcher.node'; + +describe('eventDispatcher', () => { + const MockNodeRequestHandler = vi.mocked(NodeRequestHandler); + const MockDefaultEventDispatcher = vi.mocked(DefaultEventDispatcher); + + afterAll(() => { + MockDefaultEventDispatcher.mockReset(); + MockNodeRequestHandler.mockReset(); + }) + + it('creates and returns the instance by calling DefaultEventDispatcher', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + }); + + it('uses a NodeRequestHandler', () => { + expect(Object.is(eventDispatcher, MockDefaultEventDispatcher.mock.instances[0])).toBe(true); + expect(Object.is(MockDefaultEventDispatcher.mock.calls[0][0], MockNodeRequestHandler.mock.instances[0])).toBe(true); + }); +}); diff --git a/lib/event_processor/default_dispatcher.node.ts b/lib/event_processor/default_dispatcher.node.ts new file mode 100644 index 000000000..8d2cd852c --- /dev/null +++ b/lib/event_processor/default_dispatcher.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 + * + * 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 { EventDispatcher } from '../event_processor'; +import { NodeRequestHandler } from '../utils/http_request_handler/node_request_handler'; +import { DefaultEventDispatcher } from './default_dispatcher'; + +const eventDispatcher: EventDispatcher = new DefaultEventDispatcher(new NodeRequestHandler()); + +export default eventDispatcher; diff --git a/lib/event_processor/default_dispatcher.spec.ts b/lib/event_processor/default_dispatcher.spec.ts new file mode 100644 index 000000000..0616ba3bf --- /dev/null +++ b/lib/event_processor/default_dispatcher.spec.ts @@ -0,0 +1,117 @@ +/** + * 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, vi, describe, it } from 'vitest'; +import { DefaultEventDispatcher } from './default_dispatcher'; +import { EventV1 } from '../event_processor'; + +const getEvent = (): EventV1 => { + return { + account_id: 'string', + project_id: 'string', + revision: 'string', + client_name: 'string', + client_version: 'string', + anonymize_ip: true, + enrich_decisions: false, + visitors: [], + }; +}; + +describe('DefaultEventDispatcher', () => { + it('reject the response promise if the eventObj.httpVerb is not POST', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'GET' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.resolve({ statusCode: 203 }), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + await expect(dispatcher.dispatchEvent(eventObj)).rejects.toThrow(); + }); + + it('sends correct headers and data to the requestHandler', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'POST' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.resolve({ statusCode: 203 }), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + await dispatcher.dispatchEvent(eventObj); + + expect(requestHnadler.makeRequest).toHaveBeenCalledWith( + eventObj.url, + { + 'content-type': 'application/json', + 'content-length': JSON.stringify(eventObj.params).length.toString(), + }, + 'POST', + JSON.stringify(eventObj.params) + ); + }); + + it('returns a promise that resolves with correct value if the response of the requestHandler resolves', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'POST' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.resolve({ statusCode: 203 }), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + const response = await dispatcher.dispatchEvent(eventObj); + + expect(response.statusCode).toEqual(203); + }); + + it('returns a promise that rejects if the response of the requestHandler rejects', async () => { + const eventObj = { + url: 'https://cdn.com/event', + params: getEvent(), + httpVerb: 'POST' as const, + }; + + const requestHnadler = { + makeRequest: vi.fn().mockReturnValue({ + abort: vi.fn(), + responsePromise: Promise.reject(new Error('error')), + }), + }; + + const dispatcher = new DefaultEventDispatcher(requestHnadler); + await expect(dispatcher.dispatchEvent(eventObj)).rejects.toThrow(); + }); +}); diff --git a/lib/event_processor/default_dispatcher.ts b/lib/event_processor/default_dispatcher.ts new file mode 100644 index 000000000..2097cb82c --- /dev/null +++ b/lib/event_processor/default_dispatcher.ts @@ -0,0 +1,44 @@ +/** + * 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 { RequestHandler } from '../utils/http_request_handler/http'; +import { EventDispatcher, EventDispatcherResponse, EventV1Request } from '../event_processor'; + +export class DefaultEventDispatcher implements EventDispatcher { + private requestHandler: RequestHandler; + + constructor(requestHandler: RequestHandler) { + this.requestHandler = requestHandler; + } + + async dispatchEvent( + eventObj: EventV1Request + ): Promise { + // Non-POST requests not supported + if (eventObj.httpVerb !== 'POST') { + return Promise.reject(new Error('Only POST requests are supported')); + } + + const dataString = JSON.stringify(eventObj.params); + + const headers = { + 'content-type': 'application/json', + 'content-length': dataString.length.toString(), + }; + + const abortableRequest = this.requestHandler.makeRequest(eventObj.url, headers, 'POST', dataString); + return abortableRequest.responsePromise; + } +} diff --git a/lib/modules/event_processor/eventDispatcher.ts b/lib/event_processor/eventDispatcher.ts similarity index 78% rename from lib/modules/event_processor/eventDispatcher.ts rename to lib/event_processor/eventDispatcher.ts index 15d261cf2..90b036862 100644 --- a/lib/modules/event_processor/eventDispatcher.ts +++ b/lib/event_processor/eventDispatcher.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. @@ -16,13 +16,11 @@ import { EventV1 } from "./v1/buildEventV1"; export type EventDispatcherResponse = { - statusCode: number + statusCode?: number } -export type EventDispatcherCallback = (response: EventDispatcherResponse) => void - export interface EventDispatcher { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void + dispatchEvent(event: EventV1Request): Promise } export interface EventV1Request { diff --git a/lib/modules/event_processor/eventProcessor.ts b/lib/event_processor/eventProcessor.ts similarity index 93% rename from lib/modules/event_processor/eventProcessor.ts rename to lib/event_processor/eventProcessor.ts index e0b31cc3a..fa2cab200 100644 --- a/lib/modules/event_processor/eventProcessor.ts +++ b/lib/event_processor/eventProcessor.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. @@ -18,9 +18,9 @@ import { Managed } from './managed' import { ConversionEvent, ImpressionEvent } from './events' import { EventV1Request } from './eventDispatcher' import { EventQueue, DefaultEventQueue, SingleEventQueue, EventQueueSink } from './eventQueue' -import { getLogger } from '../logging' -import { NOTIFICATION_TYPES } from '../../utils/enums' -import { NotificationSender } from '../../core/notification_center' +import { getLogger } from '../modules/logging' +import { NOTIFICATION_TYPES } from '../utils/enums' +import { NotificationSender } from '../core/notification_center' export const DEFAULT_FLUSH_INTERVAL = 30000 // Unit is ms - default flush interval is 30s export const DEFAULT_BATCH_SIZE = 10 diff --git a/lib/modules/event_processor/eventQueue.ts b/lib/event_processor/eventQueue.ts similarity index 97% rename from lib/modules/event_processor/eventQueue.ts rename to lib/event_processor/eventQueue.ts index ac9d2ac66..3b8a71966 100644 --- a/lib/modules/event_processor/eventQueue.ts +++ b/lib/event_processor/eventQueue.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. @@ -14,7 +14,7 @@ * limitations under the License. */ -import { getLogger } from '../logging'; +import { getLogger } from '../modules/logging'; // TODO change this to use Managed from js-sdk-models when available import { Managed } from './managed'; diff --git a/lib/event_processor/event_processor_factory.browser.spec.ts b/lib/event_processor/event_processor_factory.browser.spec.ts new file mode 100644 index 000000000..b63471a29 --- /dev/null +++ b/lib/event_processor/event_processor_factory.browser.spec.ts @@ -0,0 +1,55 @@ +/** + * 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 { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('./default_dispatcher.browser', () => { + return { default: {} }; +}); + +vi.mock('./forwarding_event_processor', () => { + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); + return { getForwardingEventProcessor }; +}); + +import { createForwardingEventProcessor } from './event_processor_factory.browser'; +import { getForwardingEventProcessor } from './forwarding_event_processor'; +import browserDefaultEventDispatcher from './default_dispatcher.browser'; + +describe('createForwardingEventProcessor', () => { + const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); + + beforeEach(() => { + mockGetForwardingEventProcessor.mockClear(); + }); + + it('returns forwarding event processor by calling getForwardingEventProcessor with the provided dispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createForwardingEventProcessor(eventDispatcher); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher); + }); + + it('uses the browser default event dispatcher if none is provided', () => { + const processor = createForwardingEventProcessor(); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, browserDefaultEventDispatcher); + }); +}); diff --git a/lib/event_processor/event_processor_factory.browser.ts b/lib/event_processor/event_processor_factory.browser.ts new file mode 100644 index 000000000..ea4d2d2b1 --- /dev/null +++ b/lib/event_processor/event_processor_factory.browser.ts @@ -0,0 +1,26 @@ +/** + * 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 { getForwardingEventProcessor } from './forwarding_event_processor'; +import { EventDispatcher } from './eventDispatcher'; +import { EventProcessor } from './eventProcessor'; +import defaultEventDispatcher from './default_dispatcher.browser'; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher = defaultEventDispatcher, +): EventProcessor => { + return getForwardingEventProcessor(eventDispatcher); +}; diff --git a/lib/event_processor/event_processor_factory.node.spec.ts b/lib/event_processor/event_processor_factory.node.spec.ts new file mode 100644 index 000000000..36d4ea1fa --- /dev/null +++ b/lib/event_processor/event_processor_factory.node.spec.ts @@ -0,0 +1,55 @@ +/** + * 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 { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('./default_dispatcher.node', () => { + return { default: {} }; +}); + +vi.mock('./forwarding_event_processor', () => { + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); + return { getForwardingEventProcessor }; +}); + +import { createForwardingEventProcessor } from './event_processor_factory.node'; +import { getForwardingEventProcessor } from './forwarding_event_processor'; +import nodeDefaultEventDispatcher from './default_dispatcher.node'; + +describe('createForwardingEventProcessor', () => { + const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); + + beforeEach(() => { + mockGetForwardingEventProcessor.mockClear(); + }); + + it('returns forwarding event processor by calling getForwardingEventProcessor with the provided dispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createForwardingEventProcessor(eventDispatcher); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher); + }); + + it('uses the node default event dispatcher if none is provided', () => { + const processor = createForwardingEventProcessor(); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, nodeDefaultEventDispatcher); + }); +}); diff --git a/lib/event_processor/event_processor_factory.node.ts b/lib/event_processor/event_processor_factory.node.ts new file mode 100644 index 000000000..ae793ce4f --- /dev/null +++ b/lib/event_processor/event_processor_factory.node.ts @@ -0,0 +1,25 @@ +/** + * 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 { getForwardingEventProcessor } from './forwarding_event_processor'; +import { EventDispatcher } from './eventDispatcher'; +import { EventProcessor } from './eventProcessor'; +import defaultEventDispatcher from './default_dispatcher.node'; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher = defaultEventDispatcher, +): EventProcessor => { + return getForwardingEventProcessor(eventDispatcher); +}; diff --git a/lib/event_processor/event_processor_factory.react_native.spec.ts b/lib/event_processor/event_processor_factory.react_native.spec.ts new file mode 100644 index 000000000..6de989534 --- /dev/null +++ b/lib/event_processor/event_processor_factory.react_native.spec.ts @@ -0,0 +1,56 @@ +/** + * 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 { vi, describe, it, expect, beforeEach } from 'vitest'; + +vi.mock('./default_dispatcher.browser', () => { + return { default: {} }; +}); + +vi.mock('./forwarding_event_processor', () => { + const getForwardingEventProcessor = vi.fn().mockReturnValue({}); + return { getForwardingEventProcessor }; +}); + +import { createForwardingEventProcessor } from './event_processor_factory.react_native'; +import { getForwardingEventProcessor } from './forwarding_event_processor'; +import browserDefaultEventDispatcher from './default_dispatcher.browser'; + +describe('createForwardingEventProcessor', () => { + const mockGetForwardingEventProcessor = vi.mocked(getForwardingEventProcessor); + + beforeEach(() => { + mockGetForwardingEventProcessor.mockClear(); + }); + + it('returns forwarding event processor by calling getForwardingEventProcessor with the provided dispatcher', () => { + const eventDispatcher = { + dispatchEvent: vi.fn(), + }; + + const processor = createForwardingEventProcessor(eventDispatcher); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, eventDispatcher); + }); + + it('uses the browser default event dispatcher if none is provided', () => { + const processor = createForwardingEventProcessor(); + + expect(Object.is(processor, mockGetForwardingEventProcessor.mock.results[0].value)).toBe(true); + expect(mockGetForwardingEventProcessor).toHaveBeenNthCalledWith(1, browserDefaultEventDispatcher); + }); +}); diff --git a/lib/event_processor/event_processor_factory.react_native.ts b/lib/event_processor/event_processor_factory.react_native.ts new file mode 100644 index 000000000..3763a15c1 --- /dev/null +++ b/lib/event_processor/event_processor_factory.react_native.ts @@ -0,0 +1,25 @@ +/** + * 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 { getForwardingEventProcessor } from './forwarding_event_processor'; +import { EventDispatcher } from './eventDispatcher'; +import { EventProcessor } from './eventProcessor'; +import defaultEventDispatcher from './default_dispatcher.browser'; + +export const createForwardingEventProcessor = ( + eventDispatcher: EventDispatcher = defaultEventDispatcher, +): EventProcessor => { + return getForwardingEventProcessor(eventDispatcher); +}; diff --git a/lib/modules/event_processor/events.ts b/lib/event_processor/events.ts similarity index 98% rename from lib/modules/event_processor/events.ts rename to lib/event_processor/events.ts index 65cce503b..4254a274f 100644 --- a/lib/modules/event_processor/events.ts +++ b/lib/event_processor/events.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/event_processor/forwarding_event_processor.spec.ts b/lib/event_processor/forwarding_event_processor.spec.ts new file mode 100644 index 000000000..72da66633 --- /dev/null +++ b/lib/event_processor/forwarding_event_processor.spec.ts @@ -0,0 +1,98 @@ +/** + * Copyright 2021, 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, vi } from 'vitest'; + +import { getForwardingEventProcessor } from './forwarding_event_processor'; +import { EventDispatcher, makeBatchedEventV1 } from '.'; + +function createImpressionEvent() { + return { + type: 'impression' as const, + timestamp: 69, + uuid: 'uuid', + + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: '1', + botFiltering: true, + anonymizeIP: true, + }, + + user: { + id: 'userId', + attributes: [{ entityId: 'attr1-id', key: 'attr1-key', value: 'attr1-value' }], + }, + + layer: { + id: 'layerId', + }, + + experiment: { + id: 'expId', + key: 'expKey', + }, + + variation: { + id: 'varId', + key: 'varKey', + }, + + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true, + } +} + +const getMockEventDispatcher = (): EventDispatcher => { + return { + dispatchEvent: vi.fn().mockResolvedValue({ statusCode: 200 }), + }; +}; + +const getMockNotificationCenter = () => { + return { + sendNotifications: vi.fn(), + }; +} + +describe('ForwardingEventProcessor', function() { + it('should dispatch event immediately when process is called', () => { + const dispatcher = getMockEventDispatcher(); + const mockDispatch = vi.mocked(dispatcher.dispatchEvent); + const notificationCenter = getMockNotificationCenter(); + const processor = getForwardingEventProcessor(dispatcher, notificationCenter); + processor.start(); + const event = createImpressionEvent(); + processor.process(event); + expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); + const data = mockDispatch.mock.calls[0][0].params; + expect(data).toEqual(makeBatchedEventV1([event])); + expect(notificationCenter.sendNotifications).toHaveBeenCalledOnce(); + }); + + it('should return a resolved promise when stop is called', async () => { + const dispatcher = getMockEventDispatcher(); + const notificationCenter = getMockNotificationCenter(); + const processor = getForwardingEventProcessor(dispatcher, notificationCenter); + processor.start(); + const stopPromise = processor.stop(); + expect(stopPromise).resolves.not.toThrow(); + }); + }); diff --git a/lib/plugins/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts similarity index 72% rename from lib/plugins/event_processor/forwarding_event_processor.ts rename to lib/event_processor/forwarding_event_processor.ts index e528e0202..919710c53 100644 --- a/lib/plugins/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021-2023, Optimizely + * Copyright 2021-2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,12 +17,12 @@ import { EventProcessor, ProcessableEvent, -} from '../../modules/event_processor'; -import { NotificationSender } from '../../core/notification_center'; +} from '.'; +import { NotificationSender } from '../core/notification_center'; -import { EventDispatcher } from '../../shared_types'; -import { NOTIFICATION_TYPES } from '../../utils/enums'; -import { formatEvents } from '../../core/event_builder/build_event_v1'; +import { EventDispatcher } from '../shared_types'; +import { NOTIFICATION_TYPES } from '../utils/enums'; +import { formatEvents } from '../core/event_builder/build_event_v1'; class ForwardingEventProcessor implements EventProcessor { private dispatcher: EventDispatcher; @@ -35,7 +35,7 @@ class ForwardingEventProcessor implements EventProcessor { process(event: ProcessableEvent): void { const formattedEvent = formatEvents([event]); - this.dispatcher.dispatchEvent(formattedEvent, () => {}); + this.dispatcher.dispatchEvent(formattedEvent).catch(() => {}); if (this.NotificationSender) { this.NotificationSender.sendNotifications( NOTIFICATION_TYPES.LOG_EVENT, @@ -53,6 +53,6 @@ class ForwardingEventProcessor implements EventProcessor { } } -export function createForwardingEventProcessor(dispatcher: EventDispatcher, notificationSender?: NotificationSender): EventProcessor { +export function getForwardingEventProcessor(dispatcher: EventDispatcher, notificationSender?: NotificationSender): EventProcessor { return new ForwardingEventProcessor(dispatcher, notificationSender); } diff --git a/lib/modules/event_processor/index.react_native.ts b/lib/event_processor/index.react_native.ts similarity index 95% rename from lib/modules/event_processor/index.react_native.ts rename to lib/event_processor/index.react_native.ts index 91bb29a58..27a6f3a3a 100644 --- a/lib/modules/event_processor/index.react_native.ts +++ b/lib/event_processor/index.react_native.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/modules/event_processor/index.ts b/lib/event_processor/index.ts similarity index 95% rename from lib/modules/event_processor/index.ts rename to lib/event_processor/index.ts index c4eaef01d..c91ca2d21 100644 --- a/lib/modules/event_processor/index.ts +++ b/lib/event_processor/index.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/modules/event_processor/managed.ts b/lib/event_processor/managed.ts similarity index 94% rename from lib/modules/event_processor/managed.ts rename to lib/event_processor/managed.ts index 40b786380..dfb94e0f5 100644 --- a/lib/modules/event_processor/managed.ts +++ b/lib/event_processor/managed.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/modules/event_processor/pendingEventsDispatcher.ts b/lib/event_processor/pendingEventsDispatcher.ts similarity index 73% rename from lib/modules/event_processor/pendingEventsDispatcher.ts rename to lib/event_processor/pendingEventsDispatcher.ts index 4f4c8c61b..cfa2c3e80 100644 --- a/lib/modules/event_processor/pendingEventsDispatcher.ts +++ b/lib/event_processor/pendingEventsDispatcher.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. @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getLogger } from '../logging' -import { EventDispatcher, EventV1Request, EventDispatcherCallback } from './eventDispatcher' +import { getLogger } from '../modules/logging' +import { EventDispatcher, EventV1Request, EventDispatcherResponse } from './eventDispatcher' import { PendingEventsStore, LocalStorageStore } from './pendingEventsStore' -import { uuid, getTimestamp } from '../../utils/fns' +import { uuid, getTimestamp } from '../utils/fns' const logger = getLogger('EventProcessor') @@ -41,14 +41,13 @@ export class PendingEventsDispatcher implements EventDispatcher { this.store = store } - dispatchEvent(request: EventV1Request, callback: EventDispatcherCallback): void { - this.send( + dispatchEvent(request: EventV1Request): Promise { + return this.send( { uuid: uuid(), timestamp: getTimestamp(), request, - }, - callback, + } ) } @@ -58,22 +57,18 @@ export class PendingEventsDispatcher implements EventDispatcher { logger.debug('Sending %s pending events from previous page', pendingEvents.length) pendingEvents.forEach(item => { - try { - this.send(item, () => {}) - } catch (e) - { - logger.debug(String(e)) - } + this.send(item).catch((e) => { + logger.debug(String(e)); + }); }) } - protected send(entry: DispatcherEntry, callback: EventDispatcherCallback): void { + protected async send(entry: DispatcherEntry): Promise { this.store.set(entry.uuid, entry) - this.dispatcher.dispatchEvent(entry.request, response => { - this.store.remove(entry.uuid) - callback(response) - }) + const response = await this.dispatcher.dispatchEvent(entry.request); + this.store.remove(entry.uuid); + return response; } } diff --git a/lib/modules/event_processor/pendingEventsStore.ts b/lib/event_processor/pendingEventsStore.ts similarity index 95% rename from lib/modules/event_processor/pendingEventsStore.ts rename to lib/event_processor/pendingEventsStore.ts index 6b5ee3393..ca8dbf0f7 100644 --- a/lib/modules/event_processor/pendingEventsStore.ts +++ b/lib/event_processor/pendingEventsStore.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. @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { objectValues } from '../../utils/fns' -import { getLogger } from '../logging'; +import { objectValues } from '../utils/fns' +import { getLogger } from '../modules/logging'; const logger = getLogger('EventProcessor') diff --git a/lib/modules/event_processor/reactNativeEventsStore.ts b/lib/event_processor/reactNativeEventsStore.ts similarity index 90% rename from lib/modules/event_processor/reactNativeEventsStore.ts rename to lib/event_processor/reactNativeEventsStore.ts index b0ef113f3..cf7dce9c8 100644 --- a/lib/modules/event_processor/reactNativeEventsStore.ts +++ b/lib/event_processor/reactNativeEventsStore.ts @@ -14,12 +14,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getLogger } from '../logging' -import { objectValues } from "../../utils/fns" +import { getLogger } from '../modules/logging' +import { objectValues } from '../utils/fns' import { Synchronizer } from './synchronizer' -import ReactNativeAsyncStorageCache from '../../plugins/key_value_cache/reactNativeAsyncStorageCache'; -import PersistentKeyValueCache from '../../plugins/key_value_cache/persistentKeyValueCache'; +import ReactNativeAsyncStorageCache from '../plugins/key_value_cache/reactNativeAsyncStorageCache'; +import PersistentKeyValueCache from '../plugins/key_value_cache/persistentKeyValueCache'; const logger = getLogger('ReactNativeEventsStore') diff --git a/lib/modules/event_processor/requestTracker.ts b/lib/event_processor/requestTracker.ts similarity index 98% rename from lib/modules/event_processor/requestTracker.ts rename to lib/event_processor/requestTracker.ts index ab18b36d1..192919884 100644 --- a/lib/modules/event_processor/requestTracker.ts +++ b/lib/event_processor/requestTracker.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/modules/event_processor/synchronizer.ts b/lib/event_processor/synchronizer.ts similarity index 97% rename from lib/modules/event_processor/synchronizer.ts rename to lib/event_processor/synchronizer.ts index d6bf32b7b..f0659d7af 100644 --- a/lib/modules/event_processor/synchronizer.ts +++ b/lib/event_processor/synchronizer.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/modules/event_processor/v1/buildEventV1.ts b/lib/event_processor/v1/buildEventV1.ts similarity index 99% rename from lib/modules/event_processor/v1/buildEventV1.ts rename to lib/event_processor/v1/buildEventV1.ts index 699498dc4..1232d52ec 100644 --- a/lib/modules/event_processor/v1/buildEventV1.ts +++ b/lib/event_processor/v1/buildEventV1.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/modules/event_processor/v1/v1EventProcessor.react_native.ts b/lib/event_processor/v1/v1EventProcessor.react_native.ts similarity index 92% rename from lib/modules/event_processor/v1/v1EventProcessor.react_native.ts rename to lib/event_processor/v1/v1EventProcessor.react_native.ts index bd40a88bd..f4998a37b 100644 --- a/lib/modules/event_processor/v1/v1EventProcessor.react_native.ts +++ b/lib/event_processor/v1/v1EventProcessor.react_native.ts @@ -16,13 +16,13 @@ import { uuid as id, objectEntries, -} from '../../../utils/fns' +} from '../../utils/fns' import { NetInfoState, addEventListener as addConnectionListener, } from "@react-native-community/netinfo" -import { getLogger } from '../../logging' -import { NotificationSender } from '../../../core/notification_center' +import { getLogger } from '../../modules/logging' +import { NotificationSender } from '../../core/notification_center' import { getQueue, @@ -43,9 +43,8 @@ import { formatEvents } from './buildEventV1' import { EventV1Request, EventDispatcher, - EventDispatcherResponse, } from '../eventDispatcher' -import { PersistentCacheProvider } from '../../../shared_types' +import { PersistentCacheProvider } from '../../shared_types' const logger = getLogger('ReactNativeEventProcessor') @@ -57,6 +56,7 @@ const EVENT_BUFFER_STORE_KEY = 'fs_optly_event_buffer' * React Native Events Processor with Caching support for events when app is offline. */ export class LogTierV1EventProcessor implements EventProcessor { + private id = Math.random(); private dispatcher: EventDispatcher // expose for testing public queue: EventQueue @@ -147,7 +147,6 @@ export class LogTierV1EventProcessor implements EventProcessor { // Retry pending failed events while draining queue await this.processPendingEvents() - logger.debug('draining queue with %s events', buffer.length) const eventCacheKey = id() @@ -199,16 +198,19 @@ export class LogTierV1EventProcessor implements EventProcessor { private async dispatchEvent(eventCacheKey: string, event: EventV1Request): Promise { const requestPromise = new Promise((resolve) => { - this.dispatcher.dispatchEvent(event, async ({ statusCode }: EventDispatcherResponse) => { - if (this.isSuccessResponse(statusCode)) { - await this.pendingEventsStore.remove(eventCacheKey) + this.dispatcher.dispatchEvent(event).then((response) => { + if (!response.statusCode || this.isSuccessResponse(response.statusCode)) { + return this.pendingEventsStore.remove(eventCacheKey) } else { this.shouldSkipDispatchToPreserveSequence = true - logger.warn('Failed to dispatch event, Response status Code: %s', statusCode) + logger.warn('Failed to dispatch event, Response status Code: %s', response.statusCode) + return Promise.resolve() } - resolve() - }) - sendEventNotification(this.notificationSender, event) + }).catch((e) => { + logger.warn('Failed to dispatch event, error: %s', e.message) + }).finally(() => resolve()) + + sendEventNotification(this.notificationSender, event) }) // Tracking all the requests to dispatch to make sure request is completed before fulfilling the `stop` promise this.requestTracker.trackRequest(requestPromise) diff --git a/lib/modules/event_processor/v1/v1EventProcessor.ts b/lib/event_processor/v1/v1EventProcessor.ts similarity index 91% rename from lib/modules/event_processor/v1/v1EventProcessor.ts rename to lib/event_processor/v1/v1EventProcessor.ts index 235fae83b..aac5103ef 100644 --- a/lib/modules/event_processor/v1/v1EventProcessor.ts +++ b/lib/event_processor/v1/v1EventProcessor.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. @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getLogger } from '../../logging' -import { NotificationSender } from '../../../core/notification_center' +import { getLogger } from '../../modules/logging' +import { NotificationSender } from '../../core/notification_center' import { EventDispatcher } from '../eventDispatcher' import { @@ -83,7 +83,9 @@ export class LogTierV1EventProcessor implements EventProcessor { const dispatcher = useClosingDispatcher && this.closingDispatcher ? this.closingDispatcher : this.dispatcher; - dispatcher.dispatchEvent(formattedEvent, () => { + // TODO: this does not do anything if the dispatcher fails + // to dispatch. What should be done in that case? + dispatcher.dispatchEvent(formattedEvent).finally(() => { resolve() }) sendEventNotification(this.notificationCenter, formattedEvent) diff --git a/lib/index.browser.tests.js b/lib/index.browser.tests.js index 8b43a4902..3d3952189 100644 --- a/lib/index.browser.tests.js +++ b/lib/index.browser.tests.js @@ -122,17 +122,18 @@ describe('javascript-sdk (Browser)', function() { delete global.XMLHttpRequest; }); - describe('when an eventDispatcher is not passed in', function() { - it('should wrap the default eventDispatcher and invoke sendPendingEvents', function() { - var optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager(), - errorHandler: fakeErrorHandler, - logger: silentLogger, - }); - - sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); - }); - }); + // TODO: pending event handling will be done by EventProcessor instead + // describe('when an eventDispatcher is not passed in', function() { + // it('should wrap the default eventDispatcher and invoke sendPendingEvents', function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager(), + // errorHandler: fakeErrorHandler, + // logger: silentLogger, + // }); + + // sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); + // }); + // }); describe('when an eventDispatcher is passed in', function() { it('should NOT wrap the default eventDispatcher and invoke sendPendingEvents', function() { @@ -147,24 +148,26 @@ describe('javascript-sdk (Browser)', function() { }); }); - it('should invoke resendPendingEvents at most once', function() { - var optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager(), - errorHandler: fakeErrorHandler, - logger: silentLogger, - }); + // TODO: pending event handling should be part of the event processor + // logic, not the dispatcher. Refactor accordingly. + // it('should invoke resendPendingEvents at most once', function() { + // var optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager(), + // errorHandler: fakeErrorHandler, + // logger: silentLogger, + // }); - sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); + // sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); - optlyInstance = optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager(), - errorHandler: fakeErrorHandler, - logger: silentLogger, - }); - optlyInstance.onReady().catch(function() {}); + // optlyInstance = optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager(), + // errorHandler: fakeErrorHandler, + // logger: silentLogger, + // }); + // optlyInstance.onReady().catch(function() {}); - sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); - }); + // sinon.assert.calledOnce(LocalStoragePendingEventsDispatcher.prototype.sendPendingEvents); + // }); it('should not throw if the provided config is not valid', function() { configValidator.validate.throws(new Error('Invalid config or something')); @@ -438,149 +441,151 @@ describe('javascript-sdk (Browser)', function() { }); }); - 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, - }) - ); - }); - }); - }); + // 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', () => { diff --git a/lib/index.browser.ts b/lib/index.browser.ts index f80a7b2c3..fd92d72c9 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -16,10 +16,10 @@ import logHelper from './modules/logging/logger'; import { getLogger, setErrorHandler, getErrorHandler, LogLevel } from './modules/logging'; -import { LocalStoragePendingEventsDispatcher } from './modules/event_processor'; +import { LocalStoragePendingEventsDispatcher } from './event_processor'; import configValidator from './utils/config_validator'; import defaultErrorHandler from './plugins/error_handler'; -import defaultEventDispatcher from './plugins/event_dispatcher/index.browser'; +import defaultEventDispatcher from './event_processor/default_dispatcher.browser'; import sendBeaconEventDispatcher from './plugins/event_dispatcher/send_beacon_dispatcher'; import * as enums from './utils/enums'; import * as loggerPlugin from './plugins/logger'; @@ -34,6 +34,7 @@ import { getUserAgentParser } from './plugins/odp/user_agent_parser/index.browse import * as commonExports from './common_exports'; import { PollingConfigManagerConfig } from './project_config/config_manager_factory'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.browser'; +import { createForwardingEventProcessor } from './event_processor/event_processor_factory.browser'; const logger = getLogger(); logHelper.setLogHandler(loggerPlugin.createLogger()); @@ -77,55 +78,55 @@ 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; - } + // 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 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) { @@ -137,7 +138,7 @@ const createInstance = function(config: Config): Client | null { const optimizelyOptions: OptimizelyOptions = { clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, ...config, - eventProcessor: eventProcessor.createEventProcessor(eventProcessorConfig), + // eventProcessor: eventProcessor.createEventProcessor(eventProcessorConfig), logger, errorHandler, notificationCenter, @@ -197,6 +198,7 @@ export { IUserAgentParser, getUserAgentParser, createPollingProjectConfigManager, + createForwardingEventProcessor, }; export * from './common_exports'; @@ -215,6 +217,7 @@ export default { OptimizelyDecideOption, getUserAgentParser, createPollingProjectConfigManager, + createForwardingEventProcessor, }; export * from './export_types'; diff --git a/lib/index.lite.ts b/lib/index.lite.ts index b6a6bdfe9..b7fb41def 100644 --- a/lib/index.lite.ts +++ b/lib/index.lite.ts @@ -28,7 +28,6 @@ import * as enums from './utils/enums'; import * as loggerPlugin from './plugins/logger'; import Optimizely from './optimizely'; import { createNotificationCenter } from './core/notification_center'; -import { createForwardingEventProcessor } from './plugins/event_processor/forwarding_event_processor'; import { OptimizelyDecideOption, Client, ConfigLite } from './shared_types'; import * as commonExports from './common_exports'; @@ -69,15 +68,12 @@ setLogLevel(LogLevel.ERROR); const errorHandler = getErrorHandler(); const notificationCenter = createNotificationCenter({ logger: logger, errorHandler: errorHandler }); - const eventDispatcher = config.eventDispatcher || noOpEventDispatcher; - const eventProcessor = createForwardingEventProcessor(eventDispatcher, notificationCenter); const optimizelyOptions = { clientEngine: enums.JAVASCRIPT_CLIENT_ENGINE, ...config, logger, errorHandler, - eventProcessor, notificationCenter, isValidInstance: isValidInstance, }; diff --git a/lib/index.node.tests.js b/lib/index.node.tests.js index 4acbdf5f6..8ff0edeff 100644 --- a/lib/index.node.tests.js +++ b/lib/index.node.tests.js @@ -24,7 +24,6 @@ import * as loggerPlugin from './plugins/logger'; import optimizelyFactory from './index.node'; import configValidator from './utils/config_validator'; import { getMockProjectConfigManager } from './tests/mock/mock_project_config_manager'; -import { createProjectConfig } from './project_config/project_config'; describe('optimizelyFactory', function() { describe('APIs', function() { @@ -89,122 +88,124 @@ describe('optimizelyFactory', function() { assert.equal(optlyInstance.clientVersion, '5.3.4'); }); - describe('event processor configuration', function() { - var eventProcessorSpy; - beforeEach(function() { - eventProcessorSpy = sinon.stub(eventProcessor, 'createEventProcessor').callThrough(); - }); - - afterEach(function() { - eventProcessor.createEventProcessor.restore(); - }); - - it('should ignore invalid event flush interval and use default instead', function() { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - eventFlushInterval: ['invalid', 'flush', 'interval'], - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - flushInterval: 30000, - }) - ); - }); - - it('should use default event flush interval when none is provided', function() { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - flushInterval: 30000, - }) - ); - }); - - it('should use provided event flush interval when valid', function() { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - eventFlushInterval: 10000, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - flushInterval: 10000, - }) - ); - }); - - it('should ignore invalid event batch size and use default instead', function() { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - eventBatchSize: null, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - batchSize: 10, - }) - ); - }); - - it('should use default event batch size when none is provided', function() { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - batchSize: 10, - }) - ); - }); - - it('should use provided event batch size when valid', function() { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - logger: fakeLogger, - eventBatchSize: 300, - }); - sinon.assert.calledWithExactly( - eventProcessorSpy, - sinon.match({ - batchSize: 300, - }) - ); - }); - }); + // TODO: user will create and inject an event processor + // these tests will be refactored accordingly + // describe('event processor configuration', function() { + // var eventProcessorSpy; + // beforeEach(function() { + // eventProcessorSpy = sinon.stub(eventProcessor, 'createEventProcessor').callThrough(); + // }); + + // afterEach(function() { + // eventProcessor.createEventProcessor.restore(); + // }); + + // it('should ignore invalid event flush interval and use default instead', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventFlushInterval: ['invalid', 'flush', 'interval'], + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // flushInterval: 30000, + // }) + // ); + // }); + + // it('should use default event flush interval when none is provided', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // flushInterval: 30000, + // }) + // ); + // }); + + // it('should use provided event flush interval when valid', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventFlushInterval: 10000, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // flushInterval: 10000, + // }) + // ); + // }); + + // it('should ignore invalid event batch size and use default instead', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventBatchSize: null, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // batchSize: 10, + // }) + // ); + // }); + + // it('should use default event batch size when none is provided', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // batchSize: 10, + // }) + // ); + // }); + + // it('should use provided event batch size when valid', function() { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // logger: fakeLogger, + // eventBatchSize: 300, + // }); + // sinon.assert.calledWithExactly( + // eventProcessorSpy, + // sinon.match({ + // batchSize: 300, + // }) + // ); + // }); + // }); }); }); }); diff --git a/lib/index.node.ts b/lib/index.node.ts index 0bb12d21e..98efc5d64 100644 --- a/lib/index.node.ts +++ b/lib/index.node.ts @@ -20,7 +20,7 @@ import * as enums from './utils/enums'; import * as loggerPlugin from './plugins/logger'; import configValidator from './utils/config_validator'; import defaultErrorHandler from './plugins/error_handler'; -import defaultEventDispatcher from './plugins/event_dispatcher/index.node'; +import defaultEventDispatcher from './event_processor/default_dispatcher.node'; import eventProcessorConfigValidator from './utils/event_processor_config_validator'; import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor'; @@ -28,6 +28,7 @@ import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { NodeOdpManager } from './plugins/odp_manager/index.node'; import * as commonExports from './common_exports'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.node'; +import { createForwardingEventProcessor } from './event_processor/event_processor_factory.node'; const logger = getLogger(); setLogLevel(LogLevel.ERROR); @@ -73,34 +74,35 @@ 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; - } + // 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 eventProcessorConfig = { + // dispatcher: config.eventDispatcher || defaultEventDispatcher, + // flushInterval: eventFlushInterval, + // batchSize: eventBatchSize, + // maxQueueSize: config.eventMaxQueueSize || DEFAULT_EVENT_MAX_QUEUE_SIZE, + // notificationCenter, + // }; - const eventProcessor = createEventProcessor(eventProcessorConfig); + // const eventProcessor = createEventProcessor(eventProcessorConfig); + // const eventProcessor = config.eventProcessor; const odpExplicitlyOff = config.odpOptions?.disabled === true; if (odpExplicitlyOff) { @@ -112,7 +114,7 @@ const createInstance = function(config: Config): Client | null { const optimizelyOptions = { clientEngine: enums.NODE_CLIENT_ENGINE, ...config, - eventProcessor, + // eventProcessor, logger, errorHandler, notificationCenter, @@ -141,7 +143,8 @@ export { setLogLevel, createInstance, OptimizelyDecideOption, - createPollingProjectConfigManager + createPollingProjectConfigManager, + createForwardingEventProcessor, }; export * from './common_exports'; @@ -156,7 +159,8 @@ export default { setLogLevel, createInstance, OptimizelyDecideOption, - createPollingProjectConfigManager + createPollingProjectConfigManager, + createForwardingEventProcessor, }; export * from './export_types'; diff --git a/lib/index.react_native.ts b/lib/index.react_native.ts index 3be9b300c..b2654823d 100644 --- a/lib/index.react_native.ts +++ b/lib/index.react_native.ts @@ -20,7 +20,7 @@ import Optimizely from './optimizely'; import configValidator from './utils/config_validator'; import defaultErrorHandler from './plugins/error_handler'; import * as loggerPlugin from './plugins/logger/index.react_native'; -import defaultEventDispatcher from './plugins/event_dispatcher/index.browser'; +import defaultEventDispatcher from './event_processor/default_dispatcher.browser'; import eventProcessorConfigValidator from './utils/event_processor_config_validator'; import { createNotificationCenter } from './core/notification_center'; import { createEventProcessor } from './plugins/event_processor/index.react_native'; @@ -28,6 +28,7 @@ import { OptimizelyDecideOption, Client, Config } from './shared_types'; import { BrowserOdpManager } from './plugins/odp_manager/index.browser'; import * as commonExports from './common_exports'; import { createPollingProjectConfigManager } from './project_config/config_manager_factory.react_native'; +import { createForwardingEventProcessor } from './event_processor/event_processor_factory.react_native'; import 'fast-text-encoding'; import 'react-native-get-random-values'; @@ -71,35 +72,35 @@ 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; - } + // 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 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 eventProcessor = createEventProcessor(eventProcessorConfig); const odpExplicitlyOff = config.odpOptions?.disabled === true; if (odpExplicitlyOff) { @@ -111,7 +112,7 @@ const createInstance = function(config: Config): Client | null { const optimizelyOptions = { clientEngine: enums.REACT_NATIVE_JS_CLIENT_ENGINE, ...config, - eventProcessor, + // eventProcessor, logger, errorHandler, notificationCenter, @@ -146,6 +147,7 @@ export { createInstance, OptimizelyDecideOption, createPollingProjectConfigManager, + createForwardingEventProcessor, }; export * from './common_exports'; @@ -161,6 +163,7 @@ export default { createInstance, OptimizelyDecideOption, createPollingProjectConfigManager, + createForwardingEventProcessor, }; export * from './export_types'; diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index 9047ee71a..ca375151b 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -26,7 +26,6 @@ import AudienceEvaluator from '../core/audience_evaluator'; import * as bucketer from '../core/bucketer'; import * as projectConfigManager from '../project_config/project_config_manager'; import * as enums from '../utils/enums'; -import eventDispatcher from '../plugins/event_dispatcher/index.node'; import errorHandler from '../plugins/error_handler'; import fns from '../utils/fns'; import * as logger from '../plugins/logger'; @@ -34,10 +33,9 @@ import * as decisionService from '../core/decision_service'; import * as jsonSchemaValidator from '../utils/json_schema_validator'; import * as projectConfig from '../project_config/project_config'; import testData from '../tests/test_data'; -import { createForwardingEventProcessor } from '../plugins/event_processor/forwarding_event_processor'; +import { getForwardingEventProcessor } from '../event_processor/forwarding_event_processor'; import { createEventProcessor } from '../plugins/event_processor'; import { createNotificationCenter } from '../core/notification_center'; -import { NodeOdpManager } from '../plugins/odp_manager/index.node'; import { createProjectConfig } from '../project_config/project_config'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; @@ -51,6 +49,17 @@ var FEATURE_VARIABLE_TYPES = enums.FEATURE_VARIABLE_TYPES; var buildLogMessageFromArgs = args => sprintf(args[1], ...args.splice(2)); +const getMockEventDispatcher = () => { + const dispatcher = { + dispatchEvent: sinon.spy(() => Promise.resolve({ statusCode: 200 })), + } + return dispatcher; +} + +const getMockEventProcessor = (notificationCenter) => { + return getForwardingEventProcessor(getMockEventDispatcher(), notificationCenter); +} + describe('lib/optimizely', function() { var ProjectConfigManagerStub; var globalStubErrorHandler; @@ -77,7 +86,7 @@ describe('lib/optimizely', function() { onReady: sinon.stub().returns({ then: function() {} }), }; }); - sinon.stub(eventDispatcher, 'dispatchEvent'); + // sinon.stub(eventDispatcher, 'dispatchEvent'); clock = sinon.useFakeTimers(new Date()); }); @@ -85,7 +94,7 @@ describe('lib/optimizely', function() { ProjectConfigManagerStub.restore(); logging.resetErrorHandler(); logging.resetLogger(); - eventDispatcher.dispatchEvent.restore(); + // eventDispatcher.dispatchEvent.restore(); clock.restore(); }); @@ -98,7 +107,7 @@ describe('lib/optimizely', function() { }; var createdLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO }); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: stubErrorHandler }); - var eventProcessor = createForwardingEventProcessor(stubEventDispatcher); + var eventProcessor = getForwardingEventProcessor(stubEventDispatcher); beforeEach(function() { sinon.stub(stubErrorHandler, 'handleError'); sinon.stub(createdLogger, 'log'); @@ -226,8 +235,9 @@ describe('lib/optimizely', function() { var optlyInstance; var bucketStub; var fakeDecisionResponse; + var eventDispatcher = getMockEventDispatcher(); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler }); - var eventProcessor = createForwardingEventProcessor(eventDispatcher, notificationCenter); + var eventProcessor = getForwardingEventProcessor(eventDispatcher, notificationCenter); var createdLogger = logger.createLogger({ logLevel: LOG_LEVEL.INFO, logToConsole: false, @@ -239,10 +249,8 @@ describe('lib/optimizely', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', - // datafile: testData.getTestProjectConfig(), projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -258,6 +266,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); bucketer.bucket.restore(); errorHandler.handleError.restore(); createdLogger.log.restore(); @@ -2693,7 +2702,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -2756,6 +2765,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, + eventProcessor, eventDispatcher: eventDispatcher, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, @@ -4463,6 +4473,7 @@ describe('lib/optimizely', function() { logToConsole: false, }); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -4495,6 +4506,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); bucketer.bucket.restore(); errorHandler.handleError.restore(); createdLogger.log.restore(); @@ -4605,6 +4617,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); errorHandler.handleError.restore(); createdLogger.log.restore(); fns.uuid.restore(); @@ -4968,7 +4981,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -4985,6 +4998,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); optlyInstance.notificationCenter.sendNotifications.restore(); errorHandler.handleError.restore(); createdLogger.log.restore(); @@ -5082,7 +5096,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -5151,7 +5165,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -5165,6 +5179,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); optlyInstance.notificationCenter.sendNotifications.restore(); }); @@ -5767,6 +5782,7 @@ describe('lib/optimizely', function() { describe('#decideForKeys', function() { var userId = 'tester'; beforeEach(function() { + eventDispatcher.dispatchEvent.reset(); const mockConfigManager = getMockProjectConfigManager({ initConfig: createProjectConfig(testData.getTestDecideProjectConfig()), }); @@ -5775,7 +5791,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -5789,6 +5805,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); optlyInstance.notificationCenter.sendNotifications.restore(); }); @@ -5886,7 +5903,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -5900,6 +5917,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); optlyInstance.notificationCenter.sendNotifications.restore(); }); @@ -5992,13 +6010,12 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, eventBatchSize: 1, defaultDecideOptions: [OptimizelyDecideOption.ENABLED_FLAGS_ONLY], - eventProcessor, notificationCenter, }); @@ -6006,6 +6023,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); optlyInstance.notificationCenter.sendNotifications.restore(); }); @@ -6084,6 +6102,7 @@ describe('lib/optimizely', function() { logToConsole: false, }); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -6098,7 +6117,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -6107,6 +6126,10 @@ describe('lib/optimizely', function() { }); }); + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + var userAttributes = { browser_type: 'firefox', }; @@ -6150,6 +6173,10 @@ describe('lib/optimizely', function() { var optlyInstance; var fakeDecisionResponse; var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; + var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -6165,7 +6192,6 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -6174,6 +6200,7 @@ describe('lib/optimizely', function() { eventProcessor, }); + sandbox.stub(eventDispatcher, 'dispatchEvent'); sandbox.stub(errorHandler, 'handleError'); sandbox.stub(createdLogger, 'log'); sandbox.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); @@ -6196,7 +6223,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager(), errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, eventProcessor, @@ -6228,6 +6255,10 @@ describe('lib/optimizely', function() { }; sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }); it('returns true and dispatches an impression event', function() { var user = optlyInstance.createUserContext('user1', attributes); @@ -6303,7 +6334,6 @@ describe('lib/optimizely', function() { }; var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; assert.deepEqual(callArgs[0], expectedImpressionEvent); - assert.isFunction(callArgs[1]); assert.equal( buildLogMessageFromArgs(createdLogger.log.lastCall.args), 'OPTIMIZELY: Feature test_feature_for_experiment is enabled for user user1.' @@ -6451,6 +6481,10 @@ describe('lib/optimizely', function() { sandbox.stub(optlyInstance.decisionService, 'getVariationForFeature').returns(fakeDecisionResponse); result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); }); + + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }); it('should return false', function() { assert.strictEqual(result, false); @@ -6527,7 +6561,6 @@ describe('lib/optimizely', function() { }; var callArgs = eventDispatcher.dispatchEvent.getCalls()[0].args; assert.deepEqual(callArgs[0], expectedImpressionEvent); - assert.isFunction(callArgs[1]); }); }); @@ -6543,6 +6576,10 @@ describe('lib/optimizely', function() { }); }); + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + it('should return false', function() { var result = optlyInstance.isFeatureEnabled('shared_feature', 'user1', attributes); var user = optlyInstance.createUserContext('user1', attributes); @@ -6732,12 +6769,16 @@ describe('lib/optimizely', function() { }); }); + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + it('returns an empty array if the instance is invalid', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager(), errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, eventProcessor, @@ -6781,7 +6822,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -8979,6 +9020,9 @@ describe('lib/optimizely', function() { }); var optlyInstance; var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -8993,7 +9037,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -9002,6 +9046,7 @@ describe('lib/optimizely', function() { notificationCenter, }); + sandbox.stub(eventDispatcher, 'dispatchEvent'); sandbox.stub(errorHandler, 'handleError'); sandbox.stub(createdLogger, 'log'); }); @@ -9123,6 +9168,9 @@ describe('lib/optimizely', function() { var optlyInstance; var audienceEvaluator; var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = { + dispatchEvent: () => Promise.resolve({ statusCode: 200 }), + }; var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -9137,7 +9185,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -9147,6 +9195,7 @@ describe('lib/optimizely', function() { }); audienceEvaluator = AudienceEvaluator.prototype; + sandbox.stub(eventDispatcher, 'dispatchEvent'); sandbox.stub(errorHandler, 'handleError'); sandbox.stub(createdLogger, 'log'); evalSpy = sandbox.spy(audienceEvaluator, 'evaluate'); @@ -9313,6 +9362,7 @@ describe('lib/optimizely', function() { var bucketStub; var fakeDecisionResponse; var notificationCenter; + var eventDispatcher; var eventProcessor; var createdLogger = logger.createLogger({ @@ -9326,6 +9376,7 @@ describe('lib/optimizely', function() { sinon.stub(createdLogger, 'log'); sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + eventDispatcher = getMockEventDispatcher(); eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 3, @@ -9335,6 +9386,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); bucketer.bucket.restore(); errorHandler.handleError.restore(); createdLogger.log.restore(); @@ -9353,7 +9405,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -9653,7 +9705,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -9689,7 +9741,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: mockConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, isValidInstance: true, @@ -9701,6 +9753,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); return eventProcessorStopPromise.catch(function() { // Handle rejected promise - don't want test to fail }); @@ -9725,6 +9778,7 @@ describe('lib/optimizely', function() { }); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -9737,6 +9791,7 @@ describe('lib/optimizely', function() { }); afterEach(function() { + eventDispatcher.dispatchEvent.reset(); errorHandler.handleError.restore(); createdLogger.log.restore(); }); @@ -9751,7 +9806,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', errorHandler: errorHandler, projectConfigManager, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9769,7 +9824,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager: getMockProjectConfigManager(), errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9779,6 +9834,7 @@ describe('lib/optimizely', function() { }); }); + it('returns fallback values from API methods that return meaningful values', function() { assert.isNull(optlyInstance.activate('my_experiment', 'user1')); assert.isNull(optlyInstance.getVariation('my_experiment', 'user1')); @@ -9822,7 +9878,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', projectConfigManager, errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9841,7 +9897,6 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', errorHandler: errorHandler, projectConfigManager, - eventDispatcher: eventDispatcher, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9865,7 +9920,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', errorHandler: errorHandler, projectConfigManager, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9889,7 +9944,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', errorHandler: errorHandler, projectConfigManager, - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9911,7 +9966,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', errorHandler: errorHandler, projectConfigManager: getMockProjectConfigManager(), - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9941,7 +9996,7 @@ describe('lib/optimizely', function() { clientEngine: 'node-sdk', errorHandler: errorHandler, projectConfigManager: getMockProjectConfigManager(), - eventDispatcher: eventDispatcher, + eventProcessor, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, sdkKey: '12345', @@ -9966,7 +10021,7 @@ describe('lib/optimizely', function() { optlyInstance = new Optimizely({ clientEngine: 'node-sdk', errorHandler: errorHandler, - eventDispatcher: eventDispatcher, + eventProcessor, projectConfigManager: fakeProjectConfigManager, jsonSchemaValidator: jsonSchemaValidator, logger: createdLogger, @@ -10051,7 +10106,7 @@ describe('lib/optimizely', function() { var eventProcessor; beforeEach(function() { bucketStub = sinon.stub(bucketer, 'bucket'); - eventDispatcherSpy = sinon.spy(); + eventDispatcherSpy = sinon.spy(() => Promise.resolve({ statusCode: 200 })); eventProcessor = createEventProcessor({ dispatcher: { dispatchEvent: eventDispatcherSpy }, batchSize: 1, @@ -10113,46 +10168,48 @@ describe('lib/optimizely', function() { // Note: /lib/index.browser.tests.js contains relevant Opti Client x Browser ODP Tests // TODO: Finish these tests in ODP Node.js Implementation describe('odp', () => { - var optlyInstanceWithOdp; - var bucketStub; - var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler }); - var eventProcessor = createForwardingEventProcessor(eventDispatcher, notificationCenter); - var createdLogger = logger.createLogger({ - logLevel: LOG_LEVEL.INFO, - logToConsole: false, - }); - - beforeEach(function() { - const datafile = testData.getTestProjectConfig(); - const mockConfigManager = getMockProjectConfigManager(); - mockConfigManager.setConfig(createProjectConfig(datafile, JSON.stringify(datafile))); - - optlyInstanceWithOdp = new Optimizely({ - clientEngine: 'node-sdk', - projectConfigManager: mockConfigManager, - errorHandler: errorHandler, - eventDispatcher: eventDispatcher, - jsonSchemaValidator: jsonSchemaValidator, - logger: createdLogger, - isValidInstance: true, - eventBatchSize: 1, - eventProcessor, - notificationCenter, - odpManager: new NodeOdpManager({}), - }); - - bucketStub = sinon.stub(bucketer, 'bucket'); - sinon.stub(errorHandler, 'handleError'); - sinon.stub(createdLogger, 'log'); - sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); - }); - - afterEach(function() { - bucketer.bucket.restore(); - errorHandler.handleError.restore(); - createdLogger.log.restore(); - fns.uuid.restore(); - }); + // var optlyInstanceWithOdp; + // var bucketStub; + // var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler }); + // var eventDispatcher = getMockEventDispatcher(); + // var eventProcessor = createForwardingEventProcessor(eventDispatcher, notificationCenter); + // var createdLogger = logger.createLogger({ + // logLevel: LOG_LEVEL.INFO, + // logToConsole: false, + // }); + + // beforeEach(function() { + // const datafile = testData.getTestProjectConfig(); + // const mockConfigManager = getMockProjectConfigManager(); + // mockConfigManager.setConfig(createProjectConfig(datafile, JSON.stringify(datafile))); + + // optlyInstanceWithOdp = new Optimizely({ + // clientEngine: 'node-sdk', + // projectConfigManager: mockConfigManager, + // errorHandler: errorHandler, + // eventDispatcher: eventDispatcher, + // jsonSchemaValidator: jsonSchemaValidator, + // logger: createdLogger, + // isValidInstance: true, + // eventBatchSize: 1, + // eventProcessor, + // notificationCenter, + // odpManager: new NodeOdpManager({}), + // }); + + // bucketStub = sinon.stub(bucketer, 'bucket'); + // sinon.stub(errorHandler, 'handleError'); + // sinon.stub(createdLogger, 'log'); + // sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); + // }); + + // afterEach(function() { + // eventDispatcher.dispatchEvent.reset(); + // bucketer.bucket.restore(); + // errorHandler.handleError.restore(); + // createdLogger.log.restore(); + // fns.uuid.restore(); + // }); it('should send an identify event when called with odp enabled', () => { // TODO diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index 95d3682a3..c78154311 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -17,7 +17,7 @@ import { LoggerFacade, ErrorHandler } from '../modules/logging'; import { sprintf, objectValues } from '../utils/fns'; import { NotificationCenter } from '../core/notification_center'; -import { EventProcessor } from '../modules/event_processor'; +import { EventProcessor } from '../event_processor'; import { IOdpManager } from '../core/odp/odp_manager'; import { OdpConfig } from '../core/odp/odp_config'; @@ -95,7 +95,7 @@ export default class Optimizely implements Client { protected logger: LoggerFacade; private projectConfigManager: ProjectConfigManager; private decisionService: DecisionService; - private eventProcessor: EventProcessor; + private eventProcessor?: EventProcessor; private defaultDecideOptions: { [key: string]: boolean }; protected odpManager?: IOdpManager; public notificationCenter: NotificationCenter; @@ -171,7 +171,8 @@ export default class Optimizely implements Client { this.eventProcessor = config.eventProcessor; - const eventProcessorStartedPromise = this.eventProcessor.start(); + const eventProcessorStartedPromise = this.eventProcessor ? this.eventProcessor.start() : + Promise.resolve(undefined); this.readyPromise = Promise.all([ projectConfigManagerRunningPromise, @@ -276,6 +277,11 @@ export default class Optimizely implements Client { enabled: boolean, attributes?: UserAttributes ): void { + if (!this.eventProcessor) { + this.logger.error(ERROR_MESSAGES.NO_EVENT_PROCESSOR); + return; + } + const configObj = this.projectConfigManager.getConfig(); if (!configObj) { return; @@ -364,6 +370,11 @@ export default class Optimizely implements Client { */ track(eventKey: string, userId: string, attributes?: UserAttributes, eventTags?: EventTags): void { try { + if (!this.eventProcessor) { + this.logger.error(ERROR_MESSAGES.NO_EVENT_PROCESSOR); + return; + } + if (!this.isValidInstance()) { this.logger.log(LOG_LEVEL.ERROR, LOG_MESSAGES.INVALID_OBJECT, MODULE_NAME, 'track'); return; @@ -1304,7 +1315,9 @@ export default class Optimizely implements Client { this.notificationCenter.clearAllNotificationListeners(); - const eventProcessorStoppedPromise = this.eventProcessor.stop(); + const eventProcessorStoppedPromise = this.eventProcessor ? this.eventProcessor.stop() : + Promise.resolve(); + if (this.disposeOnUpdate) { this.disposeOnUpdate(); this.disposeOnUpdate = undefined; diff --git a/lib/optimizely_user_context/index.tests.js b/lib/optimizely_user_context/index.tests.js index 8c436391c..54d34a953 100644 --- a/lib/optimizely_user_context/index.tests.js +++ b/lib/optimizely_user_context/index.tests.js @@ -27,13 +27,19 @@ import { createEventProcessor } from '../plugins/event_processor'; import { createNotificationCenter } from '../core/notification_center'; import Optimizely from '../optimizely'; import errorHandler from '../plugins/error_handler'; -import eventDispatcher from '../plugins/event_dispatcher/index.node'; import { CONTROL_ATTRIBUTES, LOG_LEVEL, LOG_MESSAGES } from '../utils/enums'; import testData from '../tests/test_data'; import { OptimizelyDecideOption } from '../shared_types'; import { getMockProjectConfigManager } from '../tests/mock/mock_project_config_manager'; import { createProjectConfig } from '../project_config/project_config'; +const getMockEventDispatcher = () => { + const dispatcher = { + dispatchEvent: sinon.spy(() => Promise.resolve({ statusCode: 200 })), + } + return dispatcher; +} + describe('lib/optimizely_user_context', function() { describe('APIs', function() { var fakeOptimizely; @@ -351,6 +357,7 @@ describe('lib/optimizely_user_context', function() { describe('when valid forced decision is set', function() { var optlyInstance; var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -370,13 +377,12 @@ describe('lib/optimizely_user_context', function() { }); sinon.stub(optlyInstance.decisionService.logger, 'log'); - sinon.stub(eventDispatcher, 'dispatchEvent'); sinon.stub(optlyInstance.notificationCenter, 'sendNotifications'); }); afterEach(function() { optlyInstance.decisionService.logger.log.restore(); - eventDispatcher.dispatchEvent.restore(); + eventDispatcher.dispatchEvent.reset(); optlyInstance.notificationCenter.sendNotifications.restore(); }); @@ -686,6 +692,7 @@ describe('lib/optimizely_user_context', function() { describe('when invalid forced decision is set', function() { var optlyInstance; var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -705,6 +712,10 @@ describe('lib/optimizely_user_context', function() { }); }); + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }) + it('should NOT return forced decision object when forced decision is set for a flag', function() { var user = optlyInstance.createUserContext(userId); var featureKey = 'feature_1'; @@ -790,6 +801,7 @@ describe('lib/optimizely_user_context', function() { logToConsole: false, }); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, @@ -809,6 +821,10 @@ describe('lib/optimizely_user_context', function() { }); }); + afterEach(() => { + eventDispatcher.dispatchEvent.reset(); + }); + it('should prioritize flag forced decision over experiment rule', function() { var user = optlyInstance.createUserContext(userId); var featureKey = 'feature_1'; @@ -835,6 +851,7 @@ describe('lib/optimizely_user_context', function() { logToConsole: false, }); var notificationCenter = createNotificationCenter({ logger: createdLogger, errorHandler: errorHandler }); + var eventDispatcher = getMockEventDispatcher(); var eventProcessor = createEventProcessor({ dispatcher: eventDispatcher, batchSize: 1, diff --git a/lib/plugins/event_dispatcher/index.browser.tests.js b/lib/plugins/event_dispatcher/index.browser.tests.js deleted file mode 100644 index b5c548c5b..000000000 --- a/lib/plugins/event_dispatcher/index.browser.tests.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright 2016-2017, 2020, 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 sinon from 'sinon'; - -import { dispatchEvent } from './index.browser'; - -describe('lib/plugins/event_dispatcher/browser', function() { - describe('APIs', function() { - describe('dispatchEvent', function() { - beforeEach(function() { - this.requests = []; - global.XMLHttpRequest = sinon.useFakeXMLHttpRequest(); - global.XMLHttpRequest.onCreate = (req) => { this.requests.push(req); }; - }); - - afterEach(function() { - delete global.XMLHttpRequest - }); - - it('should send a POST request with the specified params', function(done) { - var eventParams = { testParam: 'testParamValue' }; - var eventObj = { - url: 'https://cdn.com/event', - body: { - id: 123, - }, - httpVerb: 'POST', - params: eventParams, - }; - - var callback = sinon.spy(); - dispatchEvent(eventObj, callback); - assert.strictEqual(1, this.requests.length); - assert.strictEqual(this.requests[0].method, 'POST'); - assert.strictEqual(this.requests[0].requestBody, JSON.stringify(eventParams)); - done(); - }); - - it('should execute the callback passed to event dispatcher with a post', function(done) { - var eventParams = { testParam: 'testParamValue' }; - var eventObj = { - url: 'https://cdn.com/event', - body: { - id: 123, - }, - httpVerb: 'POST', - params: eventParams, - }; - - var callback = sinon.spy(); - dispatchEvent(eventObj, callback); - this.requests[0].respond([ - 200, - {}, - '{"url":"https://cdn.com/event","body":{"id":123},"httpVerb":"POST","params":{"testParam":"testParamValue"}}', - ]); - sinon.assert.calledOnce(callback); - done(); - }); - - it('should execute the callback passed to event dispatcher with a get', function(done) { - var eventObj = { - url: 'https://cdn.com/event', - httpVerb: 'GET', - }; - - var callback = sinon.spy(); - dispatchEvent(eventObj, callback); - this.requests[0].respond([200, {}, '{"url":"https://cdn.com/event","httpVerb":"GET"']); - sinon.assert.calledOnce(callback); - done(); - }); - }); - }); -}); diff --git a/lib/plugins/event_dispatcher/index.browser.ts b/lib/plugins/event_dispatcher/index.browser.ts deleted file mode 100644 index 9f0da6d0a..000000000 --- a/lib/plugins/event_dispatcher/index.browser.ts +++ /dev/null @@ -1,88 +0,0 @@ -/** - * Copyright 2016-2017, 2020-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. - */ -const POST_METHOD = 'POST'; -const GET_METHOD = 'GET'; -const READYSTATE_COMPLETE = 4; - -export interface Event { - url: string; - httpVerb: 'POST' | 'GET'; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - params: any; -} - - -/** - * Sample event dispatcher implementation for tracking impression and conversions - * Users of the SDK can provide their own implementation - * @param {Event} eventObj - * @param {Function} callback - */ -export const dispatchEvent = function( - eventObj: Event, - callback: (response: { statusCode: number; }) => void -): void { - const params = eventObj.params; - let url: string = eventObj.url; - let req: XMLHttpRequest; - if (eventObj.httpVerb === POST_METHOD) { - req = new XMLHttpRequest(); - req.open(POST_METHOD, url, true); - req.setRequestHeader('Content-Type', 'application/json'); - req.onreadystatechange = function() { - if (req.readyState === READYSTATE_COMPLETE && callback && typeof callback === 'function') { - try { - callback({ statusCode: req.status }); - } catch (e) { - // TODO: Log this somehow (consider adding a logger to the EventDispatcher interface) - } - } - }; - req.send(JSON.stringify(params)); - } else { - // add param for cors headers to be sent by the log endpoint - url += '?wxhr=true'; - if (params) { - url += '&' + toQueryString(params); - } - - req = new XMLHttpRequest(); - req.open(GET_METHOD, url, true); - req.onreadystatechange = function() { - if (req.readyState === READYSTATE_COMPLETE && callback && typeof callback === 'function') { - try { - callback({ statusCode: req.status }); - } catch (e) { - // TODO: Log this somehow (consider adding a logger to the EventDispatcher interface) - } - } - }; - req.send(); - } -} - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -const toQueryString = function(obj: any): string { - return Object.keys(obj) - .map(function(k) { - return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]); - }) - .join('&'); -}; - -export default { - dispatchEvent, -}; diff --git a/lib/plugins/event_dispatcher/index.node.tests.js b/lib/plugins/event_dispatcher/index.node.tests.js deleted file mode 100644 index 2afeccc7b..000000000 --- a/lib/plugins/event_dispatcher/index.node.tests.js +++ /dev/null @@ -1,112 +0,0 @@ -/** - * Copyright 2016-2018, 2020, 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 nock from 'nock'; -import sinon from 'sinon'; -import { assert } from 'chai'; - -import { dispatchEvent } from './index.node'; - -describe('lib/plugins/event_dispatcher/node', function() { - describe('APIs', function() { - describe('dispatchEvent', function() { - var stubCallback = { - callback: function() {}, - }; - - beforeEach(function() { - sinon.stub(stubCallback, 'callback'); - nock('https://cdn.com') - .post('/event') - .reply(200, { - ok: true, - }); - }); - - afterEach(function() { - stubCallback.callback.restore(); - nock.cleanAll(); - }); - - it('should send a POST request with the specified params', function(done) { - var eventObj = { - url: 'https://cdn.com/event', - params: { - id: 123, - }, - httpVerb: 'POST', - }; - - dispatchEvent(eventObj, function(resp) { - assert.equal(200, resp.statusCode); - done(); - }); - }); - - it('should execute the callback passed to event dispatcher', function(done) { - var eventObj = { - url: 'https://cdn.com/event', - params: { - id: 123, - }, - httpVerb: 'POST', - }; - - dispatchEvent(eventObj, stubCallback.callback) - .on('response', function(response) { - sinon.assert.calledOnce(stubCallback.callback); - done(); - }) - .on('error', function(error) { - assert.fail('status code okay', 'status code not okay', ''); - }); - }); - - it('rejects GET httpVerb', function() { - var eventObj = { - url: 'https://cdn.com/event', - params: { - id: 123, - }, - httpVerb: 'GET', - }; - - var callback = sinon.spy(); - dispatchEvent(eventObj, callback); - sinon.assert.notCalled(callback); - }); - - describe('in the event of an error', function() { - beforeEach(function() { - nock('https://example') - .post('/event') - .replyWithError('Connection error') - }); - - it('does not throw', function() { - var eventObj = { - url: 'https://example/event', - params: {}, - httpVerb: 'POST', - }; - - var callback = sinon.spy(); - dispatchEvent(eventObj, callback); - sinon.assert.notCalled(callback); - }); - }); - }); - }); -}); diff --git a/lib/plugins/event_dispatcher/index.node.ts b/lib/plugins/event_dispatcher/index.node.ts deleted file mode 100644 index 8efd7fb4f..000000000 --- a/lib/plugins/event_dispatcher/index.node.ts +++ /dev/null @@ -1,78 +0,0 @@ -/** - * Copyright 2016-2018, 2020-2021, 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 http from 'http'; -import https from 'https'; -import url from 'url'; - -import { Event } from '../../shared_types'; - -/** - * Dispatch an HTTP request to the given url and the specified options - * @param {Event} eventObj Event object containing - * @param {string} eventObj.url the url to make the request to - * @param {Object} eventObj.params parameters to pass to the request (i.e. in the POST body) - * @param {string} eventObj.httpVerb the HTTP request method type. only POST is supported. - * @param {function} callback callback to execute - * @return {ClientRequest|undefined} ClientRequest object which made the request, or undefined if no request was made (error) - */ -export const dispatchEvent = function( - eventObj: Event, - callback: (response: { statusCode: number }) => void -): http.ClientRequest | void { - // Non-POST requests not supported - if (eventObj.httpVerb !== 'POST') { - return; - } - - const parsedUrl = url.parse(eventObj.url); - - const dataString = JSON.stringify(eventObj.params); - - const requestOptions = { - host: parsedUrl.host, - path: parsedUrl.path, - method: 'POST', - headers: { - 'content-type': 'application/json', - 'content-length': dataString.length.toString(), - }, - }; - - const reqWrapper: { req?: http.ClientRequest } = {}; - - const requestCallback = function(response?: { statusCode: number }): void { - if (response && response.statusCode && response.statusCode >= 200 && response.statusCode < 400) { - callback(response); - } - reqWrapper.req?.destroy(); - reqWrapper.req = undefined; - }; - - reqWrapper.req = (parsedUrl.protocol === 'http:' ? http : https) - .request(requestOptions, requestCallback as (res: http.IncomingMessage) => void); - // Add no-op error listener to prevent this from throwing - reqWrapper.req.on('error', function() { - reqWrapper.req?.destroy(); - reqWrapper.req = undefined; - }); - reqWrapper.req.write(dataString); - reqWrapper.req.end(); - return reqWrapper.req; -}; - -export default { - dispatchEvent, -}; diff --git a/lib/plugins/event_dispatcher/no_op.ts b/lib/plugins/event_dispatcher/no_op.ts index 32353bae9..cbe2473d7 100644 --- a/lib/plugins/event_dispatcher/no_op.ts +++ b/lib/plugins/event_dispatcher/no_op.ts @@ -1,5 +1,5 @@ /** - * Copyright 2021, Optimizely + * Copyright 2021, 2024, Optimizely * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,8 +24,7 @@ import { Event } from '../../shared_types'; /* eslint-disable @typescript-eslint/no-unused-vars */ export const dispatchEvent = function( eventObj: Event, - callback: (response: { statusCode: number; }) => void -): void { +): any { // NoOp Event dispatcher. It does nothing really. } diff --git a/lib/plugins/event_dispatcher/send_beacon_dispatcher.ts b/lib/plugins/event_dispatcher/send_beacon_dispatcher.ts index 4cac7b7c3..3dabf0401 100644 --- a/lib/plugins/event_dispatcher/send_beacon_dispatcher.ts +++ b/lib/plugins/event_dispatcher/send_beacon_dispatcher.ts @@ -1,5 +1,5 @@ /** - * Copyright 2023, Optimizely + * 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. @@ -14,7 +14,7 @@ * limitations under the License. */ -import { EventDispatcher } from '../../modules/event_processor/eventDispatcher'; +import { EventDispatcher, EventDispatcherResponse } from '../../event_processor'; export type Event = { url: string; @@ -31,17 +31,17 @@ export type Event = { */ export const dispatchEvent = function( eventObj: Event, - callback: (response: { statusCode: number; }) => void -): void { +): Promise { const { params, url } = eventObj; const blob = new Blob([JSON.stringify(params)], { type: "application/json", }); const success = navigator.sendBeacon(url, blob); - callback({ - statusCode: success ? 200 : 500, - }); + if(success) { + return Promise.resolve({}); + } + return Promise.reject(new Error('sendBeacon failed')); } const eventDispatcher : EventDispatcher = { diff --git a/lib/plugins/event_processor/forwarding_event_processor.tests.js b/lib/plugins/event_processor/forwarding_event_processor.tests.js deleted file mode 100644 index 9a4670adc..000000000 --- a/lib/plugins/event_processor/forwarding_event_processor.tests.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Copyright 2021 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 sinon from 'sinon'; - import { createForwardingEventProcessor } from './forwarding_event_processor'; - import * as buildEventV1 from '../../core/event_builder/build_event_v1'; - - describe('lib/plugins/event_processor/forwarding_event_processor', function() { - var sandbox = sinon.sandbox.create(); - var ep; - var dispatcherSpy; - var sendNotificationsSpy; - - beforeEach(() => { - var dispatcher = { - dispatchEvent: () => {}, - }; - var notificationCenter = { - sendNotifications: () => {}, - } - dispatcherSpy = sandbox.spy(dispatcher, 'dispatchEvent'); - sendNotificationsSpy = sandbox.spy(notificationCenter, 'sendNotifications'); - sandbox.stub(buildEventV1, 'formatEvents').returns({ dummy: "event" }); - ep = createForwardingEventProcessor(dispatcher, notificationCenter); - ep.start(); - }); - - afterEach(() => { - sandbox.restore(); - }); - - it('should dispatch event immediately when process is called', () => { - ep.process({ dummy: 'event' }); - sinon.assert.calledWithExactly(dispatcherSpy, { dummy: 'event' }, sinon.match.func); - sinon.assert.calledOnce(sendNotificationsSpy); - }); - - it('should return a resolved promise when stop is called', (done) => { - ep.stop().then(done); - }); - }); diff --git a/lib/plugins/event_processor/index.react_native.ts b/lib/plugins/event_processor/index.react_native.ts index 98a826d25..9481987cb 100644 --- a/lib/plugins/event_processor/index.react_native.ts +++ b/lib/plugins/event_processor/index.react_native.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. @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '../../modules/event_processor/index.react_native'; +import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '../../event_processor/index.react_native'; export function createEventProcessor( ...args: ConstructorParameters diff --git a/lib/plugins/event_processor/index.ts b/lib/plugins/event_processor/index.ts index 70f30e23a..3fc0c3cad 100644 --- a/lib/plugins/event_processor/index.ts +++ b/lib/plugins/event_processor/index.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '../../modules/event_processor'; +import { LogTierV1EventProcessor, LocalStoragePendingEventsDispatcher } from '../../event_processor'; export function createEventProcessor( ...args: ConstructorParameters diff --git a/lib/shared_types.ts b/lib/shared_types.ts index 69c1080d3..8902820eb 100644 --- a/lib/shared_types.ts +++ b/lib/shared_types.ts @@ -20,7 +20,7 @@ */ import { ErrorHandler, LogHandler, LogLevel, LoggerFacade } from './modules/logging'; -import { EventProcessor } from './modules/event_processor'; +import { EventProcessor, EventDispatcher } from './event_processor'; import { NotificationCenter as NotificationCenterImpl } from './core/notification_center'; import { NOTIFICATION_TYPES } from './utils/enums'; @@ -40,6 +40,8 @@ import PersistentCache from './plugins/key_value_cache/persistentKeyValueCache'; import { ProjectConfig } from './project_config/project_config'; import { ProjectConfigManager } from './project_config/project_config_manager'; +export { EventDispatcher, EventProcessor } from './event_processor'; + export interface BucketerParams { experimentId: string; experimentKey: string; @@ -143,17 +145,6 @@ export interface Event { params: any; } -export interface EventDispatcher { - /** - * @param event - * Event being submitted for eventual dispatch. - * @param callback - * After the event has at least been queued for dispatch, call this function to return - * control back to the Client. - */ - dispatchEvent: (event: Event, callback: (response: { statusCode: number }) => void) => void; -} - export interface VariationVariable { id: string; value: string; @@ -291,7 +282,7 @@ export interface OptimizelyOptions { datafile?: string | object; datafileManager?: DatafileManager; errorHandler: ErrorHandler; - eventProcessor: EventProcessor; + eventProcessor?: EventProcessor; isValidInstance: boolean; jsonSchemaValidator?: { validate(jsonObject: unknown): boolean; @@ -400,9 +391,9 @@ export type PersistentCacheProvider = () => PersistentCache; * For compatibility with the previous declaration file */ export interface Config extends ConfigLite { - eventBatchSize?: number; // Maximum size of events to be dispatched in a batch - eventFlushInterval?: number; // Maximum time for an event to be enqueued - eventMaxQueueSize?: number; // Maximum size for the event queue + // eventBatchSize?: number; // Maximum size of events to be dispatched in a batch + // eventFlushInterval?: number; // Maximum time for an event to be enqueued + // eventMaxQueueSize?: number; // Maximum size for the event queue sdkKey?: string; odpOptions?: OdpOptions; persistentCacheProvider?: PersistentCacheProvider; @@ -416,8 +407,8 @@ export interface ConfigLite { projectConfigManager: ProjectConfigManager; // errorHandler object for logging error errorHandler?: ErrorHandler; - // event dispatcher function - eventDispatcher?: EventDispatcher; + // event processor + eventProcessor?: EventProcessor; // event dispatcher to use when closing closingEventDispatcher?: EventDispatcher; // The object to validate against the schema diff --git a/lib/tests/mock/mock_repeater.ts b/lib/tests/mock/mock_repeater.ts index 3f330f000..a93cbfa87 100644 --- a/lib/tests/mock/mock_repeater.ts +++ b/lib/tests/mock/mock_repeater.ts @@ -15,32 +15,8 @@ */ import { vi } from 'vitest'; - -import { Repeater } from '../../utils/repeater/repeater'; import { AsyncTransformer } from '../../utils/type'; -export class MockRepeater implements Repeater { - private handler?: AsyncTransformer; - - start(): void { - } - - stop(): void { - } - - reset(): void { - } - - setTask(handler: AsyncTransformer): void { - this.handler = handler; - } - - pushTick(failureCount: number): void { - this.handler?.(failureCount); - } -} - -//ignore ts no return type error // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export const getMockRepeater = () => { const mock = { diff --git a/lib/utils/enums/index.ts b/lib/utils/enums/index.ts index 962d06c30..78fd6f907 100644 --- a/lib/utils/enums/index.ts +++ b/lib/utils/enums/index.ts @@ -1,18 +1,18 @@ -/**************************************************************************** - * Copyright 2016-2024 Optimizely, Inc. and contributors * - * * - * Licensed under the Apache License, Version 2.0 (the "License"); * - * you may not use this file except in compliance with the License. * - * You may obtain a copy of the License at * - * * - * 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. * - ***************************************************************************/ +/** + * Copyright 2016-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. + */ /** * Contains global enums used throughout the library @@ -54,6 +54,7 @@ export const ERROR_MESSAGES = { MISSING_INTEGRATION_KEY: '%s: Integration key missing from datafile. All integrations should include a key.', NO_DATAFILE_SPECIFIED: '%s: No datafile specified. Cannot start optimizely.', NO_JSON_PROVIDED: '%s: No JSON object to validate against schema.', + NO_EVENT_PROCESSOR: 'No event processor is provided', NO_VARIATION_FOR_EXPERIMENT_KEY: '%s: No variation key %s defined in datafile for experiment %s.', ODP_CONFIG_NOT_AVAILABLE: '%s: ODP is not integrated to the project.', ODP_EVENT_FAILED: 'ODP event send failed.', diff --git a/lib/utils/event_tag_utils/index.ts b/lib/utils/event_tag_utils/index.ts index 7917f3baa..aa256ef1b 100644 --- a/lib/utils/event_tag_utils/index.ts +++ b/lib/utils/event_tag_utils/index.ts @@ -1,5 +1,5 @@ /** - * Copyright 2017, 2019-2020, 2022-2023, Optimizely + * Copyright 2017, 2019-2020, 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. @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventTags } from '../../modules/event_processor'; +import { EventTags } from '../../event_processor'; import { LoggerFacade } from '../../modules/logging'; import { diff --git a/package-lock.json b/package-lock.json index 2725c62e6..a1588ec80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4966,9 +4966,9 @@ } }, "node_modules/@sinonjs/text-encoding": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", - "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", + "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", "dev": true }, "node_modules/@socket.io/component-emitter": { diff --git a/tests/buildEventV1.spec.ts b/tests/buildEventV1.spec.ts index 7f8f56008..dafa67e60 100644 --- a/tests/buildEventV1.spec.ts +++ b/tests/buildEventV1.spec.ts @@ -19,8 +19,8 @@ import { buildConversionEventV1, buildImpressionEventV1, makeBatchedEventV1, -} from '../lib/modules/event_processor/v1/buildEventV1' -import { ImpressionEvent, ConversionEvent } from '../lib/modules/event_processor/events' +} from '../lib/event_processor/v1/buildEventV1' +import { ImpressionEvent, ConversionEvent } from '../lib/event_processor/events' describe('buildEventV1', () => { describe('buildImpressionEventV1', () => { diff --git a/tests/eventQueue.spec.ts b/tests/eventQueue.spec.ts index 0a9e5fae2..f794248dd 100644 --- a/tests/eventQueue.spec.ts +++ b/tests/eventQueue.spec.ts @@ -15,7 +15,7 @@ */ import { describe, beforeEach, afterEach, it, expect, vi } from 'vitest'; -import { DefaultEventQueue, SingleEventQueue } from '../lib/modules/event_processor/eventQueue' +import { DefaultEventQueue, SingleEventQueue } from '../lib/event_processor/eventQueue' describe('eventQueue', () => { beforeEach(() => { diff --git a/tests/index.react_native.spec.ts b/tests/index.react_native.spec.ts index 32408ee6f..6f076e614 100644 --- a/tests/index.react_native.spec.ts +++ b/tests/index.react_native.spec.ts @@ -53,7 +53,9 @@ describe('javascript-sdk/react-native', () => { describe('createInstance', () => { const fakeErrorHandler = { handleError: function() {} }; - const fakeEventDispatcher = { dispatchEvent: function() {} }; + const fakeEventDispatcher = { dispatchEvent: async function() { + return Promise.resolve({}); + } }; // @ts-ignore let silentLogger; @@ -84,7 +86,6 @@ describe('javascript-sdk/react-native', () => { const optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: getMockProjectConfigManager(), errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, // @ts-ignore logger: silentLogger, }); @@ -98,7 +99,6 @@ describe('javascript-sdk/react-native', () => { const optlyInstance = optimizelyFactory.createInstance({ projectConfigManager: getMockProjectConfigManager(), errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, // @ts-ignore logger: silentLogger, }); @@ -114,7 +114,6 @@ describe('javascript-sdk/react-native', () => { clientEngine: 'react-sdk', projectConfigManager: getMockProjectConfigManager(), errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, // @ts-ignore logger: silentLogger, }); @@ -166,182 +165,184 @@ describe('javascript-sdk/react-native', () => { }); }); - describe('event processor configuration', () => { - // @ts-ignore - let eventProcessorSpy; - beforeEach(() => { - eventProcessorSpy = vi.spyOn(eventProcessor, 'createEventProcessor'); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should use default event flush interval when none is provided', () => { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - // @ts-ignore - logger: silentLogger, - }); - - expect( - // @ts-ignore - eventProcessorSpy - ).toBeCalledWith( - expect.objectContaining({ - flushInterval: 1000, - }) - ); - }); - - describe('with an invalid flush interval', () => { - beforeEach(() => { - vi.spyOn(eventProcessorConfigValidator, 'validateEventFlushInterval').mockImplementation(() => false); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should ignore the event flush interval and use the default instead', () => { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - // @ts-ignore - logger: silentLogger, - // @ts-ignore - eventFlushInterval: ['invalid', 'flush', 'interval'], - }); - expect( - // @ts-ignore - eventProcessorSpy - ).toBeCalledWith( - expect.objectContaining({ - flushInterval: 1000, - }) - ); - }); - }); - - describe('with a valid flush interval', () => { - beforeEach(() => { - vi.spyOn(eventProcessorConfigValidator, 'validateEventFlushInterval').mockImplementation(() => true); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should use the provided event flush interval', () => { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - // @ts-ignore - logger: silentLogger, - eventFlushInterval: 9000, - }); - expect( - // @ts-ignore - eventProcessorSpy - ).toBeCalledWith( - expect.objectContaining({ - flushInterval: 9000, - }) - ); - }); - }); - - it('should use default event batch size when none is provided', () => { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - // @ts-ignore - logger: silentLogger, - }); - expect( - // @ts-ignore - eventProcessorSpy - ).toBeCalledWith( - expect.objectContaining({ - batchSize: 10, - }) - ); - }); - - describe('with an invalid event batch size', () => { - beforeEach(() => { - vi.spyOn(eventProcessorConfigValidator, 'validateEventBatchSize').mockImplementation(() => false); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should ignore the event batch size and use the default instead', () => { - optimizelyFactory.createInstance({ - datafile: testData.getTestProjectConfigWithFeatures(), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - // @ts-ignore - logger: silentLogger, - // @ts-ignore - eventBatchSize: null, - }); - expect( - // @ts-ignore - eventProcessorSpy - ).toBeCalledWith( - expect.objectContaining({ - batchSize: 10, - }) - ); - }); - }); - - describe('with a valid event batch size', () => { - beforeEach(() => { - vi.spyOn(eventProcessorConfigValidator, 'validateEventBatchSize').mockImplementation(() => true); - }); - - afterEach(() => { - vi.resetAllMocks(); - }); - - it('should use the provided event batch size', () => { - optimizelyFactory.createInstance({ - projectConfigManager: getMockProjectConfigManager({ - initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), - }), - errorHandler: fakeErrorHandler, - eventDispatcher: fakeEventDispatcher, - // @ts-ignore - logger: silentLogger, - eventBatchSize: 300, - }); - expect( - // @ts-ignore - eventProcessorSpy - ).toBeCalledWith( - expect.objectContaining({ - batchSize: 300, - }) - ); - }); - }); - }); + // TODO: user will create and inject an event processor + // these tests will be refactored accordingly + // describe('event processor configuration', () => { + // // @ts-ignore + // let eventProcessorSpy; + // beforeEach(() => { + // eventProcessorSpy = vi.spyOn(eventProcessor, 'createEventProcessor'); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should use default event flush interval when none is provided', () => { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // // @ts-ignore + // logger: silentLogger, + // }); + + // expect( + // // @ts-ignore + // eventProcessorSpy + // ).toBeCalledWith( + // expect.objectContaining({ + // flushInterval: 1000, + // }) + // ); + // }); + + // describe('with an invalid flush interval', () => { + // beforeEach(() => { + // vi.spyOn(eventProcessorConfigValidator, 'validateEventFlushInterval').mockImplementation(() => false); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should ignore the event flush interval and use the default instead', () => { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // // @ts-ignore + // logger: silentLogger, + // // @ts-ignore + // eventFlushInterval: ['invalid', 'flush', 'interval'], + // }); + // expect( + // // @ts-ignore + // eventProcessorSpy + // ).toBeCalledWith( + // expect.objectContaining({ + // flushInterval: 1000, + // }) + // ); + // }); + // }); + + // describe('with a valid flush interval', () => { + // beforeEach(() => { + // vi.spyOn(eventProcessorConfigValidator, 'validateEventFlushInterval').mockImplementation(() => true); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should use the provided event flush interval', () => { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // // @ts-ignore + // logger: silentLogger, + // eventFlushInterval: 9000, + // }); + // expect( + // // @ts-ignore + // eventProcessorSpy + // ).toBeCalledWith( + // expect.objectContaining({ + // flushInterval: 9000, + // }) + // ); + // }); + // }); + + // it('should use default event batch size when none is provided', () => { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // // @ts-ignore + // logger: silentLogger, + // }); + // expect( + // // @ts-ignore + // eventProcessorSpy + // ).toBeCalledWith( + // expect.objectContaining({ + // batchSize: 10, + // }) + // ); + // }); + + // describe('with an invalid event batch size', () => { + // beforeEach(() => { + // vi.spyOn(eventProcessorConfigValidator, 'validateEventBatchSize').mockImplementation(() => false); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should ignore the event batch size and use the default instead', () => { + // optimizelyFactory.createInstance({ + // datafile: testData.getTestProjectConfigWithFeatures(), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // // @ts-ignore + // logger: silentLogger, + // // @ts-ignore + // eventBatchSize: null, + // }); + // expect( + // // @ts-ignore + // eventProcessorSpy + // ).toBeCalledWith( + // expect.objectContaining({ + // batchSize: 10, + // }) + // ); + // }); + // }); + + // describe('with a valid event batch size', () => { + // beforeEach(() => { + // vi.spyOn(eventProcessorConfigValidator, 'validateEventBatchSize').mockImplementation(() => true); + // }); + + // afterEach(() => { + // vi.resetAllMocks(); + // }); + + // it('should use the provided event batch size', () => { + // optimizelyFactory.createInstance({ + // projectConfigManager: getMockProjectConfigManager({ + // initConfig: createProjectConfig(testData.getTestProjectConfigWithFeatures()), + // }), + // errorHandler: fakeErrorHandler, + // eventDispatcher: fakeEventDispatcher, + // // @ts-ignore + // logger: silentLogger, + // eventBatchSize: 300, + // }); + // expect( + // // @ts-ignore + // eventProcessorSpy + // ).toBeCalledWith( + // expect.objectContaining({ + // batchSize: 300, + // }) + // ); + // }); + // }); + // }); }); }); }); diff --git a/tests/pendingEventsDispatcher.spec.ts b/tests/pendingEventsDispatcher.spec.ts index 153edae5e..d39b58e22 100644 --- a/tests/pendingEventsDispatcher.spec.ts +++ b/tests/pendingEventsDispatcher.spec.ts @@ -29,20 +29,28 @@ import { LocalStoragePendingEventsDispatcher, PendingEventsDispatcher, DispatcherEntry, -} from '../lib/modules/event_processor/pendingEventsDispatcher' -import { EventDispatcher, EventV1Request } from '../lib/modules/event_processor/eventDispatcher' -import { EventV1 } from '../lib/modules/event_processor/v1/buildEventV1' -import { PendingEventsStore, LocalStorageStore } from '../lib/modules/event_processor/pendingEventsStore' +} from '../lib/event_processor/pendingEventsDispatcher' +import { EventDispatcher, EventDispatcherResponse, EventV1Request } from '../lib/event_processor/eventDispatcher' +import { EventV1 } from '../lib/event_processor/v1/buildEventV1' +import { PendingEventsStore, LocalStorageStore } from '../lib/event_processor/pendingEventsStore' import { uuid, getTimestamp } from '../lib/utils/fns' +import { resolvablePromise, ResolvablePromise } from '../lib/utils/promise/resolvablePromise'; describe('LocalStoragePendingEventsDispatcher', () => { let originalEventDispatcher: EventDispatcher let pendingEventsDispatcher: PendingEventsDispatcher + let eventDispatcherResponses: Array> beforeEach(() => { + eventDispatcherResponses = []; originalEventDispatcher = { - dispatchEvent: vi.fn(), + dispatchEvent: vi.fn().mockImplementation(() => { + const response = resolvablePromise() + eventDispatcherResponses.push(response) + return response.promise + }), } + pendingEventsDispatcher = new LocalStoragePendingEventsDispatcher({ eventDispatcher: originalEventDispatcher, }) @@ -54,54 +62,44 @@ describe('LocalStoragePendingEventsDispatcher', () => { localStorage.clear() }) - it('should properly send the events to the passed in eventDispatcher, when callback statusCode=200', () => { - const callback = vi.fn() + it('should properly send the events to the passed in eventDispatcher, when callback statusCode=200', async () => { const eventV1Request: EventV1Request = { url: 'http://cdn.com', httpVerb: 'POST', params: ({ id: 'event' } as unknown) as EventV1, } - pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + pendingEventsDispatcher.dispatchEvent(eventV1Request) + + eventDispatcherResponses[0].resolve({ statusCode: 200 }) - expect(callback).not.toHaveBeenCalled() - // manually invoke original eventDispatcher callback const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as MockInstance) .mock.calls[0] - internalDispatchCall[1]({ statusCode: 200 }) - - // assert that the original dispatch function was called with the request + + // assert that the original dispatch function was called with the request expect((originalEventDispatcher.dispatchEvent as unknown) as MockInstance).toBeCalledTimes(1) expect(internalDispatchCall[0]).toEqual(eventV1Request) - - // assert that the passed in callback to pendingEventsDispatcher was called - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith({ statusCode: 200 }) }) it('should properly send the events to the passed in eventDispatcher, when callback statusCode=400', () => { - const callback = vi.fn() const eventV1Request: EventV1Request = { url: 'http://cdn.com', httpVerb: 'POST', params: ({ id: 'event' } as unknown) as EventV1, } - pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + pendingEventsDispatcher.dispatchEvent(eventV1Request) + + eventDispatcherResponses[0].resolve({ statusCode: 400 }) - expect(callback).not.toHaveBeenCalled() - // manually invoke original eventDispatcher callback const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as MockInstance) .mock.calls[0] - internalDispatchCall[1]({ statusCode: 400 }) + + eventDispatcherResponses[0].resolve({ statusCode: 400 }) // assert that the original dispatch function was called with the request expect((originalEventDispatcher.dispatchEvent as unknown) as MockInstance).toBeCalledTimes(1) expect(internalDispatchCall[0]).toEqual(eventV1Request) - - // assert that the passed in callback to pendingEventsDispatcher was called - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith({ statusCode: 400}) }) }) @@ -109,11 +107,19 @@ describe('PendingEventsDispatcher', () => { let originalEventDispatcher: EventDispatcher let pendingEventsDispatcher: PendingEventsDispatcher let store: PendingEventsStore + let eventDispatcherResponses: Array> beforeEach(() => { + eventDispatcherResponses = []; + originalEventDispatcher = { - dispatchEvent: vi.fn(), + dispatchEvent: vi.fn().mockImplementation(() => { + const response = resolvablePromise() + eventDispatcherResponses.push(response) + return response.promise + }), } + store = new LocalStorageStore({ key: 'test', maxValues: 3, @@ -132,15 +138,14 @@ describe('PendingEventsDispatcher', () => { describe('dispatch', () => { describe('when the dispatch is successful', () => { - it('should save the pendingEvent to the store and remove it once dispatch is completed', () => { - const callback = vi.fn() + it('should save the pendingEvent to the store and remove it once dispatch is completed', async () => { const eventV1Request: EventV1Request = { url: 'http://cdn.com', httpVerb: 'POST', params: ({ id: 'event' } as unknown) as EventV1, } - pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + pendingEventsDispatcher.dispatchEvent(eventV1Request) expect(store.values()).toHaveLength(1) expect(store.get('uuid')).toEqual({ @@ -148,12 +153,12 @@ describe('PendingEventsDispatcher', () => { timestamp: 1, request: eventV1Request, }) - expect(callback).not.toHaveBeenCalled() - // manually invoke original eventDispatcher callback + eventDispatcherResponses[0].resolve({ statusCode: 200 }) + await eventDispatcherResponses[0].promise + const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as MockInstance) .mock.calls[0] - const internalCallback = internalDispatchCall[1]({ statusCode: 200 }) // assert that the original dispatch function was called with the request expect( @@ -161,24 +166,19 @@ describe('PendingEventsDispatcher', () => { ).toBeCalledTimes(1) expect(internalDispatchCall[0]).toEqual(eventV1Request) - // assert that the passed in callback to pendingEventsDispatcher was called - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith({ statusCode: 200 }) - expect(store.values()).toHaveLength(0) }) }) describe('when the dispatch is unsuccessful', () => { - it('should save the pendingEvent to the store and remove it once dispatch is completed', () => { - const callback = vi.fn() + it('should save the pendingEvent to the store and remove it once dispatch is completed', async () => { const eventV1Request: EventV1Request = { url: 'http://cdn.com', httpVerb: 'POST', params: ({ id: 'event' } as unknown) as EventV1, } - pendingEventsDispatcher.dispatchEvent(eventV1Request, callback) + pendingEventsDispatcher.dispatchEvent(eventV1Request) expect(store.values()).toHaveLength(1) expect(store.get('uuid')).toEqual({ @@ -186,23 +186,20 @@ describe('PendingEventsDispatcher', () => { timestamp: 1, request: eventV1Request, }) - expect(callback).not.toHaveBeenCalled() + + eventDispatcherResponses[0].resolve({ statusCode: 400 }) + await eventDispatcherResponses[0].promise // manually invoke original eventDispatcher callback const internalDispatchCall = ((originalEventDispatcher.dispatchEvent as unknown) as MockInstance) .mock.calls[0] - internalDispatchCall[1]({ statusCode: 400 }) - + // assert that the original dispatch function was called with the request expect( (originalEventDispatcher.dispatchEvent as unknown) as MockInstance, ).toBeCalledTimes(1) expect(internalDispatchCall[0]).toEqual(eventV1Request) - // assert that the passed in callback to pendingEventsDispatcher was called - expect(callback).toHaveBeenCalledTimes(1) - expect(callback).toHaveBeenCalledWith({ statusCode: 400 }) - expect(store.values()).toHaveLength(0) }) }) @@ -219,10 +216,9 @@ describe('PendingEventsDispatcher', () => { }) describe('when there are multiple pending events in the store', () => { - it('should dispatch all of the pending events, and remove them from store', () => { + it('should dispatch all of the pending events, and remove them from store', async () => { expect(store.values()).toHaveLength(0) - const callback = vi.fn() const eventV1Request1: EventV1Request = { url: 'http://cdn.com', httpVerb: 'POST', @@ -251,12 +247,9 @@ describe('PendingEventsDispatcher', () => { pendingEventsDispatcher.sendPendingEvents() expect(originalEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2) - // manually invoke original eventDispatcher callback - const internalDispatchCalls = ((originalEventDispatcher.dispatchEvent as unknown) as MockInstance) - .mock.calls - internalDispatchCalls[0][1]({ statusCode: 200 }) - internalDispatchCalls[1][1]({ statusCode: 200 }) - + eventDispatcherResponses[0].resolve({ statusCode: 200 }) + eventDispatcherResponses[1].resolve({ statusCode: 200 }) + await Promise.all([eventDispatcherResponses[0].promise, eventDispatcherResponses[1].promise]) expect(store.values()).toHaveLength(0) }) }) diff --git a/tests/pendingEventsStore.spec.ts b/tests/pendingEventsStore.spec.ts index 9a3fff864..9c255b118 100644 --- a/tests/pendingEventsStore.spec.ts +++ b/tests/pendingEventsStore.spec.ts @@ -15,7 +15,7 @@ */ import { describe, beforeEach, afterEach, it, expect, vi, MockInstance } from 'vitest'; -import { LocalStorageStore } from '../lib/modules/event_processor/pendingEventsStore' +import { LocalStorageStore } from '../lib/event_processor/pendingEventsStore' type TestEntry = { uuid: string diff --git a/tests/reactNativeEventsStore.spec.ts b/tests/reactNativeEventsStore.spec.ts index 0c211309f..d7155a629 100644 --- a/tests/reactNativeEventsStore.spec.ts +++ b/tests/reactNativeEventsStore.spec.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. @@ -54,8 +54,7 @@ vi.mock('../lib/plugins/key_value_cache/reactNativeAsyncStorageCache', () => { import ReactNativeAsyncStorageCache from '../lib/plugins/key_value_cache/reactNativeAsyncStorageCache'; -import { ReactNativeEventsStore } from '../lib/modules/event_processor/reactNativeEventsStore' -import PersistentKeyValueCache from '../lib/plugins/key_value_cache/persistentKeyValueCache' +import { ReactNativeEventsStore } from '../lib/event_processor/reactNativeEventsStore' const STORE_KEY = 'test-store' diff --git a/tests/reactNativeV1EventProcessor.spec.ts b/tests/reactNativeV1EventProcessor.spec.ts index a7bf8a8f5..995dd6024 100644 --- a/tests/reactNativeV1EventProcessor.spec.ts +++ b/tests/reactNativeV1EventProcessor.spec.ts @@ -17,11 +17,11 @@ import { describe, beforeEach, it, vi, expect } from 'vitest'; vi.mock('@react-native-community/netinfo'); -vi.mock('../lib/modules/event_processor/reactNativeEventsStore'); +vi.mock('../lib/event_processor/reactNativeEventsStore'); -import { ReactNativeEventsStore } from '../lib/modules/event_processor/reactNativeEventsStore'; +import { ReactNativeEventsStore } from '../lib/event_processor/reactNativeEventsStore'; import PersistentKeyValueCache from '../lib/plugins/key_value_cache/persistentKeyValueCache'; -import { LogTierV1EventProcessor } from '../lib/modules/event_processor/index.react_native'; +import { LogTierV1EventProcessor } from '../lib/event_processor/index.react_native'; import { PersistentCacheProvider } from '../lib/shared_types'; describe('LogTierV1EventProcessor', () => { @@ -58,7 +58,7 @@ describe('LogTierV1EventProcessor', () => { const noop = () => {}; new LogTierV1EventProcessor({ - dispatcher: { dispatchEvent: () => {} }, + dispatcher: { dispatchEvent: () => Promise.resolve({}) }, persistentCacheProvider: fakePersistentCacheProvider, }) diff --git a/tests/requestTracker.spec.ts b/tests/requestTracker.spec.ts index 37245835c..10c042a66 100644 --- a/tests/requestTracker.spec.ts +++ b/tests/requestTracker.spec.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. @@ -15,7 +15,7 @@ */ import { describe, it, expect } from 'vitest'; -import RequestTracker from '../lib/modules/event_processor/requestTracker' +import RequestTracker from '../lib/event_processor/requestTracker' describe('requestTracker', () => { describe('onRequestsComplete', () => { diff --git a/tests/sendBeaconDispatcher.spec.ts b/tests/sendBeaconDispatcher.spec.ts index 2b67268d3..3b69ffc27 100644 --- a/tests/sendBeaconDispatcher.spec.ts +++ b/tests/sendBeaconDispatcher.spec.ts @@ -13,16 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { describe, afterEach, it, expect } from 'vitest'; +import { describe, beforeEach, it, expect, vi, MockInstance } from 'vitest'; import sendBeaconDispatcher, { Event } from '../lib/plugins/event_dispatcher/send_beacon_dispatcher'; -import { anyString, anything, capture, instance, mock, reset, when } from 'ts-mockito'; describe('dispatchEvent', function() { - const mockNavigator = mock(); + let sendBeaconSpy: MockInstance; - afterEach(function() { - reset(mockNavigator); + beforeEach(() => { + sendBeaconSpy = vi.fn(); + navigator.sendBeacon = sendBeaconSpy as any; }); it('should call sendBeacon with correct url, data and type', async () => { @@ -33,13 +33,11 @@ describe('dispatchEvent', function() { params: eventParams, }; - when(mockNavigator.sendBeacon(anyString(), anything())).thenReturn(true); - const navigator = instance(mockNavigator); - global.navigator.sendBeacon = navigator.sendBeacon; + sendBeaconSpy.mockReturnValue(true); - sendBeaconDispatcher.dispatchEvent(eventObj, () => {}); + sendBeaconDispatcher.dispatchEvent(eventObj) - const [url, data] = capture(mockNavigator.sendBeacon).last(); + const [url, data] = sendBeaconSpy.mock.calls[0]; const blob = data as Blob; const reader = new FileReader(); @@ -57,51 +55,27 @@ describe('dispatchEvent', function() { expect(sentParams).toEqual(JSON.stringify(eventObj.params)); }); - it('should call call callback with status 200 on sendBeacon success', () => - new Promise((pass, fail) => { - var eventParams = { testParam: 'testParamValue' }; - var eventObj: Event = { - url: 'https://cdn.com/event', - httpVerb: 'POST', - params: eventParams, - }; - - when(mockNavigator.sendBeacon(anyString(), anything())).thenReturn(true); - const navigator = instance(mockNavigator); - global.navigator.sendBeacon = navigator.sendBeacon; - - sendBeaconDispatcher.dispatchEvent(eventObj, (res) => { - try { - expect(res.statusCode).toEqual(200); - pass(); - } catch(err) { - fail(err); - } - }); - }) - ); + it('should resolve the response on sendBeacon success', async () => { + const eventParams = { testParam: 'testParamValue' }; + const eventObj: Event = { + url: 'https://cdn.com/event', + httpVerb: 'POST', + params: eventParams, + }; - it('should call call callback with status 200 on sendBeacon failure', () => - new Promise((pass, fail) => { - var eventParams = { testParam: 'testParamValue' }; - var eventObj: Event = { - url: 'https://cdn.com/event', - httpVerb: 'POST', - params: eventParams, - }; - - when(mockNavigator.sendBeacon(anyString(), anything())).thenReturn(false); - const navigator = instance(mockNavigator); - global.navigator.sendBeacon = navigator.sendBeacon; - - sendBeaconDispatcher.dispatchEvent(eventObj, (res) => { - try { - expect(res.statusCode).toEqual(500); - pass(); - } catch(err) { - fail(err); - } - }); - }) - ); + sendBeaconSpy.mockReturnValue(true); + await expect(sendBeaconDispatcher.dispatchEvent(eventObj)).resolves.not.toThrow(); + }); + + it('should reject the response on sendBeacon success', async () => { + const eventParams = { testParam: 'testParamValue' }; + const eventObj: Event = { + url: 'https://cdn.com/event', + httpVerb: 'POST', + params: eventParams, + }; + + sendBeaconSpy.mockReturnValue(false); + await expect(sendBeaconDispatcher.dispatchEvent(eventObj)).rejects.toThrow(); + }); }); diff --git a/tests/v1EventProcessor.react_native.spec.ts b/tests/v1EventProcessor.react_native.spec.ts index 7722ef30c..d0fccc4b0 100644 --- a/tests/v1EventProcessor.react_native.spec.ts +++ b/tests/v1EventProcessor.react_native.spec.ts @@ -21,17 +21,18 @@ vi.mock('@react-native-async-storage/async-storage'); import { NotificationSender } from '../lib/core/notification_center' import { NOTIFICATION_TYPES } from '../lib/utils/enums' -import { LogTierV1EventProcessor } from '../lib/modules/event_processor/v1/v1EventProcessor.react_native' +import { LogTierV1EventProcessor } from '../lib/event_processor/v1/v1EventProcessor.react_native' import { EventDispatcher, EventV1Request, - EventDispatcherCallback, -} from '../lib/modules/event_processor/eventDispatcher' -import { EventProcessor, ProcessableEvent } from '../lib/modules/event_processor/eventProcessor' -import { buildImpressionEventV1, makeBatchedEventV1 } from '../lib/modules/event_processor/v1/buildEventV1' + EventDispatcherResponse, +} from '../lib/event_processor/eventDispatcher' +import { EventProcessor, ProcessableEvent } from '../lib/event_processor/eventProcessor' +import { buildImpressionEventV1, makeBatchedEventV1 } from '../lib/event_processor/v1/buildEventV1' import AsyncStorage from '../__mocks__/@react-native-async-storage/async-storage' import { triggerInternetState } from '../__mocks__/@react-native-community/netinfo' -import { DefaultEventQueue } from '../lib/modules/event_processor/eventQueue' +import { DefaultEventQueue } from '../lib/event_processor/eventQueue' +import { resolvablePromise, ResolvablePromise } from '../lib/utils/promise/resolvablePromise'; function createImpressionEvent() { return { @@ -118,13 +119,10 @@ describe('LogTierV1EventProcessorReactNative', () => { let dispatchStub: Mock beforeEach(() => { - dispatchStub = vi.fn() + dispatchStub = vi.fn().mockResolvedValue({ statusCode: 200 }) stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { - dispatchStub(event) - callback({ statusCode: 200 }) - }, + dispatchEvent: dispatchStub, } }) @@ -134,12 +132,13 @@ describe('LogTierV1EventProcessorReactNative', () => { }) describe('stop()', () => { - let localCallback: EventDispatcherCallback + let resolvableResponse: ResolvablePromise beforeEach(async () => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) - localCallback = callback + resolvableResponse = resolvablePromise() + return resolvableResponse.promise }, } }) @@ -167,18 +166,19 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(impressionEvent) await new Promise(resolve => setTimeout(resolve, 150)) - // @ts-ignore - localCallback({ statusCode: 200 }) + + resolvableResponse.resolve({ statusCode: 200 }) }) it('should return a promise that is resolved when the dispatcher callback returns a 400 response', async () => { // This test is saying that even if the request fails to send but // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved - let localCallback: any + let responsePromise: ResolvablePromise stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request): Promise { dispatchStub(event) - localCallback = callback + responsePromise = resolvablePromise() + return responsePromise.promise; }, } @@ -194,14 +194,14 @@ describe('LogTierV1EventProcessorReactNative', () => { await new Promise(resolve => setTimeout(resolve, 150)) - localCallback({ statusCode: 400 }) + resolvableResponse.resolve({ statusCode: 400 }) }) it('should return a promise when multiple event batches are sent', async () => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } @@ -226,8 +226,10 @@ describe('LogTierV1EventProcessorReactNative', () => { it('should stop accepting events after stop is called', async () => { const dispatcher = { - dispatchEvent: vi.fn((event: EventV1Request, callback: EventDispatcherCallback) => { - setTimeout(() => callback({ statusCode: 204 }), 0) + dispatchEvent: vi.fn((event: EventV1Request) => { + return new Promise(resolve => { + setTimeout(() => resolve({ statusCode: 204 }), 0) + }) }) } const processor = new LogTierV1EventProcessor({ @@ -405,13 +407,17 @@ describe('LogTierV1EventProcessorReactNative', () => { processor.process(createImpressionEvent()) // flushing should reset queue, at this point only has two events expect(dispatchStub).toHaveBeenCalledTimes(1) + + // clear the async storate cache to ensure next tests + // works correctly + await new Promise(resolve => setTimeout(resolve, 400)) }) }) describe('when a notification center is provided', () => { it('should trigger a notification when the event dispatcher dispatches an event', async () => { const dispatcher: EventDispatcher = { - dispatchEvent: vi.fn() + dispatchEvent: vi.fn().mockResolvedValue({ statusCode: 200 }) } const notificationCenter: NotificationSender = { @@ -425,8 +431,8 @@ describe('LogTierV1EventProcessorReactNative', () => { }) await processor.start() - const impressionEvent1 = createImpressionEvent() - processor.process(impressionEvent1) + const impressionEvent = createImpressionEvent() + processor.process(impressionEvent) await new Promise(resolve => setTimeout(resolve, 150)) expect(notificationCenter.sendNotifications).toBeCalledTimes(1) @@ -486,9 +492,9 @@ describe('LogTierV1EventProcessorReactNative', () => { let receivedEvents: EventV1Request[] = [] stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) - callback({ statusCode: 400 }) + return Promise.resolve({ statusCode: 400 }) }, } @@ -523,10 +529,10 @@ describe('LogTierV1EventProcessorReactNative', () => { receivedEvents = [] stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { receivedEvents.push(event) dispatchStub(event) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } @@ -549,9 +555,9 @@ describe('LogTierV1EventProcessorReactNative', () => { it('should process all the events left in buffer when the app closed last time', async () => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } @@ -579,10 +585,10 @@ describe('LogTierV1EventProcessorReactNative', () => { let receivedEvents: EventV1Request[] = [] stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { receivedEvents.push(event) dispatchStub(event) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } @@ -608,9 +614,9 @@ describe('LogTierV1EventProcessorReactNative', () => { it('should dispatch pending events first and then process events in buffer store', async () => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) - callback({ statusCode: 400 }) + return Promise.resolve({ statusCode: 400 }) }, } @@ -639,10 +645,10 @@ describe('LogTierV1EventProcessorReactNative', () => { const visitorIds: string[] = [] stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) event.params.visitors.forEach(visitor => visitorIds.push(visitor.visitor_id)) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } @@ -667,14 +673,14 @@ describe('LogTierV1EventProcessorReactNative', () => { let receivedVisitorIds: string[] = [] let dispatchCount = 0 stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) dispatchCount++ if (dispatchCount > 4) { event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) } else { - callback({ statusCode: 400 }) + return Promise.resolve({ statusCode: 400 }) } }, } @@ -722,11 +728,11 @@ describe('LogTierV1EventProcessorReactNative', () => { let receivedVisitorIds: string[] = [] let dispatchCount = 0 stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) dispatchCount++ event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) - callback({ statusCode: 400 }) + return Promise.resolve({ statusCode: 400 }) }, } @@ -775,14 +781,14 @@ describe('LogTierV1EventProcessorReactNative', () => { let receivedVisitorIds: string[] = [] let dispatchCount = 0 stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) dispatchCount++ if (dispatchCount > 4) { event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) } else { - callback({ statusCode: 400 }) + return Promise.resolve({ statusCode: 400 }) } }, } @@ -827,14 +833,14 @@ describe('LogTierV1EventProcessorReactNative', () => { let receivedVisitorIds: string[] = [] let dispatchCount = 0 stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request) { dispatchStub(event) dispatchCount++ if (dispatchCount > 4) { event.params.visitors.forEach(visitor => receivedVisitorIds.push(visitor.visitor_id)) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) } else { - callback({ statusCode: 400 }) + return Promise.resolve({ statusCode: 400 }) } }, } diff --git a/tests/v1EventProcessor.spec.ts b/tests/v1EventProcessor.spec.ts index 0649bad72..bd7333bee 100644 --- a/tests/v1EventProcessor.spec.ts +++ b/tests/v1EventProcessor.spec.ts @@ -15,16 +15,17 @@ */ import { describe, beforeEach, afterEach, it, vi, expect, Mock } from 'vitest'; -import { LogTierV1EventProcessor } from '../lib/modules/event_processor/v1/v1EventProcessor' +import { LogTierV1EventProcessor } from '../lib/event_processor/v1/v1EventProcessor' import { EventDispatcher, EventV1Request, - EventDispatcherCallback, -} from '../lib/modules/event_processor/eventDispatcher' -import { EventProcessor } from '../lib/modules/event_processor/eventProcessor' -import { buildImpressionEventV1, makeBatchedEventV1 } from '../lib/modules/event_processor/v1/buildEventV1' + EventDispatcherResponse, +} from '../lib/event_processor/eventDispatcher' +import { EventProcessor } from '../lib/event_processor/eventProcessor' +import { buildImpressionEventV1, makeBatchedEventV1 } from '../lib/event_processor/v1/buildEventV1' import { NotificationCenter, NotificationSender } from '../lib/core/notification_center' import { NOTIFICATION_TYPES } from '../lib/utils/enums' +import { resolvablePromise, ResolvablePromise } from '../lib/utils/promise/resolvablePromise'; function createImpressionEvent() { return { @@ -118,9 +119,9 @@ describe('LogTierV1EventProcessor', () => { dispatchStub = vi.fn() stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request): Promise { dispatchStub(event) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } }) @@ -130,12 +131,19 @@ describe('LogTierV1EventProcessor', () => { }) describe('stop()', () => { - let localCallback: EventDispatcherCallback + let resposePromise: ResolvablePromise beforeEach(() => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request): Promise { dispatchStub(event) - localCallback = callback + return Promise.resolve({ statusCode: 200 }) + }, + } + stubDispatcher = { + dispatchEvent(event: EventV1Request): Promise { + dispatchStub(event) + resposePromise = resolvablePromise() + return resposePromise.promise }, } }) @@ -170,7 +178,7 @@ describe('LogTierV1EventProcessor', () => { done() }) - localCallback({ statusCode: 200 }) + resposePromise.resolve({ statusCode: 200 }) }) ) @@ -178,11 +186,11 @@ describe('LogTierV1EventProcessor', () => { new Promise((done) => { // This test is saying that even if the request fails to send but // the `dispatcher` yielded control back, then the `.stop()` promise should be resolved - let localCallback: any stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request): Promise { dispatchStub(event) - localCallback = callback + resposePromise = resolvablePromise() + return Promise.resolve({statusCode: 400}) }, } @@ -199,19 +207,15 @@ describe('LogTierV1EventProcessor', () => { processor.stop().then(() => { done() }) - - localCallback({ - statusCode: 400, - }) }) ) it('should return a promise when multiple event batches are sent', () => new Promise((done) => { stubDispatcher = { - dispatchEvent(event: EventV1Request, callback: EventDispatcherCallback): void { + dispatchEvent(event: EventV1Request): Promise { dispatchStub(event) - callback({ statusCode: 200 }) + return Promise.resolve({ statusCode: 200 }) }, } @@ -237,8 +241,10 @@ describe('LogTierV1EventProcessor', () => { it('should stop accepting events after stop is called', () => { const dispatcher = { - dispatchEvent: vi.fn((event: EventV1Request, callback: EventDispatcherCallback) => { - setTimeout(() => callback({ statusCode: 204 }), 0) + dispatchEvent: vi.fn((event: EventV1Request) => { + return new Promise((resolve) => { + setTimeout(() => resolve({ statusCode: 204 }), 0) + }) }) } const processor = new LogTierV1EventProcessor({ @@ -271,10 +277,12 @@ describe('LogTierV1EventProcessor', () => { }) it('should resolve the stop promise after all dispatcher requests are done', async () => { - const dispatchCbs: Array = [] + const dispatchPromises: Array> = [] const dispatcher = { - dispatchEvent: vi.fn((event: EventV1Request, callback: EventDispatcherCallback) => { - dispatchCbs.push(callback) + dispatchEvent: vi.fn((event: EventV1Request) => { + const response = resolvablePromise(); + dispatchPromises.push(response); + return response.promise; }) } @@ -288,7 +296,7 @@ describe('LogTierV1EventProcessor', () => { for (let i = 0; i < 4; i++) { processor.process(createImpressionEvent()) } - expect(dispatchCbs.length).toBe(2) + expect(dispatchPromises.length).toBe(2) let stopPromiseResolved = false const stopPromise = processor.stop().then(() => { @@ -296,10 +304,10 @@ describe('LogTierV1EventProcessor', () => { }) expect(stopPromiseResolved).toBe(false) - dispatchCbs[0]({ statusCode: 204 }) + dispatchPromises[0].resolve({ statusCode: 204 }) vi.advanceTimersByTime(100) expect(stopPromiseResolved).toBe(false) - dispatchCbs[1]({ statusCode: 204 }) + dispatchPromises[1].resolve({ statusCode: 204 }) await stopPromise expect(stopPromiseResolved).toBe(true) }) @@ -500,9 +508,9 @@ describe('LogTierV1EventProcessor', () => { }) describe('when a notification center is provided', () => { - it('should trigger a notification when the event dispatcher dispatches an event', () => { + it('should trigger a notification when the event dispatcher dispatches an event', async () => { const dispatcher: EventDispatcher = { - dispatchEvent: vi.fn() + dispatchEvent: vi.fn().mockResolvedValue({ statusCode: 200 }) } const notificationCenter: NotificationSender = { @@ -514,7 +522,7 @@ describe('LogTierV1EventProcessor', () => { notificationCenter, batchSize: 1, }) - processor.start() + await processor.start() const impressionEvent1 = createImpressionEvent() processor.process(impressionEvent1)