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(errors): Allow searching for errors #25175

Merged
merged 10 commits into from
Oct 1, 2024
Merged
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
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/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4164,6 +4164,9 @@
"response": {
"$ref": "#/definitions/ErrorTrackingQueryResponse"
},
"searchQuery": {
"type": "string"
},
"select": {
"items": {
"$ref": "#/definitions/HogQLExpression"
Expand Down
1 change: 1 addition & 0 deletions frontend/src/queries/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1536,6 +1536,7 @@ export interface ErrorTrackingQuery extends DataNode<ErrorTrackingQueryResponse>
assignee?: integer | null
filterGroup?: PropertyGroupFilter
filterTestAccounts?: boolean
searchQuery?: string
Copy link
Contributor

Choose a reason for hiding this comment

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

Agreed this makes sense as being separate to the filterGroup

limit?: integer
}

Expand Down
25 changes: 14 additions & 11 deletions frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { LemonSelect } from '@posthog/lemon-ui'
import { LemonInput, LemonSelect } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { MemberSelect } from 'lib/components/MemberSelect'
Expand All @@ -13,19 +13,22 @@ import { errorTrackingLogic } from './errorTrackingLogic'
import { errorTrackingSceneLogic } from './errorTrackingSceneLogic'

export const FilterGroup = (): JSX.Element => {
const { filterGroup, filterTestAccounts } = useValues(errorTrackingLogic)
const { setFilterGroup, setFilterTestAccounts } = useActions(errorTrackingLogic)
const { filterGroup, filterTestAccounts, searchQuery } = useValues(errorTrackingLogic)
const { setFilterGroup, setFilterTestAccounts, setSearchQuery } = useActions(errorTrackingLogic)

return (
<div className="flex flex-1 items-center justify-between space-x-2">
<UniversalFilters
rootKey="error-tracking"
group={filterGroup}
taxonomicGroupTypes={[TaxonomicFilterGroupType.PersonProperties, TaxonomicFilterGroupType.Cohorts]}
onChange={setFilterGroup}
>
<RecordingsUniversalFilterGroup />
</UniversalFilters>
<div className="flex items-center gap-2">
<LemonInput type="search" placeholder="Search..." value={searchQuery} onChange={setSearchQuery} />
<UniversalFilters
rootKey="error-tracking"
group={filterGroup}
taxonomicGroupTypes={[TaxonomicFilterGroupType.PersonProperties, TaxonomicFilterGroupType.Cohorts]}
onChange={setFilterGroup}
>
<RecordingsUniversalFilterGroup />
</UniversalFilters>
</div>
<div>
<TestAccountFilter
size="small"
Expand Down
7 changes: 7 additions & 0 deletions frontend/src/scenes/error-tracking/errorTrackingLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const errorTrackingLogic = kea<errorTrackingLogicType>([
setAssignee: (assignee: number | null) => ({ assignee }),
setFilterGroup: (filterGroup: UniversalFiltersGroup) => ({ filterGroup }),
setFilterTestAccounts: (filterTestAccounts: boolean) => ({ filterTestAccounts }),
setSearchQuery: (searchQuery: string) => ({ searchQuery }),
setSparklineSelectedPeriod: (period: string | null) => ({ period }),
_setSparklineOptions: (options: SparklineOption[]) => ({ options }),
}),
Expand Down Expand Up @@ -71,6 +72,12 @@ export const errorTrackingLogic = kea<errorTrackingLogicType>([
setFilterTestAccounts: (_, { filterTestAccounts }) => filterTestAccounts,
},
],
searchQuery: [
'' as string,
{
setSearchQuery: (_, { searchQuery }) => searchQuery,
},
],
sparklineSelectedPeriod: [
lastDay.value as string | null,
{ persist: true },
Expand Down
23 changes: 20 additions & 3 deletions frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const errorTrackingSceneLogic = kea<errorTrackingSceneLogicType>([
connect({
values: [
errorTrackingLogic,
['dateRange', 'assignee', 'filterTestAccounts', 'filterGroup', 'sparklineSelectedPeriod'],
['dateRange', 'assignee', 'filterTestAccounts', 'filterGroup', 'sparklineSelectedPeriod', 'searchQuery'],
],
}),

Expand All @@ -39,15 +39,32 @@ export const errorTrackingSceneLogic = kea<errorTrackingSceneLogicType>([

selectors({
query: [
(s) => [s.order, s.dateRange, s.assignee, s.filterTestAccounts, s.filterGroup, s.sparklineSelectedPeriod],
(order, dateRange, assignee, filterTestAccounts, filterGroup, sparklineSelectedPeriod): DataTableNode =>
(s) => [
s.order,
s.dateRange,
s.assignee,
s.filterTestAccounts,
s.filterGroup,
s.sparklineSelectedPeriod,
s.searchQuery,
],
(
order,
dateRange,
assignee,
filterTestAccounts,
filterGroup,
sparklineSelectedPeriod,
searchQuery
): DataTableNode =>
errorTrackingQuery({
order,
dateRange,
assignee,
filterTestAccounts,
filterGroup,
sparklineSelectedPeriod,
searchQuery,
}),
],
}),
Expand Down
4 changes: 3 additions & 1 deletion frontend/src/scenes/error-tracking/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,11 @@ export const errorTrackingQuery = ({
assignee,
filterTestAccounts,
filterGroup,
searchQuery,
sparklineSelectedPeriod,
columns,
limit = 50,
}: Pick<ErrorTrackingQuery, 'order' | 'dateRange' | 'assignee' | 'filterTestAccounts' | 'limit'> & {
}: Pick<ErrorTrackingQuery, 'order' | 'dateRange' | 'assignee' | 'filterTestAccounts' | 'limit' | 'searchQuery'> & {
filterGroup: UniversalFiltersGroup
sparklineSelectedPeriod: string | null
columns?: ('error' | 'volume' | 'occurrences' | 'sessions' | 'users' | 'assignee')[]
Expand Down Expand Up @@ -77,6 +78,7 @@ export const errorTrackingQuery = ({
assignee: assignee,
filterGroup: filterGroup as PropertyGroupFilter,
filterTestAccounts: filterTestAccounts,
searchQuery: searchQuery,
limit: limit,
},
showActions: false,
Expand Down
32 changes: 31 additions & 1 deletion posthog/hogql_queries/error_tracking_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def to_query(self) -> ast.SelectQuery:

def select(self):
exprs: list[ast.Expr] = [
ast.Alias(alias="occurrences", expr=ast.Call(name="count", args=[])),
ast.Alias(
alias="occurrences", expr=ast.Call(name="count", distinct=True, args=[ast.Field(chain=["uuid"])])
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@daibhin found that due to the left outer join for overrides, sometimes events are double counted, which makes this new test I added flakey. Not sure why this doesn't happen on existing tests 🤔 , maybe to do with how/when overrides are set, since I add events after the first flush.

But anyhow, should make this case clear where we never doublecount events.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

merging since no real risk here, but let me know if you have thoughts on this change!

),
ast.Alias(
alias="sessions", expr=ast.Call(name="count", distinct=True, args=[ast.Field(chain=["$session_id"])])
),
Expand Down Expand Up @@ -126,6 +128,34 @@ def where(self):
),
)

if self.query.searchQuery:
# TODO: Refine this so it only searches the frames inside $exception_list
# TODO: Split out spaces and search for each word separately
# TODO: Add support for searching for specific properties
# TODO: Add fuzzy search support
props_to_search = ["$exception_list", "$exception_stack_trace_raw", "$exception_type", "$exception_message"]
or_exprs: list[ast.Expr] = []
for prop in props_to_search:
or_exprs.append(
ast.CompareOperation(
op=ast.CompareOperationOp.Gt,
Comment on lines +140 to +141
Copy link
Contributor

Choose a reason for hiding this comment

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

Not a concern for right now but maybe we could consider some degree of fuzzy matching in future

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

nice 👌

left=ast.Call(
name="position",
args=[
ast.Call(name="lower", args=[ast.Field(chain=["properties", prop])]),
ast.Call(name="lower", args=[ast.Constant(value=self.query.searchQuery)]),
],
),
right=ast.Constant(value=0),
)
)

exprs.append(
ast.Or(
exprs=or_exprs,
)
)

return ast.And(exprs=exprs)

def group_by(self):
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# serializer version: 1
# name: TestErrorTrackingQueryRunner.test_assignee_groups
'''
SELECT count() AS occurrences,
SELECT count(DISTINCT events.uuid) AS occurrences,
count(DISTINCT events.`$session_id`) AS sessions,
count(DISTINCT events.distinct_id) AS users,
max(toTimeZone(events.timestamp, 'UTC')) AS last_seen,
Expand All @@ -24,7 +24,7 @@
# ---
# name: TestErrorTrackingQueryRunner.test_column_names
'''
SELECT count() AS occurrences,
SELECT count(DISTINCT events.uuid) AS occurrences,
count(DISTINCT events.`$session_id`) AS sessions,
count(DISTINCT events.distinct_id) AS users,
max(toTimeZone(events.timestamp, 'UTC')) AS last_seen,
Expand Down Expand Up @@ -64,7 +64,7 @@
# ---
# name: TestErrorTrackingQueryRunner.test_column_names.1
'''
SELECT count() AS occurrences,
SELECT count(DISTINCT events.uuid) AS occurrences,
count(DISTINCT events.`$session_id`) AS sessions,
count(DISTINCT events.distinct_id) AS users,
max(toTimeZone(events.timestamp, 'UTC')) AS last_seen,
Expand Down Expand Up @@ -142,7 +142,7 @@
# ---
# name: TestErrorTrackingQueryRunner.test_fingerprints
'''
SELECT count() AS occurrences,
SELECT count(DISTINCT events.uuid) AS occurrences,
count(DISTINCT events.`$session_id`) AS sessions,
count(DISTINCT events.distinct_id) AS users,
max(toTimeZone(events.timestamp, 'UTC')) AS last_seen,
Expand All @@ -163,7 +163,7 @@
# ---
# name: TestErrorTrackingQueryRunner.test_hogql_filters
'''
SELECT count() AS occurrences,
SELECT count(DISTINCT events.uuid) AS occurrences,
count(DISTINCT events.`$session_id`) AS sessions,
count(DISTINCT events.distinct_id) AS users,
max(toTimeZone(events.timestamp, 'UTC')) AS last_seen,
Expand Down Expand Up @@ -241,3 +241,43 @@
max_bytes_before_external_group_by=0
'''
# ---
# name: TestErrorTrackingQueryRunner.test_search_query
'''
SELECT count(DISTINCT events.uuid) AS occurrences,
count(DISTINCT events.`$session_id`) AS sessions,
count(DISTINCT events.distinct_id) AS users,
max(toTimeZone(events.timestamp, 'UTC')) AS last_seen,
min(toTimeZone(events.timestamp, 'UTC')) AS first_seen,
any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')) AS description,
any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')) AS exception_type,
JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)') AS fingerprint
FROM events
LEFT OUTER JOIN
(SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id,
person_distinct_id_overrides.distinct_id AS distinct_id
FROM person_distinct_id_overrides
WHERE equals(person_distinct_id_overrides.team_id, 2)
GROUP BY person_distinct_id_overrides.distinct_id
HAVING ifNull(equals(argMax(person_distinct_id_overrides.is_deleted, person_distinct_id_overrides.version), 0), 0) SETTINGS optimize_aggregation_in_order=1) AS events__override ON equals(events.distinct_id, events__override.distinct_id)
LEFT JOIN
(SELECT person.id AS id,
replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(person.properties, 'email'), ''), 'null'), '^"|"$', '') AS properties___email
FROM person
WHERE and(equals(person.team_id, 2), ifNull(in(tuple(person.id, person.version),
(SELECT person.id AS id, max(person.version) AS version
FROM person
WHERE equals(person.team_id, 2)
GROUP BY person.id
HAVING and(ifNull(equals(argMax(person.is_deleted, person.version), 0), 0), ifNull(less(argMax(toTimeZone(person.created_at, 'UTC'), person.version), plus(now64(6, 'UTC'), toIntervalDay(1))), 0)))), 0)) SETTINGS optimize_aggregation_in_order=1) AS events__person ON equals(if(not(empty(events__override.distinct_id)), events__override.person_id, events.person_id), events__person.id)
WHERE and(equals(events.team_id, 2), equals(events.event, '$exception'), and(less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2022-01-11 00:00:00.000000', 6, 'UTC')), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2022-01-10 00:00:00.000000', 6, 'UTC')), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1)), or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('databasenot')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_stack_trace_raw'), ''), 'null'), '^"|"$', '')), lower('databasenot')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('databasenot')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('databasenot')), 0), 0)))
GROUP BY fingerprint
LIMIT 101
OFFSET 0 SETTINGS readonly=2,
max_execution_time=60,
allow_experimental_object_type=1,
format_csv_allow_double_quotes=0,
max_ast_elements=4000000,
max_expanded_ast_elements=4000000,
max_bytes_before_external_group_by=0
'''
# ---
75 changes: 75 additions & 0 deletions posthog/hogql_queries/test/test_error_tracking_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,81 @@ def test_column_names(self):
],
)

@snapshot_clickhouse_queries
def test_search_query(self):
with freeze_time("2022-01-10 12:11:00"):
_create_event(
distinct_id=self.distinct_id_one,
event="$exception",
team=self.team,
properties={
"$exception_fingerprint": ["DatabaseNotFoundX"],
"$exception_type": "DatabaseNotFoundX",
"$exception_message": "this is the same error message",
},
)
_create_event(
distinct_id=self.distinct_id_one,
event="$exception",
team=self.team,
properties={
"$exception_fingerprint": ["DatabaseNotFoundY"],
"$exception_type": "DatabaseNotFoundY",
"$exception_message": "this is the same error message",
},
)
_create_event(
distinct_id=self.distinct_id_two,
event="$exception",
team=self.team,
properties={
"$exception_fingerprint": ["xyz"],
"$exception_type": "xyz",
"$exception_message": "this is the same error message",
},
)
flush_persons_and_events()

runner = ErrorTrackingQueryRunner(
team=self.team,
query=ErrorTrackingQuery(
kind="ErrorTrackingQuery",
fingerprint=None,
dateRange=DateRange(date_from="2022-01-10", date_to="2022-01-11"),
filterTestAccounts=True,
searchQuery="databasenot",
),
)

results = sorted(self._calculate(runner)["results"], key=lambda x: x["fingerprint"])

self.assertEqual(len(results), 2)
self.assertEqual(results[0]["fingerprint"], ["DatabaseNotFoundX"])
self.assertEqual(results[0]["occurrences"], 1)
self.assertEqual(results[0]["sessions"], 1)
self.assertEqual(results[0]["users"], 1)

self.assertEqual(results[1]["fingerprint"], ["DatabaseNotFoundY"])
self.assertEqual(results[1]["occurrences"], 1)
self.assertEqual(results[1]["sessions"], 1)
self.assertEqual(results[1]["users"], 1)

def test_empty_search_query(self):
runner = ErrorTrackingQueryRunner(
team=self.team,
query=ErrorTrackingQuery(
kind="ErrorTrackingQuery",
fingerprint=None,
dateRange=DateRange(),
filterTestAccounts=False,
searchQuery="probs not found",
),
)

results = self._calculate(runner)["results"]

self.assertEqual(len(results), 0)

@snapshot_clickhouse_queries
def test_fingerprints(self):
runner = ErrorTrackingQueryRunner(
Expand Down
1 change: 1 addition & 0 deletions posthog/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -5046,6 +5046,7 @@ class ErrorTrackingQuery(BaseModel):
)
order: Optional[Order] = None
response: Optional[ErrorTrackingQueryResponse] = None
searchQuery: Optional[str] = None
select: Optional[list[str]] = None


Expand Down
Loading