diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index a77404a9ffd1a..ba60660e3805d 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -3588,6 +3588,16 @@ "ErrorTrackingQuery": { "additionalProperties": false, "properties": { + "assignee": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ] + }, "dateRange": { "$ref": "#/definitions/DateRange" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index ee85f362b7472..0a41adf57544c 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1362,6 +1362,7 @@ export interface ErrorTrackingQuery extends DataNode eventColumns?: string[] order?: 'last_seen' | 'first_seen' | 'occurrences' | 'users' | 'sessions' dateRange: DateRange + assignee?: integer | null filterGroup?: PropertyGroupFilter filterTestAccounts?: boolean limit?: integer diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx index 2ea376d39cea6..89530671584e3 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingFilters.tsx @@ -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' @@ -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 ( -
-
- Date range: - { - setDateRange({ date_from: changedDateFrom, date_to: changedDateTo }) - }} - size="small" - /> -
- {showOrder && ( +
+
- Sort by: - Date range: + { + setDateRange({ date_from: changedDateFrom, date_to: changedDateTo }) + }} size="small" />
- )} + {showOrder && ( +
+ Sort by: + +
+ )} +
+
+ Assigned to: + { + setAssignee(user?.id || null) + }} + /> +
) } diff --git a/frontend/src/scenes/error-tracking/errorTrackingLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingLogic.ts index 1470635d57fd5..acfa4b5b15715 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingLogic.ts @@ -36,6 +36,7 @@ export const errorTrackingLogic = kea([ actions({ setDateRange: (dateRange: DateRange) => ({ dateRange }), + setAssignee: (assignee: number | null) => ({ assignee }), setFilterGroup: (filterGroup: UniversalFiltersGroup) => ({ filterGroup }), setFilterTestAccounts: (filterTestAccounts: boolean) => ({ filterTestAccounts }), setSparklineSelectedPeriod: (period: string | null) => ({ period }), @@ -49,6 +50,13 @@ export const errorTrackingLogic = kea([ setDateRange: (_, { dateRange }) => dateRange, }, ], + assignee: [ + null as number | null, + { persist: true }, + { + setAssignee: (_, { assignee }) => assignee, + }, + ], filterGroup: [ DEFAULT_FILTER_GROUP as UniversalFiltersGroup, { persist: true }, diff --git a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts index 32b3226c9ab28..b391c784ff80c 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts @@ -11,7 +11,10 @@ export const errorTrackingSceneLogic = kea([ path(['scenes', 'error-tracking', 'errorTrackingSceneLogic']), connect({ - values: [errorTrackingLogic, ['dateRange', 'filterTestAccounts', 'filterGroup', 'sparklineSelectedPeriod']], + values: [ + errorTrackingLogic, + ['dateRange', 'assignee', 'filterTestAccounts', 'filterGroup', 'sparklineSelectedPeriod'], + ], }), actions({ @@ -36,11 +39,12 @@ export const errorTrackingSceneLogic = kea([ 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, diff --git a/frontend/src/scenes/error-tracking/queries.ts b/frontend/src/scenes/error-tracking/queries.ts index 6b282f996c035..eaba43b079f74 100644 --- a/frontend/src/scenes/error-tracking/queries.ts +++ b/frontend/src/scenes/error-tracking/queries.ts @@ -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 & { filterGroup: UniversalFiltersGroup sparklineSelectedPeriod: string | null columns?: ('error' | 'volume' | 'occurrences' | 'sessions' | 'users' | 'assignee')[] - limit?: number }): DataTableNode => { const select: string[] = [] if (!columns) { @@ -69,6 +66,7 @@ export const errorTrackingQuery = ({ select: select, order: order, dateRange: dateRange, + assignee: assignee, filterGroup: filterGroup as PropertyGroupFilter, filterTestAccounts: filterTestAccounts, limit: limit, diff --git a/posthog/hogql_queries/error_tracking_query_runner.py b/posthog/hogql_queries/error_tracking_query_runner.py index 624eaa7bd57c5..fb85871753d02 100644 --- a/posthog/hogql_queries/error_tracking_query_runner.py +++ b/posthog/hogql_queries/error_tracking_query_runner.py @@ -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(), ], ), @@ -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(), ], ), @@ -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): @@ -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} diff --git a/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr b/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr index aef3ac9b00dbb..690a0803db188 100644 --- a/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr +++ b/posthog/hogql_queries/test/__snapshots__/test_error_tracking_query_runner.ambr @@ -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, diff --git a/posthog/hogql_queries/test/test_error_tracking_query_runner.py b/posthog/hogql_queries/test/test_error_tracking_query_runner.py index 04032d0aa915c..e140f26eb50e3 100644 --- a/posthog/hogql_queries/test/test_error_tracking_query_runner.py +++ b/posthog/hogql_queries/test/test_error_tracking_query_runner.py @@ -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"]]) diff --git a/posthog/schema.py b/posthog/schema.py index dd666bdc05b58..d76254fae84a0 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -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