Claude-skill-registry fullstory-identify-users
Comprehensive guide for implementing Fullstory's User Identification API (setIdentity) across web applications. Teaches proper uid handling, property passing, re-identification behavior, and session management. Includes detailed good/bad examples for login flows, multi-account scenarios, and SPA applications to help developers correctly identify users for analytics and session replay.
git clone https://github.com/majiayu000/claude-skill-registry
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/fullstory-identify-users" ~/.claude/skills/majiayu000-claude-skill-registry-fullstory-identify-users && rm -rf "$T"
skills/data/fullstory-identify-users/SKILL.mdFullstory Identify Users API
Overview
Fullstory's User Identification API allows developers to associate session data with your own unique customer identifiers. By calling
FS('setIdentity'), you link a user's Fullstory session to their identity in your system, enabling you to:
- Search for sessions by customer ID
- View all sessions for a specific user across devices
- Connect Fullstory data with your internal analytics and CRM systems
- Attribute behavior patterns to known users
This skill covers implementation patterns, best practices, and common pitfalls for browser/web applications using the v2 Browser API.
Core Concepts
When Identification Happens
- On Login: Call
immediately after a successful authenticationsetIdentity - On Page Load (Already Authenticated): Call
on every page load if the user is logged insetIdentity - After Authentication Redirects: Ensure identification persists across OAuth/SSO redirects
User Identity vs Anonymous Sessions
- Anonymous Session: Default state before
is called. User is tracked via thesetIdentity
first-party cookie but not linked to your system.fs_uid - Identified Session: After
, the session is permanently linked to the providedsetIdentity
.uid
How Identification Works (Cookie Behavior)
Fullstory uses a first-party cookie (
fs_uid) to track users across sessions.
Why First-Party Cookies Matter
| Aspect | First-Party (Fullstory) | Third-Party |
|---|---|---|
| Domain | Set on YOUR domain (example.com) | Set on external domain (ads.tracker.com) |
| Browser blocking | ✅ Not blocked by browsers or ad-blockers | ❌ Often blocked by default |
| Cross-site tracking | ❌ Cannot track users across different sites | ✅ Can track across sites |
| Privacy | ✅ Data stays within your domain context | ❌ Aggregates data across web |
Key Benefit: Because Fullstory uses first-party cookies on YOUR domain, user identity cannot be connected between multiple sites using Fullstory. Each site has its own separate
cookie - your customers' data is isolated to your site only.fs_uid
Cookie-Based Session Linking
- Before identification: The
cookie links all sessions from the same browser/device together (persists for 1 year unless deleted)fs_uid - When
is called: ALL previous anonymous sessions with that samesetIdentity
cookie are retroactively merged into the identified userfs_uid - Cross-device linking: If the same
is used on different devices, all sessions across all devices are linkeduid
┌─────────────────────────────────────────────────────────────────────────┐ │ Session Merging on Identification │ ├─────────────────────────────────────────────────────────────────────────┤ │ Day 1: Anonymous visit fs_uid: abc123 → "Anonymous User" │ │ Day 3: Anonymous visit fs_uid: abc123 → Same anonymous user │ │ Day 7: User logs in, setIdentity(uid: "user_456") │ │ ↓ │ │ Result: ALL sessions (Day 1, 3, 7) now linked to "user_456" │ └─────────────────────────────────────────────────────────────────────────┘
Important: This means a user's entire journey from first visit through conversion can be tracked, even if they only identify on their 5th session.
Reference: Why Fullstory uses First-Party Cookies
Re-identification Behavior
- CRITICAL: You cannot change a user's identity once assigned within a session
- If you call
with a differentsetIdentity
, Fullstory automatically splits the session into a new sessionuid - This is by design to maintain data integrity and prevent identity pollution
Key Principles
- Use a stable, unique identifier (database ID, UUID) - never use PII like email as the uid
- Call
as early as possible after authenticationsetIdentity - Include meaningful properties for searchability (displayName, email)
- Handle logout properly by calling anonymize
setIdentity vs setProperties (User)
| Scenario | Use This API | Why |
|---|---|---|
| User logs in | | Links session to user identity |
| Initial user properties at login | with | Convenient to include with identification |
| Update user properties later | (type: 'user') | Don't re-identify just to update properties |
| Properties for anonymous user | (type: 'user') | Works without identification! |
Important: The
object inpropertiesis a convenience - you can include initial properties when identifying. However, for updating properties after identification or for anonymous users, usesetIdentitywithsetPropertiesinstead. See the fullstory-user-properties skill for details.type: 'user'
// At login - identify with initial properties FS('setIdentity', { uid: user.id, properties: {displayName: user.name, email: user.email, plan: user.plan}, }) // Later - user upgrades plan (DON'T re-identify!) FS('setProperties', { type: 'user', properties: {plan: 'enterprise', upgraded_at: new Date().toISOString()}, })
API Reference
Basic Syntax
FS('setIdentity', { uid: string, // Required: Your unique user identifier properties?: object, // Optional: Additional user properties schema?: object // Optional: Type inference for properties });
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| string | Yes | A unique identifier for the user from your system. Must be stable and unique. Maximum 256 characters. |
| object | No | Key/value pairs of additional user information |
| object | No | Type hints for property values (see Custom Properties) |
Special Property Fields
| Field | Type | Description |
|---|---|---|
| string | Shown in session list and user card in Fullstory app |
| string | Enables search via HTTP API and email-based lookups |
Supported Property Types
| Type | Description | Examples |
|---|---|---|
| String value | "premium", "enterprise" |
| Array of strings | ["admin", "beta-tester"] |
| Integer | 42, -5, 0 |
| Array of integers | [1, 2, 3] |
| Float/decimal | 99.99, -3.14 |
| Array of reals | [10.5, 20.0] |
| Boolean | true, false |
| Array of booleans | [true, false, true] |
| ISO8601 date | "2024-01-15T00:00:00Z" |
| Array of dates | ["2024-01-01", "2024-02-01"] |
Rate Limits
- Sustained: 30 calls per page per minute
- Burst: 10 calls per second
✅ GOOD IMPLEMENTATION EXAMPLES
Example 1: Basic Login Identification
// GOOD: Call setIdentity immediately after successful login async function handleLogin(credentials) { try { const response = await authenticateUser(credentials) const user = response.user // Identify user in Fullstory right after successful auth FS('setIdentity', { uid: user.id, // Use stable database ID, not email properties: { displayName: user.fullName, email: user.email, accountType: user.plan, signupDate: user.createdAt, // ISO8601 format }, }) // Continue with login flow redirectToDashboard() } catch (error) { handleLoginError(error) } }
Why this is good:
- ✅ Uses stable database ID as uid, not PII
- ✅ Called immediately after successful authentication
- ✅ Includes displayName for easy identification in Fullstory
- ✅ Includes email for searchability
- ✅ Adds business-relevant properties (accountType, signupDate)
Example 2: Page Load with Existing Session (SPA/SSR)
// GOOD: Check authentication state and identify on every page load function initializeFullstory() { const currentUser = getCurrentAuthenticatedUser() if (currentUser && currentUser.id) { // User is logged in - identify them FS('setIdentity', { uid: currentUser.id, properties: { displayName: currentUser.name, email: currentUser.email, role: currentUser.role, companyId: currentUser.organizationId, plan: currentUser.subscription.plan, trialEndsAt: currentUser.subscription.trialEnd, }, }) } // If not logged in, user remains anonymous (default state) } // Call on app initialization document.addEventListener('DOMContentLoaded', initializeFullstory)
Why this is good:
- ✅ Handles both authenticated and anonymous states
- ✅ Calls on every page load to ensure identification survives navigation
- ✅ Includes organization context for B2B analytics
- ✅ Captures subscription data for segment analysis
Example 3: React/Next.js Integration
// GOOD: React hook for Fullstory identification import {useEffect} from 'react' import {useAuth} from './auth-context' function useFullstoryIdentity() { const {user, isAuthenticated, isLoading} = useAuth() useEffect(() => { // Wait for auth state to be determined if (isLoading) return if (isAuthenticated && user) { FS('setIdentity', { uid: user.id, properties: { displayName: `${user.firstName} ${user.lastName}`, email: user.email, role: user.role, teamSize: user.team?.memberCount, features: user.enabledFeatures, // Array of strings lastLoginAt: new Date().toISOString(), }, }) } }, [user, isAuthenticated, isLoading]) } // Usage in App component function App() { useFullstoryIdentity() return <AppContent /> }
Why this is good:
- ✅ Waits for auth state to stabilize before identifying
- ✅ Re-runs when user state changes
- ✅ Handles loading states properly
- ✅ Captures feature flags for segmentation
- ✅ Updates lastLoginAt for recency tracking
Example 4: OAuth/SSO Callback Handling
// GOOD: Identify user after OAuth callback async function handleOAuthCallback() { const urlParams = new URLSearchParams(window.location.search) const code = urlParams.get('code') if (!code) { redirectToLogin() return } try { // Exchange code for tokens and user info const {user, tokens} = await exchangeOAuthCode(code) // Store tokens setAuthTokens(tokens) // Identify in Fullstory BEFORE redirecting away FS('setIdentity', { uid: user.id, properties: { displayName: user.name, email: user.email, authProvider: 'google', // Track SSO provider ssoOrganization: user.hostedDomain, }, }) // Now safe to redirect redirectToDashboard() } catch (error) { handleOAuthError(error) } }
Why this is good:
- ✅ Identifies user before any redirects
- ✅ Captures authentication provider for analytics
- ✅ Handles SSO organization context
- ✅ Proper error handling flow
Example 5: With Explicit Schema Types
// GOOD: Using schema for explicit type control FS('setIdentity', { uid: 'usr_a1b2c3d4e5', properties: { displayName: 'Sarah Johnson', email: 'sarah.johnson@company.com', accountBalance: 1500.5, loginCount: 42, isPremium: true, signupDate: '2023-06-15T00:00:00Z', permissions: ['read', 'write', 'admin'], }, schema: { accountBalance: 'real', loginCount: 'int', isPremium: 'bool', signupDate: 'date', permissions: 'strs', }, })
Why this is good:
- ✅ Explicit schema ensures proper type handling
- ✅ Enables numeric comparisons in Fullstory search
- ✅ Boolean enables is/is-not filtering
- ✅ Date enables time-based queries
- ✅ String array enables "contains" searches
❌ BAD IMPLEMENTATION EXAMPLES
Example 1: Using Email as UID
// BAD: Using email as the unique identifier FS('setIdentity', { uid: user.email, // BAD: PII as uid properties: { displayName: user.name, }, })
Why this is bad:
- ❌ Email is PII - exposes it in Fullstory URLs and exports
- ❌ Email can change when user updates their email address
- ❌ May cause session linking issues if email is reused
- ❌ Violates privacy best practices
CORRECTED VERSION:
// GOOD: Use stable database ID FS('setIdentity', { uid: user.id, // Stable, non-PII identifier properties: { displayName: user.name, email: user.email, // Email as a searchable property, not the uid }, })
Example 2: Identifying Before Authentication Completes
// BAD: Calling setIdentity before auth is confirmed function handleLoginClick() { const credentials = getFormCredentials() // BAD: Identifying with form data before server confirms identity FS('setIdentity', { uid: credentials.username, properties: { email: credentials.email, }, }) // This might fail - user isn't actually authenticated yet! authenticateUser(credentials) }
Why this is bad:
- ❌ Identifies user before authentication succeeds
- ❌ If login fails, wrong identity is associated with session
- ❌ Username from form may not match actual user ID
- ❌ Creates data integrity issues
CORRECTED VERSION:
// GOOD: Only identify after successful authentication async function handleLoginClick() { const credentials = getFormCredentials() try { const response = await authenticateUser(credentials) // Only identify AFTER server confirms auth FS('setIdentity', { uid: response.user.id, properties: { displayName: response.user.name, email: response.user.email, }, }) redirectToDashboard() } catch (error) { // Login failed - user remains anonymous showLoginError(error) } }
Example 3: Attempting to Change Identity
// BAD: Trying to switch users without proper anonymization function switchUserAccount(newUser) { // This will cause a session split, not update the identity! FS('setIdentity', { uid: newUser.id, properties: { displayName: newUser.name, }, }) }
Why this is bad:
- ❌ Cannot change identity of an already-identified user
- ❌ Causes automatic session split (may be unexpected)
- ❌ Creates confusing user journey in Fullstory
- ❌ May fragment analytics data
CORRECTED VERSION:
// GOOD: Properly handle account switching async function switchUserAccount(newUser) { // First, anonymize the current session FS('setIdentity', {anonymous: true}) // Then identify as the new user (starts fresh session) FS('setIdentity', { uid: newUser.id, properties: { displayName: newUser.name, email: newUser.email, }, }) }
Example 4: Calling setIdentity on Every Interaction
// BAD: Over-calling setIdentity function handleButtonClick(buttonId) { // BAD: Don't call setIdentity on every interaction! FS('setIdentity', { uid: currentUser.id, properties: { displayName: currentUser.name, lastAction: buttonId, // Trying to track actions via identity }, }) performAction(buttonId) }
Why this is bad:
- ❌ Wastes rate limit quota (30 calls/minute max)
- ❌ May hit burst limit (10 calls/second)
- ❌ Properties on identity shouldn't track transient actions
- ❌ Misuse of API - should use trackEvent for actions
CORRECTED VERSION:
// GOOD: Identify once, use events for actions // On page load / auth FS('setIdentity', { uid: currentUser.id, properties: { displayName: currentUser.name, email: currentUser.email, }, }) // For tracking actions - use trackEvent instead function handleButtonClick(buttonId) { FS('trackEvent', { name: 'Button Clicked', properties: { buttonId: buttonId, buttonSection: getButtonSection(buttonId), }, }) performAction(buttonId) }
Example 5: Missing displayName and email
// BAD: Minimal identification with no useful properties FS('setIdentity', { uid: user.id, // No properties at all! })
Why this is bad:
- ❌ No displayName - sessions show cryptic ID in Fullstory UI
- ❌ No email - can't search users via email
- ❌ No business context - can't segment users effectively
- ❌ Wastes the opportunity to enrich user data
CORRECTED VERSION:
// GOOD: Rich user context FS('setIdentity', { uid: user.id, properties: { displayName: user.fullName || user.username, email: user.email, role: user.role, plan: user.subscriptionPlan, companyName: user.organization?.name, signupDate: user.createdAt, }, })
Example 6: Type Mismatches in Properties
// BAD: Wrong value formats for intended types FS('setIdentity', { uid: 'usr_123', properties: { displayName: 'John Doe', loginCount: '42', // BAD: String instead of number accountBalance: '$150.00', // BAD: Currency symbol in number isPremium: 'yes', // BAD: Should be boolean signupDate: 'June 15 2023', // BAD: Not ISO8601 format }, schema: { loginCount: 'int', accountBalance: 'real', isPremium: 'bool', signupDate: 'date', }, })
Why this is bad:
- ❌ loginCount '42' won't parse correctly as int
- ❌ '$150.00' won't parse as real due to $ symbol
- ❌ 'yes' is not a valid boolean value
- ❌ Date format won't be recognized
CORRECTED VERSION:
// GOOD: Properly formatted values FS('setIdentity', { uid: 'usr_123', properties: { displayName: 'John Doe', loginCount: 42, // Number type accountBalance: 150.0, // Clean number currency: 'USD', // Currency as separate field isPremium: true, // Boolean type signupDate: '2023-06-15T00:00:00Z', // ISO8601 format }, schema: { loginCount: 'int', accountBalance: 'real', isPremium: 'bool', signupDate: 'date', }, })
COMMON IMPLEMENTATION PATTERNS
Pattern 1: Authentication State Machine
┌─────────────────┐ │ Anonymous │ ← Initial state (no setIdentity called) │ Session │ └────────┬────────┘ │ User logs in ▼ ┌─────────────────┐ │ Identified │ ← setIdentity({ uid: 'xxx' }) called │ Session │ └────────┬────────┘ │ User logs out ▼ ┌─────────────────┐ │ New Anonymous │ ← setIdentity({ anonymous: true }) called │ Session │ └─────────────────┘
Pattern 2: Multi-Account Application
// For apps where users can switch between accounts class FullstoryIdentityManager { currentUserId = null identify(user) { if (this.currentUserId && this.currentUserId !== user.id) { // Different user - must anonymize first this.anonymize() } FS('setIdentity', { uid: user.id, properties: { displayName: user.name, email: user.email, }, }) this.currentUserId = user.id } anonymize() { FS('setIdentity', {anonymous: true}) this.currentUserId = null } updateProperties(properties) { // Use setProperties for property-only updates FS('setProperties', { type: 'user', properties: properties, }) } }
Pattern 3: Progressive Identification
// For apps with guest checkout → account creation flow class ProgressiveIdentification { // During guest checkout - don't identify yet handleGuestCheckout(cart) { // User remains anonymous // Track the checkout event instead FS('trackEvent', { name: 'Guest Checkout Started', properties: { cartValue: cart.total, itemCount: cart.items.length, }, }) } // When guest creates account after checkout handleAccountCreation(user) { // NOW identify them - this links all prior anonymous activity FS('setIdentity', { uid: user.id, properties: { displayName: user.name, email: user.email, accountCreatedDuringCheckout: true, }, }) } }
INTEGRATION WITH OTHER FS APIS
Identification + User Properties
// setIdentity can include properties FS('setIdentity', { uid: user.id, properties: { displayName: user.name, email: user.email, plan: 'starter', // Initial plan at signup }, }) // Later, update properties without re-identifying // (user upgraded their plan) FS('setProperties', { type: 'user', properties: { plan: 'professional', upgradedAt: new Date().toISOString(), }, })
Identification + Events
// Identify user FS('setIdentity', { uid: user.id, properties: {displayName: user.name}, }) // Events are automatically linked to identified user FS('trackEvent', { name: 'Feature Used', properties: { featureName: 'Advanced Export', exportFormat: 'CSV', }, })
Identification + Page Properties
// User identity FS('setIdentity', { uid: user.id, properties: {displayName: user.name}, }) // Page context (separate concern) FS('setProperties', { type: 'page', properties: { pageName: 'Dashboard', dashboardView: 'analytics', }, })
TROUBLESHOOTING
Sessions Not Linking to User
Symptom: User appears anonymous despite calling setIdentity
Common Causes:
- ❌ setIdentity called after Fullstory script fully loaded
- ❌ uid is undefined, null, or empty string
- ❌ Fullstory blocked by ad blocker
- ❌ Browser privacy mode preventing storage
Solutions:
- ✅ Verify uid is a non-empty string before calling
- ✅ Check browser console for Fullstory errors
- ✅ Use async API:
await FS('setIdentityAsync', {...}) - ✅ Verify Fullstory snippet is loading correctly
Unexpected Session Splits
Symptom: User has many fragmented sessions
Common Causes:
- ❌ Calling setIdentity with different uid
- ❌ Calling setIdentity on every page without checking current identity
- ❌ Identity state not persisting across page navigations
Solutions:
- ✅ Track current identity state in your app
- ✅ Only call setIdentity when actually identifying/changing users
- ✅ Use proper logout flow with anonymize
Properties Not Appearing
Symptom: User properties don't show in Fullstory
Common Causes:
- ❌ Property values have wrong types
- ❌ Property names have invalid characters
- ❌ Hitting property limits
Solutions:
- ✅ Use schema to specify types explicitly
- ✅ Use camelCase or snake_case for property names
- ✅ Check property limits (varies by plan)
LIMITS AND CONSTRAINTS
UID Requirements
- Maximum 256 characters
- Must be a non-empty string
- Should be stable and unique per user
- Should NOT be PII (don't use email, phone, etc.)
Property Limits
- Check your Fullstory plan for specific limits
- Property names: alphanumeric, underscores, hyphens
- High cardinality properties may be limited
Call Frequency
- Sustained: 30 calls per page per minute
- Burst: 10 calls per second
- Exceeding limits may result in dropped calls
KEY TAKEAWAYS FOR AGENT
When helping developers implement User Identification:
-
Always emphasize:
- Use stable database IDs as uid, never PII
- Call setIdentity AFTER successful authentication
- Include displayName and email as properties
- You cannot change identity without anonymizing first
-
Common mistakes to watch for:
- Using email as uid
- Identifying before auth completes
- Calling setIdentity excessively
- Trying to update identity instead of using setProperties
- Missing displayName/email properties
-
Questions to ask developers:
- What's your unique user identifier? (database ID, UUID)
- When does authentication complete in your flow?
- Do users switch between accounts in your app?
- What user attributes are important for segmentation?
-
Integration considerations:
- SPA: Ensure identification survives client-side navigation
- SSR: Call on both server hydration and client-side auth changes
- OAuth: Identify in the callback, before redirects
- Mobile web: Handle app-to-browser identity handoffs
REFERENCE LINKS
- Identify Users: https://developer.fullstory.com/browser/identification/identify-users/
- Anonymize Users: https://developer.fullstory.com/browser/identification/anonymize-users/
- Set User Properties: https://developer.fullstory.com/browser/identification/set-user-properties/
- Custom Properties: https://developer.fullstory.com/browser/custom-properties/
- Help Center - Identifying Users: https://help.fullstory.com/hc/en-us/articles/360020623294-Identifying-users
This skill document was created to help Agent understand and guide developers in implementing Fullstory's User Identification API correctly for web applications.