vwo-fme-ruby-sdk 1.8.0 → 1.9.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6d45fa0809a1e1abfab41886fb30f9987d6e7910dc146d3f9c389d833870480c
4
- data.tar.gz: af6be7b298827b5cfb5facac2b4227bdc74afc21d55da7d30b4652d57a708b4e
3
+ metadata.gz: f6cd54e4729aaae54bea625e05ebba1990f2a4295b3b04352cda862d06b92603
4
+ data.tar.gz: c1664141b090c9f2fea0c456a562daadc13ca66ab971fd2cb86c3af0e1c08574
5
5
  SHA512:
6
- metadata.gz: e92a90c764fbdb5cba7417172079bc08480b71246028add35c098ef8700754b33bd736e3384444f7b3be2f36c0b2b2f93073a69b4622cbbd571b8d6530e6d237
7
- data.tar.gz: 820f2d137b7506d3bdc4eb629c0d383d9d3e04ecb99cd99f47a473977ef0f4ca92bb23df3b0f23aceefee98964d97dc57a3244a32bc996f190f3e5abc4eee596
6
+ metadata.gz: 196e6e75933ee4670db874874a43428eb7b0abddeb5b8b013c513b410d1e539dc395a884a84ffcfe2d018127acb28933d26edf1305df9f8462991b7a52935853
7
+ data.tar.gz: 47bb55030326b0eac4694f2cffa50ab85063e08c88f27462b027e0d4327221b3dfb0b6f30f15d283484a1b11a4d0a2924e24a6ddde8b11b73b7fbb0b9390ef21
@@ -16,5 +16,6 @@
16
16
 
17
17
  "EVENT_BATCH_BEFORE_FLUSHING": "Flushing event queue {manually} having {length} events for Account ID:{accountId}. {timer}",
18
18
  "EVENT_BATCH_FLUSH": "Manually flushing batch events for Account ID:{accountId} having {queueLength} events",
19
- "BATCH_QUEUE_EMPTY": "Batch queue is empty. Nothing to flush."
19
+ "BATCH_QUEUE_EMPTY": "Batch queue is empty. Nothing to flush.",
20
+ "WEB_UUID_FOUND": "VWO Web Testing identified UUID {uuid} as the Context ID for API {apiName}"
20
21
  }
@@ -65,7 +65,7 @@ class FlagApi
65
65
 
66
66
  if variation
67
67
  LoggerService.log(LogLevelEnum::INFO, "STORED_VARIATION_FOUND", {variationKey: variation.get_key, userId: context.get_id, experimentKey: stored_data[:experiment_key], experimentType: "experiment"})
68
- return GetFlagResponse.new(true, variation.get_variables)
68
+ return GetFlagResponse.new(true, variation.get_variables, context.get_uuid, context.get_session_id)
69
69
  end
70
70
  end
71
71
  elsif stored_data && stored_data[:rollout_key] && stored_data[:rollout_id]
@@ -91,7 +91,7 @@ class FlagApi
91
91
 
92
92
  if feature.nil?
93
93
  LoggerService.log(LogLevelEnum::ERROR, "FEATURE_NOT_FOUND", {featureKey: feature_key, an: ApiEnum::GET_FLAG, sId: context.get_session_id, uuid: context.get_uuid})
94
- return GetFlagResponse.new(false, [])
94
+ return GetFlagResponse.new(false, [], context.get_uuid, context.get_session_id)
95
95
  end
96
96
 
97
97
  # Segmentation evaluation
@@ -219,7 +219,7 @@ class FlagApi
219
219
  end
220
220
 
221
221
  # Return final evaluated feature flag
222
- return GetFlagResponse.new(is_enabled, experiment_variation_to_return&.get_variables || rollout_variation_to_return&.get_variables || [])
222
+ return GetFlagResponse.new(is_enabled, experiment_variation_to_return&.get_variables || rollout_variation_to_return&.get_variables || [], context.get_uuid, context.get_session_id)
223
223
  end
224
224
 
225
225
  private
@@ -40,11 +40,9 @@ class SetAttributeApi
40
40
 
41
41
  # Construct payload data for multiple attributes
42
42
  payload = NetworkUtil.get_attribute_payload_data(
43
- context.id,
44
43
  EventEnum::VWO_SYNC_VISITOR_PROP,
45
44
  attributes,
46
- context.user_agent,
47
- context.ip_address
45
+ context
48
46
  )
49
47
 
50
48
  # check if batching is enabled
@@ -63,11 +63,9 @@ class TrackApi
63
63
 
64
64
  # Prepare the payload for the track goal
65
65
  payload = NetworkUtil.get_track_goal_payload_data(
66
- context.id,
67
66
  event_name,
68
67
  event_properties,
69
- context.user_agent,
70
- context.ip_address
68
+ context
71
69
  )
72
70
 
73
71
  # check if batching is enabled
@@ -17,7 +17,7 @@
17
17
  # Define the Constants module
18
18
  module Constants
19
19
  SDK_NAME = 'vwo-fme-ruby-sdk'.freeze
20
- SDK_VERSION = '1.8.0'.freeze
20
+ SDK_VERSION = '1.9.0'.freeze
21
21
 
22
22
  MAX_TRAFFIC_PERCENT = 100
23
23
  MAX_TRAFFIC_VALUE = 10_000
@@ -31,6 +31,7 @@ module Constants
31
31
  MIN_EVENTS_PER_REQUEST = 1
32
32
 
33
33
  SEED_URL = 'https://vwo.com'.freeze # Define SEED_URL
34
+ WEB_UUID_REGEX = /\A[DJ][0-9A-Fa-f]{32}\z/.freeze
34
35
  HTTP_PROTOCOL = 'http'.freeze
35
36
  HTTPS_PROTOCOL = 'https'.freeze
36
37
 
@@ -18,7 +18,7 @@ require_relative '../../constants/constants'
18
18
 
19
19
  class SettingsModel
20
20
  attr_reader :sdk_key, :account_id, :usage_stats_account_id, :version, :collection_prefix,
21
- :features, :campaigns, :campaign_groups, :groups, :poll_interval
21
+ :features, :campaigns, :campaign_groups, :groups, :poll_interval, :is_web_connectivity_enabled
22
22
 
23
23
  def initialize(settings)
24
24
  @sdk_key = settings["sdkKey"]
@@ -31,6 +31,7 @@ class SettingsModel
31
31
  @campaigns = []
32
32
  @campaign_groups = settings["campaignGroups"] || {}
33
33
  @groups = settings["groups"] || {}
34
+ @is_web_connectivity_enabled = settings.fetch("isWebConnectivityEnabled", true)
34
35
 
35
36
  process_features(settings)
36
37
  process_campaigns(settings)
@@ -93,4 +94,8 @@ class SettingsModel
93
94
  @campaigns << CampaignModel.new.model_from_dictionary(campaign)
94
95
  end
95
96
  end
97
+
98
+ def get_is_web_connectivity_enabled
99
+ @is_web_connectivity_enabled
100
+ end
96
101
  end
@@ -53,9 +53,9 @@ class ContextModel
53
53
  @post_segmentation_variables = context[:postSegmentationVariables] if context.key?(:postSegmentationVariables)
54
54
  @vwo = ContextVWOModel.new.model_from_dictionary(context[:_vwo]) if context.key?(:_vwo)
55
55
 
56
- # check if sessionId is present in context and should be non null and non empty string
57
- if context.key?(:sessionId) && context[:sessionId].is_a?(String) && !context[:sessionId].empty?
58
- @session_id = context[:sessionId]
56
+ # check if sessionId is present in context and should be non null and non empty
57
+ if context.key?(:sessionId) && !context[:sessionId].nil? && !context[:sessionId].to_s.empty?
58
+ @session_id = context[:sessionId].to_i
59
59
  else
60
60
  @session_id = Time.now.to_i
61
61
  end
@@ -13,26 +13,38 @@
13
13
  # limitations under the License.
14
14
 
15
15
  class GetFlagResponse
16
- attr_reader :is_enabled, :get_variables, :get_variable
16
+ attr_reader :is_enabled, :get_variables, :get_variable, :get_uuid, :get_session_id
17
17
 
18
- def initialize(is_enabled, variables = [])
19
- @is_enabled = is_enabled
20
- @variables = variables
21
- end
18
+ def initialize(is_enabled, variables = [], uuid = nil, session_id = nil)
19
+ @is_enabled = is_enabled
20
+ @variables = variables
21
+ @uuid = uuid
22
+ @session_id = session_id
23
+ end
22
24
 
23
- # Define method for is_enabled
24
- def is_enabled
25
- @is_enabled
26
- end
27
-
28
- # Define method for get_variables
29
- def get_variables
30
- @variables
31
- end
32
-
33
- # Define method for get_variable
34
- def get_variable(key, default_value = nil)
35
- variable = @variables.find { |var| var.key == key }
36
- variable ? variable.value : default_value
37
- end
38
- end
25
+ # Define method for is_enabled
26
+ def is_enabled
27
+ @is_enabled
28
+ end
29
+
30
+ # Define method for get_variables
31
+ def get_variables
32
+ @variables
33
+ end
34
+
35
+ # Define method for get_variable
36
+ def get_variable(key, default_value = nil)
37
+ variable = @variables.find { |var| var.key == key }
38
+ variable ? variable.value : default_value
39
+ end
40
+
41
+ # Define method for get_uuid
42
+ def get_uuid
43
+ @uuid
44
+ end
45
+
46
+ # Define method for get_session_id
47
+ def get_session_id
48
+ @session_id
49
+ end
50
+ end
@@ -157,6 +157,17 @@ class NetworkUtil
157
157
 
158
158
  properties = _get_event_base_payload(user_id, event_name, visitor_user_agent, ip_address)
159
159
 
160
+ # use sessionId from context if present
161
+ if context.get_session_id
162
+ properties[:d][:sessionId] = context.get_session_id
163
+ end
164
+
165
+ # use uuid from context if present
166
+ if context.get_uuid && !context.get_uuid.empty?
167
+ properties[:d][:visId] = context.get_uuid
168
+ properties[:d][:msgId] = "#{context.get_uuid}-#{get_current_unix_timestamp_in_millis}"
169
+ end
170
+
160
171
  properties[:d][:event][:props][:id] = campaign_id
161
172
  properties[:d][:event][:props][:variation] = variation_id
162
173
  properties[:d][:event][:props][:isFirst] = 1
@@ -191,10 +202,21 @@ class NetworkUtil
191
202
  end
192
203
 
193
204
  # Constructs payload for tracking goals with custom event properties
194
- def get_track_goal_payload_data(user_id, event_name, event_properties, visitor_user_agent = '', ip_address = '')
195
- properties = _get_event_base_payload(user_id, event_name, visitor_user_agent, ip_address)
205
+ def get_track_goal_payload_data(event_name, event_properties, context)
206
+ properties = _get_event_base_payload(context.id, event_name, context.user_agent, context.ip_address)
196
207
  properties[:d][:event][:props][:isCustomEvent] = true
197
208
 
209
+ # use sessionId from context if present
210
+ if context.get_session_id
211
+ properties[:d][:sessionId] = context.get_session_id
212
+ end
213
+
214
+ # use uuid from context if present
215
+ if context.get_uuid && !context.get_uuid.empty?
216
+ properties[:d][:visId] = context.get_uuid
217
+ properties[:d][:msgId] = "#{context.get_uuid}-#{get_current_unix_timestamp_in_millis}"
218
+ end
219
+
198
220
  if SettingsService.instance.is_gateway_service_provided
199
221
  properties[:d][:event][:props][:variation] = 1
200
222
  properties[:d][:event][:props][:id] = 1 # Temporary value for ID
@@ -207,16 +229,27 @@ class NetworkUtil
207
229
  LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_TRACK_GOAL", {
208
230
  eventName: event_name,
209
231
  accountId: SettingsService.instance.account_id,
210
- userId: user_id
232
+ userId: context.id
211
233
  })
212
234
 
213
235
  properties
214
236
  end
215
237
 
216
- def get_attribute_payload_data(user_id, event_name, event_properties, visitor_user_agent = '', ip_address = '')
217
- properties = _get_event_base_payload(user_id, event_name, visitor_user_agent, ip_address)
238
+ def get_attribute_payload_data(event_name, event_properties, context)
239
+ properties = _get_event_base_payload(context.id, event_name, context.user_agent, context.ip_address)
218
240
  properties[:d][:event][:props][:isCustomEvent] = true
219
241
 
242
+ # use sessionId from context if present
243
+ if context.get_session_id
244
+ properties[:d][:sessionId] = context.get_session_id
245
+ end
246
+
247
+ # use uuid from context if present
248
+ if context.get_uuid && !context.get_uuid.empty?
249
+ properties[:d][:visId] = context.get_uuid
250
+ properties[:d][:msgId] = "#{context.get_uuid}-#{get_current_unix_timestamp_in_millis}"
251
+ end
252
+
220
253
  if event_properties.is_a?(Hash) && !event_properties.empty?
221
254
  event_properties.each { |key, value| properties[:d][:visitor][:props][key] = value }
222
255
  end
@@ -224,7 +257,7 @@ class NetworkUtil
224
257
  LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_SYNC_VISITOR_PROP", {
225
258
  eventName: event_name,
226
259
  accountId: SettingsService.instance.account_id,
227
- userId: user_id
260
+ userId: context.id
228
261
  })
229
262
  properties
230
263
  end
@@ -14,8 +14,7 @@
14
14
 
15
15
  require 'uuidtools'
16
16
  require 'securerandom'
17
-
18
- SEED_URL = 'https://vwo.com' # Replace with actual SEED_URL
17
+ require_relative '../constants/constants'
19
18
 
20
19
  class UUIDUtil
21
20
  # Generates a random UUID based on an API key.
@@ -34,7 +33,7 @@ class UUIDUtil
34
33
  # @param account_id [String] The account ID associated with the user.
35
34
  # @return [String] A UUID string formatted without dashes and in uppercase.
36
35
  def self.get_uuid(user_id, account_id)
37
- vwo_namespace = UUIDTools::UUID.sha1_create(UUIDTools::UUID_URL_NAMESPACE, SEED_URL)
36
+ vwo_namespace = UUIDTools::UUID.sha1_create(UUIDTools::UUID_URL_NAMESPACE, Constants::SEED_URL)
38
37
  user_id_namespace = generate_uuid(account_id, vwo_namespace)
39
38
  uuid_for_user_id_account_id = generate_uuid(user_id, user_id_namespace)
40
39
 
@@ -52,4 +51,46 @@ class UUIDUtil
52
51
  name_str = name.to_s
53
52
  UUIDTools::UUID.sha1_create(namespace, name_str)
54
53
  end
54
+
55
+ # Checks if the given ID is a valid Web UUID.
56
+ #
57
+ # @param id [String] The ID to check.
58
+ # @return [Boolean] True if the ID is a valid Web UUID, false otherwise.
59
+ def self.web_uuid?(id)
60
+ return false unless id.is_a?(String)
61
+
62
+ !!(id =~ Constants::WEB_UUID_REGEX)
63
+ end
64
+
65
+ # Generates a UUID for a user based on their user_id and account_id.
66
+ #
67
+ # @param settings [SettingsModel] The settings of the VWO client.
68
+ # @param context [Hash] The context of the user.
69
+ # @param api_name [String] The name of the API called.
70
+ # @return [String] A UUID string formatted without dashes and in uppercase.
71
+ def self.get_uuid_from_context(settings, context, api_name)
72
+ if settings.get_is_web_connectivity_enabled != false
73
+ # if web connectivity is enabled, check if context[:id] is a valid web UUID
74
+ if context && web_uuid?(context[:id])
75
+ # if context[:id] is a valid web UUID, set it as uuid
76
+ LoggerService.log(LogLevelEnum::DEBUG, "WEB_UUID_FOUND", {apiName: api_name, uuid: context[:id]})
77
+ return context[:id]
78
+ else
79
+ # if context[:useIdForWeb] is true and context[:id] is not a valid web UUID, throw error
80
+ if context && context[:useIdForWeb] == true
81
+ raise StandardError, 'UUID passed in context.id is not a valid UUID'
82
+ end
83
+ return get_uuid(
84
+ context[:id].to_s,
85
+ SettingsService.instance.account_id.to_s
86
+ )
87
+ end
88
+ else
89
+ # if web connectivity is disabled, fallback to server-side UUID derivation
90
+ return get_uuid(
91
+ context[:id].to_s,
92
+ SettingsService.instance.account_id.to_s
93
+ )
94
+ end
95
+ end
55
96
  end
@@ -24,6 +24,7 @@ require_relative 'utils/network_util'
24
24
  require_relative 'models/schemas/settings_schema_validation'
25
25
  require_relative 'services/batch_event_queue'
26
26
  require_relative 'enums/api_enum'
27
+ require_relative 'utils/uuid_util'
27
28
 
28
29
  class VWOClient
29
30
  attr_accessor :settings, :original_settings
@@ -49,11 +50,30 @@ class VWOClient
49
50
  # @return [GetFlagResponse] The flag for the given feature key and context
50
51
  def get_flag(feature_key, context)
51
52
  api_name = 'get_flag'
52
- error_response = GetFlagResponse.new(false, [])
53
+ uuid = nil
54
+ session_id = nil
55
+
56
+ # check if sessionId is present in context and should be non null and non empty
57
+ if context.is_a?(Hash) && context.key?(:sessionId) && !context[:sessionId].nil? && !context[:sessionId].to_s.empty?
58
+ session_id = context[:sessionId].to_i
59
+ else
60
+ session_id = Time.now.to_i
61
+ end
53
62
 
54
63
  begin
55
64
  hooks_service = HooksService.new(@options)
56
65
  LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
66
+ unless context.is_a?(Hash)
67
+ raise TypeError, 'Invalid context'
68
+ end
69
+ unless context[:id].is_a?(String) && !context[:id].empty?
70
+ raise TypeError, 'Invalid context, id should be a non-empty string'
71
+ end
72
+ # make a copy of the context to avoid modifying the original context
73
+ context_copy = context.dup
74
+ uuid = UUIDUtil.get_uuid_from_context(@settings, context_copy, api_name)
75
+ # add the uuid to the context copy
76
+ context_copy[:uuid] = uuid
57
77
 
58
78
  unless feature_key.is_a?(String) && !feature_key.empty?
59
79
  raise TypeError, 'feature_key should be a non-empty string'
@@ -61,18 +81,12 @@ class VWOClient
61
81
  unless SettingsService.instance.is_settings_valid
62
82
  raise TypeError, 'Invalid Settings'
63
83
  end
64
- unless context.is_a?(Hash)
65
- raise TypeError, 'Invalid context'
66
- end
67
- unless context[:id].is_a?(String) && !context[:id].empty?
68
- raise TypeError, 'Invalid context, id should be a non-empty string'
69
- end
70
84
 
71
- context_model = ContextModel.new.model_from_dictionary(context)
85
+ context_model = ContextModel.new.model_from_dictionary(context_copy)
72
86
  FlagApi.new.get(feature_key, @settings, context_model, hooks_service)
73
87
  rescue StandardError => e
74
88
  LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::GET_FLAG})
75
- error_response
89
+ GetFlagResponse.new(false, [], uuid, session_id)
76
90
  end
77
91
  end
78
92
 
@@ -87,7 +101,14 @@ class VWOClient
87
101
  begin
88
102
  hooks_service = HooksService.new(@options)
89
103
  LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
90
-
104
+
105
+ unless context.is_a?(Hash) && context[:id].is_a?(String) && !context[:id].empty?
106
+ raise TypeError, 'Invalid context, id should be a non-empty string'
107
+ end
108
+ # make a copy of the context to avoid modifying the original context
109
+ context_copy = context.dup
110
+ context_copy[:uuid] = UUIDUtil.get_uuid_from_context(@settings, context_copy, api_name)
111
+
91
112
  unless event_name.is_a?(String) && !event_name.empty?
92
113
  raise TypeError, 'event_name should be a non-empty string'
93
114
  end
@@ -97,11 +118,8 @@ class VWOClient
97
118
  unless SettingsService.instance.is_settings_valid
98
119
  raise TypeError, 'Invalid Settings'
99
120
  end
100
- unless context[:id].is_a?(String) && !context[:id].empty?
101
- raise TypeError, 'Invalid context, id should be a non-empty string'
102
- end
103
121
 
104
- context_model = ContextModel.new.model_from_dictionary(context)
122
+ context_model = ContextModel.new.model_from_dictionary(context_copy)
105
123
  TrackApi.new.track(@settings, event_name, context_model, event_properties, hooks_service)
106
124
  rescue StandardError => e
107
125
  LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::TRACK_EVENT})
@@ -118,21 +136,24 @@ class VWOClient
118
136
 
119
137
  begin
120
138
  LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
121
-
122
- unless attributes.is_a?(Hash) && !attributes.empty?
123
- raise TypeError, 'Attributes should be a hash with key-value pairs and non-empty'
124
- end
125
139
  unless context.is_a?(Hash)
126
140
  raise TypeError, 'Invalid context'
127
141
  end
128
142
  unless context[:id].is_a?(String) && !context[:id].empty?
129
143
  raise TypeError, 'Invalid context, id should be a non-empty string'
130
144
  end
145
+ # make a copy of the context to avoid modifying the original context
146
+ context_copy = context.dup
147
+ context_copy[:uuid] = UUIDUtil.get_uuid_from_context(@settings, context_copy, api_name)
148
+
149
+ unless attributes.is_a?(Hash) && !attributes.empty?
150
+ raise TypeError, 'Attributes should be a hash with key-value pairs and non-empty'
151
+ end
131
152
  unless SettingsService.instance.is_settings_valid
132
153
  raise TypeError, 'Invalid Settings'
133
154
  end
134
155
 
135
- context_model = ContextModel.new.model_from_dictionary(context)
156
+ context_model = ContextModel.new.model_from_dictionary(context_copy)
136
157
  SetAttributeApi.new.set_attribute(attributes, context_model)
137
158
  rescue StandardError => e
138
159
  LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::SET_ATTRIBUTE})
data/lib/vwo.rb CHANGED
@@ -106,4 +106,20 @@ class VWO
106
106
  puts "[ERROR]: VWO-SDK: Got error while initializing VWO: #{e.message}"
107
107
  end
108
108
  end
109
+
110
+ # Generates a UUID for a user based on their user_id and account_id.
111
+ #
112
+ # @param user_id [String] The user's ID.
113
+ # @param account_id [String] The account ID associated with the user.
114
+ # @return [String] A UUID string formatted without dashes and in uppercase.
115
+ def self.get_uuid(user_id, account_id)
116
+ # check if user_id and account_id are non-empty strings
117
+ if !user_id.is_a?(String) || user_id.empty? || !account_id.is_a?(String) || account_id.empty?
118
+ puts "User ID and account ID must be non-empty strings"
119
+ return nil
120
+ end
121
+
122
+ # generate and return a new uuid based on user_id and account_id
123
+ return UUIDUtil.get_uuid(user_id, account_id)
124
+ end
109
125
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vwo-fme-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.8.0
4
+ version: 1.9.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - VWO
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-01-21 00:00:00.000000000 Z
11
+ date: 2026-02-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools