Skip to content
Open
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
47 changes: 47 additions & 0 deletions frontend/src/generated/core/api.schemas.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 37 additions & 0 deletions frontend/src/generated/core/api.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

100 changes: 100 additions & 0 deletions frontend/src/scenes/data-management/definition/DefinitionView.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import '@testing-library/jest-dom'

import { cleanup, render, screen } from '@testing-library/react'
import { router } from 'kea-router'

import { urls } from 'scenes/urls'

import { useMocks } from '~/mocks/jest'
import { initKeaTests } from '~/test/init'
import { mockEventPropertyDefinition } from '~/test/mocks'

import { DefinitionView } from './DefinitionView'

const propertyEventsResponse = {
count: 2,
next: null,
previous: null,
results: [
{
id: 'event-definition-1',
name: '$pageview',
last_seen_at: '2026-06-01T12:00:00Z',
},
{
id: 'event-definition-2',
name: 'checkout completed',
last_seen_at: null,
},
],
source: 'event_property_metadata',
freshness:
'Updated asynchronously from ingestion metadata. Rows mean this property has been seen on the event at least once; deleted event definitions are omitted.',
}

describe('DefinitionView property event usage', () => {
beforeEach(() => {
useMocks({
get: {
'/api/projects/:team/property_definitions/:id': mockEventPropertyDefinition,
'/api/projects/:team/property_definitions/:id/events/': propertyEventsResponse,
},
})
initKeaTests()
router.actions.push(urls.propertyDefinition('1'))
})

afterEach(() => {
cleanup()
})

it('renders events that use the property with links to event definitions', async () => {
render(<DefinitionView id="1" />)

expect(await screen.findByText('Events using this property')).toBeInTheDocument()
expect(await screen.findByText('$pageview')).toBeInTheDocument()
expect(
screen.getByText((content) =>
content.includes('2 current event definitions include this property in ingestion metadata.')
)
).toBeInTheDocument()
expect(screen.getByText(propertyEventsResponse.freshness)).toBeInTheDocument()

const eventLink = await screen.findByRole('link', { name: '$pageview' })
expect(eventLink).toHaveAttribute('href', expect.stringContaining(urls.eventDefinition('event-definition-1')))
expect(screen.getByText('checkout completed')).toBeInTheDocument()
})

it('renders the empty state when no current event definitions use the property', async () => {
useMocks({
get: {
'/api/projects/:team/property_definitions/:id': mockEventPropertyDefinition,
'/api/projects/:team/property_definitions/:id/events/': {
...propertyEventsResponse,
count: 0,
results: [],
},
},
})

render(<DefinitionView id="1" />)

expect(
await screen.findByText('No current event definitions are known to use this property')
).toBeInTheDocument()
})

it('renders an error state when event usage fails to load', async () => {
useMocks({
get: {
'/api/projects/:team/property_definitions/:id': mockEventPropertyDefinition,
'/api/projects/:team/property_definitions/:id/events/': () => [500, { detail: 'Server error' }],
},
})

render(<DefinitionView id="1" />)

expect(await screen.findByText('Failed to load events that use this property.')).toBeInTheDocument()
expect(screen.getByRole('button', { name: 'Retry' })).toBeInTheDocument()
})
})
110 changes: 106 additions & 4 deletions frontend/src/scenes/data-management/definition/DefinitionView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,18 @@ import { TZLabel } from 'lib/components/TZLabel'
import { UserActivityIndicator } from 'lib/components/UserActivityIndicator/UserActivityIndicator'
import ViewRecordingsPlaylistButton from 'lib/components/ViewRecordingButton/ViewRecordingsPlaylistButton'
import { FEATURE_FLAGS } from 'lib/constants'
import { LemonBanner } from 'lib/lemon-ui/LemonBanner'
import { LemonButton } from 'lib/lemon-ui/LemonButton'
import { LemonDialog } from 'lib/lemon-ui/LemonDialog'
import { LemonTable, LemonTableColumns } from 'lib/lemon-ui/LemonTable'
import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink'
import { SpinnerOverlay } from 'lib/lemon-ui/Spinner/Spinner'
import { getPrimaryPropertyForEvent } from 'lib/utils/primaryEventProperty'
import { DefinitionLogicProps, definitionLogic } from 'scenes/data-management/definition/definitionLogic'
import {
DefinitionLogicProps,
PROPERTY_DEFINITION_EVENTS_LIMIT,
definitionLogic,
} from 'scenes/data-management/definition/definitionLogic'
import { EventDefinitionExperiments } from 'scenes/data-management/events/EventDefinitionExperiments'
import { EventDefinitionInsights } from 'scenes/data-management/events/EventDefinitionInsights'
import { EventDefinitionProperties } from 'scenes/data-management/events/EventDefinitionProperties'
Expand All @@ -28,6 +35,7 @@ import { LinkedHogFunctions } from 'scenes/hog-functions/list/LinkedHogFunctions
import { SceneExport } from 'scenes/sceneTypes'
import { urls } from 'scenes/urls'

import type { PropertyDefinitionEventUsageApi } from '~/generated/core/api.schemas'
import { SceneContent } from '~/layout/scenes/components/SceneContent'
import { SceneDivider } from '~/layout/scenes/components/SceneDivider'
import { SceneSection } from '~/layout/scenes/components/SceneSection'
Expand Down Expand Up @@ -124,9 +132,23 @@ function PrimaryPropertyDetail({ definition }: { definition: EventDefinition }):

export function DefinitionView(props: DefinitionLogicProps): JSX.Element {
const logic = definitionLogic(props)
const { definition, definitionLoading, definitionMissing, singular, isEvent, isProperty, metrics, metricsLoading } =
useValues(logic)
const { deleteDefinition } = useActions(logic)
const {
definition,
definitionLoading,
definitionMissing,
singular,
isEvent,
isProperty,
metrics,
metricsLoading,
propertyEvents,
propertyEventsLoading,
propertyEventsLoadFailed,
propertyEventsOffset,
propertyEventsNextOffset,
propertyEventsPreviousOffset,
} = useValues(logic)
const { deleteDefinition, loadPropertyEvents } = useActions(logic)

const memoizedQuery = useMemo(() => {
const columnsToUse =
Expand Down Expand Up @@ -168,6 +190,23 @@ export function DefinitionView(props: DefinitionLogicProps): JSX.Element {
isEvent ? TaxonomicFilterGroupType.Events : TaxonomicFilterGroupType.EventProperties
)

const propertyEventColumns: LemonTableColumns<PropertyDefinitionEventUsageApi> = [
{
title: 'Event',
key: 'event',
render: function Render(_, eventDefinition) {
return <LemonTableLink to={urls.eventDefinition(eventDefinition.id)} title={eventDefinition.name} />
},
},
{
title: 'Event last seen',
key: 'last_seen_at',
render: function Render(_, eventDefinition) {
return eventDefinition.last_seen_at ? <TZLabel time={eventDefinition.last_seen_at} /> : '—'
},
},
]

return (
<SceneContent>
<SceneTitleSection
Expand Down Expand Up @@ -394,6 +433,69 @@ export function DefinitionView(props: DefinitionLogicProps): JSX.Element {

<SceneDivider />

{isProperty && definition.id !== 'new' && (
<>
<SceneSection
title="Events using this property"
description={
propertyEvents
? `${propertyEvents.count.toLocaleString()} current event ${
propertyEvents.count === 1 ? 'definition includes' : 'definitions include'
} this property in ingestion metadata.`
: 'Event-property metadata is updated asynchronously from ingestion.'
}
>
{propertyEventsLoadFailed ? (
<LemonBanner type="error">
<div className="flex items-center justify-between gap-2">
<span>Failed to load events that use this property.</span>
<LemonButton
size="small"
type="secondary"
loading={propertyEventsLoading}
onClick={() => loadPropertyEvents(definition.id, propertyEventsOffset)}
>
Retry
</LemonButton>
</div>
</LemonBanner>
) : (
<>
<LemonBanner type="info" className="mb-2">
{propertyEvents?.freshness ??
'Updated asynchronously from ingestion metadata. Rows mean this property has been seen on the event at least once; deleted event definitions are omitted.'}
</LemonBanner>
<LemonTable
id={`property-events-definition-table-${definition.id}`}
data-attr="property-events-definition-table"
columns={propertyEventColumns}
dataSource={propertyEvents?.results ?? []}
emptyState="No current event definitions are known to use this property"
nouns={['event definition', 'event definitions']}
pagination={{
controlled: true,
pageSize: PROPERTY_DEFINITION_EVENTS_LIMIT,
currentPage:
Math.floor(propertyEventsOffset / PROPERTY_DEFINITION_EVENTS_LIMIT) + 1,
entryCount: propertyEvents?.count ?? 0,
onForward:
propertyEventsNextOffset !== null
? () => loadPropertyEvents(definition.id, propertyEventsNextOffset)
: undefined,
onBackward:
propertyEventsPreviousOffset !== null
? () => loadPropertyEvents(definition.id, propertyEventsPreviousOffset)
: undefined,
}}
loading={propertyEventsLoading}
/>
</>
)}
</SceneSection>
<SceneDivider />
</>
)}

{isEvent && definition.id !== 'new' && (
<>
<FlaggedFeature flag={FEATURE_FLAGS.SCHEMA_MANAGEMENT}>
Expand Down
Loading