| name | fullstory-identify-users |
| version | v2 |
| description | 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. |
| related_skills | fullstory-anonymize-users, fullstory-user-properties, fullstory-user-consent, fullstory-async-methods, fullstory-privacy-strategy, fullstory-banking, fullstory-healthcare, fullstory-saas |
Fullstory 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
setIdentityimmediately after a successful authentication - On Page Load (Already Authenticated): Call
setIdentityon every page load if the user is logged in - After Authentication Redirects: Ensure identification persists across OAuth/SSO redirects
User Identity vs Anonymous Sessions
- Anonymous Session: Default state before
setIdentityis called. User is tracked via thefs_uidfirst-party cookie but not linked to your system. - Identified Session: After
setIdentity, the session is permanently linked to the provideduid.
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
fs_uidcookie - your customers' data is isolated to your site only.
Cookie-Based Session Linking
- Before identification: The
fs_uidcookie links all sessions from the same browser/device together (persists for 1 year unless deleted) - When
setIdentityis called: ALL previous anonymous sessions with that samefs_uidcookie are retroactively merged into the identified user - Cross-device linking: If the same
uidis used on different devices, all sessions across all devices are linked
┌─────────────────────────────────────────────────────────────────────────┐
│ 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
setIdentitywith a differentuid, Fullstory automatically splits the session into a new session - 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
setIdentityas early as possible after authentication - 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 | setIdentity |
Links session to user identity |
| Initial user properties at login | setIdentity with properties |
Convenient to include with identification |
| Update user properties later | setProperties (type: 'user') |
Don't re-identify just to update properties |
| Properties for anonymous user | setProperties (type: 'user') |
Works without identification! |
Important: The
propertiesobject insetIdentityis a convenience - you can include initial properties when identifying. However, for updating properties after identification or for anonymous users, usesetPropertieswithtype: 'user'instead. See the fullstory-user-properties skill for details.
// 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 |
|---|---|---|---|
uid |
string | Yes | A unique identifier for the user from your system. Must be stable and unique. Maximum 256 characters. |
properties |
object | No | Key/value pairs of additional user information |
schema |
object | No | Type hints for property values (see Custom Properties) |
Special Property Fields
| Field | Type | Description |
|---|---|---|
displayName |
string | Shown in session list and user card in Fullstory app |
email |
string | Enables search via HTTP API and email-based lookups |
Supported Property Types
| Type | Description | Examples |
|---|---|---|
str |
String value | "premium", "enterprise" |
strs |
Array of strings | ["admin", "beta-tester"] |
int |
Integer | 42, -5, 0 |
ints |
Array of integers | [1, 2, 3] |
real |
Float/decimal | 99.99, -3.14 |
reals |
Array of reals | [10.5, 20.0] |
bool |
Boolean | true, false |
bools |
Array of booleans | [true, false, true] |
date |
ISO8601 date | "2024-01-15T00:00:00Z" |
dates |
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.50,
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.00, // 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.