Skip to content

Commit d42622e

Browse files
committed
[FSSDK-11455] Java - Add SDK Multi-Region Support for Data Hosting
1 parent 746e815 commit d42622e

File tree

8 files changed

+176
-8
lines changed

8 files changed

+176
-8
lines changed

core-api/src/main/java/com/optimizely/ab/config/DatafileProjectConfig.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ public class DatafileProjectConfig implements ProjectConfig {
6363
private final boolean anonymizeIP;
6464
private final boolean sendFlagDecisions;
6565
private final Boolean botFiltering;
66+
private final Region region;
6667
private final String hostForODP;
6768
private final String publicKeyForODP;
6869
private final List<Attribute> attributes;
@@ -113,6 +114,7 @@ public DatafileProjectConfig(String accountId, String projectId, String version,
113114
anonymizeIP,
114115
false,
115116
null,
117+
Region.US,
116118
projectId,
117119
revision,
118120
null,
@@ -135,6 +137,7 @@ public DatafileProjectConfig(String accountId,
135137
boolean anonymizeIP,
136138
boolean sendFlagDecisions,
137139
Boolean botFiltering,
140+
Region region,
138141
String projectId,
139142
String revision,
140143
String sdkKey,
@@ -158,6 +161,7 @@ public DatafileProjectConfig(String accountId,
158161
this.anonymizeIP = anonymizeIP;
159162
this.sendFlagDecisions = sendFlagDecisions;
160163
this.botFiltering = botFiltering;
164+
this.region = region != null ? region : Region.US;
161165

162166
this.attributes = Collections.unmodifiableList(attributes);
163167
this.audiences = Collections.unmodifiableList(audiences);
@@ -424,6 +428,11 @@ public Boolean getBotFiltering() {
424428
return botFiltering;
425429
}
426430

431+
@Override
432+
public Region getRegion() {
433+
return region;
434+
}
435+
427436
@Override
428437
public List<Group> getGroups() {
429438
return groups;
@@ -587,6 +596,7 @@ public String toString() {
587596
", version='" + version + '\'' +
588597
", anonymizeIP=" + anonymizeIP +
589598
", botFiltering=" + botFiltering +
599+
", region=" + region +
590600
", attributes=" + attributes +
591601
", audiences=" + audiences +
592602
", typedAudiences=" + typedAudiences +

core-api/src/main/java/com/optimizely/ab/config/ProjectConfig.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,4 +135,21 @@ public String toString() {
135135
return version;
136136
}
137137
}
138+
139+
public enum Region {
140+
US("US"), EU("EU");
141+
142+
private final String region;
143+
144+
Region(String region) {
145+
this.region = region;
146+
}
147+
148+
@Override
149+
public String toString() {
150+
return region;
151+
}
152+
}
153+
154+
Region getRegion();
138155
}

core-api/src/main/java/com/optimizely/ab/config/parser/JsonConfigParser.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,20 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
9393
sendFlagDecisions = rootObject.getBoolean("sendFlagDecisions");
9494
}
9595

96+
ProjectConfig.Region region = ProjectConfig.Region.US; // Default to US
97+
if (rootObject.has("region")) {
98+
String regionString = rootObject.getString("region");
99+
if ("EU".equalsIgnoreCase(regionString)) {
100+
region = ProjectConfig.Region.EU;
101+
}
102+
}
103+
96104
return new DatafileProjectConfig(
97105
accountId,
98106
anonymizeIP,
99107
sendFlagDecisions,
100108
botFiltering,
109+
region,
101110
projectId,
102111
revision,
103112
sdkKey,

core-api/src/main/java/com/optimizely/ab/config/parser/JsonSimpleConfigParser.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,20 @@ public ProjectConfig parseProjectConfig(@Nonnull String json) throws ConfigParse
9696
sendFlagDecisions = (Boolean) rootObject.get("sendFlagDecisions");
9797
}
9898

99+
ProjectConfig.Region region = ProjectConfig.Region.US; // Default to US
100+
if (rootObject.containsKey("region")) {
101+
String regionString = (String) rootObject.get("region");
102+
if ("EU".equalsIgnoreCase(regionString)) {
103+
region = ProjectConfig.Region.EU;
104+
}
105+
}
106+
99107
return new DatafileProjectConfig(
100108
accountId,
101109
anonymizeIP,
102110
sendFlagDecisions,
103111
botFiltering,
112+
region,
104113
projectId,
105114
revision,
106115
sdkKey,
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
*
3+
* Copyright 2016-2020, 2022, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.event.internal;
18+
19+
import com.optimizely.ab.config.ProjectConfig.Region;
20+
import java.util.HashMap;
21+
import java.util.Map;
22+
23+
/**
24+
* EventEndpoints provides region-specific endpoint URLs for Optimizely events.
25+
* Similar to the TypeScript logxEndpoint configuration.
26+
*/
27+
public class EventEndpoints {
28+
29+
private static final Map<Region, String> LOGX_ENDPOINTS = new HashMap<>();
30+
31+
static {
32+
LOGX_ENDPOINTS.put(Region.US, "https://logx.optimizely.com/v1/events");
33+
LOGX_ENDPOINTS.put(Region.EU, "https://eu.logx.optimizely.com/v1/events");
34+
}
35+
36+
/**
37+
* Get the event endpoint URL for the specified region.
38+
* Defaults to US region endpoint if region is null.
39+
*
40+
* @param region the region for which to get the endpoint
41+
* @return the endpoint URL for the specified region, or US endpoint if region is null
42+
*/
43+
public static String getEndpointForRegion(Region region) {
44+
if (region == null) {
45+
return LOGX_ENDPOINTS.get(Region.US);
46+
}
47+
return LOGX_ENDPOINTS.get(region);
48+
}
49+
50+
/**
51+
* Get the default event endpoint URL (US region).
52+
*
53+
* @return the default endpoint URL
54+
*/
55+
public static String getDefaultEndpoint() {
56+
return LOGX_ENDPOINTS.get(Region.US);
57+
}
58+
}

core-api/src/main/java/com/optimizely/ab/event/internal/EventFactory.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@
4242
*/
4343
public class EventFactory {
4444
private static final Logger logger = LoggerFactory.getLogger(EventFactory.class);
45-
public static final String EVENT_ENDPOINT = "https://logx.optimizely.com/v1/events"; // Should be part of the datafile
4645
private static final String ACTIVATE_EVENT_KEY = "campaign_activated";
4746

4847
public static LogEvent createLogEvent(UserEvent userEvent) {
@@ -52,6 +51,7 @@ public static LogEvent createLogEvent(UserEvent userEvent) {
5251
public static LogEvent createLogEvent(List<UserEvent> userEvents) {
5352
EventBatch.Builder builder = new EventBatch.Builder();
5453
List<Visitor> visitors = new ArrayList<>(userEvents.size());
54+
String eventEndpoint = "https://logx.optimizely.com/v1/events";
5555

5656
for (UserEvent userEvent: userEvents) {
5757

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

74+
eventEndpoint = EventEndpoints.getEndpointForRegion(projectConfig.getRegion());
75+
7476
builder
7577
.setClientName(ClientEngineInfo.getClientEngineName())
7678
.setClientVersion(BuildVersionInfo.getClientVersion())
@@ -85,7 +87,7 @@ public static LogEvent createLogEvent(List<UserEvent> userEvents) {
8587
}
8688

8789
builder.setVisitors(visitors);
88-
return new LogEvent(LogEvent.RequestMethod.POST, EVENT_ENDPOINT, Collections.emptyMap(), builder.build());
90+
return new LogEvent(LogEvent.RequestMethod.POST, eventEndpoint, Collections.emptyMap(), builder.build());
8991
}
9092

9193
private static Visitor createVisitor(ImpressionEvent impressionEvent) {
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
*
3+
* Copyright 2025, Optimizely and contributors
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package com.optimizely.ab.event.internal;
18+
19+
import com.optimizely.ab.config.ProjectConfig.Region;
20+
import org.junit.Test;
21+
import static org.junit.Assert.*;
22+
23+
/**
24+
* Tests for EventEndpoints class
25+
*/
26+
public class EventEndpointsTest {
27+
28+
@Test
29+
public void testGetEndpointForUSRegion() {
30+
String endpoint = EventEndpoints.getEndpointForRegion(Region.US);
31+
assertEquals("https://logx.optimizely.com/v1/events", endpoint);
32+
}
33+
34+
@Test
35+
public void testGetEndpointForEURegion() {
36+
String endpoint = EventEndpoints.getEndpointForRegion(Region.EU);
37+
assertEquals("https://eu.logx.optimizely.com/v1/events", endpoint);
38+
}
39+
40+
@Test
41+
public void testGetDefaultEndpoint() {
42+
String defaultEndpoint = EventEndpoints.getDefaultEndpoint();
43+
assertEquals("https://logx.optimizely.com/v1/events", defaultEndpoint);
44+
}
45+
46+
@Test
47+
public void testGetEndpointForNullRegion() {
48+
String endpoint = EventEndpoints.getEndpointForRegion(null);
49+
assertEquals("https://logx.optimizely.com/v1/events", endpoint);
50+
}
51+
52+
@Test
53+
public void testDefaultBehaviorAlwaysReturnsUS() {
54+
// Test that both null region and default endpoint return the same US endpoint
55+
String nullRegionEndpoint = EventEndpoints.getEndpointForRegion(null);
56+
String defaultEndpoint = EventEndpoints.getDefaultEndpoint();
57+
String usEndpoint = EventEndpoints.getEndpointForRegion(Region.US);
58+
59+
assertEquals("All should return US endpoint", usEndpoint, nullRegionEndpoint);
60+
assertEquals("All should return US endpoint", usEndpoint, defaultEndpoint);
61+
assertEquals("Should be US endpoint", "https://logx.optimizely.com/v1/events", nullRegionEndpoint);
62+
}
63+
}

core-api/src/test/java/com/optimizely/ab/event/internal/EventFactoryTest.java

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ public void createImpressionEventPassingUserAgentAttribute() throws Exception {
140140
userId, attributeMap);
141141

142142
// verify that request endpoint is correct
143-
assertThat(impressionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT));
143+
assertThat(impressionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion())));
144144

145145
EventBatch eventBatch = gson.fromJson(impressionEvent.getBody(), EventBatch.class);
146146

@@ -207,7 +207,7 @@ public void createImpressionEvent() throws Exception {
207207
userId, attributeMap);
208208

209209
// verify that request endpoint is correct
210-
assertThat(impressionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT));
210+
assertThat(impressionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion())));
211211

212212
EventBatch eventBatch = gson.fromJson(impressionEvent.getBody(), EventBatch.class);
213213

@@ -616,7 +616,7 @@ public void createConversionEvent() throws Exception {
616616
eventTagMap);
617617

618618
// verify that the request endpoint is correct
619-
assertThat(conversionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT));
619+
assertThat(conversionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion())));
620620

621621
EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class);
622622

@@ -678,7 +678,7 @@ public void createConversionEventPassingUserAgentAttribute() throws Exception {
678678
eventTagMap);
679679

680680
// verify that the request endpoint is correct
681-
assertThat(conversionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT));
681+
assertThat(conversionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion())));
682682

683683
EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class);
684684

@@ -944,7 +944,7 @@ public void createImpressionEventWithBucketingId() throws Exception {
944944
userId, attributeMap);
945945

946946
// verify that request endpoint is correct
947-
assertThat(impressionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT));
947+
assertThat(impressionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion())));
948948

949949
EventBatch impression = gson.fromJson(impressionEvent.getBody(), EventBatch.class);
950950

@@ -993,7 +993,7 @@ public void createConversionEventWithBucketingId() throws Exception {
993993
eventTagMap);
994994

995995
// verify that the request endpoint is correct
996-
assertThat(conversionEvent.getEndpointUrl(), is(EventFactory.EVENT_ENDPOINT));
996+
assertThat(conversionEvent.getEndpointUrl(), is(EventEndpoints.getEndpointForRegion(validProjectConfig.getRegion())));
997997

998998
EventBatch conversion = gson.fromJson(conversionEvent.getBody(), EventBatch.class);
999999

0 commit comments

Comments
 (0)