amplitude-experiment 1.4.0 → 1.5.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/amplitude-experiment.gemspec +2 -1
 - data/lib/amplitude-experiment.rb +10 -1
 - data/lib/experiment/cohort/cohort.rb +25 -0
 - data/lib/experiment/cohort/cohort_download_api.rb +90 -0
 - data/lib/experiment/cohort/cohort_loader.rb +39 -0
 - data/lib/experiment/cohort/cohort_storage.rb +91 -0
 - data/lib/experiment/cohort/cohort_sync_config.rb +27 -0
 - data/lib/experiment/deployment/deployment_runner.rb +135 -0
 - data/lib/experiment/error.rb +23 -0
 - data/lib/experiment/{local/fetcher.rb → flag/flag_config_fetcher.rb} +7 -5
 - data/lib/experiment/flag/flag_config_storage.rb +53 -0
 - data/lib/experiment/local/client.rb +66 -26
 - data/lib/experiment/local/config.rb +26 -2
 - data/lib/experiment/user.rb +40 -20
 - data/lib/experiment/util/flag_config.rb +60 -0
 - data/lib/experiment/util/poller.rb +24 -0
 - data/lib/experiment/util/user.rb +20 -12
 - data/lib/experiment/version.rb +1 -1
 - metadata +28 -5
 
    
        checksums.yaml
    CHANGED
    
    | 
         @@ -1,7 +1,7 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            ---
         
     | 
| 
       2 
2 
     | 
    
         
             
            SHA256:
         
     | 
| 
       3 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       4 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 3 
     | 
    
         
            +
              metadata.gz: c79062c9e6a6f0877449f49dab03aa2445cf31700ecb92010f88d5779759d73a
         
     | 
| 
      
 4 
     | 
    
         
            +
              data.tar.gz: 1cf7d73b53cb125ecfaa6987bb4569d7416c5b02e194ef9f90be65c8da0b2fe6
         
     | 
| 
       5 
5 
     | 
    
         
             
            SHA512:
         
     | 
| 
       6 
     | 
    
         
            -
              metadata.gz:  
     | 
| 
       7 
     | 
    
         
            -
              data.tar.gz:  
     | 
| 
      
 6 
     | 
    
         
            +
              metadata.gz: 45e8a5540fc7a0e9a9213bdfe7c8828137d35aee0ab6d5dab50e79cac47bf858f11db626a545f131c7a5e48bf05b04b9b82944441d5c307d9562fd42973e5edb
         
     | 
| 
      
 7 
     | 
    
         
            +
              data.tar.gz: ffebca0974025ed2e983b106468be3806eb724cb5f0a2723d0f2782b67005d679ca8cfec4edb1b5236578a855472838a1f20e81d7641e362301d35fc5bfec32c
         
     | 
| 
         @@ -24,10 +24,11 @@ Gem::Specification.new do |spec| 
     | 
|
| 
       24 
24 
     | 
    
         
             
              spec.add_development_dependency 'rake', '~> 13.0'
         
     | 
| 
       25 
25 
     | 
    
         
             
              spec.add_development_dependency 'rdoc', '= 6.4'
         
     | 
| 
       26 
26 
     | 
    
         
             
              spec.add_development_dependency 'rspec', '~> 3.6'
         
     | 
| 
       27 
     | 
    
         
            -
              spec.add_development_dependency 'rubocop', '= 1. 
     | 
| 
      
 27 
     | 
    
         
            +
              spec.add_development_dependency 'rubocop', '= 1.22.3'
         
     | 
| 
       28 
28 
     | 
    
         
             
              spec.add_development_dependency 'simplecov', '~> 0.21'
         
     | 
| 
       29 
29 
     | 
    
         
             
              spec.add_development_dependency 'webmock', '~> 3.14'
         
     | 
| 
       30 
30 
     | 
    
         
             
              spec.add_development_dependency 'yard', '~> 0.9'
         
     | 
| 
      
 31 
     | 
    
         
            +
              spec.add_development_dependency 'dotenv', '~> 2.8.1'
         
     | 
| 
       31 
32 
     | 
    
         
             
              spec.metadata['rubygems_mfa_required'] = 'false'
         
     | 
| 
       32 
33 
     | 
    
         
             
              spec.add_runtime_dependency 'ffi', '~> 1.15'
         
     | 
| 
       33 
34 
     | 
    
         
             
            end
         
     | 
    
        data/lib/amplitude-experiment.rb
    CHANGED
    
    | 
         @@ -8,7 +8,6 @@ require 'experiment/factory' 
     | 
|
| 
       8 
8 
     | 
    
         
             
            require 'experiment/remote/client'
         
     | 
| 
       9 
9 
     | 
    
         
             
            require 'experiment/local/client'
         
     | 
| 
       10 
10 
     | 
    
         
             
            require 'experiment/local/config'
         
     | 
| 
       11 
     | 
    
         
            -
            require 'experiment/local/fetcher'
         
     | 
| 
       12 
11 
     | 
    
         
             
            require 'experiment/local/assignment/assignment'
         
     | 
| 
       13 
12 
     | 
    
         
             
            require 'experiment/local/assignment/assignment_filter'
         
     | 
| 
       14 
13 
     | 
    
         
             
            require 'experiment/local/assignment/assignment_service'
         
     | 
| 
         @@ -19,6 +18,16 @@ require 'experiment/util/topological_sort' 
     | 
|
| 
       19 
18 
     | 
    
         
             
            require 'experiment/util/user'
         
     | 
| 
       20 
19 
     | 
    
         
             
            require 'experiment/util/variant'
         
     | 
| 
       21 
20 
     | 
    
         
             
            require 'experiment/error'
         
     | 
| 
      
 21 
     | 
    
         
            +
            require 'experiment/util/flag_config'
         
     | 
| 
      
 22 
     | 
    
         
            +
            require 'experiment/flag/flag_config_fetcher'
         
     | 
| 
      
 23 
     | 
    
         
            +
            require 'experiment/flag/flag_config_storage'
         
     | 
| 
      
 24 
     | 
    
         
            +
            require 'experiment/cohort/cohort_download_api'
         
     | 
| 
      
 25 
     | 
    
         
            +
            require 'experiment/cohort/cohort'
         
     | 
| 
      
 26 
     | 
    
         
            +
            require 'experiment/cohort/cohort_loader'
         
     | 
| 
      
 27 
     | 
    
         
            +
            require 'experiment/cohort/cohort_storage'
         
     | 
| 
      
 28 
     | 
    
         
            +
            require 'experiment/cohort/cohort_sync_config'
         
     | 
| 
      
 29 
     | 
    
         
            +
            require 'experiment/deployment/deployment_runner'
         
     | 
| 
      
 30 
     | 
    
         
            +
            require 'experiment/util/poller'
         
     | 
| 
       22 
31 
     | 
    
         | 
| 
       23 
32 
     | 
    
         
             
            # Amplitude Experiment Module
         
     | 
| 
       24 
33 
     | 
    
         
             
            module AmplitudeExperiment
         
     | 
| 
         @@ -0,0 +1,25 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              USER_GROUP_TYPE = 'User'.freeze
         
     | 
| 
      
 3 
     | 
    
         
            +
              # Cohort
         
     | 
| 
      
 4 
     | 
    
         
            +
              class Cohort
         
     | 
| 
      
 5 
     | 
    
         
            +
                attr_accessor :id, :last_modified, :size, :member_ids, :group_type
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
                def initialize(id, last_modified, size, member_ids, group_type = USER_GROUP_TYPE)
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @id = id
         
     | 
| 
      
 9 
     | 
    
         
            +
                  @last_modified = last_modified
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @size = size
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @member_ids = member_ids.to_set
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @group_type = group_type
         
     | 
| 
      
 13 
     | 
    
         
            +
                end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def ==(other)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  return false unless other.is_a?(Cohort)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  @id == other.id &&
         
     | 
| 
      
 19 
     | 
    
         
            +
                    @last_modified == other.last_modified &&
         
     | 
| 
      
 20 
     | 
    
         
            +
                    @size == other.size &&
         
     | 
| 
      
 21 
     | 
    
         
            +
                    @member_ids == other.member_ids &&
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @group_type == other.group_type
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,90 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'base64'
         
     | 
| 
      
 2 
     | 
    
         
            +
            require 'json'
         
     | 
| 
      
 3 
     | 
    
         
            +
            require 'net/http'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'uri'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'set'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 8 
     | 
    
         
            +
              # CohortDownloadApi
         
     | 
| 
      
 9 
     | 
    
         
            +
              class CohortDownloadApi
         
     | 
| 
      
 10 
     | 
    
         
            +
                COHORT_REQUEST_TIMEOUT_MILLIS = 5000
         
     | 
| 
      
 11 
     | 
    
         
            +
                COHORT_REQUEST_RETRY_DELAY_MILLIS = 100
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                def get_cohort(cohort_id, cohort = nil)
         
     | 
| 
      
 14 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 15 
     | 
    
         
            +
                end
         
     | 
| 
      
 16 
     | 
    
         
            +
              end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
              # DirectCohortDownloadApi
         
     | 
| 
      
 19 
     | 
    
         
            +
              class DirectCohortDownloadApi < CohortDownloadApi
         
     | 
| 
      
 20 
     | 
    
         
            +
                def initialize(api_key, secret_key, max_cohort_size, server_url, logger)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  super()
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @api_key = api_key
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @secret_key = secret_key
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @max_cohort_size = max_cohort_size
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @server_url = server_url
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @logger = logger
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def get_cohort(cohort_id, cohort = nil)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @logger.debug("getCohortMembers(#{cohort_id}): start")
         
     | 
| 
      
 31 
     | 
    
         
            +
                  errors = 0
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  loop do
         
     | 
| 
      
 34 
     | 
    
         
            +
                    begin
         
     | 
| 
      
 35 
     | 
    
         
            +
                      last_modified = cohort.nil? ? nil : cohort.last_modified
         
     | 
| 
      
 36 
     | 
    
         
            +
                      response = get_cohort_members_request(cohort_id, last_modified)
         
     | 
| 
      
 37 
     | 
    
         
            +
                      @logger.debug("getCohortMembers(#{cohort_id}): status=#{response.code}")
         
     | 
| 
      
 38 
     | 
    
         
            +
             
     | 
| 
      
 39 
     | 
    
         
            +
                      case response.code.to_i
         
     | 
| 
      
 40 
     | 
    
         
            +
                      when 200
         
     | 
| 
      
 41 
     | 
    
         
            +
                        cohort_info = JSON.parse(response.body)
         
     | 
| 
      
 42 
     | 
    
         
            +
                        @logger.debug("getCohortMembers(#{cohort_id}): end - resultSize=#{cohort_info['size']}")
         
     | 
| 
      
 43 
     | 
    
         
            +
                        return Cohort.new(
         
     | 
| 
      
 44 
     | 
    
         
            +
                          cohort_info['cohortId'],
         
     | 
| 
      
 45 
     | 
    
         
            +
                          cohort_info['lastModified'],
         
     | 
| 
      
 46 
     | 
    
         
            +
                          cohort_info['size'],
         
     | 
| 
      
 47 
     | 
    
         
            +
                          cohort_info['memberIds'].to_set,
         
     | 
| 
      
 48 
     | 
    
         
            +
                          cohort_info['groupType']
         
     | 
| 
      
 49 
     | 
    
         
            +
                        )
         
     | 
| 
      
 50 
     | 
    
         
            +
                      when 204
         
     | 
| 
      
 51 
     | 
    
         
            +
                        @logger.debug("getCohortMembers(#{cohort_id}): Cohort not modified")
         
     | 
| 
      
 52 
     | 
    
         
            +
                        return nil
         
     | 
| 
      
 53 
     | 
    
         
            +
                      when 413
         
     | 
| 
      
 54 
     | 
    
         
            +
                        raise CohortTooLargeError.new(cohort_id, "Cohort exceeds max cohort size: #{response.code}")
         
     | 
| 
      
 55 
     | 
    
         
            +
                      else
         
     | 
| 
      
 56 
     | 
    
         
            +
                        raise HTTPErrorResponseError.new(response.code, cohort_id, "Unexpected response code: #{response.code}") if response.code.to_i != 202
         
     | 
| 
      
 57 
     | 
    
         
            +
             
     | 
| 
      
 58 
     | 
    
         
            +
                      end
         
     | 
| 
      
 59 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 60 
     | 
    
         
            +
                      errors += 1 unless response && e.is_a?(HTTPErrorResponseError) && response.code.to_i == 429
         
     | 
| 
      
 61 
     | 
    
         
            +
                      @logger.debug("getCohortMembers(#{cohort_id}): request-status error #{errors} - #{e}")
         
     | 
| 
      
 62 
     | 
    
         
            +
                      raise e if errors >= 3 || e.is_a?(CohortTooLargeError)
         
     | 
| 
      
 63 
     | 
    
         
            +
                    end
         
     | 
| 
      
 64 
     | 
    
         
            +
             
     | 
| 
      
 65 
     | 
    
         
            +
                    sleep(COHORT_REQUEST_RETRY_DELAY_MILLIS / 1000.0)
         
     | 
| 
      
 66 
     | 
    
         
            +
                  end
         
     | 
| 
      
 67 
     | 
    
         
            +
                end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                private
         
     | 
| 
      
 70 
     | 
    
         
            +
             
     | 
| 
      
 71 
     | 
    
         
            +
                def get_cohort_members_request(cohort_id, last_modified)
         
     | 
| 
      
 72 
     | 
    
         
            +
                  headers = {
         
     | 
| 
      
 73 
     | 
    
         
            +
                    'Authorization' => "Basic #{basic_auth}",
         
     | 
| 
      
 74 
     | 
    
         
            +
                    'Content-Type' => 'application/json;charset=utf-8',
         
     | 
| 
      
 75 
     | 
    
         
            +
                    'X-Amp-Exp-Library' => "experiment-ruby-server/#{VERSION}"
         
     | 
| 
      
 76 
     | 
    
         
            +
                  }
         
     | 
| 
      
 77 
     | 
    
         
            +
                  url = "#{@server_url}/sdk/v1/cohort/#{cohort_id}?maxCohortSize=#{@max_cohort_size}"
         
     | 
| 
      
 78 
     | 
    
         
            +
                  url += "&lastModified=#{last_modified}" if last_modified
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                  request = Net::HTTP::Get.new(URI(url), headers)
         
     | 
| 
      
 81 
     | 
    
         
            +
                  http = PersistentHttpClient.get(@server_url, { read_timeout: COHORT_REQUEST_TIMEOUT_MILLIS }, basic_auth)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  http.request(request)
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                def basic_auth
         
     | 
| 
      
 86 
     | 
    
         
            +
                  credentials = "#{@api_key}:#{@secret_key}"
         
     | 
| 
      
 87 
     | 
    
         
            +
                  Base64.strict_encode64(credentials)
         
     | 
| 
      
 88 
     | 
    
         
            +
                end
         
     | 
| 
      
 89 
     | 
    
         
            +
              end
         
     | 
| 
      
 90 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              # CohortLoader
         
     | 
| 
      
 3 
     | 
    
         
            +
              class CohortLoader
         
     | 
| 
      
 4 
     | 
    
         
            +
                def initialize(cohort_download_api, cohort_storage)
         
     | 
| 
      
 5 
     | 
    
         
            +
                  @cohort_download_api = cohort_download_api
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @cohort_storage = cohort_storage
         
     | 
| 
      
 7 
     | 
    
         
            +
                  @jobs = {}
         
     | 
| 
      
 8 
     | 
    
         
            +
                  @lock_jobs = Mutex.new
         
     | 
| 
      
 9 
     | 
    
         
            +
                end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                def load_cohort(cohort_id)
         
     | 
| 
      
 12 
     | 
    
         
            +
                  @lock_jobs.synchronize do
         
     | 
| 
      
 13 
     | 
    
         
            +
                    unless @jobs.key?(cohort_id)
         
     | 
| 
      
 14 
     | 
    
         
            +
                      future = Concurrent::Promises.future do
         
     | 
| 
      
 15 
     | 
    
         
            +
                        load_cohort_internal(cohort_id)
         
     | 
| 
      
 16 
     | 
    
         
            +
                      ensure
         
     | 
| 
      
 17 
     | 
    
         
            +
                        remove_job(cohort_id)
         
     | 
| 
      
 18 
     | 
    
         
            +
                      end
         
     | 
| 
      
 19 
     | 
    
         
            +
                      @jobs[cohort_id] = future
         
     | 
| 
      
 20 
     | 
    
         
            +
                    end
         
     | 
| 
      
 21 
     | 
    
         
            +
                    @jobs[cohort_id]
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                private
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def load_cohort_internal(cohort_id)
         
     | 
| 
      
 28 
     | 
    
         
            +
                  stored_cohort = @cohort_storage.cohort(cohort_id)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  updated_cohort = @cohort_download_api.get_cohort(cohort_id, stored_cohort)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @cohort_storage.put_cohort(updated_cohort) unless updated_cohort.nil?
         
     | 
| 
      
 31 
     | 
    
         
            +
                end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                def remove_job(cohort_id)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  @lock_jobs.synchronize do
         
     | 
| 
      
 35 
     | 
    
         
            +
                    @jobs.delete(cohort_id)
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,91 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              # CohortStorage
         
     | 
| 
      
 3 
     | 
    
         
            +
              class CohortStorage
         
     | 
| 
      
 4 
     | 
    
         
            +
                def cohort(cohort_id)
         
     | 
| 
      
 5 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 6 
     | 
    
         
            +
                end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def cohorts
         
     | 
| 
      
 9 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def get_cohorts_for_user(user_id, cohort_ids)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def get_cohorts_for_group(group_type, group_name, cohort_ids)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
             
     | 
| 
      
 20 
     | 
    
         
            +
                def put_cohort(cohort_description)
         
     | 
| 
      
 21 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                def delete_cohort(group_type, cohort_id)
         
     | 
| 
      
 25 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def cohort_ids
         
     | 
| 
      
 29 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 30 
     | 
    
         
            +
                end
         
     | 
| 
      
 31 
     | 
    
         
            +
              end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
              class InMemoryCohortStorage < CohortStorage
         
     | 
| 
      
 34 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 35 
     | 
    
         
            +
                  super
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @lock = Mutex.new
         
     | 
| 
      
 37 
     | 
    
         
            +
                  @group_to_cohort_store = {}
         
     | 
| 
      
 38 
     | 
    
         
            +
                  @cohort_store = {}
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def cohort(cohort_id)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 43 
     | 
    
         
            +
                    @cohort_store[cohort_id]
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                def cohorts
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 49 
     | 
    
         
            +
                    @cohort_store.dup
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
             
     | 
| 
      
 53 
     | 
    
         
            +
                def get_cohorts_for_user(user_id, cohort_ids)
         
     | 
| 
      
 54 
     | 
    
         
            +
                  get_cohorts_for_group(USER_GROUP_TYPE, user_id, cohort_ids)
         
     | 
| 
      
 55 
     | 
    
         
            +
                end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
                def get_cohorts_for_group(group_type, group_name, cohort_ids)
         
     | 
| 
      
 58 
     | 
    
         
            +
                  result = Set.new
         
     | 
| 
      
 59 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 60 
     | 
    
         
            +
                    group_type_cohorts = @group_to_cohort_store[group_type] || Set.new
         
     | 
| 
      
 61 
     | 
    
         
            +
                    group_type_cohorts.each do |cohort_id|
         
     | 
| 
      
 62 
     | 
    
         
            +
                      members = @cohort_store[cohort_id]&.member_ids || Set.new
         
     | 
| 
      
 63 
     | 
    
         
            +
                      result.add(cohort_id) if cohort_ids.include?(cohort_id) && members.include?(group_name)
         
     | 
| 
      
 64 
     | 
    
         
            +
                    end
         
     | 
| 
      
 65 
     | 
    
         
            +
                  end
         
     | 
| 
      
 66 
     | 
    
         
            +
                  result
         
     | 
| 
      
 67 
     | 
    
         
            +
                end
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                def put_cohort(cohort)
         
     | 
| 
      
 70 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 71 
     | 
    
         
            +
                    @group_to_cohort_store[cohort.group_type] ||= Set.new
         
     | 
| 
      
 72 
     | 
    
         
            +
                    @group_to_cohort_store[cohort.group_type].add(cohort.id)
         
     | 
| 
      
 73 
     | 
    
         
            +
                    @cohort_store[cohort.id] = cohort
         
     | 
| 
      
 74 
     | 
    
         
            +
                  end
         
     | 
| 
      
 75 
     | 
    
         
            +
                end
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                def delete_cohort(group_type, cohort_id)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 79 
     | 
    
         
            +
                    group_cohorts = @group_to_cohort_store[group_type] || Set.new
         
     | 
| 
      
 80 
     | 
    
         
            +
                    group_cohorts.delete(cohort_id)
         
     | 
| 
      
 81 
     | 
    
         
            +
                    @cohort_store.delete(cohort_id)
         
     | 
| 
      
 82 
     | 
    
         
            +
                  end
         
     | 
| 
      
 83 
     | 
    
         
            +
                end
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                def cohort_ids
         
     | 
| 
      
 86 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 87 
     | 
    
         
            +
                    @cohort_store.keys.to_set
         
     | 
| 
      
 88 
     | 
    
         
            +
                  end
         
     | 
| 
      
 89 
     | 
    
         
            +
                end
         
     | 
| 
      
 90 
     | 
    
         
            +
              end
         
     | 
| 
      
 91 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,27 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              DEFAULT_COHORT_SYNC_URL = 'https://cohort-v2.lab.amplitude.com'.freeze
         
     | 
| 
      
 3 
     | 
    
         
            +
              EU_COHORT_SYNC_URL = 'https://cohort-v2.lab.eu.amplitude.com'.freeze
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
              # Experiment Cohort Sync Configuration
         
     | 
| 
      
 6 
     | 
    
         
            +
              class CohortSyncConfig
         
     | 
| 
      
 7 
     | 
    
         
            +
                # This configuration is used to set up the cohort loader. The cohort loader is responsible for
         
     | 
| 
      
 8 
     | 
    
         
            +
                # downloading cohorts from the server and storing them locally.
         
     | 
| 
      
 9 
     | 
    
         
            +
                #   Parameters:
         
     | 
| 
      
 10 
     | 
    
         
            +
                #     api_key (str): The project API Key
         
     | 
| 
      
 11 
     | 
    
         
            +
                #     secret_key (str): The project Secret Key
         
     | 
| 
      
 12 
     | 
    
         
            +
                #     max_cohort_size (int): The maximum cohort size that can be downloaded
         
     | 
| 
      
 13 
     | 
    
         
            +
                #     cohort_polling_interval_millis (int): The interval in milliseconds to poll for cohorts, the minimum value is 60000
         
     | 
| 
      
 14 
     | 
    
         
            +
                #     cohort_server_url (str): The server endpoint from which to request cohorts
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                attr_accessor :api_key, :secret_key, :max_cohort_size, :cohort_polling_interval_millis, :cohort_server_url
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                def initialize(api_key, secret_key, max_cohort_size: 2_147_483_647, cohort_polling_interval_millis: 60_000,
         
     | 
| 
      
 19 
     | 
    
         
            +
                               cohort_server_url: DEFAULT_COHORT_SYNC_URL)
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @api_key = api_key
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @secret_key = secret_key
         
     | 
| 
      
 22 
     | 
    
         
            +
                  @max_cohort_size = max_cohort_size
         
     | 
| 
      
 23 
     | 
    
         
            +
                  @cohort_polling_interval_millis = [cohort_polling_interval_millis, 60_000].max
         
     | 
| 
      
 24 
     | 
    
         
            +
                  @cohort_server_url = cohort_server_url
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
              end
         
     | 
| 
      
 27 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,135 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            require 'set'
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 4 
     | 
    
         
            +
              # DeploymentRunner
         
     | 
| 
      
 5 
     | 
    
         
            +
              class DeploymentRunner
         
     | 
| 
      
 6 
     | 
    
         
            +
                def initialize(
         
     | 
| 
      
 7 
     | 
    
         
            +
                  config,
         
     | 
| 
      
 8 
     | 
    
         
            +
                  flag_config_fetcher,
         
     | 
| 
      
 9 
     | 
    
         
            +
                  flag_config_storage,
         
     | 
| 
      
 10 
     | 
    
         
            +
                  cohort_storage,
         
     | 
| 
      
 11 
     | 
    
         
            +
                  logger,
         
     | 
| 
      
 12 
     | 
    
         
            +
                  cohort_loader = nil
         
     | 
| 
      
 13 
     | 
    
         
            +
                )
         
     | 
| 
      
 14 
     | 
    
         
            +
                  @config = config
         
     | 
| 
      
 15 
     | 
    
         
            +
                  @flag_config_fetcher = flag_config_fetcher
         
     | 
| 
      
 16 
     | 
    
         
            +
                  @flag_config_storage = flag_config_storage
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @cohort_storage = cohort_storage
         
     | 
| 
      
 18 
     | 
    
         
            +
                  @cohort_loader = cohort_loader
         
     | 
| 
      
 19 
     | 
    
         
            +
                  @lock = Mutex.new
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @logger = logger
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @executor = Concurrent::ThreadPoolExecutor.new(
         
     | 
| 
      
 22 
     | 
    
         
            +
                    max_threads: 10,
         
     | 
| 
      
 23 
     | 
    
         
            +
                    name: 'DeploymentRunnerExecutor'
         
     | 
| 
      
 24 
     | 
    
         
            +
                  )
         
     | 
| 
      
 25 
     | 
    
         
            +
                end
         
     | 
| 
      
 26 
     | 
    
         
            +
             
     | 
| 
      
 27 
     | 
    
         
            +
                def start
         
     | 
| 
      
 28 
     | 
    
         
            +
                  @lock.synchronize do
         
     | 
| 
      
 29 
     | 
    
         
            +
                    update_flag_configs
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @flag_poller = Poller.new(
         
     | 
| 
      
 31 
     | 
    
         
            +
                      @config.flag_config_polling_interval_millis / 1000.0,
         
     | 
| 
      
 32 
     | 
    
         
            +
                      method(:periodic_flag_update)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    )
         
     | 
| 
      
 34 
     | 
    
         
            +
                    @flag_poller.start
         
     | 
| 
      
 35 
     | 
    
         
            +
                    if @config.cohort_sync_config
         
     | 
| 
      
 36 
     | 
    
         
            +
                      @cohort_poller = Poller.new(
         
     | 
| 
      
 37 
     | 
    
         
            +
                        @config.cohort_sync_config.cohort_polling_interval_millis / 1000.0,
         
     | 
| 
      
 38 
     | 
    
         
            +
                        method(:update_cohorts)
         
     | 
| 
      
 39 
     | 
    
         
            +
                      )
         
     | 
| 
      
 40 
     | 
    
         
            +
                      @cohort_poller.start
         
     | 
| 
      
 41 
     | 
    
         
            +
                    end
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                def stop
         
     | 
| 
      
 46 
     | 
    
         
            +
                  @flag_poller&.stop
         
     | 
| 
      
 47 
     | 
    
         
            +
                  @flag_poller = nil
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @cohort_poller&.stop
         
     | 
| 
      
 49 
     | 
    
         
            +
                  @cohort_poller = nil
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                private
         
     | 
| 
      
 53 
     | 
    
         
            +
             
     | 
| 
      
 54 
     | 
    
         
            +
                def periodic_flag_update
         
     | 
| 
      
 55 
     | 
    
         
            +
                  @logger.debug('Periodic flag update: start')
         
     | 
| 
      
 56 
     | 
    
         
            +
                  update_flag_configs
         
     | 
| 
      
 57 
     | 
    
         
            +
                rescue StandardError => e
         
     | 
| 
      
 58 
     | 
    
         
            +
                  @logger.error("Error while updating flags: #{e}")
         
     | 
| 
      
 59 
     | 
    
         
            +
                end
         
     | 
| 
      
 60 
     | 
    
         
            +
             
     | 
| 
      
 61 
     | 
    
         
            +
                def update_flag_configs
         
     | 
| 
      
 62 
     | 
    
         
            +
                  flags = @flag_config_fetcher.fetch_v2
         
     | 
| 
      
 63 
     | 
    
         
            +
                  flag_configs = flags.each_with_object({}) { |flag, hash| hash[flag['key']] = flag }
         
     | 
| 
      
 64 
     | 
    
         
            +
                  flag_keys = flag_configs.values.map { |flag| flag['key'] }.to_set
         
     | 
| 
      
 65 
     | 
    
         
            +
                  @flag_config_storage.remove_if { |f| !flag_keys.include?(f['key']) }
         
     | 
| 
      
 66 
     | 
    
         
            +
             
     | 
| 
      
 67 
     | 
    
         
            +
                  unless @cohort_loader
         
     | 
| 
      
 68 
     | 
    
         
            +
                    flag_configs.each do |flag_key, flag_config|
         
     | 
| 
      
 69 
     | 
    
         
            +
                      @logger.debug("Putting non-cohort flag #{flag_key}")
         
     | 
| 
      
 70 
     | 
    
         
            +
                      @flag_config_storage.put_flag_config(flag_config)
         
     | 
| 
      
 71 
     | 
    
         
            +
                    end
         
     | 
| 
      
 72 
     | 
    
         
            +
                    return
         
     | 
| 
      
 73 
     | 
    
         
            +
                  end
         
     | 
| 
      
 74 
     | 
    
         
            +
             
     | 
| 
      
 75 
     | 
    
         
            +
                  new_cohort_ids = Set.new
         
     | 
| 
      
 76 
     | 
    
         
            +
                  flag_configs.each do |_, flag_config|
         
     | 
| 
      
 77 
     | 
    
         
            +
                    new_cohort_ids.merge(AmplitudeExperiment.get_all_cohort_ids_from_flag(flag_config))
         
     | 
| 
      
 78 
     | 
    
         
            +
                  end
         
     | 
| 
      
 79 
     | 
    
         
            +
             
     | 
| 
      
 80 
     | 
    
         
            +
                  existing_cohort_ids = @cohort_storage.cohort_ids
         
     | 
| 
      
 81 
     | 
    
         
            +
                  cohort_ids_to_download = new_cohort_ids - existing_cohort_ids
         
     | 
| 
      
 82 
     | 
    
         
            +
             
     | 
| 
      
 83 
     | 
    
         
            +
                  download_cohorts(cohort_ids_to_download)
         
     | 
| 
      
 84 
     | 
    
         
            +
             
     | 
| 
      
 85 
     | 
    
         
            +
                  updated_cohort_ids = @cohort_storage.cohort_ids
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                  flag_configs.each do |flag_key, flag_config|
         
     | 
| 
      
 88 
     | 
    
         
            +
                    cohort_ids = AmplitudeExperiment.get_all_cohort_ids_from_flag(flag_config)
         
     | 
| 
      
 89 
     | 
    
         
            +
                    @logger.debug("Storing flag #{flag_key}")
         
     | 
| 
      
 90 
     | 
    
         
            +
                    @flag_config_storage.put_flag_config(flag_config)
         
     | 
| 
      
 91 
     | 
    
         
            +
                    missing_cohorts = cohort_ids - updated_cohort_ids
         
     | 
| 
      
 92 
     | 
    
         
            +
             
     | 
| 
      
 93 
     | 
    
         
            +
                    @logger.warn("Flag #{flag_key} - failed to load cohorts: #{missing_cohorts}") if missing_cohorts.any?
         
     | 
| 
      
 94 
     | 
    
         
            +
                  end
         
     | 
| 
      
 95 
     | 
    
         
            +
             
     | 
| 
      
 96 
     | 
    
         
            +
                  delete_unused_cohorts
         
     | 
| 
      
 97 
     | 
    
         
            +
                  @logger.debug("Refreshed #{flag_configs.size} flag configs.")
         
     | 
| 
      
 98 
     | 
    
         
            +
                end
         
     | 
| 
      
 99 
     | 
    
         
            +
             
     | 
| 
      
 100 
     | 
    
         
            +
                def download_cohorts(cohort_ids)
         
     | 
| 
      
 101 
     | 
    
         
            +
                  futures = cohort_ids.map do |cohort_id|
         
     | 
| 
      
 102 
     | 
    
         
            +
                    Concurrent::Promises.future_on(@executor) do
         
     | 
| 
      
 103 
     | 
    
         
            +
                      future = @cohort_loader.load_cohort(cohort_id)
         
     | 
| 
      
 104 
     | 
    
         
            +
                      future.value!
         
     | 
| 
      
 105 
     | 
    
         
            +
                    rescue StandardError => e
         
     | 
| 
      
 106 
     | 
    
         
            +
                      @logger.error("Failed to download cohort #{cohort_id}: #{e.message}")
         
     | 
| 
      
 107 
     | 
    
         
            +
                      nil
         
     | 
| 
      
 108 
     | 
    
         
            +
                    end
         
     | 
| 
      
 109 
     | 
    
         
            +
                  end
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                  Concurrent::Promises.zip(*futures).value!
         
     | 
| 
      
 112 
     | 
    
         
            +
                end
         
     | 
| 
      
 113 
     | 
    
         
            +
             
     | 
| 
      
 114 
     | 
    
         
            +
                def update_cohorts
         
     | 
| 
      
 115 
     | 
    
         
            +
                  @logger.debug('Periodic cohort update: start')
         
     | 
| 
      
 116 
     | 
    
         
            +
                  cohort_ids = AmplitudeExperiment.get_all_cohort_ids_from_flags(@flag_config_storage.flag_configs)
         
     | 
| 
      
 117 
     | 
    
         
            +
                  download_cohorts(cohort_ids)
         
     | 
| 
      
 118 
     | 
    
         
            +
                end
         
     | 
| 
      
 119 
     | 
    
         
            +
             
     | 
| 
      
 120 
     | 
    
         
            +
                def delete_unused_cohorts
         
     | 
| 
      
 121 
     | 
    
         
            +
                  flag_cohort_ids = Set.new
         
     | 
| 
      
 122 
     | 
    
         
            +
                  @flag_config_storage.flag_configs.each do |_, flag|
         
     | 
| 
      
 123 
     | 
    
         
            +
                    flag_cohort_ids.merge(AmplitudeExperiment.get_all_cohort_ids_from_flag(flag))
         
     | 
| 
      
 124 
     | 
    
         
            +
                  end
         
     | 
| 
      
 125 
     | 
    
         
            +
             
     | 
| 
      
 126 
     | 
    
         
            +
                  storage_cohorts = @cohort_storage.cohorts
         
     | 
| 
      
 127 
     | 
    
         
            +
                  deleted_cohort_ids = storage_cohorts.keys.to_set - flag_cohort_ids
         
     | 
| 
      
 128 
     | 
    
         
            +
             
     | 
| 
      
 129 
     | 
    
         
            +
                  deleted_cohort_ids.each do |deleted_cohort_id|
         
     | 
| 
      
 130 
     | 
    
         
            +
                    deleted_cohort = storage_cohorts[deleted_cohort_id]
         
     | 
| 
      
 131 
     | 
    
         
            +
                    @cohort_storage.delete_cohort(deleted_cohort.group_type, deleted_cohort_id) if deleted_cohort
         
     | 
| 
      
 132 
     | 
    
         
            +
                  end
         
     | 
| 
      
 133 
     | 
    
         
            +
                end
         
     | 
| 
      
 134 
     | 
    
         
            +
              end
         
     | 
| 
      
 135 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/experiment/error.rb
    CHANGED
    
    | 
         @@ -9,6 +9,29 @@ module AmplitudeExperiment 
     | 
|
| 
       9 
9 
     | 
    
         
             
                end
         
     | 
| 
       10 
10 
     | 
    
         
             
              end
         
     | 
| 
       11 
11 
     | 
    
         | 
| 
      
 12 
     | 
    
         
            +
              class CohortDownloadError < StandardError
         
     | 
| 
      
 13 
     | 
    
         
            +
                attr_reader :cohort_id
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                def initialize(cohort_id, message)
         
     | 
| 
      
 16 
     | 
    
         
            +
                  super(message)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  @cohort_id = cohort_id
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              # CohortTooLargeError
         
     | 
| 
      
 22 
     | 
    
         
            +
              class CohortTooLargeError < CohortDownloadError
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
              # HTTPErrorResponseError
         
     | 
| 
      
 26 
     | 
    
         
            +
              class HTTPErrorResponseError < CohortDownloadError
         
     | 
| 
      
 27 
     | 
    
         
            +
                attr_reader :status_code
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def initialize(status_code, cohort_id, message)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  super(cohort_id, message)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  @status_code = status_code
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
       12 
35 
     | 
    
         
             
              class CycleError < StandardError
         
     | 
| 
       13 
36 
     | 
    
         
             
                # Raised when topological sorting encounters a cycle between flag dependencies.
         
     | 
| 
       14 
37 
     | 
    
         
             
                attr_reader :path
         
     | 
| 
         @@ -9,7 +9,6 @@ module AmplitudeExperiment 
     | 
|
| 
       9 
9 
     | 
    
         
             
                  @api_key = api_key
         
     | 
| 
       10 
10 
     | 
    
         
             
                  @server_url = server_url
         
     | 
| 
       11 
11 
     | 
    
         
             
                  @logger = logger
         
     | 
| 
       12 
     | 
    
         
            -
                  @http = PersistentHttpClient.get(server_url, { read_timeout: FLAG_CONFIG_TIMEOUT }, @api_key)
         
     | 
| 
       13 
12 
     | 
    
         
             
                end
         
     | 
| 
       14 
13 
     | 
    
         | 
| 
       15 
14 
     | 
    
         
             
                # Fetch local evaluation mode flag configs from the Experiment API server.
         
     | 
| 
         @@ -24,7 +23,8 @@ module AmplitudeExperiment 
     | 
|
| 
       24 
23 
     | 
    
         
             
                    'X-Amp-Exp-Library' => "experiment-ruby-server/#{VERSION}"
         
     | 
| 
       25 
24 
     | 
    
         
             
                  }
         
     | 
| 
       26 
25 
     | 
    
         
             
                  request = Net::HTTP::Get.new("#{@server_url}/sdk/v1/flags", headers)
         
     | 
| 
       27 
     | 
    
         
            -
                   
     | 
| 
      
 26 
     | 
    
         
            +
                  http = PersistentHttpClient.get(@server_url, { read_timeout: FLAG_CONFIG_TIMEOUT }, @api_key)
         
     | 
| 
      
 27 
     | 
    
         
            +
                  response = http.request(request)
         
     | 
| 
       28 
28 
     | 
    
         
             
                  raise "flagConfigs - received error response: #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPOK)
         
     | 
| 
       29 
29 
     | 
    
         | 
| 
       30 
30 
     | 
    
         
             
                  @logger.debug("[Experiment] Fetch flag configs: #{response.body}")
         
     | 
| 
         @@ -39,11 +39,12 @@ module AmplitudeExperiment 
     | 
|
| 
       39 
39 
     | 
    
         
             
                    'X-Amp-Exp-Library' => "experiment-ruby-server/#{VERSION}"
         
     | 
| 
       40 
40 
     | 
    
         
             
                  }
         
     | 
| 
       41 
41 
     | 
    
         
             
                  request = Net::HTTP::Get.new("#{@server_url}/sdk/v2/flags?v=0", headers)
         
     | 
| 
       42 
     | 
    
         
            -
                   
     | 
| 
      
 42 
     | 
    
         
            +
                  http = PersistentHttpClient.get(@server_url, { read_timeout: FLAG_CONFIG_TIMEOUT }, @api_key)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  response = http.request(request)
         
     | 
| 
       43 
44 
     | 
    
         
             
                  raise "flagConfigs - received error response: #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPOK)
         
     | 
| 
       44 
45 
     | 
    
         | 
| 
       45 
46 
     | 
    
         
             
                  @logger.debug("[Experiment] Fetch flag configs: #{response.body}")
         
     | 
| 
       46 
     | 
    
         
            -
                  response.body
         
     | 
| 
      
 47 
     | 
    
         
            +
                  JSON.parse(response.body)
         
     | 
| 
       47 
48 
     | 
    
         
             
                end
         
     | 
| 
       48 
49 
     | 
    
         | 
| 
       49 
50 
     | 
    
         
             
                # Fetch local evaluation mode flag configs from the Experiment API server.
         
     | 
| 
         @@ -58,7 +59,8 @@ module AmplitudeExperiment 
     | 
|
| 
       58 
59 
     | 
    
         
             
                    'X-Amp-Exp-Library' => "experiment-ruby-server/#{VERSION}"
         
     | 
| 
       59 
60 
     | 
    
         
             
                  }
         
     | 
| 
       60 
61 
     | 
    
         
             
                  request = Net::HTTP::Get.new("#{@server_url}/sdk/rules?eval_mode=local", headers)
         
     | 
| 
       61 
     | 
    
         
            -
                   
     | 
| 
      
 62 
     | 
    
         
            +
                  http = PersistentHttpClient.get(@server_url, { read_timeout: FLAG_CONFIG_TIMEOUT }, @api_key)
         
     | 
| 
      
 63 
     | 
    
         
            +
                  response = http.request(request)
         
     | 
| 
       62 
64 
     | 
    
         
             
                  raise "flagConfigs - received error response: #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPOK)
         
     | 
| 
       63 
65 
     | 
    
         | 
| 
       64 
66 
     | 
    
         
             
                  flag_configs = parse(response.body)
         
     | 
| 
         @@ -0,0 +1,53 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              # FlagConfigStorage
         
     | 
| 
      
 3 
     | 
    
         
            +
              class FlagConfigStorage
         
     | 
| 
      
 4 
     | 
    
         
            +
                def flag_config(key)
         
     | 
| 
      
 5 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 6 
     | 
    
         
            +
                end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                def flag_configs
         
     | 
| 
      
 9 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 10 
     | 
    
         
            +
                end
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                def put_flag_config(flag_config)
         
     | 
| 
      
 13 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 14 
     | 
    
         
            +
                end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                def remove_if(&condition)
         
     | 
| 
      
 17 
     | 
    
         
            +
                  raise NotImplementedError
         
     | 
| 
      
 18 
     | 
    
         
            +
                end
         
     | 
| 
      
 19 
     | 
    
         
            +
              end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
              # InMemoryFlagConfigStorage
         
     | 
| 
      
 22 
     | 
    
         
            +
              class InMemoryFlagConfigStorage < FlagConfigStorage
         
     | 
| 
      
 23 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 24 
     | 
    
         
            +
                  super # Call the parent class's constructor with no arguments
         
     | 
| 
      
 25 
     | 
    
         
            +
                  @flag_configs = {}
         
     | 
| 
      
 26 
     | 
    
         
            +
                  @flag_configs_lock = Mutex.new
         
     | 
| 
      
 27 
     | 
    
         
            +
                end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                def flag_config(key)
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @flag_configs_lock.synchronize do
         
     | 
| 
      
 31 
     | 
    
         
            +
                    @flag_configs[key]
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                def flag_configs
         
     | 
| 
      
 36 
     | 
    
         
            +
                  @flag_configs_lock.synchronize do
         
     | 
| 
      
 37 
     | 
    
         
            +
                    @flag_configs.dup
         
     | 
| 
      
 38 
     | 
    
         
            +
                  end
         
     | 
| 
      
 39 
     | 
    
         
            +
                end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                def put_flag_config(flag_config)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  @flag_configs_lock.synchronize do
         
     | 
| 
      
 43 
     | 
    
         
            +
                    @flag_configs[flag_config['key']] = flag_config
         
     | 
| 
      
 44 
     | 
    
         
            +
                  end
         
     | 
| 
      
 45 
     | 
    
         
            +
                end
         
     | 
| 
      
 46 
     | 
    
         
            +
             
     | 
| 
      
 47 
     | 
    
         
            +
                def remove_if
         
     | 
| 
      
 48 
     | 
    
         
            +
                  @flag_configs_lock.synchronize do
         
     | 
| 
      
 49 
     | 
    
         
            +
                    @flag_configs.delete_if { |_key, value| yield(value) }
         
     | 
| 
      
 50 
     | 
    
         
            +
                  end
         
     | 
| 
      
 51 
     | 
    
         
            +
                end
         
     | 
| 
      
 52 
     | 
    
         
            +
              end
         
     | 
| 
      
 53 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -23,11 +23,24 @@ module AmplitudeExperiment 
     | 
|
| 
       23 
23 
     | 
    
         
             
                                  else
         
     | 
| 
       24 
24 
     | 
    
         
             
                                    Logger::INFO
         
     | 
| 
       25 
25 
     | 
    
         
             
                                  end
         
     | 
| 
       26 
     | 
    
         
            -
                  @fetcher = LocalEvaluationFetcher.new(api_key, @logger, @config.server_url)
         
     | 
| 
       27 
26 
     | 
    
         
             
                  raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
         
     | 
| 
       28 
27 
     | 
    
         | 
| 
       29 
28 
     | 
    
         
             
                  @assignment_service = nil
         
     | 
| 
       30 
29 
     | 
    
         
             
                  @assignment_service = AssignmentService.new(AmplitudeAnalytics::Amplitude.new(config.assignment_config.api_key, configuration: config.assignment_config), AssignmentFilter.new(config.assignment_config.cache_capacity)) if config&.assignment_config
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                  @cohort_storage = InMemoryCohortStorage.new
         
     | 
| 
      
 32 
     | 
    
         
            +
                  @flag_config_storage = InMemoryFlagConfigStorage.new
         
     | 
| 
      
 33 
     | 
    
         
            +
                  @flag_config_fetcher = LocalEvaluationFetcher.new(@api_key, @logger, @config.server_url)
         
     | 
| 
      
 34 
     | 
    
         
            +
                  @cohort_loader = nil
         
     | 
| 
      
 35 
     | 
    
         
            +
                  unless @config.cohort_sync_config.nil?
         
     | 
| 
      
 36 
     | 
    
         
            +
                    @cohort_download_api = DirectCohortDownloadApi.new(@config.cohort_sync_config.api_key,
         
     | 
| 
      
 37 
     | 
    
         
            +
                                                                       @config.cohort_sync_config.secret_key,
         
     | 
| 
      
 38 
     | 
    
         
            +
                                                                       @config.cohort_sync_config.max_cohort_size,
         
     | 
| 
      
 39 
     | 
    
         
            +
                                                                       @config.cohort_sync_config.cohort_server_url,
         
     | 
| 
      
 40 
     | 
    
         
            +
                                                                       @logger)
         
     | 
| 
      
 41 
     | 
    
         
            +
                    @cohort_loader = CohortLoader.new(@cohort_download_api, @cohort_storage)
         
     | 
| 
      
 42 
     | 
    
         
            +
                  end
         
     | 
| 
      
 43 
     | 
    
         
            +
                  @deployment_runner = DeploymentRunner.new(@config, @flag_config_fetcher, @flag_config_storage, @cohort_storage, @logger, @cohort_loader)
         
     | 
| 
       31 
44 
     | 
    
         
             
                end
         
     | 
| 
       32 
45 
     | 
    
         | 
| 
       33 
46 
     | 
    
         
             
                # Locally evaluates flag variants for a user.
         
     | 
| 
         @@ -51,19 +64,18 @@ module AmplitudeExperiment 
     | 
|
| 
       51 
64 
     | 
    
         
             
                # @param [String[]] flag_keys The flags to evaluate with the user, if empty all flags are evaluated
         
     | 
| 
       52 
65 
     | 
    
         
             
                # @return [Hash[String, Variant]] The evaluated variants
         
     | 
| 
       53 
66 
     | 
    
         
             
                def evaluate_v2(user, flag_keys = [])
         
     | 
| 
       54 
     | 
    
         
            -
                  flags = @ 
     | 
| 
       55 
     | 
    
         
            -
                    @flags
         
     | 
| 
       56 
     | 
    
         
            -
                  end
         
     | 
| 
      
 67 
     | 
    
         
            +
                  flags = @flag_config_storage.flag_configs
         
     | 
| 
       57 
68 
     | 
    
         
             
                  return {} if flags.nil?
         
     | 
| 
       58 
69 
     | 
    
         | 
| 
       59 
70 
     | 
    
         
             
                  sorted_flags = AmplitudeExperiment.topological_sort(flags, flag_keys.to_set)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  required_cohorts_in_storage(sorted_flags)
         
     | 
| 
       60 
72 
     | 
    
         
             
                  flags_json = sorted_flags.to_json
         
     | 
| 
      
 73 
     | 
    
         
            +
                  user = enrich_user_with_cohorts(user, flags) if @config.cohort_sync_config
         
     | 
| 
      
 74 
     | 
    
         
            +
                  context = AmplitudeExperiment.user_to_evaluation_context(user)
         
     | 
| 
      
 75 
     | 
    
         
            +
                  context_json = context.to_json
         
     | 
| 
       61 
76 
     | 
    
         | 
| 
       62 
     | 
    
         
            -
                   
     | 
| 
       63 
     | 
    
         
            -
                   
     | 
| 
       64 
     | 
    
         
            -
             
     | 
| 
       65 
     | 
    
         
            -
                  @logger.debug("[Experiment] Evaluate: User: #{user_str} - Rules: #{flags}") if @config.debug
         
     | 
| 
       66 
     | 
    
         
            -
                  result = evaluation(flags_json, user_str)
         
     | 
| 
      
 77 
     | 
    
         
            +
                  @logger.debug("[Experiment] Evaluate: User: #{context_json} - Rules: #{flags}") if @config.debug
         
     | 
| 
      
 78 
     | 
    
         
            +
                  result = evaluation(flags_json, context_json)
         
     | 
| 
       67 
79 
     | 
    
         
             
                  @logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug
         
     | 
| 
       68 
80 
     | 
    
         
             
                  variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result)
         
     | 
| 
       69 
81 
     | 
    
         
             
                  @assignment_service&.track(Assignment.new(user, variants))
         
     | 
| 
         @@ -76,34 +88,62 @@ module AmplitudeExperiment 
     | 
|
| 
       76 
88 
     | 
    
         
             
                  return if @is_running
         
     | 
| 
       77 
89 
     | 
    
         | 
| 
       78 
90 
     | 
    
         
             
                  @logger.debug('[Experiment] poller - start') if @debug
         
     | 
| 
       79 
     | 
    
         
            -
                   
     | 
| 
      
 91 
     | 
    
         
            +
                  @deployment_runner.start
         
     | 
| 
       80 
92 
     | 
    
         
             
                end
         
     | 
| 
       81 
93 
     | 
    
         | 
| 
       82 
94 
     | 
    
         
             
                # Stop polling for flag configurations. Close resource like connection pool with client
         
     | 
| 
       83 
95 
     | 
    
         
             
                def stop
         
     | 
| 
       84 
     | 
    
         
            -
                  @poller_thread&.exit
         
     | 
| 
       85 
96 
     | 
    
         
             
                  @is_running = false
         
     | 
| 
       86 
     | 
    
         
            -
                  @ 
     | 
| 
      
 97 
     | 
    
         
            +
                  @deployment_runner.stop
         
     | 
| 
       87 
98 
     | 
    
         
             
                end
         
     | 
| 
       88 
99 
     | 
    
         | 
| 
       89 
100 
     | 
    
         
             
                private
         
     | 
| 
       90 
101 
     | 
    
         | 
| 
       91 
     | 
    
         
            -
                def  
     | 
| 
       92 
     | 
    
         
            -
                   
     | 
| 
       93 
     | 
    
         
            -
             
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
       95 
     | 
    
         
            -
                     
     | 
| 
       96 
     | 
    
         
            -
                     
     | 
| 
       97 
     | 
    
         
            -
             
     | 
| 
       98 
     | 
    
         
            -
             
     | 
| 
       99 
     | 
    
         
            -
             
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
                     
     | 
| 
      
 102 
     | 
    
         
            +
                def required_cohorts_in_storage(flag_configs)
         
     | 
| 
      
 103 
     | 
    
         
            +
                  stored_cohort_ids = @cohort_storage.cohort_ids
         
     | 
| 
      
 104 
     | 
    
         
            +
             
     | 
| 
      
 105 
     | 
    
         
            +
                  flag_configs.each do |flag|
         
     | 
| 
      
 106 
     | 
    
         
            +
                    flag_cohort_ids = AmplitudeExperiment.get_all_cohort_ids_from_flag(flag)
         
     | 
| 
      
 107 
     | 
    
         
            +
                    missing_cohorts = flag_cohort_ids - stored_cohort_ids
         
     | 
| 
      
 108 
     | 
    
         
            +
             
     | 
| 
      
 109 
     | 
    
         
            +
                    next unless missing_cohorts.any?
         
     | 
| 
      
 110 
     | 
    
         
            +
             
     | 
| 
      
 111 
     | 
    
         
            +
                    # Convert cohort IDs to a comma-separated string
         
     | 
| 
      
 112 
     | 
    
         
            +
                    cohort_ids_str = "[#{flag_cohort_ids.map(&:to_s).join(', ')}]"
         
     | 
| 
      
 113 
     | 
    
         
            +
                    missing_cohorts_str = "[#{missing_cohorts.map(&:to_s).join(', ')}]"
         
     | 
| 
      
 114 
     | 
    
         
            +
             
     | 
| 
      
 115 
     | 
    
         
            +
                    message = if @config.cohort_sync_config
         
     | 
| 
      
 116 
     | 
    
         
            +
                                "Evaluating flag #{flag['key']} dependent on cohorts #{cohort_ids_str} without #{missing_cohorts_str} in storage"
         
     | 
| 
      
 117 
     | 
    
         
            +
                              else
         
     | 
| 
      
 118 
     | 
    
         
            +
                                "Evaluating flag #{flag['key']} dependent on cohorts #{cohort_ids_str} without cohort syncing configured"
         
     | 
| 
      
 119 
     | 
    
         
            +
                              end
         
     | 
| 
      
 120 
     | 
    
         
            +
             
     | 
| 
      
 121 
     | 
    
         
            +
                    @logger.warn(message)
         
     | 
| 
      
 122 
     | 
    
         
            +
                  end
         
     | 
| 
      
 123 
     | 
    
         
            +
                end
         
     | 
| 
      
 124 
     | 
    
         
            +
             
     | 
| 
      
 125 
     | 
    
         
            +
                def enrich_user_with_cohorts(user, flag_configs)
         
     | 
| 
      
 126 
     | 
    
         
            +
                  grouped_cohort_ids = AmplitudeExperiment.get_grouped_cohort_ids_from_flags(flag_configs)
         
     | 
| 
      
 127 
     | 
    
         
            +
             
     | 
| 
      
 128 
     | 
    
         
            +
                  if grouped_cohort_ids.key?(USER_GROUP_TYPE)
         
     | 
| 
      
 129 
     | 
    
         
            +
                    user_cohort_ids = grouped_cohort_ids[USER_GROUP_TYPE]
         
     | 
| 
      
 130 
     | 
    
         
            +
                    user.cohort_ids = Array(@cohort_storage.get_cohorts_for_user(user.user_id, user_cohort_ids)) if user_cohort_ids && user.user_id
         
     | 
| 
       102 
131 
     | 
    
         
             
                  end
         
     | 
| 
       103 
     | 
    
         
            -
             
     | 
| 
       104 
     | 
    
         
            -
             
     | 
| 
       105 
     | 
    
         
            -
                     
     | 
| 
      
 132 
     | 
    
         
            +
             
     | 
| 
      
 133 
     | 
    
         
            +
                  user.groups&.each do |group_type, group_names|
         
     | 
| 
      
 134 
     | 
    
         
            +
                    group_name = group_names.first if group_names
         
     | 
| 
      
 135 
     | 
    
         
            +
                    next unless group_name
         
     | 
| 
      
 136 
     | 
    
         
            +
             
     | 
| 
      
 137 
     | 
    
         
            +
                    cohort_ids = grouped_cohort_ids[group_type] || []
         
     | 
| 
      
 138 
     | 
    
         
            +
                    next if cohort_ids.empty?
         
     | 
| 
      
 139 
     | 
    
         
            +
             
     | 
| 
      
 140 
     | 
    
         
            +
                    user.add_group_cohort_ids(
         
     | 
| 
      
 141 
     | 
    
         
            +
                      group_type,
         
     | 
| 
      
 142 
     | 
    
         
            +
                      group_name,
         
     | 
| 
      
 143 
     | 
    
         
            +
                      Array(@cohort_storage.get_cohorts_for_group(group_type, group_name, cohort_ids))
         
     | 
| 
      
 144 
     | 
    
         
            +
                    )
         
     | 
| 
       106 
145 
     | 
    
         
             
                  end
         
     | 
| 
      
 146 
     | 
    
         
            +
                  user
         
     | 
| 
       107 
147 
     | 
    
         
             
                end
         
     | 
| 
       108 
148 
     | 
    
         
             
              end
         
     | 
| 
       109 
149 
     | 
    
         
             
            end
         
     | 
| 
         @@ -1,8 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              module ServerZone
         
     | 
| 
      
 3 
     | 
    
         
            +
                US = 'US'.freeze
         
     | 
| 
      
 4 
     | 
    
         
            +
                EU = 'EU'.freeze
         
     | 
| 
      
 5 
     | 
    
         
            +
              end
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
       2 
7 
     | 
    
         
             
              # LocalEvaluationConfig
         
     | 
| 
       3 
8 
     | 
    
         
             
              class LocalEvaluationConfig
         
     | 
| 
       4 
9 
     | 
    
         
             
                # Default server url
         
     | 
| 
       5 
10 
     | 
    
         
             
                DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'.freeze
         
     | 
| 
      
 11 
     | 
    
         
            +
                EU_SERVER_URL = 'https://flag.lab.eu.amplitude.com'.freeze
         
     | 
| 
       6 
12 
     | 
    
         | 
| 
       7 
13 
     | 
    
         
             
                # Set to true to log some extra information to the console.
         
     | 
| 
       8 
14 
     | 
    
         
             
                # @return [Boolean] the value of debug
         
     | 
| 
         @@ -12,6 +18,10 @@ module AmplitudeExperiment 
     | 
|
| 
       12 
18 
     | 
    
         
             
                # @return [String] the value of server url
         
     | 
| 
       13 
19 
     | 
    
         
             
                attr_accessor :server_url
         
     | 
| 
       14 
20 
     | 
    
         | 
| 
      
 21 
     | 
    
         
            +
                # Location of the Amplitude data center to get flags and cohorts from, US or EU
         
     | 
| 
      
 22 
     | 
    
         
            +
                # @return [String] the value of server zone
         
     | 
| 
      
 23 
     | 
    
         
            +
                attr_accessor :server_zone
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
       15 
25 
     | 
    
         
             
                # The polling interval for flag configs.
         
     | 
| 
       16 
26 
     | 
    
         
             
                # @return [long] the value of flag config polling interval in million seconds
         
     | 
| 
       17 
27 
     | 
    
         
             
                attr_accessor :flag_config_polling_interval_millis
         
     | 
| 
         @@ -20,14 +30,28 @@ module AmplitudeExperiment 
     | 
|
| 
       20 
30 
     | 
    
         
             
                # @return [AssignmentConfig] the config instance
         
     | 
| 
       21 
31 
     | 
    
         
             
                attr_accessor :assignment_config
         
     | 
| 
       22 
32 
     | 
    
         | 
| 
      
 33 
     | 
    
         
            +
                # Configuration for downloading cohorts required for flag evaluation
         
     | 
| 
      
 34 
     | 
    
         
            +
                # @return [CohortSyncConfig] the config instance
         
     | 
| 
      
 35 
     | 
    
         
            +
                attr_accessor :cohort_sync_config
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
       23 
37 
     | 
    
         
             
                # @param [Boolean] debug Set to true to log some extra information to the console.
         
     | 
| 
       24 
38 
     | 
    
         
             
                # @param [String] server_url The server endpoint from which to request variants.
         
     | 
| 
      
 39 
     | 
    
         
            +
                # @param [String] server_zone Location of the Amplitude data center to get flags and cohorts from, US or EU
         
     | 
| 
       25 
40 
     | 
    
         
             
                # @param [Hash] bootstrap The value of bootstrap.
         
     | 
| 
       26 
41 
     | 
    
         
             
                # @param [long] flag_config_polling_interval_millis The value of flag config polling interval in million seconds.
         
     | 
| 
       27 
     | 
    
         
            -
                 
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
      
 42 
     | 
    
         
            +
                # @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation.
         
     | 
| 
      
 43 
     | 
    
         
            +
                # @param [CohortSyncConfig] cohort_sync_config Configuration for downloading cohorts required for flag evaluation
         
     | 
| 
      
 44 
     | 
    
         
            +
                def initialize(server_url: DEFAULT_SERVER_URL, server_zone: ServerZone::US, bootstrap: {},
         
     | 
| 
      
 45 
     | 
    
         
            +
                               flag_config_polling_interval_millis: 30_000, debug: false, assignment_config: nil,
         
     | 
| 
      
 46 
     | 
    
         
            +
                               cohort_sync_config: nil)
         
     | 
| 
       29 
47 
     | 
    
         
             
                  @debug = debug || false
         
     | 
| 
       30 
48 
     | 
    
         
             
                  @server_url = server_url
         
     | 
| 
      
 49 
     | 
    
         
            +
                  @server_zone = server_zone
         
     | 
| 
      
 50 
     | 
    
         
            +
                  @cohort_sync_config = cohort_sync_config
         
     | 
| 
      
 51 
     | 
    
         
            +
                  if server_url == DEFAULT_SERVER_URL && @server_zone == ServerZone::EU
         
     | 
| 
      
 52 
     | 
    
         
            +
                    @server_url = EU_SERVER_URL
         
     | 
| 
      
 53 
     | 
    
         
            +
                    @cohort_sync_config.cohort_server_url = EU_COHORT_SYNC_URL if @cohort_sync_config && @cohort_sync_config.cohort_server_url == DEFAULT_COHORT_SYNC_URL
         
     | 
| 
      
 54 
     | 
    
         
            +
                  end
         
     | 
| 
       31 
55 
     | 
    
         
             
                  @bootstrap = bootstrap
         
     | 
| 
       32 
56 
     | 
    
         
             
                  @flag_config_polling_interval_millis = flag_config_polling_interval_millis
         
     | 
| 
       33 
57 
     | 
    
         
             
                  @assignment_config = assignment_config
         
     | 
    
        data/lib/experiment/user.rb
    CHANGED
    
    | 
         @@ -80,6 +80,14 @@ module AmplitudeExperiment 
     | 
|
| 
       80 
80 
     | 
    
         
             
                # @return [Hash, nil] the value of group properties
         
     | 
| 
       81 
81 
     | 
    
         
             
                attr_accessor :group_properties
         
     | 
| 
       82 
82 
     | 
    
         | 
| 
      
 83 
     | 
    
         
            +
                # Cohort IDs for the user
         
     | 
| 
      
 84 
     | 
    
         
            +
                # @return [Hash, nil] the value of cohort_ids
         
     | 
| 
      
 85 
     | 
    
         
            +
                attr_accessor :cohort_ids
         
     | 
| 
      
 86 
     | 
    
         
            +
             
     | 
| 
      
 87 
     | 
    
         
            +
                # Cohort IDs for the user's groups
         
     | 
| 
      
 88 
     | 
    
         
            +
                # @return [Hash, nil] the value of group_cohort_ids
         
     | 
| 
      
 89 
     | 
    
         
            +
                attr_accessor :group_cohort_ids
         
     | 
| 
      
 90 
     | 
    
         
            +
             
     | 
| 
       83 
91 
     | 
    
         
             
                # @param [String, nil] device_id Device ID for associating with an identity in Amplitude
         
     | 
| 
       84 
92 
     | 
    
         
             
                # @param [String, nil] user_id User ID for associating with an identity in Amplitude
         
     | 
| 
       85 
93 
     | 
    
         
             
                # @param [String, nil] country Predefined field, must be manually provided
         
     | 
| 
         @@ -101,7 +109,8 @@ module AmplitudeExperiment 
     | 
|
| 
       101 
109 
     | 
    
         
             
                # @param [Hash, nil] group_properties Custom properties for groups
         
     | 
| 
       102 
110 
     | 
    
         
             
                def initialize(device_id: nil, user_id: nil, country: nil, city: nil, region: nil, dma: nil, ip_address: nil, language: nil,
         
     | 
| 
       103 
111 
     | 
    
         
             
                               platform: nil, version: nil, os: nil, device_manufacturer: nil, device_brand: nil,
         
     | 
| 
       104 
     | 
    
         
            -
                               device_model: nil, carrier: nil, library: nil, user_properties: nil, groups: nil, group_properties: nil 
     | 
| 
      
 112 
     | 
    
         
            +
                               device_model: nil, carrier: nil, library: nil, user_properties: nil, groups: nil, group_properties: nil,
         
     | 
| 
      
 113 
     | 
    
         
            +
                               cohort_ids: nil, group_cohort_ids: nil)
         
     | 
| 
       105 
114 
     | 
    
         
             
                  @device_id = device_id
         
     | 
| 
       106 
115 
     | 
    
         
             
                  @user_id = user_id
         
     | 
| 
       107 
116 
     | 
    
         
             
                  @country = country
         
     | 
| 
         @@ -121,31 +130,35 @@ module AmplitudeExperiment 
     | 
|
| 
       121 
130 
     | 
    
         
             
                  @user_properties = user_properties
         
     | 
| 
       122 
131 
     | 
    
         
             
                  @groups = groups
         
     | 
| 
       123 
132 
     | 
    
         
             
                  @group_properties = group_properties
         
     | 
| 
      
 133 
     | 
    
         
            +
                  @cohort_ids = cohort_ids
         
     | 
| 
      
 134 
     | 
    
         
            +
                  @group_cohort_ids = group_cohort_ids
         
     | 
| 
       124 
135 
     | 
    
         
             
                end
         
     | 
| 
       125 
136 
     | 
    
         | 
| 
       126 
137 
     | 
    
         
             
                # Return User as Hash.
         
     | 
| 
       127 
138 
     | 
    
         
             
                # @return [Hash] Hash object with user values
         
     | 
| 
       128 
139 
     | 
    
         
             
                def as_json(_options = {})
         
     | 
| 
       129 
140 
     | 
    
         
             
                  {
         
     | 
| 
       130 
     | 
    
         
            -
                    device_id 
     | 
| 
       131 
     | 
    
         
            -
                    user_id 
     | 
| 
       132 
     | 
    
         
            -
                    country 
     | 
| 
       133 
     | 
    
         
            -
                    city 
     | 
| 
       134 
     | 
    
         
            -
                    region 
     | 
| 
       135 
     | 
    
         
            -
                    dma 
     | 
| 
       136 
     | 
    
         
            -
                    ip_address 
     | 
| 
       137 
     | 
    
         
            -
                    language 
     | 
| 
       138 
     | 
    
         
            -
                    platform 
     | 
| 
       139 
     | 
    
         
            -
                    version 
     | 
| 
       140 
     | 
    
         
            -
                    os 
     | 
| 
       141 
     | 
    
         
            -
                    device_manufacturer 
     | 
| 
       142 
     | 
    
         
            -
                    device_brand 
     | 
| 
       143 
     | 
    
         
            -
                    device_model 
     | 
| 
       144 
     | 
    
         
            -
                    carrier 
     | 
| 
       145 
     | 
    
         
            -
                    library 
     | 
| 
       146 
     | 
    
         
            -
                    user_properties 
     | 
| 
       147 
     | 
    
         
            -
                    groups 
     | 
| 
       148 
     | 
    
         
            -
                    group_properties 
     | 
| 
      
 141 
     | 
    
         
            +
                    'device_id' => @device_id,
         
     | 
| 
      
 142 
     | 
    
         
            +
                    'user_id' => @user_id,
         
     | 
| 
      
 143 
     | 
    
         
            +
                    'country' => @country,
         
     | 
| 
      
 144 
     | 
    
         
            +
                    'city' => @city,
         
     | 
| 
      
 145 
     | 
    
         
            +
                    'region' => @region,
         
     | 
| 
      
 146 
     | 
    
         
            +
                    'dma' => @dma,
         
     | 
| 
      
 147 
     | 
    
         
            +
                    'ip_address' => @ip_address,
         
     | 
| 
      
 148 
     | 
    
         
            +
                    'language' => @language,
         
     | 
| 
      
 149 
     | 
    
         
            +
                    'platform' => @platform,
         
     | 
| 
      
 150 
     | 
    
         
            +
                    'version' => @version,
         
     | 
| 
      
 151 
     | 
    
         
            +
                    'os' => @os,
         
     | 
| 
      
 152 
     | 
    
         
            +
                    'device_manufacturer' => @device_manufacturer,
         
     | 
| 
      
 153 
     | 
    
         
            +
                    'device_brand' => @device_brand,
         
     | 
| 
      
 154 
     | 
    
         
            +
                    'device_model' => @device_model,
         
     | 
| 
      
 155 
     | 
    
         
            +
                    'carrier' => @carrier,
         
     | 
| 
      
 156 
     | 
    
         
            +
                    'library' => @library,
         
     | 
| 
      
 157 
     | 
    
         
            +
                    'user_properties' => @user_properties,
         
     | 
| 
      
 158 
     | 
    
         
            +
                    'groups' => @groups,
         
     | 
| 
      
 159 
     | 
    
         
            +
                    'group_properties' => @group_properties,
         
     | 
| 
      
 160 
     | 
    
         
            +
                    'cohort_ids' => @cohort_ids,
         
     | 
| 
      
 161 
     | 
    
         
            +
                    'group_cohort_ids' => @group_cohort_ids
         
     | 
| 
       149 
162 
     | 
    
         
             
                  }.compact
         
     | 
| 
       150 
163 
     | 
    
         
             
                end
         
     | 
| 
       151 
164 
     | 
    
         | 
| 
         @@ -154,5 +167,12 @@ module AmplitudeExperiment 
     | 
|
| 
       154 
167 
     | 
    
         
             
                def to_json(*options)
         
     | 
| 
       155 
168 
     | 
    
         
             
                  as_json(*options).to_json(*options)
         
     | 
| 
       156 
169 
     | 
    
         
             
                end
         
     | 
| 
      
 170 
     | 
    
         
            +
             
     | 
| 
      
 171 
     | 
    
         
            +
                def add_group_cohort_ids(group_type, group_name, cohort_ids)
         
     | 
| 
      
 172 
     | 
    
         
            +
                  @group_cohort_ids ||= {}
         
     | 
| 
      
 173 
     | 
    
         
            +
             
     | 
| 
      
 174 
     | 
    
         
            +
                  group_names = @group_cohort_ids[group_type] ||= {}
         
     | 
| 
      
 175 
     | 
    
         
            +
                  group_names[group_name] = cohort_ids
         
     | 
| 
      
 176 
     | 
    
         
            +
                end
         
     | 
| 
       157 
177 
     | 
    
         
             
              end
         
     | 
| 
       158 
178 
     | 
    
         
             
            end
         
     | 
| 
         @@ -0,0 +1,60 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              def self.cohort_filter?(condition)
         
     | 
| 
      
 3 
     | 
    
         
            +
                ['set contains any', 'set does not contain any'].include?(condition['op']) &&
         
     | 
| 
      
 4 
     | 
    
         
            +
                  condition['selector'] &&
         
     | 
| 
      
 5 
     | 
    
         
            +
                  condition['selector'][-1] == 'cohort_ids'
         
     | 
| 
      
 6 
     | 
    
         
            +
              end
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
              def self.get_grouped_cohort_condition_ids(segment)
         
     | 
| 
      
 9 
     | 
    
         
            +
                cohort_ids = {}
         
     | 
| 
      
 10 
     | 
    
         
            +
                conditions = segment['conditions'] || []
         
     | 
| 
      
 11 
     | 
    
         
            +
                conditions.each do |condition|
         
     | 
| 
      
 12 
     | 
    
         
            +
                  condition = condition[0]
         
     | 
| 
      
 13 
     | 
    
         
            +
                  next unless cohort_filter?(condition) && (condition['selector'][1].length > 2)
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  context_subtype = condition['selector'][1]
         
     | 
| 
      
 16 
     | 
    
         
            +
                  group_type =
         
     | 
| 
      
 17 
     | 
    
         
            +
                    if context_subtype == 'user'
         
     | 
| 
      
 18 
     | 
    
         
            +
                      USER_GROUP_TYPE
         
     | 
| 
      
 19 
     | 
    
         
            +
                    elsif condition['selector'].include?('groups')
         
     | 
| 
      
 20 
     | 
    
         
            +
                      condition['selector'][2]
         
     | 
| 
      
 21 
     | 
    
         
            +
                    else
         
     | 
| 
      
 22 
     | 
    
         
            +
                      next
         
     | 
| 
      
 23 
     | 
    
         
            +
                    end
         
     | 
| 
      
 24 
     | 
    
         
            +
                  cohort_ids[group_type] ||= Set.new
         
     | 
| 
      
 25 
     | 
    
         
            +
                  cohort_ids[group_type].merge(condition['values'])
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
                cohort_ids
         
     | 
| 
      
 28 
     | 
    
         
            +
              end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
              def self.get_grouped_cohort_ids_from_flag(flag)
         
     | 
| 
      
 31 
     | 
    
         
            +
                cohort_ids = {}
         
     | 
| 
      
 32 
     | 
    
         
            +
                segments = flag['segments'] || []
         
     | 
| 
      
 33 
     | 
    
         
            +
                segments.each do |segment|
         
     | 
| 
      
 34 
     | 
    
         
            +
                  get_grouped_cohort_condition_ids(segment).each do |key, values|
         
     | 
| 
      
 35 
     | 
    
         
            +
                    cohort_ids[key] ||= Set.new
         
     | 
| 
      
 36 
     | 
    
         
            +
                    cohort_ids[key].merge(values)
         
     | 
| 
      
 37 
     | 
    
         
            +
                  end
         
     | 
| 
      
 38 
     | 
    
         
            +
                end
         
     | 
| 
      
 39 
     | 
    
         
            +
                cohort_ids
         
     | 
| 
      
 40 
     | 
    
         
            +
              end
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
              def self.get_all_cohort_ids_from_flag(flag)
         
     | 
| 
      
 43 
     | 
    
         
            +
                get_grouped_cohort_ids_from_flag(flag).values.reduce(Set.new) { |acc, set| acc.merge(set) }
         
     | 
| 
      
 44 
     | 
    
         
            +
              end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
              def self.get_grouped_cohort_ids_from_flags(flags)
         
     | 
| 
      
 47 
     | 
    
         
            +
                cohort_ids = {}
         
     | 
| 
      
 48 
     | 
    
         
            +
                flags.each do |_, flag|
         
     | 
| 
      
 49 
     | 
    
         
            +
                  get_grouped_cohort_ids_from_flag(flag).each do |key, values|
         
     | 
| 
      
 50 
     | 
    
         
            +
                    cohort_ids[key] ||= Set.new
         
     | 
| 
      
 51 
     | 
    
         
            +
                    cohort_ids[key].merge(values)
         
     | 
| 
      
 52 
     | 
    
         
            +
                  end
         
     | 
| 
      
 53 
     | 
    
         
            +
                end
         
     | 
| 
      
 54 
     | 
    
         
            +
                cohort_ids
         
     | 
| 
      
 55 
     | 
    
         
            +
              end
         
     | 
| 
      
 56 
     | 
    
         
            +
             
     | 
| 
      
 57 
     | 
    
         
            +
              def self.get_all_cohort_ids_from_flags(flags)
         
     | 
| 
      
 58 
     | 
    
         
            +
                get_grouped_cohort_ids_from_flags(flags).values.reduce(Set.new) { |acc, set| acc.merge(set) }
         
     | 
| 
      
 59 
     | 
    
         
            +
              end
         
     | 
| 
      
 60 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,24 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            module AmplitudeExperiment
         
     | 
| 
      
 2 
     | 
    
         
            +
              # Poller
         
     | 
| 
      
 3 
     | 
    
         
            +
              class Poller
         
     | 
| 
      
 4 
     | 
    
         
            +
                def initialize(interval_seconds, callback)
         
     | 
| 
      
 5 
     | 
    
         
            +
                  @interval_seconds = interval_seconds
         
     | 
| 
      
 6 
     | 
    
         
            +
                  @callback = callback
         
     | 
| 
      
 7 
     | 
    
         
            +
                end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                def start
         
     | 
| 
      
 10 
     | 
    
         
            +
                  @running = true
         
     | 
| 
      
 11 
     | 
    
         
            +
                  @thread = Thread.new do
         
     | 
| 
      
 12 
     | 
    
         
            +
                    while @running
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @callback.call
         
     | 
| 
      
 14 
     | 
    
         
            +
                      sleep(@interval_seconds)
         
     | 
| 
      
 15 
     | 
    
         
            +
                    end
         
     | 
| 
      
 16 
     | 
    
         
            +
                  end
         
     | 
| 
      
 17 
     | 
    
         
            +
                end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                def stop
         
     | 
| 
      
 20 
     | 
    
         
            +
                  @running = false
         
     | 
| 
      
 21 
     | 
    
         
            +
                  @thread&.join
         
     | 
| 
      
 22 
     | 
    
         
            +
                end
         
     | 
| 
      
 23 
     | 
    
         
            +
              end
         
     | 
| 
      
 24 
     | 
    
         
            +
            end
         
     | 
    
        data/lib/experiment/util/user.rb
    CHANGED
    
    | 
         @@ -2,11 +2,13 @@ module AmplitudeExperiment 
     | 
|
| 
       2 
2 
     | 
    
         
             
              def self.user_to_evaluation_context(user)
         
     | 
| 
       3 
3 
     | 
    
         
             
                user_groups = user.groups
         
     | 
| 
       4 
4 
     | 
    
         
             
                user_group_properties = user.group_properties
         
     | 
| 
      
 5 
     | 
    
         
            +
                user_group_cohort_ids = user.group_cohort_ids
         
     | 
| 
       5 
6 
     | 
    
         
             
                user_hash = user.as_json.compact
         
     | 
| 
       6 
     | 
    
         
            -
                user_hash.delete( 
     | 
| 
       7 
     | 
    
         
            -
                user_hash.delete( 
     | 
| 
      
 7 
     | 
    
         
            +
                user_hash.delete('groups')
         
     | 
| 
      
 8 
     | 
    
         
            +
                user_hash.delete('group_properties')
         
     | 
| 
      
 9 
     | 
    
         
            +
                user_hash.delete('group_cohort_ids')
         
     | 
| 
       8 
10 
     | 
    
         | 
| 
       9 
     | 
    
         
            -
                context = user_hash.empty? ? {} : { user 
     | 
| 
      
 11 
     | 
    
         
            +
                context = user_hash.empty? ? {} : { 'user' => user_hash }
         
     | 
| 
       10 
12 
     | 
    
         | 
| 
       11 
13 
     | 
    
         
             
                return context if user_groups.nil?
         
     | 
| 
       12 
14 
     | 
    
         | 
| 
         @@ -14,20 +16,26 @@ module AmplitudeExperiment 
     | 
|
| 
       14 
16 
     | 
    
         
             
                user_groups.each do |group_type, group_name|
         
     | 
| 
       15 
17 
     | 
    
         
             
                  group_name = group_name[0] if group_name.is_a?(Array) && !group_name.empty?
         
     | 
| 
       16 
18 
     | 
    
         | 
| 
       17 
     | 
    
         
            -
                  groups[group_type 
     | 
| 
      
 19 
     | 
    
         
            +
                  groups[group_type] = { 'group_name' => group_name }
         
     | 
| 
       18 
20 
     | 
    
         | 
| 
       19 
     | 
    
         
            -
                   
     | 
| 
      
 21 
     | 
    
         
            +
                  if user_group_properties
         
     | 
| 
      
 22 
     | 
    
         
            +
                    group_properties_type = user_group_properties[group_type]
         
     | 
| 
      
 23 
     | 
    
         
            +
                    if group_properties_type.is_a?(Hash)
         
     | 
| 
      
 24 
     | 
    
         
            +
                      group_properties_name = group_properties_type[group_name]
         
     | 
| 
      
 25 
     | 
    
         
            +
                      groups[group_type]['group_properties'] = group_properties_name if group_properties_name.is_a?(Hash)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
       20 
28 
     | 
    
         | 
| 
       21 
     | 
    
         
            -
                   
     | 
| 
       22 
     | 
    
         
            -
                  next if group_properties_type.nil? || !group_properties_type.is_a?(Hash)
         
     | 
| 
      
 29 
     | 
    
         
            +
                  next unless user_group_cohort_ids
         
     | 
| 
       23 
30 
     | 
    
         | 
| 
       24 
     | 
    
         
            -
                   
     | 
| 
       25 
     | 
    
         
            -
                   
     | 
| 
       26 
     | 
    
         
            -
             
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
      
 31 
     | 
    
         
            +
                  group_cohort_ids_type = user_group_cohort_ids[group_type]
         
     | 
| 
      
 32 
     | 
    
         
            +
                  if group_cohort_ids_type.is_a?(Hash)
         
     | 
| 
      
 33 
     | 
    
         
            +
                    group_cohort_ids_name = group_cohort_ids_type[group_name]
         
     | 
| 
      
 34 
     | 
    
         
            +
                    groups[group_type]['cohort_ids'] = group_cohort_ids_name if group_cohort_ids_name.is_a?(Array)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
       28 
36 
     | 
    
         
             
                end
         
     | 
| 
       29 
37 
     | 
    
         | 
| 
       30 
     | 
    
         
            -
                context[ 
     | 
| 
      
 38 
     | 
    
         
            +
                context['groups'] = groups unless groups.empty?
         
     | 
| 
       31 
39 
     | 
    
         
             
                context
         
     | 
| 
       32 
40 
     | 
    
         
             
              end
         
     | 
| 
       33 
41 
     | 
    
         
             
            end
         
     | 
    
        data/lib/experiment/version.rb
    CHANGED
    
    
    
        metadata
    CHANGED
    
    | 
         @@ -1,14 +1,14 @@ 
     | 
|
| 
       1 
1 
     | 
    
         
             
            --- !ruby/object:Gem::Specification
         
     | 
| 
       2 
2 
     | 
    
         
             
            name: amplitude-experiment
         
     | 
| 
       3 
3 
     | 
    
         
             
            version: !ruby/object:Gem::Version
         
     | 
| 
       4 
     | 
    
         
            -
              version: 1. 
     | 
| 
      
 4 
     | 
    
         
            +
              version: 1.5.0
         
     | 
| 
       5 
5 
     | 
    
         
             
            platform: ruby
         
     | 
| 
       6 
6 
     | 
    
         
             
            authors:
         
     | 
| 
       7 
7 
     | 
    
         
             
            - Amplitude
         
     | 
| 
       8 
8 
     | 
    
         
             
            autorequire: 
         
     | 
| 
       9 
9 
     | 
    
         
             
            bindir: bin
         
     | 
| 
       10 
10 
     | 
    
         
             
            cert_chain: []
         
     | 
| 
       11 
     | 
    
         
            -
            date: 2024- 
     | 
| 
      
 11 
     | 
    
         
            +
            date: 2024-08-27 00:00:00.000000000 Z
         
     | 
| 
       12 
12 
     | 
    
         
             
            dependencies:
         
     | 
| 
       13 
13 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       14 
14 
     | 
    
         
             
              name: concurrent-ruby
         
     | 
| 
         @@ -86,14 +86,14 @@ dependencies: 
     | 
|
| 
       86 
86 
     | 
    
         
             
                requirements:
         
     | 
| 
       87 
87 
     | 
    
         
             
                - - '='
         
     | 
| 
       88 
88 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       89 
     | 
    
         
            -
                    version:  
     | 
| 
      
 89 
     | 
    
         
            +
                    version: 1.22.3
         
     | 
| 
       90 
90 
     | 
    
         
             
              type: :development
         
     | 
| 
       91 
91 
     | 
    
         
             
              prerelease: false
         
     | 
| 
       92 
92 
     | 
    
         
             
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
       93 
93 
     | 
    
         
             
                requirements:
         
     | 
| 
       94 
94 
     | 
    
         
             
                - - '='
         
     | 
| 
       95 
95 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       96 
     | 
    
         
            -
                    version:  
     | 
| 
      
 96 
     | 
    
         
            +
                    version: 1.22.3
         
     | 
| 
       97 
97 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       98 
98 
     | 
    
         
             
              name: simplecov
         
     | 
| 
       99 
99 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -136,6 +136,20 @@ dependencies: 
     | 
|
| 
       136 
136 
     | 
    
         
             
                - - "~>"
         
     | 
| 
       137 
137 
     | 
    
         
             
                  - !ruby/object:Gem::Version
         
     | 
| 
       138 
138 
     | 
    
         
             
                    version: '0.9'
         
     | 
| 
      
 139 
     | 
    
         
            +
            - !ruby/object:Gem::Dependency
         
     | 
| 
      
 140 
     | 
    
         
            +
              name: dotenv
         
     | 
| 
      
 141 
     | 
    
         
            +
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
      
 142 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 143 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 144 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 145 
     | 
    
         
            +
                    version: 2.8.1
         
     | 
| 
      
 146 
     | 
    
         
            +
              type: :development
         
     | 
| 
      
 147 
     | 
    
         
            +
              prerelease: false
         
     | 
| 
      
 148 
     | 
    
         
            +
              version_requirements: !ruby/object:Gem::Requirement
         
     | 
| 
      
 149 
     | 
    
         
            +
                requirements:
         
     | 
| 
      
 150 
     | 
    
         
            +
                - - "~>"
         
     | 
| 
      
 151 
     | 
    
         
            +
                  - !ruby/object:Gem::Version
         
     | 
| 
      
 152 
     | 
    
         
            +
                    version: 2.8.1
         
     | 
| 
       139 
153 
     | 
    
         
             
            - !ruby/object:Gem::Dependency
         
     | 
| 
       140 
154 
     | 
    
         
             
              name: ffi
         
     | 
| 
       141 
155 
     | 
    
         
             
              requirement: !ruby/object:Gem::Requirement
         
     | 
| 
         @@ -175,9 +189,17 @@ files: 
     | 
|
| 
       175 
189 
     | 
    
         
             
            - lib/amplitude/timeline.rb
         
     | 
| 
       176 
190 
     | 
    
         
             
            - lib/amplitude/utils.rb
         
     | 
| 
       177 
191 
     | 
    
         
             
            - lib/amplitude/workers.rb
         
     | 
| 
      
 192 
     | 
    
         
            +
            - lib/experiment/cohort/cohort.rb
         
     | 
| 
      
 193 
     | 
    
         
            +
            - lib/experiment/cohort/cohort_download_api.rb
         
     | 
| 
      
 194 
     | 
    
         
            +
            - lib/experiment/cohort/cohort_loader.rb
         
     | 
| 
      
 195 
     | 
    
         
            +
            - lib/experiment/cohort/cohort_storage.rb
         
     | 
| 
      
 196 
     | 
    
         
            +
            - lib/experiment/cohort/cohort_sync_config.rb
         
     | 
| 
       178 
197 
     | 
    
         
             
            - lib/experiment/cookie.rb
         
     | 
| 
      
 198 
     | 
    
         
            +
            - lib/experiment/deployment/deployment_runner.rb
         
     | 
| 
       179 
199 
     | 
    
         
             
            - lib/experiment/error.rb
         
     | 
| 
       180 
200 
     | 
    
         
             
            - lib/experiment/factory.rb
         
     | 
| 
      
 201 
     | 
    
         
            +
            - lib/experiment/flag/flag_config_fetcher.rb
         
     | 
| 
      
 202 
     | 
    
         
            +
            - lib/experiment/flag/flag_config_storage.rb
         
     | 
| 
       181 
203 
     | 
    
         
             
            - lib/experiment/local/assignment/assignment.rb
         
     | 
| 
       182 
204 
     | 
    
         
             
            - lib/experiment/local/assignment/assignment_config.rb
         
     | 
| 
       183 
205 
     | 
    
         
             
            - lib/experiment/local/assignment/assignment_filter.rb
         
     | 
| 
         @@ -193,13 +215,14 @@ files: 
     | 
|
| 
       193 
215 
     | 
    
         
             
            - lib/experiment/local/evaluation/lib/macosArm64/libevaluation_interop_api.h
         
     | 
| 
       194 
216 
     | 
    
         
             
            - lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop.dylib
         
     | 
| 
       195 
217 
     | 
    
         
             
            - lib/experiment/local/evaluation/lib/macosX64/libevaluation_interop_api.h
         
     | 
| 
       196 
     | 
    
         
            -
            - lib/experiment/local/fetcher.rb
         
     | 
| 
       197 
218 
     | 
    
         
             
            - lib/experiment/persistent_http_client.rb
         
     | 
| 
       198 
219 
     | 
    
         
             
            - lib/experiment/remote/client.rb
         
     | 
| 
       199 
220 
     | 
    
         
             
            - lib/experiment/remote/config.rb
         
     | 
| 
       200 
221 
     | 
    
         
             
            - lib/experiment/user.rb
         
     | 
| 
      
 222 
     | 
    
         
            +
            - lib/experiment/util/flag_config.rb
         
     | 
| 
       201 
223 
     | 
    
         
             
            - lib/experiment/util/hash.rb
         
     | 
| 
       202 
224 
     | 
    
         
             
            - lib/experiment/util/lru_cache.rb
         
     | 
| 
      
 225 
     | 
    
         
            +
            - lib/experiment/util/poller.rb
         
     | 
| 
       203 
226 
     | 
    
         
             
            - lib/experiment/util/topological_sort.rb
         
     | 
| 
       204 
227 
     | 
    
         
             
            - lib/experiment/util/user.rb
         
     | 
| 
       205 
228 
     | 
    
         
             
            - lib/experiment/util/variant.rb
         
     |