Claude Code Plugins

Community-maintained marketplace

Feedback

fullstory-user-consent

@majiayu000/claude-skill-registry
4
0

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.

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-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:

"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.

// 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:

// 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:

// 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)

// 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

// 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)

// 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
// <script>window['_fs_capture_on_startup'] = false;</script>
// <script>/* Fullstory snippet here */</script>

// 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)

// 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)

// 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

// 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

// 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 (
    <ConsentContext.Provider value={{
      consentStatus,
      hasConsent: consentStatus === 'granted',
      needsConsent: consentStatus === null,
      grantConsent,
      denyConsent,
      resetConsent
    }}>
      {children}
    </ConsentContext.Provider>
  );
}

export function useConsent() {
  return useContext(ConsentContext);
}

// Consent Banner Component
function ConsentBanner() {
  const { needsConsent, grantConsent, denyConsent } = useConsent();
  
  if (!needsConsent) return null;
  
  return (
    <div className="consent-banner">
      <p>We use session recording to improve your experience.</p>
      <div className="consent-buttons">
        <button onClick={grantConsent}>Accept</button>
        <button onClick={denyConsent}>Decline</button>
      </div>
    </div>
  );
}

// Settings Component
function PrivacySettings() {
  const { hasConsent, grantConsent, denyConsent, resetConsent } = useConsent();
  
  return (
    <div className="privacy-settings">
      <h2>Privacy Settings</h2>
      <label>
        <input
          type="checkbox"
          checked={hasConsent}
          onChange={(e) => e.target.checked ? grantConsent() : denyConsent()}
        />
        Allow session recording
      </label>
      <button onClick={resetConsent}>Reset Preferences</button>
    </div>
  );
}

Why this is good:

  • ✅ React-friendly state management
  • ✅ Context for app-wide access
  • ✅ Persists to localStorage
  • ✅ Syncs with Fullstory
  • ✅ Reusable components

Example 5: Consent with User Identification

// GOOD: Handle consent and identification together
class FullstoryManager {
  constructor() {
    this.consentGranted = false;
    this.currentUser = null;
  }
  
  // Call when consent is granted (from banner)
  onConsentGranted() {
    this.consentGranted = true;
    
    if (this.currentUser) {
      // User already logged in - identify them
      this.identifyUser(this.currentUser);
    } else {
      // Just enable capture anonymously
      FS('setIdentity', { consent: true });
    }
  }
  
  // Call when consent is denied
  onConsentDenied() {
    this.consentGranted = false;
    FS('setIdentity', { consent: false });
  }
  
  // Call when user logs in
  onUserLogin(user) {
    this.currentUser = user;
    
    if (this.consentGranted) {
      // Consent already granted - identify user
      this.identifyUser(user);
    }
    // If no consent, don't identify (they'll be identified when consent is granted)
  }
  
  // Call when user logs out
  onUserLogout() {
    this.currentUser = null;
    
    if (this.consentGranted) {
      // Anonymize but keep capturing (they consented)
      FS('setIdentity', { anonymous: true, consent: true });
    }
  }
  
  identifyUser(user) {
    FS('setIdentity', {
      uid: user.id,
      consent: true,
      properties: {
        displayName: user.name,
        email: user.email,
        plan: user.plan
      }
    });
  }
}

const fsManager = new FullstoryManager();

// Wire up to your auth system
authService.on('login', (user) => fsManager.onUserLogin(user));
authService.on('logout', () => fsManager.onUserLogout());

// Wire up to consent banner
consentBanner.on('accept', () => fsManager.onConsentGranted());
consentBanner.on('decline', () => fsManager.onConsentDenied());

Why this is good:

  • ✅ Handles consent + identity together
  • ✅ Correct order regardless of user flow
  • ✅ Maintains consent through login/logout
  • ✅ Covers all scenarios

❌ BAD IMPLEMENTATION EXAMPLES

Example 1: Capturing Before Consent

// BAD: Capturing without checking consent first
// This is the default snippet behavior - problematic for GDPR

// Page loads, Fullstory immediately starts capturing
// User hasn't consented yet!

// Later, user clicks "Decline"
FS('setIdentity', { consent: false });  // Too late - already captured data!

Why this is bad:

  • ❌ Data captured before consent given
  • ❌ GDPR violation risk
  • ❌ Can't un-capture what was already recorded

CORRECTED VERSION:

// GOOD: Configure snippet to wait for consent
// In your Fullstory snippet config:
window['_fs_capture_on_startup'] = false;

// Then enable capture only after consent
document.getElementById('accept').addEventListener('click', () => {
  FS('setIdentity', { consent: true });  // NOW we start capturing
});

Example 2: Not Persisting Consent

// BAD: Consent not persisted - user must consent on every page
document.getElementById('accept').addEventListener('click', () => {
  FS('setIdentity', { consent: true });
  // Consent not saved! On next page, user is asked again
});

Why this is bad:

  • ❌ User must consent on every page
  • ❌ Annoying user experience
  • ❌ May miss capture on subsequent pages

CORRECTED VERSION:

// GOOD: Persist and restore consent
document.getElementById('accept').addEventListener('click', () => {
  localStorage.setItem('fs_consent', 'granted');
  FS('setIdentity', { consent: true });
});

// On page load
const savedConsent = localStorage.getItem('fs_consent');
if (savedConsent === 'granted') {
  FS('setIdentity', { consent: true });
}

Example 3: No Way to Withdraw Consent

// BAD: Once granted, user can't withdraw consent
consentBanner.on('accept', () => {
  localStorage.setItem('consent', 'granted');
  FS('setIdentity', { consent: true });
  // No settings page to change this!
});

Why this is bad:

  • ❌ GDPR requires ability to withdraw
  • ❌ Users stuck with their choice
  • ❌ Compliance violation

CORRECTED VERSION:

// GOOD: Provide withdrawal mechanism
// In privacy settings page:
function withdrawConsent() {
  localStorage.setItem('consent', 'denied');
  FS('setIdentity', { consent: false });
  showConfirmation('Session recording has been disabled.');
}

// Make it easily accessible
// Link in footer: "Privacy Settings" -> withdrawal option

Example 4: Consent Logic Race Condition

// BAD: Race condition between consent check and identify
async function initApp() {
  const user = await getUser();
  
  // Race condition: identify might happen before consent is granted
  FS('setIdentity', {
    uid: user.id,
    properties: { name: user.name }
    // Missing consent check!
  });
  
  // Consent banner shown after - too late
  showConsentBannerIfNeeded();
}

Why this is bad:

  • ❌ User identified before consent check
  • ❌ Data captured without consent
  • ❌ Order of operations wrong

CORRECTED VERSION:

// GOOD: Check consent before identifying
async function initApp() {
  const user = await getUser();
  const hasConsent = localStorage.getItem('consent') === 'granted';
  
  if (hasConsent) {
    // Only identify if consent already granted
    FS('setIdentity', {
      uid: user.id,
      consent: true,
      properties: { name: user.name }
    });
  } else if (localStorage.getItem('consent') === null) {
    // No decision yet - show banner
    showConsentBanner();
  }
  // If explicitly denied, do nothing
}

Example 5: Ignoring CMP Status

// BAD: Not respecting CMP decisions
// CMP is configured, but Fullstory ignores it

// OneTrust says analytics is declined, but:
FS('setIdentity', { consent: true });  // BAD: Overriding CMP decision

Why this is bad:

  • ❌ Ignores user's CMP choice
  • ❌ Compliance violation
  • ❌ Inconsistent consent state

CORRECTED VERSION:

// GOOD: Respect CMP decisions
window.OptanonWrapper = function() {
  const groups = OnetrustActiveGroups || '';
  
  // Only enable if user consented to analytics in CMP
  if (groups.includes('C0002')) {
    FS('setIdentity', { consent: true });
  } else {
    FS('setIdentity', { consent: false });
  }
};

COMMON IMPLEMENTATION PATTERNS

Pattern 1: Consent State Machine

// State machine for consent handling
const ConsentStateMachine = {
  states: {
    UNKNOWN: 'unknown',     // No choice made
    GRANTED: 'granted',     // User accepted
    DENIED: 'denied',       // User declined
    WITHDRAWN: 'withdrawn'  // User withdrew consent
  },
  
  currentState: null,
  
  init() {
    const stored = localStorage.getItem('consent_state');
    this.currentState = stored || this.states.UNKNOWN;
    this.applyState();
  },
  
  transition(newState) {
    const oldState = this.currentState;
    this.currentState = newState;
    localStorage.setItem('consent_state', newState);
    
    console.log(`Consent: ${oldState} -> ${newState}`);
    this.applyState();
  },
  
  applyState() {
    switch (this.currentState) {
      case this.states.GRANTED:
        FS('setIdentity', { consent: true });
        break;
      case this.states.DENIED:
      case this.states.WITHDRAWN:
        FS('setIdentity', { consent: false });
        break;
      case this.states.UNKNOWN:
        // Show banner, don't capture
        showConsentBanner();
        break;
    }
  },
  
  accept() { this.transition(this.states.GRANTED); },
  decline() { this.transition(this.states.DENIED); },
  withdraw() { this.transition(this.states.WITHDRAWN); },
  reset() { this.transition(this.states.UNKNOWN); }
};

Pattern 2: Consent Wrapper for All FS Calls

// Wrapper that checks consent before any FS call
const ConsentManager = {
  _consentGranted: false,
  
  init(granted) {
    this._consentGranted = granted;
    if (granted) {
      FS('setIdentity', { consent: true });
    }
  },
  
  setConsent(granted) {
    this._consentGranted = granted;
    FS('setIdentity', { consent: granted });
  },
  
  // Wrapped methods that check consent
  trackEvent(name, properties) {
    if (!this._consentGranted) {
      console.debug('FS event skipped - no consent:', name);
      return;
    }
    FS('trackEvent', { name, properties });
  },
  
  setProperties(type, properties) {
    if (!this._consentGranted) {
      console.debug('FS properties skipped - no consent');
      return;
    }
    FS('setProperties', { type, properties });
  },
  
  identify(uid, properties) {
    if (!this._consentGranted) {
      console.debug('FS identify skipped - no consent');
      return;
    }
    FS('setIdentity', { uid, consent: true, properties });
  }
};

// Usage
ConsentManager.init(checkStoredConsent());
ConsentManager.trackEvent('Page Viewed', { page: '/home' });

SNIPPET CONFIGURATION

To require consent before capture, configure the Fullstory snippet:

window['_fs_capture_on_startup'] = false;  // Don't capture until consent
window['_fs_org'] = 'YOUR_ORG_ID';
window['_fs_script'] = 'edge.fullstory.com/s/fs.js';
// ... rest of snippet

Then call FS('setIdentity', { consent: true }) to start capture.


TROUBLESHOOTING

Capture Not Starting After Consent

Symptom: consent: true called but no session recorded

Common Causes:

  1. ❌ Fullstory script not loaded
  2. ❌ User on excluded page
  3. ❌ Privacy mode blocking FS

Solutions:

  • ✅ Verify FS is defined
  • ✅ Check page isn't excluded
  • ✅ Check browser privacy settings

Sessions Missing Consent Status

Symptom: Can't tell which sessions had consent

Solutions:

  • ✅ Set user property: consentGranted: true
  • ✅ Log consent status
  • ✅ Use page properties

KEY TAKEAWAYS FOR AGENT

When helping developers with Consent API:

  1. Always emphasize:

    • Configure snippet to wait for consent if GDPR applies
    • Persist consent to localStorage
    • Provide withdrawal mechanism
    • Check consent before identifying
  2. Common mistakes to watch for:

    • Capturing before consent
    • Not persisting consent
    • No withdrawal option
    • Ignoring CMP status
    • Race conditions with identity
  3. Questions to ask developers:

    • Do you need GDPR compliance?
    • Do you have an existing CMP?
    • How do users currently consent?
    • Is there a privacy settings page?
  4. Best practices to recommend:

    • Integrate with existing CMP
    • Persist consent state
    • Provide easy withdrawal
    • Test consent flows thoroughly

REFERENCE LINKS


This skill document was created to help Agent understand and guide developers in implementing Fullstory's User Consent API correctly for privacy-compliant web applications.