Skip to content

Instantly share code, notes, and snippets.

@azizpunjani
Created February 4, 2026 18:30
Show Gist options
  • Select an option

  • Save azizpunjani/9d96fe3ee2d98b6c207b8c6c6149f112 to your computer and use it in GitHub Desktop.

Select an option

Save azizpunjani/9d96fe3ee2d98b6c207b8c6c6149f112 to your computer and use it in GitHub Desktop.
PR 66718 Analysis: UAT Send Activities on Visibility Change

PR 66718 Analysis: UAT Send Activities on Visibility Change

PR: https://github.com/highspot/nutella/pull/66718
JIRA: HS-155885
Feature Flag: uat_improvements


1. How the system currently works (before changes)

  • Activities are saved to localStorage during the session
  • A timer periodically uploads activities via $.ajax if they're "too old"
  • On beforeunload, we save to localStorage but do not upload
  • When the page unloads, timers stop, data stays in localStorage

2. What problem does it have?

When users leave the page, data is saved locally but not uploaded.

  • If user never returns → data never sent
  • Backlog builds up → "large upload list" warnings (100+ keys)
  • Analytics data is delayed or lost

3. What we actually want

When the user leaves the page:

  1. Save current activity to storage (already happens)
  2. Upload pending activities immediately before the page closes

4. Simplest way + feature flag

Handler (UserActivityHandler.js)

const useVisibilityUpload = currentUser.features?.includes('uat_improvements');

if (useVisibilityUpload) {
  document.addEventListener('visibilitychange', () => {
    if (document.hidden) {
      this.saveActivity();
      this.uploader.uploadWithBeacon();
    }
  });
} else {
  $(window).on('beforeunload', () => {
    this.saveActivity();
  });
}

Uploader (UserActivityUploader.js)

uploadWithBeacon() {
  const keys = this.handler.storage.loadActivityKeys();
  if (!keys?.length) return;

  const BEACON_LIMIT = 60000;
  const url = Api.userActivity();

  for (const key of keys) {
    const payload = this.createPayload([key]);
    if (!payload || !Object.keys(payload).length) continue;

    const payloadString = JSON.stringify(payload);
    if (new Blob([payloadString]).size > BEACON_LIMIT) continue; // Skip oversized

    if (navigator.sendBeacon?.(url, new Blob([payloadString], { type: 'application/json' }))) {
      this.handler.storage.removeKeys([key]);
    }
  }
}

Why this approach?

Decision Reasoning
visibilitychange over beforeunload Fires earlier, gives time to act
sendBeacon over $.ajax Designed for page unload, reliable
New method over modifying uploadIfTooOld Keeps existing code unchanged
One key at a time Simple, no batch logic, skips oversized keys
One feature flag Easy to test and rollback

5. How does the PR compare?

Aspect Simplest Approach PR
Event visibilitychange visibilitychange
Upload sendBeacon sendBeacon
Code structure New uploadWithBeacon() Modifies uploadIfTooOld() with options object
Large payloads Send one key at a time, skip if too large Recursive splitting + AJAX fallback
Lines changed ~30 661

PR strengths

  • Handles edge cases (recursive splitting, AJAX fallback)
  • Comprehensive test coverage

PR concerns

  • uploadIfTooOld becomes overloaded (accepts number OR object)
  • AJAX fallback during unload is unreliable anyway
  • More complex than needed if oversized payloads are rare

Verdict

PR achieves the goal but with more complexity. The simpler approach covers 99% of cases with ~5% of the code.


Open Questions

  1. Does the backend deduplicate on sessionId + activityNumber? (Affects whether we should keep keys after beacon)
  2. How often are payloads > 60KB? (Determines if recursive splitting is necessary)

Last updated: Feb 4, 2026

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment