| 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:
- ❌ Fullstory blocked by ad blocker
- ❌ Page excluded from capture
- ❌ 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:
- ❌ Forgot to re-identify
- ❌ 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:
- ❌ Too many restart calls
- ❌ Shutdown/restart in rapid succession
- ❌ Missing debounce
Solutions:
- ✅ Minimize shutdown/restart cycles
- ✅ Add debouncing
- ✅ Use state tracking
KEY TAKEAWAYS FOR AGENT
When helping developers with Capture Control:
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
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
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?
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
- Capture Data: https://developer.fullstory.com/browser/fullcapture/capture-data/
- Asynchronous Methods: https://developer.fullstory.com/browser/asynchronous-methods/
This skill document was created to help Agent understand and guide developers in implementing Fullstory's Capture Control APIs correctly for web applications.