Tracking reference
One tag tracks everything automatic. Four calls cover everything custom: events, conversions with revenue, errors, guardrails. All of it lands in one database your AI agent queries over MCP.
Agent-readable version: /docs/events.md
Install
One tag, in <head>, on every page:
<script src="https://askbowtie.com/bowtie.js" async></script>
async, not defer: the tracker catches errors that happen
while the page is still parsing. Never version the URL. The domain must be registered
at askbowtie.com, otherwise the tracker disables itself.
Tracked automatically
| Signal | Event types |
|---|---|
| Page views, sessions | page_load session_start page_hidden |
| Clicks | click external_link_click rage_click |
| Forms | form_submit |
| Errors | console_error resource_error network_error csp_violation |
| Performance | web_vitals (LCP, CLS, INP, TTFB) |
UTM parameters and ad click IDs (gclid, msclkid, fbclid) are captured on landing and survive internal navigation. No cookies. Input values are never read; PII in query strings is redacted before sending.
Public API
The API lives at window.bowtie once the script loads. The script is
async, so guard calls that might run before load: window.bowtie?.track(...).
Inside user-action handlers (clicks, submits) the tracker is always ready.
Custom events
bowtie.track('signup_started', { source: 'homepage_cta' });
bowtie.track('plan_selected', { plan: 'agency', seats: 5 });
First argument: any string you choose. Second: a flat object of context, stored with the event and queryable later.
Conversions and revenue
bowtie.converted('purchase', { value: 99.00, currency: 'USD', transaction_id: 'ord_1842' });
bowtie.converted('lead_captured', { source: 'contact_form' });
bowtie.converted('trial_started');
| Field | Type | Notes |
|---|---|---|
value | number | Revenue or goal value. Optional. |
currency | string | ISO code, default USD |
transaction_id | string | Dedupe key: the same conversion never counts twice |
pixels | boolean / string | Also fire to connected ad platforms. true = primary Google Ads label, 'secondary' = secondary label. Default off. |
email, phone | string | Enhanced conversions, hashed by gtag before leaving the page. Only used when pixels fires. |
Any other keys you pass are stored with the event.
Visitor identity (cross-session funnels)
bowtie.identify('u_8f3a91'); // after signup/login — your user id or a hashed email
bowtie.identify(null); // on logout — forget the visitor
By default askbowtie is cookieless: sessions don't connect across visits, so a funnel
that completes days later (signup today, purchase from your reminder email next week)
reads as incomplete. identify() fixes that with your
identifier — it persists in localStorage, rides every event, and visitor-scoped funnels
stitch the return visit to the original one.
Zero-code alternative: add ?ab_uid=u_8f3a91 to links in your return
emails — the tracker picks it up on landing, same effect.
identify() makes cross-session tracking happen under your user
relationship. Sites that never call it keep the cookieless, no-banner default. Prefer
an internal id or a hashed email over raw PII.Application errors
bowtie.error('payment_declined', { reason: 'insufficient_funds', amount: 99.00 });
bowtie.error('validation_failed', { field: 'email' });
For errors your own code catches. Uncaught JS errors, failed resources, and failed requests are already captured automatically.
Guardrails
bowtie.guardrail('rate_limited', { endpoint: '/api/checkout', limit: '10/min' });
bowtie.guardrail('geo_blocked', { country: 'XX' });
Intentional blocks that are not failures: rate limits, quota, access denied, feature flags. Kept separate from errors so your error rate stays honest.
Utilities
bowtie.getSessionId() // current session id, for support and debugging
bowtie.flush() // send the queue now, before a hard navigation
bowtie.debug(true) // tag this tab's events debug:true, persists across pages
window.itbroke is an alias of
window.bowtie. Existing inline calls keep working unchanged.Funnels (flows)
Define the path a visitor should take once — pages and events, in order — and every
funnel question becomes one call. Define it here in the app (/app/flows)
or let your agent do it conversationally (define_flow over MCP).
define_flow('quiz funnel', steps: [
{ kind: 'page', match: '/quiz' },
{ kind: 'event', match: 'quiz_step_2' },
{ kind: 'event', match: 'signup' }
])
Evaluation is order-aware (step 2 only counts after step 1), shows fall-off and median
time per step, incidents on each step's page, a baseline window, and flags steps that are
too new to compare. Periods include @last-deploy — "did the thing I just
shipped help?"
Variants and splits
// One event name, many variants — filter by payload field:
{ kind: 'event', match: 'popup_shown', where: [{ field: 'popup_id', op: 'eq', value: 'exit-intent' }] }
// Same funnel, paid vs organic (first-touch, mutually exclusive):
get_flow('quiz funnel', segment: [{ field: 'tracking.utm_medium', op: 'eq', value: 'cpc' }])
get_flow('quiz funnel', segment: [{ field: 'tracking.utm_medium', op: 'not_exists' }])
Ops: eq, neq, in, contains, exists, not_exists, gt, gte, lt, lte. A step's
match also takes an array for any-of matching.
Return visits
Funnels are session-scoped by default. If your visitors complete later (purchase from a
reminder email days after signing up), pass scope: 'visitor' — it stitches
sessions through visitor identity.
Querying the data
Everything above is queryable by an AI agent through MCP:
claude mcp add --transport http askbowtie https://askbowtie.com/mcp \
--header "Authorization: Bearer YOUR_TOKEN"
Tools: list_domains, get_summary, get_traffic,
get_conversions, get_incidents, get_alerts,
get_performance, get_page. Custom events, revenue, errors, and
guardrails come back with their data fields intact.