Skip to content

[FSSDK-11459] Ruby - Add SDK Multi-Region Support for Data Hosting #365

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

Open
wants to merge 21 commits into
base: master
Choose a base branch
from
Open
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
6 changes: 5 additions & 1 deletion lib/optimizely/config/datafile_project_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ class DatafileProjectConfig < ProjectConfig
:group_id_map, :rollout_id_map, :rollout_experiment_id_map, :variation_id_map,
:variation_id_to_variable_usage_map, :variation_key_map, :variation_id_map_by_experiment_id,
:variation_key_map_by_experiment_id, :flag_variation_map, :integration_key_map, :integrations,
:public_key_for_odp, :host_for_odp, :all_segments
:public_key_for_odp, :host_for_odp, :all_segments, :region
# Boolean - denotes if Optimizely should remove the last block of visitors' IP address before storing event data
attr_reader :anonymize_ip

Expand Down Expand Up @@ -68,6 +68,10 @@ def initialize(datafile, logger, error_handler)
@rollouts = config.fetch('rollouts', [])
@send_flag_decisions = config.fetch('sendFlagDecisions', false)
@integrations = config.fetch('integrations', [])
@region = config.fetch('region', 'US')

# Default to US region if not specified
@region = 'US' if @region.nil? || @region.empty?

# Json type is represented in datafile as a subtype of string for the sake of backwards compatibility.
# Converting it to a first-class json type while creating Project Config
Expand Down
12 changes: 9 additions & 3 deletions lib/optimizely/event/entity/event_batch.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
module Optimizely
class EventBatch
attr_accessor :account_id, :project_id, :revision, :client_name, :client_version,
:anonymize_ip, :enrich_decisions, :visitors
:anonymize_ip, :enrich_decisions, :visitors, :region

def as_json
{
Expand All @@ -29,13 +29,14 @@ def as_json
client_version: @client_version,
anonymize_ip: @anonymize_ip,
enrich_decisions: @enrich_decisions,
visitors: @visitors
visitors: @visitors,
region: @region
}
end

class Builder
attr_reader :account_id, :project_id, :revision, :client_name, :client_version,
:anonymize_ip, :enrich_decisions, :visitors
:anonymize_ip, :enrich_decisions, :visitors, :region

def build
event_batch = EventBatch.new
Expand All @@ -47,6 +48,7 @@ def build
event_batch.anonymize_ip = @anonymize_ip
event_batch.enrich_decisions = @enrich_decisions
event_batch.visitors = @visitors
event_batch.region = @region || 'US'
event_batch
end

Expand All @@ -62,6 +64,10 @@ def with_revision(revision)
@revision = revision
end

def with_region(region)
@region = region || 'US'
end

def with_client_name(client_name)
@client_name = client_name
end
Expand Down
7 changes: 5 additions & 2 deletions lib/optimizely/event/entity/event_context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,16 @@ def initialize(
anonymize_ip:,
revision:,
client_name:,
client_version:
client_version:,
region:
)
@account_id = account_id
@project_id = project_id
@anonymize_ip = anonymize_ip
@revision = revision
@client_name = client_name
@client_version = client_version
@region = region
end

def as_json
Expand All @@ -43,7 +45,8 @@ def as_json
anonymize_ip: @anonymize_ip,
revision: @revision,
client_name: @client_name,
client_version: @client_version
client_version: @client_version,
region: @region
}
end
end
Expand Down
11 changes: 9 additions & 2 deletions lib/optimizely/event/event_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,10 @@ class EventFactory
# EventFactory builds LogEvent objects from a given user_event.
class << self
CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'
ENDPOINT = 'https://logx.optimizely.com/v1/events'
ENDPOINTS = {
US: 'https://logx.optimizely.com/v1/events',
EU: 'https://eu.logx.optimizely.com/v1/events'
}.freeze
POST_HEADERS = {'Content-Type' => 'application/json'}.freeze
ACTIVATE_EVENT_KEY = 'campaign_activated'

Expand Down Expand Up @@ -64,10 +67,14 @@ def create_log_event(user_events, logger)
builder.with_client_name(user_context[:client_name])
builder.with_anonymize_ip(user_context[:anonymize_ip])
builder.with_enrich_decisions(true)
builder.with_region(user_context[:region])

builder.with_visitors(visitors)
event_batch = builder.build
Event.new(:post, ENDPOINT, event_batch.as_json, POST_HEADERS)

endpoint = ENDPOINTS[user_context[:region].to_s.upcase.to_sym] || ENDPOINTS[:US]

Event.new(:post, endpoint, event_batch.as_json, POST_HEADERS)
end

def build_attribute_list(user_attributes, project_config)
Expand Down
2 changes: 2 additions & 0 deletions lib/optimizely/event/user_event_factory.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def self.create_impression_event(project_config, experiment, variation_id, metad
#
# Returns Event encapsulating the impression event.
event_context = Optimizely::EventContext.new(
region: project_config.region,
account_id: project_config.account_id,
project_id: project_config.project_id,
anonymize_ip: project_config.anonymize_ip,
Expand Down Expand Up @@ -67,6 +68,7 @@ def self.create_conversion_event(project_config, event, user_id, user_attributes
# Returns Event encapsulating the conversion event.

event_context = Optimizely::EventContext.new(
region: project_config.region,
account_id: project_config.account_id,
project_id: project_config.project_id,
anonymize_ip: project_config.anonymize_ip,
Expand Down
18 changes: 14 additions & 4 deletions lib/optimizely/event_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,17 @@ def get_common_params(project_config, user_id, attributes)
revision: project_config.revision,
client_name: CLIENT_ENGINE,
enrich_decisions: true,
client_version: VERSION
client_version: VERSION,
region: project_config.region || 'US'
}
end
end

class EventBuilder < BaseEventBuilder
ENDPOINT = 'https://logx.optimizely.com/v1/events'
ENDPOINTS = {
US: 'https://logx.optimizely.com/v1/events',
EU: 'https://eu.logx.optimizely.com/v1/events'
}.freeze
POST_HEADERS = {'Content-Type' => 'application/json'}.freeze
ACTIVATE_EVENT_KEY = 'campaign_activated'

Expand All @@ -122,11 +126,14 @@ def create_impression_event(project_config, experiment, variation_id, user_id, a
#
# Returns +Event+ encapsulating the impression event.

region = project_config.region || 'US'
event_params = get_common_params(project_config, user_id, attributes)
impression_params = get_impression_params(project_config, experiment, variation_id)
event_params[:visitors][0][:snapshots].push(impression_params)

Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
endpoint = ENDPOINTS[region.to_s.upcase.to_sym]

Event.new(:post, endpoint, event_params, POST_HEADERS)
end

def create_conversion_event(project_config, event, user_id, attributes, event_tags)
Expand All @@ -140,11 +147,14 @@ def create_conversion_event(project_config, event, user_id, attributes, event_ta
#
# Returns +Event+ encapsulating the conversion event.

region = project_config.region || 'US'
event_params = get_common_params(project_config, user_id, attributes)
conversion_params = get_conversion_params(event, event_tags)
event_params[:visitors][0][:snapshots] = [conversion_params]

Event.new(:post, ENDPOINT, event_params, POST_HEADERS)
endpoint = ENDPOINTS[region.to_s.upcase.to_sym]

Event.new(:post, endpoint, event_params, POST_HEADERS)
end

private
Expand Down
2 changes: 2 additions & 0 deletions lib/optimizely/project_config.rb
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ def host_for_odp; end

def all_segments; end

def region; end
Copy link
Preview

Copilot AI Jun 30, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The region method is currently a stub and returns nothing. It should return the instance variable, e.g., def region; @region; end.

Copilot uses AI. Check for mistakes.


def experiment_running?(experiment); end

def get_experiment_from_key(experiment_key); end
Expand Down
18 changes: 18 additions & 0 deletions spec/config/datafile_project_config_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
expect(project_config.sdk_key).to eq(config_body['sdkKey'])
expect(project_config.environment_key).to eq(config_body['environmentKey'])
expect(project_config.send_flag_decisions).to eq(config_body['sendFlagDecisions'])
expect(project_config.region).to eq(config_body['region'])

expected_attribute_key_map = {
'browser_type' => config_body['attributes'][0],
Expand Down Expand Up @@ -756,6 +757,23 @@
expect(project_config.rollout_experiment_id_map).to eq(expected_rollout_experiment_id_map)
end

it 'should use US region when no region is specified in datafile' do
project_config = Optimizely::DatafileProjectConfig.new(config_body_JSON, logger, error_handler)
expect(project_config.region).to eq('US')
end

it 'should parse region specified in datafile correctly' do
project_config_us = Optimizely::DatafileProjectConfig.new(config_body_JSON, logger, error_handler)
expect(project_config_us.region).to eq('US')

config_body_eu = config_body.dup
config_body_eu['region'] = 'EU'
config_body_json = JSON.dump(config_body_eu)
project_config_eu = Optimizely::DatafileProjectConfig.new(config_body_json, logger, error_handler)

expect(project_config_eu.region).to eq('EU')
end

it 'should initialize properties correctly upon creating project with typed audience dict' do
project_config = Optimizely::DatafileProjectConfig.new(JSON.dump(OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES), logger, error_handler)
config_body = OptimizelySpec::CONFIG_DICT_WITH_TYPED_AUDIENCES
Expand Down
6 changes: 4 additions & 2 deletions spec/event/event_entities_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,8 @@
revision: '42',
client_name: Optimizely::CLIENT_ENGINE,
enrich_decisions: true,
client_version: Optimizely::VERSION
client_version: Optimizely::VERSION,
region: 'US'
}

@expected_conversion_payload = {
Expand Down Expand Up @@ -103,7 +104,8 @@
revision: '42',
client_name: Optimizely::CLIENT_ENGINE,
enrich_decisions: true,
client_version: Optimizely::VERSION
client_version: Optimizely::VERSION,
region: 'US'
}
end

Expand Down
Loading