Skip to content

Commit

Permalink
feat: filter by assigned exceptions (#24683)
Browse files Browse the repository at this point in the history
  • Loading branch information
daibhin authored Aug 30, 2024
1 parent b2fd4b1 commit cbb7563
Show file tree
Hide file tree
Showing 10 changed files with 154 additions and 58 deletions.
10 changes: 10 additions & 0 deletions frontend/src/queries/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3588,6 +3588,16 @@
"ErrorTrackingQuery": {
"additionalProperties": false,
"properties": {
"assignee": {
"anyOf": [
{
"type": "integer"
},
{
"type": "null"
}
]
},
"dateRange": {
"$ref": "#/definitions/DateRange"
},
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 @@ -1362,6 +1362,7 @@ export interface ErrorTrackingQuery extends DataNode<ErrorTrackingQueryResponse>
eventColumns?: string[]
order?: 'last_seen' | 'first_seen' | 'occurrences' | 'users' | 'sessions'
dateRange: DateRange
assignee?: integer | null
filterGroup?: PropertyGroupFilter
filterTestAccounts?: boolean
limit?: integer
Expand Down
98 changes: 55 additions & 43 deletions frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { LemonSelect } from '@posthog/lemon-ui'
import { useActions, useValues } from 'kea'
import { DateFilter } from 'lib/components/DateFilter/DateFilter'
import { MemberSelect } from 'lib/components/MemberSelect'
import { TaxonomicFilterGroupType } from 'lib/components/TaxonomicFilter/types'
import UniversalFilters from 'lib/components/UniversalFilters/UniversalFilters'
import { universalFiltersLogic } from 'lib/components/UniversalFilters/universalFiltersLogic'
Expand Down Expand Up @@ -71,57 +72,68 @@ const RecordingsUniversalFilterGroup = (): JSX.Element => {
}

export const Options = ({ showOrder = true }: { showOrder?: boolean }): JSX.Element => {
const { dateRange } = useValues(errorTrackingLogic)
const { setDateRange } = useActions(errorTrackingLogic)
const { dateRange, assignee } = useValues(errorTrackingLogic)
const { setDateRange, setAssignee } = useActions(errorTrackingLogic)
const { order } = useValues(errorTrackingSceneLogic)
const { setOrder } = useActions(errorTrackingSceneLogic)

return (
<div className="flex gap-4 py-2">
<div className="flex items-center gap-1">
<span>Date range:</span>
<DateFilter
dateFrom={dateRange.date_from}
dateTo={dateRange.date_to}
onChange={(changedDateFrom, changedDateTo) => {
setDateRange({ date_from: changedDateFrom, date_to: changedDateTo })
}}
size="small"
/>
</div>
{showOrder && (
<div className="flex justify-between">
<div className="flex gap-4 py-2">
<div className="flex items-center gap-1">
<span>Sort by:</span>
<LemonSelect
onSelect={setOrder}
onChange={setOrder}
value={order}
options={[
{
value: 'last_seen',
label: 'Last seen',
},
{
value: 'first_seen',
label: 'First seen',
},
{
value: 'occurrences',
label: 'Occurrences',
},
{
value: 'users',
label: 'Users',
},
{
value: 'sessions',
label: 'Sessions',
},
]}
<span>Date range:</span>
<DateFilter
dateFrom={dateRange.date_from}
dateTo={dateRange.date_to}
onChange={(changedDateFrom, changedDateTo) => {
setDateRange({ date_from: changedDateFrom, date_to: changedDateTo })
}}
size="small"
/>
</div>
)}
{showOrder && (
<div className="flex items-center gap-1">
<span>Sort by:</span>
<LemonSelect
onSelect={setOrder}
onChange={setOrder}
value={order}
options={[
{
value: 'last_seen',
label: 'Last seen',
},
{
value: 'first_seen',
label: 'First seen',
},
{
value: 'occurrences',
label: 'Occurrences',
},
{
value: 'users',
label: 'Users',
},
{
value: 'sessions',
label: 'Sessions',
},
]}
size="small"
/>
</div>
)}
</div>
<div className="flex items-center gap-1">
<span>Assigned to:</span>
<MemberSelect
value={assignee}
onChange={(user) => {
setAssignee(user?.id || null)
}}
/>
</div>
</div>
)
}
Expand Down
8 changes: 8 additions & 0 deletions frontend/src/scenes/error-tracking/errorTrackingLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ export const errorTrackingLogic = kea<errorTrackingLogicType>([

actions({
setDateRange: (dateRange: DateRange) => ({ dateRange }),
setAssignee: (assignee: number | null) => ({ assignee }),
setFilterGroup: (filterGroup: UniversalFiltersGroup) => ({ filterGroup }),
setFilterTestAccounts: (filterTestAccounts: boolean) => ({ filterTestAccounts }),
setSparklineSelectedPeriod: (period: string | null) => ({ period }),
Expand All @@ -49,6 +50,13 @@ export const errorTrackingLogic = kea<errorTrackingLogicType>([
setDateRange: (_, { dateRange }) => dateRange,
},
],
assignee: [
null as number | null,
{ persist: true },
{
setAssignee: (_, { assignee }) => assignee,
},
],
filterGroup: [
DEFAULT_FILTER_GROUP as UniversalFiltersGroup,
{ persist: true },
Expand Down
10 changes: 7 additions & 3 deletions frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ export const errorTrackingSceneLogic = kea<errorTrackingSceneLogicType>([
path(['scenes', 'error-tracking', 'errorTrackingSceneLogic']),

connect({
values: [errorTrackingLogic, ['dateRange', 'filterTestAccounts', 'filterGroup', 'sparklineSelectedPeriod']],
values: [
errorTrackingLogic,
['dateRange', 'assignee', 'filterTestAccounts', 'filterGroup', 'sparklineSelectedPeriod'],
],
}),

actions({
Expand All @@ -36,11 +39,12 @@ export const errorTrackingSceneLogic = kea<errorTrackingSceneLogicType>([

selectors({
query: [
(s) => [s.order, s.dateRange, s.filterTestAccounts, s.filterGroup, s.sparklineSelectedPeriod],
(order, dateRange, filterTestAccounts, filterGroup, sparklineSelectedPeriod): DataTableNode =>
(s) => [s.order, s.dateRange, s.assignee, s.filterTestAccounts, s.filterGroup, s.sparklineSelectedPeriod],
(order, dateRange, assignee, filterTestAccounts, filterGroup, sparklineSelectedPeriod): DataTableNode =>
errorTrackingQuery({
order,
dateRange,
assignee,
filterTestAccounts,
filterGroup,
sparklineSelectedPeriod,
Expand Down
8 changes: 3 additions & 5 deletions frontend/src/scenes/error-tracking/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,19 +35,16 @@ const toStartOfIntervalFn = {
export const errorTrackingQuery = ({
order,
dateRange,
assignee,
filterTestAccounts,
filterGroup,
sparklineSelectedPeriod,
columns,
limit = 50,
}: {
order: ErrorTrackingQuery['order']
dateRange: DateRange
filterTestAccounts: boolean
}: Pick<ErrorTrackingQuery, 'order' | 'dateRange' | 'assignee' | 'filterTestAccounts' | 'limit'> & {
filterGroup: UniversalFiltersGroup
sparklineSelectedPeriod: string | null
columns?: ('error' | 'volume' | 'occurrences' | 'sessions' | 'users' | 'assignee')[]
limit?: number
}): DataTableNode => {
const select: string[] = []
if (!columns) {
Expand All @@ -69,6 +66,7 @@ export const errorTrackingQuery = ({
select: select,
order: order,
dateRange: dateRange,
assignee: assignee,
filterGroup: filterGroup as PropertyGroupFilter,
filterTestAccounts: filterTestAccounts,
limit: limit,
Expand Down
23 changes: 16 additions & 7 deletions posthog/hogql_queries/error_tracking_query_runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ def fingerprint_grouping_expr(self):
ast.Call(
name="has",
args=[
self.group_fingerprints(group),
self.group_fingerprints([group]),
self.extracted_fingerprint_property(),
],
),
Expand Down Expand Up @@ -131,13 +131,19 @@ def where(self):
ast.Placeholder(chain=["filters"]),
]

groups = []

if self.query.fingerprint:
group = self.group_or_default(self.query.fingerprint)
groups.append(self.group_or_default(self.query.fingerprint))
elif self.query.assignee:
groups.extend(self.error_tracking_groups.values())

if groups:
exprs.append(
ast.Call(
name="has",
args=[
self.group_fingerprints(group),
self.group_fingerprints(groups),
self.extracted_fingerprint_property(),
],
),
Expand Down Expand Up @@ -255,10 +261,12 @@ def group_or_default(self, fingerprint):
},
)

def group_fingerprints(self, group):
exprs: list[ast.Expr] = [ast.Constant(value=group["fingerprint"])]
for fp in group["merged_fingerprints"]:
exprs.append(ast.Constant(value=fp))
def group_fingerprints(self, groups):
exprs: list[ast.Expr] = []
for group in groups:
exprs.append(ast.Constant(value=group["fingerprint"]))
for fp in group["merged_fingerprints"]:
exprs.append(ast.Constant(value=fp))
return ast.Array(exprs=exprs)

def extracted_fingerprint_property(self):
Expand All @@ -284,5 +292,6 @@ def error_tracking_groups(self):
if self.query.fingerprint
else queryset.filter(status__in=[ErrorTrackingGroup.Status.ACTIVE])
)
queryset = queryset.filter(assignee=self.query.assignee) if self.query.assignee else queryset
groups = queryset.values("fingerprint", "merged_fingerprints", "status", "assignee")
return {str(item["fingerprint"]): item for item in groups}
Original file line number Diff line number Diff line change
@@ -1,4 +1,27 @@
# serializer version: 1
# name: TestErrorTrackingQueryRunner.test_assignee_groups
'''
SELECT count() 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,
multiIf(has([['SyntaxError']], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)')), ['SyntaxError'], has([['custom_fingerprint']], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)')), ['custom_fingerprint'], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)')) AS fingerprint
FROM events
WHERE and(equals(events.team_id, 2), equals(events.event, '$exception'), 1, has([['SyntaxError'], ['custom_fingerprint']], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)')))
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
'''
# ---
# name: TestErrorTrackingQueryRunner.test_column_names
'''
SELECT count() AS occurrences,
Expand Down
30 changes: 30 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 @@ -324,3 +324,33 @@ def test_merges_and_defaults_groups(self):
},
],
)

@snapshot_clickhouse_queries
def test_assignee_groups(self):
ErrorTrackingGroup.objects.create(
team=self.team,
fingerprint=["SyntaxError"],
assignee=self.user,
)
ErrorTrackingGroup.objects.create(
team=self.team,
fingerprint=["custom_fingerprint"],
assignee=self.user,
)
ErrorTrackingGroup.objects.create(
team=self.team,
fingerprint=["TypeError"],
)

runner = ErrorTrackingQueryRunner(
team=self.team,
query=ErrorTrackingQuery(
kind="ErrorTrackingQuery",
dateRange=DateRange(),
assignee=self.user.pk,
),
)

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

self.assertEqual(sorted([x["fingerprint"] for x in results]), [["SyntaxError"], ["custom_fingerprint"]])
1 change: 1 addition & 0 deletions posthog/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -4578,6 +4578,7 @@ class ErrorTrackingQuery(BaseModel):
model_config = ConfigDict(
extra="forbid",
)
assignee: Optional[int] = None
dateRange: DateRange
eventColumns: Optional[list[str]] = None
filterGroup: Optional[PropertyGroupFilter] = None
Expand Down

0 comments on commit cbb7563

Please sign in to comment.