Rules
A rule is a condition evaluated against a completed session. When it matches, it writes a tag. Tags are what you count, filter and get notified about.
Rules live in a YAML file in your repository. plan shows the difference
between that file and what is deployed. apply reconciles them. Your
definition of a conversion is reviewed like any other change, and its history
is in your version control.
A rules file
origin: https://example.com
project: myapp/home
rules:
- rule_id: any_session
tag: visited
logic: {">=": [{"var": "session.num_events"}, 1]}
- rule_id: bounce
tag: bounce
description: One action and nothing further
logic: {"==": [{"var": "session.num_events"}, 1]}
- rule_id: engaged
tag: engaged
description: Three or more actions
logic: {">=": [{"var": "session.num_events"}, 3]}
- rule_id: pricing_visitor
tag: pricing
notification_target: email
requires_events: true
description: Visited pricing, the clearest purchase intent signal
logic:
{"some": [{"var": "events"},
{"==": [{"var": "payload.path"}, "/pricing.html"]}]}
| Field | Required | Description |
|---|---|---|
rule_id |
yes | Stable identifier. Changing it deletes one rule and creates another. |
tag |
yes | The label written on a match. Several rules may write the same tag. |
logic |
yes | A JSONLogic expression returning true or false. |
description |
no | Shown in listings and in the digest. Write these. |
notification_target |
no | Send an immediate message on match. Currently email. |
requires_events |
no | Load the session’s full event list before evaluating. |
Deploying rules
clog plan --file rules.yaml --origin https://example.com --project myapp/home
example.com/myapp/home
comparing rules.yaml against deployed rules
~ any_session [modified]
notification_target: null -> "email"
= engaged [unchanged]
= bounce [unchanged]
+ pricing_visitor [new - will be created]
tag: pricing
logic: {"some": [...]}
- old_rule [removed - will be deleted]
summary: 1 modified, 1 added, 1 removed, 2 unchanged
clog apply --file rules.yaml --origin https://example.com --project myapp/home
apply shows the plan and prompts before writing. --auto-approve skips the
prompt, --dry-run writes nothing.
Exit codes make this usable in a pipeline:
| Code | Meaning |
|---|---|
| 0 | No differences |
| 1 | Differences found |
| 2 | Validation error in the YAML |
| 3 | Authentication or API error |
--strict treats any drift as a failure, for pipelines where the deployed
state must always match the file.
The session context
A rule evaluates against one session. These fields are available under
session:
| Field | Type | Description |
|---|---|---|
origin |
string | Site URL |
project |
string | Project identifier |
session |
string | Session identifier |
start_time |
int | First event, milliseconds since epoch |
end_time |
int | Last event, milliseconds since epoch |
duration |
int | Milliseconds between the two |
num_events |
int | Total events in the session |
unique_actions |
int | Distinct action names |
actions |
list | Flat list of action names, for existence checks |
action_map |
list | Ordered [action, offset_ms] pairs |
geo |
object | countryCode, region, city, lat, lon, timezone, org |
ua |
object | browser, os, device_type, is_mobile, is_bot |
With requires_events: true, the full event list is loaded and available as
events at the top level. Each entry carries action, time and payload.
Loading events costs a database read per session, so use requires_events
only when the rule reads a payload.
Patterns
A named action occurred. The cheapest form, no event load required.
logic: {"in": ["user.signup.success", {"var": "session.actions"}]}
A payload field matched. Requires events.
requires_events: true
logic:
{"some": [{"var": "events"},
{"==": [{"var": "payload.plan"}, "annual"]}]}
Reading depth. The scroll milestone payload.
requires_events: true
logic:
{"some": [{"var": "events"}, {">=": [{"var": "payload.percent"}, 75]}]}
Time on page. From the page_exit payload.
requires_events: true
logic:
{"some": [{"var": "events"}, {">=": [{"var": "payload.dwell_ms"}, 60000]}]}
Arrived from a specific referrer. The first event’s payload, without loading the full list.
logic:
{"in": ["news.ycombinator.com",
{"var": "session.events.0.payload.referrer"}]}
Several conditions. and, or and ! compose.
requires_events: true
logic:
{"and": [
{">=": [{"var": "session.num_events"}, 3]},
{"==": [{"var": "session.geo.countryCode"}, "GB"]},
{"some": [{"var": "events"},
{"==": [{"var": "payload.path"}, "/pricing.html"]}]}
]}
Human traffic only. Bot classification is available to rules.
logic: {"==": [{"var": "session.ua.is_bot"}, false]}
Testing before you deploy
Evaluate a rule against a real session:
clog test --file rules.yaml --rule pricing_visitor --session 1782693098872-7xzwuxbzd
Or against a file of known examples, which is the form to run in CI:
clog test --file rules.yaml --rule pricing_visitor --examples fixtures/sessions.json
Build the fixture file from sessions you have already seen:
clientlog --origin https://example.com --project myapp/home --format json \
sessions get --sessions 1782693098872-7xzwuxbzd > fixtures/sessions.json
A rule that has never been tested against a session that should match it, and one that should not, is a rule you are guessing about.
Notifications
notification_target: email sends a message the moment a matching session is
classified.
Reserve it for signals worth interrupting yourself for: a completed purchase, a demo request, a visit from a named account. A rule matching a third of your traffic produces a mailbox nobody reads and hides the signals that mattered. Everything else belongs in the digest.
Rules as accumulated knowledge
Rules are a record of what you have learned about your own traffic. A rule that isolated a bot pattern, a referrer worth watching, a page that predicts purchase: each one is a finding, encoded so it applies to every future session without you looking again.
That is the argument for holding them in version control alongside the site they measure. The rule and the page it depends on change together, and the commit says why.
Next: reading the digest.