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.
npm install @trackdolphin/sdkEin 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.
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):
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:
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:
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:
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:
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:
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:
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.
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
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
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:
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
externalIdim localStorage unter_td_id. Dort steht nie Klartext. nameundemailgehen zusätzlich im Klartext mit, in den eigenen Felderncontact_emailundcontact_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 inidentify()mit, undsetConsent()hat eine Einwilligung gesetzt, die Speicher gewährt (ad_storageoderanalytics_storage=granted). OhnesetConsent()passiert nichts. „Nie gefragt" ist kein Ja. Der Klartext hängt nur amidentify-Ereignis, nicht an jedem Seitenaufruf, und liegt nur im Arbeitsspeicher der Seite.- Ab jetzt trägt jedes Event automatisch
em,ph,ph_e164undexternal_id, sofern es sie nicht selbst mitbringt. Einview_itemnach dem Login ist damit einer Person zugeordnet, ohne dass du daran denkst. - Einmalig geht ein Event vom Typ
identifymit dentraitsraus. reset()löscht_td_idund 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.
Consent Mode v2
setConsent() nimmt weiterhin true/false, zusätzlich das granulare
Objekt nach Google Consent Mode v2:
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",
});trueentspricht allen vier Signalengranted,falseallendenied.- Das SDK merkt sich das Objekt und schickt es als Feld
consentmit jedem Event. Die Plattformen entscheiden damit selbst, was sie verwenden dürfen. - Gesendet wird, sobald
ad_storageoderanalytics_storagegrantedist. Bis dahin puffert die Warteschlange (mitrequireConsent: true), danach werden die gepufferten Events nachgesendet: der erste Seitenaufruf geht also nicht verloren, nur weil das Banner später kam.
Consent-Werkzeug und Herkunft des Zustands
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ätzlichwp-consent-api(WooCommerce),jtl(Consent Manager von JTL-Shop) undshopware(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-updateliefern den Zustand direkt, einsetConsent()ist dann nicht nötig, und mitrequireConsent: trueläuft die Warteschlange von selbst an.default: niemand hat entschieden. Eingtag('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.
Besucher-Cookie an die Einwilligung binden
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:
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).