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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions ee/clickhouse/views/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,45 @@ def archive(self, request: Request, *args: Any, **kwargs: Any) -> Response:
archived_experiment = service.archive_experiment(experiment, request=request)
return Response(ExperimentSerializer(archived_experiment, context=self.get_serializer_context()).data)

@extend_schema(
request=None,
responses=ExperimentSerializer,
)
@action(methods=["POST"], detail=True, required_scopes=["experiment:write"])
def pause(self, request: Request, *args: Any, **kwargs: Any) -> Response:
"""
Pause a running experiment.

Deactivates the linked feature flag so it is no longer returned by the
/decide endpoint. Users fall back to the application default (typically
the control experience), and no new exposure events are recorded (i.e.
$feature_flag_called is not fired).
Returns 400 if the experiment is not running or is already paused.
"""
experiment: Experiment = self.get_object()
service = ExperimentService(team=self.team, user=request.user)
paused_experiment = service.pause_experiment(experiment, request=request)
return Response(ExperimentSerializer(paused_experiment, context=self.get_serializer_context()).data)

@extend_schema(
request=None,
responses=ExperimentSerializer,
)
@action(methods=["POST"], detail=True, required_scopes=["experiment:write"])
def resume(self, request: Request, *args: Any, **kwargs: Any) -> Response:
"""
Resume a paused experiment.

Reactivates the linked feature flag so it is returned by /decide again.
Users are re-bucketed deterministically into the same variants they had
before the pause, and exposure tracking resumes.
Returns 400 if the experiment is not running or is not paused.
"""
experiment: Experiment = self.get_object()
service = ExperimentService(team=self.team, user=request.user)
resumed_experiment = service.resume_experiment(experiment, request=request)
return Response(ExperimentSerializer(resumed_experiment, context=self.get_serializer_context()).data)

@action(methods=["POST"], detail=True, required_scopes=["experiment:write"])
def duplicate(self, request: Request, *args: Any, **kwargs: Any) -> Response:
source_experiment: Experiment = self.get_object()
Expand Down
122 changes: 122 additions & 0 deletions ee/clickhouse/views/test/test_clickhouse_experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -3658,6 +3658,128 @@ def test_archive_experiment_endpoint_not_ended(self):
)
self.assertEqual(archive_response.status_code, status.HTTP_400_BAD_REQUEST)

def _create_running_experiment(self, name: str = "Running Test", flag_key: str = "running-flag") -> dict:
"""Helper: create an experiment and launch it via the API."""
response = self.client.post(
f"/api/projects/{self.team.id}/experiments/",
{
"name": name,
"feature_flag_key": flag_key,
"metrics": [
{
"kind": "ExperimentMetric",
"metric_type": "mean",
"source": {"kind": "EventsNode", "event": "$pageview"},
}
],
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
experiment_id = response.json()["id"]

launch_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/launch/",
)
self.assertEqual(launch_response.status_code, status.HTTP_200_OK)
return launch_response.json()

def test_pause_experiment_endpoint(self):
data = self._create_running_experiment(name="Pause Endpoint", flag_key="pause-endpoint-flag")
experiment_id = data["id"]

# Flag should be active after launch
flag = FeatureFlag.objects.get(key="pause-endpoint-flag", team=self.team)
self.assertTrue(flag.active)

pause_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/pause/",
)
self.assertEqual(pause_response.status_code, status.HTTP_200_OK)
self.assertEqual(pause_response.json()["status"], "running")
self.assertFalse(pause_response.json()["feature_flag"]["active"])

# Verify flag is now inactive
flag.refresh_from_db()
self.assertFalse(flag.active)

def test_resume_experiment_endpoint(self):
data = self._create_running_experiment(name="Resume Endpoint", flag_key="resume-endpoint-flag")
experiment_id = data["id"]

# Pause first
pause_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/pause/",
)
self.assertEqual(pause_response.status_code, status.HTTP_200_OK)

# Resume
resume_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/resume/",
)
self.assertEqual(resume_response.status_code, status.HTTP_200_OK)
self.assertEqual(resume_response.json()["status"], "running")
self.assertTrue(resume_response.json()["feature_flag"]["active"])

flag = FeatureFlag.objects.get(key="resume-endpoint-flag", team=self.team)
self.assertTrue(flag.active)

def test_pause_experiment_already_paused_returns_400(self):
data = self._create_running_experiment(name="Double Pause", flag_key="double-pause-flag")
experiment_id = data["id"]

self.client.post(f"/api/projects/{self.team.id}/experiments/{experiment_id}/pause/")

second_pause = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/pause/",
)
self.assertEqual(second_pause.status_code, status.HTTP_400_BAD_REQUEST)

def test_resume_experiment_not_paused_returns_400(self):
data = self._create_running_experiment(name="Resume Not Paused", flag_key="resume-not-paused-flag")
experiment_id = data["id"]

resume_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/resume/",
)
self.assertEqual(resume_response.status_code, status.HTTP_400_BAD_REQUEST)

def test_pause_draft_experiment_returns_400(self):
response = self.client.post(
f"/api/projects/{self.team.id}/experiments/",
{
"name": "Pause Draft",
"feature_flag_key": "pause-draft-flag",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
experiment_id = response.json()["id"]

pause_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/pause/",
)
self.assertEqual(pause_response.status_code, status.HTTP_400_BAD_REQUEST)

def test_pause_ended_experiment_returns_400(self):
response = self.client.post(
f"/api/projects/{self.team.id}/experiments/",
{
"name": "Pause Ended",
"feature_flag_key": "pause-ended-flag",
"start_date": "2024-01-01T10:00",
"end_date": "2024-01-15T10:00",
},
format="json",
)
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
experiment_id = response.json()["id"]

pause_response = self.client.post(
f"/api/projects/{self.team.id}/experiments/{experiment_id}/pause/",
)
self.assertEqual(pause_response.status_code, status.HTTP_400_BAD_REQUEST)


class TestExperimentAuxiliaryEndpoints(ClickhouseTestMixin, APILicensedTest):
def _generate_experiment(self, start_date="2024-01-01T10:23", extra_parameters=None):
Expand Down
12 changes: 0 additions & 12 deletions frontend/src/lib/utils/eventUsageLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,8 +468,6 @@ export const eventUsageLogic = kea<eventUsageLogicType>([
reportHelpButtonUsed: (help_type: HelpType) => ({ help_type }),
reportExperimentWizardStarted: (guideVisible: boolean) => ({ guideVisible }),
reportExperimentWizardGuideToggled: (visible: boolean, currentStep: string) => ({ visible, currentStep }),
reportExperimentPaused: (experiment: Experiment) => ({ experiment }),
reportExperimentResumed: (experiment: Experiment) => ({ experiment }),
reportExperimentStopped: (experiment: Experiment) => ({ experiment }),
reportExperimentReset: (experiment: Experiment) => ({ experiment }),
reportExperimentCreated: (
Expand Down Expand Up @@ -1399,16 +1397,6 @@ export const eventUsageLogic = kea<eventUsageLogicType>([
})
}
},
reportExperimentPaused: ({ experiment }) => {
posthog.capture('experiment paused', {
...getEventPropertiesForExperiment(experiment),
})
},
reportExperimentResumed: ({ experiment }) => {
posthog.capture('experiment resumed', {
...getEventPropertiesForExperiment(experiment),
})
},
reportExperimentStopped: ({ experiment }) => {
posthog.capture('experiment stopped', {
...getEventPropertiesForExperiment(experiment),
Expand Down
Loading
Loading