From d86fb8230f3b9e6c26ccb501581ac0e32eea3fe2 Mon Sep 17 00:00:00 2001
From: Abbas-khaliq <abbas.khaliq@gmail.com>
Date: Thu, 23 Dec 2021 14:42:34 +0530
Subject: [PATCH] feat(opt-out): opt-out of tracking by VWO without removing
 code

---
 CHANGELOG.md         |  5 ++-
 lib/vwo.rb           | 99 ++++++++++++++++++++++++++++++++++++++++----
 lib/vwo/constants.rb |  3 +-
 lib/vwo/enums.rb     |  2 +
 tests/test_vwo.rb    | 46 +++++++++++++++++++-
 5 files changed, 144 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8065cb9..c4be674 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -35,13 +35,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - For events architecture accounts, tracking same goal across multiple campaigns will not send multiple tracking calls. Instead, one single `POST` call would be made to track the same goal across multiple different campaigns running on the same environment.
 
 - Multiple custom dimension can be pushed via `push` API. For events architecture enabled account, only one single asynchronous call would be made to track multiple custom dimensions.
-```ruby
+
+  ```ruby
   custom_dimension_map = {
     browser: 'chrome',
     price: '20'
   }
   vwo_client_instance.push(custom_dimension_map, user_id)
-```
+  ```
 
 ## [1.24.1] - 2021-12-09
 
diff --git a/lib/vwo.rb b/lib/vwo.rb
index 209f2f9..cc5c954 100644
--- a/lib/vwo.rb
+++ b/lib/vwo.rb
@@ -65,6 +65,7 @@ def initialize(
     options = {}
   )
     options = convert_to_symbol_hash(options)
+    @is_opted_out = false
     @account_id = account_id
     @sdk_key = sdk_key
     @user_storage = user_storage
@@ -208,6 +209,10 @@ def get_settings(settings_file = nil)
   # VWO get_settings method to get settings for a particular account_id
   def get_and_update_settings_file
 
+    if is_opted_out(ApiMethods::GET_AND_UPDATE_SETTINGS_FILE)
+      return false
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
@@ -217,7 +222,7 @@ def get_and_update_settings_file
           api_name: ApiMethods.GET_AND_UPDATE_SETTINGS_FILE
         )
       )
-      return
+      return false
     end
 
     latest_settings = @settings_file_manager.get_settings_file(true)
@@ -267,6 +272,10 @@ def get_and_update_settings_file
   #                                               otherwise null in case of user not becoming part
 
   def activate(campaign_key, user_id, options = {})
+    if is_opted_out(ApiMethods::ACTIVATE)
+      return nil
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
@@ -444,6 +453,10 @@ def activate(campaign_key, user_id, options = {})
   #                                                       Otherwise null in case of user not becoming part
   #
   def get_variation_name(campaign_key, user_id, options = {})
+    if is_opted_out(ApiMethods::GET_VARIATION_NAME)
+      return nil
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
@@ -555,6 +568,10 @@ def get_variation_name(campaign_key, user_id, options = {})
   #
 
   def track(campaign_key, user_id, goal_identifier, options = {})
+    if is_opted_out(ApiMethods::TRACK)
+      return false
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
@@ -786,6 +803,10 @@ def track(campaign_key, user_id, goal_identifier, options = {})
   # @return[Boolean]  true if user becomes part of feature test/rollout, otherwise false.
 
   def feature_enabled?(campaign_key, user_id, options = {})
+    if is_opted_out(ApiMethods::IS_FEATURE_ENABLED)
+      return false
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
@@ -983,13 +1004,17 @@ def feature_enabled?(campaign_key, user_id, options = {})
   #
 
   def get_feature_variable_value(campaign_key, variable_key, user_id, options = {})
+    if is_opted_out(ApiMethods::GET_FEATURE_VARIABLE_VALUE)
+      return nil
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
         format(
           LogMessageEnum::ErrorMessages::API_CONFIG_CORRUPTED,
           file: FILE,
-          api_name: ApiMethods.GET_FEATURE_VARIABLE_VALUE
+          api_name: ApiMethods::GET_FEATURE_VARIABLE_VALUE
         )
       )
       return
@@ -1137,16 +1162,20 @@ def get_feature_variable_value(campaign_key, variable_key, user_id, options = {}
   # @return                                       true if call is made successfully, else false
 
   def push(tag_key, tag_value, user_id = nil)
+    if is_opted_out(ApiMethods::PUSH)
+      return false
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
         format(
           LogMessageEnum::ErrorMessages::API_CONFIG_CORRUPTED,
           file: FILE,
-          api_name: ApiMethods.PUSH
+          api_name: ApiMethods::PUSH
         )
       )
-      return
+      return false
     end
 
     # Argument reshuffling.
@@ -1245,6 +1274,10 @@ def is_eligible_to_send_impression()
   end
 
   def flush_events
+    if is_opted_out(ApiMethods::FLUSH_EVENTS)
+      return false
+    end
+
     unless @is_instance_valid
       @logger.log(
         LogLevelEnum::ERROR,
@@ -1254,10 +1287,13 @@ def flush_events
           api_name: ApiMethods::FLUSH_EVENTS
         )
       )
-      return
+      return false
+    end
+    result = false
+    if defined?(@batch_events) && !@batch_events_queue.nil?
+      result = @batch_events_queue.flush(manual: true)
+      @batch_events_queue.kill_thread
     end
-    result = @batch_events_queue.flush(manual: true)
-    @batch_events_queue.kill_thread
     result
   rescue StandardError => e
     @logger.log(
@@ -1294,6 +1330,55 @@ def get_goal_type_to_track(options)
     goal_type_to_track
   end
 
+  # Manually opting out of VWO SDK, No tracking will happen
+  #
+  # return[bool]
+  #
+  def set_opt_out
+    @logger.log(
+        LogLevelEnum::INFO,
+        format(
+          LogMessageEnum::InfoMessages::OPT_OUT_API_CALLED,
+          file: FILE
+        )
+      )
+      if defined?(@batch_events) && !@batch_events_queue.nil?
+        @batch_events_queue.flush(manual: true)
+        @batch_events_queue.kill_thread
+      end
+
+      @is_opted_out = true
+      @settings_file = nil
+      @user_storage = nil
+      @event_dispatcher = nil
+      @variation_decider = nil
+      @config = nil
+      @usage_stats = nil
+      @batch_event_dispatcher = nil
+      @batch_events_queue = nil
+      @batch_events = nil
+
+      return @is_opted_out
+  end
+
+
+  # Check if VWO SDK is manually opted out
+  # @param[String]          :api_name              api_name is used in logging
+  # @return[bool]
+  def is_opted_out(api_name)
+    if @is_opted_out
+      @logger.log(
+        LogLevelEnum::INFO,
+        format(
+          LogMessageEnum::InfoMessages::API_NOT_ENABLED,
+          file: FILE,
+          api: api_name
+        )
+      )
+    end
+    return @is_opted_out
+  end
+
   def is_event_arch_enabled
     return @settings_file.key?('isEventArchEnabled') && @settings_file['isEventArchEnabled']
   end
diff --git a/lib/vwo/constants.rb b/lib/vwo/constants.rb
index b98a827..845c600 100644
--- a/lib/vwo/constants.rb
+++ b/lib/vwo/constants.rb
@@ -27,7 +27,7 @@ module CONSTANTS
     HTTP_PROTOCOL = 'http://'
     HTTPS_PROTOCOL = 'https://'
     URL_NAMESPACE = '6ba7b811-9dad-11d1-80b4-00c04fd430c8'
-    SDK_VERSION = '1.25.0'
+    SDK_VERSION = '1.28.0'
     SDK_NAME = 'ruby'
     VWO_DELIMITER = '_vwo_'
     MAX_EVENTS_PER_REQUEST = 5000
@@ -102,6 +102,7 @@ module ApiMethods
       PUSH = 'push'
       GET_AND_UPDATE_SETTINGS_FILE = 'get_and_update_settings_file'
       FLUSH_EVENTS = 'flush_events'
+      OPT_OUT = 'opt_out'
     end
 
     module PushApi
diff --git a/lib/vwo/enums.rb b/lib/vwo/enums.rb
index 92721d2..94d1802 100644
--- a/lib/vwo/enums.rb
+++ b/lib/vwo/enums.rb
@@ -164,6 +164,8 @@ module InfoMessages
         GOT_ELIGIBLE_CAMPAIGNS = "(%<file>s): Got %<no_of_eligible_campaigns>s eligible winners out of %<no_of_group_campaigns>s from the Group:%<group_name>s and for User ID:%<user_id>s"
         CALLED_CAMPAIGN_NOT_WINNER = "(%<file>s): Campaign:%<campaign_key>s does not qualify from the mutually exclusive group:%<group_name>s for User ID:%<user_id>s"
         OTHER_CAMPAIGN_SATISFIES_WHITELISTING_OR_STORAGE = "(%<file>s): Campaign:%<campaign_key>s of Group:%<group_name>s satisfies %<type>s for User ID:%<user_id>s"
+        OPT_OUT_API_CALLED = "(%<file>s): You have opted out for not tracking i.e. all API calls will stop functioning and will simply early return"
+        API_NOT_ENABLED = "(%<file>s): %<api>s API is disabled as you opted out for tracking. Reinitialize the SDK to enable the normal functioning of all APIs."
       end
 
       # Warning Messages
diff --git a/tests/test_vwo.rb b/tests/test_vwo.rb
index fff89bf..f21e225 100644
--- a/tests/test_vwo.rb
+++ b/tests/test_vwo.rb
@@ -1148,7 +1148,7 @@ def flush_callback(events)
 
     def test_flush_events_with_corrupted_vwo_instance
       set_up('EMPTY_SETTINGS_FILE')
-      assert_equal(@vwo.flush_events, nil)
+      assert_equal(@vwo.flush_events, false)
     end
 
     def test_flush_events_raises_exception
@@ -1603,4 +1603,48 @@ def test_get_variation_as_user_hash_passes_whitelisting
       }
       assert_equal(vwo_instance.get_variation_name('AB_T_100_W_25_25_25_25', 'Rohit'), 'Variation-1')
     end
+
+    def test_set_opt_out_api
+      set_up('T_50_W_50_50_WS')
+      assert_equal(@vwo.set_opt_out, true)
+    end
+
+    def test_apis_when_set_opt_out_called
+      set_up('T_50_W_50_50_WS')
+
+      assert_equal(@vwo.set_opt_out, true)
+      assert_equal(@vwo.activate('T_50_W_50_50_WS', 'Ashley', {}), nil)
+      assert_equal(@vwo.get_variation_name('T_50_W_50_50_WS', 'Ashley', {}), nil)
+      goal_identifier = 'ddd'
+      assert_equal(@vwo.track('T_50_W_50_50_WS', 'Ashley', goal_identifier, {}), false)
+      assert_equal(@vwo.feature_enabled?('T_50_W_50_50_WS', 'Ashley', {}), false)
+      assert_equal(@vwo.get_feature_variable_value('FT_T_75_W_10_20_30_40_WS', 'STRING_VARIABLE', 'Ashley', {}), nil)
+      assert_equal(@vwo.get_and_update_settings_file, false)
+      assert_equal(@vwo.push('tagKey', 'tagValue', 'Ashley'), false)      
+      assert_equal(@vwo.flush_events, false)
+    end
+
+    def test_apis_when_set_opt_out_called_with_event_batch
+      def flush_callback(events)
+      end
+
+      options = {
+        batch_events: {
+          events_per_request: 3,
+          request_time_interval: 5
+        }
+      }
+      vwo_instance = initialize_vwo_with_batch_events_option('AB_T_50_W_50_50', options)
+
+      assert_equal(vwo_instance.set_opt_out, true)
+      assert_equal(vwo_instance.activate('T_50_W_50_50_WS', 'Ashley', {}), nil)
+      assert_equal(vwo_instance.get_variation_name('T_50_W_50_50_WS', 'Ashley', {}), nil)
+      goal_identifier = 'ddd'
+      assert_equal(vwo_instance.track('T_50_W_50_50_WS', 'Ashley', goal_identifier, {}), false)
+      assert_equal(vwo_instance.feature_enabled?('T_50_W_50_50_WS', 'Ashley', {}), false)
+      assert_equal(vwo_instance.get_feature_variable_value('FT_T_75_W_10_20_30_40_WS', 'STRING_VARIABLE', 'Ashley', {}), nil)
+      assert_equal(vwo_instance.get_and_update_settings_file, false)
+      assert_equal(vwo_instance.push('tagKey', 'tagValue', 'Ashley'), false)      
+      assert_equal(vwo_instance.flush_events, false)
+    end
 end