Skip to content

Commit 9bb69d5

Browse files
authored
Merge branch 'master' into fix/improve-compaction
2 parents 6fef117 + c4d6181 commit 9bb69d5

30 files changed

+1096
-161
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,7 @@ bin/mprocs*.local.yaml
118118

119119
# Debug output of our session summaries product
120120
.session-summaries
121+
122+
# Claude Code review outputs
123+
CODE_REVIEW.md
124+
CODE_DEBUGGING_SESSION.md

CODE_REVIEW.md

Lines changed: 0 additions & 71 deletions
This file was deleted.

dags/experiment_regular_metrics_timeseries.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020

2121
from posthog.hogql_queries.experiments.experiment_metric_fingerprint import compute_metric_fingerprint
2222
from posthog.hogql_queries.experiments.experiment_query_runner import ExperimentQueryRunner
23+
from posthog.hogql_queries.experiments.utils import get_experiment_stats_method
2324
from posthog.models.experiment import Experiment, ExperimentMetricResult
2425

2526
from dags.common import JobOwners
@@ -228,7 +229,7 @@ def _get_experiment_regular_metrics_timeseries(
228229
fingerprint = compute_metric_fingerprint(
229230
metric,
230231
experiment.start_date,
231-
experiment.stats_config,
232+
get_experiment_stats_method(experiment),
232233
experiment.exposure_criteria,
233234
)
234235

dags/experiment_saved_metrics_timeseries.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from posthog.hogql_queries.experiments.experiment_metric_fingerprint import compute_metric_fingerprint
2020
from posthog.hogql_queries.experiments.experiment_query_runner import ExperimentQueryRunner
21+
from posthog.hogql_queries.experiments.utils import get_experiment_stats_method
2122
from posthog.models.experiment import Experiment, ExperimentMetricResult
2223

2324
from dags.common import JobOwners
@@ -225,7 +226,7 @@ def _get_experiment_saved_metrics_timeseries(context: dagster.SensorEvaluationCo
225226
fingerprint = compute_metric_fingerprint(
226227
saved_metric.query,
227228
experiment.start_date,
228-
experiment.stats_config,
229+
get_experiment_stats_method(experiment),
229230
experiment.exposure_criteria,
230231
)
231232

ee/clickhouse/views/experiments.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from posthog.api.shared import UserBasicSerializer
2323
from posthog.api.utils import action
2424
from posthog.hogql_queries.experiments.experiment_metric_fingerprint import compute_metric_fingerprint
25+
from posthog.hogql_queries.experiments.utils import get_experiment_stats_method
2526
from posthog.models import Survey
2627
from posthog.models.activity_logging.activity_log import Detail, changes_between, log_activity
2728
from posthog.models.cohort import Cohort
@@ -131,7 +132,7 @@ def to_representation(self, instance):
131132
saved_metric["query"]["fingerprint"] = compute_metric_fingerprint(
132133
saved_metric["query"],
133134
instance.start_date,
134-
instance.stats_config,
135+
get_experiment_stats_method(instance),
135136
instance.exposure_criteria,
136137
)
137138

@@ -300,8 +301,9 @@ def create(self, validated_data: dict, *args: Any, **kwargs: Any) -> Experiment:
300301
for metric_field in ["metrics", "metrics_secondary"]:
301302
if metric_field in validated_data:
302303
for metric in validated_data[metric_field]:
304+
stats_method = "bayesian" if stats_config is None else stats_config.get("method", "bayesian")
303305
metric["fingerprint"] = compute_metric_fingerprint(
304-
metric, validated_data.get("start_date"), stats_config, validated_data.get("exposure_criteria")
306+
metric, validated_data.get("start_date"), stats_method, validated_data.get("exposure_criteria")
305307
)
306308

307309
experiment = Experiment.objects.create(
@@ -477,10 +479,11 @@ def update(self, instance: Experiment, validated_data: dict, *args: Any, **kwarg
477479
updated_metrics = []
478480
for metric in metrics:
479481
metric_copy = deepcopy(metric)
482+
stats_method = "bayesian" if stats_config is None else stats_config.get("method", "bayesian")
480483
metric_copy["fingerprint"] = compute_metric_fingerprint(
481484
metric_copy,
482485
start_date,
483-
stats_config,
486+
stats_method,
484487
exposure_criteria,
485488
)
486489
updated_metrics.append(metric_copy)

ee/clickhouse/views/test/test_clickhouse_experiments.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2695,9 +2695,9 @@ def test_metric_fingerprinting(self):
26952695
initial_metrics = response.json()["metrics"]
26962696

26972697
expected_initial_fingerprints = {
2698-
"mean": "fd431b59b934ba0d2dc650f54a37da146f7532eb7a93bbcf78b9cfdbfcad0d26",
2699-
"funnel": "d25af73845180f7327572cd9a2cd8745432f1eed5bfa6b0d35d0a368c7175038",
2700-
"ratio": "fb2f447c7b49bc8973fd3dfd5fb5cd4f33523b4ed2281f6bd33def84fe95dc62",
2698+
"mean": "1a5694c7330b8fb9f920fa6f1e6d871cc07e55e9d87447cb01a3384ed732c605",
2699+
"funnel": "bf2d01d67d7a1f608177b6f3a9971a6c263870d23ad5935054b5069286575a94",
2700+
"ratio": "3332b31c0ec0c8be353d5ed1f5740758affc9136d9721dba60434cbe104adb95",
27012701
}
27022702

27032703
for metric in initial_metrics:
@@ -2756,9 +2756,9 @@ def test_metric_fingerprinting(self):
27562756
updated_metrics = response.json()["metrics"]
27572757

27582758
expected_updated_fingerprints = {
2759-
"mean": "61e7a1f22f967262749b72ea086eae04fe2ced040eeb179d7d25a3be32a7ea31",
2760-
"funnel": "5c21352fe6e13282ca63b24f128ed7e0d8f0cc9d5988964b1bf89b11f4405b23",
2761-
"ratio": "9b81f680dbc8ff74978877e6eb3827bbafc881f7df80841648e5b97c85452977",
2759+
"mean": "d6a393e5456b71c16961c45e07eb17cb86e4f7972549033f9883c99430248c02",
2760+
"funnel": "9f7888cb2f7f9c3dac2b6482a964eef6911f97e376ed53305ed6653f7f70ce9b",
2761+
"ratio": "1b83a833a62ff9c2f01ba86be1f3e578b97749d3264e08ff9e76d863865e3ff3",
27622762
}
27632763

27642764
for metric in updated_metrics:

frontend/src/queries/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ import {
1717
ErrorTrackingQuery,
1818
EventsNode,
1919
EventsQuery,
20+
ExperimentFunnelsQuery,
21+
ExperimentMetric,
22+
ExperimentTrendsQuery,
2023
FunnelsQuery,
2124
GoalLine,
2225
GroupsQuery,
@@ -244,6 +247,12 @@ export function isErrorTrackingQuery(node?: Record<string, any> | null): node is
244247
return node?.kind === NodeKind.ErrorTrackingQuery
245248
}
246249

250+
export function isExperimentMetric(
251+
metric: ExperimentMetric | ExperimentFunnelsQuery | ExperimentTrendsQuery
252+
): metric is ExperimentMetric {
253+
return metric.kind === NodeKind.ExperimentMetric
254+
}
255+
247256
export function isErrorTrackingIssueCorrelationQuery(
248257
node?: Record<string, any> | null
249258
): node is ErrorTrackingIssueCorrelationQuery {

frontend/src/scenes/experiments/create/CreateExperiment.tsx

Lines changed: 80 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import type { Experiment } from '~/types'
2020

2121
import { ExperimentTypePanel } from './ExperimentTypePanel'
2222
import { ExposureCriteriaPanel } from './ExposureCriteriaPanel'
23+
import { MetricsPanel } from './MetricsPanel/MetricsPanel'
2324
import { VariantsPanel } from './VariantsPanel'
2425
import { createExperimentLogic } from './createExperimentLogic'
2526

@@ -44,8 +45,12 @@ const SHOW_TARGETING_PANEL = false
4445
export const CreateExperiment = ({ draftExperiment }: CreateExperimentProps): JSX.Element => {
4546
const { HogfettiComponent } = useHogfetti({ count: 100, duration: 3000 })
4647

47-
const { experiment, experimentErrors } = useValues(createExperimentLogic({ experiment: draftExperiment }))
48-
const { setExperimentValue } = useActions(createExperimentLogic({ experiment: draftExperiment }))
48+
const { experiment, experimentErrors, sharedMetrics } = useValues(
49+
createExperimentLogic({ experiment: draftExperiment })
50+
)
51+
const { setExperimentValue, setExperiment, setSharedMetrics } = useActions(
52+
createExperimentLogic({ experiment: draftExperiment })
53+
)
4954

5055
const [selectedPanel, setSelectedPanel] = useState<string | null>(null)
5156

@@ -171,9 +176,79 @@ export const CreateExperiment = ({ draftExperiment }: CreateExperimentProps): JS
171176
key: 'experiment-metrics',
172177
header: 'Metrics',
173178
content: (
174-
<div className="p-4">
175-
<span>Metrics Panel Goes Here</span>
176-
</div>
179+
<MetricsPanel
180+
experiment={experiment}
181+
sharedMetrics={sharedMetrics}
182+
onSaveMetric={(metric, context) => {
183+
const isNew = !experiment[context.field].some((m) => m.uuid === metric.uuid)
184+
185+
setExperiment({
186+
...experiment,
187+
[context.field]: isNew
188+
? [...experiment[context.field], metric]
189+
: experiment[context.field].map((m) =>
190+
m.uuid === metric.uuid ? metric : m
191+
),
192+
...(isNew && {
193+
[context.orderingField]: [
194+
...(experiment[context.orderingField] ?? []),
195+
metric.uuid,
196+
],
197+
}),
198+
})
199+
}}
200+
onDeleteMetric={(metric, context) => {
201+
if (metric.isSharedMetric) {
202+
setExperiment({
203+
...experiment,
204+
[context.orderingField]: (
205+
experiment[context.orderingField] ?? []
206+
).filter((uuid) => uuid !== metric.uuid),
207+
})
208+
setSharedMetrics({
209+
...sharedMetrics,
210+
[context.type]: sharedMetrics[context.type].filter(
211+
(m) => m.uuid !== metric.uuid
212+
),
213+
})
214+
return
215+
}
216+
217+
const metricIndex = experiment[context.field].findIndex(
218+
({ uuid }) => uuid === metric.uuid
219+
)
220+
if (metricIndex !== -1) {
221+
setExperiment({
222+
...experiment,
223+
[context.field]: experiment[context.field].filter(
224+
({ uuid }) => uuid !== metric.uuid
225+
),
226+
[context.orderingField]: (
227+
experiment[context.orderingField] ?? []
228+
).filter((uuid) => uuid !== metric.uuid),
229+
})
230+
}
231+
}}
232+
onSaveSharedMetrics={(metrics, context) => {
233+
setExperiment({
234+
...experiment,
235+
[context.orderingField]: [
236+
...(experiment[context.orderingField] ?? []),
237+
...metrics.map((metric) => metric.uuid),
238+
],
239+
saved_metrics: [
240+
...(experiment.saved_metrics ?? []),
241+
...metrics.map((metric) => ({
242+
saved_metric: metric.sharedMetricId,
243+
})),
244+
],
245+
})
246+
setSharedMetrics({
247+
...sharedMetrics,
248+
[context.type]: [...sharedMetrics[context.type], ...metrics],
249+
})
250+
}}
251+
/>
177252
),
178253
},
179254
]}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import clsx from 'clsx'
2+
3+
import { IconAreaChart } from 'lib/lemon-ui/icons'
4+
import { AddMetricButton } from 'scenes/experiments/Metrics/AddMetricButton'
5+
6+
import { MetricContext } from '~/scenes/experiments/Metrics/experimentMetricModalLogic'
7+
8+
export type EmptyMetricsPanelProps = {
9+
metricContext: MetricContext
10+
className?: string
11+
}
12+
13+
export const EmptyMetricsPanel = ({ metricContext, className }: EmptyMetricsPanelProps): JSX.Element => (
14+
<div className={clsx('border rounded bg-surface-primary pt-6 pb-8 text-secondary', className)}>
15+
<div className="flex flex-col items-center mx-auto deprecated-space-y-3">
16+
<IconAreaChart fontSize="30" />
17+
<div className="text-sm text-center text-balance max-w-sm">
18+
<p>
19+
{metricContext.type === 'secondary'
20+
? 'Secondary metrics provide additional context and help detect unintended side effects.'
21+
: 'Primary metrics represent the main goal of the experiment and directly measure if your hypothesis was successful.'}
22+
</p>
23+
</div>
24+
<AddMetricButton metricContext={metricContext} />
25+
</div>
26+
</div>
27+
)

0 commit comments

Comments
 (0)