Skip to content

Data Persistence

Learn how to save and manage calibration data across browser sessions.

Overview

Webgazer.ts can store calibration data locally, allowing users to skip recalibration on return visits. Data is stored using IndexedDB (with localStorage fallback).

Privacy Considerations

Calibration data may contain biometric information. Always:

  • Get explicit user consent before enabling persistence
  • Provide clear privacy policies
  • Allow users to delete their data
  • Consider regulatory requirements (GDPR, CCPA, etc.)

Basic Usage

Enable Data Persistence

typescript
import webgazer from '@webgazer-ts/core';

// Enable before starting
webgazer.saveDataAcrossSessions(true);
await webgazer.begin();

// Data is now automatically saved

Check for Existing Data

typescript
// Check if calibration data exists
const hasData = await webgazer.hasStoredData();

if (hasData) {
  console.log('Found existing calibration data');
  await webgazer.begin(); // Will load stored data automatically
} else {
  console.log('No calibration data - need to calibrate');
  await runCalibration();
}

Data Management

Clear Stored Data

typescript
// Clear all calibration data
await webgazer.clearData();

// Clear and restart
await webgazer.clearData();
await webgazer.begin();
await runCalibration();

Export/Import Data

typescript
// Export calibration data
const data = await webgazer.getStoredData();
const jsonData = JSON.stringify(data);

// Save to file or send to server
const blob = new Blob([jsonData], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'webgazer-calibration.json';
a.click();

// Import calibration data
async function importCalibrationData(jsonData: string) {
  const data = JSON.parse(jsonData);
  await webgazer.setStoredData(data);
  await webgazer.begin(); // Load imported data
}

Storage Options

IndexedDB (Default)

Webgazer.ts uses IndexedDB for storage by default:

typescript
// IndexedDB provides:
// - Large storage capacity (50MB+)
// - Asynchronous operations
// - Structured data storage

localStorage (Fallback)

If IndexedDB is unavailable, localStorage is used:

typescript
// localStorage limitations:
// - 5-10MB storage limit
// - Synchronous (may block UI)
// - String-only storage

Implement proper consent before storing data:

typescript
async function requestDataPersistence(): Promise<boolean> {
  return new Promise((resolve) => {
    const modal = document.createElement('div');
    modal.innerHTML = `
      <div class="consent-modal">
        <h2>Save Your Calibration?</h2>
        <p>
          We can save your eye tracking calibration to improve 
          your experience on future visits.
        </p>
        <p>
          <strong>Your privacy:</strong>
          <ul>
            <li>Data is stored only on your device</li>
            <li>No data is sent to our servers</li>
            <li>You can delete this data anytime</li>
          </ul>
        </p>
        <div class="consent-actions">
          <button id="consent-accept">Save My Calibration</button>
          <button id="consent-decline">Don't Save</button>
        </div>
        <a href="/privacy" target="_blank">Privacy Policy</a>
      </div>
    `;
    
    document.body.appendChild(modal);
    
    modal.querySelector('#consent-accept')?.addEventListener('click', () => {
      modal.remove();
      resolve(true);
    });
    
    modal.querySelector('#consent-decline')?.addEventListener('click', () => {
      modal.remove();
      resolve(false);
    });
  });
}

// Usage
const hasConsent = await requestDataPersistence();
if (hasConsent) {
  webgazer.saveDataAcrossSessions(true);
}

Data Lifecycle

Complete Example

typescript
class CalibrationDataManager {
  private readonly CONSENT_KEY = 'webgazer_consent';
  
  async initialize() {
    // Check for existing consent
    const hasConsent = this.getConsent();
    
    if (hasConsent) {
      // User previously consented
      webgazer.saveDataAcrossSessions(true);
      
      if (await webgazer.hasStoredData()) {
        console.log('Loading saved calibration...');
        await webgazer.begin();
      } else {
        console.log('No saved data found, calibrating...');
        await this.calibrate();
      }
    } else {
      // Ask for consent
      const consent = await requestDataPersistence();
      this.setConsent(consent);
      
      if (consent) {
        webgazer.saveDataAcrossSessions(true);
      }
      
      await this.calibrate();
    }
  }
  
  private getConsent(): boolean {
    return localStorage.getItem(this.CONSENT_KEY) === 'true';
  }
  
  private setConsent(consent: boolean) {
    localStorage.setItem(this.CONSENT_KEY, String(consent));
  }
  
  async clearData() {
    await webgazer.clearData();
    this.setConsent(false);
  }
  
  private async calibrate() {
    // Your calibration logic
    await runCalibration();
  }
}

// Usage
const dataManager = new CalibrationDataManager();
await dataManager.initialize();

Multi-User Support

Handle multiple users on the same device:

typescript
class MultiUserCalibration {
  private currentUser: string | null = null;
  
  async switchUser(userId: string) {
    // Save current user's data
    if (this.currentUser) {
      const data = await webgazer.getStoredData();
      this.saveUserData(this.currentUser, data);
    }
    
    // Load new user's data
    this.currentUser = userId;
    const userData = this.loadUserData(userId);
    
    if (userData) {
      await webgazer.setStoredData(userData);
      await webgazer.begin();
    } else {
      await webgazer.begin();
      await runCalibration();
    }
  }
  
  private saveUserData(userId: string, data: any) {
    const key = `webgazer_user_${userId}`;
    localStorage.setItem(key, JSON.stringify(data));
  }
  
  private loadUserData(userId: string): any | null {
    const key = `webgazer_user_${userId}`;
    const data = localStorage.getItem(key);
    return data ? JSON.parse(data) : null;
  }
  
  async deleteUser(userId: string) {
    const key = `webgazer_user_${userId}`;
    localStorage.removeItem(key);
  }
}

Data Expiration

Auto-expire old calibration data:

typescript
class CalibrationDataWithExpiry {
  private readonly EXPIRY_DAYS = 30;
  private readonly TIMESTAMP_KEY = 'webgazer_timestamp';
  
  async checkExpiry(): Promise<boolean> {
    const timestamp = localStorage.getItem(this.TIMESTAMP_KEY);
    
    if (!timestamp) return true; // No data
    
    const savedDate = new Date(timestamp);
    const now = new Date();
    const daysDiff = (now.getTime() - savedDate.getTime()) / (1000 * 60 * 60 * 24);
    
    if (daysDiff > this.EXPIRY_DAYS) {
      console.log('Calibration data expired, clearing...');
      await webgazer.clearData();
      localStorage.removeItem(this.TIMESTAMP_KEY);
      return true;
    }
    
    return false;
  }
  
  async saveWithTimestamp() {
    webgazer.saveDataAcrossSessions(true);
    localStorage.setItem(this.TIMESTAMP_KEY, new Date().toISOString());
  }
}

Privacy Controls

User Dashboard

typescript
function createPrivacyDashboard() {
  return `
    <div class="privacy-dashboard">
      <h3>Your Eye Tracking Data</h3>
      
      <div class="data-info">
        <p><strong>Status:</strong> 
          <span id="data-status">Checking...</span>
        </p>
        <p><strong>Last Updated:</strong> 
          <span id="data-timestamp">Unknown</span>
        </p>
        <p><strong>Storage Size:</strong> 
          <span id="data-size">Calculating...</span>
        </p>
      </div>
      
      <div class="data-actions">
        <button id="export-data">Download My Data</button>
        <button id="delete-data" class="danger">Delete All Data</button>
      </div>
      
      <div class="privacy-info">
        <h4>What We Store</h4>
        <ul>
          <li>Eye tracking calibration parameters</li>
          <li>Regression model weights</li>
          <li>Configuration preferences</li>
        </ul>
        
        <h4>What We DON'T Store</h4>
        <ul>
          <li>Camera images or video</li>
          <li>Personal identifying information</li>
          <li>Browsing history</li>
        </ul>
      </div>
    </div>
  `;
}

// Implement dashboard actions
async function initPrivacyDashboard() {
  const hasData = await webgazer.hasStoredData();
  document.getElementById('data-status')!.textContent = 
    hasData ? 'Active' : 'No data stored';
  
  document.getElementById('export-data')?.addEventListener('click', async () => {
    const data = await webgazer.getStoredData();
    const blob = new Blob([JSON.stringify(data, null, 2)], 
      { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `webgazer-data-${Date.now()}.json`;
    a.click();
  });
  
  document.getElementById('delete-data')?.addEventListener('click', async () => {
    if (confirm('Delete all calibration data? You will need to recalibrate.')) {
      await webgazer.clearData();
      alert('Data deleted successfully');
      location.reload();
    }
  });
}

Best Practices

  1. Always get consent before storing data
  2. Provide clear privacy information
  3. Allow easy data deletion
  4. Implement data expiration
  5. Don't store unnecessary data
  6. Test in private/incognito mode
  7. Handle storage errors gracefully

Error Handling

typescript
try {
  webgazer.saveDataAcrossSessions(true);
  await webgazer.begin();
} catch (error) {
  if (error.name === 'QuotaExceededError') {
    console.error('Storage quota exceeded');
    // Ask user to clear old data or disable persistence
  } else if (error.name === 'SecurityError') {
    console.error('Storage blocked by browser settings');
    // Fall back to session-only mode
  } else {
    console.error('Failed to load stored data:', error);
  }
}

Next Steps

Based on Webgazer.js by Brown HCI