diff --git a/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx index ef23b0f396d24..20b48ea0e2978 100644 --- a/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx +++ b/frontend/src/scenes/settings/project/SessionRecordingSettings.tsx @@ -82,7 +82,7 @@ export function ReplayGeneral(): JSX.Element { updateCurrentTeam({ session_recording_config: { ...currentTeam?.session_recording_config, - recordCanvas: checked, + record_canvas: checked, }, }) }} @@ -90,7 +90,7 @@ export function ReplayGeneral(): JSX.Element { bordered checked={ currentTeam?.session_recording_config - ? !!currentTeam?.session_recording_config?.recordCanvas + ? !!currentTeam?.session_recording_config?.record_canvas : false } /> diff --git a/frontend/src/types.ts b/frontend/src/types.ts index 4ce63def33cfe..13de3f1c43b19 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -357,7 +357,7 @@ export interface TeamType extends TeamBasicType { | { recordHeaders?: boolean; recordBody?: boolean } | undefined | null - session_recording_config: { recordCanvas?: boolean } | undefined | null + session_recording_config: { record_canvas?: boolean } | undefined | null autocapture_exceptions_opt_in: boolean surveys_opt_in?: boolean autocapture_exceptions_errors_to_ignore: string[] diff --git a/posthog/api/decide.py b/posthog/api/decide.py index 1df8055b557bf..0055a318ddf60 100644 --- a/posthog/api/decide.py +++ b/posthog/api/decide.py @@ -250,7 +250,7 @@ def get_decide(request: HttpRequest): if isinstance(linked_flag, Dict): linked_flag = linked_flag.get("key") - response["sessionRecording"] = { + session_recording_response = { "endpoint": "/s/", "consoleLogRecordingEnabled": capture_console_logs, "recorderVersion": "v2", @@ -260,6 +260,19 @@ def get_decide(request: HttpRequest): "networkPayloadCapture": team.session_recording_network_payload_capture_config or None, } + if isinstance(team.session_recording_config, Dict): + record_canvas = team.session_recording_config["record_canvas"] or False + session_recording_response.update( + { + "recordCanvas": record_canvas, + # hard coded during beta while we decide on sensible values + "canvasFps": 4 if record_canvas else None, + "canvasQuality": "0.6" if record_canvas else None, + } + ) + + response["sessionRecording"] = session_recording_response + response["surveys"] = True if team.surveys_opt_in else False site_apps = [] diff --git a/posthog/api/team.py b/posthog/api/team.py index 2393a493927b9..3a95b16b2abb2 100644 --- a/posthog/api/team.py +++ b/posthog/api/team.py @@ -102,6 +102,7 @@ class Meta: "session_recording_minimum_duration_milliseconds", "session_recording_linked_flag", "session_recording_network_payload_capture_config", + "session_recording_config", "recording_domains", "inject_web_apps", "surveys_opt_in", @@ -146,6 +147,7 @@ class Meta: "session_recording_minimum_duration_milliseconds", "session_recording_linked_flag", "session_recording_network_payload_capture_config", + "session_recording_config", "effective_membership_level", "access_control", "week_start_day", @@ -208,6 +210,18 @@ def validate_session_recording_network_payload_capture_config(self, value) -> Di return value + def validate_session_recording_config(self, value) -> Dict | None: + if value is None: + return None + + if not isinstance(value, Dict): + raise exceptions.ValidationError("Must provide a dictionary or None.") + + if not all(key in ["record_canvas"] for key in value.keys()): + raise exceptions.ValidationError("Must provide a dictionary with only 'record_canvas' key.") + + return value + def validate(self, attrs: Any) -> Any: if "primary_dashboard" in attrs and attrs["primary_dashboard"].team != self.instance: raise exceptions.PermissionDenied("Dashboard does not belong to this team.") diff --git a/posthog/api/test/__snapshots__/test_decide.ambr b/posthog/api/test/__snapshots__/test_decide.ambr index 79f2ae80438a2..81ac07613f463 100644 --- a/posthog/api/test/__snapshots__/test_decide.ambr +++ b/posthog/api/test/__snapshots__/test_decide.ambr @@ -1,6 +1,3 @@ -# name: TestDatabaseCheckForDecide.test_decide_doesnt_error_out_when_database_is_down_and_database_check_isnt_cached - 'SELECT 1' ---- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down ' SELECT "posthog_user"."id", @@ -54,6 +51,7 @@ "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_config", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", "posthog_team"."surveys_opt_in", @@ -89,22 +87,6 @@ LIMIT 21 /*controller='team-detail',route='api/projects/%28%3FP%3Cid%3E%5B%5E/.%5D%2B%29/%3F%24'*/ ' --- -# name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.10 - ' - 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" = 2) - ' ---- # name: TestDecide.test_decide_doesnt_error_out_when_database_is_down.2 ' SELECT "posthog_organizationmembership"."id", @@ -298,6 +280,7 @@ "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_config", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", "posthog_team"."surveys_opt_in", @@ -457,6 +440,7 @@ "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_config", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", "posthog_team"."surveys_opt_in", @@ -610,6 +594,7 @@ "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_config", "posthog_team"."capture_console_log_opt_in", "posthog_team"."capture_performance_opt_in", "posthog_team"."surveys_opt_in", diff --git a/posthog/api/test/test_decide.py b/posthog/api/test/test_decide.py index 839131875a03d..c5700e2d34cf9 100644 --- a/posthog/api/test/test_decide.py +++ b/posthog/api/test/test_decide.py @@ -312,6 +312,31 @@ def test_session_recording_empty_linked_flag(self, *args): expected_status_code=status.HTTP_400_BAD_REQUEST, ) + def test_session_recording_config(self, *args): + # :TRICKY: Test for regression around caching + + self._update_team( + { + "session_recording_opt_in": True, + } + ) + + response = self._post_decide().json() + assert "recordCanvas" not in response["sessionRecording"] + assert "canvasFps" not in response["sessionRecording"] + assert "canvasQuality" not in response["sessionRecording"] + + self._update_team( + { + "session_recording_config": {"record_canvas": True}, + } + ) + + response = self._post_decide().json() + self.assertEqual(response["sessionRecording"]["recordCanvas"], True) + self.assertEqual(response["sessionRecording"]["canvasFps"], 4) + self.assertEqual(response["sessionRecording"]["canvasQuality"], "0.6") + def test_exception_autocapture_opt_in(self, *args): # :TRICKY: Test for regression around caching response = self._post_decide().json() diff --git a/posthog/api/test/test_team.py b/posthog/api/test/test_team.py index 6945defe10b3b..5e3245251a50b 100644 --- a/posthog/api/test/test_team.py +++ b/posthog/api/test/test_team.py @@ -714,6 +714,22 @@ def test_can_set_and_unset_session_recording_network_payload_capture_config(self second_get_response = self.client.get("/api/projects/@current/") assert second_get_response.json()["session_recording_network_payload_capture_config"] is None + def test_can_set_and_unset_session_recording_config(self) -> None: + # can set + first_patch_response = self.client.patch( + "/api/projects/@current/", + {"session_recording_config": {"record_canvas": True}}, + ) + assert first_patch_response.status_code == status.HTTP_200_OK + get_response = self.client.get("/api/projects/@current/") + assert get_response.json()["session_recording_config"] == {"record_canvas": True} + + # can unset + response = self.client.patch("/api/projects/@current/", {"session_recording_config": None}) + assert response.status_code == status.HTTP_200_OK + second_get_response = self.client.get("/api/projects/@current/") + assert second_get_response.json()["session_recording_config"] is None + def create_team(organization: Organization, name: str = "Test team") -> Team: """