Skip to content

👷‍♀️ [Devx] Make developer extension queries compatible with exclusion #3434

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

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
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
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,7 @@ export function EventsTabSide({
mb="sm"
/>

{facetRegistry && (
<FacetList
facetRegistry={facetRegistry}
facetValuesFilter={filters.facetValuesFilter}
onExcludedFacetValuesChange={(newExcludedFacetValues) =>
onFiltersChange({ ...filters, facetValuesFilter: newExcludedFacetValues })
}
/>
)}
{facetRegistry && <FacetList facetRegistry={facetRegistry} filters={filters} onFiltersChange={onFiltersChange} />}
</Box>
)
}
Original file line number Diff line number Diff line change
@@ -1,28 +1,30 @@
import { Box, Button, Card, Checkbox, Collapse, Flex, Text } from '@mantine/core'
import React from 'react'
import type { FacetValuesFilter, FacetRegistry } from '../../../hooks/useEvents'
import type { FacetValuesFilter, EventFilters, FacetRegistry } from '../../../hooks/useEvents'
import { generateQueryFromFacetValues, DEFAULT_FILTERS } from '../../../hooks/useEvents'
import type { Facet } from '../../../facets.constants'
import { FACET_ROOT, FacetValue } from '../../../facets.constants'
import * as classes from './facetList.module.css'
import { computeSelectionState } from './computeFacetState'

export function FacetList({
facetRegistry,
facetValuesFilter,
onExcludedFacetValuesChange,
filters,
onFiltersChange,
}: {
facetRegistry: FacetRegistry
facetValuesFilter: FacetValuesFilter
onExcludedFacetValuesChange: (newExcludedFacetValues: FacetValuesFilter) => void
filters: EventFilters
onFiltersChange: (newFilters: EventFilters) => void
}) {
return (
<FacetField
facet={FACET_ROOT}
depth={0}
facetRegistry={facetRegistry}
facetValuesFilter={facetValuesFilter}
onExcludedFacetValuesChange={onExcludedFacetValuesChange}
facetValuesFilter={filters.facetValuesFilter}
onFiltersChange={onFiltersChange}
parentList={[]}
currentFilters={filters}
/>
)
}
Expand All @@ -33,14 +35,16 @@ function FacetField({
facetRegistry,
facetValuesFilter,
parentList,
onExcludedFacetValuesChange,
onFiltersChange,
currentFilters,
}: {
facet: Facet
depth: number
facetRegistry: FacetRegistry
facetValuesFilter: FacetValuesFilter
parentList: string[]
onExcludedFacetValuesChange: (newExcludedFacetValues: FacetValuesFilter) => void
onFiltersChange: (newFilters: EventFilters) => void
currentFilters: EventFilters
}) {
const facetValueCounts = facetRegistry.getFacetValueCounts(facet.path)

Expand All @@ -62,7 +66,8 @@ function FacetField({
facetRegistry={facetRegistry}
facetValuesFilter={facetValuesFilter}
parentList={parentList.includes(facetValue) ? parentList : [...parentList, facetValue]}
onExcludedFacetValuesChange={onExcludedFacetValuesChange}
onFiltersChange={onFiltersChange}
currentFilters={currentFilters}
/>
))}
</Box>
Expand All @@ -79,7 +84,8 @@ function FacetValue({
facetRegistry,
facetValuesFilter,
parentList,
onExcludedFacetValuesChange,
onFiltersChange,
currentFilters,
}: {
facet: Facet
facetValue: FacetValue
Expand All @@ -88,7 +94,8 @@ function FacetValue({
facetRegistry: FacetRegistry
facetValuesFilter: FacetValuesFilter
parentList: string[]
onExcludedFacetValuesChange: (newExcludedFacetValues: FacetValuesFilter) => void
onFiltersChange: (newFilters: EventFilters) => void
currentFilters: EventFilters
}) {
const isTopLevel = depth === 0
const facetSelectState = computeSelectionState(facetValuesFilter, facetRegistry, facet, facetValue, parentList)
Expand All @@ -105,7 +112,12 @@ function FacetValue({
indeterminate={facetSelectState === 'partial-selected'} // can only populate direct parents
onChange={() => {
const filterType = facetSelectState === 'selected' ? 'exclude' : 'include'
onExcludedFacetValuesChange(toggleFacetValue(filterType, facet, facetValuesFilter, facetValue))
const newFacetValuesFilter = toggleFacetValue(filterType, facet, facetValuesFilter, facetValue)
onFiltersChange({
...currentFilters,
facetValuesFilter: newFacetValuesFilter,
query: generateQueryFromFacetValues(newFacetValuesFilter),
})
}}
/>
<Text>{facetValueCount}</Text>
Expand All @@ -115,7 +127,12 @@ function FacetValue({
w="40px"
onClick={() => {
const filterType = isOnly ? 'exclude' : 'include'
onExcludedFacetValuesChange(toggleFacetValue(filterType, facet, facetValuesFilter, facetValue))
const newFacetValuesFilter = toggleFacetValue(filterType, facet, facetValuesFilter, facetValue)
onFiltersChange({
...currentFilters,
facetValuesFilter: newFacetValuesFilter,
query: generateQueryFromFacetValues(newFacetValuesFilter),
Comment on lines +133 to +134
Copy link
Member

Choose a reason for hiding this comment

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

💬 suggestion: query and facetValuesFilter now kind of represent the same thing but in two different formats. It would be nice to keep only one of the two, probably query as it can express more things. This would:

  • simplify the logic, as we don't need to sync the two
  • make the UI more predictable, as the two cannot be out of sync
  • make things faster, as we wouldn't need to filter events based on both the query and facets.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed, these two formats coexisting is kind of getting out of hands.

})
}}
>
{isOnly ? 'all' : 'only'}
Expand All @@ -135,7 +152,8 @@ function FacetValue({
depth={depth + 1}
facetValuesFilter={facetValuesFilter}
parentList={parentList.includes(facetValue) ? parentList : [...parentList, facetValue]}
onExcludedFacetValuesChange={onExcludedFacetValuesChange}
onFiltersChange={onFiltersChange}
currentFilters={currentFilters}
/>
))}
</Box>
Expand Down
177 changes: 161 additions & 16 deletions developer-extension/src/panel/hooks/useEvents/eventFilters.spec.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
import type { RumEvent } from '../../../../../packages/rum-core/src/rumEvent.types'
import type { LogsEvent } from '../../../../../packages/logs/src/logsEvent.types'
import { isSafari } from '../../../../../packages/core/src/tools/utils/browserDetection'
import { parseQuery, matchWithWildcard, filterFacets } from './eventFilters'
import {
RUM_ACTION_EVENT,
RUM_ERROR_EVENT,
RUM_XHR_RESOURCE_EVENT,
LOGS_EVENT,
RUM_BEACON_EVENT,
} from '../../test/events'
import {
parseQuery,
matchWithWildcard,
filterFacets,
generateQueryFromFacetValues,
applyEventFilters,
DEFAULT_FILTERS,
} from './eventFilters'
import type { FacetValuesFilter } from './eventFilters'
import { FacetRegistry } from './facetRegistry'
const RUM_ERROR_EVENT = { type: 'error' } as RumEvent
const RUM_ACTION_EVENT = { type: 'action' } as RumEvent
const LOGS_EVENT = { status: 'info', origin: 'logger' } as LogsEvent
const RUM_XHR_RESOURCE_EVENT = { type: 'resource', resource: { type: 'xhr' } } as RumEvent

if (!isSafari()) {
describe('filterFacets', () => {
Expand Down Expand Up @@ -67,30 +75,52 @@ if (!isSafari()) {

describe('parseQuery', () => {
it('return a simple field', () => {
expect(parseQuery('foo:bar')).toEqual([['foo', 'bar']])
expect(parseQuery('foo:bar')).toEqual([['include', 'foo', 'bar']])
})
it('return intermediary fields', () => {
expect(parseQuery('foo.bar:baz')).toEqual([['foo.bar', 'baz']])
expect(parseQuery('foo.bar:baz')).toEqual([['include', 'foo.bar', 'baz']])
})
it('return multiple fields', () => {
expect(parseQuery('foo:bar baz:qux')).toEqual([
['foo', 'bar'],
['baz', 'qux'],
['include', 'foo', 'bar'],
['include', 'baz', 'qux'],
])
})
it('parse escaped whitespace with backslashes in search terms', () => {
expect(parseQuery('foo:bar\\ baz')).toEqual([['foo', 'bar\\ baz']])
expect(parseQuery('foo:bar\\ baz')).toEqual([['include', 'foo', 'bar\\ baz']])
})
it('parse escaped whitespace with backslashes in keys', () => {
expect(parseQuery('foo\\ bar:baz')).toEqual([['foo\\ bar', 'baz']])
expect(parseQuery('foo\\ bar:baz')).toEqual([['include', 'foo\\ bar', 'baz']])
})
it('return multiple fields with escaped whitespace', () => {
expect(parseQuery('foo\\ bar:baz\\ qux')).toEqual([['foo\\ bar', 'baz\\ qux']])
expect(parseQuery('foo\\ bar:baz\\ qux')).toEqual([['include', 'foo\\ bar', 'baz\\ qux']])
expect(parseQuery('foo:bar\\ baz qux:quux\\ corge')).toEqual([
['foo', 'bar\\ baz'],
['qux', 'quux\\ corge'],
['include', 'foo', 'bar\\ baz'],
['include', 'qux', 'quux\\ corge'],
])
})
it('should parse simple queries', () => {
expect(parseQuery('resource.type:beacon')).toEqual([['include', 'resource.type', 'beacon']])
})
it('should parse queries with multiple values for the same field', () => {
expect(parseQuery('resource.type:beacon resource.type:xhr resource.type:image')).toEqual([
['include', 'resource.type', 'beacon'],
['include', 'resource.type', 'xhr'],
['include', 'resource.type', 'image'],
])
})
it('should parse queries with exclude prefix', () => {
expect(parseQuery('-resource.type:beacon')).toEqual([['exclude', 'resource.type', 'beacon']])
})
it('should parse mixed include and exclude queries', () => {
expect(parseQuery('resource.type:beacon -resource.type:xhr')).toEqual([
['include', 'resource.type', 'beacon'],
['exclude', 'resource.type', 'xhr'],
])
})
it('should handle values with colons', () => {
expect(parseQuery('url:https://example.com:8080')).toEqual([['include', 'url', 'https://example.com:8080']])
})
})

describe('matchWithWildcard', () => {
Expand Down Expand Up @@ -137,4 +167,119 @@ if (!isSafari()) {
expect(matchWithWildcard('foo', '*BAR*')).toBe(false)
})
})

describe('applyEventFilters with query', () => {
const facetRegistry = new FacetRegistry()
facetRegistry.addEvent(RUM_ACTION_EVENT)
facetRegistry.addEvent(RUM_ERROR_EVENT)
facetRegistry.addEvent(RUM_XHR_RESOURCE_EVENT)
facetRegistry.addEvent(LOGS_EVENT)

it('should filter events by resource type', () => {
const filters = {
...DEFAULT_FILTERS,
query: 'resource.type:beacon',
}
const result = applyEventFilters(
filters,
[RUM_BEACON_EVENT, RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT, LOGS_EVENT],
facetRegistry
)
expect(result).toEqual([RUM_BEACON_EVENT])
})

it('should filter events by multiple quries', () => {
const filters = {
...DEFAULT_FILTERS,
query: 'action.type:click resource.type:xhr',
}
const result = applyEventFilters(
filters,
[RUM_ACTION_EVENT, RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT, LOGS_EVENT],
facetRegistry
)
expect(result).toEqual([RUM_ACTION_EVENT, RUM_XHR_RESOURCE_EVENT])
})

it('should handle exclude queries', () => {
const filters = {
...DEFAULT_FILTERS,
query: '-resource.type:beacon',
}
const result = applyEventFilters(
filters,
[RUM_BEACON_EVENT, RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT, LOGS_EVENT],
facetRegistry
)
expect(result).toEqual([RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT, LOGS_EVENT])
})

it('should handle mixed include and exclude queries', () => {
const filters = {
...DEFAULT_FILTERS,
query: 'type:resource -resource.type:xhr',
}
const result = applyEventFilters(
filters,
[RUM_BEACON_EVENT, RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT, LOGS_EVENT],
facetRegistry
)
expect(result).toEqual([RUM_BEACON_EVENT])
})

it('should filter events by event source', () => {
const filters = {
...DEFAULT_FILTERS,
query: '$eventSource:rum',
}
const result = applyEventFilters(
filters,
[RUM_BEACON_EVENT, RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT, LOGS_EVENT],
facetRegistry
)
expect(result).toEqual([RUM_BEACON_EVENT, RUM_ERROR_EVENT, RUM_XHR_RESOURCE_EVENT])
})
})

describe('generateQueryFromFacetValues', () => {
it('should generate query for single facet value', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include' as const,
facetValues: {
'resource.type': ['beacon'],
},
}
expect(generateQueryFromFacetValues(facetValuesFilter)).toBe('resource.type:beacon')
})

it('should generate query for multiple facet values', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include' as const,
facetValues: {
'resource.type': ['beacon', 'xhr', 'image'],
},
}
expect(generateQueryFromFacetValues(facetValuesFilter)).toBe(
'resource.type:beacon resource.type:xhr resource.type:image'
)
})

it('should generate query with exclude prefix', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'exclude' as const,
facetValues: {
'resource.type': ['beacon'],
},
}
expect(generateQueryFromFacetValues(facetValuesFilter)).toBe('-resource.type:beacon')
})

it('should handle empty facet values', () => {
const facetValuesFilter: FacetValuesFilter = {
type: 'include' as const,
facetValues: {},
}
expect(generateQueryFromFacetValues(facetValuesFilter)).toBe('')
})
})
}
Loading