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:
- hash input = +experience_id + String(visitor_id)+ (experience FIRST, no delimiter) — JS +bucketing-manager.ts:97+, PHP +BucketingManager.php:89+.
- +hash = MurmurHash3.hash(input, seed)+ — the proven Story 1.2 module; never reimplemented here.
- +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).
- +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 +nil+ when 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 |