---
title: Passive Data Collection
description: If you already have the Churnkey embed installed, Churnkey can activate passive collection for your organization without code changes.
---

Passive data collection lets Churnkey passively understand how your users behave inside your app — page views, navigation, sessions, and (optionally) plan / MRR context tied to a customer.

It's designed to be lightweight (no extra infrastructure, no SDK to manage), privacy-friendly (no clicks, no form values, no replays), and to plug into the rest of Churnkey: when a user later hits a Pause Wall or Cancel Flow, their anonymous browsing history merges onto their customer profile automatically.

::alert{type="info"}
Passive collection is **not** a replacement for product analytics tools like PostHog, Amplitude, or Mixpanel. It feeds Churnkey's churn-prevention models with the minimum signal they need — page views, sessions, and plan context.
::

## When to use it

Turn this on if you want Churnkey to:

- See which pages users visit before they cancel, downgrade, or churn.
- Detect silent-churn risks (e.g. a new upgrader who stops engaging in their first 90 days).
- Power plan-matching, activation, and post-upgrade interventions with real behavioral context — not just billing data.

You don't need this to run Pause Walls or Cancel Flows. It's purely additive context.

## Quick start

### Already have the Churnkey embed?

If you've already deployed the Churnkey install snippet for Cancel Flows, the Pause Wall, or the Failed Payment Wall, passive collection can be activated for your organization with no code changes.

1. Contact your Customer Success manager or email [support@churnkey.co](mailto:support@churnkey.co) with your `appId` to request activation.
2. Once Churnkey enables passive collection for your organization, your existing embed script automatically starts collecting anonymous page views and sessions on every page where it's installed.

That's it. The embed reads your `appId` from the same install snippet (`https://assets.churnkey.co/js/app.js?appId=YOUR_APP_ID`) and the rest happens server-side.

::alert{type="info"}
Until passive collection is enabled for your organization, the embed quietly checks availability and stays out of the way. There is no risk in deploying the standard install snippet before activation.
::

### Don't have the embed yet?

Drop in the standard install snippet first — see the [Cancel Flows quick start](/cancel-flows/quick-start-guide) for the canonical version. Passive collection rides on the same script:

```javascript
<script>
!function(){
  if (!window.churnkey || !window.churnkey.created) {
    window.churnkey = { created: true };
    const a = document.createElement('script');
    a.src = 'https://assets.churnkey.co/js/app.js?appId=YOUR_APP_ID';
    a.async = true;
    const b = document.getElementsByTagName('script')[0];
    b.parentNode.insertBefore(a, b);
  }
}();
</script>
```

Then contact your Customer Success manager or email [support@churnkey.co](mailto:support@churnkey.co) to request activation — same as above.

### Identifying the user (after login)

The auto-init flow above runs anonymously by default — visitors get a stable `ck_…` id stored in first-party cookie + `localStorage`. Once your app knows who the user is (typically after login), call `churnkey.identify` to attach their billing-system customer id and any super-properties:

```html
<script>
  churnkey.identify('cus_abc123', {
    subscriptionId: 'sub_abc123',
    customerAttributes: {
      plan: 'pro',
      planInterval: 'monthly',
      mrr: 75,
      signupDate: '2026-01-15',
      industry: 'saas',
      teamSize: 4,
    },
  });
</script>
```

`customerAttributes` is the same free-form bag of super-properties you may already pass to `churnkey.init({ customerAttributes })` for cancel flows. Common keys (`plan`, `planInterval`, `mrr`, `signupDate`) are recognized by Churnkey's models, but you can include anything else you find useful. Keep the object compact — payloads larger than 4 KB are discarded server-side.

Every event the visitor generated anonymously before this call is retroactively tagged with the `customerId` server-side — you don't need to re-send anything.

## Configuration reference

Most installs don't need any explicit configuration — the canonical install snippet (`?appId=` in the script src) is enough. If you need hooks like `before_send`, want to disable page-view tracking, or want to defer collection behind a consent flow, call `churnkey.dataCollectionSetup()`:

:field-schema{schema="/types/passive-data-collection-setup.type"}

### Suppressing or scrubbing events

Use `before_send` to drop sensitive paths or strip query strings before events leave the browser:

```js
churnkey.dataCollectionSetup({
  before_send: (event) => {
    // Don't track the account-settings area
    if (event.path.startsWith('/settings')) return null;

    // Strip ?token= and ?email= from URLs
    event.url = event.url.replace(/[?&](token|email)=[^&]*/g, '');
    return event;
  },
});
```

Returning `null` drops the event entirely.

### Manual start (advanced)

If you need to defer collection until after some app-level gate (for example, cookie consent), pass `startPassively: false` and call `churnkey.startPassiveCapture()` when you're ready:

```js
churnkey.dataCollectionSetup({ startPassively: false });

// Later, after consent:
churnkey.startPassiveCapture();
```

## What gets collected

Every event sent by passive collection has the following shape:

:field-schema{schema="/types/behavior-event.type"}

Single-page app navigation (`pushState`, `replaceState`, `popstate`) is auto-detected — you don't need to fire a manual event when the route changes.

### What we don't collect

- Click events, form values, keystrokes, or paste content
- Session replays or DOM snapshots
- Cross-site tracking — each site has its own anon id, stored in first-party storage
- Anything you remove or rewrite in `before_send`

## Identity and sessions

Churnkey separates **anonymous** browsing from **identified** browsing and stitches them together automatically:

1. **First visit, anonymous.** As soon as the embed loads on a page where passive collection is enabled, we mint a `ck_…` anon id and store it in a first-party cookie plus `localStorage`. Every event in this state is tagged with the anon id only.
2. **You identify the user.** Calling `churnkey.identify(customerId, { … })` sends a one-time `identify` call linking the anon id to your `customerId`. From then on, events are tagged with both ids.
3. **The user later hits a Pause Wall or Cancel Flow.** The customer id surfaced there is automatically merged into the same profile — no extra call needed on your side.

Sessions roll over after 30 minutes of inactivity, so a user who comes back the next day starts a new `sessionId` but keeps the same `anonId`.

## Performance and privacy

- **Non-blocking.** Requests use `fetch` with `keepalive: true`, so they survive page unloads without delaying navigation.
- **Batched.** Events flush every 5 seconds, every 20 events, on tab hide (`visibilitychange`), and on page unload (`pagehide`) — whichever comes first.
- **Silent on failure.** If the network call fails, we drop it. Passive collection should never surface an error or block the UI.
- **Lightweight.** Roughly 6 KB gzipped, loaded as part of the Churnkey embed you already have.
- **First-party only.** The anon id is stored in a `SameSite=Lax` cookie (1 year) and mirrored in `localStorage`. No third-party cookies, no cross-site tracking. If a user clears either store, they get a new anon id on their next visit.
- **Page metadata, not page contents.** We capture URL, path, title, and the first H1 — never form inputs, `<textarea>` values, or contents of `contenteditable` elements.

Use `before_send` to scrub anything you'd rather not send.

## Viewing data in the dashboard

Once events start arriving, you'll find them in your Churnkey dashboard under **Insights**. At this stage the page surfaces two tables:

- **Sessions** — one row per visitor session, with start time, page count, and last-known customer.
- **Events** — the raw stream of `[Churnkey] Page Viewed` events with their full URL and page metadata.

## FAQ

::collapsible{name="Do I need to call identify at all?"}
Not for anonymous tracking. As long as you have the standard Churnkey install snippet on your page and Churnkey has enabled passive collection for your organization, anonymous page views start flowing automatically. Call `churnkey.identify(customerId, …)` once you know who the user is (after login) so the visitor's history merges onto their customer profile.
::

::collapsible{name="Do I need to call identify on every page?"}
Once per session is enough — `identify` is idempotent and the resulting `customerId` is remembered for the rest of the session. SPA route changes between calls are auto-tracked via `pushState` / `replaceState` / `popstate`, so a single call after login is enough.
::

::collapsible{name="Does this replace my product analytics tool (PostHog, Amplitude, Mixpanel)?"}
No. Passive collection feeds Churnkey's churn-prevention models with the minimum signal they need. It is not a general-purpose product analytics tool — there are no funnels, dashboards, or query builders.
::

::collapsible{name="What if I don't pass a customerId?"}
We collect anonymously under a `ck_…` id. This is useful for marketing sites and pre-signup behavior. As soon as you identify the user — either by calling `churnkey.identify(customerId)` or by running a Pause Wall / Cancel Flow with that customer — the prior anonymous history merges onto their profile.
::

::collapsible{name="Can I use passive collection without a Pause Wall or Cancel Flow?"}
Yes. `churnkey.identify` and the auto-init flow work standalone. Identity merging from Pause Walls / Cancel Flows is purely additive — without one, events stay attached to the `customerId` you pass in (or the anon id if you don't identify the user).
::

::collapsible{name="How do I keep certain pages or query parameters out of the data?"}
Use `before_send`. Return `null` to drop an event entirely, or mutate `event.url` / `event.path` to redact sensitive values before the event is queued.
::

::collapsible{name="Where do I see the data?"}
In your Churnkey dashboard under **Insights**. You'll see Sessions and Events tables once events start landing.
::

## Need help?

Email [support@churnkey.co](mailto:support@churnkey.co) with your `appId` and we'll take a look.
