Handling anonymous users
Overview
Sometimes you need to roll out features, run experiments, log analytics events, create AI loops, or tune configuration for anonymous users rather than logged-in users, e.g. visitors to your marketing site.
Hypertune is designed for flexibility so you're not constrained to a baked-in User
type. Instead you can define your own input types in your schema, and then target features and run experiments on any kind of unit.
A unit can be a logged-in user, an anonymous user (i.e. a device), an IP address, a wallet address, a server, a business, or any other entity you choose.
You can set the Unit ID on each Split Control. By default, this is set to context.user.id
. Hypertune will use the Unit ID to randomly assign units to arms of your split. For test splits, this assignment is stable so the same unit will always be assigned to the same arm.
When you build an analytics funnel to view results, you can set the Unit ID of each event step. This enables Hypertune to join each step of your funnel together correctly, across splits and events.
Setup
To handle anonymous users, first add an anonymousId
field to your User
input type:
input User {
id: String!
anonymousId: String!
name: String!
email: String!
}
Create setAnonymousIdIfNeeded
and getAnonymousId
helper functions:
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
import {
RequestCookies,
ResponseCookies,
} from "next/dist/compiled/@edge-runtime/cookies";
const anonymousIdCookieName = "hypertuneAnonymousId";
export async function getAnonymousId(): Promise<string | null> {
const cookieStore = await cookies();
return cookieStore.get(anonymousIdCookieName)?.value ?? null;
}
export function setAnonymousIdIfNeeded(
request: NextRequest,
response: NextResponse,
): void {
if (request.cookies.get(anonymousIdCookieName)) {
return;
}
const newAnonymousId = crypto.randomUUID();
response.cookies.set({
name: anonymousIdCookieName,
value: newAnonymousId,
path: "/",
});
addResponseCookiesToRequest(request, response);
}
function addResponseCookiesToRequest(
request: NextRequest,
response: NextResponse,
): void {
mutateRequestHeaders(request, response, (newRequestHeaders) => {
const setCookies = new ResponseCookies(response.headers);
const newRequestCookies = new RequestCookies(newRequestHeaders);
setCookies.getAll().forEach((cookie) => newRequestCookies.set(cookie));
});
}
const xMiddlewareOverrideHeadersHeaderName = "x-middleware-override-headers";
const xMiddlewareRequestHeaderNamePrefix = "x-middleware-request-";
function mutateRequestHeaders(
request: NextRequest,
response: NextResponse,
mutate: (newRequestHeaders: Headers) => void,
): void {
const newRequestHeaders = new Headers(request.headers);
// Add existing request header overrides to new request headers
response.headers.forEach((value, key) => {
if (key.toLowerCase().startsWith(xMiddlewareRequestHeaderNamePrefix)) {
const originalName = key.slice(xMiddlewareRequestHeaderNamePrefix.length);
newRequestHeaders.set(originalName, value);
}
});
mutate(newRequestHeaders);
const dummyResponse = NextResponse.next({
request: { headers: newRequestHeaders },
});
// Add request header overrides to response
dummyResponse.headers.forEach((value, key) => {
if (
key === xMiddlewareOverrideHeadersHeaderName ||
key.toLowerCase().startsWith(xMiddlewareRequestHeaderNamePrefix)
) {
response.headers.set(key, value);
}
});
}
Call setAnonymousIdIfNeeded
in middleware:
import { NextRequest, NextResponse } from "next/server";
import { setAnonymousIdIfNeeded } from "@/lib/utils";
export const config = {
matcher: "/:path*",
};
export async function middleware(request: NextRequest) {
const response = NextResponse.next();
setAnonymousIdIfNeeded(request, response);
return response;
}
Finally, pass the anonymous ID to Hypertune:
import "server-only";
import { unstable_noStore as noStore } from "next/cache";
import { createSource } from "@/generated/hypertune";
import { getAnonymousId } from "@/lib/utils";
const hypertuneSource = createSource({
token: process.env.NEXT_PUBLIC_HYPERTUNE_TOKEN!,
});
export default async function getHypertune() {
noStore();
await hypertuneSource.initIfNeeded(); // Check for flag updates
const anonymousId = getAnonymousId() ?? "error";
return hypertuneSource.root({
args: {
context: {
environment: process.env.NODE_ENV,
user: { id: "1", anonymousId, name: "Test", email: "[email protected]" },
},
},
});
}
Now you can set context.user.anonymousId
as the Unit ID for:
Split Controls with a split that you want to run on anonymous users
Event steps in funnels when viewing analytics for anonymous users
Last updated