Custom events
Tocadule ships with a set of standard commerce events (product_view, add_to_cart,
checkout_started, purchase, …). But tocadule.track() accepts any event name you
want. If you have a behaviour that matters to your store — wishlist_added,
size_guide_opened, filter_applied, gift_message_written — you can send it, and
Tocadule stores it, counts it, and lets you drill into it.
This page explains exactly what happens to a non-standard event once you fire it, and where it shows up in the dashboard so you can confirm it arrived.
tocadule.track('size_guide_opened', {
product_id: 'SKU-102',
category: 'shoes'
})Prerequisite: the install stub
tocadule.track() only works once the tracking object exists on the page. The snippet
loads asynchronously, so any inline call that runs before it finishes would be lost —
unless you install the tiny stub loader in your site-wide <head>. The stub queues every
track / identify / cart / requestPush call into window.tocadule._q and the real
snippet replays the queue in order once it boots.
<script>window.tocadule=window.tocadule||{};window.tocadule._q=window.tocadule._q||[];['track','identify','cart','requestPush'].forEach(function(m){window.tocadule[m]=window.tocadule[m]||function(){window.tocadule._q.push([m,arguments])}});</script>
<script async data-workspace="YOUR_WORKSPACE_ID" src="https://app.tocadule.com/snippet.js"></script>Install the stub — don't rely on the raw async snippet on its own. Without the stub,
window.tocadule doesn't exist until snippet.js finishes loading, so an inline
tocadule.track(...) that runs first either throws (if it's unguarded) or is silently
skipped by an if (window.tocadule) guard. The stub defines window.tocadule synchronously,
so once it's in your <head> every call — guarded or not — is queued into _q and replayed
in order. Tocadule's standard event scripts wrap the call in if (window.tocadule) { … } as a
belt-and-braces check; with the stub installed that guard is simply always true.
Naming rules
tocadule.track(name, props)accepts any string as the event name — there is no server-side allow-list on thetrackpath. The request only needs a workspace and an event name to be stored.- Convention: use
snake_case(lowercase letters, digits, underscores). It keeps your event catalog readable and matches the standard event names. Tocadule's no-code Event Rules builder enforcessnake_case; hand-writtentrack()calls are not forced to, but you should follow it anyway. - Event names are case-sensitive and matched exactly.
Size_Guideandsize_guidebecome two separate rows in your catalog — pick one spelling and stick to it.
What Tocadule does with a custom event
When a custom event arrives at /events/track, it goes through the same ingestion
pipeline as a standard event. Nothing is thrown away for being non-canonical — the
event_name is stored verbatim. Concretely, for every event:
| Step | What happens |
|---|---|
| Stored | A row is written to the events table with your exact event_name and the properties JSON you sent. |
| Contact resolution | If the visitor has an anonymous_id, we link the event to their contact. If the event carries an email (and analytics consent isn't denied), we auto-create/link the contact from it — no separate identify() needed. |
| Geo enrichment | Country / region / city / postal / timezone are resolved from the request IP (MaxMind, offline) and merged into properties (your own values win). Lat/lon only with analytics consent. |
| Auto-attached props | The snippet automatically adds UTM params, device / browser / OS, traffic source, page locale, and ad click IDs (gclid, gbraid, fbclid, …) to every event before it's sent. |
| Consent | Both consent lanes are recorded on the row: consent_state (analytics) and ad_consent (advertising). |
| Bot flag | The user agent is checked; bot traffic is stored with is_bot = true and excluded from analytics by default. |
| Dedup | Browser events dedup on anonymous_id; external/webhook events on source_event_id. You never set these yourself. |
Identity works on any event. You can put email, phone, first_name, last_name,
or external_id on a custom event's properties. If we don't know the visitor yet we
create and link the contact; if we do, we enrich it. This is what lifts match quality on
otherwise-anonymous browsing events.
Canonical mapping vs. passthrough
Tocadule keeps a fixed set of canonical commerce events and a table of known aliases
(ViewContent → product_view, orders/create → purchase, and so on). This mapping is
what powers the built-in funnel, the standard-event analytics, and the alias-tolerant
segment presets.
- A canonical event (or a known alias of one) is aggregated onto its canonical row and can flow into the standard funnel, gap-fill measurement, and default ad-destination forwarding.
- A custom event has no canonical key. It is passed through and stored as-is. It is fully queryable, but it is not folded into the canonical Data Collection funnel, it is not part of the commerce gap-fill counters, and it is not forwarded to Meta / TikTok / Google unless you explicitly add it to a destination's forward list.
In short: standard events get the built-in commerce machinery for free; custom events are stored, counted, and available for you to build on, but you opt them into forwarding and funnels yourself.
Where custom events show up
Once you fire a custom event, you can confirm it landed and inspect it in the dashboard:
- Events feed — the most recent events across your workspace, newest first, each with
its
event_name,properties, contact, and timestamp. Your custom event appears here within seconds of firing. - Event catalog — a per-event summary (over the last 30 days) with total fires, unique contacts, and last-fired time. Every distinct event name you've sent — standard or custom — has a row here. This is the fastest way to verify a new event is arriving and how often.
- Event drill-in — filter the feed by a single
event_nameto see the raw rows and the exact property keys/values each one carried. Use this to confirm yourpropertiespayload is shaped the way you expect. - Property inspector — for any event name, Tocadule lists the distinct property keys (and sample values) it has seen in the last 30 days, so you can see which fields your custom event is actually delivering.
Custom events do not appear on the canonical Data Collection page — that page only lists the standard commerce events and their sources. Use the Events feed / catalog to find and verify custom events.
Example: a fully-shaped custom event
Custom events use the same track() signature as the standard ones. A good pattern is to
guard placeholder tokens so an un-replaced template variable never sends junk (the same
sub() helper used in Tocadule's standard scripts):
<script>
if (window.tocadule) {
var sub = function(v){ return v && String(v).indexOf('{') < 0 ? v : undefined; };
tocadule.track('wishlist_added', {
product_id: sub('{productId}'),
name: sub('{productName}'),
price: parseFloat('{productPrice}') || 0,
currency: 'TRY',
// Optional — a logged-in shopper's email auto-links the contact.
email: sub('{email}')
});
}
</script>Firing events without code (Event Rules)
If you can't add script to a page, you can create custom events from Tocadule's no-code Event Rules. A rule watches for a URL match, an element click, a form submit, or a custom JS event, and fires an event name you choose with properties pulled from the page (static values, CSS selectors, form fields, URL params, or cookies).
Event Rules require the event name to be snake_case, and each rule has a Test action
that fires a synthetic event so you can confirm it lands in your Events feed before wiring
it to the real trigger condition.
Good to know
- No schema to declare. You don't register custom events ahead of time — the first time you fire one, its row appears in the catalog.
- Properties are free-form JSON. Send whatever scalar fields are useful. Product fields
(
name/title,price/amount,image/image_url,url/link,id/product_id) are recognised by common aliases so product cards render without extra mapping. - Keep PII off the cart.
tocadule.cart()is intentionally PII-free — never put an email on it. Put contact identifiers ontrack()events instead.