Frameworks
React Router and Remix
React Router analytics with Stickybeak starts by adding its script to your rendered pages; this guide shows you where it belongs and how to track page views, goals and purchase revenue.
For library mode with Vite, use the React guide.
Add the script
Put your site ID in an environment variable
Set
VITE_STICKYBEAK_SITE_IDin.env.productionor production host settings.VITE_STICKYBEAK_SITE_ID=YOUR_SITE_IDAdd the tag to the Layout in app/root.tsx
import { Links, Meta, Outlet, Scripts, ScrollRestoration } from "react-router"; const siteId = import.meta.env.VITE_STICKYBEAK_SITE_ID; export function Layout({ children }: { children: React.ReactNode }) { return ( <html lang="en"> <head> <meta charSet="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <Meta /> <Links /> {siteId && ( <script defer data-site={siteId} data-domain="example.com" src="https://app.usestickybeak.com/_cfw/s.js" /> )} </head> <body> {children} <ScrollRestoration /> <Scripts /> </body> </html> ); } export default function App() { return <Outlet />; }In Remix v2 the file is the same; the imports come from
@remix-run/react.Build and deploy
In
react-router devthe variable isn't set, so nothing loads.
This also works with ssr: false (SPA mode) and prerendering: the root layout is still rendered into the HTML file.
Check it's working
Test the deployed site, then verify your install.
Track goals
Add data-goal to rendered links or buttons. For other events, use the typed queued helper below. See Custom goals.
<Link to="/signup" data-goal="start_signup" data-goal-plan="pro">Start free trial</Link>
type GoalProps = Record<string, string | number | boolean>;
type Stickybeak = ((goal: string, props?: GoalProps) => void) & { q?: unknown[][] };
declare global {
interface Window {
stickybeak?: Stickybeak;
}
}
export function track(goal: string, props?: GoalProps): void {
if (typeof window === "undefined") return; // on the server: nothing to do
if (!window.stickybeak) {
// Hold calls made before the script loads; it sends them when it arrives.
const queue: Stickybeak = (...args) => {
(queue.q ??= []).push(args);
};
window.stickybeak = queue;
}
window.stickybeak(goal, props);
}
import { useEffect } from "react";
import { Form, useActionData } from "react-router";
import { track } from "~/lib/stickybeak";
import type { Route } from "./+types/newsletter";
export async function action({ request }: Route.ActionArgs) {
const form = await request.formData();
await subscribe(String(form.get("email")));
return { subscribed: true };
}
export default function Newsletter() {
const data = useActionData<typeof action>();
useEffect(() => {
if (data?.subscribed) track("newsletter_signup");
}, [data]);
return (
<Form method="post">
<input type="email" name="email" required />
<button>Subscribe</button>
</Form>
);
}
Track purchases
After confirming an order in the browser, send:
window.stickybeak?.("purchase", { revenue: 49, currency: "USD", order_id: "ORDER_ID" });
Use real values; see Revenue.
Page changes
Links, navigate(), action redirects, back and forward count new paths. Replace, query-only and hash-only changes do not. See Single-page apps.
If it isn't working
Check the production site ID and domain, then see Troubleshooting.