Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4d2e173
feat(cohorts): add used-in references endpoint and UI
ricardothe3rd Apr 15, 2026
b45389a
fix(cohorts): address review feedback on used_in endpoint
ricardothe3rd Apr 15, 2026
49924a6
Merge branch 'upstream/master' into feat/cohort-used-in-references
ricardothe3rd Apr 21, 2026
74b1340
Merge branch 'master' into feat/cohort-used-in-references
dmarticus Apr 22, 2026
09509e6
Merge branch 'master' into feat/cohort-used-in-references
dmarticus Apr 27, 2026
ca1312b
Merge branch 'master' into feat/cohort-used-in-references
dmarticus Apr 28, 2026
1058116
feat(cohorts): address review on used_in endpoint
ricardothe3rd Apr 28, 2026
a16f631
chore(cohorts): regenerate openapi types for used_in response
ricardothe3rd Apr 28, 2026
61e7e04
feat(cohorts): use generated client and group used_in references in b…
ricardothe3rd Apr 28, 2026
7dc8c56
fix(cohorts): satisfy mypy on used_in endpoint and deletion guard
ricardothe3rd Apr 28, 2026
2d77fa9
chore(cohorts): sync mcp yaml definitions with new used_in operation
ricardothe3rd Apr 29, 2026
5b6de09
Merge branch 'master' into feat/cohort-used-in-references
ricardothe3rd Apr 29, 2026
ee9dfe0
chore(messaging): silence mypy unreachable on async-generator marker
ricardothe3rd Apr 29, 2026
61abd52
Merge remote-tracking branch 'upstream/master' into feat/cohort-used-…
ricardothe3rd Apr 30, 2026
ff5e2fe
Merge branch 'master' into feat/cohort-used-in-references
dmarticus Apr 30, 2026
9c60ef8
Merge remote-tracking branch 'origin/master' into feat/cohort-used-in…
dmarticus May 6, 2026
9a07b67
Merge branch 'master' into feat/cohort-used-in-references
dmarticus May 18, 2026
fe77087
Merge branch 'master' into feat/cohort-used-in-references
dmarticus May 18, 2026
f033770
Merge branch 'master' into feat/cohort-used-in-references
haacked Jun 10, 2026
e01db36
fix(cohorts): address review feedback on used-in references endpoint
haacked Jun 10, 2026
c4c9016
fix(cohorts): apply code review fixes to used-in endpoint
haacked Jun 10, 2026
e768c61
fix(cohorts): skip exception capture for 404s when loading used-in re…
haacked Jun 10, 2026
dde067f
Merge branch 'master' into feat/cohort-used-in-references
haacked Jun 11, 2026
7f4d510
Merge branch 'master' into feat/cohort-used-in-references
haacked Jun 11, 2026
cdcd54b
Merge branch 'master' into feat/cohort-used-in-references
haacked Jun 11, 2026
6e3054a
chore(cohorts): remove duplicate cohorts_used_in entry from MCP core.…
haacked Jun 11, 2026
4d68fff
chore(cohorts): align nosemgrep suppressions on used-in queries
haacked Jun 11, 2026
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
65 changes: 64 additions & 1 deletion frontend/src/scenes/cohorts/CohortEdit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ import {
} from '~/layout/scenes/SceneLayout'
import { AndOrFilterSelect } from '~/queries/nodes/InsightViz/PropertyGroupFilters/AndOrFilterSelect'
import { Query } from '~/queries/Query/Query'
import { ActivityScope, CohortType, SidePanelTab } from '~/types'
import { ActivityScope, CohortType, InsightShortId, SidePanelTab } from '~/types'

import type { CohortUsedInResponseApi } from 'products/cohorts/frontend/generated/api.schemas'

import { AddPersonToCohortModal } from './AddPersonToCohortModal'
import { addPersonToCohortModalLogic } from './addPersonToCohortModalLogic'
Expand All @@ -51,6 +53,65 @@ import { PersonDisplayNameType, RemovePersonFromCohortButton } from './RemovePer

const RESOURCE_TYPE = 'cohort'

function UsedInBanner({ usedIn }: { usedIn: CohortUsedInResponseApi }): JSX.Element | null {
const sections = [
{
title: 'Feature flags',
block: usedIn.feature_flags,
items: usedIn.feature_flags.results.map((flag) => ({
key: `flag-${flag.id}`,
url: urls.featureFlag(flag.id),
label: flag.name || flag.key,
})),
},
{
title: 'Insights',
block: usedIn.insights,
items: usedIn.insights.results.map((insight) => ({
key: `insight-${insight.id}`,
url: urls.insightView(insight.short_id as InsightShortId),
label: insight.name,
})),
},
{
title: 'Cohorts',
block: usedIn.cohorts,
items: usedIn.cohorts.results.map((c) => ({
key: `cohort-${c.id}`,
url: urls.cohort(c.id),
label: c.name,
})),
},
].filter((section) => section.items.length > 0)

if (sections.length === 0) {
return null
}

return (
<LemonBanner type="info">
<h4 className="font-semibold mb-1">Used in</h4>
<div className="space-y-2">
{sections.map(({ title, block, items }) => (
<div key={title}>
<h5 className="text-xs font-semibold uppercase opacity-60 mb-0">
{title}
{block.has_more && ` (${block.results.length} of ${block.total} shown)`}
</h5>
<ul className="list-disc pl-4 mb-0 space-y-0.5">
{items.map(({ key, url, label }) => (
<li key={key}>
<Link to={url}>{label}</Link>
</li>
))}
</ul>
</div>
))}
</div>
</LemonBanner>
)
}

export interface CohortEditProps {
id?: CohortType['id']
attachTo?: BuiltLogic<Logic> | LogicWrapper<Logic>
Expand Down Expand Up @@ -97,6 +158,7 @@ export function CohortEdit({ id, attachTo }: CohortEditProps): JSX.Element {
canRemovePersonFromCohort,
isPendingCalculation,
isCalculatingOrPending,
usedIn,
staticCohortMode,
activeTab,
} = useValues(logic)
Expand Down Expand Up @@ -429,6 +491,7 @@ export function CohortEdit({ id, attachTo }: CohortEditProps): JSX.Element {
</div>
</div>
</SceneSection>
{!isNewCohort && usedIn && <UsedInBanner usedIn={usedIn} />}
{cohort.is_static && staticCohortMode === 'criteria' ? (
<>
<SceneDivider />
Expand Down
29 changes: 27 additions & 2 deletions frontend/src/scenes/cohorts/cohortEditLogic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,16 @@ jest.mock('lib/lemon-ui/LemonToast/LemonToast', () => ({
},
}))

const mockUsedInResponse = {
feature_flags: {
results: [{ id: 7, key: 'my-flag', name: 'My Flag' }],
total: 1,
has_more: false,
},
insights: { results: [], total: 0, has_more: false },
cohorts: { results: [], total: 0, has_more: false },
}

describe('cohortEditLogic', () => {
let logic: ReturnType<typeof cohortEditLogic.build>
async function initCohortLogic(props: CohortLogicProps = { id: 'new' }): Promise<void> {
Expand All @@ -64,6 +74,7 @@ describe('cohortEditLogic', () => {
get: {
'/api/projects/:team_id/cohorts/': toPaginatedResponse([mockCohort]),
'/api/projects/:team_id/cohorts/:id/': mockCohort,
'/api/projects/:team_id/cohorts/:id/used_in/': mockUsedInResponse,
},
post: {
'/api/projects/:team_id/cohorts/': mockCohort,
Expand All @@ -81,7 +92,15 @@ describe('cohortEditLogic', () => {
await initCohortLogic({ id: 1 })
await expectLogic(logic).toDispatchActions(['fetchCohort'])

expect(api.get).toHaveBeenCalledTimes(1)
// One call for the cohort itself, one for its used-in references
expect(api.get).toHaveBeenCalledTimes(2)
})

it('loads used-in references on mount before the cohort has resolved', async () => {
await initCohortLogic({ id: 1 })
await expectLogic(logic).toDispatchActions(['loadUsedIn', 'loadUsedInSuccess'])

expect(logic.values.usedIn).toEqual(mockUsedInResponse)
})

it('loads new cohort on mount', async () => {
Expand Down Expand Up @@ -158,7 +177,13 @@ describe('cohortEditLogic', () => {
},
})
logic.actions.submitCohort()
}).toDispatchActions(['setCohort', 'submitCohort', 'submitCohortSuccess'])
}).toDispatchActions([
'setCohort',
'submitCohort',
'submitCohortSuccess',
'saveCohortSuccess',
'loadUsedIn',
])
expect(api.update).toHaveBeenCalledTimes(1)
})

Expand Down
38 changes: 37 additions & 1 deletion frontend/src/scenes/cohorts/cohortEditLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import posthog from 'posthog-js'
import { v4 as uuidv4 } from 'uuid'

import api from 'lib/api'
import { ApiError } from 'lib/api-error'
import { tryShowMCPHint } from 'lib/components/MCPHint/mcpHintLogic'
import { SetupTaskId, globalSetupLogic } from 'lib/components/ProductSetup'
import { ENTITY_MATCH_TYPE } from 'lib/constants'
Expand All @@ -38,6 +39,7 @@ import {
validateGroup,
} from 'scenes/cohorts/cohortUtils'
import { personsLogic } from 'scenes/persons/personsLogic'
import { teamLogic } from 'scenes/teamLogic'
import { urls } from 'scenes/urls'

import { refreshTreeItem } from '~/layout/panel-layout/ProjectTree/projectTreeLogic'
Expand All @@ -59,6 +61,9 @@ import {
PropertyType,
} from '~/types'

import { cohortsUsedInRetrieve } from 'products/cohorts/frontend/generated/api'
import type { CohortUsedInResponseApi } from 'products/cohorts/frontend/generated/api.schemas'

import type { cohortEditLogicType } from './cohortEditLogicType'

export type CohortLogicProps = {
Expand Down Expand Up @@ -89,6 +94,7 @@ export const cohortEditLogic = kea<cohortEditLogicType>([
key((props) => (props.id === 'new' || !props.id ? 'new' : props.id)),
path(['scenes', 'cohorts', 'cohortLogicEdit']),
connect(() => ({
values: [teamLogic, ['currentProjectId']],
actions: [eventUsageLogic, ['reportExperimentExposureCohortEdited']],
logic: [cohortsModel],
})),
Expand Down Expand Up @@ -369,7 +375,7 @@ export const cohortEditLogic = kea<cohortEditLogicType>([
},
})),

loaders(({ actions, values, key }) => ({
loaders(({ actions, values, key, props }) => ({
cohort: [
NEW_COHORT,
{
Expand Down Expand Up @@ -584,6 +590,30 @@ export const cohortEditLogic = kea<cohortEditLogicType>([
},
},
],

usedIn: [
null as CohortUsedInResponseApi | null,
{
loadUsedIn: async () => {
// On mount `values.cohort` is still NEW_COHORT (fetchCohort hasn't resolved),
// so fall back to the id from props.
const id = values.cohort.id !== 'new' ? values.cohort.id : props.id
if (!id || id === 'new') {
return null
}
try {
return await cohortsUsedInRetrieve(String(values.currentProjectId), Number(id))
} catch (error) {
// A 404 just means the endpoint isn't deployed yet (deploy skew) or the
// cohort is gone; neither is worth reporting.
if (!(error instanceof ApiError) || error.status !== 404) {
posthog.captureException(error, { feature: 'cohort-used-in' })
}
return null
}
},
},
],
})),
listeners(({ actions, values }) => ({
setCriteria: ({ newCriteria, groupIndex, criteriaIndex }) => {
Expand Down Expand Up @@ -631,6 +661,11 @@ export const cohortEditLogic = kea<cohortEditLogicType>([
'There was an error submitting this cohort. Make sure the cohort filters are correct.',
})
},
// Refresh once the save request actually resolves; submitCohortSuccess fires as soon
// as the synchronous submit handler dispatches saveCohort.
saveCohortSuccess: () => {
actions.loadUsedIn()
},
checkIfFinishedCalculating: async ({ cohort }, breakpoint) => {
const isPendingCalculation = checkIsPendingCalculation(cohort)
const isCalculatingOrPending = cohort.is_calculating || isPendingCalculation
Expand Down Expand Up @@ -713,6 +748,7 @@ export const cohortEditLogic = kea<cohortEditLogicType>([
}
} else {
actions.fetchCohort(props.id)
actions.loadUsedIn()
Comment thread
haacked marked this conversation as resolved.
}
Comment thread
haacked marked this conversation as resolved.
}),
beforeUnmount(({ values }) => {
Expand Down
Loading
Loading