| name | fullstory-user-properties |
| version | v2 |
| description | Comprehensive guide for implementing Fullstory's User Properties API (setProperties with type 'user') for web applications. Teaches proper property naming, type handling, incremental updates, and special fields (displayName, email). Includes detailed good/bad examples for CRM integration, progressive profiling, and subscription tracking to help developers enrich user profiles for analytics and segmentation. |
| related_skills | ["fullstory-identify-users","fullstory-page-properties","fullstory-analytics-events","fullstory-data-scoping-decoration"] |
Fullstory User Properties API
Overview
Fullstory's User Properties API allows developers to capture custom user data that enriches user profiles for search, filtering, segmentation, and analytics. Unlike setIdentity which links a session to a known user ID, setProperties with type: 'user' lets you add or update attributes about any user - including anonymous users.
Important: Every new browser/device starts as an anonymous user, tracked via the fs_uid first-party cookie (1-year expiry). You can set user properties on anonymous users before they ever identify. These properties persist across sessions and transfer when/if the user later identifies via setIdentity.
Key use cases:
- Anonymous User Enrichment: Add attributes before the user logs in (referral source, landing page, visitor type)
- Progressive Profiling: Update properties as you learn more about the user
- Subscription/Plan Changes: Track plan upgrades without re-identifying
- Preference Tracking: Store user settings and preferences
- CRM Sync: Mirror key CRM fields in Fullstory
Core Concepts
setIdentity vs setProperties
| API | Purpose | When to Use | Works for Anonymous? |
|---|
setIdentity | Link session to a known user ID + optional initial properties | Login, authentication | No (converts anonymous → identified) |
setProperties (user) | Add/update properties for the current user | Anytime - works for anonymous AND identified users | Yes ✅ |
Key Distinction: Use setIdentity when you need to link a session to a known user (requires a uid). Use setProperties when you just want to add or update attributes about the current user - this works for both identified AND anonymous users.
Anonymous Users in Fullstory
Every user starts as anonymous, tracked via the fs_uid first-party cookie:
- Cookie-based identity: Fullstory sets an
fs_uid cookie (1-year expiry) that tracks the same anonymous user across sessions and page views
- Persistent across sessions: As long as the cookie exists, all sessions are linked to the same anonymous user
- Can receive user properties: Use
setProperties to add attributes to anonymous users
- Properties transfer on identification: When
setIdentity is called, ALL previous sessions (linked by the cookie) merge into the identified user
- Searchable and segmentable: Anonymous users work just like identified users in Fullstory
Reference: Why Fullstory uses First-Party Cookies
FS('setProperties', {
type: 'user',
properties: {
landing_page: '/pricing',
referral_source: 'google_ads',
campaign: 'spring_sale_2024'
}
});
FS('setIdentity', {
uid: 'user_abc123',
properties: {
displayName: 'Jane Smith',
email: 'jane@example.com'
}
});
When to Use Each
User logs in → setIdentity({ uid: "user_123", properties: { displayName: "Jane" } })
↓
User updates profile → setProperties({ type: 'user', properties: { plan: "pro" } })
↓
User upgrades plan → setProperties({ type: 'user', properties: { plan: "enterprise" } })
For anonymous users (not yet logged in):
FS('setProperties', {
type: 'user',
properties: {
visitor_type: 'returning',
referral_source: 'google_ads',
landing_page: '/pricing'
}
});
Property Persistence
- User properties persist across sessions
- Properties can be updated at any time
- New properties are added; existing properties are overwritten
- Properties cannot be deleted via the API (contact support)
Special Fields
| Field | Behavior |
|---|
displayName | Shown in session list and user card in Fullstory UI |
email | Enables email-based search and HTTP API lookups |
API Reference
Basic Syntax
FS('setProperties', {
type: 'user',
properties: object,
schema?: object
});
Parameters
| Parameter | Type | Required | Description |
|---|
type | string | Yes | Must be 'user' for user properties |
properties | object | Yes | Key/value pairs of user data |
schema | object | No | Explicit type inference for properties |
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: Post-Identification Profile Enrichment
FS('setIdentity', {
uid: user.id,
properties: {
displayName: user.name,
email: user.email
}
});
async function loadUserProfile() {
const profile = await fetchUserProfile(user.id);
FS('setProperties', {
type: 'user',
properties: {
companyName: profile.company.name,
companySize: profile.company.employeeCount,
industry: profile.company.industry,
role: profile.role,
department: profile.department,
signupSource: profile.attribution.source,
referralCode: profile.attribution.referralCode
}
});
}
Why this is good:
- ✅ Quick identification on login (doesn't block on profile load)
- ✅ Rich data added once available
- ✅ Clean separation of concerns
- ✅ Properties available for segmentation
Example 2: Subscription/Plan Updates
async function handlePlanUpgrade(newPlan) {
await processUpgrade(newPlan);
FS('setProperties', {
type: 'user',
properties: {
plan: newPlan.name,
planTier: newPlan.tier,
monthlyPrice: newPlan.price,
billingCycle: newPlan.billingCycle,
planChangedAt: new Date().toISOString(),
previousPlan: getCurrentPlan().name
},
schema: {
monthlyPrice: 'real',
planChangedAt: 'date'
}
});
FS('trackEvent', {
name: 'Plan Upgraded',
properties: {
fromPlan: getCurrentPlan().name,
toPlan: newPlan.name,
priceDifference: newPlan.price - getCurrentPlan().
}
});
}
Why this is good:
- ✅ Updates user properties without re-identification
- ✅ Tracks both current state (property) and change (event)
- ✅ Uses schema for proper type handling
- ✅ Captures before/after for analysis
Example 3: Progressive Profiling (Onboarding)
class OnboardingFlow {
completeBasicInfo(data) {
FS('setProperties', {
type: 'user',
properties: {
companyName: data.companyName,
companySize: data.companySize,
onboardingStep: 1,
onboardingStartedAt: new Date().toISOString()
},
schema: {
onboardingStep: 'int',
onboardingStartedAt: 'date'
}
});
}
completeUseCaseSelection(useCases) {
FS('setProperties', {
type: 'user',
properties: {
primaryUseCase: useCases.primary,
secondaryUseCases: useCases.secondary,
onboardingStep: 2
},
schema: {
secondaryUseCases: 'strs',
onboardingStep: 'int'
}
});
}
() {
(, {
: ,
: {
: integrations.,
: integrations..,
:
},
: {
: ,
: ,
:
}
});
}
() {
(, {
: ,
: {
: ,
: ().(),
:
},
: {
: ,
: ,
:
}
});
}
}
Why this is good:
- ✅ Builds profile incrementally
- ✅ Each step adds relevant properties
- ✅ Tracks progress via onboardingStep
- ✅ Enables segment analysis of drop-off points
Example 4: Feature Usage Tracking
class FeatureUsageTracker {
trackFeatureFirstUse(featureName) {
const propertyName = `firstUsed_${featureName}`;
FS('setProperties', {
type: 'user',
properties: {
[propertyName]: new Date().toISOString()
},
schema: {
[propertyName]: 'date'
}
});
}
updateFeatureEngagement(features) {
FS('setProperties', {
type: 'user',
properties: {
featuresUsed: features.used,
mostUsedFeature: features.mostUsed,
featureUsageScore: features.engagementScore,
lastActiveFeature: features.lastUsed,
lastFeatureUseAt: new Date().toISOString()
},
schema: {
featuresUsed: 'strs',
featureUsageScore: 'int',
lastFeatureUseAt: 'date'
}
});
}
}
const tracker = new FeatureUsageTracker();
tracker.();
tracker.({
: [, , ],
: ,
: ,
:
});
Why this is good:
- ✅ Tracks feature adoption dates
- ✅ Maintains engagement metrics
- ✅ Enables feature-based segmentation
- ✅ Supports adoption analysis
Example 5: CRM Data Sync
async function syncCRMData(userId) {
const crmData = await fetchFromCRM(userId);
FS('setProperties', {
type: 'user',
properties: {
accountOwner: crmData.owner.name,
accountStage: crmData.stage,
dealValue: crmData.opportunity.value,
closeDate: crmData.opportunity.expectedClose,
healthScore: crmData.health.score,
churnRisk: crmData.health.churnRisk,
npsScore: crmData.health.nps,
lastContactDate: crmData.lastContact,
meetingsScheduled: crmData.meetings.scheduled,
supportTicketsOpen: crmData.support.openTickets,
crmSyncedAt: new Date().toISOString()
},
: {
: ,
: ,
: ,
: ,
: ,
: ,
: ,
: ,
:
}
});
}
Why this is good:
- ✅ Bridges CRM and product analytics
- ✅ Enables sales context in session replay
- ✅ Supports health-based segmentation
- ✅ Tracks sync time for data freshness
❌ BAD IMPLEMENTATION EXAMPLES
Example 1: Using setProperties Instead of setIdentity
FS('setProperties', {
type: 'user',
properties: {
uid: user.id,
displayName: user.name,
email: user.email
}
});
Why this is bad:
- ❌ setProperties doesn't establish identity
- ❌ uid as a property doesn't link sessions
- ❌ User remains anonymous
- ❌ Misunderstanding of API purpose
CORRECTED VERSION:
FS('setIdentity', {
uid: user.id,
properties: {
displayName: user.name,
email: user.email
}
});
Example 2: Calling Before Identification
function updateUserPreferences(preferences) {
FS('setProperties', {
type: 'user',
properties: {
theme: preferences.theme,
language: preferences.language
}
});
}
Why this is bad:
- ❌ Properties on anonymous users are session-scoped
- ❌ Data won't persist across sessions
- ❌ Can't segment by these properties reliably
CORRECTED VERSION:
function updateUserPreferences(preferences) {
if (isUserIdentified()) {
FS('setProperties', {
type: 'user',
properties: {
theme: preferences.theme,
language: preferences.language
}
});
}
}
Example 3: Excessive Calls
function handleFormFieldChange(fieldName, value) {
FS('setProperties', {
type: 'user',
properties: {
[`form_${fieldName}`]: value
}
});
}
Why this is bad:
- ❌ Will hit rate limits (30/min, 10/sec)
- ❌ Wastes API calls on intermediate states
- ❌ Transient form data isn't good for user properties
CORRECTED VERSION:
function handleFormSubmit(formData) {
FS('setProperties', {
type: 'user',
properties: {
preferredContact: formData.contactMethod,
marketingOptIn: formData.optIn,
timezone: formData.timezone
}
});
FS('trackEvent', {
name: 'Preferences Updated',
properties: formData
});
}
Example 4: Wrong Type for Properties
FS('setProperties', {
properties: {
plan: 'premium'
}
});
Why this is bad:
- ❌ Missing required
type parameter
- ❌ API call will fail or behave unexpectedly
- ❌ Easy to miss in testing
CORRECTED VERSION:
FS('setProperties', {
type: 'user',
properties: {
plan: 'premium'
}
});
Example 5: Type Mismatches
FS('setProperties', {
type: 'user',
properties: {
accountBalance: '$1,234.56',
loginCount: 'forty-two',
isPremium: 'yes',
signupDate: 'January 15, 2024'
},
schema: {
accountBalance: 'real',
loginCount: 'int',
isPremium: 'bool',
signupDate: 'date'
}
});
Why this is bad:
- ❌ Values don't match declared types
- ❌ Parsing will fail
- ❌ Properties won't be queryable correctly
CORRECTED VERSION:
FS('setProperties', {
type: 'user',
properties: {
accountBalance: 1234.56,
currency: 'USD',
loginCount: 42,
isPremium: true,
signupDate: '2024-01-15T00:00:00Z'
},
schema: {
accountBalance: 'real',
loginCount: 'int',
isPremium: 'bool',
signupDate: 'date'
}
});
Example 6: Overwriting Important Properties
function updateLastActivity() {
FS('setProperties', {
type: 'user',
properties: {
displayName: 'Active User',
lastActivityAt: new Date().toISOString()
}
});
}
Why this is bad:
- ❌ Overwrites displayName with generic value
- ❌ Loses actual user name in Fullstory UI
- ❌ Makes sessions hard to identify
CORRECTED VERSION:
function updateLastActivity() {
FS('setProperties', {
type: 'user',
properties: {
lastActivityAt: new Date().toISOString(),
isRecentlyActive: true
},
schema: {
lastActivityAt: 'date',
isRecentlyActive: 'bool'
}
});
}
COMMON IMPLEMENTATION PATTERNS
Pattern 1: Property Manager Class
class UserPropertyManager {
constructor() {
this.pendingProperties = {};
this.flushTimeout = null;
}
queue(properties, schema = {}) {
Object.assign(this.pendingProperties, properties);
if (this.flushTimeout) clearTimeout(this.flushTimeout);
this.flushTimeout = setTimeout(() => this.flush(), 1000);
}
flush() {
if (Object.keys(this.pendingProperties).length === 0) return;
FS('setProperties', {
type: 'user',
properties: this.pendingProperties
});
. = {};
. = ;
}
() {
.({
: ().(),
: data.,
: data.
});
}
() {
(, {
: ,
: {
: plan.,
: plan.,
: ().()
}
});
}
}
Pattern 2: Property Sync on Page Load
async function syncUserProperties() {
const user = await getCurrentUser();
if (!user) return;
const [profile, subscription, usage] = await Promise.all([
fetchProfile(user.id),
fetchSubscription(user.id),
fetchUsageStats(user.id)
]);
FS('setProperties', {
type: 'user',
properties: {
displayName: profile.fullName,
email: profile.email,
role: profile.role,
plan: subscription.plan,
planStatus: subscription.status,
mrr: subscription.mrr,
lastLoginAt: usage.lastLogin,
totalLogins: usage.loginCount,
daysActive: usage.activeDays
},
schema: {
: ,
: ,
: ,
:
}
});
}
().(syncUserProperties);
Pattern 3: Event-Driven Property Updates
const eventPropertyMap = {
'trial_started': (event) => ({
trialStartedAt: new Date().toISOString(),
trialPlan: event.plan,
isTrialing: true
}),
'trial_converted': (event) => ({
trialConvertedAt: new Date().toISOString(),
isTrialing: false,
isPaying: true,
plan: event.plan
}),
'trial_expired': (event) => ({
trialExpiredAt: new Date().toISOString(),
isTrialing: false,
isPaying: false
}),
'feature_enabled': (event) => ({
[`feature_${event.feature}_enabled`]: true,
[`feature_${event.feature}_enabledAt`]: new Date().toISOString()
})
};
function handleBusinessEvent(eventName, eventData) {
(, {
: eventName,
: eventData
});
propertyUpdater = eventPropertyMap[eventName];
(propertyUpdater) {
(, {
: ,
: (eventData)
});
}
}
RELATIONSHIP WITH OTHER APIs
setIdentity + setProperties Workflow
FS('setIdentity', {
uid: user.id,
properties: {
displayName: user.name,
email: user.email
}
});
FS('setProperties', {
type: 'user',
properties: {
company: companyData.name,
role: roleData.title
}
});
setProperties (user) vs setProperties (page)
FS('setProperties', {
type: 'user',
properties: {
plan: 'enterprise',
accountAge: 365
}
});
FS('setProperties', {
type: 'page',
properties: {
pageName: 'Dashboard',
filters: ['active', 'recent']
}
});
setProperties vs trackEvent
FS('setProperties', {
type: 'user',
properties: {
plan: 'professional',
seats: 10
}
});
FS('trackEvent', {
name: 'Plan Upgraded',
properties: {
from: 'starter',
to: 'professional',
seatChange: 5
}
});
TROUBLESHOOTING
Properties Not Appearing
Symptom: User properties don't show in Fullstory
Common Causes:
- ❌ User not identified first
- ❌ Missing
type: 'user' parameter
- ❌ Type mismatches in values
- ❌ Rate limits exceeded
Solutions:
- ✅ Ensure setIdentity called first
- ✅ Always include
type: 'user'
- ✅ Use schema for explicit typing
- ✅ Batch updates to avoid rate limits
Properties Show Wrong Values
Symptom: Property values are incorrect or unexpected type
Common Causes:
- ❌ Value format doesn't match schema type
- ❌ Formatted strings for numeric values
- ❌ Boolean as string ("true" vs true)
Solutions:
- ✅ Use clean numeric values
- ✅ Use actual boolean types
- ✅ Format dates as ISO8601
- ✅ Specify schema explicitly
displayName Keeps Getting Overwritten
Symptom: User's display name changes unexpectedly
Common Causes:
- ❌ Multiple places setting displayName
- ❌ Automated scripts overwriting
- ❌ Race conditions in property updates
Solutions:
- ✅ Set displayName only in identification flow
- ✅ Audit all setProperties calls
- ✅ Use dedicated fields for other "name" data
LIMITS AND CONSTRAINTS
Property Limits
- Check your Fullstory plan for specific limits
- Property names: alphanumeric, underscores, hyphens
- Avoid high-cardinality properties
Call Frequency
- Sustained: 30 calls per page per minute
- Burst: 10 calls per second
Value Requirements
- Strings: Must be valid UTF-8
- Numbers: Standard JSON number format
- Dates: ISO8601 format
- Arrays: Maximum length varies by plan
KEY TAKEAWAYS FOR AGENT
When helping developers implement User Properties:
-
Always emphasize:
- User must be identified first (setIdentity)
- Include
type: 'user' parameter
- Use schema for non-string types
- Batch updates to respect rate limits
-
Common mistakes to watch for:
- Missing type parameter
- Setting properties before identification
- Excessive call frequency
- Type mismatches in values
- Overwriting displayName accidentally
-
Questions to ask developers:
- Will the user be anonymous or identified? (Both work - setProperties doesn't require identification)
- How often will these properties be updated?
- What data types are these values?
- Do you need to track the change as an event too?
-
Best practices to recommend:
- Set core properties in setIdentity
- Use setProperties for subsequent updates
- Track important changes as events too
- Consider property batching for frequent updates
REFERENCE LINKS
This skill document was created to help Agent understand and guide developers in implementing Fullstory's User Properties API correctly for web applications.