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
2 changes: 1 addition & 1 deletion frontend/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6337,7 +6337,7 @@ snapshots:
scenes-app-experiments--experiment-frequentist-two-variants--light:
hash: v1.k794b7964.ec64583fd2debf87822296f1c9fc10e73c1c6b7e31fe55f0ca974d3920f8bd14.Ch64gBgjo0wR0Ypvkhap6047UPjXBaWSQCNoKDOiNHc
scenes-app-experiments--experiment-stopped-with-conclusion--dark:
hash: v1.k794b7964.37e8127d5a33175889cf1da00e80998b1858fd3713fe5c0c1ec42ff0bb1da220.oIA6fAIpYfvfCAh_6TQs-uqDeMEONwTb3luZF1rzBs4
hash: v1.k794b7964.e347bbe9e217d7cbd915b09b362a9d995285c1f6492229235ce3dcef74e49f30.5xV99CaEDXp-xFACHoS4M9vQ6zFGOjestb-iW79F5Q4
scenes-app-experiments--experiment-stopped-with-conclusion--light:
hash: v1.k794b7964.793a4b6109b2c0ab65b07d9eaefd1c8862f12bfc7f33b2f452ad20044396b2a7.6B0Meq7yO75vBEoBmuP2dNl2oKTugzzr_IKTcJHhEi0
scenes-app-experiments--experiment-with-funnel-metric--dark:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,33 +24,18 @@ import { isLaunched } from '../experimentsLogic'
interface MinimalExperimentSummaryContext {
experiment_id: number | string
experiment_name: string
/** ISO8601 timestamp of when the frontend last refreshed the data */
frontend_last_refresh: string | null
}

function useExperimentSummaryMaxTool(): ReturnType<typeof useMaxTool> {
const { experiment, orderedPrimaryMetricsWithResults, primaryMetricsResults, secondaryMetricsResults } =
useValues(experimentLogic)

// Get the most recent last_refresh timestamp from metric results
const frontendLastRefresh = useMemo(() => {
const allResults = [...(primaryMetricsResults || []), ...(secondaryMetricsResults || [])]
const timestamps = allResults
.map((r) => r?.last_refresh)
.filter((t): t is string => typeof t === 'string')
.sort()
.reverse()
return timestamps[0] || null
}, [primaryMetricsResults, secondaryMetricsResults])
const { experiment, orderedPrimaryMetricsWithResults } = useValues(experimentLogic)

// Simplified context - backend will fetch full data using experiment_id
const maxToolContext = useMemo(
(): MinimalExperimentSummaryContext => ({
experiment_id: experiment.id,
experiment_name: experiment.name || 'Unnamed experiment',
frontend_last_refresh: frontendLastRefresh,
}),
[experiment.id, experiment.name, frontendLastRefresh]
[experiment.id, experiment.name]
)

const shouldShowMaxSummaryTool = useMemo(() => {
Expand Down
36 changes: 0 additions & 36 deletions products/experiments/backend/experiment_summary_data_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
from dataclasses import dataclass
from datetime import datetime
from typing import Any, TypeIs, Union
from zoneinfo import ZoneInfo

from django.conf import settings

Expand Down Expand Up @@ -57,12 +56,6 @@ class ExposureQueryResult:

MAX_CONCURRENT_EXPERIMENT_SUMMARY_QUERIES = 10

# This threshold is just to avoid minor discrepancies in timestamps.
# The check itself compares the frontend timestamp with the last
# backend refresh timestamp. So leaving the browser open while not
# having any new data on the backend will not trigger a warning.
FRESHNESS_THRESHOLD_SECONDS = 60

ExperimentMetricType = Union[
ExperimentMeanMetric, ExperimentFunnelMetric, ExperimentRatioMetric, ExperimentRetentionMetric
]
Expand Down Expand Up @@ -398,32 +391,3 @@ def process_metric_results(
pending_calculation=pending_calculation,
omitted_metric_count=omitted_metric_count,
)

def check_data_freshness(
self, frontend_last_refresh: str | None, backend_last_refresh: datetime | None
) -> str | None:
"""
Check if there's a significant difference between frontend and backend data freshness.
Returns a warning message if data might have changed, None otherwise.
"""
if not frontend_last_refresh or not backend_last_refresh:
return None

try:
frontend_time = datetime.fromisoformat(frontend_last_refresh.replace("Z", "+00:00"))
if frontend_time.tzinfo is None:
frontend_time = frontend_time.replace(tzinfo=ZoneInfo("UTC"))
if backend_last_refresh.tzinfo is None:
backend_last_refresh = backend_last_refresh.replace(tzinfo=ZoneInfo("UTC"))

time_diff = abs((backend_last_refresh - frontend_time).total_seconds())
if time_diff > FRESHNESS_THRESHOLD_SECONDS:
return (
f"**Note:** The experiment data has been updated since you loaded this page "
f"(approximately {int(time_diff / 60)} minutes ago). "
f"The summary below reflects the most current data."
)
except (ValueError, TypeError):
pass

return None
46 changes: 1 addition & 45 deletions products/experiments/backend/max_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@
from pydantic import BaseModel, Field
from rest_framework.exceptions import ValidationError

from posthog.schema import MaxExperimentMetricResult

from posthog.clickhouse.query_tagging import Product, tags_context
from posthog.event_usage import EventSource
from posthog.session_recordings.session_recording_api import list_recordings_from_query
Expand Down Expand Up @@ -214,7 +212,7 @@ def create_experiment() -> Experiment:
class ExperimentSummaryArgs(BaseModel):
experiment_id: int | None = Field(
default=None,
description="The ID of the experiment to summarize. Only required when results context is not already available (e.g. when the user asks about an experiment from chat).",
description="The ID of the experiment to summarize. Not needed when the user is viewing an experiment (taken from context).",
)


Expand All @@ -239,14 +237,6 @@ async def _arun_impl(self, experiment_id: int | None = None) -> tuple[str, dict[

resolved_experiment_id = int(resolved_experiment_id)

# When frontend context has metrics data, use it directly
if (
context.get("primary_metrics_results") is not None
or context.get("secondary_metrics_results") is not None
):
return await self._format_from_context(resolved_experiment_id, context)

# Otherwise, fetch data via the data service (agent-initiated call)
return await self._fetch_and_format(resolved_experiment_id)

except MaxToolAccessDeniedError:
Expand All @@ -262,40 +252,6 @@ async def _arun_impl(self, experiment_id: int | None = None) -> tuple[str, dict[
)
return f"Failed to summarize experiment: {str(e)}", {"error": "summary_failed", "details": str(e)}

async def _format_from_context(self, experiment_id: int, context: dict) -> tuple[str, dict[str, Any]]:
"""Format experiment data using pre-computed context from the frontend."""
experiment_context = ExperimentContext(team=self._team, experiment_id=experiment_id)
experiment = await experiment_context.aget_experiment()
if experiment is None:
return f"Experiment {experiment_id} not found", {"error": "not_found"}

await self.check_object_access(experiment, "viewer", resource="experiment", action="read")

try:
primary_metrics = [MaxExperimentMetricResult(**m) for m in context.get("primary_metrics_results", [])]
secondary_metrics = [MaxExperimentMetricResult(**m) for m in context.get("secondary_metrics_results", [])]
except Exception as e:
capture_exception(
e,
properties={
"team_id": self._team.id,
"user_id": self._user.id,
"experiment_id": experiment_id,
},
)
return f"Invalid experiment context: {str(e)}", {"error": "invalid_context", "details": str(e)}

exposures = context.get("exposures")

formatted_data = await experiment_context.format_experiment_results_data(
experiment,
exposures=exposures,
primary_metrics_results=primary_metrics,
secondary_metrics_results=secondary_metrics,
)

return self._build_result(experiment, formatted_data, primary_metrics, secondary_metrics)

async def _fetch_and_format(self, experiment_id: int) -> tuple[str, dict[str, Any]]:
"""Fetch experiment data from query runners and format it."""
experiment_context = ExperimentContext(team=self._team, experiment_id=experiment_id)
Expand Down
53 changes: 0 additions & 53 deletions products/experiments/backend/test/test_experiment_summary_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,59 +248,6 @@ async def acreate_experiment(self, name="test-experiment", feature_flag=None, wi
metrics_secondary=[],
)

@freeze_time("2020-01-10T12:00:00Z")
async def test_check_data_freshness_no_warning_when_recent(self):
data_service = ExperimentSummaryDataService(self.team, self.user)

# 30 seconds difference - well within the 1 minute threshold
frontend_refresh = "2020-01-10T11:59:00Z"
backend_refresh = datetime(2020, 1, 10, 11, 59, 30, tzinfo=ZoneInfo("UTC"))

warning = data_service.check_data_freshness(frontend_refresh, backend_refresh)
self.assertIsNone(warning)

@freeze_time("2020-01-10T12:00:00Z")
async def test_check_data_freshness_warning_when_stale(self):
data_service = ExperimentSummaryDataService(self.team, self.user)

frontend_refresh = "2020-01-10T10:00:00Z"
backend_refresh = datetime(2020, 1, 10, 11, 30, tzinfo=ZoneInfo("UTC"))

warning = data_service.check_data_freshness(frontend_refresh, backend_refresh)
assert warning is not None
self.assertIn("data has been updated", warning)

@freeze_time("2020-01-10T12:00:00Z")
async def test_check_data_freshness_warning_at_threshold_boundary(self):
data_service = ExperimentSummaryDataService(self.team, self.user)

# 61 seconds difference - just over the 1 minute (60 second) threshold
frontend_refresh = "2020-01-10T11:58:00Z"
backend_refresh = datetime(2020, 1, 10, 11, 59, 1, tzinfo=ZoneInfo("UTC"))

warning = data_service.check_data_freshness(frontend_refresh, backend_refresh)
assert warning is not None
self.assertIn("data has been updated", warning)

@freeze_time("2020-01-10T12:00:00Z")
async def test_check_data_freshness_no_warning_at_threshold_boundary(self):
data_service = ExperimentSummaryDataService(self.team, self.user)

# Exactly 60 seconds - at the threshold (not over), should NOT trigger warning
frontend_refresh = "2020-01-10T11:58:00Z"
backend_refresh = datetime(2020, 1, 10, 11, 59, 0, tzinfo=ZoneInfo("UTC"))

warning = data_service.check_data_freshness(frontend_refresh, backend_refresh)
self.assertIsNone(warning)

@freeze_time("2020-01-10T12:00:00Z")
async def test_check_data_freshness_handles_none_values(self):
data_service = ExperimentSummaryDataService(self.team, self.user)

self.assertIsNone(data_service.check_data_freshness(None, None))
self.assertIsNone(data_service.check_data_freshness("2020-01-10T10:00:00Z", None))
self.assertIsNone(data_service.check_data_freshness(None, datetime.now(ZoneInfo("UTC"))))

@freeze_time("2020-01-10T12:00:00Z")
async def test_fetch_experiment_data_with_mocked_query_runners(self):
experiment = await self.acreate_experiment(name="query-runner-test", with_metrics=True)
Expand Down
Loading
Loading