Consent

Headless and reactive. Declare what the app wants to track, tag emits with a category, and the facade gates delivery on the decisions. No UI ships here: a signal tells you exactly what to prompt for.

@mmstack/telemetry-corenpm

#Declaring requirements

Pass a consent config to provideTelemetry. Each requirement names a category and, optionally, the sink it applies to. An emit opts into a category through the last argument.

import { provideTelemetry } from '@mmstack/telemetry-core';
import { posthogSink } from '@mmstack/telemetry-posthog';

provideTelemetry({
  sinks: [posthogSink({ posthog })],
  consent: {
    requirements: [
      { id: 'analytics', category: 'analytics', purpose: 'Anonymous product analytics' },
    ],
  },
});

telemetry.event('checkout', { plan: 'pro' }, { category: 'analytics' });

#Prompting and deciding

pending() is exactly the set of requirements without a decision yet, so it is what you render a prompt for. decide(id, granted) records an answer, and (with a store) persists it. Because requirements can be a signal, when a new section of the app needs new tracking, pending() becomes just the delta, and you re-prompt for only the new items.

// in your consent prompt component
const pending = telemetry.pending();  // requirements still needing an answer

telemetry.decide('analytics', true);  // grant
telemetry.decide('analytics', false); // deny

#How gating works

The default mode is 'required': a categorized emit needs an explicit grant, and an undecided or undeclared category is dropped. You cannot track something you did not ask consent for. Set mode: 'implicit' to flip that to opt-out. Uncategorized emits are never gated.

While an async store hydrates a returning visitor's decisions, an undecided emit is held briefly and then delivered or dropped according to what was stored, so a stored denial is never raced by an early emit.

#Persistence

localStorageConsentStore() persists decisions in the browser and is a noop on the server. The ConsentStore interface allows an async get/set, so a server-side store works the same way.

import { localStorageConsentStore, provideTelemetry } from '@mmstack/telemetry-core';

provideTelemetry({
  sinks: [posthogSink({ posthog })],
  consent: {
    requirements: [{ id: 'analytics', category: 'analytics', purpose: '...' }],
    store: localStorageConsentStore(),
  },
});