-
Notifications
You must be signed in to change notification settings - Fork 1.6k
feat(experiments): support time units in conversion windows #30871
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PR Summary
The changes introduce multi-unit conversion window support for experiment metrics by refactoring both frontend and backend logic to replace the old hour-only configuration.
- Updated
/frontend/src/queries/schema/schema-general.ts
and/posthog/schema.py
to replacetime_window_hours
withconversion_window
andconversion_window_unit
. - Refactored
/frontend/src/scenes/experiments/ExperimentMetricForm.tsx
by extracting conversion window handling into a new component at/frontend/src/scenes/experiments/ExperimentMetricConversionWindowFilter.tsx
. - Modified
/posthog/hogql_queries/experiments/funnel_query_utils.py
and/posthog/hogql_queries/experiments/experiment_query_runner.py
to incorporate the new conversion window logic. - Added utility
conversion_window_to_seconds
in/posthog/hogql_queries/experiments/base_query_utils.py
with corresponding test updates.
9 file(s) reviewed, 1 comment(s)
Edit PR Review Bot Settings | Greptile
onChange={(value) => | ||
handleSetMetric({ | ||
...metric, | ||
conversion_window: value === 'experiment_duration' ? undefined : 14, | ||
conversion_window_unit: | ||
value === 'experiment_duration' ? undefined : FunnelConversionWindowTimeUnit.Day, | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
logic: Switching to time_window mode always resets conversion_window to 14 and unit to Day. Consider preserving the user's previous conversion_window_unit if available.
Size Change: +400 B (0%) Total Size: 13.2 MB ℹ️ View Unchanged
|
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
📸 UI snapshots have been updated2 snapshot changes in total. 0 added, 2 modified, 0 deleted:
Triggered by this commit. |
📸 UI snapshots have been updated2 snapshot changes in total. 0 added, 2 modified, 0 deleted:
Triggered by this commit. |
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
const options: LemonSelectOption<FunnelConversionWindowTimeUnit>[] = Object.keys(TIME_INTERVAL_BOUNDS).map( | ||
(unit) => ({ | ||
label: capitalizeFirstLetter(pluralize(metric.conversion_window ?? 72, unit, `${unit}s`, false)), | ||
value: unit as FunnelConversionWindowTimeUnit, | ||
}) | ||
) | ||
const intervalBounds = TIME_INTERVAL_BOUNDS[metric.conversion_window_unit ?? FunnelConversionWindowTimeUnit.Day] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
⛏️ Complete and absolute nitpick: you could extract these as functions that take an ExperimentMetric
argument. Then, they could also be moved out into FunnelConversionWindowFilter
scene, removing the TIME_INTERVAL_BOUNDS
import (hiding that away).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As the functions are only used here and are pretty straight forward, I think I prefer leaving them there for now.
)} | ||
</div> | ||
</div> | ||
<ExperimentMetricConversionWindowFilter metric={metric} handleSetMetric={handleSetMetric} /> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
def conversion_window_to_seconds(conversion_window: int, conversion_window_unit: FunnelConversionWindowTimeUnit) -> int: | ||
match conversion_window_unit: | ||
case FunnelConversionWindowTimeUnit.SECOND: | ||
multiplier = 1 | ||
case FunnelConversionWindowTimeUnit.MINUTE: | ||
multiplier = 60 | ||
case FunnelConversionWindowTimeUnit.HOUR: | ||
multiplier = 60 * 60 | ||
case FunnelConversionWindowTimeUnit.DAY: | ||
multiplier = 24 * 60 * 60 | ||
case FunnelConversionWindowTimeUnit.WEEK: | ||
multiplier = 7 * 24 * 60 * 60 | ||
case FunnelConversionWindowTimeUnit.MONTH: | ||
multiplier = 30 * 24 * 60 * 60 | ||
case _: | ||
raise ValueError(f"Unsupported conversion window unit: {conversion_window_unit}") | ||
|
||
return conversion_window * multiplier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When I see a switch
/case,
I think could it be better?. We could use if
and make this hilariously recursive:
def conversion_window_to_seconds(conversion_window: int, conversion_window_unit: FunnelConversionWindowTimeUnit) -> int:
# Base case
if conversion_window_unit == FunnelConversionWindowTimeUnit.SECOND:
return conversion_window
elif conversion_window_unit == FunnelConversionWindowTimeUnit.MINUTE:
return conversion_window_to_seconds(conversion_window * 60, FunnelConversionWindowTimeUnit.SECOND)
elif conversion_window_unit == FunnelConversionWindowTimeUnit.HOUR:
return conversion_window_to_seconds(conversion_window * 60, FunnelConversionWindowTimeUnit.MINUTE)
elif conversion_window_unit == FunnelConversionWindowTimeUnit.DAY:
return conversion_window_to_seconds(conversion_window * 24, FunnelConversionWindowTimeUnit.HOUR)
elif conversion_window_unit == FunnelConversionWindowTimeUnit.WEEK:
return conversion_window_to_seconds(conversion_window * 7, FunnelConversionWindowTimeUnit.DAY)
elif conversion_window_unit == FunnelConversionWindowTimeUnit.MONTH:
return conversion_window_to_seconds(conversion_window * 30, FunnelConversionWindowTimeUnit.DAY)
else:
raise ValueError(f"Unsupported conversion window unit: {conversion_window_unit}")
or try to make it shorter with a map
:
def conversion_window_to_seconds(conversion_window: int, conversion_window_unit: FunnelConversionWindowTimeUnit) -> int:
unit_multipliers = {
FunnelConversionWindowTimeUnit.SECOND: 1,
FunnelConversionWindowTimeUnit.MINUTE: 60,
FunnelConversionWindowTimeUnit.HOUR: 3600,
FunnelConversionWindowTimeUnit.DAY: 86400,
FunnelConversionWindowTimeUnit.WEEK: 604800,
FunnelConversionWindowTimeUnit.MONTH: 2592000,
}
if conversion_window_unit not in unit_multipliers:
raise ValueError(f"Unsupported conversion window unit: {conversion_window_unit}")
return conversion_window * unit_multipliers[conversion_window_unit]
and then I realize that sometimes a case
is 🆗. But a recursive solution could be fun. Just imagine the face of someone finding that code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Recursion would be fun. But it felt a little to verbose in this case. I went with the map solution, that felt best. 7656d52
(#30871)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was hoping for some good ol' recursion :(
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📸 UI snapshots have been updated3 snapshot changes in total. 0 added, 3 modified, 0 deleted:
Triggered by this commit. |
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
109e6b7
to
1167cc2
Compare
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
📸 UI snapshots have been updated1 snapshot changes in total. 0 added, 1 modified, 0 deleted:
Triggered by this commit. |
Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
Problem
We currently only support specifying hours. Cumbersome to enter days f.ex.
Changes
How did you test this code?