hackle-ruby-sdk 0.0.1 → 1.0.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 +4 -4
 - data/README.md +1 -3
 - data/hackle-ruby-sdk.gemspec +1 -1
 - data/lib/hackle-ruby-sdk.rb +2 -21
 - data/lib/hackle.rb +76 -0
 - data/lib/hackle/client.rb +127 -0
 - data/lib/{hackle-ruby-sdk → hackle}/config.rb +1 -1
 - data/lib/hackle/decision/bucketer.rb +44 -0
 - data/lib/hackle/decision/decider.rb +69 -0
 - data/lib/{hackle-ruby-sdk → hackle}/events/event_dispatcher.rb +31 -24
 - data/lib/{hackle-ruby-sdk → hackle}/events/event_processor.rb +22 -11
 - data/lib/hackle/events/user_event.rb +61 -0
 - data/lib/{hackle-ruby-sdk → hackle}/http/http.rb +7 -9
 - data/lib/hackle/models/bucket.rb +26 -0
 - data/lib/hackle/models/event.rb +26 -0
 - data/lib/hackle/models/event_type.rb +22 -0
 - data/lib/hackle/models/experiment.rb +69 -0
 - data/lib/hackle/models/slot.rb +22 -0
 - data/lib/hackle/models/user.rb +24 -0
 - data/lib/hackle/models/variation.rb +21 -0
 - data/lib/{hackle-ruby-sdk → hackle}/version.rb +2 -2
 - data/lib/hackle/workspaces/http_workspace_fetcher.rb +24 -0
 - data/lib/{hackle-ruby-sdk → hackle}/workspaces/polling_workspace_fetcher.rb +4 -1
 - data/lib/hackle/workspaces/workspace.rb +100 -0
 - metadata +22 -19
 - data/lib/hackle-ruby-sdk/client.rb +0 -108
 - data/lib/hackle-ruby-sdk/decision/bucketer.rb +0 -26
 - data/lib/hackle-ruby-sdk/decision/decider.rb +0 -54
 - data/lib/hackle-ruby-sdk/events/event.rb +0 -33
 - data/lib/hackle-ruby-sdk/models/bucket.rb +0 -15
 - data/lib/hackle-ruby-sdk/models/event_type.rb +0 -10
 - data/lib/hackle-ruby-sdk/models/experiment.rb +0 -39
 - data/lib/hackle-ruby-sdk/models/slot.rb +0 -15
 - data/lib/hackle-ruby-sdk/models/variation.rb +0 -11
 - data/lib/hackle-ruby-sdk/workspaces/http_workspace_fetcher.rb +0 -24
 - data/lib/hackle-ruby-sdk/workspaces/workspace.rb +0 -78
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: 2eb9518d11c4980c9a6abf036b8400fad5de381db6c60d2fc98b68e37cda8851
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: faf6536122c01e9a55f95eef4e6497313a7e09e2ff16ea1123ca6c7fdc56470a
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: cbf193163d0b5926f052e76b7b01e0ca472297ef5da32b2317e90a1f35450cefa6496a72bcd0e9d6c7faf83947b6a9379e07be7111f5fa0a5030b891013c6c1c
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: e9d8ed3a4439684fb0b3308edb0a2e48c36ba041fa8f081e33ddad237ee8a5b546f0c98a1ec05b5c4123f76fe9289bc2ea3e84d9b02b068a3833472eec77691e
         
     | 
    
        data/README.md
    CHANGED
    
    | 
         @@ -1,8 +1,6 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            # Hackle::Ruby::Sdk
         
     | 
| 
       2 
2 
     | 
    
         | 
| 
       3 
     | 
    
         
            -
            Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hackle 
     | 
| 
       4 
     | 
    
         
            -
             
     | 
| 
       5 
     | 
    
         
            -
            TODO: Delete this and the text above, and describe your gem
         
     | 
| 
      
 3 
     | 
    
         
            +
            Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/hackle`.
         
     | 
| 
       6 
4 
     | 
    
         | 
| 
       7 
5 
     | 
    
         
             
            ## Installation
         
     | 
| 
       8 
6 
     | 
    
         | 
    
        data/hackle-ruby-sdk.gemspec
    CHANGED
    
    
    
        data/lib/hackle-ruby-sdk.rb
    CHANGED
    
    | 
         @@ -1,22 +1,3 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
             
     | 
| 
       2 
     | 
    
         
            -
            require 'hackle-ruby-sdk/decision/decider'
         
     | 
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
       3 
2 
     | 
    
         | 
| 
       4 
     | 
    
         
            -
            require 'hackle 
     | 
| 
       5 
     | 
    
         
            -
            require 'hackle-ruby-sdk/events/event_dispatcher'
         
     | 
| 
       6 
     | 
    
         
            -
            require 'hackle-ruby-sdk/events/event_processor'
         
     | 
| 
       7 
     | 
    
         
            -
             
     | 
| 
       8 
     | 
    
         
            -
            require 'hackle-ruby-sdk/http/http'
         
     | 
| 
       9 
     | 
    
         
            -
             
     | 
| 
       10 
     | 
    
         
            -
            require 'hackle-ruby-sdk/models/bucket'
         
     | 
| 
       11 
     | 
    
         
            -
            require 'hackle-ruby-sdk/models/event_type'
         
     | 
| 
       12 
     | 
    
         
            -
            require 'hackle-ruby-sdk/models/experiment'
         
     | 
| 
       13 
     | 
    
         
            -
            require 'hackle-ruby-sdk/models/slot'
         
     | 
| 
       14 
     | 
    
         
            -
            require 'hackle-ruby-sdk/models/variation'
         
     | 
| 
       15 
     | 
    
         
            -
             
     | 
| 
       16 
     | 
    
         
            -
            require 'hackle-ruby-sdk/workspaces/http_workspace_fetcher'
         
     | 
| 
       17 
     | 
    
         
            -
            require 'hackle-ruby-sdk/workspaces/polling_workspace_fetcher'
         
     | 
| 
       18 
     | 
    
         
            -
            require 'hackle-ruby-sdk/workspaces/workspace'
         
     | 
| 
       19 
     | 
    
         
            -
             
     | 
| 
       20 
     | 
    
         
            -
            require 'hackle-ruby-sdk/client'
         
     | 
| 
       21 
     | 
    
         
            -
            require 'hackle-ruby-sdk/config'
         
     | 
| 
       22 
     | 
    
         
            -
            require 'hackle-ruby-sdk/version'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'hackle'
         
     | 
    
        data/lib/hackle.rb
    ADDED
    
    | 
         @@ -0,0 +1,76 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'hackle/client'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'hackle/config'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'hackle/version'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Hackle
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
              #
         
     | 
| 
      
 10 
     | 
    
         
            +
              # Instantiates a Hackle client.
         
     | 
| 
      
 11 
     | 
    
         
            +
              #
         
     | 
| 
      
 12 
     | 
    
         
            +
              # @see Client#initialize
         
     | 
| 
      
 13 
     | 
    
         
            +
              #
         
     | 
| 
      
 14 
     | 
    
         
            +
              # @param sdk_key [String] The SDK key of your Hackle environment
         
     | 
| 
      
 15 
     | 
    
         
            +
              # @param options Optional parameters of configuration options
         
     | 
| 
      
 16 
     | 
    
         
            +
              #
         
     | 
| 
      
 17 
     | 
    
         
            +
              # @return [Client] The Hackle client instance.
         
     | 
| 
      
 18 
     | 
    
         
            +
              #
         
     | 
| 
      
 19 
     | 
    
         
            +
              def self.client(sdk_key:, **options)
         
     | 
| 
      
 20 
     | 
    
         
            +
                config = Config.new(options)
         
     | 
| 
      
 21 
     | 
    
         
            +
                sdk_info = SdkInfo.new(key: sdk_key)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                http_workspace_fetcher = HttpWorkspaceFetcher.new(config: config, sdk_info: sdk_info)
         
     | 
| 
      
 24 
     | 
    
         
            +
                polling_workspace_fetcher = PollingWorkspaceFetcher.new(config: config, http_fetcher: http_workspace_fetcher)
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                event_dispatcher = EventDispatcher.new(config: config, sdk_info: sdk_info)
         
     | 
| 
      
 27 
     | 
    
         
            +
                event_processor = EventProcessor.new(config: config, event_dispatcher: event_dispatcher)
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                polling_workspace_fetcher.start!
         
     | 
| 
      
 30 
     | 
    
         
            +
                event_processor.start!
         
     | 
| 
      
 31 
     | 
    
         
            +
             
     | 
| 
      
 32 
     | 
    
         
            +
                Client.new(
         
     | 
| 
      
 33 
     | 
    
         
            +
                  config: config,
         
     | 
| 
      
 34 
     | 
    
         
            +
                  workspace_fetcher: polling_workspace_fetcher,
         
     | 
| 
      
 35 
     | 
    
         
            +
                  event_processor: event_processor,
         
     | 
| 
      
 36 
     | 
    
         
            +
                  decider: Decider.new
         
     | 
| 
      
 37 
     | 
    
         
            +
                )
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
             
     | 
| 
      
 40 
     | 
    
         
            +
              #
         
     | 
| 
      
 41 
     | 
    
         
            +
              # Instantiate a user to be used for the hackle sdk.
         
     | 
| 
      
 42 
     | 
    
         
            +
              #
         
     | 
| 
      
 43 
     | 
    
         
            +
              # The only required parameter is `id`, which must uniquely identify each user.
         
     | 
| 
      
 44 
     | 
    
         
            +
              #
         
     | 
| 
      
 45 
     | 
    
         
            +
              # @example
         
     | 
| 
      
 46 
     | 
    
         
            +
              #  Hackle.user(id: 'ae2182e0')
         
     | 
| 
      
 47 
     | 
    
         
            +
              #  Hackle.user(id: 'ae2182e0', app_version: '1.0.1', paying_customer: false)
         
     | 
| 
      
 48 
     | 
    
         
            +
              #
         
     | 
| 
      
 49 
     | 
    
         
            +
              # @param id [String] The identifier of the user. (e.g. device_id, account_id etc.)
         
     | 
| 
      
 50 
     | 
    
         
            +
              # @param properties Additional properties of the user. (e.g. app_version, membership_grade, etc.)
         
     | 
| 
      
 51 
     | 
    
         
            +
              #
         
     | 
| 
      
 52 
     | 
    
         
            +
              # @return [User] The configured user object.
         
     | 
| 
      
 53 
     | 
    
         
            +
              #
         
     | 
| 
      
 54 
     | 
    
         
            +
              def self.user(id:, **properties)
         
     | 
| 
      
 55 
     | 
    
         
            +
                User.new(id: id, properties: properties)
         
     | 
| 
      
 56 
     | 
    
         
            +
              end
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
              #
         
     | 
| 
      
 59 
     | 
    
         
            +
              # Instantiate an event to be used for the hackle sdk.
         
     | 
| 
      
 60 
     | 
    
         
            +
              #
         
     | 
| 
      
 61 
     | 
    
         
            +
              # The only required parameter is `key`, which must uniquely identify each event.
         
     | 
| 
      
 62 
     | 
    
         
            +
              #
         
     | 
| 
      
 63 
     | 
    
         
            +
              # @example
         
     | 
| 
      
 64 
     | 
    
         
            +
              #  Hackle.event(key: 'purchase')
         
     | 
| 
      
 65 
     | 
    
         
            +
              #  Hackle.event(key: 'purchase', value: 42000.0, app_version: '1.0.1', payment_method: 'CARD' )
         
     | 
| 
      
 66 
     | 
    
         
            +
              #
         
     | 
| 
      
 67 
     | 
    
         
            +
              # @param key [String] The unique key of the events.
         
     | 
| 
      
 68 
     | 
    
         
            +
              # @param value [Float] Optional numeric value of the events (e.g. purchase_amount, quantity, etc.)
         
     | 
| 
      
 69 
     | 
    
         
            +
              # @param properties Additional properties of the events (e.g. app_version, os_type, etc.)
         
     | 
| 
      
 70 
     | 
    
         
            +
              #
         
     | 
| 
      
 71 
     | 
    
         
            +
              # @return [Event] The configured event object.
         
     | 
| 
      
 72 
     | 
    
         
            +
              #
         
     | 
| 
      
 73 
     | 
    
         
            +
              def self.event(key:, value: nil, **properties)
         
     | 
| 
      
 74 
     | 
    
         
            +
                Event.new(key: key, value: value, properties: properties)
         
     | 
| 
      
 75 
     | 
    
         
            +
              end
         
     | 
| 
      
 76 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,127 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'hackle/decision/bucketer'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'hackle/decision/decider'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            require 'hackle/events/user_event'
         
     | 
| 
      
 7 
     | 
    
         
            +
            require 'hackle/events/event_dispatcher'
         
     | 
| 
      
 8 
     | 
    
         
            +
            require 'hackle/events/event_processor'
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
            require 'hackle/http/http'
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
            require 'hackle/models/bucket'
         
     | 
| 
      
 13 
     | 
    
         
            +
            require 'hackle/models/event'
         
     | 
| 
      
 14 
     | 
    
         
            +
            require 'hackle/models/event_type'
         
     | 
| 
      
 15 
     | 
    
         
            +
            require 'hackle/models/experiment'
         
     | 
| 
      
 16 
     | 
    
         
            +
            require 'hackle/models/slot'
         
     | 
| 
      
 17 
     | 
    
         
            +
            require 'hackle/models/user'
         
     | 
| 
      
 18 
     | 
    
         
            +
            require 'hackle/models/variation'
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
            require 'hackle/workspaces/http_workspace_fetcher'
         
     | 
| 
      
 21 
     | 
    
         
            +
            require 'hackle/workspaces/polling_workspace_fetcher'
         
     | 
| 
      
 22 
     | 
    
         
            +
            require 'hackle/workspaces/workspace'
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
            module Hackle
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
              #
         
     | 
| 
      
 27 
     | 
    
         
            +
              # A client for Hackle API.
         
     | 
| 
      
 28 
     | 
    
         
            +
              #
         
     | 
| 
      
 29 
     | 
    
         
            +
              class Client
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                #
         
     | 
| 
      
 32 
     | 
    
         
            +
                # Initializes a Hackle client.
         
     | 
| 
      
 33 
     | 
    
         
            +
                #
         
     | 
| 
      
 34 
     | 
    
         
            +
                # @param config [Config]
         
     | 
| 
      
 35 
     | 
    
         
            +
                # @param workspace_fetcher [PollingWorkspaceFetcher]
         
     | 
| 
      
 36 
     | 
    
         
            +
                # @param event_processor [EventProcessor]
         
     | 
| 
      
 37 
     | 
    
         
            +
                # @param decider [Decider]
         
     | 
| 
      
 38 
     | 
    
         
            +
                #
         
     | 
| 
      
 39 
     | 
    
         
            +
                def initialize(config:, workspace_fetcher:, event_processor:, decider:)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  @logger = config.logger
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  # @type [PollingWorkspaceFetcher]
         
     | 
| 
      
 43 
     | 
    
         
            +
                  @workspace_fetcher = workspace_fetcher
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  # @type [EventProcessor]
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @event_processor = event_processor
         
     | 
| 
      
 47 
     | 
    
         
            +
             
     | 
| 
      
 48 
     | 
    
         
            +
                  # @type [Decider]
         
     | 
| 
      
 49 
     | 
    
         
            +
                  @decider = decider
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                #
         
     | 
| 
      
 53 
     | 
    
         
            +
                # Decide the variation to expose to the user for experiment.
         
     | 
| 
      
 54 
     | 
    
         
            +
                #
         
     | 
| 
      
 55 
     | 
    
         
            +
                # This method return the control variation 'A' if:
         
     | 
| 
      
 56 
     | 
    
         
            +
                # - The experiment key is invalid
         
     | 
| 
      
 57 
     | 
    
         
            +
                # - The experiment has not started yet
         
     | 
| 
      
 58 
     | 
    
         
            +
                # - The user is not allocated to the experiment
         
     | 
| 
      
 59 
     | 
    
         
            +
                # - The decided variation has been dropped
         
     | 
| 
      
 60 
     | 
    
         
            +
                #
         
     | 
| 
      
 61 
     | 
    
         
            +
                # @param experiment_key [Integer] The unique key of the experiment. MUST NOT be nil.
         
     | 
| 
      
 62 
     | 
    
         
            +
                # @param user [User] the user to participate in the experiment. MUST NOT be nil.
         
     | 
| 
      
 63 
     | 
    
         
            +
                # @param default_variation [String] The default variation of the experiment.
         
     | 
| 
      
 64 
     | 
    
         
            +
                #
         
     | 
| 
      
 65 
     | 
    
         
            +
                # @return [String] The decided variation for the user, or default variation
         
     | 
| 
      
 66 
     | 
    
         
            +
                #
         
     | 
| 
      
 67 
     | 
    
         
            +
                def variation(experiment_key:, user:, default_variation: 'A')
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                  return default_variation if experiment_key.nil? || !experiment_key.is_a?(Integer)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  return default_variation if user.nil? || !user.is_a?(User) || !user.valid?
         
     | 
| 
      
 71 
     | 
    
         
            +
             
     | 
| 
      
 72 
     | 
    
         
            +
                  workspace = @workspace_fetcher.fetch
         
     | 
| 
      
 73 
     | 
    
         
            +
                  return default_variation if workspace.nil?
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  experiment = workspace.get_experiment(experiment_key: experiment_key)
         
     | 
| 
      
 76 
     | 
    
         
            +
                  return default_variation if experiment.nil?
         
     | 
| 
      
 77 
     | 
    
         
            +
             
     | 
| 
      
 78 
     | 
    
         
            +
                  decision = @decider.decide(experiment: experiment, user: user)
         
     | 
| 
      
 79 
     | 
    
         
            +
                  case decision
         
     | 
| 
      
 80 
     | 
    
         
            +
                  when Decision::NotAllocated
         
     | 
| 
      
 81 
     | 
    
         
            +
                    default_variation
         
     | 
| 
      
 82 
     | 
    
         
            +
                  when Decision::ForcedAllocated
         
     | 
| 
      
 83 
     | 
    
         
            +
                    decision.variation_key
         
     | 
| 
      
 84 
     | 
    
         
            +
                  when Decision::NaturalAllocated
         
     | 
| 
      
 85 
     | 
    
         
            +
                    exposure_event = UserEvent::Exposure.new(user: user, experiment: experiment, variation: decision.variation)
         
     | 
| 
      
 86 
     | 
    
         
            +
                    @event_processor.process(event: exposure_event)
         
     | 
| 
      
 87 
     | 
    
         
            +
                    decision.variation.key
         
     | 
| 
      
 88 
     | 
    
         
            +
                  else
         
     | 
| 
      
 89 
     | 
    
         
            +
                    default_variation
         
     | 
| 
      
 90 
     | 
    
         
            +
                  end
         
     | 
| 
      
 91 
     | 
    
         
            +
             
     | 
| 
      
 92 
     | 
    
         
            +
                rescue => e
         
     | 
| 
      
 93 
     | 
    
         
            +
                  @logger.error { "Unexpected error while deciding variation for experiment[#{experiment_key}]. Returning default variation[#{default_variation}]: #{e.inspect}" }
         
     | 
| 
      
 94 
     | 
    
         
            +
                  default_variation
         
     | 
| 
      
 95 
     | 
    
         
            +
                end
         
     | 
| 
      
 96 
     | 
    
         
            +
             
     | 
| 
      
 97 
     | 
    
         
            +
                #
         
     | 
| 
      
 98 
     | 
    
         
            +
                # Records the event that occurred by the user.
         
     | 
| 
      
 99 
     | 
    
         
            +
                #
         
     | 
| 
      
 100 
     | 
    
         
            +
                # @param event [Event] the event that occurred.
         
     | 
| 
      
 101 
     | 
    
         
            +
                # @param user [User] the user that occurred the event.
         
     | 
| 
      
 102 
     | 
    
         
            +
                #
         
     | 
| 
      
 103 
     | 
    
         
            +
                def track(event:, user:)
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                  return if event.nil? || !event.is_a?(Event) || !event.valid?
         
     | 
| 
      
 106 
     | 
    
         
            +
                  return if user.nil? || !user.is_a?(User) || !user.valid?
         
     | 
| 
      
 107 
     | 
    
         
            +
             
     | 
| 
      
 108 
     | 
    
         
            +
                  workspace = @workspace_fetcher.fetch
         
     | 
| 
      
 109 
     | 
    
         
            +
                  return if workspace.nil?
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                  event_type = workspace.get_event_type(event_type_key: event.key)
         
     | 
| 
      
 112 
     | 
    
         
            +
                  track_event = UserEvent::Track.new(user: user, event_type: event_type, event: event)
         
     | 
| 
      
 113 
     | 
    
         
            +
                  @event_processor.process(event: track_event)
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                rescue => e
         
     | 
| 
      
 116 
     | 
    
         
            +
                  @logger.error { "Unexpected error while tracking event: #{e.inspect}" }
         
     | 
| 
      
 117 
     | 
    
         
            +
                end
         
     | 
| 
      
 118 
     | 
    
         
            +
             
     | 
| 
      
 119 
     | 
    
         
            +
                #
         
     | 
| 
      
 120 
     | 
    
         
            +
                # Shutdown the background task and release the resources used for the background task.
         
     | 
| 
      
 121 
     | 
    
         
            +
                #
         
     | 
| 
      
 122 
     | 
    
         
            +
                def close
         
     | 
| 
      
 123 
     | 
    
         
            +
                  @workspace_fetcher.stop!
         
     | 
| 
      
 124 
     | 
    
         
            +
                  @event_processor.stop!
         
     | 
| 
      
 125 
     | 
    
         
            +
                end
         
     | 
| 
      
 126 
     | 
    
         
            +
              end
         
     | 
| 
      
 127 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'murmurhash3'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Hackle
         
     | 
| 
      
 6 
     | 
    
         
            +
              class Bucketer
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                # @param bucket [Bucket]
         
     | 
| 
      
 9 
     | 
    
         
            +
                # @param user [User]
         
     | 
| 
      
 10 
     | 
    
         
            +
                #
         
     | 
| 
      
 11 
     | 
    
         
            +
                # @return [Slot, nil]
         
     | 
| 
      
 12 
     | 
    
         
            +
                def bucketing(bucket:, user:)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  slot_number = calculate_slot_number(
         
     | 
| 
      
 14 
     | 
    
         
            +
                    seed: bucket.seed,
         
     | 
| 
      
 15 
     | 
    
         
            +
                    slot_size: bucket.slot_size,
         
     | 
| 
      
 16 
     | 
    
         
            +
                    user_id: user.id
         
     | 
| 
      
 17 
     | 
    
         
            +
                  )
         
     | 
| 
      
 18 
     | 
    
         
            +
                  bucket.get_slot(slot_number: slot_number)
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                # @param seed [Integer]
         
     | 
| 
      
 22 
     | 
    
         
            +
                # @param slot_size [Integer]
         
     | 
| 
      
 23 
     | 
    
         
            +
                # @param user_id [String]
         
     | 
| 
      
 24 
     | 
    
         
            +
                #
         
     | 
| 
      
 25 
     | 
    
         
            +
                # @return [Integer]
         
     | 
| 
      
 26 
     | 
    
         
            +
                def calculate_slot_number(seed:, slot_size:, user_id:)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  hash_value = hash(data: user_id, seed: seed)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  hash_value.abs % slot_size
         
     | 
| 
      
 29 
     | 
    
         
            +
                end
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                # @param data [String]
         
     | 
| 
      
 32 
     | 
    
         
            +
                # @param seed [Integer]
         
     | 
| 
      
 33 
     | 
    
         
            +
                #
         
     | 
| 
      
 34 
     | 
    
         
            +
                # @return [Integer]
         
     | 
| 
      
 35 
     | 
    
         
            +
                def hash(data:, seed:)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  unsigned_value = MurmurHash3::V32.str_hash(data, seed)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  if (unsigned_value & 0x80000000).zero?
         
     | 
| 
      
 38 
     | 
    
         
            +
                    unsigned_value
         
     | 
| 
      
 39 
     | 
    
         
            +
                  else
         
     | 
| 
      
 40 
     | 
    
         
            +
                    -((unsigned_value ^ 0xFFFFFFFF) + 1)
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,69 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Hackle
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Decision
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
                class NotAllocated < Decision
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                class ForcedAllocated < Decision
         
     | 
| 
      
 10 
     | 
    
         
            +
                  # @return [String]
         
     | 
| 
      
 11 
     | 
    
         
            +
                  attr_reader :variation_key
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  # @param variation_key [String]
         
     | 
| 
      
 14 
     | 
    
         
            +
                  def initialize(variation_key:)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    @variation_key = variation_key
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                class NaturalAllocated < Decision
         
     | 
| 
      
 20 
     | 
    
         
            +
                  # @return [Variation]
         
     | 
| 
      
 21 
     | 
    
         
            +
                  attr_reader :variation
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  # @param variation [Variation]
         
     | 
| 
      
 24 
     | 
    
         
            +
                  def initialize(variation:)
         
     | 
| 
      
 25 
     | 
    
         
            +
                    @variation = variation
         
     | 
| 
      
 26 
     | 
    
         
            +
                  end
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              class Decider
         
     | 
| 
      
 31 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @bucketer = Bucketer.new
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                # @param experiment [Experiment]
         
     | 
| 
      
 36 
     | 
    
         
            +
                # @param user [User]
         
     | 
| 
      
 37 
     | 
    
         
            +
                #
         
     | 
| 
      
 38 
     | 
    
         
            +
                # @return [Decision]
         
     | 
| 
      
 39 
     | 
    
         
            +
                def decide(experiment:, user:)
         
     | 
| 
      
 40 
     | 
    
         
            +
                  case experiment
         
     | 
| 
      
 41 
     | 
    
         
            +
                  when Experiment::Completed
         
     | 
| 
      
 42 
     | 
    
         
            +
                    Decision::ForcedAllocated.new(variation_key: experiment.winner_variation_key)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  when Experiment::Running
         
     | 
| 
      
 44 
     | 
    
         
            +
                    decide_running(running_experiment: experiment, user: user)
         
     | 
| 
      
 45 
     | 
    
         
            +
                  else
         
     | 
| 
      
 46 
     | 
    
         
            +
                    NotAllocated.new
         
     | 
| 
      
 47 
     | 
    
         
            +
                  end
         
     | 
| 
      
 48 
     | 
    
         
            +
                end
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                # @param running_experiment [Experiment::Running]
         
     | 
| 
      
 51 
     | 
    
         
            +
                # @param user [User]
         
     | 
| 
      
 52 
     | 
    
         
            +
                #
         
     | 
| 
      
 53 
     | 
    
         
            +
                # @return [Decision]
         
     | 
| 
      
 54 
     | 
    
         
            +
                def decide_running(running_experiment:, user:)
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  overridden_variation = running_experiment.get_overridden_variation(user: user)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  return Decision::ForcedAllocated.new(variation_key: overridden_variation.key) unless overridden_variation.nil?
         
     | 
| 
      
 58 
     | 
    
         
            +
             
     | 
| 
      
 59 
     | 
    
         
            +
                  allocated_slot = @bucketer.bucketing(bucket: running_experiment.bucket, user: user)
         
     | 
| 
      
 60 
     | 
    
         
            +
                  return Decision::NotAllocated.new if allocated_slot.nil?
         
     | 
| 
      
 61 
     | 
    
         
            +
             
     | 
| 
      
 62 
     | 
    
         
            +
                  allocated_variation = running_experiment.get_variation(variation_id: allocated_slot.variation_id)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  return Decision::NotAllocated.new if allocated_variation.nil?
         
     | 
| 
      
 64 
     | 
    
         
            +
                  return Decision::NotAllocated.new if allocated_variation.dropped
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                  Decision::NaturalAllocated.new(variation: allocated_variation)
         
     | 
| 
      
 67 
     | 
    
         
            +
                end
         
     | 
| 
      
 68 
     | 
    
         
            +
              end
         
     | 
| 
      
 69 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -6,10 +6,10 @@ module Hackle 
     | 
|
| 
       6 
6 
     | 
    
         
             
                DEFAULT_DISPATCH_WORKER_SIZE = 2
         
     | 
| 
       7 
7 
     | 
    
         
             
                DEFAULT_DISPATCH_QUEUE_CAPACITY = 50
         
     | 
| 
       8 
8 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
                def initialize(config 
     | 
| 
      
 9 
     | 
    
         
            +
                def initialize(config:, sdk_info:)
         
     | 
| 
       10 
10 
     | 
    
         
             
                  @logger = config.logger
         
     | 
| 
       11 
     | 
    
         
            -
                  @client = HTTP.client(config.event_uri)
         
     | 
| 
       12 
     | 
    
         
            -
                  @headers = HTTP.sdk_headers(sdk_info)
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @client = HTTP.client(base_uri: config.event_uri)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @headers = HTTP.sdk_headers(sdk_info: sdk_info)
         
     | 
| 
       13 
13 
     | 
    
         
             
                  @dispatcher_executor = Concurrent::ThreadPoolExecutor.new(
         
     | 
| 
       14 
14 
     | 
    
         
             
                    min_threads: DEFAULT_DISPATCH_WORKER_SIZE,
         
     | 
| 
       15 
15 
     | 
    
         
             
                    max_threads: DEFAULT_DISPATCH_WORKER_SIZE,
         
     | 
| 
         @@ -17,10 +17,10 @@ module Hackle 
     | 
|
| 
       17 
17 
     | 
    
         
             
                  )
         
     | 
| 
       18 
18 
     | 
    
         
             
                end
         
     | 
| 
       19 
19 
     | 
    
         | 
| 
       20 
     | 
    
         
            -
                def dispatch(events)
         
     | 
| 
       21 
     | 
    
         
            -
                  payload = create_payload(events)
         
     | 
| 
      
 20 
     | 
    
         
            +
                def dispatch(events:)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  payload = create_payload(events: events)
         
     | 
| 
       22 
22 
     | 
    
         
             
                  begin
         
     | 
| 
       23 
     | 
    
         
            -
                    @dispatcher_executor.post { dispatch_payload(payload) }
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @dispatcher_executor.post { dispatch_payload(payload: payload) }
         
     | 
| 
       24 
24 
     | 
    
         
             
                  rescue Concurrent::RejectedExecutionError
         
     | 
| 
       25 
25 
     | 
    
         
             
                    @logger.warn { 'Dispatcher executor queue is full. Event dispatch rejected' }
         
     | 
| 
       26 
26 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -35,7 +35,7 @@ module Hackle 
     | 
|
| 
       35 
35 
     | 
    
         | 
| 
       36 
36 
     | 
    
         
             
                private
         
     | 
| 
       37 
37 
     | 
    
         | 
| 
       38 
     | 
    
         
            -
                def dispatch_payload(payload)
         
     | 
| 
      
 38 
     | 
    
         
            +
                def dispatch_payload(payload:)
         
     | 
| 
       39 
39 
     | 
    
         
             
                  request = Net::HTTP::Post.new('/api/v1/events', @headers)
         
     | 
| 
       40 
40 
     | 
    
         
             
                  request.content_type = 'application/json'
         
     | 
| 
       41 
41 
     | 
    
         
             
                  request.body = payload.to_json
         
     | 
| 
         @@ -43,19 +43,19 @@ module Hackle 
     | 
|
| 
       43 
43 
     | 
    
         
             
                  response = @client.request(request)
         
     | 
| 
       44 
44 
     | 
    
         | 
| 
       45 
45 
     | 
    
         
             
                  status_code = response.code.to_i
         
     | 
| 
       46 
     | 
    
         
            -
                  HTTP.check_successful(status_code)
         
     | 
| 
      
 46 
     | 
    
         
            +
                  HTTP.check_successful(status_code: status_code)
         
     | 
| 
       47 
47 
     | 
    
         
             
                rescue => e
         
     | 
| 
       48 
48 
     | 
    
         
             
                  @logger.error { "Failed to dispatch events: #{e.inspect}" }
         
     | 
| 
       49 
49 
     | 
    
         
             
                end
         
     | 
| 
       50 
50 
     | 
    
         | 
| 
       51 
     | 
    
         
            -
                def create_payload(events)
         
     | 
| 
      
 51 
     | 
    
         
            +
                def create_payload(events:)
         
     | 
| 
       52 
52 
     | 
    
         
             
                  exposure_events = []
         
     | 
| 
       53 
53 
     | 
    
         
             
                  track_events = []
         
     | 
| 
       54 
54 
     | 
    
         
             
                  events.each do |event|
         
     | 
| 
       55 
55 
     | 
    
         
             
                    case event
         
     | 
| 
       56 
     | 
    
         
            -
                    when  
     | 
| 
      
 56 
     | 
    
         
            +
                    when UserEvent::Exposure
         
     | 
| 
       57 
57 
     | 
    
         
             
                      exposure_events << create_exposure_event(event)
         
     | 
| 
       58 
     | 
    
         
            -
                    when  
     | 
| 
      
 58 
     | 
    
         
            +
                    when UserEvent::Track
         
     | 
| 
       59 
59 
     | 
    
         
             
                      track_events << create_track_event(event)
         
     | 
| 
       60 
60 
     | 
    
         
             
                    end
         
     | 
| 
       61 
61 
     | 
    
         
             
                  end
         
     | 
| 
         @@ -65,24 +65,31 @@ module Hackle 
     | 
|
| 
       65 
65 
     | 
    
         
             
                  }
         
     | 
| 
       66 
66 
     | 
    
         
             
                end
         
     | 
| 
       67 
67 
     | 
    
         | 
| 
       68 
     | 
    
         
            -
                 
     | 
| 
      
 68 
     | 
    
         
            +
                #
         
     | 
| 
      
 69 
     | 
    
         
            +
                # @param exposure [UserEvent::Exposure]
         
     | 
| 
      
 70 
     | 
    
         
            +
                #
         
     | 
| 
      
 71 
     | 
    
         
            +
                def create_exposure_event(exposure)
         
     | 
| 
       69 
72 
     | 
    
         
             
                  {
         
     | 
| 
       70 
     | 
    
         
            -
                    timestamp:  
     | 
| 
       71 
     | 
    
         
            -
                    userId:  
     | 
| 
       72 
     | 
    
         
            -
                    experimentId:  
     | 
| 
       73 
     | 
    
         
            -
                    experimentKey:  
     | 
| 
       74 
     | 
    
         
            -
                    variationId:  
     | 
| 
       75 
     | 
    
         
            -
                    variationKey:  
     | 
| 
      
 73 
     | 
    
         
            +
                    timestamp: exposure.timestamp,
         
     | 
| 
      
 74 
     | 
    
         
            +
                    userId: exposure.user.id,
         
     | 
| 
      
 75 
     | 
    
         
            +
                    experimentId: exposure.experiment.id,
         
     | 
| 
      
 76 
     | 
    
         
            +
                    experimentKey: exposure.experiment.key,
         
     | 
| 
      
 77 
     | 
    
         
            +
                    variationId: exposure.variation.id,
         
     | 
| 
      
 78 
     | 
    
         
            +
                    variationKey: exposure.variation.key
         
     | 
| 
       76 
79 
     | 
    
         
             
                  }
         
     | 
| 
       77 
80 
     | 
    
         
             
                end
         
     | 
| 
       78 
81 
     | 
    
         | 
| 
       79 
     | 
    
         
            -
                 
     | 
| 
      
 82 
     | 
    
         
            +
                #
         
     | 
| 
      
 83 
     | 
    
         
            +
                # @param track [UserEvent::Track]
         
     | 
| 
      
 84 
     | 
    
         
            +
                #
         
     | 
| 
      
 85 
     | 
    
         
            +
                def create_track_event(track)
         
     | 
| 
       80 
86 
     | 
    
         
             
                  {
         
     | 
| 
       81 
     | 
    
         
            -
                    timestamp:  
     | 
| 
       82 
     | 
    
         
            -
                    userId:  
     | 
| 
       83 
     | 
    
         
            -
                    eventTypeId:  
     | 
| 
       84 
     | 
    
         
            -
                    eventTypeKey:  
     | 
| 
       85 
     | 
    
         
            -
                    value: event.value
         
     | 
| 
      
 87 
     | 
    
         
            +
                    timestamp: track.timestamp,
         
     | 
| 
      
 88 
     | 
    
         
            +
                    userId: track.user.id,
         
     | 
| 
      
 89 
     | 
    
         
            +
                    eventTypeId: track.event_type.id,
         
     | 
| 
      
 90 
     | 
    
         
            +
                    eventTypeKey: track.event_type.key,
         
     | 
| 
      
 91 
     | 
    
         
            +
                    value: track.event.value,
         
     | 
| 
      
 92 
     | 
    
         
            +
                    properties: track.event.properties
         
     | 
| 
       86 
93 
     | 
    
         
             
                  }
         
     | 
| 
       87 
94 
     | 
    
         
             
                end
         
     | 
| 
       88 
95 
     | 
    
         
             
              end
         
     |