Event Definitions

An Event Definition turns one or more raw auto-kind events into a named conversion. It's the analytics-side counterpart to a Tag Manager Tag of type event — except where Tag Manager fires client-side, an Event Definition is server-side and runs every time processBatch() ingests a row.

The goal: keep auto-collection broad and noisy ("we capture every click"), then have a small layer of human-named conversions on top ("add_to_cart is a click on #buy-now"). Conversions stay queryable by name in funnels, cohorts, and dashboards without anyone going back to the customer site.

The Shape of a Definition

{
  _id: ObjectId(...),
  siteId: ObjectId(...),
  name: 'add_to_cart',
  description: 'User clicked the primary CTA on a product page',
  status: 'active',                       // 'active' | 'paused' | 'archived'
  matchRules: [
    { field: 'event_name',          op: 'equals',       value: 'click' },
    { field: 'properties.selector_path', op: 'matches',      value: '#buy-now' },
    { field: 'page_path',           op: 'starts_with',  value: '/product/' },
  ],
  matchMode: 'all',                       // 'all' (AND) | 'any' (OR)
  derivedEventName: 'add_to_cart',        // the synthetic name written to BQ
  derivedEventKind: 'custom',             // 'custom' (default) | 'tag'
  propertyExtractors: [
    { name: 'product_id', from: 'properties.id' },
  ],
  lastMatchedAt: Date(...),
  totalMatches: 1247,
  createdBy: ObjectId(...),
  createdAt: Date(...),
}

Match Rules

op Semantics
equals Exact (===) comparison.
not_equals Exact inequality.
contains String includes (case-sensitive).
starts_with String prefix.
ends_with String suffix.
matches Treated as CSS selector for selector_path fields; treated as regex for all other fields.
gt / gte / lt / lte Numeric comparison (auto-coerces).
exists Truthy presence.
not_exists Falsy / missing.

field paths support dot notation into the event object, including properties.*, utm.*, and consent.*.

How Matching Runs

Inside processBatch():

  1. After validation and site guards, the rows are passed to eventDefinitionMatcher.deriveBatch(rows, definitions).
  2. For each row, every active definition is tested. The check uses the matchMode (all = AND across rules, any = OR).
  3. Every match produces a derived row — a clone of the source row with event_name replaced by derivedEventName, event_kind set to derivedEventKind, and any propertyExtractors materialized into properties.*.
  4. Both the source row and the derived row are written to BigQuery — the source so the raw auto stream stays complete, the derived so it shows up as a named event everywhere.
  5. recordMatchStats() increments totalMatches and updates lastMatchedAt (async, doesn't block the response).

A single source event can produce multiple derived rows if it matches multiple definitions. There is no precedence — they all fire.

CRUD via UI

Open Web Analytics > Event Definitions. The page lists every definition for the site with its totalMatches last-30-days, lastMatchedAt, and a sparkline showing daily match volume.

AI Suggestions Inbox

The hourly AI Suggestions job is the discovery surface for new definitions you should create.

How it works:

  1. Once per hour, analyticsAiSuggestionsJob.js (Cloud Run Job, triggered via /api/analytics/ai-suggest-tick) runs for every active site.

  2. For each site it samples up to 2,000 event_kind = 'auto' rows from the last hour, stratified by (selector_path, page_path, event_name) so it sees the diversity of activity instead of just the dominant cluster.

  3. The sample plus a compact summary (top 20 clusters, total volume, distinct visitors) goes to Gemini with the tool declaration:

    propose_named_event({
      name: string,                  // snake_case
      description: string,           // 1-2 sentence rationale
      matchRules: MatchRule[],
      matchMode: 'all' | 'any',
      suggestedFunnelStep: string | null,  // hint for adding to a funnel
      confidence: number,            // 0-1
    })
    
  4. Every proposal is upserted into aiTagSuggestionModel keyed on a hash of (name, matchRules) so re-proposals don't create dupes.

  5. The Suggestions Inbox UI shows proposals ranked by confidence × estimatedMatchesPerDay.

  6. Accept creates the EventDefinition immediately. Dismiss marks the suggestion status: 'dismissed' so it won't be re-proposed.

  7. Dismiss + tell us why captures freeform feedback that's included in the next prompt so the model can avoid that class of suggestion.

The job is cron-triggered via the ticks system using the x-tick-secret header — see orchestrations docs for the cron infrastructure. AI cost is logged in llmUsageModel per site so you can attribute spend.

When Derived Rows Land in BigQuery

Derived rows have:

In the events table these look like any other event. Funnel and cohort queries treat them as first-class.

Common Patterns

What Definitions Don't Do