fullstory-anonymize-users
Comprehensive guide for implementing Fullstory's User Anonymization API (setIdentity with anonymous:true) for web applications. Teaches proper logout handling, session management, privacy compliance, and user switching scenarios. Includes detailed good/bad examples for logout flows, multi-user applications, and privacy-conscious implementations.
$ 安裝
git clone https://github.com/fullstorydev/fs-skills /tmp/fs-skills && cp -r /tmp/fs-skills/core/fullstory-anonymize-users ~/.claude/skills/fs-skills// tip: Run this command in your terminal to install the skill
name: fullstory-anonymize-users version: v2 description: Comprehensive guide for implementing Fullstory's User Anonymization API (setIdentity with anonymous:true) for web applications. Teaches proper logout handling, session management, privacy compliance, and user switching scenarios. Includes detailed good/bad examples for logout flows, multi-user applications, and privacy-conscious implementations. related_skills:
- fullstory-identify-users
- fullstory-user-consent
- fullstory-capture-control
- fullstory-async-methods
Fullstory Anonymize Users API
Overview
Fullstory's Anonymize Users API allows developers to release the identity of the current user and create a new anonymous session. This is essential for:
- User Logout: Properly ending an identified session when a user logs out
- Account Switching: Allowing users to switch between accounts cleanly
- Privacy Compliance: Implementing "forget me" or privacy-conscious features
- Shared Devices: Ensuring one user's session doesn't bleed into another's
When you call FS('setIdentity', { anonymous: true }), the current session ends and a fresh anonymous session begins. The previously identified user remains in Fullstory's records, but subsequent activity is no longer linked to them.
Core Concepts
What Happens When You Anonymize
- Current session is closed and marked as belonging to the identified user
- A new
fs_uidcookie is generated - breaking the link to all previous sessions - New anonymous session begins with a new session ID and new cookie
- Previous user data is preserved - anonymizing doesn't delete history
- Subsequent activity is anonymous until a new
setIdentitycall
Cookie Behavior: Normally, the
fs_uidfirst-party cookie (1-year expiry) links all sessions from the same browser together. Whenanonymizeis called, Fullstory generates a newfs_uidcookie, effectively creating a "new device" from Fullstory's perspective. Any futuresetIdentitycalls will only merge sessions from the new cookie, not the old one.Reference: Why Fullstory uses First-Party Cookies
Session Lifecycle
┌─────────────┐ Login ┌─────────────┐ Logout ┌─────────────┐
│ Anonymous │ ───────► │ Identified │ ───────► │ New Anon │
│ Session A │ │ Session B │ │ Session C │
└─────────────┘ └─────────────┘ └─────────────┘
│
setIdentity │ setIdentity
(uid: 'xxx') │ (anonymous: true)
When to Anonymize
| Scenario | Should Anonymize? | Reason |
|---|---|---|
| User logs out | ✅ Yes | Prevents session attribution to wrong user |
| User switches accounts | ✅ Yes | Clean slate before new identification |
| User requests data deletion | ❓ Consider | Part of broader privacy implementation |
| User clears browser data | ❌ No | Fullstory handles this automatically |
| Page navigation | ❌ No | Identity persists across pages |
| Session timeout | ❓ Depends | Based on your security requirements |
API Reference
Basic Syntax
FS('setIdentity', { anonymous: true });
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
anonymous | boolean | Yes | Must be true to anonymize the user |
Rate Limits
- Sustained: 30 calls per page per minute
- Burst: 10 calls per second
Async Version
await FS('setIdentityAsync', { anonymous: true });
✅ GOOD IMPLEMENTATION EXAMPLES
Example 1: Basic Logout Handler
// GOOD: Proper logout with Fullstory anonymization
async function handleLogout() {
try {
// 1. Call your backend logout endpoint
await fetch('/api/auth/logout', { method: 'POST' });
// 2. Clear local authentication state
clearAuthTokens();
clearUserState();
// 3. Anonymize in Fullstory BEFORE redirecting
FS('setIdentity', { anonymous: true });
// 4. Redirect to login page
window.location.href = '/login';
} catch (error) {
console.error('Logout failed:', error);
// Still anonymize even if backend fails
FS('setIdentity', { anonymous: true });
window.location.href = '/login';
}
}
Why this is good:
- ✅ Anonymizes before redirect
- ✅ Handles errors gracefully
- ✅ Clears local state before anonymizing
- ✅ Ensures next user won't be associated with previous user
Example 2: React Logout with Auth Context
// GOOD: React hook pattern for logout with Fullstory
import { useCallback } from 'react';
import { useAuth } from './auth-context';
import { useNavigate } from 'react-router-dom';
function useLogout() {
const { clearAuth } = useAuth();
const navigate = useNavigate();
const logout = useCallback(async () => {
// Track the logout action before anonymizing
FS('trackEvent', {
name: 'User Logged Out',
properties: {
logoutMethod: 'manual',
sessionDuration: getSessionDuration()
}
});
// Clear application auth state
await clearAuth();
// Anonymize the Fullstory session
FS('setIdentity', { anonymous: true });
// Navigate to home/login
navigate('/login');
}, [clearAuth, navigate]);
return logout;
}
// Usage
function LogoutButton() {
const logout = useLogout();
return <button onClick={logout}>Sign Out</button>;
}
Why this is good:
- ✅ Tracks logout event before anonymizing (preserves attribution)
- ✅ Integrated with React ecosystem
- ✅ Reusable across components
- ✅ Clean navigation after logout
Example 3: Account Switching
// GOOD: Clean account switching with proper session boundaries
async function switchAccount(newAccountId) {
const newUser = await fetchAccountDetails(newAccountId);
// Track the switch event under current user
FS('trackEvent', {
name: 'Account Switch Initiated',
properties: {
targetAccountId: newAccountId
}
});
// Step 1: Anonymize current session
FS('setIdentity', { anonymous: true });
// Step 2: Set up new account context
await setupAccountContext(newUser);
// Step 3: Identify as new user
FS('setIdentity', {
uid: newUser.id,
properties: {
displayName: newUser.name,
email: newUser.email,
accountType: newUser.type
}
});
// Refresh UI
window.location.reload();
}
Why this is good:
- ✅ Tracks event before identity change
- ✅ Cleanly separates sessions between accounts
- ✅ No data contamination between accounts
- ✅ New user gets fresh identification
Example 4: Session Timeout Handler
// GOOD: Handling session timeout with Fullstory
class SessionManager {
constructor() {
this.timeoutDuration = 30 * 60 * 1000; // 30 minutes
this.timeoutId = null;
this.lastActivity = Date.now();
}
startTimeout() {
this.resetTimeout();
document.addEventListener('click', () => this.resetTimeout());
document.addEventListener('keypress', () => this.resetTimeout());
}
resetTimeout() {
this.lastActivity = Date.now();
if (this.timeoutId) clearTimeout(this.timeoutId);
this.timeoutId = setTimeout(() => {
this.handleSessionTimeout();
}, this.timeoutDuration);
}
async handleSessionTimeout() {
// Track timeout event while still identified
FS('trackEvent', {
name: 'Session Timeout',
properties: {
inactivityDuration: Date.now() - this.lastActivity,
lastPage: window.location.pathname
}
});
// Anonymize the session
FS('setIdentity', { anonymous: true });
// Clear auth and redirect
clearAuthState();
showTimeoutModal();
}
}
Why this is good:
- ✅ Tracks timeout before anonymizing
- ✅ Captures useful debugging info
- ✅ Clean session boundary on timeout
- ✅ User feedback via modal
Example 5: Privacy-Conscious Implementation
// GOOD: "Incognito mode" toggle for privacy-conscious users
class PrivacyManager {
constructor() {
this.isIncognitoMode = false;
this.originalUserId = null;
}
async enableIncognitoMode(currentUserId) {
// Store original user ID for potential re-identification
this.originalUserId = currentUserId;
this.isIncognitoMode = true;
// Track before anonymizing
FS('trackEvent', {
name: 'Incognito Mode Enabled',
properties: {}
});
// Anonymize - activity won't be linked to user
FS('setIdentity', { anonymous: true });
// Update UI
showIncognitoIndicator();
}
async disableIncognitoMode() {
if (!this.originalUserId) return;
this.isIncognitoMode = false;
// Re-identify user
const user = await getCurrentUser();
FS('setIdentity', {
uid: user.id,
properties: {
displayName: user.name,
email: user.email
}
});
// Track re-enablement
FS('trackEvent', {
name: 'Incognito Mode Disabled',
properties: {}
});
hideIncognitoIndicator();
this.originalUserId = null;
}
}
Why this is good:
- ✅ Gives users control over tracking
- ✅ Maintains ability to re-identify
- ✅ Clear user feedback
- ✅ Events tracked at session boundaries
❌ BAD IMPLEMENTATION EXAMPLES
Example 1: Forgetting to Anonymize on Logout
// BAD: No Fullstory anonymization on logout
function handleLogout() {
clearAuthTokens();
clearUserState();
window.location.href = '/login';
// Missing FS('setIdentity', { anonymous: true })!
}
Why this is bad:
- ❌ Next user's activity may be attributed to previous user
- ❌ Session continues under wrong identity
- ❌ Data integrity issues in analytics
- ❌ Privacy concern if sharing device
CORRECTED VERSION:
// GOOD: Include Fullstory anonymization
function handleLogout() {
clearAuthTokens();
clearUserState();
// Anonymize before redirect
FS('setIdentity', { anonymous: true });
window.location.href = '/login';
}
Example 2: Anonymizing After Redirect
// BAD: Anonymizing after navigation starts
function handleLogout() {
window.location.href = '/login';
// BAD: This may never execute - page is already navigating!
FS('setIdentity', { anonymous: true });
}
Why this is bad:
- ❌ Navigation starts before anonymization
- ❌ FS call may not complete
- ❌ Session may not properly close
CORRECTED VERSION:
// GOOD: Anonymize BEFORE navigation
async function handleLogout() {
// Anonymize first
await FS('setIdentityAsync', { anonymous: true });
// Then navigate
window.location.href = '/login';
}
Example 3: Anonymizing Repeatedly
// BAD: Calling anonymize multiple times
function handleLogout() {
// Excessive calls
FS('setIdentity', { anonymous: true });
FS('setIdentity', { anonymous: true });
FS('setIdentity', { anonymous: true });
window.location.href = '/login';
}
Why this is bad:
- ❌ Wastes API call quota
- ❌ Creates unnecessary session splits
- ❌ May hit rate limits
- ❌ No benefit from multiple calls
CORRECTED VERSION:
// GOOD: Single anonymization call
function handleLogout() {
FS('setIdentity', { anonymous: true });
window.location.href = '/login';
}
Example 4: Using Wrong Parameter
// BAD: Wrong way to anonymize
FS('setIdentity', { uid: null }); // BAD: uid shouldn't be null
FS('setIdentity', { uid: 'anonymous' }); // BAD: This identifies as user "anonymous"!
FS('setIdentity', { uid: '' }); // BAD: Empty string uid
FS('setIdentity', {}); // BAD: Missing required parameters
Why this is bad:
- ❌ uid: null may cause errors
- ❌ uid: 'anonymous' creates an identified user named "anonymous"
- ❌ Empty string uid is invalid
- ❌ Empty object doesn't anonymize
CORRECTED VERSION:
// GOOD: Proper anonymization syntax
FS('setIdentity', { anonymous: true });
Example 5: Anonymizing Without Tracking Important Events
// BAD: Missing opportunity to track logout event
function handleLogout() {
// Just anonymizing without capturing useful data
FS('setIdentity', { anonymous: true });
window.location.href = '/login';
}
Why this is bad:
- ❌ No record of intentional logout vs session timeout
- ❌ Can't analyze logout patterns
- ❌ Loses attribution for the logout event itself
CORRECTED VERSION:
// GOOD: Track event before anonymizing
function handleLogout() {
// Track while still identified
FS('trackEvent', {
name: 'User Logged Out',
properties: {
logoutMethod: 'user_initiated',
pageAtLogout: window.location.pathname
}
});
// Then anonymize
FS('setIdentity', { anonymous: true });
window.location.href = '/login';
}
Example 6: Anonymizing During Errors Instead of Proper Handling
// BAD: Using anonymization to "hide" errors
function handleError(error) {
// Don't use anonymization to hide error attribution!
FS('setIdentity', { anonymous: true });
console.error(error);
}
Why this is bad:
- ❌ Loses error attribution to user for debugging
- ❌ Makes it harder to help affected users
- ❌ Misuse of anonymization API
- ❌ Creates confusing session boundaries
CORRECTED VERSION:
// GOOD: Log errors while identified, only anonymize on logout
function handleError(error) {
// Track the error - attribution helps debugging!
FS('trackEvent', {
name: 'Application Error',
properties: {
errorMessage: error.message,
errorCode: error.code,
page: window.location.pathname
}
});
// Show error UI without anonymizing
showErrorMessage(error);
}
COMMON IMPLEMENTATION PATTERNS
Pattern 1: Logout Service
// Centralized logout service
class LogoutService {
static async logout(options = {}) {
const {
trackEvent = true,
redirectUrl = '/login',
reason = 'user_initiated'
} = options;
// Track logout if requested
if (trackEvent) {
FS('trackEvent', {
name: 'User Logged Out',
properties: {
reason: reason,
sessionDuration: getSessionDuration()
}
});
}
// Backend logout
try {
await fetch('/api/logout', { method: 'POST' });
} catch (e) {
console.warn('Backend logout failed:', e);
}
// Clear client state
clearAuthTokens();
clearUserState();
clearLocalStorage();
// Anonymize Fullstory
FS('setIdentity', { anonymous: true });
// Redirect
if (redirectUrl) {
window.location.href = redirectUrl;
}
}
}
// Usage
await LogoutService.logout();
await LogoutService.logout({ reason: 'session_timeout', redirectUrl: '/timeout' });
Pattern 2: Multi-Tenant Application
// For apps with workspace/tenant switching
class TenantManager {
async switchTenant(newTenantId) {
const currentUser = getCurrentUser();
// Track switch under current context
FS('trackEvent', {
name: 'Tenant Switch',
properties: {
fromTenant: currentUser.tenantId,
toTenant: newTenantId
}
});
// Start fresh session for new tenant context
FS('setIdentity', { anonymous: true });
// Update tenant context
await loadTenantContext(newTenantId);
// Re-identify with new tenant context
FS('setIdentity', {
uid: currentUser.id,
properties: {
displayName: currentUser.name,
email: currentUser.email,
tenantId: newTenantId,
tenantName: await getTenantName(newTenantId)
}
});
}
}
Pattern 3: Kiosk/Shared Device Mode
// For shared devices like kiosks
class KioskMode {
sessionTimeout = 5 * 60 * 1000; // 5 minutes
async startSession(user) {
FS('setIdentity', {
uid: user.id,
properties: {
displayName: user.name,
deviceMode: 'kiosk',
locationId: getKioskLocation()
}
});
// Auto-logout after timeout
setTimeout(() => this.endSession(), this.sessionTimeout);
}
async endSession() {
FS('trackEvent', {
name: 'Kiosk Session Ended',
properties: {
endReason: 'timeout',
location: getKioskLocation()
}
});
FS('setIdentity', { anonymous: true });
// Reset to welcome screen
showWelcomeScreen();
}
}
RELATIONSHIP WITH OTHER FS APIs
Anonymize vs setProperties
// setProperties updates user info without changing identity
FS('setProperties', {
type: 'user',
properties: { lastActiveAt: new Date().toISOString() }
});
// anonymous: true ends the session entirely
FS('setIdentity', { anonymous: true }); // New session starts
Anonymize + Re-identify Flow
// Common pattern: switch users
FS('setIdentity', { anonymous: true }); // End session 1
// Some time passes, new user logs in
FS('setIdentity', { // Start session 2
uid: newUser.id,
properties: { displayName: newUser.name }
});
TROUBLESHOOTING
Session Not Properly Ending
Symptom: New user activity appears under old user
Common Causes:
- ❌ Anonymize called after page navigation starts
- ❌ Anonymize never called (missing from logout flow)
- ❌ Using wrong syntax (uid: null instead of anonymous: true)
Solutions:
- ✅ Use async version and await completion
- ✅ Audit all logout paths to ensure anonymization
- ✅ Use
{ anonymous: true }syntax
Too Many Session Splits
Symptom: User has many short, fragmented sessions
Common Causes:
- ❌ Calling anonymize on every page load
- ❌ Calling anonymize on errors
- ❌ Multiple anonymize calls in single flow
Solutions:
- ✅ Only anonymize on actual logout/switch events
- ✅ Audit code for unintended anonymize calls
- ✅ Use single anonymize call per logout flow
LIMITS AND CONSTRAINTS
Call Frequency
- Sustained: 30 calls per page per minute
- Burst: 10 calls per second
- Single call per logout is sufficient
Session Behavior
- Anonymization creates a new session immediately
- Previous session data remains intact and attributed
- Subsequent activity is anonymous until next identification
KEY TAKEAWAYS FOR AGENT
When helping developers implement User Anonymization:
-
Always emphasize:
- Anonymize BEFORE navigation/redirects
- Use
{ anonymous: true }syntax exactly - Track important events BEFORE anonymizing
- Single call per logout is sufficient
-
Common mistakes to watch for:
- Forgetting to anonymize on logout
- Anonymizing after redirect starts
- Using wrong syntax (uid: null, uid: 'anonymous')
- Over-calling anonymize
- Missing trackEvent before anonymize
-
Questions to ask developers:
- What are all the logout/signout paths in your app?
- Do users switch between accounts?
- Is this a shared device scenario?
- What events should be tracked before logout?
-
Integration considerations:
- Must anonymize in all logout paths
- Consider session timeout handling
- Account switching needs anonymize between identifications
- Order matters: track events → anonymize → redirect
REFERENCE LINKS
- Anonymize Users: https://developer.fullstory.com/browser/identification/anonymize-users/
- Identify Users: https://developer.fullstory.com/browser/identification/identify-users/
- Help Center - Anonymizing: https://help.fullstory.com/hc/en-us/articles/360020623514-Anonymizing-Users
This skill document was created to help Agent understand and guide developers in implementing Fullstory's User Anonymization API correctly for web applications.
Repository
