vwo-fme-ruby-sdk 1.4.1 → 1.6.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: 314b352ba0d4578530e72be01599eee2846ed17f080adf40ddaadfe404d1a849
4
- data.tar.gz: 54f0126d7666337219026a5c47c046a9516c60ffa658ae8e3d45f379258dc946
3
+ metadata.gz: aba596eb41128d18fecdbac11316dfe345d0fdcd2b8c1a0a612b1145bde5b779
4
+ data.tar.gz: 0ca14ec46f853f5cd86171ee4a0a7c93fb57f009872f847e2777fc3375e01cd6
5
5
  SHA512:
6
- metadata.gz: 7dcceb86720c7f1d8c763d0974beb710cca033724bf76f35a884b80f3c7bb12b3adea108b04fdccdc85f297e585ee9813bf64ce9a854313e15ee775f34d20780
7
- data.tar.gz: 21c80bd1c369b7344c93d3e72858fa4a47d4ad2199b9df63bf8c95371e92f39adcbd82012903ab26c7e09e1873e285d201229d040c0781fb1071f213ff4c2f9d
6
+ metadata.gz: 398d9be0a02c8b364712eb1b1ef38061138ecfcd71a8ec4f8ec5a6ad5d7ef1b0a6ee678f8ed1cb057bc6db337baf885cd3852c0ad935d480d5cd705ab7e2768c
7
+ data.tar.gz: ae7f284247ccc40c0c881dcd7ab23665989b71f3252931e755417a428cc606cf5e7af091cac9a28f20d274c5a782afab095633635ced57a23879b965579d56a6
@@ -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.4.1'.freeze
20
+ SDK_VERSION = '1.6.0'.freeze
21
21
 
22
22
  MAX_TRAFFIC_PERCENT = 100
23
23
  MAX_TRAFFIC_VALUE = 10_000
@@ -15,6 +15,7 @@
15
15
  # Event types
16
16
  module EventEnum
17
17
  VWO_VARIATION_SHOWN = 'vwo_variationShown'
18
- VWO_SYNC_VISITOR_PROP = 'vwo_syncVisitorProp',
18
+ VWO_SYNC_VISITOR_PROP = 'vwo_syncVisitorProp'
19
19
  VWO_INIT_CALLED = 'vwo_fmeSdkInit'
20
- end
20
+ VWO_USAGE_STATS = 'vwo_sdkUsageStats'
21
+ end
@@ -84,6 +84,7 @@ class SettingsSchema
84
84
  optional(:sdkKey).maybe(:string)
85
85
  required(:version).filled(:integer)
86
86
  required(:accountId).filled(:integer)
87
+ optional(:usageStatsAccountId).maybe(:integer)
87
88
  optional(:features).array(:hash)
88
89
  required(:campaigns).array(:hash)
89
90
  optional(:groups).maybe(:hash)
@@ -17,12 +17,13 @@ require_relative '../campaign/feature_model'
17
17
  require_relative '../../constants/constants'
18
18
 
19
19
  class SettingsModel
20
- attr_reader :sdk_key, :account_id, :version, :collection_prefix,
20
+ attr_reader :sdk_key, :account_id, :usage_stats_account_id, :version, :collection_prefix,
21
21
  :features, :campaigns, :campaign_groups, :groups, :poll_interval
22
22
 
23
23
  def initialize(settings)
24
24
  @sdk_key = settings["sdkKey"]
25
25
  @account_id = settings["accountId"]
26
+ @usage_stats_account_id = settings["usageStatsAccountId"]
26
27
  @version = settings["version"]
27
28
  @collection_prefix = settings["collectionPrefix"]
28
29
  @poll_interval = settings["pollInterval"] || Constants::POLLING_INTERVAL
@@ -59,6 +60,10 @@ class SettingsModel
59
60
  @account_id
60
61
  end
61
62
 
63
+ def get_usage_stats_account_id
64
+ @usage_stats_account_id
65
+ end
66
+
62
67
  def get_version
63
68
  @version
64
69
  end
@@ -15,14 +15,15 @@
15
15
  require_relative './context_vwo_model'
16
16
 
17
17
  class ContextModel
18
- attr_accessor :id, :user_agent, :ip_address, :custom_variables, :variation_targeting_variables, :vwo
18
+ attr_accessor :id, :user_agent, :ip_address, :custom_variables, :variation_targeting_variables, :post_segmentation_variables, :vwo
19
19
 
20
- def initialize(id = nil, user_agent = nil, ip_address = nil, custom_variables = {}, variation_targeting_variables = {}, vwo = nil)
20
+ def initialize(id = nil, user_agent = nil, ip_address = nil, custom_variables = {}, variation_targeting_variables = {}, post_segmentation_variables = {}, vwo = nil)
21
21
  @id = id
22
22
  @user_agent = user_agent
23
23
  @ip_address = ip_address
24
24
  @custom_variables = custom_variables || {}
25
25
  @variation_targeting_variables = variation_targeting_variables || {}
26
+ @post_segmentation_variables = post_segmentation_variables || {}
26
27
  @vwo = vwo
27
28
  end
28
29
 
@@ -45,6 +46,7 @@ class ContextModel
45
46
  @ip_address = context[:ipAddress]
46
47
  @custom_variables = context[:customVariables] if context.key?(:customVariables)
47
48
  @variation_targeting_variables = context[:variationTargetingVariables] if context.key?(:variationTargetingVariables)
49
+ @post_segmentation_variables = context[:postSegmentationVariables] if context.key?(:postSegmentationVariables)
48
50
  @vwo = ContextVWOModel.new.model_from_dictionary(context[:_vwo]) if context.key?(:_vwo)
49
51
 
50
52
  self
@@ -90,6 +92,14 @@ class ContextModel
90
92
  @variation_targeting_variables = variation_targeting_variables
91
93
  end
92
94
 
95
+ def get_post_segmentation_variables
96
+ @post_segmentation_variables
97
+ end
98
+
99
+ def set_post_segmentation_variables(post_segmentation_variables)
100
+ @post_segmentation_variables = post_segmentation_variables
101
+ end
102
+
93
103
  def get_vwo
94
104
  @vwo
95
105
  end
@@ -21,9 +21,9 @@ module SegmentOperandRegexEnum
21
21
  REGEX_MATCH = '^regex\\((.*)\\)'
22
22
  STARTING_STAR = '^\\*'
23
23
  ENDING_STAR = '\\*$'
24
- GREATER_THAN_MATCH = '^gt\\((\\d+\\.?\\d*|\\.\\d+)\\)'
25
- GREATER_THAN_EQUAL_TO_MATCH = '^gte\\((\\d+\\.?\\d*|\\.\\d+)\\)'
26
- LESS_THAN_MATCH = '^lt\\((\\d+\\.?\\d*|\\.\\d+)\\)'
27
- LESS_THAN_EQUAL_TO_MATCH = '^lte\\((\\d+\\.?\\d*|\\.\\d+)\\)'
24
+ GREATER_THAN_MATCH = '^gt\\((\\d+(?:\\.\\d+)*|\\.\\d+)\\)'
25
+ GREATER_THAN_EQUAL_TO_MATCH = '^gte\\((\\d+(?:\\.\\d+)*|\\.\\d+)\\)'
26
+ LESS_THAN_MATCH = '^lt\\((\\d+(?:\\.\\d+)*|\\.\\d+)\\)'
27
+ LESS_THAN_EQUAL_TO_MATCH = '^lte\\((\\d+(?:\\.\\d+)*|\\.\\d+)\\)'
28
28
  end
29
29
 
@@ -27,4 +27,7 @@ module SegmentOperatorValueEnum
27
27
  BROWSER_AGENT = 'browser_string'
28
28
  UA = 'ua'
29
29
  FEATURE_ID = 'featureId'
30
+ IP = 'ip_address'
31
+ BROWSER_VERSION = 'browser_version'
32
+ OS_VERSION = 'os_version'
30
33
  end
@@ -57,6 +57,12 @@ class SegmentEvaluator
57
57
  SegmentOperandEvaluator.new.evaluate_user_dsl(sub_dsl, properties)
58
58
  when SegmentOperatorValueEnum::UA
59
59
  SegmentOperandEvaluator.new.evaluate_user_agent_dsl(sub_dsl, @context)
60
+ when SegmentOperatorValueEnum::IP
61
+ SegmentOperandEvaluator.new.evaluate_string_operand_dsl(sub_dsl, @context, SegmentOperatorValueEnum::IP)
62
+ when SegmentOperatorValueEnum::BROWSER_VERSION
63
+ SegmentOperandEvaluator.new.evaluate_string_operand_dsl(sub_dsl, @context, SegmentOperatorValueEnum::BROWSER_VERSION)
64
+ when SegmentOperatorValueEnum::OS_VERSION
65
+ SegmentOperandEvaluator.new.evaluate_string_operand_dsl(sub_dsl, @context, SegmentOperatorValueEnum::OS_VERSION)
60
66
  else
61
67
  false
62
68
  end
@@ -15,15 +15,22 @@
15
15
  require_relative '../utils/segment_util'
16
16
  require_relative '../enums/segment_operand_value_enum'
17
17
  require_relative '../enums/segment_operand_regex_enum'
18
+ require_relative '../enums/segment_operator_value_enum'
18
19
  require_relative '../../../utils/data_type_util'
19
20
  require_relative '../../../utils/gateway_service_util'
20
21
  require_relative '../../../enums/url_enum'
21
22
  require_relative '../../../services/logger_service'
22
23
  require_relative '../../../models/user/context_model'
23
24
  require_relative '../../../enums/log_level_enum'
25
+
26
+ # SegmentOperandEvaluator class provides methods to evaluate different types of DSL (Domain Specific Language)
27
+ # expressions based on the segment conditions defined for custom variables, user IDs, and user agents.
24
28
  class SegmentOperandEvaluator
29
+ # Regex pattern to check if a string contains non-numeric characters (except decimal point)
30
+ NON_NUMERIC_PATTERN = /[^0-9.]/
31
+
25
32
  # Evaluates the custom variable DSL
26
- # @param dsl_operand_value [String] The operand value to evaluate
33
+ # @param dsl_operand_value [Hash] The operand value to evaluate
27
34
  # @param properties [Hash] The properties to evaluate the operand against
28
35
  # @return [Boolean] True if the operand value matches the tag value, false otherwise
29
36
  def evaluate_custom_variable_dsl(dsl_operand_value, properties)
@@ -42,10 +49,11 @@ class SegmentOperandEvaluator
42
49
  return false
43
50
  end
44
51
 
45
- tag_value = pre_process_tag_value(properties[operand_key])
52
+ tag_value = properties[operand_key]
53
+ attribute_value = pre_process_tag_value(tag_value)
46
54
  list_id = match[1]
47
55
 
48
- query_params_obj = { attribute: tag_value, listId: list_id }
56
+ query_params_obj = { attribute: attribute_value, listId: list_id }
49
57
 
50
58
  begin
51
59
  res = get_from_gateway_service(query_params_obj, UrlEnum::ATTRIBUTE_CHECK)
@@ -60,9 +68,12 @@ class SegmentOperandEvaluator
60
68
 
61
69
  false
62
70
  else
63
- tag_value = pre_process_tag_value(properties[operand_key])
64
- processed_values = process_values(*pre_process_operand_value(operand), tag_value)
65
- extract_result(processed_values[:operand_type], processed_values[:operand_value], processed_values[:tag_value])
71
+ tag_value = properties[operand_key]
72
+ tag_value = pre_process_tag_value(tag_value)
73
+ processed_operand = pre_process_operand_value(operand)
74
+ processed_values = process_values(processed_operand[:operand_value], tag_value)
75
+ tag_value = processed_values[:tag_value]
76
+ extract_result(processed_operand[:operand_type], processed_values[:operand_value], tag_value)
66
77
  end
67
78
  end
68
79
 
@@ -80,21 +91,144 @@ class SegmentOperandEvaluator
80
91
  # @param context [ContextModel] The context to evaluate the operand against
81
92
  # @return [Boolean] True if the operand value matches the tag value, false otherwise
82
93
  def evaluate_user_agent_dsl(dsl_operand_value, context)
83
- return false unless context.get_user_agent
94
+ operand = dsl_operand_value
95
+ unless context.get_user_agent
96
+ LoggerService.log(LogLevelEnum::INFO, 'To Evaluate UserAgent segmentation, please provide userAgent in context', nil)
97
+ return false
98
+ end
84
99
 
85
100
  tag_value = CGI.unescape(context.get_user_agent)
86
- processed_values = process_values(*pre_process_operand_value(dsl_operand_value), tag_value)
87
- extract_result(processed_values[:operand_type], processed_values[:operand_value], processed_values[:tag_value])
101
+ processed_operand = pre_process_operand_value(operand)
102
+ processed_values = process_values(processed_operand[:operand_value], tag_value)
103
+ tag_value = processed_values[:tag_value]
104
+ extract_result(processed_operand[:operand_type], processed_values[:operand_value], tag_value)
88
105
  end
89
106
 
90
- # Pre-processes the tag value
91
- # @param tag_value [String] The tag value to pre-process
92
- # @return [String] The pre-processed tag value
93
- def pre_process_tag_value(tag_value)
94
- return '' if tag_value.nil?
95
- return tag_value if [true, false].include?(tag_value)
107
+ # Evaluates a given string tag value against a DSL operand value.
108
+ # @param dsl_operand_value [String] The DSL operand string (e.g., "contains(\"value\")").
109
+ # @param context [ContextModel] The context object containing the value to evaluate.
110
+ # @param operand_type [String] The type of operand being evaluated (ip_address, browser_version, os_version).
111
+ # @return [Boolean] True if tag value matches DSL operand criteria, false otherwise.
112
+ def evaluate_string_operand_dsl(dsl_operand_value, context, operand_type)
113
+ operand = dsl_operand_value.to_s
114
+
115
+ # Determine the tag value based on operand type
116
+ tag_value = get_tag_value_for_operand_type(context, operand_type)
117
+
118
+ if tag_value.nil?
119
+ log_missing_context_error(operand_type)
120
+ return false
121
+ end
122
+
123
+ operand_type_and_value = pre_process_operand_value(operand)
124
+ processed_values = process_values(operand_type_and_value[:operand_value], tag_value, operand_type)
125
+ processed_tag_value = processed_values[:tag_value]
126
+
127
+ extract_result(
128
+ operand_type_and_value[:operand_type],
129
+ processed_values[:operand_value].to_s.strip.gsub(/"/, ''),
130
+ processed_tag_value
131
+ )
132
+ end
133
+
134
+ # Evaluates IP address DSL expression.
135
+ # @param dsl_operand_value [String] The DSL expression for the IP address.
136
+ # @param context [ContextModel] The context object containing the IP address.
137
+ # @return [Boolean] True if the IP address matches the DSL condition, otherwise false.
138
+ def evaluate_ip_dsl(dsl_operand_value, context)
139
+ evaluate_string_operand_dsl(dsl_operand_value, context, SegmentOperatorValueEnum::IP)
140
+ end
141
+
142
+ # Evaluates browser version DSL expression.
143
+ # @param dsl_operand_value [String] The DSL expression for the browser version.
144
+ # @param context [ContextModel] The context object containing the user agent info.
145
+ # @return [Boolean] True if the browser version matches the DSL condition, otherwise false.
146
+ def evaluate_browser_version_dsl(dsl_operand_value, context)
147
+ evaluate_string_operand_dsl(dsl_operand_value, context, SegmentOperatorValueEnum::BROWSER_VERSION)
148
+ end
149
+
150
+ # Evaluates OS version DSL expression.
151
+ # @param dsl_operand_value [String] The DSL expression for the OS version.
152
+ # @param context [ContextModel] The context object containing the user agent info.
153
+ # @return [Boolean] True if the OS version matches the DSL condition, otherwise false.
154
+ def evaluate_os_version_dsl(dsl_operand_value, context)
155
+ evaluate_string_operand_dsl(dsl_operand_value, context, SegmentOperatorValueEnum::OS_VERSION)
156
+ end
157
+
158
+ # Gets the appropriate tag value based on the operand type.
159
+ # @param context [ContextModel] The context object.
160
+ # @param operand_type [String] The type of operand.
161
+ # @return [String, nil] The tag value or nil if not available.
162
+ def get_tag_value_for_operand_type(context, operand_type)
163
+ case operand_type
164
+ when SegmentOperatorValueEnum::IP
165
+ context.get_ip_address
166
+ when SegmentOperatorValueEnum::BROWSER_VERSION
167
+ get_browser_version_from_context(context)
168
+ else
169
+ # Default works for OS version
170
+ get_os_version_from_context(context)
171
+ end
172
+ end
173
+
174
+ # Gets browser version from context.
175
+ # @param context [ContextModel] The context object.
176
+ # @return [String, nil] The browser version or nil if not available.
177
+ def get_browser_version_from_context(context)
178
+ user_agent = context.get_vwo&.get_ua_info
179
+ return nil unless user_agent && user_agent.is_a?(Hash) && !user_agent.empty?
180
+
181
+ # Assuming UserAgent dictionary contains browser_version
182
+ if user_agent.key?('browser_version')
183
+ return user_agent['browser_version']&.to_s
184
+ end
185
+ nil
186
+ end
187
+
188
+ # Gets OS version from context.
189
+ # @param context [ContextModel] The context object.
190
+ # @return [String, nil] The OS version or nil if not available.
191
+ def get_os_version_from_context(context)
192
+ user_agent = context.get_vwo&.get_ua_info
193
+ return nil unless user_agent && user_agent.is_a?(Hash) && !user_agent.empty?
194
+
195
+ # Assuming UserAgent dictionary contains os_version
196
+ if user_agent.key?('os_version')
197
+ return user_agent['os_version']&.to_s
198
+ end
199
+ nil
200
+ end
201
+
202
+ # Logs appropriate error message for missing context.
203
+ # @param operand_type [String] The type of operand.
204
+ def log_missing_context_error(operand_type)
205
+ case operand_type
206
+ when SegmentOperatorValueEnum::IP
207
+ LoggerService.log(LogLevelEnum::INFO, 'To evaluate IP segmentation, please provide ipAddress in context', nil)
208
+ when SegmentOperatorValueEnum::BROWSER_VERSION
209
+ LoggerService.log(LogLevelEnum::INFO, 'To evaluate browser version segmentation, please provide userAgent in context', nil)
210
+ else
211
+ LoggerService.log(LogLevelEnum::INFO, 'To evaluate OS version segmentation, please provide userAgent in context', nil)
212
+ end
213
+ end
96
214
 
97
- tag_value.to_s
215
+ # Pre-processes the tag value to ensure it is in the correct format for evaluation.
216
+ # @param tag_value [Any] The value to be processed.
217
+ # @return [String, Boolean] The processed tag value, either as a string or a boolean.
218
+ def pre_process_tag_value(tag_value)
219
+ # Default to empty string if undefined
220
+ if tag_value.nil?
221
+ tag_value = ''
222
+ end
223
+ # Convert boolean values to boolean type
224
+ if DataTypeUtil.is_boolean(tag_value)
225
+ tag_value = tag_value ? true : false
226
+ end
227
+ # Convert all non-null values to string
228
+ unless tag_value.nil?
229
+ tag_value = tag_value.to_s
230
+ end
231
+ tag_value
98
232
  end
99
233
 
100
234
  # Pre-processes the operand value
@@ -106,16 +240,23 @@ class SegmentOperandEvaluator
106
240
  { operand_type: SegmentOperandValueEnum::LOWER_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::LOWER_MATCH) }
107
241
  when /#{SegmentOperandRegexEnum::WILDCARD_MATCH}/
108
242
  value = extract_operand_value(operand, SegmentOperandRegexEnum::WILDCARD_MATCH)
109
- if value.match?(SegmentOperandRegexEnum::STARTING_STAR) && value.match?(SegmentOperandRegexEnum::ENDING_STAR)
243
+ starting_star = match_with_regex(value, SegmentOperandRegexEnum::STARTING_STAR)
244
+ ending_star = match_with_regex(value, SegmentOperandRegexEnum::ENDING_STAR)
245
+
246
+ # Determine specific wildcard type
247
+ if starting_star && ending_star
110
248
  type = SegmentOperandValueEnum::STARTING_ENDING_STAR_VALUE
111
- value = value.gsub(/^\*|\*$/, '')
112
- elsif value.match?(SegmentOperandRegexEnum::STARTING_STAR)
249
+ elsif starting_star
113
250
  type = SegmentOperandValueEnum::STARTING_STAR_VALUE
114
- value = value.gsub(/^\*/, '')
115
- elsif value.match?(SegmentOperandRegexEnum::ENDING_STAR)
251
+ elsif ending_star
116
252
  type = SegmentOperandValueEnum::ENDING_STAR_VALUE
117
- value = value.gsub(/\*$/, '')
118
253
  end
254
+
255
+ # Remove wildcard characters from the operand value
256
+ value = value
257
+ .gsub(Regexp.new(SegmentOperandRegexEnum::STARTING_STAR), '')
258
+ .gsub(Regexp.new(SegmentOperandRegexEnum::ENDING_STAR), '')
259
+
119
260
  { operand_type: type, operand_value: value }
120
261
  when /#{SegmentOperandRegexEnum::REGEX_MATCH}/
121
262
  { operand_type: SegmentOperandValueEnum::REGEX_VALUE, operand_value: extract_operand_value(operand, SegmentOperandRegexEnum::REGEX_MATCH) }
@@ -137,29 +278,54 @@ class SegmentOperandEvaluator
137
278
  # @param regex [String] The regex to match the operand against
138
279
  # @return [String] The extracted operand value
139
280
  def extract_operand_value(operand, regex)
140
- match = operand.match(/#{regex}/)
141
- match ? match[1] : ''
281
+ match_result = match_with_regex(operand, regex)
282
+ match_result && match_result[1] ? match_result[1] : ''
142
283
  end
143
284
 
144
- # Processes the values from the operand and tag value
145
- # @param operand_type [Symbol] The type of operand
146
- # @param operand_value [String] The value of the operand
147
- # @param tag_value [String] The value of the tag
148
- # @return [Hash] The processed operand value and tag value
149
- def process_values(operand_type, operand_value, tag_value)
150
- # Extract values from arrays if needed
151
- operand_type = operand_type[1] if operand_type.is_a?(Array)
152
- operand_value = operand_value[1] if operand_value.is_a?(Array)
153
- tag_value = tag_value[1] if tag_value.is_a?(Array)
285
+ # Processes numeric values from operand and tag values, converting them to strings.
286
+ # @param operand_value [Any] The operand value to process.
287
+ # @param tag_value [Any] The tag value to process.
288
+ # @param operand_type [String] The type of operand being evaluated (optional).
289
+ # @return [Hash] An object containing the processed operand and tag values as strings.
290
+ def process_values(operand_value, tag_value, operand_type = nil)
291
+ if [SegmentOperatorValueEnum::IP, SegmentOperatorValueEnum::BROWSER_VERSION, SegmentOperatorValueEnum::OS_VERSION].include?(operand_type)
292
+ return {
293
+ operand_value: operand_value,
294
+ tag_value: tag_value
295
+ }
296
+ end
297
+
298
+ # Convert operand and tag values to floats
299
+ if NON_NUMERIC_PATTERN.match?(tag_value.to_s)
300
+ return {
301
+ operand_value: operand_value,
302
+ tag_value: tag_value
303
+ }
304
+ end
154
305
 
155
306
  processed_operand_value = operand_value.to_f
156
307
  processed_tag_value = tag_value.to_f
157
308
 
158
- if processed_operand_value == 0 || processed_tag_value == 0
159
- return { operand_type: operand_type, operand_value: operand_value, tag_value: tag_value }
309
+ # Return original values if conversion fails
310
+ if processed_operand_value == 0 && operand_value.to_s != '0' && operand_value.to_s != '0.0'
311
+ return {
312
+ operand_value: operand_value,
313
+ tag_value: tag_value
314
+ }
315
+ end
316
+
317
+ if processed_tag_value == 0 && tag_value.to_s != '0' && tag_value.to_s != '0.0'
318
+ return {
319
+ operand_value: operand_value,
320
+ tag_value: tag_value
321
+ }
160
322
  end
161
323
 
162
- { operand_type: operand_type, operand_value: processed_operand_value.to_s, tag_value: processed_tag_value.to_s }
324
+ # Convert numeric values back to strings
325
+ {
326
+ operand_value: processed_operand_value.to_s,
327
+ tag_value: processed_tag_value.to_s
328
+ }
163
329
  end
164
330
 
165
331
  # Extracts the result from the operand value and tag value
@@ -168,31 +334,81 @@ class SegmentOperandEvaluator
168
334
  # @param tag_value [String] The value of the tag
169
335
  # @return [Boolean] True if the operand value matches the tag value, false otherwise
170
336
  def extract_result(operand_type, operand_value, tag_value)
337
+ result = false
338
+
339
+ return false if tag_value.nil?
340
+
341
+ # Ensure operand_value and tag_value are strings
342
+ operand_value_str = operand_value.to_s
343
+ tag_value_str = tag_value.to_s
344
+
171
345
  case operand_type
172
346
  when SegmentOperandValueEnum::LOWER_VALUE
173
- operand_value.downcase == tag_value.downcase
347
+ result = operand_value_str.downcase == tag_value_str.downcase
174
348
  when SegmentOperandValueEnum::STARTING_ENDING_STAR_VALUE
175
- tag_value.include?(operand_value)
349
+ result = tag_value_str.include?(operand_value_str)
176
350
  when SegmentOperandValueEnum::STARTING_STAR_VALUE
177
- tag_value.end_with?(operand_value)
351
+ result = tag_value_str.end_with?(operand_value_str)
178
352
  when SegmentOperandValueEnum::ENDING_STAR_VALUE
179
- tag_value.start_with?(operand_value)
353
+ result = tag_value_str.start_with?(operand_value_str)
180
354
  when SegmentOperandValueEnum::REGEX_VALUE
181
355
  begin
182
- !!Regexp.new(operand_value).match?(tag_value)
356
+ pattern = Regexp.new(operand_value_str)
357
+ result = pattern.match?(tag_value_str)
183
358
  rescue StandardError
184
- false
359
+ result = false
185
360
  end
186
361
  when SegmentOperandValueEnum::GREATER_THAN_VALUE
187
- operand_value.to_f < tag_value.to_f
188
- when SegmentOperandValueEnum::LESS_THAN_VALUE
189
- operand_value.to_f > tag_value.to_f
362
+ result = compare_versions(tag_value_str, operand_value_str) > 0
190
363
  when SegmentOperandValueEnum::GREATER_THAN_EQUAL_TO_VALUE
191
- operand_value.to_f <= tag_value.to_f
364
+ result = compare_versions(tag_value_str, operand_value_str) >= 0
365
+ when SegmentOperandValueEnum::LESS_THAN_VALUE
366
+ result = compare_versions(tag_value_str, operand_value_str) < 0
192
367
  when SegmentOperandValueEnum::LESS_THAN_EQUAL_TO_VALUE
193
- operand_value.to_f >= tag_value.to_f
368
+ result = compare_versions(tag_value_str, operand_value_str) <= 0
194
369
  else
195
- tag_value == operand_value
370
+ # For version-like strings, use version comparison; otherwise use string comparison
371
+ if version_string?(tag_value_str) && version_string?(operand_value_str)
372
+ result = compare_versions(tag_value_str, operand_value_str) == 0
373
+ else
374
+ result = tag_value_str == operand_value_str
375
+ end
376
+ end
377
+
378
+ result
379
+ end
380
+
381
+ # Checks if a string appears to be a version string (contains only digits and dots).
382
+ # @param str [String] The string to check.
383
+ # @return [Boolean] True if the string appears to be a version string.
384
+ def version_string?(str)
385
+ /^(\d+\.)*\d+$/.match?(str)
386
+ end
387
+
388
+ # Compares two version strings using semantic versioning rules.
389
+ # Supports formats like "1.2.3", "1.0", "2.1.4.5", etc.
390
+ # @param version1 [String] First version string.
391
+ # @param version2 [String] Second version string.
392
+ # @return [Integer] -1 if version1 < version2, 0 if equal, 1 if version1 > version2.
393
+ def compare_versions(version1, version2)
394
+ # Split versions by dots and convert to integers
395
+ parts1 = version1.split('.').map { |part| part.match?(/^\d+$/) ? part.to_i : 0 }
396
+ parts2 = version2.split('.').map { |part| part.match?(/^\d+$/) ? part.to_i : 0 }
397
+
398
+ # Find the maximum length to handle different version formats
399
+ max_length = [parts1.length, parts2.length].max
400
+
401
+ (0...max_length).each do |i|
402
+ part1 = i < parts1.length ? parts1[i] : 0
403
+ part2 = i < parts2.length ? parts2[i] : 0
404
+
405
+ if part1 < part2
406
+ return -1
407
+ elsif part1 > part2
408
+ return 1
409
+ end
196
410
  end
411
+
412
+ 0 # Versions are equal
197
413
  end
198
414
  end
@@ -33,4 +33,17 @@ def send_sdk_init_event(settings_fetch_time, sdk_init_time)
33
33
  # Send the constructed payload via POST request
34
34
  NetworkUtil.send_event(properties, payload)
35
35
  end
36
+ end
37
+
38
+ # Sends a usage stats event to VWO.
39
+ # @param usage_stats_account_id - The account id for usage stats.
40
+ def send_sdk_usage_stats_event(usage_stats_account_id)
41
+ # create query parameters
42
+ properties = NetworkUtil.get_events_base_properties(EventEnum::VWO_USAGE_STATS, nil, nil, true, usage_stats_account_id)
43
+
44
+ # create payload
45
+ payload = NetworkUtil.get_sdk_usage_stats_payload_data(EventEnum::VWO_USAGE_STATS, usage_stats_account_id)
46
+
47
+ # send event
48
+ NetworkUtil.send_event(properties, payload)
36
49
  end
@@ -72,7 +72,7 @@ end
72
72
  def add_is_gateway_service_required_flag(settings)
73
73
  pattern = /
74
74
  (?!custom_variable\s*:\s*{\s*"name"\s*:\s*") # Prevent matching inside custom_variable
75
- \b(country|region|city|os|device|device_type|browser_string|ua)\b
75
+ \b(country|region|city|os|device|device_type|browser_string|ua|browser_version|os_version)\b
76
76
  |
77
77
  "custom_variable"\s*:\s*{\s*"name"\s*:\s*"inlist\([^)]*\)"
78
78
  /x
@@ -35,12 +35,10 @@ def create_and_send_impression_for_variation_shown(settings, campaign_id, variat
35
35
 
36
36
  # Construct payload data for tracking the user
37
37
  payload = NetworkUtil.get_track_user_payload_data(
38
- context.get_id,
39
38
  EventEnum::VWO_VARIATION_SHOWN,
40
39
  campaign_id,
41
40
  variation_id,
42
- context.get_user_agent,
43
- context.get_ip_address
41
+ context
44
42
  )
45
43
 
46
44
  # check if batching is enabled
@@ -24,6 +24,7 @@ require_relative '../packages/network_layer/models/response_model'
24
24
  require_relative '../utils/url_util'
25
25
  require_relative '../utils/uuid_util'
26
26
  require_relative '../utils/usage_stats_util'
27
+ require_relative '../models/user/context_model'
27
28
 
28
29
  class NetworkUtil
29
30
  class << self
@@ -80,52 +81,81 @@ class NetworkUtil
80
81
  end
81
82
 
82
83
  # Builds generic properties for different tracking calls
83
- def get_events_base_properties(event_name, visitor_user_agent = '', ip_address = '')
84
- sdk_key = SettingsService.instance.sdk_key || ''
85
- {
86
- en: event_name,
87
- a: SettingsService.instance.account_id,
88
- env: sdk_key,
89
- eTime: get_current_unix_timestamp_in_millis,
90
- random: get_random_number,
91
- p: 'FS',
92
- visitor_ua: visitor_user_agent || '',
93
- visitor_ip: ip_address || '',
94
- url: "#{UrlUtil.get_base_url}#{UrlEnum::EVENTS}"
84
+ def get_events_base_properties(event_name, visitor_user_agent = '', ip_address = '', is_usage_stats_event = false, usage_stat_account_id = '')
85
+ properties = {
86
+ en: event_name,
87
+ a: SettingsService.instance.account_id,
88
+ eTime: get_current_unix_timestamp_in_millis,
89
+ random: get_random_number,
90
+ p: 'FS',
91
+ visitor_ua: visitor_user_agent || '',
92
+ visitor_ip: ip_address || '',
93
+ url: "#{UrlUtil.get_base_url}#{UrlEnum::EVENTS}"
95
94
  }
95
+
96
+ if !is_usage_stats_event
97
+ # set env key for standard sdk events
98
+ properties[:env] = SettingsService.instance.sdk_key
99
+ else
100
+ # set env key for usage stats events
101
+ properties[:a] = usage_stat_account_id
102
+ end
103
+
104
+ properties
96
105
  end
97
106
 
98
107
  # Builds base payload for tracking events
99
- def _get_event_base_payload(user_id, event_name, visitor_user_agent = '', ip_address = '')
100
- uuid = UUIDUtil.get_uuid(user_id.to_s, SettingsService.instance.account_id)
108
+ def _get_event_base_payload(user_id, event_name, visitor_user_agent = '', ip_address = '', is_usage_stats_event = false, usage_stat_account_id = '')
109
+ account_id = SettingsService.instance.account_id
110
+
111
+ if is_usage_stats_event
112
+ account_id = usage_stat_account_id
113
+ end
114
+
115
+ uuid = UUIDUtil.get_uuid(user_id.to_s, account_id.to_s)
101
116
  sdk_key = SettingsService.instance.sdk_key
102
117
 
103
- {
104
- d: {
105
- msgId: "#{uuid}-#{get_current_unix_timestamp_in_millis}",
106
- visId: uuid,
107
- sessionId: get_current_unix_timestamp,
108
- event: {
109
- props: {
110
- vwo_sdkName: Constants::SDK_NAME,
111
- vwo_sdkVersion: Constants::SDK_VERSION,
112
- vwo_envKey: sdk_key
113
- },
114
- name: event_name,
115
- time: get_current_unix_timestamp_in_millis
116
- },
117
- visitor: {
118
- props: {
119
- vwo_fs_environment: sdk_key
120
- }
118
+ payload = {
119
+ d: {
120
+ msgId: "#{uuid}-#{get_current_unix_timestamp_in_millis}",
121
+ visId: uuid,
122
+ sessionId: get_current_unix_timestamp,
123
+ event: {
124
+ props: {
125
+ vwo_sdkName: Constants::SDK_NAME,
126
+ vwo_sdkVersion: Constants::SDK_VERSION,
127
+ },
128
+ name: event_name,
129
+ time: get_current_unix_timestamp_in_millis
130
+ }
121
131
  }
122
132
  }
123
- }
133
+
134
+ if !is_usage_stats_event
135
+ # set env key for standard sdk events
136
+ payload[:d][:event][:props][:vwo_envKey] = sdk_key
137
+
138
+ # set visitor props for standard sdk events
139
+ payload[:d][:visitor] = {
140
+ props: {
141
+ vwo_fs_environment: sdk_key
142
+ }
143
+ }
144
+ end
145
+
146
+ payload
124
147
  end
125
148
 
126
149
  # Builds track-user payload data
127
- def get_track_user_payload_data(user_id, event_name, campaign_id, variation_id, visitor_user_agent = '', ip_address = '')
150
+ def get_track_user_payload_data(event_name, campaign_id, variation_id, context)
151
+ user_id = context.get_id
152
+ visitor_user_agent = context.get_user_agent
153
+ ip_address = context.get_ip_address
154
+ custom_variables = context.get_custom_variables
155
+ post_segmentation_variables = context.get_post_segmentation_variables
156
+
128
157
  properties = _get_event_base_payload(user_id, event_name, visitor_user_agent, ip_address)
158
+
129
159
  properties[:d][:event][:props][:id] = campaign_id
130
160
  properties[:d][:event][:props][:variation] = variation_id
131
161
  properties[:d][:event][:props][:isFirst] = 1
@@ -134,10 +164,20 @@ class NetworkUtil
134
164
  properties[:d][:visitor_ua] = visitor_user_agent if visitor_user_agent && !visitor_user_agent.empty?
135
165
  properties[:d][:visitor_ip] = ip_address if ip_address && !ip_address.empty?
136
166
 
137
- # check if usage stats size is greater than 0
138
- usage_stats = UsageStatsUtil.get_usage_stats
139
- if usage_stats.size > 0
140
- properties[:d][:event][:props][:vwoMeta] = usage_stats
167
+ # Add post-segmentation variables if they exist in custom variables
168
+ if post_segmentation_variables&.any? && custom_variables&.any?
169
+ post_segmentation_variables.each do |key|
170
+ # Try to get value using string key first, then symbol key
171
+ value = custom_variables[key] || custom_variables[key.to_sym]
172
+ if value
173
+ properties[:d][:visitor][:props][key] = value
174
+ end
175
+ end
176
+ end
177
+
178
+ # Add IP address as a standard attribute if available
179
+ if ip_address && !ip_address.empty?
180
+ properties[:d][:visitor][:props][:ip] = ip_address
141
181
  end
142
182
 
143
183
  LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_TRACK_USER", {
@@ -207,6 +247,18 @@ class NetworkUtil
207
247
  properties
208
248
  end
209
249
 
250
+ # Constructs the payload for usage stats called event.
251
+ # @param event_name - The name of the event.
252
+ # @param usage_stats_account_id - The account id for usage stats.
253
+ # @returns The constructed payload with required fields.
254
+ def get_sdk_usage_stats_payload_data(event_name, usage_stats_account_id)
255
+ user_id = SettingsService.instance.account_id.to_s + "_" + SettingsService.instance.sdk_key
256
+ properties = _get_event_base_payload(user_id, event_name, nil, nil, true, usage_stats_account_id)
257
+ properties[:d][:event][:props][:product] = Constants::PRODUCT_NAME
258
+ properties[:d][:event][:props][:vwoMeta] = UsageStatsUtil.get_usage_stats
259
+ properties
260
+ end
261
+
210
262
  # Sends a POST API request with given properties and payload
211
263
  def send_post_api_request(properties, payload)
212
264
  network_instance = NetworkManager.instance
@@ -229,16 +281,10 @@ class NetworkUtil
229
281
  if network_instance.get_client.get_should_use_threading
230
282
  network_instance.get_client.get_thread_pool.post {
231
283
  response = network_instance.post(request)
232
- if response.get_status_code == 200
233
- UsageStatsUtil.clear_usage_stats
234
- end
235
284
  response
236
285
  }
237
286
  else
238
287
  response = network_instance.post(request)
239
- if response.get_status_code == 200
240
- UsageStatsUtil.clear_usage_stats
241
- end
242
288
  response
243
289
  end
244
290
  rescue ResponseModel => err
@@ -259,15 +305,20 @@ class NetworkUtil
259
305
  headers[HeadersEnum::USER_AGENT] = payload[:d][:visitor_ua] if payload[:d][:visitor_ua]
260
306
  headers[HeadersEnum::IP] = payload[:d][:visitor_ip] if payload[:d][:visitor_ip]
261
307
 
308
+ url = Constants::HOST_NAME
309
+ if UrlUtil.get_collection_prefix && !UrlUtil.get_collection_prefix.empty?
310
+ url = "#{url}/#{UrlUtil.get_collection_prefix}"
311
+ end
312
+
262
313
  request = RequestModel.new(
263
- UrlUtil.get_base_url,
314
+ url,
264
315
  HttpMethodEnum::POST,
265
316
  UrlEnum::EVENTS,
266
317
  properties,
267
318
  payload,
268
319
  headers,
269
- SettingsService.instance.protocol,
270
- SettingsService.instance.port
320
+ Constants::HTTPS_PROTOCOL,
321
+ nil
271
322
  )
272
323
 
273
324
  begin
@@ -42,5 +42,12 @@ class UrlUtil
42
42
 
43
43
  base_url
44
44
  end
45
+
46
+ # Retrieves the collection prefix.
47
+ #
48
+ # @return [String] The collection prefix.
49
+ def get_collection_prefix
50
+ @collection_prefix
51
+ end
45
52
  end
46
53
  end
@@ -69,6 +69,8 @@ class UsageStatsUtil
69
69
 
70
70
  data = {}
71
71
 
72
+ data[:a] = SettingsService.instance.account_id
73
+ data[:env] = SettingsService.instance.sdk_key
72
74
  data[:ig] = 1 if integrations
73
75
  data[:eb] = 1 if event_batching
74
76
  data[:gs] = 1 if gateway_service
@@ -84,7 +86,7 @@ class UsageStatsUtil
84
86
  data[:ll] = LogLevelToNumber.to_number(logger[:level]) || -1
85
87
  end
86
88
 
87
- data[:pi] = 1 if poll_interval
89
+ data[:pi] = poll_interval if poll_interval
88
90
 
89
91
  if vwo_meta && vwo_meta.key?(:ea)
90
92
  data[:_ea] = 1
data/lib/vwo.rb CHANGED
@@ -93,6 +93,14 @@ class VWO
93
93
  # send the sdk init info to vwo server
94
94
  send_sdk_init_event(SettingsService.instance.settings_fetch_time, time_taken_for_init.to_s)
95
95
  end
96
+
97
+ # send the usage stats event to vwo server
98
+ # get usage stats account id from settings
99
+ usage_stats_account_id = @@instance.original_settings["usageStatsAccountId"]
100
+ if usage_stats_account_id
101
+ send_sdk_usage_stats_event(usage_stats_account_id)
102
+ end
103
+
96
104
  @@instance
97
105
  rescue StandardError => e
98
106
  puts "[ERROR]: VWO-SDK: Got error while initializing VWO: #{e.message}"
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.4.1
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - VWO
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-08-07 00:00:00.000000000 Z
11
+ date: 2025-09-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools