Skip to content

Conversation

@francoischalifour
Copy link
Member

Enables calling identify(), track(), page(), alias(), and updateTraits() before init(). Commands are queued and replayed when init() is called with granted consent.

Usage

// ✅ Now works — no init() required upfront
altertable.identify('user123', { email: 'user@example.com' });
altertable.track('Page Loaded');

// Later...
altertable.init(apiKey, { trackingConsent: 'granted' });
// → Queued commands are flushed automatically

Details

Why this is needed

In frameworks like React, effects run bottom-to-top. When init() is called in a layout component and tracking happens in a child component, the child's effect fires first:

// Layout.tsx (parent)
useEffect(() => {
  altertable.init(apiKey); // Runs SECOND
}, [altertable]);

// Page.tsx (child)
useEffect(() => {
  altertable.track('Page Loaded'); // Runs FIRST — would throw before this PR
}, [altertable]);

This is now handled gracefully — calls before init() are queued and replayed.

Architecture

Replaced EventQueue (event-only) with generic Queue<QueueItem> that handles both:

  • Commands: Pre-init method calls with captured arguments
  • Events: Built payloads waiting for consent
flowchart TD
    A[Method called] --> B{Initialized?}
    B -->|No| C[Queue command + runtime context]
    B -->|Yes| D{Consent granted?}
    D -->|Yes| E[Send immediately]
    D -->|Pending/Dismissed| F[Queue event payload]
    D -->|Denied| G[Discard]
    
    H[init called] --> I{Consent status?}
    I -->|Granted| J[Flush queue]
    I -->|Denied| K[Clear queue]
    I -->|Pending| L[Keep queued]
Loading

Runtime context capture

For time-sensitive data (track(), page()), browser state is captured at call time:

type RuntimeContext = {
  timestamp: string;   // ISO timestamp
  url: string | null;  // window.location.href
  referrer: string | null;
  viewport: string | null;
};

This ensures queued events reflect the user's state when the method was called, not when the queue is flushed.

Key behaviors

Method Before this PR After this PR (before init) After this PR (after init with granted consent)
track() ❌ Throws Queued with context Replayed with original timestamp/URL
identify() ❌ Throws Queued Replayed, sets distinct_id for subsequent items
page() ❌ Throws Queued with context Replayed with original viewport/referrer
alias() ❌ Throws Queued Replayed
updateTraits() ❌ Throws Queued Replayed (fails gracefully if no prior identify)
reset() ❌ Throws Clears queue Clears queue + session
configure() ❌ Throws ❌ Throws Works normally
getTrackingConsent() ❌ Throws Returns "pending" Returns actual value

Queue overflow

Queue has 1000 item capacity. When full, oldest items are dropped with a dev warning:

[Altertable] Queue is full (1000 items). Dropping track call.

@francoischalifour francoischalifour merged commit 24fd79a into main Jan 8, 2026
1 check passed
@francoischalifour francoischalifour deleted the fc/pre-init-queue branch January 8, 2026 09:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants