Measure Measure
Sign In Start Free Trial
← Blog
remix react-router analytics privacy integration

Adding Analytics to Remix Apps in 2026

by Jules

Remix v2 and React Router v7 are now effectively the same thing — React Router v7 is the path forward for Remix apps, with file-based routing, server-side rendering, and the loader/action model that makes Remix feel different from other React frameworks.

That hybrid architecture creates a few wrinkles when adding analytics. You need client-side tracking for SPA navigation, and you want server-side event tracking for actions that happen without a full page load. Measure.events handles both.

Here’s how to add privacy-first analytics (900 bytes, no cookies, no consent banner) to a Remix or React Router v7 app.

In Remix and React Router v7, the right place to inject global scripts is root.tsx. Use the links export to declare the script — Remix will handle placement in the document head:

// app/root.tsx
import {
  Links,
  Meta,
  Outlet,
  Scripts,
  ScrollRestoration,
} from "@remix-run/react";
// or: from "react-router" in React Router v7

export function links() {
  return [
    {
      rel: "preconnect",
      href: "https://lets.measure.events",
    },
  ];
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
        <script
          defer
          data-site-key="YOUR_SITE_KEY"
          src="https://lets.measure.events/api/script/YOUR_SITE_KEY"
        />
      </head>
      <body>
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

The preconnect link hint tells the browser to establish a connection to the analytics domain early, reducing latency when the deferred script finally loads.

Client-Side Pageview Tracking with useEffect and useLocation

Remix is an SPA after the first load — navigating between routes doesn’t reload the page. You need to track route changes explicitly using useLocation:

// app/root.tsx (updated)
import { useEffect } from "react";
import { useLocation } from "@remix-run/react";
// or: from "react-router" in React Router v7

declare global {
  interface Window {
    measure?: {
      track: (event: string, data?: Record<string, unknown>) => void;
      trackPageview: (data?: { path?: string }) => void;
    };
  }
}

function Analytics() {
  const location = useLocation();

  useEffect(() => {
    // Fire on every route change
    window.measure?.trackPageview({ path: location.pathname + location.search });
  }, [location.pathname, location.search]);

  return null;
}

export default function App() {
  return (
    <html lang="en">
      <head>
        <Meta />
        <Links />
        <script
          defer
          data-site-key="YOUR_SITE_KEY"
          src="https://lets.measure.events/api/script/YOUR_SITE_KEY"
        />
      </head>
      <body>
        <Analytics />
        <Outlet />
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

The Analytics component renders nothing but fires a pageview event every time the route changes. Putting it inside App (inside the router context) gives it access to useLocation.

Custom Event Tracking in Components

For tracking user interactions, create a small utility that wraps the window.measure API:

// app/utils/analytics.ts
export function track(event: string, data: Record<string, unknown> = {}): void {
  window.measure?.track(event, data);
}

Use it in your route components:

// app/routes/pricing.tsx
import { track } from "~/utils/analytics";

export default function Pricing() {
  function handleSignupClick(plan: string) {
    track("signup_click", { plan, source: "pricing_page" });
  }

  return (
    <div>
      <button onClick={() => handleSignupClick("pro")}>
        Start Pro Trial
      </button>
    </div>
  );
}

Server-Side Events via Resource Routes

This is where Remix’s server/client model gets interesting. Sometimes you want to track events that happen server-side — form submissions, purchases, API calls — without relying on client-side JavaScript.

Create a resource route that accepts POST requests and forwards events to Measure.events:

// app/routes/api.track.ts
import { ActionFunctionArgs, json } from "@remix-run/node";
// or: from "react-router" in React Router v7

export async function action({ request }: ActionFunctionArgs) {
  const { event, data } = await request.json();

  await fetch("https://lets.measure.events/api/v1/events", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.MEASURE_API_KEY}`,
    },
    body: JSON.stringify({ event, data }),
  });

  return json({ ok: true });
}

Call it from your actions when something important happens:

// app/routes/checkout.tsx
import { ActionFunctionArgs, redirect } from "@remix-run/node";

export async function action({ request }: ActionFunctionArgs) {
  const formData = await request.formData();
  const plan = formData.get("plan") as string;

  // ... process checkout ...

  // Track server-side — happens even if client JS fails
  await fetch(`${process.env.BASE_URL}/api/track`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      event: "checkout_complete",
      data: { plan },
    }),
  });

  return redirect("/dashboard");
}

Server-side tracking is especially useful for:

  • Purchase confirmations (don’t lose the event if the user closes the tab)
  • Form submissions processed on the server
  • API endpoint usage
  • Webhook handlers

SSR Considerations

Remix renders on the server first. The tracking script uses defer so it only loads client-side, but be careful with direct window.measure calls in code that runs server-side.

The utility function approach handles this safely:

// app/utils/analytics.ts
export function track(event: string, data: Record<string, unknown> = {}): void {
  // Safe to call anywhere — no-ops on the server
  if (typeof window !== "undefined") {
    window.measure?.track(event, data);
  }
}

MCP Server Setup for AI-Powered Queries

Once your analytics are collecting data, set up the MCP server to query it from your AI tools:

npx -y @measure-events/mcp

Add to your Cursor or Claude Code config:

{
  "mcpServers": {
    "measure": {
      "command": "npx",
      "args": ["-y", "@measure-events/mcp"],
      "env": {
        "MEASURE_API_KEY": "your_api_key"
      }
    }
  }
}

With this configured, you can ask your AI assistant questions directly in your editor:

“What Remix routes are getting the most traffic?” “How many checkout_complete events fired this week?” “What’s the top referrer for my app?”

No other analytics tool has native MCP support. For developers building Remix apps with AI-assisted workflows in 2026, this makes your analytics data part of your development context instead of a separate dashboard tab.

Getting Started

  1. Sign up at lets.measure.events/sign-up
  2. Create a site and get your site key
  3. Add the script to root.tsx with the <script defer> tag
  4. Add the Analytics component with useLocation for SPA route tracking
  5. Create the /api/track resource route for server-side events
  6. Optional: install the MCP server for AI-powered queries

Total setup: under 15 minutes including the server-side resource route. Bytes added to your client bundle: 0.

Ready to see accurate analytics?

No cookies. No consent banners. No personal data. $29/mo with a 14-day free trial.

Start free trial →