Skip to content

[Detail Bug] Analytics: events not flushed on page hide/visibility change; final flush drops queued events beyond batch size #165

@detail-app

Description

@detail-app

Detail Bug Report

https://app.detail.dev/org_ea7bf3e3-a2f4-4402-9351-baa0e1eaa1f5/bugs/bug_5d8903f7-f47c-4bb0-b0be-e0f0a2911dc5

Summary

  • Context: The EventQueue class is responsible for buffering analytics events and flushing them to the API either when a threshold is reached, a timer fires, or the user leaves the page.
  • Bug: The onPageLeave method incorrectly inverts the isAccessible logic for pagehide and visibilitychange events, and the flush method only processes a limited number of events, leading to significant data loss when users navigate away or close tabs (especially on mobile).
  • Actual vs. expected: Currently, when the page is hidden (e.g., visibilityState === "hidden"), isAccessible is set to true, which prevents the flush from occurring. Additionally, flush only takes up to flushAt (default 20) items from the queue, even if more are present.
  • Impact: Permanent loss of analytics data for events occurring at the end of a session or when switching tabs, particularly affecting mobile users where beforeunload is unreliable.

Code with Bug

// src/queue/EventQueue.ts

document.addEventListener("pagehide", () => {
  isAccessible = document.visibilityState === "hidden"; // <-- BUG 🔴 Inverts meaning: true when page is hidden
  handleOnLeave();
});

document.addEventListener("visibilitychange", () => {
  isAccessible = true; // <-- BUG 🔴 Always true, preventing flush in consumer check
  if (document.visibilityState === "hidden") {
    handleOnLeave();
  } else {
    // ...
  }
});

// And the consumer:
this.onPageLeave(async (isAccessible: boolean) => {
  if (isAccessible === false) { // <-- BUG 🔴 Flush is skipped when isAccessible incorrectly stays true
    await this.flush();
  }
});

// Also in flush:
const items = this.queue.splice(0, this.flushAt); // <-- BUG 🔴 Only flushes first N items; remainder can be lost on page leave

Explanation

  • For pagehide/visibilitychange when the document becomes hidden, isAccessible is incorrectly set to true. The page-leave consumer only flushes when isAccessible === false, so the final flush() is skipped precisely when the user is leaving/hiding the page.
  • Even when flush() does run on page leave, it only removes up to flushAt items from the queue. Because the page-leave flow calls flush() once, any queued events beyond the first batch remain in memory and are lost when the page unloads.

Recommended Fix

  • Correct the isAccessible logic for pagehide and visibilitychange so it reflects whether the page is still accessible.
  • On page-leave/manual flush, drain the entire queue (or iterate until empty) rather than splicing only flushAt items.
// src/queue/EventQueue.ts

document.addEventListener("pagehide", () => {
  isAccessible = document.visibilityState !== "hidden"; // <-- FIX 🟢
  handleOnLeave();
});

async flush(callback?: (...args: any) => void) {
  // ...
  const items = this.queue.splice(0, this.queue.length); // <-- FIX 🟢 drain all items on flush
  // ...
}

History

This bug was introduced in commit 39c461f. This commit implemented the initial version of the EventQueue and its flush logic, which contained several behavioral errors including inverted page-leave detection and capped event flushing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions