Crossdeck Docs
Dashboard

Internal traffic

Analytics 6 min read · Query-time exclusion, never destructive

The developer machine you test from, the internal tools that hammer your API, the tab you leave open on your own product all day — that's real traffic, but it isn't your customers. Internal traffic filtering keeps an account's own activity out of its dashboards, the way GA4's "internal traffic" does. The fast path is a single click: Crossdeck detects the network your dashboard request came from and excludes it — no IDs to copy, nothing to look up. And it does this with one important difference from GA4: Crossdeck never drops the events. They are always collected and stored. The exclusion is a view applied at read time, on every surface, and it reverses with a single toggle.

TL;DR

What counts as internal

Every event Crossdeck stores carries an actor_type — a small enum describing who generated it:

ValueMeaning
customerA real person using your app. The default — every event is a customer event until a rule says otherwise.
internalActivity you generate yourself: a developer machine, an internal tool, you browsing your own product. Excluded from dashboards by default.
testReserved. A future slot for sandbox / synthetic actors. Not wired up yet — present so the model doesn't need a migration later.

"Internal" is the only classification active today. It answers a narrow, practical question: is this event me, or is it a customer? Getting that line right is what makes "active users" mean active customers, and what stops your own paywall-testing from showing up as a conversion.

Collected always, filtered on read

This is the load-bearing principle, and it is worth stating plainly: Crossdeck never drops an internal event at ingestion. Internal traffic is collected, validated, and stored exactly like customer traffic. Classification — and exclusion — happen later, at query time, every time a dashboard reads.

The alternative — dropping events that match an internal rule at the door — is how most "internal traffic" features work, and it is a trap. A fat-fingered CIDR range, an IP that turns out to be a shared corporate gateway, a rule a teammate adds without thinking it through: any of these can silently delete real signal you will never get back. There is no undo on data you didn't keep.

Why query-time and not ingestion-time?

Because rules change and identity resolves over time. An event you couldn't classify at ingest (anonymous, no resolved user yet) might become classifiable an hour later when the session stitches to a flagged account. A query-time model re-asks the question on every read against the current rules; an ingestion-time model freezes a verdict the instant the event arrives and can never revisit it. Keeping the raw firehose intact is what makes the retroactive case below possible at all.

Excluding your own traffic

You manage this in Settings → Internal traffic. The primary control is a single button; two more signals cover the cases it can't reach. You can use any combination of them.

1 · One click — "Exclude my network"

Click Exclude my network and Crossdeck reads the public IP your dashboard request came from — derived exactly the way the SDK stamps source_ip on your visitors' events — and saves it as an exclusion. No IDs to copy, nothing to look up. This is GA4's "exclude my traffic" button, and for most accounts it is the only step you need: a developer browsing their own product is anonymous traffic, and its source IP is the one signal that visit shares with you.

IPv6 is excluded by network, not by exact address.

If you're on IPv6, your device's address suffix rotates for privacy (RFC 4941) — excluding today's exact /128 would stop matching you tomorrow. So Crossdeck excludes the stable /64 network prefix instead. On IPv4, where a home or office shares one public address behind NAT, it excludes that exact host. Either way the saved rule keeps matching you across sessions — which is the whole point.

2 · IP addresses and CIDR ranges, by hand

Below the button you can add individual IP addresses or CIDR ranges yourself — your office or VPN egress, a QA device's network, an internal dashboard that hammers your API. Any event whose source IP falls inside a listed range is internal. Each saved range shows as a chip you can remove with one click.

IP is a blunt signal — and that's fine here.

A residential or coffee-shop IP is shared and reassigned; a corporate gateway can sit in front of real customers. The never-drop design is your safety net: if a range turns out to be too broad, remove the chip and the affected events reclassify back to customer on the next read. Nothing was lost.

3 · The browser opt-out flag

Visit any tracked surface with ?crossdeck_internal=1 in the URL. The web SDK persists a flag in that browser's localStorage, and every later event from that browser is tagged internal — until you clear it by visiting with ?crossdeck_internal=0.

This covers the cases IP and identity miss: a dynamic home IP that changes nightly, or you browsing your own product logged out. It is per-browser and self-service — no Settings change required to set or clear it.

Bookmark the on/off URLs.

Keep https://yourapp.com/?crossdeck_internal=1 and …=0 as two bookmarks. One click tags the browser before a testing session; one click clears it after. Because the flag lives in localStorage, it survives reloads and new tabs on the same browser profile, but not a different browser, a private window, or a cleared cache.

What about flagging a specific user?

Earlier versions let you mark an individual Crossdeck User as internal in Settings. We retired that field in favour of the one-click model above — for the developer excluding their own testing, IP is simpler and doesn't depend on being signed in (your self-traffic is usually anonymous, which an ID rule can't match). The resolved-identity signal itself didn't go away: any user you flagged previously is still honoured at read time, and it remains the authoritative reason when an identity is known. Per-user internal flagging returns as a managed feature alongside Teams.

How a signal wins

An event is internal if any of these signals matches — you don't have to pick one:

  1. IP / CIDR — the event's source IP falls in a range you added (via the button or by hand). The primary signal for excluding your own traffic.
  2. Resolved identity — the event resolves to a Crossdeck User that was flagged internal. Honoured for any user flagged previously, and authoritative whenever an identity is known.
  3. Opt-out flag — the browser carries the persisted ?crossdeck_internal=1 flag.

The verdict doesn't depend on the order — a single match makes the event internal. A customer event stays customer only when none of these match.

Retroactive reclassification

Identity resolves over time. A visitor lands anonymously, clicks around, and only signs in three pages later — at which point identity stitching connects the earlier anonymous events to the now-known user. If that user is flagged internal, those earlier events must become internal too, even though they were unclassifiable when they arrived.

Crossdeck handles this by persisting the raw signals on every event — the resolved user ID (if known at the time), the source IP, and the browser opt-out flag — rather than a single classification stamped once at ingest. Because the inputs are kept, actor_type can always be re-derived from the current rules:

This is why we store signals, not a verdict.

A value stamped at ingest ("internal: true") is frozen — it can't learn that the session later belonged to you. Storing the inputs and re-deriving on demand is what lets a week-old anonymous event become correctly internal the moment its identity is known. It is the same instinct behind the never-drop rule: keep the evidence, re-decide on read.

Showing internal traffic

Every dashboard carries a "Show internal traffic" toggle. It is off by default — your own activity is hidden on Overview, People, Activity, Revenue, Funnels, and Cohorts without any setup. Turn it on to fold internal events back into the view while you are debugging or developing.

It is purely a view preference. Turning it on does not change what was collected, and turning it off does not delete anything — both states read the same stored firehose and differ only in the filter applied. There is no ingestion-time version of this switch, by design.

What gets stored

For the curious, the shape underneath:

The two paths agree because they answer the same question from the same stored inputs; the hot path is just an optimisation so live dashboards don't wait for the warehouse.

Bot traffic

Internal traffic is you; bot traffic is no one. Crawlers, scrapers, headless browsers, and uptime monitors generate real requests that aren't humans at all, and Crossdeck classifies them separately. Every event carries an is_bot flag, and every dashboard query filters bots by default — a "show bots" toggle on the Activity page folds them back in when you want them (knowing Googlebot crawls you is useful for SEO debugging).

Classification runs at ingest, on the request's headers, checking three signals in order — the first one present wins:

  1. Cloudflare verified bot. Cloudflare marks known good bots — Googlebot, Bingbot, Applebot, social-preview fetchers — by reverse-DNS-checking the caller's IP against the bot's published ranges. If the request carries Cloudflare's cf-verified-bot header, it's a bot. Highest confidence: this is verified identity, not a guess.
  2. Cloudflare bot score. Cloudflare's ML assigns requests a 1–99 score, lower meaning more bot-like. A score below 30 — Cloudflare's own threshold — classifies as bot; this catches scrapers, headless browsers, and AI crawlers that aren't on the verified list. A score of 30 or above is trusted as human, and no further check runs.
  3. User-Agent patterns. When neither Cloudflare header is present, a small curated list (~50 patterns) covers the major crawlers, automation frameworks, and monitoring services. The list is deliberately short: a misclassified human silently disappears from your analytics, while a missed bot only inflates a count slightly — so false positives are the failure mode it's tuned against. An empty User-Agent is never penalised, because some legitimate clients (older mobile browsers, embedded webviews) ship without one.

One difference from internal traffic is worth being explicit about: the bot verdict is stamped once, at ingest — on every event in that request — rather than re-derived at read time. Future requests from the same visitor are re-classified independently, so a scraper that rotates User-Agents is caught session by session, not retroactively across its history. Like internal traffic, though, bot events are never dropped: they're stored with is_bot set, and the exclusion is a query-time filter you can lift with the toggle.