Skip to content

feat: add odp segment manager #310

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 5 commits into from
Aug 19, 2022
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
110 changes: 110 additions & 0 deletions lib/optimizely/odp/odp_config.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# frozen_string_literal: true

#
# Copyright 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.
#

require 'optimizely/logger'

module Optimizely
class OdpConfig
# Contains configuration used for ODP integration.
#
# @param api_host - The host URL for the ODP audience segments API (optional).
# @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional).
# @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
def initialize(api_key = nil, api_host = nil, segments_to_check = [])
@api_key = api_key
@api_host = api_host
@segments_to_check = segments_to_check
@mutex = Mutex.new
end

# Replaces the existing configuration
#
# @param api_host - The host URL for the ODP audience segments API (optional).
# @param api_key - The public API key for the ODP account from which the audience segments will be fetched (optional).
# @param segments_to_check - An array of all ODP segments used in the current datafile (associated with api_host/api_key).
#
# @return - True if the provided values were different than the existing values.

def update(api_key = nil, api_host = nil, segments_to_check = [])
@mutex.synchronize do
break false if @api_key == api_key && @api_host == api_host && @segments_to_check == segments_to_check

@api_key = api_key
@api_host = api_host
@segments_to_check = segments_to_check
break true
end
end

# Returns the api host for odp connections
#
# @return - The api host.

def api_host
@mutex.synchronize { @api_host.clone }
end

# Returns the api host for odp connections
#
# @return - The api host.

def api_host=(api_host)
@mutex.synchronize { @api_host = api_host.clone }
end

# Returns the api key for odp connections
#
# @return - The api key.

def api_key
@mutex.synchronize { @api_key.clone }
end

# Replace the api key with the provided string
#
# @param api_key - An api key

def api_key=(api_key)
@mutex.synchronize { @api_key = api_key.clone }
end

# Returns An array of qualified segments for this user
#
# @return - An array of segments names.

def segments_to_check
@mutex.synchronize { @segments_to_check.clone }
end

# Replace qualified segments with provided segments
#
# @param segments - An array of segment names

def segments_to_check=(segments_to_check)
@mutex.synchronize { @segments_to_check = segments_to_check.clone }
end

# Returns True if odp is integrated
#
# @return - bool

def odp_integrated?
@mutex.synchronize { !@api_key.nil? && !@api_host.nil? }
end
end
end
95 changes: 95 additions & 0 deletions lib/optimizely/odp/odp_segment_manager.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# frozen_string_literal: true

#
# Copyright 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.
#

require 'optimizely/logger'
require_relative 'zaius_graphql_api_manager'

module Optimizely
class OdpSegmentManager
# Schedules connections to ODP for audience segmentation and caches the results
attr_reader :odp_config, :segments_cache, :zaius_manager, :logger

def initialize(odp_config, segments_cache, api_manager = nil, logger = nil, proxy_config = nil)
@odp_config = odp_config
@logger = logger || NoOpLogger.new
@zaius_manager = api_manager || ZaiusGraphQLApiManager.new(logger: @logger, proxy_config: proxy_config)
@segments_cache = segments_cache
end

# Returns qualified segments for the user from the cache or the ODP server if not in the cache.
#
# @param user_key - The key for identifying the id type.
# @param user_value - The id itself.
# @param options - An array of OptimizelySegmentOptions used to ignore and/or reset the cache.
#
# @return - Array of qualified segments.
def fetch_qualified_segments(user_key, user_value, options)
unless @odp_config.odp_integrated?
@logger.log(Logger::ERROR, format(Optimizely::Helpers::Constants::ODP_LOGS[:FETCH_SEGMENTS_FAILED], 'ODP is not enabled'))
return nil
end

odp_api_key = @odp_config.api_key
odp_api_host = @odp_config.api_host
segments_to_check = @odp_config&.segments_to_check

unless segments_to_check&.size&.positive?
@logger.log(Logger::DEBUG, 'No segments are used in the project. Returning empty list')
return []
end

cache_key = make_cache_key(user_key, user_value)

ignore_cache = options.include?(OptimizelySegmentOption::IGNORE_CACHE)
reset_cache = options.include?(OptimizelySegmentOption::RESET_CACHE)

reset if reset_cache

unless ignore_cache || reset_cache
segments = @segments_cache.lookup(cache_key)
unless segments.nil?
@logger.log(Logger::DEBUG, 'ODP cache hit. Returning segments from cache.')
return segments
end
end

@logger.log(Logger::DEBUG, 'ODP cache miss. Making a call to ODP server.')

segments = @zaius_manager.fetch_segments(odp_api_key, odp_api_host, user_key, user_value, segments_to_check)
@segments_cache.save(cache_key, segments) unless segments.nil? || ignore_cache
segments
end

def reset
@segments_cache.reset
nil
end

private

def make_cache_key(user_key, user_value)
"#{user_key}-$-#{user_value}"
end
end

class OptimizelySegmentOption
# Options for the OdpSegmentManager
IGNORE_CACHE = :IGNORE_CACHE
RESET_CACHE = :RESET_CACHE
end
end
16 changes: 8 additions & 8 deletions lib/optimizely/odp/zaius_graphql_api_manager.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
require 'json'

module Optimizely
class ZaiusGraphQlApiManager
class ZaiusGraphQLApiManager
# Interface that handles fetching audience segments.

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

status = response.code.to_i
if status >= 400
log_failure(status)
return []
return nil
end

begin
response = JSON.parse(response.body)
rescue JSON::ParserError
log_failure('JSON decode error')
return []
return nil
end

if response.include?('errors')
Expand All @@ -78,21 +78,21 @@ def fetch_segments(api_key, api_host, user_key, user_value, segments_to_check)
else
log_failure(error_class)
end
return []
return nil
end

audiences = response.dig('data', 'customer', 'audiences', 'edges')
unless audiences
log_failure('decode error')
return []
return nil
end

audiences.filter_map do |edge|
name = edge.dig('node', 'name')
state = edge.dig('node', 'state')
unless name && state
log_failure('decode error')
return []
return nil
end
state == 'qualified' ? name : nil
end
Expand Down
Loading