diff --git a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java index 969eb8fb6..f9267d257 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java @@ -63,6 +63,7 @@ public class DatafileProjectConfig implements ProjectConfig { private final boolean anonymizeIP; private final boolean sendFlagDecisions; private final Boolean botFiltering; + private final String region; private final String hostForODP; private final String publicKeyForODP; private final List attributes; @@ -115,6 +116,7 @@ public DatafileProjectConfig(String accountId, String projectId, String version, anonymizeIP, false, null, + null, projectId, revision, null, @@ -138,6 +140,7 @@ public DatafileProjectConfig(String accountId, boolean anonymizeIP, boolean sendFlagDecisions, Boolean botFiltering, + String region, String projectId, String revision, String sdkKey, @@ -162,6 +165,7 @@ public DatafileProjectConfig(String accountId, this.anonymizeIP = anonymizeIP; this.sendFlagDecisions = sendFlagDecisions; this.botFiltering = botFiltering; + this.region = region != null ? region : "US"; this.attributes = Collections.unmodifiableList(attributes); this.audiences = Collections.unmodifiableList(audiences); @@ -434,6 +438,11 @@ public Boolean getBotFiltering() { return botFiltering; } + @Override + public String getRegion() { + return region; + } + @Override public List getGroups() { return groups; @@ -612,6 +621,7 @@ public String toString() { ", version='" + version + '\'' + ", anonymizeIP=" + anonymizeIP + ", botFiltering=" + botFiltering + + ", region=" + region + ", attributes=" + attributes + ", audiences=" + audiences + ", typedAudiences=" + typedAudiences + diff --git a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java index 96a0c6488..c992d068d 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java +++ b/core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java @@ -142,4 +142,6 @@ public String toString() { return version; } } + + String getRegion(); } diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java index 499a5fc5c..99b06c447 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileGsonDeserializer.java @@ -121,11 +121,18 @@ public ProjectConfig deserialize(JsonElement json, Type typeOfT, JsonDeserializa sendFlagDecisions = jsonObject.get("sendFlagDecisions").getAsBoolean(); } + String region = "US"; + + if (jsonObject.has("region")) { + region = jsonObject.get("region").getAsString(); + } + return new DatafileProjectConfig( accountId, anonymizeIP, sendFlagDecisions, botFiltering, + region, projectId, revision, sdkKey, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java index e38425cf4..2dfc60b24 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/DatafileJacksonDeserializer.java @@ -95,11 +95,18 @@ public DatafileProjectConfig deserialize(JsonParser parser, DeserializationConte } } + String region = "US"; + + if (node.hasNonNull("region")) { + region = node.get("region").textValue(); + } + return new DatafileProjectConfig( accountId, anonymizeIP, sendFlagDecisions, botFiltering, + region, projectId, revision, sdkKey, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java index 4582e4749..e3552f490 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java @@ -100,11 +100,17 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse sendFlagDecisions = rootObject.getBoolean("sendFlagDecisions"); } + String region = "US"; // Default to US + if (rootObject.has("region")) { + String regionString = rootObject.getString("region"); + } + return new DatafileProjectConfig( accountId, anonymizeIP, sendFlagDecisions, botFiltering, + region, projectId, revision, sdkKey, diff --git a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java index b9a170880..419d59995 100644 --- a/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java +++ b/core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java @@ -103,11 +103,17 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse sendFlagDecisions = (Boolean) rootObject.get("sendFlagDecisions"); } + String region = "US"; // Default to US + if (rootObject.containsKey("region")) { + String regionString = (String) rootObject.get("region"); + } + return new DatafileProjectConfig( accountId, anonymizeIP, sendFlagDecisions, botFiltering, + region, projectId, revision, sdkKey, diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/EventEndpoints.java b/core-api/src/main/java/com/optimizely/ab/event/internal/EventEndpoints.java new file mode 100644 index 000000000..3035a0c88 --- /dev/null +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/EventEndpoints.java @@ -0,0 +1,47 @@ +/** + * + * Copyright 2016-2020, 2022, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package com.optimizely.ab.event.internal; +import java.util.HashMap; +import java.util.Map; + +/** + * EventEndpoints provides region-specific endpoint URLs for Optimizely events. + * Similar to the TypeScript logxEndpoint configuration. + */ +public class EventEndpoints { + + private static final Map LOGX_ENDPOINTS = new HashMap<>(); + + static { + LOGX_ENDPOINTS.put("US", "https://logx.optimizely.com/v1/events"); + LOGX_ENDPOINTS.put("EU", "https://eu.logx.optimizely.com/v1/events"); + } + + /** + * Get the event endpoint URL for the specified region. + * Defaults to US region endpoint if region is null. + * + * @param region the region for which to get the endpoint + * @return the endpoint URL for the specified region, or US endpoint if region is null + */ + public static String getEndpointForRegion(String region) { + if (region != null && region.equals("EU")) { + return LOGX_ENDPOINTS.get("EU"); + } + return LOGX_ENDPOINTS.get("US"); + } +} diff --git a/core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java b/core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java index 47839810d..f200f963d 100644 --- a/core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java +++ b/core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java @@ -42,7 +42,6 @@ */ public class EventFactory { private static final Logger logger = LoggerFactory.getLogger(EventFactory.class); - public static final String EVENT_ENDPOINT = "https://logx.optimizely.com/v1/events"; // Should be part of the datafile private static final String ACTIVATE_EVENT_KEY = "campaign_activated"; public static LogEvent createLogEvent(UserEvent userEvent) { @@ -52,6 +51,7 @@ public static LogEvent createLogEvent(UserEvent userEvent) { public static LogEvent createLogEvent(List userEvents) { EventBatch.Builder builder = new EventBatch.Builder(); List visitors = new ArrayList<>(userEvents.size()); + String eventEndpoint = "https://logx.optimizely.com/v1/events"; for (UserEvent userEvent: userEvents) { @@ -71,6 +71,8 @@ public static LogEvent createLogEvent(List userEvents) { UserContext userContext = userEvent.getUserContext(); ProjectConfig projectConfig = userContext.getProjectConfig(); + eventEndpoint = EventEndpoints.getEndpointForRegion(projectConfig.getRegion()); + builder .setClientName(ClientEngineInfo.getClientEngineName()) .setClientVersion(BuildVersionInfo.getClientVersion()) @@ -85,7 +87,7 @@ public static LogEvent createLogEvent(List userEvents) { } builder.setVisitors(visitors); - return new LogEvent(LogEvent.RequestMethod.POST, EVENT_ENDPOINT, Collections.emptyMap(), builder.build()); + return new LogEvent(LogEvent.RequestMethod.POST, eventEndpoint, Collections.emptyMap(), builder.build()); } private static Visitor createVisitor(ImpressionEvent impressionEvent) { diff --git a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java index a59721d4f..0d8f5d3c0 100644 --- a/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java +++ b/core-api/src/test/java/com/optimizely/ab/config/ValidProjectConfigV4.java @@ -41,6 +41,7 @@ public class ValidProjectConfigV4 { private static final String ENVIRONMENT_KEY = "production"; private static final String VERSION = "4"; private static final Boolean SEND_FLAG_DECISIONS = true; + private static final String REGION = "US"; // attributes private static final String ATTRIBUTE_HOUSE_ID = "553339214"; @@ -1564,6 +1565,7 @@ public static ProjectConfig generateValidProjectConfigV4() { ANONYMIZE_IP, SEND_FLAG_DECISIONS, BOT_FILTERING, + REGION, PROJECT_ID, REVISION, SDK_KEY, @@ -1666,6 +1668,7 @@ public static ProjectConfig generateValidProjectConfigV4_holdout() { ANONYMIZE_IP, SEND_FLAG_DECISIONS, BOT_FILTERING, + REGION, PROJECT_ID, REVISION, SDK_KEY, diff --git a/core-api/src/test/java/com/optimizely/ab/event/ForwardingEventProcessorTest.java b/core-api/src/test/java/com/optimizely/ab/event/ForwardingEventProcessorTest.java index 591b73129..30f62d3c9 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/ForwardingEventProcessorTest.java +++ b/core-api/src/test/java/com/optimizely/ab/event/ForwardingEventProcessorTest.java @@ -50,7 +50,7 @@ public void setUp() throws Exception { eventProcessor = new ForwardingEventProcessor(logEvent -> { assertNotNull(logEvent.getEventBatch()); assertEquals(logEvent.getRequestMethod(), LogEvent.RequestMethod.POST); - assertEquals(logEvent.getEndpointUrl(), EventFactory.EVENT_ENDPOINT); + assertEquals(logEvent.getEndpointUrl(), EventEndpoints.getEndpointForRegion("US")); atomicBoolean.set(true); }, notificationCenter); } diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/EventEndpointsTest.java b/core-api/src/test/java/com/optimizely/ab/event/internal/EventEndpointsTest.java new file mode 100644 index 000000000..cf2016a3e --- /dev/null +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/EventEndpointsTest.java @@ -0,0 +1,68 @@ +/** + * + * Copyright 2025, Optimizely and contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * 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. + */ +package com.optimizely.ab.event.internal; + +import org.junit.Test; +import static org.junit.Assert.*; + +/** + * Tests for EventEndpoints class to test event endpoints + */ +public class EventEndpointsTest { + + @Test + public void testGetEndpointForUSRegion() { + String endpoint = EventEndpoints.getEndpointForRegion("US"); + assertEquals("https://logx.optimizely.com/v1/events", endpoint); + } + + @Test + public void testGetEndpointForEURegion() { + String endpoint = EventEndpoints.getEndpointForRegion("EU"); + assertEquals("https://eu.logx.optimizely.com/v1/events", endpoint); + } + + @Test + public void testGetDefaultEndpoint() { + String defaultEndpoint = EventEndpoints.getEndpointForRegion("US"); + assertEquals("https://logx.optimizely.com/v1/events", defaultEndpoint); + } + + @Test + public void testGetEndpointForNullRegion() { + String endpoint = EventEndpoints.getEndpointForRegion(null); + assertEquals("https://logx.optimizely.com/v1/events", endpoint); + } + + @Test + public void testGetEndpointForInvalidRegion() { + String endpoint = EventEndpoints.getEndpointForRegion("ZZ"); + assertEquals("https://logx.optimizely.com/v1/events", endpoint); + } + + @Test + public void testDefaultBehaviorAlwaysReturnsUS() { + // Test that both null region and default endpoint return the same US endpoint + String nullRegionEndpoint = EventEndpoints.getEndpointForRegion(null); + String defaultEndpoint = EventEndpoints.getEndpointForRegion("US"); + String usEndpoint = EventEndpoints.getEndpointForRegion("US"); + + assertEquals("All should return US endpoint", usEndpoint, nullRegionEndpoint); + assertEquals("All should return US endpoint", usEndpoint, defaultEndpoint); + assertEquals("Should be US endpoint", "https://logx.optimizely.com/v1/events", nullRegionEndpoint); + } +} diff --git a/core-api/src/test/java/com/optimizely/ab/event/internal/EventFactoryTest.java b/core-api/src/test/java/com/optimizely/ab/event/internal/EventFactoryTest.java index e347074a8..08a8b7da9 100644 --- a/core-api/src/test/java/com/optimizely/ab/event/internal/EventFactoryTest.java +++ b/core-api/src/test/java/com/optimizely/ab/event/internal/EventFactoryTest.java @@ -140,7 +140,7 @@ public void createImpressionEventPassingUserAgentAttribute() throws Exception { userId, attributeMap); // verify that request endpoint is correct - assertThat(impressionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT)); + assertThat(impressionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion()))); EventBatch eventBatch = gson.fromJson(impressionEvent.getBody(), EventBatch.class); @@ -207,7 +207,7 @@ public void createImpressionEvent() throws Exception { userId, attributeMap); // verify that request endpoint is correct - assertThat(impressionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT)); + assertThat(impressionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion()))); EventBatch eventBatch = gson.fromJson(impressionEvent.getBody(), EventBatch.class); @@ -616,7 +616,7 @@ public void createConversionEvent() throws Exception { eventTagMap); // verify that the request endpoint is correct - assertThat(conversionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT)); + assertThat(conversionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion()))); EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class); @@ -678,7 +678,7 @@ public void createConversionEventPassingUserAgentAttribute() throws Exception { eventTagMap); // verify that the request endpoint is correct - assertThat(conversionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT)); + assertThat(conversionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion()))); EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class); @@ -944,7 +944,7 @@ public void createImpressionEventWithBucketingId() throws Exception { userId, attributeMap); // verify that request endpoint is correct - assertThat(impressionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT)); + assertThat(impressionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion()))); EventBatch impression = gson.fromJson(impressionEvent.getBody(), EventBatch.class); @@ -993,7 +993,7 @@ public void createConversionEventWithBucketingId() throws Exception { eventTagMap); // verify that the request endpoint is correct - assertThat(conversionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT)); + assertThat(conversionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion()))); EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class); diff --git a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java index 7d165ffbc..8cce38389 100644 --- a/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java +++ b/core-api/src/test/java/com/optimizely/ab/optimizelyconfig/OptimizelyConfigServiceTest.java @@ -212,6 +212,7 @@ private ProjectConfig generateOptimizelyConfig() { true, true, true, + "US", "3918735994", "1480511547", "ValidProjectConfigV4",