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 {key, 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 ->
+{id, name, key, status: DISABLED}+
* feature NOT declared at all -> +{key, 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