diff --git a/posthog/api/decide.py b/posthog/api/decide.py index 7357a87133425..06ef04aed4594 100644 --- a/posthog/api/decide.py +++ b/posthog/api/decide.py @@ -31,7 +31,7 @@ from posthog.models.feature_flag.flag_analytics import increment_request_count from posthog.models.filters.mixins.utils import process_bool from posthog.models.utils import execute_with_timeout -from posthog.plugins.site import get_decide_site_apps, get_decide_site_functions +from posthog.plugins.site import get_decide_site_apps from posthog.utils import ( get_ip_address, label_for_team_id_to_track, @@ -297,8 +297,6 @@ def get_decide(request: HttpRequest): try: with execute_with_timeout(200, DATABASE_FOR_FLAG_MATCHING): site_apps = get_decide_site_apps(team, using_database=DATABASE_FOR_FLAG_MATCHING) - with execute_with_timeout(200, DATABASE_FOR_FLAG_MATCHING): - site_apps += get_decide_site_functions(team, using_database=DATABASE_FOR_FLAG_MATCHING) except Exception: pass diff --git a/posthog/api/hog_function.py b/posthog/api/hog_function.py index 339400a45aa78..a382ebda866e0 100644 --- a/posthog/api/hog_function.py +++ b/posthog/api/hog_function.py @@ -189,15 +189,18 @@ def validate(self, attrs): if "hog" in attrs: if attrs["type"] in TYPES_WITH_JAVASCRIPT_SOURCE: - # Upon creation, this code will be run before the model has an "id". - # If that's the case, the code just makes sure transpilation doesn't throw. We'll re-transpile after creation. - id = str(instance.id) if instance else "__" try: + # Validate transpilation using the model instance attrs["transpiled"] = get_transpiled_function( - id, attrs["hog"], attrs["filters"], attrs["inputs"], team + HogFunction( + team=team, + hog=attrs["hog"], + filters=attrs["filters"], + inputs=attrs["inputs"], + ) ) except TranspilerError: - raise serializers.ValidationError({"hog": f"Error in TypeScript code"}) + raise serializers.ValidationError({"hog": "Error in TypeScript code"}) attrs["bytecode"] = None else: attrs["bytecode"] = compile_hog(attrs["hog"]) @@ -231,11 +234,7 @@ def create(self, validated_data: dict, *args, **kwargs) -> HogFunction: request = self.context["request"] validated_data["created_by"] = request.user hog_function = super().create(validated_data=validated_data) - if validated_data.get("type") in TYPES_WITH_JAVASCRIPT_SOURCE: - # Re-run the transpilation now that we have an ID - hog_function.transpiled = get_transpiled_function( - str(hog_function.id), hog_function.hog, hog_function.filters, hog_function.inputs, hog_function.team - ) + return hog_function def update(self, instance: HogFunction, validated_data: dict, *args, **kwargs) -> HogFunction: diff --git a/posthog/api/site_app.py b/posthog/api/site_app.py index 8c49bfdf0eab8..ee43c9b14f61b 100644 --- a/posthog/api/site_app.py +++ b/posthog/api/site_app.py @@ -8,7 +8,6 @@ from posthog.exceptions import generate_exception_response from posthog.logging.timing import timed from posthog.plugins.site import get_transpiled_site_source, get_site_config_from_schema -from posthog.models.hog_functions.hog_function import HogFunction @csrf_exempt @@ -36,34 +35,3 @@ def get_site_app(request: HttpRequest, id: int, token: str, hash: str) -> HttpRe type="server_error", status_code=status.HTTP_404_NOT_FOUND, ) - - -@csrf_exempt -@timed("posthog_cloud_site_app_endpoint") -def get_site_function(request: HttpRequest, id: str, hash: str) -> HttpResponse: - try: - # TODO: Should we add a token as well? Is the UUID enough? - function = ( - HogFunction.objects.filter( - id=id, enabled=True, type__in=("site_destination", "site_app"), transpiled__isnull=False - ) - .values_list("transpiled") - .first() - ) - if not function: - raise Exception("No function found") - - response = HttpResponse(content=function[0], content_type="application/javascript") - response["Cache-Control"] = "public, max-age=31536000" # Cache for 1 year - statsd.incr(f"posthog_cloud_raw_endpoint_success", tags={"endpoint": "site_function"}) - return response - except Exception as e: - capture_exception(e, {"data": {"id": id}}) - statsd.incr("posthog_cloud_raw_endpoint_failure", tags={"endpoint": "site_function"}) - return generate_exception_response( - "site_function", - "Unable to serve site function source code.", - code="missing_site_function_source", - type="server_error", - status_code=status.HTTP_404_NOT_FOUND, - ) diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 56f6257a978d2..e8d2bc640db4f 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -2245,6 +2245,97 @@ ''' # --- # name: TestDecide.test_web_app_queries.10 + ''' + SELECT "posthog_remoteconfig"."id", + "posthog_remoteconfig"."team_id", + "posthog_remoteconfig"."config", + "posthog_remoteconfig"."updated_at", + "posthog_remoteconfig"."synced_at" + FROM "posthog_remoteconfig" + WHERE "posthog_remoteconfig"."team_id" = 99999 + LIMIT 21 + ''' +# --- +# name: TestDecide.test_web_app_queries.11 + ''' + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_web_vitals_opt_in", + "posthog_team"."autocapture_web_vitals_allowed_metrics", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."person_processing_opt_out", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_recording_url_trigger_config", + "posthog_team"."session_recording_url_blocklist_config", + "posthog_team"."session_recording_event_trigger_config", + "posthog_team"."session_replay_config", + "posthog_team"."survey_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."capture_dead_clicks", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 99999 + LIMIT 21 + ''' +# --- +# name: TestDecide.test_web_app_queries.12 + ''' + SELECT COUNT(*) AS "__count" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."active" + AND NOT "posthog_featureflag"."deleted" + AND "posthog_featureflag"."team_id" = 99999) + ''' +# --- +# name: TestDecide.test_web_app_queries.13 ''' SELECT "posthog_pluginconfig"."id", "posthog_pluginconfig"."web_token", @@ -2260,17 +2351,36 @@ AND "posthog_pluginconfig"."team_id" = 99999) ''' # --- -# name: TestDecide.test_web_app_queries.11 +# name: TestDecide.test_web_app_queries.14 ''' - SELECT "posthog_hogfunction"."id", - "posthog_hogfunction"."updated_at", - "posthog_hogfunction"."type" - FROM "posthog_hogfunction" - WHERE ("posthog_hogfunction"."enabled" - AND "posthog_hogfunction"."team_id" = 99999 - AND "posthog_hogfunction"."transpiled" IS NOT NULL - AND "posthog_hogfunction"."type" IN ('site_destination', - 'site_app')) + SELECT "posthog_pluginconfig"."id", + "posthog_pluginconfig"."web_token", + "posthog_pluginsourcefile"."updated_at", + "posthog_plugin"."updated_at", + "posthog_pluginconfig"."updated_at" + FROM "posthog_pluginconfig" + INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") + INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") + WHERE ("posthog_pluginconfig"."enabled" + AND "posthog_pluginsourcefile"."filename" = 'site.ts' + AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' + AND "posthog_pluginconfig"."team_id" = 99999) + ''' +# --- +# name: TestDecide.test_web_app_queries.15 + ''' + SELECT "posthog_pluginconfig"."id", + "posthog_pluginconfig"."web_token", + "posthog_pluginsourcefile"."updated_at", + "posthog_plugin"."updated_at", + "posthog_pluginconfig"."updated_at" + FROM "posthog_pluginconfig" + INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") + INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") + WHERE ("posthog_pluginconfig"."enabled" + AND "posthog_pluginsourcefile"."filename" = 'site.ts' + AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' + AND "posthog_pluginconfig"."team_id" = 99999) ''' # --- # name: TestDecide.test_web_app_queries.2 @@ -2552,17 +2662,64 @@ # --- # name: TestDecide.test_web_app_queries.9 ''' - SELECT "posthog_pluginconfig"."id", - "posthog_pluginconfig"."web_token", - "posthog_pluginsourcefile"."updated_at", - "posthog_plugin"."updated_at", - "posthog_pluginconfig"."updated_at" - FROM "posthog_pluginconfig" - INNER JOIN "posthog_plugin" ON ("posthog_pluginconfig"."plugin_id" = "posthog_plugin"."id") - INNER JOIN "posthog_pluginsourcefile" ON ("posthog_plugin"."id" = "posthog_pluginsourcefile"."plugin_id") - WHERE ("posthog_pluginconfig"."enabled" - AND "posthog_pluginsourcefile"."filename" = 'site.ts' - AND "posthog_pluginsourcefile"."status" = 'TRANSPILED' - AND "posthog_pluginconfig"."team_id" = 99999) + SELECT "posthog_team"."id", + "posthog_team"."uuid", + "posthog_team"."organization_id", + "posthog_team"."project_id", + "posthog_team"."api_token", + "posthog_team"."app_urls", + "posthog_team"."name", + "posthog_team"."slack_incoming_webhook", + "posthog_team"."created_at", + "posthog_team"."updated_at", + "posthog_team"."anonymize_ips", + "posthog_team"."completed_snippet_onboarding", + "posthog_team"."has_completed_onboarding_for", + "posthog_team"."ingested_event", + "posthog_team"."autocapture_opt_out", + "posthog_team"."autocapture_web_vitals_opt_in", + "posthog_team"."autocapture_web_vitals_allowed_metrics", + "posthog_team"."autocapture_exceptions_opt_in", + "posthog_team"."autocapture_exceptions_errors_to_ignore", + "posthog_team"."person_processing_opt_out", + "posthog_team"."session_recording_opt_in", + "posthog_team"."session_recording_sample_rate", + "posthog_team"."session_recording_minimum_duration_milliseconds", + "posthog_team"."session_recording_linked_flag", + "posthog_team"."session_recording_network_payload_capture_config", + "posthog_team"."session_recording_url_trigger_config", + "posthog_team"."session_recording_url_blocklist_config", + "posthog_team"."session_recording_event_trigger_config", + "posthog_team"."session_replay_config", + "posthog_team"."survey_config", + "posthog_team"."capture_console_log_opt_in", + "posthog_team"."capture_performance_opt_in", + "posthog_team"."capture_dead_clicks", + "posthog_team"."surveys_opt_in", + "posthog_team"."heatmaps_opt_in", + "posthog_team"."session_recording_version", + "posthog_team"."signup_token", + "posthog_team"."is_demo", + "posthog_team"."access_control", + "posthog_team"."week_start_day", + "posthog_team"."inject_web_apps", + "posthog_team"."test_account_filters", + "posthog_team"."test_account_filters_default_checked", + "posthog_team"."path_cleaning_filters", + "posthog_team"."timezone", + "posthog_team"."data_attributes", + "posthog_team"."person_display_name_properties", + "posthog_team"."live_events_columns", + "posthog_team"."recording_domains", + "posthog_team"."primary_dashboard_id", + "posthog_team"."extra_settings", + "posthog_team"."modifiers", + "posthog_team"."correlation_config", + "posthog_team"."session_recording_retention_period_days", + "posthog_team"."external_data_workspace_id", + "posthog_team"."external_data_workspace_last_synced_at" + FROM "posthog_team" + WHERE "posthog_team"."id" = 99999 + LIMIT 21 ''' # --- diff --git a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr index 2734c9be79617..4be955c775a21 100644 --- a/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr +++ b/posthog/api/test/__snapshots__/test_organization_feature_flag.ambr @@ -136,13 +136,6 @@ "posthog_team"."modifiers", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" @@ -151,6 +144,18 @@ ''' # --- # name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.12 + ''' + SELECT "posthog_remoteconfig"."id", + "posthog_remoteconfig"."team_id", + "posthog_remoteconfig"."config", + "posthog_remoteconfig"."updated_at", + "posthog_remoteconfig"."synced_at" + FROM "posthog_remoteconfig" + WHERE "posthog_remoteconfig"."team_id" = 99999 + LIMIT 21 + ''' +# --- +# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.13 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -206,6 +211,13 @@ "posthog_team"."modifiers", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", + "posthog_team"."plugins_opt_in", + "posthog_team"."opt_out_capture", + "posthog_team"."event_names", + "posthog_team"."event_names_with_usage", + "posthog_team"."event_properties", + "posthog_team"."event_properties_with_usage", + "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" @@ -213,19 +225,16 @@ LIMIT 21 ''' # --- -# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.13 +# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.14 ''' - SELECT "posthog_remoteconfig"."id", - "posthog_remoteconfig"."team_id", - "posthog_remoteconfig"."config", - "posthog_remoteconfig"."updated_at", - "posthog_remoteconfig"."synced_at" - FROM "posthog_remoteconfig" - WHERE "posthog_remoteconfig"."team_id" = 99999 - LIMIT 21 + SELECT COUNT(*) AS "__count" + FROM "posthog_featureflag" + WHERE ("posthog_featureflag"."active" + AND NOT "posthog_featureflag"."deleted" + AND "posthog_featureflag"."team_id" = 99999) ''' # --- -# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.14 +# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.15 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -295,15 +304,6 @@ LIMIT 21 ''' # --- -# name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.15 - ''' - SELECT COUNT(*) AS "__count" - FROM "posthog_featureflag" - WHERE ("posthog_featureflag"."active" - AND NOT "posthog_featureflag"."deleted" - AND "posthog_featureflag"."team_id" = 99999) - ''' -# --- # name: TestOrganizationFeatureFlagCopy.test_copy_feature_flag_create_new.16 ''' SELECT "posthog_dashboardtile"."id" diff --git a/posthog/api/test/test_decide.py b/posthog/api/test/test_decide.py index 2d675900ab837..f6175ba7408b8 100644 --- a/posthog/api/test/test_decide.py +++ b/posthog/api/test/test_decide.py @@ -38,7 +38,6 @@ from posthog.models.cohort.cohort import Cohort from posthog.models.feature_flag.feature_flag import FeatureFlagHashKeyOverride from posthog.models.group.group import Group -from posthog.models.hog_functions.hog_function import HogFunction from posthog.models.organization import Organization, OrganizationMembership from posthog.models.person import PersonDistinctId from posthog.models.personal_api_key import hash_key_value @@ -667,7 +666,7 @@ def test_web_app_queries(self, *args): # caching flag definitions in the above mean fewer queries # 3 of these queries are just for setting transaction scope - with self.assertNumQueries(8): + with self.assertNumQueries(4): response = self._post_decide() self.assertEqual(response.status_code, status.HTTP_200_OK) injected = response.json()["siteApps"] @@ -692,52 +691,13 @@ def test_site_app_injection(self, *args): ) self.team.refresh_from_db() self.assertTrue(self.team.inject_web_apps) - with self.assertNumQueries(9): + with self.assertNumQueries(5): response = self._post_decide() self.assertEqual(response.status_code, status.HTTP_200_OK) injected = response.json()["siteApps"] self.assertEqual(len(injected), 1) self.assertTrue(injected[0]["url"].startswith(f"/site_app/{plugin_config.id}/{plugin_config.web_token}/")) - def test_site_function_injection(self, *args): - # yype: site_app - site_app = HogFunction.objects.create( - team=self.team, - name="my_function", - hog="function onLoad(){}", - type="site_app", - transpiled="function onLoad(){}", - enabled=True, - ) - - self.team.refresh_from_db() - self.assertTrue(self.team.inject_web_apps) - with self.assertNumQueries(9): - response = self._post_decide() - self.assertEqual(response.status_code, status.HTTP_200_OK) - injected = response.json()["siteApps"] - self.assertEqual(len(injected), 1) - self.assertTrue(injected[0]["url"].startswith(f"/site_function/{site_app.id}/")) - - # yype: site_destination - site_destination = HogFunction.objects.create( - team=self.team, - name="my_function", - hog="function onLoad(){}", - type="site_destination", - transpiled="function onLoad(){}", - enabled=True, - ) - - self.team.refresh_from_db() - self.assertTrue(self.team.inject_web_apps) - with self.assertNumQueries(8): - response = self._post_decide() - self.assertEqual(response.status_code, status.HTTP_200_OK) - injected = response.json()["siteApps"] - self.assertEqual(len(injected), 2) - self.assertTrue(injected[1]["url"].startswith(f"/site_function/{site_destination.id}/")) - def test_feature_flags(self, *args): self.team.app_urls = ["https://example.com"] self.team.save() @@ -4733,7 +4693,7 @@ def test_site_apps_in_decide_use_replica(self, mock_is_connected): # update caches self._post_decide(api_version=3) - with self.assertNumQueries(8, using="replica"), self.assertNumQueries(0, using="default"): + with self.assertNumQueries(4, using="replica"), self.assertNumQueries(0, using="default"): response = self._post_decide(api_version=3) self.assertEqual(response.status_code, status.HTTP_200_OK) injected = response.json()["siteApps"] diff --git a/posthog/api/test/test_site_app.py b/posthog/api/test/test_site_app.py index 23408defe752f..92340e67144bd 100644 --- a/posthog/api/test/test_site_app.py +++ b/posthog/api/test/test_site_app.py @@ -2,7 +2,7 @@ from rest_framework import status from posthog.api.site_app import get_site_config_from_schema -from posthog.models import Plugin, PluginConfig, PluginSourceFile, HogFunction +from posthog.models import Plugin, PluginConfig, PluginSourceFile from posthog.test.base import BaseTest @@ -82,32 +82,3 @@ def test_get_site_config_from_schema(self): config = {"in_site": "123", "not_in_site": "12345"} self.assertEqual(get_site_config_from_schema(schema, config), {"in_site": "123"}) self.assertEqual(get_site_config_from_schema(None, None), {}) - - def test_site_function(self): - # Create a HogFunction object - hog_function = HogFunction.objects.create( - enabled=True, - team=self.team, - type="site_app", - transpiled="function test() {}", - ) - - response = self.client.get( - f"/site_function/{hog_function.id}/somehash/", - HTTP_ORIGIN="http://127.0.0.1:8000", - ) - - self.assertEqual(response.status_code, status.HTTP_200_OK) - self.assertEqual(response.content.decode("utf-8"), hog_function.transpiled) - self.assertEqual(response["Cache-Control"], "public, max-age=31536000") - - def test_site_function_not_found(self): - response = self.client.get( - f"/site_function/non-existent-id/somehash/", - HTTP_ORIGIN="http://127.0.0.1:8000", - ) - - self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) - response_json = response.json() - self.assertEqual(response_json["code"], "missing_site_function_source") - self.assertEqual(response_json["detail"], "Unable to serve site function source code.") diff --git a/posthog/cdp/site_functions.py b/posthog/cdp/site_functions.py index 6f4f7a3d5ffa5..3896f4f73515e 100644 --- a/posthog/cdp/site_functions.py +++ b/posthog/cdp/site_functions.py @@ -3,18 +3,13 @@ from posthog.cdp.filters import hog_function_filters_to_expr from posthog.cdp.validation import transpile_template_code from posthog.hogql.compiler.javascript import JavaScriptCompiler +from posthog.models.hog_functions.hog_function import HogFunction from posthog.models.plugin import transpile -from posthog.models.team.team import Team -def get_transpiled_function(id: str, source: str, filters: dict, inputs: dict, team: Team) -> str: +def get_transpiled_function(hog_function: HogFunction) -> str: # Wrap in IIFE = Immediately Invoked Function Expression = to avoid polluting global scope - response = "(function() {\n\n" - - # PostHog-JS adds itself to the window object for us to use - response += f"const posthog = window['__$$ph_site_app_{id}_posthog'] || window['__$$ph_site_app_{id}'] || window['posthog'];\n" - response += f"const missedInvocations = window['__$$ph_site_app_{id}_missed_invocations'] || (() => []);\n" - response += f"const callback = window['__$$ph_site_app_{id}_callback'] || (() => {'{}'});\n" + response = "(function() {\n" # Build the inputs in three parts: # 1) a simple object with constants/scalars @@ -27,7 +22,7 @@ def get_transpiled_function(id: str, source: str, filters: dict, inputs: dict, t compiler = JavaScriptCompiler() # TODO: reorder inputs to make dependencies work - for key, input in inputs.items(): + for key, input in (hog_function.inputs or {}).items(): value = input.get("value") key_string = json.dumps(str(key) or "") if (isinstance(value, str) and "{" in value) or isinstance(value, dict) or isinstance(value, list): @@ -38,9 +33,8 @@ def get_transpiled_function(id: str, source: str, filters: dict, inputs: dict, t inputs_object.append(f"{key_string}: {json.dumps(value)}") # Convert the filters to code - filters_expr = hog_function_filters_to_expr(filters, team, {}) + filters_expr = hog_function_filters_to_expr(hog_function.filters or {}, hog_function.team, {}) filters_code = compiler.visit(filters_expr) - # Start with the STL functions response += compiler.get_stl_code() + "\n" @@ -60,43 +54,47 @@ def get_transpiled_function(id: str, source: str, filters: dict, inputs: dict, t response += "default: return null; }\n" response += "} catch (e) { if(!initial) {console.error('[POSTHOG-JS] Unable to compute value for inputs', key, e);} return null } }\n" response += "\n".join(inputs_append) + "\n" + response += "return inputs;}\n" - # See plugin-transpiler/src/presets.ts - # transpile(source, 'site') == `(function () {let exports={};${code};return exports;})` - response += f"const response = {transpile(source, 'site')}();" + response += f"const source = {transpile(hog_function.hog, 'site')}();" + # We are exposing an init function which is what the client will use to actually run this setup code. + # The return includes any extra methods that the client might need to use - so far just processEvent response += ( """ - function processEvent(globals) { - if (!('onEvent' in response)) { return; }; - const inputs = buildInputs(globals); - const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; - let __getGlobal = (key) => filterGlobals[key]; - const filterMatches = """ + let processEvent = undefined; + if ('onEvent' in source) { + processEvent = function processEvent(globals) { + if (!('onEvent' in source)) { return; }; + const inputs = buildInputs(globals); + const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; + let __getGlobal = (key) => filterGlobals[key]; + const filterMatches = """ + filters_code + """; - if (filterMatches) { response.onEvent({ ...globals, inputs, posthog }); } + if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + } } - if ('onLoad' in response) { - const r = response.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); - const done = (success = true) => { - if (success) { - missedInvocations().forEach(processEvent); - posthog.on('eventCaptured', (event) => { processEvent(posthog.siteApps.globalsForEvent(event)) }); - } else { - console.error('[POSTHOG-JS] Site function failed to load', response) - } - callback(success); - }; - if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => done(false)).then(() => done(true)) } else { done(true) } - } else if ('onEvent' in response) { - missedInvocations().forEach(processEvent); - posthog.on('eventCaptured', (event) => { processEvent(posthog.siteApps.globalsForEvent(event)) }) + + function init(config) { + const posthog = config.posthog; + const callback = config.callback; + if ('onLoad' in source) { + const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } + } else { + callback(true); + } + + return { + processEvent: processEvent + } } - """ + + return { init: init };""" ) - response += "\n})();" + response += "\n})" return response diff --git a/posthog/cdp/test/test_site_functions.py b/posthog/cdp/test/test_site_functions.py index 9852201353cdc..44f0e11f373ae 100644 --- a/posthog/cdp/test/test_site_functions.py +++ b/posthog/cdp/test/test_site_functions.py @@ -1,6 +1,11 @@ +import subprocess +import tempfile +from inline_snapshot import snapshot +import pytest from django.test import TestCase from posthog.cdp.site_functions import get_transpiled_function from posthog.models.action.action import Action +from posthog.models.hog_functions.hog_function import HogFunction from posthog.models.organization import Organization from posthog.models.project import Project from posthog.models.plugin import TranspilerError @@ -19,93 +24,132 @@ def setUp(self): name="Test project", ) + self.hog_function = HogFunction( + id="123", + team=self.team, + name="Test Hog Function", + hog='export function onLoad() { console.log("Hello, World!"); }', + filters={}, + inputs={}, + ) + + def compile_and_run(self): + result = get_transpiled_function(self.hog_function) + with tempfile.NamedTemporaryFile(delete=False) as f: + f.write(result.encode("utf-8")) + f.flush() + # NOTE: Nodejs isn't the right environment as it is really for the browser but we are only checking the output is valid, not that it actually runs + # More of a sanity check that our templating isn't broken + output = subprocess.check_output(["node", f.name]) + assert output == b"" + + return result + def test_get_transpiled_function_basic(self): - id = "123" - source = 'export function onLoad() { console.log("Hello, World!"); }' - filters: dict = {} - inputs: dict = {} - team = self.team + result = self.compile_and_run() + assert isinstance(result, str) + assert 'console.log("Hello, World!")' in result + + # NOTE: We do one inlne snapshot here so we can have an easy glance at what it generally looks like - all other tests we should just check specific parts + assert result == snapshot( + """\ +(function() { + +function buildInputs(globals, initial) { +let inputs = { +}; +let __getGlobal = (key) => key === 'inputs' ? inputs : globals[key]; +return inputs;} +const source = (function () {let exports={};"use strict"; + +Object.defineProperty(exports, "__esModule", { + value: true +}); +exports.onLoad = onLoad; +function onLoad() { + console.log("Hello, World!"); +};return exports;})(); + let processEvent = undefined; + if ('onEvent' in source) { + processEvent = function processEvent(globals) { + if (!('onEvent' in source)) { return; }; + const inputs = buildInputs(globals); + const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; + let __getGlobal = (key) => filterGlobals[key]; + const filterMatches = true; + if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + } + } + + function init(config) { + const posthog = config.posthog; + const callback = config.callback; + if ('onLoad' in source) { + const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } + } else { + callback(true); + } - result = get_transpiled_function(id, source, filters, inputs, team) + return { + processEvent: processEvent + } + } - self.assertIsInstance(result, str) - self.assertIn('console.log("Hello, World!")', result) - self.assertIn(f"window['__$$ph_site_app_{id}_posthog']", result) + return { init: init }; +})\ +""" + ) def test_get_transpiled_function_with_static_input(self): - id = "123" - source = "export function onLoad() { console.log(inputs.message); }" - filters: dict = {} - inputs = {"message": {"value": "Hello, Inputs!"}} - team = self.team + self.hog_function.hog = "export function onLoad() { console.log(inputs.message); }" + self.hog_function.inputs = {"message": {"value": "Hello, Inputs!"}} - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.message);", result) - self.assertIn("inputs = {", result) - self.assertIn('"message": "Hello, Inputs!"', result) + assert "console.log(inputs.message);" in result + assert "inputs = {" in result + assert '"message": "Hello, Inputs!"' in result def test_get_transpiled_function_with_template_input(self): - id = "123" - source = "export function onLoad() { console.log(inputs.greeting); }" - filters: dict = {} - inputs = {"greeting": {"value": "Hello, {person.properties.name}!"}} - team = self.team - - result = get_transpiled_function(id, source, filters, inputs, team) - - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.greeting);", result) - # Check that the input processing code is included - self.assertIn("function getInputsKey", result) - self.assertIn('inputs["greeting"] = getInputsKey("greeting");', result) - self.assertIn('case "greeting": return ', result) - self.assertIn('__getGlobal("person")', result) + self.hog_function.hog = "export function onLoad() { console.log(inputs.greeting); }" + self.hog_function.inputs = {"greeting": {"value": "Hello, {person.properties.name}!"}} + result = self.compile_and_run() + + assert "console.log(inputs.greeting);" in result + assert "function getInputsKey" in result + assert 'inputs["greeting"] = getInputsKey("greeting");' in result + assert 'case "greeting": return ' in result + assert '__getGlobal("person")' in result def test_get_transpiled_function_with_filters(self): - id = "123" - source = "export function onEvent(event) { console.log(event.event); }" - filters: dict = {"events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}]} - inputs: dict = {} - team = self.team + self.hog_function.hog = "export function onEvent(event) { console.log(event.event); }" + self.hog_function.filters = {"events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}]} - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(event.event);", result) - self.assertIn("const filterMatches = ", result) - self.assertIn('__getGlobal("event") == "$pageview"', result) - self.assertIn("if (filterMatches) { response.onEvent({", result) + assert "console.log(event.event);" in result + assert "const filterMatches = " in result + assert '__getGlobal("event") == "$pageview"' in result + assert "if (filterMatches) { source.onEvent({" in result def test_get_transpiled_function_with_invalid_template_input(self): - id = "123" - source = "export function onLoad() { console.log(inputs.greeting); }" - filters: dict = {} - inputs = {"greeting": {"value": "Hello, {person.properties.nonexistent_property}!"}} - team = self.team + self.hog_function.hog = "export function onLoad() { console.log(inputs.greeting); }" + self.hog_function.inputs = {"greeting": {"value": "Hello, {person.properties.nonexistent_property}!"}} - # This should not raise an exception during transpilation - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.greeting);", result) + assert "console.log(inputs.greeting);" in result def test_get_transpiled_function_with_syntax_error_in_source(self): - id = "123" - source = 'export function onLoad() { console.log("Missing closing brace");' - filters: dict = {} - inputs: dict = {} - team = self.team + self.hog_function.hog = 'export function onLoad() { console.log("Missing closing brace");' - with self.assertRaises(TranspilerError): - get_transpiled_function(id, source, filters, inputs, team) + with pytest.raises(TranspilerError): + get_transpiled_function(self.hog_function) def test_get_transpiled_function_with_complex_inputs(self): - id = "123" - source = "export function onLoad() { console.log(inputs.complexInput); }" - filters: dict = {} - inputs = { + self.hog_function.hog = "export function onLoad() { console.log(inputs.complexInput); }" + self.hog_function.inputs = { "complexInput": { "value": { "nested": "{event.properties.url}", @@ -113,129 +157,101 @@ def test_get_transpiled_function_with_complex_inputs(self): } } } - team = self.team - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.complexInput);", result) - self.assertIn("function getInputsKey", result) - self.assertIn('inputs["complexInput"] = getInputsKey("complexInput");', result) + assert "console.log(inputs.complexInput);" in result + assert "function getInputsKey" in result + assert 'inputs["complexInput"] = getInputsKey("complexInput");' in result def test_get_transpiled_function_with_empty_inputs(self): - id = "123" - source = 'export function onLoad() { console.log("No inputs"); }' - filters: dict = {} - inputs: dict = {} - team = self.team + self.hog_function.hog = 'export function onLoad() { console.log("No inputs"); }' + self.hog_function.inputs = {} - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn('console.log("No inputs");', result) - self.assertIn("let inputs = {\n};", result) + assert 'console.log("No inputs");' in result + assert "let inputs = {\n};" in result def test_get_transpiled_function_with_non_template_string(self): - id = "123" - source = "export function onLoad() { console.log(inputs.staticMessage); }" - filters: dict = {} - inputs = {"staticMessage": {"value": "This is a static message."}} - team = self.team + self.hog_function.hog = "export function onLoad() { console.log(inputs.staticMessage); }" + self.hog_function.inputs = {"staticMessage": {"value": "This is a static message."}} - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.staticMessage);", result) - # Since the value does not contain '{', it should be added directly to inputs object - self.assertIn('"staticMessage": "This is a static message."', result) - self.assertNotIn("function getInputsKey", result) + assert "console.log(inputs.staticMessage);" in result + assert '"staticMessage": "This is a static message."' in result + assert "function getInputsKey" not in result def test_get_transpiled_function_with_list_inputs(self): - id = "123" - source = "export function onLoad() { console.log(inputs.messages); }" - filters: dict = {} - inputs = {"messages": {"value": ["Hello", "World", "{person.properties.name}"]}} - team = self.team + self.hog_function.hog = "export function onLoad() { console.log(inputs.messages); }" + self.hog_function.inputs = {"messages": {"value": ["Hello", "World", "{person.properties.name}"]}} - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.messages);", result) - self.assertIn("function getInputsKey", result) - self.assertIn('inputs["messages"] = getInputsKey("messages");', result) + assert "console.log(inputs.messages);" in result + assert "function getInputsKey" in result + assert 'inputs["messages"] = getInputsKey("messages");' in result def test_get_transpiled_function_with_event_filter(self): - id = "123" - source = "export function onEvent(event) { console.log(event.properties.url); }" - filters: dict = { + self.hog_function.hog = "export function onEvent(event) { console.log(event.properties.url); }" + self.hog_function.filters = { "events": [{"id": "$pageview", "name": "$pageview", "type": "events"}], "filter_test_accounts": True, } - inputs: dict = {} - team = self.team - # Assume that team.test_account_filters is set up - team.test_account_filters = [{"key": "email", "value": "@test.com", "operator": "icontains", "type": "person"}] - team.save() - - result = get_transpiled_function(id, source, filters, inputs, team) - - self.assertIsInstance(result, str) - self.assertIn("console.log(event.properties.url);", result) - self.assertIn("const filterMatches = ", result) - self.assertIn('__getGlobal("event") == "$pageview"', result) - self.assertIn( - '(ilike(__getProperty(__getProperty(__getGlobal("person"), "properties", true), "email", true), "%@test.com%")', - result, + + self.team.test_account_filters = [ + {"key": "email", "value": "@test.com", "operator": "icontains", "type": "person"} + ] + self.team.save() + + result = self.compile_and_run() + + assert "console.log(event.properties.url);" in result + assert "const filterMatches = " in result + assert '__getGlobal("event") == "$pageview"' in result + assert ( + '(ilike(__getProperty(__getProperty(__getGlobal("person"), "properties", true), "email", true), "%@test.com%")' + in result ) def test_get_transpiled_function_with_groups(self): - id = "123" - source = "export function onLoad() { console.log(inputs.groupInfo); }" - filters: dict = {} - inputs = {"groupInfo": {"value": "{groups['company']}"}} - team = self.team + self.hog_function.hog = "export function onLoad() { console.log(inputs.groupInfo); }" + self.hog_function.inputs = {"groupInfo": {"value": "{groups['company']}"}} - # Set up group type mapping - GroupTypeMapping.objects.create(team=team, group_type="company", group_type_index=0, project=self.project) + GroupTypeMapping.objects.create(team=self.team, group_type="company", group_type_index=0, project=self.project) - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.groupInfo);", result) - self.assertIn('inputs["groupInfo"] = getInputsKey("groupInfo");', result) - self.assertIn('__getProperty(__getGlobal("groups"), "company", false)', result) + assert "console.log(inputs.groupInfo);" in result + assert 'inputs["groupInfo"] = getInputsKey("groupInfo");' in result + assert '__getProperty(__getGlobal("groups"), "company", false)' in result def test_get_transpiled_function_with_missing_group(self): - id = "123" - source = "export function onLoad() { console.log(inputs.groupInfo); }" - filters: dict = {} - inputs = {"groupInfo": {"value": "{groups['nonexistent']}"}} - team = self.team + self.hog_function.hog = "export function onLoad() { console.log(inputs.groupInfo); }" + self.hog_function.inputs = {"groupInfo": {"value": "{groups['nonexistent']}"}} - result = get_transpiled_function(id, source, filters, inputs, team) + result = self.compile_and_run() - self.assertIsInstance(result, str) - self.assertIn("console.log(inputs.groupInfo);", result) - self.assertIn('inputs["groupInfo"] = getInputsKey("groupInfo");', result) - self.assertIn('__getProperty(__getGlobal("groups"), "nonexistent"', result) + assert "console.log(inputs.groupInfo);" in result + assert 'inputs["groupInfo"] = getInputsKey("groupInfo");' in result + assert '__getProperty(__getGlobal("groups"), "nonexistent"' in result def test_get_transpiled_function_with_complex_filters(self): action = Action.objects.create(team=self.team, name="Test Action") action.steps = [{"event": "$pageview", "url": "https://example.com"}] # type: ignore action.save() - id = "123" - source = "export function onEvent(event) { console.log(event.event); }" - filters: dict = { + + self.hog_function.hog = "export function onEvent(event) { console.log(event.event); }" + self.hog_function.filters = { "events": [{"id": "$pageview", "name": "$pageview", "type": "events"}], "actions": [{"id": str(action.pk), "name": "Test Action", "type": "actions"}], "filter_test_accounts": True, } - inputs: dict = {} - team = self.team - result = get_transpiled_function(id, source, filters, inputs, team) - - self.assertIsInstance(result, str) - self.assertIn("console.log(event.event);", result) - self.assertIn("const filterMatches = ", result) - self.assertIn('__getGlobal("event") == "$pageview"', result) - self.assertIn("https://example.com", result) + + result = self.compile_and_run() + + assert "console.log(event.event);" in result + assert "const filterMatches = " in result + assert '__getGlobal("event") == "$pageview"' in result + assert "https://example.com" in result diff --git a/posthog/models/remote_config.py b/posthog/models/remote_config.py index 2599857cc2939..1bc1141f350c9 100644 --- a/posthog/models/remote_config.py +++ b/posthog/models/remote_config.py @@ -3,8 +3,6 @@ from typing import Optional from django.conf import settings from django.db import models -from django.db.models.signals import post_save -from django.dispatch.dispatcher import receiver from django.utils import timezone from prometheus_client import Counter from sentry_sdk import capture_exception @@ -12,9 +10,14 @@ from posthog.database_healthcheck import DATABASE_FOR_FLAG_MATCHING from posthog.models.feature_flag.feature_flag import FeatureFlag +from posthog.models.hog_functions.hog_function import HogFunction +from posthog.models.plugin import PluginConfig +from posthog.models.team.team import Team from posthog.models.utils import UUIDModel, execute_with_timeout -from posthog.models.team import Team +from django.db.models.signals import post_save +from django.dispatch.dispatcher import receiver + CELERY_TASK_REMOTE_CONFIG_SYNC = Counter( "posthog_remote_config_sync", @@ -41,6 +44,12 @@ def get_array_js_content(): return _array_js_content +def indent_js(js_content: str, indent: int = 4) -> str: + joined = "\n".join([f"{' ' * indent}{line}" for line in js_content.split("\n")]) + + return joined + + class RemoteConfig(UUIDModel): """ RemoteConfig is a helper model. There is one per team and stores a highly cacheable JSON object @@ -179,21 +188,44 @@ def build_js_config(self): # NOTE: This is the web focused config for the frontend that includes site apps from posthog.plugins.site import get_site_apps_for_team, get_site_config_from_schema + from posthog.cdp.site_functions import get_transpiled_function + from posthog.models import HogFunction # Add in the site apps as an array of objects - site_apps = [] + site_apps_js = [] for site_app in get_site_apps_for_team(self.team.id): config = get_site_config_from_schema(site_app.config_schema, site_app.config) - # NOTE: It is an object as we can later add other properties such as a consent ID - site_apps.append( - f"{{ token: '{site_app.token}', load: function(posthog) {{ {site_app.source}().inject({{ config:{json.dumps(config)}, posthog:posthog }}) }} }}" + site_apps_js.append( + indent_js( + f"\n{{\n id: '{site_app.token}',\n init: function(config) {{\n {indent_js(site_app.source, indent=4)}().inject({{ config:{json.dumps(config)}, posthog:config.posthog }});\n config.callback();\n }}\n}}" + ) ) - js_content = f""" - (function() {{ - window._POSTHOG_CONFIG = {json.dumps(self.config)}; - window._POSTHOG_SITE_APPS = [{','.join(site_apps)}]; - }})(); + site_functions = HogFunction.objects.filter( + team=self.team, enabled=True, type__in=("site_destination", "site_app") + ).all() + + site_functions_js = [] + + for site_function in site_functions: + try: + source = get_transpiled_function(site_function) + # NOTE: It is an object as we can later add other properties such as a consent ID + # Indentation to make it more readable (and therefore debuggable) + site_functions_js.append( + indent_js( + f"\n{{\n id: '{site_function.id}',\n init: function(config) {{ return {indent_js(source, indent=4)}().init(config) }} \n}}" + ) + ) + except Exception: + # TODO: Should we track this to somewhere? + logger.exception(f"Failed to build JS for site function {site_function.id}") + pass + + js_content = f"""(function() {{ + window._POSTHOG_CONFIG = {json.dumps(self.config)}; + window._POSTHOG_JS_APPS = [{','.join(site_apps_js + site_functions_js)}]; +}})(); """.strip() return js_content @@ -219,6 +251,7 @@ def sync(self, force=False): logger.info(f"Syncing RemoteConfig for team {self.team_id}") + # TODO: We might still want to invalidate certain caches here due to site apps changing try: config = self.build_config() # Compare the config to the current one and only update if it has changed @@ -242,17 +275,29 @@ def __str__(self): return f"RemoteConfig {self.team_id}" -def rebuild_remote_config(team: "Team"): +def _update_team_remote_config(team_id: int): from posthog.tasks.remote_config import update_team_remote_config - update_team_remote_config.delay(team.id) + update_team_remote_config.delay(team_id) @receiver(post_save, sender=Team) def team_saved(sender, instance: "Team", created, **kwargs): - rebuild_remote_config(instance) + _update_team_remote_config(instance.id) @receiver(post_save, sender=FeatureFlag) def feature_flag_saved(sender, instance: "FeatureFlag", created, **kwargs): - rebuild_remote_config(instance.team) + _update_team_remote_config(instance.team_id) + + +@receiver(post_save, sender=PluginConfig) +def site_app_saved(sender, instance: "PluginConfig", created, **kwargs): + if instance.team_id: + _update_team_remote_config(instance.team_id) + + +@receiver(post_save, sender=HogFunction) +def site_function_saved(sender, instance: "HogFunction", created, **kwargs): + if instance.enabled and instance.type in ("site_destination", "site_app") and instance.transpiled: + _update_team_remote_config(instance.team_id) diff --git a/posthog/models/test/test_remote_config.py b/posthog/models/test/test_remote_config.py index cfe66d48d8e6b..2f6cb299bf9ee 100644 --- a/posthog/models/test/test_remote_config.py +++ b/posthog/models/test/test_remote_config.py @@ -1,6 +1,7 @@ from decimal import Decimal from inline_snapshot import snapshot from posthog.models.feature_flag.feature_flag import FeatureFlag +from posthog.models.hog_functions.hog_function import HogFunction, HogFunctionType from posthog.models.plugin import Plugin, PluginConfig, PluginSourceFile from posthog.models.project import Project from posthog.models.remote_config import RemoteConfig @@ -139,9 +140,9 @@ def test_renders_js_including_config(self): assert js == snapshot( """\ (function() { - window._POSTHOG_CONFIG = {"token": "phc_12345", "surveys": false, "heatmaps": false, "siteApps": [], "analytics": {"endpoint": "/i/v0/e/"}, "hasFeatureFlags": false, "sessionRecording": false, "captureDeadClicks": false, "capturePerformance": {"web_vitals": false, "network_timing": true, "web_vitals_allowed_metrics": null}, "autocapture_opt_out": false, "supportedCompression": ["gzip", "gzip-js"], "autocaptureExceptions": false, "defaultIdentifiedOnly": false, "elementsChainAsString": true}; - window._POSTHOG_SITE_APPS = []; - })();\ + window._POSTHOG_CONFIG = {"token": "phc_12345", "surveys": false, "heatmaps": false, "siteApps": [], "analytics": {"endpoint": "/i/v0/e/"}, "hasFeatureFlags": false, "sessionRecording": false, "captureDeadClicks": false, "capturePerformance": {"web_vitals": false, "network_timing": true, "web_vitals_allowed_metrics": null}, "autocapture_opt_out": false, "supportedCompression": ["gzip", "gzip-js"], "autocaptureExceptions": false, "defaultIdentifiedOnly": false, "elementsChainAsString": true}; + window._POSTHOG_JS_APPS = []; +})();\ """ ) @@ -183,8 +184,258 @@ def test_renders_js_including_site_apps(self): assert js == snapshot( """\ (function() { - window._POSTHOG_CONFIG = {"token": "phc_12345", "surveys": false, "heatmaps": false, "siteApps": [], "analytics": {"endpoint": "/i/v0/e/"}, "hasFeatureFlags": false, "sessionRecording": false, "captureDeadClicks": false, "capturePerformance": {"web_vitals": false, "network_timing": true, "web_vitals_allowed_metrics": null}, "autocapture_opt_out": false, "supportedCompression": ["gzip", "gzip-js"], "autocaptureExceptions": false, "defaultIdentifiedOnly": false, "elementsChainAsString": true}; - window._POSTHOG_SITE_APPS = [{ token: 'tokentoken', load: function(posthog) { (function () { return { inject: (data) => console.log('injected!', data)}; })().inject({ config:{}, posthog:posthog }) } },{ token: 'tokentoken', load: function(posthog) { (function () { return { inject: (data) => console.log('injected 2!', data)}; })().inject({ config:{}, posthog:posthog }) } },{ token: 'tokentoken', load: function(posthog) { (function () { return { inject: (data) => console.log('injected but disabled!', data)}; })().inject({ config:{}, posthog:posthog }) } }]; - })();\ -""" + window._POSTHOG_CONFIG = {"token": "phc_12345", "surveys": false, "heatmaps": false, "siteApps": [], "analytics": {"endpoint": "/i/v0/e/"}, "hasFeatureFlags": false, "sessionRecording": false, "captureDeadClicks": false, "capturePerformance": {"web_vitals": false, "network_timing": true, "web_vitals_allowed_metrics": null}, "autocapture_opt_out": false, "supportedCompression": ["gzip", "gzip-js"], "autocaptureExceptions": false, "defaultIdentifiedOnly": false, "elementsChainAsString": true}; + window._POSTHOG_JS_APPS = [ + { + id: 'tokentoken', + init: function(config) { + (function () { return { inject: (data) => console.log('injected!', data)}; })().inject({ config:{}, posthog:config.posthog }); + config.callback(); + } + }, + { + id: 'tokentoken', + init: function(config) { + (function () { return { inject: (data) => console.log('injected 2!', data)}; })().inject({ config:{}, posthog:config.posthog }); + config.callback(); + } + }, + { + id: 'tokentoken', + init: function(config) { + (function () { return { inject: (data) => console.log('injected but disabled!', data)}; })().inject({ config:{}, posthog:config.posthog }); + config.callback(); + } + }]; +})();\ +""" # noqa: W291, W293 + ) + + def test_renders_js_including_site_functions(self): + non_site_app = HogFunction.objects.create( + name="Test", + type=HogFunctionType.DESTINATION, + team=self.team, + enabled=True, + filters={ + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "filter_test_accounts": True, + }, + ) + + site_destination = HogFunction.objects.create( + name="Test", + type=HogFunctionType.SITE_DESTINATION, + team=self.team, + enabled=True, + filters={ + "events": [{"id": "$pageview", "name": "$pageview", "type": "events", "order": 0}], + "filter_test_accounts": True, + }, + ) + + site_app = HogFunction.objects.create( + name="Test", + type=HogFunctionType.SITE_APP, + team=self.team, + enabled=True, + ) + + self.remote_config.build_config() + js = self.remote_config.build_js_config() + assert str(non_site_app.id) not in js + assert str(site_destination.id) in js + assert str(site_app.id) in js + + js = js.replace(str(non_site_app.id), "NON_SITE_APP_ID") + js = js.replace(str(site_destination.id), "SITE_DESTINATION_ID") + js = js.replace(str(site_app.id), "SITE_APP_ID") + + # TODO: Come up with a good way of solidly testing this, ideally by running it in an actual browser environment + assert js == snapshot( + """\ +(function() { + window._POSTHOG_CONFIG = {"token": "phc_12345", "surveys": false, "heatmaps": false, "siteApps": [], "analytics": {"endpoint": "/i/v0/e/"}, "hasFeatureFlags": false, "sessionRecording": false, "captureDeadClicks": false, "capturePerformance": {"web_vitals": false, "network_timing": true, "web_vitals_allowed_metrics": null}, "autocapture_opt_out": false, "supportedCompression": ["gzip", "gzip-js"], "autocaptureExceptions": false, "defaultIdentifiedOnly": false, "elementsChainAsString": true}; + window._POSTHOG_JS_APPS = [ + { + id: 'SITE_DESTINATION_ID', + init: function(config) { return (function() { + function toString (value) { return __STLToString(value) } + function match (str, pattern) { return !str || !pattern ? false : new RegExp(pattern).test(str) } + function ilike (str, pattern) { return __like(str, pattern, true) } + function __like(str, pattern, caseInsensitive = false) { + if (caseInsensitive) { + str = str.toLowerCase() + pattern = pattern.toLowerCase() + } + pattern = String(pattern) + .replaceAll(/[-/\\\\^$*+?.()|[\\]{}]/g, '\\\\$&') + .replaceAll('%', '.*') + .replaceAll('_', '.') + return new RegExp(pattern).test(str) + } + function __getProperty(objectOrArray, key, nullish) { + if ((nullish && !objectOrArray) || key === 0) { return null } + if (Array.isArray(objectOrArray)) { return key > 0 ? objectOrArray[key - 1] : objectOrArray[objectOrArray.length + key] } + else { return objectOrArray[key] } + } + function __STLToString(arg) { + if (arg && __isHogDate(arg)) { return `${arg.year}-${arg.month.toString().padStart(2, '0')}-${arg.day.toString().padStart(2, '0')}`; } + else if (arg && __isHogDateTime(arg)) { return __DateTimeToString(arg); } + return __printHogStringOutput(arg); } + function __printHogStringOutput(obj) { if (typeof obj === 'string') { return obj } return __printHogValue(obj) } + function __printHogValue(obj, marked = new Set()) { + if (typeof obj === 'object' && obj !== null && obj !== undefined) { + if (marked.has(obj) && !__isHogDateTime(obj) && !__isHogDate(obj) && !__isHogError(obj)) { return 'null'; } + marked.add(obj); + try { + if (Array.isArray(obj)) { + if (obj.__isHogTuple) { return obj.length < 2 ? `tuple(${obj.map((o) => __printHogValue(o, marked)).join(', ')})` : `(${obj.map((o) => __printHogValue(o, marked)).join(', ')})`; } + return `[${obj.map((o) => __printHogValue(o, marked)).join(', ')}]`; + } + if (__isHogDateTime(obj)) { const millis = String(obj.dt); return `DateTime(${millis}${millis.includes('.') ? '' : '.0'}, ${__escapeString(obj.zone)})`; } + if (__isHogDate(obj)) return `Date(${obj.year}, ${obj.month}, ${obj.day})`; + if (__isHogError(obj)) { return `${String(obj.type)}(${__escapeString(obj.message)}${obj.payload ? `, ${__printHogValue(obj.payload, marked)}` : ''})`; } + if (obj instanceof Map) { return `{${Array.from(obj.entries()).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; } + return `{${Object.entries(obj).map(([key, value]) => `${__printHogValue(key, marked)}: ${__printHogValue(value, marked)}`).join(', ')}}`; + } finally { + marked.delete(obj); + } + } else if (typeof obj === 'boolean') return obj ? 'true' : 'false'; + else if (obj === null || obj === undefined) return 'null'; + else if (typeof obj === 'string') return __escapeString(obj); + if (typeof obj === 'function') return `fn<${__escapeIdentifier(obj.name || 'lambda')}(${obj.length})>`; + return obj.toString(); + } + function __isHogError(obj) {return obj && obj.__hogError__ === true} + function __escapeString(value) { + const singlequoteEscapeCharsMap = { '\\b': '\\\\b', '\\f': '\\\\f', '\\r': '\\\\r', '\\n': '\\\\n', '\\t': '\\\\t', '\\0': '\\\\0', '\\v': '\\\\v', '\\\\': '\\\\\\\\', "'": "\\\\'" } + return `'${value.split('').map((c) => singlequoteEscapeCharsMap[c] || c).join('')}'`; + } + function __escapeIdentifier(identifier) { + const backquoteEscapeCharsMap = { '\\b': '\\\\b', '\\f': '\\\\f', '\\r': '\\\\r', '\\n': '\\\\n', '\\t': '\\\\t', '\\0': '\\\\0', '\\v': '\\\\v', '\\\\': '\\\\\\\\', '`': '\\\\`' } + if (typeof identifier === 'number') return identifier.toString(); + if (/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(identifier)) return identifier; + return `\\`${identifier.split('').map((c) => backquoteEscapeCharsMap[c] || c).join('')}\\``; + } + function __isHogDateTime(obj) { return obj && obj.__hogDateTime__ === true } + function __isHogDate(obj) { return obj && obj.__hogDate__ === true } + function __DateTimeToString(dt) { + if (__isHogDateTime(dt)) { + const date = new Date(dt.dt * 1000); + const timeZone = dt.zone || 'UTC'; + const milliseconds = Math.floor(dt.dt * 1000 % 1000); + const options = { timeZone, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }; + const formatter = new Intl.DateTimeFormat('en-US', options); + const parts = formatter.formatToParts(date); + let year, month, day, hour, minute, second; + for (const part of parts) { + switch (part.type) { + case 'year': year = part.value; break; + case 'month': month = part.value; break; + case 'day': day = part.value; break; + case 'hour': hour = part.value; break; + case 'minute': minute = part.value; break; + case 'second': second = part.value; break; + default: break; + } + } + const getOffset = (date, timeZone) => { + const tzDate = new Date(date.toLocaleString('en-US', { timeZone })); + const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' })); + const offset = (tzDate - utcDate) / 60000; // in minutes + const sign = offset >= 0 ? '+' : '-'; + const absOffset = Math.abs(offset); + const hours = Math.floor(absOffset / 60); + const minutes = absOffset % 60; + return `${sign}${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`; + }; + let offset = 'Z'; + if (timeZone !== 'UTC') { + offset = getOffset(date, timeZone); + } + let isoString = `${year}-${month}-${day}T${hour}:${minute}:${second}`; + isoString += `.${milliseconds.toString().padStart(3, '0')}`; + isoString += offset; + return isoString; + } + } + function buildInputs(globals, initial) { + let inputs = { + }; + let __getGlobal = (key) => key === 'inputs' ? inputs : globals[key]; + return inputs;} + const source = (function () {let exports={};"use strict";;return exports;})(); + let processEvent = undefined; + if ('onEvent' in source) { + processEvent = function processEvent(globals) { + if (!('onEvent' in source)) { return; }; + const inputs = buildInputs(globals); + const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; + let __getGlobal = (key) => filterGlobals[key]; + const filterMatches = !!(!!(!ilike(__getProperty(__getProperty(__getGlobal("person"), "properties", true), "email", true), "%@posthog.com%") && ((!match(toString(__getProperty(__getGlobal("properties"), "$host", true)), "^(localhost|127\\\\.0\\\\.0\\\\.1)($|:)")) ?? 1) && (__getGlobal("event") == "$pageview"))); + if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + } + } + + function init(config) { + const posthog = config.posthog; + const callback = config.callback; + if ('onLoad' in source) { + const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } + } else { + callback(true); + } + + return { + processEvent: processEvent + } + } + + return { init: init }; + })().init(config) } + }, + { + id: 'SITE_APP_ID', + init: function(config) { return (function() { + + function buildInputs(globals, initial) { + let inputs = { + }; + let __getGlobal = (key) => key === 'inputs' ? inputs : globals[key]; + return inputs;} + const source = (function () {let exports={};"use strict";;return exports;})(); + let processEvent = undefined; + if ('onEvent' in source) { + processEvent = function processEvent(globals) { + if (!('onEvent' in source)) { return; }; + const inputs = buildInputs(globals); + const filterGlobals = { ...globals.groups, ...globals.event, person: globals.person, inputs, pdi: { distinct_id: globals.event.distinct_id, person: globals.person } }; + let __getGlobal = (key) => filterGlobals[key]; + const filterMatches = true; + if (filterMatches) { source.onEvent({ ...globals, inputs, posthog }); } + } + } + + function init(config) { + const posthog = config.posthog; + const callback = config.callback; + if ('onLoad' in source) { + const r = source.onLoad({ inputs: buildInputs({}, true), posthog: posthog }); + if (r && typeof r.then === 'function' && typeof r.finally === 'function') { r.catch(() => callback(false)).then(() => callback(true)) } else { callback(true) } + } else { + callback(true); + } + + return { + processEvent: processEvent + } + } + + return { init: init }; + })().init(config) } + }]; +})();\ +""" # noqa: W291, W293 ) diff --git a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr index 95e70a1ec2328..1630ec4265914 100644 --- a/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr +++ b/posthog/tasks/test/__snapshots__/test_process_scheduled_changes.ambr @@ -86,69 +86,6 @@ ''' # --- # name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.10 - ''' - SELECT "posthog_team"."id", - "posthog_team"."uuid", - "posthog_team"."organization_id", - "posthog_team"."project_id", - "posthog_team"."api_token", - "posthog_team"."app_urls", - "posthog_team"."name", - "posthog_team"."slack_incoming_webhook", - "posthog_team"."created_at", - "posthog_team"."updated_at", - "posthog_team"."anonymize_ips", - "posthog_team"."completed_snippet_onboarding", - "posthog_team"."has_completed_onboarding_for", - "posthog_team"."ingested_event", - "posthog_team"."autocapture_opt_out", - "posthog_team"."autocapture_web_vitals_opt_in", - "posthog_team"."autocapture_web_vitals_allowed_metrics", - "posthog_team"."autocapture_exceptions_opt_in", - "posthog_team"."autocapture_exceptions_errors_to_ignore", - "posthog_team"."person_processing_opt_out", - "posthog_team"."session_recording_opt_in", - "posthog_team"."session_recording_sample_rate", - "posthog_team"."session_recording_minimum_duration_milliseconds", - "posthog_team"."session_recording_linked_flag", - "posthog_team"."session_recording_network_payload_capture_config", - "posthog_team"."session_recording_url_trigger_config", - "posthog_team"."session_recording_url_blocklist_config", - "posthog_team"."session_recording_event_trigger_config", - "posthog_team"."session_replay_config", - "posthog_team"."survey_config", - "posthog_team"."capture_console_log_opt_in", - "posthog_team"."capture_performance_opt_in", - "posthog_team"."capture_dead_clicks", - "posthog_team"."surveys_opt_in", - "posthog_team"."heatmaps_opt_in", - "posthog_team"."session_recording_version", - "posthog_team"."signup_token", - "posthog_team"."is_demo", - "posthog_team"."access_control", - "posthog_team"."week_start_day", - "posthog_team"."inject_web_apps", - "posthog_team"."test_account_filters", - "posthog_team"."test_account_filters_default_checked", - "posthog_team"."path_cleaning_filters", - "posthog_team"."timezone", - "posthog_team"."data_attributes", - "posthog_team"."person_display_name_properties", - "posthog_team"."live_events_columns", - "posthog_team"."recording_domains", - "posthog_team"."primary_dashboard_id", - "posthog_team"."extra_settings", - "posthog_team"."modifiers", - "posthog_team"."correlation_config", - "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id", - "posthog_team"."external_data_workspace_last_synced_at" - FROM "posthog_team" - WHERE "posthog_team"."id" = 99999 - LIMIT 21 - ''' -# --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.11 ''' SELECT "posthog_remoteconfig"."id", "posthog_remoteconfig"."team_id", @@ -160,7 +97,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.12 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.11 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -230,7 +167,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.13 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.12 ''' SELECT COUNT(*) AS "__count" FROM "posthog_featureflag" @@ -239,7 +176,7 @@ AND "posthog_featureflag"."team_id" = 99999) ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.14 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.13 ''' SELECT "posthog_organization"."id", "posthog_organization"."name", @@ -265,7 +202,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.15 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.14 ''' SELECT "posthog_experiment"."id", "posthog_experiment"."name", @@ -291,7 +228,7 @@ WHERE "posthog_experiment"."feature_flag_id" = 99999 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.16 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.15 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -361,7 +298,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.17 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.16 ''' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -383,7 +320,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.18 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.17 ''' SELECT "posthog_user"."id", "posthog_user"."password", @@ -416,7 +353,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.19 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.18 ''' SELECT "posthog_featureflag"."id", "posthog_featureflag"."key", @@ -439,19 +376,7 @@ AND "posthog_featureflag"."team_id" = 99999) ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.2 - ''' - SELECT "posthog_remoteconfig"."id", - "posthog_remoteconfig"."team_id", - "posthog_remoteconfig"."config", - "posthog_remoteconfig"."updated_at", - "posthog_remoteconfig"."synced_at" - FROM "posthog_remoteconfig" - WHERE "posthog_remoteconfig"."team_id" = 99999 - LIMIT 21 - ''' -# --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.20 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.19 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -507,13 +432,6 @@ "posthog_team"."modifiers", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" @@ -521,70 +439,19 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.21 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.2 ''' - SELECT "posthog_team"."id", - "posthog_team"."uuid", - "posthog_team"."organization_id", - "posthog_team"."project_id", - "posthog_team"."api_token", - "posthog_team"."app_urls", - "posthog_team"."name", - "posthog_team"."slack_incoming_webhook", - "posthog_team"."created_at", - "posthog_team"."updated_at", - "posthog_team"."anonymize_ips", - "posthog_team"."completed_snippet_onboarding", - "posthog_team"."has_completed_onboarding_for", - "posthog_team"."ingested_event", - "posthog_team"."autocapture_opt_out", - "posthog_team"."autocapture_web_vitals_opt_in", - "posthog_team"."autocapture_web_vitals_allowed_metrics", - "posthog_team"."autocapture_exceptions_opt_in", - "posthog_team"."autocapture_exceptions_errors_to_ignore", - "posthog_team"."person_processing_opt_out", - "posthog_team"."session_recording_opt_in", - "posthog_team"."session_recording_sample_rate", - "posthog_team"."session_recording_minimum_duration_milliseconds", - "posthog_team"."session_recording_linked_flag", - "posthog_team"."session_recording_network_payload_capture_config", - "posthog_team"."session_recording_url_trigger_config", - "posthog_team"."session_recording_url_blocklist_config", - "posthog_team"."session_recording_event_trigger_config", - "posthog_team"."session_replay_config", - "posthog_team"."survey_config", - "posthog_team"."capture_console_log_opt_in", - "posthog_team"."capture_performance_opt_in", - "posthog_team"."capture_dead_clicks", - "posthog_team"."surveys_opt_in", - "posthog_team"."heatmaps_opt_in", - "posthog_team"."session_recording_version", - "posthog_team"."signup_token", - "posthog_team"."is_demo", - "posthog_team"."access_control", - "posthog_team"."week_start_day", - "posthog_team"."inject_web_apps", - "posthog_team"."test_account_filters", - "posthog_team"."test_account_filters_default_checked", - "posthog_team"."path_cleaning_filters", - "posthog_team"."timezone", - "posthog_team"."data_attributes", - "posthog_team"."person_display_name_properties", - "posthog_team"."live_events_columns", - "posthog_team"."recording_domains", - "posthog_team"."primary_dashboard_id", - "posthog_team"."extra_settings", - "posthog_team"."modifiers", - "posthog_team"."correlation_config", - "posthog_team"."session_recording_retention_period_days", - "posthog_team"."external_data_workspace_id", - "posthog_team"."external_data_workspace_last_synced_at" - FROM "posthog_team" - WHERE "posthog_team"."id" = 99999 + SELECT "posthog_remoteconfig"."id", + "posthog_remoteconfig"."team_id", + "posthog_remoteconfig"."config", + "posthog_remoteconfig"."updated_at", + "posthog_remoteconfig"."synced_at" + FROM "posthog_remoteconfig" + WHERE "posthog_remoteconfig"."team_id" = 99999 LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.22 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.20 ''' SELECT "posthog_remoteconfig"."id", "posthog_remoteconfig"."team_id", @@ -596,7 +463,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.23 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.21 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -666,7 +533,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.24 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.22 ''' SELECT COUNT(*) AS "__count" FROM "posthog_featureflag" @@ -675,7 +542,7 @@ AND "posthog_featureflag"."team_id" = 99999) ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.25 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.23 ''' SELECT "posthog_organization"."id", "posthog_organization"."name", @@ -701,7 +568,7 @@ LIMIT 21 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.26 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.24 ''' SELECT "posthog_experiment"."id", "posthog_experiment"."name", @@ -727,7 +594,7 @@ WHERE "posthog_experiment"."feature_flag_id" = 99999 ''' # --- -# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.27 +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.25 ''' SELECT "posthog_team"."id", "posthog_team"."uuid", @@ -797,6 +664,42 @@ LIMIT 21 ''' # --- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.26 + ''' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE "posthog_scheduledchange"."id" = 99999 + LIMIT 21 + ''' +# --- +# name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.27 + ''' + SELECT "posthog_scheduledchange"."id", + "posthog_scheduledchange"."record_id", + "posthog_scheduledchange"."model_name", + "posthog_scheduledchange"."payload", + "posthog_scheduledchange"."scheduled_at", + "posthog_scheduledchange"."executed_at", + "posthog_scheduledchange"."failure_reason", + "posthog_scheduledchange"."team_id", + "posthog_scheduledchange"."created_at", + "posthog_scheduledchange"."created_by_id", + "posthog_scheduledchange"."updated_at" + FROM "posthog_scheduledchange" + WHERE "posthog_scheduledchange"."id" = 99999 + LIMIT 21 + ''' +# --- # name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.28 ''' SELECT "posthog_scheduledchange"."id", @@ -905,19 +808,23 @@ # --- # name: TestProcessScheduledChanges.test_schedule_feature_flag_multiple_changes.30 ''' - SELECT "posthog_scheduledchange"."id", - "posthog_scheduledchange"."record_id", - "posthog_scheduledchange"."model_name", - "posthog_scheduledchange"."payload", - "posthog_scheduledchange"."scheduled_at", - "posthog_scheduledchange"."executed_at", - "posthog_scheduledchange"."failure_reason", - "posthog_scheduledchange"."team_id", - "posthog_scheduledchange"."created_at", - "posthog_scheduledchange"."created_by_id", - "posthog_scheduledchange"."updated_at" - FROM "posthog_scheduledchange" - WHERE "posthog_scheduledchange"."id" = 99999 + SELECT "posthog_featureflag"."id", + "posthog_featureflag"."key", + "posthog_featureflag"."name", + "posthog_featureflag"."filters", + "posthog_featureflag"."rollout_percentage", + "posthog_featureflag"."team_id", + "posthog_featureflag"."created_by_id", + "posthog_featureflag"."created_at", + "posthog_featureflag"."deleted", + "posthog_featureflag"."active", + "posthog_featureflag"."rollback_conditions", + "posthog_featureflag"."performed_rollback", + "posthog_featureflag"."ensure_experience_continuity", + "posthog_featureflag"."usage_dashboard_id", + "posthog_featureflag"."has_enriched_analytics" + FROM "posthog_featureflag" + WHERE "posthog_featureflag"."key" = 'flag-1' LIMIT 21 ''' # --- @@ -1126,13 +1033,6 @@ "posthog_team"."modifiers", "posthog_team"."correlation_config", "posthog_team"."session_recording_retention_period_days", - "posthog_team"."plugins_opt_in", - "posthog_team"."opt_out_capture", - "posthog_team"."event_names", - "posthog_team"."event_names_with_usage", - "posthog_team"."event_properties", - "posthog_team"."event_properties_with_usage", - "posthog_team"."event_properties_numerical", "posthog_team"."external_data_workspace_id", "posthog_team"."external_data_workspace_last_synced_at" FROM "posthog_team" diff --git a/posthog/urls.py b/posthog/urls.py index 078a66c5af8b3..4806a6f65cc07 100644 --- a/posthog/urls.py +++ b/posthog/urls.py @@ -211,7 +211,6 @@ def opt_slash_path(route: str, view: Callable, name: Optional[str] = None) -> UR sharing.SharingViewerPageViewSet.as_view({"get": "retrieve"}), ), path("site_app////", site_app.get_site_app), - path("site_function///", site_app.get_site_function), path("array//config", remote_config.RemoteConfigAPIView.as_view()), path("array//config.js", remote_config.RemoteConfigJSAPIView.as_view()), path("array//array.js", remote_config.RemoteConfigArrayJSAPIView.as_view()),