Class: ConvertSdk::BucketingManager Private

Inherits:
Object
  • Object
show all
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

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.

Parameters:

  • config (Config)

    supplies max_traffic, hash_seed, max_hash.

  • log_manager (LogManager, nil) (defaults to: nil)

    optional debug logger for decisioning internals; absent in lean unit contexts.



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.

Parameters:

  • buckets (Hash{String=>Numeric})

    variation id => traffic percentage.

  • visitor_id (#to_s)

    the visitor identifier.

  • experience_id (String) (defaults to: "")

    the experience identifier (default "").

  • seed (Integer) (defaults to: @hash_seed)

    MurmurHash3 seed (default Config hash seed).

  • redistribute (Numeric) (defaults to: 0)

    per-bucket widening offset (default 0).

Returns:

  • (Hash{Symbol=>Object}, nil)

    {variation_id:, bucketing_allocation:} or nil (caller treats nil as VARIATION_NOT_DECIDED).



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).

Parameters:

  • buckets (Hash{String=>Numeric})

    variation id => traffic percentage.

  • value (Integer)

    a bucket value in [0, max_traffic).

  • redistribute (Numeric) (defaults to: 0)

    per-bucket widening offset (default 0).

Returns:

  • (String, nil)

    the selected variation id, or nil.



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.

Parameters:

  • visitor_id (#to_s)

    the visitor identifier (coerced via String() before hashing, matching JS String(visitorId)).

  • experience_id (String) (defaults to: "")

    the experience identifier; prefixed to the visitor id to form the hash input. Defaults to "".

  • seed (Integer) (defaults to: @hash_seed)

    MurmurHash3 seed; defaults to the Config hash seed.

Returns:

  • (Integer)

    the bucket value in [0, max_traffic).



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