Agent skill
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.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/fullstory-capture-control
SKILL.md
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.
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?