simeonGriggs.dev
Twitter
GitHub

Add Vercel Analytics to a Remix Application

Vercel offers simple, privacy-focused analytics and web vitals data. These can be instantly set up on Next.js and other frameworks. It's a little extra work for your Remix app, but worth it.

View in Sanity Studio

2022-12-07

Vercel's fast deployments and seamless GitHub integration make it an excellent choice for hosting Remix applications. For Vercel-hosted projects, we can also benefit from analytics reporting.

Audiences

Audience data will show how many people are visiting the site and some additional information like where they're from and what they're using.

Before beginning, you will need to opt-in to Audiences analytics from the project dashboard.

Now in your Remix project, from the command line, install the @vercel/analytics package:

npm install @vercel/analytics

Now add the <Analytics /> component to your root route so that it loads on every page view:

// ./app/routes/root.tsx
// add this to your imports
import { Analytics } from '@vercel/analytics/react';
// then inside your default export
export default function App() {
return (
<html>
<head>
</head>
<body>
<Outlet />
{/* 👇 Add the "Analytics" component inside the body */}
<Analytics />
<Scripts />
<LiveReload />
</body>
</html>
);
}
export default MyApp;

Web Vitals

You will need to opt-in to Web Vitals in the Analytics section of your Vercel project's dashboard. Vercel supports Next and Gatsby with no additional steps. We need to set up the Web Vitals API for other frameworks ourselves.

Again we'll need to install a package into our App, this time web-vitals:

npm install web-vitals

The code we'll need to add to the Remix application is adapted from Vercel's Create React App example.

Add these two new files to your ./app directory:

// ./app/reportWebVitals.ts
import type {CLSReportCallback} from 'web-vitals'
const reportWebVitals = (onPerfEntry: CLSReportCallback) => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({onCLS, onFID, onFCP, onLCP, onTTFB}) => {
onCLS(onPerfEntry)
onFID(onPerfEntry)
onFCP(onPerfEntry)
onLCP(onPerfEntry)
onTTFB(onPerfEntry)
})
}
}
export default reportWebVitals

...and

// ./app/vitals.ts
import type {Metric} from 'web-vitals'
const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals'
function getConnectionSpeed() {
const isSupported = !!(navigator as any)?.connection?.effectiveType
return isSupported ? (navigator as any)?.connection?.effectiveType : ''
}
export function sendToVercelAnalytics(metric: Metric) {
// This requires us passing the variable from a loader in root.tsx
const analyticsId = window?.ENV?.VERCEL_ANALYTICS_ID
if (!analyticsId) {
return
}
const body = {
dsn: analyticsId,
id: metric.id,
page: window.location.pathname,
href: window.location.href,
event_name: metric.name,
value: metric.value.toString(),
speed: getConnectionSpeed(),
}
const blob = new Blob([new URLSearchParams(body).toString()], {
// This content type is necessary for `sendBeacon`
type: 'application/x-www-form-urlencoded',
})
if (navigator.sendBeacon) {
navigator.sendBeacon(vitalsUrl, blob)
} else
fetch(vitalsUrl, {
body: blob,
method: 'POST',
credentials: 'omit',
keepalive: true,
})
}

Adding the analytics ID

You'll see in this last file that reporting web vitals back to Vercel requires the variable VERCEL_ANALYTICS_ID which is present during the build.

However, the web vitals script needs to run client-side, and in Remix, we need to handle environment variables a little differently than you're used to by writing them to the global window.

If you don't already have something like this setup, you'll need to update your loader function in your root route.

// ./app/root.tsx
export const loader = () => {
return {
ENV: {
VERCEL_ANALYTICS_ID: process.env.VERCEL_ANALYTICS_ID,
},
}
}
// Optional:
// If you're not already handling TS support for environment variables
declare global {
interface Window {
ENV: SerializeFrom<typeof loader>['ENV']
}
}
export default function App() {
const {ENV} = useLoaderData<typeof loader>()
// Then just before your closing </body> tag
return (
<html>
<head>
</head>
<body>
<Outlet />
<Analytics />
<Scripts />
<LiveReload />
{/* 👇 Write the ENV values to the window */}
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(ENV)}`,
}}
/>
</body>
</html>
);
}

Lastly, we bring this all together by updating entry.client.tsx, where the reporting function will run.

// ./app/entry.client.tsx
import {RemixBrowser} from '@remix-run/react'
import {hydrateRoot} from 'react-dom/client'
import reportWebVitals from './reportWebVitals'
import {sendToVercelAnalytics} from './vitals'
hydrateRoot(document, <RemixBrowser />)
reportWebVitals(sendToVercelAnalytics)

Re-deploy your Remix application, and you should see Web Vitals analytics pouring in.

It's important to know that at first, you may see heavily skewed figures based on a few data points; however, as more come in, it should be simpler to reason about the actual performance of your app.

Go further