npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

brixon-tracking

v0.8.0

Published

Brixon Website-Tracking v2 — typed event catalog, consent-gated PostHog boot, destination adapters, bx_uid identity utils and server-truth capture. See SYSTEM-tracking/docs/website-tracking-v2.md

Readme

brixon-tracking

Brixon Website-Tracking v2 als npm-Paket (M5): typisierter Event-Katalog, track() mit Destination-Adaptern (PostHog als Backbone, GA4 gefiltert und adressiert, Browser-Conversions je Plattform), consent-gated PostHog-Boot, bx_uid-Identity-Utils und Server-Truth-Capture.

Konzept: SYSTEM-tracking/docs/website-tracking-v2.md (§4 Identity, §5 Consent, §6 Taxonomie, §6.1 Instrumentierungs-Standard). Event-Katalog geseedet aus SYSTEM-tracking/docs/events-foundation.md.

Changes in 0.8.0 (unreleased)

  • Added the csauerborn-website property contract, its purchase/newsletter conversions and the commerce lifecycle catalogue (checkout_started, order bump, upsell, purchase and payment status events). Event schemas now support booleans, string enums, string arrays and optional parameters.
  • Event authority (capture) and source conversion eligibility (adEligible) are independent generated fields. Only the canonical server conversion is destination-routable.
  • PostHogBoot now has route-level pageview, pageleave, exception and replay controls, registers source_app/page_variant before the initial pageview, uses personless SDK mode and retries the first-party UID request at most five times. Each attempt has a bounded timeout; revoke and lifecycle cleanup abort an in-flight UID request before PostHog can boot.
  • brixon-tracking/server includes a Worker-native Fetch Capture API client; brixon-tracking/astro includes an allowlisted, header-scrubbing PostHog ingest proxy.
  • The DW&P registry now covers every ads-eligible confirmed server-side form flow and explicitly excludes the corresponding browser echoes from conversion routing. Applications and feedback stay PostHog-only operational events.
  • PostHogBoot accepts an optional locale and registers it before the initial pageview.
  • createConversionTracker() exposes the actual delivered result so API routes can distinguish a successfully handled business request from a failed analytics delivery without throwing.
  • Browser configuration, declarative click listeners and reset state are shared across package entry points without creating state during server rendering.
  • Attribution snapshots and hidden fields no longer expose retired proxy identifiers or proprietary backup click-ID query parameters.
  • TrackingHiddenFields includes ttp; its optional business eventId remains separate from the attribution snapshot and is not persisted by the package.

Breaking Changes in 0.6.0

Fuenf Aenderungen, die eine bestehende Integration still veraendern koennen.

1. Das Paket sendet keine Consent-Kommandos mehr. initGoogleAnalyticsTag feuerte bis 0.5.1 gtag('consent','update',{analytics_storage:…}). Der CMP besitzt den Consent Mode jetzt vollstaendig — fuer gtag und fuer Microsoft UET. Wer sich dafuer auf das Paket verlassen hat, braucht den CMP. Der Grund, warum das traegt und warum die halbe Variante schlechter war: gtag wertet einen nicht gesetzten Consent-Typ als granted, nicht als denied. Der Default-Denied-Block des CMP ist also der eigentliche Schutz, und das Paket setzte nur analytics_storage, nie ad_storage, ad_user_data oder ad_personalization. Die einzige Garantie des Pakets bleibt: es laedt nie vor Consent.

2. googleAnalyticsAdapter filtert die Properties. An GA4 gehen nur event_id, value, currency und die im Event-Katalog deklarierten Parameter des jeweiligen Events; erweiterbar ueber allowProperties. Vorher wurde alles durchgereicht — inklusive email und phone_e164, wenn ein Aufrufer sie mitgab, denn die Event-Schemas sind bewusst looseObject. Praktische Folge: conversion_outcome und utm_* erreichen GA4 nicht mehr.

3. Der skipEvents-Default ist gewachsen — GA4_ENHANCED_MEASUREMENT_EVENTS statt nur page_view. GA4 Enhanced Measurement erfasst form_start, form_submit, video_start, video_progress und video_complete selbst, per Default aktiv fuer neue Web-Streams; das Paket feuerte sie ein zweites Mal obendrauf, und form_submit ist ueblicherweise Key Event und wird nach Google Ads importiert. Wer die Schalter im GA4-Stream abdreht und die reicheren Paket-Events will, uebergibt skipEvents: ["page_view"].

4. Ein gesetztes conversion_outcome faellt nicht mehr auf den Event-Namen zurueck. Wer conversion_outcome mitgibt und LinkedIn oder Google Ads bisher nur ueber die Event-Namen-Map konfiguriert hat, muss jetzt linkedinOutcomes bzw. googleOutcomes setzen — sonst feuern diese beiden Browser-Conversions nicht mehr. Das Paket warnt in dem Fall auf der Konsole, unabhaengig vom debug-Flag: still waere genau die Fehlerklasse, die diese Aenderung behebt. Grund fuer den Fail-closed: ein Tippfehler im Outcome liess den Browser einen anderen Event-Namen melden als der Server, und Meta wie TikTok deduplizieren auf (Event-Name, event_id) — beide Kopien blieben stehen.

5. dataLayerAdapter ist entfernt. Er existierte nur, um während einer GTM-Migration dieselben Events zusätzlich in window.dataLayer zu schieben. Es gibt keine Dual-Run-Phase, also war er toter Code — und zwar gefährlicher als der Rest: er reichte properties ungefiltert weiter, eine email aus einem track()-Aufruf landete damit in einer globalen Variable, die jedes Script auf der Seite lesen kann. Wer ihn registriert hatte, nimmt ihn aus configureTracking heraus. window.dataLayer selbst bleibt: gtag.js braucht die Queue als Command-Bus, das ist kein GTM-Datenmodell.

Entries

| Entry | Inhalt | Peers | |---|---|---| | brixon-tracking/posthog | Schlanker Browser-Einstieg mit track(), configureTracking(), posthogAdapter, startDataTrackListener, PostHog-Boot und Event-ID-Helpern; enthält keine Legacy-Destinations | posthog-js | | brixon-tracking/react/posthog | Schlanker React-Einstieg ausschließlich für PostHogBoot | react >=18, posthog-js | | brixon-tracking | eventCatalog/eventSchemas, track() + configureTracking(), Adapter posthogAdapter/googleAnalyticsAdapter/adConversionAdapter, startDataTrackListener, consent-gated Init fuer GA4, Meta, TikTok, LinkedIn, Google Ads und Microsoft UET, readTrackingParams(), Identity-/Click-ID-/Consent-/event_id-Helper | posthog-js |

brixon-tracking configures the shared Zod module with jitless: true before creating its schemas. This is intentional: strict CSPs report Zod's otherwise caught Function capability probe as a violation. Consumers sharing that Zod instance therefore use the interpreter instead of generated validators. | brixon-tracking/react | PostHogBoot, ScrollDepthTracker, useTrackingParams, TrackingHiddenFields, GoogleAnalyticsTag, MetaPixel, TikTokPixel, LinkedInInsight, GoogleAdsTag, MicrosoftUet | react >=18, posthog-js | | brixon-tracking/server | createConversionTracker() (kanonischer conversion-Vertrag), Worker-native createPostHogFetchClient(), trackCanary(), trackServer() (Low-Level-Events), createUidRouteHandler() (Web-Standard Request/Response — Next, Astro, Workers), readBxUid(), createConversionEventId() | posthog-node >=5 optional; Fetch-Client ohne Peer | | brixon-tracking/astro | initPostHogAstro(), gehärteter createPostHogIngestProxy(), Cloudflare-Experiment-Delivery, HTMLRewriter-/Island-Fallback, same-origin Exposure-Endpoint; Re-Exports der Ad-Pixel-Initialisierung und readTrackingParams() | posthog-js |

Astro Experiment Delivery auf Cloudflare

Der vollstaendige Integrations-, Bindings-, Diagnose- und Abnahmevertrag steht in docs/astro-experiment-delivery.md.

brixon-tracking/astro liefert serverseitige Textvarianten ohne D1- oder PostHog-Aufruf im Request-Hot-Path aus. Der Adapter liest ein signiertes, kleiner als 8 KB gehaltenes Manifest ueber ein Cloudflare Service Binding, cached dessen Revision maximal fuenf Sekunden und weist Varianten lokal per Web-Crypto-Hash zu. Jeder Fehler, Bot-Traffic, Selector-Konflikt oder Control-Text-Drift liefert die unveraenderte Control.

import {
  createAstroExperimentDelivery,
  handleIslandExperimentExposure,
  installIslandExperimentFallback,
} from "brixon-tracking/astro";

const deliverExperiments = createAstroExperimentDelivery({
  siteId: "website-dwp",
  propertyId: "dwp",
  controlPlane: env.EXPERIMENT_CONTROL_PLANE,
  exposureQueue: env.EXPERIMENT_EXPOSURES,
  exposureStateSecret: env.EXPERIMENT_STATE_SECRET,
  operationalMetrics: env.EXPERIMENT_METRICS,
});

export default {
  async fetch(request, env, context) {
    const url = new URL(request.url);
    if (url.pathname === "/__brixon/experiment-exposure") {
      return handleIslandExperimentExposure({
        request,
        siteId: "website-dwp",
        propertyId: "dwp",
        queue: env.EXPERIMENT_EXPOSURES,
        exposureStateSecret: env.EXPERIMENT_STATE_SECRET,
        operationalMetrics: env.EXPERIMENT_METRICS,
      });
    }
    const astroResponse = await env.ASTRO.fetch(request);
    return deliverExperiments({ request, response: astroResponse, executionContext: context });
  },
};

Den Browser-Fallback einmal im Client-Bootstrap installieren. Er greift nur bei spaet gerenderten Islands, prueft Selector und Control-Hash erneut und meldet die Exposure same-origin; die HttpOnly-Identitaet bleibt im Worker.

installIslandExperimentFallback();

Der eigentliche Astro-/Asset-Fetch bleibt gemeinsam cachebar; erst die danach erzeugte personalisierte HTML-Response erhaelt private, no-store. Sichere Textsetzung (html: false) ist absichtlich die einzige v2-Mutation.

Erfolgreiche Exposures werden in einem HMAC-signierten, HttpOnly bx_exp_state-Cookie mit hoechstens fuenf aktiven Experimenten markiert. Eine erneute Seite wendet dieselbe Variante weiterhin an, erzeugt aber kein zweites Exposure. Island-Payloads sind ebenfalls signiert; der same-origin Endpoint stellt bx_uid serverseitig wieder her und weist gefaelschte Varianten ab.

Manifest-Eintraege tragen einen expliziten deliveryMode. Der normale Modus experiment erzeugt die deduplizierte Exposure. Der ausschliesslich von der Control Plane signierte Modus promotion_override liefert einen zertifizierten Gewinner zu 100 %, unterdrueckt jedoch Queue-Event, Island-Report und bx_exp_state-Eintrag. So kann die permanente Source-of-Truth-Aenderung verifiziert werden, ohne eine zweite Messphase zu erfinden.

Experimentteilnehmer erhalten bx_uid bewusst bereits vor dem allgemeinen Analytics-Consent. Vor Consent wird ausschliesslich die pseudonyme Exposure und die notwendige Server-Truth-Lead-Zuordnung erfasst; Autocapture, Replay und Werbepixel bleiben consent-gated. Dokumentierte Rechtsgrundlage, Retention und Datenschutzhinweis sind Go-live-Gates, keine Annahmen des Pakets.

Quickstart (Next.js App Router)

// layout.tsx (Client-Bereich)
import { PostHogBoot, ScrollDepthTracker } from "brixon-tracking/react";

<PostHogBoot
  token={process.env.NEXT_PUBLIC_POSTHOG_PROJECT_TOKEN}
  propertyId="dwp"
  environment={process.env.NODE_ENV === "production" ? "production" : "development"}
  sourceApp="dwp-website"
  capturePageview="history_change"
  capturePageleave
  captureExceptions
  sessionReplay={{ enabled: true, maskAllInputs: true }}
  debug={process.env.NODE_ENV === "development"}
  isolateNetwork={process.env.NEXT_PUBLIC_E2E_MODE === "1"}
/>

isolateNetwork keeps same-origin capture available for browser assertions, but disables PostHog remote config, recording and lazy-loaded extensions. Use it only in isolated browser suites, not as a production privacy setting.

Bei einem Hidden-/Prerender-Dokument bleibt bereits posthog.init() pending, damit der SDK-eigene Visibility-Guard keinen spaeter verworfenen initialen Pageview als erledigt markieren kann. Sobald das Dokument sichtbar ist und der externe Analytics-Grant weiterhin gilt, registriert der loaded-Callback property_id und environment, spiegelt den Grant mit captureEventName: false in posthog-js und dispatcht danach brixon:posthog-boot. Dadurch enthaelt der einmalige initiale $pageview beide Properties, es entsteht kein $opt_in, und Listener des Boot-Signals koennen sofort sicher tracken. Ein Widerruf waehrend des Pending-Boots verhindert den Init; ein spaeterer sichtbarer Re-Grant finalisiert den Boot genau einmal. Jeder UID-Versuch hat standardmaessig ein Timeout von fuenf Sekunden (uidRequestTimeoutMs, maximal 15 Sekunden). Consent-Widerruf und Cleanup brechen den aktiven Request ab; ein Re-Grant waehrend dieses Abbruchs startet nach Freigabe des Boot-Guards mit einem frischen Consent-Epoch neu. Nach einem Widerruf wird außerdem die alte In-memory-Identität sofort verworfen. Ein Re-Grant holt eine neue anonyme bx_uid und beginnt damit eine neue PostHog-Session; Browser- und Server-Events verwenden die vom Consent-Endpunkt gelöschte Identität nicht weiter. Der posthogAdapter puffert waehrend eines bereits consent-granted, paketverwalteten Boots hoechstens 50 Events. Ein Revoke leert diese Queue; Events ohne Consent werden nicht bei einem spaeteren Grant nachgesendet.

Routen mit sensitiven Inhalten schalten die nativen Funktionen explizit aus:

initPostHogAstro({
  token: import.meta.env.PUBLIC_POSTHOG_KEY,
  propertyId: "csauerborn",
  sourceApp: "csauerborn-website",
  capturePageview: false,
  capturePageleave: false,
  captureExceptions: false,
  sessionReplay: { enabled: false },
});
// Einmalig im Client-Bootstrap (z. B. eigene TrackingProvider-Komponente):
import {
  configureTracking,
  posthogAdapter,
  googleAnalyticsAdapter,
  adConversionAdapter,
  startDataTrackListener,
} from "brixon-tracking";

configureTracking({
  destinations: [
    posthogAdapter(),
    googleAnalyticsAdapter(),
    adConversionAdapter({
      // Prefer registry outcomes when one generic RESPOND event serves several forms.
      // With the fallback off, only an explicit commercial outcome reaches Meta.
      useEventNameFallback: false,
      // metaEvents: {} override optional (Default-Map deckt die Standard-Events ab)
      // LinkedIn needs the browser conversion-rule IDs:
      // linkedinOutcomes: { sales_lead: 123456 },
    }),
  ],
  validate: process.env.NODE_ENV === "development",
});
startDataTrackListener();

RESPOND-Vertrag

Ein RESPOND-Call sendet den beschreibenden Namen, zum Beispiel assessment_complete, ueber den posthogAdapter an PostHog. Nach erfolgreicher Transaktion sendet createConversionTracker() zusaetzlich genau einen autoritativen serverseitigen Event-Record namens conversion; der beschreibende Name steht dort in source_event. capture: "server" bezeichnet diese Autoritaet; adEligible klassifiziert davon unabhaengig, ob der Source-Event eine kanonische Conversion erzeugen darf. Keines von beiden ist eine Client-Skip-Regel. Ads-Destinationen duerfen ausschliesslich conversion filtern, nie die beschreibenden Client-Events.

// src/app/api/uid/route.ts
import { createUidRouteHandler } from "brixon-tracking/server";
export const POST = createUidRouteHandler({
  secure: process.env.NODE_ENV === "production",
});
// Conversion-Route (Server-Truth):
import pkg from "brixon-tracking/package.json";
import {
  createConversionTracker,
  createConversionEventId,
  readBxUid,
} from "brixon-tracking/server";
import { PostHog } from "posthog-node";

const client = new PostHog(process.env.POSTHOG_KEY!, {
  host: "https://eu.i.posthog.com", flushAt: 1, flushInterval: 0,
});
const trackConversion = createConversionTracker({
  sourceApp: "borggalea-website",
  environment: process.env.NODE_ENV === "production" ? "production" : "development",
  schemaVersion: pkg.version,
  resolveConsent: (request) => ({
    allowed: /(?:^|;\\s*)brixon_consent=accepted(?:;|$)/.test(
      "headers" in request ? request.headers.get("cookie") ?? "" : request.cookieHeader ?? ""
    ),
    source: "brixon_consent_cookie",
    policyVersion: "2026-07",
    observedAt: new Date().toISOString(),
  }),
  delivery: "immediate",
});
const eventId = createConversionEventId(); // gleiche ID → PostHog, n8n, Client-Echo
await trackConversion({
  client,
  sourceEvent: "contact_form_submitted",
  request: req,
  currentUrl: conversionUrl,
  bxUid: readBxUid(req.headers.get("cookie")),
  eventId,
  sessionId: phSessionId, // Hidden Field
  properties: { email, form_name: "contact" },
});

external_id_sha256 ist ein optionales Meta-Match-Signal. Der Aufrufer darf es nur aus einer echten bx_uid bilden, wenn Analytics-Consent vorliegt, und muss den bereits normalisierten SHA-256-Wert übergeben. Bei Marketing-Consent ohne Analytics-Consent bleibt das Feld weg; die event-spezifische no-consent-<event_id>-Identität darf niemals als external_id gehasht werden.

Cloudflare Worker brauchen posthog-node nicht:

import {
  createConversionTracker,
  createPostHogFetchClient,
} from "brixon-tracking/server";
import { createPostHogIngestProxy } from "brixon-tracking/astro";

const proxyPostHog = createPostHogIngestProxy();

export default {
  async fetch(request: Request, env: Env) {
    const url = new URL(request.url);
    if (url.pathname.startsWith("/ingest/")) return proxyPostHog(request);

    const client = createPostHogFetchClient({
      token: env.PUBLIC_POSTHOG_KEY,
      timeoutMs: 10_000,
    });
    // Pass `client` to createConversionTracker(...)(...). Stable eventId and
    // timestamp still come from the site's durable outbox. Its saved request
    // context can be passed as { cookieHeader, userAgent }.
  },
};

Der Proxy akzeptiert nur die vom gepinnten posthog-js verwendeten Capture-, Flags-, Replay- und Static-Pfade sowie exakt /array/<project-token>/config.js für die Remote-Konfiguration. Er entfernt Cookies, Authorization und eingehende Forwarding-/Cloudflare-Header, setzt X-Forwarded-For ausschliesslich aus CF-Connecting-IP und verwirft Upstream-Set-Cookie. POST-Bodies sind auf 5 MiB begrenzt: deklarierte Uebergroesse wird vor dem Lesen abgewiesen, Streams ohne Content-Length werden waehrend des Lesens begrenzt. Das laesst deutlichen Spielraum fuer komprimierte Replay-Batches, verhindert aber unbeschraenkten Isolate-Speicher. Explizit fremde Origin-/Sec-Fetch-Site: cross-site-Requests werden blockiert; normale same-origin sendBeacon-Requests und Clients ohne diese Browser-Header bleiben kompatibel.

Deklaratives Klick-Tracking (GTM-Trigger-Ersatz)

<!-- CTA-Shortcut (Pilot-Muster) -->
<a href="/kontakt" data-track-cta="hero">Jetzt anfragen</a>

<!-- Beliebiges Katalog-Event -->
<button data-track="assessment_start"
        data-track-assessment-name="quickcheck"
        data-track-assessment-type="ai">Start</button>

Ad-Pixel-Basis-Tags (M7-Cutover)

Consent-gated Basis-Snippets fuer GA4, Meta Pixel, TikTok Pixel, LinkedIn Insight Tag, Google Ads und Microsoft UET. Meta/TikTok/LinkedIn/Ads/UET verwenden das Marketing-Consent-Flag (window.__brixonConsentMarketing === true), GA4 das Analytics-Consent-Flag (window.__brixonConsentAnalytics === true). Die Listener-Mechanik ist jeweils per consentGranted/consentEventNames konfigurierbar.

consentGranted muss Live-State lesen, keine eingefrorene Closure. Die React-Wrapper leiten den aktuellen Consent-Wert im Render ab und nutzen ihn als Effect-Dependency, und sie reichen dem Core eine stabile Bruecke statt der Closure vom Mount. Das ist noetig, weil React 18 setState innerhalb des CMP-Dispatches batcht: im selben Tick sieht das Gate sonst noch den alten Wert, danach rendert React neu — aber nichts prueft den Consent erneut, und das Pixel bliebe trotz Zustimmung aus.

// layout.tsx (Client-Bereich) — fehlende Env-Var = Kill-Switch (No-op):
import {
  GoogleAnalyticsTag,
  MetaPixel,
  TikTokPixel,
  LinkedInInsight,
  GoogleAdsTag,
  MicrosoftUet,
} from "brixon-tracking/react";

<GoogleAnalyticsTag measurementId={process.env.NEXT_PUBLIC_GA4_MEASUREMENT_ID} />
<MetaPixel pixelId={process.env.NEXT_PUBLIC_META_PIXEL_ID} />
<TikTokPixel pixelId={process.env.NEXT_PUBLIC_TIKTOK_PIXEL_ID} />
<LinkedInInsight partnerId={process.env.NEXT_PUBLIC_LINKEDIN_PARTNER_ID} />
<GoogleAdsTag conversionId={process.env.NEXT_PUBLIC_GOOGLE_ADS_ID} />
<MicrosoftUet tagId={process.env.NEXT_PUBLIC_MICROSOFT_UET_TAG_ID} />
// Astro (Client-Script) — Re-Exports aus brixon-tracking/astro:
import {
  initGoogleAnalyticsTag,
  initMetaPixel,
  initTikTokPixel,
  initLinkedInInsight,
  initMicrosoftUet,
} from "brixon-tracking/astro";

initGoogleAnalyticsTag({
  measurementId: import.meta.env.PUBLIC_GA4_MEASUREMENT_ID,
  consentEventNames: ["brixon:consent"],
});
initMetaPixel({
  pixelId: import.meta.env.PUBLIC_META_PIXEL_ID,
  consentEventNames: ["brixon:consent"],
  consentGranted: () => window.__brixonConsentMarketing === true,
});
initTikTokPixel({
  pixelId: import.meta.env.PUBLIC_TIKTOK_PIXEL_ID,
  consentEventNames: ["brixon:consent"],
  consentGranted: () => window.__brixonConsentMarketing === true,
});
initLinkedInInsight({
  partnerId: import.meta.env.PUBLIC_LINKEDIN_PARTNER_ID,
  consentEventNames: ["brixon:consent"],
  consentGranted: () => window.__brixonConsentMarketing === true,
});
initMicrosoftUet({
  tagId: import.meta.env.PUBLIC_MICROSOFT_UET_TAG_ID,
  consentEventNames: ["brixon:consent"],
  consentGranted: () => window.__brixonConsentMarketing === true,
});

Env-Var-Konvention (Next.js; fuer Astro dieselben Namen mit PUBLIC_ statt NEXT_PUBLIC_):

| Var | Plattform | Consent | Format | |---|---|---|---| | NEXT_PUBLIC_GA4_MEASUREMENT_ID | GA4 | Analytics | G-XXXXXXXXXX | | NEXT_PUBLIC_META_PIXEL_ID | Meta Pixel | Marketing | numerisch | | NEXT_PUBLIC_TIKTOK_PIXEL_ID | TikTok Pixel | Marketing | Pixel-Code | | NEXT_PUBLIC_LINKEDIN_PARTNER_ID | LinkedIn Insight | Marketing | numerisch | | NEXT_PUBLIC_GOOGLE_ADS_ID | Google Ads | Marketing | AW-XXXXXXXXX | | NEXT_PUBLIC_MICROSOFT_UET_TAG_ID | Microsoft UET | Marketing | numerische Tag-ID |

Eine fehlende oder leere Var ist ein bewusster No-op: kein Script, kein Global, kein Netzwerk-Request. Das ist der Kill-Switch ohne Deploy.

Regeln:

  • Es laedt nur die offiziellen Basis-Snippets, jeweils genau einmal: GA4/Google Ads gtag.js, Meta fbevents.js, TikTok events.js und LinkedIn insight.min.js. Conversion-Events feuert der separate adConversionAdapter.
  • GA4 nutzt den normalen Tracking-Schnipsel ohne Gateway oder GTM. gtag.js braucht intern weiterhin window.dataLayer als Command-Queue; das Paket legt sie selbst an. Es gibt keinen fachlichen GTM-Data-Layer.
  • trackInitialPageView: false schaltet bei GA4 den initialen Config-Pageview aus; bei Meta und TikTok zusaetzlich den paketverwalteten History-Hook.
  • GoogleAnalyticsTag und GoogleAdsTag teilen sich vorhandenes gtag/dataLayer und injizieren keinen zweiten Loader.
  • SPA — hier lag bis 0.5.1 eine Doppelzaehlung. Meta und TikTok tracken History-Changes selbst, per Default: Meta feuert bei jedem pushState einen PageView, TikTok "will measure an additional pageview representing a view on the new page URL". Das Paket hat obendrauf eigene Hooks installiert, also gab es zwei PageViews pro Routenwechsel auf beiden Plattformen. Ab 0.6.0 werden die Hersteller-Listener abgeschaltet (fbq.disablePushState = true vor dem Init, historyObserver: false in ttq.load) und der eigene Hook bleibt — er ist der einzige, der vor jedem Fire den aktuellen Consent liest; der native feuert nach einem Widerruf ungebremst weiter. GA4 nutzt die History-Erkennung von Enhanced Measurement, Microsoft UET sein enableAutoSpaTracking; fuer beide installiert das Paket bewusst keinen eigenen Hook. LinkedIn hat keinen SPA-Pfad, das ist Absicht (der Insight Tag ist ein Retargeting-Cookie, kein Pageview-Analytics).
  • Consent-Widerruf: Geladene Pixel lassen sich nicht vollstaendig entladen; die erste Garantie ist deshalb "nie vor Consent laden". Ein bereits gebootetes Meta Pixel erhaelt bei jeder echten Zustandsaenderung zusaetzlich das dokumentierte fbq('consent','revoke'|'grant'). Die paketverwalteten Meta- PageViews und alle Browser-Conversions bleiben waehrend Denial hart gesperrt. TikToks Queue stellt revokeConsent/grantConsent bereit. LinkedIn bietet hier keinen belastbaren Runtime-Revoke-Vertrag: Das Paket stoppt seine eigenen lintrk-Conversions sofort; die vollstaendige Wirkung fuer das bereits geladene Basis-Tag gilt erst ab dem naechsten MPA-Dokument-Load und muss vor Release als Consent-Gate getestet werden.
  • CSP fuer Microsoft UET, falls gesetzt: script-src https://bat.bing.com https://bat.bing.net und dieselben Hosts in img-src (UET sendet seine Events als Image-Beacon).

Meta Advanced Matching

initMetaPixel({ pixelId, advancedMatching }) reicht Kundendaten als drittes Argument an fbq('init', …) — Meta: "Be sure to place advanced matching parameters in the pixel base code or the values will not be treated as manual advanced matching values." Belegt sind elf Felder: em, fn, ln, ph, external_id, ge, db, ct, st, zp, country.

Damit kehrt sich fuer Meta die bisherige Regel "keine PII an den Client-Pixel" um. Fuer TikTok und alle anderen gilt sie weiter: dort gehen nur value, currency und content_name mit. Gehashte Kundendaten aus dem Browser sind eine neue Verarbeitung — Vermerk in Datenschutzerklaerung und VVZ, analog zur maskAllInputs: false-Entscheidung. Meta verlangt vertraglich "robust and sufficiently prominent notice" und im EWR nachweisbaren Consent.

Drei Dinge, die man leicht verwechselt:

Klartext an den Pixel, nicht vorhashen. "Values will be hashed automatically by the pixel using SHA-256." Die CAPI verlangt genau umgekehrt SHA-256 vom Aufrufer. Zwei gegenlaeufige Regeln fuer dieselben Felder — wer hier selbst hasht, macht die Werte unbrauchbar.

Die Telefonnummer braucht die Laendervorwahl vom Aufrufer. Metas Normalisierung lautet nur "remove symbols, letters, and any leading zeros". Aus 0170 1234567 wird damit 1701234567, und das matcht nichts. Uebergib +49 170 1234567 oder 0049 170 1234567 — eine Vorwahl kann das Paket nicht erraten, und es tut auch nicht so.

reinitWithAdvancedMatching ist Default aus. Aktiviert ruft es bei neuen AM-Daten ein zweites fbq('init'). Das ist in Metas Doku weder dokumentiert noch verboten noch in seinem Verhalten beschrieben — die einzige Stelle, an der es ueberhaupt diskutiert wird, ist ein unbeantworteter Community-Thread. Wer es einschaltet, verifiziert es je Property im Events Manager gegen die EMQ, bevor er sich darauf verlaesst. Der dokumentierte Weg ist stattdessen: AM zum Init-Zeitpunkt, wenn die Identitaet schon feststeht (serverseitig gerenderte Danke-Seite, eingeloggter Bereich) — auf dem Astro/MPA-Stack ist das der Normalfall. Ergaenzend deckt Automatic Advanced Matching (ein Schalter im Events Manager, kein Code) den gewoehnlichen Formularfall ab; es kennt aber weder external_id noch db und sieht nichts, was nur in SPA-State oder einem fetch-Body steht.

Eine AM-Payload, deren einzige Felder fn + ge oder ln + ge sind, wird verworfen und der Pixel ohne AM initialisiert — Meta zaehlt solche Kombinationen als invalides Event.

Unser Consent-Modell umgeht dabei eine dokumentierte Falle: Advanced Matching muss im init stehen. Das Paket erzeugt deshalb vor dem ersten Grant weder Stub noch Loader noch init. Erst spaetere Zustandswechsel des gebooteten Pixels werden mit fbq('consent','revoke'|'grant') gespiegelt.

Client-Conversions und Plattformpfade

Meta, TikTok und LinkedIn unterstuetzen das redundante Setup: dieselbe Conversion feuert clientseitig und serverseitig und wird ueber eine geteilte event_id dedupliziert. Der adConversionAdapter uebernimmt den Client-Teil; er feuert nur, wenn das jeweilige Basis-Pixel bereits gebootet ist und der aktuelle Marketing-Consent weiterhin true ist. Google Ads nutzt dieselbe ID als transaction_id; die Serverkopie muss dieselbe Conversion Action und dieselbe Order-/Transaction-ID verwenden. GA4 besitzt keine allgemeine Browser/Server-Deduplizierung und bleibt fuer diese Events clientseitiger Owner.

Bei einem eigenen CMP-Status wird derselbe aktuelle Getter an Pixel-Boot und Conversion-Adapter gegeben. AdConversionConfig.consentGranted wird vor jedem Conversion-Call neu ausgewertet; ohne Getter bleibt window.__brixonConsentMarketing === true der Default.

import {
  adConversionAdapter,
  configureTracking,
  googleAnalyticsAdapter,
  initGoogleAnalyticsTag,
  initLinkedInInsight,
  initMetaPixel,
  initTikTokPixel,
  posthogAdapter,
} from "brixon-tracking";

const marketingConsentGranted = () => customConsent.marketing === true;
const analyticsConsentGranted = () => customConsent.analytics === true;

initGoogleAnalyticsTag({
  measurementId: ga4MeasurementId,
  consentGranted: analyticsConsentGranted,
});
initMetaPixel({ pixelId: metaPixelId, consentGranted: marketingConsentGranted });
initTikTokPixel({ pixelId: tiktokPixelId, consentGranted: marketingConsentGranted });
initLinkedInInsight({ partnerId: linkedinPartnerId, consentGranted: marketingConsentGranted });
configureTracking({
  destinations: [
    posthogAdapter(),
    googleAnalyticsAdapter({ consentGranted: analyticsConsentGranted }),
    adConversionAdapter({
      consentGranted: marketingConsentGranted,
      linkedinOutcomes: { sales_lead: linkedinBrowserConversionId },
    }),
  ],
});

Die eine Regel: EINE event_id pro realer Conversion — am Call-Site erzeugen und an track() UND an die Server-Route geben. Sie darf bei Retries nicht neu erzeugt werden. Im PostHog-Destination-Mapping wird sie fuer Meta und TikTok als event_id, fuer LinkedIn als eventId und fuer Google Ads als Order-/Transaction-ID verwendet. Der Adapter akzeptiert nur RESPOND-Events mit Server-Truth und verwirft den Browserpfad ohne gueltige UUIDv7-event_id. Dieses Fail-closed-Verhalten gilt ab 0.5.0; Consumer muessen die ID vor dem Request erzeugen und im bestaetigten Client-Echo wiederverwenden. Serverseitig wird dieselbe ID auch als PostHog-uuid verwendet. Fuer spaeter bestaetigte Transaktionen muss createConversionTracker() zusaetzlich einen stabilen, serverseitig gespeicherten timestamp erhalten; so bleiben uuid, Eventname, Zeitpunkt und distinct_id bei einem Retry identisch.

import { generateEventId } from "brixon-tracking";
import { track } from "brixon-tracking";

// Im Submit-Handler:
const eventId = generateEventId();

// 1) Server-Request mit der gemeinsamen id
const response = await fetch("/api/lead-magnet/request", {
  method: "POST",
  body: JSON.stringify({ ...formData, event_id: eventId, phSessionId }),
});
if (!response.ok) throw new Error("delivery failed");

// 2) Erst nach bestaetigtem 2xx: Client-Echo + Meta-Browser-Conversion
track("lead_magnet_lead", {
  lead_magnet_name: "ki-readiness",
  lead_magnet_type: "pdf",
  event_id: eventId,
  conversion_outcome: "lead_magnet",
});

Mapping kanonisch -> Plattform-Event:

  • Meta: DEFAULT_META_EVENT_MAP (z. B. lead_magnet_lead -> Lead, meeting_booking -> Schedule). Ueber metaEvents ueberschreibbar; null schaltet Meta fuer ein Event ab. application_submit fehlt bewusst (kein Marketing-Lead). Fuer generische RESPOND-Events mappt DEFAULT_META_OUTCOME_MAP das Property conversion_outcome; mit useEventNameFallback: false erreicht nur ein ausdrueckliches kommerzielles Outcome den Meta-Pixel.
  • TikTok: DEFAULT_TIKTOK_EVENT_MAP und DEFAULT_TIKTOK_OUTCOME_MAP verwenden aktuelle Standardevents wie Lead, Download, Schedule, CompleteRegistration und Purchase. Browser- und Serverevent brauchen denselben Eventnamen und dieselbe event_id.
  • LinkedIn: linkedinConversions (kanonisch -> numerische conversion_id). Fuer generische Events kann linkedinOutcomes stattdessen nach conversion_outcome mappen. Der Browser-Call enthaelt event_id; die CAPI muss sie als eventId erhalten. LinkedIn verlangt je eine Conversion Rule fuer Browser und Server.
  • Google Ads: googleConversionId plus googleLabels (nach Event-Namen) oder googleOutcomes (nach conversion_outcome) sendet die Browserconversion mit transaction_id = event_id. googleOutcomes ist ab 0.6.0 da, weil Google vorher als einzige Plattform nicht nach Outcome routen konnte — im empfohlenen Setup mit useEventNameFallback: false landeten damit Newsletter, Lead Magnet und Sales Lead auf derselben Conversion Action. Eine redundante Serverkopie muss dieselbe Conversion Action und ID verwenden; order_id dedupliziert nur innerhalb einer Conversion Action.
  • Microsoft UET: microsoftEvents (nach Event-Namen) oder microsoftOutcomes (nach Outcome, Default aus der Registry: sales_lead -> generate_sales_lead, meeting -> book_appointment, …). Der Browser-Call sendet event_id, revenue_value und currency. Vorsicht mit event_id: es ist nur in Microsofts CAPI-Doku belegt, fehlt in der offiziellen UET-Parametertabelle und hat keinen publizierten Wire-Namen. Vor dem Verlassen darauf im UET Tag Helper pruefen, dass es den Beacon erreicht. transaction_id existiert bei UET auch, ist aber ein Ecommerce-Attribut und kein Dedup-Key.
  • GA4: googleAnalyticsAdapter sendet Katalogevents direkt clientseitig, gefiltert — nur event_id, value, currency und die im Katalog deklarierten Parameter des Events, erweiterbar per allowProperties. conversion_outcome und utm_* erreichen GA4 also nicht. Das PostHog-Setup darf dieselbe GA4-Conversion nicht erneut per Measurement Protocol senden, weil GA4 allgemeine Events nicht ueber event_id dedupliziert.

Attribution zum Submit-Zeitpunkt

readTrackingParams() liest Click-IDs, UTMs, $session_id und die Conversion-URL in dem Moment, in dem eine Geschaeftsaktion abgeschickt wird. Ab 0.6.0 liegt die Logik im Core statt im /react-Entry und ist damit auch aus brixon-tracking und brixon-tracking/astro erreichbar — vorher zog sie React mit, und der Astro-Stack hatte keinen Weg, Click-IDs mitzugeben. Ohne die fehlen der CAPI ihre Match-Keys.

// Astro / vanilla
import { readTrackingParams, setSessionIdResolver } from "brixon-tracking/astro";

const tracking = readTrackingParams();
// tracking.gclid, .gbraid, .wbraid, .fbclid, .fbc, .fbp, .ttclid, .ttp,
// .msclkid, .liFatId, .gaClientId, .utm_*, .conversionUrl, .phSessionId
// React — unveraenderte Signatur
import { useTrackingParams } from "brixon-tracking/react";
const tracking = useTrackingParams();

Neu gegenueber 0.5.1:

  • gbraid und wbraid — Googles iOS-Click-IDs. gbraid bei einem Klick auf eine Web-Anzeige, der in die iOS-App fuehrt, wbraid umgekehrt. Beide sind eigenstaendige ClickConversion-Identifier neben gclid und case sensitive; nichts davon wird normalisiert.
  • ttclid auch aus dem First-Party-Cookie — das TikTok-Pixel legt es unter demselben Namen ab, der URL-Parameter ueberlebt keinen Seitenwechsel. Werte koennen 1000 Zeichen lang sein.
  • ttp aus dem _ttp-Cookie, offizieller TikTok-Match-Key. Setzt voraus, dass First-Party-Cookies in den Events-Manager-Pixel-Einstellungen aktiv sind.
  • fbc wird aus fbclid abgeleitet, wenn das _fbc-Cookie fehlt — Meta erlaubt das ausdruecklich fuer die CAPI und schreibt subdomainIndex = 1 vor, wenn der Wert ohne Cookie erzeugt wird. Der abgeleitete Wert wird pro fbclid gecacht, damit der Snapshot referenziell stabil bleibt.
  • msclkid wird unveraendert durchgereicht. Microsofts eigene Seiten widersprechen sich beim Format (Parametertabelle: 32-stellige GUID mit Status-Suffix; CAPI-Doku: hyphenierte UUID), also normalisiert das Paket nicht.

setSessionIdResolver() injiziert den Session-ID-Getter. Ohne Aufruf liest der Snapshot window.__brixonPosthog — die Instanz, die startConsentGatedPostHog ablegt. window.posthog setzt posthog-js nur im Snippet- und CJS-Build, nicht im ESM-Build, den jeder Bundler nimmt; es bleibt nur als zweiter Versuch stehen. Der /react-Entry registriert den Resolver selbst mit seiner importierten Instanz.

TrackingHiddenFields rendert dieselben Werte einschliesslich ttp als Hidden Fields. Der Dedup-Key bleibt davon getrennt: Das optionale eventId-Prop rendert ihn fuer den klassischen Form-Post, waehrend der Attributions-Snapshot ihn weder liest noch persistiert.

Merge-Regeln (nicht verhandelbar, v2 §4.3)

  • Der Client ruft niemals identify() auf.
  • Website-Server-Events setzen keine $set-Person-Properties.
  • Nur der n8n Lead-Processor merged ($identify mit distinct_id = person_uid).

Entwicklung

npm install
npm test        # vitest
npm run test:astro-fixture # echter Astro-Build fuer Control + Late-Island
npm run build   # tsup: ESM + CJS + dts
npm run validate # typecheck + unit + package build + echte Astro-Fixture

Vor einem Release ist npm run validate verpflichtend. Ein Publish, eine Cloudflare-Bindings-Aenderung oder ein Preview-/Produktionsdeploy ist nicht Teil dieses Scripts und benoetigt eine separate Freigabe.