Class: ConvertSdk::BucketingManager Private
- Inherits:
-
Object
- Object
- ConvertSdk::BucketingManager
- Defined in:
- lib/convert_sdk/bucketing_manager.rb
Overview
This class is part of a private API. You should avoid using this class if possible, as it may be removed or be changed in the future.
Deterministic visitor bucketing — the cross-SDK variation-assignment engine.
Given an experience id, a visitor id, and a caller-built buckets hash
(variation id => traffic percentage), this resolves a variation
BYTE-IDENTICALLY to the JS SDK bucketing-manager.ts and the proven PHP
port BucketingManager.php. A visitor MUST bucket into the same variation on
web (JS), PHP, and Ruby — the cross-SDK distribution spec is the CI proof.
The pipeline mirrors JS exactly, link for link:
1. hash input = +experience_id + String(visitor_id)+ (experience FIRST, no
delimiter) — JS +bucketing-manager.ts:97+, PHP +BucketingManager.php:89+.
2. +hash = MurmurHash3.hash(input, seed)+ — the proven Story 1.2 module;
never reimplemented here.
3. +value = ((hash / 4_294_967_296.0) * max_traffic).to_i+ — float division
then multiply then truncate, operation ORDER preserved. Ruby Float is
IEEE-754 double like JS Number, and +Integer()+-via-+to_i+ truncates
toward zero, matching JS +parseInt(String(val), 10)+ at +bm.ts:99+
(behaviourally floor for all non-negative hash values).
4. +select_bucket+ walks variation cumulative ranges in insertion order:
+prev += pct * 100 + redistribute+; the first variation satisfying the
STRICT upper-bound +value < prev+ wins — JS +bm.ts:60-85+, PHP
+BucketingManager.php:50-72+. No covering range => +nil+ (the caller
treats +nil+ as VARIATION_NOT_DECIDED).
Traffic allocation is NOT this class's concern: the caller
(ExperienceManager/DataManager) constructs buckets with only the
traffic-allocated variations before invoking. BucketingManager is
allocation-agnostic and answers one question deterministically: "given this
experience config and this visitor id, which variation?"
Pure in-memory computation (NFR1) — no I/O, no store access. Bucketing
constants (+max_traffic+, hash_seed, max_hash) come from the injected
Config, never inline literals. Logging stays at debug for the decisioning
internals (FR56); never-crash is the caller's contract, but the class rescues
nothing here because its inputs are caller-validated.
Instance Method Summary collapse
-
#bucket_for_visitor(buckets, visitor_id, experience_id: "", seed: @hash_seed, redistribute: 0) ⇒ Hash{Symbol=>Object}?
private
Resolve a visitor to a variation, returning the assignment and its bucket value, or
nilwhen no variation range covers the visitor. -
#initialize(config:, log_manager: nil) ⇒ BucketingManager
constructor
private
Build a bucketing engine bound to a Config's frozen bucketing constants.
-
#select_bucket(buckets, value, redistribute = 0) ⇒ String?
private
Select the variation whose cumulative range contains
value. -
#value_visitor_based(visitor_id, experience_id: "", seed: @hash_seed) ⇒ Integer
private
Compute the deterministic bucket value for a visitor.
Constructor Details
#initialize(config:, log_manager: nil) ⇒ BucketingManager
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Build a bucketing engine bound to a Config's frozen bucketing constants.
47 48 49 50 51 52 |
# File 'lib/convert_sdk/bucketing_manager.rb', line 47 def initialize(config:, log_manager: nil) @max_traffic = config.max_traffic @hash_seed = config.hash_seed @max_hash = config.max_hash.to_f @log_manager = log_manager end |
Instance Method Details
#bucket_for_visitor(buckets, visitor_id, experience_id: "", seed: @hash_seed, redistribute: 0) ⇒ Hash{Symbol=>Object}?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Resolve a visitor to a variation, returning the assignment and its bucket
value, or nil when no variation range covers the visitor.
119 120 121 122 123 124 125 126 127 128 129 130 131 132 |
# File 'lib/convert_sdk/bucketing_manager.rb', line 119 def bucket_for_visitor(buckets, visitor_id, experience_id: "", seed: @hash_seed, redistribute: 0) value = value_visitor_based(visitor_id, experience_id: experience_id, seed: seed) selected = select_bucket(buckets, value, redistribute) @log_manager&.debug( "BucketingManager#bucket_for_visitor: " \ "experience_id=#{experience_id.inspect} visitor_id=#{visitor_id.inspect} " \ "bucket_value=#{value} selected_variation_id=#{selected.inspect}" ) return nil if selected.nil? { variation_id: selected, bucketing_allocation: value } end |
#select_bucket(buckets, value, redistribute = 0) ⇒ String?
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Select the variation whose cumulative range contains value.
Walks buckets in insertion order accumulating +pct * 100 + redistribute+
per entry, returning the first variation id satisfying the strict
upper-bound value < prev. Returns nil when no range covers value
(including an empty buckets hash).
87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/convert_sdk/bucketing_manager.rb', line 87 def select_bucket(buckets, value, redistribute = 0) variation = nil # Float accumulator: JS does `prev += buckets[id]*100 + redistribute` in # IEEE-754 double arithmetic (bm.ts:68). Ruby Float is the same double, so # accumulating in Float mirrors JS exactly. value (Integer) < prev (Float) # compares identically to the JS strict upper-bound check. prev = 0.0 buckets.each do |variation_id, percentage| prev += (percentage.to_f * 100) + redistribute if value < prev variation = variation_id break end end @log_manager&.debug( "BucketingManager#select_bucket: " \ "value=#{value} redistribute=#{redistribute} variation=#{variation.inspect}" ) variation end |
#value_visitor_based(visitor_id, experience_id: "", seed: @hash_seed) ⇒ Integer
This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.
Compute the deterministic bucket value for a visitor.
62 63 64 65 66 67 68 69 70 71 72 73 74 |
# File 'lib/convert_sdk/bucketing_manager.rb', line 62 def value_visitor_based(visitor_id, experience_id: "", seed: @hash_seed) input = "#{experience_id}#{visitor_id}" hash = MurmurHash3.hash(input, seed) scaled = (hash / @max_hash) * @max_traffic result = scaled.to_i @log_manager&.debug( "BucketingManager#value_visitor_based: " \ "experience_id=#{experience_id.inspect} visitor_id=#{visitor_id.inspect} " \ "seed=#{seed} hash=#{hash} scaled=#{scaled} result=#{result}" ) result end |