fullstory-capture-control

Comprehensive guide for implementing Fullstory's Capture Control APIs (shutdown/restart) for web applications. Teaches proper session management, capture pausing, and resource optimization. Includes detailed good/bad examples for performance-sensitive sections, privacy zones, and SPA cleanup to help developers control when Fullstory captures sessions.

$ 安裝

git clone https://github.com/fullstorydev/fs-skills /tmp/fs-skills && cp -r /tmp/fs-skills/core/fullstory-capture-control ~/.claude/skills/fs-skills

// tip: Run this command in your terminal to install the skill


name: fullstory-capture-control version: v2 description: Comprehensive guide for implementing Fullstory's Capture Control APIs (shutdown/restart) for web applications. Teaches proper session management, capture pausing, and resource optimization. Includes detailed good/bad examples for performance-sensitive sections, privacy zones, and SPA cleanup to help developers control when Fullstory captures sessions. related_skills:

  • fullstory-user-consent
  • fullstory-async-methods
  • fullstory-identify-users
  • fullstory-observe-callbacks

Fullstory Capture Control API (Shutdown/Restart)

Overview

Fullstory's Capture Control APIs allow developers to programmatically stop and restart session capture. This provides fine-grained control over when Fullstory records sessions, which is useful for:

  • Performance Optimization: Pause capture during resource-intensive operations
  • Privacy Zones: Stop capture in sensitive areas (PII entry, etc.)
  • Resource Management: Reduce browser overhead when not needed
  • Testing: Control capture during development/testing
  • Conditional Recording: Only capture certain user journeys

Core Concepts

Shutdown vs Restart

MethodEffectUse Case
FS('shutdown')Stops capture, clears sessionEnd recording permanently or temporarily
FS('restart')Resumes capture, new sessionResume after shutdown

Session Behavior

Active Session  →  FS('shutdown')  →  Capture Stopped (session ends)
                                              ↓
                                    FS('restart')
                                              ↓
                                    New Session Begins

Key Points

BehaviorDescription
New session on restartRestart creates a new session, not continues old one
Identity preservedIf identified before shutdown, re-identify after restart
Properties clearedPage/element properties reset on restart
Async availableBoth have async versions (shutdownAsync, restartAsync)

API Reference

Shutdown

// Stop capture
FS('shutdown');

// Async version
await FS('shutdownAsync');

Restart

// Resume capture (starts new session)
FS('restart');

// Async version
await FS('restartAsync');

Parameters

Both methods take no parameters.

Return Values

MethodSync ReturnAsync Return
FS('shutdown')undefinedPromise (resolves when stopped)
FS('restart')undefinedPromise (resolves when started)

✅ GOOD IMPLEMENTATION EXAMPLES

Example 1: Pause During Heavy Operations

// GOOD: Pause capture during performance-intensive operations
async function processLargeDataset(data) {
  // Pause Fullstory to free up resources
  await FS('shutdownAsync');
  
  console.log('Fullstory paused for data processing');
  
  try {
    // Perform heavy operation
    const result = await heavyProcessing(data);
    
    return result;
  } finally {
    // Always restart, even if processing fails
    await FS('restartAsync');
    
    // Re-identify user (identity lost on restart)
    const user = getCurrentUser();
    if (user) {
      FS('setIdentity', {
        uid: user.id,
        properties: {
          displayName: user.name
        }
      });
    }
    
    console.log('Fullstory resumed');
  }
}

// Usage
const results = await processLargeDataset(largeDataset);

Why this is good:

  • ✅ Frees up resources during heavy processing
  • ✅ Uses try/finally to ensure restart
  • ✅ Re-identifies user after restart
  • ✅ Logs state changes for debugging

Example 2: Privacy Zone Implementation

// GOOD: Stop capture in sensitive areas
class PrivacyZoneManager {
  constructor() {
    this.isInPrivacyZone = false;
    this.userBeforeShutdown = null;
  }
  
  async enterPrivacyZone(zoneName) {
    if (this.isInPrivacyZone) return;
    
    // Store current user for re-identification later
    this.userBeforeShutdown = getCurrentUser();
    
    // Log entry before shutdown
    FS('log', {
      level: 'info',
      msg: `Entering privacy zone: ${zoneName}`
    });
    
    // Track the transition
    FS('trackEvent', {
      name: 'Privacy Zone Entered',
      properties: { zone: zoneName }
    });
    
    // Shutdown capture
    await FS('shutdownAsync');
    this.isInPrivacyZone = true;
    
    console.log(`Entered privacy zone: ${zoneName}`);
  }
  
  async exitPrivacyZone(zoneName) {
    if (!this.isInPrivacyZone) return;
    
    // Restart capture
    await FS('restartAsync');
    this.isInPrivacyZone = false;
    
    // Re-identify user
    if (this.userBeforeShutdown) {
      FS('setIdentity', {
        uid: this.userBeforeShutdown.id,
        properties: {
          displayName: this.userBeforeShutdown.name,
          email: this.userBeforeShutdown.email
        }
      });
    }
    
    // Track the transition
    FS('trackEvent', {
      name: 'Privacy Zone Exited',
      properties: { zone: zoneName }
    });
    
    // Log exit
    FS('log', {
      level: 'info',
      msg: `Exited privacy zone: ${zoneName}`
    });
    
    this.userBeforeShutdown = null;
    console.log(`Exited privacy zone: ${zoneName}`);
  }
}

// Usage
const privacyManager = new PrivacyZoneManager();

// When navigating to sensitive page
await privacyManager.enterPrivacyZone('account-settings');

// When leaving sensitive page
await privacyManager.exitPrivacyZone('account-settings');

Why this is good:

  • ✅ Clean API for privacy zones
  • ✅ Preserves user identity for re-identification
  • ✅ Logs and tracks zone transitions
  • ✅ State management prevents double calls

Example 3: SPA Route-Based Control

// GOOD: Control capture based on route in SPA
const routeConfig = {
  '/dashboard': { capture: true },
  '/settings': { capture: true },
  '/settings/security': { capture: false },  // Privacy zone
  '/admin': { capture: false },              // Internal only
  '/checkout': { capture: true },
  '/checkout/payment': { capture: false },   // PCI compliance
};

class RouteBasedCapture {
  constructor() {
    this.isCapturing = true;  // Assume capturing on start
    this.currentUser = null;
    
    this.setupRouteListener();
  }
  
  setupRouteListener() {
    // For React Router, Vue Router, etc.
    window.addEventListener('popstate', () => this.handleRouteChange());
    
    // Intercept pushState
    const originalPushState = history.pushState;
    history.pushState = (...args) => {
      originalPushState.apply(history, args);
      this.handleRouteChange();
    };
  }
  
  async handleRouteChange() {
    const path = window.location.pathname;
    const config = this.getRouteConfig(path);
    
    if (config.capture && !this.isCapturing) {
      await this.startCapture();
    } else if (!config.capture && this.isCapturing) {
      await this.stopCapture();
    }
    
    // Set page properties if capturing
    if (this.isCapturing && config.pageName) {
      FS('setProperties', {
        type: 'page',
        properties: { pageName: config.pageName }
      });
    }
  }
  
  getRouteConfig(path) {
    // Find matching config (exact match or parent)
    for (const [route, config] of Object.entries(routeConfig)) {
      if (path === route || path.startsWith(route + '/')) {
        return config;
      }
    }
    return { capture: true };  // Default: capture
  }
  
  async startCapture() {
    await FS('restartAsync');
    this.isCapturing = true;
    
    // Re-identify
    this.currentUser = this.currentUser || getCurrentUser();
    if (this.currentUser) {
      FS('setIdentity', {
        uid: this.currentUser.id,
        properties: {
          displayName: this.currentUser.name
        }
      });
    }
    
    console.log('Fullstory capture started');
  }
  
  async stopCapture() {
    // Save user before shutdown
    this.currentUser = getCurrentUser();
    
    await FS('shutdownAsync');
    this.isCapturing = false;
    
    console.log('Fullstory capture stopped');
  }
  
  setUser(user) {
    this.currentUser = user;
  }
}

// Initialize
const captureController = new RouteBasedCapture();

Why this is good:

  • ✅ Configurable per-route capture
  • ✅ Handles SPA navigation
  • ✅ Re-identifies on restart
  • ✅ Preserves user across shutdown

Example 4: Development/Testing Controls

// GOOD: Control capture for testing and development
const DevCaptureControls = {
  isOverridden: false,
  
  // Disable capture for current session (dev/testing)
  disableForSession() {
    sessionStorage.setItem('fs_disabled', 'true');
    FS('shutdown');
    this.isOverridden = true;
    console.log('Fullstory disabled for this session');
  },
  
  // Re-enable capture
  enableForSession() {
    sessionStorage.removeItem('fs_disabled');
    if (this.isOverridden) {
      FS('restart');
      this.isOverridden = false;
      console.log('Fullstory re-enabled');
    }
  },
  
  // Check if should capture on page load
  init() {
    if (sessionStorage.getItem('fs_disabled') === 'true') {
      FS('shutdown');
      this.isOverridden = true;
      console.log('Fullstory disabled (session override)');
    }
    
    // Also check URL param for easy testing
    if (new URLSearchParams(window.location.search).has('no_fullstory')) {
      this.disableForSession();
    }
  },
  
  // Add keyboard shortcut (Ctrl+Shift+F)
  setupKeyboardShortcut() {
    document.addEventListener('keydown', (e) => {
      if (e.ctrlKey && e.shiftKey && e.key === 'F') {
        if (this.isOverridden) {
          this.enableForSession();
        } else {
          this.disableForSession();
        }
      }
    });
  }
};

// Initialize on page load
DevCaptureControls.init();
DevCaptureControls.setupKeyboardShortcut();

// Also expose for console access
window.DevCaptureControls = DevCaptureControls;

Why this is good:

  • ✅ Easy toggle for developers
  • ✅ Session-persistent disable
  • ✅ URL parameter support
  • ✅ Keyboard shortcut for quick toggle
  • ✅ Console access for debugging

Example 5: Conditional Capture Based on User State

// GOOD: Only capture for specific user segments
class ConditionalCapture {
  constructor(captureRules) {
    this.rules = captureRules;
    this.isCapturing = false;
  }
  
  async evaluateAndUpdate(user) {
    const shouldCapture = this.shouldCaptureUser(user);
    
    if (shouldCapture && !this.isCapturing) {
      await this.startCapture(user);
    } else if (!shouldCapture && this.isCapturing) {
      await this.stopCapture();
    } else if (shouldCapture && this.isCapturing) {
      // Just update identity
      this.identifyUser(user);
    }
  }
  
  shouldCaptureUser(user) {
    // Evaluate rules
    for (const rule of this.rules) {
      if (!rule.check(user)) {
        console.log(`Capture blocked by rule: ${rule.name}`);
        return false;
      }
    }
    return true;
  }
  
  async startCapture(user) {
    await FS('restartAsync');
    this.isCapturing = true;
    this.identifyUser(user);
    
    FS('log', {
      level: 'info',
      msg: `Capture started for user: ${user.id}`
    });
  }
  
  async stopCapture() {
    await FS('shutdownAsync');
    this.isCapturing = false;
    
    console.log('Capture stopped based on rules');
  }
  
  identifyUser(user) {
    FS('setIdentity', {
      uid: user.id,
      properties: {
        displayName: user.name,
        plan: user.plan,
        role: user.role
      }
    });
  }
}

// Example rules
const captureRules = [
  {
    name: 'not_internal',
    check: (user) => !user.email.endsWith('@ourcompany.com')
  },
  {
    name: 'not_bot',
    check: (user) => !user.isBot
  },
  {
    name: 'has_consent',
    check: (user) => user.trackingConsent === true
  },
  {
    name: 'paying_customer',
    check: (user) => user.plan !== 'free'  // Only capture paid users
  }
];

const conditionalCapture = new ConditionalCapture(captureRules);

// On user load/change
authService.on('userChanged', (user) => {
  conditionalCapture.evaluateAndUpdate(user);
});

Why this is good:

  • ✅ Configurable capture rules
  • ✅ Filters out internal/bot traffic
  • ✅ Respects consent
  • ✅ Can segment by plan/role
  • ✅ Logs why capture is blocked

Example 6: Cleanup on Page Unload

// GOOD: Clean shutdown on page unload
class CaptureLifecycleManager {
  constructor() {
    this.setupUnloadHandler();
    this.setupVisibilityHandler();
  }
  
  setupUnloadHandler() {
    window.addEventListener('beforeunload', () => {
      // Use sync version - async may not complete
      FS('shutdown');
    });
  }
  
  setupVisibilityHandler() {
    // Optional: pause when tab is hidden (saves resources)
    document.addEventListener('visibilitychange', () => {
      if (document.hidden) {
        // User switched tabs - could pause
        // FS('shutdown');  // Uncomment if you want this behavior
      } else {
        // User returned - could resume
        // FS('restart');  // Uncomment if pausing on hidden
      }
    });
  }
  
  // For SPAs: call when app unmounts
  cleanup() {
    FS('shutdown');
  }
}

// Initialize
const fsLifecycle = new CaptureLifecycleManager();

// For React apps
// useEffect(() => {
//   return () => fsLifecycle.cleanup();
// }, []);

Why this is good:

  • ✅ Clean session end on page close
  • ✅ Optional tab visibility handling
  • ✅ SPA cleanup method
  • ✅ Uses sync version for beforeunload

❌ BAD IMPLEMENTATION EXAMPLES

Example 1: Not Re-identifying After Restart

// BAD: Forgot to re-identify after restart
async function pauseAndResume() {
  await FS('shutdownAsync');
  
  // ... do work ...
  
  await FS('restartAsync');
  // BAD: User is now anonymous! Identity was lost on shutdown
}

Why this is bad:

  • ❌ Identity lost on shutdown
  • ❌ New session is anonymous
  • ❌ Can't link sessions together

CORRECTED VERSION:

// GOOD: Re-identify after restart
async function pauseAndResume() {
  const user = getCurrentUser();  // Save before shutdown
  
  await FS('shutdownAsync');
  
  // ... do work ...
  
  await FS('restartAsync');
  
  // Re-identify
  if (user) {
    FS('setIdentity', {
      uid: user.id,
      properties: { displayName: user.name }
    });
  }
}

Example 2: Using Shutdown for Consent (Wrong API)

// BAD: Using shutdown instead of consent API
function handleConsentDeclined() {
  FS('shutdown');  // BAD: Wrong approach for consent
}

function handleConsentGranted() {
  FS('restart');  // BAD: Should use consent API
}

Why this is bad:

  • ❌ shutdown/restart not designed for consent
  • ❌ Doesn't properly signal consent state
  • ❌ Consent API exists for this purpose

CORRECTED VERSION:

// GOOD: Use consent API for consent
function handleConsentDeclined() {
  FS('setIdentity', { consent: false });
}

function handleConsentGranted() {
  FS('setIdentity', { consent: true });
}

Example 3: Shutdown Without Restart Logic

// BAD: Shutdown with no way to restart
function handleSensitiveArea() {
  FS('shutdown');
  // No mechanism to restart when leaving sensitive area!
}

Why this is bad:

  • ❌ Capture permanently stopped
  • ❌ No way to resume
  • ❌ Loses rest of session

CORRECTED VERSION:

// GOOD: Paired shutdown/restart
let isShutdown = false;

function enterSensitiveArea() {
  if (!isShutdown) {
    FS('shutdown');
    isShutdown = true;
  }
}

function leaveSensitiveArea() {
  if (isShutdown) {
    FS('restart');
    isShutdown = false;
    // Re-identify user
    reidentifyUser();
  }
}

Example 4: Async Version in beforeunload

// BAD: Using async in beforeunload (won't complete)
window.addEventListener('beforeunload', async () => {
  await FS('shutdownAsync');  // BAD: Won't complete before page unloads
});

Why this is bad:

  • ❌ Async code may not complete before unload
  • ❌ Session may not end cleanly
  • ❌ beforeunload doesn't wait for promises

CORRECTED VERSION:

// GOOD: Use sync version in beforeunload
window.addEventListener('beforeunload', () => {
  FS('shutdown');  // Sync version - fires immediately
});

Example 5: Rapid Shutdown/Restart Cycles

// BAD: Toggling too rapidly
document.addEventListener('scroll', () => {
  if (isInSensitiveArea()) {
    FS('shutdown');  // Called on every scroll event!
  } else {
    FS('restart');   // Creates new session every scroll!
  }
});

Why this is bad:

  • ❌ Excessive API calls
  • ❌ Creates many fragmented sessions
  • ❌ Performance impact
  • ❌ Data loss from constant restarts

CORRECTED VERSION:

// GOOD: Debounced state changes
let isCapturing = true;

const updateCaptureState = debounce(() => {
  const shouldCapture = !isInSensitiveArea();
  
  if (shouldCapture && !isCapturing) {
    FS('restart');
    reidentifyUser();
    isCapturing = true;
  } else if (!shouldCapture && isCapturing) {
    FS('shutdown');
    isCapturing = false;
  }
}, 500);

document.addEventListener('scroll', updateCaptureState);

COMMON IMPLEMENTATION PATTERNS

Pattern 1: Capture Controller Singleton

// Singleton for capture state management
const CaptureController = {
  _isCapturing: true,
  _user: null,
  
  isCapturing() {
    return this._isCapturing;
  },
  
  setUser(user) {
    this._user = user;
  },
  
  async pause(reason = 'unspecified') {
    if (!this._isCapturing) return;
    
    // Log before shutdown
    FS('log', {
      level: 'info',
      msg: `Capture paused: ${reason}`
    });
    
    await FS('shutdownAsync');
    this._isCapturing = false;
    
    console.log(`FS capture paused: ${reason}`);
  },
  
  async resume(reason = 'unspecified') {
    if (this._isCapturing) return;
    
    await FS('restartAsync');
    this._isCapturing = true;
    
    // Re-identify
    if (this._user) {
      FS('setIdentity', {
        uid: this._user.id,
        properties: {
          displayName: this._user.name
        }
      });
    }
    
    // Log after restart
    FS('log', {
      level: 'info',
      msg: `Capture resumed: ${reason}`
    });
    
    console.log(`FS capture resumed: ${reason}`);
  }
};

// Usage
await CaptureController.pause('entering-payment-form');
await CaptureController.resume('leaving-payment-form');

Pattern 2: React Hook for Capture Control

// React hook for capture control
import { useEffect, useRef, useCallback } from 'react';

function useCaptureControl() {
  const isCapturingRef = useRef(true);
  const userRef = useRef(null);
  
  const setUser = useCallback((user) => {
    userRef.current = user;
  }, []);
  
  const pause = useCallback(async () => {
    if (!isCapturingRef.current) return;
    
    await FS('shutdownAsync');
    isCapturingRef.current = false;
  }, []);
  
  const resume = useCallback(async () => {
    if (isCapturingRef.current) return;
    
    await FS('restartAsync');
    isCapturingRef.current = true;
    
    if (userRef.current) {
      FS('setIdentity', {
        uid: userRef.current.id,
        properties: { displayName: userRef.current.name }
      });
    }
  }, []);
  
  // Cleanup on unmount
  useEffect(() => {
    return () => {
      FS('shutdown');
    };
  }, []);
  
  return { pause, resume, setUser };
}

// Privacy zone component
function PrivacyZone({ children }) {
  const { pause, resume } = useCaptureControl();
  
  useEffect(() => {
    pause();
    return () => resume();
  }, [pause, resume]);
  
  return children;
}

// Usage
function PaymentForm() {
  return (
    <PrivacyZone>
      <form>
        {/* Capture is paused within this component */}
        <CreditCardInput />
      </form>
    </PrivacyZone>
  );
}

TROUBLESHOOTING

Sessions Not Resuming

Symptom: After restart, no new session created

Common Causes:

  1. ❌ Fullstory blocked by ad blocker
  2. ❌ Page excluded from capture
  3. ❌ Rate limits hit

Solutions:

  • ✅ Check browser console for errors
  • ✅ Verify page isn't excluded
  • ✅ Add delay between shutdown/restart

Identity Lost After Restart

Symptom: User is anonymous after restart

Common Causes:

  1. ❌ Forgot to re-identify
  2. ❌ User data not saved before shutdown

Solutions:

  • ✅ Always re-identify after restart
  • ✅ Save user data before shutdown

Fragmented Sessions

Symptom: Many short sessions for same user

Common Causes:

  1. ❌ Too many restart calls
  2. ❌ Shutdown/restart in rapid succession
  3. ❌ Missing debounce

Solutions:

  • ✅ Minimize shutdown/restart cycles
  • ✅ Add debouncing
  • ✅ Use state tracking

KEY TAKEAWAYS FOR AGENT

When helping developers with Capture Control:

  1. Always emphasize:

    • Re-identify after restart (identity is lost)
    • Use sync version in beforeunload
    • Debounce rapid state changes
    • Use consent API for consent, not shutdown
  2. Common mistakes to watch for:

    • Forgetting to re-identify
    • Using shutdown for consent
    • Async in beforeunload
    • Rapid shutdown/restart cycles
    • No restart logic after shutdown
  3. Questions to ask developers:

    • Why do you need to pause capture?
    • Is this for privacy/consent or performance?
    • How will users resume capture?
    • Do you need to preserve user identity?
  4. Best practices to recommend:

    • Use consent API for consent management
    • Create paired enter/exit for privacy zones
    • Always save user before shutdown
    • Debounce state transitions

REFERENCE LINKS


This skill document was created to help Agent understand and guide developers in implementing Fullstory's Capture Control APIs correctly for web applications.