---
name: fullstory-user-consent
version: v2
description: Comprehensive guide for implementing Fullstory's User Consent API for web applications. Teaches proper consent flow implementation, selective capture modes, GDPR/CCPA compliance patterns, and cookie consent integration. Includes detailed good/bad examples for consent banners, preference centers, and privacy-conscious recording to help developers implement privacy-compliant session recording.
related_skills:
- fullstory-identify-users
- fullstory-anonymize-users
- fullstory-capture-control
---
# Fullstory User Consent API
## Overview
Fullstory's User Consent API allows developers to implement privacy-compliant session recording by conditioning capture on explicit user consent. This is essential for:
- **GDPR Compliance**: Obtain consent before recording EU users
- **CCPA Compliance**: Allow California users to opt-out
- **Cookie Consent Integration**: Tie Fullstory to your consent management platform (CMP)
- **Selective Capture**: Record only users who have consented
- **Privacy Controls**: Give users control over their data
## Core Concepts
### ⚠️ Critical: Two Different Consent Approaches
Fullstory has **two separate mechanisms** for consent. Using the wrong one is a common mistake:
| Approach | API | What It Controls | Use Case |
| -------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------- | ------------------------------------------------- |
| **Element-Level Consent** | `FS('setIdentity', { consent: true/false })` | Only elements marked "Capture with consent" in Fullstory Privacy Settings | Selective capture of specific sensitive elements |
| **Holistic Consent (CMP)** | `_fs_capture_on_startup = false` + `FS('start')` | **ALL** Fullstory capture | GDPR cookie banners, consent management platforms |
### Element-Level Consent (setIdentity with consent)
Per the [official documentation](https://developer.fullstory.com/browser/fullcapture/user-consent/):
> "HTML Elements that have been configured to 'Capture data with user consent' in Fullstory's privacy settings are only captured after an invocation of `FS('setIdentity', { consent: true })`."
**This does NOT control all Fullstory capture** - only specific elements you've pre-configured in the Fullstory UI to require consent.
> **Why `setIdentity`?** This is Fullstory's API design choice - the `consent` parameter happens to be passed through the `setIdentity` method. You do **NOT** need to provide a `uid` or actually identify the user. You can simply call `FS('setIdentity', { consent: true })` for anonymous users - it just toggles the element-level consent flag.
```javascript
// This works - no uid required, user stays anonymous
FS('setIdentity', {consent: true})
// This also works - consent + identification combined
FS('setIdentity', {uid: 'user_123', consent: true})
```
| Consent State | Effect |
| ---------------- | ---------------------------------------------- |
| `consent: true` | Capture elements marked "Capture with consent" |
| `consent: false` | Don't capture those specific elements |
| Not set | Those specific elements not captured |
### Holistic Consent (Consent Management Platforms / GDPR)
For **cookie consent banners** and **consent management platforms** where you need to delay **ALL** Fullstory capture until consent is given, use the [capture delay approach](https://developer.fullstory.com/browser/fullcapture/capture-data/#manually-delay-data-capture):
```javascript
// BEFORE the Fullstory snippet loads
window['_fs_capture_on_startup'] = false
// ... Fullstory snippet loads but does NOT capture ...
// AFTER user gives consent via your CMP/cookie banner
FS('start') // NOW Fullstory begins capturing
```
### Choosing the Right Approach
```
Do you need to delay ALL Fullstory capture until consent?
│
├─ YES (GDPR cookie banner, CMP integration)
│ └─ Use: _fs_capture_on_startup = false + FS('start')
│
└─ NO (Just want some elements to require extra consent)
└─ Use: FS('setIdentity', { consent: true/false })
```
### Combining Both Approaches
For maximum privacy compliance, you can use both:
```javascript
// Step 1: Delay all capture until cookie consent
window['_fs_capture_on_startup'] = false
// Step 2: User accepts cookies via your CMP
onCookieConsentAccepted(() => {
FS('start') // Begin general capture
// Step 3: Later, user opts into extra data sharing
// (for elements marked "Capture with consent" in FS settings)
if (userAcceptedEnhancedTracking) {
FS('setIdentity', {consent: true})
}
})
```
---
## API Reference
### Approach 1: Holistic Consent (Recommended for GDPR/CMP)
```javascript
// BEFORE Fullstory snippet - prevent capture on startup
window['_fs_capture_on_startup'] = false
// AFTER user consents - start all capture
FS('start')
// If user later revokes consent - stop all capture
FS('shutdown')
// If user consents again
FS('restart')
```
| Method | Effect |
| ------------------------------------------ | --------------------------------------- |
| `window['_fs_capture_on_startup'] = false` | Prevent ALL capture until `FS('start')` |
| `FS('start')` | Begin capture (after delay) |
| `FS('shutdown')` | Stop all capture |
| `FS('restart')` | Resume capture after shutdown |
### Approach 2: Element-Level Consent
```javascript
// Enable capture of elements marked "Capture with consent"
FS('setIdentity', { consent: true });
// Disable capture of those specific elements
FS('setIdentity', { consent: false });
// Combine with user identification
FS('setIdentity', {
uid: string,
consent: true,
properties?: object
});
```
| Parameter | Type | Required | Description |
| ------------ | ------- | -------- | -------------------------------------------------------------- |
| `consent` | boolean | Yes | `true` enables, `false` disables element-level consent capture |
| `uid` | string | No | User identifier (if also identifying) |
| `properties` | object | No | User properties (if identifying) |
### Rate Limits
- Standard API rate limits apply (30 calls/min sustained)
---
## ✅ GOOD IMPLEMENTATION EXAMPLES
### Example 1: GDPR Cookie Consent Banner (Holistic Approach)
```javascript
// GOOD: Holistic consent for GDPR/cookie banners
// This approach delays ALL Fullstory capture until consent is given
// STEP 1: In your HTML, BEFORE the Fullstory snippet
//
//
// STEP 2: Your consent manager class
class GDPRConsentManager {
constructor() {
this.consentKey = 'fs_consent'
this.initFromStorage()
}
initFromStorage() {
const storedConsent = localStorage.getItem(this.consentKey)
if (storedConsent === 'granted') {
this.grantConsent()
}
// If denied or not set, Fullstory stays disabled (capture never started)
}
grantConsent() {
localStorage.setItem(this.consentKey, 'granted')
// Start ALL Fullstory capture
FS('start')
// If user is logged in, also identify them
const currentUser = getCurrentUser()
if (currentUser) {
FS('setIdentity', {
uid: currentUser.id,
properties: {
displayName: currentUser.name,
email: currentUser.email,
},
})
}
console.log('Fullstory capture started')
}
revokeConsent() {
localStorage.setItem(this.consentKey, 'denied')
// Stop ALL Fullstory capture
FS('shutdown')
console.log('Fullstory capture stopped')
}
hasConsent() {
return localStorage.getItem(this.consentKey) === 'granted'
}
resetConsent() {
localStorage.removeItem(this.consentKey)
FS('shutdown') // Stop capture when consent is reset
// Show banner again
showConsentBanner()
}
}
// Initialize
const consent = new GDPRConsentManager()
// Wire up to consent banner
document.getElementById('accept-cookies').addEventListener('click', () => {
consent.grantConsent()
hideConsentBanner()
})
document.getElementById('decline-cookies').addEventListener('click', () => {
consent.revokeConsent()
hideConsentBanner()
})
```
**Why this is good:**
- ✅ Uses `_fs_capture_on_startup = false` to **prevent ALL capture** until consent
- ✅ Uses `FS('start')` / `FS('shutdown')` for holistic control
- ✅ Persists consent choice in localStorage
- ✅ Restores consent state on page load
- ✅ Handles logged-in users properly
- ✅ Complies with GDPR requirement that NO tracking occurs before consent
### Example 2: Element-Level Consent (Selective Capture)
```javascript
// GOOD: Element-level consent for specific sensitive elements
// Use this when you want general capture but extra consent for certain elements
// First, in Fullstory Privacy Settings, mark specific elements as
// "Capture data with user consent" (e.g., form fields with sensitive data)
// Then in your code:
const ElementConsentManager = {
// User opts into enhanced tracking (for elements marked "Capture with consent")
enableEnhancedTracking() {
FS('setIdentity', {consent: true})
console.log('Enhanced tracking enabled for consent-marked elements')
},
// User opts out of enhanced tracking
disableEnhancedTracking() {
FS('setIdentity', {consent: false})
console.log('Enhanced tracking disabled')
},
}
// Usage in a preferences panel
document.getElementById('enhanced-tracking-checkbox').addEventListener('change', (e) => {
if (e.target.checked) {
ElementConsentManager.enableEnhancedTracking()
} else {
ElementConsentManager.disableEnhancedTracking()
}
})
```
**Why this is good:**
- ✅ General Fullstory capture still works
- ✅ Only specific pre-configured elements require extra consent
- ✅ Gives users granular control over sensitive data capture
### Example 3: Region-Based Consent (GDPR for EU Only)
```javascript
// GOOD: Full GDPR-compliant consent flow with region detection
// IMPORTANT: Set window['_fs_capture_on_startup'] = false BEFORE snippet loads
const RegionalConsent = {
// Check if user is in EU (simplified - use proper geolocation service in production)
isEUUser() {
return Intl.DateTimeFormat().resolvedOptions().timeZone.includes('Europe')
},
// Initialize based on region and consent state
async initialize() {
const needsConsent = this.isEUUser()
const hasConsent = this.getStoredConsent()
if (!needsConsent) {
// Non-EU: can capture without explicit consent (check local laws)
FS('start') // Use start() since we delayed capture
return
}
if (hasConsent === 'granted') {
FS('setIdentity', {consent: true})
} else if (hasConsent === 'denied') {
FS('setIdentity', {consent: false})
} else {
// No consent recorded - show banner, don't capture yet
this.showConsentBanner()
}
},
getStoredConsent() {
return localStorage.getItem('gdpr_consent_analytics')
},
recordConsent(granted, method) {
const consentRecord = {
granted,
timestamp: new Date().toISOString(),
method, // 'banner', 'settings', etc.
userAgent: navigator.userAgent,
}
localStorage.setItem('gdpr_consent_analytics', granted ? 'granted' : 'denied')
localStorage.setItem('gdpr_consent_record', JSON.stringify(consentRecord))
// Send to backend for compliance records
fetch('/api/consent', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(consentRecord),
}).catch(console.warn)
// Update Fullstory
FS('setIdentity', {consent: granted})
},
showConsentBanner() {
document.getElementById('gdpr-banner').style.display = 'block'
},
hideConsentBanner() {
document.getElementById('gdpr-banner').style.display = 'none'
},
// Required: Allow users to withdraw consent
withdrawConsent() {
this.recordConsent(false, 'user_withdrawal')
// Clear any stored Fullstory data
// Note: Fullstory doesn't store client-side, this is for other trackers
alert('Your consent has been withdrawn. Session recording has been disabled.')
},
// Required: Export user data (for GDPR data access requests)
async requestDataExport() {
// Redirect to Fullstory's data request process
// Or contact your Fullstory admin
window.location.href = '/privacy/data-request'
},
}
// Initialize on page load
GDPRConsent.initialize()
// Banner buttons
document.getElementById('accept-all').addEventListener('click', () => {
GDPRConsent.recordConsent(true, 'banner')
GDPRConsent.hideConsentBanner()
})
document.getElementById('reject-all').addEventListener('click', () => {
GDPRConsent.recordConsent(false, 'banner')
GDPRConsent.hideConsentBanner()
})
// Settings page withdrawal
document.getElementById('withdraw-consent').addEventListener('click', () => {
GDPRConsent.withdrawConsent()
})
```
**Why this is good:**
- ✅ Region-based consent requirements
- ✅ Records consent for compliance
- ✅ Provides withdrawal mechanism
- ✅ Backend record keeping
- ✅ Non-EU users not blocked
### Example 3: CMP (Consent Management Platform) Integration
```javascript
// GOOD: Integrate with OneTrust, CookieBot, or similar CMP
class CMPIntegration {
// OneTrust integration
static initOneTrust() {
// Listen for OneTrust consent changes
window.OptanonWrapper = function () {
const consentGroups = OnetrustActiveGroups || ''
// Check if analytics category is consented
// C0002 is typically the analytics category - verify your setup
if (consentGroups.includes('C0002')) {
FS('setIdentity', {consent: true})
} else {
FS('setIdentity', {consent: false})
}
}
// Also handle initial state
if (typeof OnetrustActiveGroups !== 'undefined') {
window.OptanonWrapper()
}
}
// CookieBot integration
static initCookieBot() {
window.addEventListener('CookiebotOnAccept', () => {
if (Cookiebot.consent.statistics) {
FS('setIdentity', {consent: true})
}
})
window.addEventListener('CookiebotOnDecline', () => {
FS('setIdentity', {consent: false})
})
// Check initial state
if (typeof Cookiebot !== 'undefined' && Cookiebot.consent) {
if (Cookiebot.consent.statistics) {
FS('setIdentity', {consent: true})
} else {
FS('setIdentity', {consent: false})
}
}
}
// TrustArc integration
static initTrustArc() {
window.truste = window.truste || {}
window.truste.eu = window.truste.eu || {}
window.truste.eu.bindMap = {
behaviorManager: {
init: function () {
// Check consent status
const consent = truste.eu.getBehavior()
if (consent.analytics === 'on') {
FS('setIdentity', {consent: true})
} else {
FS('setIdentity', {consent: false})
}
},
},
}
}
// Generic CMP API (IAB TCF v2)
static initTCFv2() {
if (typeof __tcfapi === 'function') {
__tcfapi('addEventListener', 2, (tcData, success) => {
if (success && tcData.eventStatus === 'useractioncomplete') {
// Check for purpose 1 (store and access) and purpose 5 (measurement)
const hasConsent = tcData.purpose?.consents?.[1] && tcData.purpose?.consents?.[5]
FS('setIdentity', {consent: hasConsent})
}
})
}
}
}
// Initialize based on your CMP
// CMPIntegration.initOneTrust();
// CMPIntegration.initCookieBot();
// CMPIntegration.initTCFv2();
```
**Why this is good:**
- ✅ Works with popular CMPs
- ✅ Handles consent changes
- ✅ Checks initial state
- ✅ IAB TCF v2 compliant option
### Example 4: React Consent Hook
```jsx
// GOOD: React hook for consent management
import {useState, useEffect, useCallback, createContext, useContext} from 'react'
const ConsentContext = createContext(null)
export function ConsentProvider({children}) {
const [consentStatus, setConsentStatus] = useState(() => {
return localStorage.getItem('analytics_consent') // 'granted', 'denied', or null
})
useEffect(() => {
// Sync with Fullstory on mount and changes
if (consentStatus === 'granted') {
FS('setIdentity', {consent: true})
} else if (consentStatus === 'denied') {
FS('setIdentity', {consent: false})
}
}, [consentStatus])
const grantConsent = useCallback(() => {
localStorage.setItem('analytics_consent', 'granted')
setConsentStatus('granted')
}, [])
const denyConsent = useCallback(() => {
localStorage.setItem('analytics_consent', 'denied')
setConsentStatus('denied')
}, [])
const resetConsent = useCallback(() => {
localStorage.removeItem('analytics_consent')
setConsentStatus(null)
}, [])
return (
We use session recording to improve your experience.