Strategic guide for data privacy in Fullstory implementations. Teaches developers how to decide what data to send, mask, exclude, hash, or replace with joinable keys...
This guide provides strategic guidance for implementing privacy-conscious Fullstory semantic decoration. It helps developers decide:
Remember: Fullstory is a tool for understanding user experience, NOT a database for storing customer data. Send only what's needed for analysis.
Fullstory uses first-party cookies set on YOUR domain, which provides inherent privacy benefits:
| Privacy Aspect | Fullstory Approach |
|---|---|
| Cookie Domain | Set on YOUR domain (e.g., yoursite.com), not Fullstory's |
| Cross-Site Tracking | β Impossible - each site has its own isolated fs_uid cookie |
| Data Isolation | Your user data is completely isolated from other Fullstory customers |
| Browser Compatibility | β First-party cookies aren't blocked by browsers or ad-blockers |
| User Control | Users can clear cookies to reset their identity on your site |
Key Privacy Guarantee: A user's identity CANNOT be connected across different sites using Fullstory. If the same person visits Site A and Site B (both using Fullstory), they have separate, unlinked identities on each site.
| Cookie | Duration | Purpose | User Impact |
|---|---|---|---|
fs_uid |
1 year | Links sessions from same browser | Can clear to "start fresh" |
fs_cid |
1 year | Stores consent state | Remembers consent choice |
fs_lua |
30 min | Last activity timestamp | Session timeout management |
Reference: Why Fullstory uses First-Party Cookies
Fullstory offers a Private by Default modeβa privacy-first capture approach that inverts the default behavior:
| Mode | Default Behavior | Best For |
|---|---|---|
| Standard | Capture everything, add fs-mask/fs-exclude to protect | Low-sensitivity sites |
| Private by Default | Mask everything, add fs-unmask to reveal | High-sensitivity applications |
How Private by Default Works:
.fs-unmask to navigation, buttons, product namesRecommended for:
Enable via:
Reference: Fullstory Private by Default
"Capture only what you need to understand the user experience."
| Data Category | Ask Yourself | Recommendation |
|---|---|---|
| User identifiers | Do I need to link sessions? | Use hashed/internal IDs |
| Names | Do I need the actual name? | Mask or use initials |
| Emails | Is email essential for lookup? | Hash or use user ID |
| Addresses | Do I need full address? | Mask street, keep city/state |
| Financial | Do I need actual amounts? | Use ranges ("$100-$500") |
| Health | Is this needed at all? | Usually NO - exclude entirely |
Build privacy into your semantic decoration from the start, not as an afterthought:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β PHASE 1: DESIGN β
β - Identify all data fields β
β - Classify by sensitivity β
β - Determine minimum needed for analysis β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PHASE 2: IMPLEMENT β
β - Apply privacy controls (exclude/mask/unmask) β
β - Hash or tokenize identifiers β
β - Use joinable keys for linkage β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PHASE 3: VERIFY β
β - Review session replays β
β - Audit captured properties/events β
β - Test edge cases (errors, modals, dynamic content) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β PHASE 4: MAINTAIN β
β - Regular privacy audits β
β - Update for new features/fields β
β - Monitor for accidental exposure β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
| Classification | Examples | Fullstory Handling |
|---|---|---|
| Public | Product names, prices, UI text | Unmask |
| Internal | Order IDs, session IDs | Send as-is or hash |
| Confidential | Names, emails, addresses | Mask or hash |
| Restricted | SSN, credit cards, passwords | Exclude always |
| Regulated | Health data, financial data | Exclude + comply with regulations |
| Approach | When to Use | Example |
|---|---|---|
| Internal ID | Always preferred | setIdentity({ uid: "user_12345" }) |
| Hashed email | Need email linkage | setIdentity({ uid: sha256(email) }) |
| Raw email | Only if required for support | setIdentity({ uid: "user@example.com" }) |
| Joinable key | Link to external system | setIdentity({ uid: "cust_abc123" }) |
| Data Type | Send Raw? | Hash? | Joinable Key? | Exclude? |
|---|---|---|---|---|
| Account ID | β | |||
| Account tier (Gold/Silver) | β | |||
| Company name | β οΈ Consider | β
company_id |
||
| User's full name | β
user_id |
β οΈ Mask | ||
| β | β
user_id |
|||
| Phone | β Don't send | |||
| Address | β Don't send | |||
| SSN/Tax ID | β Never | |||
| Account balance | β οΈ Ranges | |||
| Credit score | β Never |
| Data Type | Recommendation | Example |
|---|---|---|
| Product ID | Send raw | { product_id: "SKU-123" } |
| Product name | Send raw | { product_name: "Wireless Headphones" } |
| Price | Send raw | { price: 199.99 } |
| Quantity | Send raw | { quantity: 2 } |
| Order ID | Send raw | { order_id: "ORD-789" } |
| Search query | β οΈ Consider | May contain PII |
| Error message | β οΈ Sanitize | May contain PII |
| User-generated content | β οΈ Consider | Comments, reviews |
Instead of sending sensitive data to Fullstory, send an identifier that can be joined in your analytics warehouse:
Fullstory Your Data Warehouse
βββββββββββββββββββ βββββββββββββββββββββββ
User Session: β user_id: "u123" β β β user_id: "u123" β
β session_events β β email: "john@co.com" β
β page_views β β name: "John Smith" β
βββββββββββββββββββ β ssn: "***-**-****" β
βββββββββββββββββββββββ
β
JOIN ON user_id
β
βββββββββββββββββββββββββββββββββββββββββββββββ
β Combined analytics with full PII context β
β (in YOUR secure data warehouse) β
βββββββββββββββββββββββββββββββββββββββββββββββ
// BAD: Sending PII directly to Fullstory
FS('setIdentity', {
uid: user.email, // PII!
displayName: user.fullName, // PII!
email: user.email, // PII!
})
FS('setProperties', {
type: 'user',
properties: {
ssn: user.ssn, // NEVER!
phone: user.phone, // PII!
address: user.address, // PII!
},
})
// GOOD: Using joinable keys
FS('setIdentity', {
uid: user.internalId, // Not PII, just an ID
})
FS('setProperties', {
type: 'user',
properties: {
// Business context, not PII
account_tier: user.tier,
signup_date: user.createdAt,
plan_type: user.subscription.plan,
// Joinable keys for your warehouse
crm_id: user.salesforceId, // Join to Salesforce
support_id: user.zendeskId, // Join to Zendesk
},
})
| System | Key to Send | Join In Warehouse |
|---|---|---|
| Salesforce | salesforce_account_id |
Account, Contact objects |
| HubSpot | hubspot_contact_id |
Contact properties |
| Zendesk | zendesk_user_id |
User tickets, interactions |
| Stripe | stripe_customer_id |
Payment, subscription data |
| Segment | segment_user_id |
Full user profile |
| Internal DB | user_id, account_id |
All internal data |
When you need to identify users but can't use internal IDs:
| Scenario | Recommendation |
|---|---|
| Need email for session linking | Hash email |
| Multiple systems use email as key | Hash email |
| Legal requires pseudonymization | Hash all PII |
| Support needs to find user | Consider: do they need Fullstory or CRM? |
β οΈ SECURITY WARNING: Client-side hashing is NOT a security control - it's only a privacy measure. JavaScript code is inspectable, and unsalted hashes are reversible via rainbow tables. True security requires server-side processing. Use hashing only to prevent accidental PII exposure in Fullstory, not to protect against determined attackers.
// PREFERRED: Hash on your backend, pass to frontend
// This keeps the hashing logic and any salt server-side
const userFromAPI = await fetchUser() // { id, hashedEmail: "5e884..." }
FS('setIdentity', {
uid: userFromAPI.hashedEmail,
})
// ---
// ALTERNATIVE: Client-side hashing (less secure, but better than raw PII)
async function hashForFullstory(value) {
const encoder = new TextEncoder()
const data = encoder.encode(value.toLowerCase().trim())
const hashBuffer = await crypto.subtle.digest('SHA-256', data)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('')
}
const hashedEmail = await hashForFullstory(user.email)
FS('setIdentity', {
uid: hashedEmail, // e.g., "5e884898da28047d..."
})
| Requirement | Fullstory Approach |
|---|---|
| Consent before processing | Use FS('setIdentity', { consent: true/false }) |
| Right to be forgotten | Use Fullstory's deletion API |
| Data minimization | Use joinable keys, not raw PII |
| Purpose limitation | Only capture UX-relevant data |
| Pseudonymization | Hash identifiers |
// GDPR-compliant identification
function identifyUserGDPR(user, hasConsent) {
FS('setIdentity', {
uid: hashEmail(user.email), // Pseudonymized
consent: hasConsent, // Explicit consent
})
if (hasConsent) {
FS('setProperties', {
type: 'user',
properties: {
// Only essential, non-PII data
account_tier: user.tier,
country: user.country, // May be needed for analysis
language: user.language,
},
})
}
}
| Requirement | Fullstory Approach |
|---|---|
| PHI protection | Exclude ALL health-related content |
| Minimum necessary | Capture only UX metrics |
| Audit trail | Use Fullstory's audit logs |
| BAA required | Ensure signed with Fullstory |
// HIPAA-compliant approach
// DO NOT capture:
// - Diagnoses, conditions
// - Medications
// - Treatment plans
// - Provider names
// - Insurance info
// - Any PHI
FS('setIdentity', {
uid: patient.internalId, // Not PHI
})
FS('setProperties', {
type: 'user',
properties: {
// OK: Non-PHI operational data
portal_type: 'patient',
preferred_language: 'en',
login_method: 'SSO',
// NOT OK - do not include:
// diagnosis: patient.conditions β
// provider: patient.doctorName β
},
})
| Data Type | Allowed in Fullstory? |
|---|---|
| Card number | β Never (auto-excluded) |
| CVV/CVC | β Never (auto-excluded) |
| Expiry date | β Never (exclude) |
| Cardholder name | β No (exclude) |
| Last 4 digits | β οΈ Maybe (for reference) |
| Card brand | β Yes (Visa, MC) |
| Transaction ID | β Yes |
| Amount | β Yes |
// PCI-compliant event tracking
FS('trackEvent', {
name: 'purchase_completed',
properties: {
// OK: Non-sensitive transaction data
order_id: order.id,
amount: order.total,
currency: order.currency,
card_brand: order.payment.cardBrand, // "Visa"
// NOT OK - do not include:
// card_number: order.payment.number β
// cvv: order.payment.cvv β
},
})
| Requirement | Fullstory Approach |
|---|---|
| Right to know | Document what Fullstory captures |
| Right to delete | Use Fullstory's deletion API |
| Right to opt-out | Use consent API |
| Non-discrimination | Same experience regardless of opt-out |
Should I send this identifier to Fullstory?
β
ββ Is it an internal ID (no PII)?
β ββ YES β Send as uid β
β
ββ Is it an email address?
β ββ Is email required for support lookup?
β β ββ YES β Consider: Can support use internal system instead?
β β β ββ YES β Send hashed email or internal ID
β β β ββ NO β Send email (document justification)
β β ββ NO β Send hashed email
β ββ Can you link via internal ID?
β ββ YES β Use internal ID instead
β
ββ Is it a phone number?
β ββ β Don't send, use internal ID β
β
ββ Is it a username?
β ββ β Could be PII, prefer internal ID
β
ββ Is it SSN/Tax ID/Government ID?
ββ β NEVER send βββ
Should I send this property to Fullstory?
β
ββ Is it regulated data (PHI, financial details)?
β ββ YES β Don't send β
β
ββ Does it contain PII?
β ββ Is PII necessary for analysis?
β β ββ YES β Can you generalize? (age range vs DOB)
β β β ββ YES β Send generalized
β β β ββ NO β Document justification, minimize
β β ββ NO β Don't send, use joinable key
β ββ Can you use a joinable key instead?
β ββ YES β Send key, join in warehouse
β
ββ Is it business context (tier, plan, industry)?
β ββ YES β Send β
β
ββ Is it behavioral (feature flags, A/B variant)?
β ββ YES β Send β
β
ββ Is it operational (version, platform)?
ββ YES β Send β
// Options for handling names
// Option 1: Don't send at all (use joinable key)
FS('setIdentity', {uid: user.id})
// Look up name in your CRM/database
// Option 2: Send only first name (if needed for greetings analysis)
FS('setProperties', {
type: 'user',
properties: {
first_name_initial: user.firstName.charAt(0),
},
})
// Option 3: Mask in UI, don't send as property
// (handled by fs-mask class in HTML)
// Options for handling emails
// Option 1: Internal ID (preferred)
FS('setIdentity', {uid: user.id})
// Option 2: Hashed email (for cross-system linking)
FS('setIdentity', {uid: sha256(user.email.toLowerCase())})
// Option 3: Email domain only (for B2B analysis)
FS('setProperties', {
type: 'user',
properties: {
email_domain: user.email.split('@')[1], // "company.com"
},
})
// Options for handling money
// Option 1: Send actual values (usually OK for transactions)
FS('trackEvent', {
name: 'purchase',
properties: {
amount: 149.99,
currency: 'USD',
},
})
// Option 2: Send ranges (for sensitive financial data)
function getAmountRange(amount) {
if (amount < 100) return '$0-$100'
if (amount < 500) return '$100-$500'
if (amount < 1000) return '$500-$1000'
return '$1000+'
}
FS('setProperties', {
type: 'user',
properties: {
account_balance_range: getAmountRange(user.balance),
},
})
// Option 3: Percentiles (for comparative analysis)
FS('setProperties', {
type: 'user',
properties: {
spending_percentile: user.spendingPercentile, // 75
},
})
// Options for handling dates
// Option 1: Full date (usually OK for non-sensitive)
FS('setProperties', {
type: 'user',
properties: {
signup_date: user.createdAt.toISOString(),
},
})
// Option 2: Relative time (for age-sensitive data)
function getAgeRange(birthDate) {
const age = calculateAge(birthDate)
if (age < 18) return 'under-18'
if (age < 25) return '18-24'
if (age < 35) return '25-34'
// etc.
}
FS('setProperties', {
type: 'user',
properties: {
age_range: getAgeRange(user.dateOfBirth),
},
})
// Option 3: Don't send DOB (link via joinable key)
// Options for handling location
// Option 1: Country/region only (usually OK)
FS('setProperties', {
type: 'user',
properties: {
country: user.address.country,
region: user.address.state,
},
})
// Option 2: City (if needed for analysis)
FS('setProperties', {
type: 'user',
properties: {
metro_area: user.address.city, // Consider privacy implications
},
})
// Option 3: Don't send address details
// Full addresses are PII - mask in UI, don't send as properties
Use this checklist before launch and periodically:
setIdentity calls and their valuessetProperties calls (user and page)trackEvent calls and their propertiesWhen helping developers with privacy strategy:
This skill document provides strategic guidance for privacy-conscious Fullstory implementations. Always consult your legal and compliance teams for specific regulatory requirements.