diff --git a/posthog/apps.py b/posthog/apps.py index c1ce28915570c..a036a8a7c68c4 100644 --- a/posthog/apps.py +++ b/posthog/apps.py @@ -2,15 +2,15 @@ import posthoganalytics import structlog -from django.apps import AppConfig, apps +from asgiref.sync import async_to_sync +from django.apps import AppConfig from django.conf import settings from posthoganalytics.client import Client from posthoganalytics.exception_capture import Integrations from posthog.git import get_git_branch, get_git_commit_short -from posthog.settings import SELF_CAPTURE, SKIP_ASYNC_MIGRATIONS_SETUP from posthog.tasks.tasks import sync_all_organization_available_product_features -from posthog.utils import get_machine_id, get_self_capture_api_token +from posthog.utils import get_machine_id, initialize_self_capture_api_token logger = structlog.get_logger(__name__) @@ -32,7 +32,13 @@ def ready(self): elif settings.TEST or os.environ.get("OPT_OUT_CAPTURE", False): posthoganalytics.disabled = True elif settings.DEBUG: - User = apps.get_model("posthog", "User") + # In dev, analytics is by default turned to self-capture, i.e. data going into this very instance of PostHog + # Due to ASGI's workings, we can't query for the right project API key in this `ready()` method + # Instead, we configure self-capture with `self_capture_wrapper()` in posthog/asgi.py - see that file + # Self-capture for WSGI is initialized here + posthoganalytics.disabled = True + if settings.SERVER_GATEWAY_INTERFACE == "WSGI": + async_to_sync(initialize_self_capture_api_token)() # log development server launch to posthog if os.getenv("RUN_MAIN") == "true": @@ -48,25 +54,13 @@ def ready(self): {"git_rev": get_git_commit_short(), "git_branch": get_git_branch()}, ) - try: - user = User.objects.filter(last_login__isnull=False).order_by("-last_login").first() - local_api_key = get_self_capture_api_token(user) - except: - local_api_key = None - - if SELF_CAPTURE and local_api_key: - posthoganalytics.api_key = local_api_key - posthoganalytics.host = settings.SITE_URL - else: - posthoganalytics.disabled = True - # load feature flag definitions if not already loaded if not posthoganalytics.disabled and posthoganalytics.feature_flag_definitions() is None: posthoganalytics.load_feature_flags() from posthog.async_migrations.setup import setup_async_migrations - if SKIP_ASYNC_MIGRATIONS_SETUP: + if settings.SKIP_ASYNC_MIGRATIONS_SETUP: logger.warning("Skipping async migrations setup. This is unsafe in production!") else: setup_async_migrations() diff --git a/posthog/asgi.py b/posthog/asgi.py index 3ac5d2ff51524..c6784de57779b 100644 --- a/posthog/asgi.py +++ b/posthog/asgi.py @@ -1,4 +1,6 @@ import os + +from django.conf import settings from django.core.asgi import get_asgi_application from django.http.response import HttpResponse @@ -18,4 +20,22 @@ async def inner(scope, receive, send): return inner -application = lifetime_wrapper(get_asgi_application()) +# PostHogConfig.ready() handles setting the global analytics key in WSGI. The same code couldn't run +# in ASGI because ready() doesn't expose an async interface. +def self_capture_wrapper(func): + if not settings.DEBUG or not settings.SELF_CAPTURE: + return func + + async def inner(scope, receive, send): + if not getattr(inner, "debug_analytics_initialized", False): + from posthog.utils import initialize_self_capture_api_token + + await initialize_self_capture_api_token() + # Set a flag to indicate that the analytics key has been set, so we don't run the code on every request. + inner.debug_analytics_initialized = True # type: ignore + return await func(scope, receive, send) + + return inner + + +application = lifetime_wrapper(self_capture_wrapper(get_asgi_application())) diff --git a/posthog/utils.py b/posthog/utils.py index 49588f36e8ca2..4bc1f77a29c75 100644 --- a/posthog/utils.py +++ b/posthog/utils.py @@ -30,8 +30,10 @@ from celery.schedules import crontab from dateutil import parser from dateutil.relativedelta import relativedelta +from django.apps import apps from django.conf import settings from django.core.cache import cache +from django.db import ProgrammingError from django.db.utils import DatabaseError from django.http import HttpRequest, HttpResponse from django.template.loader import get_template @@ -40,7 +42,6 @@ from rest_framework import serializers from rest_framework.request import Request from sentry_sdk import configure_scope -from posthog.exceptions_capture import capture_exception from posthog.cloud_utils import get_cached_instance_license, is_cloud from posthog.constants import AvailableFeature @@ -48,6 +49,7 @@ RequestParsingError, UnspecifiedCompressionFallbackParsingError, ) +from posthog.exceptions_capture import capture_exception from posthog.git import get_git_branch, get_git_commit_short from posthog.metrics import KLUDGES_COUNTER from posthog.redis import get_client @@ -364,11 +366,9 @@ def render_template( context["js_posthog_ui_host"] = "https://us.posthog.com" elif settings.SELF_CAPTURE: - api_token = get_self_capture_api_token(request.user) - - if api_token: - context["js_posthog_api_key"] = api_token - context["js_posthog_host"] = "" + if posthoganalytics.api_key: + context["js_posthog_api_key"] = posthoganalytics.api_key + context["js_posthog_host"] = "" # Becomes location.origin in the frontend else: context["js_posthog_api_key"] = "sTMFPsFhdP1Ssg" context["js_posthog_host"] = "https://internal-t.posthog.com" @@ -499,22 +499,35 @@ def render_template( return response -def get_self_capture_api_token(user: Optional[Union["AbstractBaseUser", "AnonymousUser"]]) -> Optional[str]: - from posthog.models import Team - - # Get the current user's team (or first team in the instance) to set self capture configs - team: Optional[Team] = None - if user and getattr(user, "team", None): - team = user.team # type: ignore - else: - try: - team = Team.objects.only("api_token").first() - except Exception: - pass +async def initialize_self_capture_api_token(): + """ + Configures `posthoganalytics` for self-capture, in an ASGI-compatible, async way. + """ - if team: - return team.api_token - return None + User = apps.get_model("posthog", "User") + Team = apps.get_model("posthog", "Team") + try: + user = ( + await User.objects.filter(last_login__isnull=False) + .order_by("-last_login") + .select_related("current_team") + .aget() + ) + # Get the current user's team (or first team in the instance) to set self capture configs + team = None + if user and getattr(user, "team", None): + team = user.current_team + else: + team = await Team.objects.only("api_token").aget() + local_api_key = team.api_token + except (User.DoesNotExist, Team.DoesNotExist, ProgrammingError): + local_api_key = None + + # This is running _after_ PostHogConfig.ready(), so we re-enable posthoganalytics while setting the params + if local_api_key is not None: + posthoganalytics.disabled = False + posthoganalytics.api_key = local_api_key + posthoganalytics.host = settings.SITE_URL def get_default_event_name(team: "Team"):