Skip to content

fix(experiments): match frontend breakdownFilter wrapper for saved metric cache#60412

Merged
jurajmajerik merged 1 commit into
masterfrom
experiments/saved-metric-cache-key-merge
May 28, 2026
Merged

fix(experiments): match frontend breakdownFilter wrapper for saved metric cache#60412
jurajmajerik merged 1 commit into
masterfrom
experiments/saved-metric-cache-key-merge

Conversation

@jurajmajerik
Copy link
Copy Markdown
Contributor

@jurajmajerik jurajmajerik commented May 28, 2026

Problem

#60200 warmed the response cache from the daily Temporal recalc. For experiments using saved metrics, the warmed entries were never read: the frontend wraps every saved metric with a breakdownFilter field before posting to /query, and the activity wasn't doing the same wrap. Different wrap → different cache key.

Changes

In _calculate_experiment_saved_metric_sync, apply the same breakdownFilter wrap the frontend applies. Cache keys now match.

How did you test this code?

  • New test mirrors the inline-metric one but with a saved metric.
  • mypy clean on the changed files.

…metric cache

The frontend's sharedMetricsToExperimentMetrics merges link.metadata.breakdowns into a breakdownFilter on every saved metric before posting to /query, even when breakdowns are empty. The Temporal activity was using the raw saved_metric.query, producing a different cache key. Result: the warmed entry was never read.
@jurajmajerik jurajmajerik requested a review from a team May 28, 2026 11:03
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 28, 2026

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
posthog/temporal/experiments/test_cache_warming.py:105-195
**Test only covers empty breakdowns path**

The test wires up `metadata={"type": "primary"}` (no `breakdowns` key), so `saved_metric_metadata.get("breakdowns") or []` always produces `[]`. A saved metric whose link carries real breakdowns (e.g. `metadata={"type": "primary", "breakdowns": [{"property": "plan", "type": "person"}]}`) takes a slightly different code path — the cache key now includes those breakdown values and the activity must produce the same serialisation as the frontend. That path has no coverage, so a regression there would be silent.

Reviews (1): Last reviewed commit: "fix(experiments): match frontend's break..." | Re-trigger Greptile

Comment on lines +105 to +195
@freeze_time("2020-01-10T12:00:00Z")
def test_temporal_activity_warms_query_cache_for_saved_metric(self):
"""
Saved metrics take a different path: the frontend merges
ExperimentToSavedMetric.metadata.breakdowns into a breakdownFilter on
the metric before posting to /query. The activity must apply the same
merge or the cache key diverges. This test covers that path.
"""
feature_flag = self.create_feature_flag()
experiment = self.create_experiment(
feature_flag=feature_flag,
start_date=datetime(2020, 1, 1, 0, 0, 0),
)

metric = ExperimentMeanMetric(uuid=str(uuid4()), source=EventsNode(event="purchase"))
metric_dict = metric.model_dump(mode="json")

saved_metric = ExperimentSavedMetric.objects.create(
name="test saved metric",
team=self.team,
query=metric_dict,
created_by=self.user,
)
# No breakdowns on the link metadata — mirrors the production scenario
# where the frontend still wraps the metric with breakdownFilter: {breakdowns: []}.
ExperimentToSavedMetric.objects.create(
experiment=experiment,
saved_metric=saved_metric,
metadata={"type": "primary"},
)

feature_flag_property = f"$feature/{feature_flag.key}"
for variant in ("control", "test"):
for i in range(5):
distinct_id = f"user_{variant}_{i}"
_create_person(distinct_ids=[distinct_id], team_id=self.team.pk)
_create_event(
team=self.team,
event="$feature_flag_called",
distinct_id=distinct_id,
timestamp="2020-01-02T12:00:00Z",
properties={
feature_flag_property: variant,
"$feature_flag_response": variant,
"$feature_flag": feature_flag.key,
},
)
_create_event(
team=self.team,
event="purchase",
distinct_id=distinct_id,
timestamp="2020-01-02T12:01:00Z",
properties={feature_flag_property: variant},
)
flush_persons_and_events()

# Build the metric the way the frontend does for saved metrics:
# wrap with breakdownFilter merged from link.metadata.breakdowns
# (see sharedMetricsToExperimentMetrics in experimentLogic.tsx).
frontend_metric_dict = {
**metric_dict,
"breakdownFilter": {"breakdowns": []},
}
frontend_query = ExperimentQuery.model_validate(
{
"kind": "ExperimentQuery",
"experiment_id": experiment.id,
"metric": frontend_metric_dict,
}
)

cache.clear()

fingerprint = compute_metric_fingerprint(
metric_dict,
experiment.start_date,
get_experiment_stats_method(experiment),
experiment.exposure_criteria,
only_count_matured_users=experiment.only_count_matured_users,
)
with patch("posthog.temporal.experiments.activities.close_old_connections"):
activity_result = _calculate_experiment_saved_metric_sync.func( # type: ignore[attr-defined]
experiment.id, metric_dict["uuid"], fingerprint
)
self.assertTrue(activity_result.success, msg=activity_result.error_message)

warm_response = ExperimentQueryRunner(query=frontend_query, team=self.team).run(
execution_mode=ExecutionMode.RECENT_CACHE_CALCULATE_BLOCKING_IF_STALE,
)
assert isinstance(warm_response, CachedExperimentQueryResponse)
self.assertTrue(warm_response.is_cached)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Test only covers empty breakdowns path

The test wires up metadata={"type": "primary"} (no breakdowns key), so saved_metric_metadata.get("breakdowns") or [] always produces []. A saved metric whose link carries real breakdowns (e.g. metadata={"type": "primary", "breakdowns": [{"property": "plan", "type": "person"}]}) takes a slightly different code path — the cache key now includes those breakdown values and the activity must produce the same serialisation as the frontend. That path has no coverage, so a regression there would be silent.

Prompt To Fix With AI
This is a comment left during a code review.
Path: posthog/temporal/experiments/test_cache_warming.py
Line: 105-195

Comment:
**Test only covers empty breakdowns path**

The test wires up `metadata={"type": "primary"}` (no `breakdowns` key), so `saved_metric_metadata.get("breakdowns") or []` always produces `[]`. A saved metric whose link carries real breakdowns (e.g. `metadata={"type": "primary", "breakdowns": [{"property": "plan", "type": "person"}]}`) takes a slightly different code path — the cache key now includes those breakdown values and the activity must produce the same serialisation as the frontend. That path has no coverage, so a regression there would be silent.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@jurajmajerik jurajmajerik merged commit 3ca2866 into master May 28, 2026
225 checks passed
@jurajmajerik jurajmajerik deleted the experiments/saved-metric-cache-key-merge branch May 28, 2026 12:03
@deployment-status-posthog
Copy link
Copy Markdown

deployment-status-posthog Bot commented May 28, 2026

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-05-28 12:31 UTC Run
prod-us ✅ Deployed 2026-05-28 12:53 UTC Run
prod-eu ✅ Deployed 2026-05-28 12:55 UTC Run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants