Claude Code Plugins

Community-maintained marketplace

Feedback

fullstory-capture-control

@fullstorydev/fs-skills
2
0

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.

Install Skill

1Download skill
2Enable skills in Claude

Open claude.ai/settings/capabilities and find the "Skills" section

3Upload to Claude

Click "Upload skill" and select the downloaded ZIP file

Note: Please verify skill by going through its instructions before using it.

SKILL.md

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

Method Effect Use Case
FS('shutdown') Stops capture, clears session End recording permanently or temporarily
FS('restart') Resumes capture, new session Resume after shutdown

Session Behavior

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

Key Points

Behavior Description
New session on restart Restart creates a new session, not continues old one
Identity preserved If identified before shutdown, re-identify after restart
Properties cleared Page/element properties reset on restart
Async available Both 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

Method Sync Return Async Return
FS('shutdown') undefined Promise (resolves when stopped)
FS('restart') undefined Promise (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.