xiaohongshu
xiaohongshu
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 h...
Full skill instructions
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:
| Method | Effect | Use Case |
|---|---|---|
FS('shutdown') | Stops capture, clears session | End recording permanently or temporarily |
FS('restart') | Resumes capture, new session | Resume after shutdown |
Active Session → FS('shutdown') → Capture Stopped (session ends)
↓
FS('restart')
↓
New Session Begins
| 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) |
// Stop capture
FS('shutdown')
// Async version
await FS('shutdownAsync')
// Resume capture (starts new session)
FS('restart')
// Async version
await FS('restartAsync')
Both methods take no parameters.
| Method | Sync Return | Async Return |
|---|---|---|
FS('shutdown') | undefined | Promise (resolves when stopped) |
FS('restart') | undefined | Promise (resolves when started) |
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
// 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:
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},
})
}
}
// 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:
CORRECTED VERSION:
// GOOD: Use consent API for consent
function handleConsentDeclined() {
FS('setIdentity', {consent: false})
}
function handleConsentGranted() {
FS('setIdentity', {consent: true})
}
// BAD: Shutdown with no way to restart
function handleSensitiveArea() {
FS('shutdown')
// No mechanism to restart when leaving sensitive area!
}
Why this is bad:
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()
}
}
// 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:
CORRECTED VERSION:
// GOOD: Use sync version in beforeunload
window.addEventListener('beforeunload', () => {
FS('shutdown') // Sync version - fires immediately
})
// 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:
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)
// 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')
// 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>
)
}
Symptom: After restart, no new session created
Common Causes:
Solutions:
Symptom: User is anonymous after restart
Common Causes:
Solutions:
Symptom: Many short sessions for same user
Common Causes:
Solutions:
When helping developers with Capture Control:
Always emphasize:
Common mistakes to watch for:
Questions to ask developers:
Best practices to recommend:
This skill document was created to help Agent understand and guide developers in implementing Fullstory's Capture Control APIs correctly for web applications.
xiaohongshu
technical spec
product ux expert
database patterns
Conduct multi-agent task orchestration and workflow coordination.
Initialize project with Conductor artifacts (product definition,
Expert in web animations, transitions, and motion design using Framer Motion and CSS
Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...
PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.
Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...
Display project status, active tracks, and next actions
Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.
Generate, edit, and beat-sync AI video with leading models in one workspace.
The world's fastest calendar for remote work
Transform Your Design with AI Designer by ImgCreator.ai
Revolutionizing Video Production with AI-Powered Creativity
Extend an image past the frame and let AI fill the new aspect ratio.
Discover your celebrity doppelgänger with StarByFace!
ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.
Opus.ai: Revolutionize Your Web Experience