diff --git a/lib/event_processor/event_builder/log_event.spec.ts b/lib/event_processor/event_builder/log_event.spec.ts index 3dfa07f08..ad3b22b94 100644 --- a/lib/event_processor/event_builder/log_event.spec.ts +++ b/lib/event_processor/event_builder/log_event.spec.ts @@ -17,9 +17,12 @@ import { describe, it, expect } from 'vitest'; import { makeEventBatch, + buildLogEvent, } from './log_event'; -import { ImpressionEvent, ConversionEvent } from './user_event'; +import { ImpressionEvent, ConversionEvent, UserEvent } from './user_event'; +import { Region } from '../../project_config/project_config'; + describe('makeEventBatch', () => { it('should build a batch with single impression event when experiment and variation are defined', () => { @@ -804,3 +807,64 @@ describe('makeEventBatch', () => { }) }) +describe('buildLogEvent', () => { + it('should select the correct URL based on the event context region', () => { + const baseEvent: ImpressionEvent = { + type: 'impression', + timestamp: 69, + uuid: 'uuid', + context: { + accountId: 'accountId', + projectId: 'projectId', + clientName: 'node-sdk', + clientVersion: '3.0.0', + revision: 'revision', + botFiltering: true, + anonymizeIP: true + }, + user: { + id: 'userId', + attributes: [] + }, + layer: { + id: 'layerId' + }, + experiment: { + id: 'expId', + key: 'expKey' + }, + variation: { + id: 'varId', + key: 'varKey' + }, + ruleKey: 'expKey', + flagKey: 'flagKey1', + ruleType: 'experiment', + enabled: true + }; + + // Test for US region + const usEvent = { + ...baseEvent, + context: { + ...baseEvent.context, + region: 'US' as Region + } + }; + + const usResult = buildLogEvent([usEvent]); + expect(usResult.url).toBe('https://logx.optimizely.com/v1/events'); + + // Test for EU region + const euEvent = { + ...baseEvent, + context: { + ...baseEvent.context, + region: 'EU' as Region + } + }; + + const euResult = buildLogEvent([euEvent]); + expect(euResult.url).toBe('https://eu.logx.optimizely.com/v1/events'); + }); +}); diff --git a/lib/event_processor/event_builder/log_event.ts b/lib/event_processor/event_builder/log_event.ts index 0afdccaeb..d3ec940fa 100644 --- a/lib/event_processor/event_builder/log_event.ts +++ b/lib/event_processor/event_builder/log_event.ts @@ -19,10 +19,16 @@ import { CONTROL_ATTRIBUTES } from '../../utils/enums'; import { LogEvent } from '../event_dispatcher/event_dispatcher'; import { EventTags } from '../../shared_types'; +import { Region } from '../../project_config/project_config'; const ACTIVATE_EVENT_KEY = 'campaign_activated' const CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom' +export const logxEndpoint: Record = { + US: 'https://logx.optimizely.com/v1/events', + EU: 'https://eu.logx.optimizely.com/v1/events', +} + export type EventBatch = { account_id: string project_id: string @@ -215,8 +221,11 @@ function makeVisitor(data: ImpressionEvent | ConversionEvent): Visitor { } export function buildLogEvent(events: UserEvent[]): LogEvent { + const region = events[0]?.context.region || 'US'; + const url = logxEndpoint[region]; + return { - url: 'https://logx.optimizely.com/v1/events', + url, httpVerb: 'POST', params: makeEventBatch(events), } diff --git a/lib/event_processor/event_builder/user_event.spec.ts b/lib/event_processor/event_builder/user_event.spec.ts new file mode 100644 index 000000000..e8cb373b3 --- /dev/null +++ b/lib/event_processor/event_builder/user_event.spec.ts @@ -0,0 +1,81 @@ +import { describe, it, expect, vi } from 'vitest'; +import { buildImpressionEvent, buildConversionEvent } from './user_event'; +import { createProjectConfig, ProjectConfig } from '../../project_config/project_config'; +import { DecisionObj } from '../../core/decision_service'; +import testData from '../../tests/test_data'; + +describe('buildImpressionEvent', () => { + it('should use correct region from projectConfig in event context', () => { + const projectConfig = createProjectConfig( + testData.getTestProjectConfig(), + ) + + const experiment = projectConfig.experiments[0]; + const variation = experiment.variations[0]; + + const decisionObj = { + experiment, + variation, + decisionSource: 'experiment', + } as DecisionObj; + + + const impressionEvent = buildImpressionEvent({ + configObj: projectConfig, + decisionObj, + userId: 'test_user', + flagKey: 'test_flag', + enabled: true, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(impressionEvent.context.region).toBe('US'); + + projectConfig.region = 'EU'; + + const impressionEventEU = buildImpressionEvent({ + configObj: projectConfig, + decisionObj, + userId: 'test_user', + flagKey: 'test_flag', + enabled: true, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(impressionEventEU.context.region).toBe('EU'); + }); +}); + +describe('buildConversionEvent', () => { + it('should use correct region from projectConfig in event context', () => { + const projectConfig = createProjectConfig( + testData.getTestProjectConfig(), + ) + + const conversionEvent = buildConversionEvent({ + configObj: projectConfig, + userId: 'test_user', + eventKey: 'test_event', + eventTags: { revenue: 1000 }, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(conversionEvent.context.region).toBe('US'); + + projectConfig.region = 'EU'; + + const conversionEventEU = buildConversionEvent({ + configObj: projectConfig, + userId: 'test_user', + eventKey: 'test_event', + eventTags: { revenue: 1000 }, + clientEngine: 'node-sdk', + clientVersion: '1.0.0', + }); + + expect(conversionEventEU.context.region).toBe('EU'); + }); +}); diff --git a/lib/event_processor/event_builder/user_event.tests.js b/lib/event_processor/event_builder/user_event.tests.js index 19964e931..30f271d0e 100644 --- a/lib/event_processor/event_builder/user_event.tests.js +++ b/lib/event_processor/event_builder/user_event.tests.js @@ -26,6 +26,7 @@ describe('user_event', function() { beforeEach(function() { configObj = { + region: 'US', accountId: 'accountId', projectId: 'projectId', revision: '69', @@ -106,6 +107,7 @@ describe('user_event', function() { timestamp: 100, uuid: 'uuid', context: { + region: 'US', accountId: 'accountId', projectId: 'projectId', revision: '69', @@ -200,6 +202,7 @@ describe('user_event', function() { timestamp: 100, uuid: 'uuid', context: { + region: 'US', accountId: 'accountId', projectId: 'projectId', revision: '69', @@ -270,6 +273,7 @@ describe('user_event', function() { timestamp: 100, uuid: 'uuid', context: { + region: 'US', accountId: 'accountId', projectId: 'projectId', revision: '69', @@ -336,6 +340,7 @@ describe('user_event', function() { timestamp: 100, uuid: 'uuid', context: { + region: 'US', accountId: 'accountId', projectId: 'projectId', revision: '69', diff --git a/lib/event_processor/event_builder/user_event.ts b/lib/event_processor/event_builder/user_event.ts index eaa1734d6..5d098e87b 100644 --- a/lib/event_processor/event_builder/user_event.ts +++ b/lib/event_processor/event_builder/user_event.ts @@ -23,6 +23,7 @@ import { getEventId, getLayerId, ProjectConfig, + Region, } from '../../project_config/project_config'; import { EventTags, UserAttributes } from '../../shared_types'; @@ -35,6 +36,7 @@ export type VisitorAttribute = { } type EventContext = { + region?: Region; accountId: string; projectId: string; revision: string; @@ -96,7 +98,12 @@ export type UserEvent = ImpressionEvent | ConversionEvent; export const areEventContextsEqual = (eventA: UserEvent, eventB: UserEvent): boolean => { const contextA = eventA.context const contextB = eventB.context + + const regionA: Region = contextA.region || 'US'; + const regionB: Region = contextB.region || 'US'; + return ( + regionA === regionB && contextA.accountId === contextB.accountId && contextA.projectId === contextB.projectId && contextA.clientName === contextB.clientName && @@ -127,6 +134,7 @@ const buildBaseEvent = ({ timestamp: fns.currentTimestamp(), uuid: fns.uuid(), context: { + region: configObj.region, accountId: configObj.accountId, projectId: configObj.projectId, revision: configObj.revision, diff --git a/lib/project_config/project_config.spec.ts b/lib/project_config/project_config.spec.ts index 5a0259ee4..b7e7bea31 100644 --- a/lib/project_config/project_config.spec.ts +++ b/lib/project_config/project_config.spec.ts @@ -16,7 +16,7 @@ import { describe, it, expect, beforeEach, afterEach, vi, assert, Mock } from 'vitest'; import { sprintf } from '../utils/fns'; import { keyBy } from '../utils/fns'; -import projectConfig, { ProjectConfig } from './project_config'; +import projectConfig, { ProjectConfig, Region } from './project_config'; import { FEATURE_VARIABLE_TYPES, LOG_LEVEL } from '../utils/enums'; import testDatafile from '../tests/test_data'; import configValidator from '../utils/config_validator'; @@ -40,6 +40,27 @@ const logger = getMockLogger(); describe('createProjectConfig', () => { let configObj: ProjectConfig; + it('should use US region when no region is specified in datafile', () => { + const datafile = testDatafile.getTestProjectConfig(); + const config = projectConfig.createProjectConfig(datafile); + + expect(config.region).toBe('US'); + }); + + it('should parse region specified in datafile correctly', () => { + const datafileUs = testDatafile.getTestProjectConfig(); + datafileUs.region = 'US'; + + const configUs = projectConfig.createProjectConfig(datafileUs); + expect(configUs.region).toBe('US'); + + const datafileEu = testDatafile.getTestProjectConfig(); + datafileEu.region = 'EU'; + const configEu = projectConfig.createProjectConfig(datafileEu); + + expect(configEu.region).toBe('EU'); + }); + it('should set properties correctly when createProjectConfig is called', () => { const testData: Record = testDatafile.getTestProjectConfig(); configObj = projectConfig.createProjectConfig(testData as JSON); diff --git a/lib/project_config/project_config.ts b/lib/project_config/project_config.ts index e91c4743a..23e79989a 100644 --- a/lib/project_config/project_config.ts +++ b/lib/project_config/project_config.ts @@ -70,7 +70,10 @@ interface VariableUsageMap { [id: string]: VariationVariable; } +export type Region = 'US' | 'EU'; + export interface ProjectConfig { + region: Region; revision: string; projectId: string; sdkKey: string; @@ -155,6 +158,10 @@ function createMutationSafeDatafileCopy(datafile: any): ProjectConfig { export const createProjectConfig = function(datafileObj?: JSON, datafileStr: string | null = null): ProjectConfig { const projectConfig = createMutationSafeDatafileCopy(datafileObj); + if (!projectConfig.region) { + projectConfig.region = 'US'; // Default to US region if not specified + } + projectConfig.__datafileStr = datafileStr === null ? JSON.stringify(datafileObj) : datafileStr; /*