Skip to content

Querying Signals

$snipform->signals() is a query builder. Chain a period and any number of clauses, then call a terminal: sessions(), metrics(), graph() or live().

use SnipForm\Query\SessionField;
$rows = $snipform->signals()
->last28Days()
->where(SessionField::COUNTRY, 'US')
->whereStartsWith(SessionField::ENTRY_PATH, '/blog')
->whereGte(SessionField::TIME_ON_SITE, 60)
->sessions();
foreach ($rows as $session) {
echo "{$session->entryPath} from {$session->source}\n";
}

Each call to signals() starts a fresh chain. The builder posts the same JSON the Signals Analytics API accepts, so everything documented for the API applies here.

The default period is last_7. Use the typed shorthands, or period() with a Period case or string.

->today()
->yesterday()
->last7Days()
->last28Days()
->monthToDate()
->yearToDate()
->last12Months()
// Explicit Y-m-d range (sets the period to custom)
->between('2026-01-01', '2026-01-31')
->customPeriod('2026-01-01', '2026-01-31')
->customPeriod()->fromDate('2026-01-01')->toDate('2026-01-31')
// By enum or string
use SnipForm\Query\Period;
->period(Period::LAST_28)
->period('last_28')

Unknown period strings throw InvalidPeriodException before any request is made. The valid set is today, yesterday, last_7, last_28, month_to_date, year_to_date, last_12_months and custom.

The first argument to every where*() method is a field id from the Field Catalog. Pass it as a SessionField enum case or as a plain string.

MethodOperatorNotes
where($field, $value)equalsAn array value means “any of”
orWhere($field, $value)equalsJoined with OR
whereNot($field, $value)equals, negated
orWhereNot($field, $value)equals, negated, OR
whereStartsWith($field, $prefix)starts_withKeyword fields
whereContains($field, $needle)containsKeyword fields
whereRegex($field, $pattern)regexKeyword fields
whereGt / whereGte / whereLt / whereLte($field, $n)gt / gte / lt / lteNumeric fields
whereBetween($field, $min, $max)betweenNumeric fields, inclusive
whereExists($field)existsField has a value
whereNotExists($field)exists, negatedField is absent
$snipform->signals()
->last7Days()
->where('device', ['mobile', 'tablet']) // IN
->whereNot('channel', 'direct')
->whereExists('utm_campaign')
->whereBetween('views', 3, 10)
->sessions();

With a SessionField case the SDK knows the field’s type and rejects an impossible operator before the request leaves:

$snipform->signals()->whereBetween(SessionField::COUNTRY, 0, 10);
// IncompatibleFieldOperator: Operator `between` is not valid for field
// `country` (type: keyword). Valid ops: equals, contains, starts_with, regex, exists.

Plain strings skip that check (the server still rejects an unknown field with a 403). Strings are the escape hatch for fields the server adds before the enum catches up.

The enum covers every id in the catalog: entry and exit page, referrer, pages, tags, geo, browser, device, OS, bot, channel and UTM attribution, source, acquisition money, short links, forms, events and the session metrics (BOUNCED, ENTRY_TS, LAST_TS, TIME_ON_SITE, AVG_MAX_SCROLL, VIEWS, SCREEN_WIDTH, SCREEN_HEIGHT, LAST_PAGE).

Clauses are sent as { id, op, value, where, not } and combined inside one group: property AND period AND (clause1 OP clause2 OP clause3). An orWhere can widen the match within your clauses but never escape the property or the period. The full semantics, including nested fields like tags_key and event_name, are in Query Language.

sessions(int $perPage = 50) returns a PaginatedCollection of SessionRow objects. Iterating walks every page; first(), count(), all() and page($n) are covered under Pagination.

$rows = $snipform->signals()->where('device', 'mobile')->sessions();
foreach ($rows as $session) { ... }
$rows->first();
$rows->count();
$rows->page(2); // a Page with meta + next()/prev()

SessionRow fields:

PropertyTypeFrom
idstringSession id
entryTs, lastTsint, ?intUnix seconds
country, countryCode, city?stringGeo
device, os, browser?stringDevice class, platform, browser name
entryPath, exitPath?string
referrerDomain, source, channel?stringchannel is the channel category
utmSource, utmMedium, utmCampaign, utmContent, utmTerm?string
viewsintPage views
timeOnSiteintSeconds
bouncedbool
tagsarraySession tags as {key, value} rows

Use ->asRaw()->sessions() to receive the full session document as an array.

metrics(bool $withDevices = false) returns a MetricsResult with the current-period headline numbers.

$m = $snipform->signals()->last28Days()->where('utm_content', 'pub_12345')->metrics();
$m->sessions; // int
$m->views; // int
$m->viewsPerSession; // float
$m->bounceRate; // float, 0-100
$m->duration; // int, seconds
$m->avgScroll; // float, 0-100
$m->showing; // human-readable span, e.g. "Jul 25 - Aug 21, 2026"
$m->tookMs; // server query time

The API also returns the previous period, the difference and the trend for every metric. The typed object keeps only the current values; switch to raw mode for the rest:

$raw = $snipform->signals()->last28Days()->asRaw()->metrics();
$raw['analytics']['period_metrics']['summary']['sessions']['previous'];
$raw['analytics']['period_metrics']['summary']['sessions']['percent'];
$raw['meta']['showing'];

graph(string $metric = 'sessions', ?string $interval = null, bool $compare = false) returns a GraphResult: one series for the chain’s period, bucketed by interval.

$g = $snipform->signals()->last28Days()->where('country', 'US')->graph('sessions');
$g = $snipform->signals()->last28Days()->graph('bounce', 'day', compare: true);
$g->metric; // 'bounce'
$g->metricLabel; // 'Bounce rate'
$g->series; // GraphPoint[] - one per bucket: ->label, ->value
$g->previousSeries; // GraphPoint[] - the preceding period, only when compare is true
$g->projectedSeries; // GraphPoint[] - projection for the still-running bucket
$g->showing; // human-readable span
$g->tookMs;
MetricMeaning
sessionsSessions
viewsPage views
views_sessionViews per session
bounceBounce rate
durationTime on site, minutes
scrollAverage scroll depth

Intervals are hour, day, week and month. Leave the interval null and the server picks one that fits the period.

live(string $metric = 'sessions') is the same shape for the last five minutes. The chain’s period is ignored; its clauses still apply.

$now = $snipform->signals()->where('device', 'mobile')->live(); // sessions
$now = $snipform->signals()->live('views');
$now->showing; // 'Live - last 5 minutes'

A builder is JsonSerializable and serialises as if sessions() had been called, so return $snipform->signals()->last7Days(); from a controller yields page 1 of sessions. End the chain with an explicit terminal when you mean something else.

For debugging, buildPayload() shows exactly what a chain will post:

$snipform->signals()->last28Days()->where('country', 'US')->buildPayload();
// ['period' => 'last_28', 'clauses' => [['id' => 'country', 'op' => 'equals', 'value' => 'US']]]

where and not are omitted from a clause when they hold their defaults (and, false). The endpoints are POST /v2/property/signals/sessions, /analytics/metrics, /analytics/graph and /analytics/live; see Signals Analytics.