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 lets an account keep its own activity out of its dashboards, the way GA4's "internal traffic" does — with one important difference: 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.

Three ways to define internal traffic

You define what counts as internal in Settings → Internal traffic. There are three independent signals; you can use any combination of them.

1 · Flag a Crossdeck User

Toggle an individual Crossdeck User as internal. Every event that resolves to that user's ID is internal — across every device and session they use, past and future. This is the strongest and most durable signal: it follows the human, not the machine or the network they happen to be on.

Use it for yourself and your team's own accounts on your product. Because it keys off the resolved Crossdeck User ID, it composes directly with identity resolution — the moment an anonymous session stitches to a flagged user, its events reclassify (see retroactive reclassification).

2 · IP addresses and CIDR ranges

Add individual IP addresses or CIDR ranges (for example, your office or VPN egress). Any event whose source IP falls inside a listed range is internal. This is the right tool for traffic that isn't tied to a logged-in identity — an internal dashboard hitting your API, a QA device that never signs in, a marketing site preview.

IP is the bluntest signal.

A residential or coffee-shop IP is shared and reassigned; a corporate gateway can sit in front of real customers. Prefer the user flag where an identity exists. The never-drop design is your safety net here — if a range turns out to be too broad, widen or remove it 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.

How a signal wins

An event is internal if any of the three signals matches. They are evaluated in priority order, strongest first:

  1. Identity — the resolved Crossdeck User ID is flagged internal. Authoritative whenever an identity is known.
  2. IP / CIDR — the event's source IP falls in a listed range.
  3. Opt-out flag — the browser carries the persisted ?crossdeck_internal=1 flag.

The order matters for explainability, not for the verdict: identity is checked first so that "internal because this is a flagged teammate" wins over the weaker, network- or browser-level reasons. A customer event stays customer only when none of the three 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.