Documentation
¶
Overview ¶
Package query is the segmentation backbone: filter events by their properties and break them down (group by) a property. Every report (funnel, retention, trends) gets filtering + breakdown by composing these over the event slice before the deterministic compute — the thing that makes analytics powerful.
Index ¶
- Variables
- func Apply(events []event.Event, filters []Filter) []event.Event
- func ApplyMode(evs []event.Event, filters []Filter, anyMode bool) []event.Event
- func FirstUnknownProp(events []event.Event, filters []Filter) (string, []string)
- func Keeper(filters []Filter) func(event.Event) bool
- func Matches(e event.Event, filters []Filter) bool
- func ScopeUsers(events []event.Event, filters []Filter, anyMode bool) []event.Event
- func StampFirstTouch(events []event.Event, prop string) []event.Event
- func StampFirstTouchAll(events []event.Event, props []string) []event.Event
- func StampForFilters(evs []event.Event, filters []Filter) []event.Event
- func Validate(filters []Filter) error
- type Filter
- type FirstTouch
- type Group
- type Op
Constants ¶
This section is empty.
Variables ¶
var NonProduction = map[string]bool{ "development": true, "preview": true, "staging": true, "test": true, "ci": true, }
NonProduction lists the env values hidden from every default-scoped query. The browser SDK stamps localhost and dev tunnels as "development", and Netlify deploy previews plus staging/preview/dev subdomains as "preview"; anyone can set their own via init({env}).
An explicit set, NOT `env != "production"`. The two failure modes are not symmetric: showing a bit of preview traffic is noise the viewer can filter away, while hiding real traffic is invisible from the dashboard and reads as "your product recorded nothing today". So an unrecognised value stays VISIBLE — someone who writes env:"prod" or env:"live" must never have their production numbers silently disappear because it did not match a magic string.
Functions ¶
func ApplyMode ¶ added in v0.9.1
ApplyMode is Apply with an any/all switch: all = every filter must match (the default AND), any = at least one must (the OR mode of the dashboard's filter builder). Dev-traffic exclusion applies in both modes, before the user filters.
func FirstUnknownProp ¶ added in v0.9.1
FirstUnknownProp returns the first filter property that uses a POSITIVE operator (eq/contains/gt/lt/in/set/regex — ones that can only match when the property exists) yet appears on NO event, plus a sorted list of the properties that DO exist. This is how a filtered report tells a typo ("plann=pro" → no such property) apart from a genuine empty result, instead of silently returning 0 as if it were the honest answer. Negative operators (neq/notin/notset/notcontains) are skipped: a missing property legitimately satisfies them. Returns ("", nil) when every positive filter is known.
func Keeper ¶ added in v0.21.0
Keeper returns the per-event predicate Apply uses, so a caller streaming through Store.Scan can filter as events arrive instead of materializing everything first and filtering after. The dashboard used to hold two full copies of history — one raw, one filtered — which is most of why memory tracked total events rather than the query.
This exists so there is exactly ONE definition of "does this event belong in a default-scoped query". Apply is written in terms of it. A second hand-rolled copy of the env rule in a streaming caller would drift the moment either changed, and the symptom would be two screens quietly disagreeing about the same number.
func Matches ¶ added in v0.9.5
Apply returns the events matching ALL filters — and enforces the one default scope of the whole query layer: events stamped env=development are EXCLUDED unless the filters explicitly reference "env". Localhost traffic polluting production funnels is the classic silent report-corruptor; asking for dev data stays one filter away (env eq development). Living inside Apply means every surface (HTTP API, MCP, dashboard) inherits the same rule — the agreement test depends on that. Matches reports whether a single event satisfies ALL of the filters (AND). Unlike Apply it does NOT apply the default dev-env exclusion — that's a stream-level concern; callers that want it pre-filter the stream with Apply first. Used for per-step conditions in sequenced cohorts, where each step constrains one event by name + its own property filters.
func ScopeUsers ¶ added in v0.9.1
ScopeUsers keeps every event of any user who has at least one event matching the filters — user-level scoping (vs Apply's event-level). Funnels use this so a filter on a user attribute (plan, device) that isn't present on every step event scopes the POPULATION, not the events, and later steps aren't dropped. Empty filters = unchanged.
func StampFirstTouch ¶ added in v0.9.1
func StampFirstTouchAll ¶ added in v0.21.0
StampFirstTouchAll is StampFirstTouch for several properties in ONE pass.
Callers used to chain it: `for _, p := range eightProps { evs = StampFirstTouch(evs, p) }`. Because stamping copies every event's property map, that is eight full copies of history and eight fresh maps per event — 1.6M map allocations for 200k events, and measured as 47% of everything the dashboard allocated. Stamping N properties at once allocates one map per event instead of N, because each pass only ever ADDED a key.
Batching is safe precisely because the properties are distinct: stamping "device" never changes any event's "country" value, so the first-touch lookup for a later property reads the same input either way and the result is identical. The parity test in query_test.go pins that equivalence against the chained implementation rather than trusting the argument.
func StampForFilters ¶ added in v0.9.3
StampForFilters first-touch-stamps every acquisition/user-attribute property a filter targets, so "signups where device=mobile" (or referrer/utm/country/…) means "signups by users acquired on mobile" — the same number the dashboard and ask bar report — instead of a silent 0 because the signup event never carried the property. Product/event props (plan, amount) are left untouched; they're set at the step itself, so an event-level filter is right. Both GET /v1 and the MCP tools call this before applying filters, so all four surfaces agree.
Types ¶
type FirstTouch ¶ added in v0.21.0
type FirstTouch struct {
// contains filtered or unexported fields
}
FirstTouch holds each user's earliest value for a set of properties. Building it is a scan that allocates nothing per event; applying it is what costs, because stamping has to copy a property map.
Separating the two matters because the population you must LOOK AT to find a user's first touch (all of history — the country is on the landing pageview) is much larger than the population you need to STAMP (in segmentBlame, only the two event names in the funnel step being blamed). Fused together, every caller paid to copy history.
func BuildFirstTouch ¶ added in v0.21.0
func BuildFirstTouch(events []event.Event, props []string) *FirstTouch
BuildFirstTouch scans every event once and records, per property, each user's value from their earliest event carrying it. Nothing is copied.
func (*FirstTouch) Stamp ¶ added in v0.21.0
func (f *FirstTouch) Stamp(events []event.Event) []event.Event
Stamp returns a copy of events carrying each user's first-touch values. The input is never mutated: callers re-read the original events to decide what a conversion event natively carries, and stamped values leaking back into that check would silently skip properties that must be stamped.
type Group ¶
type Group struct {
Value string `json:"value"`
Events []event.Event `json:"-"`
Count int `json:"count"`
}
Group is one breakdown bucket: a property value and its events.
type Op ¶
type Op string
Op is a filter comparison.
const ( Eq Op = "eq" Neq Op = "neq" Contains Op = "contains" Gt Op = "gt" Lt Op = "lt" In Op = "in" // value is one of a list — expresses OR over one property (source in [hn, twitter]) NotIn Op = "notin" // value is none of a list (or the property is missing) Set Op = "set" // the property exists on the event (value ignored) NotSet Op = "notset" // the property is missing (value ignored) Regex Op = "regex" // value is a Go regexp matched against the stringified property NotContains Op = "ncontains" )