Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,8 @@ See [Client Types](#client-types) section for detailed explanation.

OAuth supports all the same scopes as Personal API Keys. Each scope has a `read` and/or `write` action (e.g., `experiment:read`, `experiment:write`).

Project and environment responses return `live_events_token` only when the authenticated caller can read queries. For scoped credentials, this requires `query:read`, `query:write`, or `*`. A key with only `project:read` receives `null` for this field.

For a complete list of available scopes, see [frontend/src/lib/scopes.tsx](https://github.com/PostHog/posthog/blob/master/frontend/src/lib/scopes.tsx#L15).

### OpenID Connect Scopes
Expand Down
3 changes: 3 additions & 0 deletions posthog/admin/inlines/team_experiments_config_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ def save(self, commit: bool = True) -> TeamExperimentsConfig:
self.instance.experiment_recalculation_times = recalculation_times_from_legacy(
self.instance.experiment_recalculation_time
)
self.instance.experiment_recalculation_time = legacy_from_recalculation_times(
self.instance.experiment_recalculation_times
)
return super().save(commit)


Expand Down
8 changes: 6 additions & 2 deletions posthog/admin/test_team_experiments_config_inline.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

from django.forms import ModelForm, modelform_factory

from parameterized import parameterized

from posthog.admin.inlines.team_experiments_config_inline import TeamExperimentsConfigInlineForm
from posthog.models.team.extensions import get_or_create_team_extension

Expand All @@ -26,7 +28,8 @@ def test_rejects_recalculation_times_the_api_would_reject(self):
self.assertFalse(form.is_valid())
self.assertIn("experiment_recalculation_times", form.errors)

def test_admin_saves_keep_recalculation_fields_in_sync(self):
@parameterized.expand([("08:00:00",), ("08:30:00",), ("08:00:15",)])
def test_admin_saves_keep_recalculation_fields_in_sync(self, legacy_time: str) -> None:
# The hourly workflows read the legacy column; an admin edit that skips the
# sync leaves recalculations running at the old hour.
form = self._form({"experiment_recalculation_times": '["14:00:00", "02:00:00"]'})
Expand All @@ -38,10 +41,11 @@ def test_admin_saves_keep_recalculation_fields_in_sync(self):
# with the list field holding its unchanged value.
form = self._form(
{
"experiment_recalculation_time": "08:00:00",
"experiment_recalculation_time": legacy_time,
"experiment_recalculation_times": '["14:00:00", "02:00:00"]',
}
)
self.assertTrue(form.is_valid(), form.errors)
config = form.save()
self.assertEqual(config.experiment_recalculation_times, ["08:00:00"])
self.assertEqual(config.experiment_recalculation_time, time(hour=8))
2 changes: 1 addition & 1 deletion posthog/api/product_enablement.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
from rest_framework.response import Response

from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.team import TEAM_CONFIG_ADMIN_FIELDS_SET
from posthog.api.team.team_config import TEAM_CONFIG_ADMIN_FIELDS_SET
from posthog.helpers.impersonation import is_impersonated
from posthog.models import OrganizationMembership, User
from posthog.models.activity_logging.activity_log import Detail, dict_changes_between, log_activity
Expand Down
61 changes: 34 additions & 27 deletions posthog/api/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,39 +25,46 @@
from posthog.api.shared import ProjectBackwardCompatBasicSerializer
from posthog.api.tagged_item import TaggedItemSerializerMixin

# These are imported from team.py for now. They are part of the legacy /api/environments/ surface and are
# These are imported from the team API package for now. They are part of the legacy /api/environments/ surface and are
# expected to move project-side (or to a neutral module) in a later PR once /api/environments/ is retired —
# project.py must NOT depend on team.py at that point. The parity *logic* (config writes, retention check,
# project.py must NOT depend on the team API package at that point. The parity *logic* (config writes, retention check,
# and the team-config actions) is defined locally below rather than imported, so it survives that removal.
from posthog.api.team import (
TEAM_CONFIG_FIELD_ACCESS_CONTROLLED_FIELDS,
TEAM_CONFIG_FIELDS,
TEAM_CONFIG_MEMBER_FIELDS_SET,
EvaluationContextSuggestionRequestSerializer,
EvaluationContextSuggestionResponseSerializer,
EventIngestionRestrictionSerializer,
TeamCustomerAnalyticsConfigSerializer,
TeamFeatureFlagPolicyConfigSerializer,
from posthog.api.team.conversations_settings import (
handle_conversations_token_on_update,
report_conversations_settings_changes,
strip_managed_conversations_settings,
)
from posthog.api.team.integration_config import (
TeamLogsConfigSerializer,
TeamMarketingAnalyticsConfigSerializer,
TeamRevenueAnalyticsConfigSerializer,
TeamSerializer,
TeamTracingConfigSerializer,
TeamWorkflowsConfigSerializer,
_default_data_color_theme_id,
_format_serializer_errors,
get_or_mint_live_events_token,
handle_conversations_token_on_update,
handle_experiments_config,
handle_logs_config,
handle_tracing_config,
heatmaps_screenshot_secret_for_reader,
report_conversations_settings_changes,
team_event_ingestion_restrictions_view,
validate_secret_token_generation,
)
from posthog.api.team.live_events import _default_data_color_theme_id, live_events_token_for_request
from posthog.api.team.marketing_config import TeamMarketingAnalyticsConfigSerializer
from posthog.api.team.settings_validation import (
_format_serializer_errors,
heatmaps_screenshot_secret_for_reader,
validate_team_attrs,
validate_team_workflows_config,
)
from posthog.api.team.team_config import (
TEAM_CONFIG_FIELD_ACCESS_CONTROLLED_FIELDS,
TEAM_CONFIG_FIELDS,
TEAM_CONFIG_MEMBER_FIELDS_SET,
TeamCustomerAnalyticsConfigSerializer,
TeamFeatureFlagPolicyConfigSerializer,
TeamRevenueAnalyticsConfigSerializer,
TeamWorkflowsConfigSerializer,
)
from posthog.api.team.team_serializer import (
EvaluationContextSuggestionRequestSerializer,
EvaluationContextSuggestionResponseSerializer,
TeamSerializer,
)
from posthog.api.team.viewsets import EventIngestionRestrictionSerializer, team_event_ingestion_restrictions_view
from posthog.api.utils import validate_authorized_url_wildcards
from posthog.auth import SessionAuthentication
from posthog.cloud_utils import get_cached_instance_license, is_cloud
Expand Down Expand Up @@ -142,7 +149,7 @@

# --- Backward-compatibility logic for the /api/projects/ surface ---
# These mirror the behaviour of the legacy /api/environments/ (TeamViewSet/TeamSerializer) endpoints, operating
# on a project's passthrough Team. They live here — not imported from team.py — so /api/projects/ keeps working
# on a project's passthrough Team. They live here — not imported from the team API package — so /api/projects/ keeps working
# after /api/environments/ is retired. Until then both surfaces intentionally carry equivalent logic; the
# introspection test in test_team_project_parity.py guards against drift.
def capture_team_config_diff(team: Team, key: str, before: dict, after: dict, *, context: dict) -> None:
Expand Down Expand Up @@ -360,7 +367,7 @@
return response.Response({"enabled": config.enabled, "default_groups": config.default_groups})


def team_settings_as_of_view(team: Team, request: request.Request) -> response.Response:

Check warning on line 370 in posthog/api/project.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`team_settings_as_of_view` has cyclomatic complexity 14 (warn >10)

Check warning on line 370 in posthog/api/project.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`team_settings_as_of_view` has cyclomatic complexity 14 (warn >10)
"""
Return the project settings as of the provided timestamp.
Query params:
Expand Down Expand Up @@ -420,7 +427,7 @@
return response.Response(snapshot)


def team_default_evaluation_contexts_view(

Check warning on line 430 in posthog/api/project.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`team_default_evaluation_contexts_view` has cyclomatic complexity 14 (warn >10)

Check warning on line 430 in posthog/api/project.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`team_default_evaluation_contexts_view` has cyclomatic complexity 14 (warn >10)
team: Team, request: request.Request, user_permissions: UserPermissions
) -> response.Response:
"""Manage default evaluation contexts for a project."""
Expand Down Expand Up @@ -534,6 +541,8 @@
context_name = normalize_context_name(context_name)
if not context_name:
return response.Response({"error": "context_name is required"}, status=400)
if len(context_name) > 255:
return response.Response({"error": "context_name must be 255 characters or fewer"}, status=400)

hidden = request.method == "POST"

Expand Down Expand Up @@ -647,6 +656,7 @@
def validate_conversations_settings(self, value: dict | None) -> dict | None:
if value is None:
return value
strip_managed_conversations_settings(value)
# Filter out None values from widget_domains if present
if "widget_domains" in value and value["widget_domains"] is not None:
value["widget_domains"] = [domain for domain in value["widget_domains"] if domain]
Expand Down Expand Up @@ -991,10 +1001,7 @@
return cached_group_types_for_project(project)

def get_live_events_token(self, project: Project) -> Optional[str]:
team = project.passthrough_team
request = self.context.get("request")
user_id = request.user.id if request and hasattr(request, "user") and request.user.is_authenticated else None
return get_or_mint_live_events_token(team, user_id)
return live_events_token_for_request(project.passthrough_team, self.context.get("request"))

@extend_schema_field(serializers.CharField(allow_null=True))
def get_heatmaps_screenshot_secret(self, project: Project) -> Optional[str]:
Expand Down Expand Up @@ -1190,7 +1197,7 @@

return project

def update(self, instance: Project, validated_data: dict[str, Any]) -> Project:

Check warning on line 1200 in posthog/api/project.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

lint:complexity

`update` has cyclomatic complexity 25 (warn >10)

Check warning on line 1200 in posthog/api/project.py

View workflow job for this annotation

GitHub Actions / Python code quality (depot-ubuntu-24.04)

`update` has cyclomatic complexity 25 (warn >10)
# Unlike the other taggable serializers, this update() never delegates to super().update():
# the passthrough loop below setattr()s everything left in validated_data onto the Project
# or its Team. So tags come out here and are written at the end.
Expand Down
4 changes: 2 additions & 2 deletions posthog/api/rest_router.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,6 @@
search,
sharing,
tagged_item,
team,
uploaded_media,
user,
user_facet_settings,
Expand All @@ -87,6 +86,7 @@
from .organization_notification_locks import OrganizationNotificationLockViewSet
from .session import SessionViewSet
from .taxonomic_search_intent import SearchIntentViewSet
from .team import viewsets


@decorators.api_view(["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"])
Expand All @@ -104,7 +104,7 @@ def api_not_found(request):

# Nested endpoints shared
projects_router = routers.add("projects", router.register(r"projects", project.RootProjectViewSet, "projects"))
projects_router.register(r"environments", team.ProjectEnvironmentsViewSet, "project_environments", ["project_id"])
projects_router.register(r"environments", viewsets.ProjectEnvironmentsViewSet, "project_environments", ["project_id"])

projects_router.register(r"sdk_health", SdkHealthViewSet, "project_sdk_health", ["project_id"])
projects_router.register(
Expand Down
Loading
Loading