Skip to main content

Platform Nuances & Behavior

This page documents behaviors that are correct by design but not immediately obvious from the basic docs. Read through these before deploying to production — they'll save you debugging time.


SDK Initialization & Timing

Init timeout only covers config fetch

The initTimeoutMs option (default: 10 seconds) only applies to the initial config fetch from the server. Feature gate loading, flow fetching, and content loading happen after init resolves. This means init can succeed while flows are still loading in the background.

// init resolves here — but flows may still be loading
await DAP("init", { tenantKey: "wfx_...", baseUrl: "..." });

// Use onBeforeFlowStart to know when a specific flow is about to render
DAP("init", {
tenantKey: "wfx_...",
onBeforeFlowStart: function (flowId) {
// Navigate to the right page, close modals, etc.
return navigateToPage(flowId);
},
});

Calling identify() before init completes

If you call identify() before init finishes, the call is queued and processed once initialization completes. However, any events emitted between init and identify may carry anonymous context — the backend receives them before it knows who the user is.

Recommended ordering:

DAP("init", { tenantKey: "wfx_..." });
DAP("identify", { userId: "user-42", traits: { plan: "pro" } });
// Now it's safe to track custom events
DAP("track", "feature_used", { feature: "export" });

Content Refresh & Polling

Minimum refresh interval is 120 seconds

The contentRefreshInterval option accepts a value in milliseconds, but values below 120000 (2 minutes) are silently clamped to 120000. This matches the server-side cache TTL — polling faster would return the same cached response. No error or warning is logged unless debug: true is enabled.

// You set 30 seconds...
DAP("init", { tenantKey: "wfx_...", contentRefreshInterval: 30000 });
// ...but the SDK actually polls every 120 seconds
info

Polling pauses automatically when the browser tab is hidden and resumes with an immediate refresh when the tab becomes visible again.


Event Pipeline

Batching parameters

The SDK batches events before sending them to the server:

ParameterValueDescription
Batch size30 eventsEvents are flushed when the batch reaches this size
Flush interval1 secondEvents are flushed at least this often
Max queue size1,000 eventsHard cap on queued events

Queue overflow

When the queue exceeds 1,000 events, the oldest 10% are dropped with a console warning. This can happen on pages with heavy click tracking or rapid custom event emission.

If you're tracking high-frequency interactions, consider disabling auto-tracking and tracking specific events manually:

DAP("init", { tenantKey: "wfx_...", autoTrack: false });
// Track only the interactions you care about
DAP("track", "button_click", { buttonId: "checkout" });

SendBeacon and partial delivery

On page unload (tab close, navigation away), the SDK uses navigator.sendBeacon() to flush remaining events. SendBeacon has a browser-imposed ~64KB payload limit. The SDK splits large batches into 60KB chunks.

caution

If a batch requires multiple chunks, some may succeed while others fail. This can cause gaps in event data for sessions that end with a large event backlog. For critical events, consider flushing manually before navigation:

DAP("flush"); // Force-send all queued events
window.location.href = "/next-page";

Storage & Persistence

Fallback chain

The SDK uses a layered storage strategy. If the preferred storage is unavailable (blocked by browser policy, quota exceeded, or unavailable in the current context), it falls back to the next option:

DataPrimary storageFallback
Anonymous IDCookie (365-day expiry)In-memory
User ID & traitssessionStorageIn-memory
Session IDsessionStorageIn-memory
caution

In private/incognito browsing or when storage is blocked (e.g., cross-origin iframes), the SDK falls back to in-memory storage. This means identity and session data are lost on page reload — each page load creates a new anonymous session.

Frequency gating and storage failures

Flow frequency settings use browser storage to remember whether a user has already seen a flow:

  • "Show once" — stored in localStorage (persists across sessions)
  • "Once per session" — stored in sessionStorage (cleared when the tab closes)

If storage access fails (blocked by browser policy, quota exceeded), the SDK fails open — the flow is shown rather than hidden. This is a deliberate design choice: it's better to show a flow twice than to permanently hide it from users who should see it.


DOM & Visual Rendering

Overlay z-index

The SDK renders all overlays (tooltips, modals, beacons, highlights) inside a Shadow DOM container at z-index: 2147483647 — the maximum CSS integer value. The Shadow DOM provides CSS isolation from your application, but z-index stacking contexts still apply.

If your application uses extremely high z-index values for its own modals or overlays, they may appear above or below SDK content depending on stacking context. Use onBeforeFlowStart to close competing overlays before a flow starts.

Selector resilience (handling DOM changes)

CSS selectors are captured at authoring time (when you record a flow or place a tooltip). If the DOM changes between authoring and runtime — dynamic class names, restructured components, framework updates — the original selector may not match.

The SDK handles this with progressive selector fallback:

  1. Try the original selector
  2. Strip positional pseudo-classes (:nth-child, :last-of-type)
  3. Strip utility classes (e.g., Tailwind classes)
  4. Try the last 2-3 selector segments only
  5. Try the leaf element selector alone

A fallback variant is only accepted if it matches exactly one element — ambiguous matches are rejected to avoid targeting the wrong element. If all variants fail, the content item is skipped.

tip

If a tooltip or beacon stops appearing after a deploy, it's likely due to a selector that no longer matches. Use the Content Health dashboard to detect and fix broken selectors.


Sessions & Identity

Session timeout

Sessions expire after 30 minutes of inactivity. Activity is tracked via a heartbeat that fires every minute. When the browser tab is hidden (user switches tabs), the heartbeat pauses — background tabs do not keep sessions alive indefinitely.

Identity sync edge case

When you call identify(), the SDK updates local state immediately (optimistic update) and sends an API call to notify the backend. If the API call fails (network error, timeout), the local state is still updated — the user sees targeted content for that session.

However, on the next page reload, the SDK may revert to anonymous because the backend never learned about the identity. Events emitted between the failed identify call and the next reload may be attributed to the anonymous ID.

info

Enable debug: true to see identity sync errors in the console. Look for [DAP] identify API call failed messages.


Localization

Locale resolution order

The SDK resolves the display locale in this priority order:

  1. config.locale — the value passed to DAP('init', { locale: '...' })
  2. Tenant default locale — configured in Settings > Tenant Settings in the dashboard
  3. Browser detection — the user's browser language setting

Regional variants (e.g., pt-BR) fall back to the base language (pt) if the regional variant has no translations available.


API: Authentication & Origin Validation

Origin header requirements depend on HTTP method

API keys bound to a site validate the request's Origin header against allowed domains. However, this validation is asymmetric by HTTP method:

  • GET / HEAD requestsOrigin header is optional (to support server-side rendering and preview tools)
  • POST / PUT / PATCH / DELETE requestsOrigin header is required; missing origin returns 401 AUTH_ORIGIN_REQUIRED

Localhost always bypasses domain validation

Requests from localhost, 127.0.0.1, *.localhost, and browser extension origins (chrome-extension://, moz-extension://) always bypass domain validation, regardless of the API key's allowed domains.

caution

This means the SDK always works during local development, even if the production domain is not configured. Always test with your production domain configured before deploying — a missing domain allowlist entry will cause 401 errors in production that you won't see locally.


API: Rate Limits

Rate limits are per-instance, not distributed

Rate limits are enforced in-memory on each API server instance, not via a shared store. In a multi-instance deployment, the effective rate limit is approximately:

effective_limit ≈ configured_limit × number_of_instances

This means rate limits are approximate, not exact. A single user can potentially exceed the documented limit if their requests are distributed across instances.


API: Idempotency

Using the Idempotency-Key header

Mutation endpoints (POST, PUT, PATCH, DELETE) support an optional Idempotency-Key header for safe retries. The key must be a valid UUID.

ScenarioBehavior
First request with a keyProcessed normally, response cached for 24 hours
Repeat request with same keyReturns the cached response with idempotency-replay: true header
Concurrent request with same keyReturns 409 Conflict (retry after the first request completes)
Redis unavailableIdempotency checking is skipped; request proceeds normally

Debugging

Enable debug mode in development

Many SDK subsystems — event pipeline, storage, identity, content refresh, selector resolution — fail silently in production mode to avoid disrupting the end user experience. This means problems can go unnoticed during development if debug mode is off.

Always enable debug: true during development:

DAP("init", {
tenantKey: "wfx_...",
baseUrl: "...",
debug: true, // Logs all SDK operations with [DAP] prefix
});

Debug mode logs every SDK lifecycle event, content fetch, targeting evaluation, and error to the browser console with a [DAP] prefix. Disable it in production to avoid console noise.