fullstory-privacy-strategy
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. Covers regulatory compliance (GDPR, HIPAA, PCI, CCPA), privacy-by-design principles, and balancing analytics value with privacy protection. Acts as a decision framework that references core privacy skills.
$ Installer
git clone https://github.com/fullstorydev/fs-skills /tmp/fs-skills && cp -r /tmp/fs-skills/meta/fullstory-privacy-strategy ~/.claude/skills/fs-skills// tip: Run this command in your terminal to install the skill
name: fullstory-privacy-strategy version: v2 description: 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. Covers regulatory compliance (GDPR, HIPAA, PCI, CCPA), privacy-by-design principles, and balancing analytics value with privacy protection. Acts as a decision framework that references core privacy skills. related_skills:
- fullstory-privacy-controls
- fullstory-user-consent
- fullstory-identify-users
- fullstory-user-properties
- fullstory-analytics-events
- fullstory-banking
- fullstory-healthcare
- fullstory-gaming
- fullstory-ecommerce
- fullstory-saas
- fullstory-travel
- fullstory-media-entertainment
Fullstory Privacy Strategy
Overview
This guide provides strategic guidance for implementing privacy-conscious Fullstory semantic decoration. It helps developers decide:
- What data to send to Fullstory
- What data to never send
- What data to hash/encrypt before sending
- When to use joinable keys instead of raw data
- How to balance analytics value with privacy protection
Remember: Fullstory is a tool for understanding user experience, NOT a database for storing customer data. Send only what's needed for analysis.
Fullstory's Privacy Architecture
First-Party Cookies (Not Third-Party)
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 Transparency
| 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
Private by Default Mode
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:
- All text is masked by default - No text captured unless explicitly unmasked
- Zero accidental exposure - Impossible to accidentally capture sensitive data
- Selective unmasking - Add
.fs-unmaskto navigation, buttons, product names - Session replay shows wireframes - See user behavior without seeing data
Recommended for:
- โ Healthcare applications (HIPAA)
- โ Banking/financial services (PCI, GLBA)
- โ Multi-tenant SaaS (customer data protection)
- โ Enterprise applications
- โ Any application where "default open" is too risky
Enable via:
- New accounts: Select during onboarding wizard
- Existing accounts: Contact Fullstory Support
Reference: Fullstory Private by Default
Core Privacy Principles
1. Minimum Necessary Data
"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 |
2. Privacy by Design
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 โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
3. Data Classification Framework
| 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 |
Decision Matrix: What Data to Send
User Identification
| 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" }) |
User Properties
| 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 |
Event Properties
| 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 |
Joinable Keys Strategy
Instead of sending sensitive data to Fullstory, send an identifier that can be joined in your analytics warehouse:
The Pattern
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) โ
โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
Implementation Example
// 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
}
});
Joinable Keys for Common Systems
| 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 |
Hashing Strategy
When you need to identify users but can't use internal IDs:
When to Hash
| 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? |
Hash Implementation
โ ๏ธ 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..."
});
Important Considerations
- Server-side hashing is preferred: Hash on backend, send hash to client
- Consistent hashing: Always lowercase, trim whitespace before hashing
- Salt optional for Fullstory: Unsalted hashes allow you to join later
- Document mapping: Keep record of hash โ original for support lookups
- Not a security measure: Hashing prevents accidental exposure, not malicious extraction
Regulatory Compliance Guide
GDPR (Europe)
| 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
}
});
}
}
HIPAA (Healthcare - USA)
| 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 โ
}
});
PCI DSS (Payment Cards)
| 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 โ
}
});
CCPA (California - USA)
| 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 |
Data Category Decision Trees
User Identification Decision Tree
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 โโโ
Property Decision Tree
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 โ
Common Patterns by Data Type
Names
// 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)
Email Addresses
// 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"
}
});
Monetary Values
// 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
}
});
Dates
// 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)
Locations
// 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
Privacy Audit Checklist
Use this checklist before launch and periodically:
Session Replay Audit
- Watch 10 random replays from each user type
- Check all form fields for proper masking/exclusion
- Verify error messages don't expose PII
- Check modals and pop-ups for sensitive content
- Review any user-generated content areas
- Verify third-party widgets are properly handled
Properties Audit
- List all
setIdentitycalls and their values - List all
setPropertiescalls (user and page) - List all
trackEventcalls and their properties - For each: Is this data necessary? Is it minimized?
- Verify no regulated data in properties
- Check that joinable keys are used where appropriate
Console/Network Audit
- Check if console capture is enabled
- If yes, verify no PII in console logs
- Review network request capture settings
- Verify sensitive APIs are excluded
Compliance Verification
- Consent mechanism implemented (if required)
- Deletion API integrated (if required)
- Privacy policy updated
- Team trained on privacy requirements
KEY TAKEAWAYS FOR AGENT
When helping developers with privacy strategy:
- Default to privacy: When in doubt, don't send it
- Joinable keys are your friend: Send IDs, join PII in warehouse
- Hash when needed: For cross-system linking without raw PII
- Know the regulations: GDPR, HIPAA, PCI, CCPA have specific requirements
- Audit regularly: Privacy leaks happen with new features
Questions to Ask Developers
- "What regulations apply to your business?"
- "Can you use an internal ID instead of email?"
- "Is this data necessary for understanding UX?"
- "Can you join this data in your warehouse instead?"
- "Have you tested session replay for PII exposure?"
Common Mistakes to Watch For
- Sending email as uid when internal ID exists
- Including PII in event properties "for debugging"
- Not masking dynamically loaded content
- Forgetting about console log capture
- Assuming auto-detection catches everything
REFERENCE LINKS
Core Skills
- Privacy Controls - Technical implementation
- User Consent - Consent API
- Identify Users - User identification
Fullstory Documentation
- Privacy Overview: https://help.fullstory.com/hc/en-us/articles/360020623574
- Private by Default: https://help.fullstory.com/hc/en-us/articles/360044349073
- Data Deletion API: https://developer.fullstory.com/deletion
- GDPR Compliance: https://www.fullstory.com/legal/gdpr
This skill document provides strategic guidance for privacy-conscious Fullstory implementations. Always consult your legal and compliance teams for specific regulatory requirements.
Repository
