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
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:
| Parameter | Value | Description |
|---|---|---|
| Batch size | 30 events | Events are flushed when the batch reaches this size |
| Flush interval | 1 second | Events are flushed at least this often |
| Max queue size | 1,000 events | Hard 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.
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:
| Data | Primary storage | Fallback |
|---|---|---|
| Anonymous ID | Cookie (365-day expiry) | In-memory |
| User ID & traits | sessionStorage | In-memory |
| Session ID | sessionStorage | In-memory |
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:
- Try the original selector
- Strip positional pseudo-classes (
:nth-child,:last-of-type) - Strip utility classes (e.g., Tailwind classes)
- Try the last 2-3 selector segments only
- 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.
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.
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:
config.locale— the value passed toDAP('init', { locale: '...' })- Tenant default locale — configured in Settings > Tenant Settings in the dashboard
- 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/HEADrequests —Originheader is optional (to support server-side rendering and preview tools)POST/PUT/PATCH/DELETErequests —Originheader is required; missing origin returns401 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.
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.
| Scenario | Behavior |
|---|---|
| First request with a key | Processed normally, response cached for 24 hours |
| Repeat request with same key | Returns the cached response with idempotency-replay: true header |
| Concurrent request with same key | Returns 409 Conflict (retry after the first request completes) |
| Redis unavailable | Idempotency 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.