Passive Data Collection
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.
- Contact your Customer Success manager or email [email protected] with your
appIdto request activation. - 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():
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.
(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.
(event) => void. Observer hook called for every event after before_send has accepted it. Useful for debugging or mirroring events to your own analytics.
Per-event-type toggles. Each key is an optional boolean and defaults to true. Set pageViews to false to disable automatic page-view events.
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:
The event name. For page views this is [Churnkey] Page Viewed.
Stable per-browser anonymous id (ck_…). Persisted in a first-party cookie and localStorage.
Session id (ck_sess_…). Rotates after 30 minutes of inactivity.
Full page URL at the time the event fired.
Pathname portion of the URL (no query, no host).
document.title at the time the event fired.
Text of the first <h1> on the page, truncated to 200 characters.
document.referrer, if any.
Set once you've identified the user via churnkey.identify(customerId, …) or after a Pause Wall / Cancel Flow runs.
Set once you've passed subscriptionId to identify() or it's been merged in via Pause Wall / Cancel Flow.
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.
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:
- 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 pluslocalStorage. Every event in this state is tagged with the anon id only. - You identify the user. Calling
churnkey.identify(customerId, { … })sends a one-timeidentifycall linking the anon id to yourcustomerId. From then on, events are tagged with both ids. - 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
fetchwithkeepalive: 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=Laxcookie (1 year) and mirrored inlocalStorage. 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 ofcontenteditableelements.
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 Viewedevents with their full URL and page metadata.
FAQ
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.
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.
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.
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.
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).
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.
In your Churnkey dashboard under Insights. You'll see Sessions and Events tables once events start landing.
Need help?
Email [email protected] with your appId and we'll take a look.