Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/ingestion/acceptance_tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ requests-toolbelt==1.0.0
boto3==1.29.7
docker==6.1.3
python-multipart==0.0.6
posthog==6.7.6
posthog==7.20.4
4 changes: 2 additions & 2 deletions ee/clickhouse/views/groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from django.utils import timezone

import structlog
import posthoganalytics
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter
from loginas.utils import is_impersonated_session
Expand Down Expand Up @@ -44,6 +43,7 @@
)
from posthog.models.user import User
from posthog.personhog_client.converters import GroupTypeMappingResult
from posthog.ph_client import feature_enabled_or_false
from posthog.rbac.user_access_control import UserAccessControlSerializerMixin
from posthog.utils import str_to_bool

Expand Down Expand Up @@ -871,7 +871,7 @@ def property_values(self, request: request.Request, **kw):
)

def _is_crm_enabled(self, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"crm-iteration-one",
str(user.distinct_id),
groups={"organization": str(self.team.organization.id)},
Expand Down
8 changes: 4 additions & 4 deletions ee/clickhouse/views/test/test_clickhouse_groups.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def test_find_missing_group_key(self):
self.assertEqual(response.status_code, 400)

@freeze_time("2021-05-02")
@patch(f"{PATH}.posthoganalytics.feature_enabled", return_value=False)
@patch(f"{PATH}.feature_enabled_or_false", return_value=False)
def test_retrieve_group_crm_disabled(self, _):
index: GroupTypeIndex = 0
key = "key"
Expand Down Expand Up @@ -207,7 +207,7 @@ def test_retrieve_group_crm_disabled(self, _):
self.assertEqual(0, Notebook.objects.filter(team=self.team).count())

@freeze_time("2021-05-02")
@patch(f"{PATH}.posthoganalytics.feature_enabled", return_value=True)
@patch(f"{PATH}.feature_enabled_or_false", return_value=True)
def test_retrieve_group_crm_enabled(self, _):
index: GroupTypeIndex = 0
key = "key"
Expand Down Expand Up @@ -246,7 +246,7 @@ def test_retrieve_group_crm_enabled(self, _):
self.assertEqual(notebook.content[1]["type"], "text")

@freeze_time("2021-05-02")
@patch(f"{PATH}.posthoganalytics.feature_enabled", return_value=True)
@patch(f"{PATH}.feature_enabled_or_false", return_value=True)
def test_find_with_skip_create_notebook_does_not_create_notebook(self, _):
index: GroupTypeIndex = 0
key = "key"
Expand Down Expand Up @@ -295,7 +295,7 @@ def test_retrieve_group_with_notebook(self):

@freeze_time("2021-05-02")
@patch("products.notebooks.backend.logic.ResourceNotebook.objects.create", side_effect=IntegrityError)
@patch(f"{PATH}.posthoganalytics.feature_enabled", return_value=True)
@patch(f"{PATH}.feature_enabled_or_false", return_value=True)
def test_retrieve_group_notebook_transaction_rollback(self, _, mock_relationship_create):
index: GroupTypeIndex = 0
key = "key"
Expand Down
25 changes: 13 additions & 12 deletions ee/hogai/utils/feature_flags.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import posthoganalytics

from posthog.models import Team, User
from posthog.ph_client import feature_enabled_or_false

from products.business_knowledge.backend.logic import has_feature_flag as bk_has_feature_flag

Expand All @@ -14,7 +15,7 @@ def is_privacy_mode_enabled(team: Team) -> bool:
"""
Check if privacy mode is enabled for a team's organization.
"""
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-privacy-mode",
str(team.organization_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -24,7 +25,7 @@ def is_privacy_mode_enabled(team: Team) -> bool:


def has_phai_tasks_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-tasks",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -34,7 +35,7 @@ def has_phai_tasks_feature_flag(team: Team, user: User) -> bool:


def has_task_tool_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-task-tool",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -44,7 +45,7 @@ def has_task_tool_feature_flag(team: Team, user: User) -> bool:


def has_conversation_topic_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"posthog-ai-web-analytics-nudge",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -54,7 +55,7 @@ def has_conversation_topic_feature_flag(team: Team, user: User) -> bool:


def has_memory_tool_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-memory-tool",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -64,7 +65,7 @@ def has_memory_tool_feature_flag(team: Team, user: User) -> bool:


def has_plan_mode_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-plan-mode",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -74,7 +75,7 @@ def has_plan_mode_feature_flag(team: Team, user: User) -> bool:


def has_experiment_summary_tool_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"experiment-ai-summary",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -84,7 +85,7 @@ def has_experiment_summary_tool_feature_flag(team: Team, user: User) -> bool:


def is_core_memory_disabled(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-core-mem-disabled",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -94,7 +95,7 @@ def is_core_memory_disabled(team: Team, user: User) -> bool:


def has_mcp_servers_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"mcp-servers",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -104,7 +105,7 @@ def has_mcp_servers_feature_flag(team: Team, user: User) -> bool:


def has_sandbox_mode_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"phai-sandbox-mode",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -114,7 +115,7 @@ def has_sandbox_mode_feature_flag(team: Team, user: User) -> bool:


def has_user_interview_mode_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"user-interviews",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand All @@ -124,7 +125,7 @@ def has_user_interview_mode_feature_flag(team: Team, user: User) -> bool:


def has_customer_analytics_mode_feature_flag(team: Team, user: User) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"customer-analytics-csp",
str(user.distinct_id),
groups={"organization": str(team.organization_id)},
Expand Down
4 changes: 2 additions & 2 deletions posthog/api/email_verification.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
from django.contrib.auth.models import AbstractBaseUser
from django.contrib.auth.tokens import PasswordResetTokenGenerator

import posthoganalytics
from rest_framework import exceptions

from posthog.exceptions_capture import capture_exception
from posthog.models.user import User
from posthog.ph_client import feature_enabled_or_false
from posthog.tasks.email import send_email_verification

VERIFICATION_DISABLED_FLAG = "email-verification-disabled"


def is_email_verification_disabled(user: User) -> bool:
# using disabled here so that the default state (if no flag exists) is that verification defaults to ON.
return user.organization is not None and posthoganalytics.feature_enabled(
return user.organization is not None and feature_enabled_or_false(
VERIFICATION_DISABLED_FLAG,
str(user.organization.id),
groups={"organization": str(user.organization.id)},
Expand Down
6 changes: 3 additions & 3 deletions posthog/hogql/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
from django.db.models import Prefetch, Q

import structlog
import posthoganalytics
from opentelemetry import trace
from pydantic import BaseModel, ConfigDict

Expand Down Expand Up @@ -143,6 +142,7 @@
from posthog.models.group_type_mapping import get_group_types_for_project
from posthog.models.organization import OrganizationMembership
from posthog.models.team.team import Team, WeekStartDay
from posthog.ph_client import feature_enabled_or_false
from posthog.rbac.user_access_control import NO_ACCESS_LEVEL, UserAccessControl
from posthog.schema_enums import DatabaseSerializedFieldType, PersonsOnEventsMode, SessionTableVersion
from posthog.synthetic_user import SyntheticUser
Expand Down Expand Up @@ -1089,7 +1089,7 @@ def _fetch_sources(
is_direct_query = connection_id is not None

with timings.measure("feature_flags", emit_span=True):
is_managed_viewset_enabled = posthoganalytics.feature_enabled(
is_managed_viewset_enabled = feature_enabled_or_false(
"managed-viewsets",
str(team.uuid),
groups={
Expand Down Expand Up @@ -1131,7 +1131,7 @@ def _fetch_sources(
team, user, user_access_control
)

is_hogql_warehouse_access_control_enabled = posthoganalytics.feature_enabled(
is_hogql_warehouse_access_control_enabled = feature_enabled_or_false(
"hogql-warehouse-access-control",
str(team.uuid),
groups={"organization": str(team.organization_id), "project": str(team.id)},
Expand Down
16 changes: 16 additions & 0 deletions posthog/hogql_queries/ai/ai_table_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from posthog.clickhouse.query_tagging import Product, tag_queries, tags_context
from posthog.hogql_queries.ai.ai_column_rewriter import rewrite_expr_for_events_table, rewrite_query_for_events_table
from posthog.hogql_queries.ai.ai_property_rewriter import rewrite_expr_for_ai_events_table
from posthog.ph_client import feature_enabled_or_false

AI_EVENTS_QUERY_TOTAL = Counter(
"posthog_ai_events_query_total",
Expand Down Expand Up @@ -38,6 +39,21 @@ class AIEventsUnavailableError(Exception):
and the caller opted out of the events fallback (``fall_back_to_events=False``)."""


def is_ai_events_enabled(team: Team) -> bool:
"""Kill switch for ai_events table reads.

When disabled, all single-trace runners skip the ai_events attempt
and query the events table directly.
"""
return feature_enabled_or_false(
"ai-events-table-rollout",
str(team.id),
groups={"organization": str(team.organization_id)},
group_properties={"organization": {"id": str(team.organization_id)}},
send_feature_flag_events=False,
)


class AIEventsExpiredError(AIEventsUnavailableError):
"""The requested AI events exist in the shared events table but have aged out of
ai_events (past its retention TTL)."""
Expand Down
33 changes: 32 additions & 1 deletion posthog/hogql_queries/ai/test/test_ai_table_resolver.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,38 @@

from posthog.hogql import ast

from posthog.hogql_queries.ai.ai_table_resolver import AIEventsExpiredError, AIEventsNotFoundError, query_ai_events
from posthog.hogql_queries.ai.ai_table_resolver import (
AIEventsExpiredError,
AIEventsNotFoundError,
is_ai_events_enabled,
query_ai_events,
)


class TestIsAiEventsEnabled:
@patch("posthog.hogql_queries.ai.ai_table_resolver.feature_enabled_or_false", return_value=True)
def test_returns_true_when_flag_enabled(self, mock_flag):
team = Mock(id=123, organization_id="org_abc")
assert is_ai_events_enabled(team) is True
mock_flag.assert_called_once_with(
"ai-events-table-rollout",
"123",
groups={"organization": "org_abc"},
group_properties={"organization": {"id": "org_abc"}},
send_feature_flag_events=False,
)

@patch("posthog.hogql_queries.ai.ai_table_resolver.feature_enabled_or_false", return_value=False)
def test_returns_false_when_flag_disabled(self, mock_flag):
team = Mock(id=456, organization_id="org_xyz")
assert is_ai_events_enabled(team) is False
mock_flag.assert_called_once_with(
"ai-events-table-rollout",
"456",
groups={"organization": "org_xyz"},
group_properties={"organization": {"id": "org_xyz"}},
send_feature_flag_events=False,
)


class TestQueryAiEvents:
Expand Down
6 changes: 3 additions & 3 deletions posthog/hogql_queries/hogql_cohort_query.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
from numbers import Number
from typing import Any, Literal, Optional, Union, cast

import posthoganalytics
from rest_framework.exceptions import ValidationError

from posthog.schema import (
Expand Down Expand Up @@ -49,6 +48,7 @@
from posthog.hogql_queries.utils.query_date_range import QueryDateRange
from posthog.models import Filter, Property, Team
from posthog.models.property import OperatorInterval, PropertyGroup
from posthog.ph_client import feature_enabled_or_false
from posthog.types import AnyPropertyFilter

from products.cohorts.backend.models.cohort import Cohort
Expand Down Expand Up @@ -589,7 +589,7 @@ def _get_condition_for_property(self, prop: Property) -> ast.SelectQuery | ast.S
raise ValueError(f"Invalid property type for Cohort queries: {prop.type}")

def _should_combine_person_properties_and(self) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"hogql-cohort-combine-person-properties",
str(self.team.uuid),
groups={
Expand All @@ -609,7 +609,7 @@ def _should_combine_person_properties_and(self) -> bool:
)

def _should_combine_person_properties_or(self) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"hogql-cohort-combine-person-properties-or",
str(self.team.uuid),
groups={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from django.conf import settings

import structlog
import posthoganalytics

from posthog.schema import (
CachedFunnelsQueryResponse,
Expand Down Expand Up @@ -52,6 +51,7 @@
from posthog.models import Team
from posthog.models.filters.mixins.utils import cached_property
from posthog.models.user import User
from posthog.ph_client import feature_enabled_or_false

logger = structlog.get_logger(__name__)

Expand Down Expand Up @@ -494,7 +494,7 @@ def _is_compare_active(self) -> bool:
return self._team_flag_funnels_compare()

def _team_flag_funnels_compare(self) -> bool:
return posthoganalytics.feature_enabled(
return feature_enabled_or_false(
"product-analytics-funnels-compare",
str(self.team.uuid),
groups={
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3091,7 +3091,7 @@ def test_cohort_modifier_with_too_few_cohorts(self, patch_create_default_modifie
),
]
)
@patch("posthog.hogql_queries.insights.trends.trends_query_runner.posthoganalytics.feature_enabled")
@patch("posthog.hogql_queries.insights.trends.trends_query_runner.feature_enabled_or_false")
def test_session_property_pre_aggregation_modifier_gate(
self,
_name: str,
Expand All @@ -3107,7 +3107,7 @@ def test_session_property_pre_aggregation_modifier_gate(
)
assert runner.modifiers.sessionPropertyPreAggregation is expected

@patch("posthog.hogql_queries.insights.trends.trends_query_runner.posthoganalytics.feature_enabled")
@patch("posthog.hogql_queries.insights.trends.trends_query_runner.feature_enabled_or_false")
def test_session_property_pre_aggregation_modifier_clears_on_dashboard_reapply(self, patch_feature_enabled):
# apply_dashboard_filters re-runs __post_init__. The modifier must reflect the *current*
# query state, not the initial one — so a session-breakdown query that gets overridden
Expand Down
Loading
Loading