@addilytics/react-router
v0.0.1
Published
React Router server middleware and bundled navigation tracking.
Readme
@addilytics/react-router
Server-side pageviews, SPA navigations, and custom events for React Router Framework Mode.
This adapter runs as root route middleware. It waits for React Router's final Response, including an error boundary response, then passes that exact object back unchanged. It records HTML document requests only. React Router .data requests and __manifest requests don't count as pageviews.
Server-only tracking remains the default. You can add a small browser entry that reports committed React Router navigations to a same-origin relay. The site key and ingest endpoint stay in server code. The helper is bundled by your app; Addilytics does not load a hosted or CDN script.
Manual setup
Create the middleware once and export it from app/root.tsx:
import { createAddilyticsMiddleware } from '@addilytics/react-router';
const analytics = createAddilyticsMiddleware({
endpoint: 'https://addilytics.example',
siteKey: process.env.ADDILYTICS_KEY!
});
export const middleware: Route.MiddlewareFunction[] = [analytics];Order root middleware as authentication/session/context initialization, Addilytics, then routes and response-mutating middleware. Authentication must run first because relay requests finish inside the Addilytics middleware and do not reach later middleware. Addilytics must still wrap response mutators so it sees their final status and content type.
export const middleware: Route.MiddlewareFunction[] = [
authAndSessionMiddleware,
analytics,
responseMiddleware
];React Router 7 projects must enable future.v8_middleware in react-router.config.ts. React Router 8 doesn't need that flag.
The default status policy records 2xx and 404 HTML responses. Supply trackStatuses if you want a different policy. For example, this includes rendered error boundaries with a 500 status:
const analytics = createAddilyticsMiddleware({
endpoint: 'https://addilytics.example',
siteKey: process.env.ADDILYTICS_KEY!,
trackStatuses: (status) => (status >= 200 && status < 300) || status === 404 || status === 500
});SPA pageviews
Use hybrid mode to record the initial document request on the server and later client-side
navigations in the browser. Set the same mode on both sides.
// app/addilytics.server.ts
import type { ReactRouterOptions } from '@addilytics/react-router';
export default {
endpoint: 'https://addilytics.example',
mode: 'hybrid',
siteKey: process.env.ADDILYTICS_KEY!
} satisfies ReactRouterOptions;// app/root.tsx
import { AddilyticsBrowser } from '@addilytics/react-router/browser';
export default function Root() {
return (
<html>
<body>
<AddilyticsBrowser mode="hybrid" />
<Outlet />
<Scripts />
</body>
</html>
);
}AddilyticsBrowser uses useLocation, so it runs after React Router commits a location. It renders
nothing. Mount it once under the root router. Hybrid mode skips its first browser event because the
server already recorded that document. client mode records the initial browser render and every
later navigation, while mode: 'client' on the server disables automatic document pageviews.
React Router runs middleware only for matched routes. Add a resource route for the relay so the root middleware can intercept its POST before the fallback action runs:
// app/routes.ts
import { route } from '@react-router/dev/routes';
export default [
// Your application routes...
route('__addilytics', 'routes/addilytics.ts')
];// app/routes/addilytics.ts
export function action() {
// The root Addilytics middleware handles this route first.
return new Response(null, { status: 404 });
}Hash-only changes are ignored by default. Pass trackHashChanges on the browser component to count
them. Restoring a page from the browser back-forward cache records a fresh pageview.
The relay defaults to /__addilytics. If you change it, the two option names differ but their values
must match:
// Server
createAddilyticsMiddleware({
endpoint: 'https://addilytics.example',
mode: 'hybrid',
relayPath: '/internal/analytics',
siteKey: process.env.ADDILYTICS_KEY!
});
// Browser
<AddilyticsBrowser endpoint="/internal/analytics" mode="hybrid" />;The browser sends only an event ID, timestamp, pathname, allowlisted campaign parameters, and the referrer origin. The middleware derives request metadata and forwards the event with the server-only site key. The relay rejects cross-origin requests and malformed payloads.
Custom events
The middleware puts a request-bound tracker in React Router's context. Loaders and actions can use it without reconstructing a Request:
import { addilyticsContext } from '@addilytics/react-router';
export async function action({ context }: Route.ActionArgs) {
const order = await createOrder();
await context.get(addilyticsContext).track('purchase', {
props: { currency: 'USD', total: order.total }
});
return order;
}The adapter accepts user and ip callbacks with the full middleware arguments. Explicit values passed to track take priority over user:
const analytics = createAddilyticsMiddleware({
endpoint: 'https://addilytics.example',
siteKey: process.env.ADDILYTICS_KEY!,
user: ({ context }) => context.get(userContext).id,
ip: ({ request }) => request.headers.get('x-real-ip')
});Background delivery
If your deployment runtime has waitUntil, put its execution context into the React Router context returned by getLoadContext:
import { RouterContextProvider } from 'react-router';
import { addilyticsExecutionContext } from '@addilytics/react-router';
function getLoadContext(_request: Request, executionContext: ExecutionContext) {
const context = new RouterContextProvider();
context.set(addilyticsExecutionContext, executionContext);
return context;
}You can also pass a waitUntil resolver in the adapter options when your server adapter stores it elsewhere. Without waitUntil, the middleware waits for delivery before returning the response.
Vite setup
The Vite plugin can add the middleware when your root route doesn't already export middleware. Keep credentials in a .server.ts or .server.js module:
// app/addilytics.server.ts
import type { ReactRouterOptions } from '@addilytics/react-router';
export default {
endpoint: 'https://addilytics.example',
mode: 'hybrid',
siteKey: process.env.ADDILYTICS_KEY!
} satisfies ReactRouterOptions;// vite.config.ts
import { reactRouter } from '@react-router/dev/vite';
import { addilyticsReactRouter } from '@addilytics/react-router/vite';
import { defineConfig } from 'vite';
export default defineConfig({
plugins: [reactRouter(), addilyticsReactRouter({ optionsModule: 'app/addilytics.server.ts' })]
});The plugin edits the configured root route while Vite compiles the server. It doesn't patch emitted chunks, and it never imports the options module into the browser build. The build fails if the root already exports middleware, the options file isn't server-only, or the server build never loads the configured root. Compose by hand when your root already has middleware:
export const middleware: Route.MiddlewareFunction[] = [
authAndSessionMiddleware,
analytics,
responseMiddleware
];