Skip to content
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

feat(experiments): Tag support for shared metrics #28061

Merged
merged 37 commits into from
Jan 31, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
6ac68ad
Tag support for shared metrics
danielbachhuber Jan 29, 2025
e1ce7c3
Memoize
danielbachhuber Jan 29, 2025
99207c8
Update query snapshots
github-actions[bot] Jan 29, 2025
7cd8741
Update UI snapshots for `chromium` (2)
github-actions[bot] Jan 29, 2025
8d8c0a6
See if this unbreaks CI
danielbachhuber Jan 29, 2025
b4e327b
More precise import
danielbachhuber Jan 29, 2025
c31ffa5
Ditch memoization; unnecessary complexity
danielbachhuber Jan 29, 2025
59cc36d
Rename migration
danielbachhuber Jan 29, 2025
ca82cd1
Fix type issues
danielbachhuber Jan 29, 2025
2543f93
Update UI snapshots for `chromium` (2)
github-actions[bot] Jan 29, 2025
b2ef2df
Attempt to fix CI again
danielbachhuber Jan 29, 2025
46b64d2
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 29, 2025
889f548
Restore original migration
danielbachhuber Jan 30, 2025
203011d
Try a simplified migration
danielbachhuber Jan 30, 2025
d25c407
Production only has a check constraint, not a unique constraint
danielbachhuber Jan 30, 2025
dab9062
Try nullifying `unique_together`
danielbachhuber Jan 30, 2025
a6af645
Use `<LemonButton>` to make it look more like a button
danielbachhuber Jan 30, 2025
7b9c05d
Filter out dupes
danielbachhuber Jan 30, 2025
ef9590c
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 30, 2025
2b5420f
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 30, 2025
a0835ea
Try modifying Django's state directly
danielbachhuber Jan 30, 2025
bc6572a
Use `AddConstraintNotValid()` to avoid locking the table
danielbachhuber Jan 30, 2025
11e1b71
Merge branch 'master' into experiments/tagged-shared-metrics
danielbachhuber Jan 30, 2025
1cec059
Merge branch 'master' into experiments/tagged-shared-metrics
danielbachhuber Jan 30, 2025
cf13268
Fix type issue
danielbachhuber Jan 30, 2025
94d129c
Fix type issue
danielbachhuber Jan 30, 2025
dcfb518
Reset migrations again
danielbachhuber Jan 30, 2025
f34e0d4
Restore originally generated migration
danielbachhuber Jan 30, 2025
e0ff8d7
Restore comment
danielbachhuber Jan 30, 2025
b97a752
Split migration into separate statements to avoid locking
danielbachhuber Jan 30, 2025
4c2115b
Restore state definitions
danielbachhuber Jan 30, 2025
ca4e927
Swap statement order
danielbachhuber Jan 30, 2025
3c5c531
Add original SQL
danielbachhuber Jan 30, 2025
71505ac
Fix constraints
danielbachhuber Jan 30, 2025
6978977
Merge branch 'master' into experiments/tagged-shared-metrics
danielbachhuber Jan 30, 2025
2145230
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 30, 2025
c58b35c
Update UI snapshots for `chromium` (1)
github-actions[bot] Jan 31, 2025
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
4 changes: 3 additions & 1 deletion ee/clickhouse/views/experiment_saved_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from posthog.api.routing import TeamAndOrgViewSetMixin
from posthog.api.shared import UserBasicSerializer
from posthog.api.tagged_item import TaggedItemSerializerMixin
from posthog.models.experiment import ExperimentSavedMetric, ExperimentToSavedMetric
from posthog.schema import ExperimentFunnelsQuery, ExperimentTrendsQuery

Expand All @@ -30,7 +31,7 @@ class Meta:
]


class ExperimentSavedMetricSerializer(serializers.ModelSerializer):
class ExperimentSavedMetricSerializer(TaggedItemSerializerMixin, serializers.ModelSerializer):
created_by = UserBasicSerializer(read_only=True)

class Meta:
Expand All @@ -43,6 +44,7 @@ class Meta:
"created_by",
"created_at",
"updated_at",
"tags",
]
read_only_fields = [
"id",
Expand Down
3 changes: 2 additions & 1 deletion ee/clickhouse/views/test/test_experiment_saved_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ def test_create_update_experiment_saved_metrics(self) -> None:
"series": [{"kind": "EventsNode", "event": "$pageview"}],
},
},
"tags": ["tag1"],
},
format="json",
)
Expand All @@ -124,7 +125,7 @@ def test_create_update_experiment_saved_metrics(self) -> None:
},
)
self.assertEqual(response.json()["created_by"]["id"], self.user.pk)

self.assertEqual(response.json()["tags"], ["tag1"])
# Generate experiment to have saved metric
ff_key = "a-b-tests"
response = self.client.post(
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 50 additions & 3 deletions frontend/src/scenes/experiments/Metrics/SharedMetricModal.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { LemonBanner, LemonButton, LemonModal, Link } from '@posthog/lemon-ui'
import { LemonBanner, LemonButton, LemonLabel, LemonModal, Link } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { IconOpenInNew } from 'lib/lemon-ui/icons'
import { LemonTable } from 'lib/lemon-ui/LemonTable'
import { useState } from 'react'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'

import { Experiment } from '~/types'
import { NodeKind } from '~/queries/schema/schema-general'
import { AvailableFeature, Experiment } from '~/types'

import { experimentLogic } from '../experimentLogic'
import { MetricDisplayFunnels, MetricDisplayTrends } from '../ExperimentView/components'
Expand Down Expand Up @@ -39,6 +42,8 @@ export function SharedMetricModal({
const [selectedMetricIds, setSelectedMetricIds] = useState<SharedMetric['id'][]>([])
const mode = editingSharedMetricId ? 'edit' : 'create'

const { hasAvailableFeature } = useValues(userLogic)

if (!sharedMetrics) {
return <></>
}
Expand Down Expand Up @@ -66,6 +71,15 @@ export function SharedMetricModal({
!experiment.saved_metrics.some((savedMetric) => savedMetric.saved_metric === metric.id)
)

const availableTags = Array.from(
new Set(
availableSharedMetrics
.filter((metric: SharedMetric) => metric.tags)
.flatMap((metric: SharedMetric) => metric.tags)
.filter(Boolean)
)
).sort() as string[]

return (
<LemonModal
isOpen={isOpen}
Expand Down Expand Up @@ -123,6 +137,27 @@ export function SharedMetricModal({
} already in use with this experiment.`}
</LemonBanner>
)}
{hasAvailableFeature(AvailableFeature.TAGGING) && availableTags.length > 0 && (
<div className="flex flex-wrap gap-2">
<LemonLabel>Quick select:</LemonLabel>
{availableTags.map((tag: string, index: number) => (
<LemonButton
key={index}
size="xsmall"
type="secondary"
onClick={() => {
setSelectedMetricIds(
availableSharedMetrics
.filter((metric: SharedMetric) => metric.tags?.includes(tag))
.map((metric: SharedMetric) => metric.id)
)
}}
>
{tag}
</LemonButton>
))}
</div>
)}
<LemonTable
dataSource={availableSharedMetrics}
columns={[
Expand Down Expand Up @@ -155,11 +190,23 @@ export function SharedMetricModal({
dataIndex: 'description',
key: 'description',
},
...(hasAvailableFeature(AvailableFeature.TAGGING)
? [
{
title: 'Tags',
dataIndex: 'tags' as keyof SharedMetric,
key: 'tags',
render: (_: any, metric: SharedMetric) => (
<ObjectTags tags={metric.tags || []} staticOnly />
),
},
]
: []),
{
title: 'Type',
key: 'type',
render: (_, metric: SharedMetric) =>
metric.query.kind.replace('Experiment', '').replace('Query', ''),
metric.query.kind === NodeKind.ExperimentTrendsQuery ? 'Trend' : 'Funnel',
},
]}
footer={
Expand Down
20 changes: 20 additions & 0 deletions frontend/src/scenes/experiments/SharedMetrics/SharedMetric.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { IconCheckCircle } from '@posthog/icons'
import { LemonButton, LemonDialog, LemonInput, LemonLabel, Spinner } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { SceneExport } from 'scenes/sceneTypes'

import { themeLogic } from '~/layout/navigation-3000/themeLogic'
import { tagsModel } from '~/models/tagsModel'
import { NodeKind } from '~/queries/schema/schema-general'

import { getDefaultFunnelsMetric, getDefaultTrendsMetric } from '../utils'
Expand All @@ -25,6 +27,8 @@ export function SharedMetric(): JSX.Element {
useActions(sharedMetricLogic)
const { isDarkModeOn } = useValues(themeLogic)

const { tags: allExistingTags } = useValues(tagsModel)

if (!sharedMetric || !sharedMetric.query) {
return (
<div className="fixed inset-0 flex justify-center items-center">
Expand Down Expand Up @@ -104,6 +108,22 @@ export function SharedMetric(): JSX.Element {
}}
/>
</div>
<div className="mb-4">
<LemonLabel>Tags</LemonLabel>
<div className="mt-2">
<ObjectTags
tags={sharedMetric.tags || []}
onChange={(newTags) => {
setSharedMetric({
tags: newTags,
})
}}
saving={false}
Copy link
Contributor

Choose a reason for hiding this comment

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

style: saving prop is hardcoded to false - should reflect actual saving state from sharedMetricLogic to show proper loading state

tagsAvailable={allExistingTags}
data-attr="shared-metric-tags"
/>
</div>
</div>
{sharedMetric.query.kind === NodeKind.ExperimentTrendsQuery ? (
<SharedTrendsMetricForm />
) : (
Expand Down
102 changes: 63 additions & 39 deletions frontend/src/scenes/experiments/SharedMetrics/SharedMetrics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,17 @@ import { IconArrowLeft, IconPencil } from '@posthog/icons'
import { LemonBanner, LemonButton, LemonTable, LemonTableColumn, LemonTableColumns } from '@posthog/lemon-ui'
import { useValues } from 'kea'
import { router } from 'kea-router'
import { ObjectTags } from 'lib/components/ObjectTags/ObjectTags'
import { createdByColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
import { createdAtColumn } from 'lib/lemon-ui/LemonTable/columnUtils'
import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink'
import stringWithWBR from 'lib/utils/stringWithWBR'
import { SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'
import { userLogic } from 'scenes/userLogic'

import { NodeKind } from '~/queries/schema'
import { AvailableFeature } from '~/types'

import { SharedMetric } from './sharedMetricLogic'
import { sharedMetricsLogic } from './sharedMetricsLogic'
Expand All @@ -17,48 +22,67 @@ export const scene: SceneExport = {
logic: sharedMetricsLogic,
}

const columns: LemonTableColumns<SharedMetric> = [
{
key: 'name',
title: 'Name',
render: (_, sharedMetric) => {
return (
<LemonTableLink
to={sharedMetric.id ? urls.experimentsSharedMetric(sharedMetric.id) : undefined}
title={stringWithWBR(sharedMetric.name, 17)}
/>
)
},
},
{
key: 'description',
title: 'Description',
dataIndex: 'description',
},
createdByColumn<SharedMetric>() as LemonTableColumn<SharedMetric, keyof SharedMetric | undefined>,
createdAtColumn<SharedMetric>() as LemonTableColumn<SharedMetric, keyof SharedMetric | undefined>,
{
key: 'actions',
title: 'Actions',
render: (_, sharedMetric) => {
return (
<LemonButton
className="max-w-72"
type="secondary"
size="xsmall"
icon={<IconPencil />}
onClick={() => {
router.actions.push(urls.experimentsSharedMetric(sharedMetric.id))
}}
/>
)
},
},
]

export function SharedMetrics(): JSX.Element {
const { sharedMetrics, sharedMetricsLoading } = useValues(sharedMetricsLogic)

const { hasAvailableFeature } = useValues(userLogic)

const columns: LemonTableColumns<SharedMetric> = [
{
key: 'name',
title: 'Name',
render: (_, sharedMetric) => {
return (
<LemonTableLink
to={sharedMetric.id ? urls.experimentsSharedMetric(sharedMetric.id) : undefined}
title={stringWithWBR(sharedMetric.name, 17)}
/>
)
},
},
{
key: 'description',
title: 'Description',
dataIndex: 'description',
},
...(hasAvailableFeature(AvailableFeature.TAGGING)
? [
{
title: 'Tags',
dataIndex: 'tags' as keyof SharedMetric,
render: function Render(tags: SharedMetric['tags']) {
return tags ? <ObjectTags tags={tags} staticOnly /> : null
},
} as LemonTableColumn<SharedMetric, keyof SharedMetric | undefined>,
]
: []),
{
title: 'Type',
key: 'type',
render: (_, metric: SharedMetric) =>
metric.query.kind === NodeKind.ExperimentTrendsQuery ? 'Trend' : 'Funnel',
},
createdByColumn<SharedMetric>() as LemonTableColumn<SharedMetric, keyof SharedMetric | undefined>,
createdAtColumn<SharedMetric>() as LemonTableColumn<SharedMetric, keyof SharedMetric | undefined>,
{
key: 'actions',
title: 'Actions',
render: (_, sharedMetric) => {
return (
<LemonButton
className="max-w-72"
type="secondary"
size="xsmall"
icon={<IconPencil />}
onClick={() => {
router.actions.push(urls.experimentsSharedMetric(sharedMetric.id))
}}
/>
)
},
},
]

return (
<div className="space-y-4">
<LemonButton
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@ export interface SharedMetric {
created_by: UserBasicType | null
created_at: string | null
updated_at: string | null
tags: string[]
}

export const NEW_SHARED_METRIC: Partial<SharedMetric> = {
name: '',
description: '',
query: getDefaultTrendsMetric(),
tags: [],
}

export const sharedMetricLogic = kea<sharedMetricLogicType>([
Expand Down
3 changes: 2 additions & 1 deletion posthog/api/test/__snapshots__/test_insight.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -2078,7 +2078,8 @@
"posthog_taggeditem"."event_definition_id",
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id"
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id"
FROM "posthog_taggeditem"
WHERE "posthog_taggeditem"."insight_id" IN (1,
2,
Expand Down
6 changes: 6 additions & 0 deletions posthog/api/test/dashboards/__snapshots__/test_dashboard.ambr
Original file line number Diff line number Diff line change
Expand Up @@ -852,6 +852,7 @@
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id",
"posthog_tag"."id",
"posthog_tag"."name",
"posthog_tag"."team_id"
Expand Down Expand Up @@ -1224,6 +1225,7 @@
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id",
"posthog_tag"."id",
"posthog_tag"."name",
"posthog_tag"."team_id"
Expand Down Expand Up @@ -5315,6 +5317,7 @@
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id",
"posthog_tag"."id",
"posthog_tag"."name",
"posthog_tag"."team_id"
Expand Down Expand Up @@ -5709,6 +5712,7 @@
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id",
"posthog_tag"."id",
"posthog_tag"."name",
"posthog_tag"."team_id"
Expand Down Expand Up @@ -9541,6 +9545,7 @@
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id",
"posthog_tag"."id",
"posthog_tag"."name",
"posthog_tag"."team_id"
Expand Down Expand Up @@ -11585,6 +11590,7 @@
"posthog_taggeditem"."property_definition_id",
"posthog_taggeditem"."action_id",
"posthog_taggeditem"."feature_flag_id",
"posthog_taggeditem"."experiment_saved_metric_id",
"posthog_tag"."id",
"posthog_tag"."name",
"posthog_tag"."team_id"
Expand Down
Loading
Loading