Skip to content

ApiManager

Ahmed Abbas edited this page Apr 23, 2025 · 1 revision

Chapter 8: ApiManager

Welcome back! In Chapter 7: RuleManager, we saw how the SDK acts like a bouncer, checking if visitors meet specific criteria before they can participate in an experiment. We've covered how the SDK manages visitors, experiments, features, data, bucketing, and rules internally.

But how does the SDK get the initial setup information (like the details of your experiments and those rules) from Convert's servers? And how does it report back what happened (like which variation a visitor saw, or if they completed a goal)?

The Problem: Talking to the Outside World

Imagine the Convert SDK running in your visitor's browser is like a local branch office. It needs to:

  1. Get Instructions: Receive the latest operational plan (your project configuration, including experiments, features, audiences) from the headquarters (Convert servers).
  2. Send Reports: Send status updates (tracking events like "visitor A saw variation B", "visitor C completed goal X") back to headquarters so you can see the results in your Convert dashboard.

How does this local branch office (the SDK) communicate reliably with headquarters (Convert servers)?

What is ApiManager? The SDK's Messenger

Meet the ApiManager! Think of it as the dedicated messenger or communication link between the Convert SDK running on your website and the Convert backend servers.

Its primary jobs are:

  1. Fetching the Project Plan (getConfig): When the SDK starts up (Chapter 1: ConvertSDK / Core), if you provided an sdkKey, the ApiManager is responsible for contacting the Convert servers and downloading the necessary project configuration data (all your experiments, features, rules, etc.).
  2. Sending Tracking Reports (enqueue, releaseQueue): When the SDK makes a decision (like assigning a visitor to a variation using the BucketingManager) or tracks a conversion, the DataManager tells the ApiManager about it. The ApiManager doesn't send a separate message for every single event. Instead, it acts efficiently:
    • Collects Messages: It gathers these small tracking reports into a queue.
    • Sends in Batches: It periodically sends these collected reports back to the Convert tracking servers in batches. This is much more efficient than sending dozens of tiny messages individually.

In short, ApiManager handles all the "talking" the SDK needs to do with the external Convert platform.

How it's Used (Mostly Behind the Scenes)

Like several other managers (ExperienceManager, BucketingManager, RuleManager), you'll rarely interact directly with the ApiManager. It's mainly used internally by other core components:

  • By ConvertSDK / Core: During initialization, the Core class uses the ApiManager's getConfig() method to fetch the project configuration if an sdkKey was provided.

    // Simplified conceptual code inside Core.fetchConfig()
    async fetchConfig() {
      try {
        // Tell the ApiManager to get the data
        const data = await this._apiManager.getConfig();
        // ... process the received data ...
      } catch (error) { /* Handle error */ }
    }
  • By DataManager: When the DataManager records a bucketing decision or a conversion event, it doesn't send it immediately. It tells the ApiManager to add the event to its queue using enqueue().

    // Simplified conceptual code inside DataManager._retrieveBucketing()
    _retrieveBucketing(visitorId, experience, /*...*/) {
      // ... bucketing logic determines chosenVariationId ...
    
      // Store the decision locally...
      this.putData(visitorId, { bucketing: { /*...*/ } });
    
      // Tell ApiManager to queue a tracking event for this decision
      if (enableTracking) {
        const eventData = { type: 'bucketing', /* other details */ };
        this._apiManager.enqueue(visitorId, eventData);
      }
    
      // Return the variation details...
    }

So, ApiManager is the behind-the-scenes worker handling the communication initiated by other parts of the SDK.

Under the Hood: How Communication Works

Let's look at the two main tasks: fetching data and sending tracking.

1. Fetching Configuration (getConfig)

  • When Core.fetchConfig() calls apiManager.getConfig(), the ApiManager prepares and sends a simple HTTP GET request.
  • Target: It sends the request to the Convert configuration endpoint URL (which it gets from the SDK settings, like https://cdn.convert.com). The URL includes your unique sdkKey (e.g., https://cdn.convert.com/config/YOUR_SDK_KEY).
  • Response: The Convert server responds with a large JSON object containing all the details about your project (experiences, features, audiences, goals, etc.).
  • Result: The ApiManager receives this JSON data and passes it back to the Core, which then gives it to the DataManager for storage.
sequenceDiagram
    participant Core as ConvertSDK / Core
    participant ApiMgr as ApiManager
    participant ConvertAPI as Convert Config API

    Core->>+ApiMgr: getConfig()
    Note over ApiMgr: Prepare HTTP GET request for /config/YOUR_SDK_KEY
    ApiMgr->>+ConvertAPI: GET /config/YOUR_SDK_KEY
    ConvertAPI-->>-ApiMgr: Respond with Project JSON data
    ApiMgr-->>-Core: Return Project JSON data
Loading

2. Sending Tracking Events (enqueue and releaseQueue)

This is a bit more complex because of the batching mechanism.

  • Step 1: Enqueue: When DataManager calls apiManager.enqueue(visitorId, eventData), the ApiManager doesn't send anything yet. It simply adds the eventData (like {type: 'conversion', goalId: '123'}) to an internal list (_requestsQueue.items) associated with the visitorId. It also increments a counter (_requestsQueue.length).

  • Step 2: Check Batch Conditions: After adding the event, ApiManager checks two things:

    • Did the queue size just reach the batchSize limit (e.g., 10 events)?
    • Was this the first event added to an empty queue?
  • Step 3: Release Queue (if needed):

    • Batch Size Reached: If the queue is full (length === batchSize), the ApiManager immediately calls its own releaseQueue('size') method.
    • First Event Added: If this was the first event, the ApiManager starts a timer (startQueue()). This timer is set for the releaseInterval (e.g., 10 seconds). If the timer runs out before the batch size is reached, it will trigger releaseQueue('timeout').
  • Step 4: Send Batch (releaseQueue): When releaseQueue is called (either by batch size or timeout):

    • It stops any active timer (stopQueue()).
    • It copies the current contents of the queue (_requestsQueue.items).
    • It resets the internal queue (_requestsQueue.reset()) so new events can be collected.
    • It packages the copied events into a payload suitable for the Convert tracking API.
    • It sends this payload as an HTTP POST request to the Convert tracking endpoint URL (like https://track.convert.com/track/YOUR_SDK_KEY).
    • Optimization: In browsers, it might try using navigator.sendBeacon() for this POST request, which is more reliable if the user navigates away quickly.
sequenceDiagram
    participant DataMgr as DataManager
    participant ApiMgr as ApiManager
    participant ConvertAPI as Convert Track API

    DataMgr->>+ApiMgr: enqueue(visitor1, event1)
    Note over ApiMgr: Add event1 to queue. Queue size = 1.\nStart 10s timer.
    DataMgr->>+ApiMgr: enqueue(visitor2, event2)
    Note over ApiMgr: Add event2 to queue. Queue size = 2.
    DataMgr->>+ApiMgr: enqueue(visitor1, event3)
    Note over ApiMgr: Add event3 to visitor1's events. Queue size = 3.
    loop Every releaseInterval (e.g., 10s) OR when Queue fills up
        ApiMgr->>ApiMgr: releaseQueue() Triggered (e.g., by timeout)
        Note over ApiMgr: Stop timer. Copy queue items. Reset queue.
        Note over ApiMgr: Prepare HTTP POST payload with events 1, 2, 3.
        ApiMgr->>+ConvertAPI: POST /track/YOUR_SDK_KEY (payload: [event1, event2, event3])
        ConvertAPI-->>-ApiMgr: Acknowledge receipt (e.g., HTTP 200 OK)
        ApiMgr-->>ApiMgr: Handle response (log success/error)
    end
Loading

This batching ensures the SDK doesn't overload the network with too many small requests, improving performance.

Code Dive: ApiManager Implementation

Let's look at simplified snippets from packages/api/src/api-manager.ts.

1. The Constructor

Sets up endpoints, batching parameters, and initial tracking data structure.

// File: packages/api/src/api-manager.ts (Simplified)

import { HttpClient, HttpRequest, HttpResponse } from '@convertcom/js-sdk-utils';
// ... other imports ...

const DEFAULT_BATCH_SIZE = 10;
const DEFAULT_RELEASE_INTERVAL = 10000; // 10 seconds
const DEFAULT_CONFIG_ENDPOINT = 'https://cdn.convert.com';
const DEFAULT_TRACK_ENDPOINT = 'https://track.convert.com';

export class ApiManager implements ApiManagerInterface {
  private _requestsQueue: VisitorsQueue;
  private _requestsQueueTimerID: number; // For the release interval timer

  private readonly _configEndpoint: string;
  private readonly _trackEndpoint: string;
  private _sdkKey: string;
  private _projectId: string;
  private _trackingEvent: TrackingEvent; // Base structure for tracking payload
  private _trackingEnabled: boolean; // Can be turned off in config

  readonly batchSize: number;
  readonly releaseInterval: number;

  constructor(config?: Config, { /* dependencies */ }) {
    // Get URLs from config or use defaults
    this._configEndpoint = config?.api?.endpoint?.config || DEFAULT_CONFIG_ENDPOINT;
    this._trackEndpoint = config?.api?.endpoint?.track || DEFAULT_TRACK_ENDPOINT;

    // Get batch settings from config or use defaults
    this.batchSize = Number(config?.events?.batch_size) || DEFAULT_BATCH_SIZE;
    this.releaseInterval = Number(config?.events?.release_interval) || DEFAULT_RELEASE_INTERVAL;

    // SDK Key needed for API calls
    this._sdkKey = config?.sdkKey || '';
    this._projectId = config?.data?.project?.id; // May be set later via setData

    // Tracking enabled by default, can be disabled
    this._trackingEnabled = config?.network?.tracking !== false;

    // Base structure for the payload sent to the tracking API
    this._trackingEvent = {
      accountId: config?.data?.account_id,
      projectId: this._projectId,
      visitors: []
      // ... other fields like enrichData, source ...
    };

    // Initialize the queue object with methods to push/reset
    this._requestsQueue = {
      length: 0,
      items: [],
      push(/* ... implementation ... */){ /* Adds item and increments length */ },
      reset(){ this.items = []; this.length = 0; }
    };
    // ... store loggerManager, eventManager ...
  }
  // ... other methods ...
}
  • Reads configuration for API endpoints (_configEndpoint, _trackEndpoint).
  • Reads batching parameters (batchSize, releaseInterval).
  • Stores the sdkKey needed for API paths.
  • Sets up the internal _requestsQueue object and the base _trackingEvent payload.

2. Fetching Configuration (getConfig)

Makes the GET request to the config endpoint.

// File: packages/api/src/api-manager.ts (Simplified)

// Inside ApiManager class:
  getConfig(): Promise<ConfigResponseData> {
    this._loggerManager?.trace?.('ApiManager.getConfig()');
    // Basic request structure
    const path: Path = {
      base: this._configEndpoint,
      route: `/config/${this._sdkKey}` // Append SDK key to path
      // Note: Actual code might add query params like '?environment=...'
    };

    // Use the internal request method (which uses HttpClient)
    return new Promise((resolve, reject) => {
      this.request('get', path)
        .then(({ data }) => resolve(data)) // Extract data from response
        .catch(reject);
    });
  }

  // Internal helper using HttpClient (Highly Simplified)
  private async request(
    method: string, path: Path, data: Record<string, any> = {}
  ): Promise<HttpResponse> {
    const requestConfig: HttpRequest = {
      method: <HttpMethod>method,
      path: path.route,
      baseURL: path.base,
      data: data, // Body for POST requests
      headers: { 'Content-Type': 'application/json' /* ... other headers */ }
    };
    // Assume HttpClient makes the actual network call
    return HttpClient.request(requestConfig);
  }
  • Constructs the path object with the base URL and the specific route /config/YOUR_SDK_KEY.
  • Calls the internal request helper (which uses the HttpClient utility we saw referenced before) to make the GET request.
  • Returns the data part of the successful response.

3. Enqueuing Tracking Events (enqueue)

Adds an event to the queue and checks if the queue should be released.

// File: packages/api/src/api-manager.ts (Simplified)

// Inside ApiManager class:
  enqueue(
    visitorId: string,
    eventRequest: VisitorTrackingEvents, // The event data {type, ...}
    segments?: VisitorSegments // Optional visitor segment info
  ): void {
    this._loggerManager?.trace?.('ApiManager.enqueue()', { eventRequest });

    // Add the event to the internal queue list
    this._requestsQueue.push(visitorId, eventRequest, segments);

    // Only manage queue timing/release if tracking is enabled
    if (this._trackingEnabled) {
      // If queue is now full, release it immediately
      if (this._requestsQueue.length >= this.batchSize) {
        this.releaseQueue('size'); // Release due to size limit
      }
      // If this was the *first* item added (queue was empty before)
      // start the timer for the release interval.
      else if (this._requestsQueue.length === 1) {
        this.startQueue(); // Start the timeout timer
      }
    }
  }

  // Helper to start the release timer
  private startQueue(): void {
    // Clear any existing timer first
    clearTimeout(this._requestsQueueTimerID);
    // Set a new timer
    this._requestsQueueTimerID = setTimeout(() => {
      this.releaseQueue('timeout'); // Release due to timeout
    }, this.releaseInterval) as any;
  }

  // Helper to stop the release timer
  private stopQueue(): void {
    clearTimeout(this._requestsQueueTimerID);
  }
  • Calls this._requestsQueue.push to add the event data.
  • Checks if tracking is enabled.
  • Checks if the queue length has reached batchSize, calling releaseQueue if it has.
  • Checks if the queue length is now 1 (meaning it was empty before), calling startQueue to begin the timeout if it is.

4. Releasing the Queue (releaseQueue)

Sends the batched events to the tracking endpoint.

// File: packages/api/src/api-manager.ts (Simplified)

// Inside ApiManager class:
  async releaseQueue(reason?: string): Promise<any> {
    // Do nothing if queue is empty or tracking is off
    if (!this._requestsQueue.length || !this._trackingEnabled) return;

    this._loggerManager?.info?.('ApiManager.releaseQueue()', { reason });

    // Stop the interval timer since we are sending now
    this.stopQueue();

    // Prepare the payload: Copy current queue items into the tracking structure
    const payload: TrackingEvent = {
      ...this._trackingEvent, // Base info (accountId, projectId)
      visitors: this._requestsQueue.items.slice() // Copy visitors and their events
      // source: 'js-sdk' // Add source info
    };

    // Reset the queue for the next batch BEFORE sending
    this._requestsQueue.reset();

    // Define the tracking API path
    const path: Path = {
      base: this._trackEndpoint,
      route: `/track/${this._sdkKey}`
    };

    try {
      // Send the payload via POST request
      // NOTE: Actual implementation might try navigator.sendBeacon first
      const result = await this.request('post', path, payload);
      // ... logging success, maybe fire event ...
      return result;
    } catch (error) {
      // TODO: Handle errors (e.g., retry logic, exponential backoff)
      this._loggerManager?.error?.('ApiManager.releaseQueue() failed', { error });
      // Consider re-adding items to queue or alternative error handling
      // For simplicity here, we just log the error.
      throw error; // Re-throw for caller to potentially handle
    }
  }
  • Checks if the queue has items and tracking is enabled.
  • Stops the timeout timer (stopQueue).
  • Copies the queue items (this._requestsQueue.items.slice()) into the payload.
  • Important: Resets the queue (_requestsQueue.reset()) immediately before the network request, so new events arriving during the request are added to a fresh queue.
  • Calls this.request to send the payload via HTTP POST to the /track/YOUR_SDK_KEY endpoint.
  • Includes basic error handling (logging).

Conclusion

The ApiManager is the vital communication link between the Convert SDK operating in the user's environment and the Convert backend servers. It acts as the messenger, responsible for:

  1. Fetching the project configuration (getConfig) when the SDK initializes (if using an sdkKey).
  2. Collecting and Batching tracking events (enqueue).
  3. Sending these batched events (releaseQueue) efficiently back to Convert for reporting.

While you don't usually call its methods directly, understanding the ApiManager helps clarify how the SDK gets its instructions and reports results back to your Convert dashboard. It relies on batching and timers to do this efficiently.

We've now seen how most of the core managers interact and rely on each other. But how do these components signal to each other when something important happens, like when the configuration data has been successfully fetched and the SDK is ready? For internal communication, the SDK uses an event system.

Let's explore this internal notification system in the next chapter: Chapter 9: EventManager!

Clone this wiki locally