-
Notifications
You must be signed in to change notification settings - Fork 2
DataManager
Welcome back! In the previous chapters, we explored the Context object for individual visitors, the ExperienceManager for A/B tests, and the FeatureManager for feature flags. You might have noticed a pattern: when we asked the SDK to make a decision (like "which variation should user123 see?" or "is dark-mode enabled?"), the managers didn't just guess. They needed information!
But where does all this information live? How does the SDK know about your specific experiments, features, targeting rules, and which variation 'user123' was assigned to last time?
Imagine you're building a complex application. You have user settings, product information, application configuration, and maybe temporary data about the user's current session. Where do you keep all this data so different parts of your application can access it when needed? You probably use a database or some central configuration store.
The Convert SDK faces a similar challenge. It needs a central place to:
- Store Project Setup: Keep track of all the experiences, features, audiences, goals, etc., defined in your Convert account for your specific project.
- Fetch Updates: Get the latest version of this project setup from the Convert API or use data you provide directly.
- Remember Visitor Decisions: If 'user123' is assigned to 'variation-B' of the headline test, the SDK needs to remember this so they see the same headline consistently.
- Store Visitor Segments: Keep track of visitor properties or segments that might affect targeting.
- Provide Data Access: Allow other managers (like ExperienceManager and FeatureManager) to easily retrieve the specific information they need.
How does the SDK manage this central pool of information?
Meet the DataManager
! Think of it as the central librarian or database interface for your Convert SDK project. It's responsible for holding and managing all the essential information.
Its key responsibilities are:
- The Bookshelf (Project Configuration): It holds the entire configuration downloaded from Convert (or provided by you), including details about all your experiences, features, audiences, goals, etc.
-
The Index Cards (Visitor State): It keeps track of visitor-specific information, most importantly:
- Which variation a visitor has been assigned to for each experience (bucketing decisions).
- Any relevant visitor segments or properties.
- Fetching New Books (Data Retrieval): It works with the ApiManager to fetch the latest project configuration from Convert's servers.
- Checking Books Out (Providing Data): It offers methods for other managers to look up specific pieces of information (e.g., "get me the details for the 'headline-test' experience" or "find the goal with ID '12345'").
-
Using a Filing System (Data Persistence): It can optionally use a
DataStoreManager
(which you can configure) to save and retrieve visitor bucketing information (e.g., using browser cookies or localStorage) so that decisions persist across visits.
In short, DataManager
is the single source of truth within the SDK for both the overall project setup and the state of individual visitors.
As a developer using the SDK, you will very rarely interact directly with the DataManager
. It's primarily an internal component used by other managers.
Remember how Context.runExperience()
delegated to ExperienceManager.selectVariation()
? And how ExperienceManager.selectVariation()
immediately delegated further? That final delegation step often lands here, at the DataManager
.
Let's revisit the flow when you ask for an experience variation:
- You call
visitorContext.runExperience('headline-test')
. - The
Context
callsexperienceManager.selectVariation(...)
. - The
ExperienceManager
callsdataManager.getBucketing('user123', 'headline-test', ...)
to handle the entire process.
Similarly, when the FeatureManager
needs to know if a feature is enabled (runFeature
), it asks the DataManager
to perform the necessary bucketing checks across relevant experiences (getBucketing
).
Even fetching basic information relies on the DataManager
:
// Inside ExperienceManager (simplified conceptual code)
// Method to get details of one experience
getExperience(key) {
// Asks DataManager to find the entity named 'key' in the 'experiences' list
const experienceData = this._dataManager.getEntity(key, 'experiences');
return experienceData;
}
// Method to get a list of all experiences
getList() {
// Asks DataManager for the entire list of 'experiences'
const allExperiences = this._dataManager.getEntitiesList('experiences');
return allExperiences;
}
- The
ExperienceManager
doesn't store the experience data itself; it asks theDataManager
for it using methods likegetEntity
andgetEntitiesList
.
So, while you don't call DataManager
methods directly, understanding its role is key to understanding how the SDK manages data and makes decisions.
Let's peek inside the library.
1. Storing Project Configuration:
- When the SDK initializes (Chapter 1: ConvertSDK / Core), the ApiManager fetches the configuration data (a large JSON object) from Convert's servers (if using an
sdkKey
). - This data is passed to the
DataManager
and stored internally (often in a property likethis._data
). - The
DataManager
provides helper methods to navigate this large JSON structure efficiently:-
getEntitiesList('experiences')
: Returns the array of all experience objects. -
getEntity('headline-test', 'experiences')
: Finds and returns the specific experience object with the key 'headline-test'. -
getEntityById('100567', 'goals')
: Finds and returns the goal object with the ID '100567'.
-
2. Storing Visitor State:
- Decisions like "user123 gets variation-B" need to be remembered.
- The
DataManager
uses an internal, temporary cache (like a JavascriptMap
called_bucketedVisitors
) to store these decisions for the current session.Map({ 'account-project-user123': { bucketing: { 'exp1_id': 'varB_id' }, segments: {...} } })
-
Persistence (Optional): To remember decisions across page loads or visits, the
DataManager
can use aDataStoreManager
.- The
DataStoreManager
is a wrapper around adataStore
object that you might provide in the SDK configuration. - This
dataStore
could be configured to use browser cookies, localStorage, sessionStorage, or even a custom backend storage system you implement. - When
DataManager
stores a decision (usingputData
), it updates its internal cache and tells theDataStoreManager
to save it persistently (e.g., set a cookie). - When
DataManager
needs a decision (usinggetData
), it checks its internal cache first, and if not found, asks theDataStoreManager
to retrieve it from the persistent store (e.g., read a cookie).
- The
graph LR
DM[DataManager]
cache["Internal Cache (_bucketedVisitors Map)"]
dsm["DataStoreManager (Optional)"]
Store[(Persistent Store <br/> e.g., Cookies, localStorage)]
subgraph DataManager Scope
DM -- Reads/Writes --> cache
DM -- Reads/Writes --> dsm
end
subgraph External Persistence
dsm -- Reads/Writes --> Store
end
style Store fill:#eee,stroke:#333,stroke-width:1px
3. Orchestrating Bucketing (getBucketing
):
This is where the DataManager
truly acts as the central coordinator. When experienceManager.selectVariation
calls dataManager.getBucketing('user123', 'headline-test', { ... })
:
-
Check Cache/Store:
DataManager
first calls itsgetData('user123')
method. This checks the internal_bucketedVisitors
map. If not found there, it asks theDataStoreManager
(if configured) to check the persistent store (e.g., cookies) for a previously saved decision for 'headline-test' for 'user123'. -
Return Cached Decision (If Found): If a valid, previous decision is found,
DataManager
retrieves the corresponding variation details and returns them immediately. Fast path! -
Fetch Experience Details: If no decision is cached,
DataManager
retrieves the full definition of the 'headline-test' experience (variations, rules, traffic split) from its stored project configuration (this._data
). -
Check Targeting Rules: It uses the RuleManager to evaluate the experience's targeting rules (audiences, locations) against the visitor's properties (
visitorProperties
,locationProperties
). -
Rules Fail: If the visitor doesn't meet the targeting criteria,
DataManager
returns aRuleError
. -
Perform Bucketing: If the rules pass,
DataManager
calls the BucketingManager. It provides the visitor ID ('user123') and the traffic allocation defined for the variations of 'headline-test'. TheBucketingManager
calculates which variation ID the visitor falls into. -
Store New Decision:
DataManager
receives the chosen variation ID from theBucketingManager
. It then calls itsputData('user123', { bucketing: { 'headline-test-id': 'chosen-variation-id' } })
method. This saves the decision to the internal cache (_bucketedVisitors
) and also tells theDataStoreManager
(if present) to save it persistently (e.g., update the cookie). -
Return New Decision: Finally,
DataManager
retrieves the full details of the chosen variation and returns theBucketedVariation
object.
Here's a sequence diagram illustrating this flow:
sequenceDiagram
participant EM as ExperienceManager
participant DM as DataManager
participant Cache as Internal Cache
participant Store as DataStore (Optional)
participant RM as RuleManager
participant BM as BucketingManager
EM->>+DM: getBucketing('user123', 'headline-test', ...)
DM->>+Cache: Get decision for 'user123'/'headline-test'?
Cache-->>-DM: Not found
DM->>+Store: Get decision for 'user123'/'headline-test'?
Store-->>-DM: Not found
DM->>DM: Get 'headline-test' definition (rules, variations)
DM->>+RM: Check rules match visitor properties?
RM-->>-DM: Rules Pass
DM->>+BM: Calculate variation for 'user123' (based on traffic split)
BM-->>-DM: variationId = 'variation-B-id'
DM->>+Cache: Store decision: 'user123'/'headline-test' -> 'variation-B-id'
Cache-->>-DM: OK
DM->>+Store: Store decision: 'user123'/'headline-test' -> 'variation-B-id'
Store-->>-DM: OK
DM->>DM: Get full details for 'variation-B-id'
DM-->>-EM: Return BucketedVariation object for 'variation-B'
Let's glance at simplified snippets from data-manager.ts
.
1. The Constructor (data-manager.ts
)
Sets up dependencies and the optional DataStore.
// File: packages/data/src/data-manager.ts (Simplified)
import {BucketingManagerInterface} from '@convertcom/js-sdk-bucketing';
import {RuleManagerInterface} from '@convertcom/js-sdk-rules';
import {DataStoreManager} from './data-store-manager';
// ... other imports
export class DataManager implements DataManagerInterface {
private _data: ConfigResponseData; // Holds the main project config JSON
private _bucketingManager: BucketingManagerInterface;
private _ruleManager: RuleManagerInterface;
private _dataStoreManager: DataStoreManagerInterface; // Wrapper for persistence
private _bucketedVisitors = new Map(); // Internal cache
constructor(
config: Config,
{ // Dependencies injected by Core
bucketingManager,
ruleManager,
eventManager,
apiManager,
loggerManager
}: { /* ... */ }
) {
// Store references to other managers
this._bucketingManager = bucketingManager;
this._ruleManager = ruleManager;
// ... store others ...
// Store the main config data if provided initially
this._data = config?.data;
// Initialize the DataStoreManager if a dataStore is provided in config
this.dataStoreManager = config?.dataStore;
}
// dataStoreManager setter/getter allows initializing it
set dataStoreManager(dataStore: any) {
this._dataStoreManager = null;
if (dataStore) {
// Creates the wrapper if a valid store is provided
this._dataStoreManager = new DataStoreManager(this._config, {
dataStore: dataStore, /* ... */
});
}
}
get dataStoreManager(): DataStoreManagerInterface { /* ... */ }
// ... other methods ...
}
- Stores references to the managers it needs to collaborate with (
RuleManager
,BucketingManager
). - Initializes the internal cache (
_bucketedVisitors
). - Sets up the
DataStoreManager
if adataStore
(like cookies) is configured for persistence.
2. Accessing Project Data (data-manager.ts
)
Methods to retrieve parts of the main configuration.
// File: packages/data/src/data-manager.ts (Simplified)
// Inside DataManager class:
// Holds the big JSON object from Convert API or config.data
set data(data: ConfigResponseData) {
if (this.isValidConfigData(data)) {
this._data = data;
// Store project/account IDs too
} else { /* Log error */ }
}
get data(): ConfigResponseData {
return this._data;
}
// Get a whole list (e.g., all experiences)
getEntitiesList(entityType: string): Array<Entity | string> {
// Uses helper to safely access nested property in this._data
// Example: entityType 'experiences' -> this._data['experiences']
return objectDeepValue(this._data, entityType) || [];
}
// Find a single item by its key (e.g., find experience with key 'headline-test')
getEntity(key: string, entityType: string): Entity {
// Finds item in the list from getEntitiesList where item.key === key
return this._getEntityByField(key, entityType, 'key');
}
// Find a single item by its ID (e.g., find goal with id '100567')
getEntityById(id: string, entityType: string): Entity {
// Finds item in the list where item.id === id
return this._getEntityByField(id, entityType, 'id');
}
// Internal helper used by getEntity and getEntityById
private _getEntityByField(identity: string, entityType: string, field: IdentityField): Entity {
const list = this.getEntitiesList(entityType) as Array<Entity>;
// Loop through the list and find the matching item
for (let i = 0; i < list.length; i++) {
if (list[i] && String(list[i]?.[field]) === String(identity)) {
return list[i];
}
}
return null; // Not found
}
- These methods provide structured ways to access the potentially large
this._data
configuration object.
3. Managing Visitor Data (data-manager.ts
)
Methods for storing and retrieving visitor-specific decisions.
// File: packages/data/src/data-manager.ts (Simplified)
// Inside DataManager class:
// Stores or updates data for a visitor
putData(visitorId: string, newData: StoreData = {}) {
const storeKey = this.getStoreKey(visitorId); // e.g., "account-project-visitor123"
const currentData = this.getData(visitorId) || {}; // Get existing data
const updatedData = objectDeepMerge(currentData, newData); // Merge old and new
// Update the internal cache
this._bucketedVisitors.set(storeKey, updatedData);
// Optional: Limit cache size (remove oldest if too large)
// If DataStoreManager is configured, tell it to save persistently
if (this.dataStoreManager) {
this.dataStoreManager.enqueue(storeKey, updatedData); // Enqueue for saving
}
}
// Retrieves data for a visitor
getData(visitorId: string): StoreData {
const storeKey = this.getStoreKey(visitorId);
// 1. Check fast internal cache first
const memoryData = this._bucketedVisitors.get(storeKey);
// 2. If DataStoreManager exists, check persistent store
let storeData = null;
if (this.dataStoreManager) {
storeData = this.dataStoreManager.get(storeKey); // e.g., read cookie
}
// Merge results (memory cache overrides persistent store if both exist)
return objectDeepMerge(storeData || {}, memoryData || {});
}
// Helper to create a unique key for storage
getStoreKey(visitorId: string): string {
// Combines account, project, and visitor IDs for uniqueness
return `${this._accountId}-${this._projectId}-${visitorId}`;
}
-
putData
updates both the internal cache (_bucketedVisitors
) and potentially queues the data for persistent storage viaDataStoreManager
. -
getData
checks the cache first, then the persistent store, merging the results.
4. Orchestrating Bucketing (data-manager.ts
)
The core getBucketing
method coordinates the process.
// File: packages/data/src/data-manager.ts (Simplified)
// Inside DataManager class:
getBucketing(
visitorId: string,
key: string, // e.g., 'headline-test'
attributes: BucketingAttributes // contains visitorProperties, etc.
): BucketedVariation | RuleError | BucketingError {
// This calls an internal helper that does the main work
return this._getBucketingByField(visitorId, key, 'key', attributes);
}
private _getBucketingByField(
visitorId: string,
identity: string, // The key or ID of the experience
identityField: IdentityField, // 'key' or 'id'
attributes: BucketingAttributes
): BucketedVariation | RuleError | BucketingError {
// 1. Check cache/store first (simplified view - happens inside matchRules/retrieveBucketing)
const {bucketing: cachedBucketing} = this.getData(visitorId) || {};
// ... logic to check if decision for this experience exists and is valid ...
// if (cachedDecisionIsValid) return retrieveVariation(cachedDecision);
// 2. If not cached, check rules
const experience = this.matchRulesByField(visitorId, identity, identityField, attributes);
// matchRulesByField uses this._ruleManager internally
if (!experience) return null; // No match or archived/invalid experience
if (/* experience is a RuleError */) return experience; // Rule check failed
// 3. If rules pass, perform bucketing & store result
return this._retrieveBucketing(
visitorId,
attributes.visitorProperties,
attributes.updateVisitorProperties, // Should we store visitor props?
experience as ConfigExperience, // The matched experience
attributes.forceVariationId, // Optional override
attributes.enableTracking // Should we send tracking event?
);
}
// Helper that performs the actual bucketing and stores the result
private _retrieveBucketing(
visitorId: string, visitorProperties: any, updateVisitorProperties: boolean,
experience: ConfigExperience, forceVariationId?: string, enableTracking = true
): BucketedVariation | BucketingError {
// Check if already bucketed (re-checks cache/store specifically for this experience)
const {bucketing} = this.getData(visitorId) || {};
const storedVariationId = bucketing?.[experience.id.toString()];
// ... logic to use storedVariationId if valid ...
// If not stored or invalid:
// Build variation buckets { variationId: trafficAllocation, ... }
const buckets = /* ... get variations and traffic % from experience ... */;
// Ask BucketingManager to pick one
const decision = this._bucketingManager.getBucketForVisitor(
buckets, visitorId, /* options */
);
const chosenVariationId = forceVariationId || decision?.variationId;
if (!chosenVariationId) return BucketingError.VARIAION_NOT_DECIDED;
// Store the new decision!
this.putData(visitorId, {
bucketing: { [experience.id.toString()]: chosenVariationId },
// Optionally store visitorProperties too if updateVisitorProperties is true
});
// Enqueue tracking event via ApiManager (if enableTracking)
if (enableTracking) { /* ... this._apiManager.enqueue(...) ... */ }
// Get full variation details and return
const variationDetails = this.retrieveVariation(experience.id, chosenVariationId);
return { /* ... build BucketedVariation object ... */ };
}
- This shows the high-level orchestration: check cache/rules first (
matchRulesByField
), then if needed, perform bucketing (_retrieveBucketing
which uses_bucketingManager
) and store the result (putData
).
The DataManager
is the unsung hero working tirelessly behind the scenes. It acts as the central library and filing system for the Convert SDK, holding all project configuration (experiences, features, etc.) and managing visitor-specific state like bucketing decisions and segments.
You've learned:
- Why a central data management component is necessary.
- That
DataManager
stores both project-wide configuration and visitor-specific state. - How it uses an internal cache and optionally a persistent
DataStore
(like cookies) to remember visitor bucketing. - That it orchestrates the bucketing process by coordinating with the RuleManager and BucketingManager.
- That you typically interact with it indirectly through other managers like ExperienceManager and FeatureManager.
We saw that when rules pass, the DataManager
relies on the BucketingManager to perform the crucial step of actually assigning a visitor to a specific variation based on traffic percentages. How does that mathematical assignment work?
Let's dive into the hashing and allocation logic in the next chapter: Chapter 6: BucketingManager!
Copyrights © 2025 All Rights Reserved by Convert Insights, Inc.
Core Modules
- ConvertSDK / Core
- Context
- ExperienceManager
- FeatureManager
- DataManager
- BucketingManager
- RuleManager
- ApiManager
- EventManager
- Config / Types
Integration Guides