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:
- Repository: View the code on GitHub
- Live Site: Check out the SaaS
Hopefully my experience with users and their desire to open tabs and jump around like jack rabbits help.
Cheers
답글 남기기
댓글을 달기 위해서는 로그인해야합니다.