feat: improve how disabled works for the SDKS (to improve consent management)
This commit is contained in:
76
apps/public/content/docs/(tracking)/consent-management.mdx
Normal file
76
apps/public/content/docs/(tracking)/consent-management.mdx
Normal file
@@ -0,0 +1,76 @@
|
||||
---
|
||||
title: Consent management
|
||||
description: Queue all tracking until the user gives consent, then flush everything with a single call.
|
||||
---
|
||||
|
||||
import { Callout } from 'fumadocs-ui/components/callout';
|
||||
|
||||
Some jurisdictions require explicit user consent before you can track events or record sessions. OpenPanel has built-in support for this: initialise with `disabled: true` and nothing is sent until you call `ready()`.
|
||||
|
||||
## How it works
|
||||
|
||||
When `disabled: true` is set, all calls to `track`, `identify`, `screenView`, and session replay chunks are held in an in-memory queue instead of being sent to the API. Once the user consents, call `ready()` and the entire queue is flushed immediately.
|
||||
|
||||
```ts
|
||||
const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
disabled: true, // nothing sent until ready() is called
|
||||
});
|
||||
|
||||
// Later, when the user accepts your consent banner:
|
||||
op.ready();
|
||||
```
|
||||
|
||||
If the user declines, simply don't call `ready()`. The queue is discarded when the page unloads.
|
||||
|
||||
## With session replay
|
||||
|
||||
Session replay chunks are also queued while `disabled: true`. Once `ready()` is called, buffered replay chunks flush along with any queued events.
|
||||
|
||||
```ts
|
||||
const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
disabled: true,
|
||||
trackScreenViews: true,
|
||||
sessionReplay: { enabled: true },
|
||||
});
|
||||
|
||||
// User accepts consent:
|
||||
op.ready();
|
||||
```
|
||||
|
||||
<Callout type="info">
|
||||
The replay recorder starts as soon as the page loads (so no interactions are missed), but no data is sent until `ready()` is called.
|
||||
</Callout>
|
||||
|
||||
## Waiting for a user profile
|
||||
|
||||
If you want to hold events until you know who the user is rather than waiting for explicit consent, use `waitForProfile` instead. Events are queued until `identify()` is called with a `profileId`.
|
||||
|
||||
```ts
|
||||
const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
waitForProfile: true,
|
||||
});
|
||||
|
||||
// Events queue here...
|
||||
op.track('page_view');
|
||||
|
||||
// Queue is flushed once a profileId is set:
|
||||
op.identify({ profileId: 'user_123' });
|
||||
```
|
||||
|
||||
If the user never authenticates, the queue is never flushed automatically — no events will be sent. To handle anonymous users or guest flows, call `ready()` explicitly when you know the user won't identify:
|
||||
|
||||
```ts
|
||||
// User skipped login — flush queued events without a profileId
|
||||
op.ready();
|
||||
```
|
||||
|
||||
`ready()` always releases the queue regardless of whether `waitForProfile` or `disabled` is set.
|
||||
|
||||
## Related
|
||||
|
||||
- [Consent management guide](/guides/consent-management) — full walkthrough with a cookie banner example
|
||||
- [Session replay](/docs/session-replay) — privacy controls for replay recordings
|
||||
- [Identify users](/docs/get-started/identify-users) — link events to a user profile
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
"pages": ["sdks", "how-it-works", "session-replay", "..."]
|
||||
"pages": ["sdks", "how-it-works", "session-replay", "consent-management", "..."]
|
||||
}
|
||||
|
||||
233
apps/public/content/guides/consent-management.mdx
Normal file
233
apps/public/content/guides/consent-management.mdx
Normal file
@@ -0,0 +1,233 @@
|
||||
---
|
||||
title: "Consent management with OpenPanel"
|
||||
description: "Learn how to queue analytics events and session replays until the user gives consent, then flush everything at once with a single call."
|
||||
difficulty: beginner
|
||||
timeToComplete: 15
|
||||
date: 2026-03-02
|
||||
updated: 2026-03-02
|
||||
cover: /content/cover-default.jpg
|
||||
team: OpenPanel Team
|
||||
steps:
|
||||
- name: "Initialise with disabled: true"
|
||||
anchor: "disable"
|
||||
- name: "Build a consent banner"
|
||||
anchor: "banner"
|
||||
- name: "Call ready() on consent"
|
||||
anchor: "ready"
|
||||
- name: "Handle decline"
|
||||
anchor: "decline"
|
||||
- name: "Persist consent across page loads"
|
||||
anchor: "persist"
|
||||
---
|
||||
|
||||
# Consent management with OpenPanel
|
||||
|
||||
Privacy regulations like GDPR and CCPA require that you obtain explicit user consent before tracking behaviour or recording sessions. This guide shows how to use OpenPanel's built-in queue to hold all tracking until the user makes a choice, then flush everything at once—or discard it silently on decline.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- OpenPanel installed via the `@openpanel/web` npm package or the script tag
|
||||
- Your Client ID from the [OpenPanel dashboard](https://dashboard.openpanel.dev/onboarding)
|
||||
|
||||
## Initialise with `disabled: true` [#disable]
|
||||
|
||||
Pass `disabled: true` when creating the OpenPanel instance. All tracking calls (`track`, `identify`, `screenView`, session replay chunks) are held in an in-memory queue instead of being sent to the API.
|
||||
|
||||
### Script tag
|
||||
|
||||
```html title="index.html"
|
||||
<script>
|
||||
window.op=window.op||function(){var n=[];return new Proxy(function(){arguments.length&&n.push([].slice.call(arguments))},{get:function(t,r){return"q"===r?n:function(){n.push([r].concat([].slice.call(arguments)))}} ,has:function(t,r){return"q"===r}}) }();
|
||||
window.op('init', {
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
disabled: true,
|
||||
});
|
||||
</script>
|
||||
<script src="https://openpanel.dev/op1.js" defer async></script>
|
||||
```
|
||||
|
||||
### NPM package
|
||||
|
||||
```ts title="op.ts"
|
||||
import { OpenPanel } from '@openpanel/web';
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
disabled: true,
|
||||
});
|
||||
```
|
||||
|
||||
From this point on, any `op.track(...)` calls elsewhere in your app are safely queued and not transmitted.
|
||||
|
||||
## Build a consent banner [#banner]
|
||||
|
||||
How you build the UI is up to you. The key is to call `op.ready()` when the user accepts, and do nothing (or call `op.clear()`) when they decline.
|
||||
|
||||
```tsx title="ConsentBanner.tsx"
|
||||
import { op } from './op';
|
||||
|
||||
export function ConsentBanner() {
|
||||
function handleAccept() {
|
||||
localStorage.setItem('consent', 'granted');
|
||||
op.ready(); // flushes the queue and enables all future tracking
|
||||
hideBanner();
|
||||
}
|
||||
|
||||
function handleDecline() {
|
||||
localStorage.setItem('consent', 'denied');
|
||||
hideBanner(); // queue is discarded on page unload
|
||||
}
|
||||
|
||||
return (
|
||||
<div role="dialog" aria-label="Cookie consent">
|
||||
<p>
|
||||
We use analytics to improve our product. Do you consent to anonymous
|
||||
usage tracking?
|
||||
</p>
|
||||
<button type="button" onClick={handleAccept}>Accept</button>
|
||||
<button type="button" onClick={handleDecline}>Decline</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Call `ready()` on consent [#ready]
|
||||
|
||||
`op.ready()` does two things:
|
||||
|
||||
1. Clears the `disabled` flag so all future events are sent immediately
|
||||
2. Flushes the entire queue — every event and session replay chunk buffered since page load is sent at once
|
||||
|
||||
This means you don't lose any events that happened before the user made their choice. The screen view for the page they landed on, any clicks they made while the banner was visible—all of it is captured and sent the moment they consent.
|
||||
|
||||
## Handle session replay [#replay]
|
||||
|
||||
If you have session replay enabled, the recorder starts capturing DOM changes as soon as the page loads (so no interactions are missed), but no data leaves the browser until `ready()` is called.
|
||||
|
||||
```ts title="op.ts"
|
||||
export const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
disabled: true,
|
||||
sessionReplay: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
On `op.ready()`, buffered replay chunks flush along with the queued events. The full session from the start of the page load is preserved.
|
||||
|
||||
## Handle decline [#decline]
|
||||
|
||||
If the user declines, don't call `ready()`. The queue lives only in memory and is automatically discarded when the tab closes or the page navigates away. No data is ever sent.
|
||||
|
||||
If you want to be explicit, you can clear the queue immediately:
|
||||
|
||||
```ts
|
||||
function handleDecline() {
|
||||
localStorage.setItem('consent', 'denied');
|
||||
// op stays disabled — nothing will be sent
|
||||
// The in-memory queue will be garbage collected
|
||||
}
|
||||
```
|
||||
|
||||
## Persist consent across page loads [#persist]
|
||||
|
||||
The `disabled` flag resets on every page load. You need to check the stored consent choice on initialisation and skip `disabled: true` if consent was already granted.
|
||||
|
||||
```ts title="op.ts"
|
||||
import { OpenPanel } from '@openpanel/web';
|
||||
|
||||
const hasConsent = localStorage.getItem('consent') === 'granted';
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
disabled: !hasConsent,
|
||||
});
|
||||
```
|
||||
|
||||
And in your banner component, only show it when no choice has been stored:
|
||||
|
||||
```tsx title="ConsentBanner.tsx"
|
||||
export function ConsentBanner() {
|
||||
const stored = localStorage.getItem('consent');
|
||||
if (stored) return null; // already decided, don't show
|
||||
|
||||
// ... render banner
|
||||
}
|
||||
```
|
||||
|
||||
## Full example
|
||||
|
||||
```ts title="op.ts"
|
||||
import { OpenPanel } from '@openpanel/web';
|
||||
|
||||
const hasConsent = localStorage.getItem('consent') === 'granted';
|
||||
|
||||
export const op = new OpenPanel({
|
||||
clientId: 'YOUR_CLIENT_ID',
|
||||
trackScreenViews: true,
|
||||
disabled: !hasConsent,
|
||||
sessionReplay: {
|
||||
enabled: true,
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
```tsx title="ConsentBanner.tsx"
|
||||
import { op } from './op';
|
||||
|
||||
export function ConsentBanner() {
|
||||
if (localStorage.getItem('consent')) return null;
|
||||
|
||||
return (
|
||||
<div role="dialog" aria-label="Cookie consent">
|
||||
<p>We use analytics to improve our product. Do you consent?</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
localStorage.setItem('consent', 'granted');
|
||||
op.ready();
|
||||
}}
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
localStorage.setItem('consent', 'denied');
|
||||
}}
|
||||
>
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [Consent management docs](/docs/consent-management) — quick reference for `disabled` and `waitForProfile`
|
||||
- [Session replay](/docs/session-replay) — privacy controls for what gets recorded
|
||||
- [Identify users](/docs/get-started/identify-users) — link events to a user profile
|
||||
|
||||
<Faqs>
|
||||
<FaqItem question="Are events lost if the user declines?">
|
||||
No events are sent if the user declines and you never call `ready()`. The queue lives in memory and is discarded when the page unloads.
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="What happens to events tracked before the banner appears?">
|
||||
They sit in the queue. If the user later accepts, they are all flushed. If the user declines, they are discarded.
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Does session replay start before consent?">
|
||||
The recorder starts capturing DOM changes immediately so the full session can be reconstructed, but nothing is transmitted until `ready()` is called.
|
||||
</FaqItem>
|
||||
|
||||
<FaqItem question="Do I need to handle this differently for the script tag vs npm?">
|
||||
No. The `disabled` option and `ready()` method work the same in both.
|
||||
</FaqItem>
|
||||
</Faqs>
|
||||
Reference in New Issue
Block a user