wingify-fme-ruby-sdk 1.50.0 → 1.60.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: ae10f6a02b6a5ddb698aed1f7ac3eb5461678e016dbd0e7084668bca8bcd0f62
4
- data.tar.gz: ff5cae6fb46441e45f5b50fa1a8ce0149a356b622b41c0da7fedde092c75f87e
3
+ metadata.gz: edbf0906fddff5a86daaf76539bfb4c221f688f236c426a55ead7b225603d3db
4
+ data.tar.gz: a95ba0f7c1ac4dd31471ffd6b0a781a6e278baa9a5c1d7c512f7e00f4dd2f669
5
5
  SHA512:
6
- metadata.gz: 06aaf6ca460223d50c225308579926ce7349142606a7d9ff3fc3a758d3c2d43a79276dcc02e7bf5c16c2915e76368442049f4b1fed011bbe650b2d10ed1d8ba1
7
- data.tar.gz: ea43e8123d418bba625bf55dffed678713bc1513c90895aa60cce177ca77959ce460f919da56cc1bb1ce58d2ee8287c9274ee7f15399ddb974eefdd9d4700677
6
+ metadata.gz: 3f0883eba6a6e467812169f70ec2f3c9d1f49fef233f423462c4d975948c15a54680aeb83b2e6c27c427beb89a36fef7475ce274f54a91c231f75733201a01b6
7
+ data.tar.gz: a9f6718ce695f063215f29176554e3ba3447e3d94f0cf8e2befdfd514e87fd25575b0103eae529a7d51d58aae3b4e9d728a36e9c1eef773adf813b522c744831
@@ -11,6 +11,7 @@
11
11
  "IMPRESSION_FOR_TRACK_USER": "Impression built for vwo_variationShown({brand} standard event for tracking user) event haivng Account ID:{accountId}, User ID:{userId}, and experiment ID:{campaignId}",
12
12
  "IMPRESSION_FOR_TRACK_GOAL": "Impression built for event:{eventName} event having Account ID:{accountId}, and user ID:{userId}",
13
13
  "IMPRESSION_FOR_SYNC_VISITOR_PROP": "Impression built for {eventName}({brand} internal event) event for Account ID:{accountId}, and user ID:{userId}",
14
+ "IMPRESSION_FOR_USAGE_TRACKING": "Impression built for vwo_feTrackUsage({brand} standard event for usage tracking call) event having Account ID:{accountId}, and user ID:{userId}",
14
15
 
15
16
  "CONFIG_BATCH_EVENT_LIMIT_EXCEEDED": "Impression event - {endPoint} failed due to exceeding payload size. Parameter eventsPerRequest in batchEvents config in init API has value:{eventsPerRequest} for account ID:{accountId}. Please read the official documentation for knowing the size limits",
16
17
 
@@ -59,5 +59,9 @@
59
59
 
60
60
  "ERROR_EVALUATING_RULE": "Error occurred while evaluating rule. Error: {err}",
61
61
  "BATCH_FLUSH_FAILED": "Failed to send batch events. Re-enqueuing events for retry.",
62
- "ERROR_PARSING_GATEWAY_URL": "Error occurred while parsing gateway service URL: {err}"
62
+ "ERROR_PARSING_GATEWAY_URL": "Error occurred while parsing gateway service URL: {err}",
63
+
64
+ "INVALID_WEB_TESTING_CAMPAIGNS_DUPLICATE_KEY": "Duplicate campaignId found in webTestingCampaigns",
65
+ "INVALID_WEB_TESTING_CAMPAIGNS_JSON": "webTestingCampaigns provided in context is not a valid JSON string",
66
+ "INVALID_WEB_TESTING_CAMPAIGNS_TYPE": "webTestingCampaigns provided in context is of type {kind}, but must be of type string or object"
63
67
  }
@@ -30,6 +30,7 @@
30
30
  "MEG_WINNER_CAMPAIGN": "MEG: Campaign {campaignKey} is the winner for group {groupId} for user ID:{userId} {algo}",
31
31
  "SETTINGS_UPDATED": "Settings fetched and updated successfully on the current {brand} client instance when API: {apiName} got called having isViaWebhook param as {isViaWebhook}",
32
32
  "NETWORK_CALL_SUCCESS": "Impression for {event} - {endPoint} was successfully received by {brand} having Account ID:{accountId}, UUID: {uuid}",
33
+ "USAGE_TRACKING_DISPATCHED": "FE track usage call dispatched for Account ID:{accountId}, User ID:{userId}, and feature key:{featureKey}",
33
34
 
34
35
  "EVENT_BATCH_DEFAULTS": "{parameter} in SDK configuration is missing or invalid (should be greater than {minLimit}). Using default value: {defaultValue}",
35
36
  "EVENT_QUEUE": "Event with payload:{event} pushed to the {queueType} queue",
@@ -40,6 +40,10 @@ class FlagApi
40
40
  rollout_variation_to_return = nil
41
41
  experiment_variation_to_return = nil
42
42
  should_check_for_experiments_rules = false
43
+ # Flag to check if variation shown event has been fired
44
+ is_variation_shown_fired = false
45
+ # Flag to check if tracking usage is enabled
46
+ is_tracking_usage_enabled = settings.get_is_tracking_usage_enabled
43
47
 
44
48
  passed_rules_information = {} # for storing integration callback
45
49
  evaluated_feature_map = {}
@@ -65,6 +69,8 @@ class FlagApi
65
69
 
66
70
  if variation
67
71
  LoggerService.log(LogLevelEnum::INFO, "STORED_VARIATION_FOUND", {variationKey: variation.get_key, userId: context.get_id, experimentKey: stored_data[:experiment_key], experimentType: "experiment"})
72
+ # Send usage tracking call when user is served from storage
73
+ send_tracking_usage(settings, context, is_tracking_usage_enabled, feature_key)
68
74
  return GetFlagResponse.new(true, variation.get_variables, context.get_uuid, context.get_session_id)
69
75
  end
70
76
  end
@@ -91,6 +97,8 @@ class FlagApi
91
97
 
92
98
  if feature.nil?
93
99
  LoggerService.log(LogLevelEnum::ERROR, "FEATURE_NOT_FOUND", {featureKey: feature_key, an: ApiEnum::GET_FLAG, sId: context.get_session_id, uuid: context.get_uuid})
100
+ # Send usage tracking call when feature is not found
101
+ send_tracking_usage(settings, context, is_tracking_usage_enabled, feature_key)
94
102
  return GetFlagResponse.new(false, [], context.get_uuid, context.get_session_id)
95
103
  end
96
104
 
@@ -106,6 +114,12 @@ class FlagApi
106
114
  rollout_rules.each do |rule|
107
115
  result = evaluate_rule(settings, feature, rule, context, evaluated_feature_map, nil, storage_service, decision)
108
116
  pre_segmentation_result = result[:pre_segmentation_result]
117
+ whitelisted_object = result[:whitelisted_object]
118
+
119
+ if whitelisted_object.is_a?(Hash) && !whitelisted_object.empty?
120
+ is_variation_shown_fired = true
121
+ end
122
+
109
123
  updated_decision = result[:updated_decision]
110
124
  decision.merge!(updated_decision) if updated_decision
111
125
 
@@ -131,6 +145,8 @@ class FlagApi
131
145
  update_integrations_decision_object(passed_rollout_campaign, variation, passed_rules_information, decision)
132
146
 
133
147
  create_and_send_impression_for_variation_shown(settings, passed_rollout_campaign.get_id, variation.get_id, context, feature_key)
148
+ # set is_variation_shown_fired to true since we already sent variation shown call for rollout
149
+ is_variation_shown_fired = true
134
150
  end
135
151
  end
136
152
  elsif rollout_rules.empty?
@@ -147,11 +163,16 @@ class FlagApi
147
163
  result = evaluate_rule(settings, feature, rule, context, evaluated_feature_map, meg_group_winner_campaigns, storage_service, decision)
148
164
  pre_segmentation_result = result[:pre_segmentation_result]
149
165
  whitelisted_object = result[:whitelisted_object]
166
+
167
+ if whitelisted_object.is_a?(Hash) && !whitelisted_object.empty?
168
+ is_variation_shown_fired = true
169
+ end
170
+
150
171
  updated_decision = result[:updated_decision]
151
172
  decision.merge!(updated_decision) if updated_decision
152
173
 
153
174
  if pre_segmentation_result
154
- if whitelisted_object.nil?
175
+ if whitelisted_object.nil? || whitelisted_object.empty?
155
176
  experiment_rules_to_evaluate << rule
156
177
  else
157
178
  is_enabled = true
@@ -176,6 +197,8 @@ class FlagApi
176
197
  update_integrations_decision_object(campaign, variation, passed_rules_information, decision)
177
198
 
178
199
  create_and_send_impression_for_variation_shown(settings, campaign.get_id, variation.get_id, context, feature_key)
200
+ # set is_variation_shown_fired to true since we already sent variation shown call for experiment
201
+ is_variation_shown_fired = true
179
202
  end
180
203
  end
181
204
  end
@@ -216,8 +239,15 @@ class FlagApi
216
239
  context,
217
240
  feature_key
218
241
  )
242
+ # set is_variation_shown_fired to true since we already sent variation shown call for impact analysis
243
+ is_variation_shown_fired = true
244
+ end
245
+
246
+ # Send usage tracking call when no primary variation_shown event was dispatched.
247
+ # If a primary variation_shown event was fired, the server already has the usage tracking signal.
248
+ if !is_variation_shown_fired
249
+ send_tracking_usage(settings, context, is_tracking_usage_enabled, feature_key)
219
250
  end
220
-
221
251
  # Return final evaluated feature flag
222
252
  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
253
  end
@@ -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.50.0'.freeze
20
+ SDK_VERSION = '1.60.0'.freeze
21
21
 
22
22
  # --- Brand-specific constants ---
23
23
 
@@ -19,4 +19,5 @@ module EventEnum
19
19
  DEBUGGER_EVENT = 'vwo_sdkDebug'
20
20
  INIT_CALLED = 'vwo_fmeSdkInit'
21
21
  USAGE_STATS = 'vwo_sdkUsageStats'
22
+ USER_EVALUATED = 'vwo_feTrackUsage'
22
23
  end
@@ -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, :is_web_connectivity_enabled
21
+ :features, :campaigns, :campaign_groups, :groups, :poll_interval, :is_web_connectivity_enabled, :is_tracking_usage_enabled
22
22
 
23
23
  def initialize(settings)
24
24
  @sdk_key = settings["sdkKey"]
@@ -32,6 +32,7 @@ class SettingsModel
32
32
  @campaign_groups = settings["campaignGroups"] || {}
33
33
  @groups = settings["groups"] || {}
34
34
  @is_web_connectivity_enabled = settings.fetch("isWebConnectivityEnabled", true)
35
+ @is_tracking_usage_enabled = settings.fetch("isMAU", false)
35
36
 
36
37
  process_features(settings)
37
38
  process_campaigns(settings)
@@ -98,4 +99,8 @@ class SettingsModel
98
99
  def get_is_web_connectivity_enabled
99
100
  @is_web_connectivity_enabled
100
101
  end
102
+
103
+ def get_is_tracking_usage_enabled
104
+ @is_tracking_usage_enabled
105
+ end
101
106
  end
@@ -17,15 +17,16 @@ require_relative '../../utils/uuid_util'
17
17
  require_relative '../../services/settings_service'
18
18
 
19
19
  class ContextModel
20
- attr_accessor :id, :user_agent, :ip_address, :custom_variables, :variation_targeting_variables, :post_segmentation_variables, :vwo, :session_id, :uuid, :bucketing_seed
20
+ attr_accessor :id, :user_agent, :ip_address, :custom_variables, :variation_targeting_variables, :post_segmentation_variables, :platform_variables, :vwo, :session_id, :uuid, :bucketing_seed
21
21
 
22
- def initialize(id = nil, user_agent = nil, ip_address = nil, custom_variables = {}, variation_targeting_variables = {}, post_segmentation_variables = {}, vwo = nil, session_id = nil, uuid = nil, bucketing_seed = nil)
22
+ def initialize(id = nil, user_agent = nil, ip_address = nil, custom_variables = {}, variation_targeting_variables = {}, post_segmentation_variables = {}, platform_variables = {}, vwo = nil, session_id = nil, uuid = nil, bucketing_seed = nil)
23
23
  @id = id
24
24
  @user_agent = user_agent
25
25
  @ip_address = ip_address
26
26
  @custom_variables = custom_variables || {}
27
27
  @variation_targeting_variables = variation_targeting_variables || {}
28
28
  @post_segmentation_variables = post_segmentation_variables || {}
29
+ @platform_variables = platform_variables || {}
29
30
  @vwo = vwo
30
31
  @session_id = session_id
31
32
  @uuid = uuid
@@ -52,6 +53,7 @@ class ContextModel
52
53
  @custom_variables = context[:customVariables] if context.key?(:customVariables)
53
54
  @variation_targeting_variables = context[:variationTargetingVariables] if context.key?(:variationTargetingVariables)
54
55
  @post_segmentation_variables = context[:postSegmentationVariables] if context.key?(:postSegmentationVariables)
56
+ @platform_variables = context[:platformVariables] if context.key?(:platformVariables)
55
57
  @vwo = ContextVWOModel.new.model_from_dictionary(context[:_vwo]) if context.key?(:_vwo)
56
58
  @bucketing_seed = context[:bucketingSeed].to_s if context.key?(:bucketingSeed)
57
59
 
@@ -120,6 +122,14 @@ class ContextModel
120
122
  @post_segmentation_variables = post_segmentation_variables
121
123
  end
122
124
 
125
+ def get_platform_variables
126
+ @platform_variables
127
+ end
128
+
129
+ def set_platform_variables(platform_variables)
130
+ @platform_variables = platform_variables
131
+ end
132
+
123
133
  def get_vwo
124
134
  @vwo
125
135
  end
@@ -24,6 +24,8 @@ require_relative '../../../services/settings_service'
24
24
  require_relative '../../../utils/data_type_util'
25
25
  require_relative '../../../enums/log_level_enum'
26
26
  require_relative '../../../enums/api_enum'
27
+ require_relative '../enums/segment_operator_value_enum'
28
+
27
29
  class SegmentationManager
28
30
  @@instance = nil # Singleton instance
29
31
 
@@ -72,6 +74,43 @@ class SegmentationManager
72
74
  end
73
75
 
74
76
  def validate_segmentation(dsl, properties)
77
+ if has_campaign_variation_node?(dsl)
78
+ platform_vars = @evaluator.context&.get_platform_variables
79
+ web_testing_campaigns = platform_vars && (platform_vars[:webTestingCampaigns] || platform_vars['webTestingCampaigns'])
80
+ return false unless web_testing_campaigns
81
+ end
82
+
75
83
  @evaluator.is_segmentation_valid(dsl, properties)
76
84
  end
85
+
86
+ private
87
+
88
+ # Checks if the provided DSL contains a campaign variation node.
89
+ # This is used to determine if web testing pre-segmentation should be evaluated.
90
+ #
91
+ # @param dsl [Hash] The DSL (Domain Specific Language) representing the segmentation rules.
92
+ # @return [Boolean] Returns true if a web campaign variation node is found, false otherwise.
93
+ def has_campaign_variation_node?(dsl)
94
+ # Return false immediately if the DSL is not a hash
95
+ return false unless dsl.is_a?(Hash)
96
+
97
+ # Iterate through each operator and operand in the DSL
98
+ dsl.each do |operator, operand|
99
+ # Check if the current operator matches the web campaign variation operator
100
+ return true if operator.to_s == SegmentOperatorValueEnum::WEB_CAMPAIGN_VARIATION
101
+
102
+ # If the operand is an array, recursively check each sub-DSL within it
103
+ if operand.is_a?(Array)
104
+ operand.each do |sub_dsl|
105
+ return true if has_campaign_variation_node?(sub_dsl)
106
+ end
107
+ # If the operand is a hash, recursively check the hash itself
108
+ elsif operand.is_a?(Hash)
109
+ return true if has_campaign_variation_node?(operand)
110
+ end
111
+ end
112
+
113
+ # Return false if no campaign variation node is found in the entire DSL structure
114
+ false
115
+ end
77
116
  end
@@ -30,4 +30,5 @@ module SegmentOperatorValueEnum
30
30
  IP = 'ip_address'
31
31
  BROWSER_VERSION = 'browser_version'
32
32
  OS_VERSION = 'os_version'
33
+ WEB_CAMPAIGN_VARIATION = 'campaignVariation'
33
34
  end
@@ -57,6 +57,8 @@ class SegmentEvaluator
57
57
  SegmentOperandEvaluator.new.evaluate_custom_variable_dsl(sub_dsl, properties)
58
58
  when SegmentOperatorValueEnum::USER
59
59
  SegmentOperandEvaluator.new.evaluate_user_dsl(sub_dsl, properties)
60
+ when SegmentOperatorValueEnum::WEB_CAMPAIGN_VARIATION
61
+ SegmentOperandEvaluator.new.evaluate_web_testing_campaign_variation_dsl(sub_dsl, @context)
60
62
  when SegmentOperatorValueEnum::UA
61
63
  SegmentOperandEvaluator.new.evaluate_user_agent_dsl(sub_dsl, @context)
62
64
  when SegmentOperatorValueEnum::IP
@@ -13,6 +13,7 @@
13
13
  # limitations under the License.
14
14
 
15
15
  require_relative '../utils/segment_util'
16
+ require_relative '../utils/web_testing_segment_util'
16
17
  require_relative '../enums/segment_operand_value_enum'
17
18
  require_relative '../enums/segment_operand_regex_enum'
18
19
  require_relative '../enums/segment_operator_value_enum'
@@ -105,6 +106,22 @@ class SegmentOperandEvaluator
105
106
  extract_result(processed_operand[:operand_type], processed_values[:operand_value], tag_value)
106
107
  end
107
108
 
109
+ # Evaluates Web Testing pre-segmentation against `context.platform_variables[:webTestingCampaigns]`.
110
+ # @param dsl_operand_value [String] The operand value to evaluate
111
+ # @param context [ContextModel] The context to evaluate the operand against
112
+ # @return [Boolean] True if the operand value matches the tag value, false otherwise
113
+ def evaluate_web_testing_campaign_variation_dsl(dsl_operand_value, context)
114
+ operand = dsl_operand_value.to_s.strip
115
+ assigned_variations_by_campaign_id = WebTestingSegmentUtil.parse_web_testing_campaigns_from_context(context)
116
+ result_obj = WebTestingSegmentUtil.evaluate_web_testing_campaign_variation(operand, assigned_variations_by_campaign_id)
117
+
118
+ if result_obj[:invalid_format]
119
+ return false
120
+ end
121
+
122
+ result_obj[:result]
123
+ end
124
+
108
125
  # Evaluates a given string tag value against a DSL operand value.
109
126
  # @param dsl_operand_value [String] The DSL operand string (e.g., "contains(\"value\")").
110
127
  # @param context [ContextModel] The context object containing the value to evaluate.
@@ -0,0 +1,146 @@
1
+ # Copyright 2024-2026 Wingify Software Pvt. Ltd.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+ require_relative '../../../enums/api_enum'
17
+ require_relative '../../../services/logger_service'
18
+ require_relative '../../../enums/log_level_enum'
19
+ require_relative '../../../utils/data_type_util'
20
+
21
+ module WebTestingSegmentUtil
22
+ class << self
23
+ # Normalizes Web Testing campaign map keys and variation values to strings.
24
+ # @param raw_assignments [Hash] The raw assignments map from the context.
25
+ # @return [Hash] The normalized assignments map with campaign_id as key and variation_id as value.
26
+ def normalize_web_testing_campaigns_map(raw_assignments)
27
+ campaign_id_to_variation_id = {}
28
+ raw_assignments.each do |campaign_id, assigned_variation_id|
29
+ unless assigned_variation_id.nil? || campaign_id.to_s.empty?
30
+ campaign_id_to_variation_id[campaign_id.to_s] = assigned_variation_id.to_s
31
+ end
32
+ end
33
+ campaign_id_to_variation_id
34
+ end
35
+
36
+ # Parses `context.platform_variables[:webTestingCampaigns]` (JSON string or plain object).
37
+ # @param context [ContextModel]
38
+ # @return [Hash, nil]
39
+ def parse_web_testing_campaigns_from_context(context)
40
+ platform_variables = context.get_platform_variables || {}
41
+ # support both string and symbol keys
42
+ web_testing_campaigns_input = platform_variables[:webTestingCampaigns] || platform_variables['webTestingCampaigns']
43
+
44
+ if web_testing_campaigns_input.nil?
45
+ return nil
46
+ end
47
+
48
+ if web_testing_campaigns_input.is_a?(Hash)
49
+ return normalize_web_testing_campaigns_map(web_testing_campaigns_input)
50
+ end
51
+
52
+ if web_testing_campaigns_input.is_a?(String)
53
+ trimmed_json = web_testing_campaigns_input.strip
54
+ if trimmed_json.empty?
55
+ return nil
56
+ end
57
+
58
+ begin
59
+ # Check for duplicate keys using regex
60
+ all_campaign_id_tokens = trimmed_json.scan(/"([^"\\]*)"\s*:/).flatten
61
+ if all_campaign_id_tokens.any?
62
+ campaign_ids = all_campaign_id_tokens
63
+ has_duplicate_campaign_id = campaign_ids.length != campaign_ids.uniq.length
64
+ if has_duplicate_campaign_id
65
+ LoggerService.log(
66
+ LogLevelEnum::ERROR,
67
+ 'INVALID_WEB_TESTING_CAMPAIGNS_DUPLICATE_KEY',
68
+ { an: ApiEnum::GET_FLAG, uuid: context.get_uuid, sId: context.get_session_id }
69
+ )
70
+ end
71
+ end
72
+
73
+ parsed_assignments = JSON.parse(trimmed_json)
74
+ if parsed_assignments.is_a?(Hash)
75
+ return normalize_web_testing_campaigns_map(parsed_assignments)
76
+ end
77
+
78
+ LoggerService.log(
79
+ LogLevelEnum::ERROR,
80
+ 'INVALID_WEB_TESTING_CAMPAIGNS_JSON',
81
+ { an: ApiEnum::GET_FLAG, uuid: context.get_uuid, sId: context.get_session_id }
82
+ )
83
+ rescue JSON::ParserError
84
+ LoggerService.log(
85
+ LogLevelEnum::ERROR,
86
+ 'INVALID_WEB_TESTING_CAMPAIGNS_JSON',
87
+ { an: ApiEnum::GET_FLAG, uuid: context.get_uuid, sId: context.get_session_id }
88
+ )
89
+ end
90
+ return nil
91
+ end
92
+
93
+ # For array, numeric, boolean etc.
94
+ kind = web_testing_campaigns_input.is_a?(Array) ? 'array' : DataTypeUtil.get_type(web_testing_campaigns_input).downcase
95
+ LoggerService.log(
96
+ LogLevelEnum::ERROR,
97
+ 'INVALID_WEB_TESTING_CAMPAIGNS_TYPE',
98
+ { kind: kind, an: ApiEnum::GET_FLAG, uuid: context.get_uuid, sId: context.get_session_id }
99
+ )
100
+ nil
101
+ end
102
+
103
+ # Evaluates campaignVariation operand encoding:
104
+ # - "!C" — user is not in campaign C (no entry in map)
105
+ # - "C_!V" — user is in campaign C and assigned variation is not V
106
+ # - "C_V" — user is in campaign C with variation V
107
+ # - "C" (digits only) — user is in campaign C (any variation)
108
+ def evaluate_web_testing_campaign_variation(campaign_variation_operand, assigned_variations_by_campaign_id)
109
+ assignments = assigned_variations_by_campaign_id || {}
110
+
111
+ # match type !C
112
+ if match = /^!(\d+)$/.match(campaign_variation_operand)
113
+ campaign_id = match[1]
114
+ return { result: !assignments.key?(campaign_id), invalid_format: false }
115
+ end
116
+
117
+ # match type C_!V
118
+ if match = /^(\d+)_!(\d+)$/.match(campaign_variation_operand)
119
+ campaign_id = match[1]
120
+ variation_id = match[2]
121
+ if !assignments.key?(campaign_id)
122
+ return { result: false, invalid_format: false }
123
+ end
124
+ return { result: assignments[campaign_id] != variation_id, invalid_format: false }
125
+ end
126
+
127
+ # match type C_V
128
+ if match = /^(\d+)_(\d+)$/.match(campaign_variation_operand)
129
+ campaign_id = match[1]
130
+ variation_id = match[2]
131
+ if !assignments.key?(campaign_id)
132
+ return { result: false, invalid_format: false }
133
+ end
134
+ return { result: assignments[campaign_id] == variation_id, invalid_format: false }
135
+ end
136
+
137
+ # match type C
138
+ if match = /^(\d+)$/.match(campaign_variation_operand)
139
+ campaign_id = match[1]
140
+ return { result: assignments.key?(campaign_id), invalid_format: false }
141
+ end
142
+
143
+ { result: false, invalid_format: true }
144
+ end
145
+ end
146
+ end
@@ -64,3 +64,38 @@ def create_and_send_impression_for_variation_shown(settings, campaign_id, variat
64
64
  NetworkUtil.send_post_api_request(properties, payload, { campaign_type: campaign_type, feature_key: feature_key, campaign_key: campaign_key, variation_name: variation_name })
65
65
  end
66
66
  end
67
+
68
+ # Sends the usage tracking payload to the server.
69
+ # Called whenever a user is evaluated via get_flag() but no variation_shown event is dispatched.
70
+ #
71
+ # @param settings [SettingsModel] The settings model containing configuration.
72
+ # @param context [ContextModel] The user context model.
73
+ # @param is_tracking_usage_enabled [Boolean] Whether usage tracking is active.
74
+ # @param feature_key [String] The key of the evaluated feature.
75
+ def send_tracking_usage(settings, context, is_tracking_usage_enabled, feature_key)
76
+ return unless is_tracking_usage_enabled
77
+
78
+ # Construct payload data for tracking usage
79
+ payload = NetworkUtil.get_tracking_usage_payload_data(context)
80
+
81
+ # Get base properties for the event
82
+ properties = NetworkUtil.get_events_base_properties(
83
+ EventEnum::USER_EVALUATED,
84
+ URI.encode_www_form_component(context.get_user_agent), # Encode user agent for URL safety
85
+ context.get_ip_address
86
+ )
87
+ LoggerService.log(LogLevelEnum::INFO, "USAGE_TRACKING_DISPATCHED", {
88
+ accountId: settings.account_id,
89
+ userId: context.get_id,
90
+ featureKey: feature_key
91
+ })
92
+
93
+ # check if batching is enabled
94
+ if BatchEventsQueue.instance
95
+ # add the payload to the batch events queue
96
+ BatchEventsQueue.instance.enqueue(payload)
97
+ else
98
+ # Send the constructed payload via POST request
99
+ NetworkUtil.send_post_api_request(properties, payload)
100
+ end
101
+ end
@@ -293,6 +293,33 @@ class NetworkUtil
293
293
  properties
294
294
  end
295
295
 
296
+ # Constructs the payload data for usage tracking call.
297
+ # Sent to server when a user is evaluated via get_flag() but no variation_shown
298
+ # event is dispatched by the SDK.
299
+ # @param context [ContextModel] The context model instance.
300
+ # @returns [Hash] The constructed payload.
301
+ def get_tracking_usage_payload_data(context)
302
+ properties = _get_event_base_payload(context.get_id, EventEnum::USER_EVALUATED, context.get_user_agent, context.get_ip_address)
303
+
304
+ # use sessionId from context if present
305
+ if context.get_session_id
306
+ properties[:d][:sessionId] = context.get_session_id
307
+ end
308
+
309
+ # use uuid from context if present
310
+ if context.get_uuid && !context.get_uuid.empty?
311
+ properties[:d][:visId] = context.get_uuid
312
+ properties[:d][:msgId] = "#{context.get_uuid}-#{get_current_unix_timestamp_in_millis}"
313
+ end
314
+
315
+ LoggerService.log(LogLevelEnum::DEBUG, "IMPRESSION_FOR_USAGE_TRACKING", {
316
+ accountId: SettingsService.instance.account_id,
317
+ userId: context.get_id
318
+ })
319
+
320
+ properties
321
+ end
322
+
296
323
  # Sends a POST API request with given properties and payload
297
324
  def send_post_api_request(properties, payload, campaign_info = {})
298
325
  network_instance = NetworkManager.instance
data/lib/wingify.rb CHANGED
@@ -43,6 +43,11 @@ module Wingify
43
43
  .init_batch
44
44
 
45
45
  if options[:settings]
46
+ if SettingsSchema.new.is_settings_valid(options[:settings])
47
+ SettingsService.instance.is_settings_valid = true
48
+ else
49
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_SETTINGS_SCHEMA", { accountId: options[:account_id], sdkKey: options[:sdk_key], settings: options[:settings], an: ApiEnum::INIT}, false)
50
+ end
46
51
  return @@wingify_builder.build(options[:settings])
47
52
  end
48
53
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: wingify-fme-ruby-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.50.0
4
+ version: 1.60.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Wingify
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-06-03 00:00:00.000000000 Z
11
+ date: 2026-06-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: uuidtools
@@ -269,6 +269,7 @@ files:
269
269
  - lib/wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.rb
270
270
  - lib/wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb
271
271
  - lib/wingify/packages/segmentation_evaluator/utils/segment_util.rb
272
+ - lib/wingify/packages/segmentation_evaluator/utils/web_testing_segment_util.rb
272
273
  - lib/wingify/packages/storage/connector.rb
273
274
  - lib/wingify/packages/storage/storage.rb
274
275
  - lib/wingify/services/batch_event_queue.rb