Why browser connectivity events aren’t enough
Listening for the browser’s online and offline events and firing analytics from those listeners seems like the obvious way to measure offline usage. Every major browser supports these events, but relying on them has serious limitations:
- Connectivity events can flicker for a split second during a brief network blip that the user never perceives. Logging every one of those transitions collects noise and is hard to justify from a privacy perspective.
- Analytics pings sent while offline never reach the server. You’d have to store a local timestamp and replay the activity when connectivity returns, which only works if the user comes back to the site.
- The
onlineevent indicates network access, not actual internet connectivity. A user can be “online” while your tracking request still fails. - Even if the user stays on the page while offline, other analytics events—scrolls, clicks, and so on—are also lost. Those might be the more valuable signals.
- Merely knowing a user was offline doesn’t tell you what broke. In single-page applications (SPAs), a dropped connection rarely produces a browser error page. Instead, dynamic parts of the page fail silently, which is far more useful to measure.
You can use this approach for a coarse picture of offline behavior, but the blind spots are substantial, especially around measuring the drop-off that happens when a user encounters a broken offline experience and never returns.
Track from the service worker instead
The service worker that enables offline functionality is also a better place to track it. You can buffer analytics pings in IndexedDB while the user is offline and replay them when the connection is restored. For Google Analytics, the Workbox module workbox-google-analytics already implements this pattern. Note that hits deferred longer than four hours may not be processed by Google Analytics.
In a Workbox-based service worker, enabling this is a two-line change:
import * as googleAnalytics from 'workbox-google-analytics';
googleAnalytics.initialize();
That replays existing pageview and event pings, but the replay is indistinguishable from traffic that never went offline. You can mark the deferred hits with a custom dimension to separate online from offline interactions:
import * as googleAnalytics from 'workbox-google-analytics';
googleAnalytics.initialize({
parameterOverrides: {
customDimension1: 'offline',
},
});
If the user closes the tab while offline, the service worker normally goes to sleep and can’t flush the queue. The Workbox Google Analytics module avoids this by relying on the Background Sync API, which delivers the buffered data when the connection returns even after the browser or tab is closed.
This approach does have an inherent chicken-and-egg problem: until you have at least a basic offline experience, users won’t linger on your site without a connection, so you won’t see much offline data. But once instrumentation is in place, you can compare session length and engagement for users who carry the offline dimension versus your regular users, which quantifies how much those offline drop-offs are costing you.
Failed loads in SPAs and lazy-loaded pages
On a conventional multi-page site, a user who tries to navigate while offline gets the browser’s default error page, which at least communicates what’s wrong. SPAs behave differently: the user stays on the same document while new content is fetched via AJAX. There’s no navigation, no browser error page—just components that render with errors, go stale, or stop responding. The same kind of silent breakage appears on multi-page sites that lazy-load assets: the initial document loads online, but the user goes offline before scrolling, and everything below the fold quietly fails.
Because these failures are invisible to users, they’re worth tracking. A service worker can catch network errors at the source. Workbox’s global catch handler can notify the page that a request failed by sending a message event:
import { setCatchHandler } from 'workbox-routing';
setCatchHandler(({ event }) => {
// https://developer.mozilla.org/docs/Web/API/Client/postMessage
event.waitUntil(async function () {
// Exit early if we don't have access to the client.
// Eg, if it's cross-origin.
if (!event.clientId) return;
// Get the client.
const client = await clients.get(event.clientId);
// Exit early if we don't get the client.
// Eg, if it closed.
if (!client) return;
// Send a message to the client.
client.postMessage({
action: "network_fail",
url: event.request.url,
destination: event.request.destination
});
return Response.error();
}());
});
You might only care about failures on particular routes, say anything under /products/*. That can be filtered inside setCatchHandler with a regular expression against the URI. Or, for cleaner separation as the service worker grows, implement registerRoute with a custom handler that encapsulates the error logic in its own route:
import { registerRoute } from 'workbox-routing';
import { NetworkOnly } from 'workbox-strategies';
const networkOnly = new NetworkOnly();
registerRoute(
new RegExp('https:\/\/example\.com\/products\/.+'),
async (params) => {
try {
// Attempt a network request.
return await networkOnly.handle(params);
} catch (error) {
// If it fails, report the error.
const event = params.event;
if (!event.clientId) return;
const client = await clients.get(event.clientId);
if (!client) return;
client.postMessage({
action: "network_fail",
url: event.request.url,
destination: "products"
});
return Response.error();
}
}
);
The page then listens for the message event and sends the analytics ping. As usual, buffer those requests in the service worker while the user is offline, which the workbox-google-analytics plugin handles for Google Analytics. This example is Google Analytics-specific, but the pattern carries over to other analytics providers:
if ("serviceWorker" in navigator) {
// ... SW registration here
// track offline error events
navigator.serviceWorker.addEventListener("message", event => {
if (gtag && event.data && event.data.action === "network_fail") {
gtag("event", "network_fail", {
event_category: event.data.destination,
// event_label: event.data.url,
// value: event.data.value
});
}
});
}
Captured failed loads then feed into your analytics reporting, where they can guide cache coverage and general error handling to make the site more dependable on flaky connections.
Start tracking, then improve offline coverage
Choosing an offline tracking strategy comes down to a trade-off between complexity and completeness. No offline experience means minimal offline usage to measure, so put the full instrumentation in place first, then extend offline capabilities incrementally. A sensible first step is an offline error page, which is easy to build with Workbox and is a UX best practice comparable to a custom 404. From there, move toward more advanced offline fallbacks and eventually real offline content. Explain the available offline behavior to users, and usage will trend upward—everyone loses connectivity sometimes.



