Client API
:::caution Pre-release This is a pre-1.0 library (v0.1.1) — API may change without notice. :::
Client-side SDK for browser environments. Import from @next-story/journey-recorder/client.
initJourney(config)
Initialize the journey recorder and start tracking user interactions.
import { initJourney } from "@next-story/journey-recorder/client";
const journey = initJourney({
siteId: "my-site",
collectUrl: "/api/journey/collect",
conversionRoutePattern: "/checkout",
});
Parameters
config: JourneyConfig
-
siteId(required):string
The site's canonical key, used for diagnostics and demo readability. Never sent in the request body; the collector derives the actualsite_idfrom the verified token's origin claim. -
collectUrl(required):string
Same-origin Route Handler path, e.g./api/journey/collect. -
identityResolver(optional):() => string
A function that returns a first-party identifier. Called on initialization. Defaults to minting and reading the__ns_aidcookie. The authoritative identity resolution happens server-side. -
conversionRoutePattern(optional):string
One URL pattern. Apage_viewwhose normalizedroute_patternmatches this string automatically emits aconversionevent. Example:/checkout(exact match only; no regex, no wildcards). -
releaseId(optional):string
Release identifier, e.g. fromprocess.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA. Stamped on every event batch for debugging version-specific issues. -
requireConsent(optional):boolean
Whentrue, recording fails closed unlessglobalThis.__nsJourneyConsent = trueis set. Default:false. See Consent for details.
Returns
Journey
An object with three methods:
interface Journey {
track(name: "conversion", props?: { name?: string }): void;
reportRoute(pathname: string): void;
stop(): void;
}
-
track("conversion", props?): Explicitly emit a conversion event. Thenameparameter inpropsis optional and becomesconversion_namein the event. Useful for checkout completions, sign-ups, or other goal actions. -
reportRoute(pathname): Report a route change. Call withusePathname()in auseEffect(). Idempotent — duplicate calls are deduplicated client-side. Intentionally does not readuseSearchParams()to preserve privacy (query strings are normalized away). -
stop(): Tear down the recorder. Call during component unmount or HMR. After callingstop(), a subsequentinitJourney()call will restart.
Example
"use client";
import { initJourney } from "@next-story/journey-recorder/client";
import { useEffect } from "react";
import { usePathname } from "next/navigation";
export function JourneyRecorder() {
const pathname = usePathname();
useEffect(() => {
const journey = initJourney({
siteId: "my-site",
collectUrl: "/api/journey/collect",
});
journey.reportRoute(pathname);
// Explicit conversion on button click
const handleCheckout = () => {
journey.track("conversion", { name: "checkout_click" });
};
window.addEventListener("checkout-click", handleCheckout);
return () => {
window.removeEventListener("checkout-click", handleCheckout);
journey.stop();
};
}, [pathname]);
return null;
}
useJourneyRoute() (Next.js App Router)
Route source for the App Router. Call from a "use client" component's effect with usePathname():
import { useJourneyRoute } from "@next-story/journey-recorder/client";
import { usePathname } from "next/navigation";
export function RouteReporter() {
const pathname = usePathname();
useJourneyRoute(pathname);
return null;
}
Or inline in your layout:
"use client";
import { usePathname } from "next/navigation";
import { initJourney } from "@next-story/journey-recorder/client";
import { useEffect } from "react";
export default function RootLayout({ children }: { children: React.ReactNode }) {
const pathname = usePathname();
useEffect(() => {
const journey = initJourney({
siteId: "my-site",
collectUrl: "/api/journey/collect",
});
journey.reportRoute(pathname);
return () => journey.stop();
}, [pathname]);
return <html><body>{children}</body></html>;
}
Events per Session
The client enforces a hard cap of 200 events per session (defined in packages/journey-recorder/src/shared/event.ts). Events after the 200th are dropped client-side, and a truncated flag is set on all subsequent batches so the server can log the condition.
Consent Handling
When requireConsent is true, no events are recorded until globalThis.__nsJourneyConsent = true is explicitly set by your consent banner:
// When user opts in:
globalThis.__nsJourneyConsent = true;
// When user opts out:
globalThis.__nsJourneyConsent = false;
Listen for consent changes:
import { onConsentChange } from "@next-story/journey-recorder/client";
onConsentChange((consentGranted) => {
console.log("Consent changed:", consentGranted);
});