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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f40750aa31cc9abd90da453ed66d9c8af7ab7732d4887b248e47b9a878a159a7
4
- data.tar.gz: 87064809c68b48a71ec4769f36fb797fc6757b763626ceb9e6a5e5a1d5656049
3
+ metadata.gz: d1474764e3ae979eb8f3799a76f42513ff707a7307d7dc2eae9d9d6da4bef36f
4
+ data.tar.gz: 889f7ba4cc9860523c7a5b6854b19e75b54ac8ae6330414007142779cbe7f2f3
5
5
  SHA512:
6
- metadata.gz: c235e7a780773a4ea5873a1139917fdb3929b4bb27139a02a960f040c863226cc01f0ac3188fc4628c45897d2823c41ba383dc1d9f517f15b69c671d971b6397
7
- data.tar.gz: cd4d84c2d088dfc265910c7a57147f9d54ef34d747959d172ff1623b21f96b85f1ff409bdde06900aedec6a15941e19b2a0d83da5f0ff8d8e09b4dc1027f08d4
6
+ metadata.gz: 705c6b31291bf96b3f9ea93ea3c166aa8e61f28b80bf0253261d42374895e1b8f923b3536b9308a3cd03b2ba9f7d4654abdfd2e2894fe19a802d3f658b454e3a
7
+ data.tar.gz: ff192b072846e12ede79277411bde7e38075be600707138a8a25090c6e325425aa1ed72c10b09fdda9c9a1e1c4dcb409c32f06ed26e3b36a27b4db0127ceeecc
@@ -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
  module AmplitudeExperiment
2
2
  DAY_MILLIS = 86_400_000
3
3
  # Assignment
4
+ # @deprecated Assignment tracking is deprecated. Use Exposure with ExposureService instead.
4
5
  class Assignment
5
6
  attr_accessor :user, :results, :timestamp
6
7
 
@@ -1,5 +1,6 @@
1
1
  module AmplitudeExperiment
2
2
  # AssignmentConfig
3
+ # @deprecated Assignment tracking is deprecated. Use ExposureConfig with ExposureService instead.
3
4
  class AssignmentConfig < AmplitudeAnalytics::Config
4
5
  attr_accessor :api_key, :cache_capacity
5
6
 
@@ -1,5 +1,6 @@
1
1
  module AmplitudeExperiment
2
2
  # AssignmentFilter
3
+ # @deprecated Assignment tracking is deprecated. Use ExposureFilter with ExposureService instead.
3
4
  class AssignmentFilter
4
5
  def initialize(size, ttl_millis = DAY_MILLIS)
5
6
  @cache = LRUCache.new(size, ttl_millis)
@@ -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,10 @@
1
+ module AmplitudeExperiment
2
+ # Options for evaluating variants for a user.
3
+ class EvaluateOptions
4
+ attr_accessor :tracks_exposure
5
+
6
+ def initialize(tracks_exposure: nil)
7
+ @tracks_exposure = tracks_exposure
8
+ end
9
+ end
10
+ 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
@@ -1,3 +1,3 @@
1
1
  module AmplitudeExperiment
2
- VERSION = '1.8.0'.freeze
2
+ VERSION = '1.9.0'.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.8.0
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: 2025-12-15 00:00:00.000000000 Z
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