Skip to content

[FSSDK-11207] add multi region support for logx events #1072

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 65 additions & 1 deletion lib/event_processor/event_builder/log_event.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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');
});
});
11 changes: 10 additions & 1 deletion lib/event_processor/event_builder/log_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Region, string> = {
US: 'https://logx.optimizely.com/v1/events',
EU: 'https://eu.logx.optimizely.com/v1/events',
}

export type EventBatch = {
account_id: string
project_id: string
Expand Down Expand Up @@ -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),
}
Expand Down
81 changes: 81 additions & 0 deletions lib/event_processor/event_builder/user_event.spec.ts
Original file line number Diff line number Diff line change
@@ -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');
});
});
5 changes: 5 additions & 0 deletions lib/event_processor/event_builder/user_event.tests.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe('user_event', function() {

beforeEach(function() {
configObj = {
region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
Expand Down Expand Up @@ -106,6 +107,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
Expand Down Expand Up @@ -200,6 +202,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
Expand Down Expand Up @@ -270,6 +273,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
Expand Down Expand Up @@ -336,6 +340,7 @@ describe('user_event', function() {
timestamp: 100,
uuid: 'uuid',
context: {
region: 'US',
accountId: 'accountId',
projectId: 'projectId',
revision: '69',
Expand Down
8 changes: 8 additions & 0 deletions lib/event_processor/event_builder/user_event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
getEventId,
getLayerId,
ProjectConfig,
Region,
} from '../../project_config/project_config';

import { EventTags, UserAttributes } from '../../shared_types';
Expand All @@ -35,6 +36,7 @@ export type VisitorAttribute = {
}

type EventContext = {
region?: Region;
accountId: string;
projectId: string;
revision: string;
Expand Down Expand Up @@ -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 &&
Expand Down Expand Up @@ -127,6 +134,7 @@ const buildBaseEvent = <T extends EventType>({
timestamp: fns.currentTimestamp(),
uuid: fns.uuid(),
context: {
region: configObj.region,
accountId: configObj.accountId,
projectId: configObj.projectId,
revision: configObj.revision,
Expand Down
23 changes: 22 additions & 1 deletion lib/project_config/project_config.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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<string, any> = testDatafile.getTestProjectConfig();
configObj = projectConfig.createProjectConfig(testData as JSON);
Expand Down
7 changes: 7 additions & 0 deletions lib/project_config/project_config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

/*
Expand Down
Loading