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.

$ 安裝

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 AspectFullstory Approach
Cookie DomainSet on YOUR domain (e.g., yoursite.com), not Fullstory's
Cross-Site Tracking❌ Impossible - each site has its own isolated fs_uid cookie
Data IsolationYour user data is completely isolated from other Fullstory customers
Browser Compatibility✅ First-party cookies aren't blocked by browsers or ad-blockers
User ControlUsers 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

CookieDurationPurposeUser Impact
fs_uid1 yearLinks sessions from same browserCan clear to "start fresh"
fs_cid1 yearStores consent stateRemembers consent choice
fs_lua30 minLast activity timestampSession 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:

ModeDefault BehaviorBest For
StandardCapture everything, add fs-mask/fs-exclude to protectLow-sensitivity sites
Private by DefaultMask everything, add fs-unmask to revealHigh-sensitivity applications

How Private by Default Works:

  1. All text is masked by default - No text captured unless explicitly unmasked
  2. Zero accidental exposure - Impossible to accidentally capture sensitive data
  3. Selective unmasking - Add .fs-unmask to navigation, buttons, product names
  4. 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 CategoryAsk YourselfRecommendation
User identifiersDo I need to link sessions?Use hashed/internal IDs
NamesDo I need the actual name?Mask or use initials
EmailsIs email essential for lookup?Hash or use user ID
AddressesDo I need full address?Mask street, keep city/state
FinancialDo I need actual amounts?Use ranges ("$100-$500")
HealthIs 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

ClassificationExamplesFullstory Handling
PublicProduct names, prices, UI textUnmask
InternalOrder IDs, session IDsSend as-is or hash
ConfidentialNames, emails, addressesMask or hash
RestrictedSSN, credit cards, passwordsExclude always
RegulatedHealth data, financial dataExclude + comply with regulations

Decision Matrix: What Data to Send

User Identification

ApproachWhen to UseExample
Internal IDAlways preferredsetIdentity({ uid: "user_12345" })
Hashed emailNeed email linkagesetIdentity({ uid: sha256(email) })
Raw emailOnly if required for supportsetIdentity({ uid: "user@example.com" })
Joinable keyLink to external systemsetIdentity({ uid: "cust_abc123" })

User Properties

Data TypeSend Raw?Hash?Joinable Key?Exclude?
Account ID
Account tier (Gold/Silver)
Company name⚠️ Considercompany_id
User's full nameuser_id⚠️ Mask
Emailuser_id
Phone❌ Don't send
Address❌ Don't send
SSN/Tax ID❌ Never
Account balance⚠️ Ranges
Credit score❌ Never

Event Properties

Data TypeRecommendationExample
Product IDSend raw{ product_id: "SKU-123" }
Product nameSend raw{ product_name: "Wireless Headphones" }
PriceSend raw{ price: 199.99 }
QuantitySend raw{ quantity: 2 }
Order IDSend raw{ order_id: "ORD-789" }
Search query⚠️ ConsiderMay contain PII
Error message⚠️ SanitizeMay contain PII
User-generated content⚠️ ConsiderComments, 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

SystemKey to SendJoin In Warehouse
Salesforcesalesforce_account_idAccount, Contact objects
HubSpothubspot_contact_idContact properties
Zendeskzendesk_user_idUser tickets, interactions
Stripestripe_customer_idPayment, subscription data
Segmentsegment_user_idFull user profile
Internal DBuser_id, account_idAll internal data

Hashing Strategy

When you need to identify users but can't use internal IDs:

When to Hash

ScenarioRecommendation
Need email for session linkingHash email
Multiple systems use email as keyHash email
Legal requires pseudonymizationHash all PII
Support needs to find userConsider: 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

  1. Server-side hashing is preferred: Hash on backend, send hash to client
  2. Consistent hashing: Always lowercase, trim whitespace before hashing
  3. Salt optional for Fullstory: Unsalted hashes allow you to join later
  4. Document mapping: Keep record of hash → original for support lookups
  5. Not a security measure: Hashing prevents accidental exposure, not malicious extraction

Regulatory Compliance Guide

GDPR (Europe)

RequirementFullstory Approach
Consent before processingUse FS('setIdentity', { consent: true/false })
Right to be forgottenUse Fullstory's deletion API
Data minimizationUse joinable keys, not raw PII
Purpose limitationOnly capture UX-relevant data
PseudonymizationHash 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)

RequirementFullstory Approach
PHI protectionExclude ALL health-related content
Minimum necessaryCapture only UX metrics
Audit trailUse Fullstory's audit logs
BAA requiredEnsure 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 TypeAllowed 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)

RequirementFullstory Approach
Right to knowDocument what Fullstory captures
Right to deleteUse Fullstory's deletion API
Right to opt-outUse consent API
Non-discriminationSame 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 setIdentity calls and their values
  • List all setProperties calls (user and page)
  • List all trackEvent calls 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:

  1. Default to privacy: When in doubt, don't send it
  2. Joinable keys are your friend: Send IDs, join PII in warehouse
  3. Hash when needed: For cross-system linking without raw PII
  4. Know the regulations: GDPR, HIPAA, PCI, CCPA have specific requirements
  5. Audit regularly: Privacy leaks happen with new features

Questions to Ask Developers

  1. "What regulations apply to your business?"
  2. "Can you use an internal ID instead of email?"
  3. "Is this data necessary for understanding UX?"
  4. "Can you join this data in your warehouse instead?"
  5. "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

Fullstory Documentation


This skill document provides strategic guidance for privacy-conscious Fullstory implementations. Always consult your legal and compliance teams for specific regulatory requirements.