Trackdolphin

German This text is available in German only for now. The site around it is in English, the text itself is not.

SDK für eigene Anbindungen

Nicht jeder Kunde hat WooCommerce oder Shopware. Ein Fitnessstudio verkauft keine Artikel, es bucht Probetrainings; ein Buchungssystem hat gar keinen Warenkorb. Für alles außerhalb der Plugins gibt es das SDK.

bash
npm install @trackdolphin/sdk

Ein Paket, drei Einstiege: @trackdolphin/sdk für den Browser, @trackdolphin/sdk/proxy für die Route, über die der Browser sendet, und @trackdolphin/sdk/server für den Kauf aus deinem Backend. Alle drei liefern ES-Module mit Typdefinitionen und haben keine Abhängigkeiten.

Laufen in deinem Projekt Microsoft Clarity, PostHog oder Sentry, beschriftet das SDK deren Aufzeichnungen und Fehler zusätzlich mit Werbekanal, Kampagne und Bestellwert (siehe Fremde Werkzeuge beschriften).

Einbindung

Das Browser-SDK braucht eine Adresse, an die es sendet. Es gibt zwei, und die Wahl entscheidet, wie viel von deinem Tracking ankommt.

Empfohlen: die eigene Proxy-Route

Der Browser spricht nur mit deiner Domain, dein Server spricht mit dem Collector. Werbeblocker sehen keinen fremden Hostnamen; Safari sieht einen First-Party-Request und lässt das Besucher-Cookie leben, weil dein Server es per Set-Cookie setzt. Ein per JavaScript gesetztes kappt es nach sieben Tagen. Warum ein CNAME das nicht leistet: 100 % Abdeckung.

@trackdolphin/sdk/proxy ist der Handler dafür: eine Funktion über Web-Standard Request und Response, ohne Node-Abhängigkeiten, lauffähig auf Node, Cloudflare Workers und Vercel Edge.

ts
import { createCollectProxy } from "@trackdolphin/sdk/proxy";

// Die Collector-URL bleibt im Server-Environment.
const proxy = createCollectProxy({ endpoint: process.env.TD_ENDPOINT! });

Der Handler nimmt nur POST (bis 64 KB) an, ergänzt client_ip und client_user_agent aus den Kopfzeilen (der Collector läuft hinter Cloudflare und sähe sonst deinen Server als Absender), reicht die Tracking-Cookies (_td_vid, _td_attr, _fbp, _fbc, _ga, _gcl_aw, _gcl_au) über eine Positivliste weiter und setzt _td_vid als Server-Cookie mit 90 Tagen Laufzeit, falls es fehlt. Der Browser bekommt 204 (Collector hat angenommen) oder 202 (hat er nicht: die Ursache geht an log, nie an den Besucher).

Optionen: shopId (nur, wenn endpoint nicht dein Ingest-Host ist), visitorCookie: { requireConsent, maxAgeDays } (mit requireConsent: true gibt es Cookie und visitor_id nur, wenn das Event ein consent-Objekt mit ad_storage oder analytics_storage = granted trägt), maxBodyBytes, timeoutMs, fetchImpl, log.

Die Route je Framework, eine Datei, mehr nicht:

SvelteKit: src/routes/td/+server.ts (kompletter Weg im Schnellstart SvelteKit):

ts
import { TD_ENDPOINT } from "$env/static/private";
import { createCollectProxy } from "@trackdolphin/sdk/proxy";
import type { RequestHandler } from "./$types";

const proxy = createCollectProxy({ endpoint: TD_ENDPOINT });

export const POST: RequestHandler = ({ request }) => proxy(request);
export const OPTIONS = POST;

Next.js (App Router): app/td/route.ts (mehr unter Next.js); der Route Handler nimmt Request und gibt Response zurück, also ist der Proxy selbst der Handler:

ts
import { createCollectProxy } from "@trackdolphin/sdk/proxy";

export const POST = createCollectProxy({ endpoint: process.env.TD_ENDPOINT! });

Nuxt (Nitro, h3 v1): server/api/td.post.ts; die Endung .post beschränkt die Route auf POST, toWebRequest und sendWebResponse übersetzen zwischen h3-Event und Web-Standard:

ts
import { createCollectProxy } from "@trackdolphin/sdk/proxy";
import { sendWebResponse, toWebRequest } from "h3";

const proxy = createCollectProxy({ endpoint: process.env.TD_ENDPOINT! });

export default defineEventHandler(async (event) =>
  sendWebResponse(event, await proxy(toWebRequest(event))),
);

Astro: src/pages/td.ts; die Route braucht den Server-Modus (output: "server" oder export const prerender = false), sonst gibt es kein request:

ts
import type { APIRoute } from "astro";
import { createCollectProxy } from "@trackdolphin/sdk/proxy";

export const prerender = false;
const proxy = createCollectProxy({ endpoint: import.meta.env.TD_ENDPOINT });

export const POST: APIRoute = ({ request }) => proxy(request);

Remix: app/routes/td.ts; die action bekommt jeden Nicht-GET-Request:

ts
import type { ActionFunctionArgs } from "@remix-run/node";
import { createCollectProxy } from "@trackdolphin/sdk/proxy";

const proxy = createCollectProxy({ endpoint: process.env.TD_ENDPOINT! });

export const action = ({ request }: ActionFunctionArgs) => proxy(request);

Im Browser dann relativ:

js
import { init, viewItem } from "@trackdolphin/sdk";

init({ endpoint: "/td" });

viewItem([{ id: "SKU-1", name: "Laufschuh", price: 119.9 }], 119.9);

Bei relativem endpoint (oder einer absoluten URL auf derselben Herkunft) legt das SDK kein eigenes _td_vid an: die Route vergibt die Kennung, das SDK liest sie nur. Sonst gäbe es zwei Kennungen für einen Besucher. Wer das ausdrücklich anders will, setzt visitorId: "client" oder "server".

Mit fallbackEndpoint (der direkten Collector-URL) prüft das SDK beim ersten Event einer Sitzung, ob die Route trägt. Antwortet sie nicht, mit 404 oder 5xx, geht der Rest der Sitzung direkt an den Collector, ohne Tarnung, aber nicht verloren. Die Entscheidung liegt in sessionStorage (_td_tx).

Alternative: die direkte Collector-URL

Ohne Server-Route (statisches Frontend, fremdes Hosting, schnelle Probe) sendet das SDK direkt an den Collector:

js
init({ endpoint: "https://abc123.trdph.com/collect" });

Sendest du nicht an den eigenen Host, gib zusätzlich shopId an.

Was du dabei verlierst: Werbeblocker blockieren den Hostnamen, sobald er auf einer Liste steht. Der Anteil liegt je nach Zielgruppe bei zehn bis vierzig Prozent der Browser-Events. Safari kappt das per JavaScript gesetzte Besucher-Cookie auf sieben Tage; wer nach einer Woche kauft, ist ein neuer Besucher ohne Klick-Zuordnung. Ein eigener Hostname per CNAME (Einrichtung → Eigene Tracking-Domain) hilft gegen das Erste, nicht gegen das Zweite. Der Server-Kauf ist von alledem unberührt.

Was das SDK im Browser übernimmt

Egal auf welchem Weg, das SDK erledigt still einiges, was sonst schiefgeht:

  • First-Touch-Attribution über 90 Tage, damit der Kanal auch beim zweiten Besuch noch bekannt ist
  • Klick-IDs aus der Adresszeile in Cookies sichern (gclid, gbraid, wbraid, fbclid, ttclid, msclkid, epik), auch dann, wenn ein Framework die Query vor dem Start des SDK schon entfernt hat (siehe Next.js)
  • Besucherkennung: vom Server-Cookie der Proxy-Route gelesen oder, beim direkten Weg, selbst als First-Party-Cookie gesetzt; sie verbindet die anonyme Reise, bis eine gehashte E-Mail bekannt wird
  • Warteschlange, solange keine Einwilligung vorliegt

Auf dem Server

Der Abschluss gehört auf den Server: dort kann ihn kein Werbeblocker verhindern und niemand fälschen.

ts
import { createClient } from "@trackdolphin/sdk/server";

const td = createClient({ endpoint: process.env.TD_ENDPOINT! });

await td.purchase({
  event_id: `order_${order.id}`,   // aus der Bestellnummer, nicht zufällig
  value: order.total,
  currency: "EUR",
  email: order.email,              // wird lokal gehasht, nie im Klartext gesendet
  phone: order.phone,
});

createClient liefert track, purchase, lead, trackMany und newEventId. Bei Netzwerkfehlern und 5xx wird mit Backoff wiederholt, bei 4xx nicht: das sind Anwendungsfehler, die kein Neuversuch behebt.

Reihenfolge: erst das Geschäft, dann die Messung

ts
const lead = await createLead(input);   // zuerst
try {
  await td.lead("probetraining", { event_id: `lead_${lead.id}`, email: input.email });
} catch (e) {
  logger.error("Tracking fehlgeschlagen, Lead ist gesichert", e);
}

Fällt das Tracking aus, ist der Lead trotzdem da. Andersherum kostet ein Ausfall bei uns dich einen Kunden: das darf nie passieren.

Die event_id leiten, nicht würfeln

ts
event_id: `order_${order.id}`

Eine abgeleitete Kennung macht jeden Versand wiederholbar: Zeitüberschreitung, Neustart, doppelter Webhook: die Plattform erkennt die Dublette und zählt einmal. Ein Zufallswert erzeugt bei jedem Versuch eine neue Conversion.

Dieselbe event_id im Browser (purchase({ event_id, … })) und auf dem Server: nur so dedupliziert Meta zwischen Pixel und Conversions API.

Personen erkennen (identify)

Sobald du weißt, wer da ist (Login, Newsletter-Anmeldung, ausgefülltes Checkout-Formular), sag es dem SDK:

js
import { identify, reset } from "@trackdolphin/sdk";

await identify({
  email: user.email,          // wird hier im Browser gehasht
  phone: user.phone,
  name: user.name,            // Klartext, nur für die Personenansicht
  externalId: user.id,        // deine Kundennummer oder User-ID
  traits: { plan: "pro", newsletter: true },
});

// Beim Logout:
reset();

Was dabei passiert:

  • E-Mail und Telefon werden lokal gehasht (SHA-256; Telefon einmal ohne und einmal mit Plus, weil Meta und Google sich darin unterscheiden). Dauerhaft gespeichert werden nur die Hashes und externalId im localStorage unter _td_id. Dort steht nie Klartext.
  • name und email gehen zusätzlich im Klartext mit, in den eigenen Feldern contact_email und contact_name. Sie stehen danach in der Personenansicht des Dashboards und gehen an keine Werbeplattform; dorthin gehen weiterhin ausschließlich Hashes. Zwei Bedingungen müssen erfüllt sein, sonst bleibt der Klartext im Browser: Du gibst ihn in identify() mit, und setConsent() hat eine Einwilligung gesetzt, die Speicher gewährt (ad_storage oder analytics_storage = granted). Ohne setConsent() passiert nichts. „Nie gefragt" ist kein Ja. Der Klartext hängt nur am identify-Ereignis, nicht an jedem Seitenaufruf, und liegt nur im Arbeitsspeicher der Seite.
  • Ab jetzt trägt jedes Event automatisch em, ph, ph_e164 und external_id, sofern es sie nicht selbst mitbringt. Ein view_item nach dem Login ist damit einer Person zugeordnet, ohne dass du daran denkst.
  • Einmalig geht ein Event vom Typ identify mit den traits raus.
  • reset() löscht _td_id und vergisst den Klartext. Attribution und Besucher-Cookie bleiben. Der Kanal des Besuchs ändert sich durch einen Logout nicht.

In der App bilden sich Kohorten aus external_id und em: Alle Events mit derselben Kennung hängen an derselben Person, auch rückwirkend über die anonyme Besucherkennung.

setConsent() nimmt weiterhin true/false, zusätzlich das granulare Objekt nach Google Consent Mode v2:

js
import { init, setConsent } from "@trackdolphin/sdk";

init({ endpoint: "/td", requireConsent: true });

// Aus dem Callback deines Cookie-Banners:
setConsent({
  ad_storage: "granted",
  analytics_storage: "granted",
  ad_user_data: "granted",
  ad_personalization: "denied",
});
  • true entspricht allen vier Signalen granted, false allen denied.
  • Das SDK merkt sich das Objekt und schickt es als Feld consent mit jedem Event. Die Plattformen entscheiden damit selbst, was sie verwenden dürfen.
  • Gesendet wird, sobald ad_storage oder analytics_storage granted ist. Bis dahin puffert die Warteschlange (mit requireConsent: true), danach werden die gepufferten Events nachgesendet: der erste Seitenaufruf geht also nicht verloren, nur weil das Banner später kam.

Ab SDK 0.2.1 erkennt das SDK passiv, welches Consent-Werkzeug auf der Seite läuft, und schreibt zwei Zusatzschlüssel in consent:

  • cmp: der Name des Werkzeugs (cookiebot, usercentrics, borlabs, complianz, real-cookie-banner, consentmanager, klaro (ab 0.5.1), tcf, shopify, google-consent-mode), leer wenn keins erkannt wurde. Die Shop-Plugins melden mit derselben Erkennung zusätzlich wp-consent-api (WooCommerce), jtl (Consent Manager von JTL-Shop) und shopware (Cookie-Manager von Shopware).
  • source: woher der Zustand stammt. api: setConsent() wurde gerufen. cmp: das Werkzeug hat entschieden; Cookiebot, Usercentrics, Klaro, jedes TCF-2-Werkzeug, Shopify Customer Privacy und ein Consent-Mode-update liefern den Zustand direkt, ein setConsent() ist dann nicht nötig, und mit requireConsent: true läuft die Warteschlange von selbst an. default: niemand hat entschieden. Ein gtag('consent','default',…) ist eine Voreinstellung, keine Zustimmung, und wird genau so gemeldet.

Ohne Werkzeug und ohne setConsent() steht am Ereignis nur { "source": "default", "cmp": "" }; das SDK erfindet kein granted. Die Tracking-Gesundheit meldet, wenn ein Werkzeug erkannt ist, aber nie ein Zustimmungssignal ankommt: Dann misst Trackdolphin, als hätte jeder zugestimmt, und das sollte der Betreiber wissen.

Das Besucher-Cookie _td_vid ist rein zufällig, first-party und sagt nichts über die Person aus. Wer es trotzdem erst mit Einwilligung setzen will oder muss:

js
init({
  endpoint: "/td",
  requireConsent: true,
  visitorCookieRequiresConsent: true,
});

Dann wird das Cookie erst geschrieben und visitor_id erst mitgeschickt, sobald ad_storage oder analytics_storage gewährt ist. Vorher gibt es kein Cookie, und Events vor der Entscheidung lassen sich später nicht mehr zu derselben Person zusammenführen. Das ist der Preis.

Über die Proxy-Route gilt dasselbe serverseitig: createCollectProxy({ …, visitorCookie: { requireConsent: true } }) setzt das Cookie nur, wenn das Event selbst die Einwilligung trägt: das SDK schickt sie mit jedem Event, sobald setConsent() aufgerufen wurde.

Klartext bleibt bei dir

E-Mail und Telefonnummer werden lokal gehasht, bevor irgendetwas das Gerät oder deinen Server verlässt. Telefonnummern nach E.164 ohne Plus: 0171… wird zu 49171…, sonst trifft der Hash bei Meta und Google nie.

Siehe Match-Qualität.

Next.js, SvelteKit und andere Frameworks

Für den App Router gibt es eine eigene Seite mit Route Handler, TrackdolphinProvider, Seitenaufrufen über usePathname und dem Server-Kauf: Next.js (App Router). Für SvelteKit den Schnellstart SvelteKit (vom leeren Projekt bis zum ersten Kauf).

Historie einspielen

Auch der Backfill läuft über das SDK (siehe Historie und Erkennungsquote).