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 {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
!!valueboolean 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 |