Skip to content

FeatureManager

Ahmed Abbas edited this page Apr 23, 2025 · 2 revisions

Chapter 4: FeatureManager

In the previous chapter on ExperienceManager, we learned how the SDK manages A/B tests (or "experiences") where visitors are assigned to different variations. But what if you don't need multiple variations? What if you just want to switch a specific piece of functionality on or off for certain users, like rolling out a new beta feature?

This is where Feature Flags come in, and the SDK has a dedicated manager for them.

The Problem: Toggling Features On and Off

Imagine you've developed a cool new "Dark Mode" for your website. You're not ready to release it to everyone yet. You want to:

  1. Enable it only for internal testers first.
  2. Later, enable it for 10% of all visitors.
  3. Eventually, roll it out to everyone.
  4. Maybe pass some configuration data (like the exact dark theme color) along with the feature flag.

Doing this with a full A/B test (ExperienceManager) feels like overkill. You don't have multiple variations of Dark Mode; you just want to turn it on or off. You need a simpler way to control the availability of this feature.

What is FeatureManager? Your Feature Control Panel

Think of the FeatureManager as the control panel or switchboard for your website's features. While the ExperienceManager is directing complex A/B tests with different variations, the FeatureManager is focused on the simpler task of flipping switches: turning individual features on or off for specific visitors.

Key jobs of the FeatureManager:

  1. Knows the Features: It can list all features defined in your Convert project (getList(), getFeature()).
  2. Checks the Switch: It can tell you if a specific feature (like "dark-mode") should be enabled for a particular visitor (isFeatureEnabled(), runFeature()).
  3. Provides Configuration: If a feature is enabled, it can also give you any configuration variables associated with it (e.g., the themeColor for Dark Mode).
  4. Uses Underlying Logic: Just like the ExperienceManager, it doesn't make decisions in isolation. It relies on the underlying system (especially the DataManager) to figure out if a visitor meets the rules and bucketing criteria set up in Convert experiences that might enable this feature.

In essence, FeatureManager lets you ask: "For this visitor, is feature X turned on?".

Using the FeatureManager (via Context)

Similar to experiences, you typically won't use the FeatureManager directly. You'll use the convenient methods provided by the visitor's Context object.

Let's check if our 'user123' should get the "Dark Mode" feature. Assume the feature is identified by the key 'dark-mode' in your Convert project.

  1. Get the Context: First, ensure you have the context for your visitor.

    // Assuming 'convert' is your initialized SDK instance
    const visitorId = 'user123';
    const visitorContext = convert.createContext(visitorId, { country: 'Canada' });
  2. Check if the Feature is Enabled: Use context.isFeatureEnabled().

    const featureKey = 'dark-mode';
    
    const isEnabled = visitorContext.isFeatureEnabled(featureKey);
    
    if (isEnabled) {
      console.log(`Feature '${featureKey}' is ENABLED for visitor '${visitorId}'!`);
      // Now you can apply the dark mode styles
    } else {
      console.log(`Feature '${featureKey}' is DISABLED for visitor '${visitorId}'.`);
      // Use the default light mode
    }
    • visitorContext.isFeatureEnabled(featureKey) asks the SDK (specifically, the FeatureManager via the Context) whether the feature 'dark-mode' should be active for 'user123'.
    • It returns a simple boolean: true if enabled, false otherwise.
  3. Getting Feature Details (including variables): If you need more than just on/off, like configuration values, use context.runFeature().

    const featureKey = 'dark-mode';
    
    // Attributes might be needed if targeting rules depend on them
    const attributes = { locationProperties: {}, visitorProperties: { country: 'Canada' } };
    
    const featureResult = visitorContext.runFeature(featureKey, attributes);
    
    console.log(featureResult);
    
    if (featureResult && featureResult.status === 'enabled') {
      console.log(`Feature '${featureKey}' is ENABLED.`);
      console.log('Variables:', featureResult.variables); // Access any variables
      // Example: Apply dark mode using featureResult.variables.themeColor
    } else {
      console.log(`Feature '${featureKey}' is DISABLED.`);
    }
    • visitorContext.runFeature(featureKey, attributes) asks the SDK for the full status and data of the feature for this visitor.
    • The attributes are optional but important if your feature targeting rules depend on them (like "only enable for users in Canada").
    • Output: It returns a BucketedFeature object.
      • If enabled, it looks like: { key: 'dark-mode', status: 'enabled', variables: { themeColor: '#111', fontSize: '16px' }, id: '...', experienceKey: '...', ... }
      • If disabled, it looks like: { key: 'dark-mode', status: 'disabled', id: '...' }
    • Notice the status property ('enabled' or 'disabled') and the variables object when enabled.

Under the Hood: How runFeature (and isFeatureEnabled) Works

How does the SDK decide if 'dark-mode' is enabled for 'user123'? It's a bit different from A/B testing. A feature isn't usually tested in isolation; it's often enabled as part of one or more experiences.

  1. Call Context Method: Your code calls visitorContext.runFeature('dark-mode', ...).
  2. Delegate to FeatureManager: The Context object passes the request to the FeatureManager's runFeature method.
  3. FeatureManager Orchestrates: The FeatureManager doesn't do bucketing itself. Instead, it needs to find out if the visitor is bucketed into any variation of any relevant experience that happens to enable the 'dark-mode' feature. It does this using its runFeatures helper method (which we'll look at below).
  4. Call runFeatures: The runFeature method calls the internal runFeatures method, potentially filtering to only consider experiences linked to 'dark-mode'.
  5. runFeatures Logic:
    • Get Relevant Experiences: It asks the DataManager for the list of experiences that might affect the 'dark-mode' feature (or all experiences if not filtered).
    • Check Each Experience: For each relevant experience, it asks the DataManager to perform bucketing for 'user123': dataManager.getBucketing(visitorId, experience.key, attributes). This involves the RuleManager and BucketingManager we saw in the previous chapter.
    • Examine Variation: If the visitor is bucketed into a variation for an experience, runFeatures inspects that variation's definition (specifically, its changes data).
    • Feature Enabled? It checks if the variation's changes explicitly list the 'dark-mode' feature (feature_id) as being turned on.
    • Collect Results: If it finds any variation (across all checked experiences) that enables 'dark-mode' for 'user123', it gathers the feature details (key, status='enabled', variables).
  6. Return Result: runFeatures returns a list of enabled features. runFeature picks the relevant one ('dark-mode') from this list. If 'dark-mode' wasn't found in any enabled variation's changes, it returns the disabled status object.
  7. Context Returns: The Context returns the final BucketedFeature object to your code.

Here's a simplified diagram:

sequenceDiagram
    participant YourCode as Your Website Code
    participant Context as Visitor Context
    participant FeatureMgr as FeatureManager
    participant DataMgr as DataManager
    participant BucketingEtc as Bucketing/Rules

    YourCode->>Context: runFeature('dark-mode', ...)
    activate Context
    Context->>FeatureMgr: runFeature('user123', 'dark-mode', ...)
    activate FeatureMgr
    FeatureMgr->>FeatureMgr: runFeatures('user123', ..., {features: ['dark-mode']})
    FeatureMgr->>DataMgr: Get relevant experiences for 'dark-mode'
    activate DataMgr
    DataMgr-->>FeatureMgr: List of Experiences (e.g., Exp1, Exp2)
    deactivate DataMgr
    loop For Each Experience (Exp1, Exp2)
        FeatureMgr->>DataMgr: getBucketing('user123', Exp.key, ...)
        activate DataMgr
        DataMgr->>BucketingEtc: Check rules & perform bucketing
        activate BucketingEtc
        BucketingEtc-->>DataMgr: Return Variation (or RuleError)
        deactivate BucketingEtc
        DataMgr-->>FeatureMgr: Return Variation (e.g., VarA for Exp1)
        deactivate DataMgr
        Note over FeatureMgr: Inspect VarA's changes. Does it enable 'dark-mode'?
    end
    alt 'dark-mode' was enabled by some variation
        FeatureMgr-->>Context: Return BucketedFeature (status: 'enabled', variables: {...})
    else 'dark-mode' was not enabled
        FeatureMgr-->>Context: Return BucketedFeature (status: 'disabled')
    end
    deactivate FeatureMgr
    Context-->>YourCode: Return BucketedFeature object
    deactivate Context
Loading

The key idea is that features are often switched on/off as part of the variations within experiences. FeatureManager figures out which features are enabled by checking the outcome of the visitor's bucketing across relevant experiences.

Code Dive: FeatureManager Implementation

Let's look at simplified snippets.

1. The Constructor (feature-manager.ts)

Like other managers, it mainly stores references to dependencies.

// File: packages/js-sdk/src/feature-manager.ts (Simplified)

import {DataManagerInterface} from '@convertcom/js-sdk-data';
import {LogManagerInterface} from '@convertcom/js-sdk-logger';
// ... other imports ...

export class FeatureManager implements FeatureManagerInterface {
  private _dataManager: DataManagerInterface; // Stores DataManager
  private _loggerManager: LogManagerInterface | null;

  constructor(
    config: Config,
    { dataManager, loggerManager }: { // Receives managers
      dataManager: DataManagerInterface;
      loggerManager?: LogManagerInterface;
    }
  ) {
    // Store the DataManager for later use
    this._dataManager = dataManager;
    this._loggerManager = loggerManager;
    // ... logging ...
  }
  // ... other methods ...
}
  • Very straightforward: it gets the DataManager from the main SDK Core and saves it in this._dataManager.

2. The runFeature Method (feature-manager.ts)

This method uses the more general runFeatures method internally.

// File: packages/js-sdk/src/feature-manager.ts (Simplified)

// Inside FeatureManager class:
  runFeature(
    visitorId: string,
    featureKey: string,
    attributes: BucketingAttributes,
    experienceKeys?: Array<string> // Optional: Limit which experiences to check
  ): BucketedFeature | RuleError | Array<BucketedFeature | RuleError> {

    // Check if the feature is even defined in the project data
    const declaredFeature = this._dataManager.getEntity(featureKey, 'features');

    if (declaredFeature) {
      // === Core Logic: Call runFeatures ===
      // Ask runFeatures to find enabled features, filtering by our featureKey
      const features = this.runFeatures(visitorId, attributes, {
        features: [featureKey], // Filter for our specific feature
        experiences: experienceKeys // Optional experience filter
      });

      if (features.length > 0) {
         // Found the feature enabled in one or more experiences
         return features.length === 1 ? features[0] : features; // Return single or array
      }
      // If runFeatures didn't find it enabled, return disabled status
      return { key: featureKey, status: FeatureStatus.DISABLED, /*...*/ };

    } else {
      // Feature wasn't even declared in the project data
      return { key: featureKey, status: FeatureStatus.DISABLED, /*...*/ };
    }
  }
  • It first checks if the feature key (featureKey) exists using _dataManager.getEntity.
  • The main work is done by calling this.runFeatures, passing the featureKey in the filter object. This tells runFeatures to only care about this specific feature.
  • It handles the case where the feature is enabled (returning the result from runFeatures) or disabled.

3. The runFeatures Method (Simplified Logic)

This is where the core feature flag evaluation happens.

// File: packages/js-sdk/src/feature-manager.ts (Simplified logic)

// Inside FeatureManager class:
  runFeatures(
    visitorId: string,
    attributes: BucketingAttributes,
    filter?: Record<string, Array<string>> // Filter by feature/experience keys
  ): Array<BucketedFeature | RuleError> {

    const bucketedFeatures: Array<BucketedFeature> = [];
    const experiencesToCheck = /* Get relevant experiences from DataManager based on filter */;

    // Loop through experiences the visitor might be part of
    for (const experience of experiencesToCheck) {
      // Ask DataManager to bucket the visitor for THIS experience
      const variationOrError = this._dataManager.getBucketing(
        visitorId,
        experience.key,
        attributes
      );

      // Skip if visitor didn't qualify (RuleError) or wasn't bucketed
      if (/* variationOrError is an error or null */) continue;

      const bucketedVariation = variationOrError as BucketedVariation;

      // === Check the variation's changes ===
      // Loop through changes defined in the variation data
      for (const change of bucketedVariation.changes || []) {
        // Check if this change enables a feature flag
        if (change.type === VariationChangeType.FULLSTACK_FEATURE) {
          const featureId = change.data?.feature_id;
          const featureData = this._dataManager.getEntityById(featureId, 'features');

          // Is this the feature we're looking for (based on filter)?
          if (/* featureData matches the filter OR no filter was passed */) {
             const variables = /* Extract variables from change.data */;
             // Maybe cast variable types (string to number, etc.)

             // Found an enabled feature! Add it to our results.
             bucketedFeatures.push({
                key: featureData.key,
                id: featureId,
                status: FeatureStatus.ENABLED,
                variables: variables,
                experienceKey: experience.key // Record which experience enabled it
                // ... other details
             });
          }
        }
      }
    } // End loop through experiences

    // If no feature filter was provided, add all OTHER declared features
    // with status DISABLED to the list (optional step, not shown in detail)

    return bucketedFeatures; // Return the list of found enabled features
  }
  • It gets the relevant experiences from the DataManager.
  • It loops through each experience and calls _dataManager.getBucketing to see which variation the visitor gets (if any).
  • Crucially, it inspects the changes array within the bucketedVariation object.
  • If a change is of type FULLSTACK_FEATURE, it extracts the feature_id and checks if it matches the desired feature (if filtering).
  • If it's a match, it builds a BucketedFeature object with status: 'enabled' and extracts any variables.
  • It returns an array containing all the features found to be enabled for this visitor based on their bucketing outcomes.

Conclusion

The FeatureManager provides a focused way to handle feature flags within the Convert SDK. It allows you to easily check if a feature should be enabled for a visitor and retrieve any associated configuration variables.

You've learned:

  1. What feature flags are and why FeatureManager is useful.
  2. How to use context.isFeatureEnabled() for a simple on/off check.
  3. How to use context.runFeature() to get the feature's status and variables.
  4. That FeatureManager works by checking the results of visitor bucketing across relevant experiences (via the DataManager) to see if any variation enables the target feature.

Throughout the last few chapters, we've seen the ExperienceManager and FeatureManager constantly relying on another component to get experiment data, check rules, and perform bucketing. This central component is the DataManager.

Ready to see how the SDK stores and accesses all the project configuration data? Let's dive into the DataManager in the next chapter!

Clone this wiki locally