Hero
Back to all guides

Consent management with OpenPanel

Learn how to queue analytics events and session replays until the user gives consent, then flush everything at once with a single call.

OpenPanel Team

2026-03-02

Updated on 2026-03-02

Beginner
15 min

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

Initialise with disabled: true

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

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

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.

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.

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

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

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.

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

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:

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

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.

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:

ConsentBanner.tsx
export function ConsentBanner() {
  const stored = localStorage.getItem('consent');
  if (stored) return null; // already decided, don't show

  // ... render banner
}

Full example

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,
  },
});
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>
  );
}

Loved by builders everywhere

From indie hackers to global teams, OpenPanel helps people understand their users effortlessly.

  • Steven Tey
    Steven Tey
    @steventey

    Open-source Mixpanel alternative just dropped → http://git.new/openpanel

    It combines the power of Mixpanel + the ease of use of @PlausibleHQ into a fully open-source product.

    Built by @CarlLindesvard and it’s already tracking 750K+ events 🤩

  • Pontus Abrahamsson - oss/acc
    Pontus Abrahamsson - oss/acc
    @pontusab

    Thanks, OpenPanel is a beast, love it!

  • Piotr Kulpinski
    Piotr Kulpinski
    @piotrkulpinski

    The Overview tab in OpenPanel is great. It has everything I need from my analytics: the stats, the graph, traffic sources, locations, devices, etc.

    The UI is beautiful ✨ Clean, modern look, very pleasing to the eye.

  • greg hodson 🍜
    greg hodson 🍜
    @h0dson

    i second this, openpanel is killing it

  • Jacob 🍀 Build in Public
    Jacob 🍀 Build in Public
    @javayhuwx

    🤯 wow, it's amazing! Just integrate @OpenPanelDev into http://indiehackers.site last night, and now I can see visitors coming from all round the world.

    OpenPanel has a more beautiful UI and much more powerful features when compared to Umami.

    #buildinpublic #indiehackers

  • Lee
    Lee
    @DutchEngIishman

    Day two of marketing.

    I like this upward trend..

    P.S. website went live on Sunday

    P.P.S. Openpanel by @CarlLindesvard is awesome.

  • Thomas Sanlis
    Thomas Sanlis
    @T_Zahil

    We're now sponsoring @OpenPanelDev with Uneed 🥳

    If you're looking for open source analytics, OpenPanel is BY FAR the best I've ever seen

    Bonus: 1-click install on Coolify 🥰

Ready to understand your users better?
Start tracking in minutes

Join thousands of companies using OpenPanel. Free 30-day trial, no credit card required. Self-host for free or use our cloud.

Start free trial