diff --git a/lib/client_factory.ts b/lib/client_factory.ts index 7307075cf..87a239246 100644 --- a/lib/client_factory.ts +++ b/lib/client_factory.ts @@ -31,7 +31,7 @@ export type OptimizelyFactoryConfig = Config & { requestHandler: RequestHandler; } -export const getOptimizelyInstance = (config: OptimizelyFactoryConfig): Client => { +export const getOptimizelyInstance = (config: OptimizelyFactoryConfig): Optimizely => { const { clientEngine, clientVersion, diff --git a/lib/event_processor/batch_event_processor.spec.ts b/lib/event_processor/batch_event_processor.spec.ts index a95dd262f..6d7674fd5 100644 --- a/lib/event_processor/batch_event_processor.spec.ts +++ b/lib/event_processor/batch_event_processor.spec.ts @@ -875,7 +875,7 @@ describe('BatchEventProcessor', async () => { }); describe('retryFailedEvents', () => { - it('should disptach only failed events from the store and not dispatch queued events', async () => { + it('should dispatch only failed events from the store and not dispatch queued events', async () => { const eventDispatcher = getMockDispatcher(); const mockDispatch: MockInstance = eventDispatcher.dispatchEvent; mockDispatch.mockResolvedValue({}); @@ -921,7 +921,7 @@ describe('BatchEventProcessor', async () => { ])); }); - it('should disptach only failed events from the store and not dispatch events that are being dispatched', async () => { + it('should dispatch only failed events from the store and not dispatch events that are being dispatched', async () => { const eventDispatcher = getMockDispatcher(); const mockDispatch: MockInstance = eventDispatcher.dispatchEvent; const mockResult1 = resolvablePromise(); @@ -977,7 +977,7 @@ describe('BatchEventProcessor', async () => { ])); }); - it('should disptach events in correct batch size and separate events with differnt contexts in separate batch', async () => { + it('should dispatch events in correct batch size and separate events with differnt contexts in separate batch', async () => { const eventDispatcher = getMockDispatcher(); const mockDispatch: MockInstance = eventDispatcher.dispatchEvent; mockDispatch.mockResolvedValue({}); @@ -1023,7 +1023,7 @@ describe('BatchEventProcessor', async () => { }); describe('when failedEventRepeater is fired', () => { - it('should disptach only failed events from the store and not dispatch queued events', async () => { + it('should dispatch only failed events from the store and not dispatch queued events', async () => { const eventDispatcher = getMockDispatcher(); const mockDispatch: MockInstance = eventDispatcher.dispatchEvent; mockDispatch.mockResolvedValue({}); @@ -1071,7 +1071,7 @@ describe('BatchEventProcessor', async () => { ])); }); - it('should disptach only failed events from the store and not dispatch events that are being dispatched', async () => { + it('should dispatch only failed events from the store and not dispatch events that are being dispatched', async () => { const eventDispatcher = getMockDispatcher(); const mockDispatch: MockInstance = eventDispatcher.dispatchEvent; const mockResult1 = resolvablePromise(); @@ -1129,7 +1129,7 @@ describe('BatchEventProcessor', async () => { ])); }); - it('should disptach events in correct batch size and separate events with differnt contexts in separate batch', async () => { + it('should dispatch events in correct batch size and separate events with differnt contexts in separate batch', async () => { const eventDispatcher = getMockDispatcher(); const mockDispatch: MockInstance = eventDispatcher.dispatchEvent; mockDispatch.mockResolvedValue({}); @@ -1277,7 +1277,7 @@ describe('BatchEventProcessor', async () => { expect(failedEventRepeater.stop).toHaveBeenCalledOnce(); }); - it('should disptach the events in queue using the closing dispatcher if available', async () => { + it('should dispatch the events in queue using the closing dispatcher if available', async () => { const eventDispatcher = getMockDispatcher(); const closingEventDispatcher = getMockDispatcher(); closingEventDispatcher.dispatchEvent.mockResolvedValue({}); @@ -1408,4 +1408,76 @@ describe('BatchEventProcessor', async () => { await expect(processor.onTerminated()).resolves.not.toThrow(); }); }); + + describe('flushImmediately', () => { + it('should dispatch the events in queue using the closing dispatcher if available', async () => { + const eventDispatcher = getMockDispatcher(); + const closingEventDispatcher = getMockDispatcher(); + closingEventDispatcher.dispatchEvent.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + closingEventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + processor.flushImmediately(); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); + + expect(processor.isRunning()).toBe(true); + }); + + + it('should dispatch the events in queue using eventDispatcher if closingEventDispatcher is not available', async () => { + const eventDispatcher = getMockDispatcher(); + eventDispatcher.dispatchEvent.mockResolvedValue({}); + + const dispatchRepeater = getMockRepeater(); + const failedEventRepeater = getMockRepeater(); + + const processor = new BatchEventProcessor({ + eventDispatcher, + dispatchRepeater, + failedEventRepeater, + batchSize: 100, + }); + + processor.start(); + await processor.onRunning(); + + const events: ProcessableEvent[] = []; + for(let i = 0; i < 10; i++) { + const event = createImpressionEvent(`id-${i}`); + events.push(event); + await processor.process(event); + } + + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(0); + + processor.flushImmediately(); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); + + expect(processor.isRunning()).toBe(true); + }); + }); }); diff --git a/lib/event_processor/batch_event_processor.ts b/lib/event_processor/batch_event_processor.ts index 6ad19eaf8..86f7ff148 100644 --- a/lib/event_processor/batch_event_processor.ts +++ b/lib/event_processor/batch_event_processor.ts @@ -230,14 +230,14 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { }); } - private async flush(closing = false): Promise { + private async flush(useClosingDispatcher = false): Promise { const batch = this.createNewBatch(); if (!batch) { return; } this.dispatchRepeater.reset(); - this.dispatchBatch(batch, closing); + this.dispatchBatch(batch, useClosingDispatcher); } async process(event: ProcessableEvent): Promise { @@ -332,6 +332,13 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { } } + flushImmediately(): Promise { + if (!this.isRunning()) { + return Promise.resolve(); + } + return this.flush(true); + } + stop(): void { if (this.isDone()) { return; diff --git a/lib/event_processor/event_builder/log_event.ts b/lib/event_processor/event_builder/log_event.ts index d3ec940fa..4d4048950 100644 --- a/lib/event_processor/event_builder/log_event.ts +++ b/lib/event_processor/event_builder/log_event.ts @@ -222,7 +222,7 @@ function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { export function buildLogEvent(events: UserEvent[]): LogEvent { const region = events[0]?.context.region || 'US'; - const url = logxEndpoint[region]; + const url = logxEndpoint[region] || logxEndpoint['US']; return { url, diff --git a/lib/event_processor/event_processor.ts b/lib/event_processor/event_processor.ts index 3589ce3a5..585c71f68 100644 --- a/lib/event_processor/event_processor.ts +++ b/lib/event_processor/event_processor.ts @@ -28,4 +28,5 @@ export interface EventProcessor extends Service { process(event: ProcessableEvent): Promise; onDispatch(handler: Consumer): Fn; setLogger(logger: LoggerFacade): void; + flushImmediately(): Promise; } diff --git a/lib/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts index 80ce1c763..f578992c7 100644 --- a/lib/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -69,4 +69,8 @@ export class ForwardingEventProcessor extends BaseService implements EventProces onDispatch(handler: Consumer): Fn { return this.eventEmitter.on('dispatch', handler); } + + flushImmediately(): Promise { + return Promise.resolve(); + } } diff --git a/lib/index.browser.ts b/lib/index.browser.ts index 4cbfc7c69..0f644a844 100644 --- a/lib/index.browser.ts +++ b/lib/index.browser.ts @@ -37,7 +37,7 @@ export const createInstance = function(config: Config): Client { window.addEventListener( unloadEvent, () => { - client.close(); + client.flushImmediately(); }, ); } diff --git a/lib/odp/event_manager/odp_event_manager.spec.ts b/lib/odp/event_manager/odp_event_manager.spec.ts index 6fb5db08a..68484d788 100644 --- a/lib/odp/event_manager/odp_event_manager.spec.ts +++ b/lib/odp/event_manager/odp_event_manager.spec.ts @@ -1010,4 +1010,45 @@ describe('DefaultOdpEventManager', () => { await expect(odpEventManager.onTerminated()).resolves.not.toThrow(); expect(odpEventManager.getState()).toBe(ServiceState.Terminated); }); + + it('should flush the queue when flushImmediately() is called in running state', async () => { + const repeater = getMockRepeater(); + + const apiManager = getMockApiManager(); + apiManager.sendEvents.mockResolvedValue({ statusCode: 200 }); + + const odpEventManager = new DefaultOdpEventManager({ + repeater: repeater, + apiManager: apiManager, + batchSize: 30, + retryConfig: { + maxRetries: 3, + backoffProvider: vi.fn(), + }, + }); + + odpEventManager.updateConfig({ + integrated: true, + odpConfig: config, + }); + + odpEventManager.start(); + await expect(odpEventManager.onRunning()).resolves.not.toThrow(); + + const events: OdpEvent[] = []; + for(let i = 0; i < 10; i++) { + events.push(makeEvent(i)); + odpEventManager.sendEvent(events[i]); + } + + await exhaustMicrotasks(); + expect(apiManager.sendEvents).not.toHaveBeenCalled(); + + odpEventManager.flushImmediately(); + await exhaustMicrotasks(); + + expect(apiManager.sendEvents).toHaveBeenCalledTimes(1); + expect(apiManager.sendEvents).toHaveBeenCalledWith(config, events); + expect(odpEventManager.isRunning()).toBe(true); + }); }); diff --git a/lib/odp/event_manager/odp_event_manager.ts b/lib/odp/event_manager/odp_event_manager.ts index 3a9c591cc..d1a30d3ff 100644 --- a/lib/odp/event_manager/odp_event_manager.ts +++ b/lib/odp/event_manager/odp_event_manager.ts @@ -42,6 +42,7 @@ export interface OdpEventManager extends Service { updateConfig(odpIntegrationConfig: OdpIntegrationConfig): void; sendEvent(event: OdpEvent): void; setLogger(logger: LoggerFacade): void; + flushImmediately(): Promise; } export type RetryConfig = { @@ -160,6 +161,13 @@ export class DefaultOdpEventManager extends BaseService implements OdpEventManag this.startPromise.resolve(); } + flushImmediately(): Promise { + if (!this.isRunning()) { + return Promise.resolve(); + } + return this.flush(); + } + stop(): void { if (this.isDone()) { return; diff --git a/lib/odp/odp_manager.spec.ts b/lib/odp/odp_manager.spec.ts index 376a663cf..9ae0daf69 100644 --- a/lib/odp/odp_manager.spec.ts +++ b/lib/odp/odp_manager.spec.ts @@ -53,6 +53,7 @@ const getMockOdpEventManager = () => { sendEvent: vi.fn(), makeDisposable: vi.fn(), setLogger: vi.fn(), + flushImmediately: vi.fn(), }; }; @@ -780,6 +781,29 @@ describe('DefaultOdpManager', () => { odpManager.makeDisposable(); expect(eventManager.makeDisposable).toHaveBeenCalled(); + }); + + it('should call flushImmediately() on eventManager when flushImmediately() is called on odpManager', async () => { + const eventManager = getMockOdpEventManager(); + eventManager.onRunning.mockResolvedValue({}); + const segmentManager = getMockOdpSegmentManager(); + + eventManager.flushImmediately.mockResolvedValue({}); + + const odpManager = new DefaultOdpManager({ + segmentManager, + eventManager, + }); + + odpManager.updateConfig({ integrated: true, odpConfig: config }); + odpManager.start(); + + await odpManager.onRunning(); + + odpManager.flushImmediately(); + + expect(eventManager.flushImmediately).toHaveBeenCalledOnce(); + expect(odpManager.isRunning()).toBe(true); }) }); diff --git a/lib/odp/odp_manager.ts b/lib/odp/odp_manager.ts index 2f8256c38..feaca24b9 100644 --- a/lib/odp/odp_manager.ts +++ b/lib/odp/odp_manager.ts @@ -40,6 +40,7 @@ export interface OdpManager extends Service { setClientInfo(clientEngine: string, clientVersion: string): void; setVuid(vuid: string): void; setLogger(logger: LoggerFacade): void; + flushImmediately(): Promise; } export type OdpManagerConfig = { @@ -145,6 +146,13 @@ export class DefaultOdpManager extends BaseService implements OdpManager { this.stopPromise.reject(error); } + flushImmediately(): Promise { + if (!this.isRunning()) { + return Promise.resolve(); + } + return this.eventManager.flushImmediately(); + } + stop(): void { if (this.isDone()) { return; diff --git a/lib/optimizely/index.spec.ts b/lib/optimizely/index.spec.ts index 81509fd1e..4548ffbb7 100644 --- a/lib/optimizely/index.spec.ts +++ b/lib/optimizely/index.spec.ts @@ -872,4 +872,37 @@ describe('Optimizely', () => { }); }); }); + + it('should flush eventProcessor and odpManager on flushImmediately()', async () => { + const projectConfigManager = getMockProjectConfigManager({ + initConfig: createProjectConfig(testData.getTestProjectConfig()), + }); + + const eventProcessor = getForwardingEventProcessor(eventDispatcher); + const odpManager = extractOdpManager(createOdpManager({})); + + const optimizely = new Optimizely({ + clientEngine: 'node-sdk', + projectConfigManager, + jsonSchemaValidator, + logger, + eventProcessor, + odpManager, + disposable: true, + cmabService: {} as any + }); + + odpManager?.updateConfig({ integrated: false }); + await optimizely.onReady(); + + const eventProcessorFlushSpy = vi.spyOn(eventProcessor, 'flushImmediately').mockResolvedValue(Promise.resolve()); + const odpManagerFlushSpy = vi.spyOn(odpManager!, 'flushImmediately').mockResolvedValue(Promise.resolve()); + + await optimizely.flushImmediately(); + + expect(eventProcessorFlushSpy).toHaveBeenCalled(); + expect(odpManagerFlushSpy).toHaveBeenCalled(); + + expect(optimizely.isRunning()).toBe(true); + }); }); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index f6e2b4f35..b8707a006 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -1243,6 +1243,24 @@ export default class Optimizely extends BaseService implements Client { } } + flushImmediately(): Promise { + const flushPromises = []; + + if (!this.isRunning()) { + return Promise.resolve(); + } + + if (this.eventProcessor) { + flushPromises.push(this.eventProcessor.flushImmediately()); + } + + if(this.odpManager) { + flushPromises.push(this.odpManager.flushImmediately()); + } + + return Promise.all(flushPromises); + } + /** * Stop background processes belonging to this instance, including: * diff --git a/package-lock.json b/package-lock.json index 4e6e462c9..4820c783c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,7 +35,6 @@ "eslint-plugin-prettier": "^3.1.2", "happy-dom": "^16.6.0", "jiti": "^2.4.1", - "json-loader": "^0.5.4", "karma": "^6.4.0", "karma-browserstack-launcher": "^1.5.1", "karma-chai": "^0.1.0", @@ -57,7 +56,6 @@ "ts-loader": "^9.3.1", "ts-node": "^8.10.2", "tsconfig-paths": "^4.2.0", - "tslib": "^2.4.0", "typescript": "^4.7.4", "vitest": "^2.0.5", "webpack": "^5.74.0" @@ -9109,12 +9107,6 @@ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, - "node_modules/json-loader": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/json-loader/-/json-loader-0.5.7.tgz", - "integrity": "sha512-QLPs8Dj7lnf3e3QYS1zkCo+4ZwqOiF9d/nZnYozTISxXWCfNs9yuky5rJw4/W34s7POaNlbZmQGaB5NiXCbP4w==", - "dev": true - }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -13685,7 +13677,8 @@ "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "dev": true, + "peer": true }, "node_modules/tsutils": { "version": "3.21.0", diff --git a/package.json b/package.json index 9aa609e6c..675bb7d4f 100644 --- a/package.json +++ b/package.json @@ -117,7 +117,6 @@ "eslint-plugin-prettier": "^3.1.2", "happy-dom": "^16.6.0", "jiti": "^2.4.1", - "json-loader": "^0.5.4", "karma": "^6.4.0", "karma-browserstack-launcher": "^1.5.1", "karma-chai": "^0.1.0", @@ -139,7 +138,6 @@ "ts-loader": "^9.3.1", "ts-node": "^8.10.2", "tsconfig-paths": "^4.2.0", - "tslib": "^2.4.0", "typescript": "^4.7.4", "vitest": "^2.0.5", "webpack": "^5.74.0"