DEV Community

Cover image for Behavioral Analysis: The Challenge of the Tab
Anthony Yerhot
Anthony Yerhot

Posted on

Behavioral Analysis: The Challenge of the Tab

Currently working on a Saas that reads a user’s behavior on a site. The goal is keeping it lightweight and privacy centered.

All interactions are recorded in the browser. When the browser hides the page, a compact summary is calculated and sent to a cloudflare worker for insertion into a D1 DB.

The exit events are strictly bound using this function.

// Bind exit triggers strictly once
if (this.config.endpoint && !this._hasSetupTriggers) {
    const handleExit = (event) => {
        if (this._hasSent) return; 
        if (document.visibilityState === 'hidden' || event.type === 'pagehide' || event.type === 'beforeunload') {
            this._hasSent = true;
            this.forceSend();
        }
    };
    document.addEventListener('visibilitychange', handleExit);
    window.addEventListener('pagehide', handleExit);
    window.addEventListener('beforeunload', handleExit);
    this._hasSetupTriggers = true;
}

Enter fullscreen mode Exit fullscreen mode

2 attempts are made to send.

1st

const success = navigator.sendBeacon(this.config.endpoint, payload);

Enter fullscreen mode Exit fullscreen mode

And 2nd fallback

if (!success) {
    fetch(this.config.endpoint, { method: 'POST', body: payload, keepalive: true }).catch(e => console.error(e));
}

Enter fullscreen mode Exit fullscreen mode

But what about if the user returns to the tab?

The function keeps recording when they return. But it won't fire again because of the way I set up handleExit.

const handleExit = (event) => {
    if (this._hasSent) return; // <-- THE LOCK
    if (document.visibilityState === 'hidden' || event.type === 'pagehide' || event.type === 'beforeunload') {
        this._hasSent = true;      // <-- LOCKING IT
        this.forceSend();
    }
};

Enter fullscreen mode Exit fullscreen mode

Because it locks hasSent to true I can't fire another beacon for the session.

I need to turn this._hasSent = false when they return so I can fire again.

const handleVisibilityChange = (event) => {
    // 1. User hides the tab, navigates away, or closes the browser
    if (document.visibilityState === 'hidden' || event.type === 'pagehide' || event.type === 'beforeunload') {
        if (this._hasSent) return; // Prevent duplicate immediate fires
        this._hasSent = true;      // Lock the beacon
        this.forceSend();          // Fire navigator.sendBeacon
    } 
    // 2. User returns to the tab
    else if (document.visibilityState === 'visible') {
        this._hasSent = false;     // Unlock the beacon so it can fire again later
    }
};

document.addEventListener('visibilitychange', handleVisibilityChange);
window.addEventListener('pagehide', handleVisibilityChange);
window.addEventListener('beforeunload', handleVisibilityChange);

Enter fullscreen mode Exit fullscreen mode

But this creates another problem, multiple writes for the same session ID (primary key).

I use write or replace for duplicate session IDs. This takes the latest data and scrubs the old.

// --- 7. DATA INGESTION (shape_db) ---
// Force the Project ID to be the securely retrieved domain_whitelist.
const permanentProjectId = authData.domain_whitelist || 'unknown_domain';

// UPDATED: INSERT OR REPLACE handles tab-switching updates for the same sessionId
await env.DB.prepare(`
    INSERT OR REPLACE INTO sessions (
        id, project_id, schema_version, url, timestamp, 
        session_duration_ms, event_count, engine_version, tune_version, device, metrics, shapes
    ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
    data.sessionId, 
    permanentProjectId, 
    data.payloadSchema,                 
    data.url || '',
    data.timestamp || new Date().toISOString(),
    data.sessionDurationMs,
    data.eventCount || 0,
    data.engineVersion || '',
    data.tuneVersion || '',
    data.device || 'unknown',
    JSON.stringify(data.metrics, null, 2), 
    JSON.stringify(data.shapes, null, 2)
).run();

Enter fullscreen mode Exit fullscreen mode

Full project here:

Hopefully my experience with users and their desire to open tabs and jump around like jack rabbits help.

Cheers

Top comments (0)