amplitude-experiment 1.8.0 → 1.9.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/lib/amplitude-experiment.rb +5 -0
- data/lib/experiment/local/assignment/assignment.rb +1 -0
- data/lib/experiment/local/assignment/assignment_config.rb +1 -0
- data/lib/experiment/local/assignment/assignment_filter.rb +1 -0
- data/lib/experiment/local/assignment/assignment_service.rb +1 -0
- data/lib/experiment/local/client.rb +9 -1
- data/lib/experiment/local/config.rb +9 -1
- data/lib/experiment/local/evaluate_options.rb +10 -0
- data/lib/experiment/local/exposure/exposure.rb +22 -0
- data/lib/experiment/local/exposure/exposure_config.rb +12 -0
- data/lib/experiment/local/exposure/exposure_filter.rb +20 -0
- data/lib/experiment/local/exposure/exposure_service.rb +71 -0
- data/lib/experiment/version.rb +1 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d1474764e3ae979eb8f3799a76f42513ff707a7307d7dc2eae9d9d6da4bef36f
|
|
4
|
+
data.tar.gz: 889f7ba4cc9860523c7a5b6854b19e75b54ac8ae6330414007142779cbe7f2f3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 705c6b31291bf96b3f9ea93ea3c166aa8e61f28b80bf0253261d42374895e1b8f923b3536b9308a3cd03b2ba9f7d4654abdfd2e2894fe19a802d3f658b454e3a
|
|
7
|
+
data.tar.gz: ff192b072846e12ede79277411bde7e38075be600707138a8a25090c6e325425aa1ed72c10b09fdda9c9a1e1c4dcb409c32f06ed26e3b36a27b4db0127ceeecc
|
data/lib/amplitude-experiment.rb
CHANGED
|
@@ -9,10 +9,15 @@ require 'experiment/remote/client'
|
|
|
9
9
|
require 'experiment/remote/fetch_options'
|
|
10
10
|
require 'experiment/local/client'
|
|
11
11
|
require 'experiment/local/config'
|
|
12
|
+
require 'experiment/local/evaluate_options'
|
|
12
13
|
require 'experiment/local/assignment/assignment'
|
|
13
14
|
require 'experiment/local/assignment/assignment_filter'
|
|
14
15
|
require 'experiment/local/assignment/assignment_service'
|
|
15
16
|
require 'experiment/local/assignment/assignment_config'
|
|
17
|
+
require 'experiment/local/exposure/exposure'
|
|
18
|
+
require 'experiment/local/exposure/exposure_filter'
|
|
19
|
+
require 'experiment/local/exposure/exposure_service'
|
|
20
|
+
require 'experiment/local/exposure/exposure_config'
|
|
16
21
|
require 'experiment/util/lru_cache'
|
|
17
22
|
require 'experiment/util/hash'
|
|
18
23
|
require 'experiment/util/user'
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require_relative '../../../amplitude'
|
|
2
2
|
module AmplitudeExperiment
|
|
3
3
|
# AssignmentService
|
|
4
|
+
# @deprecated Assignment tracking is deprecated. Use ExposureService with Exposure tracking instead.
|
|
4
5
|
class AssignmentService
|
|
5
6
|
def initialize(amplitude, assignment_filter)
|
|
6
7
|
@amplitude = amplitude
|
|
@@ -26,6 +26,10 @@ module AmplitudeExperiment
|
|
|
26
26
|
@assignment_service = nil
|
|
27
27
|
@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
|
|
28
28
|
|
|
29
|
+
# Exposure service is always instantiated, using deployment key if no api key provided
|
|
30
|
+
@exposure_service = nil
|
|
31
|
+
@exposure_service = ExposureService.new(AmplitudeAnalytics::Amplitude.new(config.exposure_config.api_key, configuration: config.exposure_config), ExposureFilter.new(config.exposure_config.cache_capacity)) if config&.exposure_config
|
|
32
|
+
|
|
29
33
|
@cohort_storage = InMemoryCohortStorage.new
|
|
30
34
|
@flag_config_storage = InMemoryFlagConfigStorage.new
|
|
31
35
|
@flag_config_fetcher = LocalEvaluationFetcher.new(@api_key, @logger, @config.server_url)
|
|
@@ -53,6 +57,7 @@ module AmplitudeExperiment
|
|
|
53
57
|
AmplitudeExperiment.filter_default_variants(variants)
|
|
54
58
|
end
|
|
55
59
|
|
|
60
|
+
# TODO: ruby backwards compatibility for evaluate_v2 to be looked at again
|
|
56
61
|
# Locally evaluates flag variants for a user.
|
|
57
62
|
# This function will only evaluate flags for the keys specified in the flag_keys argument. If flag_keys is
|
|
58
63
|
# missing or None, all flags are evaluated. This function differs from evaluate as it will return a default
|
|
@@ -60,8 +65,9 @@ module AmplitudeExperiment
|
|
|
60
65
|
#
|
|
61
66
|
# @param [User] user The user to evaluate
|
|
62
67
|
# @param [String[]] flag_keys The flags to evaluate with the user, if empty all flags are evaluated
|
|
68
|
+
# @param [EvaluateOptions] options Optional evaluation options
|
|
63
69
|
# @return [Hash[String, Variant]] The evaluated variants
|
|
64
|
-
def evaluate_v2(user, flag_keys = [])
|
|
70
|
+
def evaluate_v2(user, flag_keys = [], options = nil)
|
|
65
71
|
flags = @flag_config_storage.flag_configs
|
|
66
72
|
return {} if flags.nil?
|
|
67
73
|
|
|
@@ -74,6 +80,8 @@ module AmplitudeExperiment
|
|
|
74
80
|
result = @engine.evaluate(context, sorted_flags)
|
|
75
81
|
@logger.debug("[Experiment] evaluate - result: #{result}") if @config.debug
|
|
76
82
|
variants = AmplitudeExperiment.evaluation_variants_json_to_variants(result)
|
|
83
|
+
@exposure_service&.track(Exposure.new(user, variants)) if options&.tracks_exposure == true
|
|
84
|
+
# @deprecated Assignment tracking is deprecated. Use ExposureService with Exposure tracking instead.
|
|
77
85
|
@assignment_service&.track(Assignment.new(user, variants))
|
|
78
86
|
variants
|
|
79
87
|
end
|
|
@@ -35,9 +35,14 @@ module AmplitudeExperiment
|
|
|
35
35
|
attr_accessor :flag_config_polling_interval_millis
|
|
36
36
|
|
|
37
37
|
# Configuration for automatically tracking assignment events after an evaluation.
|
|
38
|
+
# @deprecated use exposure_config instead
|
|
38
39
|
# @return [AssignmentConfig] the config instance
|
|
39
40
|
attr_accessor :assignment_config
|
|
40
41
|
|
|
42
|
+
# Configuration for automatically tracking exposure events after an evaluation.
|
|
43
|
+
# @return [ExposureConfig] the config instance
|
|
44
|
+
attr_accessor :exposure_config
|
|
45
|
+
|
|
41
46
|
# Configuration for downloading cohorts required for flag evaluation
|
|
42
47
|
# @return [CohortSyncConfig] the config instance
|
|
43
48
|
attr_accessor :cohort_sync_config
|
|
@@ -48,7 +53,8 @@ module AmplitudeExperiment
|
|
|
48
53
|
# @param [String] server_zone Location of the Amplitude data center to get flags and cohorts from, US or EU
|
|
49
54
|
# @param [Hash] bootstrap The value of bootstrap.
|
|
50
55
|
# @param [long] flag_config_polling_interval_millis The value of flag config polling interval in million seconds.
|
|
51
|
-
# @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation.
|
|
56
|
+
# @param [AssignmentConfig] assignment_config Configuration for automatically tracking assignment events after an evaluation. @deprecated use exposure_config instead
|
|
57
|
+
# @param [ExposureConfig] exposure_config Configuration for automatically tracking exposure events after an evaluation.
|
|
52
58
|
# @param [CohortSyncConfig] cohort_sync_config Configuration for downloading cohorts required for flag evaluation
|
|
53
59
|
def initialize(server_url: DEFAULT_SERVER_URL,
|
|
54
60
|
server_zone: ServerZone::US,
|
|
@@ -57,6 +63,7 @@ module AmplitudeExperiment
|
|
|
57
63
|
debug: false,
|
|
58
64
|
logger: nil,
|
|
59
65
|
assignment_config: nil,
|
|
66
|
+
exposure_config: nil,
|
|
60
67
|
cohort_sync_config: nil)
|
|
61
68
|
@logger = logger
|
|
62
69
|
if logger.nil?
|
|
@@ -73,6 +80,7 @@ module AmplitudeExperiment
|
|
|
73
80
|
@bootstrap = bootstrap
|
|
74
81
|
@flag_config_polling_interval_millis = flag_config_polling_interval_millis
|
|
75
82
|
@assignment_config = assignment_config
|
|
83
|
+
@exposure_config = exposure_config
|
|
76
84
|
end
|
|
77
85
|
end
|
|
78
86
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module AmplitudeExperiment
|
|
2
|
+
# Exposure is a class that represents a user's exposure to a set of flags.
|
|
3
|
+
class Exposure
|
|
4
|
+
attr_accessor :user, :results, :timestamp
|
|
5
|
+
|
|
6
|
+
def initialize(user, results)
|
|
7
|
+
@user = user
|
|
8
|
+
@results = results
|
|
9
|
+
@timestamp = (Time.now.to_f * 1000).to_i
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def canonicalize
|
|
13
|
+
sb = "#{@user&.user_id&.strip} #{@user&.device_id&.strip} "
|
|
14
|
+
results.sort.to_h.each do |key, value|
|
|
15
|
+
next unless value.key
|
|
16
|
+
|
|
17
|
+
sb += "#{key.strip} #{value.key&.strip} "
|
|
18
|
+
end
|
|
19
|
+
sb
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module AmplitudeExperiment
|
|
2
|
+
# ExposureConfig
|
|
3
|
+
class ExposureConfig < AmplitudeAnalytics::Config
|
|
4
|
+
attr_accessor :api_key, :cache_capacity
|
|
5
|
+
|
|
6
|
+
def initialize(api_key = nil, cache_capacity = 65_536, **kwargs)
|
|
7
|
+
super(**kwargs)
|
|
8
|
+
@api_key = api_key
|
|
9
|
+
@cache_capacity = cache_capacity
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module AmplitudeExperiment
|
|
2
|
+
# ExposureFilter
|
|
3
|
+
class ExposureFilter
|
|
4
|
+
attr_accessor :ttl_millis
|
|
5
|
+
|
|
6
|
+
def initialize(size, ttl_millis = DAY_MILLIS)
|
|
7
|
+
@cache = LRUCache.new(size, ttl_millis)
|
|
8
|
+
@ttl_millis = ttl_millis
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def should_track(exposure)
|
|
12
|
+
return false if exposure.results.empty?
|
|
13
|
+
|
|
14
|
+
canonical_exposure = exposure.canonicalize
|
|
15
|
+
track = @cache.get(canonical_exposure).nil?
|
|
16
|
+
@cache.put(canonical_exposure, 0) if track
|
|
17
|
+
track
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
require_relative '../../../amplitude'
|
|
2
|
+
module AmplitudeExperiment
|
|
3
|
+
# ExposureService
|
|
4
|
+
class ExposureService
|
|
5
|
+
def initialize(amplitude, exposure_filter)
|
|
6
|
+
@amplitude = amplitude
|
|
7
|
+
@exposure_filter = exposure_filter
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def track(exposure)
|
|
11
|
+
return unless @exposure_filter.should_track(exposure)
|
|
12
|
+
|
|
13
|
+
events = ExposureService.to_exposure_events(exposure, @exposure_filter.ttl_millis)
|
|
14
|
+
events.each do |event|
|
|
15
|
+
@amplitude.track(event)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.to_exposure_events(exposure, ttl_millis)
|
|
20
|
+
events = []
|
|
21
|
+
canonicalized = exposure.canonicalize
|
|
22
|
+
exposure.results.each do |flag_key, variant|
|
|
23
|
+
track_exposure = variant.metadata ? variant.metadata.fetch('trackExposure', true) : true
|
|
24
|
+
next unless track_exposure
|
|
25
|
+
|
|
26
|
+
# Skip default variant exposures
|
|
27
|
+
is_default = variant.metadata ? variant.metadata.fetch('default', false) : false
|
|
28
|
+
next if is_default
|
|
29
|
+
|
|
30
|
+
# Determine user properties to set and unset.
|
|
31
|
+
set_props = {}
|
|
32
|
+
unset_props = {}
|
|
33
|
+
flag_type = variant.metadata['flagType'] if variant.metadata
|
|
34
|
+
if flag_type != 'mutual-exclusion-group'
|
|
35
|
+
if variant.key
|
|
36
|
+
set_props["[Experiment] #{flag_key}"] = variant.key
|
|
37
|
+
elsif variant.value
|
|
38
|
+
set_props["[Experiment] #{flag_key}"] = variant.value
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Build event properties.
|
|
43
|
+
event_properties = {}
|
|
44
|
+
event_properties['[Experiment] Flag Key'] = flag_key
|
|
45
|
+
if variant.key
|
|
46
|
+
event_properties['[Experiment] Variant'] = variant.key
|
|
47
|
+
elsif variant.value
|
|
48
|
+
event_properties['[Experiment] Variant'] = variant.value
|
|
49
|
+
end
|
|
50
|
+
event_properties['metadata'] = variant.metadata if variant.metadata
|
|
51
|
+
|
|
52
|
+
# Build event.
|
|
53
|
+
event = AmplitudeAnalytics::BaseEvent.new(
|
|
54
|
+
'[Experiment] Exposure',
|
|
55
|
+
user_id: exposure.user.user_id,
|
|
56
|
+
device_id: exposure.user.device_id,
|
|
57
|
+
event_properties: event_properties,
|
|
58
|
+
user_properties: {
|
|
59
|
+
'$set' => set_props,
|
|
60
|
+
'$unset' => unset_props
|
|
61
|
+
},
|
|
62
|
+
insert_id: "#{exposure.user.user_id} #{exposure.user.device_id} #{AmplitudeExperiment.hash_code("#{flag_key} #{canonicalized}")} #{exposure.timestamp / ttl_millis}"
|
|
63
|
+
)
|
|
64
|
+
event.groups = exposure.user.groups if exposure.user.groups
|
|
65
|
+
|
|
66
|
+
events << event
|
|
67
|
+
end
|
|
68
|
+
events
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
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.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Amplitude
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date:
|
|
11
|
+
date: 2026-01-07 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: concurrent-ruby
|
|
@@ -213,6 +213,11 @@ files:
|
|
|
213
213
|
- lib/experiment/local/assignment/assignment_service.rb
|
|
214
214
|
- lib/experiment/local/client.rb
|
|
215
215
|
- lib/experiment/local/config.rb
|
|
216
|
+
- lib/experiment/local/evaluate_options.rb
|
|
217
|
+
- lib/experiment/local/exposure/exposure.rb
|
|
218
|
+
- lib/experiment/local/exposure/exposure_config.rb
|
|
219
|
+
- lib/experiment/local/exposure/exposure_filter.rb
|
|
220
|
+
- lib/experiment/local/exposure/exposure_service.rb
|
|
216
221
|
- lib/experiment/persistent_http_client.rb
|
|
217
222
|
- lib/experiment/remote/client.rb
|
|
218
223
|
- lib/experiment/remote/config.rb
|