Setup custom events
Track anything that matters: feature usage, workflow milestones, revenue events. From the browser or from your server.
Client-side events
The analytics script exposes a low-level window.grademypage.track(eventType, options?) for any event that isn't a conversion. Pass any eventType you want (use snake_case), plus an optional eventName and eventData object.
// Fire any event you want — pick a stable eventType string.
window.grademypage?.track('video_played');
// With a name and custom properties.
window.grademypage?.track('feature_used', {
eventName: 'pdf_export',
eventData: { plan: 'pro', pages: 12 },
});track vs trackConversion
trackConversion is a one-line shorthand for track('conversion', ...). Use it when the event is a goal you want to chart on the Conversions panel; use track for everything else.
// Low-level — any eventType you choose.
window.grademypage?.track('onboarding_step', {
eventName: 'completed',
eventData: { step: 3 },
});
// Shorthand — always fires eventType: 'conversion'.
window.grademypage?.trackConversion('signup');
// …is exactly the same as:
window.grademypage?.track('conversion', { eventName: 'signup' });Events fired automatically
| Event | Payload |
|---|---|
| pageview | pathname, fullUrl, referrer, UTMs |
| scroll | { depth: 25 | 50 | 75 | 100 } |
| engagement | { engagementSeconds: number } |
| experiment_impression | { experimentId, variantId, variantName } |
Server-side events
For events that must always be recorded (payment confirmations, subscription changes, abuse signals), call the server-events endpoint from your backend. It uses a secret key instead of the public project key.
POST https://www.grademypage.com/api/analytics/server-events
Content-Type: application/json
Authorization: Bearer <YOUR_SECRET_KEY>
{
"eventType": "conversion",
"eventName": "subscription_started",
"eventData": {
"plan": "pro_annual",
"revenue": 249.00
},
"idempotencyKey": "sub_1Oxyz_created"
}Stripe webhook example
// app/api/webhooks/stripe/route.ts
export async function POST(req: Request) {
const event = await verifyStripeWebhook(req);
if (event.type === "checkout.session.completed") {
await fetch("https://www.grademypage.com/api/analytics/server-events", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${process.env.GMP_SECRET_KEY}`,
},
body: JSON.stringify({
eventType: "conversion",
eventName: "purchase",
eventData: {
plan: event.data.object.metadata.plan,
revenue: event.data.object.amount_total / 100,
},
idempotencyKey: event.id,
}),
});
}
return Response.json({ received: true });
}Idempotency
Webhooks retry. Pass a stable idempotencyKey (like the Stripe event ID) and Grademypage will dedupe, so at-least-once delivery becomes exactly-once in your reports.
Attributing to experiments
If the server-side conversion should count toward an A/B experiment variant, your backend needs to know which variant the visitor was bucketed into. The browser knows; you need to forward that context.
- On the page where the conversion starts (signup form, checkout button, etc.), read the visitor's current assignment:
const [assignment] = window.grademypage?.getExperimentAssignments?.() ?? []; // { experimentId, variantId, variantName } | undefined - Forward
experimentIdandvariantIdthrough whatever carries your conversion to the backend (a hidden form field, your signup API payload, Stripemetadata, ClerkunsafeMetadata, etc.). - Pass them to
/server-eventsas top-level fields:{ "eventType": "conversion", "eventName": "signup", "idempotencyKey": "clerk_evt_123", "visitor": { "id": "user_abc" }, "experimentId": "exp_hero_v2", "variantId": "var_b" }
experimentId/variantId keep working. The fields are optional; only attribution to experiments requires them.