Skip to main content
Initialize the SDK from one client component. Do not use next/script or add a raw script tag alongside the SDK.

Install the package

npm install @bilyai/js

Configure the store URL

Copy the exact, complete SDK URL from Bily > Settings > Apps > More settings > Install tracking into the app’s public build-time settings. The URL is public installation data; it does not contain a private token. Use .env.local only for local development; it is normally not committed.
.env.local
NEXT_PUBLIC_BILY_SCRIPT_URL="https://tracking.example.com/b.js?shop=store.example.com"
In dotenv files, keep the double quotes so # cannot start a comment. If the exact Bily-provided URL contains a literal dollar sign, escape only that character as \$ in the dotenv file so Next.js preserves it. For every preview and production deployment, set the same variable in the deployment build environment before running next build. In a deployment settings field, enter the exact raw URL without dotenv quotes or escapes. Next.js inlines NEXT_PUBLIC_ values into the browser bundle at build time, so rebuild and redeploy after changing the value. If the production build reads .env.production, .env.production.local, or another .env* file, use the quoted dotenv form above instead. Aside from that escape, keep the URL unchanged, including its query string. Restart the local development server after changing .env.local.

Create a client tracking component

The browser script records the initial page view. This component saves the initial URL and tracks only later App Router transitions.
app/bily-tracking.tsx
"use client";

import { useEffect, useRef } from "react";
import { usePathname, useSearchParams } from "next/navigation";
import { init, track } from "@bilyai/js";

const BILY_SCRIPT_URL = process.env.NEXT_PUBLIC_BILY_SCRIPT_URL;

export function BilyTracking() {
  const pathname = usePathname();
  const searchParams = useSearchParams();
  const search = searchParams?.toString() ?? "";
  const previousUrl = useRef<string | null>(null);

  useEffect(() => {
    if (!BILY_SCRIPT_URL) {
      throw new Error("NEXT_PUBLIC_BILY_SCRIPT_URL is required");
    }
    init({ scriptUrl: BILY_SCRIPT_URL });
  }, []);

  useEffect(() => {
    const currentUrl = window.location.href;

    if (previousUrl.current === null) {
      previousUrl.current = currentUrl;
      return;
    }

    if (currentUrl === previousUrl.current) return;
    previousUrl.current = currentUrl;

    track("PageView", {
      sourceUrl: currentUrl,
      pageTitle: document.title,
    });
  }, [pathname, search]);

  return null;
}
The null-safe search value also supports applications that still contain a pages/ directory while migrating to the App Router. URL comparison prevents that compatibility transition from creating a duplicate initial page view.

Mount it in the root layout

Wrap the component in Suspense because it reads the current search parameters.
app/layout.tsx
import { Suspense } from "react";
import { BilyTracking } from "./bily-tracking";

export default function RootLayout({
  children,
}: Readonly<{ children: React.ReactNode }>) {
  return (
    <html lang="en">
      <body>
        <Suspense fallback={null}>
          <BilyTracking />
        </Suspense>
        {children}
      </body>
    </html>
  );
}

Optional: cover remaining Pages Router routes

The App Router root layout does not wrap routes that still live in pages/. During a hybrid migration, mount the same component once in the Pages Router root too. If pages/_app.tsx already exists, add only the Bily mount and preserve all existing imports, providers, layouts, global CSS, data hooks, and error boundaries. Use the minimal example below only when the app does not already have a Custom App.
pages/_app.tsx
import type { AppProps } from "next/app";
import { Suspense } from "react";
import { BilyTracking } from "../app/bily-tracking";

export default function App({ Component, pageProps }: AppProps) {
  return (
    <>
      <Suspense fallback={null}>
        <BilyTracking />
      </Suspense>
      <Component {...pageProps} />
    </>
  );
}
Mount BilyTracking only once within each route tree. The root layout covers App Router routes; _app.tsx covers Pages Router routes. Use a separate complete URL for each environment. Do not construct it from a hostname or add query parameters in code.

Track client actions

Call track() from client code after the operation succeeds.
app/workspaces/create-workspace-button.tsx
"use client";

import { track } from "@bilyai/js";

export function CreateWorkspaceButton() {
  async function handleCreate() {
    const workspace = await createWorkspace();
    track("workspace_created", { workspace_id: workspace.id });
  }

  return <button onClick={handleCreate}>Create workspace</button>;
}

Track page views

Protect client data