Skip to content

Commit cfc1efa

Browse files
feat: add odp segment manager (#310)
* add odp segment manager * add odp config
1 parent 6c12bfd commit cfc1efa

File tree

5 files changed

+449
-33
lines changed

5 files changed

+449
-33
lines changed

lib/optimizely/odp/odp_config.rb

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
# Copyright 2022, Optimizely and contributors
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
require 'optimizely/logger'
20+
21+
module Optimizely
22+
class OdpConfig
23+
# Contains configuration used for ODP integration.
24+
#
25+
# @param api_host - The host URL for the ODP audience segments API (optional).
26+
# @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional).
27+
# @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
28+
def initialize(api_key = nil, api_host = nil, segments_to_check = [])
29+
@api_key = api_key
30+
@api_host = api_host
31+
@segments_to_check = segments_to_check
32+
@mutex = Mutex.new
33+
end
34+
35+
# Replaces the existing configuration
36+
#
37+
# @param api_host - The host URL for the ODP audience segments API (optional).
38+
# @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional).
39+
# @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
40+
#
41+
# @return - True if the provided values were different than the existing values.
42+
43+
def update(api_key = nil, api_host = nil, segments_to_check = [])
44+
@mutex.synchronize do
45+
break false if @api_key == api_key && @api_host == api_host && @segments_to_check == segments_to_check
46+
47+
@api_key = api_key
48+
@api_host = api_host
49+
@segments_to_check = segments_to_check
50+
break true
51+
end
52+
end
53+
54+
# Returns the api host for odp connections
55+
#
56+
# @return - The api host.
57+
58+
def api_host
59+
@mutex.synchronize { @api_host.clone }
60+
end
61+
62+
# Returns the api host for odp connections
63+
#
64+
# @return - The api host.
65+
66+
def api_host=(api_host)
67+
@mutex.synchronize { @api_host = api_host.clone }
68+
end
69+
70+
# Returns the api key for odp connections
71+
#
72+
# @return - The api key.
73+
74+
def api_key
75+
@mutex.synchronize { @api_key.clone }
76+
end
77+
78+
# Replace the api key with the provided string
79+
#
80+
# @param api_key - An api key
81+
82+
def api_key=(api_key)
83+
@mutex.synchronize { @api_key = api_key.clone }
84+
end
85+
86+
# Returns An array of qualified segments for this user
87+
#
88+
# @return - An array of segments names.
89+
90+
def segments_to_check
91+
@mutex.synchronize { @segments_to_check.clone }
92+
end
93+
94+
# Replace qualified segments with provided segments
95+
#
96+
# @param segments - An array of segment names
97+
98+
def segments_to_check=(segments_to_check)
99+
@mutex.synchronize { @segments_to_check = segments_to_check.clone }
100+
end
101+
102+
# Returns True if odp is integrated
103+
#
104+
# @return - bool
105+
106+
def odp_integrated?
107+
@mutex.synchronize { !@api_key.nil? && !@api_host.nil? }
108+
end
109+
end
110+
end
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# frozen_string_literal: true
2+
3+
#
4+
# Copyright 2022, Optimizely and contributors
5+
#
6+
# Licensed under the Apache License, Version 2.0 (the "License");
7+
# you may not use this file except in compliance with the License.
8+
# You may obtain a copy of the License at
9+
#
10+
# http://www.apache.org/licenses/LICENSE-2.0
11+
#
12+
# Unless required by applicable law or agreed to in writing, software
13+
# distributed under the License is distributed on an "AS IS" BASIS,
14+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
# See the License for the specific language governing permissions and
16+
# limitations under the License.
17+
#
18+
19+
require 'optimizely/logger'
20+
require_relative 'zaius_graphql_api_manager'
21+
22+
module Optimizely
23+
class OdpSegmentManager
24+
# Schedules connections to ODP for audience segmentation and caches the results
25+
attr_reader :odp_config, :segments_cache, :zaius_manager, :logger
26+
27+
def initialize(odp_config, segments_cache, api_manager = nil, logger = nil, proxy_config = nil)
28+
@odp_config = odp_config
29+
@logger = logger || NoOpLogger.new
30+
@zaius_manager = api_manager || ZaiusGraphQLApiManager.new(logger: @logger, proxy_config: proxy_config)
31+
@segments_cache = segments_cache
32+
end
33+
34+
# Returns qualified segments for the user from the cache or the ODP server if not in the cache.
35+
#
36+
# @param user_key - The key for identifying the id type.
37+
# @param user_value - The id itself.
38+
# @param options - An array of OptimizelySegmentOptions used to ignore and/or reset the cache.
39+
#
40+
# @return - Array of qualified segments.
41+
def fetch_qualified_segments(user_key, user_value, options)
42+
unless @odp_config.odp_integrated?
43+
@logger.log(Logger::ERROR, format(Optimizely::Helpers::Constants::ODP_LOGS[:FETCH_SEGMENTS_FAILED], 'ODP is not enabled'))
44+
return nil
45+
end
46+
47+
odp_api_key = @odp_config.api_key
48+
odp_api_host = @odp_config.api_host
49+
segments_to_check = @odp_config&.segments_to_check
50+
51+
unless segments_to_check&.size&.positive?
52+
@logger.log(Logger::DEBUG, 'No segments are used in the project. Returning empty list')
53+
return []
54+
end
55+
56+
cache_key = make_cache_key(user_key, user_value)
57+
58+
ignore_cache = options.include?(OptimizelySegmentOption::IGNORE_CACHE)
59+
reset_cache = options.include?(OptimizelySegmentOption::RESET_CACHE)
60+
61+
reset if reset_cache
62+
63+
unless ignore_cache || reset_cache
64+
segments = @segments_cache.lookup(cache_key)
65+
unless segments.nil?
66+
@logger.log(Logger::DEBUG, 'ODP cache hit. Returning segments from cache.')
67+
return segments
68+
end
69+
end
70+
71+
@logger.log(Logger::DEBUG, 'ODP cache miss. Making a call to ODP server.')
72+
73+
segments = @zaius_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check)
74+
@segments_cache.save(cache_key, segments) unless segments.nil? || ignore_cache
75+
segments
76+
end
77+
78+
def reset
79+
@segments_cache.reset
80+
nil
81+
end
82+
83+
private
84+
85+
def make_cache_key(user_key, user_value)
86+
"#{user_key}-$-#{user_value}"
87+
end
88+
end
89+
90+
class OptimizelySegmentOption
91+
# Options for the OdpSegmentManager
92+
IGNORE_CACHE = :IGNORE_CACHE
93+
RESET_CACHE = :RESET_CACHE
94+
end
95+
end

lib/optimizely/odp/zaius_graphql_api_manager.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
require 'json'
2020

2121
module Optimizely
22-
class ZaiusGraphQlApiManager
22+
class ZaiusGraphQLApiManager
2323
# Interface that handles fetching audience segments.
2424

2525
def initialize(logger: nil, proxy_config: nil)
@@ -52,23 +52,23 @@ def fetch_segments(api_key, api_host, user_key, user_value, segments_to_check)
5252
rescue SocketError, Timeout::Error, Net::ProtocolError, Errno::ECONNRESET => e
5353
@logger.log(Logger::DEBUG, "GraphQL download failed: #{e}")
5454
log_failure('network error')
55-
return []
55+
return nil
5656
rescue Errno::EINVAL, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError => e
5757
log_failure(e)
58-
return []
58+
return nil
5959
end
6060

6161
status = response.code.to_i
6262
if status >= 400
6363
log_failure(status)
64-
return []
64+
return nil
6565
end
6666

6767
begin
6868
response = JSON.parse(response.body)
6969
rescue JSON::ParserError
7070
log_failure('JSON decode error')
71-
return []
71+
return nil
7272
end
7373

7474
if response.include?('errors')
@@ -78,21 +78,21 @@ def fetch_segments(api_key, api_host, user_key, user_value, segments_to_check)
7878
else
7979
log_failure(error_class)
8080
end
81-
return []
81+
return nil
8282
end
8383

8484
audiences = response.dig('data', 'customer', 'audiences', 'edges')
8585
unless audiences
8686
log_failure('decode error')
87-
return []
87+
return nil
8888
end
8989

9090
audiences.filter_map do |edge|
9191
name = edge.dig('node', 'name')
9292
state = edge.dig('node', 'state')
9393
unless name && state
9494
log_failure('decode error')
95-
return []
95+
return nil
9696
end
9797
state == 'qualified' ? name : nil
9898
end

0 commit comments

Comments
 (0)