Custom Events
Track signups, purchases, and any other action that matters with one function call.
The track() API
Import track from the same entry point you use to install the tracker:
import { track } from 'produl-tracker/next'
// or 'produl-tracker/react', 'produl-tracker/vue', 'produl-tracker/remix'
track('cta_click', { location: 'hero', variant: 'blue' })The signature is:
track(eventName: string, properties?: Record<string, unknown>): voidCalls are buffered via a command queue (window.ma), so track() is safe to call before the tracker script has finished loading. Queued events flush as soon as the tracker is ready.
Framework examples
Next.js (App Router)
Mark the component as a Client Component and import track from produl-tracker/next.
'use client'
import { track } from 'produl-tracker/next'
export default function SignupButton({ plan }: { plan: string }) {
return (
<button
onClick={async () => {
await createAccount(plan)
track('signup', { plan })
}}
>
Get started
</button>
)
}React (Vite / CRA)
import { track } from 'produl-tracker/react'
export default function CheckoutButton({ productId }: { productId: string }) {
return (
<button
onClick={() => {
track('add_to_cart', { product_id: productId })
openCheckout(productId)
}}
>
Buy now
</button>
)
}Vue 3
Import track from produl-tracker/vue and call it anywhere in your component logic.
<script setup lang="ts">
import { track } from 'produl-tracker/vue'
function handleUpgrade(plan: string) {
track('upgrade_clicked', { plan })
router.push('/checkout')
}
</script>
<template>
<button @click="handleUpgrade('pro')">Upgrade to Pro</button>
</template>Remix
import { track } from 'produl-tracker/remix'
export default function PricingPage() {
return (
<button
onClick={() => track('pricing_cta_clicked', { section: 'hero' })}
>
Start free trial
</button>
)
}Plain HTML (script tag)
When using the plain script tag, call window.ma.track() directly. The queue handles calls made before the script has loaded.
<!-- Queued call: safe even if called before tracker.min.js loads -->
<button onclick="window.ma('event', 'cta_click', { location: 'footer' })">
Get started
</button>
<script>
document.getElementById('upgrade-btn').addEventListener('click', function () {
window.ma('event', 'upgrade_clicked', { plan: 'pro' })
})
</script>Script-tag queue
window.ma('event', name, props)as a command queue. Calls made before the tracker loads are buffered and flushed once it is ready. The npm SDK's track() export writes to the same queue, so both approaches are interchangeable.Auto-tracked events
Produl automatically fires three event types without any extra code — they appear in your dashboard alongside your manual events.
File downloads — file_download
Fired whenever a visitor clicks a link that points to a downloadable file. Recognised extensions: .pdf, .zip, .doc, .docx, .xls, .xlsx, .csv, .pptx, .mp3, .mp4, .dmg, .exe.
{
"url": "https://example.com/docs/guide.pdf",
"filename": "guide.pdf",
"type": "pdf"
}Outbound clicks — outbound_click
Fired when a visitor clicks a link to an external domain (any hostname different from the current page).
{
"url": "https://github.com/your-org/repo",
"text": "View on GitHub"
}404 errors — 404
Fired 500 ms after page load if the page title contains the string "404" or "not found" (case-insensitive). Use this to monitor broken links without any backend instrumentation.
{
"path": "/old/broken-url",
"referrer": "https://external-site.com/article"
}Auto-tracked events count against your plan quota
file_download, outbound_click, and 404 events are billed the same as manual events. If you have a high-traffic site with many outbound links, check your monthly usage in the dashboard.Naming conventions
Event names are arbitrary strings, but dashboards group events by exact name — so consistency matters. We recommend:
- snake_case:
sign_up,cta_click,form_submit - Present tense verbs:
subscribe, notsubscribed - Noun + verb for context:
video_played,invoice_paid
Renames split your history
Event properties
Properties are an optional second argument — any JSON-serializable object. Keys should be short and snake_case. Values can be strings, numbers, booleans, or nested objects/arrays.
track('video_played', {
video_id: 'onboarding-01',
duration_seconds: 12,
autoplay: false,
user_plan: 'pro',
})Properties are fully queryable in the Ask Produl natural-language interface and custom dashboards. For example: "How many pro users played the onboarding video this week?"
Do not include PII
Viewing events
Custom events appear in the Custom Events widget on your site dashboard. The widget shows each event name ranked by total count, with a secondary column showing how many unique visitors triggered it. The top 10 events are shown; use Ask Produl to query the full event list or filter by property values.
The widget is available on all plans. Events are grouped by exact name and aggregated for the selected date range — the same date picker that controls your pageview charts also controls the event counts.
Server-side events
Some events happen server-side — webhook deliveries, Stripe subscriptions, cron job outcomes. For those, use the Server SDK which POSTs events directly to /api/collect with an authenticated server key.
See Server-Side Tracking for install and auth details.
Best practices
- Track outcomes, not implementation details.
checkout_completedis more durable thanclick_stripe_submit_v2. - Keep property keys stable. Adding new keys is fine; renaming or removing them will break existing dashboards and alerts.
- Use property values, not event names, for variants. Prefer
track('signup', { plan: "pro" })overtrack('signup_pro'). - Don't over-track. You only have so many events per month on each plan. Track things you would plausibly make a decision from.
- Verify with the debug overlay. Add
debugto your<Analytics>component temporarily — it shows a live counter of events sent and lets you confirm eachtrack()call fires correctly before deploying.