Passive Data Collection

If you already have the Churnkey embed installed, Churnkey can activate passive collection for your organization without code changes.
View Markdown

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.

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 [email protected] 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.

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 for the canonical version. Passive collection rides on the same script:

<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 [email protected] 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:

<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():

appIdstring
optional

Your Churnkey app id. Only needed if the install snippet didn't include ?appId=… in the script src (e.g. consent-gated installs). Find it on Churnkey | Settings | Account.

before_sendobject
optional

(event) => event | null. Hook called for every event before it leaves the browser. Return a (possibly mutated) event to send it, or null to drop it. Use this to redact sensitive paths or strip query strings.

on_eventobject
optional

(event) => void. Observer hook called for every event after before_send has accepted it. Useful for debugging or mirroring events to your own analytics.

trackobject
optional

Per-event-type toggles. Each key is an optional boolean and defaults to true. Set pageViews to false to disable automatic page-view events.

startPassivelyboolean
optional

Whether to start collecting immediately. Defaults to true. Set to false if you need to defer collection (e.g. behind cookie consent), then call window.churnkey.startPassiveCapture() when ready.

Suppressing or scrubbing events

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

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:

churnkey.dataCollectionSetup({ startPassively: false });

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

What gets collected

Every event sent by passive collection has the following shape:

eventstring
required

The event name. For page views this is [Churnkey] Page Viewed.

anonIdstring
required

Stable per-browser anonymous id (ck_…). Persisted in a first-party cookie and localStorage.

sessionIdstring
required

Session id (ck_sess_…). Rotates after 30 minutes of inactivity.

urlstring
required

Full page URL at the time the event fired.

pathstring
required

Pathname portion of the URL (no query, no host).

titlestring
optional

document.title at the time the event fired.

h1string
optional

Text of the first <h1> on the page, truncated to 200 characters.

referrerstring
optional

document.referrer, if any.

customerIdstring
optional

Set once you've identified the user via churnkey.identify(customerId, …) or after a Pause Wall / Cancel Flow runs.

subscriptionIdstring
optional

Set once you've passed subscriptionId to identify() or it's been merged in via Pause Wall / Cancel Flow.

customerAttributesobject
optional

Free-form super-properties (plan, planInterval, mrr, signupDate, industry, teamSize, …) attached when you called churnkey.identify(customerId, { customerAttributes }). Same bag the existing churnkey.init({ customerAttributes }) flow uses for cancel flows. Keep this object compact; payloads larger than 4 KB are discarded server-side.

tsinteger
required

Client timestamp in milliseconds since the epoch.

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

Need help?

Email [email protected] with your appId and we'll take a look.