amplitude-experiment 1.0.0.beta.4 → 1.0.0.beta.7

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 653e4953eeea7497bb43f1e76c144c8fe7360dcc53c7aa3f9c63e3b28e1052ee
4
- data.tar.gz: 9b8ea31ccb93aebd815019f6dab4f739ea49b96177800dc6a39b9f1907f64ad0
3
+ metadata.gz: 3c1c474415a95c82dd30feb5730a517d3df489f39ac6b307583a66d20b844fc3
4
+ data.tar.gz: f215b72c1dc816aa205c68d3b9fac5b4685ca4ee90cacc035f6f2106bd1cf9eb
5
5
  SHA512:
6
- metadata.gz: 4a1b221d66a28d1cc53c20c5eba088baa3a180ec628603d8801ed759ef893b6a74162abb8396ed3bac2f97f51aa45e7536adedc4509f1d719c2495ff2e129df1
7
- data.tar.gz: 1757f94f26bf55016c8604ee065ae4d24d95b431ac7a27687190fe8e138d6ac402d94248eba90f9c23d94fd918179d28da8ffc02c9c03004e5bdeaafa8e24db1
6
+ metadata.gz: e55171f2998fcebf9f9a49a785445e1731b0b86a0d98a5b0ff8111702cb7a5c03df8c3e641492acaa30bb45b3750fc237c79d6a7ee964424cd8ebb5061180835
7
+ data.tar.gz: 3ec5bfd97ed595841541074b9f7079e94a7b0452a8b53ecdd722dfd18956ed40f12d26503c04d6a9a84ae2ec2c9d4119dea0420b4566bb591f959296710df080
data/README.md CHANGED
@@ -25,7 +25,7 @@ gem install amplitude-experiment --pre
25
25
  ```
26
26
 
27
27
 
28
- ## Quick Start
28
+ ## Remote Evaluation Quick Start
29
29
  ```ruby
30
30
  require 'amplitude-experiment'
31
31
 
@@ -33,10 +33,10 @@ require 'amplitude-experiment'
33
33
  apiKey = 'YOUR-API-KEY'
34
34
 
35
35
  # (2) Initialize the experiment client
36
- experiment = Experiment.init(api_key)
36
+ experiment = AmplitudeExperiment.initialize_remote(api_key)
37
37
 
38
38
  # (3) Fetch variants for a user
39
- user = Experiment::User.new(user_id: 'user@company.com', device_id: 'abcezas123', user_properties: {'premium' => true})
39
+ user = AmplitudeExperiment::User.new(user_id: 'user@company.com', device_id: 'abcezas123', user_properties: {'premium' => true})
40
40
 
41
41
  # (4) Lookup a flag's variant
42
42
  #
@@ -64,6 +64,33 @@ unless variant.nil?
64
64
  end
65
65
  ```
66
66
 
67
+ ## Local Evaluation Quick Start
68
+
69
+ ```ruby
70
+ require 'amplitude-experiment'
71
+
72
+ # (1) Get your deployment's API key
73
+ apiKey = 'YOUR-API-KEY'
74
+
75
+ # (2) Initialize the experiment client
76
+ experiment = AmplitudeExperiment.initialize_local(api_key)
77
+
78
+ # (3) Start the local evaluation client
79
+ experiment.start
80
+
81
+ # (4) Evaluate a user
82
+ user = AmplitudeExperiment::User.new(user_id: 'user@company.com', device_id: 'abcezas123', user_properties: {'premium' => true})
83
+ variants = experiment.evaluate(user)
84
+ variant = variants['YOUR-FLAG-KEY']
85
+ unless variant.nil?
86
+ if variant.value == 'on'
87
+ # Flag is on
88
+ else
89
+ # Flag is off
90
+ end
91
+ end
92
+ ```
93
+
67
94
  ## More Information
68
95
  Please visit our :100:[Developer Center](https://www.docs.developers.amplitude.com/experiment/sdks/ruby-sdk/) for more instructions on using our the SDK.
69
96
 
@@ -1 +1,17 @@
1
- require 'experiment'
1
+ require 'experiment/version'
2
+ require 'experiment/persistent_http_client'
3
+ require 'experiment/remote/config'
4
+ require 'experiment/cookie'
5
+ require 'experiment/user'
6
+ require 'experiment/variant'
7
+ require 'experiment/factory'
8
+ require 'experiment/remote/client'
9
+ require 'experiment/local/client'
10
+ require 'experiment/local/config'
11
+ require 'experiment/local/cache'
12
+ require 'experiment/local/fetcher'
13
+ require 'experiment/local/poller'
14
+
15
+ # Amplitude Experiment Module
16
+ module AmplitudeExperiment
17
+ end
@@ -1,6 +1,6 @@
1
1
  require 'base64'
2
2
 
3
- module Experiment
3
+ module AmplitudeExperiment
4
4
  # This class provides utility functions for parsing and handling identity from Amplitude cookies.
5
5
  class AmplitudeCookie
6
6
  # Get the cookie name that Amplitude sets for the provided
@@ -1,6 +1,7 @@
1
1
  # Provides factory methods for storing singleton instance of Client
2
- module Experiment
3
- @instances = {}
2
+ module AmplitudeExperiment
3
+ @local_instance = {}
4
+ @remote_instance = {}
4
5
  @default_instance = '$default_instance'
5
6
 
6
7
  # Initializes a singleton Client. This method returns a default singleton instance, subsequent calls to
@@ -8,8 +9,23 @@ module Experiment
8
9
  #
9
10
  # @param [String] api_key The environment API Key
10
11
  # @param [Config] config Optional Config.
11
- def self.init(api_key, config = nil)
12
- @instances.store(@default_instance, Client.new(api_key, config)) unless @instances.key?(@default_instance)
13
- @instances.fetch(@default_instance)
12
+ def self.initialize_remote(api_key, config = nil)
13
+ unless @local_instance.key?(@default_instance)
14
+ @local_instance.store(@default_instance, RemoteEvaluationClient.new(api_key, config))
15
+ end
16
+ @local_instance.fetch(@default_instance)
17
+ end
18
+
19
+ # Initializes a local evaluation Client. A local evaluation client can evaluate local flags or experiments for a
20
+ # user without requiring a remote call to the amplitude evaluation server. In order to best leverage local
21
+ # evaluation, all flags, and experiments being evaluated server side should be configured as local.
22
+ #
23
+ # @param [String] api_key The environment API Key
24
+ # @param [Config] config Optional Config.
25
+ def self.initialize_local(api_key, config = nil)
26
+ unless @remote_instance.key?(@default_instance)
27
+ @remote_instance.store(@default_instance, LocalEvaluationClient.new(api_key, config))
28
+ end
29
+ @remote_instance.fetch(@default_instance)
14
30
  end
15
31
  end
@@ -0,0 +1,53 @@
1
+ require 'uri'
2
+ require 'logger'
3
+
4
+ module AmplitudeExperiment
5
+ # InMemoryFlagConfigCache
6
+ # The place to store the flag configs fetched from the server
7
+ class InMemoryFlagConfigCache
8
+ attr_accessor :cache
9
+
10
+ def initialize(flag_configs = {})
11
+ @semaphore = Mutex.new
12
+ @cache = flag_configs
13
+ end
14
+
15
+ def get(flag_key)
16
+ @semaphore.synchronize do
17
+ @cache.fetch(flag_key, nil)
18
+ end
19
+ end
20
+
21
+ def caches
22
+ @semaphore.synchronize do
23
+ @cache
24
+ end
25
+ end
26
+
27
+ def put(flag_key, flag_config)
28
+ @semaphore.synchronize do
29
+ @cache.store(flag_key, flag_config.clone)
30
+ end
31
+ end
32
+
33
+ def put_all(flag_configs)
34
+ @semaphore.synchronize do
35
+ flag_configs.each do |key, value|
36
+ @cache.store(key, value.clone) if value
37
+ end
38
+ end
39
+ end
40
+
41
+ def delete(flag_key)
42
+ @semaphore.synchronize do
43
+ @cache.delete(flag_key)
44
+ end
45
+ end
46
+
47
+ def clear
48
+ @semaphore.synchronize do
49
+ @cache = {}
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,84 @@
1
+ require 'uri'
2
+ require 'logger'
3
+ require 'experiment/local/evaluation/evaluation'
4
+
5
+ module AmplitudeExperiment
6
+ # Main client for fetching variant data.
7
+ class LocalEvaluationClient
8
+ # Creates a new Experiment Client instance.
9
+ #
10
+ # @param [String] api_key The environment API Key
11
+ # @param [LocalEvaluationConfig] config The config object
12
+ def initialize(api_key, config = nil)
13
+ @api_key = api_key
14
+ @config = config || LocalEvaluationConfig.new
15
+ @cache = InMemoryFlagConfigCache.new(@config.bootstrap)
16
+ @logger = Logger.new($stdout)
17
+ @logger.level = if @config.debug
18
+ Logger::DEBUG
19
+ else
20
+ Logger::INFO
21
+ end
22
+ @fetcher = LocalEvaluationFetcher.new(api_key, @config.debug, @config.server_url)
23
+ @poller = FlagConfigPoller.new(@fetcher, @cache, @config.debug)
24
+
25
+ raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
26
+ end
27
+
28
+ # Locally evaluates flag variants for a user.
29
+ #
30
+ # @param [User] user The user to evaluate
31
+ # @param [String[]] flag_keys The flags to evaluate with the user. If empty, all flags from the flag cache are evaluated
32
+ #
33
+ # @return [Hash[String, Variant]] The evaluated variants
34
+ def evaluate(user, flag_keys = [])
35
+ flag_configs = []
36
+ if flag_keys.empty?
37
+ @cache.cache.each do |_, value|
38
+ flag_configs.push(value)
39
+ end
40
+ else
41
+ flag_configs = get_flag_configs(flag_keys)
42
+ end
43
+ flag_configs_str = flag_configs.to_json
44
+ user_str = user.to_json
45
+ @logger.debug("[Experiment] Evaluate: User: #{user_str} - Rules: #{flag_configs_str}") if @config.debug
46
+ result_json = evaluation(flag_configs_str, user_str)
47
+ @logger.debug(`[Experiment] evaluate - result: #{variants}`) if @config.debug
48
+ result = JSON.parse(result_json)
49
+ variants = {}
50
+ result.each do |key, value|
51
+ next if value['isDefaultVariant']
52
+
53
+ variant_key = value['variant']['key']
54
+ variant_payload = value['variant']['payload']
55
+ variants.store(key, Variant.new(variant_key, variant_payload))
56
+ end
57
+ variants
58
+ end
59
+
60
+ # Fetch initial flag configurations and start polling for updates.
61
+ # You must call this function to begin polling for flag config updates.
62
+ def start
63
+ @poller.start
64
+ end
65
+
66
+ # Stop polling for flag configurations. Close resource like connection pool with client
67
+ def stop
68
+ @poller.stop
69
+ end
70
+
71
+ private
72
+
73
+ def get_flag_configs(flag_keys = [])
74
+ return @cache.cache if flag_keys.empty?
75
+
76
+ flag_configs = []
77
+ flag_keys.each do |key|
78
+ flag_config = @cache.get(key)
79
+ flag_configs.push(flag_config) if flag_config
80
+ end
81
+ flag_configs
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ module AmplitudeExperiment
2
+ # LocalEvaluationConfig
3
+ class LocalEvaluationConfig
4
+ # Default server url
5
+ DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'.freeze
6
+
7
+ # Set to true to log some extra information to the console.
8
+ # @return [Boolean] the value of debug
9
+ attr_accessor :debug
10
+
11
+ # The server endpoint from which to request variants.
12
+ # @return [String] the value of server url
13
+ attr_accessor :server_url
14
+
15
+ # The server endpoint from which to request variants.
16
+ # @return [Hash] the value of bootstrap
17
+ attr_accessor :bootstrap
18
+
19
+ # The server endpoint from which to request variants.
20
+ # @return [long] the value of flag config polling interval in million seconds
21
+ attr_accessor :flag_config_polling_interval_millis
22
+
23
+ # @param [Boolean] debug Set to true to log some extra information to the console.
24
+ # @param [String] server_url The server endpoint from which to request variants.
25
+ # @param [Hash] bootstrap The value of bootstrap.
26
+ # @param [long] flag_config_polling_interval_millis The value of flag config polling interval in million seconds.
27
+ def initialize(server_url = DEFAULT_SERVER_URL, bootstrap = {},
28
+ flag_config_polling_interval_millis = 30_000, debug: false)
29
+ @debug = debug || false
30
+ @server_url = server_url
31
+ @bootstrap = bootstrap
32
+ @flag_config_polling_interval_millis = flag_config_polling_interval_millis
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,48 @@
1
+ # rubocop:disable all
2
+ require 'ffi'
3
+
4
+ # The evaluation wrapper
5
+ module EvaluationInterop
6
+ extend FFI::Library
7
+ host_os = RbConfig::CONFIG['host_os']
8
+ cpu = RbConfig::CONFIG['host_cpu']
9
+ evaluation_dir = File.dirname(__FILE__)
10
+ ffi_lib ["#{evaluation_dir}/lib/macosX64/libevaluation_interop.dylib"] if host_os =~ /darwin|mac os/ && cpu =~ /x86_64/
11
+ ffi_lib ["#{evaluation_dir}/lib/macosArm64/libevaluation_interop.dylib"] if host_os =~ /darwin|mac os/ && cpu =~ /arm64/
12
+ ffi_lib ["#{evaluation_dir}/lib/linuxX64/libevaluation_interop.so"] if host_os =~ /linux/ && cpu =~ /x86_64/
13
+ ffi_lib ["#{evaluation_dir}/lib/linuxArm64/libevaluation_interop.so"] if host_os =~ /linux/ && cpu =~ /arm64/
14
+
15
+ class Root < FFI::Struct
16
+ layout :evaluate, callback([:string, :string], :pointer)
17
+ end
18
+
19
+ class Kotlin < FFI::Struct
20
+ layout :root, Root
21
+ end
22
+
23
+ class Libevaluation_interop_ExportedSymbols < FFI::Struct
24
+ layout :DisposeStablePointer, callback([:pointer], :void),
25
+ :DisposeString, callback([:string], :void),
26
+ :IsInstance, callback([:pointer, :string], :pointer),
27
+ :createNullableByte, callback([:string], :pointer),
28
+ :createNullableShort, callback([:pointer], :pointer),
29
+ :createNullableInt, callback([:pointer], :pointer),
30
+ :createNullableLong, callback([:pointer], :pointer),
31
+ :createNullableFloat, callback([:pointer], :pointer),
32
+ :createNullableDouble, callback([:pointer], :pointer),
33
+ :createNullableChar, callback([:pointer], :pointer),
34
+ :createNullableBoolean, callback([:pointer], :pointer),
35
+ :createNullableUnit, callback([], :pointer),
36
+ :kotlin, Kotlin
37
+ end
38
+
39
+ attach_function :libevaluation_interop_symbols, [], Libevaluation_interop_ExportedSymbols.by_ref
40
+ end
41
+
42
+ def evaluation(rule_json, user_json)
43
+ lib = EvaluationInterop.libevaluation_interop_symbols()
44
+ fn = lib[:kotlin][:root][:evaluate]
45
+ evaluation_result = fn.call(rule_json, user_json)
46
+ evaluation_result.read_string
47
+ end
48
+ # rubocop:disable all
@@ -0,0 +1,45 @@
1
+ module AmplitudeExperiment
2
+ # LocalEvaluationFetcher
3
+ # Fetch local evaluation mode flag configs from the Experiment API server.
4
+ # These flag configs can be used to perform local evaluation.
5
+ class LocalEvaluationFetcher
6
+ FLAG_CONFIG_TIMEOUT = 5000
7
+
8
+ def initialize(api_key, debug, server_url = 'https://api.lab.amplitude.com')
9
+ @api_key = api_key
10
+ @uri = "#{server_url}/sdk/rules?eval_mode=local"
11
+ @debug = debug
12
+ @http = PersistentHttpClient.get(@uri, { read_timeout: FLAG_CONFIG_TIMEOUT })
13
+ end
14
+
15
+ # Fetch local evaluation mode flag configs from the Experiment API server.
16
+ # These flag configs can be used to perform local evaluation.
17
+ #
18
+ # @return [Hash] The flag configs
19
+ def fetch
20
+ # fetch flag_configs
21
+ headers = {
22
+ 'Authorization' => "Api-Key #{@api_key}",
23
+ 'Content-Type' => 'application/json;charset=utf-8'
24
+ }
25
+ request = Net::HTTP::Get.new(@uri, headers)
26
+ response = @http.request(request)
27
+ raise `flagConfigs - received error response: #{response.status}: #{response.body}` unless response.is_a?(Net::HTTPOK)
28
+
29
+ flag_configs = parse(response.body)
30
+ @logger.debug("[Experiment] Fetch flag configs: #{request.body}") if @debug
31
+ flag_configs
32
+ end
33
+
34
+ private
35
+
36
+ def parse(flag_configs_str)
37
+ flag_config_obj = {}
38
+ flag_configs_array = JSON.parse(flag_configs_str)
39
+ flag_configs_array.each do |flag_config|
40
+ flag_config_obj.store(flag_config['flagKey'], flag_config) if flag_config.key?('flagKey')
41
+ end
42
+ flag_config_obj
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,48 @@
1
+ module AmplitudeExperiment
2
+ # FlagConfigPoller
3
+ # update the flag_config cache value
4
+ class FlagConfigPoller
5
+ FLAG_CONFIG_POLLING_INTERVAL_MILLIS = 30_000
6
+
7
+ def initialize(fetcher, cache, debug, poll_interval_millis: FLAG_CONFIG_POLLING_INTERVAL_MILLIS)
8
+ @fetcher = fetcher
9
+ @cache = cache
10
+ @poll_interval_millis = poll_interval_millis
11
+ @logger = Logger.new($stdout)
12
+ @debug = debug
13
+ @poller_thread = nil
14
+ @is_running = false
15
+ end
16
+
17
+ # Fetch initial flag configurations and start polling for updates.
18
+ # You must call this function to begin polling for flag config updates.
19
+ # Calling this function while the poller is already running does nothing.
20
+ def start
21
+ return if @is_running
22
+
23
+ @logger.debug('[Experiment] poller - start') if @debug
24
+ run
25
+ end
26
+
27
+ # Stop polling for flag configurations.
28
+ # Calling this function while the poller is not running will do nothing.
29
+ def stop
30
+ @poller_thread&.exit
31
+ @is_running = false
32
+ @poller_thread = nil
33
+ end
34
+
35
+ private
36
+
37
+ def run
38
+ @is_running = true
39
+ flag_configs = @fetcher.fetch
40
+ @cache.clear
41
+ @cache.put_all(flag_configs)
42
+ @poller_thread = Thread.new do
43
+ sleep @poll_interval_millis
44
+ run
45
+ end
46
+ end
47
+ end
48
+ end
@@ -1,4 +1,4 @@
1
- module Experiment
1
+ module AmplitudeExperiment
2
2
  # Persist Http Client to reuse connection and reduce IO
3
3
  class PersistentHttpClient
4
4
  DEFAULT_OPTIONS = { read_timeout: 80 }.freeze
@@ -3,16 +3,16 @@ require 'json'
3
3
  require 'uri'
4
4
  require 'logger'
5
5
 
6
- module Experiment
6
+ module AmplitudeExperiment
7
7
  # Main client for fetching variant data.
8
- class Client
8
+ class RemoteEvaluationClient
9
9
  # Creates a new Experiment Client instance.
10
10
  #
11
11
  # @param [String] api_key The environment API Key
12
12
  # @param [Config] config
13
13
  def initialize(api_key, config = nil)
14
14
  @api_key = api_key
15
- @config = config || Config.new
15
+ @config = config || RemoteEvaluationConfig.new
16
16
  @logger = Logger.new($stdout)
17
17
  @logger.level = if @config.debug
18
18
  Logger::DEBUG
@@ -40,6 +40,7 @@ module Experiment
40
40
  #
41
41
  # This method will automatically retry if configured (default).
42
42
  # @param [User] user
43
+ # @yield [User, Hash] callback block takes user object and variants hash
43
44
  def fetch_async(user, &callback)
44
45
  Thread.new do
45
46
  variants = fetch_internal(user)
@@ -138,7 +139,7 @@ module Experiment
138
139
  # @return [User, Hash] user with library context
139
140
  def add_context(user)
140
141
  user = {} if user.nil?
141
- user.library = "experiment-ruby-server/#{VERSION}" if user.library.nil?
142
+ user.library = "experiment-ruby-server/#{VERSION}"
142
143
  user
143
144
  end
144
145
  end
@@ -1,6 +1,6 @@
1
- module Experiment
1
+ module AmplitudeExperiment
2
2
  # Configuration
3
- class Config
3
+ class RemoteEvaluationConfig
4
4
  # Default server url
5
5
  DEFAULT_SERVER_URL = 'https://api.lab.amplitude.com'.freeze
6
6
 
@@ -1,4 +1,4 @@
1
- module Experiment
1
+ module AmplitudeExperiment
2
2
  # Defines a user context for evaluation.
3
3
  # `device_id` and `user_id` are used for identity resolution.
4
4
  # All other predefined fields and user properties are used for
@@ -1,4 +1,4 @@
1
- module Experiment
1
+ module AmplitudeExperiment
2
2
  # Variant
3
3
  class Variant
4
4
  # The value of the variant determined by the flag configuration.
@@ -1,3 +1,3 @@
1
- module Experiment
2
- VERSION = '1.0.0.beta.4'.freeze
1
+ module AmplitudeExperiment
2
+ VERSION = '1.0.0.beta.7'.freeze
3
3
  end
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.0.0.beta.4
4
+ version: 1.0.0.beta.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Amplitude
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-06-30 00:00:00.000000000 Z
11
+ date: 2022-08-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: '0.9'
111
+ - !ruby/object:Gem::Dependency
112
+ name: ffi
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 1.15.5
118
+ type: :runtime
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 1.15.5
111
125
  description: Amplitude Experiment Ruby Server SDK
112
126
  email:
113
127
  - sdk@amplitude.com
@@ -119,12 +133,17 @@ files:
119
133
  - Gemfile
120
134
  - README.md
121
135
  - lib/amplitude-experiment.rb
122
- - lib/experiment.rb
123
- - lib/experiment/client.rb
124
- - lib/experiment/config.rb
125
136
  - lib/experiment/cookie.rb
126
137
  - lib/experiment/factory.rb
138
+ - lib/experiment/local/cache.rb
139
+ - lib/experiment/local/client.rb
140
+ - lib/experiment/local/config.rb
141
+ - lib/experiment/local/evaluation/evaluation.rb
142
+ - lib/experiment/local/fetcher.rb
143
+ - lib/experiment/local/poller.rb
127
144
  - lib/experiment/persistent_http_client.rb
145
+ - lib/experiment/remote/client.rb
146
+ - lib/experiment/remote/config.rb
128
147
  - lib/experiment/user.rb
129
148
  - lib/experiment/variant.rb
130
149
  - lib/experiment/version.rb
data/lib/experiment.rb DELETED
@@ -1,12 +0,0 @@
1
- require 'experiment/version'
2
- require 'experiment/config'
3
- require 'experiment/cookie'
4
- require 'experiment/user'
5
- require 'experiment/variant'
6
- require 'experiment/factory'
7
- require 'experiment/persistent_http_client'
8
- require 'experiment/client'
9
-
10
- # Amplitude Experiment Module
11
- module Experiment
12
- end