Skip to content

[FSSDK-11455] Java - Add SDK Multi-Region Support for Data Hosting #573

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 8 commits into from
Aug 1, 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
Original file line number Diff line number Diff line change
Expand Up @@ -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<Attribute> attributes;
Expand Down Expand Up @@ -115,6 +116,7 @@ public DatafileProjectConfig(String accountId, String projectId, String version,
anonymizeIP,
false,
null,
null,
projectId,
revision,
null,
Expand All @@ -138,6 +140,7 @@ public DatafileProjectConfig(String accountId,
boolean anonymizeIP,
boolean sendFlagDecisions,
Boolean botFiltering,
String region,
String projectId,
String revision,
String sdkKey,
Expand All @@ -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);
Expand Down Expand Up @@ -434,6 +438,11 @@ public Boolean getBotFiltering() {
return botFiltering;
}

@Override
public String getRegion() {
return region;
}

@Override
public List<Group> getGroups() {
return groups;
Expand Down Expand Up @@ -612,6 +621,7 @@ public String toString() {
", version='" + version + '\'' +
", anonymizeIP=" + anonymizeIP +
", botFiltering=" + botFiltering +
", region=" + region +
", attributes=" + attributes +
", audiences=" + audiences +
", typedAudiences=" + typedAudiences +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,6 @@ public String toString() {
return version;
}
}

String getRegion();
}
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, String> 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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -52,6 +51,7 @@ public static LogEvent createLogEvent(UserEvent userEvent) {
public static LogEvent createLogEvent(List<UserEvent> userEvents) {
EventBatch.Builder builder = new EventBatch.Builder();
List<Visitor> visitors = new ArrayList<>(userEvents.size());
String eventEndpoint = "https://logx.optimizely.com/v1/events";

for (UserEvent userEvent: userEvents) {

Expand All @@ -71,6 +71,8 @@ public static LogEvent createLogEvent(List<UserEvent> userEvents) {
UserContext userContext = userEvent.getUserContext();
ProjectConfig projectConfig = userContext.getProjectConfig();

eventEndpoint = EventEndpoints.getEndpointForRegion(projectConfig.getRegion());

builder
.setClientName(ClientEngineInfo.getClientEngineName())
.setClientVersion(BuildVersionInfo.getClientVersion())
Expand All @@ -85,7 +87,7 @@ public static LogEvent createLogEvent(List<UserEvent> 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -1564,6 +1565,7 @@ public static ProjectConfig generateValidProjectConfigV4() {
ANONYMIZE_IP,
SEND_FLAG_DECISIONS,
BOT_FILTERING,
REGION,
PROJECT_ID,
REVISION,
SDK_KEY,
Expand Down Expand Up @@ -1666,6 +1668,7 @@ public static ProjectConfig generateValidProjectConfigV4_holdout() {
ANONYMIZE_IP,
SEND_FLAG_DECISIONS,
BOT_FILTERING,
REGION,
PROJECT_ID,
REVISION,
SDK_KEY,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ private ProjectConfig generateOptimizelyConfig() {
true,
true,
true,
"US",
"3918735994",
"1480511547",
"ValidProjectConfigV4",
Expand Down
Loading