Docs

Next.js

npm install https://api.3x.example/v1/sdk/3x-engine-sdk-0.1.0.tgz
// app/layout.tsx
import { X3Analytics } from "@3x-engine/sdk/next";

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <X3Analytics publishableKey={process.env.NEXT_PUBLIC_X3_KEY!} host="/n3" />
      </body>
    </html>
  );
}

<X3Analytics> renders next/script with strategy="afterInteractive". It does not use useSearchParams, so static pages stay static. App Router and Pages Router navigations are captured through the History API.

Props

PropMeaning
publishableKeypk_3x_…
hostAPI origin or proxy path. Use /n3 with the proxy route; without a proxy the API origin, e.g. https://api.3x.example
configClient config fetched on the server with fetchClientConfig() – the tracker starts without a round trip
nonceCSP nonce; on dynamically rendered pages Next.js applies the request nonce automatically
// app/layout.tsx – config fetched on the server (cached by Next for 60 s)
import { X3Analytics, fetchClientConfig } from "@3x-engine/sdk/next";

export default async function RootLayout({ children }: { children: React.ReactNode }) {
  const config = await fetchClientConfig({ publishableKey: process.env.NEXT_PUBLIC_X3_KEY!, apiOrigin: process.env.X3_API_ORIGIN! });
  return (
    <html lang="en">
      <body>
        {children}
        <X3Analytics publishableKey={process.env.NEXT_PUBLIC_X3_KEY!} host="/n3" config={config ?? undefined} />
      </body>
    </html>
  );
}

Pages Router

// pages/_app.tsx
import type { AppProps } from "next/app";
import { X3Analytics } from "@3x-engine/sdk/next";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Component {...pageProps} />
      <X3Analytics publishableKey={process.env.NEXT_PUBLIC_X3_KEY!} host="/n3" />
    </>
  );
}

Events from components

<X3Analytics> loads the script-tag build, so components talk to window.x3. A tiny helper queues calls until the script has loaded:

// lib/x3.ts
type X3Command = (...args: unknown[]) => void;

export function x3(...args: unknown[]): void {
  if (typeof window === "undefined") return;
  const w = window as unknown as { x3?: X3Command; x3q?: unknown[][] };
  w.x3 ??= (...queued: unknown[]) => void (w.x3q ??= []).push(queued);
  w.x3(...args);
}
"use client";
import { x3 } from "@/lib/x3";

export function SignupButton() {
  return <button onClick={() => x3("track", "signup_click", { plan: "pro" })}>Start trial</button>;
}

Methods that return something (getIds(), identify(), conversion()) are available as window.x3.getIds() etc. once the script has loaded. Prefer the npm package everywhere instead? Use <X3Provider> from @3x-engine/sdk/react (React) – then import { track } from "@3x-engine/sdk" talks to the same tracker. Do not combine both on one page.

Server actions and route handlers

Leads, conversions and server-side events need a secret key and therefore run on the server: Server helper. Pass getIds().sessionId from the browser so 3x can attach them to the visit.

Middleware / proxy.ts

If your app has middleware.ts (or proxy.ts), exclude the proxy path from its matcher so tracking requests are not rewritten or localized:

export const config = { matcher: ["/((?!n3|_next|api|.*\\..*).*)"] };

Full recipes: Next.js App Router, Next.js Pages Router.