Skip to content

Commit 83e4fa2

Browse files
committed
fix: handle soft navigation in App Router
- Add soft-navigation event type to fetch scripts on client-side navigation - Use usePathname() to detect navigation changes - Dispatch pageView to immediate backends on soft navigation - Revert pages router to use hard navigation (maintains existing behavior)
1 parent 455402a commit 83e4fa2

4 files changed

Lines changed: 61 additions & 6 deletions

File tree

e2e/test-app/src/nextlytics.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ import { auth } from "./auth";
88
type ScriptMode = "once" | "on-params-change" | "every-render";
99
type ScriptElement = { async?: boolean; body?: string; src?: string; mode?: ScriptMode };
1010
type JavascriptTemplate = { items: ScriptElement[] };
11-
type ClientAction = { items: Array<{ type: "script-template"; templateId: string; params: unknown }> };
11+
type ClientAction = {
12+
items: Array<{ type: "script-template"; templateId: string; params: unknown }>;
13+
};
1214

1315
const CONSOLE_TEMPLATE_ID = "console-test";
1416

e2e/test-app/src/pages/pages-home.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { useSession } from "next-auth/react";
2-
import Link from "next/link";
32

43
export default function PagesHome() {
54
const { data: session } = useSession();
@@ -24,9 +23,9 @@ export default function PagesHome() {
2423
</div>
2524
)}
2625
<nav>
27-
<Link href="/pages-test" data-testid="test-page-link">
26+
<a href="/pages-test" data-testid="test-page-link">
2827
Test Page
29-
</Link>
28+
</a>
3029
</nav>
3130
</main>
3231
);

packages/core/src/client.tsx

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
useReducer,
1111
useRef,
1212
} from "react";
13+
import { usePathname } from "next/navigation";
1314
import type {
1415
ClientContext,
1516
JavascriptTemplate,
@@ -146,7 +147,7 @@ function NextlyticsScripts({
146147
}) {
147148
const context = useContext(NextlyticsContext);
148149
if (!context) {
149-
throw new Error("NextlyticsScripts should be called within NextlyticsContext")
150+
throw new Error("NextlyticsScripts should be called within NextlyticsContext");
150151
}
151152

152153
const { scriptsRef, subscribersRef, templates, requestId } = context;
@@ -224,6 +225,11 @@ async function sendEventToServer(
224225
export function NextlyticsClient(props: { ctx: NextlyticsContext; children?: ReactNode }) {
225226
const { requestId, scripts: initialScripts = [], templates = {} } = props.ctx;
226227

228+
// Track pathname for soft navigation detection
229+
const pathname = usePathname();
230+
const initialPathRef = useRef<string | null>(null);
231+
const lastPathRef = useRef<string | null>(null);
232+
227233
// Refs for dynamic scripts (from sendEvent calls) - stable, no re-renders
228234
const scriptsRef = useRef<TemplatizedScriptInsertion<unknown>[]>([]);
229235
const subscribersRef = useRef<Set<() => void>>(new Set());
@@ -241,11 +247,27 @@ export function NextlyticsClient(props: { ctx: NextlyticsContext; children?: Rea
241247

242248
// Send client-init on mount (once per requestId)
243249
useEffect(() => {
250+
initialPathRef.current = pathname;
251+
lastPathRef.current = pathname;
244252
const clientContext = createClientContext();
245253
sendEventToServer(requestId, "client-init", clientContext).then(({ scripts }) => {
246254
if (scripts?.length) addScripts(scripts);
247255
});
248-
}, [requestId, addScripts]);
256+
}, [requestId, addScripts, pathname]);
257+
258+
// Detect soft navigation and fetch scripts for new page
259+
useEffect(() => {
260+
// Skip if this is the initial render or same path
261+
if (initialPathRef.current === null || pathname === lastPathRef.current) {
262+
return;
263+
}
264+
lastPathRef.current = pathname;
265+
266+
const clientContext = createClientContext();
267+
sendEventToServer(requestId, "soft-navigation", clientContext).then(({ scripts }) => {
268+
if (scripts?.length) addScripts(scripts);
269+
});
270+
}, [pathname, requestId, addScripts]);
249271

250272
return (
251273
<NextlyticsContext.Provider value={contextValue}>

packages/core/src/middleware.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,38 @@ async function handleEventPost(
289289
// Also update "immediate" backends with client context
290290
after(() => updateEvent(pageRenderId, { clientContext, userContext, anonymousUserId }, ctx));
291291

292+
// Filter to script-template only
293+
const scripts = actions.items.filter((i) => i.type === "script-template");
294+
return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : undefined });
295+
} else if (type === "soft-navigation") {
296+
// Soft navigation in App Router - layout didn't re-render, so we need to
297+
// dispatch pageView and return scripts that would have been in the initial render
298+
const clientContext = payload as unknown as ClientInitPayload;
299+
const serverContext = reconstructServerContext(apiCallServerContext, clientContext);
300+
301+
const { anonId: anonymousUserId } = await resolveAnonymousUser({
302+
ctx,
303+
serverContext,
304+
config,
305+
});
306+
307+
const event: NextlyticsEvent = {
308+
eventId: generateId(), // New event ID for the soft navigation
309+
parentEventId: pageRenderId,
310+
type: "pageView",
311+
collectedAt: new Date().toISOString(),
312+
anonymousUserId,
313+
serverContext,
314+
clientContext,
315+
userContext,
316+
properties: {},
317+
};
318+
319+
// Dispatch to "immediate" backends (same as middleware would do)
320+
const { clientActions, completion } = dispatchEvent(event, ctx, "immediate");
321+
const actions = await clientActions;
322+
after(() => completion);
323+
292324
// Filter to script-template only
293325
const scripts = actions.items.filter((i) => i.type === "script-template");
294326
return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : undefined });

0 commit comments

Comments
 (0)