Skip to content

Commit ff19d0a

Browse files
rashidspmikeproeng37
authored andcommitted
feat(api): Accepting all types for attributes values (#125)
1 parent 8a80d21 commit ff19d0a

12 files changed

+363
-17
lines changed

lib/optimizely.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,8 @@ def is_feature_enabled(feature_flag_key, user_id, attributes = nil)
258258
}, @logger, Logger::ERROR
259259
)
260260

261+
return false unless user_inputs_valid?(attributes)
262+
261263
feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
262264
unless feature_flag
263265
@logger.log(Logger::ERROR, "No feature flag was found for key '#{feature_flag_key}'.")
@@ -307,6 +309,8 @@ def get_enabled_features(user_id, attributes = nil)
307309

308310
return enabled_features unless Optimizely::Helpers::Validator.inputs_valid?({user_id: user_id}, @logger, Logger::ERROR)
309311

312+
return enabled_features unless user_inputs_valid?(attributes)
313+
310314
@config.feature_flags.each do |feature|
311315
enabled_features.push(feature['key']) if is_feature_enabled(
312316
feature['key'],
@@ -450,6 +454,8 @@ def get_feature_variable_for_type(feature_flag_key, variable_key, variable_type,
450454
@logger, Logger::ERROR
451455
)
452456

457+
return nil unless user_inputs_valid?(attributes)
458+
453459
feature_flag = @config.get_feature_flag_from_key(feature_flag_key)
454460
unless feature_flag
455461
@logger.log(Logger::INFO, "No feature flag was found for key '#{feature_flag_key}'.")

lib/optimizely/decision_service.rb

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -366,17 +366,18 @@ def get_bucketing_id(user_id, attributes)
366366
#
367367
# user_id - String user ID
368368
# attributes - Hash user attributes
369-
# By default, the bucketing ID should be the user ID
370-
bucketing_id = user_id
369+
# Returns String representing bucketing ID if it is a String type in attributes else return user ID
371370

372-
# If the bucketing ID key is defined in attributes, then use that in place of the userID
373-
if attributes && attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']].is_a?(String)
374-
unless attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']].empty?
375-
bucketing_id = attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']]
376-
@config.logger.log(Logger::DEBUG, "Setting the bucketing ID '#{bucketing_id}'")
377-
end
371+
return user_id unless attributes
372+
373+
bucketing_id = attributes[Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID']]
374+
375+
if bucketing_id
376+
return bucketing_id if bucketing_id.is_a?(String)
377+
378+
@config.logger.log(Logger::WARN, 'Bucketing ID attribute is not a string. Defaulted to user ID.')
378379
end
379-
bucketing_id
380+
user_id
380381
end
381382
end
382383
end

lib/optimizely/event_builder.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,12 @@
1515
# See the License for the specific language governing permissions and
1616
# limitations under the License.
1717
#
18-
require_relative './audience'
19-
require_relative './params'
20-
require_relative './version'
21-
require_relative '../optimizely/helpers/event_tag_utils'
18+
require_relative 'audience'
19+
require_relative 'helpers/constants'
20+
require_relative 'helpers/event_tag_utils'
21+
require_relative 'params'
22+
require_relative 'version'
23+
2224
require 'securerandom'
2325

2426
module Optimizely
@@ -74,9 +76,9 @@ def get_common_params(user_id, attributes)
7476
visitor_attributes = []
7577

7678
attributes&.keys&.each do |attribute_key|
77-
# Omit null attribute values
79+
# Omit attribute values that are not supported by the log endpoint.
7880
attribute_value = attributes[attribute_key]
79-
unless attribute_value.nil?
81+
if Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
8082
attribute_id = @config.get_attribute_id attribute_key
8183
if attribute_id
8284
visitor_attributes.push(

lib/optimizely/helpers/constants.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,8 @@ module Constants
327327
'v3' => '3',
328328
'v4' => '4'
329329
}.freeze
330+
331+
ATTRIBUTE_VALID_TYPES = [FalseClass, Float, Integer, String, TrueClass].freeze
330332
end
331333
end
332334
end

lib/optimizely/helpers/validator.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,19 @@ def attributes_valid?(attributes)
3434
attributes.is_a?(Hash)
3535
end
3636

37+
def attribute_valid?(attribute_key, attribute_value)
38+
# Determines if provided attribute_key and attribute_value are valid.
39+
#
40+
# attribute_key - Variable which needs to be validated.
41+
# attribute_value - Variable which needs to be validated.
42+
#
43+
# Returns boolean depending on validity of attribute_key and attribute_value.
44+
45+
return false unless attribute_key.is_a?(String) || attribute_key.is_a?(Symbol)
46+
47+
Helpers::Constants::ATTRIBUTE_VALID_TYPES.any? { |type| attribute_value.is_a?(type) }
48+
end
49+
3750
def event_tags_valid?(event_tags)
3851
# Determines if provided event tags are valid.
3952
#

spec/condition_spec.rb

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# frozen_string_literal: true
22

33
#
4-
# Copyright 2016-2017, Optimizely and contributors
4+
# Copyright 2016-2018, Optimizely and contributors
55
#
66
# Licensed under the Apache License, Version 2.0 (the "License");
77
# you may not use this file except in compliance with the License.
@@ -145,4 +145,19 @@
145145
condition = JSON.parse(condition)
146146
expect(@condition_evaluator.evaluate(condition)).to be true
147147
end
148+
149+
it 'should evaluate to true when user attributes match the audience conditions' do
150+
user_attributes = {
151+
'device_type' => 'iPhone',
152+
'is_firefox' => false,
153+
'num_users' => 15,
154+
'pi_value' => 3.14
155+
}
156+
condition_evaluator = Optimizely::ConditionEvaluator.new(user_attributes)
157+
condition = '["and", ["or", ["or", {"name": "device_type", "type": "custom_attribute", "value": "iPhone"}]],
158+
["or", ["or", {"name": "is_firefox", "type": "custom_attribute", "value": false}]], ["or", ["or", {"name": "num_users",
159+
"type": "custom_attribute", "value": 15}]], ["or", ["or", {"name": "pi_value", "type": "custom_attribute", "value": 3.14}]]]'
160+
condition = JSON.parse(condition)
161+
expect(condition_evaluator.evaluate(condition)).to be true
162+
end
148163
end

spec/decision_service_spec.rb

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,4 +676,41 @@
676676
end
677677
end
678678
end
679+
describe '#get_bucketing_id' do
680+
it 'should log a message and return user ID when bucketing ID is not a String' do
681+
user_attributes = {
682+
'browser_type' => 'firefox',
683+
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => 5
684+
}
685+
expect(decision_service.send(:get_bucketing_id, 'test_user', user_attributes)).to eq('test_user')
686+
expect(spy_logger).to have_received(:log).once.with(Logger::WARN, 'Bucketing ID attribute is not a string. Defaulted to user ID.')
687+
end
688+
689+
it 'should not log any message and return user ID when bucketing ID is nil' do
690+
user_attributes = {
691+
'browser_type' => 'firefox',
692+
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => nil
693+
}
694+
expect(decision_service.send(:get_bucketing_id, 'test_user', user_attributes)).to eq('test_user')
695+
expect(spy_logger).not_to have_received(:log)
696+
end
697+
698+
it 'should not log any message and return given bucketing ID when bucketing ID is a String' do
699+
user_attributes = {
700+
'browser_type' => 'firefox',
701+
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => 'i_am_bucketing_id'
702+
}
703+
expect(decision_service.send(:get_bucketing_id, 'test_user', user_attributes)).to eq('i_am_bucketing_id')
704+
expect(spy_logger).not_to have_received(:log)
705+
end
706+
707+
it 'should not log any message and return empty String when bucketing ID is empty String' do
708+
user_attributes = {
709+
'browser_type' => 'firefox',
710+
Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BUCKETING_ID'] => ''
711+
}
712+
expect(decision_service.send(:get_bucketing_id, 'test_user', user_attributes)).to eq('')
713+
expect(spy_logger).not_to have_received(:log)
714+
end
715+
end
679716
end

spec/event_builder_spec.rb

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,81 @@
125125
expect(impression_event.http_verb).to eq(:post)
126126
end
127127

128+
it 'should create a valid Event when create_impression_event is called with attributes of different valid types' do
129+
@expected_impression_params[:visitors][0][:attributes] = [
130+
{
131+
entity_id: '111094',
132+
key: 'browser_type',
133+
type: 'custom',
134+
value: 'firefox'
135+
}, {
136+
entity_id: '111095',
137+
key: 'boolean_key',
138+
type: 'custom',
139+
value: true
140+
}, {
141+
entity_id: '111096',
142+
key: 'integer_key',
143+
type: 'custom',
144+
value: 5
145+
}, {
146+
entity_id: '111097',
147+
key: 'double_key',
148+
type: 'custom',
149+
value: 5.5
150+
},
151+
entity_id: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
152+
key: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
153+
type: 'custom',
154+
value: true
155+
]
156+
157+
experiment = config.get_experiment_from_key('test_experiment')
158+
attributes = {
159+
'browser_type' => 'firefox',
160+
'boolean_key' => true,
161+
'integer_key' => 5,
162+
'double_key' => 5.5
163+
}
164+
impression_event = @event_builder.create_impression_event(experiment, '111128', 'test_user', attributes)
165+
expect(impression_event.params).to eq(@expected_impression_params)
166+
expect(impression_event.url).to eq(@expected_endpoint)
167+
expect(impression_event.http_verb).to eq(:post)
168+
end
169+
170+
it 'should create a valid Event and exclude attributes of invalid types' do
171+
@expected_impression_params[:visitors][0][:attributes] = [
172+
{
173+
entity_id: '111094',
174+
key: 'browser_type',
175+
type: 'custom',
176+
value: 'firefox'
177+
},
178+
{
179+
entity_id: '111096',
180+
key: 'integer_key',
181+
type: 'custom',
182+
value: 5
183+
},
184+
entity_id: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
185+
key: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
186+
type: 'custom',
187+
value: true
188+
]
189+
190+
experiment = config.get_experiment_from_key('test_experiment')
191+
attributes = {
192+
'browser_type' => 'firefox',
193+
'boolean_key' => nil,
194+
'integer_key' => 5,
195+
'double_key' => {}
196+
}
197+
impression_event = @event_builder.create_impression_event(experiment, '111128', 'test_user', attributes)
198+
expect(impression_event.params).to eq(@expected_impression_params)
199+
expect(impression_event.url).to eq(@expected_endpoint)
200+
expect(impression_event.http_verb).to eq(:post)
201+
end
202+
128203
it 'should create a valid Event when create_impression_event is called with attributes as a false value' do
129204
@expected_impression_params[:visitors][0][:attributes].unshift(
130205
entity_id: '111094',

spec/project_config_spec.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,10 @@
4242
expect(project_config.revision).to eq(config_body['revision'])
4343

4444
expected_attribute_key_map = {
45-
'browser_type' => config_body['attributes'][0]
45+
'browser_type' => config_body['attributes'][0],
46+
'boolean_key' => config_body['attributes'][1],
47+
'integer_key' => config_body['attributes'][2],
48+
'double_key' => config_body['attributes'][3]
4649
}
4750
expected_audience_id_map = {
4851
'11154' => config_body['audiences'][0],

0 commit comments

Comments
 (0)