Event System

Internal pub/sub event system for SDK lifecycle events

In ApiManager, we saw how the SDK acts like a messenger, talking to the Convert servers to fetch configuration data and send back tracking reports. This handles communication with the outside world.

But how do different parts inside the SDK talk to each other? For example, when the configuration data is fetched successfully, how does the rest of the SDK know "the data is here, we're ready to go!"?

The Problem: Internal Announcements

Imagine a large office building (the SDK). Different departments (the Managers like DataManager, ApiManager, etc.) are working on their tasks. When one department finishes something important, how do they notify other departments that might need to know?

  • The Mail Room (ApiManager) receives a big package (the project configuration). How does it announce this arrival?
  • The Decision Desk (BucketingManager) assigns someone to a specific project (buckets a visitor). How can the Logging Department know about this decision to record it?
  • The Front Desk (Core) needs to know when the entire office is officially open for business (SDK is initialized and ready).

They need a structured way to make announcements and for interested departments to listen to those announcements.

What is EventManager? The Office Intercom System

Meet the EventManager! Think of it as the internal intercom or announcement system for the Convert SDK. It allows different parts of the SDK to broadcast messages (events) and other parts (or even you, the developer) to listen for specific messages.

It works on a simple Publish/Subscribe (or "Pub/Sub") model:

  1. Publishing (Firing Events): When something significant happens, a component uses the EventManager to fire an event. This is like making an announcement over the intercom. For example:

    • The Core fires the Ready event when initialization is complete.
    • The Core fires ConfigUpdated when new configuration data is fetched.
    • The Context fires Bucketing when a visitor is assigned to a variation.
    • The Context fires Conversion when trackConversion is called.
  2. Subscribing (Listening with on): Other components (or your code) can register interest in specific events using the on method. When that event fires, the listener's callback function is executed.

This system allows different parts of the SDK to communicate and react to happenings without being tightly coupled or needing direct references to each other.

How it's Used

1. The onReady() Convenience Method:

The most common way you'll interact with the EventManager's work is indirectly, through the SDK's onReady() method (see Core).

When you create an SDK instance, it starts initializing. Once the initial configuration data is fetched and processed, the Core module uses the EventManager to fire the Ready event. The onReady() method had previously set up a listener for this specific event. When the Ready event is fired, the listener's callback runs, which resolves the readiness signal, allowing your application code to proceed.

2. Subscribing to SDK Events:

You can listen for internal events by calling on(eventName, callbackFunction) on the SDK instance (which wraps the EventManager's on method).

  • The first argument is the event name, typically from the SystemEvents enumeration.
  • The second argument is a callback function that receives event-related data and potentially an error object.
  • When the event fires, all registered callbacks for that event are executed.

For example, you can subscribe to the Bucketing event to be notified whenever a visitor is assigned to a variation, or subscribe to the Conversion event to react when a conversion is tracked. This allows for advanced use cases, like sending SDK event data to your own analytics system or debugging internal SDK behaviour.

3. Available Events:

The SDK uses the SystemEvents enumeration for event names:

EventValueTriggered By
Ready'ready'SDK initialization complete
ConfigUpdated'config.updated'Configuration refreshed
Bucketing'bucketing'Visitor bucketed into a variation
Conversion'conversion'Conversion tracked
Segments'segments'Segments evaluated
LocationActivated'location.activated'Location rules matched
LocationDeactivated'location.deactivated'Location rules no longer matched
Audiences'audiences'Audience rules evaluated
ApiQueueReleased'api.queue.released'Tracking queue flushed

Under the Hood: The Intercom Mechanism

How does the EventManager manage announcements and listeners?

1. Keeping Lists (listeners): The EventManager maintains an internal record (a map/dictionary). The keys are event names (e.g., 'ready', 'bucketing'), and the value for each key is a list of all the callback functions that have subscribed to that event.

// Internal state of listeners
{
  'ready': [callback1, callback2],
  'bucketing': [callbackA],
  'conversion': [callbackX, callbackY],
}

2. Subscribing (on): When on('eventName', callbackFn) is called, it finds the list for 'eventName' in the listeners record (or creates it if it doesn't exist) and adds callbackFn to that list.

3. Firing (fire): When some code calls fire('eventName', args, error), the EventManager:

  1. Looks up 'eventName' in its listeners record.
  2. If it finds a list of listeners for that event:
    • It iterates through each callback function in the list.
    • It calls each function, passing the args and error object to it.
  3. If no listeners are found for 'eventName', it does nothing.

4. Deferred Events (like Ready): What if you register a listener for the Ready event after the SDK has already initialized and fired it? You still want your callback to run!

The EventManager has a mechanism for this using the deferred flag in the fire method.

  • When fire('ready', ..., deferred: true) is called, besides notifying current listeners, it also stores the fact that the 'ready' event has happened (in another internal record called deferred).
  • Later, when on('ready', callbackFn) is called, the on method checks the deferred record. If it sees that 'ready' has already fired, it immediately executes the callbackFn with the stored arguments.

This ensures that listeners added after a deferred event has fired still get notified.

Sequence Diagram: onReady Flow

sequenceDiagram
    participant UserCode as Your Code
    participant Core as ConvertSDK / Core
    participant EventMgr as EventManager
    participant ApiMgr as ApiManager

    UserCode->>Core: new ConvertSDK()
    UserCode->>Core: onReady()
    Core->>+EventMgr: on('ready', callbackForPromise)
    EventMgr-->>-Core: Listener registered
    Core->>+ApiMgr: getConfig()
    Note right of Core: SDK Initialization starts...
    ApiMgr-->>Core: Returns Config Data
    Core->>Core: Process data, setup complete.
    Core->>+EventMgr: fire('ready', null, null, true)  // Fire READY event (deferred)
    Note right of EventMgr: Found listener 'callbackForPromise'
    EventMgr->>Core: Execute callbackForPromise()
    Core->>UserCode: Resolve the onReady() Promise
    EventMgr-->>-Core: 
    UserCode-->>UserCode: .then() block runs

Sequence Diagram: Deferred Event (listener registered after fire)

sequenceDiagram
    participant UserCode as Your Code
    participant Core as Core
    participant EventMgr as EventManager

    UserCode->>Core: ConvertSDK::create()
    Core->>Core: Initialize, fetch config...
    Core->>+EventMgr: fire('ready', null, null, deferred: true)
    Note right of EventMgr: No listeners yet — store as deferred
    EventMgr-->>-Core: Done
    Core-->>UserCode: Return Core instance
    UserCode->>+EventMgr: on('ready', callback)
    Note right of EventMgr: 'ready' already fired (deferred) — execute immediately
    EventMgr->>UserCode: Execute callback()
    EventMgr-->>-UserCode: Done

Implementation Details

The EventManager implementation consists of three main elements:

1. Internal State:

The EventManager initializes with:

  • A listeners map -- stores arrays of callbacks keyed by event name.
  • A deferred map -- stores events that fired before listeners were registered, along with their arguments and error data.
  • An optional mapper function -- transforms event arguments before passing them to listeners (defaults to identity/passthrough).

2. The on Method:

When a listener is registered:

  • It ensures an array exists for the given event name in the listeners map.
  • It appends the callback to that array.
  • It checks the deferred store -- if the event already fired as deferred, it immediately calls fire again so the new listener is notified with the stored arguments.

3. The fire Method:

When an event is fired:

  • It retrieves the list of listener functions for the given event.
  • It loops through the list and calls each function, passing the (optionally mapper-transformed) arguments and error.
  • Each listener call is wrapped in error handling so a failing listener does not crash the SDK.
  • If the deferred flag is set to true and the event has not been stored already, it saves the event arguments in the deferred store for future late-subscribing listeners.

4. The removeListeners Method:

Removes all registered listeners for a given event name.

Conclusion

The EventManager is the SDK's internal communication backbone, implementing a simple publish/subscribe pattern. It allows different components to signal important occurrences (fire) and others to react to them (on) without direct dependencies.

Key takeaways:

  1. Why an internal event system is needed for communication between SDK modules.
  2. The "intercom" or "pub/sub" analogy for EventManager.
  3. How components fire events (like Ready, Bucketing, Conversion).
  4. How components (or your code) can listen using on.
  5. How deferred events ensure listeners added late are still notified (important for Ready).
  6. The SystemEvents enumeration provides predefined names for all standard SDK events.