Trackdolphin

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.