How we use Google Analytics without breaking our promise
OmniViewer promises one thing above all: your file never leaves your browser. So how can we also run Google Analytics? Because the two never touch. Analytics here counts which page you landed on — / vs /mp3 vs /json — and literally nothing else. Not the file. Not its name. Not its bytes. Here is exactly how, in code.
The promise, and the tension
OmniViewer reads your file directly in the browser. Drop a 10 GB log, a private JSON export, an MP3 — the page reads only the bytes it needs to paint the screen, and those bytes are never sent anywhere. That is the whole product.
Analytics is normally the moment a privacy promise quietly dies. The stock Google Analytics snippet reports page_location — the full URL. And OmniViewer deliberately puts data in the URL: a #payload= share link carries the file's bytes in the fragment, and a ?url= link carries a source address. Ship the full URL to Google and you would ship exactly the thing we swore to keep local.
So we don't. Every hit is pinned to the pathname only — the part before the ? and the #. That pathname is also precisely the signal we actually want (did people land on the MP3 tool or the JSON tool?), so there is no reason to send anything more.
The one-line version. Google Analytics tells us that someone opened /mp3. It never learns which MP3 — because the MP3 is in the fragment, and the fragment never leaves the machine it was dropped on.
Where your bytes go: nowhere
Four things are involved when you use OmniViewer: your browser, the static host the app is downloaded from (Amazon S3 behind CloudFront), Google Analytics, and — the important one — your file. Follow the arrows:
/mp3 — and nothing else can cross.- S3 → browser: the static app (HTML, JavaScript, styles). A one-way download, exactly like fetching any web page.
- Browser → Google Analytics: the pathname (
/,/mp3,/json) plus GA's own anonymous counters. That's the whole payload. - Your file → anywhere: never happens. There is no request that carries it, because there is nowhere to carry it to.
- The URL's
?queryor#fragment→ Google: stripped before every hit. The#payload=bytes and any?url=stay in the browser.
The actual code
No hand-waving — this is the real analytics path from omni-app.js. Two details do all the work: it only initialises on the production host, and every hit is pinned to location.pathname, never location.href.
const GA_ID = 'G-M4FRK95PED';
// Analytics runs on the real domain ONLY. On localhost, in CI, and in the
// test suite this is false, so Google is never even contacted.
const analyticsOn =
location.hostname === 'omniviewer.org' ||
location.hostname === 'www.omniviewer.org';
// Pathname only — never the ?query (may hold ?url=) or the #fragment
// (may hold a whole #payload= file). Falls back to '/' for the home route.
const pagePath = () => location.pathname || '/';
const pageLoc = () => location.origin + pagePath();
function initAnalytics() {
if (!analyticsOn) return;
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag() { window.dataLayer.push(arguments); };
window.gtag('js', new Date());
// The page_location we hand GA is rebuilt from the bare path, so the
// query string and fragment are dropped before anything is sent.
window.gtag('config', GA_ID, { page_location: pageLoc(), page_path: pagePath() });
}
And when you drop a file and the app rewrites the URL to its format route (/mp3, /json, …), we report that landing ourselves — again, path only:
function trackPage() {
gtag('event', 'page_view', {
page_location: pageLoc(), // origin + pathname, nothing else
page_path: pagePath(),
page_title: document.title,
});
}
Compare with the snippet every tutorial gives you, which sends the whole address:
// The stock snippet — DON'T use this on a page that puts data in the URL.
gtag('config', GA_ID);
// → GA reads location.href by default, i.e. it would send:
// https://omniviewer.org/mp3#payload=<your entire file, base64>
// https://omniviewer.org/json?url=https://internal.example/secret.json
That difference — pageLoc() instead of the default location.href — is the entire reason a share link's bytes stay yours. The share-link page keeps its own script-free, analytics-free CSP for the same reason.
Want to check? Open your browser's Network tab and filter for google-analytics. On any OmniViewer page you'll see the outgoing hit — inspect it and the dl / page_location parameter is a bare path like /mp3. Drop a file, add a #payload=, and watch it stay a bare path.
Remember: there is no server
It's worth being blunt about the architecture, because it's what makes the promise structural rather than a pinky-swear. OmniViewer has no backend. There is no application server, no API, no database, no upload endpoint. The entire site is a bundle of static files sitting in an Amazon S3 bucket, handed out by a CDN.
A static host can only do one thing: send you files when you ask. It has no code path that receives your file, because there is no code running anywhere except in your own browser. You can't leak data to a server that was never built. Google Analytics is the single third party in the whole picture, and — as the code above shows — it's fed nothing but the page path.
Offline is always a good thing — trust no one
Here's the cleanest proof of all: OmniViewer works with the network turned off. Load the page once, kill your Wi-Fi, and keep dropping files. Everything still works — the viewer, the hex tab, the format tools — because all of it runs locally. (With the network off, the analytics hit simply never fires, which is fine; it was never load-bearing.)
That's not a party trick, it's a security posture: trust no one. The best way to guarantee your file isn't going somewhere it shouldn't is for there to be no working path off your machine at all. A tool that keeps functioning offline is a tool that provably wasn't depending on phoning home. So a good habit with any viewer that handles sensitive data:
- Load it once, then go offline and confirm it still works. If it does, its promises are backed by physics, not policy.
- Prefer tools with no server to trust in the first place — you can't audit a backend you can't see, but you can watch a browser's Network tab.
- Treat "your data is safe with us" as weaker than "your data never reaches us."
OmniViewer is built for exactly that test. Turn off the network and it keeps its promise, because keeping your bytes local was never a matter of trust — it's the only thing the architecture can do.