From b2e406735ce489f721088094f69cb1bb7fcf8882 Mon Sep 17 00:00:00 2001 From: Raju Ahmed Date: Tue, 26 Nov 2024 00:33:27 +0600 Subject: [PATCH] [FSSDK-10941] event processor files and directories cleanup - part 2 --- ...batch_event_processor.react_native.spec.ts | 4 +- .../batch_event_processor.spec.ts | 58 +- lib/event_processor/batch_event_processor.ts | 20 +- .../default_dispatcher.spec.ts | 4 +- lib/event_processor/default_dispatcher.ts | 4 +- .../event_builder/index.tests.js | 1708 ----------------- lib/event_processor/event_builder/index.ts | 322 ---- ...ild_event_v1.spec.ts => log_event.spec.ts} | 10 +- .../{build_event_v1.ts => log_event.ts} | 23 +- ...t_helpers.tests.js => user_event.tests.js} | 4 +- .../{event_helpers.ts => user_event.ts} | 146 +- lib/event_processor/event_dispatcher.ts | 8 +- lib/event_processor/event_processor.ts | 6 +- lib/event_processor/events.ts | 101 - .../forwarding_event_processor.spec.ts | 12 +- .../forwarding_event_processor.ts | 10 +- lib/optimizely/index.tests.js | 1 - lib/optimizely/index.ts | 122 +- lib/utils/event_tag_utils/index.ts | 2 +- lib/utils/fns/index.ts | 2 +- 20 files changed, 185 insertions(+), 2382 deletions(-) delete mode 100644 lib/event_processor/event_builder/index.tests.js delete mode 100644 lib/event_processor/event_builder/index.ts rename lib/event_processor/event_builder/{build_event_v1.spec.ts => log_event.spec.ts} (98%) rename lib/event_processor/event_builder/{build_event_v1.ts => log_event.ts} (93%) rename lib/event_processor/event_builder/{event_helpers.tests.js => user_event.tests.js} (98%) rename lib/event_processor/event_builder/{event_helpers.ts => user_event.ts} (78%) delete mode 100644 lib/event_processor/events.ts diff --git a/lib/event_processor/batch_event_processor.react_native.spec.ts b/lib/event_processor/batch_event_processor.react_native.spec.ts index ea1612f4c..b2b50fe0f 100644 --- a/lib/event_processor/batch_event_processor.react_native.spec.ts +++ b/lib/event_processor/batch_event_processor.react_native.spec.ts @@ -50,7 +50,7 @@ import { getMockRepeater } from '../tests/mock/mock_repeater'; import { getMockAsyncCache } from '../tests/mock/mock_cache'; import { EventWithId } from './batch_event_processor'; -import { formatEvents } from './event_builder/build_event_v1'; +import { buildLogEvent } from './event_builder/build_event_v1'; import { createImpressionEvent } from '../tests/mock/create_event'; import { ProcessableEvent } from './event_processor'; @@ -138,7 +138,7 @@ describe('ReactNativeNetInfoEventProcessor', () => { await exhaustMicrotasks(); - expect(eventDispatcher.dispatchEvent).toHaveBeenCalledWith(formatEvents(events)); + expect(eventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); }); it('should unsubscribe from netinfo listener when stopped', async () => { diff --git a/lib/event_processor/batch_event_processor.spec.ts b/lib/event_processor/batch_event_processor.spec.ts index 2c81f9215..61318b92c 100644 --- a/lib/event_processor/batch_event_processor.spec.ts +++ b/lib/event_processor/batch_event_processor.spec.ts @@ -19,7 +19,7 @@ import { EventWithId, BatchEventProcessor } from './batch_event_processor'; import { getMockSyncCache } from '../tests/mock/mock_cache'; import { createImpressionEvent } from '../tests/mock/create_event'; import { ProcessableEvent } from './event_processor'; -import { formatEvents } from './event_builder/build_event_v1'; +import { buildLogEvent } from './event_builder/build_event_v1'; import { resolvablePromise } from '../utils/promise/resolvablePromise'; import { advanceTimersByTime } from '../../tests/testUtils'; import { getMockLogger } from '../tests/mock/mock_logger'; @@ -139,9 +139,9 @@ describe('QueueingEventProcessor', async () => { await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(3); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents([events[0], events[1]])); - expect(mockDispatch.mock.calls[1][0]).toEqual(formatEvents([events[2], events[3]])); - expect(mockDispatch.mock.calls[2][0]).toEqual(formatEvents([events[4]])); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([events[0], events[1]])); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent([events[2], events[3]])); + expect(mockDispatch.mock.calls[2][0]).toEqual(buildLogEvent([events[4]])); }); }); @@ -202,7 +202,7 @@ describe('QueueingEventProcessor', async () => { await processor.process(event); expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); - expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(formatEvents(events)); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); events = [event]; for(let i = 101; i < 200; i++) { @@ -217,7 +217,7 @@ describe('QueueingEventProcessor', async () => { await processor.process(event); expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); - expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(formatEvents(events)); + expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(buildLogEvent(events)); }); it('should flush queue is context of the new event is different and enqueue the new event', async () => { @@ -250,11 +250,11 @@ describe('QueueingEventProcessor', async () => { await processor.process(newEvent); expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); - expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(formatEvents(events)); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); await dispatchRepeater.execute(0); expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); - expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(formatEvents([newEvent])); + expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(buildLogEvent([newEvent])); }); it('should store the event in the eventStore with increasing ids', async () => { @@ -313,7 +313,7 @@ describe('QueueingEventProcessor', async () => { await dispatchRepeater.execute(0); expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); - expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(formatEvents(events)); + expect(eventDispatcher.dispatchEvent.mock.calls[0][0]).toEqual(buildLogEvent(events)); events = []; for(let i = 1; i < 15; i++) { @@ -324,7 +324,7 @@ describe('QueueingEventProcessor', async () => { await dispatchRepeater.execute(0); expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(2); - expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(formatEvents(events)); + expect(eventDispatcher.dispatchEvent.mock.calls[1][0]).toEqual(buildLogEvent(events)); }); it('should not retry failed dispatch if retryConfig is not provided', async () => { @@ -397,7 +397,7 @@ describe('QueueingEventProcessor', async () => { expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(4); expect(backoffController.backoff).toHaveBeenCalledTimes(3); - const request = formatEvents(events); + const request = buildLogEvent(events); for(let i = 0; i < 4; i++) { expect(eventDispatcher.dispatchEvent.mock.calls[i][0]).toEqual(request); } @@ -444,7 +444,7 @@ describe('QueueingEventProcessor', async () => { expect(eventDispatcher.dispatchEvent).toHaveBeenCalledTimes(201); expect(backoffController.backoff).toHaveBeenCalledTimes(200); - const request = formatEvents(events); + const request = buildLogEvent(events); for(let i = 0; i < 201; i++) { expect(eventDispatcher.dispatchEvent.mock.calls[i][0]).toEqual(request); } @@ -723,7 +723,7 @@ describe('QueueingEventProcessor', async () => { await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(1); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents(failedEvents)); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent(failedEvents)); const eventsInStore = [...cache.getAll().values()].sort((a, b) => a.id < b.id ? -1 : 1).map(e => e.event); expect(eventsInStore).toEqual(expect.arrayContaining([ @@ -761,7 +761,7 @@ describe('QueueingEventProcessor', async () => { dispatchRepeater.execute(0); await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(1); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents([eventA, eventB])); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([eventA, eventB])); const failedEvents: ProcessableEvent[] = []; @@ -776,7 +776,7 @@ describe('QueueingEventProcessor', async () => { await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch.mock.calls[1][0]).toEqual(formatEvents(failedEvents)); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent(failedEvents)); mockResult2.resolve({}); await exhaustMicrotasks(); @@ -826,10 +826,10 @@ describe('QueueingEventProcessor', async () => { // events 0 1 4 5 6 7 have one context, and 2 3 have different context // batches should be [0, 1], [2, 3], [4, 5, 6], [7] expect(mockDispatch).toHaveBeenCalledTimes(4); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents([failedEvents[0], failedEvents[1]])); - expect(mockDispatch.mock.calls[1][0]).toEqual(formatEvents([failedEvents[2], failedEvents[3]])); - expect(mockDispatch.mock.calls[2][0]).toEqual(formatEvents([failedEvents[4], failedEvents[5], failedEvents[6]])); - expect(mockDispatch.mock.calls[3][0]).toEqual(formatEvents([failedEvents[7]])); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([failedEvents[0], failedEvents[1]])); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent([failedEvents[2], failedEvents[3]])); + expect(mockDispatch.mock.calls[2][0]).toEqual(buildLogEvent([failedEvents[4], failedEvents[5], failedEvents[6]])); + expect(mockDispatch.mock.calls[3][0]).toEqual(buildLogEvent([failedEvents[7]])); }); }); @@ -873,7 +873,7 @@ describe('QueueingEventProcessor', async () => { await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(1); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents(failedEvents)); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent(failedEvents)); const eventsInStore = [...cache.getAll().values()].sort((a, b) => a.id < b.id ? -1 : 1).map(e => e.event); expect(eventsInStore).toEqual(expect.arrayContaining([ @@ -913,7 +913,7 @@ describe('QueueingEventProcessor', async () => { dispatchRepeater.execute(0); await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(1); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents([eventA, eventB])); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([eventA, eventB])); const failedEvents: ProcessableEvent[] = []; @@ -928,7 +928,7 @@ describe('QueueingEventProcessor', async () => { await exhaustMicrotasks(); expect(mockDispatch).toHaveBeenCalledTimes(2); - expect(mockDispatch.mock.calls[1][0]).toEqual(formatEvents(failedEvents)); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent(failedEvents)); mockResult2.resolve({}); await exhaustMicrotasks(); @@ -980,10 +980,10 @@ describe('QueueingEventProcessor', async () => { // events 0 1 4 5 6 7 have one context, and 2 3 have different context // batches should be [0, 1], [2, 3], [4, 5, 6], [7] expect(mockDispatch).toHaveBeenCalledTimes(4); - expect(mockDispatch.mock.calls[0][0]).toEqual(formatEvents([failedEvents[0], failedEvents[1]])); - expect(mockDispatch.mock.calls[1][0]).toEqual(formatEvents([failedEvents[2], failedEvents[3]])); - expect(mockDispatch.mock.calls[2][0]).toEqual(formatEvents([failedEvents[4], failedEvents[5], failedEvents[6]])); - expect(mockDispatch.mock.calls[3][0]).toEqual(formatEvents([failedEvents[7]])); + expect(mockDispatch.mock.calls[0][0]).toEqual(buildLogEvent([failedEvents[0], failedEvents[1]])); + expect(mockDispatch.mock.calls[1][0]).toEqual(buildLogEvent([failedEvents[2], failedEvents[3]])); + expect(mockDispatch.mock.calls[2][0]).toEqual(buildLogEvent([failedEvents[4], failedEvents[5], failedEvents[6]])); + expect(mockDispatch.mock.calls[3][0]).toEqual(buildLogEvent([failedEvents[7]])); }); }); @@ -1012,7 +1012,7 @@ describe('QueueingEventProcessor', async () => { await dispatchRepeater.execute(0); expect(dispatchListener).toHaveBeenCalledTimes(1); - expect(dispatchListener.mock.calls[0][0]).toEqual(formatEvents([event, event2])); + expect(dispatchListener.mock.calls[0][0]).toEqual(buildLogEvent([event, event2])); }); it('should remove event handler when function returned from onDispatch is called', async () => { @@ -1041,7 +1041,7 @@ describe('QueueingEventProcessor', async () => { await dispatchRepeater.execute(0); expect(dispatchListener).toHaveBeenCalledTimes(1); - expect(dispatchListener.mock.calls[0][0]).toEqual(formatEvents([event, event2])); + expect(dispatchListener.mock.calls[0][0]).toEqual(buildLogEvent([event, event2])); unsub(); @@ -1119,7 +1119,7 @@ describe('QueueingEventProcessor', async () => { processor.stop(); expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledTimes(1); - expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledWith(formatEvents(events)); + expect(closingEventDispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent(events)); }); it('should cancel retry of active dispatches', async () => { diff --git a/lib/event_processor/batch_event_processor.ts b/lib/event_processor/batch_event_processor.ts index 3d000a5df..287510b46 100644 --- a/lib/event_processor/batch_event_processor.ts +++ b/lib/event_processor/batch_event_processor.ts @@ -16,8 +16,8 @@ import { EventProcessor, ProcessableEvent } from "./event_processor"; import { Cache } from "../utils/cache/cache"; -import { EventDispatcher, EventDispatcherResponse, EventV1Request } from "./event_dispatcher"; -import { formatEvents } from "./event_builder/build_event_v1"; +import { EventDispatcher, EventDispatcherResponse, LogEvent } from "./event_dispatcher"; +import { buildLogEvent } from "./event_builder/log_event"; import { BackoffController, ExponentialBackoff, IntervalRepeater, Repeater } from "../utils/repeater/repeater"; import { LoggerFacade } from "../modules/logging"; import { BaseService, ServiceState, StartupLog } from "../service"; @@ -26,7 +26,7 @@ import { RunResult, runWithRetry } from "../utils/executor/backoff_retry_runner" import { isSuccessStatusCode } from "../utils/http_request_handler/http_util"; import { EventEmitter } from "../utils/event_emitter/event_emitter"; import { IdGenerator } from "../utils/id_generator"; -import { areEventContextsEqual } from "./events"; +import { areEventContextsEqual } from "./event_builder/user_event"; export type EventWithId = { id: string; @@ -51,7 +51,7 @@ export type BatchEventProcessorConfig = { }; type EventBatch = { - request: EventV1Request, + request: LogEvent, ids: string[], } @@ -66,7 +66,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { private idGenerator: IdGenerator = new IdGenerator(); private runningTask: Map> = new Map(); private dispatchingEventIds: Set = new Set(); - private eventEmitter: EventEmitter<{ dispatch: EventV1Request }> = new EventEmitter(); + private eventEmitter: EventEmitter<{ dispatch: LogEvent }> = new EventEmitter(); private retryConfig?: RetryConfig; constructor(config: BatchEventProcessorConfig) { @@ -85,7 +85,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { this.failedEventRepeater?.setTask(() => this.retryFailedEvents()); } - onDispatch(handler: Consumer): Fn { + onDispatch(handler: Consumer): Fn { return this.eventEmitter.on('dispatch', handler); } @@ -119,7 +119,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { if (currentBatch.length === this.batchSize || (currentBatch.length > 0 && !areEventContextsEqual(currentBatch[0].event, event.event))) { batches.push({ - request: formatEvents(currentBatch.map((e) => e.event)), + request: buildLogEvent(currentBatch.map((e) => e.event)), ids: currentBatch.map((e) => e.id), }); currentBatch = []; @@ -129,7 +129,7 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { if (currentBatch.length > 0) { batches.push({ - request: formatEvents(currentBatch.map((e) => e.event)), + request: buildLogEvent(currentBatch.map((e) => e.event)), ids: currentBatch.map((e) => e.id), }); } @@ -153,10 +153,10 @@ export class BatchEventProcessor extends BaseService implements EventProcessor { }); this.eventQueue = []; - return { request: formatEvents(events), ids }; + return { request: buildLogEvent(events), ids }; } - private async executeDispatch(request: EventV1Request, closing = false): Promise { + private async executeDispatch(request: LogEvent, closing = false): Promise { const dispatcher = closing && this.closingEventDispatcher ? this.closingEventDispatcher : this.eventDispatcher; return dispatcher.dispatchEvent(request).then((res) => { if (res.statusCode && !isSuccessStatusCode(res.statusCode)) { diff --git a/lib/event_processor/default_dispatcher.spec.ts b/lib/event_processor/default_dispatcher.spec.ts index f7cdc718f..e3f73ffad 100644 --- a/lib/event_processor/default_dispatcher.spec.ts +++ b/lib/event_processor/default_dispatcher.spec.ts @@ -15,9 +15,9 @@ */ import { expect, vi, describe, it } from 'vitest'; import { DefaultEventDispatcher } from './default_dispatcher'; -import { EventV1 } from './event_builder/build_event_v1'; +import { EventBatch } from './event_builder/build_event_v1'; -const getEvent = (): EventV1 => { +const getEvent = (): EventBatch => { return { account_id: 'string', project_id: 'string', diff --git a/lib/event_processor/default_dispatcher.ts b/lib/event_processor/default_dispatcher.ts index 3105b49e1..43cb062c3 100644 --- a/lib/event_processor/default_dispatcher.ts +++ b/lib/event_processor/default_dispatcher.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { RequestHandler } from '../utils/http_request_handler/http'; -import { EventDispatcher, EventDispatcherResponse, EventV1Request } from './event_dispatcher'; +import { EventDispatcher, EventDispatcherResponse, LogEvent } from './event_dispatcher'; export class DefaultEventDispatcher implements EventDispatcher { private requestHandler: RequestHandler; @@ -24,7 +24,7 @@ export class DefaultEventDispatcher implements EventDispatcher { } async dispatchEvent( - eventObj: EventV1Request + eventObj: LogEvent ): Promise { // Non-POST requests not supported if (eventObj.httpVerb !== 'POST') { diff --git a/lib/event_processor/event_builder/index.tests.js b/lib/event_processor/event_builder/index.tests.js deleted file mode 100644 index 4fd6053a9..000000000 --- a/lib/event_processor/event_builder/index.tests.js +++ /dev/null @@ -1,1708 +0,0 @@ -/** - * Copyright 2016-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 sinon from 'sinon'; -import { assert } from 'chai'; - -import fns from '../../utils/fns'; -import testData from '../../tests/test_data'; -import projectConfig from '../../project_config/project_config'; -import packageJSON from '../../../package.json'; -import { getConversionEvent, getImpressionEvent } from './'; - -describe('lib/core/event_builder', function() { - describe('APIs', function() { - var mockLogger; - var configObj; - var clock; - - beforeEach(function() { - configObj = projectConfig.createProjectConfig(testData.getTestProjectConfig()); - clock = sinon.useFakeTimers(new Date().getTime()); - sinon.stub(fns, 'uuid').returns('a68cf1ad-0393-4e18-af87-efe8f01a7c9c'); - mockLogger = { - log: sinon.stub(), - }; - }); - - afterEach(function() { - clock.restore(); - fns.uuid.restore(); - }); - - describe('getImpressionEvent', function() { - it('should create proper params for getImpressionEvent without attributes', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: true, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - ruleKey: 'exp1', - flagKey: 'flagKey1', - enabled: true, - ruleType: 'experiment', - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with attributes as a string value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [ - { - entity_id: '111094', - type: 'custom', - value: 'firefox', - key: 'browser_type', - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: false, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - var eventOptions = { - attributes: { browser_type: 'firefox' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - variationId: '111128', - ruleKey: 'exp1', - flagKey: 'flagKey1', - enabled: false, - ruleType: 'experiment', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with attributes as a false value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [ - { - entity_id: '111094', - type: 'custom', - value: false, - key: 'browser_type', - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: true, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { browser_type: false }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - ruleKey: 'exp1', - flagKey: 'flagKey1', - ruleType: 'experiment', - enabled: true, - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with attributes as a zero value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [ - { - entity_id: '111094', - type: 'custom', - value: 0, - key: 'browser_type', - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: true, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { browser_type: 0 }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - ruleKey: 'exp1', - flagKey: 'flagKey1', - ruleType: 'experiment', - enabled: true, - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should not fill in userFeatures for getImpressionEvent when attribute is not in the datafile', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: false, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { invalid_attribute: 'sorry_not_sorry' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - ruleKey: 'exp1', - flagKey: 'flagKey1', - ruleType: 'experiment', - enabled: false, - variationId: '111128', - userId: 'testUser', - logger: mockLogger, - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering enabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '572018', - project_id: '594001', - visitors: [ - { - attributes: [ - { - entity_id: '$opt_user_agent', - key: '$opt_user_agent', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '$opt_bot_filtering', - key: '$opt_bot_filtering', - type: 'custom', - value: true, - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '595008', - experiment_id: '595010', - campaign_id: '595005', - metadata: { - flag_key: 'flagKey2', - rule_key: 'exp2', - rule_type: 'experiment', - variation_key: 'var', - enabled: false, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '595005', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '35', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: true, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { $opt_user_agent: 'Chrome' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - experimentId: '595010', - ruleKey: 'exp2', - flagKey: 'flagKey2', - ruleType: 'experiment', - enabled: false, - variationId: '595008', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering disabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - v4ConfigObj.botFiltering = false; - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '572018', - project_id: '594001', - visitors: [ - { - attributes: [ - { - entity_id: '$opt_user_agent', - key: '$opt_user_agent', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '$opt_bot_filtering', - key: '$opt_bot_filtering', - type: 'custom', - value: false, - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '595008', - experiment_id: '595010', - campaign_id: '595005', - metadata: { - flag_key: 'flagKey2', - rule_key: 'exp2', - rule_type: 'experiment', - variation_key: 'var', - enabled: false, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '595005', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '35', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: true, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { $opt_user_agent: 'Chrome' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - experimentId: '595010', - ruleKey: 'exp2', - flagKey: 'flagKey2', - ruleType: 'experiment', - enabled: false, - variationId: '595008', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getImpressionEvent with typed attributes', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [ - { - entity_id: '111094', - key: 'browser_type', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '323434545', - key: 'boolean_key', - type: 'custom', - value: true, - }, - { - entity_id: '616727838', - key: 'integer_key', - type: 'custom', - value: 10, - }, - { - entity_id: '808797686', - key: 'double_key', - type: 'custom', - value: 3.14, - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: false, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { - browser_type: 'Chrome', - boolean_key: true, - integer_key: 10, - double_key: 3.14, - }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - ruleKey: 'exp1', - flagKey: 'flagKey1', - ruleType: 'experiment', - enabled: false, - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should remove invalid params from impression event payload', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - attributes: [ - { - entity_id: '111094', - key: 'browser_type', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '808797687', - key: 'valid_positive_number', - type: 'custom', - value: Math.pow(2, 53), - }, - { - entity_id: '808797688', - key: 'valid_negative_number', - type: 'custom', - value: -Math.pow(2, 53), - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - decisions: [ - { - variation_id: '111128', - experiment_id: '111127', - campaign_id: '4', - metadata: { - flag_key: 'flagKey1', - rule_key: 'exp1', - rule_type: 'experiment', - variation_key: 'control', - enabled: true, - }, - }, - ], - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '4', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'campaign_activated', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { - browser_type: 'Chrome', - valid_positive_number: Math.pow(2, 53), - valid_negative_number: -Math.pow(2, 53), - invalid_number: Math.pow(2, 53) + 2, - array: [1, 2, 3], - }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - experimentId: '111127', - ruleKey: 'exp1', - flagKey: 'flagKey1', - ruleType: 'experiment', - enabled: true, - variationId: '111128', - userId: 'testUser', - }; - - var actualParams = getImpressionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - - describe('getConversionEvent', function() { - it('should create proper params for getConversionEvent without attributes or event value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - visitor_id: 'testUser', - attributes: [], - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '111095', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEvent', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getConversionEvent with attributes', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - visitor_id: 'testUser', - attributes: [ - { - entity_id: '111094', - type: 'custom', - value: 'firefox', - key: 'browser_type', - }, - ], - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '111095', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEvent', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { browser_type: 'firefox' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getConversionEvent with event value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - revenue: 4200, - }, - timestamp: Math.round(new Date().getTime()), - revenue: 4200, - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 4200, - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create proper params for getConversionEvent with attributes and event value', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [ - { - entity_id: '111094', - type: 'custom', - value: 'firefox', - key: 'browser_type', - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - revenue: 4200, - }, - timestamp: Math.round(new Date().getTime()), - revenue: 4200, - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { browser_type: 'firefox' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 4200, - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should not fill in userFeatures for getConversion when attribute is not in the datafile', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - timestamp: Math.round(new Date().getTime()), - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { invalid_attribute: 'sorry_not_sorry' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - sinon.assert.calledOnce(mockLogger.log); - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering enabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '572018', - project_id: '594001', - visitors: [ - { - attributes: [ - { - entity_id: '$opt_user_agent', - key: '$opt_user_agent', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '$opt_bot_filtering', - key: '$opt_bot_filtering', - type: 'custom', - value: true, - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '594089', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'item_bought', - }, - ], - }, - ], - }, - ], - revision: '35', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: true, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { $opt_user_agent: 'Chrome' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - eventKey: 'item_bought', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should fill in userFeatures for user agent and bot filtering (bot filtering disabled)', function() { - var v4ConfigObj = projectConfig.createProjectConfig(testData.getTestProjectConfigWithFeatures()); - v4ConfigObj.botFiltering = false; - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '572018', - project_id: '594001', - visitors: [ - { - attributes: [ - { - entity_id: '$opt_user_agent', - key: '$opt_user_agent', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '$opt_bot_filtering', - key: '$opt_bot_filtering', - type: 'custom', - value: false, - }, - ], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '594089', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'item_bought', - }, - ], - }, - ], - }, - ], - revision: '35', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: true, - enrich_decisions: true, - }, - }; - - var eventOptions = { - attributes: { $opt_user_agent: 'Chrome' }, - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: v4ConfigObj, - eventKey: 'item_bought', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should create the correct snapshot for multiple experiments attached to the event', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - visitor_id: 'testUser', - attributes: [], - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '111100', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEventWithMultipleExperiments', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEventWithMultipleExperiments', - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should remove invalid params from conversion event payload', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - visitor_id: 'testUser', - attributes: [ - { - entity_id: '111094', - key: 'browser_type', - type: 'custom', - value: 'Chrome', - }, - { - entity_id: '808797687', - key: 'valid_positive_number', - type: 'custom', - value: Math.pow(2, 53), - }, - { - entity_id: '808797688', - key: 'valid_negative_number', - type: 'custom', - value: -Math.pow(2, 53), - }, - ], - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '111100', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEventWithMultipleExperiments', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEventWithMultipleExperiments', - logger: mockLogger, - userId: 'testUser', - attributes: { - browser_type: 'Chrome', - valid_positive_number: Math.pow(2, 53), - valid_negative_number: -Math.pow(2, 53), - invalid_number: -Math.pow(2, 53) - 2, - array: [1, 2, 3], - }, - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and event tags are passed it', function() { - it('should create proper params for getConversionEvent with event tags', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - 'non-revenue': 'cool', - }, - timestamp: Math.round(new Date().getTime()), - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and the event tags contain an entry for "revenue"', function() { - it('should include the revenue value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - 'non-revenue': 'cool', - revenue: 4200, - }, - timestamp: Math.round(new Date().getTime()), - revenue: 4200, - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 4200, - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should include revenue value of 0 in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - revenue: 0, - }, - timestamp: Math.round(new Date().getTime()), - revenue: 0, - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 0, - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and the revenue value is invalid', function() { - it('should not include the revenue value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - 'non-revenue': 'cool', - revenue: 'invalid revenue', - }, - timestamp: Math.round(new Date().getTime()), - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - revenue: 'invalid revenue', - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - }); - - describe('and the event tags contain an entry for "value"', function() { - it('should include the event value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - 'non-revenue': 'cool', - value: '13.37', - }, - timestamp: Math.round(new Date().getTime()), - value: 13.37, - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - value: '13.37', - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - it('should include the falsy event values in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - value: '0.0', - }, - timestamp: Math.round(new Date().getTime()), - value: 0.0, - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - value: '0.0', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - - describe('and the event value is invalid', function() { - it('should not include the event value in the event object', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - client_version: packageJSON.version, - project_id: '111001', - visitors: [ - { - attributes: [], - visitor_id: 'testUser', - snapshots: [ - { - events: [ - { - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - tags: { - 'non-revenue': 'cool', - value: 'invalid value', - }, - timestamp: Math.round(new Date().getTime()), - key: 'testEvent', - entity_id: '111095', - }, - ], - }, - ], - }, - ], - account_id: '12001', - client_name: 'node-sdk', - revision: '42', - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - eventTags: { - value: 'invalid value', - 'non-revenue': 'cool', - }, - logger: mockLogger, - userId: 'testUser', - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - }); - }); - - describe('createEventWithBucketingId', function() { - it('should send proper bucketingID with user attributes', function() { - var expectedParams = { - url: 'https://logx.optimizely.com/v1/events', - httpVerb: 'POST', - params: { - account_id: '12001', - project_id: '111001', - visitors: [ - { - visitor_id: 'testUser', - attributes: [ - { - entity_id: '$opt_bucketing_id', - key: '$opt_bucketing_id', - type: 'custom', - value: 'variation', - }, - ], - snapshots: [ - { - events: [ - { - timestamp: Math.round(new Date().getTime()), - entity_id: '111095', - uuid: 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - key: 'testEvent', - }, - ], - }, - ], - }, - ], - revision: '42', - client_name: 'node-sdk', - client_version: packageJSON.version, - anonymize_ip: false, - enrich_decisions: true, - }, - }; - - var eventOptions = { - clientEngine: 'node-sdk', - clientVersion: packageJSON.version, - configObj: configObj, - eventKey: 'testEvent', - logger: mockLogger, - userId: 'testUser', - attributes: { $opt_bucketing_id: 'variation' }, - }; - - var actualParams = getConversionEvent(eventOptions); - - assert.deepEqual(actualParams, expectedParams); - }); - }); - }); - }); -}); diff --git a/lib/event_processor/event_builder/index.ts b/lib/event_processor/event_builder/index.ts deleted file mode 100644 index 813038f05..000000000 --- a/lib/event_processor/event_builder/index.ts +++ /dev/null @@ -1,322 +0,0 @@ -/** - * Copyright 2016-2022, 2024, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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 { LoggerFacade } from '../../modules/logging'; -import { EventV1 as CommonEventParams } from '../event_builder/build_event_v1'; - -import fns from '../../utils/fns'; -import { CONTROL_ATTRIBUTES, RESERVED_EVENT_KEYWORDS } from '../../utils/enums'; -import { - getAttributeId, - getEventId, - getLayerId, - getVariationKeyFromId, - ProjectConfig, -} from '../../project_config/project_config'; -import * as eventTagUtils from '../../utils/event_tag_utils'; -import { isAttributeValid } from '../../utils/attributes_validator'; -import { EventTags, UserAttributes, Event as EventLoggingEndpoint } from '../../shared_types'; - -const ACTIVATE_EVENT_KEY = 'campaign_activated'; -const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'; -const ENDPOINT = 'https://logx.optimizely.com/v1/events'; -const HTTP_VERB = 'POST'; - -interface ImpressionOptions { - // Object representing user attributes and values which need to be recorded - attributes?: UserAttributes; - // The client we are using: node or javascript - clientEngine: string; - // The version of the client - clientVersion: string; - // Object representing project configuration, including datafile information and mappings for quick lookup - configObj: ProjectConfig; - // Experiment for which impression needs to be recorded - experimentId: string | null; - // Key of an experiment for which impression needs to be recorded - ruleKey: string; - // Key for a feature flag - flagKey: string; - // Boolean representing if feature is enabled - enabled: boolean; - // Type for the decision source - ruleType: string; - // Event key representing the event which needs to be recorded - eventKey?: string; - // ID for variation which would be presented to user - variationId: string | null; - // Logger object - logger: LoggerFacade; - // ID for user - userId: string; -} - -interface ConversionEventOptions { - // Object representing user attributes and values which need to be recorded - attributes?: UserAttributes; - // The client we are using: node or javascript - clientEngine: string; - // The version of the client - clientVersion: string; - // Object representing project configuration, including datafile information and mappings for quick lookup - configObj: ProjectConfig; - // Event key representing the event which needs to be recorded - eventKey: string; - // Logger object - logger: LoggerFacade; - // ID for user - userId: string; - // Object with event-specific tags - eventTags?: EventTags; -} - -type Metadata = { - flag_key: string; - rule_key: string; - rule_type: string; - variation_key: string; - enabled: boolean; -} - -type Decision = { - campaign_id: string | null; - experiment_id: string | null; - variation_id: string | null; - metadata: Metadata; -} - -type SnapshotEvent = { - entity_id: string | null; - timestamp: number; - uuid: string; - key: string; - revenue?: number; - value?: number; - tags?: EventTags; -} - -interface Snapshot { - decisions?: Decision[]; - events: SnapshotEvent[]; -} - -/** - * Get params which are used same in both conversion and impression events - * @param {ImpressionOptions|ConversionEventOptions} options Object containing values needed to build impression/conversion event - * @return {CommonEventParams} Common params with properties that are used in both conversion and impression events - */ -function getCommonEventParams({ - attributes, - userId, - clientEngine, - clientVersion, - configObj, - logger, -}: ImpressionOptions | ConversionEventOptions): CommonEventParams { - - const anonymize_ip = configObj.anonymizeIP ? configObj.anonymizeIP : false; - const botFiltering = configObj.botFiltering; - - const visitor = { - snapshots: [], - visitor_id: userId, - attributes: [], - }; - - const commonParams: CommonEventParams = { - account_id: configObj.accountId, - project_id: configObj.projectId, - visitors: [visitor], - revision: configObj.revision, - client_name: clientEngine, - client_version: clientVersion, - anonymize_ip: anonymize_ip, - enrich_decisions: true, - }; - - if (attributes) { - // Omit attribute values that are not supported by the log endpoint. - Object.keys(attributes || {}).forEach(function(attributeKey) { - const attributeValue = attributes[attributeKey]; - if (isAttributeValid(attributeKey, attributeValue)) { - const attributeId = getAttributeId(configObj, attributeKey, logger); - if (attributeId) { - commonParams.visitors[0].attributes.push({ - entity_id: attributeId, - key: attributeKey, - type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, - value: attributeValue!, - }); - } - } - }); - } - - - if (typeof botFiltering === 'boolean') { - commonParams.visitors[0].attributes.push({ - entity_id: CONTROL_ATTRIBUTES.BOT_FILTERING, - key: CONTROL_ATTRIBUTES.BOT_FILTERING, - type: CUSTOM_ATTRIBUTE_FEATURE_TYPE, - value: botFiltering, - }); - } - - return commonParams; -} - -/** - * Creates object of params specific to impression events - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string|null} experimentId ID of experiment for which impression needs to be recorded - * @param {string|null} variationId ID for variation which would be presented to user - * @param {string} ruleKey Key of experiment for which impression needs to be recorded - * @param {string} ruleType Type for the decision source - * @param {string} flagKey Key for a feature flag - * @param {boolean} enabled Boolean representing if feature is enabled - * @return {Snapshot} Impression event params - */ -function getImpressionEventParams( - configObj: ProjectConfig, - experimentId: string | null, - variationId: string | null, - ruleKey: string, - ruleType: string, - flagKey: string, - enabled: boolean -): Snapshot { - - const campaignId = experimentId ? getLayerId(configObj, experimentId) : null; - - let variationKey = variationId ? getVariationKeyFromId(configObj, variationId) : null; - variationKey = variationKey || ''; - - const impressionEventParams = { - decisions: [ - { - campaign_id: campaignId, - experiment_id: experimentId, - variation_id: variationId, - metadata: { - flag_key: flagKey, - rule_key: ruleKey, - rule_type: ruleType, - variation_key: variationKey, - enabled: enabled, - } - }, - ], - events: [ - { - entity_id: campaignId, - timestamp: fns.currentTimestamp(), - key: ACTIVATE_EVENT_KEY, - uuid: fns.uuid(), - }, - ], - }; - - return impressionEventParams; -} - -/** - * Creates object of params specific to conversion events - * @param {ProjectConfig} configObj Object representing project configuration - * @param {string} eventKey Event key representing the event which needs to be recorded - * @param {LoggerFacade} logger Logger object - * @param {EventTags} eventTags Values associated with the event. - * @return {Snapshot} Conversion event params - */ -function getVisitorSnapshot( - configObj: ProjectConfig, - eventKey: string, - logger: LoggerFacade, - eventTags?: EventTags, -): Snapshot { - const snapshot: Snapshot = { - events: [], - }; - - const eventDict: SnapshotEvent = { - entity_id: getEventId(configObj, eventKey), - timestamp: fns.currentTimestamp(), - uuid: fns.uuid(), - key: eventKey, - }; - - if (eventTags) { - const revenue = eventTagUtils.getRevenueValue(eventTags, logger); - if (revenue !== null) { - eventDict[RESERVED_EVENT_KEYWORDS.REVENUE] = revenue; - } - - const eventValue = eventTagUtils.getEventValue(eventTags, logger); - if (eventValue !== null) { - eventDict[RESERVED_EVENT_KEYWORDS.VALUE] = eventValue; - } - - eventDict['tags'] = eventTags; - } - snapshot.events.push(eventDict); - - return snapshot; -} - -/** - * Create impression event params to be sent to the logging endpoint - * @param {ImpressionOptions} options Object containing values needed to build impression event - * @return {EventLoggingEndpoint} Params to be used in impression event logging endpoint call - */ -export function getImpressionEvent(options: ImpressionOptions): EventLoggingEndpoint { - const commonParams = getCommonEventParams(options); - const impressionEventParams = getImpressionEventParams( - options.configObj, - options.experimentId, - options.variationId, - options.ruleKey, - options.ruleType, - options.flagKey, - options.enabled, - ); - commonParams.visitors[0].snapshots.push(impressionEventParams); - - const impressionEvent: EventLoggingEndpoint = { - httpVerb: HTTP_VERB, - url: ENDPOINT, - params: commonParams, - } - - return impressionEvent; -} - -/** - * Create conversion event params to be sent to the logging endpoint - * @param {ConversionEventOptions} options Object containing values needed to build conversion event - * @return {EventLoggingEndpoint} Params to be used in conversion event logging endpoint call - */ -export function getConversionEvent(options: ConversionEventOptions): EventLoggingEndpoint { - - const commonParams = getCommonEventParams(options); - const snapshot = getVisitorSnapshot(options.configObj, options.eventKey, options.logger, options.eventTags); - commonParams.visitors[0].snapshots = [snapshot]; - - const conversionEvent: EventLoggingEndpoint = { - httpVerb: HTTP_VERB, - url: ENDPOINT, - params: commonParams, - } - - return conversionEvent; -} diff --git a/lib/event_processor/event_builder/build_event_v1.spec.ts b/lib/event_processor/event_builder/log_event.spec.ts similarity index 98% rename from lib/event_processor/event_builder/build_event_v1.spec.ts rename to lib/event_processor/event_builder/log_event.spec.ts index b1082dc7e..54a9c2acf 100644 --- a/lib/event_processor/event_builder/build_event_v1.spec.ts +++ b/lib/event_processor/event_builder/log_event.spec.ts @@ -18,10 +18,10 @@ import { describe, it, expect } from 'vitest'; import { buildConversionEventV1, buildImpressionEventV1, - makeBatchedEventV1, -} from './build_event_v1'; + makeEventBatch, +} from './log_event'; -import { ImpressionEvent, ConversionEvent } from '../events' +import { ImpressionEvent, ConversionEvent } from './user_event'; describe('buildImpressionEventV1', () => { it('should build an ImpressionEventV1 when experiment and variation are defined', () => { @@ -637,7 +637,7 @@ describe('buildConversionEventV1', () => { }) }) -describe('makeBatchedEventV1', () => { +describe('makeEventBatch', () => { it('should batch Conversion and Impression events together', () => { const conversionEvent: ConversionEvent = { type: 'conversion', @@ -714,7 +714,7 @@ describe('makeBatchedEventV1', () => { enabled: true, } - const result = makeBatchedEventV1([impressionEvent, conversionEvent]) + const result = makeEventBatch([impressionEvent, conversionEvent]) expect(result).toEqual({ client_name: 'node-sdk', diff --git a/lib/event_processor/event_builder/build_event_v1.ts b/lib/event_processor/event_builder/log_event.ts similarity index 93% rename from lib/event_processor/event_builder/build_event_v1.ts rename to lib/event_processor/event_builder/log_event.ts index 2cd794ca0..d648690da 100644 --- a/lib/event_processor/event_builder/build_event_v1.ts +++ b/lib/event_processor/event_builder/log_event.ts @@ -17,17 +17,16 @@ import { EventTags, ConversionEvent, ImpressionEvent, -} from '../events'; + UserEvent, +} from './user_event'; -import { Event } from '../../shared_types'; - -type ProcessableEvent = ConversionEvent | ImpressionEvent +import { LogEvent } from '../event_dispatcher'; const ACTIVATE_EVENT_KEY = 'campaign_activated' const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' const BOT_FILTERING_KEY = '$opt_bot_filtering' -export type EventV1 = { +export type EventBatch = { account_id: string project_id: string revision: string @@ -89,10 +88,10 @@ export type SnapshotEvent = { * Given an array of batchable Decision or ConversionEvent events it returns * a single EventV1 with proper batching * - * @param {ProcessableEvent[]} events - * @returns {EventV1} + * @param {UserEvent[]} events + * @returns {EventBatch} */ -export function makeBatchedEventV1(events: ProcessableEvent[]): EventV1 { +export function makeEventBatch(events: UserEvent[]): EventBatch { const visitors: Visitor[] = [] const data = events[0] @@ -222,7 +221,7 @@ function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { * @export * @interface EventBuilderV1 */ -export function buildImpressionEventV1(data: ImpressionEvent): EventV1 { +export function buildImpressionEventV1(data: ImpressionEvent): EventBatch { const visitor = makeVisitor(data) visitor.snapshots.push(makeDecisionSnapshot(data)) @@ -240,7 +239,7 @@ export function buildImpressionEventV1(data: ImpressionEvent): EventV1 { } } -export function buildConversionEventV1(data: ConversionEvent): EventV1 { +export function buildConversionEventV1(data: ConversionEvent): EventBatch { const visitor = makeVisitor(data) visitor.snapshots.push(makeConversionSnapshot(data)) @@ -258,10 +257,10 @@ export function buildConversionEventV1(data: ConversionEvent): EventV1 { } } -export function formatEvents(events: ProcessableEvent[]): Event { +export function buildLogEvent(events: UserEvent[]): LogEvent { return { url: 'https://logx.optimizely.com/v1/events', httpVerb: 'POST', - params: makeBatchedEventV1(events), + params: makeEventBatch(events), } } diff --git a/lib/event_processor/event_builder/event_helpers.tests.js b/lib/event_processor/event_builder/user_event.tests.js similarity index 98% rename from lib/event_processor/event_builder/event_helpers.tests.js rename to lib/event_processor/event_builder/user_event.tests.js index b241ecaf0..085435f09 100644 --- a/lib/event_processor/event_builder/event_helpers.tests.js +++ b/lib/event_processor/event_builder/user_event.tests.js @@ -19,9 +19,9 @@ import { assert } from 'chai'; import fns from '../../utils/fns'; import * as projectConfig from '../../project_config/project_config'; import * as decision from '../../core/decision'; -import { buildImpressionEvent, buildConversionEvent } from './event_helpers'; +import { buildImpressionEvent, buildConversionEvent } from './user_event'; -describe('lib/event_builder/event_helpers', function() { +describe('user_event', function() { var configObj; beforeEach(function() { diff --git a/lib/event_processor/event_builder/event_helpers.ts b/lib/event_processor/event_builder/user_event.ts similarity index 78% rename from lib/event_processor/event_builder/event_helpers.ts rename to lib/event_processor/event_builder/user_event.ts index 58b5cdb08..4db0aa8a4 100644 --- a/lib/event_processor/event_builder/event_helpers.ts +++ b/lib/event_processor/event_builder/user_event.ts @@ -1,5 +1,5 @@ /** - * Copyright 2019-2022, 2024, 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,15 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getLogger } from '../../modules/logging'; - -import fns from '../../utils/fns'; -import * as eventTagUtils from '../../utils/event_tag_utils'; -import * as attributesValidator from '../../utils/attributes_validator'; -import * as decision from '../../core/decision'; - -import { EventTags, UserAttributes } from '../../shared_types'; import { DecisionObj } from '../../core/decision_service'; +import * as decision from '../../core/decision'; +import { isAttributeValid } from '../../utils/attributes_validator'; +import * as eventTagUtils from '../../utils/event_tag_utils'; +import fns from '../../utils/fns'; import { getAttributeId, getEventId, @@ -29,90 +25,105 @@ import { ProjectConfig, } from '../../project_config/project_config'; +import { getLogger } from '../../modules/logging'; +import { UserAttributes } from '../../shared_types'; + const logger = getLogger('EVENT_BUILDER'); -interface ImpressionConfig { - decisionObj: DecisionObj; - userId: string; - flagKey: string; - enabled: boolean; - userAttributes?: UserAttributes; - clientEngine: string; - clientVersion: string; - configObj: ProjectConfig; +export type VisitorAttribute = { + entityId: string + key: string + value: string | number | boolean } -type VisitorAttribute = { - entityId: string; - key: string; - value: string | number | boolean; +type EventContext = { + accountId: string; + projectId: string; + revision: string; + clientName: string; + clientVersion: string; + anonymizeIP: boolean; + botFiltering?: boolean; } -interface ImpressionEvent { - type: 'impression'; +export type BaseUserEvent = { + type: 'impression' | 'conversion'; timestamp: number; uuid: string; + context: EventContext; user: { id: string; attributes: VisitorAttribute[]; }; - context: EventContext; +}; + +export type ImpressionEvent = BaseUserEvent & { + type: 'impression'; + layer: { id: string | null; - }; + } | null; + experiment: { id: string | null; key: string; } | null; + variation: { id: string | null; key: string; } | null; - ruleKey: string, - flagKey: string, - ruleType: string, - enabled: boolean, + ruleKey: string; + flagKey: string; + ruleType: string; + enabled: boolean; +}; + +export type EventTags = { + [key: string]: string | number | null; +}; + +export type ConversionEvent = BaseUserEvent & { + type: 'conversion'; + + event: { + id: string | null; + key: string; + } + + revenue: number | null; + value: number | null; + tags?: EventTags; } -type EventContext = { - accountId: string; - projectId: string; - revision: string; - clientName: string; - clientVersion: string; - anonymizeIP: boolean; - botFiltering: boolean | undefined; +export type UserEvent = ImpressionEvent | ConversionEvent; + +export const areEventContextsEqual = (eventA: UserEvent, eventB: UserEvent): boolean => { + const contextA = eventA.context + const contextB = eventB.context + return ( + contextA.accountId === contextB.accountId && + contextA.projectId === contextB.projectId && + contextA.clientName === contextB.clientName && + contextA.clientVersion === contextB.clientVersion && + contextA.revision === contextB.revision && + contextA.anonymizeIP === contextB.anonymizeIP && + contextA.botFiltering === contextB.botFiltering + ) } -interface ConversionConfig { - eventKey: string; - eventTags?: EventTags; +export type ImpressionConfig = { + decisionObj: DecisionObj; userId: string; + flagKey: string; + enabled: boolean; userAttributes?: UserAttributes; clientEngine: string; clientVersion: string; configObj: ProjectConfig; } -interface ConversionEvent { - type: 'conversion'; - timestamp: number; - uuid: string; - user: { - id: string; - attributes: VisitorAttribute[]; - }; - context: EventContext; - event: { - id: string | null; - key: string; - }; - revenue: number | null; - value: number | null; - tags: EventTags | undefined; -} - /** * Creates an ImpressionEvent object from decision data @@ -179,6 +190,16 @@ export const buildImpressionEvent = function({ }; }; +export type ConversionConfig = { + eventKey: string; + eventTags?: EventTags; + userId: string; + userAttributes?: UserAttributes; + clientEngine: string; + clientVersion: string; + configObj: ProjectConfig; +} + /** * Creates a ConversionEvent object from track * @param {ConversionConfig} config @@ -230,16 +251,17 @@ export const buildConversionEvent = function({ }; }; -function buildVisitorAttributes( + +const buildVisitorAttributes = ( configObj: ProjectConfig, attributes?: UserAttributes -): VisitorAttribute[] { +): VisitorAttribute[] => { const builtAttributes: VisitorAttribute[] = []; // Omit attribute values that are not supported by the log endpoint. if (attributes) { Object.keys(attributes || {}).forEach(function(attributeKey) { const attributeValue = attributes[attributeKey]; - if (attributesValidator.isAttributeValid(attributeKey, attributeValue)) { + if (isAttributeValid(attributeKey, attributeValue)) { const attributeId = getAttributeId(configObj, attributeKey, logger); if (attributeId) { builtAttributes.push({ diff --git a/lib/event_processor/event_dispatcher.ts b/lib/event_processor/event_dispatcher.ts index 3872e6e90..f58c3fea2 100644 --- a/lib/event_processor/event_dispatcher.ts +++ b/lib/event_processor/event_dispatcher.ts @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventV1 } from "./event_builder/build_event_v1"; +import { EventBatch } from "./event_builder/log_event"; export type EventDispatcherResponse = { statusCode?: number } export interface EventDispatcher { - dispatchEvent(event: EventV1Request): Promise + dispatchEvent(event: LogEvent): Promise } -export interface EventV1Request { +export interface LogEvent { url: string httpVerb: 'POST' | 'PUT' | 'GET' | 'PATCH' - params: EventV1, + params: EventBatch, } diff --git a/lib/event_processor/event_processor.ts b/lib/event_processor/event_processor.ts index 1aee1a857..29df80a6c 100644 --- a/lib/event_processor/event_processor.ts +++ b/lib/event_processor/event_processor.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ConversionEvent, ImpressionEvent } from './events' -import { EventV1Request } from './event_dispatcher' +import { ConversionEvent, ImpressionEvent } from './event_builder/user_event' +import { LogEvent } from './event_dispatcher' import { getLogger } from '../modules/logging' import { Service } from '../service' import { Consumer, Fn } from '../utils/type'; @@ -26,5 +26,5 @@ export type ProcessableEvent = ConversionEvent | ImpressionEvent export interface EventProcessor extends Service { process(event: ProcessableEvent): Promise; - onDispatch(handler: Consumer): Fn; + onDispatch(handler: Consumer): Fn; } diff --git a/lib/event_processor/events.ts b/lib/event_processor/events.ts deleted file mode 100644 index 4254a274f..000000000 --- a/lib/event_processor/events.ts +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright 2022, 2024, Optimizely - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * 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. - */ -export type VisitorAttribute = { - entityId: string - key: string - value: string | number | boolean -} - -export interface BaseEvent { - type: 'impression' | 'conversion' - timestamp: number - uuid: string - - // projectConfig stuff - context: { - accountId: string - projectId: string - clientName: string - clientVersion: string - revision: string - anonymizeIP: boolean - botFiltering?: boolean - } -} - -export interface ImpressionEvent extends BaseEvent { - type: 'impression' - - user: { - id: string - attributes: VisitorAttribute[] - } - - layer: { - id: string | null - } | null - - experiment: { - id: string | null - key: string - } | null - - variation: { - id: string | null - key: string - } | null - - ruleKey: string - flagKey: string - ruleType: string - enabled: boolean -} - -export interface ConversionEvent extends BaseEvent { - type: 'conversion' - - user: { - id: string - attributes: VisitorAttribute[] - } - - event: { - id: string | null - key: string - } - - revenue: number | null - value: number | null - tags: EventTags | undefined -} - -export type EventTags = { - [key: string]: string | number | null -} - -export function areEventContextsEqual(eventA: BaseEvent, eventB: BaseEvent): boolean { - const contextA = eventA.context - const contextB = eventB.context - return ( - contextA.accountId === contextB.accountId && - contextA.projectId === contextB.projectId && - contextA.clientName === contextB.clientName && - contextA.clientVersion === contextB.clientVersion && - contextA.revision === contextB.revision && - contextA.anonymizeIP === contextB.anonymizeIP && - contextA.botFiltering === contextB.botFiltering - ) -} diff --git a/lib/event_processor/forwarding_event_processor.spec.ts b/lib/event_processor/forwarding_event_processor.spec.ts index 3675c010f..3651df273 100644 --- a/lib/event_processor/forwarding_event_processor.spec.ts +++ b/lib/event_processor/forwarding_event_processor.spec.ts @@ -17,7 +17,7 @@ import { expect, describe, it, vi } from 'vitest'; import { getForwardingEventProcessor } from './forwarding_event_processor'; import { EventDispatcher } from './event_dispatcher'; -import { formatEvents, makeBatchedEventV1 } from './event_builder/build_event_v1'; +import { buildLogEvent, makeEventBatch } from './event_builder/build_event_v1'; import { createImpressionEvent } from '../tests/mock/create_event'; import { ServiceState } from '../service'; @@ -50,7 +50,7 @@ describe('ForwardingEventProcessor', () => { processor.process(event); expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); const data = mockDispatch.mock.calls[0][0].params; - expect(data).toEqual(makeBatchedEventV1([event])); + expect(data).toEqual(makeEventBatch([event])); }); it('should emit dispatch event when event is dispatched', async() => { @@ -67,9 +67,9 @@ describe('ForwardingEventProcessor', () => { const event = createImpressionEvent(); processor.process(event); expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); - expect(dispatcher.dispatchEvent).toHaveBeenCalledWith(formatEvents([event])); + expect(dispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent([event])); expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith(formatEvents([event])); + expect(listener).toHaveBeenCalledWith(buildLogEvent([event])); }); it('should remove dispatch listener when the function returned from onDispatch is called', async() => { @@ -86,9 +86,9 @@ describe('ForwardingEventProcessor', () => { let event = createImpressionEvent(); processor.process(event); expect(dispatcher.dispatchEvent).toHaveBeenCalledOnce(); - expect(dispatcher.dispatchEvent).toHaveBeenCalledWith(formatEvents([event])); + expect(dispatcher.dispatchEvent).toHaveBeenCalledWith(buildLogEvent([event])); expect(listener).toHaveBeenCalledOnce(); - expect(listener).toHaveBeenCalledWith(formatEvents([event])); + expect(listener).toHaveBeenCalledWith(buildLogEvent([event])); unsub(); event = createImpressionEvent('id-a'); diff --git a/lib/event_processor/forwarding_event_processor.ts b/lib/event_processor/forwarding_event_processor.ts index 99bccabd2..caf0752aa 100644 --- a/lib/event_processor/forwarding_event_processor.ts +++ b/lib/event_processor/forwarding_event_processor.ts @@ -15,17 +15,17 @@ */ -import { EventV1Request } from './event_dispatcher'; +import { LogEvent } from './event_dispatcher'; import { EventProcessor, ProcessableEvent } from './event_processor'; import { EventDispatcher } from '../shared_types'; -import { formatEvents } from './event_builder/build_event_v1'; +import { buildLogEvent } from './event_builder/log_event'; import { BaseService, ServiceState } from '../service'; import { EventEmitter } from '../utils/event_emitter/event_emitter'; import { Consumer, Fn } from '../utils/type'; class ForwardingEventProcessor extends BaseService implements EventProcessor { private dispatcher: EventDispatcher; - private eventEmitter: EventEmitter<{ dispatch: EventV1Request }>; + private eventEmitter: EventEmitter<{ dispatch: LogEvent }>; constructor(dispatcher: EventDispatcher) { super(); @@ -34,7 +34,7 @@ class ForwardingEventProcessor extends BaseService implements EventProcessor { } process(event: ProcessableEvent): Promise { - const formattedEvent = formatEvents([event]); + const formattedEvent = buildLogEvent([event]); const res = this.dispatcher.dispatchEvent(formattedEvent); this.eventEmitter.emit('dispatch', formattedEvent); return res; @@ -61,7 +61,7 @@ class ForwardingEventProcessor extends BaseService implements EventProcessor { this.stopPromise.resolve(); } - onDispatch(handler: Consumer): Fn { + onDispatch(handler: Consumer): Fn { return this.eventEmitter.on('dispatch', handler); } } diff --git a/lib/optimizely/index.tests.js b/lib/optimizely/index.tests.js index f0dd8e00e..b2cabc776 100644 --- a/lib/optimizely/index.tests.js +++ b/lib/optimizely/index.tests.js @@ -6028,7 +6028,6 @@ describe('lib/optimizely', function() { userContext: user, reasons: [], }; - console.log(decisionsMap); assert.deepEqual(Object.values(decisionsMap).length, 2); assert.deepEqual(decision1, expectedDecision1); assert.deepEqual(decision2, expectedDecision2); diff --git a/lib/optimizely/index.ts b/lib/optimizely/index.ts index c2d247b1d..d564fb16f 100644 --- a/lib/optimizely/index.ts +++ b/lib/optimizely/index.ts @@ -41,8 +41,9 @@ import { newErrorDecision } from '../optimizely_decision'; import OptimizelyUserContext from '../optimizely_user_context'; import { ProjectConfigManager } from '../project_config/project_config_manager'; import { createDecisionService, DecisionService, DecisionObj } from '../core/decision_service'; -import { getImpressionEvent, getConversionEvent } from '../event_processor/event_builder'; -import { buildImpressionEvent, buildConversionEvent } from '../event_processor/event_builder/event_helpers'; +// import { getImpressionEvent, getConversionEvent } from '../event_processor/event_builder'; +import { buildLogEvent } from '../event_processor/event_builder/log_event'; +import { buildImpressionEvent, buildConversionEvent, ImpressionEvent } from '../event_processor/event_builder/user_event'; import fns from '../utils/fns'; import { validate } from '../utils/attributes_validator'; import * as eventTagsValidator from '../utils/event_tags_validator'; @@ -300,68 +301,16 @@ export default class Optimizely implements Client { clientVersion: this.clientVersion, configObj: configObj, }); - // TODO is it okay to not pass a projectConfig as second argument - this.eventProcessor.process(impressionEvent); - this.emitNotificationCenterActivate(decisionObj, flagKey, userId, enabled, attributes); - } - - /** - * Emit the ACTIVATE notification on the notificationCenter - * @param {DecisionObj} decisionObj Decision object - * @param {string} flagKey Key for a feature flag - * @param {string} userId ID of user to whom the variation was shown - * @param {boolean} enabled Boolean representing if feature is enabled - * @param {UserAttributes} attributes Optional user attributes - */ - private emitNotificationCenterActivate( - decisionObj: DecisionObj, - flagKey: string, - userId: string, - enabled: boolean, - attributes?: UserAttributes - ): void { - const configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return; - } - - const ruleType = decisionObj.decisionSource; - const experimentKey = decision.getExperimentKey(decisionObj); - const experimentId = decision.getExperimentId(decisionObj); - const variationKey = decision.getVariationKey(decisionObj); - const variationId = decision.getVariationId(decisionObj); - - let experiment; - if (experimentId !== null && variationKey !== '') { - experiment = configObj.experimentIdMap[experimentId]; - } + this.eventProcessor.process(impressionEvent); - const impressionEventOptions = { - attributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: configObj, - experimentId: experimentId, - ruleKey: experimentKey, - flagKey: flagKey, - ruleType: ruleType, - userId: userId, - enabled: enabled, - variationId: variationId, - logger: this.logger, - }; - const impressionEvent = getImpressionEvent(impressionEventOptions); - let variation; - if (experiment && experiment.variationKeyMap && variationKey !== '') { - variation = experiment.variationKeyMap[variationKey]; - } + const logEvent = buildLogEvent([impressionEvent]); this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.ACTIVATE, { - experiment: experiment, + experiment: decisionObj.experiment, userId: userId, attributes: attributes, - variation: variation, - logEvent: impressionEvent, + variation: decisionObj.variation, + logEvent, }); } @@ -413,57 +362,22 @@ export default class Optimizely implements Client { this.logger.log(LOG_LEVEL.INFO, LOG_MESSAGES.TRACK_EVENT, MODULE_NAME, eventKey, userId); // TODO is it okay to not pass a projectConfig as second argument this.eventProcessor.process(conversionEvent); - this.emitNotificationCenterTrack(eventKey, userId, attributes, eventTags); + + const logEvent = buildLogEvent([conversionEvent]); + this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.TRACK, { + eventKey, + userId, + attributes, + eventTags, + logEvent, + }); } catch (e) { this.logger.log(LOG_LEVEL.ERROR, e.message); this.errorHandler.handleError(e); this.logger.log(LOG_LEVEL.ERROR, LOG_MESSAGES.NOT_TRACKING_USER, MODULE_NAME, userId); } } - /** - * Send TRACK event to notificationCenter - * @param {string} eventKey - * @param {string} userId - * @param {UserAttributes} attributes - * @param {EventTags} eventTags Values associated with the event. - */ - private emitNotificationCenterTrack( - eventKey: string, - userId: string, - attributes?: UserAttributes, - eventTags?: EventTags - ): void { - try { - const configObj = this.projectConfigManager.getConfig(); - if (!configObj) { - return; - } - - const conversionEventOptions = { - attributes: attributes, - clientEngine: this.clientEngine, - clientVersion: this.clientVersion, - configObj: configObj, - eventKey: eventKey, - eventTags: eventTags, - logger: this.logger, - userId: userId, - }; - const conversionEvent = getConversionEvent(conversionEventOptions); - - this.notificationCenter.sendNotifications(NOTIFICATION_TYPES.TRACK, { - eventKey: eventKey, - userId: userId, - attributes: attributes, - eventTags: eventTags, - logEvent: conversionEvent, - }); - } catch (ex) { - this.logger.log(LOG_LEVEL.ERROR, ex.message); - this.errorHandler.handleError(ex); - } - } - + /** * Gets variation where visitor will be bucketed. * @param {string} experimentKey diff --git a/lib/utils/event_tag_utils/index.ts b/lib/utils/event_tag_utils/index.ts index 1be540540..9836afa14 100644 --- a/lib/utils/event_tag_utils/index.ts +++ b/lib/utils/event_tag_utils/index.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { EventTags } from '../../event_processor/events'; +import { EventTags } from '../../event_processor/event_builder/user_event'; import { LoggerFacade } from '../../modules/logging'; import { diff --git a/lib/utils/fns/index.ts b/lib/utils/fns/index.ts index 056278548..98606a77a 100644 --- a/lib/utils/fns/index.ts +++ b/lib/utils/fns/index.ts @@ -41,7 +41,7 @@ export function assign(target: any, ...sources: any[]): any { } } -function currentTimestamp(): number { +export function currentTimestamp(): number { return Math.round(new Date().getTime()); }