Skip to content

Query Language

Every analytics endpoint takes an optional clauses array. A clause names a field by its catalog id, an operator, and a value. Clauses combine with and unless you say otherwise.

{
"period": "last_28",
"clauses": [
{ "id": "channel", "op": "equals", "value": ["paid_search", "paid_social"] },
{ "id": "entry_path", "op": "starts_with", "value": "/blog/" },
{ "id": "time_on_site", "op": "gte", "value": 60 },
{ "id": "device", "op": "equals", "value": "mobile", "where": "and", "not": true }
]
}

Read: paid sessions that landed on the blog, stayed at least a minute, and were not on mobile.

KeyRequiredTypeMeaning
idyesstringA field id from the catalog. Unknown ids are a 422 naming the offending clause
opyesstringAn operator valid for the field’s type (below)
valueyesscalar, array or nullWhat to compare against. Arrays mean “any of”. null with op: "exists" tests presence
wherenoand | orHow this clause joins the one before it. Default and
notnoboolNegate the clause. Default false

Validation errors are specific: clauses[1] missing required op, clauses[2].id foo is not a valid filter field.

Each field has a type; the type decides the operators.

opMatches whenValue
equalsThe stored value is exactly one of the given valuesstring or array of strings
containsThe stored value contains the substringstring, or array (any of)
starts_withThe stored value begins with the prefix, case-insensitivestring
regexThe stored value matches the pattern (Lucene regular expression syntax, whole-value match)string
existsThe field is present on the sessionnull
opMatches whenValue
equalsexact valuenumber
gt, gte, lt, ltecomparisonnumber
betweeninclusive range[min, max]
opValue
istrue or false

short_link_id, short_link_group_id, cost_entry_id and snip_form_id hold record ids. Only equals makes sense for them; pass one id or an array of ids taken from the short links API or the dashboard.

Two things are true at once, and together they keep an or from doing damage:

  1. Tenancy and time are outside the clauses. The engine compiles (property AND real-users AND window) AND (clause1 OP clause2 OP clause3). An or clause can widen the match to other sessions of this property in this window, never beyond.
  2. where joins a clause to the clause before it, left to right, with no precedence grouping. A, or B, C reads as A or B, and C in the order the search engine resolves it: two or clauses next to each other form an alternative set, a following and clause applies to the whole query.

Use not: true rather than trying to express negation through operators; it works with every operator, including exists (which becomes “does not exist”).

{ "id": "country_code", "op": "equals", "value": ["ZA", "NA", "BW"] }

One clause with an array is an IN. Prefer this over several or clauses on the same field.

Sessions carry two lists of objects: tags ({key, value} pairs from the URL) and event_tags ({name, value, value_float} for every event fired). Their catalog ids are tags_key, tags_value, event_name, event_value, event_value_num.

Clauses on the same parent are combined inside one object. That is what makes “a tag with key fbclid AND value abc” mean the same tag rather than any tag with that key plus any tag with that value:

[
{ "id": "event_name", "op": "equals", "value": "purchase" },
{ "id": "event_value_num", "op": "gte", "value": 5000 }
]

This matches sessions with a purchase event worth 5000 or more, not sessions with a purchase and some other event over 5000.

Two consequences worth knowing:

  • Nested clauses always and onto the rest of the query. where and not act between clauses of the same parent (tags with tags, events with events).
  • { "id": "event_name", "op": "exists", "value": null } means “fired at least one event”.

event_value is the event value as text; event_value_num is the same value parsed as a number, for comparisons. An event fired as signals('purchase', 4999) is found by either.

page_url, page_path and page_count query the per-page view list (page_tags) as flat fields. page_path: /pricing matches any session that viewed /pricing at any point, not only as entry or exit. They are not nested, so a page_path clause and a page_count clause do not have to refer to the same page.

The dashboard’s filter bar writes the same clauses as one-line expressions, handy for reading a filter off a shared URL and turning it into JSON. The REST endpoints do not accept this form; send the JSON.

ShorthandJSON clause
entry_path:/home{ "id": "entry_path", "op": "equals", "value": "/home" }
entry_path:"/a,b"quotes protect commas: value: "/a,b"
device:mobile,tablet{ "id": "device", "op": "equals", "value": ["mobile", "tablet"] }
entry_path:/blog/*{ "id": "entry_path", "op": "starts_with", "value": "/blog/" }
entry_title:*welcome*{ "id": "entry_title", "op": "contains", "value": "welcome" }
entry_path:/^\/admin\//{ "id": "entry_path", "op": "regex", "value": "^\\/admin\\/" }
time_on_site:>60{ "id": "time_on_site", "op": "gt", "value": 60 }
views:>=3{ "id": "views", "op": "gte", "value": 3 }
views:[3 TO 10]{ "id": "views", "op": "between", "value": [3, 10] }
bounced{ "id": "bounced", "op": "exists", "value": null }
not_bounced{ "id": "bounced", "op": "exists", "value": null, "not": true }
or_device:tablet{ "id": "device", "op": "equals", "value": "tablet", "where": "or" }
not_device:mobile{ ..., "not": true }
or_not_device:mobile{ ..., "where": "or", "not": true }
tags_key:fbclid{ "id": "tags_key", "op": "equals", "value": "fbclid" }
event_value_num:>10{ "id": "event_value_num", "op": "gt", "value": 10 }

Every analytics response echoes the applied clauses under meta.filters, resolved to their full form (id, field, subfield, type, label, op, value, where, not). That is the shape to store if you want to re-run the same query later, and it is also what the dashboard renders as filter chips.