Consent Management

Implement GDPR-compliant tracking with IAB TCF 2.0 support.

Overview

Alke Analytics is designed for privacy compliance. The SDK:

  • Reads consent status from your CMP (Consent Management Platform)
  • Adapts data collection based on user choices

How It Works

The SDK automatically listens for the IAB TCF 2.0 __tcfapi:

  1. Detects if a CMP is present
  2. Listens for consent changes
  3. Queues events while CMP UI is shown
  4. Releases/discards events based on consent decision
// No configuration needed - automatic detection
alkeAnalytics.setConfig({
    propertyId: 'your-property-uuid'
});
PurposeIDDescription
Store/access information1Required for cookies (userId, sessionId)
Measure advertising7Ad creative tracking
Understand audiences9Analytics and segmentation
Develop/improve services10Performance metrics

No-exemption mode

By default the SDK collects data under the consent exemption: reports are sent immediately and individual sensitive fields are degraded until the relevant consent is granted.

Set requireExplicitConsent: true to switch to no-exemption mode. While consent applies, no report leaves the browser until the user grants explicit consent to the Alke Tech vendor (IAB TCF vendor 1506):

alkeAnalytics.setConfig({
    propertyId: 'your-property-uuid',
    requireExplicitConsent: true
});

Behaviour:

  • The tag keeps working normally — pageview/event tracking and ad-request targeting (alke_id) run as usual, so you never miss ad requests. Only the transmission of data to the Alke Analytics servers is held back. Cookie behaviour (alke_u, alke_s, alke_v) is unchanged compared to the default mode — cookies still follow the same consent rules and are not written before consent regardless of this option.

  • While consent is pending, reports are buffered client-side, keyed per logical event (a counter per tracking call; the video id for videos — not the pageview id). If the same event is updated (for example a partial then a final report, or video heartbeats), only its most recent version is kept. Two distinct events (e.g. a pageview and a custom event) remain separate reports.

  • As soon as explicit vendor consent is granted, all pending reports are flushed at once — each logical event sent once, at its most recent version — and every later report is sent without delay.

  • If explicit consent is never obtained, no data is sent at all and the buffered reports are dropped when the page unloads. This is the intended behaviour of the mode — without verifiable explicit consent, nothing is reported (unlike the default mode, which would still send degraded data).

  • Explicit vendor consent is the only thing that releases a report. Consent is not required is not consent was given, so none of the following sends anything: a refusal, no decision, a CMP that errors or never loads, a CMP blocked by an ad blocker, a cmpId absent from the bundled allowlist, a CMP reporting gdprApplies = false, and a visitor outside the GDPR perimeter.

    This is the trade-off of the mode, and it is deliberate: traffic that never consents is never measured. If you want the consent exemption to keep carrying that traffic, use the default mode. Cookie behaviour and the consent-wrapped fields are unaffected — they keep following their own rules, identical in both modes.

  • The client-side buffer is bounded (up to 1000 reports, de-duplicated per logical event); beyond that the least recently updated held report is dropped (a continuously re-reported event, like the current pageview or video heartbeats, is kept). A normal page never reaches this limit — it just caps memory on a long-lived SPA that never obtains consent.

Use this mode when a publisher requires that analytics collection be strictly gated behind explicit consent rather than relying on the legitimate-interest / exemption basis.

Available on the native SDKs too, with one platform caveat — see the iOS, Android and Tizen tabs.

The Report Always Goes Out

Consent degrades a report; it never suppresses one. A reader who refused, and a reader who has not answered yet, both produce a report — with vq = 0, no nl, no campaign, and cr telling the two apart.

This matters for your numbers: until this change the iOS and tvOS SDKs dropped a refused reader’s report entirely, so Apple traffic never appeared in the unknown engagement bucket and any web-versus-app volume comparison was short by your refusal rate. Expect a step up in reported app pageviews when this ships.

Campaign parameters are gated too. utm_source, utm_medium, utm_campaign, utm_term and utm_content require purposes 1, 7 and 9 — the same ones the web tag requires — and are simply absent without them. The campaign still drives the arrival category, the newsletter rule and the visit count internally: knowing it is not reporting it.

The SDKs follow the web contract: an identifier is always produced and always sent. Consent decides only whether it is kept.

Consent stateIdentifier reportedStored identifier
Not required, or acceptedthe stored one, created if absentwritten
Required, no answer yetthe stored one if any, otherwise ephemeralleft alone — neither created nor deleted
Refusedthe one the page started withdeleted

Two consequences worth planning for:

  • Granting consent promotes, it does not renumber. An identifier the current page has already reported is written to storage as-is, so one page never reports two readers. Refusing and later accepting, on the other hand, yields a new identifier: the erased one is never recovered.
  • Without storage consent the identifier lives for one page, regenerated at every pushNavigation(). On native this is deliberately stricter than the web tag, which keeps one identifier for the whole document — so a single-page web app reports one reader where a native app reports one per screen. Expect a one-off rise in “new users” proportional to your refusal rate when this ships.

On iOS and tvOS the stored identifier lives in the Keychain, which survives app deletion — which is exactly why a refusal deletes it rather than leaving it in place.

The same three states apply to the session and to the buffered report queue, with one distinction the user ID does not have: a read is not a write.

Consent stateReadingWriting
Not required, or acceptedpasseswritten
Required, no answer yetpassesnothing
Refusednothing, and what was left is purgednothing

Reads pass while the CMP has not answered because the web tag does the same — it reads alke_s unconditionally and gates only the write — so a session opened before the answer keeps being used once it arrives.

Two consequences:

  • Without storage consent the session does not survive an app restart, so pageNumberInSession starts again at 1 on each launch. It still grows from screen to screen within one launch, where the web restarts at 1 on every page load: a native screen is not a document load.
  • The offline queue is kept in memory only, so events buffered without storage consent are lost if the process is killed. The web behaves the same way, dropping its buffer at unload.

Wrap sensitive values so they’re only sent with consent. The wrapper checks both vendor consent (Alke vendor ID: 1506) and purpose consent before sending the actual value.

// holdUntilConsent(value, fallback, purposes)

// Email only sent with purpose 1 consent
alkeAnalytics.setCustomData(1,
    alkeAnalytics.holdUntilConsent(userEmail, null, [1])
);

// User ID with anonymous fallback
alkeAnalytics.setCustomData(2,
    alkeAnalytics.holdUntilConsent(userId, "anonymous", [1, 9])
);

// Named dimensions accept the same wrapper
alkeAnalytics.setDimension('goal',
    alkeAnalytics.holdUntilConsent('subscription', 'account', [1, 7])
);

On setDimension(), both branches of the wrapper are validated — the consented value and the fallback. A fallback outside the dimension’s accepted shape rejects the whole call and returns false, rather than being accepted here and dropped on arrival. See Content Dimensions.

Behavior

If consent is not granted for all required purposes:

  • null fallback → dimension not sent
  • String fallback → fallback value sent instead

The consent status is checked at send time, so if consent changes after setting the wrapper, the latest status is used.

Blocking Tracking (Web)

Allow users to opt-out completely, regardless of CMP:

// Permanently block tracking (sets cookie for 13 months)
alkeAnalytics.blockTracking();

// Re-enable tracking
alkeAnalytics.unblockTracking();

This sets a alke_no_tracking cookie that persists across sessions.

Event Queuing

When a CMP UI is displayed (consent not yet given):

  1. Events are queued (not dropped)
  2. After 30 seconds without decision, queued events are sent with partial data
  3. When consent is given, queued events are sent with full data
  4. When consent is refused, sensitive fields are stripped
No Data Loss
Pageviews are never lost. If consent is refused, they’re sent without consent-dependent fields (user ID, UTM parameters, etc.).

Personal Data Scrubbing (Web)

UTM parameters arrive from the URL, where you do not control what a third party may have put in them. The five of them — utm_source, utm_medium, utm_campaign, utm_term, utm_content — are scanned before being reported, and a value shaped like personal or identifying data is discarded rather than stored:

  • an email address
  • a UUID
  • a hash digest: MD5, SHA-1, SHA-256, SHA-384, SHA-512

This is a floor, not a consent-gated behaviour: it applies whatever the consent state. It covers the UTM parameters and the URL dimensions pageUrl and assetUrl, whether the collector derived them or you set them yourself. Everything else you set — setCustomData() and the non-URL dimensions — is reported as given, so wrap anything sensitive with holdUntilConsent().

CMP Compatibility

The SDK works with any IAB TCF 2.0 compliant CMP, including:

  • AppConsent
  • Cookiebot
  • Didomi
  • OneTrust
  • Quantcast Choice
  • Sourcepoint
  • TrustArc
CMP Validation
The SDK validates the CMP ID against the official IAB vendor list. Unregistered CMPs are ignored.