To exclude your own visits from analytics, mark every browser you use with your tool’s per-browser opt-out, usually one console command or a special link. To exclude preview deployments, staging and localhost, allow only your production hostname, or better, never load the tracker outside production. IP address rules, the usual first answer, only hold for an office with a fixed address: a phone, a home connection, a VPN or IPv6 gives you a new address and quietly undoes them.
Below are the exact steps for Google Analytics 4, Plausible, Umami, Fathom, Matomo, PostHog and Vercel Web Analytics, from each vendor’s own docs as of November 2026, and for invisit, the cookieless analytics tool we build. We ran the code we show, and each section says how.
What counts as internal traffic, and what removes each kind
Internal traffic is any visit that comes from building, testing or watching your site rather than using it. Each kind looks different to an analytics tool, so each needs its own fix:
| Traffic | What works | What fails |
|---|---|---|
| Your own browsers: laptop, home, phone, VPN | A per-browser opt-out, once in each browser | IP rules, as soon as you change networks |
| Teammates and QA | The same opt-out in their browsers, or tagging signed-in staff | IP rules for anyone outside the office |
| An office with a fixed address | An IP rule covering IPv4 and IPv6 | Nothing, while the address stays |
| Preview and staging deployments | Not loading the tracker there, or a hostname allowlist | Browser opt-outs, because each preview is a new origin |
| localhost | Not loading the tracker, a tracker that skips it, or an allowlist | An opt-out set on production, for the same reason |
| Uptime monitors | Nothing to do for HTTP checks, which never run JavaScript | Browser checks, which run a real browser |
| End-to-end tests | Test builds without analytics, or automation detection | Headed browsers, which send an ordinary user agent |
Browser opt-outs live in localStorage (for Matomo, a cookie), which belongs to one origin, so www, the bare domain, localhost:3000 and every preview URL each need their own. Hostnames, not opt-outs, are the tool for environments.
Why IP address rules miss your own visits
An IP rule matches the address a request arrives from, and yours changes more often than it seems.
- IPv6 privacy addresses rotate. RFC 8981 gives temporary IPv6 addresses a default preferred lifetime of one day, so a rule for Monday’s address misses you on Tuesday.
- You have two addresses. Plausible’s docs warn that a device with both IPv4 and IPv6 can send visits over either, so blocking one still records the other.
- Phones and VPNs move. Mobile data changes address as you move, and a VPN shows its exit server’s address, shared with strangers.
A range survives longer than an address. A temporary IPv6 address keeps your network’s prefix and randomises only the interface part, so a CIDR rule on the prefix holds until your provider changes it. GA4, Matomo and Umami’s IGNORE_IP accept CIDR ranges. Plausible accepts full addresses only, up to 30 per site, and Fathom’s wildcards work for IPv4 only.
Which analytics tools can exclude what
As of November 2026, from each vendor’s documentation, plus the scripts and source code where the docs say nothing:
| Tool | Your own browser | IP rules | Hostname allowlist (previews, staging) | localhost by default | Already recorded visits |
|---|---|---|---|---|---|
| Google Analytics 4 | Opt-out add-on, or ga-disable-<ID> from your code |
Internal traffic rules, CIDR or regex, IPv4 and IPv6 | Web hostname data filter, Include only | Counted | Stay: filters work from activation on |
| Plausible | localStorage.plausible_ignore |
Up to 30 full addresses, no ranges | Hostnames shield, up to 30 | Skipped, file: pages too |
Stay |
| Umami | localStorage key umami.disabled |
IGNORE_IP with CIDR, self-hosted only |
data-domains on the script tag, exact hostnames |
Counted | No removal documented |
| Fathom | fathom.blockTrackingForMe() |
Firewall IPs, wildcards for IPv4 only | Firewall Domains allow list | Counted | Removable with Data removals, and undoable |
| Matomo | matomo_ignore cookie from Personal settings |
Per site or global, wildcards and CIDR | “Only track visits … above URLs” | Counted, file: pages skipped |
Stay, unless deleted with the GDPR tools |
| PostHog | posthog.opt_out_capturing() |
No IP list documented | $host in the internal users filter, or a Drop events transformation |
Counted | Hidden: the internal users filter runs at query time |
| Vercel Web Analytics | No opt-out documented; a beforeSend that returns null |
None documented | Previews kept as a separate environment | Never sent from development mode | Stay; environment and hostname are view filters |
| invisit | #invisit-ignore link |
None | Allowed domains, each with its subdomains, up to 10 | Counted until allowed domains are set | Stay; a hostname filter narrows past reports |
Fathom alone removes past visits by the same filters as its dashboard, with an undo; Matomo deletes them through its GDPR tools; PostHog’s exclusion works backwards because it filters at query time. GA4 sits at the other extreme: an active data filter discards matching events for good.
How to exclude internal traffic in Google Analytics 4
GA4 excludes internal traffic in two steps: a rule that labels matching events with a traffic_type parameter, and a data filter that drops them. You need the Editor role, and a property can hold up to 10 data filters (Google’s guide).
- In Admin, under Data collection and modification, open Data streams, pick your web stream, then Configure tag settings, Show more, Define internal traffic, Create.
- Keep
traffic_typeasinternal. Set IP address match type to equals, begins with, ends with, contains, is in range (CIDR) or matches regular expression, and add your IPv4 and IPv6 addresses. Conditions are OR. - Under Data filters, choose Create Filter, then Internal Traffic, set the operation to Exclude and the state to Testing.
- After 24 to 36 hours, build a Free form exploration with Test data filter name and Event name as rows and Event count as the value, to see what the filter would drop.
- Switch the filter to Active.
Step 5 is permanent: filters run “from the point of creation forward”, and excluded data “is never processed and will never be available in Analytics or BigQuery” (data filters). You can also set traffic_type yourself, to tag signed-in staff from your own code instead of by IP.
GA4 has two more filter types for teams:
- Developer traffic drops events sent in debug mode, which Tag Assistant and preview mode turn on, while DebugView keeps showing them (filter developer traffic). To turn debug mode off, remove the
debug_modeparameter; Google notes that setting it tofalsedoes not turn it off. - Web hostname traffic keeps previews, staging and localhost out by name. It arrived with Exclude on June 11, 2026 and gained Include only on September 21, 2026 (release notes). An Include only filter with “exactly matches” for each production hostname drops everything else, including events with no hostname; Measurement Protocol events are exempt (hostname filter).
For your own browsers, Google’s Opt-out Browser Add-on works, but on every site that uses Google Analytics. To exclude only your property, set the documented ga-disable-<measurement ID> flag before the Google tag runs (Turn off Google Analytics), keyed to a localStorage entry:
<script>
// Run localStorage.setItem('ga-opt-out', '1') once in each browser you use.
if (localStorage.getItem('ga-opt-out') === '1') window['ga-disable-G-XXXXXXXXXX'] = true
</script>
<!-- the Google tag follows -->We loaded this with the live gtag.js in Chromium and intercepted its requests: without the flag it sent a page_view to /g/collect, with it nothing. Set the flag on every page, before any gtag() call.
How to exclude your own visits in Plausible
In Plausible, block your IP address under site settings, Shields, IP Addresses, or set a flag in each browser for addresses that change (exclude internal traffic). Open your site, then the browser console:
localStorage.plausible_ignore = true
// to be counted again: delete localStorage.plausible_ignoreDo it for every domain, subdomain and browser. On the next page the console should say “Ignoring Event: localStorage flag” (visitor opt-out).
The Shields menu holds the rest. IP Addresses takes full IPv4 or IPv6 addresses, one at a time, up to 30. Hostnames is an allow list of up to 30: *.example.com covers subdomains but not example.com itself, *example.com covers both. Pages blocks paths such as /admin/*, and Countries blocks whole countries. Blocked visits are dropped before they are stored and do not count toward your monthly pageviews.
Plausible’s script source ignores localhost, 127.x addresses, [::1] and file: pages unless you pass captureOnLocalhost: true to the npm package, and ignores browsers that report navigator.webdriver or run Cypress. A phone opening your dev server at 192.168.1.20:3000 is not on that list and is counted.
How to exclude your own visits in Umami
In Umami, set umami.disabled in each browser’s console on your site; it applies per website (exclude my own visits):
localStorage.setItem('umami.disabled', 1)
// to be counted again: localStorage.removeItem('umami.disabled')For previews and staging, add data-domains to the script tag. The tracker then runs only where window.location.hostname exactly matches one of the listed names, so list www separately (tracker configuration):
<script
defer
src="https://cloud.umami.is/script.js"
data-website-id="<website id>"
data-domains="example.com,www.example.com"
></script>Umami’s tracker has no localhost rule of its own, so without data-domains local visits are sent (tracker source). IP exclusion exists only on a self-hosted server, as the IGNORE_IP environment variable with addresses and CIDR ranges such as 10.0.0.0/24, 2001:db8::/32 (environment variables); Umami Cloud documents no equivalent.
How to exclude your own visits in Fathom
In Fathom, run fathom.blockTrackingForMe() in the console on your site; fathom.enableTrackingForMe() undoes it (exclude your own visits). It is kept in localStorage until you clear site data.
Everything else is under Settings, Sites, your site, Firewall (firewall settings):
- Domains: once you allow one or more domains, data from any other domain “will be blocked instantly”.
*.example.comdoes not coverexample.com, so add both. Fathom calls this the best option for local, staging and test environments, which its script otherwise counts. - IPs: full addresses, or wildcards such as
100.100.100.*, which work for IPv4 only. - Pages, Countries, Referrers and Events lists, each with Block or Allow.
Fathom is the one tool here that cleans up the past. Data removals, under your site’s Manage settings, lets you filter past visits (staging traffic, test runs) and remove them from the dashboard, exports and API. They stop counting toward your usage, and you can undo a removal, since removed visits are hidden, not deleted.
How to exclude internal traffic in Matomo
In Matomo, exclude office addresses under Administration, Websites, Settings, in the Global list of Excluded IPs, or per site. It takes single addresses, wildcards such as 132.4.3.* and CIDR ranges such as 2001:db8::/32, and works even with IP anonymisation on (exclude by IP).
For your own browser, the link under “Exclude your visits using a cookie” in Personal settings sets a matomo_ignore cookie (exclude your traffic). That cookie belongs to your Matomo server’s domain, so on a site under another domain the browser sees it as a third-party cookie, which Safari blocks entirely (WebKit). An opt-out form on your own site sets its cookie on your domain instead.
For previews and staging, edit the website under Administration, Websites, Manage and tick “Only track visits and actions when the action URL starts with one of the above URLs”; each valid subdomain must be listed (own URLs only). The Global list of user agents to exclude drops monitors and test tools by user agent, with regular expressions since Matomo 4.1.1 (user agents).
None of these is retroactive. Matomo points you to its GDPR tools to delete visits already recorded.
How to filter out internal users in PostHog
In PostHog, define internal traffic under Settings, Project, “Filter out internal and test users”, then switch the filter on per insight or for all of them (internal and test users). Each entry describes what to keep, so write negative conditions: email does not contain @example.com for staff, $host does not contain localhost for local development, or a cohort.
Defining the list does not apply it: the same settings turn it on for existing insights and as the default for new ones. Because the filter runs at query time, it hides old internal events too, but the events were still captured and count toward your usage.
To stop them at the door instead, add a Drop events transformation under Data pipeline with a property filter such as $host. Dropped events “do not count towards your usage/billing” and are gone for good. For a single browser, run posthog.opt_out_capturing() in the console; the choice is kept in localStorage by default (data collection).
How to keep your own visits out of Vercel Web Analytics
Vercel Web Analytics documents no own-visit opt-out and no IP rules, but local development is excluded by design. With NODE_ENV set to development or test, @vercel/analytics loads a debug script that logs events to the console (package docs); we downloaded that script and it contains no network call. A production build loads its script and endpoint from /_vercel/insights on the deployment itself, so a build running on your laptop has nothing to load.
Preview deployments are collected and shown as their own environment: the dashboard’s environment menu offers Production, Preview or All Environments, with Production selected by default (using Web Analytics).
For your own browsers, use beforeSend, which drops an event when it returns null, from a client component:
'use client'
import { Analytics } from '@vercel/analytics/next'
// Run localStorage.setItem('va-opt-out', '1') once in each browser you use.
export function VercelAnalytics() {
return (
<Analytics
beforeSend={(event) => (localStorage.getItem('va-opt-out') === '1' ? null : event)}
/>
)
}Render <VercelAnalytics /> in your root layout instead of <Analytics />, since a Server Component cannot pass a function. Served locally with this beforeSend, Vercel’s production script sent its page view without the flag and nothing with it.
How to exclude your own visits and previews in invisit
invisit has no IP rules. It keeps internal traffic out with a per-browser link, an allowlist of domains, excluded paths, and checks for automated browsers in its script and at the collector (excluding traffic).
For your own browsers, open any page of your site with #invisit-ignore at the end of the address. A notice reads “invisit: this browser is now excluded from analytics”, and from then on that browser sends nothing at all, not even the tracker’s configuration request. Tabs already open stop too, because the tracker checks the flag before every event. #invisit-unignore turns counting back on. The flag is kept in that origin’s localStorage, so do it on www and the bare domain separately, and on your phone; a private window forgets it, and a tracker started with storage: 'none' ignores it. Events your server sends with @invisitdev/server are not affected.
For previews, staging and localhost, add your domain under Settings, Collection, Allowed domains. Events are then accepted only from those domains and their subdomains, up to 10 per site; with an empty list, any origin is accepted. A bare localhost cannot be added, since an allowed domain needs a dot. The collector refuses any other host, and the tracker, once it has read the list, logs the accepted domains to the console and goes quiet. Refused requests appear for 14 days under Settings, Blocked traffic, as “Origin not allowed”, “Local development host” or “No origin”.
We ran the published invisit.js against a local collector. Without allowed domains, the tracker counts localhost like any host. On a file: page it records no page views, since it only records http and https addresses, but its heartbeats still go out, so a site without allowed domains can show a live visitor in Realtime while that page is open. With allowed domains set, the first page view of each load leaves before the configuration arrives, and the collector refuses it.
For internal pages, excludePaths: ['/admin/*'] in init() or data-exclude-paths on the script tag keeps them out (paths). An excluded page sends no events or heartbeats and is not used as the referrer of the page that follows it. Opening a page from the heatmap view stops collection in that tab, so reviewing a heatmap adds nothing.
invisit cannot remove visits already recorded, short of deleting the site. Dashboard filters can narrow any report, past dates included, to one hostname, but they only include, never exclude.
Do uptime monitors and test runners count as visits?
HTTP uptime checks do not count in any script-based tool: they request the page and read the status code, and the tracker’s JavaScript never runs. Browser checks and end-to-end tests do run it; Checkly’s browser checks, for one, use Playwright in Chromium.
We launched Chromium from Playwright 1.64.1 (Chromium 156) on Linux three ways and recorded what a collector receives:
| Playwright launch | User agent says | navigator.webdriver |
|---|---|---|
| Headless (the default shell) | HeadlessChrome/156.0.8078.4 |
true |
Headless, channel: 'chromium' |
HeadlessChrome/156.0.0.0 |
true |
| Headed, under Xvfb | Chrome/156.0.0.0 |
true |
The headless shell also names HeadlessChrome in its Sec-CH-UA header; the other two do not. Each tool checks different signals:
| Tool | Headless run | Headed run | What it checks |
|---|---|---|---|
| Google Analytics 4 | Not documented | Not documented | Known bots, excluded automatically, count not shown |
| Plausible | Skipped | Skipped | navigator.webdriver, window.Cypress, in the script |
| Umami | Dropped | Counted | The isbot package on the user agent, at the server |
| Fathom | Depends on the network | Depends on the network | Bot user agents, missing user agents and datacenter IP ranges |
| Matomo | Not documented | Not documented | Its bot list plus your excluded user agents |
| PostHog | Skipped | Skipped | A blocked user agent list and navigator.webdriver, in posthog-js |
| Vercel Web Analytics | Skipped | Skipped | navigator.webdriver or “Headless” in the user agent, in the script |
| invisit | Skipped | Skipped | navigator.webdriver in the script, user agent at the collector |
Sources: Plausible’s track.js, PostHog’s bot check, Fathom’s bot detection, GA4’s known bot exclusion, and Vercel’s production script as served on November 3, 2026. For Umami we ran our recorded user agents through isbot 5.2.2, the version Umami depends on. Fathom drops traffic from datacenter ranges such as AWS, Azure and Google Cloud, which covers CI runners hosted there.
invisit refuses the headless runs as a headless browser, along with user agents that name Puppeteer, Selenium, Cypress or an uptime service such as Pingdom or UptimeRobot; refused bots are never billed. A headed run sends an ordinary Chrome user agent, but navigator.webdriver is true there too, and invisit’s script stops whenever it is, unless you turn on the debug option to test your own setup. Our bot traffic guide covers crawlers and bots in general.
Browser checks against production driven by Playwright, Puppeteer or Selenium need nothing extra for invisit, since its script stays quiet whenever navigator.webdriver is true. A tool that hides that flag, or a person doing a manual QA pass, still needs the #invisit-ignore opt-out described above. For end-to-end tests against your own builds, the next section removes the question.
How to keep preview and staging builds from loading analytics at all
The surest exclusion is a tracker that never ships: decide at build time, from what your host already tells the build. Vercel sets VERCEL_ENV to production, preview or development during builds (system variables), and Netlify sets CONTEXT to production, deploy-preview, branch-deploy or dev (build variables).
In a Vite app, turn that into a constant:
import { defineConfig } from 'vite'
// Vercel sets VERCEL_ENV and Netlify sets CONTEXT during every build. Any other build,
// a local `vite build` included, gets no analytics.
const production = process.env.VERCEL_ENV === 'production' || process.env.CONTEXT === 'production'
export default defineConfig({
define: { __ANALYTICS__: JSON.stringify(production) },
})declare const __ANALYTICS__: boolean
// Replaced with true or false at build time: a preview build drops this block,
// so the tracker is never bundled or downloaded there.
if (__ANALYTICS__) {
import('@invisitdev/web').then(({ init }) => init({ site: 'YOUR_SITE_ID' }))
}Import ./analytics once from your entry file. We built it with Vite 8.3.5 and @invisitdev/web 0.2.1: with VERCEL_ENV=preview, CONTEXT=deploy-preview, CONTEXT=branch-deploy or neither set, the output was one 721-byte script with no trace of the tracker; with VERCEL_ENV=production or CONTEXT=production, the tracker’s chunks were in the bundle. Any tracker’s init() fits inside the block.
In Next.js on Vercel, NEXT_PUBLIC_VERCEL_ENV carries the same value into the browser (framework variables). Our Next.js guide shows the other route, a separate site for previews, which lets you check events before you merge.
Checklist: keep internal traffic out from day one
Set these up before launch. Most tools cannot clean up what they already recorded, and GA4 cannot undo a filter.
- Production builds are the only ones that load the tracker, decided at build time.
- The production site allows only your hostnames,
wwwand the bare domain included. - Every browser you and your team use is opted out, phones included, on each hostname.
- IP rules, if any, cover the office’s IPv4 address and its IPv6 range, not single addresses.
- End-to-end tests run against builds without analytics; browser checks against production opt out first.
- In GA4, every data filter ran in Testing for 24 to 36 hours before you made it Active.
- Internal pages such as
/adminare excluded by path.
