Skip to content

Commit 868984d

Browse files
authored
Release version 1.0.0 (#14)
* Add CLA to CONTRIBUTING (#11) * Improve exception handling (#10) * Properly handle attributes not in V1 datafiles (#12) * Bump version to 1.0.0 * Update gemspec
1 parent 6c6963a commit 868984d

File tree

11 files changed

+236
-91
lines changed

11 files changed

+236
-91
lines changed

CHANGELOG

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,11 @@
1+
-------------------------------------------------------------------------------
2+
1.0.0
3+
* Introduce support for Full Stack projects in Optimizely X with no breaking changes from previous version.
4+
* Introduce more graceful exception handling in instantiation and core methods.
5+
* Update whitelisting to take precedence over audience condition evaluation.
6+
* Fix bug activating/tracking with an attribute not in the datafile.
7+
-------------------------------------------------------------------------------
8+
19
-------------------------------------------------------------------------------
210
0.1.2
311
* Add support for V2 datafile and event endpoint.

CONTRIBUTING.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
#Contributing to the Optimizely Ruby SDK
2-
We welcome contributions and feedback! Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code.
2+
We welcome contributions and feedback! All contributors must sign our [Contributor License Agreement (CLA)](https://docs.google.com/a/optimizely.com/forms/d/e/1FAIpQLSf9cbouWptIpMgukAKZZOIAhafvjFCV8hS00XJLWQnWDFtwtA/viewform) to be eligible to contribute. Please read the [README](README.md) to set up your development environment, then read the guidelines below for information on submitting your code.
33

44
##Development process
55

6-
1. Create a branch off of `master`: `git checkout -b YOUR_NAME/branch_name`.
6+
1. Create a branch off of `devel`: `git checkout -b YOUR_NAME/branch_name`.
77
2. Commit your changes. Make sure to add tests!
88
3. `git push` your changes to GitHub.
9-
4. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `master`.
10-
5. Open a pull request from `YOUR_NAME/branch_name` to `master`.
9+
4. Make sure that all unit tests are passing and that there are no merge conflicts between your branch and `devel`.
10+
5. Open a pull request from `YOUR_NAME/branch_name` to `devel`.
1111
6. A repository maintainer will review your pull request and, if all goes well, merge it!
1212

1313
##Pull request acceptance criteria

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#Optimizely Ruby SDK
22

3-
This repository houses the Ruby SDK for Optimizely's server-side testing product, which is currently in private beta.
3+
This repository houses the Ruby SDK for Optimizely's Full Stack product.
44

55
##Getting Started
66

@@ -13,7 +13,7 @@ gem install optimizely-sdk
1313
```
1414

1515
###Using the SDK
16-
See the Optimizely server-side testing [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set up your first custom project and use the SDK. **Please note that you must be a member of the private server-side testing beta to create custom projects and use this SDK.**
16+
See the Optimizely Full Stack [developer documentation](http://developers.optimizely.com/server/reference/index.html) to learn how to set up your first Full Stack project and use the SDK.
1717

1818
##Development
1919

lib/optimizely.rb

Lines changed: 70 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111

1212
module Optimizely
1313
class Project
14+
15+
# Boolean representing if the instance represents a usable Optimizely Project
16+
attr_reader :is_valid
17+
1418
attr_accessor :config
1519
attr_accessor :bucketer
1620
attr_accessor :event_builder
@@ -33,14 +37,37 @@ def initialize(datafile, event_dispatcher = nil, logger = nil, error_handler = n
3337
# By default all exceptions will be suppressed.
3438
# skip_json_validation - Optional boolean param to skip JSON schema validation of the provided datafile.
3539

40+
@is_valid = true
3641
@logger = logger || NoOpLogger.new
3742
@error_handler = error_handler || NoOpErrorHandler.new
3843
@event_dispatcher = event_dispatcher || EventDispatcher.new
39-
validate_inputs(datafile, skip_json_validation)
4044

41-
@config = ProjectConfig.new(datafile, @logger, @error_handler)
42-
@bucketer = Bucketer.new(@config)
43-
@event_builder = EVENT_BUILDERS_BY_VERSION[@config.version].new(@config, @bucketer)
45+
begin
46+
validate_inputs(datafile, skip_json_validation)
47+
rescue InvalidInputError => e
48+
@is_valid = false
49+
logger = SimpleLogger.new
50+
logger.log(Logger::ERROR, e.message)
51+
return
52+
end
53+
54+
begin
55+
@config = ProjectConfig.new(datafile, @logger, @error_handler)
56+
rescue
57+
@is_valid = false
58+
logger = SimpleLogger.new
59+
logger.log(Logger::ERROR, InvalidInputError.new('datafile').message)
60+
return
61+
end
62+
63+
begin
64+
@bucketer = Bucketer.new(@config)
65+
@event_builder = EVENT_BUILDERS_BY_VERSION[@config.version].new(@config, @bucketer)
66+
rescue
67+
@is_valid = false
68+
logger = SimpleLogger.new
69+
logger.log(Logger::ERROR, InvalidDatafileVersionError.new)
70+
end
4471
end
4572

4673
def activate(experiment_key, user_id, attributes = nil)
@@ -51,7 +78,13 @@ def activate(experiment_key, user_id, attributes = nil)
5178
# attributes - Hash representing user attributes and values to be recorded.
5279
#
5380
# Returns variation key representing the variation the user will be bucketed in.
54-
# Returns nil if experiment is not Running or if user is not in experiment.
81+
# Returns nil if experiment is not Running, if user is not in experiment, or if datafile is invalid.
82+
83+
unless @is_valid
84+
logger = SimpleLogger.new
85+
logger.log(Logger::ERROR, InvalidDatafileError.new('activate').message)
86+
return nil
87+
end
5588

5689
if attributes && !attributes_valid?(attributes)
5790
@logger.log(Logger::INFO, "Not activating user '#{user_id}'.")
@@ -75,7 +108,11 @@ def activate(experiment_key, user_id, attributes = nil)
75108
@logger.log(Logger::INFO,
76109
'Dispatching impression event to URL %s with params %s.' % [impression_event.url,
77110
impression_event.params])
78-
@event_dispatcher.dispatch_event(impression_event)
111+
begin
112+
@event_dispatcher.dispatch_event(impression_event)
113+
rescue => e
114+
@logger.log(Logger::ERROR, "Unable to dispatch impression event. Error: #{e}")
115+
end
79116

80117
@config.get_variation_key_from_id(experiment_key, variation_id)
81118
end
@@ -88,7 +125,13 @@ def get_variation(experiment_key, user_id, attributes = nil)
88125
# attributes - Hash representing user attributes.
89126
#
90127
# Returns variation key where visitor will be bucketed.
91-
# Returns nil if experiment is not Running or if user is not in experiment.
128+
# Returns nil if experiment is not Running, if user is not in experiment, or if datafile is invalid.
129+
130+
unless @is_valid
131+
logger = SimpleLogger.new
132+
logger.log(Logger::ERROR, InvalidDatafileError.new('get_variation').message)
133+
return nil
134+
end
92135

93136
if attributes && !attributes_valid?(attributes)
94137
@logger.log(Logger::INFO, "Not activating user '#{user_id}.")
@@ -112,7 +155,11 @@ def track(event_key, user_id, attributes = nil, event_value = nil)
112155
# attributes - Hash representing visitor attributes and values which need to be recorded.
113156
# event_value - Value associated with the event. Can be used to represent revenue in cents.
114157

115-
# Create and dispatch conversion event
158+
unless @is_valid
159+
logger = SimpleLogger.new
160+
logger.log(Logger::ERROR, InvalidDatafileError.new('track').message)
161+
return nil
162+
end
116163

117164
return nil if attributes && !attributes_valid?(attributes)
118165

@@ -134,14 +181,21 @@ def track(event_key, user_id, attributes = nil, event_value = nil)
134181
end
135182

136183
# Don't track events without valid experiments attached
137-
return if valid_experiment_keys.empty?
184+
if valid_experiment_keys.empty?
185+
@logger.log(Logger::INFO, "There are no valid experiments for event '#{event_key}' to track.")
186+
return nil
187+
end
138188

139189
conversion_event = @event_builder.create_conversion_event(event_key, user_id, attributes,
140190
event_value, valid_experiment_keys)
141191
@logger.log(Logger::INFO,
142192
'Dispatching conversion event to URL %s with params %s.' % [conversion_event.url,
143193
conversion_event.params])
144-
@event_dispatcher.dispatch_event(conversion_event)
194+
begin
195+
@event_dispatcher.dispatch_event(conversion_event)
196+
rescue => e
197+
@logger.log(Logger::ERROR, "Unable to dispatch conversion event. Error: #{e}")
198+
end
145199
end
146200

147201
private
@@ -156,7 +210,7 @@ def preconditions_valid?(experiment_key, user_id, attributes)
156210
# Returns boolean representing whether all preconditions are valid.
157211

158212
unless @config.experiment_running?(experiment_key)
159-
@logger.log(Logger::INFO, "Experiment '#{experiment_key} is not running.")
213+
@logger.log(Logger::INFO, "Experiment '#{experiment_key}' is not running.")
160214
return false
161215
end
162216

@@ -166,7 +220,7 @@ def preconditions_valid?(experiment_key, user_id, attributes)
166220

167221
unless Audience.user_in_experiment?(@config, experiment_key, attributes)
168222
@logger.log(Logger::INFO,
169-
"User '#{user_id} does not meet the conditions to be in experiment '#{experiment_key}.")
223+
"User '#{user_id}' does not meet the conditions to be in experiment '#{experiment_key}'.")
170224
return false
171225
end
172226

@@ -184,12 +238,12 @@ def attributes_valid?(attributes)
184238

185239
def validate_inputs(datafile, skip_json_validation)
186240
unless skip_json_validation
187-
raise InvalidDatafileError unless Helpers::Validator.datafile_valid?(datafile)
241+
raise InvalidInputError.new('datafile') unless Helpers::Validator.datafile_valid?(datafile)
188242
end
189243

190-
raise InvalidLoggerError unless Helpers::Validator.logger_valid?(@logger)
191-
raise InvalidErrorHandlerError unless Helpers::Validator.error_handler_valid?(@error_handler)
192-
raise InvalidEventDispatcherError unless Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
244+
raise InvalidInputError.new('logger') unless Helpers::Validator.logger_valid?(@logger)
245+
raise InvalidInputError.new('error_handler') unless Helpers::Validator.error_handler_valid?(@error_handler)
246+
raise InvalidInputError.new('event_dispatcher') unless Helpers::Validator.event_dispatcher_valid?(@event_dispatcher)
193247
end
194248
end
195249
end

lib/optimizely/event_builder.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -274,7 +274,10 @@ def add_attributes(attributes)
274274
attributes.keys.each do |attribute_key|
275275
attribute_value = attributes[attribute_key]
276276
next unless attribute_value
277-
segment_id = @config.attribute_key_map[attribute_key]['segmentId']
277+
278+
# Skip attributes not in the datafile
279+
segment_id = @config.get_segment_id(attribute_key)
280+
next unless segment_id
278281
segment_param = sprintf(ATTRIBUTE_PARAM_FORMAT,
279282
segment_prefix: Params::SEGMENT_PREFIX, segment_id: segment_id)
280283
params[segment_param] = attribute_value

lib/optimizely/exceptions.rb

Lines changed: 20 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,59 +25,51 @@ def initialize(msg = 'Attributes provided are in an invalid format.')
2525
end
2626
end
2727

28-
class InvalidDatafileError < Error
29-
# Raised when an invalid datafile is provided
30-
31-
def initialize(msg = 'Provided datafile is in an invalid format.')
32-
super
33-
end
34-
end
35-
36-
class InvalidErrorHandlerError < Error
37-
# Raised when an invalid error handler is provided
28+
class InvalidExperimentError < Error
29+
# Raised when an invalid experiment key is provided
3830

39-
def initialize(msg = 'Provided error_handler is in an invalid format.')
31+
def initialize(msg = 'Provided experiment is not in datafile.')
4032
super
4133
end
4234
end
4335

44-
class InvalidEventDispatcherError < Error
45-
# Raised when an invalid event dispatcher is provided
36+
class InvalidEventError < Error
37+
# Raised when an invalid event key is provided
4638

47-
def initialize(msg = 'Provided event_dispatcher is in an invalid format.')
39+
def initialize(msg = 'Provided event is not in datafile.')
4840
super
4941
end
5042
end
5143

52-
class InvalidExperimentError < Error
53-
# Raised when an invalid experiment key is provided
44+
class InvalidVariationError < Error
45+
# Raised when an invalid variation key or ID is provided
5446

55-
def initialize(msg = 'Provided experiment is not in datafile.')
47+
def initialize(msg = 'Provided variation is not in datafile.')
5648
super
5749
end
5850
end
5951

60-
class InvalidGoalError < Error
61-
# Raised when an invalid event key is provided
52+
class InvalidDatafileError < Error
53+
# Raised when a public method fails due to an invalid datafile
6254

63-
def initialize(msg = 'Provided event is not in datafile.')
64-
super
55+
def initialize(aborted_method)
56+
super("Provided datafile is in an invalid format. Aborting #{aborted_method}.")
6557
end
6658
end
6759

68-
class InvalidLoggerError < Error
69-
# Raised when an invalid logger is provided
60+
class InvalidDatafileVersionError < Error
61+
# Raised when a datafile with an unsupported version is provided
7062

71-
def initialize(msg = 'Provided logger is in an invalid format.')
63+
def initialize(msg = 'Provided datafile is an unsupported version.')
7264
super
7365
end
7466
end
7567

76-
class InvalidVariationError < Error
77-
# Raised when an invalid variation key or ID is provided
68+
class InvalidInputError < Error
69+
# Abstract error raised when an invalid input is provided during Project instantiation
7870

79-
def initialize(msg = 'Provided variation is not in datafile.')
80-
super
71+
def initialize(type)
72+
super("Provided #{type} is in an invalid format.")
8173
end
8274
end
8375
end

lib/optimizely/project_config.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@ def get_experiment_ids_for_goal(goal_key)
139139
goal = @event_key_map[goal_key]
140140
return goal['experimentIds'] if goal
141141
@logger.log Logger::ERROR, "Event '#{goal_key}' is not in datafile."
142-
@error_handler.handle_error InvalidGoalError
142+
@error_handler.handle_error InvalidEventError
143143
[]
144144
end
145145

@@ -257,6 +257,14 @@ def get_attribute_id(attribute_key)
257257
nil
258258
end
259259

260+
def get_segment_id(attribute_key)
261+
attribute = @attribute_key_map[attribute_key]
262+
return attribute['segmentId'] if attribute
263+
@logger.log Logger::ERROR, "Attribute key '#{attribute_key}' is not in datafile."
264+
@error_handler.handle_error InvalidAttributeError
265+
nil
266+
end
267+
260268
def user_in_forced_variation?(experiment_key, user_id)
261269
# Determines if a given user is in a forced variation
262270
#

lib/optimizely/version.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
module Optimizely
2-
VERSION = '0.1.2'.freeze
2+
VERSION = '1.0.0'.freeze
33
end

optimizely-sdk.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
88
spec.email = ["developers@optimizely.com"]
99

1010
spec.summary = "Ruby SDK for Optimizely's testing framework"
11-
spec.description = "A Ruby SDK for Optimizely's server-side testing product, which is currently in private beta."
11+
spec.description = "A Ruby SDK for Optimizely's Full Stack testing product."
1212
spec.homepage = "https://www.optimizely.com/"
1313
spec.license = "Apache-2.0"
1414

spec/project_config_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -300,7 +300,7 @@
300300

301301
describe 'get_experiment_ids_for_goal' do
302302
it 'should raise an error when provided goal key is invalid' do
303-
expect { config.get_experiment_ids_for_goal('invalid_key') }.to raise_error(Optimizely::InvalidGoalError)
303+
expect { config.get_experiment_ids_for_goal('invalid_key') }.to raise_error(Optimizely::InvalidEventError)
304304
end
305305
end
306306

0 commit comments

Comments
 (0)