wingify-fme-ruby-sdk 1.50.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.
Files changed (88) hide show
  1. checksums.yaml +7 -0
  2. data/lib/resources/debug_messages.json +21 -0
  3. data/lib/resources/error_messages.json +63 -0
  4. data/lib/resources/info_messages.json +43 -0
  5. data/lib/resources/warn_messages.json +1 -0
  6. data/lib/vwo.rb +40 -0
  7. data/lib/wingify/api/get_flag.rb +244 -0
  8. data/lib/wingify/api/set_attribute.rb +57 -0
  9. data/lib/wingify/api/track_event.rb +80 -0
  10. data/lib/wingify/constants/constants.rb +106 -0
  11. data/lib/wingify/decorators/storage_decorator.rb +82 -0
  12. data/lib/wingify/enums/api_enum.rb +23 -0
  13. data/lib/wingify/enums/campaign_type_enum.rb +19 -0
  14. data/lib/wingify/enums/debug_category_enum.rb +20 -0
  15. data/lib/wingify/enums/decision_types_enum.rb +18 -0
  16. data/lib/wingify/enums/event_enum.rb +22 -0
  17. data/lib/wingify/enums/headers_enum.rb +20 -0
  18. data/lib/wingify/enums/hooks_enum.rb +17 -0
  19. data/lib/wingify/enums/http_method_enum.rb +18 -0
  20. data/lib/wingify/enums/log_level_enum.rb +21 -0
  21. data/lib/wingify/enums/log_level_to_number.rb +27 -0
  22. data/lib/wingify/enums/status_enum.rb +19 -0
  23. data/lib/wingify/enums/storage_enum.rb +22 -0
  24. data/lib/wingify/enums/url_enum.rb +22 -0
  25. data/lib/wingify/models/campaign/campaign_model.rb +192 -0
  26. data/lib/wingify/models/campaign/feature_model.rb +111 -0
  27. data/lib/wingify/models/campaign/impact_campaign_model.rb +38 -0
  28. data/lib/wingify/models/campaign/metric_model.rb +44 -0
  29. data/lib/wingify/models/campaign/rule_model.rb +56 -0
  30. data/lib/wingify/models/campaign/variable_model.rb +51 -0
  31. data/lib/wingify/models/campaign/variation_model.rb +137 -0
  32. data/lib/wingify/models/gateway_service_model.rb +39 -0
  33. data/lib/wingify/models/schemas/settings_schema_validation.rb +104 -0
  34. data/lib/wingify/models/settings/settings_model.rb +101 -0
  35. data/lib/wingify/models/storage/storage_data_model.rb +44 -0
  36. data/lib/wingify/models/user/context_model.rb +154 -0
  37. data/lib/wingify/models/user/context_vwo_model.rb +38 -0
  38. data/lib/wingify/models/user/get_flag_response.rb +50 -0
  39. data/lib/wingify/models/vwo_options_model.rb +129 -0
  40. data/lib/wingify/packages/decision_maker/decision_maker.rb +60 -0
  41. data/lib/wingify/packages/logger/core/log_manager.rb +92 -0
  42. data/lib/wingify/packages/logger/core/transport_manager.rb +76 -0
  43. data/lib/wingify/packages/logger/log_message_builder.rb +70 -0
  44. data/lib/wingify/packages/logger/logger.rb +38 -0
  45. data/lib/wingify/packages/logger/transports/console_transport.rb +49 -0
  46. data/lib/wingify/packages/network_layer/client/network_client.rb +276 -0
  47. data/lib/wingify/packages/network_layer/handlers/request_handler.rb +37 -0
  48. data/lib/wingify/packages/network_layer/manager/network_manager.rb +145 -0
  49. data/lib/wingify/packages/network_layer/models/global_request_model.rb +105 -0
  50. data/lib/wingify/packages/network_layer/models/request_model.rb +179 -0
  51. data/lib/wingify/packages/network_layer/models/response_model.rb +62 -0
  52. data/lib/wingify/packages/segmentation_evaluator/core/segmentation_manager.rb +77 -0
  53. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_regex_enum.rb +29 -0
  54. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operand_value_enum.rb +26 -0
  55. data/lib/wingify/packages/segmentation_evaluator/enums/segment_operator_value_enum.rb +33 -0
  56. data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_evaluator.rb +218 -0
  57. data/lib/wingify/packages/segmentation_evaluator/evaluators/segment_operand_evaluator.rb +415 -0
  58. data/lib/wingify/packages/segmentation_evaluator/utils/segment_util.rb +44 -0
  59. data/lib/wingify/packages/storage/connector.rb +26 -0
  60. data/lib/wingify/packages/storage/storage.rb +47 -0
  61. data/lib/wingify/services/batch_event_queue.rb +179 -0
  62. data/lib/wingify/services/campaign_decision_service.rb +161 -0
  63. data/lib/wingify/services/hooks_service.rb +51 -0
  64. data/lib/wingify/services/logger_service.rb +114 -0
  65. data/lib/wingify/services/settings_service.rb +178 -0
  66. data/lib/wingify/services/storage_service.rb +66 -0
  67. data/lib/wingify/utils/batch_event_dispatcher_util.rb +178 -0
  68. data/lib/wingify/utils/brand_context.rb +33 -0
  69. data/lib/wingify/utils/brand_util.rb +53 -0
  70. data/lib/wingify/utils/campaign_util.rb +284 -0
  71. data/lib/wingify/utils/data_type_util.rb +105 -0
  72. data/lib/wingify/utils/debugger_service_util.rb +40 -0
  73. data/lib/wingify/utils/decision_util.rb +259 -0
  74. data/lib/wingify/utils/event_util.rb +55 -0
  75. data/lib/wingify/utils/function_util.rb +141 -0
  76. data/lib/wingify/utils/gateway_service_util.rb +101 -0
  77. data/lib/wingify/utils/impression_util.rb +66 -0
  78. data/lib/wingify/utils/log_message_util.rb +42 -0
  79. data/lib/wingify/utils/meg_util.rb +357 -0
  80. data/lib/wingify/utils/network_util.rb +503 -0
  81. data/lib/wingify/utils/rule_evaluation_util.rb +57 -0
  82. data/lib/wingify/utils/settings_util.rb +38 -0
  83. data/lib/wingify/utils/usage_stats_util.rb +119 -0
  84. data/lib/wingify/utils/uuid_util.rb +96 -0
  85. data/lib/wingify/wingify_builder.rb +261 -0
  86. data/lib/wingify/wingify_client.rb +227 -0
  87. data/lib/wingify.rb +117 -0
  88. metadata +327 -0
@@ -0,0 +1,119 @@
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
+
16
+ require_relative '../constants/constants'
17
+ require_relative '../enums/log_level_to_number'
18
+
19
+ # Manages usage statistics for the SDK.
20
+ # Tracks various features and configurations being used by the client.
21
+ # Implements Singleton pattern to ensure a single instance.
22
+ class UsageStatsUtil
23
+ @instance = nil
24
+ @usage_stats_data = {}
25
+
26
+ class << self
27
+ # Provides access to the singleton instance of UsageStatsUtil.
28
+ #
29
+ # @return [UsageStatsUtil] The single instance of UsageStatsUtil
30
+ def instance
31
+ @instance ||= new
32
+ end
33
+
34
+ # Sets usage statistics based on provided options.
35
+ # Maps various SDK features and configurations to boolean flags.
36
+ #
37
+ # @param options [Hash] Configuration options for the SDK
38
+ def set_usage_stats(options)
39
+ instance.set_usage_stats(options)
40
+ end
41
+
42
+ # Retrieves the current usage statistics.
43
+ #
44
+ # @return [Hash] Record containing boolean flags for various SDK features in use
45
+ def get_usage_stats
46
+ instance.get_usage_stats
47
+ end
48
+
49
+ def clear_usage_stats
50
+ instance.clear_usage_stats
51
+ end
52
+ end
53
+
54
+ private_class_method :new
55
+
56
+ def initialize
57
+ @usage_stats_data = {}
58
+ end
59
+
60
+ def set_usage_stats(options)
61
+ storage = options[:storage]
62
+ logger = options[:logger]
63
+ event_batching = options[:batch_event_data]
64
+ integrations = options[:integrations]
65
+ poll_interval = options[:poll_interval]
66
+ vwo_meta = options[:_vwo_meta]
67
+ gateway_service = options[:gateway_service]
68
+ threading = options[:threading]
69
+
70
+ data = {}
71
+
72
+ data[:a] = SettingsService.instance.account_id
73
+ data[:env] = SettingsService.instance.sdk_key
74
+ data[:ig] = 1 if integrations
75
+ data[:eb] = 1 if event_batching
76
+ data[:gs] = 1 if gateway_service
77
+
78
+ # if logger has transport or transports, then it is custom logger
79
+ if logger && (logger.key?(:transport) || logger.key?(:transports))
80
+ data[:cl] = 1
81
+ end
82
+
83
+ data[:ss] = 1 if storage
84
+
85
+ if logger && logger.key?(:level)
86
+ data[:ll] = LogLevelToNumber.to_number(logger[:level]) || -1
87
+ end
88
+
89
+ data[:pi] = poll_interval if poll_interval
90
+
91
+ if vwo_meta && vwo_meta.key?(:ea)
92
+ data[:_ea] = 1
93
+ end
94
+
95
+ # check if threading is not passed or is if passed then enabled should be true
96
+ if !threading || (threading && threading[:enabled] == true)
97
+ data[:th] = 1
98
+ # check if max_pool_size is passed
99
+ if threading && threading[:max_pool_size]
100
+ data[:th_mps] = threading[:max_pool_size]
101
+ end
102
+ end
103
+
104
+ if defined?(RUBY_VERSION)
105
+ data[:lv] = RUBY_VERSION
106
+ end
107
+
108
+ @usage_stats_data = data
109
+ end
110
+
111
+ def get_usage_stats
112
+ @usage_stats_data
113
+ end
114
+
115
+ def clear_usage_stats
116
+ @usage_stats_data = {}
117
+ end
118
+ end
119
+
@@ -0,0 +1,96 @@
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 'uuidtools'
16
+ require 'securerandom'
17
+ require_relative '../constants/constants'
18
+
19
+ class UUIDUtil
20
+ # Generates a random UUID based on an API key.
21
+ #
22
+ # @param sdk_key [String] The API key used to generate a namespace for the UUID.
23
+ # @return [String] A random UUID string.
24
+ def self.get_random_uuid(sdk_key)
25
+ namespace = UUIDTools::UUID.sha1_create(UUIDTools::UUID_DNS_NAMESPACE, sdk_key)
26
+ random_uuid = UUIDTools::UUID.sha1_create(namespace, SecureRandom.uuid)
27
+ random_uuid.to_s
28
+ end
29
+
30
+ # Generates a UUID for a user based on their user_id and account_id.
31
+ #
32
+ # @param user_id [String] The user's ID.
33
+ # @param account_id [String] The account ID associated with the user.
34
+ # @return [String] A UUID string formatted without dashes and in uppercase.
35
+ def self.get_uuid(user_id, account_id)
36
+ vwo_namespace = UUIDTools::UUID.sha1_create(UUIDTools::UUID_URL_NAMESPACE, Constants::SEED_URL)
37
+ user_id_namespace = generate_uuid(account_id, vwo_namespace)
38
+ uuid_for_user_id_account_id = generate_uuid(user_id, user_id_namespace)
39
+
40
+ uuid_for_user_id_account_id.to_s.delete('-').upcase
41
+ end
42
+
43
+ # Helper function to generate a UUID v5 based on a name and a namespace.
44
+ #
45
+ # @param name [String] The name from which to generate the UUID.
46
+ # @param namespace [UUIDTools::UUID] The namespace used to generate the UUID.
47
+ # @return [UUIDTools::UUID] A UUID string or nil if inputs are invalid.
48
+ def self.generate_uuid(name, namespace)
49
+ return nil if name.nil? || namespace.nil?
50
+ # Convert name to string to handle integer inputs
51
+ name_str = name.to_s
52
+ UUIDTools::UUID.sha1_create(namespace, name_str)
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
96
+ end
@@ -0,0 +1,261 @@
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 'uri'
17
+ require_relative './enums/log_level_enum'
18
+ require_relative 'wingify_client'
19
+ require_relative './services/settings_service'
20
+ require_relative './packages/storage/storage'
21
+ require_relative './packages/network_layer/manager/network_manager'
22
+ require_relative './packages/segmentation_evaluator/core/segmentation_manager'
23
+ require_relative './services/logger_service'
24
+ require_relative './services/batch_event_queue'
25
+ require_relative './utils/function_util'
26
+ require_relative './constants/constants'
27
+ require_relative './utils/usage_stats_util'
28
+ require_relative './enums/api_enum'
29
+ require_relative './utils/batch_event_dispatcher_util'
30
+
31
+ class WingifyBuilder
32
+ attr_reader :settings, :storage, :log_manager, :is_settings_fetch_in_progress, :wingify_instance, :is_valid_poll_interval_passed_from_init
33
+
34
+ # Initialize the VWOBuilder with the given options
35
+ # @param options [Hash] The options for the VWOBuilder
36
+ def initialize(options)
37
+ @options = options
38
+ @settings = nil
39
+ @storage = nil
40
+ @log_manager = nil
41
+ @is_settings_fetch_in_progress = false
42
+ @is_valid_poll_interval_passed_from_init = false
43
+ @wingify_instance = nil
44
+ end
45
+
46
+ # Initializes the batch event processing system
47
+ # Validates batch event settings and configures the BatchEventsQueue
48
+ # Sets up event dispatcher and flushes any existing events
49
+ # @raise [StandardError] If batch event configuration is invalid
50
+ def init_batch
51
+ # if gateway service is configured, then do not initialize batch event queue
52
+ if SettingsService.instance.is_gateway_service_provided
53
+ LoggerService.log(LogLevelEnum::INFO, "GATEWAY_AND_BATCH_EVENTS_CONFIG_MISMATCH")
54
+ return self
55
+ end
56
+ begin
57
+ if @options.key?(:batch_event_data)
58
+ if @options[:batch_event_data].is_a?(Hash)
59
+ # Validate batch event parameters
60
+ events_per_request = @options[:batch_event_data][:events_per_request]
61
+ request_time_interval = @options[:batch_event_data][:request_time_interval]
62
+
63
+ if (!events_per_request.is_a?(Numeric) || events_per_request <= 0) &&
64
+ (!request_time_interval.is_a?(Numeric) || request_time_interval <= 0)
65
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_BATCH_EVENTS_CONFIG", { an: ApiEnum::INIT})
66
+ end
67
+
68
+ BatchEventsQueue.configure(
69
+ @options[:batch_event_data].merge(
70
+ {
71
+ account_id: @options[:account_id],
72
+ dispatcher: method(:dispatcher)
73
+ }
74
+ )
75
+ )
76
+ @batch_event_data = @options[:batch_event_data]
77
+ else
78
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_BATCH_EVENTS_CONFIG", { an: ApiEnum::INIT})
79
+ end
80
+ end
81
+ rescue StandardError => e
82
+ LoggerService.log(LogLevelEnum::ERROR, "FAILED_TO_INITIALIZE_SERVICE", { service: 'Batch Event Queue', err: e.message, an: ApiEnum::INIT})
83
+ end
84
+ end
85
+
86
+ # Set the network manager
87
+ # @return [VWOBuilder] The VWOBuilder instance
88
+ def set_network_manager
89
+ begin
90
+ network_instance = NetworkManager.instance(@options[:threading] || {})
91
+ retry_config = @options[:retry_config]
92
+ client = (@options[:network] && @options[:network][:client]) ? @options[:network][:client] : nil
93
+ network_instance.attach_client(client, retry_config)
94
+ LoggerService.log(LogLevelEnum::DEBUG, "SERVICE_INITIALIZED", {service: "Network Layer"})
95
+ network_instance.get_config.set_development_mode(@options[:is_development_mode]) if @options[:is_development_mode]
96
+ self
97
+ rescue StandardError => e
98
+ LoggerService.log(LogLevelEnum::ERROR, "FAILED_TO_INITIALIZE_SERVICE", { service: 'Network Manager', err: e.message, an: ApiEnum::INIT})
99
+ self
100
+ end
101
+ end
102
+
103
+ # Set the segmentation manager
104
+ # @return [VWOBuilder] The VWOBuilder instance
105
+ def set_segmentation
106
+ SegmentationManager.instance.attach_evaluator(@options[:segmentation]) if @options[:segmentation]
107
+ LoggerService.log(LogLevelEnum::DEBUG, "SERVICE_INITIALIZED", {service: "Segmentation Evaluator"})
108
+ self
109
+ end
110
+
111
+ # Fetch the settings from the server
112
+ # @param force [Boolean] Whether to force the fetch of settings
113
+ # @return [Hash] The settings
114
+ def fetch_settings(force = false)
115
+ return @settings if !force && @settings
116
+
117
+ @is_settings_fetch_in_progress = true
118
+ settings = SettingsService.new(@options).get_settings()
119
+ @is_settings_fetch_in_progress = false
120
+ @settings = settings unless force
121
+ settings
122
+ end
123
+
124
+ # Get the settings from the server
125
+ # @param force [Boolean] Whether to force the fetch of settings
126
+ # @return [Hash] The settings
127
+ def get_settings(force = false)
128
+ return @settings if !force && @settings
129
+
130
+ begin
131
+ fetch_settings(force)
132
+ rescue StandardError => e
133
+ LoggerService.log(LogLevelEnum::ERROR, "ERROR_FETCHING_SETTINGS", { err: e.message, an: ApiEnum::INIT})
134
+ {}
135
+ end
136
+ end
137
+
138
+ # Set the storage
139
+ # @return [VWOBuilder] The VWOBuilder instance
140
+ def set_storage
141
+ if @options[:storage]
142
+ @storage = Storage.instance.attach_connector(@options[:storage])
143
+ Storage.instance.is_storage_enabled = true
144
+ else
145
+ @storage = nil
146
+ end
147
+ LoggerService.log(LogLevelEnum::DEBUG, "SERVICE_INITIALIZED", {service: "Storage"})
148
+ self
149
+ end
150
+
151
+ # Set the settings service
152
+ # @return [VWOBuilder] The VWOBuilder instance
153
+ def set_settings_service
154
+ @settings_service = SettingsService.new(@options)
155
+ self
156
+ end
157
+
158
+ # Set the logger
159
+ # @return [VWOBuilder] The VWOBuilder instance
160
+ def set_logger
161
+ begin
162
+ @log_manager = LoggerService.new(@options[:logger] || {})
163
+ LoggerService.log(LogLevelEnum::DEBUG, "SERVICE_INITIALIZED", {service: "Logger"})
164
+ rescue => e
165
+ puts "Got error while setting logger: #{e.message}"
166
+ end
167
+ self
168
+ end
169
+
170
+ # Initialize the polling
171
+ # @return [VWOBuilder] The VWOBuilder instance
172
+ def init_polling
173
+ poll_interval = @options[:poll_interval]
174
+
175
+ if poll_interval && poll_interval.is_a?(Numeric) && poll_interval >= 1000
176
+ # this is to check if the poll_interval passed in options is valid
177
+ @is_valid_poll_interval_passed_from_init = true
178
+ check_and_poll
179
+ elsif poll_interval
180
+ # only log error if poll_interval is present in options
181
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_POLLING_CONFIGURATION", {
182
+ key: 'poll_interval',
183
+ correctType: 'number >= 1000',
184
+ an: ApiEnum::INIT
185
+ })
186
+ end
187
+ self
188
+ end
189
+
190
+ # Build the WingifyClient instance
191
+ # @param settings [Hash] The settings for the client instance
192
+ # @return [WingifyClient] The client instance
193
+ def build(settings)
194
+ @wingify_instance = WingifyClient.new(settings, @options)
195
+ # if poll_interval is not present in options, set it to the pollInterval from settings
196
+ update_poll_interval_and_check_and_poll(settings)
197
+ @wingify_instance
198
+ end
199
+
200
+ def update_poll_interval_and_check_and_poll(settings, should_check_and_poll = true)
201
+ # only update the poll_interval if it poll_interval is not valid or not present in options
202
+ if !@is_valid_poll_interval_passed_from_init
203
+ @options[:poll_interval] = settings["pollInterval"] || Constants::POLLING_INTERVAL
204
+ LoggerService.log(LogLevelEnum::DEBUG, "USING_POLL_INTERVAL_FROM_SETTINGS", {
205
+ source: settings["pollInterval"] ? 'settings' : 'default',
206
+ pollInterval: @options[:poll_interval]
207
+ })
208
+ end
209
+ # should_check_and_poll will be true only when we are updating the poll_interval first time from self.build method
210
+ # if we are updating the poll_interval already running polling, we don't need to check and poll again
211
+ if should_check_and_poll && !@is_valid_poll_interval_passed_from_init
212
+ check_and_poll
213
+ end
214
+ end
215
+
216
+ # This method is used to check and poll the settings from the server
217
+ # @return [VWOBuilder] The VWOBuilder instance
218
+ def check_and_poll
219
+ @thread_pool = NetworkManager.instance.get_client.get_thread_pool
220
+ @thread_pool.post do
221
+ loop do
222
+ sleep(@options[:poll_interval]/ 1000.0)
223
+ begin
224
+ latest_settings = fetch_settings(true)
225
+ if latest_settings && latest_settings.to_json != @settings.to_json
226
+ @settings = latest_settings
227
+ LoggerService.log(LogLevelEnum::INFO, "POLLING_SET_SETTINGS")
228
+ @wingify_instance.update_settings(latest_settings.clone, false) if @wingify_instance
229
+ update_poll_interval_and_check_and_poll(latest_settings, false)
230
+ elsif latest_settings
231
+ LoggerService.log(LogLevelEnum::INFO, "POLLING_NO_CHANGE_IN_SETTINGS")
232
+ end
233
+ rescue StandardError => e
234
+ LoggerService.log(LogLevelEnum::ERROR, "ERROR_UPDATING_SETTINGS", { err: e.message, an: ApiEnum::INIT})
235
+ end
236
+ end
237
+ end
238
+ end
239
+
240
+ def dispatcher(events, callback)
241
+ BatchEventDispatcherUtil.dispatch(
242
+ {
243
+ ev: events
244
+ },
245
+ callback,
246
+ {
247
+ a: @options[:account_id],
248
+ env: @options[:sdk_key]
249
+ }
250
+ )
251
+ end
252
+
253
+ # Initialize the usage stats
254
+ # @return [VWOBuilder] The VWOBuilder instance
255
+ def init_usage_stats
256
+ return self if @options[:is_usage_stats_disabled]
257
+
258
+ UsageStatsUtil.set_usage_stats(@options)
259
+ self
260
+ end
261
+ end
@@ -0,0 +1,227 @@
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_relative 'services/settings_service'
16
+ require_relative 'models/user/context_model'
17
+ require_relative 'api/get_flag'
18
+ require_relative 'api/set_attribute'
19
+ require_relative 'api/track_event'
20
+ require_relative 'utils/settings_util'
21
+ require_relative 'services/logger_service'
22
+ require_relative 'enums/log_level_enum'
23
+ require_relative 'utils/network_util'
24
+ require_relative 'models/schemas/settings_schema_validation'
25
+ require_relative 'services/batch_event_queue'
26
+ require_relative 'enums/api_enum'
27
+ require_relative 'utils/uuid_util'
28
+
29
+ class WingifyClient
30
+ attr_accessor :settings, :original_settings
31
+ attr_reader :options
32
+
33
+ def initialize(settings, options)
34
+ @options = options
35
+ @settings = settings
36
+ @original_settings = settings.dup
37
+
38
+ begin
39
+ set_settings_and_add_campaigns_to_rules(settings, self)
40
+ rescue StandardError => e
41
+ LoggerService.log(LogLevelEnum::ERROR, "ERROR_ADDING_CAMPAIGNS_TO_RULES", { err: e.message, an: ApiEnum::INIT})
42
+ end
43
+ LoggerService.log(LogLevelEnum::INFO, "CLIENT_INITIALIZED")
44
+ self
45
+ end
46
+
47
+ # Get the flag for a given feature key and context
48
+ # @param feature_key [String] The key of the feature to get the flag for
49
+ # @param context [Hash] The context of the user
50
+ # @return [GetFlagResponse] The flag for the given feature key and context
51
+ def get_flag(feature_key, context)
52
+ api_name = 'get_flag'
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
62
+
63
+ begin
64
+ hooks_service = HooksService.new(@options)
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
77
+
78
+ # Validate bucketingSeed: must be a non-empty, non-whitespace-only string
79
+ if context_copy.key?(:bucketingSeed)
80
+ seed = context_copy[:bucketingSeed]
81
+ if seed.nil? || seed.is_a?(Numeric) || seed.is_a?(Hash) || seed.is_a?(Array) || (seed.is_a?(String) && seed.strip.empty?)
82
+ LoggerService.log(LogLevelEnum::ERROR, "INVALID_BUCKETING_SEED", { apiName: api_name, type: seed.class.name })
83
+ context_copy.delete(:bucketingSeed)
84
+ end
85
+ end
86
+
87
+ unless feature_key.is_a?(String) && !feature_key.empty?
88
+ raise TypeError, 'feature_key should be a non-empty string'
89
+ end
90
+ unless SettingsService.instance.is_settings_valid
91
+ raise TypeError, 'Invalid Settings'
92
+ end
93
+
94
+ context_model = ContextModel.new.model_from_dictionary(context_copy)
95
+ FlagApi.new.get(feature_key, @settings, context_model, hooks_service)
96
+ rescue StandardError => e
97
+ LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::GET_FLAG})
98
+ GetFlagResponse.new(false, [], uuid, session_id)
99
+ end
100
+ end
101
+
102
+ # Track an event with given properties and context
103
+ # @param event_name [String] The name of the event to track
104
+ # @param context [Hash] The context of the user
105
+ # @param event_properties [Hash] The properties of the event
106
+ # @return [Hash] The result of the event tracking
107
+ def track_event(event_name, context, event_properties = {})
108
+ api_name = 'track_event'
109
+
110
+ begin
111
+ hooks_service = HooksService.new(@options)
112
+ LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
113
+
114
+ unless context.is_a?(Hash) && context[:id].is_a?(String) && !context[:id].empty?
115
+ raise TypeError, 'Invalid context, id should be a non-empty string'
116
+ end
117
+ # make a copy of the context to avoid modifying the original context
118
+ context_copy = context.dup
119
+ context_copy[:uuid] = UUIDUtil.get_uuid_from_context(@settings, context_copy, api_name)
120
+
121
+ unless event_name.is_a?(String) && !event_name.empty?
122
+ raise TypeError, 'event_name should be a non-empty string'
123
+ end
124
+ unless event_properties.is_a?(Hash)
125
+ raise TypeError, 'event_properties should be a hash'
126
+ end
127
+ unless SettingsService.instance.is_settings_valid
128
+ raise TypeError, 'Invalid Settings'
129
+ end
130
+
131
+ context_model = ContextModel.new.model_from_dictionary(context_copy)
132
+ TrackApi.new.track(@settings, event_name, context_model, event_properties, hooks_service)
133
+ rescue StandardError => e
134
+ LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::TRACK_EVENT})
135
+ { event_name: false }
136
+ end
137
+ end
138
+
139
+ # Set attributes for a given context
140
+ # @param attributes [Hash] The attributes to set
141
+ # @param context [Hash] The context of the user
142
+ # @return [Hash] The result of the attribute setting
143
+ def set_attribute(attributes, context = nil)
144
+ api_name = 'set_attribute'
145
+
146
+ begin
147
+ LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
148
+ unless context.is_a?(Hash)
149
+ raise TypeError, 'Invalid context'
150
+ end
151
+ unless context[:id].is_a?(String) && !context[:id].empty?
152
+ raise TypeError, 'Invalid context, id should be a non-empty string'
153
+ end
154
+ # make a copy of the context to avoid modifying the original context
155
+ context_copy = context.dup
156
+ context_copy[:uuid] = UUIDUtil.get_uuid_from_context(@settings, context_copy, api_name)
157
+
158
+ unless attributes.is_a?(Hash) && !attributes.empty?
159
+ raise TypeError, 'Attributes should be a hash with key-value pairs and non-empty'
160
+ end
161
+ unless SettingsService.instance.is_settings_valid
162
+ raise TypeError, 'Invalid Settings'
163
+ end
164
+
165
+ context_model = ContextModel.new.model_from_dictionary(context_copy)
166
+ SetAttributeApi.new.set_attribute(attributes, context_model)
167
+ rescue StandardError => e
168
+ LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::SET_ATTRIBUTE})
169
+ end
170
+ end
171
+
172
+ # Update the settings of the VWO client instance
173
+ # @param settings [Hash] The settings to update with (optional)
174
+ # @param is_via_webhook [Boolean] Whether the update is via webhook (default: true)
175
+ # @return [void]
176
+ def update_settings(settings = nil, is_via_webhook = true)
177
+ api_name = 'update_settings'
178
+
179
+ begin
180
+ LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
181
+
182
+ # Fetch settings from server or use provided settings if not empty
183
+ settings_to_update = if settings.nil? || settings.empty?
184
+ SettingsService.instance.fetch_settings(is_via_webhook)
185
+ else
186
+ settings
187
+ end
188
+
189
+ # Validate settings schema
190
+ unless SettingsSchema.new.is_settings_valid(settings_to_update)
191
+ raise TypeError, "Got Invalid Settings, settings: #{settings_to_update}"
192
+ end
193
+
194
+ # Set the settings on the client instance
195
+ set_settings_and_add_campaigns_to_rules(settings_to_update, self)
196
+ LoggerService.log(LogLevelEnum::INFO, "SETTINGS_UPDATED", {apiName: api_name, isViaWebhook: is_via_webhook})
197
+ rescue StandardError => e
198
+ LoggerService.log(
199
+ LogLevelEnum::ERROR,
200
+ "UPDATING_CLIENT_INSTANCE_FAILED_WHEN_WEBHOOK_TRIGGERED",
201
+ {
202
+ apiName: api_name,
203
+ isViaWebhook: is_via_webhook,
204
+ err: e.message,
205
+ an: ApiEnum::UPDATE_SETTINGS
206
+ }, false
207
+ )
208
+ end
209
+ end
210
+
211
+ # Flushes the batch events queue
212
+ # @return [void]
213
+ def flush_events
214
+ api_name = 'flush_events'
215
+ begin
216
+ LoggerService.log(LogLevelEnum::DEBUG, "API_CALLED", {apiName: api_name})
217
+ if BatchEventsQueue.instance.nil?
218
+ raise StandardError, "Batch events queue is not initialized"
219
+ end
220
+ # flush the batch events queue
221
+ @response = BatchEventsQueue.instance.flush(true)
222
+ @response
223
+ rescue StandardError => e
224
+ LoggerService.log(LogLevelEnum::ERROR, "EXECUTION_FAILED", {apiName: api_name, err: e.message, an: ApiEnum::FLUSH_EVENTS})
225
+ end
226
+ end
227
+ end