# Targeting

Targeting decides who enters an experiment, and where and when: the **pages** it runs on, the **trigger** that starts it on the page, and the **audience** of visitors who may enter. A visitor is assigned a variation only when all three are true on the current page. TryFast writes one sentence that sums it up, shown in lists and on approval pages, for example "Pages starting with /pricing, except URLs containing /checkout/success, new visitors on phones, when the page loads."

```json
{
  "pages": {
    "include": [{"match": "prefix", "value": "/pricing", "ignore_query": true, "case_sensitive": false}],
    "exclude": [{"match": "contains", "value": "/admin"}]
  },
  "trigger": {"type": "immediate"},
  "audience_id": null,
  "audience": {
    "op": "and",
    "items": [
      {"type": "device", "op": "in", "value": ["mobile", "tablet"]},
      {"op": "or", "items": [
        {"type": "utm", "key": "source", "op": "equals", "value": "newsletter"},
        {"type": "visitor_type", "op": "equals", "value": "new"}
      ]}
    ]
  }
}
```

## Pages

`include` and `exclude` are lists of URL rules (up to 20 each). A page matches when it matches at least one `include` rule and no `exclude` rule. An empty `include` list means every page of the site.

| `match` | Matches when the URL |
|---|---|
| `all` | always |
| `exact` | equals the value (a trailing slash is ignored) |
| `prefix` | starts with the value |
| `contains` | contains the value |
| `regex` | matches the regular expression |

- A value starting with `/` is compared with the path (plus the query string when `ignore_query` is `false`). Any other value is compared with host plus path, for example `shop.example.com/pricing`.
- `regex` is tested against the full URL `https://host/path?query`. Up to 500 characters; use `case_sensitive` instead of inline flags such as `(?i)`.
- Comparisons ignore case unless `case_sensitive` is `true`. `ignore_query` defaults to `true`.
- TryFast's own `tf_*` parameters and the `#fragment` are removed before matching.

Example: `{"match": "exact", "value": "/cart"}` matches `https://shop.example/cart/?x=1#y` but not `https://shop.example/cart/items`.

Page rules are checked on every page view, including route changes in single-page apps: when a visitor leaves the experiment's pages, its changes are reverted, and they come back (same variation) when the visitor returns.

## Trigger

| `type` | The experiment starts on the page |
|---|---|
| `immediate` (default) | as soon as the page loads |
| `custom_event` | when your code calls `tryfast.event(name)`: `{"type": "custom_event", "name": "opened_cart"}` |
| `element` | when an element matching `selector` appears: `{"type": "element", "selector": "#price"}` |
| `delay` | after `ms` milliseconds (up to 600,000) |
| `manual` | when your code calls `tryfast.activate("experiment-key")` |
| `js` | when your code returns `true` or calls `ctx.activate()`: `{"type": "js", "code": "return window.cartTotal > 100;"}` |

Visitors are counted in the experiment only when the trigger fires, so a test on a checkout drawer counts only visitors who opened it. Triggers other than `immediate` usually fire after the page is visible, so changes can flicker; prefer changes to elements that appear with the trigger.

## Audience

An audience is a group of conditions joined with `and` or `or`. Groups can contain one more level of groups, with up to 30 conditions in total. Set it inline (`audience`) or reuse a saved audience of the project (`audience_id`). When the experiment starts, a saved audience is copied into the experiment (`audience_snapshot`), so editing the saved audience later never changes a running test.

The audience is checked only when a visitor is first assigned. A visitor who entered a "new visitors" test keeps seeing their variation on their second visit.

### Conditions

The complete catalog with operators is served at `GET /api/v1/targeting/conditions`.

| `type` | Condition | `key` | Value |
|---|---|---|---|
| `landing_page` | First page of the visit | | text |
| `referrer` | Where the visit came from (origin) | | text |
| `query_param` | A query parameter of the current page | parameter name | text |
| `utm` | A UTM parameter of the visit's landing page | `source`, `medium`, `campaign`, `term` or `content` | text |
| `traffic_type` | How the visit started | | `direct`, `organic`, `paid`, `email`, `social`, `referral` |
| `device` | Device type | | `desktop`, `tablet`, `mobile` |
| `browser` | Browser | | `chrome`, `safari`, `firefox`, `edge`, `samsung`, `opera`, `other` |
| `os` | Operating system | | `windows`, `macos`, `ios`, `android`, `linux`, `chromeos`, `other` |
| `language` | Browser language | | language tags; `de` also matches `de-AT` |
| `visitor_type` | New or returning | | `new`, `returning` |
| `visit_count` | Number of visits, including this one | | number |
| `pageviews` | Pages viewed in this visit, including this one | | number |
| `cookie` | A first-party cookie | cookie name | text |
| `attribute` | A visitor attribute set with `tryfast.set()` | attribute name | text, number or boolean |
| `data_layer` | A value in `window.dataLayer` | dot path, such as `page.category` | text or number |
| `custom_event` | `tryfast.event(name)` fired on this page or in this visit | event name | none (`fired_page`, `fired_visit`) |
| `element` | An element exists on the page | | CSS selector (`exists`, `not_exists`) |
| `js` | Your own code returns true | | function body (`is_true`) |

Operators: text conditions use `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `regex`, `not_regex`, `exists`, `not_exists` (case-insensitive unless `case_sensitive`); number conditions use `eq`, `neq`, `gt`, `gte`, `lt`, `lte`; list conditions use `in` and `not_in` with up to 50 values.

Traffic types: `paid` for visits with `gclid`, `fbclid`, `msclkid` or a `utm_medium` of `cpc`, `ppc` or `paid`; `email` for `utm_medium` `email` or `newsletter`; `organic` for search engine referrers; `social` for known social networks; `referral` for other sites; `direct` without a referrer.

### Conditions that need time

Some conditions cannot be decided the moment the page starts: an element that renders later, a data layer value pushed after load, a custom event, a promise in your code. TryFast keeps such conditions pending and checks again when the page changes, until DOMContentLoaded plus 3 seconds; then a pending condition counts as false. A JavaScript condition that throws counts as false and the error shows in `tryfast.state()`.

Country targeting is not available yet. Also planned: time windows, weekdays, "saw experiment X" and "converted on goal Y".

## Check what happened

Open the page with `?tf_debug=1`: the console shows every page rule and condition with the visitor's actual value and the result. `tryfast.state()` returns the same. See [QA and preview](qa-and-preview.md#debugging).
