Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
091fec6
Add feature flag for flag dependencies
haacked Jun 27, 2025
fd0828f
Add new types for flag dependencies
haacked Jun 27, 2025
8f8e849
Support property type of flag when saving
haacked Jun 27, 2025
34a2291
Flag operator only supports evaluates to
haacked Jun 27, 2025
b739341
Map property type to filters and vice versa
haacked Jun 27, 2025
a291427
Allow selecting `false` values
haacked Jul 8, 2025
c177147
Add API for getting flag keys from IDs
haacked Jul 8, 2025
b8b1d3d
Exclude the current flag from list of dependencies
haacked Jul 8, 2025
54601e9
Add PropertyFilterType.FlagDependency
haacked Jul 8, 2025
cc1f1a5
Add UI to select flag dependencies
haacked Jul 8, 2025
f855221
Flag dependency is not multi-select
haacked Jul 8, 2025
d2f42e3
Look up the propertyOptions once
haacked Jul 8, 2025
0949c9f
Flag values are not editable
haacked Jul 8, 2025
77995b5
Add support for typed values to PropertyValue
haacked Jul 8, 2025
a65dc30
Format boolean flag dependency values
haacked Jul 8, 2025
830244a
Add tooltip for multivariate flag options
haacked Jul 10, 2025
2fabc5e
Add circular dependency detection for feature flags
haacked Jul 12, 2025
79743b2
Fix TypeScript errors in frontend flag dependency code
haacked Jul 14, 2025
5f13dbe
Fix test expectations for flag property validation
haacked Jul 14, 2025
ea2a1f7
Address AI PR feedback
haacked Jul 14, 2025
a159651
Update schema.json for flag dependency types
haacked Jul 14, 2025
fd29c0e
Fix mypy errors
haacked Jul 14, 2025
43c1ad6
Remove fixed violations.
haacked Jul 14, 2025
9579bfa
Update Python schema for flag dependency types
haacked Jul 14, 2025
881c2f4
Fix operator order to ensure flag dependencies show 'evaluates to'
haacked Jul 15, 2025
4a13f73
Fix up some tests
haacked Jul 15, 2025
20a4b49
Fix the property value tests for flag dependencies
haacked Jul 16, 2025
e07df5b
Exclude flag properties from HogQL
haacked Jul 16, 2025
8dc4284
Be more explicit about the type
haacked Jul 16, 2025
25dcb05
Add tests of the TaxonomicPropertyFilter
haacked Jul 16, 2025
1463ee8
Don't define LemonInputSelect twice
haacked Jul 16, 2025
0e5584c
Remove extraneous comment
haacked Jul 16, 2025
ec38629
Add warning when flag not found
haacked Jul 16, 2025
7a04744
Move test into TaxonomicPropertyFilter.test.tsx
haacked Jul 16, 2025
cd0f303
Add clarifying comment
haacked Jul 16, 2025
6858b84
Fix TypeScript errors in TaxonomicPropertyFilter test
haacked Jul 16, 2025
663a88d
Add flagIds selector
haacked Jul 16, 2025
e21290f
Fix tests for mixed boolean and variant values
haacked Jul 16, 2025
ff43dab
Add warning about local evaluation for flag dependencies
haacked Jul 17, 2025
ebe2a4a
Show flag dependency warning in both edit and view modes
haacked Jul 17, 2025
44fd35f
Make flag dependency chicklet link to dependent flag
haacked Jul 17, 2025
44116a6
Optimize circular dependency detection with batch queries
haacked Jul 17, 2025
a230cfa
Fix mypy type errors in circular dependency detection
haacked Jul 17, 2025
8d26892
Update UI snapshots for `chromium` (13)
github-actions[bot] Jul 18, 2025
d8d5b30
Update UI snapshots for `chromium` (14)
github-actions[bot] Jul 18, 2025
6e39fce
Update UI snapshots for `chromium` (15)
github-actions[bot] Jul 18, 2025
73e8efa
Update UI snapshots for `chromium` (9)
github-actions[bot] Jul 18, 2025
1dc1647
Update query snapshots
github-actions[bot] Jul 18, 2025
170dca7
Update UI snapshots for `chromium` (11)
github-actions[bot] Jul 18, 2025
c36327d
Update UI snapshots for `chromium` (4)
github-actions[bot] Jul 18, 2025
2d6c1ce
Add clarifying comment
haacked Jul 18, 2025
d5e26fe
Update UI snapshots for `chromium` (12)
github-actions[bot] Jul 18, 2025
34ec74d
Update UI snapshots for `chromium` (12)
github-actions[bot] Jul 18, 2025
c9b2668
Use a more specific type
haacked Jul 18, 2025
a8409d8
Improve the assertion
haacked Jul 18, 2025
8e63fc5
Move imports to the top of the file
haacked Jul 18, 2025
cd77ba5
Fix operator map for insights
haacked Jul 18, 2025
3c75f64
Update UI snapshots for `chromium` (9)
github-actions[bot] Jul 18, 2025
5488af9
Use the correct type
haacked Jul 18, 2025
dace5dd
Fix operator display
haacked Jul 18, 2025
7b0c3a2
Update UI snapshots for `chromium` (13)
github-actions[bot] Jul 18, 2025
ad01db1
Update UI snapshots for `chromium` (15)
github-actions[bot] Jul 18, 2025
b7faef3
Update UI snapshots for `chromium` (4)
github-actions[bot] Jul 18, 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
3 changes: 3 additions & 0 deletions frontend/src/lib/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,9 @@ const api = {
async get(id: FeatureFlagType['id']): Promise<FeatureFlagType> {
return await new ApiRequest().featureFlag(id).get()
},
async bulkKeys(ids: FeatureFlagType['id'][]): Promise<{ keys: Record<string, string> }> {
return await new ApiRequest().featureFlags().withAction('bulk_keys').create({ data: { ids } })
},
async createStaticCohort(id: FeatureFlagType['id']): Promise<{ cohort: CohortType }> {
return await new ApiRequest().featureFlagCreateStaticCohort(id).create()
},
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/lib/components/Cards/InsightCard/InsightDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { CodeSnippet, Language } from 'lib/components/CodeSnippet'
import {
convertPropertiesToPropertyGroup,
formatPropertyLabel,
getPropertyTypeFromFilter,
isAnyPropertyfilter,
isCohortPropertyFilter,
isPropertyFilterWithOperator,
Expand All @@ -16,7 +17,8 @@ import { LemonRow } from 'lib/lemon-ui/LemonRow'
import { LemonTag } from 'lib/lemon-ui/LemonTag/LemonTag'
import { Link } from 'lib/lemon-ui/Link'
import { ProfilePicture } from 'lib/lemon-ui/ProfilePicture'
import { allOperatorsMapping, capitalizeFirstLetter } from 'lib/utils'
import { capitalizeFirstLetter, chooseOperatorMap } from 'lib/utils'

import React from 'react'
import { BreakdownTag } from 'scenes/insights/filters/BreakdownFilter/BreakdownTag'
import { humanizePathsEventTypes } from 'scenes/insights/utils'
Expand Down Expand Up @@ -130,12 +132,14 @@ function CompactPropertyFiltersDisplay({
/>
)}
</span>
{
allOperatorsMapping[
{(() => {
const propertyType = getPropertyTypeFromFilter(leafFilter)
const operatorMap = chooseOperatorMap(propertyType)
return operatorMap[
(isPropertyFilterWithOperator(leafFilter) && leafFilter.operator) ||
'exact'
]
}{' '}
})()}{' '}
<b>
{isAnyPropertyfilter(leafFilter) &&
(Array.isArray(leafFilter.value)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ export function PropertiesTable({
[PropertyDefinitionType.Meta]: TaxonomicFilterGroupType.Metadata,
[PropertyDefinitionType.Resource]: TaxonomicFilterGroupType.Resources,
[PropertyDefinitionType.Log]: TaxonomicFilterGroupType.LogAttributes,
[PropertyDefinitionType.FlagValue]: TaxonomicFilterGroupType.EventFeatureFlags,
}

const propertyType = propertyTypeMap[type] || TaxonomicFilterGroupType.EventProperties
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { LemonSelect, LemonSelectProps } from '@posthog/lemon-ui'
import { allOperatorsToHumanName } from 'lib/components/DefinitionPopover/utils'
import { dayjs } from 'lib/dayjs'
import {
allOperatorsMapping,
chooseOperatorMap,
isMobile,
isOperatorCohort,
Expand All @@ -11,6 +10,7 @@ import {
isOperatorRange,
isOperatorRegex,
} from 'lib/utils'
import { getPropertyTypeFromFilter } from 'lib/components/PropertyFilters/utils'
import { useEffect, useState } from 'react'

import {
Expand Down Expand Up @@ -47,6 +47,7 @@ interface OperatorSelectProps extends Omit<LemonSelectProps<any>, 'options'> {
operators: Array<PropertyOperator>
onChange: (operator: PropertyOperator) => void
defaultOpen?: boolean
propertyType?: PropertyFilterType
}

function getValidationError(operator: PropertyOperator, value: any, property?: string): string | null {
Expand Down Expand Up @@ -114,6 +115,8 @@ export function OperatorValueSelect({
propertyType = PropertyType.Selector
} else if (propertyKey === 'id' && type === PropertyFilterType.Cohort) {
propertyType = PropertyType.Cohort
} else if (type === PropertyFilterType.FlagDependency) {
propertyType = PropertyType.Flag
} else if (propertyKey === 'assignee' && type === PropertyFilterType.ErrorTrackingIssue) {
propertyType = PropertyType.Assignee
} else if (
Expand Down Expand Up @@ -152,6 +155,7 @@ export function OperatorValueSelect({
<OperatorSelect
operator={currentOperator || PropertyOperator.Exact}
operators={operators}
propertyType={type}
onChange={(newOperator: PropertyOperator) => {
const tentativeValidationError =
newOperator && value ? getValidationError(newOperator, value, propertyKey) : null
Expand Down Expand Up @@ -228,9 +232,20 @@ export function OperatorValueSelect({
)
}

export function OperatorSelect({ operator, operators, onChange, className, size }: OperatorSelectProps): JSX.Element {
export function OperatorSelect({
operator,
operators,
onChange,
className,
size,
propertyType,
}: OperatorSelectProps): JSX.Element {
// Use contextual operator mapping based on property type
const propertyTypeForMapping = getPropertyTypeFromFilter({ type: propertyType })
const operatorMap = chooseOperatorMap(propertyTypeForMapping)

const operatorOptions = operators.map((op) => ({
label: <span className="operator-value-option">{allOperatorsMapping[op || PropertyOperator.Exact]}</span>,
label: <span className="operator-value-option">{operatorMap[op || PropertyOperator.Exact]}</span>,
value: op || PropertyOperator.Exact,
}))
return (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import '@testing-library/jest-dom'
import { render, screen } from '@testing-library/react'
import { Provider } from 'kea'
import { PropertyValue } from './PropertyValue'
import { PropertyFilterType, PropertyOperator } from '~/types'
import { initKeaTests } from '~/test/init'

// Mock kea hooks
jest.mock('kea', () => {
const actual = jest.requireActual('kea')
return {
...actual,
useValues: jest.fn(() => ({
formatPropertyValueForDisplay: (_key: string, value: any) => value,
describeProperty: () => null,
options: {
testProp: {
values: [{ name: true }, { name: false }, { name: 'some-variant' }, { name: 'true' }],
status: 'loaded',
allowCustomValues: true,
},
},
})),
useActions: jest.fn(() => ({
loadPropertyValues: jest.fn(),
})),
}
})

describe('PropertyValue with Flag Dependencies', () => {
beforeEach(() => {
initKeaTests()
})

const defaultProps = {
propertyKey: 'testProp',
type: PropertyFilterType.FlagDependency,
operator: PropertyOperator.Exact,
value: true,
onSet: jest.fn(),
endpoint: 'test',
eventNames: [],
addRelativeDateTimeOptions: false,
groupTypeIndex: undefined,
editable: true,
preloadValues: false,
}

it('preserves boolean values for flag dependencies', () => {
render(
<Provider>
<PropertyValue {...defaultProps} />
</Provider>
)

// The component should render the boolean value
// Since it's a select component, the value should be in the placeholder or input
const input = screen.getByPlaceholderText('true')
expect(input).toBeInTheDocument()
})

it('handles string variant values', () => {
const props = {
...defaultProps,
value: 'some-variant',
}

render(
<Provider>
<PropertyValue {...props} />
</Provider>
)

// The component should render the string value
const input = screen.getByPlaceholderText('some-variant')
expect(input).toBeInTheDocument()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { DurationPicker } from 'lib/components/DurationPicker/DurationPicker'
import { PropertyFilterDatePicker } from 'lib/components/PropertyFilters/components/PropertyFilterDatePicker'
import { propertyFilterTypeToPropertyDefinitionType } from 'lib/components/PropertyFilters/utils'
import { dayjs } from 'lib/dayjs'
import { LemonInputSelect } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { LemonInputSelect, LemonInputSelectOption } from 'lib/lemon-ui/LemonInputSelect/LemonInputSelect'
import { formatDate, isOperatorDate, isOperatorFlag, isOperatorMulti, toString } from 'lib/utils'
import { useEffect } from 'react'

Expand Down Expand Up @@ -62,8 +62,10 @@ export function PropertyValue({
}: PropertyValueProps): JSX.Element {
const { formatPropertyValueForDisplay, describeProperty, options } = useValues(propertyDefinitionsModel)
const { loadPropertyValues } = useActions(propertyDefinitionsModel)
const propertyOptions = options[propertyKey]
const isFlagDependencyProperty = type === PropertyFilterType.FlagDependency

const isMultiSelect = operator && isOperatorMulti(operator)
const isMultiSelect = operator && !isFlagDependencyProperty && isOperatorMulti(operator)
const isDateTimeProperty = operator && isOperatorDate(operator)
const propertyDefinitionType = propertyFilterTypeToPropertyDefinitionType(type)

Expand Down Expand Up @@ -98,7 +100,7 @@ export function PropertyValue({
}
}, [propertyKey, isDateTimeProperty])

const displayOptions = options[propertyKey]?.values || []
const displayOptions = propertyOptions?.values || []

const onSearchTextChange = (newInput: string): void => {
if (!Object.keys(options).includes(newInput) && !(operator && isOperatorFlag(operator))) {
Expand Down Expand Up @@ -142,6 +144,15 @@ export function PropertyValue({
(label) => String(formatPropertyValueForDisplay(propertyKey, label, propertyDefinitionType, groupTypeIndex))
)

// For flag dependencies, we need to preserve the original typed values
const typedValues = isFlagDependencyProperty
? value === null || value === undefined
? []
: Array.isArray(value)
? value
: [value]
: formattedValues

if (!editable) {
return <>{formattedValues.join(' or ')}</>
}
Expand Down Expand Up @@ -199,15 +210,30 @@ export function PropertyValue({
)
}

function formatLabelContent(value: any): JSX.Element {
const name = toString(value)
if (name === '') {
return <i>(empty string)</i>
}
// Render boolean flag values with code tags to distinguish them from string values
// e.g. true vs "true" - this is important for flag dependencies where type matters
if (isFlagDependencyProperty && typeof value === 'boolean') {
return <code>{name}</code>
}
return <>{formatPropertyValueForDisplay(propertyKey, name, propertyDefinitionType, groupTypeIndex)}</>
}

return (
<LemonInputSelect
className={inputClassName}
data-attr="prop-val"
loading={options[propertyKey]?.status === 'loading'}
value={formattedValues}
loading={propertyOptions?.status === 'loading'}
value={isFlagDependencyProperty ? typedValues : formattedValues}
mode={isMultiSelect ? 'multiple' : 'single'}
allowCustomValues={options[propertyKey]?.allowCustomValues ?? true}
onChange={(nextVal) => (isMultiSelect ? setValue(nextVal) : setValue(nextVal[0]))}
allowCustomValues={propertyOptions?.allowCustomValues ?? true}
onChange={(nextVal) =>
isMultiSelect ? setValue(nextVal as PropertyFilterValue) : setValue(nextVal[0] ?? null)
}
onInputChange={onSearchTextChange}
placeholder={placeholder}
size={size}
Expand All @@ -219,21 +245,36 @@ export function PropertyValue({
: undefined
}
popoverClassName="max-w-200"
options={displayOptions.map(({ name: _name }, index) => {
const name = toString(_name)
return {
options={displayOptions.map(({ name: value }, index) => {
const name = toString(value)
const hasVariants = isFlagDependencyProperty && displayOptions.length > 2
let tooltip: string | undefined = undefined

// Add tooltip for boolean values when flag has variants
if (isFlagDependencyProperty && typeof value === 'boolean' && hasVariants) {
tooltip = value
? 'Matches any variant of the flag'
: "Flag is disabled or doesn't match any conditions"
}

const option: LemonInputSelectOption<PropertyFilterValue> = {
key: name,
label: name,
labelComponent: (
<span key={name} data-attr={'prop-val-' + index} className="ph-no-capture" title={name}>
{name === '' ? (
<i>(empty string)</i>
) : (
formatPropertyValueForDisplay(propertyKey, name, propertyDefinitionType, groupTypeIndex)
)}
{formatLabelContent(value)}
</span>
),
}

if (isFlagDependencyProperty) {
option.value = value
if (tooltip) {
option.tooltip = tooltip
}
}

return option
})}
/>
)
Expand Down
Loading
Loading