From 1b2520f38bff65e225806e89b00896e8e17fedf7 Mon Sep 17 00:00:00 2001 From: Phil Haack Date: Wed, 5 Aug 2026 15:21:08 -0700 Subject: [PATCH] Recommend ends with over contains for email domain targeting Document the new starts with / ends with property filter operators and update examples that used contains to match email domains. Contains is imprecise for domains: `email contains @posthog.com` also matches `bad-person@posthog.computer.com`. Generated-By: PostHog Code Task-Id: 1abaac9e-785c-4a7d-9d27-1903d5195e77 --- contents/blog/what-is-a-feature-flag.mdx | 2 +- contents/docs/data/cohorts.mdx | 6 ++++-- contents/docs/data/property-filters.mdx | 4 ++++ contents/docs/feature-flags/canary-release.md | 4 ++-- contents/docs/feature-flags/creating-feature-flags.mdx | 2 ++ contents/docs/feature-flags/troubleshooting.mdx | 2 +- contents/docs/product-analytics/trends/filters.mdx | 8 ++++++-- contents/docs/support/workflows.mdx | 8 ++++---- contents/tutorials/filter-internal-users.mdx | 4 ++-- 9 files changed, 26 insertions(+), 14 deletions(-) diff --git a/contents/blog/what-is-a-feature-flag.mdx b/contents/blog/what-is-a-feature-flag.mdx index 563ce1f2f720..4bd2be016c11 100644 --- a/contents/blog/what-is-a-feature-flag.mdx +++ b/contents/blog/what-is-a-feature-flag.mdx @@ -49,7 +49,7 @@ In PostHog, feature flags support percentage-based rollouts (ship to 5% of users - **Progressive rollouts** – Ship to a small group, monitor for issues, then expand. This is the classic [canary release](/tutorials/canary-release) pattern. - **Kill switches** – Instantly disable a broken feature without rolling back a deployment. No hotfix, no deploy, no waiting. -- **Internal testing** – Release to your team first (set email contains `@yourcompany.com`), then to beta users, then to everyone. +- **Internal testing** – Release to your team first (set email ends with `@yourcompany.com`), then to beta users, then to everyone. - **Operational toggles** – Maintenance mode, circuit breakers, or infrastructure controls that stay permanent. [Feature flags have a lot of uses](/product-engineers/feature-flag-benefits-use-cases), but measuring impact isn't one of them. They let you turn something on and off, but, on their own they won't tell you whether "on" is actually good. That's when you pair them with [analytics](/docs/product-analytics) and [experiments](/docs/experiments). diff --git a/contents/docs/data/cohorts.mdx b/contents/docs/data/cohorts.mdx index 552df0ef034c..b7e638a94d10 100644 --- a/contents/docs/data/cohorts.mdx +++ b/contents/docs/data/cohorts.mdx @@ -76,7 +76,7 @@ You can use cohorts to answer questions like: Cohorts are sometimes confused with [groups](/docs/product-analytics/group-analytics), but they each serve different purposes: -- **Cohorts** represent a specific set of users – e.g., a list of users whose email contains a certain string (like a company's domain). +- **Cohorts** represent a specific set of users – e.g., a list of users whose email ends with a certain string (like a company's domain). - **Groups** aggregate events based on entities, such as organizations or companies, but do not necessarily connect to a user. They enable you to analyze trends, insights, and dashboards at an entity-level (like a company or organization), as opposed to a user-level. @@ -267,7 +267,9 @@ Has or doesn't have a person property with the following operators: | **Operator** | **Example** | | -------------------------------------------- | --------------------------------------------------------------- | | `equals` or `does not equal` | `Pineapple on pizza` property `does not equal` "true" | -| `contains` or `does not contain` | `email` property `does not contain` "gmail", "yahoo", "hotmail" | +| `contains` or `does not contain` | `Job title` property `contains` "engineer" | +| `starts with` or `does not start with` | `$initial_current_url` property `starts with` "https://app." | +| `ends with` or `does not end with` | `email` property `does not end with` "@gmail.com", "@yahoo.com", "@hotmail.com" | | `matches regex` or `does not match regex` | `Country code` property `matches regex` "us\|uk\|aus" | | `greater than` or `greater than or equal to` | `Age` property `greater than` "21" | | `less than` or `less than or equal to` | `Age` property `less than or equal to` "21" | diff --git a/contents/docs/data/property-filters.mdx b/contents/docs/data/property-filters.mdx index 462128d687c4..d016272721ba 100644 --- a/contents/docs/data/property-filters.mdx +++ b/contents/docs/data/property-filters.mdx @@ -18,8 +18,12 @@ String operators work on text-based properties. These are the most common operat |----------|-------------| | `exact` / `is not` | Matches when the property value equals (or does not equal) any value in the list | | `contains` / `does not contain` | Matches when the property value contains (or does not contain) the substring | +| `starts with` / `does not start with` | Matches when the property value starts (or does not start) with the substring | +| `ends with` / `does not end with` | Matches when the property value ends (or does not end) with the substring | | `matches regex` / `does not match regex` | Matches when the property value matches (or does not match) a regular expression | +> **Tip:** To target by email domain, use `ends with` instead of `contains`. For example, `email contains @posthog.com` also matches `bad-person@posthog.computer.com`, while `email ends with @posthog.com` only matches addresses on that exact domain. + String properties also support [semver operators](#semver-operators) for comparing version strings like `1.2.3`. These appear alongside the regular string operators in the filter dropdown. ## Numeric operators diff --git a/contents/docs/feature-flags/canary-release.md b/contents/docs/feature-flags/canary-release.md index 64025aaea241..9ba7a0b39b7a 100644 --- a/contents/docs/feature-flags/canary-release.md +++ b/contents/docs/feature-flags/canary-release.md @@ -38,9 +38,9 @@ Beyond testing the flag yourself, here is a recommended step-by-step release wit 1. **Just yourself:** set email to equal to your own, like `email equals ian@posthog.com`. Test it yourself to make sure the feature flag works and the feature works as expected. -2. **Internal team:** set email to contain your domain, like `email contains posthog.com` or `email equals `. This enables [testing in production](/product-engineers/testing-in-production). Make sure to communicate with your team about what is being released so they know to test and look out for issues. +2. **Internal team:** set email to end with your domain, like `email ends with @posthog.com` or `email equals `. This enables [testing in production](/product-engineers/testing-in-production). Make sure to communicate with your team about what is being released so they know to test and look out for issues. -3. **Beta users, organizations:** [use early access management](/docs/feature-flags/early-access-feature-management), set email to contain a company domain, or set the group name to equal theirs, like `email contains twitter.com` or `organization_name equals twitter`. To ensure you are aware of issues, communicate with them and monitor a related usage dashboard. +3. **Beta users, organizations:** [use early access management](/docs/feature-flags/early-access-feature-management), set email to end with a company domain, or set the group name to equal theirs, like `email ends with @twitter.com` or `organization_name equals twitter`. To ensure you are aware of issues, communicate with them and monitor a related usage dashboard. 4. **Expanded beta:** set release to a percentage of users or to match a popular property. Monitor insights and sessions related to the feature compared to those without. The overall metrics are more important here. diff --git a/contents/docs/feature-flags/creating-feature-flags.mdx b/contents/docs/feature-flags/creating-feature-flags.mdx index 818369786e7e..3a2e319ae1b9 100644 --- a/contents/docs/feature-flags/creating-feature-flags.mdx +++ b/contents/docs/feature-flags/creating-feature-flags.mdx @@ -136,6 +136,8 @@ Rollout percentages support decimal values with up to two decimal places of prec - If the property you want to target lives in another system, like a plan tier in Stripe or a lifecycle stage in Salesforce, sync that table into the [data warehouse](/docs/data-warehouse) and map it onto person or group properties with [warehouse properties](/docs/data-warehouse/warehouse-properties). Flags can then target it like any other property. A [join](/docs/data-warehouse/join) does not work here, because flags evaluate against stored properties rather than at query time. +> **Tip:** To target your team by email domain, use the `ends with` operator (like `email ends with @posthog.com`) rather than `contains`, which also matches lookalike domains like `posthog.computer.com`. See [property filter operators](/docs/data/property-filters#string-operators) for the full list. + When you target a person property of `distinct_id equals`, the value field searches persons by name, email, or distinct_id, so you don't need to paste a raw ID. A person with multiple distinct_ids – for example an anonymous device ID plus an identified email – appears as a separate option per ID, so you can pick the specific one the SDK captures under. The stored filter value is still the literal distinct_id, so flag evaluation is unchanged. greater than | The property is greater than a specific value _(only available for numeric properties)_ | @@ -92,7 +96,7 @@ For a complete list of all available operators, including numeric range operator This is the value that PostHog compares to the property using the operation you specified. If the operation returns true, then the event is included in the insight, and if it returns false it is excluded. -Some operations – equals, doesn't equal, contains, and doesn't contain – allow you to pass multiple values to compare against, in which case the event is included if any of these values return true when compared. +Some operations – equals, doesn't equal, contains, doesn't contain, starts with, doesn't start with, ends with, and doesn't end with – allow you to pass multiple values to compare against, in which case the event is included if any of these values return true when compared. ## Types of filters @@ -172,4 +176,4 @@ This option finds a user's very first occurrence of an event type, and then chec This option finds the first time a user performed an event type _that also matches your filters_. Previous events of the same type from that user that don't match the filters are ignored. -**Example:** You're filtering for pageview events to `/about`. A user first visited `/pricing` and then later visited `/about`. PostHog ignores the `/pricing` view and counts the `/about` view, as it's the first one that matches the filter. +**Example:** You're filtering for pageview events to `/about`. A user first visited `/pricing` and then later visited `/about`. PostHog ignores the `/pricing` view and counts the `/about` view, as it's the first one that matches the filter. \ No newline at end of file diff --git a/contents/docs/support/workflows.mdx b/contents/docs/support/workflows.mdx index 18206acf0c0f..25793b823d80 100644 --- a/contents/docs/support/workflows.mdx +++ b/contents/docs/support/workflows.mdx @@ -167,7 +167,7 @@ Use `if/else` blocks in your workflow to branch logic. event.properties.channel_source = "slack" event.properties.status = "new" event.properties.priority = "high" -event.properties.customer_email contains "@enterprise.com" +event.properties.customer_email ends with "@enterprise.com" event.properties.new_status = "resolved" event.properties.message_content contains "urgent" ``` @@ -290,14 +290,14 @@ Tag tickets from specific customer domains for prioritization. **Flow:** -1. **If** `event.properties.customer_email` contains `@bigcorp.com` +1. **If** `event.properties.customer_email` ends with `@bigcorp.com` - **Update ticket:** ``` tags = ["enterprise", "priority"] ``` -2. **Else if** `event.properties.customer_email` contains `@startup.io` +2. **Else if** `event.properties.customer_email` ends with `@startup.io` - **Update ticket:** ``` @@ -355,7 +355,7 @@ sla_unit = "hour" assignee = Engineering role ``` -3. **Else if** `event.properties.customer_email` contains `@enterprise.com` +3. **Else if** `event.properties.customer_email` ends with `@enterprise.com` - **Update ticket:** ``` diff --git a/contents/tutorials/filter-internal-users.mdx b/contents/tutorials/filter-internal-users.mdx index 10ac1d2dde81..ef3acb11fd90 100644 --- a/contents/tutorials/filter-internal-users.mdx +++ b/contents/tutorials/filter-internal-users.mdx @@ -48,7 +48,7 @@ Any internal users you identify for a project will apply for _all_ users on that ## Step 2: Add an internal user filter -> **Tip:** Inline person property filters (e.g., "email does not contain your-domain.com") work everywhere, including real-time [CDP](/docs/cdp) destinations. Alternatively, you can [create a cohort](/docs/data/cohorts#method-2-create-a-new-cohort-from-the-cohorts-page) and add it with a "not in" operator – this works well for analytics queries (insights, dashboards) and also works in CDP destinations if the cohort contains **exclusively person property filters**. Cohorts with behavioral filters or no properties defined will cause CDP destinations to error. +> **Tip:** Inline person property filters (e.g., "email does not end with @your-domain.com") work everywhere, including real-time [CDP](/docs/cdp) destinations. Alternatively, you can [create a cohort](/docs/data/cohorts#method-2-create-a-new-cohort-from-the-cohorts-page) and add it with a "not in" operator – this works well for analytics queries (insights, dashboards) and also works in CDP destinations if the cohort contains **exclusively person property filters**. Cohorts with behavioral filters or no properties defined will cause CDP destinations to error. In project settings, scroll down to the ‘_Filter out internal and test users_’ section and click ‘_+ Add filter_’ to begin. @@ -56,7 +56,7 @@ Adding a filter here works exactly the same as it does when you add a filter to One of the easiest filters to create is to filter out internal users identified by their email address. We do this at PostHog, using the following filter to create insights based only on users who do not have a PostHog email address: -> email ∌ (doesn’t contain) @posthog.com +> email ⇻ (doesn’t end with) @posthog.com