Something went wrong. Try again.
a tool for shared writing and social publishing
Something went wrong. Try again.
TypeScript
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566import { after } from "next/server";import { tinybird } from "lib/tinybird";import { supabaseServerClient } from "supabase/serverClient";import { keyEntitlements } from "./identityPayload";import { isPro, PRO_ENTITLEMENT_KEY } from "./entitlements";
// Active-user counts are "distinct identities with any event"; finer questions// (who moved from reading to writing) are query-time filters on `event` and// `properties`, so add event names and properties freely rather than columns.export type UserEvent = | "page_view" // client beacon on every route change (see actions/trackPageView.ts) | "push"; // ran replicache mutations
// Only signed-in sessions are tracked: the identity row keys the actor so a// person on several devices counts once, and anonymous traffic is left to the// Vercel analytics drain.export type TrackedIdentity = { id: string; atp_did?: string | null };
// Deliberately unthrottled: dedupe happens in Tinybird's endpoints, not here.// Ingestion runs in `after()` so it never blocks or fails the user-facing// request, which also means this must be called from a Next request context.export function trackUserEvent( identity: TrackedIdentity, event: UserEvent, properties: Record<string, string> = {},) { if (!process.env.TINYBIRD_TOKEN) return; after(async () => { try { await tinybird.userEvents.ingest({ timestamp: Date.now(), identity_id: identity.id, did: identity.atp_did ?? "", event, properties: new Map( Object.entries({ ...properties, ...(await proProperties(identity)) }), ), }); } catch (e) { console.error("[trackUserEvent] ingest failed:", e); } });}
// Stamped on every event so the active-user pipes can split by Pro status as// it was at the time, rather than joining today's status onto old rows. Looked// up here instead of at the call sites because none of them load entitlements,// and running inside `after()` keeps it off the request path. A failed lookup// leaves the row unstamped (counted as free) rather than losing the event.async function proProperties(identity: TrackedIdentity) { const { data, error } = await supabaseServerClient .from("user_entitlements") .select("entitlement_key, granted_at, expires_at, source, metadata") .eq("identity_id", identity.id) .eq("entitlement_key", PRO_ENTITLEMENT_KEY); if (error) { console.error("[trackUserEvent] entitlement lookup failed:", error); return {}; } const entitlements = keyEntitlements(data); return { pro: String(isPro(entitlements)), pro_source: entitlements[PRO_ENTITLEMENT_KEY]?.source ?? "", };}