Module: Featurevisor::Bucketer

Defined in:
lib/featurevisor/bucketer.rb

Overview

Bucketer module for handling feature flag bucketing

Constant Summary collapse

MAX_BUCKETED_NUMBER =

Maximum bucketed number (100% * 1000 to include three decimal places)

100_000
HASH_SEED =

Hash seed for consistent bucketing

1
MAX_HASH_VALUE =

Maximum hash value for 32-bit integers

2**32
DEFAULT_BUCKET_KEY_SEPARATOR =

Default separator for bucket keys

"."

Class Method Summary collapse

Class Method Details

.build_bucket_key(attribute_keys, context, type, feature_key) ⇒ Array

Build bucket key array from attribute keys and context

Parameters:

  • attribute_keys (Array<String>)

    Array of attribute keys

  • context (Hash)

    User context

  • type (String)

    Bucketing type ("plain", "and", "or")

  • feature_key (String)

    Feature key to append

Returns:

  • (Array)

    Array of bucket key components



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# File 'lib/featurevisor/bucketer.rb', line 75

def self.build_bucket_key(attribute_keys, context, type, feature_key)
  bucket_key = []

  attribute_keys.each do |attribute_key|
    attribute_value = Featurevisor::Conditions.get_value_from_context(context, attribute_key)

    next if attribute_value.nil? && !context_path_exists?(context, attribute_key)

    if type == "plain" || type == "and"
      bucket_key << attribute_value
    elsif type == "or" && bucket_key.empty?
      # For "or" type, only take the first available value
      bucket_key << attribute_value
    end
  end

  bucket_key << feature_key
  bucket_key.map { |value| javascript_string(value) }
end

.context_path_exists?(context, path) ⇒ Boolean

Returns:

  • (Boolean)


95
96
97
98
99
100
101
102
103
# File 'lib/featurevisor/bucketer.rb', line 95

def self.context_path_exists?(context, path)
  path.split(".").reduce(context) do |current, key|
    return false unless current.is_a?(Hash)
    return false unless current.key?(key.to_sym) || current.key?(key)

    current.key?(key.to_sym) ? current[key.to_sym] : current[key]
  end
  true
end

.get_bucket_key(options) ⇒ String

Get bucket key from feature configuration and context

Parameters:

  • options (Hash)

    Options hash containing:

    • feature_key [String] The feature key
    • bucket_by [String, Array, Hash] Bucketing strategy
    • context [Hash] User context
    • diagnostics [DiagnosticReporter] Diagnostic reporter

Returns:

  • (String)

    The bucket key

Raises:

  • (StandardError)

    If bucket_by is invalid



36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/featurevisor/bucketer.rb', line 36

def self.get_bucket_key(options)
  feature_key = options[:feature_key]
  bucket_by = options[:bucket_by]
  context = options[:context]
  diagnostics = options[:diagnostics]

  type, attribute_keys = parse_bucket_by(bucket_by, diagnostics, feature_key)

  bucket_key = build_bucket_key(attribute_keys, context, type, feature_key)

  bucket_key.join(DEFAULT_BUCKET_KEY_SEPARATOR)
end

.get_bucketed_number(bucket_key) ⇒ Integer

Get bucketed number from a bucket key

Parameters:

  • bucket_key (String)

    The bucket key to hash

Returns:

  • (Integer)

    Bucket value between 0 and 100000



21
22
23
24
25
26
# File 'lib/featurevisor/bucketer.rb', line 21

def self.get_bucketed_number(bucket_key)
  hash_value = Featurevisor.murmur_hash_v3(bucket_key, HASH_SEED)
  ratio = hash_value.to_f / MAX_HASH_VALUE

  (ratio * MAX_BUCKETED_NUMBER).floor
end

.javascript_string(value) ⇒ Object



105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# File 'lib/featurevisor/bucketer.rb', line 105

def self.javascript_string(value)
  return "" if value.nil?
  return value ? "true" : "false" if value == true || value == false
  if value.is_a?(Float)
    return "NaN" if value.nan?
    return value.positive? ? "Infinity" : "-Infinity" if value.infinite?
    return "0" if value.zero?
    return value.to_i.to_s if value == value.to_i && value.abs < 1e21
    if value.abs >= 1e-6 && value.abs < 1e21
      return scientific_to_fixed(value.to_s)
    end

    coefficient, exponent = value.to_s.downcase.split("e")
    return "#{coefficient.delete_suffix('.0')}e#{Integer(exponent) >= 0 ? '+' : ''}#{Integer(exponent)}"
  end
  return value.map { |item| javascript_string(item) }.join(",") if value.is_a?(Array)
  return "[object Object]" if value.is_a?(Hash)

  value.to_s
end

.parse_bucket_by(bucket_by, diagnostics, feature_key) ⇒ Array

Parse bucket_by configuration to determine type and attribute keys

Parameters:

  • bucket_by (String, Array<String>, Hash)

    Bucketing strategy

  • diagnostics (DiagnosticReporter)

    Diagnostic reporter

  • feature_key (String)

    Feature key for error logging

Returns:

  • (Array)

    Tuple of [type, attribute_keys]



56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/featurevisor/bucketer.rb', line 56

def self.parse_bucket_by(bucket_by, diagnostics, feature_key)
  if bucket_by.is_a?(String)
    ["plain", [bucket_by]]
  elsif bucket_by.is_a?(Array)
    ["and", bucket_by]
  elsif bucket_by.is_a?(Hash) && bucket_by[:or].is_a?(Array)
    ["or", bucket_by[:or]]
  else
    diagnostics.error("invalid bucketBy", { feature_key: feature_key, bucket_by: bucket_by })
    raise StandardError, "invalid bucketBy"
  end
end