optimizely-sdk 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,154 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 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
+ require_relative 'entity/event_batch'
19
+ require_relative 'entity/conversion_event'
20
+ require_relative 'entity/decision'
21
+ require_relative 'entity/impression_event'
22
+ require_relative 'entity/snapshot'
23
+ require_relative 'entity/snapshot_event'
24
+ require_relative 'entity/visitor'
25
+ require 'optimizely/helpers/validator'
26
+ module Optimizely
27
+ class EventFactory
28
+ # EventFactory builds LogEvent objects from a given user_event.
29
+ class << self
30
+ CUSTOM_ATTRIBUTE_FEATURE_TYPE = 'custom'
31
+ ENDPOINT = 'https://logx.optimizely.com/v1/events'
32
+ POST_HEADERS = {'Content-Type' => 'application/json'}.freeze
33
+ ACTIVATE_EVENT_KEY = 'campaign_activated'
34
+
35
+ def create_log_event(user_events, logger)
36
+ @logger = logger
37
+ builder = Optimizely::EventBatch::Builder.new
38
+
39
+ user_events = [user_events] unless user_events.is_a? Array
40
+
41
+ visitors = []
42
+ user_context = nil
43
+ user_events.each do |user_event|
44
+ if user_event.is_a? Optimizely::ImpressionEvent
45
+ visitor = create_impression_event_visitor(user_event)
46
+ visitors.push(visitor)
47
+ elsif user_event.is_a? Optimizely::ConversionEvent
48
+ visitor = create_conversion_event_visitor(user_event)
49
+ visitors.push(visitor)
50
+ else
51
+ @logger.log(Logger::WARN, 'invalid UserEvent added in a list.')
52
+ next
53
+ end
54
+ user_context = user_event.event_context
55
+ end
56
+
57
+ return nil if visitors.empty?
58
+
59
+ builder.with_account_id(user_context[:account_id])
60
+ builder.with_project_id(user_context[:project_id])
61
+ builder.with_client_version(user_context[:client_version])
62
+ builder.with_revision(user_context[:revision])
63
+ builder.with_client_name(user_context[:client_name])
64
+ builder.with_anonymize_ip(user_context[:anonymize_ip])
65
+ builder.with_enrich_decisions(true)
66
+
67
+ builder.with_visitors(visitors)
68
+ event_batch = builder.build
69
+ Event.new(:post, ENDPOINT, event_batch.as_json, POST_HEADERS)
70
+ end
71
+
72
+ def build_attribute_list(user_attributes, project_config)
73
+ visitor_attributes = []
74
+ user_attributes&.keys&.each do |attribute_key|
75
+ # Omit attribute values that are not supported by the log endpoint.
76
+ attribute_value = user_attributes[attribute_key]
77
+ next unless Helpers::Validator.attribute_valid?(attribute_key, attribute_value)
78
+
79
+ attribute_id = project_config.get_attribute_id attribute_key
80
+ next if attribute_id.nil?
81
+
82
+ visitor_attributes.push(
83
+ entity_id: attribute_id,
84
+ key: attribute_key,
85
+ type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
86
+ value: attribute_value
87
+ )
88
+ end
89
+
90
+ return visitor_attributes unless Helpers::Validator.boolean? project_config.bot_filtering
91
+
92
+ # Append Bot Filtering Attribute
93
+ visitor_attributes.push(
94
+ entity_id: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
95
+ key: Optimizely::Helpers::Constants::CONTROL_ATTRIBUTES['BOT_FILTERING'],
96
+ type: CUSTOM_ATTRIBUTE_FEATURE_TYPE,
97
+ value: project_config.bot_filtering
98
+ )
99
+ end
100
+
101
+ private
102
+
103
+ def create_impression_event_visitor(impression_event)
104
+ decision = Optimizely::Decision.new(
105
+ campaign_id: impression_event.experiment_layer_id,
106
+ experiment_id: impression_event.experiment_id,
107
+ variation_id: impression_event.variation_id
108
+ )
109
+
110
+ snapshot_event = Optimizely::SnapshotEvent.new(
111
+ entity_id: impression_event.experiment_layer_id,
112
+ timestamp: impression_event.timestamp,
113
+ uuid: impression_event.uuid,
114
+ key: ACTIVATE_EVENT_KEY
115
+ )
116
+
117
+ snapshot = Optimizely::Snapshot.new(
118
+ events: [snapshot_event.as_json],
119
+ decisions: [decision.as_json]
120
+ )
121
+
122
+ visitor = Optimizely::Visitor.new(
123
+ snapshots: [snapshot.as_json],
124
+ visitor_id: impression_event.user_id,
125
+ attributes: impression_event.visitor_attributes
126
+ )
127
+ visitor.as_json
128
+ end
129
+
130
+ def create_conversion_event_visitor(conversion_event)
131
+ revenue_value = Helpers::EventTagUtils.get_revenue_value(conversion_event.tags, @logger)
132
+ numeric_value = Helpers::EventTagUtils.get_numeric_value(conversion_event.tags, @logger)
133
+ snapshot_event = Optimizely::SnapshotEvent.new(
134
+ entity_id: conversion_event.event['id'],
135
+ timestamp: conversion_event.timestamp,
136
+ uuid: conversion_event.uuid,
137
+ key: conversion_event.event['key'],
138
+ revenue: revenue_value,
139
+ value: numeric_value,
140
+ tags: conversion_event.tags
141
+ )
142
+
143
+ snapshot = Optimizely::Snapshot.new(events: [snapshot_event.as_json])
144
+
145
+ visitor = Optimizely::Visitor.new(
146
+ snapshots: [snapshot.as_json],
147
+ visitor_id: conversion_event.user_id,
148
+ attributes: conversion_event.visitor_attributes
149
+ )
150
+ visitor.as_json
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 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
+ module Optimizely
19
+ class EventProcessor
20
+ # EventProcessor interface is used to provide an intermediary processing stage within
21
+ # event production. It's assumed that the EventProcessor dispatches events via a provided
22
+ # EventDispatcher.
23
+ def process(user_event); end
24
+ end
25
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 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
+ require_relative 'event_processor'
19
+ module Optimizely
20
+ class ForwardingEventProcessor < EventProcessor
21
+ # ForwardingEventProcessor is a basic transformation stage for converting
22
+ # the event batch into a LogEvent to be dispatched.
23
+ def initialize(event_dispatcher, logger = nil, notification_center = nil)
24
+ @event_dispatcher = event_dispatcher
25
+ @logger = logger || NoOpLogger.new
26
+ @notification_center = notification_center
27
+ end
28
+
29
+ def process(user_event)
30
+ log_event = Optimizely::EventFactory.create_log_event(user_event, @logger)
31
+
32
+ begin
33
+ @event_dispatcher.dispatch_event(log_event)
34
+ @notification_center&.send_notifications(
35
+ NotificationCenter::NOTIFICATION_TYPES[:LOG_EVENT],
36
+ log_event
37
+ )
38
+ rescue StandardError => e
39
+ @logger.log(Logger::ERROR, "Error dispatching event: #{log_event} #{e.message}.")
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 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
+ require_relative 'entity/conversion_event'
19
+ require_relative 'entity/impression_event'
20
+ require_relative 'entity/event_context'
21
+ require_relative 'event_factory'
22
+ module Optimizely
23
+ class UserEventFactory
24
+ # UserEventFactory builds ImpressionEvent and ConversionEvent objects from a given user_event.
25
+ def self.create_impression_event(project_config, experiment, variation_id, user_id, user_attributes)
26
+ # Create impression Event to be sent to the logging endpoint.
27
+ #
28
+ # project_config - Instance of ProjectConfig
29
+ # experiment - Instance Experiment for which impression needs to be recorded.
30
+ # variation_id - String ID for variation which would be presented to user.
31
+ # user_id - String ID for user.
32
+ # attributes - Hash Representing user attributes and values which need to be recorded.
33
+ #
34
+ # Returns Event encapsulating the impression event.
35
+ event_context = Optimizely::EventContext.new(
36
+ account_id: project_config.account_id,
37
+ project_id: project_config.project_id,
38
+ anonymize_ip: project_config.anonymize_ip,
39
+ revision: project_config.revision,
40
+ client_name: CLIENT_ENGINE,
41
+ client_version: VERSION
42
+ ).as_json
43
+
44
+ visitor_attributes = Optimizely::EventFactory.build_attribute_list(user_attributes, project_config)
45
+ experiment_layer_id = project_config.experiment_key_map[experiment['key']]['layerId']
46
+ Optimizely::ImpressionEvent.new(
47
+ event_context: event_context,
48
+ user_id: user_id,
49
+ experiment_layer_id: experiment_layer_id,
50
+ experiment_id: experiment['id'],
51
+ variation_id: variation_id,
52
+ visitor_attributes: visitor_attributes,
53
+ bot_filtering: project_config.bot_filtering
54
+ )
55
+ end
56
+
57
+ def self.create_conversion_event(project_config, event, user_id, user_attributes, event_tags)
58
+ # Create conversion Event to be sent to the logging endpoint.
59
+ #
60
+ # project_config - Instance of ProjectConfig
61
+ # event - Event which needs to be recorded.
62
+ # user_id - String ID for user.
63
+ # attributes - Hash Representing user attributes and values which need to be recorded.
64
+ # event_tags - Hash representing metadata associated with the event.
65
+ #
66
+ # Returns Event encapsulating the conversion event.
67
+
68
+ event_context = Optimizely::EventContext.new(
69
+ account_id: project_config.account_id,
70
+ project_id: project_config.project_id,
71
+ anonymize_ip: project_config.anonymize_ip,
72
+ revision: project_config.revision,
73
+ client_name: CLIENT_ENGINE,
74
+ client_version: VERSION
75
+ ).as_json
76
+
77
+ Optimizely::ConversionEvent.new(
78
+ event_context: event_context,
79
+ event: event,
80
+ user_id: user_id,
81
+ visitor_attributes: Optimizely::EventFactory.build_attribute_list(user_attributes, project_config),
82
+ tags: event_tags,
83
+ bot_filtering: project_config.bot_filtering
84
+ )
85
+ end
86
+ end
87
+ end
@@ -362,7 +362,7 @@ module Optimizely
362
362
 
363
363
  CONFIG_MANAGER = {
364
364
  'DATAFILE_URL_TEMPLATE' => 'https://cdn.optimizely.com/datafiles/%s.json',
365
- # Default time in seconds to block the get_config call until config has been initialized.
365
+ # Default time in seconds to block the 'config' method call until 'config' instance has been initialized.
366
366
  'DEFAULT_BLOCKING_TIMEOUT' => 15,
367
367
  # Default config update interval of 5 minutes
368
368
  'DEFAULT_UPDATE_INTERVAL' => 5 * 60,
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright 2019, 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
+ module Optimizely
19
+ module Helpers
20
+ module DateTimeUtils
21
+ module_function
22
+
23
+ def create_timestamp
24
+ # Returns Integer current UTC timestamp
25
+ utc = Time.now.getutc
26
+ (utc.to_f * 1000).to_i
27
+ end
28
+ end
29
+ end
30
+ end
@@ -132,6 +132,15 @@ module Optimizely
132
132
  variables.delete :user_id
133
133
  end
134
134
 
135
+ if variables.include? :variable_type
136
+ # Empty variable_type is a valid user ID.
137
+ unless variables[:variable_type].is_a?(String) || !variables[:variable_type]
138
+ is_valid = false
139
+ logger.log(level, "#{Constants::INPUT_VARIABLES['VARIABLE_TYPE']} is invalid")
140
+ end
141
+ variables.delete :variable_type
142
+ end
143
+
135
144
  variables.each do |key, value|
136
145
  next if value.is_a?(String) && !value.empty?
137
146
 
@@ -24,6 +24,7 @@ module Optimizely
24
24
  # DEPRECATED: ACTIVATE notification type is deprecated since relase 3.1.0.
25
25
  ACTIVATE: 'ACTIVATE: experiment, user_id, attributes, variation, event',
26
26
  DECISION: 'DECISION: type, user_id, attributes, decision_info',
27
+ LOG_EVENT: 'LOG_EVENT: type, log_event',
27
28
  OPTIMIZELY_CONFIG_UPDATE: 'optimizely_config_update',
28
29
  TRACK: 'TRACK: event_key, user_id, attributes, event_tags, event'
29
30
  }.freeze
@@ -137,6 +138,10 @@ module Optimizely
137
138
  end
138
139
  end
139
140
 
141
+ def notification_count(notification_type)
142
+ @notifications.include?(notification_type) ? @notifications[notification_type].count : 0
143
+ end
144
+
140
145
  private
141
146
 
142
147
  def notification_type_valid?(notification_type)
@@ -17,8 +17,56 @@
17
17
  #
18
18
 
19
19
  require 'optimizely'
20
+ require 'optimizely/event_dispatcher'
21
+ require 'optimizely/event/batch_event_processor'
20
22
  module Optimizely
21
23
  class OptimizelyFactory
24
+ attr_reader :max_event_batch_size, :max_event_flush_interval
25
+
26
+ # Convenience method for setting the maximum number of events contained within a batch.
27
+ # @param batch_size Integer - Sets size of EventQueue.
28
+ # @param logger - Optional LoggerInterface Provides a log method to log messages.
29
+ def self.max_event_batch_size(batch_size, logger)
30
+ unless batch_size.is_a? Integer
31
+ logger.log(
32
+ Logger::ERROR,
33
+ "Batch size is invalid, setting to default batch size #{BatchEventProcessor::DEFAULT_BATCH_SIZE}."
34
+ )
35
+ return
36
+ end
37
+
38
+ unless batch_size.positive?
39
+ logger.log(
40
+ Logger::ERROR,
41
+ "Batch size is negative, setting to default batch size #{BatchEventProcessor::DEFAULT_BATCH_SIZE}."
42
+ )
43
+ return
44
+ end
45
+ @max_event_batch_size = batch_size
46
+ end
47
+
48
+ # Convenience method for setting the maximum time interval in milliseconds between event dispatches.
49
+ # @param flush_interval Numeric - Time interval between event dispatches.
50
+ # @param logger - Optional LoggerInterface Provides a log method to log messages.
51
+ def self.max_event_flush_interval(flush_interval, logger)
52
+ unless flush_interval.is_a? Numeric
53
+ logger.log(
54
+ Logger::ERROR,
55
+ "Flush interval is invalid, setting to default flush interval #{BatchEventProcessor::DEFAULT_BATCH_INTERVAL}."
56
+ )
57
+ return
58
+ end
59
+
60
+ unless flush_interval.positive?
61
+ logger.log(
62
+ Logger::ERROR,
63
+ "Flush interval is negative, setting to default flush interval #{BatchEventProcessor::DEFAULT_BATCH_INTERVAL}."
64
+ )
65
+ return
66
+ end
67
+ @max_event_flush_interval = flush_interval
68
+ end
69
+
22
70
  # Returns a new optimizely instance.
23
71
  #
24
72
  # @params sdk_key - Required String uniquely identifying the fallback datafile corresponding to project.
@@ -29,7 +77,7 @@ module Optimizely
29
77
 
30
78
  # Returns a new optimizely instance.
31
79
  #
32
- # @param config_manager - Required ConfigManagerInterface Responds to get_config.
80
+ # @param config_manager - Required ConfigManagerInterface Responds to 'config' method.
33
81
  def self.default_instance_with_config_manager(config_manager)
34
82
  Optimizely::Project.new(nil, nil, nil, nil, nil, nil, nil, config_manager)
35
83
  end
@@ -44,8 +92,11 @@ module Optimizely
44
92
  # By default all exceptions will be suppressed.
45
93
  # @param skip_json_validation - Optional Boolean param to skip JSON schema validation of the provided datafile.
46
94
  # @param user_profile_service - Optional UserProfileServiceInterface Provides methods to store and retreive user profiles.
47
- # @param config_manager - Optional ConfigManagerInterface Responds to get_config.
95
+ # @param config_manager - Optional ConfigManagerInterface Responds to 'config' method.
48
96
  # @param notification_center - Optional Instance of NotificationCenter.
97
+ #
98
+ # if @max_event_batch_size and @max_event_flush_interval are nil then default batchsize and flush_interval
99
+ # will be used to setup batchEventProcessor.
49
100
  def self.custom_instance(
50
101
  sdk_key,
51
102
  datafile = nil,
@@ -57,6 +108,13 @@ module Optimizely
57
108
  config_manager = nil,
58
109
  notification_center = nil
59
110
  )
111
+ event_processor = BatchEventProcessor.new(
112
+ event_dispatcher: event_dispatcher || EventDispatcher.new,
113
+ batch_size: @max_event_batch_size,
114
+ flush_interval: @max_event_flush_interval,
115
+ notification_center: notification_center
116
+ )
117
+
60
118
  Optimizely::Project.new(
61
119
  datafile,
62
120
  event_dispatcher,
@@ -66,7 +124,8 @@ module Optimizely
66
124
  user_profile_service,
67
125
  sdk_key,
68
126
  config_manager,
69
- notification_center
127
+ notification_center,
128
+ event_processor
70
129
  )
71
130
  end
72
131
  end