Skip to content

fix(upload): guard against envelope.data being null after async compress await - #1107

Open
Vitaliy Ponomarenko (ponomarenko) wants to merge 2 commits into
microsoft:masterfrom
ponomarenko:fix/upload-envelope-null-after-await
Open

fix(upload): guard against envelope.data being null after async compress await#1107
Vitaliy Ponomarenko (ponomarenko) wants to merge 2 commits into
microsoft:masterfrom
ponomarenko:fix/upload-envelope-null-after-await

Conversation

@ponomarenko

@ponomarenko Vitaliy Ponomarenko (ponomarenko) commented Jun 30, 2026

Copy link
Copy Markdown

Problem

upload() is an async function. After the await compress(payload) call, the JS event loop is yielded. During that window a page navigation can trigger clarity.stop(), which calls envelope.stop() and sets envelope.data = null.

When the microtask resumes, the very next line:

send(payload, zipped, envelope.data.sequence, last);
//            ^^^^^^^^^^^^^^^^^^^ 💥 TypeError: Cannot read properties of null (reading 'sequence')

throws an unhandled promise rejection that Angular's Zone.js surfaces as an error.

The existing guard added by #894 (if (!envelope.data) return;) is placed before the await and therefore does not cover this post-await window.

This error is reproducible on Angular SPAs with frequent route transitions where Clarity flushes data at the moment the user navigates away.

Fix

1. Add a second null guard in upload() immediately after the await:

let zipped = last ? null : await compress(payload);
// Guard: stop() may have been called while compress() was awaiting (async race condition).
// envelope.stop() sets envelope.data = null, so we must re-check before accessing envelope.data.sequence.
if (!envelope.data) { return; }
metric.sum(Metric.TotalBytes, zipped ? zipped.length : payload.length);
send(payload, zipped, envelope.data.sequence, last);

2. Add a null guard in delay() which also reads envelope.data.sequence without any prior null check:

// Before:
let gap = ... : envelope.data.sequence * config.delay;

// After:
let gap = ... : (envelope.data ? envelope.data.sequence * config.delay : Setting.MinUploadDelay);

Root cause diagram

upload() starts           stop() fires during navigation
     │                           │
     ▼                           │
if (!envelope.data) return  ←  ✅ check passes, data is set
     │                           │
     ▼                           │
await compress(payload)  ─────── yields event loop ──────► envelope.stop() → data = null
     │                                                            │
     ▼                                                            │
metric.sum(...)                                                   │
send(..., envelope.data.sequence, ...)  ◄──── data is NULL here ─┘
                 💥 TypeError

…ess await

When upload() awaits compress(payload), the JS event loop is yielded.
During that window, a page navigation can trigger clarity.stop() which
calls envelope.stop() and sets envelope.data = null. On resumption,
accessing envelope.data.sequence throws:

  TypeError: Cannot read properties of null (reading 'sequence')

The existing `if (!envelope.data) return` check is placed before the
await and therefore does not protect the post-await code path.

Fix: add a second null guard immediately after the await so we exit
cleanly when stop() has already torn down the envelope.

Also add a null guard in delay() which reads envelope.data.sequence
without any prior null check.
Copilot AI review requested due to automatic review settings June 30, 2026 11:45

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes an async race in clarity-js upload scheduling where envelope.data can become null during the await compress(payload) window (e.g., during SPA navigations that trigger clarity.stop()), preventing a TypeError from reading envelope.data.sequence.

Changes:

  • Add a post-await null guard in upload() to avoid accessing envelope.data.sequence after shutdown.
  • Add a defensive null guard in delay() to avoid reading envelope.data.sequence when envelope.data is null.

Comment thread packages/clarity-js/src/data/upload.ts Outdated
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@ponomarenko

Copy link
Copy Markdown
Author

Any updates?

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants