Class: ConvertSdk::FeatureManager Private

Inherits:
Object
  • Object
show all
Defined in:
lib/convert_sdk/feature_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.

Feature resolution + typed-variable casting — the MAPPING + CASTING layer that turns the Epic 2 bucketing decisions (Story 2.11) into typed feature flags (FR24–FR27).

== Features resolve THROUGH experiences (FR26)

There is NO independent feature decision path. A feature is ENABLED exactly when the visitor is bucketed — via the ordered decision flow owned by DataManager#get_bucketing — into a variation that carries that feature. The carrying link lives in the variation's +changes+: a change with +type == "fullStackFeature"+ whose +data.feature_id+ matches a declared feature, and whose +data.variables_data+ holds the raw (string) variable values. This manager maps those bucketed variations onto declared features and casts the variable values; it NEVER re-evaluates rules (that would be a parity bug — the decision flow is owned in ONE place, the DataManager).

== Typed variables (FR27) — the developer-experience core

Each declared feature lists its variables as +type+; the bucketed variation supplies the raw values. #cast_type mirrors the JS +castType+ contract (javascript-sdk +packages/utils/src/types-utils.ts:13-54+) EXACTLY — five literal type strings:

string -> String(value) boolean -> "true" -> true, "false" -> false, else truthiness integer -> true->1, false->0, else parseInt-style (leading digits) float -> true->1.0, false->0.0, else parseFloat-style (leading number) json -> already a Hash/Array? as-is; else JSON.parse, on FAILURE -> raw String

There is NO +number+ type in the JS switch — none is added here. An unknown type returns the value unchanged (the JS +default+ branch). Casting is data-driven from the config's declared variable types — no per-feature cases.

== Miss semantics (AC#5; feature-manager.ts:206-218)

A miss is NEVER an exception. #run_feature returns a frozen BucketedFeature with +status == FeatureStatus::DISABLED+:

  • feature DECLARED but visitor not bucketed into a carrying variation -> +name, key, status: DISABLED+
  • feature NOT declared at all -> +status: DISABLED+ Each miss is PAIRED with a +debug+ reason log (a Ruby observability addition; JS returns the disabled feature silently).

== Sticky transitivity

A returning visitor's stored bucketing (2.11) drives feature stability automatically — there is NO feature-level storage here.

Constant Summary collapse

FULLSTACK_FEATURE =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

Variation-change type that carries a fullstack feature link. Wire value byte-identical to the JS enum (variation-change-type.ts:13). Held here (not inlined at the use site) so the wire string lives in ONE place.

"fullStackFeature"
JS_FALSEY =

This constant is part of a private API. You should avoid using this constant if possible, as it may be removed or be changed in the future.

The values JS treats as falsey for the +!!value+ boolean cast (after the explicit "true"/"false" string checks): nil, false, "", and 0.

[nil, false, "", 0].freeze

Instance Method Summary collapse

Constructor Details

#initialize(data_manager:, log_manager: nil) ⇒ FeatureManager

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.

Returns a new instance of FeatureManager.

Parameters:

  • data_manager (DataManager)

    the 2.11 decision-flow owner (config readers + +get_bucketing+).

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

    optional debug/warn logger.



68
69
70
71
# File 'lib/convert_sdk/feature_manager.rb', line 68

def initialize(data_manager:, log_manager: nil)
  @data_manager = data_manager
  @log_manager = log_manager
end

Instance Method Details

#cast_type(value, type) ⇒ 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.

Cast a raw variable value to its declared type. Mirrors JS +castType+ (types-utils.ts:13-54) exactly — see the class doc for the truth table. Never raises: non-numeric integer/float inputs degrade to a leading-number parse (0 / 0.0 when there is no leading number), and a +json+ parse failure falls back to the raw String (JS +catch -> String(value)+).

Parameters:

  • value (Object)

    the raw (typically String) variable value.

  • type (String)

    the declared type: string/boolean/integer/float/json.

Returns:

  • (Object)

    the cast value.



143
144
145
146
147
148
149
150
151
152
# File 'lib/convert_sdk/feature_manager.rb', line 143

def cast_type(value, type)
  case type
  when "string"  then value.to_s
  when "boolean" then cast_boolean(value)
  when "integer" then cast_integer(value)
  when "float"   then cast_float(value)
  when "json"    then cast_json(value)
  else value # JS default branch — unknown type passes through unchanged.
  end
end

#run_feature(visitor_id, feature_key, attributes = {}) ⇒ BucketedFeature+

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 SINGLE feature for a visitor (FR24).

Mirrors JS +runFeature+ (feature-manager.ts:180-219): the feature is looked up by key; if declared, the bucketing flow runs FILTERED to this feature. On one carrying variation a single ENABLED BucketedFeature is returned; on several (the feature appears in multiple bucketed variations) an Array of ENABLED BucketedFeatures; on none the DISABLED fallback (+id,name,key+). An undeclared feature returns the +key+-only DISABLED fallback. Each miss is paired with a debug log; never raises.

Parameters:

  • visitor_id (String)

    the visitor identifier.

  • feature_key (String)

    the feature +key+ to resolve.

  • attributes (Hash) (defaults to: {})

    bucketing attributes (+:visitor_properties+, +:location_properties+, +:environment+) — see DataManager#get_bucketing.

Returns:



89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
# File 'lib/convert_sdk/feature_manager.rb', line 89

def run_feature(visitor_id, feature_key, attributes = {})
  declared = @data_manager.feature_by_key(feature_key)
  unless declared
    @log_manager&.debug("FeatureManager#run_feature: feature not declared key=#{feature_key}")
    return disabled_feature(key: feature_key)
  end

  enabled = run_features(visitor_id, attributes, features: [feature_key])
  if enabled.empty?
    @log_manager&.debug("FeatureManager#run_feature: not bucketed into a carrying variation key=#{feature_key}")
    return disabled_from_declared(declared)
  end

  enabled.length == 1 ? enabled.first : enabled
end

#run_features(visitor_id, attributes = {}, experiences: nil, features: nil) ⇒ Array<BucketedFeature>

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 ALL applicable features for a visitor (FR25).

Mirrors JS +runFeatures+ (feature-manager.ts:327-463) under the Ruby across-all-experiences parity decision (Story 2.11 ExperienceManager#select_variations): misses are FILTERED OUT of the bucketed-variation set (sentinels never propagate), then every declared feature carried by a bucketed variation is collected as an ENABLED BucketedFeature (variables cast per declared type). When NO +features+ filter is supplied, every declared feature NOT already enabled is appended as a DISABLED BucketedFeature — so callers always see the full feature roster. With a +features+ filter, only enabled matches are returned (no DISABLED padding). Never raises.

Parameters:

  • visitor_id (String)

    the visitor identifier.

  • attributes (Hash) (defaults to: {})

    bucketing attributes (see #run_feature).

  • experiences (Array<String>, nil) (defaults to: nil)

    optional experience-key filter.

  • features (Array<String>, nil) (defaults to: nil)

    optional feature-key filter (suppresses the DISABLED padding).

Returns:



123
124
125
126
127
128
129
130
131
132
# File 'lib/convert_sdk/feature_manager.rb', line 123

def run_features(visitor_id, attributes = {}, experiences: nil, features: nil)
  declared_by_id = features_by_id
  variations = bucketed_variations(visitor_id, attributes, experiences)

  bucketed = collect_enabled(variations, declared_by_id, features)

  # Pad with DISABLED features ONLY when no feature filter is supplied.
  append_disabled(bucketed, declared_by_id) if features.nil?
  bucketed
end