# Targeting

Filter who sees a test with AND/OR rules over 18 fields: location, device, traffic source, UTM tags, time, cookies, custom properties.

Filter who sees a test with AND/OR rules over 18 fields — location, device, traffic source, UTM tags, time, cookies, and your own custom properties.

URL matching decides which *pages* a test runs on; targeting decides which *visitors* see it once they're on those pages. Leave it empty and the test runs for everyone matching the URL — the right default for most experiments. Add rules when you want to compare variants in a specific audience (mobile only, paid social only, enterprise plan customers) or when you're running a personalization test where the rules **are** the variants.

Targeting rules form a tree. The root is an AND or OR group; each child is either a condition (one field, one operator, one or more values) or another nested group. The SDK and the server both evaluate the tree, with the field deciding where evaluation happens — geo fields resolve from the visitor IP server-side, cookies evaluate in the browser, and most fields work on both sides.

## Rule tree at a glance

```text
AND
├── country in ["US", "CA"]
├── device_type equals "mobile"
└── OR
    ├── utm_source equals "newsletter"
    └── custom_property[plan] equals "pro"
```

Read top-down: a visitor matches when they're in the US or Canada, on a mobile device, **and** either came from the newsletter or has a Pro plan. Mix and nest groups freely.

## Field reference

Eighteen built-in fields grouped by what they describe. *Server-resolved* fields are evaluated from the visitor's IP and mirrored to the client. *Server + client* fields work on both sides. *Client-only* fields (cookies) evaluate only in the browser because the server never sees the value.

### Location

Geo-IP derived. Resolved server-side and mirrored to the SDK so the browser agrees.

| Field | Label | Type | Evaluation | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `country` | Country | string | Server-resolved | ISO-3166 country code resolved from the visitor IP. Use the country picker for a known list. | `country in ["US", "CA", "MX"]` |
| `region` | Region / State | string | Server-resolved | Sub-country region resolved from the visitor IP. Granularity varies by country. | `region equals "California"` |

### Device & browser

Inferred from user-agent. Evaluable everywhere.

| Field | Label | Type | Evaluation | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `device_type` | Device type | enum | Server + client | desktop, mobile, or tablet. | `device_type equals "mobile"` |
| `os` | Operating system | enum | Server + client | windows, macos, ios, android, linux, other. | `os in ["ios", "android"]` |
| `browser` | Browser | enum | Server + client | chrome, firefox, safari, edge, opera, other. | `browser equals "safari"` |
| `screen_width` | Screen width | number | Server + client | Reported screen width in CSS pixels. Useful for layout-specific tests. | `screen_width greater_than 1280` |
| `language` | Browser language | string | Server + client | BCP 47 language tag from the browser, e.g. en-US, pt-BR. Use starts_with for language families. | `language starts_with "fr"` |

### Traffic source

Where the visitor came from. Sourced from referrer and URL parameters.

| Field | Label | Type | Evaluation | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `traffic_source` | Traffic source bucket | enum | Server + client | direct, organic_search, paid_search, social, email, referral. Otter picks the bucket from referrer + utm_medium. | `traffic_source in ["paid_search", "social"]` |
| `utm_source` | utm_source | string | Server + client | Raw utm_source from the URL. | `utm_source equals "newsletter"` |
| `utm_medium` | utm_medium | string | Server + client | Raw utm_medium from the URL. | `utm_medium contains "cpc"` |
| `utm_campaign` | utm_campaign | string | Server + client | Raw utm_campaign from the URL. | `utm_campaign starts_with "spring-"` |
| `referrer` | Referrer | string | Server + client | Document referrer (where the visitor came from). Empty for direct navigation. | `referrer contains "reddit.com"` |

### Time

Server-side clock + visitor's local context.

| Field | Label | Type | Evaluation | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `time_of_day` | Time of day | time | Server + client | Local visitor time. Supports between for time windows. | `time_of_day between [09:00, 17:00]` |
| `day_of_week` | Day of week | enum | Server + client | monday through sunday. | `day_of_week in ["saturday", "sunday"]` |

### Visitor & custom signals

Built-in audience signals plus whatever you push from your code.

| Field | Label | Type | Evaluation | Description | Example |
| --- | --- | --- | --- | --- | --- |
| `visitor_type` | Visitor type | enum | Server + client | new or returning, derived from the Otter visitor UUID cookie. | `visitor_type equals "returning"` |
| `url_param` | URL parameter | keyed | Server + client | Test a specific query string parameter by key. | `url_param[plan] equals "pro"` |
| `cookie` | Cookie | keyed | Client-only | Read any cookie on the visitor's browser by name. Client-only because the server never sees the cookie value. | `cookie[loggedIn] equals "true"` |
| `custom_property` | Custom property | keyed | Server + client | Any property you set via optimo.identify({ ... }). Use for plan, signup date, LTV, internal flags, etc. | `custom_property[plan] equals "enterprise"` |

## Operator reference

Operators available depend on the field's data type. The wizard only offers valid combinations.

| Group | Description | Operators |
| --- | --- | --- |
| Equality | Direct comparisons. Work on string, enum, and keyed fields. | `equals`, `not_equals`, `in`, `not_in` |
| Substring (string fields only) | Match against any part of the string. regex evaluates case-insensitively; you anchor it yourself. | `contains`, `not_contains`, `starts_with`, `regex` |
| Existence | Check whether the field is set on the visitor at all. | `exists`, `not_exists` |
| Numeric & time | Range comparisons. Available on screen_width (numeric) and time_of_day (time). | `greater_than`, `greater_than_or_equal`, `less_than`, `less_than_or_equal`, `between` |

> **Targeting tips:**
>
> - **Start broad, narrow later.** Most tests should target everyone. Add conditions only when you have a specific audience hypothesis ("this CTA works better for mobile") or when running personalization.
> - **Prefer signals you control over geo-IP.** Cookies, UTM tags, and custom properties from `optimo.identify()` are deterministic. Geo-IP is mostly right but can mis-classify VPN, corporate, and mobile-carrier traffic.
> - **Be careful with regex.** Most targeting bugs come from a regex that looked right but matched the wrong audience. Test the pattern in DevTools (`new RegExp(pattern, 'i').test(value)`) before saving.
> - **Finalize targeting before launch.** Editing targeting mid-test doesn't reassign existing visitors, but it does change who enters going forward, which can shift the variant ratio.

## Frequently asked questions

### What happens if I leave targeting empty?

Empty targeting means &ldquo;everyone matching the page URL.&rdquo; The test runs for every visitor who lands on a page that matches the test's URL rule. This is the right default — only narrow the audience when you have a specific reason.

### How do AND vs OR groups work?

Rules live in a tree. The top level is a group with an operator (AND or OR) and a list of children. Each child is either a condition (e.g. country equals US) or another nested group. AND means every child must match. OR means at least one must. You can nest groups arbitrarily deep, which lets you express patterns like &ldquo;(US OR Canada) AND device = mobile.&rdquo;

### Where exactly does Otter evaluate targeting — server or browser?

Both, depending on the field. Country and region resolve from the visitor's IP server-side, then mirror to the SDK so the client agrees. UTM, device, browser, OS, day of week, screen width, and similar fields evaluate on both sides. Cookies evaluate only in the browser because the server never sees cookie contents.

### Will narrowing targeting reset my test?

It doesn't reset assignment, but it does change the eligible audience for new visitors. Visitors already assigned keep their variant (assignment is sticky), but new visitors who no longer match the targeting won't enter the test. This can shift the variant ratio over time, so prefer to finalize targeting before launch.

### How do custom properties get into the targeting context?

Call optimo.identify({ plan: 'pro', signupYear: 2024 }) from your code. Whatever you pass becomes a property on the visitor; targeting can then test custom_property with key=plan, operator=equals, values=['pro']. Identify is also the right hook for user-scoped data like signup date, lifetime value, or feature flags exposed to A/B.

### What's the difference between visitor_type and custom_property?

visitor_type is a derived field with two values — &ldquo;new&rdquo; (no prior visit in our records) and &ldquo;returning&rdquo; (we've seen this visitor before). It's calculated automatically from the visitor UUID cookie. custom_property is whatever you tell us about the visitor via optimo.identify — completely under your control.

### Why are my country/region rules sometimes wrong?

Geo-IP isn't perfect. We use a current commercial database and refresh it weekly, but VPN traffic, corporate proxies, and mobile carrier hopping can produce surprising results. For business-critical targeting, prefer signals you control (cookies, UTM tags, custom properties) over IP-derived ones.

### Can I use regex in targeting?

Yes — string fields support a regex operator that runs case-insensitive matches against the value. The regex evaluates against the field's value directly (not anchored unless you anchor it yourself). Use sparingly: regex is the source of most targeting bugs because typos pass validation but match the wrong audiences in production.

### Do targeting rules apply to preview mode?

No. Preview mode bypasses both targeting and triggers so you can QA any variant on any page the test targets. This is intentional — you couldn't review a personalized variant otherwise. Real visitors still go through full targeting evaluation.

### What's the difference between an A/B/n test's targeting and a Personalization test's targeting?

Mechanically the same — both use the same rule tree and the same fields. The difference is intent. In an A/B/n test, you pick one audience; everyone in that audience is randomly assigned a variant. In a Personalization test, you write one rule set per variant, and each visitor sees the variant whose audience they match (with no randomization). The wizard surfaces the right UI for each.

---

Canonical page: https://www.otterab.com/docs/building-tests/targeting
