--- name: fullstory-async-methods version: v2 description: Comprehensive guide for implementing Fullstory's Asynchronous API methods (Async suffix variants) for web applications. Teaches proper Promise handling, await patterns, error handling, and when to use async vs fire-and-forget methods. Includes detailed good/bad examples for initialization waiting, session URL retrieval, and conditional flows to help developers handle Fullstory's asynchronous nature correctly. related_skills: - fullstory-observe-callbacks - fullstory-identify-users - fullstory-analytics-events - fullstory-capture-control --- # Fullstory Asynchronous Methods API ## Overview Fullstory's Browser API provides asynchronous versions of all methods by appending `Async` to the method name. These async methods return Promise-like objects that resolve when Fullstory has started and the action completes. This is essential for: - **Initialization Waiting**: Wait for Fullstory to fully bootstrap before taking actions - **Session URL Retrieval**: Get the session replay URL for logging, support tickets, etc. - **Error Handling**: Know if an API call succeeded or failed - **Sequential Operations**: Ensure operations complete in order - **Conditional Logic**: Take action based on Fullstory state ## Core Concepts ### Sync vs Async Methods | Method Type | Returns | Use When | | ----------------------- | ------------ | ------------------------------------------ | | `FS('methodName')` | undefined | Fire-and-forget, don't need result | | `FS('methodNameAsync')` | Promise-like | Need result, error handling, or sequencing | ### Promise-like Object The object returned from async methods: - Can be `await`ed - Supports `.then()` chaining - **Important**: `.catch()` may not work in older browsers without Promise polyfill - May reject if Fullstory fails to initialize ### Available Async Methods Every FS method has an async variant: | Sync Method | Async Method | | ---------------------------- | --------------------------------- | | `FS('setIdentity', {...})` | `FS('setIdentityAsync', {...})` | | `FS('setProperties', {...})` | `FS('setPropertiesAsync', {...})` | | `FS('trackEvent', {...})` | `FS('trackEventAsync', {...})` | | `FS('getSession')` | `FS('getSessionAsync')` | | `FS('shutdown')` | `FS('shutdownAsync')` | | `FS('restart')` | `FS('restartAsync')` | | `FS('log', {...})` | `FS('logAsync', {...})` | --- ## API Reference ### Basic Syntax ```javascript // Async/await pattern const result = await FS('methodNameAsync', params) // Promise pattern FS('methodNameAsync', params).then((result) => { /* handle result */ }) ``` ### Return Values | Method | Resolves With | | -------------------- | ------------------------------------ | | `getSessionAsync` | Session URL string | | `setIdentityAsync` | undefined (completion signal) | | `setPropertiesAsync` | undefined | | `trackEventAsync` | undefined | | `shutdownAsync` | undefined | | `restartAsync` | undefined | | `observeAsync` | Observer object with `.disconnect()` | ### Rejection Scenarios The Promise may reject when: - Malformed or missing configuration (no `_fs_org`) - User on unsupported browser - Error in `rec/settings` or `rec/page` calls - Organization over quota - Fullstory script blocked by ad blocker (may not reliably reject) --- ## ✅ GOOD IMPLEMENTATION EXAMPLES ### Example 1: Get Session URL for Support ```javascript // GOOD: Get session URL for support ticket async function attachSessionToSupportTicket(ticketId) { try { const sessionUrl = await FS('getSessionAsync') // Attach to support ticket await updateSupportTicket(ticketId, { fullstoryUrl: sessionUrl, attachedAt: new Date().toISOString(), }) console.log('Session attached to ticket:', sessionUrl) return sessionUrl } catch (error) { console.warn('Could not get Fullstory session:', error) // Continue without session URL - non-critical return null } } // Usage document.getElementById('help-button').addEventListener('click', async () => { const ticket = await createSupportTicket(userIssue) await attachSessionToSupportTicket(ticket.id) showTicketConfirmation(ticket) }) ``` **Why this is good:** - ✅ Uses try/catch for error handling - ✅ Gracefully handles Fullstory being unavailable - ✅ Non-blocking failure (user can still submit ticket) - ✅ Returns null on failure for caller to handle ### Example 2: Wait for Fullstory Before Critical Actions ```javascript // GOOD: Ensure Fullstory is ready before identifying async function initializeAnalytics(user) { try { // Wait for Fullstory to be ready await FS('setIdentityAsync', { uid: user.id, properties: { displayName: user.name, email: user.email, }, }) console.log('User identified successfully') // Now safe to track initial events await FS('trackEventAsync', { name: 'Session Started', properties: { entryPage: window.location.pathname, referrer: document.referrer, }, }) return true } catch (error) { console.error('Fullstory initialization failed:', error) // Analytics failure shouldn't break the app return false } } // Usage in app bootstrap async function bootstrap() { const user = await authenticateUser() // Initialize analytics (don't block on failure) initializeAnalytics(user) // Continue app initialization renderApp() } ``` **Why this is good:** - ✅ Waits for identification to complete - ✅ Sequential: identify before tracking events - ✅ Handles errors gracefully - ✅ Doesn't block app on analytics failure ### Example 3: Session URL in Error Reports ```javascript // GOOD: Include session URL in error logging async function captureError(error, context = {}) { let sessionUrl = null try { // Try to get session URL, but don't let it block error reporting sessionUrl = await Promise.race([ FS('getSessionAsync'), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 2000)), ]) } catch (e) { // Session URL unavailable - continue without it } // Send error to monitoring service await errorMonitor.captureException(error, { ...context, fullstoryUrl: sessionUrl, timestamp: new Date().toISOString(), }) // Also log to Fullstory if available if (typeof FS !== 'undefined') { FS('log', { level: 'error', msg: error.message, }) } } // Usage window.addEventListener('error', (event) => { captureError(event.error, { source: 'window.onerror', filename: event.filename, lineno: event.lineno, }) }) ``` **Why this is good:** - ✅ Timeout prevents hanging on unresponsive FS - ✅ Error reporting continues without session URL - ✅ Enriches error context when available - ✅ Logs error to Fullstory too ### Example 4: Observer Pattern with Async ```javascript // GOOD: Set up Fullstory observers with proper cleanup async function setupFullstoryObservers() { const observers = [] try { // Observer for when Fullstory starts capturing const startObserver = await FS('observeAsync', { type: 'start', callback: () => { console.log('Fullstory started capturing') initializeSessionTracking() }, }) observers.push(startObserver) // Observer for session URL availability const sessionObserver = await FS('observeAsync', { type: 'session', callback: (session) => { console.log('Session URL:', session.url) storeSessionUrl(session.url) }, }) observers.push(sessionObserver) // Return cleanup function return () => { observers.forEach((obs) => obs.disconnect()) } } catch (error) { console.warn('Could not set up Fullstory observers:', error) return () => {} // No-op cleanup } } // Usage with React function App() { useEffect(() => { let cleanup = () => {} setupFullstoryObservers().then((cleanupFn) => { cleanup = cleanupFn }) return () => cleanup() }, []) return } ``` **Why this is good:** - ✅ Proper async observer setup - ✅ Cleanup function for component unmount - ✅ Handles initialization failure - ✅ Multiple observers managed together ### Example 5: Conditional Feature Based on FS Status ```javascript // GOOD: Enable features only if Fullstory is working class SessionReplayFeature { constructor() { this.isAvailable = false this.sessionUrl = null } async initialize() { try { // Check if Fullstory is capturing this.sessionUrl = await FS('getSessionAsync') this.isAvailable = true return true } catch (error) { this.isAvailable = false console.info('Session replay feature unavailable:', error.message) return false } } getShareableLink() { if (!this.isAvailable || !this.sessionUrl) { return null } return this.sessionUrl } renderShareButton() { if (!this.isAvailable) { return null // Don't show button if FS unavailable } return `` } } // Usage const sessionReplay = new SessionReplayFeature() async function initializeUI() { await sessionReplay.initialize() if (sessionReplay.isAvailable) { showSessionReplayUI() } } ``` **Why this is good:** - ✅ Graceful degradation when FS unavailable - ✅ Feature flag based on actual FS status - ✅ No broken UI if FS blocked - ✅ Clear availability check ### Example 6: Sequential Operations ```javascript // GOOD: Ensure proper sequence of FS operations async function completeCheckout(orderData) { try { // 1. First, ensure user is identified await FS('setIdentityAsync', { uid: orderData.userId, properties: { displayName: orderData.customerName, email: orderData.customerEmail, }, }) // 2. Update user properties with purchase info await FS('setPropertiesAsync', { type: 'user', properties: { lifetimeValue: orderData.customerLTV, totalOrders: orderData.customerOrderCount, lastOrderAt: new Date().toISOString(), }, }) // 3. Track the purchase event await FS('trackEventAsync', { name: 'Order Completed', properties: { orderId: orderData.id, revenue: orderData.total, itemCount: orderData.items.length, }, }) // 4. Get session URL for order records const sessionUrl = await FS('getSessionAsync') // 5. Update order with session URL await saveOrderSessionUrl(orderData.id, sessionUrl) console.log('Checkout tracked successfully') } catch (error) { // Log but don't fail checkout console.error('Analytics tracking failed:', error) } } ``` **Why this is good:** - ✅ Operations happen in correct order - ✅ User identified before properties set - ✅ Event tracked after user data set - ✅ Session URL captured at end - ✅ Errors don't break checkout --- ## ❌ BAD IMPLEMENTATION EXAMPLES ### Example 1: Blocking App on Fullstory ```javascript // BAD: Blocking application startup on Fullstory async function startApp() { // This will hang if Fullstory is blocked! const sessionUrl = await FS('getSessionAsync') // App never starts if FS fails renderApp() } ``` **Why this is bad:** - ❌ App hangs if Fullstory blocked by ad blocker - ❌ Promise may never resolve - ❌ Critical path depends on non-critical service - ❌ No timeout or error handling **CORRECTED VERSION:** ```javascript // GOOD: Non-blocking initialization async function startApp() { // Start app immediately renderApp() // Initialize analytics separately try { await Promise.race([ FS('getSessionAsync'), new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000)), ]) enableAnalyticsFeatures() } catch (error) { console.warn('Fullstory unavailable, continuing without analytics') } } ``` ### Example 2: Missing Error Handling ```javascript // BAD: No error handling for async call async function trackPurchase(order) { const sessionUrl = await FS('getSessionAsync') // May throw! saveSessionToOrder(order.id, sessionUrl) // Never runs if above fails await FS('trackEventAsync', { // Also may throw name: 'Purchase', properties: {orderId: order.id}, }) } ``` **Why this is bad:** - ❌ Unhandled promise rejection - ❌ Subsequent code won't run on failure - ❌ No graceful degradation - ❌ Could crash in strict mode **CORRECTED VERSION:** ```javascript // GOOD: Proper error handling async function trackPurchase(order) { let sessionUrl = null try { sessionUrl = await FS('getSessionAsync') } catch (error) { console.warn('Could not get session URL:', error) } if (sessionUrl) { saveSessionToOrder(order.id, sessionUrl) } try { await FS('trackEventAsync', { name: 'Purchase', properties: { orderId: order.id, hasSessionUrl: !!sessionUrl, }, }) } catch (error) { console.warn('Could not track purchase event:', error) } } ``` ### Example 3: Using .catch() Without Polyfill ```javascript // BAD: .catch() may not work in older browsers FS('getSessionAsync') .then((url) => console.log('Session:', url)) .catch((err) => console.error('Error:', err)) // May fail silently in IE11! ``` **Why this is bad:** - ❌ `.catch()` not supported in browsers without Promise - ❌ Fullstory's Promise-like object may not implement catch - ❌ Errors may go unhandled **CORRECTED VERSION:** ```javascript // GOOD: Use try/catch with async/await async function getSession() { try { const url = await FS('getSessionAsync') console.log('Session:', url) return url } catch (err) { console.error('Error:', err) return null } } // OR: Use .then() only with error callback FS('getSessionAsync').then( (url) => console.log('Session:', url), (err) => console.error('Error:', err), // Second arg to .then() works ) ``` ### Example 4: Unnecessary Async Usage ```javascript // BAD: Using async when you don't need the result async function handleButtonClick() { // Don't need to await fire-and-forget events await FS('trackEventAsync', { name: 'Button Clicked', properties: {buttonId: 'submit'}, }) // User waits unnecessarily proceedWithAction() } ``` **Why this is bad:** - ❌ Adds unnecessary latency to user action - ❌ User waits for analytics to complete - ❌ No value from awaiting (result not used) **CORRECTED VERSION:** ```javascript // GOOD: Fire-and-forget for events function handleButtonClick() { // Don't await - fire and forget FS('trackEvent', { name: 'Button Clicked', properties: {buttonId: 'submit'}, }) // Proceed immediately proceedWithAction() } ``` ### Example 5: Race Condition with Async ```javascript // BAD: Race condition between identify and track async function onLogin(user) { // These run in parallel - trackEvent may fire before identity! FS('setIdentityAsync', {uid: user.id}) FS('trackEventAsync', {name: 'Login'}) } ``` **Why this is bad:** - ❌ Event may fire before identity is set - ❌ Event could be attributed to anonymous user - ❌ Data integrity issue **CORRECTED VERSION:** ```javascript // GOOD: Sequential with proper awaiting async function onLogin(user) { // First identify await FS('setIdentityAsync', { uid: user.id, properties: {displayName: user.name}, }) // Then track event (now properly attributed) await FS('trackEventAsync', { name: 'Login', properties: {method: 'password'}, }) } // OR: For non-critical, use sync versions (they queue properly) function onLogin(user) { FS('setIdentity', {uid: user.id}) // Queued first FS('trackEvent', {name: 'Login'}) // Queued second // Fullstory processes queue in order } ``` --- ## COMMON IMPLEMENTATION PATTERNS ### Pattern 1: Safe Async Wrapper ```javascript // Wrapper for safe FS async calls with timeout async function safeFS(method, params, options = {}) { const {timeout = 5000, fallback = null} = options // Check if FS exists if (typeof FS === 'undefined') { console.warn(`FS not available for ${method}`) return fallback } try { const result = await Promise.race([ FS(method, params), new Promise((_, reject) => setTimeout(() => reject(new Error(`FS ${method} timeout`)), timeout), ), ]) return result } catch (error) { console.warn(`FS ${method} failed:`, error.message) return fallback } } // Usage const sessionUrl = await safeFS('getSessionAsync', undefined, { timeout: 3000, fallback: null, }) await safeFS('trackEventAsync', { name: 'Page View', properties: {page: '/home'}, }) ``` ### Pattern 2: Initialization Status Manager ```javascript // Track Fullstory initialization status class CaptureStatusManager { constructor() { this.status = 'pending' this.sessionUrl = null this.error = null this.callbacks = [] } async initialize() { try { this.sessionUrl = await FS('getSessionAsync') this.status = 'ready' this.callbacks.forEach((cb) => cb(this.sessionUrl)) } catch (error) { this.status = 'failed' this.error = error } return this.status === 'ready' } onReady(callback) { if (this.status === 'ready') { callback(this.sessionUrl) } else if (this.status === 'pending') { this.callbacks.push(callback) } // If failed, don't call } isReady() { return this.status === 'ready' } getSessionUrl() { return this.sessionUrl } } // Global instance const fsStatus = new CaptureStatusManager() // Initialize once fsStatus.initialize() // Use anywhere fsStatus.onReady((url) => { console.log('FS ready with session:', url) }) ``` ### Pattern 3: Analytics Queue with Fallback ```javascript // Queue analytics calls with sync fallback class AnalyticsQueue { constructor() { this.useAsync = true this.pending = [] } async track(eventName, properties) { if (this.useAsync) { try { await FS('trackEventAsync', { name: eventName, properties, }) } catch (error) { // Fall back to sync console.warn('Async tracking failed, using sync') this.useAsync = false FS('trackEvent', {name: eventName, properties}) } } else { FS('trackEvent', {name: eventName, properties}) } } async identify(uid, properties) { if (this.useAsync) { try { await FS('setIdentityAsync', {uid, properties}) } catch (error) { this.useAsync = false FS('setIdentity', {uid, properties}) } } else { FS('setIdentity', {uid, properties}) } } } ``` --- ## WHEN TO USE ASYNC VS SYNC ### Use Async When: | Scenario | Why | | --------------------- | --------------------------------- | | Need session URL | Must wait for URL to be available | | Error handling needed | Need to know if call failed | | Sequential operations | Must ensure order of operations | | Conditional logic | Need result to decide next action | | Initialization checks | Need to know when FS is ready | ### Use Sync (Fire-and-Forget) When: | Scenario | Why | | -------------------------- | --------------------------- | | Simple event tracking | Don't need confirmation | | Non-critical operations | Failure is acceptable | | Performance critical paths | Don't want to add latency | | Rapid-fire events | Queueing handles order | | User-facing actions | Don't delay user experience | --- ## TROUBLESHOOTING ### Promise Never Resolves **Symptom**: `await FS('methodAsync')` hangs forever **Common Causes**: 1. ❌ Fullstory script blocked by ad blocker 2. ❌ Script failed to load 3. ❌ Network issues preventing initialization **Solutions**: - ✅ Always use timeout wrapper - ✅ Don't block critical paths - ✅ Implement fallback behavior ### Rejection Errors **Symptom**: Promise rejects with error **Common Causes**: 1. ❌ Missing `_fs_org` configuration 2. ❌ Unsupported browser 3. ❌ Organization over quota 4. ❌ Configuration error **Solutions**: - ✅ Check Fullstory setup - ✅ Verify configuration - ✅ Handle rejections gracefully ### .catch() Not Working **Symptom**: Errors not caught by `.catch()` **Common Causes**: 1. ❌ Browser doesn't have native Promise 2. ❌ Fullstory's Promise-like doesn't implement catch **Solutions**: - ✅ Use async/await with try/catch - ✅ Use `.then()` with error callback --- ## KEY TAKEAWAYS FOR AGENT When helping developers with Async Methods: 1. **Always emphasize**: - Use timeouts to prevent hanging - Handle rejections gracefully - Don't block critical paths on FS - Use try/catch, not .catch() 2. **Common mistakes to watch for**: - Blocking app startup on FS - Missing error handling - Using async when sync would work - Race conditions between calls - .catch() without polyfill check 3. **Questions to ask developers**: - Do you need the result? - Is this on a critical path? - What should happen if FS fails? - Is proper sequencing required? 4. **Best practices to recommend**: - Wrap in timeout for safety - Use sync for fire-and-forget - Graceful degradation always - Don't let analytics break core features --- ## REFERENCE LINKS - **Asynchronous Methods**: https://developer.fullstory.com/browser/asynchronous-methods/ - **Get Session Details**: https://developer.fullstory.com/browser/get-session-details/ - **Callbacks and Delegates**: https://developer.fullstory.com/browser/fullcapture/callbacks-and-delegates/ --- _This skill document was created to help Agent understand and guide developers in implementing Fullstory's Asynchronous Methods correctly for web applications._