amplitude-experiment 1.0.0.beta.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f67be77264977612b2728ee49db7923a26414cdeefc71d15059cbcd6564031ac
4
+ data.tar.gz: d59583f45788bea92e6c5855b0cb2f754a87a2b022784f6a3e89eca239173819
5
+ SHA512:
6
+ metadata.gz: fd383c6a79f4164fbc3897d12256da8c19b5824be542faa23b43384ad32aa781c6cfee5b4101d015cd6fbef5994818ddc8ddd77addd83f8522c9716063f1dd45
7
+ data.tar.gz: b0d7f6429e872a86027cd0f34b2e796b04be39cc672616e5e99e543b32635dbe8226c06356f941fcfd7e118d608a4d151ce80fa6b6be10cfa5e41ae08e656138
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,61 @@
1
+ <p align="center">
2
+ <a href="https://amplitude.com" target="_blank" align="center">
3
+ <img src="https://static.amplitude.com/lightning/46c85bfd91905de8047f1ee65c7c93d6fa9ee6ea/static/media/amplitude-logo-with-text.4fb9e463.svg" width="280">
4
+ </a>
5
+ <br />
6
+ </p>
7
+
8
+ # experiment-ruby-server
9
+ Ruby Server SDK for Experiment
10
+
11
+ ## Installation
12
+ Into Gemfile from rubygems.org:
13
+ ```ruby
14
+ gem 'amplitude-experiment'
15
+ ```
16
+ Into environment gems from rubygems.org:
17
+ ```ruby
18
+ gem install 'amplitude-experiment'
19
+ ```
20
+
21
+ ## Quick Start
22
+ ```ruby
23
+ require 'amplitude-experiment'
24
+
25
+ # (1) Get your deployment's API key
26
+ apiKey = 'YOUR-API-KEY'
27
+
28
+ # (2) Initialize the experiment client
29
+ experiment = Experiment.init(api_key)
30
+
31
+ # (3) Fetch variants for a user
32
+ user = Experiment::User.new(user_id: 'user@company.com', device_id: 'abcezas123', user_properties: {'premium' => true})
33
+
34
+ # (4) Lookup a flag's variant
35
+ #
36
+ # To fetch asynchronous
37
+ experiment.fetch_async(user) do |_, variants|
38
+ variant = variants['YOUR-FLAG-KEY']
39
+ unless variant.nil?
40
+ if variant.value == 'on'
41
+ # Flag is on
42
+ else
43
+ # Flag is off
44
+ end
45
+ end
46
+ end
47
+
48
+ # To fetch synchronous
49
+ variants = experiment.fetch(user)
50
+ variant = variants['YOUR-FLAG-KEY']
51
+ unless variant.nil?
52
+ if variant.value == 'on'
53
+ # Flag is on
54
+ else
55
+ # Flag is off
56
+ end
57
+ end
58
+ ```
59
+
60
+ ## Need Help?
61
+ If you have any problems or issues over our SDK, feel free to [create a github issue](https://github.com/amplitude/experiments-ruby-server/issues/new) or submit a request on [Amplitude Help](https://help.amplitude.com/hc/en-us/requests/new).
@@ -0,0 +1 @@
1
+ require 'experiment'
@@ -0,0 +1,146 @@
1
+ require 'net/http'
2
+ require 'json'
3
+ require 'uri'
4
+ require 'logger'
5
+
6
+ module Experiment
7
+ # Main client for fetching variant data.
8
+ class Client
9
+ # Creates a new Experiment Client instance.
10
+ #
11
+ # @param [String] api_key The environment API Key
12
+ # @param [Config] config
13
+ def initialize(api_key, config = nil)
14
+ @api_key = api_key
15
+ @config = config || Config.new
16
+ @logger = Logger.new($stdout)
17
+ @logger.level = if @config.debug
18
+ Logger::DEBUG
19
+ else
20
+ Logger::INFO
21
+ end
22
+ raise ArgumentError, 'Experiment API key is empty' if @api_key.nil? || @api_key.empty?
23
+ end
24
+
25
+ # Fetch all variants for a user synchronous.
26
+ #
27
+ # This method will automatically retry if configured (default).
28
+ # @param [User] user
29
+ # @return [Hash] Variants Hash
30
+ def fetch(user)
31
+ fetch_internal(user)
32
+ rescue StandardError => e
33
+ @logger.error("[Experiment] Failed to fetch variants: #{e.message}")
34
+ {}
35
+ end
36
+
37
+ # Fetch all variants for a user asynchronous.
38
+ #
39
+ # This method will automatically retry if configured (default).
40
+ # @param [User] user
41
+ def fetch_async(user, &callback)
42
+ Thread.new do
43
+ variants = fetch_internal(user)
44
+ yield(user, variants) unless callback.nil?
45
+ variants
46
+ rescue StandardError => e
47
+ @logger.error("[Experiment] Failed to fetch variants: #{e.message}")
48
+ yield(user, {}) unless callback.nil?
49
+ {}
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ # @param [User] user
56
+ def fetch_internal(user)
57
+ @logger.debug("[Experiment] Fetching variants for user: #{user.as_json}")
58
+ do_fetch(user, @config.fetch_timeout_millis)
59
+ rescue StandardError => e
60
+ @logger.error("[Experiment] Fetch failed: #{e.message}")
61
+ begin
62
+ return retry_fetch(user)
63
+ rescue StandardError => err
64
+ @logger.error("[Experiment] Retry Fetch failed: #{err.message}")
65
+ end
66
+ throw e
67
+ end
68
+
69
+ # @param [User] user
70
+ def retry_fetch(user)
71
+ return {} if @config.fetch_retries.zero?
72
+
73
+ @logger.debug('[Experiment] Retrying fetch')
74
+ delay_millis = @config.fetch_retry_backoff_min_millis
75
+ err = nil
76
+ @config.fetch_retries.times do
77
+ sleep(delay_millis / 1000.0)
78
+ begin
79
+ return do_fetch(user, @config.fetch_retry_timeout_millis)
80
+ rescue StandardError => e
81
+ @logger.error("[Experiment] Retry failed: #{e.message}")
82
+ err = e
83
+ end
84
+ delay_millis = [delay_millis * @config.fetch_retry_backoff_scalar, @config.fetch_retry_backoff_max_millis].min
85
+ end
86
+ throw err unless err.nil?
87
+ end
88
+
89
+ # @param [User] user
90
+ # @param [Integer] timeout_millis
91
+ def do_fetch(user, timeout_millis)
92
+ start_time = Time.now
93
+ user_context = add_context(user)
94
+ endpoint = "#{@config.server_url}/sdk/vardata"
95
+ headers = {
96
+ 'Authorization' => "Api-Key #{@api_key}",
97
+ 'Content-Type' => 'application/json;charset=utf-8'
98
+ }
99
+ uri = URI(endpoint)
100
+ http = Net::HTTP.new(uri.host, uri.port)
101
+ http.use_ssl = true
102
+ http.read_timeout = timeout_millis / 1000 if (timeout_millis / 1000) > 0
103
+ request = Net::HTTP::Post.new(uri, headers)
104
+ request.body = user_context.to_json
105
+ if request.body.length > 8000
106
+ @logger.warn("[Experiment] encoded user object length #{request.body.length} cannot be cached by CDN; must be < 8KB")
107
+ end
108
+ @logger.debug("[Experiment] Fetch variants for user: #{request.body}")
109
+ response = http.request(request)
110
+ end_time = Time.now
111
+ elapsed = (end_time - start_time) * 1000.0
112
+ @logger.debug("[Experiment] Fetch complete in #{elapsed.round(3)} ms")
113
+ json = JSON.parse(response.body)
114
+ variants = parse_json_variants(json)
115
+ @logger.debug("[Experiment] Fetched variants: #{variants}")
116
+ variants
117
+ end
118
+
119
+ # Parse JSON response hash
120
+ #
121
+ # @param [Hash] json
122
+ # @return [Hash] Hash with String => Variant
123
+ def parse_json_variants(json)
124
+ variants = {}
125
+ json.each do |key, value|
126
+ variant_value = ''
127
+ if value.key?('value')
128
+ variant_value = value.fetch('value')
129
+ elsif value.key?('key')
130
+ # value was previously under the "key" field
131
+ variant_value = value.fetch('key')
132
+ end
133
+ variants.store(key, Variant.new(variant_value, value.fetch('payload')))
134
+ end
135
+ variants
136
+ end
137
+
138
+ # @param [User] user
139
+ # @return [User, Hash] user with library context
140
+ def add_context(user)
141
+ user = {} if user.nil?
142
+ user.library = "experiment-ruby-server/#{VERSION}" if user.library.nil?
143
+ user
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,65 @@
1
+ module Experiment
2
+ # Configuration
3
+ class Config
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 [Boolean] the value of server url
13
+ attr_accessor :server_url
14
+
15
+ # The request timeout, in milliseconds, used when fetching variants triggered by calling start() or setUser().
16
+ # @return [Integer] the value of fetch_timeout_millis
17
+ attr_accessor :fetch_timeout_millis
18
+
19
+ # The number of retries to attempt before failing.
20
+ # @return [Integer] the value of fetch_retries
21
+ attr_accessor :fetch_retries
22
+
23
+ # Retry backoff minimum (starting backoff delay) in milliseconds. The minimum backoff is scaled
24
+ # by `fetch_retry_backoff_scalar` after each retry failure.
25
+ # @return [Integer] the value of fetch_retry_backoff_min_millis
26
+ attr_accessor :fetch_retry_backoff_min_millis
27
+
28
+ # Retry backoff maximum in milliseconds. If the scaled backoff is greater than the max,
29
+ # the max is used for all subsequent retries.
30
+ # @return [Integer] the value of fetch_retry_backoff_max_millis
31
+ attr_accessor :fetch_retry_backoff_max_millis
32
+
33
+ # Scales the minimum backoff exponentially.
34
+ # @return [Float] the value of fetch_retry_backoff_scalar
35
+ attr_accessor :fetch_retry_backoff_scalar
36
+
37
+ # The request timeout for retrying fetch requests.
38
+ # @return [Integer] the value of fetch_retry_timeout_millis
39
+ attr_accessor :fetch_retry_timeout_millis
40
+
41
+ # @param [Boolean] debug Set to true to log some extra information to the console.
42
+ # @param [String] server_url The server endpoint from which to request variants.
43
+ # @param [Integer] fetch_timeout_millis The request timeout, in milliseconds, used when fetching variants
44
+ # triggered by calling start() or setUser().
45
+ # @param [Integer] fetch_retries The number of retries to attempt before failing.
46
+ # @param [Integer] fetch_retry_backoff_min_millis Retry backoff minimum (starting backoff delay) in milliseconds.
47
+ # The minimum backoff is scaled by `fetch_retry_backoff_scalar` after each retry failure.
48
+ # @param [Integer] fetch_retry_backoff_max_millis Retry backoff maximum in milliseconds. If the scaled backoff is
49
+ # greater than the max, the max is used for all subsequent retries.
50
+ # @param [Float] fetch_retry_backoff_scalar Scales the minimum backoff exponentially.
51
+ # @param [Integer] fetch_retry_timeout_millis The request timeout for retrying fetch requests.
52
+ def initialize(debug: false, server_url: DEFAULT_SERVER_URL, fetch_timeout_millis: 10_000, fetch_retries: 0,
53
+ fetch_retry_backoff_min_millis: 500, fetch_retry_backoff_max_millis: 10_000,
54
+ fetch_retry_backoff_scalar: 1.5, fetch_retry_timeout_millis: 10_000)
55
+ @debug = debug
56
+ @server_url = server_url
57
+ @fetch_timeout_millis = fetch_timeout_millis
58
+ @fetch_retries = fetch_retries
59
+ @fetch_retry_backoff_min_millis = fetch_retry_backoff_min_millis
60
+ @fetch_retry_backoff_max_millis = fetch_retry_backoff_max_millis
61
+ @fetch_retry_backoff_scalar = fetch_retry_backoff_scalar
62
+ @fetch_retry_timeout_millis = fetch_retry_timeout_millis
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,41 @@
1
+ require 'base64'
2
+
3
+ module Experiment
4
+ # This class provides utility functions for parsing and handling identity from Amplitude cookies.
5
+ class AmplitudeCookie
6
+ # Get the cookie name that Amplitude sets for the provided
7
+ #
8
+ # @param [String] api_key The Amplitude API Key
9
+ # @return [String] The cookie name that Amplitude sets for the provided
10
+ def self.cookie_name(api_key)
11
+ raise ArgumentError, 'Invalid Amplitude API Key' if api_key.nil? || api_key.length < 6
12
+
13
+ "amp_#{api_key[0..5]}"
14
+ end
15
+
16
+ # Parse a cookie string and returns user
17
+ #
18
+ # @param [String] amplitude_cookie A string from the amplitude cookie
19
+ # @return [User] a Experiment User context containing a device_id and user_id
20
+ def self.parse(amplitude_cookie)
21
+ values = amplitude_cookie.split('.', -1)
22
+ user_id = nil
23
+ unless values[1].nil? || values[1].empty?
24
+ begin
25
+ user_id = Base64.decode64(values[1]).force_encoding('UTF-8')
26
+ rescue StandardError
27
+ user_id = nil
28
+ end
29
+ end
30
+ User.new(user_id: user_id, device_id: values[0])
31
+ end
32
+
33
+ # Generates a cookie string to set for the Amplitude Javascript SDK
34
+ #
35
+ # @param [String] device_id A device id to set
36
+ # @return [String] A cookie string to set for the Amplitude Javascript SDK to read
37
+ def self.generate(device_id)
38
+ "#{device_id}.........."
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,15 @@
1
+ # Provides factory methods for storing singleton instance of Client
2
+ module Experiment
3
+ @instances = {}
4
+ @default_instance = '$default_instance'
5
+
6
+ # Initializes a singleton Client. This method returns a default singleton instance, subsequent calls to
7
+ # init will return the initial instance regardless of input.
8
+ #
9
+ # @param [String] api_key The environment API Key
10
+ # @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)
14
+ end
15
+ end
@@ -0,0 +1,137 @@
1
+ module Experiment
2
+ # Defines a user context for evaluation.
3
+ # `device_id` and `user_id` are used for identity resolution.
4
+ # All other predefined fields and user properties are used for
5
+ # rule based user targeting.
6
+ class User
7
+ # Device ID for associating with an identity in Amplitude
8
+ # @return [String, nil] the value of device id
9
+ attr_accessor :device_id
10
+
11
+ # User ID for associating with an identity in Amplitude
12
+ # @return [String, nil] the value of user id
13
+ attr_accessor :user_id
14
+
15
+ # Predefined field, must be manually provided
16
+ # @return [String, nil] the value of country
17
+ attr_accessor :country
18
+
19
+ # Predefined field, must be manually provided
20
+ # @return [String, nil] the value of city
21
+ attr_accessor :city
22
+
23
+ # Predefined field, must be manually provided
24
+ # @return [String, nil] the value of region
25
+ attr_accessor :region
26
+
27
+ # Predefined field, must be manually provided
28
+ # @return [String, nil] the value of dma
29
+ attr_accessor :dma
30
+
31
+ # Predefined field, must be manually provided
32
+ # @return [String, nil] the value of language
33
+ attr_accessor :language
34
+
35
+ # Predefined field, must be manually provided
36
+ # @return [String, nil] the value of platform
37
+ attr_accessor :platform
38
+
39
+ # Predefined field, must be manually provided
40
+ # @return [String, nil] the value of version
41
+ attr_accessor :version
42
+
43
+ # Predefined field, must be manually provided
44
+ # @return [String, nil] the value of os
45
+ attr_accessor :os
46
+
47
+ # Predefined field, must be manually provided
48
+ # @return [String, nil] the value of device manufacturer
49
+ attr_accessor :device_manufacturer
50
+
51
+ # Predefined field, must be manually provided
52
+ # @return [String, nil] the value of device brand
53
+ attr_accessor :device_brand
54
+
55
+ # Predefined field, must be manually provided
56
+ # @return [String, nil] the value of device model
57
+ attr_accessor :device_model
58
+
59
+ # Predefined field, must be manually provided
60
+ # @return [String, nil] the value of carrier
61
+ attr_accessor :carrier
62
+
63
+ # Predefined field, auto populated, can be manually overridden
64
+ # @return [String, nil] the value of library
65
+ attr_accessor :library
66
+
67
+ # Custom user properties
68
+ # @return [Hash, nil] the value of user properties
69
+ attr_accessor :user_properties
70
+
71
+ # @param [String, nil] device_id Device ID for associating with an identity in Amplitude
72
+ # @param [String, nil] user_id User ID for associating with an identity in Amplitude
73
+ # @param [String, nil] country Predefined field, must be manually provided
74
+ # @param [String, nil] city Predefined field, must be manually provided
75
+ # @param [String, nil] region Predefined field, must be manually provided
76
+ # @param [String, nil] dma Predefined field, must be manually provided
77
+ # @param [String, nil] language Predefined field, must be manually provided
78
+ # @param [String, nil] platform Predefined field, must be manually provided
79
+ # @param [String, nil] version Predefined field, must be manually provided
80
+ # @param [String, nil] os Predefined field, must be manually provided
81
+ # @param [String, nil] device_manufacturer Predefined field, must be manually provided
82
+ # @param [String, nil] device_brand Predefined field, must be manually provided
83
+ # @param [String, nil] device_model Predefined field, must be manually provided
84
+ # @param [String, nil] carrier Predefined field, must be manually provided
85
+ # @param [String, nil] library Predefined field, auto populated, can be manually overridden
86
+ # @param [Hash, nil] user_properties Custom user properties
87
+ def initialize(device_id: nil, user_id: nil, country: nil, city: nil, region: nil, dma: nil, language: nil,
88
+ platform: nil, version: nil, os: nil, device_manufacturer: nil, device_brand: nil,
89
+ device_model: nil, carrier: nil, library: nil, user_properties: nil)
90
+ @device_id = device_id
91
+ @user_id = user_id
92
+ @country = country
93
+ @city = city
94
+ @region = region
95
+ @dma = dma
96
+ @language = language
97
+ @platform = platform
98
+ @version = version
99
+ @os = os
100
+ @device_manufacturer = device_manufacturer
101
+ @device_brand = device_brand
102
+ @device_model = device_model
103
+ @carrier = carrier
104
+ @library = library
105
+ @user_properties = user_properties
106
+ end
107
+
108
+ # Return User as Hash.
109
+ # @return [Hash] Hash object with user values
110
+ def as_json(_options = {})
111
+ {
112
+ device_id: @device_id,
113
+ user_id: @user_id,
114
+ country: @country,
115
+ city: @city,
116
+ region: @region,
117
+ dma: @dma,
118
+ language: @language,
119
+ platform: @platform,
120
+ version: @version,
121
+ os: @os,
122
+ device_manufacturer: @device_manufacturer,
123
+ device_brand: @device_brand,
124
+ device_model: @device_model,
125
+ carrier: @carrier,
126
+ library: @library,
127
+ user_properties: @user_properties
128
+ }
129
+ end
130
+
131
+ # Return user information as JSON string.
132
+ # @return [String] details about user as json string
133
+ def to_json(*options)
134
+ as_json(*options).to_json(*options)
135
+ end
136
+ end
137
+ end
@@ -0,0 +1,26 @@
1
+ module Experiment
2
+ # Variant
3
+ class Variant
4
+ # The value of the variant determined by the flag configuration.
5
+ # @return [String] the value of variant value
6
+ attr_accessor :value
7
+
8
+ # The attached payload, if any.
9
+ # @return [Object, nil] the value of variant payload
10
+ attr_accessor :payload
11
+
12
+ # @param [String] value The value of the variant determined by the flag configuration.
13
+ # @param [Object, nil] payload The attached payload, if any.
14
+ def initialize(value, payload = nil)
15
+ @value = value
16
+ @payload = payload
17
+ end
18
+
19
+ # Determine if current variant equal other variant
20
+ # @param [Variant] other
21
+ def ==(other)
22
+ value == other.value &&
23
+ payload == other.payload
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,3 @@
1
+ module Experiment
2
+ VERSION = '1.0.0.beta.1'.freeze
3
+ end
data/lib/experiment.rb ADDED
@@ -0,0 +1,10 @@
1
+ require 'experiment/config'
2
+ require 'experiment/cookie'
3
+ require 'experiment/user'
4
+ require 'experiment/variant'
5
+ require 'experiment/factory'
6
+ require 'experiment/client'
7
+
8
+ # Amplitude Experiment Module
9
+ module Experiment
10
+ end
metadata ADDED
@@ -0,0 +1,154 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: amplitude-experiment
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.beta.1
5
+ platform: ruby
6
+ authors:
7
+ - Amplitude
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rake
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '13.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '13.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rdoc
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '6.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '6.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.6'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.6'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.21'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.21'
83
+ - !ruby/object:Gem::Dependency
84
+ name: webmock
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.14'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.14'
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '0.9'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '0.9'
111
+ description: Amplitude Experiment Ruby Server SDK
112
+ email:
113
+ - sdk@amplitude.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files:
117
+ - README.md
118
+ files:
119
+ - Gemfile
120
+ - README.md
121
+ - lib/amplitude-experiment.rb
122
+ - lib/experiment.rb
123
+ - lib/experiment/client.rb
124
+ - lib/experiment/config.rb
125
+ - lib/experiment/cookie.rb
126
+ - lib/experiment/factory.rb
127
+ - lib/experiment/user.rb
128
+ - lib/experiment/variant.rb
129
+ - lib/experiment/version.rb
130
+ homepage: https://github.com/amplitude/experiment-ruby-server
131
+ licenses:
132
+ - MIT
133
+ metadata:
134
+ rubygems_mfa_required: 'false'
135
+ post_install_message:
136
+ rdoc_options: []
137
+ require_paths:
138
+ - lib
139
+ required_ruby_version: !ruby/object:Gem::Requirement
140
+ requirements:
141
+ - - ">="
142
+ - !ruby/object:Gem::Version
143
+ version: '2.0'
144
+ required_rubygems_version: !ruby/object:Gem::Requirement
145
+ requirements:
146
+ - - ">"
147
+ - !ruby/object:Gem::Version
148
+ version: 1.3.1
149
+ requirements: []
150
+ rubygems_version: 3.1.6
151
+ signing_key:
152
+ specification_version: 4
153
+ summary: Amplitude Experiment Ruby Server SDK
154
+ test_files: []