Class: ConvertSdk::FeatureManager Private
- Inherits:
-
Object
- Object
- ConvertSdk::FeatureManager
- 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
-
#cast_type(value, type) ⇒ Object
private
Cast a raw variable value to its declared type.
-
#initialize(data_manager:, log_manager: nil) ⇒ FeatureManager
constructor
private
A new instance of FeatureManager.
-
#run_feature(visitor_id, feature_key, attributes = {}) ⇒ BucketedFeature+
private
Resolve a SINGLE feature for a visitor (FR24).
-
#run_features(visitor_id, attributes = {}, experiences: nil, features: nil) ⇒ Array<BucketedFeature>
private
Resolve ALL applicable features for a visitor (FR25).
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.
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)+).
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.
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.
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 |