Instrumenting a video player is one attribute. Doing it in the Next.js App Router is a question about boundaries. Four values have to reach the player, and each one lives somewhere different.
A public key belongs in the browser bundle. A video title only the server knows. A viewer ID comes from a session. A Secret Key must never leave the server at all.
Get the split wrong one way and your metrics are anonymous. Get it wrong the other way and you've published a credential.
TL;DR
Tracking turns on the moment metadata-workspace-key lands on the <fastpix-player> element. That key is built for client code, so NEXT_PUBLIC_ is right for it. It's catastrophic for the Access Token ID or Secret Key. Resolve the video title, video ID and viewer ID in the Server Component that already fetched the media. Pass them down as props, so the first event carries them. Import the player inside useEffect. Client components are still server-rendered for the initial HTML, so a module-scope import puts the whole player bundle in the payload. An unstable viewer ID is worse than an empty one, because wrong numbers still look fine.
Which FastPix keys are safe in NEXT_PUBLIC_
The whole integration rests on one distinction. It's also the one people get backwards.
metadata-workspace-key says which workspace playback events belong to. The browser is what sends those events, so the key is built to sit in client code. Prefixing it with NEXT_PUBLIC_ is correct, not risky.
The Access Token ID and Secret Key are a different class of value. They authenticate API calls and belong on the server. Prefix either one and it ships as a literal string in a JavaScript file, to every visitor.
| Value | Where it belongs | NEXT_PUBLIC_ prefix |
|---|---|---|
| Workspace key | Browser | Correct |
| Access Token ID | Server only | Never |
| Secret Key | Server only | Never |
| Playback ID for a public video | Browser | Not a secret, no prefix needed |
| Signed playback token | Minted on the server per request | Never a build-time value |
What you need before you start
- A Next.js application on the App Router.
- A Workspace key from set up a workspace.
- A playback ID for a video that has finished encoding.
Adding video analytics to the Next.js player element
There's no separate analytics package for the FastPix web player. Add metadata-workspace-key to the element and tracking starts. Every other metadata attribute is an optional dimension on top:
"use client";
import { useEffect } from "react";
export default function Player({
playbackId,
title,
videoId,
viewerId,
}: {
playbackId: string;
title: string;
videoId: string;
viewerId?: string;
}) {
useEffect(() => {
import("@fastpix/fp-player");
}, []);
return (
<fastpix-player
playback-id={playbackId}
stream-type="on-demand"
metadata-workspace-key={process.env.NEXT_PUBLIC_FASTPIX_WORKSPACE_KEY}
metadata-video-title={title}
metadata-video-id={videoId}
metadata-viewer-user-id={viewerId}
/>
);
}The import sits inside useEffect for a reason, and it isn't the one most people give. Player 1.0.21 and later guard their customElements registration, so a module-scope import no longer throws during server rendering. What it does do is put roughly 228 KB of player script into the bundle for every visitor, including the ones who never reach the video. The same trade-off governs a hero video, covered in background video in Next.js without killing LCP.
Which Next.js component supplies each metadata value
This integration is Next.js-shaped rather than generic for one reason. App Router gives every value a natural home. Put a value in the wrong home and the data arrives technically present and useless.
The video title and ID come from a Server Component. The page rendering the player already fetched the media. Pass them down as props. Fetching them again in the client is a second round trip for data you already hold.
The viewer ID comes from your session on the server. Read it in a client component and you get whatever the browser happens to know. On first render that's often nothing. Pass it from the server and the very first event carries it.
The workspace key comes from the environment at build time. It's the same for every viewer, so there's nothing to resolve per request.
// app/play/[mediaId]/page.tsx
import { Fastpix } from "@fastpix/fastpix-node";
import Player from "../../components/Player";
const fastpix = new Fastpix({
security: {
username: process.env.FASTPIX_USERNAME,
password: process.env.FASTPIX_PASSWORD,
},
});
export default async function Page({ params }: { params: Promise<{ mediaId: string }> }) {
const { mediaId } = await params;
const media = await fastpix.manageVideos.get({ mediaId });
if (!("data" in media)) throw new Error("Media not found.");
return (
<Player
playbackId={media.data!.playbackIds![0].id!}
title={media.data!.title ?? "Untitled"}
videoId={mediaId}
viewerId={await currentUserId()}
/>
);
}That page runs on the server. The SDK client and its credentials never reach the browser, and only the four resolved strings cross. Every attribute and its mapped field is listed in monitor video data.
You can put a real stream behind this first. The free plan covers 10 videos and 100K streaming minutes a month, no credit card.
A viewer ID that changes per visit breaks unique viewers
Unique viewers, returning viewers and per-account debugging all depend on metadata-viewer-user-id being stable for the same person.
Generate that ID in the client on each load and every session looks like a new person. The dashboard still fills up. The charts still render. The unique-viewer number is meaningless. That's worse than leaving the field empty, because empty is visibly missing and wrong isn't.
So pass an ID your server already trusts. Your user ID works. So does a stable anonymous ID you set in a cookie and can read back. If you have neither, leave the field out until you do.
Custom dimensions for Next.js video analytics
There are more fields here than most people use. metadata-video-series, metadata-video-content-type, metadata-player-name, metadata-experiment-name, and metadata-custom-1 through metadata-custom-10. They exist so a QoE dashboard can answer a product question, not an infrastructure one.
Fill the ones your product segments on. Course ID, creator tier, plan, experiment arm. Those tell you a specific cohort is being underserved. An average startup time never will.
Do it at integration time. Metadata attaches at playback and can't be backfilled. A dimension added next quarter has no history behind it and nothing to compare against.
Privacy attributes are worth knowing in the same pass. respect-do-not-track honours the browser's DNT signal. disable-cookies collects without them. disable-data-monitoring turns tracking off on one player while leaving its metadata attributes in place.
Start tracking playback in your Next.js app
Add metadata-workspace-key from a NEXT_PUBLIC_ variable. Pass the title, video ID and viewer ID down from the Server Component that already fetched the media. Keep the player import inside useEffect. Pick your custom dimensions in the same sitting, because they can't be backfilled.
The free plan covers 10 videos and 100K streaming minutes a month, no credit card. Real sessions show up in the dashboard before anything ships.
Frequently Asked Questions (FAQs)
How do I add video analytics to a Next.js app?
Add metadata-workspace-key to the <fastpix-player> element and tracking starts. Pass that key from a NEXT_PUBLIC_ environment variable, since it is built for client code. Resolve the video title, video ID and viewer ID in the Server Component that renders the player. Import the player inside useEffect, which keeps about 228 KB of player script out of the initial bundle.
Is it safe to put the FastPix workspace key in NEXT_PUBLIC_?
Yes. The workspace key says which workspace playback events belong to. The browser is what sends those events, so the key is meant to be public. The Access Token ID and Secret Key are the opposite. Prefix either one and you publish a credential to every visitor.
Where should video metadata come from in the App Router?
From wherever the value already exists. The video title and ID come from the Server Component that fetched the media and passed them down as props. The viewer ID comes from your session on the server, so the first event carries it. The workspace key comes from the environment at build time. It is identical for every viewer.
Why are my unique viewer numbers wrong?
Usually because metadata-viewer-user-id is generated in the client on each load. Every session then looks like a new person. Pass an ID your server already trusts, such as your user ID or a stable anonymous ID from a cookie. An unstable ID is worse than an empty field because empty is visibly missing and wrong is not.
Can I turn tracking off for one player?
Yes. Add disable-data-monitoring to that element. It stops sending events while the metadata attributes stay in place. respect-do-not-track honors the browser's DNT signal. disable-cookies collects without setting them, which suits a consent-gated player.





