Session replay and search analytics
Orbit ships two capabilities whose names are easy to confuse, and this page keeps them separate: session replay, a piece of the inbound message routing model that decides where a matched inbound message goes — and search analytics, a workspace-analytics surface that records what your team types into the search box and reads it back as four aggregate panels. Read section 1–2 for the ownership split and for replay itself, then the DSL reference, the worked zero-result remediation, and the scope rules so you can run either capability end to end.Two families — which page owns which decision
sessionReplay and searchAnalytics families live on different sides of the platform, and the decision each family owns is different:
Because the two reads sit on different surfaces, you never “configure” a routing rule and a search analytics panel in the same place — a replay-target rule is an inbound routing concern, a zero-result investigation is a Search Analytics concern. The membership boundary is unambiguous as long as you keep that split:
sessionReplay owns routing-target decisions, searchAnalytics owns what your team’s search behaviour reveals.
Inbound rules with replay targets (sessionReplay)
Session replay sits in the same rule engine as inbound message routing. A rule evaluates the inbound message against one field (number, sender, keyword, or regex), in ascending priority order, and the first enabled match dispatches to one target. A session-replay target hands the message to the recorder rather than to a raw webhook endpoint or the default inbox handler — useful when the inbound reply chain (a WhatsApp bot, a support-thread opener) wants the pre-chat session context that the Orbit web widget captures. Priority is the ordering tool: a lower priority value wins; leave gaps (10, 20, 30) so you can insert exceptions without renumbering. Fall-through is deliberate: when no rule matches, the message takes the default path (inbox persistence plus the tenant-wide received event). Replay targets never originate an outbound reply — no match type sends a message on your behalf; the replay-handling target only receives a message that already arrived.
What a search records (searchAnalytics)
Search analytics is the workspace-analytics counterpart built on a per-tenant table that every ?q= list controller (messages, contacts, calls, campaigns, plus the inbox live query and the global command-palette search) writes to immediately after the search parses and the result count resolves. The write captures the normalized query text (whitespace-collapsed, lowercased, personal data such as phone numbers and emails masked), the surface it ran against, whether the query parsed successfully, and the result count (zero for a zero-result run). A click on a result updates the row with the clicked entity id, and a click recorded by its exact query text (used when a static command-palette row is opened before the search response returns an analytics-row id) inserts or stamps a row within a short attribution window.
Per-destination granularity is deliberate. The top-queries, zero-result, click-through, and parse-failures panels project the distinct surfaces a query ran against as an array — the dashboard groups runs of the same normalized query text across surfaces while still telling you which destination produced them. A surface filter narrows any panel to one destination; all (the default) aggregates across them.
DSL reference — which surface accepts which grammar
The four list endpoints that take a fielded?q= DSL each ship their own field allowlist; a field name outside the allowlist for that surface is rejected with INVALID_QUERY_FIELD before any data is probed, and enum fields validate their value up front. The grammar is shared; the searchable fields are per surface. Beyond the field list per surface, the operators each field accepts also differ — use the Operators column here before you guess:
Field names are snake_case so dashboard URLs and saved filters read naturally, and the parser combines clauses with AND/OR/NOT, parenthesized grouping, comparison operators (
:value, >value, <value), null checks (field:null / field:!null), ranges (created:[2026-05-01 TO 2026-06-01]), and * wildcards on string fields. See search message history for the full operator-reference tables; the field lists above come from the per-surface allowlists on the shared search grammar. When you are not sure whether > or IN holds on a field, check the Operators fragment above first — guessing body>foo fails with INVALID_QUERY_OP (an operator rejection, not a field rejection), which the zero-result playbook below distinguishes from INVALID_QUERY_FIELD.
Zero-result playbook — telling “no data” from “no parse”, with a worked remediation
The zero-result panel is the most actionable read, and the remediation depends on which of two mechanisms produced it. Before touching either, open the parse-failures panel on the same window and search for the same phrase there. A worked pass looks like this. Say the zero-result panel showsrma return label at 42 runs over 30 days, all against the messages surface, with a low click-through. The parse-failures panel shows the same phrase, with a representative sample of rma:RMA-1234 and an error of INVALID_QUERY_FIELD — the messages surface rejects rma because it is not a searchable field on that surface. Two fixes are possible, and a third is not:
- Teach the team the accepted fields on the messages surface (
status,channel,direction,from,to,body,message_id,created,updated,conversation_id,contact_id) and ask them to usebody:—body:"rma"— when they want the return-flow phrasing. - Rename the convention. The team’s support macro for “returns” lives in a tag on the contact rather than in message text: run it on the contacts surface as
tag:returns(which accepts:,NOT, andIN— no wildcard). Same phrase, two different surfaces, two different operator allowlists. - What not to do:
rma>100(a comparison operator on a non-numeric alias that does not exist) orrma:RMA-1*(a wildcard suffix on a rejected field). Both still fail on the parse step.
rma return label example end to end; the short version above is the decision tree to run in the field.
PII in panels. The parse-failures panel’s representative sample and every aggregated query_normalized value pass through the same normalization used at write time — whitespace collapsed, lowercased, and phone/email values format-preserving masked (phone:+14155551234 aggregates as phone:+**********1234). The raw query_text copy is stored for debugging but never returned by per-tenant aggregation endpoints, so panels are safe to show across your team.
Impersonation exclusion and deduplication
Two write-side guards keep the aggregates faithful:- Support impersonation. Searches a Devotel support engineer runs while impersonating your workspace are tagged at write time and excluded from every per-tenant aggregation. Your panels reflect your team’s real behaviour — but if support assisted you and demonstrably ran searches, those runs will not appear. The exclusion is deliberate: support-generated zero-result rows would otherwise read as your own team’s unsatisfied demand.
- Rapid-repeat dedupe. The same normalized query by the same user on the same surface inside the same UTC minute writes one row, not ten — keystroke spam from debounced clients collapses. Incremental typing fragments are also merged: when you pause mid-word (“co” before finishing “contact”), the shorter prefix row is removed in favour of the completed query, provided the fragment had not already produced a click. A deliberate re-run of the same query minutes later gets its own row.
Roles and scope
Owner, admin, developer, and viewer roles can all read the four aggregations over the dashboard and the Search Analytics API — the read surface inherits the same posture as the console per role. Every recording and click-attribution write stays in the tenant schema that the request resolved; the org id is stamped server-side from the request context and never taken from the client, so a cross-workspace read or write is structurally impossible. See tenant isolation for the schema-per-tenant model the table lives in.See also
- Inbound message routing — the rule engine
sessionReplaytargets plug into - How routing picks a sender — the outbound half of the same routing decision
- Search Analytics API — full endpoint shapes for the four read aggregations and click attribution
- Read Search Analytics — operator walkthrough of the dashboard panels and the worked zero-result example
- Search message history — the fielded DSL every per-surface search uses
- Analytics pipeline — the read-replica aggregation pattern the wider Insights surface shares
- Tenant isolation — the schema-per-tenant model recording scope sits on