Trackdolphin

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

Next.js (App Router)

Zwei Dinge sind bei Next.js anders als bei einer klassischen Seite: Der Browser soll nur mit deiner Domain sprechen: dafür gibt es einen Route Handler, der an den Collector weiterreicht. Und das SDK läuft im Browser; Server-Komponenten kennen weder document noch location. Deshalb gehört init() in eine Client-Komponente, die im Layout einmal eingehängt wird, und Seitenaufrufe kommen aus usePathname, weil ein Wechsel zwischen Routen keine neue Seite lädt.

bash
npm install @trackdolphin/sdk

Die Collector-URL aus dem Dashboard (Einrichtung) steht nur im Server-Environment, kein NEXT_PUBLIC_, der Browser braucht sie nicht:

bash
# .env.local
TD_ENDPOINT=https://abc123.trdph.com/collect

Die Proxy-Route

Ein Route Handler nimmt Web-Standard Request und gibt Response zurück: genau die Schnittstelle des Proxys. createCollectProxy liefert den Handler direkt:

ts
// app/td/route.ts
import { createCollectProxy } from "@trackdolphin/sdk/proxy";

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

OPTIONS musst du nicht selbst anlegen. Next.js beantwortet es aus den vorhandenen Methoden. Die Route ergänzt Besucher-IP und User-Agent, setzt die Besucherkennung _td_vid als Server-Cookie (das ist der Unterschied zu einem JS-Cookie, das Safari nach sieben Tagen kappt) und reicht Klick-ID-Cookies über eine Positivliste weiter. Fehler stehen im Server-Log, nie im Browser. Warum dieser Weg und nicht ein CNAME: 100 % Abdeckung.

Provider im Layout

tsx
// app/trackdolphin-provider.tsx
"use client";

import { useEffect } from "react";
import { usePathname } from "next/navigation";
import { init, track } from "@trackdolphin/sdk";

export function TrackdolphinProvider() {
  const pathname = usePathname();

  // Einmal starten — der Effekt läuft erst nach der Hydration. Relativ,
  // also an die eigene Route; die Besucherkennung vergibt die Route.
  useEffect(() => {
    init({ endpoint: "/td", requireConsent: true });
  }, []);

  // Jede Route ist ein Seitenaufruf. Das erste Rendern zählt mit, weil der
  // Effekt beim ersten pathname ebenfalls läuft.
  useEffect(() => {
    track({ type: "custom", custom_name: "page_view" });
  }, [pathname]);

  return null;
}
tsx
// app/layout.tsx
import { TrackdolphinProvider } from "./trackdolphin-provider";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="de">
      <body>
        <TrackdolphinProvider />
        {children}
      </body>
    </html>
  );
}

Die Einwilligung aus dem Cookie-Banner übergibst du mit setConsent(): als true/false oder granular nach Consent Mode v2. Mit requireConsent: true bleiben Events bis dahin in der Warteschlange.

Soll das SDK bei einem Ausfall der Route (Deployment, Middleware, die /td abfängt) nicht schweigen, gib die direkte Collector-URL als Rückfall mit, dann ist sie allerdings im Client-Bundle:

ts
init({ endpoint: "/td", fallbackEndpoint: process.env.NEXT_PUBLIC_TD_FALLBACK });

Warum die gclid sonst verloren geht

init() läuft erst im useEffect, also nach der Hydration. Bis dahin ist oft schon etwas mit der Adresszeile passiert: ein Locale-Redirect (/?gclid=…/de/), ein Trailing-Slash-Redirect, ein router.replace, das die Query „aufräumt“, oder ein Middleware-Rewrite. Dann ist location.search leer, und ein SDK, das nur dort nachsieht, hat die gclid nie gesehen. Die Conversion kommt bei Google Ads ohne Klick an.

Das SDK liest deshalb zusätzlich die URL aus dem Navigations-Eintrag der Performance-API (performance.getEntriesByType("navigation")). Der trägt die Adresse, mit der die Seite tatsächlich geladen wurde, vor allen clientseitigen replaceState-Aufrufen. Er wird nur als Rückfall benutzt: Trägt die aktuelle Adresse selbst Signale, gelten diese; und die First-Touch-Logik bleibt unverändert, ein späterer direkter Besuch überschreibt den bezahlten Klick nicht.

Kauf: Browser und Server mit derselben event_id

Der verlässliche Kauf kommt vom Server: aus der Bestellung, nicht aus dem DataLayer. Damit Meta beide Seiten als eine Conversion erkennt, teilen sich Browser und Server die event_id. Ableiten, nicht würfeln:

ts
// app/api/checkout/complete/route.ts  (Route Handler — oder eine Server Action)
import { createClient } from "@trackdolphin/sdk/server";

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

export async function POST(req: Request) {
  const order = await finishOrder(await req.json());   // zuerst das Geschäft

  try {
    await td.purchase({
      event_id: `order_${order.id}`,
      value: order.total,
      currency: "EUR",
      email: order.email,          // wird hier gehasht, verlässt den Server nie im Klartext
      phone: order.phone,
      gclid: order.gclid,          // aus dem Formular mitgeschickt, siehe unten
    });
  } catch (e) {
    console.error("Tracking fehlgeschlagen, Bestellung ist gesichert", e);
  }

  return Response.json({ orderId: order.id });
}
tsx
// Danke-Seite (Client-Komponente): dieselbe event_id im Browser
"use client";

import { useEffect } from "react";
import { purchase } from "@trackdolphin/sdk";

export function PurchaseBeacon({ orderId, total }: { orderId: string; total: number }) {
  useEffect(() => {
    purchase({ event_id: `order_${orderId}`, value: total, currency: "EUR" });
  }, [orderId, total]);
  return null;
}

Die Klick-IDs kennt nur der Browser. readAttribution() liefert die gesicherte First-Touch-Attribution (gclid, fbclid, utm_*, …). Schick sie mit dem Checkout-Formular an deinen Server, damit das Server-Event sie tragen kann. Das Browser-Event bekommt sie automatisch.

Was das SDK von selbst tut

  • First-Touch-Attribution über 90 Tage, Klick-IDs aus der Adresszeile (mit dem oben beschriebenen Rückfall)
  • Besucherkennung aus dem Server-Cookie der Route lesen, nicht selbst anlegen, sonst gäbe es zwei
  • Hashing von E-Mail und Telefon im Browser, identify() für eingeloggte Nutzer (siehe Personen erkennen)
  • sendBeacon mit fetch-Rückfall, damit ein Klick auf „Kaufen“ das Event nicht abreißt

Alles Weitere zur API steht auf der SDK-Seite.