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
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,28 @@ export function MetricBreakdownAttribution({
metric: ExperimentFunnelMetric
onChange: (attributionType: BreakdownAttributionType, attributionValue?: number) => void
}): JSX.Element {
const { funnel_order_type } = metric
const { breakdownAttributionType, breakdownAttributionValue, funnel_order_type } = metric
const stepCount = metric.series?.length || 0

/**
* hardcoded for now, this have to be configured at the metric level.
* We need to construct the current value shown based on the attribution type and the funnel order.
* we default to `first touch`, but if the funnel is unordered, insterad of the step choice
* we show the "any step" option.
* For the step attribution type, we need to show the value.
*/
const breakdownAttributionType = BreakdownAttributionType.FirstTouch
const breakdownAttributionValue = 0
const currentValue: BreakdownAttributionType | `${BreakdownAttributionType.Step}/${number}` =
!breakdownAttributionType
? BreakdownAttributionType.FirstTouch
: breakdownAttributionType === BreakdownAttributionType.Step
? funnel_order_type === StepOrderValue.UNORDERED
? BreakdownAttributionType.Step
: `${breakdownAttributionType}/${breakdownAttributionValue || 0}`
Comment thread
rodrigoi marked this conversation as resolved.
Resolved
: breakdownAttributionType

return (
<LemonSelect
size="small"
value={breakdownAttributionType}
value={currentValue}
placeholder="Attribution"
options={[
{ value: BreakdownAttributionType.FirstTouch, label: 'First touchpoint' },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import {
isSignificant,
isWinning,
} from '~/scenes/experiments/MetricsView/shared/utils'
import { Experiment, InsightType } from '~/types'
import { Experiment, InsightType, BreakdownAttributionType } from '~/types'

import { ChartCell } from './ChartCell'
import {
Expand Down Expand Up @@ -87,6 +87,7 @@ interface CollapsibleBreakdownSectionProps {
colors: ReturnType<typeof useChartColors>
scale: ReturnType<typeof useAxisScale>
onRemoveBreakdown: (index: number) => void
onBreakdownAttributionChange: (attributionType: BreakdownAttributionType, attributionValue?: number) => void
onBreakdownLimitChange: (breakdownLimit: number) => void
onRetry: () => void
query?: Record<string, any>
Expand All @@ -107,6 +108,7 @@ function CollapsibleBreakdownSection({
colors,
scale,
onRemoveBreakdown,
onBreakdownAttributionChange,
onBreakdownLimitChange,
onRetry,
query,
Expand Down Expand Up @@ -171,7 +173,7 @@ function CollapsibleBreakdownSection({
<MetricBreakdowns
metric={metric}
onRemoveBreakdown={onRemoveBreakdown}
onAttributionChange={() => {}}
onAttributionChange={onBreakdownAttributionChange}
onBreakdownLimitChange={onBreakdownLimitChange}
/>
</div>
Expand Down Expand Up @@ -527,6 +529,7 @@ interface MetricRowGroupProps {
onDeleteMetric?: () => void
onBreakdownChange: (breakdown: Breakdown) => void
onRemoveBreakdown: (index: number) => void
onBreakdownAttributionChange: (attributionType: BreakdownAttributionType, attributionValue?: number) => void
onBreakdownLimitChange: (breakdownLimit: number) => void
error?: any
isLoading?: boolean
Expand All @@ -551,6 +554,7 @@ export function MetricRowGroup({
onDeleteMetric,
onBreakdownChange,
onRemoveBreakdown,
onBreakdownAttributionChange,
onBreakdownLimitChange,
error,
isLoading,
Expand Down Expand Up @@ -902,7 +906,7 @@ export function MetricRowGroup({
metric={metric}
isAlternatingRow={isAlternatingRow}
onRemoveBreakdown={onRemoveBreakdown}
onAttributionChange={() => {}}
onAttributionChange={onBreakdownAttributionChange}
onBreakdownLimitChange={onBreakdownLimitChange}
/>
)}
Expand Down Expand Up @@ -1236,6 +1240,7 @@ export function MetricRowGroup({
colors={colors}
scale={scale}
onRemoveBreakdown={onRemoveBreakdown}
onBreakdownAttributionChange={onBreakdownAttributionChange}
onBreakdownLimitChange={onBreakdownLimitChange}
onRetry={handleRetry}
query={debugQuery}
Expand Down
12 changes: 12 additions & 0 deletions frontend/src/scenes/experiments/MetricsView/new/MetricsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export function MetricsTable({
duplicateSharedMetricAsInlineMetric,
updateExperimentMetrics,
updateMetricBreakdown,
updateMetricBreakdownAttribution,
updateMetricBreakdownLimit,
removeMetricBreakdown,
removeMetric,
Expand Down Expand Up @@ -279,6 +280,17 @@ export function MetricsTable({

removeMetricBreakdown(metric.uuid, index, breakdown)
}}
onBreakdownAttributionChange={(attributionType, attributionValue) => {
if (!metric.uuid) {
return
}

updateMetricBreakdownAttribution(
metric.uuid,
attributionType,
attributionValue
)
}}
onBreakdownLimitChange={(breakdownLimit) => {
if (!metric.uuid) {
return
Expand Down
143 changes: 141 additions & 2 deletions frontend/src/scenes/experiments/experimentLogic.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import {
ExperimentTrendsQuery,
FunnelsQuery,
InsightVizNode,
isExperimentFunnelMetric,
NodeKind,
ProductIntentContext,
ProductKey,
Expand Down Expand Up @@ -464,7 +465,18 @@ const sharedMetricsToExperimentMetrics = (
.filter(({ metadata }) => metadata.type === type)
.map(({ query, metadata }) => ({
...query,
/**
* for funnel shared metrics, merge the breakdown attribution from metadata into the query
*/
...(metadata?.breakdownAttributionType !== undefined &&
isExperimentFunnelMetric(query) && {
breakdownAttributionType: metadata.breakdownAttributionType,
breakdownAttributionValue: metadata.breakdownAttributionValue,
}),
// Merge breakdowns from metadata into the query
/**
* merge breakdown limits from metadata into the query
*/
breakdownFilter: {
...query?.breakdownFilter,
breakdowns: metadata?.breakdowns || [],
Expand All @@ -483,6 +495,8 @@ export type ExperimentSavedMetric = {
metadata: {
type: 'primary' | 'secondary'
breakdowns?: Breakdown[]
breakdownAttributionType?: BreakdownAttributionType
breakdownAttributionValue?: number
breakdown_limit?: number
}
created_at: string
Expand Down Expand Up @@ -1315,6 +1329,11 @@ export interface experimentLogicActions {
breakdown: Breakdown
uuid: string
}
updateMetricBreakdownAttribution: (
uuid: string,
attributionType: BreakdownAttributionType,
attributionValue?: number
) => { attributionType: BreakdownAttributionType; attributionValue: number | undefined; uuid: string }
updateMetricBreakdownLimit: (
uuid: string,
breakdownLimit: number
Expand Down Expand Up @@ -1718,6 +1737,15 @@ export const experimentLogic = kea<experimentLogicType>([
) => ({ isSecondary, orderedUuids, removedUuids, movedUuids }),
updateMetricBreakdown: (uuid: string, breakdown: Breakdown) => ({ uuid, breakdown }),
removeMetricBreakdown: (uuid: string, index: number, breakdown: Breakdown) => ({ uuid, index, breakdown }),
updateMetricBreakdownAttribution: (
uuid: string,
attributionType: BreakdownAttributionType,
attributionValue?: number
) => ({
uuid,
attributionType,
attributionValue,
}),
updateMetricBreakdownLimit: (uuid: string, breakdownLimit: number) => ({ uuid, breakdownLimit }),
// METRICS RESULTS
clearMetricsResults: true,
Expand Down Expand Up @@ -2078,6 +2106,74 @@ export const experimentLogic = kea<experimentLogicType>([
[metricsKey]: metrics,
}
},
updateMetricBreakdownAttribution: (state, { uuid, attributionType, attributionValue }) => {
/**
* if the uuid is a shared metric, update saved_metrics metadata of the many to many
* relationship, so the breakdown limit is exclusive to this experiment
*/
const savedMetrics: ExperimentSavedMetric[] = [...(state?.saved_metrics || [])]
const savedMetricIndex = savedMetrics.findIndex(
({ query: { uuid: savedMetricUuid } }) => savedMetricUuid === uuid
)

/**
* if saved metric found...
*/
if (savedMetricIndex !== -1) {
const savedMetric = savedMetrics[savedMetricIndex]
savedMetrics[savedMetricIndex] = {
...savedMetric,
metadata: {
...savedMetric.metadata,
breakdownAttributionType: attributionType,
breakdownAttributionValue: attributionValue,
},
}

/**
* return the experiment state with the updated saved metrics.
*/
return {
...state,
saved_metrics: savedMetrics,
}
}

/**
* figure out if it's a primary or secondary metric
*/
const metricsKey =
(state?.metrics || ([] as ExperimentMetric[])).findIndex((m) => m.uuid === uuid) > -1
? 'metrics'
: 'metrics_secondary'

/**
* get the metrics group and find the metric index by uuid.
* bail if not found
*/
const metrics = [...(state?.[metricsKey] || [])]
const targetIndex = metrics.findIndex((m) => m.uuid === uuid)
if (targetIndex === -1) {
return state
}

/**
* reconstruct the metric with the updated breakdown attribution
*/
metrics[targetIndex] = {
...metrics[targetIndex],
breakdownAttributionType: attributionType,
breakdownAttributionValue: attributionValue,
} as ExperimentMetric

/**
* return the updated experiment state
*/
return {
...state,
[metricsKey]: metrics,
}
},
updateMetricBreakdownLimit: (state, { uuid, breakdownLimit }) => {
/**
* if the uuid is a shared metric, update saved_metrics metadata of the many to many
Expand Down Expand Up @@ -3676,17 +3772,52 @@ export const experimentLogic = kea<experimentLogicType>([
actions.loadSecondaryMetricsResults(true)
}
},
updateMetricBreakdownLimit: async ({ uuid }) => {
updateMetricBreakdownAttribution: async ({ uuid }) => {
/**
* build the experiment payload with all experiment metrics
*/
const updatePayload: Partial<Experiment> & { update_feature_flag_params?: boolean } = {
metrics: values.experiment.metrics,
metrics_secondary: values.experiment.metrics_secondary,
update_feature_flag_params: false,
}

/**
* for shared metrics, we save the breakdown attribution on the many to many relationship metadata
*/
const savedMetrics: ExperimentSavedMetric[] = [...(values.experiment.saved_metrics || [])]
const sharedMetric = savedMetrics.find(({ query: { uuid: savedMetricUuid } }) => savedMetricUuid === uuid)
if (sharedMetric) {
updatePayload.saved_metrics_ids = savedMetrics.map(({ saved_metric, metadata }) => ({
id: saved_metric,
metadata,
}))
}

/**
* find if the updated metris is primary
* guard against failed persist calling recalculations by awaiting the experiment save
*/
await asyncActions.updateExperiment(updatePayload)

/**
* figure out if it's a primary metric
*/
const isPrimary = sharedMetric
? sharedMetric.metadata.type === 'primary'
: values.experiment.metrics.some((m) => m.uuid === uuid)

/**
* updating a breakdown limit triggers a recalculation.
*/
if (values.featureFlags[FEATURE_FLAGS.EXPERIMENTS_METRICS_RECALCULATION]) {
actions.refreshExperimentResults(true, 'config_change')
} else if (isPrimary) {
actions.loadPrimaryMetricsResults(true)
} else {
actions.loadSecondaryMetricsResults(true)
}
},
updateMetricBreakdownLimit: async ({ uuid }) => {
/**
* build the update payload with all experiment metrics
*/
Expand All @@ -3699,6 +3830,8 @@ export const experimentLogic = kea<experimentLogicType>([
/**
* for shared metrics, we save the breakdown limit on the many to many relationship metadata
*/
const savedMetrics: ExperimentSavedMetric[] = [...(values.experiment.saved_metrics || [])]
const sharedMetric = savedMetrics.find(({ query: { uuid: savedMetricUuid } }) => savedMetricUuid === uuid)
if (sharedMetric) {
updatePayload.saved_metrics_ids = savedMetrics.map(({ saved_metric, metadata }) => ({
id: saved_metric,
Expand All @@ -3711,6 +3844,12 @@ export const experimentLogic = kea<experimentLogicType>([
*/
await asyncActions.updateExperiment(updatePayload)

/**
* find if the updated metris is primary
*/
const isPrimary = sharedMetric
? sharedMetric.metadata.type === 'primary'
: values.experiment.metrics.some((m) => m.uuid === uuid)
/**
* updating a breakdown limit triggers a recalculation.
*/
Expand Down
21 changes: 20 additions & 1 deletion frontend/src/scenes/experiments/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1198,7 +1198,16 @@ export function getOrderedMetricsWithResults(
name: sharedMetric.name,
sharedMetricId: sharedMetric.saved_metric,
isSharedMetric: true,
// Merge breakdowns from metadata into breakdownFilter
/**
* Merge per-experiment breakdown attribution from metadata into the query
*/
...(sharedMetric.metadata?.breakdownAttributionType !== undefined && {
breakdownAttributionType: sharedMetric.metadata.breakdownAttributionType,
breakdownAttributionValue: sharedMetric.metadata.breakdownAttributionValue,
}),
/**
* Merge breakdowns from metadata into breakdownFilter
*/
breakdownFilter: {
...sharedMetric.query?.breakdownFilter,
breakdowns: sharedMetric.metadata?.breakdowns || [],
Expand Down Expand Up @@ -1408,6 +1417,16 @@ export const metricResults =
name: sharedMetric.name,
sharedMetricId: sharedMetric.saved_metric,
isSharedMetric: true,
/**
* Merge per-experiment breakdown attribution from metadata into the query
*/
...(sharedMetric.metadata?.breakdownAttributionType !== undefined && {
breakdownAttributionType: sharedMetric.metadata.breakdownAttributionType,
breakdownAttributionValue: sharedMetric.metadata.breakdownAttributionValue,
}),
/**
* Merge breakdowns from metadata into breakdownFilter
*/
breakdownFilter: {
...sharedMetric.query?.breakdownFilter,
breakdowns: sharedMetric.metadata?.breakdowns || [],
Expand Down
Loading