diff --git a/api/boards/create-chart.mdx b/api/boards/create-chart.mdx index fa2a326..9ef1297 100644 --- a/api/boards/create-chart.mdx +++ b/api/boards/create-chart.mdx @@ -143,21 +143,27 @@ Each step in the `steps` array has the following shape: | `op` | SQL equivalent | Notes | | ------------ | ---------------------- | ----------------------------------------------------- | -| `equals` | `= 'value'` | Exact match | -| `notEquals` | `!= 'value'` | Inverse match | +| `eq` | `= 'value'` | Exact match | +| `neq` | `!= 'value'` | Inverse match | | `in` | `IN (...)` | Pipe-delimited value: `"metamask\|rainbow\|coinbase"` | -| `notIn` | `NOT IN (...)` | Pipe-delimited value | +| `nin` | `NOT IN (...)` | Pipe-delimited value | | `gt` | `> value` | Numeric comparison | | `gte` | `>= value` | Numeric comparison | | `lt` | `< value` | Numeric comparison | | `lte` | `<= value` | Numeric comparison | | `startsWith` | `startsWith(col, 'v')` | String prefix | | `endsWith` | `endsWith(col, 'v')` | String suffix | -| `includes` | `like '%v%'` | Substring match | +| `contains` | `like '%v%'` | Substring match | +| `notEmpty` | `col != ''` | Value-less existence check; `value` is ignored | +| `isEmpty` | `col = ''` | Value-less existence check; `value` is ignored | - For `in` and `notIn`, join multiple values with a pipe `|`. Escape a literal - pipe with `\|` and a literal backslash with `\\`. + For `in` and `nin`, join multiple values with a pipe `|`. Escape a literal + pipe with `\|` and a literal backslash with `\\`. The legacy numeric long + forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are deprecated + aliases for `gt` / `gte` / `lt` / `lte` and remain accepted. `notEmpty` and + `isEmpty` are rejected on the numeric event columns `volume` / `revenue` / + `points`. ### `settings` for Funnel Charts @@ -221,8 +227,8 @@ Filter step 2 to MetaMask wallets on Ethereum mainnet: { "type": "event", "event": "connect", - "rdns": { "op": "equals", "value": "io.metamask" }, - "chain_id": { "op": "equals", "value": "1" } + "rdns": { "op": "eq", "value": "io.metamask" }, + "chain_id": { "op": "eq", "value": "1" } }, { "type": "event", "event": "transaction" } ], @@ -266,7 +272,7 @@ Filter step 2 to MetaMask wallets on Ethereum mainnet: { "type": "event", "event": "page", - "device": { "op": "equals", "value": "mobile" } + "device": { "op": "eq", "value": "mobile" } }, { "type": "event", @@ -333,7 +339,7 @@ Each `RetentionUserFilter`: | Field | Type | Description | | ------- | ---------------- | ------------------------------------------------------------------------ | | `field` | string | User property (e.g. `device`, `browser`, `os`, `utm_source`, `location`) | -| `op` | string | Comparison operator (same set as `StepFilterCondition.op`) | +| `op` | string | Comparison operator (same set as `StepFilterCondition.op`, minus the substring operators `startsWith` / `endsWith` / `contains`) | | `value` | string \| number | Value to compare against | ```json @@ -345,8 +351,8 @@ Each `RetentionUserFilter`: "settings": { "retentionFilter": { "type": "event", "event": "transaction" }, "retentionUserFilters": [ - { "field": "device", "op": "equals", "value": "desktop" }, - { "field": "utm_source", "op": "notEquals", "value": "direct" } + { "field": "device", "op": "eq", "value": "desktop" }, + { "field": "utm_source", "op": "neq", "value": "direct" } ] } } @@ -550,21 +556,27 @@ Each step in the `steps` array has the following shape: | `op` | SQL equivalent | Notes | | ------------ | ---------------------- | ----------------------------------------------------- | -| `equals` | `= 'value'` | Exact match | -| `notEquals` | `!= 'value'` | Inverse match | +| `eq` | `= 'value'` | Exact match | +| `neq` | `!= 'value'` | Inverse match | | `in` | `IN (...)` | Pipe-delimited value: `"metamask\|rainbow\|coinbase"` | -| `notIn` | `NOT IN (...)` | Pipe-delimited value | +| `nin` | `NOT IN (...)` | Pipe-delimited value | | `gt` | `> value` | Numeric comparison | | `gte` | `>= value` | Numeric comparison | | `lt` | `< value` | Numeric comparison | | `lte` | `<= value` | Numeric comparison | | `startsWith` | `startsWith(col, 'v')` | String prefix | | `endsWith` | `endsWith(col, 'v')` | String suffix | -| `includes` | `like '%v%'` | Substring match | +| `contains` | `like '%v%'` | Substring match | +| `notEmpty` | `col != ''` | Value-less existence check; `value` is ignored | +| `isEmpty` | `col = ''` | Value-less existence check; `value` is ignored | - For `in` and `notIn`, join multiple values with a pipe `|`. Escape a literal - pipe with `\|` and a literal backslash with `\\`. + For `in` and `nin`, join multiple values with a pipe `|`. Escape a literal + pipe with `\|` and a literal backslash with `\\`. The legacy numeric long + forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) are deprecated + aliases for `gt` / `gte` / `lt` / `lte` and remain accepted. `notEmpty` and + `isEmpty` are rejected on the numeric event columns `volume` / `revenue` / + `points`. ### `settings` for Funnel Charts @@ -628,8 +640,8 @@ Filter step 2 to MetaMask wallet connections on Ethereum mainnet: { "type": "event", "event": "connect", - "rdns": { "op": "equals", "value": "io.metamask" }, - "chain_id": { "op": "equals", "value": "1" } + "rdns": { "op": "eq", "value": "io.metamask" }, + "chain_id": { "op": "eq", "value": "1" } }, { "type": "event", "event": "transaction" } ], @@ -673,7 +685,7 @@ Filter step 2 to MetaMask wallet connections on Ethereum mainnet: { "type": "event", "event": "page", - "device": { "op": "equals", "value": "mobile" } + "device": { "op": "eq", "value": "mobile" } }, { "type": "event", @@ -740,7 +752,7 @@ Each `RetentionUserFilter`: | Field | Type | Description | | ------- | ---------------- | ------------------------------------------------------------------------ | | `field` | string | User property (e.g. `device`, `browser`, `os`, `utm_source`, `location`) | -| `op` | string | Comparison operator (same set as `StepFilterCondition.op`) | +| `op` | string | Comparison operator (same set as `StepFilterCondition.op`, minus the substring operators `startsWith` / `endsWith` / `contains`) | | `value` | string \| number | Value to compare against | ```json @@ -752,8 +764,8 @@ Each `RetentionUserFilter`: "settings": { "retentionFilter": { "type": "event", "event": "transaction" }, "retentionUserFilters": [ - { "field": "device", "op": "equals", "value": "desktop" }, - { "field": "utm_source", "op": "notEquals", "value": "direct" } + { "field": "device", "op": "eq", "value": "desktop" }, + { "field": "utm_source", "op": "neq", "value": "direct" } ] } } diff --git a/api/idempotency.mdx b/api/idempotency.mdx index c77d7e3..541a095 100644 --- a/api/idempotency.mdx +++ b/api/idempotency.mdx @@ -72,7 +72,7 @@ curl -X POST https://api.formo.so/v0/alerts \ "name": "Daily revenue drop", "trigger_type": "event", "trigger_filters": [ - { "name": "event", "operator": "equals", "value": "transaction" } + { "name": "event", "operator": "eq", "value": "transaction" } ], "recipient": [{ "type": "webhook", "value": ["https://myapp.com/formo-webhook"] }] }' diff --git a/api/openapi.json b/api/openapi.json index 8ce66bd..dce71f6 100644 --- a/api/openapi.json +++ b/api/openapi.json @@ -168,7 +168,8 @@ "type": "string" }, "operator": { - "type": "string" + "type": "string", + "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `startsWith`, `endsWith`, `contains`, `notEmpty`, `isEmpty`) and their legacy aliases (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`, `like`, `=`, `!=`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." }, "value": { "type": "string" @@ -262,24 +263,26 @@ }, "StepFilterCondition": { "type": "object", - "description": "A filter condition for an event property on a funnel step. The `op` field specifies the comparison operator and `value` is the value to compare against. For `in` and `notIn` operators the value is a pipe-delimited string (e.g. `\"metamask|rainbow|coinbase\"`).", + "description": "A filter condition for an event property on a funnel step. The `op` field specifies the comparison operator and `value` is the value to compare against. For `in` and `nin` operators the value is a pipe-delimited string (e.g. `\"metamask|rainbow|coinbase\"`).", "properties": { "op": { "type": "string", "enum": [ - "equals", - "notEquals", + "eq", + "neq", "in", - "notIn", + "nin", "gt", "gte", "lt", "lte", "startsWith", "endsWith", - "includes" + "contains", + "notEmpty", + "isEmpty" ], - "description": "Comparison operator. Use `in` / `notIn` for multi-value matching (pipe-delimited value string)." + "description": "Comparison operator. The terse comparison tokens (`gt`, `gte`, `lt`, `lte`) are canonical; the legacy long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) remain accepted and equivalent (deprecated). Use `in` / `nin` for multi-value matching (pipe-delimited value string). `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on the property; the `value` is ignored. Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points` — a number has no emptiness semantics." }, "value": { "oneOf": [ @@ -290,7 +293,7 @@ "type": "number" } ], - "description": "The comparison value. For `in` / `notIn`, use a pipe-delimited string: `\"metamask|rainbow|coinbase\"`." + "description": "The comparison value. For `in` / `nin`, use a pipe-delimited string: `\"metamask|rainbow|coinbase\"`." } }, "required": [ @@ -300,7 +303,7 @@ }, "FunnelStep": { "type": "object", - "description": "A single step in a funnel or user-path flow.\n\n`type` and `event` are required. Any additional property key becomes a filter condition using the `StepFilterCondition` schema (e.g. `\"rdns\": { \"op\": \"equals\", \"value\": \"io.metamask\" }`).\n\nStandard filterable columns: `origin`, `device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`. Any other key is treated as a JSON event property and extracted via `JSONExtractString(properties, '')`.", + "description": "A single step in a funnel or user-path flow.\n\n`type` and `event` are required. Any additional property key becomes a filter condition using the `StepFilterCondition` schema (e.g. `\"rdns\": { \"op\": \"eq\", \"value\": \"io.metamask\" }`).\n\nStandard filterable columns: `origin`, `device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`. Any other key is treated as a JSON event property and extracted via `JSONExtractString(properties, '')`.", "properties": { "type": { "type": "string", @@ -351,7 +354,7 @@ }, "AnalyticsFilterCondition": { "type": "object", - "description": "A single analytics filter condition. Use `in` / `notIn` with a pipe-delimited string value (e.g. `\"a|b|c\"`) for multi-value matches.", + "description": "A single analytics filter condition. Use `in` / `nin` with a pipe-delimited string value (e.g. `\"a|b|c\"`) for multi-value matches.", "properties": { "field": { "type": "string", @@ -360,19 +363,21 @@ "op": { "type": "string", "enum": [ - "equals", - "notEquals", - "greater", - "less", - "greaterOrEqual", - "lessOrEqual", + "eq", + "neq", + "gt", + "lt", + "gte", + "lte", "in", - "notIn", + "nin", "startsWith", "endsWith", - "includes" + "contains", + "notEmpty", + "isEmpty" ], - "description": "Comparison operator. `startsWith` / `endsWith` / `includes` are substring matches on text columns (e.g. `referrer`, `ref`, `utm_*`, `origin`, `builder_codes`)." + "description": "Comparison operator. `startsWith` / `endsWith` / `contains` are substring matches on text columns (e.g. `referrer`, `ref`, `utm_*`, `origin`, `builder_codes`). `notEmpty` matches rows where the column has any non-empty value, and `isEmpty` is its complement (the column is empty/unset); both ignore `value` (pass any placeholder). Existence checks are rejected on the numeric event columns `volume`/`revenue`/`points`." }, "value": { "oneOf": [ @@ -386,7 +391,7 @@ "type": "boolean" } ], - "description": "Value to compare against. For `in` / `notIn`, pass a pipe-delimited string (e.g. `\"chrome|firefox\"`)." + "description": "Value to compare against. For `in` / `nin`, pass a pipe-delimited string (e.g. `\"chrome|firefox\"`)." } }, "required": [ @@ -406,19 +411,18 @@ "op": { "type": "string", "enum": [ - "equals", - "notEquals", + "eq", + "neq", "in", - "notIn", + "nin", "gt", "gte", "lt", "lte", - "startsWith", - "endsWith", - "includes" + "notEmpty", + "isEmpty" ], - "description": "Comparison operator." + "description": "Comparison operator. The terse numeric tokens (`gt`, `gte`, `lt`, `lte`) are canonical; the legacy long forms (`greater`, `greaterOrEqual`, `less`, `lessOrEqual`) remain accepted and equivalent (deprecated). `notEmpty` (\"is not empty\") and `isEmpty` (\"is empty\") are value-less existence checks on a string user property; the `value` is ignored. Substring operators (`startsWith` / `endsWith` / `contains`) are not supported on retention user filters." }, "value": { "oneOf": [ @@ -449,13 +453,13 @@ "op": { "type": "string", "enum": [ - "greater", - "greaterOrEqual", - "less", - "lessOrEqual", - "equals" + "gt", + "gte", + "lt", + "lte", + "eq" ], - "default": "greater", + "default": "gt", "description": "Comparison operator. Numeric operators coerce both sides via toFloat64OrZero, so a numeric op on a non-numeric value yields no match (not an error)." }, "value": { @@ -634,7 +638,7 @@ "$ref": "#/components/schemas/FunnelStep" }, "minItems": 2, - "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"equals\", \"value\": \"io.metamask\" }`)." + "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"eq\", \"value\": \"io.metamask\" }`)." }, "settings": { "$ref": "#/components/schemas/ChartSettings" @@ -709,7 +713,7 @@ "$ref": "#/components/schemas/FunnelStep" }, "minItems": 2, - "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"equals\", \"value\": \"io.metamask\" }`)." + "description": "Ordered list of funnel steps. Required for `funnel` (minimum 2 steps).\n\nEach element is a `FunnelStep`; add property filters as extra keys on the step object (e.g. `\"rdns\": { \"op\": \"eq\", \"value\": \"io.metamask\" }`)." }, "settings": { "$ref": "#/components/schemas/ChartSettings" @@ -1631,8 +1635,7 @@ "type": "object", "required": [ "field", - "op", - "value" + "op" ], "properties": { "field": { @@ -1649,11 +1652,23 @@ "lt", "lte", "in", - "nin" - ] + "nin", + "contains", + "notEmpty", + "isEmpty", + "equals", + "notEquals", + "greater", + "greaterOrEqual", + "less", + "lessOrEqual", + "notIn", + "includes" + ], + "description": "Comparison operator. The terse tokens (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `notEmpty`, `isEmpty`) are canonical; the long-form spellings (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`) are deprecated aliases that remain accepted and are folded to canonical at the boundary. `contains` (case-insensitive substring) is only supported on social fields (e.g. `users.twitter`, `users.email`). `notEmpty` (\"is not empty\" / \"is set\") is a string existence check for user string attributes (device/os/utm_*/referrer/…) and social fields; `isEmpty` (\"is empty\" / \"is not set\") is its complement, supported for user string attributes only (not social fields). Both ignore `value`. `users.lifecycle` supports only `eq` (a single stage) and `in` (a list of stages)." }, "value": { - "description": "Filter value (string, number, boolean, or array)" + "description": "Filter value (string, number, boolean, or array). Required for every operator except the existence checks `notEmpty` and `isEmpty`, which ignore it." }, "scope": { "type": "string", @@ -1766,7 +1781,7 @@ "AnalyticsFilters": { "name": "filters", "in": "query", - "description": "Array of filter conditions, JSON-encoded in the query string. Each entry is `{ field, op, value }`. Use `in` / `notIn` with a pipe-delimited `value` (e.g. `\"chrome|firefox\"`) for multi-value matching.", + "description": "Array of filter conditions, JSON-encoded in the query string. Each entry is `{ field, op, value }`. Use `in` / `nin` with a pipe-delimited `value` (e.g. `\"chrome|firefox\"`) for multi-value matching.", "content": { "application/json": { "schema": { @@ -1778,7 +1793,7 @@ "example": [ { "field": "location", - "op": "equals", + "op": "eq", "value": "US" }, { @@ -1893,7 +1908,7 @@ "example": [ { "field": "net_worth_usd", - "op": "greater", + "op": "gt", "value": "1000" } ] @@ -1936,7 +1951,7 @@ "protocol", 1, "uniswap-v3", - "greater", + "gt", "0" ] ] @@ -1961,7 +1976,7 @@ 1, "", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", - "greater", + "gt", "100" ] ] @@ -1983,7 +1998,7 @@ "example": [ [ 1, - "greater", + "gt", "0" ] ] @@ -2006,7 +2021,7 @@ { "event": "swap", "event_type": "track", - "op": "greaterOrEqual", + "op": "gte", "times": 1, "relative_to": "first_seen", "window": { "value": 24, "unit": "hour" } @@ -2018,7 +2033,7 @@ "AnalyticsSourceFilter": { "name": "source_filter", "in": "query", - "description": "JSON object filtering users by raw event source; which SDK/integration the event was sent from (the raw `channel` column). Format: `{ op, value }` where `op` is one of `equals`, `notEquals`, `in`, `notIn` and `value` is one of `web`, `api`, `import`, `mobile`, `server`, `onchain` (pipe-delimited for `in`/`notIn`). Distinct from `channel_filter`, which matches the classified acquisition channel.", + "description": "JSON object filtering users by raw event source; which SDK/integration the event was sent from (the raw `channel` column). Format: `{ op, value }` where `op` is one of `eq`, `neq`, `in`, `nin` and `value` is one of `web`, `api`, `import`, `mobile`, `server`, `onchain` (pipe-delimited for `in`/`nin`). Distinct from `channel_filter`, which matches the classified acquisition channel.", "content": { "application/json": { "schema": { @@ -2034,7 +2049,7 @@ "AnalyticsChannelFilter": { "name": "channel_filter", "in": "query", - "description": "JSON object filtering users by classified acquisition channel; the precomputed `channel_type` column (GA4-style attribution). Format: `{ op, value }` where `op` is one of `equals`, `notEquals`, `in`, `notIn` and `value` is one of `Direct`, `Organic Search`, `Organic Social`, `Organic Video`, `Paid Search`, `Paid Social`, `Paid Video`, `Email`, `Referrals`, `Display`, `AI`, `Referrers` (pipe-delimited for `in`/`notIn`). Distinct from `source_filter`, which matches the raw SDK source.", + "description": "JSON object filtering users by classified acquisition channel; the precomputed `channel_type` column (GA4-style attribution). Format: `{ op, value }` where `op` is one of `eq`, `neq`, `in`, `nin` and `value` is one of `Direct`, `Organic Search`, `Organic Social`, `Organic Video`, `Paid Search`, `Paid Social`, `Paid Video`, `Email`, `Referrals`, `Display`, `AI`, `Referrers` (pipe-delimited for `in`/`nin`). Distinct from `source_filter`, which matches the raw SDK source.", "content": { "application/json": { "schema": { @@ -2063,7 +2078,7 @@ [ "whale", 1, - "equals", + "eq", "true" ] ] @@ -2281,7 +2296,7 @@ "trigger_filters": [ { "name": "event", - "operator": "equals", + "operator": "eq", "value": "transaction" }, { @@ -2351,7 +2366,8 @@ "type": "string" }, "operator": { - "type": "string" + "type": "string", + "description": "Comparison operator for the trigger filter. Both the canonical filter operators (`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `startsWith`, `endsWith`, `contains`, `notEmpty`, `isEmpty`) and their legacy aliases (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`, `like`, `=`, `!=`) are accepted; the value is normalized to canonical form before the alert condition is evaluated." }, "value": { "type": "string" @@ -2409,17 +2425,17 @@ { "name": "type", "value": "swap", - "operator": "equals" + "operator": "eq" }, { "name": "device", "value": "mobile", - "operator": "equals" + "operator": "eq" }, { "name": "volume", "value": "1000", - "operator": "greater", + "operator": "gt", "numericThreshold": "1000" } ], @@ -2453,12 +2469,12 @@ { "name": "utm_source", "value": "google", - "operator": "equals" + "operator": "eq" }, { "name": "utm_medium", "value": "cpc", - "operator": "equals" + "operator": "eq" } ], "recipient": [ @@ -2480,12 +2496,12 @@ { "name": "location", "value": "US", - "operator": "equals" + "operator": "eq" }, { "name": "net_worth_usd", "value": "10000", - "operator": "greaterOrEqual", + "operator": "gte", "numericThreshold": "10000" } ], @@ -2508,13 +2524,13 @@ { "name": "apps", "value": "uniswap", - "operator": "greater", + "operator": "gt", "numericThreshold": "5" }, { "name": "chains", "value": "1", - "operator": "greaterOrEqual", + "operator": "gte", "numericThreshold": "3" } ], @@ -2580,7 +2596,7 @@ "trigger_filters": [ { "name": "event", - "operator": "equals", + "operator": "eq", "value": "transaction" }, { @@ -3245,11 +3261,11 @@ "type": "event", "event": "connect", "rdns": { - "op": "equals", + "op": "eq", "value": "io.metamask" }, "chain_id": { - "op": "equals", + "op": "eq", "value": "1" } }, @@ -3310,7 +3326,7 @@ "type": "event", "event": "page", "device": { - "op": "equals", + "op": "eq", "value": "mobile" } }, @@ -3464,7 +3480,7 @@ "retentionUserFilters": [ { "field": "device", - "op": "equals", + "op": "eq", "value": "desktop" } ] @@ -4100,7 +4116,7 @@ "projectId": "proj_abc123", "title": "High net worth desktop users", "filterSet": [ - "{\"field\":\"device\",\"op\":\"equals\",\"value\":\"desktop\"}", + "{\"field\":\"device\",\"op\":\"eq\",\"value\":\"desktop\"}", "{\"field\":\"net_worth_usd\",\"op\":\"greaterOrEqual\",\"value\":100000}" ] } @@ -4152,9 +4168,9 @@ "value": { "title": "High-value mobile users", "filterSets": [ - "device::equals::mobile", - "net_worth_usd::greaterOrEqual::10000", - "utm_source::equals::paid_ads" + "device::eq::mobile", + "net_worth_usd::gte::10000", + "utm_source::eq::paid_ads" ] } }, @@ -4172,7 +4188,7 @@ "value": { "title": "Non-US users", "filterSets": [ - "location::notEquals::US" + "location::neq::US" ] } }, @@ -4181,9 +4197,9 @@ "value": { "title": "Organic power users", "filterSets": [ - "net_worth_usd::greater::1000", - "utm_source::notIn::google_ads|facebook_ads", - "lifecycle::equals::power" + "net_worth_usd::gt::1000", + "utm_source::nin::google_ads|facebook_ads", + "lifecycle::eq::power" ] } }, @@ -4192,19 +4208,19 @@ "value": { "title": "Multi-chain whales", "filterSets": [ - "chains::greaterOrEqual::3", - "net_worth_usd::greater::100000", - "apps::greater::5" + "chains::gte::3", + "net_worth_usd::gt::100000", + "apps::gt::5" ] } }, "behavioralFilterSimple": { "summary": "Users who performed 'signature' event at least once in last 30 days", - "description": "Behavioural filters use the 'events' filter key with a base64-encoded JSON array as the value. The JSON contains event name, frequency operator (greater/greaterOrEqual/equals/lessOrEqual/less), times count, and date range.", + "description": "Behavioural filters use the 'events' filter key with a base64-encoded JSON array as the value. The JSON contains event name, frequency operator (gt/gte/eq/lte/lt), times count, and date range.", "value": { "title": "Recent signers", "filterSets": [ - "events::equals::W3siZXZlbnQiOiAic2lnbmF0dXJlIiwgIm9wZXJhdG9yIjogImdyZWF0ZXJPckVxdWFsIiwgInRpbWVzIjogMSwgInByZXNldCI6ICJsYXN0XzMwZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0wOS0xOSIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifV0=" + "events::eq::W3siZXZlbnQiOiAic2lnbmF0dXJlIiwgIm9wZXJhdG9yIjogImdyZWF0ZXJPckVxdWFsIiwgInRpbWVzIjogMSwgInByZXNldCI6ICJsYXN0XzMwZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0wOS0xOSIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifV0=" ] } }, @@ -4214,8 +4230,8 @@ "value": { "title": "Indian MetaMask users", "filterSets": [ - "events::equals::W3siZXZlbnQiOiAiY29ubmVjdCIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5IiwgInJkbnMiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICJpby5tZXRhbWFzayJ9LCAiY2hhaW5faWQiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICIxIn19XQ==", - "location::equals::IN" + "events::eq::W3siZXZlbnQiOiAiY29ubmVjdCIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5IiwgInJkbnMiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICJpby5tZXRhbWFzayJ9LCAiY2hhaW5faWQiOiB7Im9wIjogImVxdWFscyIsICJ2YWx1ZSI6ICIxIn19XQ==", + "location::eq::IN" ] } }, @@ -4225,8 +4241,8 @@ "value": { "title": "Active transactors", "filterSets": [ - "events::equals::W3siZXZlbnQiOiAicGFnZSIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDUsICJwcmVzZXQiOiAibGFzdF83ZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0xMC0xMiIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifSwgeyJldmVudCI6ICJ0cmFuc2FjdGlvbiIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5In1d", - "device::equals::desktop" + "events::eq::W3siZXZlbnQiOiAicGFnZSIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDUsICJwcmVzZXQiOiAibGFzdF83ZCIsICJkYXRlX2Zyb20iOiAiMjAyNS0xMC0xMiIsICJkYXRlX3RvIjogIjIwMjUtMTAtMTkifSwgeyJldmVudCI6ICJ0cmFuc2FjdGlvbiIsICJvcGVyYXRvciI6ICJncmVhdGVyT3JFcXVhbCIsICJ0aW1lcyI6IDEsICJwcmVzZXQiOiAibGFzdF8zMGQiLCAiZGF0ZV9mcm9tIjogIjIwMjUtMDktMTkiLCAiZGF0ZV90byI6ICIyMDI1LTEwLTE5In1d", + "device::eq::desktop" ] } } @@ -7274,7 +7290,7 @@ "example": [ { "field": "device", - "op": "equals", + "op": "eq", "value": "desktop" } ] @@ -7407,7 +7423,7 @@ "get": { "operationId": "getAnalyticsFunnel", "summary": "Get multi-step conversion funnel", - "description": "Multi-step conversion funnel. For an ordered list of step specs, returns one row per step with the unique-user count, conversion ratios against step 1 and the previous step, drop-off ratio, and median time-to-convert (from-previous and from-start, in seconds).\n\n**Closed vs open:** `funnel_type=closed` (default) requires the user to fire the steps in order within `window_seconds`. `funnel_type=open` counts whoever fired step k regardless of order, and adds a `dropped_off_users` column for each step.\n\n**Breakdown:** when `group_by` is set to a column from the breakdown allowlist (`device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`), the response gains a `breakdown` column and is grouped per (step, top-N category + 'Others', up to `limit` which defaults to 5). First-touch attribution is used by default; pass `attribution=last_touch` to bucket by each user's latest value.\n\n**Steps:** the `steps` query param is a JSON-encoded array of 2 to 10 step specs of shape `{type, event, name, filters?: [{operand, operator, value}]}`. Use `name` (e.g. `\"page::0\"`) as a unique step id so the same event re-used at different steps can be disambiguated in the response. `operator` accepts `equals | notEquals | in | notIn | gt | lt | gte | lte | startsWith | endsWith | includes`. `operand` may target a standard event column or a JSON property on `properties`.", + "description": "Multi-step conversion funnel. For an ordered list of step specs, returns one row per step with the unique-user count, conversion ratios against step 1 and the previous step, drop-off ratio, and median time-to-convert (from-previous and from-start, in seconds).\n\n**Closed vs open:** `funnel_type=closed` (default) requires the user to fire the steps in order within `window_seconds`. `funnel_type=open` counts whoever fired step k regardless of order, and adds a `dropped_off_users` column for each step.\n\n**Breakdown:** when `group_by` is set to a column from the breakdown allowlist (`device`, `browser`, `os`, `location`, `referrer`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`), the response gains a `breakdown` column and is grouped per (step, top-N category + 'Others', up to `limit` which defaults to 5). First-touch attribution is used by default; pass `attribution=last_touch` to bucket by each user's latest value.\n\n**Steps:** the `steps` query param is a JSON-encoded array of 2 to 10 step specs of shape `{type, event, name, filters?: [{operand, operator, value}]}`. Use `name` (e.g. `\"page::0\"`) as a unique step id so the same event re-used at different steps can be disambiguated in the response. `operator` accepts `equals | notEquals | in | notIn | gt | lt | gte | lte | greater | greaterOrEqual | less | lessOrEqual | startsWith | endsWith | includes | notEmpty | isEmpty` (the short numeric aliases `gt`/`gte`/`lt`/`lte` and their canonical long forms are equivalent; `notEmpty`/`isEmpty` are value-less existence checks, rejected on the numeric event columns `volume`/`revenue`/`points`). `operand` may target a standard event column or a JSON property on `properties`.", "tags": [ "Query" ], @@ -7442,7 +7458,7 @@ "schema": { "type": "string" }, - "description": "JSON-encoded array of 2 to 10 step specs. Each step is `{type, event, name, filters?: [{operand, operator, value}]}`. `type` is the event type (e.g. `event` for page views, `track` for custom events, `transaction`, `signature`, `decoded_log`). `event` is the event name. `name` is the unique step id (use `\"::\"` to disambiguate repeated events).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `startsWith`, `endsWith`, `includes`. For `in`/`notIn`, pass the values as a `|`-separated string in `value` (e.g. `\"ethereum|polygon|base\"`).\n\n**Standard columns** (rendered via direct column access): `origin`, `device`, `browser`, `os`, `location`, `referrer`, `direct`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`, `version`, `locale`, `timezone`, `page_path`. Anything else is treated as a JSON property and read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", + "description": "JSON-encoded array of 2 to 10 step specs. Each step is `{type, event, name, filters?: [{operand, operator, value}]}`. `type` is the event type (e.g. `event` for page views, `track` for custom events, `transaction`, `signature`, `decoded_log`). `event` is the event name. `name` is the unique step id (use `\"::\"` to disambiguate repeated events).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty` (the short numeric aliases and their canonical long forms are equivalent; `notEmpty`/`isEmpty` are value-less existence checks, rejected on numeric event columns `volume`/`revenue`/`points`). For `in`/`notIn`, pass the values as a `|`-separated string in `value` (e.g. `\"ethereum|polygon|base\"`).\n\n**Standard columns** (rendered via direct column access): `origin`, `device`, `browser`, `os`, `location`, `referrer`, `direct`, `ref`, `utm_source`, `utm_medium`, `utm_campaign`, `utm_content`, `utm_term`, `builder_codes`, `version`, `locale`, `timezone`, `page_path`. Anything else is treated as a JSON property and read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", "examples": { "simple": { "summary": "Simple 3-step funnel: page → connect → swap", @@ -7678,7 +7694,7 @@ "schema": { "type": "string" }, - "description": "JSON-encoded start-step spec of shape `{type, event, resolved_event, filters?: [...], status_type?, status_value?}`. Use `\"resolved_event\":\"__ALL_PAGE_VIEWS__\"` to match any page view; pass a page path for a specific landing page; or pass an event name for `track`/`decoded_log` start events. `filters` use `{operand, operator, value, values?}` (use `values` for `in`/`notIn`).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `startsWith`, `endsWith`, `includes`. Standard columns (`device`, `browser`, `os`, `location`, `referrer`, `utm_*`, `page_path`, etc.) are read directly; everything else is read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", + "description": "JSON-encoded start-step spec of shape `{type, event, resolved_event, filters?: [...], status_type?, status_value?}`. Use `\"resolved_event\":\"__ALL_PAGE_VIEWS__\"` to match any page view; pass a page path for a specific landing page; or pass an event name for `track`/`decoded_log` start events. `filters` use `{operand, operator, value, values?}` (use `values` for `in`/`notIn`).\n\n**Filter operators:** `equals`, `notEquals`, `in`, `notIn`, `gt`, `lt`, `gte`, `lte`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `startsWith`, `endsWith`, `includes`, `notEmpty`, `isEmpty` (the short numeric aliases and their canonical long forms are equivalent; `notEmpty`/`isEmpty` are value-less existence checks, rejected on numeric event columns `volume`/`revenue`/`points`). Standard columns (`device`, `browser`, `os`, `location`, `referrer`, `utm_*`, `page_path`, etc.) are read directly; everything else is read from `properties` via `JSONExtractString` (or `JSONExtractFloat` for numeric comparators).", "examples": { "anyPageView": { "summary": "Match any page view as the start step", diff --git a/api/profiles/search.mdx b/api/profiles/search.mdx index b9eb2b5..d93eb5d 100644 --- a/api/profiles/search.mdx +++ b/api/profiles/search.mdx @@ -146,6 +146,12 @@ Each condition in the `conditions` array has: | `in` | In array | `{"field": "users.lifecycle", "op": "in", "value": ["New", "Power user"]}` | | `nin` | Not in array | `{"field": "users.location", "op": "nin", "value": ["US", "UK"]}` | | `contains` | Case-insensitive substring match for social fields only | `{"field": "users.twitter", "op": "contains", "value": "vitalik"}` | +| `notEmpty` | Field is set (non-empty). String fields only — value is ignored | `{"field": "users.email", "op": "notEmpty"}` | +| `isEmpty` | Field is not set (empty). String user attributes only — value is ignored | `{"field": "users.utm_source", "op": "isEmpty"}` | + + +**Deprecated aliases.** The long-form spellings `equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, and `includes` are deprecated but still accepted on this endpoint — they are folded to the canonical operators above at the input boundary, so existing integrations keep working. Use the canonical operators for new integrations; the aliases may be retired in a future release. `notEmpty` / `isEmpty` are value-less existence checks (any `value` is ignored) and are supported on string user/profile attributes only — not on numeric metrics (`net_worth_usd`, `volume`, `revenue`, `points`), the `lifecycle` enum, or chain/app/token/label fields. + --- @@ -263,8 +269,8 @@ Filter by user lifecycle stage. Valid values: `At Risk`, `Churned`, `New`, `Powe Social fields support two modes: -- Presence checks: use an empty string value to match profiles where the field is present. -- Value matching: use `eq`, `neq`, or `contains` with a non-empty string to match the actual social value. +- Presence checks: use `notEmpty` to match profiles where the field is set (or `isEmpty` for the opposite). The value is ignored. +- Value matching: use `eq`, `neq`, or `contains` (case-insensitive substring) with a non-empty string to match the actual social value. | Field | Description | |-------|-------------| @@ -291,7 +297,7 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.email", "op": "eq", "value": "" } + { "field": "users.email", "op": "notEmpty" } ], "logic": "and" } @@ -302,8 +308,8 @@ Social fields support two modes: ```json { "conditions": [ - { "field": "users.ens", "op": "eq", "value": "" }, - { "field": "users.farcaster", "op": "eq", "value": "" } + { "field": "users.ens", "op": "notEmpty" }, + { "field": "users.farcaster", "op": "notEmpty" } ], "logic": "and" } @@ -591,7 +597,7 @@ curl -sS -X GET \ -d '{ "conditions": [ { "field": "users.net_worth_usd", "op": "gt", "value": 10000 }, - { "field": "users.ens", "op": "eq", "value": "" }, + { "field": "users.ens", "op": "notEmpty" }, { "field": "chains.1.balance", "op": "gt", "value": 0 } ], "logic": "and" diff --git a/api/query/event-timeseries.mdx b/api/query/event-timeseries.mdx index e5a8226..369bc20 100644 --- a/api/query/event-timeseries.mdx +++ b/api/query/event-timeseries.mdx @@ -8,4 +8,4 @@ Returns total event counts per day. Combine with `filters` to scope to a specifi Set `group_by` to a breakdown dimension (`channel_type`, `device`, `browser`, `os`, `location`, `referrer`, `ref`, `builder_codes`, or any UTM column) to split the series by that dimension. When set, `event_key` carries the dimension's value (empty → `Direct`) instead of the event type/name; the response keeps the top 100 values and buckets the rest as `Others`. -`filters` supports the string operators `startsWith`, `endsWith`, and `includes` (substring matches on text columns such as `referrer`, `ref`, `utm_*`) alongside the standard comparison operators. An unsupported operator now returns `400 Invalid query parameters` rather than silently matching everything. +`filters` supports the string operators `startsWith`, `endsWith`, and `contains` (substring matches on text columns such as `referrer`, `ref`, `utm_*`) alongside the standard comparison operators. An unsupported operator now returns `400 Invalid query parameters` rather than silently matching everything. diff --git a/api/query/funnel.mdx b/api/query/funnel.mdx index 82ab4d3..822fbe9 100644 --- a/api/query/funnel.mdx +++ b/api/query/funnel.mdx @@ -34,6 +34,8 @@ The `steps` query parameter is a JSON-encoded array of 2 to 10 step specs. Each - `startsWith` - `endsWith` - `includes` + - `notEmpty` / `isEmpty` - value-less existence checks (the `value` is ignored; rejected on the numeric event columns `volume`/`revenue`/`points`) + - The numeric long forms `greater`, `greaterOrEqual`, `less`, `lessOrEqual` are accepted as equivalents of `gt` / `gte` / `lt` / `lte`. - **`operand`** may target a standard event column or a JSON property on `properties`. ## Example diff --git a/cli/analytics.mdx b/cli/analytics.mdx index 7234666..9079e7a 100644 --- a/cli/analytics.mdx +++ b/cli/analytics.mdx @@ -52,7 +52,7 @@ Requires `query:read` scope on your API key. ### `--filters` -A JSON array of `{ field, op, value }` conditions, e.g. `[{"field":"location","op":"equals","value":"US"}]`. For multi-value matching, use `in` / `notIn` with a pipe-delimited `value` (e.g. `"chrome|firefox"`). +A JSON array of `{ field, op, value }` conditions, e.g. `[{"field":"location","op":"eq","value":"US"}]`. For multi-value matching, use `in` / `nin` with a pipe-delimited `value` (e.g. `"chrome|firefox"`). ### `--params` (pipe-specific) @@ -87,7 +87,7 @@ formo analytics funnel \ formo analytics top_wallets --date-from 2026-04-01 --date-to 2026-04-30 --params '{"limit":10}' # Retention restricted to US visitors -formo analytics retention --filters '[{"field":"location","op":"equals","value":"US"}]' +formo analytics retention --filters '[{"field":"location","op":"eq","value":"US"}]' # Revenue timeseries for a specific wallet formo analytics revenue_timeseries \ diff --git a/cli/profiles.mdx b/cli/profiles.mdx index 8834b1b..bccc34e 100644 --- a/cli/profiles.mdx +++ b/cli/profiles.mdx @@ -125,7 +125,9 @@ The response is a paginated envelope: `{ data, total, page, size, has_more }`. ### Filter Operators -`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin` +`eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `nin`, `contains`, `notEmpty`, `isEmpty` + +The long-form spellings (`equals`, `notEquals`, `greater`, `greaterOrEqual`, `less`, `lessOrEqual`, `notIn`, `includes`) are deprecated aliases — still accepted by the API, but new usage should stick to the canonical operators above. ---