diff --git a/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png b/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png index 476a6507dab73..bef1e957a7f15 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png and b/frontend/__snapshots__/scenes-app-errortracking--list-page--dark.png differ diff --git a/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png b/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png index c3e70a0523ba4..46c495edc53bd 100644 Binary files a/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png and b/frontend/__snapshots__/scenes-app-errortracking--list-page--light.png differ diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index f08cae4a66b03..4c10f7c0660e5 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -5,7 +5,6 @@ import { ActivityLogItem } from 'lib/components/ActivityLog/humanizeActivity' import { apiStatusLogic } from 'lib/logic/apiStatusLogic' import { objectClean, toParams } from 'lib/utils' import posthog from 'posthog-js' -import { stringifiedFingerprint } from 'scenes/error-tracking/utils' import { RecordingComment } from 'scenes/session-recordings/player/inspector/playerInspectorLogic' import { SavedSessionRecordingPlaylistsResult } from 'scenes/session-recordings/saved-playlists/savedSessionRecordingPlaylistsLogic' @@ -14,7 +13,7 @@ import { Variable } from '~/queries/nodes/DataVisualization/types' import { DashboardFilter, DatabaseSerializedFieldType, - ErrorTrackingGroup, + ErrorTrackingIssue, HogCompileResponse, HogQLVariable, QuerySchema, @@ -712,14 +711,12 @@ class ApiRequest { return this.projectsDetail(teamId).addPathComponent('error_tracking') } - public errorTrackingGroup(fingerprint: ErrorTrackingGroup['fingerprint'], teamId?: TeamType['id']): ApiRequest { - return this.errorTracking(teamId) - .addPathComponent('group') - .addPathComponent(stringifiedFingerprint(fingerprint)) + public errorTrackingIssue(id: ErrorTrackingIssue['id'], teamId?: TeamType['id']): ApiRequest { + return this.errorTracking(teamId).addPathComponent('issue').addPathComponent(id) } - public errorTrackingGroupMerge(fingerprint: ErrorTrackingGroup['fingerprint']): ApiRequest { - return this.errorTrackingGroup(fingerprint).addPathComponent('merge') + public errorTrackingIssueMerge(into: ErrorTrackingIssue['id']): ApiRequest { + return this.errorTrackingIssue(into).addPathComponent('merge') } public errorTrackingSymbolSets(teamId?: TeamType['id']): ApiRequest { @@ -1862,21 +1859,22 @@ const api = { errorTracking: { async updateIssue( - fingerprint: ErrorTrackingGroup['fingerprint'], - data: Partial> - ): Promise { - return await new ApiRequest().errorTrackingGroup(fingerprint).update({ data }) + id: ErrorTrackingIssue['id'], + data: Partial> + ): Promise { + return await new ApiRequest().errorTrackingIssue(id).update({ data }) }, - async merge( - primaryFingerprint: ErrorTrackingGroup['fingerprint'], - mergingFingerprints: ErrorTrackingGroup['fingerprint'][] + async mergeInto( + primaryIssueId: ErrorTrackingIssue['id'], + mergingIssueIds: ErrorTrackingIssue['id'][] ): Promise<{ content: string }> { return await new ApiRequest() - .errorTrackingGroup(primaryFingerprint) - .create({ data: { merging_fingerprints: mergingFingerprints } }) + .errorTrackingIssueMerge(primaryIssueId) + .create({ data: { ids: mergingIssueIds } }) }, - async updateSymbolSet(id: ErrorTrackingSymbolSet['id'], data: FormData): Promise { + + async updateSymbolSet(id: ErrorTrackingSymbolSet['id'], data: FormData): Promise { return await new ApiRequest().errorTrackingSymbolSet(id).update({ data }) }, diff --git a/frontend/src/queries/schema.json b/frontend/src/queries/schema.json index 053eab4793c45..50d70878c64bc 100644 --- a/frontend/src/queries/schema.json +++ b/frontend/src/queries/schema.json @@ -1893,7 +1893,7 @@ }, "results": { "items": { - "$ref": "#/definitions/ErrorTrackingGroup" + "$ref": "#/definitions/ErrorTrackingIssue" }, "type": "array" }, @@ -4580,7 +4580,7 @@ }, "results": { "items": { - "$ref": "#/definitions/ErrorTrackingGroup" + "$ref": "#/definitions/ErrorTrackingIssue" }, "type": "array" }, @@ -5498,7 +5498,7 @@ "enum": ["actions", "events", "data_warehouse", "new_entity"], "type": "string" }, - "ErrorTrackingGroup": { + "ErrorTrackingIssue": { "additionalProperties": false, "properties": { "assignee": { @@ -5507,31 +5507,19 @@ "description": { "type": ["string", "null"] }, - "exception_type": { - "type": ["string", "null"] - }, - "fingerprint": { - "items": { - "type": "string" - }, - "type": "array" - }, "first_seen": { "format": "date-time", "type": "string" }, + "id": { + "type": "string" + }, "last_seen": { "format": "date-time", "type": "string" }, - "merged_fingerprints": { - "items": { - "items": { - "type": "string" - }, - "type": "array" - }, - "type": "array" + "name": { + "type": ["string", "null"] }, "occurrences": { "type": "number" @@ -5549,13 +5537,12 @@ "volume": {} }, "required": [ - "fingerprint", - "exception_type", - "merged_fingerprints", + "id", + "name", + "description", "occurrences", "sessions", "users", - "description", "first_seen", "last_seen", "assignee", @@ -5585,11 +5572,8 @@ "filterTestAccounts": { "type": "boolean" }, - "fingerprint": { - "items": { - "type": "string" - }, - "type": "array" + "issueId": { + "type": "string" }, "kind": { "const": "ErrorTrackingQuery", @@ -5658,7 +5642,7 @@ }, "results": { "items": { - "$ref": "#/definitions/ErrorTrackingGroup" + "$ref": "#/definitions/ErrorTrackingIssue" }, "type": "array" }, @@ -9856,7 +9840,7 @@ }, "results": { "items": { - "$ref": "#/definitions/ErrorTrackingGroup" + "$ref": "#/definitions/ErrorTrackingIssue" }, "type": "array" }, @@ -10529,7 +10513,7 @@ }, "results": { "items": { - "$ref": "#/definitions/ErrorTrackingGroup" + "$ref": "#/definitions/ErrorTrackingIssue" }, "type": "array" }, diff --git a/frontend/src/queries/schema.ts b/frontend/src/queries/schema.ts index 166eab904b81c..ae5d085a349c4 100644 --- a/frontend/src/queries/schema.ts +++ b/frontend/src/queries/schema.ts @@ -1934,7 +1934,7 @@ export type CachedSessionAttributionExplorerQueryResponse = CachedQueryResponse< export interface ErrorTrackingQuery extends DataNode { kind: NodeKind.ErrorTrackingQuery - fingerprint?: string[] + issueId?: string select?: HogQLExpression[] order?: 'last_seen' | 'first_seen' | 'occurrences' | 'users' | 'sessions' dateRange: DateRange @@ -1945,14 +1945,13 @@ export interface ErrorTrackingQuery extends DataNode limit?: integer } -export interface ErrorTrackingGroup { - fingerprint: string[] - exception_type: string | null - merged_fingerprints: string[][] +export interface ErrorTrackingIssue { + id: string + name: string | null + description: string | null occurrences: number sessions: number users: number - description: string | null /** @format date-time */ first_seen: string /** @format date-time */ @@ -1963,7 +1962,7 @@ export interface ErrorTrackingGroup { status: 'archived' | 'active' | 'resolved' | 'pending_release' } -export interface ErrorTrackingQueryResponse extends AnalyticsQueryResponseBase { +export interface ErrorTrackingQueryResponse extends AnalyticsQueryResponseBase { hasMore?: boolean limit?: integer offset?: integer diff --git a/frontend/src/scenes/error-tracking/AssigneeSelect.tsx b/frontend/src/scenes/error-tracking/AssigneeSelect.tsx index c45ab1afac27d..aab0690d16a6a 100644 --- a/frontend/src/scenes/error-tracking/AssigneeSelect.tsx +++ b/frontend/src/scenes/error-tracking/AssigneeSelect.tsx @@ -3,7 +3,7 @@ import { LemonButton, LemonButtonProps, ProfilePicture } from '@posthog/lemon-ui import { MemberSelect } from 'lib/components/MemberSelect' import { fullName } from 'lib/utils' -import { ErrorTrackingGroup } from '../../queries/schema' +import { ErrorTrackingIssue } from '../../queries/schema' export const AssigneeSelect = ({ assignee, @@ -11,7 +11,7 @@ export const AssigneeSelect = ({ showName = false, ...buttonProps }: { - assignee: ErrorTrackingGroup['assignee'] + assignee: ErrorTrackingIssue['assignee'] onChange: (userId: number | null) => void showName?: boolean } & Partial>): JSX.Element => { diff --git a/frontend/src/scenes/error-tracking/ErrorTracking.scss b/frontend/src/scenes/error-tracking/ErrorTracking.scss index dabda01de319a..2a981bfdbf970 100644 --- a/frontend/src/scenes/error-tracking/ErrorTracking.scss +++ b/frontend/src/scenes/error-tracking/ErrorTracking.scss @@ -1,4 +1,4 @@ -.ErrorTracking__group { +.ErrorTracking__issue { height: calc(100vh - 12rem); min-height: 25rem; } diff --git a/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx b/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx index 2a97ca624ab98..ef101016a6337 100644 --- a/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTracking.stories.tsx @@ -8,7 +8,6 @@ import { mswDecorator } from '~/mocks/browser' import { NodeKind } from '~/queries/schema' import { errorTrackingEventsQueryResponse, errorTrackingQueryResponse } from './__mocks__/error_tracking_query' -import { stringifiedFingerprint } from './utils' const meta: Meta = { title: 'Scenes-App/ErrorTracking', @@ -41,7 +40,7 @@ export function ListPage(): JSX.Element { export function GroupPage(): JSX.Element { useEffect(() => { - router.actions.push(urls.errorTrackingGroup(stringifiedFingerprint(['TypeError']))) + router.actions.push(urls.errorTrackingIssue('id')) }, []) return } diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx index 3270a14d564cd..5b364cf617129 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingGroupScene.tsx @@ -3,11 +3,10 @@ import './ErrorTracking.scss' import { LemonButton, LemonDivider } from '@posthog/lemon-ui' import { useActions, useValues } from 'kea' import { PageHeader } from 'lib/components/PageHeader' -import { base64Decode } from 'lib/utils' import { useEffect } from 'react' import { SceneExport } from 'scenes/sceneTypes' -import { ErrorTrackingGroup } from '~/queries/schema' +import { ErrorTrackingIssue } from '~/queries/schema' import { AssigneeSelect } from './AssigneeSelect' import ErrorTrackingFilters from './ErrorTrackingFilters' @@ -18,12 +17,10 @@ import { SymbolSetUploadModal } from './SymbolSetUploadModal' export const scene: SceneExport = { component: ErrorTrackingGroupScene, logic: errorTrackingGroupSceneLogic, - paramsToProps: ({ params: { fingerprint } }): (typeof errorTrackingGroupSceneLogic)['props'] => ({ - fingerprint: JSON.parse(base64Decode(decodeURIComponent(fingerprint))), - }), + paramsToProps: ({ params: { id } }): (typeof errorTrackingGroupSceneLogic)['props'] => ({ id }), } -const STATUS_LABEL: Record = { +const STATUS_LABEL: Record = { active: 'Active', archived: 'Archived', resolved: 'Resolved', @@ -31,14 +28,14 @@ const STATUS_LABEL: Record = { } export function ErrorTrackingGroupScene(): JSX.Element { - const { group, groupLoading, hasGroupActions } = useValues(errorTrackingGroupSceneLogic) - const { updateGroup, loadGroup } = useActions(errorTrackingGroupSceneLogic) + const { issue, issueLoading, hasGroupActions } = useValues(errorTrackingGroupSceneLogic) + const { updateIssue, loadIssue } = useActions(errorTrackingGroupSceneLogic) useEffect(() => { // don't like doing this but scene logics do not unmount after being loaded // so this refreshes the group on each page visit in case any changes occurred - if (!groupLoading) { - loadGroup() + if (!issueLoading) { + loadIssue() } }, []) @@ -46,20 +43,20 @@ export function ErrorTrackingGroupScene(): JSX.Element { <> updateGroup({ assignee })} + assignee={issue.assignee} + onChange={(assignee) => updateIssue({ assignee })} type="secondary" showName />
- updateGroup({ status: 'archived' })}> + updateIssue({ status: 'archived' })}> Archive - updateGroup({ status: 'resolved' })}> + updateIssue({ status: 'resolved' })}> Resolve
@@ -68,10 +65,10 @@ export function ErrorTrackingGroupScene(): JSX.Element { updateGroup({ status: 'active' })} + onClick={() => updateIssue({ status: 'active' })} tooltip="Mark as active" > - {STATUS_LABEL[group.status]} + {STATUS_LABEL[issue.status]} ) ) : ( diff --git a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx index 54f05ae67caf2..e9b68fef636a1 100644 --- a/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx +++ b/frontend/src/scenes/error-tracking/ErrorTrackingScene.tsx @@ -11,7 +11,7 @@ import { urls } from 'scenes/urls' import { insightVizDataNodeKey } from '~/queries/nodes/InsightViz/InsightViz' import { Query } from '~/queries/Query/Query' -import { ErrorTrackingGroup } from '~/queries/schema' +import { ErrorTrackingIssue } from '~/queries/schema' import { QueryContext, QueryContextColumnComponent, QueryContextColumnTitleComponent } from '~/queries/types' import { InsightLogicProps } from '~/types' @@ -20,7 +20,6 @@ import { errorTrackingDataNodeLogic } from './errorTrackingDataNodeLogic' import ErrorTrackingFilters from './ErrorTrackingFilters' import { errorTrackingLogic } from './errorTrackingLogic' import { errorTrackingSceneLogic } from './errorTrackingSceneLogic' -import { stringifiedFingerprint } from './utils' export const scene: SceneExport = { component: ErrorTrackingScene, @@ -28,7 +27,7 @@ export const scene: SceneExport = { } export function ErrorTrackingScene(): JSX.Element { - const { query, selectedRowIndexes } = useValues(errorTrackingSceneLogic) + const { query, selectedIssueIds } = useValues(errorTrackingSceneLogic) const insightProps: InsightLogicProps = { dashboardItemId: 'new-ErrorTrackingQuery', @@ -56,29 +55,29 @@ export function ErrorTrackingScene(): JSX.Element { - {selectedRowIndexes.length === 0 ? : } + {selectedIssueIds.length === 0 ? : } ) } const ErrorTrackingActions = (): JSX.Element => { - const { selectedRowIndexes } = useValues(errorTrackingSceneLogic) - const { setSelectedRowIndexes } = useActions(errorTrackingSceneLogic) - const { mergeGroups } = useActions(errorTrackingDataNodeLogic) + const { selectedIssueIds } = useValues(errorTrackingSceneLogic) + const { setSelectedIssueIds } = useActions(errorTrackingSceneLogic) + const { mergeIssues } = useActions(errorTrackingDataNodeLogic) return (
- setSelectedRowIndexes([])}> + setSelectedIssueIds([])}> Unselect all - {selectedRowIndexes.length > 1 && ( + {selectedIssueIds.length > 1 && ( { - mergeGroups(selectedRowIndexes) - setSelectedRowIndexes([]) + mergeIssues(selectedIssueIds) + setSelectedIssueIds([]) }} > Merge @@ -111,13 +110,12 @@ const CustomVolumeColumnHeader: QueryContextColumnTitleComponent = ({ columnName const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => { const { hasGroupActions } = useValues(errorTrackingLogic) - const { selectedRowIndexes } = useValues(errorTrackingSceneLogic) - const { setSelectedRowIndexes } = useActions(errorTrackingSceneLogic) + const { selectedIssueIds } = useValues(errorTrackingSceneLogic) + const { setSelectedIssueIds } = useActions(errorTrackingSceneLogic) - const rowIndex = props.recordIndex - const record = props.record as ErrorTrackingGroup + const record = props.record as ErrorTrackingIssue - const checked = selectedRowIndexes.includes(props.recordIndex) + const checked = selectedIssueIds.includes(record.id) return (
@@ -126,16 +124,16 @@ const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => { className={clsx('pt-1 group-hover:visible', !checked && 'invisible')} checked={checked} onChange={(newValue) => { - setSelectedRowIndexes( + setSelectedIssueIds( newValue - ? [...selectedRowIndexes, rowIndex] - : selectedRowIndexes.filter((id) => id != rowIndex) + ? [...new Set([...selectedIssueIds, record.id])] + : selectedIssueIds.filter((id) => id != record.id) ) }} /> )}
{record.description}
@@ -147,23 +145,20 @@ const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => {
} className="flex-1" - to={urls.errorTrackingGroup(stringifiedFingerprint(record.fingerprint))} + to={urls.errorTrackingIssue(record.id)} />
) } const AssigneeColumn: QueryContextColumnComponent = (props) => { - const { assignGroup } = useActions(errorTrackingDataNodeLogic) + const { assignIssue } = useActions(errorTrackingDataNodeLogic) - const record = props.record as ErrorTrackingGroup + const record = props.record as ErrorTrackingIssue return (
- assignGroup(props.recordIndex, assigneeId)} - /> + assignIssue(record.id, assigneeId)} />
) } diff --git a/frontend/src/scenes/error-tracking/__mocks__/error_tracking_query.ts b/frontend/src/scenes/error-tracking/__mocks__/error_tracking_query.ts index 50fd5d545853c..01a293f12a86a 100644 --- a/frontend/src/scenes/error-tracking/__mocks__/error_tracking_query.ts +++ b/frontend/src/scenes/error-tracking/__mocks__/error_tracking_query.ts @@ -9,7 +9,7 @@ const eventProperties = JSON.stringify({ distinct_id: 'person_id', $exception_message: "Cannot read properties of undefined (reading 'onLCP')", $exception_type: 'TypeError', - $exception_fingerprint: ['TypeError'], + $exception_fingerprint: 'fingerprint', $exception_personURL: 'https://us.posthog.com/project/:id/person/:person_id', $exception_level: 'error', $sentry_event_id: '790b4d4b9ec6430fb88f18ba2dc7e7c4', @@ -89,16 +89,15 @@ const errorTrackingQueryResponse = { columns: ['occurrences', 'sessions', 'users', 'last_seen', 'first_seen', 'description', 'fingerprint', 'volume'], hasMore: false, results: [ - { fingerprint: ['TypeError'], occurrences: 1000, sessions: 750, users: 500 }, - { fingerprint: ['SyntaxError'], occurrences: 800, sessions: 200, users: 50 }, - { fingerprint: ['Error'], occurrences: 6, sessions: 3, users: 1 }, - ].map(({ fingerprint, occurrences, sessions, users }) => ({ + { name: 'TypeError', occurrences: 1000, sessions: 750, users: 500 }, + { name: ['SyntaxError'], occurrences: 800, sessions: 200, users: 50 }, + { name: 'Error', occurrences: 6, sessions: 3, users: 1 }, + ].map(({ name, occurrences, sessions, users }) => ({ assignee: null, - description: `This is a ${fingerprint} error`, - fingerprint: fingerprint, + description: `This is a ${name} error`, + name: name, first_seen: '2023-07-07T00:00:00.000000-00:00', last_seen: '2024-07-07T00:00:00.000000-00:00', - merged_fingerprints: [], occurrences: occurrences, sessions: sessions, users: users, diff --git a/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx b/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx index 1b47979cb8b7a..e173c7e5a259c 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx +++ b/frontend/src/scenes/error-tracking/errorTrackingDataNodeLogic.tsx @@ -2,10 +2,10 @@ import { actions, connect, kea, listeners, path, props } from 'kea' import api from 'lib/api' import { dataNodeLogic, DataNodeLogicProps } from '~/queries/nodes/DataNode/dataNodeLogic' -import { ErrorTrackingGroup } from '~/queries/schema' +import { ErrorTrackingIssue } from '~/queries/schema' import type { errorTrackingDataNodeLogicType } from './errorTrackingDataNodeLogicType' -import { mergeGroups } from './utils' +import { mergeIssues } from './utils' export interface ErrorTrackingDataNodeLogicProps { query: DataNodeLogicProps['query'] @@ -18,52 +18,54 @@ export const errorTrackingDataNodeLogic = kea([ connect(({ key, query }: ErrorTrackingDataNodeLogicProps) => ({ values: [dataNodeLogic({ key, query }), ['response']], - actions: [dataNodeLogic({ key, query }), ['setResponse']], + actions: [dataNodeLogic({ key, query }), ['setResponse', 'loadData']], })), actions({ - mergeGroups: (indexes: number[]) => ({ indexes }), - assignGroup: (recordIndex: number, assigneeId: number | null) => ({ - recordIndex, - assigneeId, - }), + mergeIssues: (ids: string[]) => ({ ids }), + assignIssue: (id: string, assigneeId: number | null) => ({ id, assigneeId }), }), listeners(({ values, actions }) => ({ - mergeGroups: async ({ indexes }) => { - const results = values.response?.results as ErrorTrackingGroup[] + mergeIssues: async ({ ids }) => { + const results = values.response?.results as ErrorTrackingIssue[] - const groups = results.filter((_, id) => indexes.includes(id)) - const primaryGroup = groups.shift() + const issues = results.filter(({ id }) => ids.includes(id)) + const primaryIssue = issues.shift() - if (primaryGroup && groups.length > 0) { - const mergingFingerprints = groups.map((g) => g.fingerprint) - const mergedGroup = mergeGroups(primaryGroup, groups) + if (primaryIssue && issues.length > 0) { + const mergingIds = issues.map((g) => g.id) + const mergedIssue = mergeIssues(primaryIssue, issues) // optimistically update local results actions.setResponse({ ...values.response, results: results - // remove merged groups - .filter((_, id) => !indexes.includes(id)) - .map((group) => - // replace primary group - mergedGroup.fingerprint === group.fingerprint ? mergedGroup : group + // remove merged issues + .filter(({ id }) => !ids.includes(id)) + .map((issue) => + // replace primary issue + mergedIssue.id === issue.id ? mergedIssue : issue ), }) - await api.errorTracking.merge(primaryGroup?.fingerprint, mergingFingerprints) + await api.errorTracking.mergeInto(primaryIssue.id, mergingIds) + actions.loadData(true) } }, - assignGroup: async ({ recordIndex, assigneeId }) => { + assignIssue: async ({ id, assigneeId }) => { const response = values.response if (response) { const params = { assignee: assigneeId } - const results = response.results as ErrorTrackingGroup[] - const group = { ...results[recordIndex], ...params } - results.splice(recordIndex, 1, group) - // optimistically update local results - actions.setResponse({ ...response, results: results }) - await api.errorTracking.updateIssue(group.fingerprint, params) + const results = response.results as ErrorTrackingIssue[] + const recordIndex = results.findIndex((r) => r.id === id) + if (recordIndex > -1) { + const issue = { ...results[recordIndex], ...params } + results.splice(recordIndex, 1, issue) + // optimistically update local results + actions.setResponse({ ...response, results: results }) + await api.errorTracking.updateIssue(issue.id, params) + actions.loadData(true) + } } }, })), diff --git a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts index 506fcdec0e1e0..64445e5116185 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingGroupSceneLogic.ts @@ -6,12 +6,12 @@ import { Dayjs, dayjs } from 'lib/dayjs' import { Scene } from 'scenes/sceneTypes' import { urls } from 'scenes/urls' -import { ErrorTrackingGroup } from '~/queries/schema' +import { ErrorTrackingIssue } from '~/queries/schema' import { Breadcrumb } from '~/types' import type { errorTrackingGroupSceneLogicType } from './errorTrackingGroupSceneLogicType' import { errorTrackingLogic } from './errorTrackingLogic' -import { errorTrackingGroupEventsQuery, errorTrackingGroupQuery } from './queries' +import { errorTrackingIssueEventsQuery, errorTrackingIssueQuery } from './queries' export interface ErrorTrackingEvent { uuid: string @@ -26,10 +26,10 @@ export interface ErrorTrackingEvent { } export interface ErrorTrackingGroupSceneLogicProps { - fingerprint: ErrorTrackingGroup['fingerprint'] + id: ErrorTrackingIssue['id'] } -export enum ErrorGroupTab { +export enum IssueTab { Overview = 'overview', Breakdowns = 'breakdowns', } @@ -37,23 +37,23 @@ export enum ErrorGroupTab { export const errorTrackingGroupSceneLogic = kea([ path((key) => ['scenes', 'error-tracking', 'errorTrackingGroupSceneLogic', key]), props({} as ErrorTrackingGroupSceneLogicProps), - key((props) => JSON.stringify(props.fingerprint)), + key((props) => props.id), connect({ values: [errorTrackingLogic, ['dateRange', 'filterTestAccounts', 'filterGroup', 'hasGroupActions']], }), actions({ - setErrorGroupTab: (tab: ErrorGroupTab) => ({ tab }), + setTab: (tab: IssueTab) => ({ tab }), setActiveEventUUID: (uuid: ErrorTrackingEvent['uuid']) => ({ uuid }), - updateGroup: (group: Partial>) => ({ group }), + updateIssue: (issue: Partial>) => ({ issue }), }), reducers(() => ({ - errorGroupTab: [ - ErrorGroupTab.Overview as ErrorGroupTab, + tab: [ + IssueTab.Overview as IssueTab, { - setErrorGroupTab: (_, { tab }) => tab, + setTab: (_, { tab }) => tab, }, ], activeEventUUID: [ @@ -65,13 +65,13 @@ export const errorTrackingGroupSceneLogic = kea ({ - group: [ - null as ErrorTrackingGroup | null, + issue: [ + null as ErrorTrackingIssue | null, { - loadGroup: async () => { + loadIssue: async () => { const response = await api.query( - errorTrackingGroupQuery({ - fingerprint: props.fingerprint, + errorTrackingIssueQuery({ + issueId: props.id, dateRange: values.dateRange, filterTestAccounts: values.filterTestAccounts, filterGroup: values.filterGroup, @@ -81,13 +81,13 @@ export const errorTrackingGroupSceneLogic = kea { - const response = await api.errorTracking.updateIssue(props.fingerprint, group) - return { ...values.group, ...response } + updateIssue: async ({ issue }) => { + const response = await api.errorTracking.updateIssue(props.id, issue) + return { ...values.issue, ...response } }, }, ], @@ -96,9 +96,9 @@ export const errorTrackingGroupSceneLogic = kea { const response = await api.query( - errorTrackingGroupEventsQuery({ + errorTrackingIssueEventsQuery({ select: ['uuid', 'properties', 'timestamp', 'person'], - fingerprints: values.combinedFingerprints, + issueId: props.id, dateRange: values.dateRange, filterTestAccounts: values.filterTestAccounts, filterGroup: values.filterGroup, @@ -120,7 +120,7 @@ export const errorTrackingGroupSceneLogic = kea ({ - loadGroupSuccess: () => { + loadIssueSuccess: () => { actions.loadEvents() }, loadEventsSuccess: () => { @@ -132,9 +132,9 @@ export const errorTrackingGroupSceneLogic = kea [s.group], - (group): Breadcrumb[] => { - const exceptionType = group?.exception_type || 'Unknown Type' + (s) => [s.issue], + (issue): Breadcrumb[] => { + const exceptionType = issue?.name || 'Unknown Type' return [ { key: Scene.ErrorTracking, @@ -148,20 +148,14 @@ export const errorTrackingGroupSceneLogic = kea [s.group], - (group): ErrorTrackingGroup['fingerprint'][] => - group ? [group.fingerprint, ...group.merged_fingerprints] : [], - ], }), actionToUrl(({ values }) => ({ - setErrorGroupTab: () => { + setTab: () => { const searchParams = router.values.searchParams - if (values.errorGroupTab != ErrorGroupTab.Overview) { - searchParams['tab'] = values.errorGroupTab + if (values.tab != IssueTab.Overview) { + searchParams['tab'] = values.tab } return [router.values.location.pathname, searchParams] @@ -169,9 +163,9 @@ export const errorTrackingGroupSceneLogic = kea ({ - [urls.errorTrackingGroup('*')]: (_, searchParams) => { + [urls.errorTrackingIssue('*')]: (_, searchParams) => { if (searchParams.tab) { - actions.setErrorGroupTab(searchParams.tab) + actions.setTab(searchParams.tab) } }, })), diff --git a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts index f2f2b8713fb4c..e1128d177e0de 100644 --- a/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts +++ b/frontend/src/scenes/error-tracking/errorTrackingSceneLogic.ts @@ -27,7 +27,7 @@ export const errorTrackingSceneLogic = kea([ actions({ setOrder: (order: ErrorTrackingQuery['order']) => ({ order }), - setSelectedRowIndexes: (ids: number[]) => ({ ids }), + setSelectedIssueIds: (ids: string[]) => ({ ids }), }), reducers({ @@ -38,10 +38,10 @@ export const errorTrackingSceneLogic = kea([ setOrder: (_, { order }) => order, }, ], - selectedRowIndexes: [ - [] as number[], + selectedIssueIds: [ + [] as string[], { - setSelectedRowIndexes: (_, { ids }) => ids, + setSelectedIssueIds: (_, { ids }) => ids, }, ], }), @@ -84,6 +84,6 @@ export const errorTrackingSceneLogic = kea([ }), subscriptions(({ actions }) => ({ - query: () => actions.setSelectedRowIndexes([]), + query: () => actions.setSelectedIssueIds([]), })), ]) diff --git a/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx b/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx index 323bbdf3da110..7abbbcd962589 100644 --- a/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx +++ b/frontend/src/scenes/error-tracking/groups/OverviewTab.tsx @@ -12,14 +12,14 @@ import { PropertyIcons } from 'scenes/session-recordings/playlist/SessionRecordi import { ErrorTrackingEvent, errorTrackingGroupSceneLogic } from '../errorTrackingGroupSceneLogic' export const OverviewTab = (): JSX.Element => { - const { events, groupLoading, eventsLoading, activeEventUUID } = useValues(errorTrackingGroupSceneLogic) + const { events, issueLoading, eventsLoading, activeEventUUID } = useValues(errorTrackingGroupSceneLogic) const { loadEvents, setActiveEventUUID } = useActions(errorTrackingGroupSceneLogic) return ( -
+
{ beforeAll(() => { @@ -136,23 +131,3 @@ describe('parseSparklineSelection', () => { expect(parseSparklineSelection('6w')).toEqual({ value: 6, displayAs: 'week' }) }) }) - -describe('stringifyFingerprints', () => { - it('works for basic case', async () => { - expect(stringifyFingerprints([['a', 'b', 'c']])).toEqual("[['a','b','c']]") - expect(stringifyFingerprints([['a']])).toEqual("[['a']]") - expect(stringifyFingerprints([])).toEqual('[]') - }) - - it('escapes single quotes correctly', async () => { - expect(stringifyFingerprints([["a'"]])).toEqual("[['a\\'']]") - expect(stringifyFingerprints([["a'", "b'"]])).toEqual("[['a\\'','b\\'']]") - expect(stringifyFingerprints([["a'", "b'"], ["c'"]])).toEqual("[['a\\'','b\\''],['c\\'']]") - }) - - it('escapes double quotes correctly', async () => { - expect(stringifyFingerprints([['a"']])).toEqual("[['a\"']]") - expect(stringifyFingerprints([['a"', 'b"']])).toEqual("[['a\"','b\"']]") - expect(stringifyFingerprints([['a"', 'b"'], ['c"']])).toEqual("[['a\"','b\"'],['c\"']]") - }) -}) diff --git a/frontend/src/scenes/error-tracking/queries.ts b/frontend/src/scenes/error-tracking/queries.ts index 6061773ecc562..6aa1c3f8d7f5c 100644 --- a/frontend/src/scenes/error-tracking/queries.ts +++ b/frontend/src/scenes/error-tracking/queries.ts @@ -4,7 +4,7 @@ import { range } from 'lib/utils' import { DataTableNode, DateRange, - ErrorTrackingGroup, + ErrorTrackingIssue, ErrorTrackingQuery, EventsQuery, InsightVizNode, @@ -117,36 +117,36 @@ export const generateSparklineProps = ({ return { labels, data } } -export const errorTrackingGroupQuery = ({ - fingerprint, +export const errorTrackingIssueQuery = ({ + issueId, dateRange, filterTestAccounts, filterGroup, }: { - fingerprint: string[] + issueId: string dateRange: DateRange filterTestAccounts: boolean filterGroup: UniversalFiltersGroup }): ErrorTrackingQuery => { return { kind: NodeKind.ErrorTrackingQuery, - fingerprint: fingerprint, + issueId: issueId, dateRange: dateRange, filterGroup: filterGroup as PropertyGroupFilter, filterTestAccounts: filterTestAccounts, } } -export const errorTrackingGroupEventsQuery = ({ +export const errorTrackingIssueEventsQuery = ({ select, - fingerprints, + issueId, dateRange, filterTestAccounts, filterGroup, offset, }: { select: string[] - fingerprints: ErrorTrackingGroup['fingerprint'][] + issueId: ErrorTrackingIssue['id'] dateRange: DateRange filterTestAccounts: boolean filterGroup: UniversalFiltersGroup @@ -155,11 +155,9 @@ export const errorTrackingGroupEventsQuery = ({ const group = filterGroup.values[0] as UniversalFiltersGroup const properties = group.values as AnyPropertyFilter[] - const where = [ - `has(${stringifyFingerprints( - fingerprints - )}, JSONExtract(ifNull(properties.$exception_fingerprint,'[]'),'Array(String)'))`, - ] + // TODO: fix this where clause. It does not take into account the events + // associated with issues that have been merged into this primary issue + const where = [`eq(${issueId}, properties.$exception_issue_id)`] const query: EventsQuery = { kind: NodeKind.EventsQuery, @@ -182,16 +180,6 @@ export const errorTrackingGroupEventsQuery = ({ return query } -// JSON.stringify wraps strings in double quotes and HogQL only supports single quote strings -export const stringifyFingerprints = (fingerprints: ErrorTrackingGroup['fingerprint'][]): string => { - // so we escape all single quoted strings and replace double quotes with single quotes, unless they're already escaped. - // Also replace escaped double quotes with regular double quotes - this isn't valid JSON, but we aren't trying to generate JSON so its ok. - return JSON.stringify(fingerprints) - .replace(/'/g, "\\'") - .replace(/(? { +describe('mergeIssues', () => { it('arbitrary values', async () => { - const primaryGroup: ErrorTrackingGroup = { + const primaryIssue: ErrorTrackingIssue = { + id: 'primaryId', assignee: 400, description: 'This is the original description', - exception_type: 'TypeError', - fingerprint: ['Fingerprint'], + name: 'TypeError', first_seen: '2024-07-22T13:15:07.074000Z', last_seen: '2024-07-20T13:15:50.186000Z', - merged_fingerprints: [['ExistingFingerprint']], occurrences: 250, sessions: 100, status: 'active', @@ -32,15 +31,14 @@ describe('mergeGroups', () => { ], } - const mergingGroups: ErrorTrackingGroup[] = [ + const mergingIssues: ErrorTrackingIssue[] = [ { + id: 'secondId', assignee: 100, description: 'This is another description', - exception_type: 'SyntaxError', - fingerprint: ['Fingerprint2'], + name: 'SyntaxError', first_seen: '2024-07-21T13:15:07.074000Z', last_seen: '2024-07-20T13:15:50.186000Z', - merged_fingerprints: [['NestedFingerprint']], occurrences: 10, sessions: 5, status: 'active', @@ -61,13 +59,12 @@ describe('mergeGroups', () => { ], }, { + id: 'thirdId', assignee: 400, description: 'This is another description', - exception_type: 'SyntaxError', - fingerprint: ['Fingerprint3'], + name: 'SyntaxError', first_seen: '2024-07-21T13:15:07.074000Z', last_seen: '2024-07-22T13:15:50.186000Z', - merged_fingerprints: [], occurrences: 1, sessions: 1, status: 'active', @@ -88,13 +85,12 @@ describe('mergeGroups', () => { ], }, { + id: 'fourthId', assignee: null, description: 'This is another description', - exception_type: 'SyntaxError', - fingerprint: ['Fingerprint4'], + name: 'SyntaxError', first_seen: '2023-07-22T13:15:07.074000Z', last_seen: '2024-07-22T13:15:50.186000Z', - merged_fingerprints: [], occurrences: 1000, sessions: 500, status: 'active', @@ -116,28 +112,19 @@ describe('mergeGroups', () => { }, ] - const mergedGroup = mergeGroups(primaryGroup, mergingGroups) + const mergedIssue = mergeIssues(primaryIssue, mergingIssues) - expect(mergedGroup).toEqual({ + expect(mergedIssue).toEqual({ // retains values from primary group + id: 'primaryId', assignee: 400, description: 'This is the original description', - exception_type: 'TypeError', - fingerprint: ['Fingerprint'], + name: 'TypeError', status: 'active', // earliest first_seen first_seen: '2023-07-22T13:15:07.074Z', // latest last_seen last_seen: '2024-07-22T13:15:50.186Z', - // retains previously merged_fingerprints - // adds new fingerprints AND their nested fingerprints - merged_fingerprints: [ - ['ExistingFingerprint'], - ['Fingerprint2'], - ['NestedFingerprint'], - ['Fingerprint3'], - ['Fingerprint4'], - ], // sums counts occurrences: 1261, sessions: 606, diff --git a/frontend/src/scenes/error-tracking/utils.ts b/frontend/src/scenes/error-tracking/utils.ts index e07ef8a6c2f5c..5ef545f005a98 100644 --- a/frontend/src/scenes/error-tracking/utils.ts +++ b/frontend/src/scenes/error-tracking/utils.ts @@ -1,44 +1,37 @@ import { dayjs } from 'lib/dayjs' -import { base64Encode } from 'lib/utils' -import { ErrorTrackingGroup } from '~/queries/schema' - -export const mergeGroups = ( - primaryGroup: ErrorTrackingGroup, - mergingGroups: ErrorTrackingGroup[] -): ErrorTrackingGroup => { - const mergingFingerprints = mergingGroups.flatMap((g) => [g.fingerprint, ...g.merged_fingerprints]) - - const mergedFingerprints = [...primaryGroup.merged_fingerprints] - mergedFingerprints.push(...mergingFingerprints) +import { ErrorTrackingIssue } from '~/queries/schema' +export const mergeIssues = ( + primaryIssue: ErrorTrackingIssue, + mergingIssues: ErrorTrackingIssue[] +): ErrorTrackingIssue => { const sum = (value: 'occurrences' | 'users' | 'sessions'): number => { - return mergingGroups.reduce((sum, g) => sum + g[value], primaryGroup[value]) + return mergingIssues.reduce((sum, g) => sum + g[value], primaryIssue[value]) } - const [firstSeen, lastSeen] = mergingGroups.reduce( + const [firstSeen, lastSeen] = mergingIssues.reduce( (res, g) => { const firstSeen = dayjs(g.first_seen) const lastSeen = dayjs(g.last_seen) return [res[0].isAfter(firstSeen) ? firstSeen : res[0], res[1].isBefore(lastSeen) ? lastSeen : res[1]] }, - [dayjs(primaryGroup.first_seen), dayjs(primaryGroup.last_seen)] + [dayjs(primaryIssue.first_seen), dayjs(primaryIssue.last_seen)] ) - const volume = primaryGroup.volume + const volume = primaryIssue.volume if (volume) { const dataIndex = 3 - const data = mergingGroups.reduce( + const data = mergingIssues.reduce( (sum: number[], g) => g.volume[dataIndex].map((num: number, idx: number) => num + sum[idx]), - primaryGroup.volume[dataIndex] + primaryIssue.volume[dataIndex] ) volume.splice(dataIndex, 1, data) } return { - ...primaryGroup, - merged_fingerprints: mergedFingerprints, + ...primaryIssue, occurrences: sum('occurrences'), sessions: sum('sessions'), users: sum('users'), @@ -47,7 +40,3 @@ export const mergeGroups = ( volume: volume, } } - -export const stringifiedFingerprint = (fingerprint: string[]): string => { - return base64Encode(JSON.stringify(fingerprint)) -} diff --git a/frontend/src/scenes/scenes.ts b/frontend/src/scenes/scenes.ts index 8d4e73759c647..93972268e2edd 100644 --- a/frontend/src/scenes/scenes.ts +++ b/frontend/src/scenes/scenes.ts @@ -559,7 +559,7 @@ export const routes: Record = { [urls.earlyAccessFeature(':id')]: Scene.EarlyAccessFeature, [urls.errorTracking()]: Scene.ErrorTracking, [urls.errorTrackingConfiguration()]: Scene.ErrorTrackingConfiguration, - [urls.errorTrackingGroup(':fingerprint')]: Scene.ErrorTrackingGroup, + [urls.errorTrackingIssue(':id')]: Scene.ErrorTrackingGroup, [urls.surveys()]: Scene.Surveys, [urls.survey(':id')]: Scene.Survey, [urls.surveyTemplates()]: Scene.SurveyTemplates, diff --git a/frontend/src/scenes/urls.ts b/frontend/src/scenes/urls.ts index 4cd64504e3237..bfcc75e8bd7e3 100644 --- a/frontend/src/scenes/urls.ts +++ b/frontend/src/scenes/urls.ts @@ -164,8 +164,7 @@ export const urls = { earlyAccessFeature: (id: string): string => `/early_access_features/${id}`, errorTracking: (): string => '/error_tracking', errorTrackingConfiguration: (): string => '/error_tracking/configuration', - errorTrackingGroup: (fingerprint: string): string => - `/error_tracking/${fingerprint === ':fingerprint' ? fingerprint : encodeURIComponent(fingerprint)}`, + errorTrackingIssue: (id: string): string => `/error_tracking/${id}`, surveys: (tab?: SurveysTabs): string => `/surveys${tab ? `?tab=${tab}` : ''}`, /** @param id A UUID or 'new'. ':id' for routing. */ survey: (id: string): string => `/surveys/${id}`, diff --git a/frontend/src/scenes/web-analytics/tiles/WebAnalyticsErrorTracking.tsx b/frontend/src/scenes/web-analytics/tiles/WebAnalyticsErrorTracking.tsx index 59692aa627c70..59c6cfcd56821 100644 --- a/frontend/src/scenes/web-analytics/tiles/WebAnalyticsErrorTracking.tsx +++ b/frontend/src/scenes/web-analytics/tiles/WebAnalyticsErrorTracking.tsx @@ -3,22 +3,21 @@ import { TZLabel } from 'lib/components/TZLabel' import { IconOpenInNew } from 'lib/lemon-ui/icons' import { LemonButton } from 'lib/lemon-ui/LemonButton' import { LemonTableLink } from 'lib/lemon-ui/LemonTable/LemonTableLink' -import { stringifiedFingerprint } from 'scenes/error-tracking/utils' import { urls } from 'scenes/urls' import { ErrorTrackingTile } from 'scenes/web-analytics/webAnalyticsLogic' import { QueryFeature } from '~/queries/nodes/DataTable/queryFeatures' import { Query } from '~/queries/Query/Query' -import { ErrorTrackingGroup } from '~/queries/schema' +import { ErrorTrackingIssue } from '~/queries/schema' import { QueryContext, QueryContextColumnComponent } from '~/queries/types' export const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => { - const record = props.record as ErrorTrackingGroup + const record = props.record as ErrorTrackingIssue return (
{record.description}
@@ -28,7 +27,7 @@ export const CustomGroupTitleColumn: QueryContextColumnComponent = (props) => {
} className="flex-1" - to={urls.errorTrackingGroup(stringifiedFingerprint(record.fingerprint))} + to={urls.errorTrackingIssue(record.id)} />
) diff --git a/posthog/hogql_queries/error_tracking_query_runner.py b/posthog/hogql_queries/error_tracking_query_runner.py index 58317854b56ff..20eebdeb1ac2c 100644 --- a/posthog/hogql_queries/error_tracking_query_runner.py +++ b/posthog/hogql_queries/error_tracking_query_runner.py @@ -1,4 +1,6 @@ import re +import structlog + from posthog.hogql import ast from posthog.hogql.constants import LimitContext from posthog.hogql_queries.insights.paginators import HogQLHasMorePaginator @@ -11,6 +13,9 @@ ) from posthog.hogql.parser import parse_expr from posthog.models.filters.mixins.utils import cached_property +from posthog.models.error_tracking import ErrorTrackingIssue + +logger = structlog.get_logger(__name__) class ErrorTrackingQueryRunner(QueryRunner): @@ -32,7 +37,7 @@ def to_query(self) -> ast.SelectQuery: select_from=ast.JoinExpr(table=ast.Field(chain=["events"])), where=self.where(), order_by=self.order_by, - group_by=self.group_by(), + group_by=[ast.Field(chain=["properties", "$exception_issue_id"])], ) def select(self): @@ -48,82 +53,14 @@ def select(self): ), ast.Alias(alias="last_seen", expr=ast.Call(name="max", args=[ast.Field(chain=["timestamp"])])), ast.Alias(alias="first_seen", expr=ast.Call(name="min", args=[ast.Field(chain=["timestamp"])])), - ast.Alias( - alias="description", - expr=ast.Call( - name="nullIf", - args=[ - ast.Call( - name="coalesce", - args=[ - self.extracted_exception_list_property("value"), - ast.Call(name="any", args=[ast.Field(chain=["properties", "$exception_message"])]), - ], - ), - ast.Constant(value=""), - ], - ), - ), - ast.Alias( - alias="exception_type", - expr=ast.Call( - name="nullIf", - args=[ - ast.Call( - name="coalesce", - args=[ - self.extracted_exception_list_property("type"), - ast.Call(name="any", args=[ast.Field(chain=["properties", "$exception_type"])]), - ], - ), - ast.Constant(value=""), - ], - ), - ), + ast.Alias(alias="id", expr=ast.Field(chain=["properties", "$exception_issue_id"])), ] - if not self.query.fingerprint: - exprs.append(self.fingerprint_grouping_expr) - if self.query.select: exprs.extend([parse_expr(x) for x in self.query.select]) return exprs - @property - def fingerprint_grouping_expr(self): - groups = self.error_tracking_groups.values() - - expr: ast.Expr = self.extracted_fingerprint_property() - - if groups: - args: list[ast.Expr] = [] - for group in groups: - # set the "fingerprint" of an exception to match that of the groups primary fingerprint - # replaces exceptions in "merged_fingerprints" with the group fingerprint - args.extend( - [ - ast.Call( - name="has", - args=[ - self.group_fingerprints([group]), - self.extracted_fingerprint_property(), - ], - ), - ast.Constant(value=group["fingerprint"]), - ] - ) - - # default to $exception_fingerprint property for exception events that don't match a group - args.append(self.extracted_fingerprint_property()) - - expr = ast.Call( - name="multiIf", - args=args, - ) - - return ast.Alias(alias="fingerprint", expr=expr) - def where(self): exprs: list[ast.Expr] = [ ast.CompareOperation( @@ -131,25 +68,20 @@ def where(self): left=ast.Field(chain=["event"]), right=ast.Constant(value="$exception"), ), + ast.Call( + name="isNotNull", + args=[ast.Field(chain=["properties", "$exception_issue_id"])], + ), ast.Placeholder(expr=ast.Field(chain=["filters"])), ] - groups = [] - - if 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: + if self.query.issueId: exprs.append( - ast.Call( - name="has", - args=[ - self.group_fingerprints(groups), - self.extracted_fingerprint_property(), - ], - ), + ast.CompareOperation( + op=ast.CompareOperationOp.Eq, + left=ast.Field(chain=["properties", "$exception_issue_id"]), + right=ast.Constant(value=self.query.issueId), + ) ) if self.query.searchQuery: @@ -200,9 +132,6 @@ def where(self): return ast.And(exprs=exprs) - def group_by(self): - return None if self.query.fingerprint else [ast.Field(chain=["fingerprint"])] - def calculate(self): query_result = self.paginator.execute_hogql_query( query=self.to_query(), @@ -231,12 +160,22 @@ def calculate(self): ) def results(self, columns: list[str], query_results: list): - mapped_results = [dict(zip(columns, value)) for value in query_results] results = [] + mapped_results = [dict(zip(columns, value)) for value in query_results] + + issue_ids = [result["id"] for result in mapped_results] + issues = self.error_tracking_issues(issue_ids) + for result_dict in mapped_results: - fingerprint = self.query.fingerprint if self.query.fingerprint else result_dict["fingerprint"] - group = self.group_or_default(fingerprint) - results.append(result_dict | group) + issue = issues.get(result_dict["id"]) + if issue: + results.append(issue | result_dict | {"assignee": self.query.assignee}) + else: + logger.error( + "error tracking issue not found", + issue_id=result_dict["id"], + exc_info=True, + ) return results @@ -257,65 +196,20 @@ def order_by(self): def properties(self): return self.query.filterGroup.values[0].values if self.query.filterGroup else None - def group_or_default(self, fingerprint): - return self.error_tracking_groups.get( - str(fingerprint), - { - "fingerprint": fingerprint, - "assignee": None, - "merged_fingerprints": [], - "status": "active", - # "status": str(ErrorTrackingGroup.Status.ACTIVE), - }, - ) - - 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_exception_list_property(self, property): - return ast.Call( - name="JSON_VALUE", - args=[ - ast.Call(name="any", args=[ast.Field(chain=["properties", "$exception_list"])]), - ast.Constant(value=f"$[0].{property}"), - ], + def error_tracking_issues(self, ids): + queryset = ErrorTrackingIssue.objects.filter(team=self.team, id__in=ids) + queryset = ( + queryset.filter(id=self.query.issueId) + if self.query.issueId + else queryset.filter(status__in=[ErrorTrackingIssue.Status.ACTIVE]) ) - - def extracted_fingerprint_property(self): - return ast.Call( - name="JSONExtract", - args=[ - ast.Call( - name="ifNull", - args=[ - ast.Field(chain=["properties", "$exception_fingerprint"]), - ast.Constant(value="[]"), - ], - ), - ast.Constant(value="Array(String)"), - ], + queryset = ( + queryset.filter(errortrackingissueassignment__user_id=self.query.assignee) + if self.query.assignee + else queryset ) - - @cached_property - def error_tracking_groups(self): - return {} - # queryset = ErrorTrackingGroup.objects.filter(team=self.team) - # # :TRICKY: Ideally we'd have no null characters in the fingerprint, but if something made it into the pipeline with null characters - # # (because rest of the system supports it), try cleaning it up here. Make sure this cleaning is consistent with the rest of the system. - # cleaned_fingerprint = [part.replace("\x00", "\ufffd") for part in self.query.fingerprint or []] - # queryset = ( - # queryset.filter(fingerprint=cleaned_fingerprint) - # 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} + issues = queryset.values("id", "status", "name", "description") + return {str(item["id"]): item for item in issues} def search_tokenizer(query: str) -> list[str]: 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 72b1396910a22..dd53fefcdb46c 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,27 +1,4 @@ # serializer version: 1 -# name: TestErrorTrackingQueryRunner.test_assignee_groups - ''' - 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, - 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, 99999), 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(DISTINCT events.uuid) AS occurrences, @@ -29,9 +6,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), 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 + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') AS id FROM events LEFT OUTER JOIN (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, @@ -50,8 +25,8 @@ WHERE equals(person.team_id, 99999) 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, 99999), equals(events.event, '$exception'), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1)) - GROUP BY fingerprint + WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '')), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1)) + GROUP BY replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, @@ -69,8 +44,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', ''))), '') AS exception_type + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') AS id FROM events LEFT OUTER JOIN (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, @@ -89,28 +63,8 @@ WHERE equals(person.team_id, 99999) 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, 99999), equals(events.event, '$exception'), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1), has([['SyntaxError']], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)'))) - 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_fingerprints - ''' - 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, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', ''))), '') AS exception_type - FROM events - WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), 1, has([['SyntaxError']], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)'))) + WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '')), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1), ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), '01936e7f-d7ff-7314-b2d4-7627981e34f0'), 0)) + GROUP BY replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, @@ -121,323 +75,6 @@ max_bytes_before_external_group_by=0 ''' # --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters - ''' - 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, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', ''))), '') AS exception_type - FROM events - WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), 1, has([['SyntaxError', 'Cannot use \'in\' operator to search for \'wireframes\' in ‹\b\0”\fýf\0ì½é–"\0Ø']], JSONExtract(ifNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_fingerprint'), ''), 'null'), '^"|"$', ''), '[]'), 'Array(String)'))) - 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_fingerprints_with_null_characters.1 - ''' - SELECT "posthog_person"."id", - "posthog_person"."created_at", - "posthog_person"."properties_last_updated_at", - "posthog_person"."properties_last_operation", - "posthog_person"."team_id", - "posthog_person"."properties", - "posthog_person"."is_user_id", - "posthog_person"."is_identified", - "posthog_person"."uuid", - "posthog_person"."version" - FROM "posthog_person" - INNER JOIN "posthog_persondistinctid" ON ("posthog_person"."id" = "posthog_persondistinctid"."person_id") - WHERE ("posthog_persondistinctid"."distinct_id" = 'user_1' - AND "posthog_persondistinctid"."team_id" = 2) - LIMIT 21 - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.10 - ''' - SELECT "posthog_datawarehousejoin"."created_by_id", - "posthog_datawarehousejoin"."created_at", - "posthog_datawarehousejoin"."deleted", - "posthog_datawarehousejoin"."deleted_at", - "posthog_datawarehousejoin"."id", - "posthog_datawarehousejoin"."team_id", - "posthog_datawarehousejoin"."source_table_name", - "posthog_datawarehousejoin"."source_table_key", - "posthog_datawarehousejoin"."joining_table_name", - "posthog_datawarehousejoin"."joining_table_key", - "posthog_datawarehousejoin"."field_name" - FROM "posthog_datawarehousejoin" - WHERE ("posthog_datawarehousejoin"."team_id" = 2 - AND NOT ("posthog_datawarehousejoin"."deleted" - AND "posthog_datawarehousejoin"."deleted" IS NOT NULL)) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.11 - ''' - SELECT "posthog_propertydefinition"."name", - "posthog_propertydefinition"."property_type" - FROM "posthog_propertydefinition" - WHERE ("posthog_propertydefinition"."name" IN ('$exception_message', - '$exception_type', - '$exception_fingerprint') - AND "posthog_propertydefinition"."team_id" = 2 - AND "posthog_propertydefinition"."type" IN (1, - 2, - 3, - 4, - 5 /* ... */)) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.2 - ''' - SELECT "posthog_errortrackinggroup"."fingerprint", - "posthog_errortrackinggroup"."merged_fingerprints", - "posthog_errortrackinggroup"."status", - "posthog_errortrackinggroup"."assignee_id" - FROM "posthog_errortrackinggroup" - WHERE ("posthog_errortrackinggroup"."team_id" = 2 - AND "posthog_errortrackinggroup"."fingerprint" = (ARRAY['SyntaxError', - 'Cannot use ''in'' operator to search for ''wireframes'' in ‹�” ýf�ì½é–"¹’0ø*Lö¹SY A�Ξ÷ԝf - ˆ�Ø'])::text[]) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.3 - ''' - SELECT "posthog_grouptypemapping"."id", - "posthog_grouptypemapping"."team_id", - "posthog_grouptypemapping"."group_type", - "posthog_grouptypemapping"."group_type_index", - "posthog_grouptypemapping"."name_singular", - "posthog_grouptypemapping"."name_plural" - FROM "posthog_grouptypemapping" - WHERE "posthog_grouptypemapping"."team_id" = 2 - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.4 - ''' - SELECT "posthog_datawarehousetable"."created_by_id", - "posthog_datawarehousetable"."created_at", - "posthog_datawarehousetable"."updated_at", - "posthog_datawarehousetable"."deleted", - "posthog_datawarehousetable"."deleted_at", - "posthog_datawarehousetable"."id", - "posthog_datawarehousetable"."name", - "posthog_datawarehousetable"."format", - "posthog_datawarehousetable"."team_id", - "posthog_datawarehousetable"."url_pattern", - "posthog_datawarehousetable"."credential_id", - "posthog_datawarehousetable"."external_data_source_id", - "posthog_datawarehousetable"."columns", - "posthog_datawarehousetable"."row_count", - "posthog_user"."id", - "posthog_user"."password", - "posthog_user"."last_login", - "posthog_user"."first_name", - "posthog_user"."last_name", - "posthog_user"."is_staff", - "posthog_user"."is_active", - "posthog_user"."date_joined", - "posthog_user"."uuid", - "posthog_user"."current_organization_id", - "posthog_user"."current_team_id", - "posthog_user"."email", - "posthog_user"."pending_email", - "posthog_user"."temporary_token", - "posthog_user"."distinct_id", - "posthog_user"."is_email_verified", - "posthog_user"."requested_password_reset_at", - "posthog_user"."has_seen_product_intro_for", - "posthog_user"."strapi_id", - "posthog_user"."theme_mode", - "posthog_user"."partial_notification_settings", - "posthog_user"."anonymize_data", - "posthog_user"."toolbar_mode", - "posthog_user"."hedgehog_config", - "posthog_user"."events_column_config", - "posthog_user"."email_opt_in", - "posthog_datawarehousecredential"."created_by_id", - "posthog_datawarehousecredential"."created_at", - "posthog_datawarehousecredential"."id", - "posthog_datawarehousecredential"."access_key", - "posthog_datawarehousecredential"."access_secret", - "posthog_datawarehousecredential"."team_id", - "posthog_externaldatasource"."created_by_id", - "posthog_externaldatasource"."created_at", - "posthog_externaldatasource"."updated_at", - "posthog_externaldatasource"."deleted", - "posthog_externaldatasource"."deleted_at", - "posthog_externaldatasource"."id", - "posthog_externaldatasource"."source_id", - "posthog_externaldatasource"."connection_id", - "posthog_externaldatasource"."destination_id", - "posthog_externaldatasource"."team_id", - "posthog_externaldatasource"."sync_frequency", - "posthog_externaldatasource"."status", - "posthog_externaldatasource"."source_type", - "posthog_externaldatasource"."job_inputs", - "posthog_externaldatasource"."are_tables_created", - "posthog_externaldatasource"."prefix" - FROM "posthog_datawarehousetable" - LEFT OUTER JOIN "posthog_user" ON ("posthog_datawarehousetable"."created_by_id" = "posthog_user"."id") - LEFT OUTER JOIN "posthog_datawarehousecredential" ON ("posthog_datawarehousetable"."credential_id" = "posthog_datawarehousecredential"."id") - LEFT OUTER JOIN "posthog_externaldatasource" ON ("posthog_datawarehousetable"."external_data_source_id" = "posthog_externaldatasource"."id") - WHERE ("posthog_datawarehousetable"."team_id" = 2 - AND NOT ("posthog_datawarehousetable"."deleted" - AND "posthog_datawarehousetable"."deleted" IS NOT NULL)) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.5 - ''' - SELECT "posthog_datawarehousesavedquery"."created_by_id", - "posthog_datawarehousesavedquery"."created_at", - "posthog_datawarehousesavedquery"."deleted", - "posthog_datawarehousesavedquery"."deleted_at", - "posthog_datawarehousesavedquery"."id", - "posthog_datawarehousesavedquery"."name", - "posthog_datawarehousesavedquery"."team_id", - "posthog_datawarehousesavedquery"."columns", - "posthog_datawarehousesavedquery"."external_tables", - "posthog_datawarehousesavedquery"."query", - "posthog_datawarehousesavedquery"."status", - "posthog_datawarehousesavedquery"."last_run_at" - FROM "posthog_datawarehousesavedquery" - WHERE ("posthog_datawarehousesavedquery"."team_id" = 2 - AND NOT ("posthog_datawarehousesavedquery"."deleted" - AND "posthog_datawarehousesavedquery"."deleted" IS NOT NULL)) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.6 - ''' - SELECT "posthog_datawarehousejoin"."created_by_id", - "posthog_datawarehousejoin"."created_at", - "posthog_datawarehousejoin"."deleted", - "posthog_datawarehousejoin"."deleted_at", - "posthog_datawarehousejoin"."id", - "posthog_datawarehousejoin"."team_id", - "posthog_datawarehousejoin"."source_table_name", - "posthog_datawarehousejoin"."source_table_key", - "posthog_datawarehousejoin"."joining_table_name", - "posthog_datawarehousejoin"."joining_table_key", - "posthog_datawarehousejoin"."field_name" - FROM "posthog_datawarehousejoin" - WHERE ("posthog_datawarehousejoin"."team_id" = 2 - AND NOT ("posthog_datawarehousejoin"."deleted" - AND "posthog_datawarehousejoin"."deleted" IS NOT NULL)) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.7 - ''' - SELECT "posthog_grouptypemapping"."id", - "posthog_grouptypemapping"."team_id", - "posthog_grouptypemapping"."group_type", - "posthog_grouptypemapping"."group_type_index", - "posthog_grouptypemapping"."name_singular", - "posthog_grouptypemapping"."name_plural" - FROM "posthog_grouptypemapping" - WHERE "posthog_grouptypemapping"."team_id" = 2 - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.8 - ''' - SELECT "posthog_datawarehousetable"."created_by_id", - "posthog_datawarehousetable"."created_at", - "posthog_datawarehousetable"."updated_at", - "posthog_datawarehousetable"."deleted", - "posthog_datawarehousetable"."deleted_at", - "posthog_datawarehousetable"."id", - "posthog_datawarehousetable"."name", - "posthog_datawarehousetable"."format", - "posthog_datawarehousetable"."team_id", - "posthog_datawarehousetable"."url_pattern", - "posthog_datawarehousetable"."credential_id", - "posthog_datawarehousetable"."external_data_source_id", - "posthog_datawarehousetable"."columns", - "posthog_datawarehousetable"."row_count", - "posthog_user"."id", - "posthog_user"."password", - "posthog_user"."last_login", - "posthog_user"."first_name", - "posthog_user"."last_name", - "posthog_user"."is_staff", - "posthog_user"."is_active", - "posthog_user"."date_joined", - "posthog_user"."uuid", - "posthog_user"."current_organization_id", - "posthog_user"."current_team_id", - "posthog_user"."email", - "posthog_user"."pending_email", - "posthog_user"."temporary_token", - "posthog_user"."distinct_id", - "posthog_user"."is_email_verified", - "posthog_user"."requested_password_reset_at", - "posthog_user"."has_seen_product_intro_for", - "posthog_user"."strapi_id", - "posthog_user"."theme_mode", - "posthog_user"."partial_notification_settings", - "posthog_user"."anonymize_data", - "posthog_user"."toolbar_mode", - "posthog_user"."hedgehog_config", - "posthog_user"."events_column_config", - "posthog_user"."email_opt_in", - "posthog_datawarehousecredential"."created_by_id", - "posthog_datawarehousecredential"."created_at", - "posthog_datawarehousecredential"."id", - "posthog_datawarehousecredential"."access_key", - "posthog_datawarehousecredential"."access_secret", - "posthog_datawarehousecredential"."team_id", - "posthog_externaldatasource"."created_by_id", - "posthog_externaldatasource"."created_at", - "posthog_externaldatasource"."updated_at", - "posthog_externaldatasource"."deleted", - "posthog_externaldatasource"."deleted_at", - "posthog_externaldatasource"."id", - "posthog_externaldatasource"."source_id", - "posthog_externaldatasource"."connection_id", - "posthog_externaldatasource"."destination_id", - "posthog_externaldatasource"."team_id", - "posthog_externaldatasource"."sync_frequency", - "posthog_externaldatasource"."status", - "posthog_externaldatasource"."source_type", - "posthog_externaldatasource"."job_inputs", - "posthog_externaldatasource"."are_tables_created", - "posthog_externaldatasource"."prefix" - FROM "posthog_datawarehousetable" - LEFT OUTER JOIN "posthog_user" ON ("posthog_datawarehousetable"."created_by_id" = "posthog_user"."id") - LEFT OUTER JOIN "posthog_datawarehousecredential" ON ("posthog_datawarehousetable"."credential_id" = "posthog_datawarehousecredential"."id") - LEFT OUTER JOIN "posthog_externaldatasource" ON ("posthog_datawarehousetable"."external_data_source_id" = "posthog_externaldatasource"."id") - WHERE ("posthog_datawarehousetable"."team_id" = 2 - AND NOT ("posthog_datawarehousetable"."deleted" - AND "posthog_datawarehousetable"."deleted" IS NOT NULL)) - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_fingerprints_with_null_characters.9 - ''' - SELECT "posthog_datawarehousesavedquery"."created_by_id", - "posthog_datawarehousesavedquery"."created_at", - "posthog_datawarehousesavedquery"."deleted", - "posthog_datawarehousesavedquery"."deleted_at", - "posthog_datawarehousesavedquery"."id", - "posthog_datawarehousesavedquery"."name", - "posthog_datawarehousesavedquery"."team_id", - "posthog_datawarehousesavedquery"."columns", - "posthog_datawarehousesavedquery"."external_tables", - "posthog_datawarehousesavedquery"."query", - "posthog_datawarehousesavedquery"."status", - "posthog_datawarehousesavedquery"."last_run_at" - FROM "posthog_datawarehousesavedquery" - WHERE ("posthog_datawarehousesavedquery"."team_id" = 2 - AND NOT ("posthog_datawarehousesavedquery"."deleted" - AND "posthog_datawarehousesavedquery"."deleted" IS NOT NULL)) - ''' -# --- # name: TestErrorTrackingQueryRunner.test_hogql_filters ''' SELECT count(DISTINCT events.uuid) AS occurrences, @@ -445,9 +82,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), 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 + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') AS id FROM events LEFT OUTER JOIN (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, @@ -466,8 +101,29 @@ WHERE equals(person.team_id, 99999) 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, 99999), equals(events.event, '$exception'), ifNull(equals(events__person.properties___email, 'email@posthog.com'), 0)) - GROUP BY fingerprint + WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '')), ifNull(equals(events__person.properties___email, 'email@posthog.com'), 0)) + GROUP BY replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') + 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_issue_grouping + ''' + 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, + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') AS id + FROM events + WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '')), 1, ifNull(equals(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', ''), '01936e7f-d7ff-7314-b2d4-7627981e34f0'), 0)) + GROUP BY replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, @@ -485,9 +141,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), 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 + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') AS id FROM events LEFT OUTER JOIN (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, @@ -506,8 +160,8 @@ WHERE equals(person.team_id, 99999) 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, 99999), 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_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 + WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '')), 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_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 replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, @@ -525,9 +179,7 @@ count(DISTINCT events.distinct_id) AS users, max(toTimeZone(events.timestamp, 'UTC')) AS last_seen, min(toTimeZone(events.timestamp, 'UTC')) AS first_seen, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), 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 + replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') AS id FROM events LEFT OUTER JOIN (SELECT argMax(person_distinct_id_overrides.person_id, person_distinct_id_overrides.version) AS person_id, @@ -546,54 +198,8 @@ WHERE equals(person.team_id, 99999) 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, 99999), equals(events.event, '$exception'), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1), and(or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('databasenotfoundX')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('databasenotfoundX')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('databasenotfoundX')), 0), 0)), or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('clickhouse/client/execute.py')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('clickhouse/client/execute.py')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('clickhouse/client/execute.py')), 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 - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_search_query_with_null_characters - ''' - 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, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), 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 - WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), and(less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2021-01-11 00:00:00.000000', 6, 'UTC')), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2021-01-10 00:00:00.000000', 6, 'UTC'))), or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('wireframe')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('wireframe')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('wireframe')), 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 - ''' -# --- -# name: TestErrorTrackingQueryRunner.test_search_query_with_null_characters.1 - ''' - 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, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].value'), any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', ''))), '') AS description, - nullIf(coalesce(JSON_VALUE(any(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), '$[0].type'), 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 - WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), and(less(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2021-01-11 00:00:00.000000', 6, 'UTC')), greaterOrEquals(toTimeZone(events.timestamp, 'UTC'), toDateTime64('2021-01-10 00:00:00.000000', 6, 'UTC'))), or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('f\0ì½é')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('f\0ì½é')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('f\0ì½é')), 0), 0))) - GROUP BY fingerprint + WHERE and(equals(events.team_id, 99999), equals(events.event, '$exception'), isNotNull(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '')), ifNull(notILike(events__person.properties___email, '%@posthog.com%'), 1), and(or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('databasenotfoundX')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('databasenotfoundX')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('databasenotfoundX')), 0), 0)), or(ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_list'), ''), 'null'), '^"|"$', '')), lower('clickhouse/client/execute.py')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_type'), ''), 'null'), '^"|"$', '')), lower('clickhouse/client/execute.py')), 0), 0), ifNull(greater(position(lower(replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_message'), ''), 'null'), '^"|"$', '')), lower('clickhouse/client/execute.py')), 0), 0)))) + GROUP BY replaceRegexpAll(nullIf(nullIf(JSONExtractRaw(events.properties, '$exception_issue_id'), ''), 'null'), '^"|"$', '') LIMIT 101 OFFSET 0 SETTINGS readonly=2, max_execution_time=60, 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 01c6731d2c768..0071779a0e18c 100644 --- a/posthog/hogql_queries/test/test_error_tracking_query_runner.py +++ b/posthog/hogql_queries/test/test_error_tracking_query_runner.py @@ -11,6 +11,7 @@ PersonPropertyFilter, PropertyOperator, ) +from posthog.models.error_tracking import ErrorTrackingIssue from posthog.test.base import ( APIBaseTest, ClickhouseTestMixin, @@ -182,6 +183,24 @@ class TestErrorTrackingQueryRunner(ClickhouseTestMixin, APIBaseTest): distinct_id_one = "user_1" distinct_id_two = "user_2" + issue_one = "01936e7f-d7ff-7314-b2d4-7627981e34f0" + issue_two = "01936e80-5e69-7e70-b837-871f5cdad28b" + issue_three = "01936e80-aa51-746f-aec4-cdf16a5c5332" + + def create_events_and_issue(self, issue_id, distinct_ids, exception_list=None): + event_properties = {"$exception_issue_id": issue_id} + if exception_list: + event_properties["$exception_list"] = exception_list + + for distinct_id in distinct_ids: + _create_event( + distinct_id=distinct_id, + event="$exception", + team=self.team, + properties=event_properties, + ) + + ErrorTrackingIssue.objects.create(id=issue_id, team=self.team) def setUp(self): super().setUp() @@ -202,44 +221,12 @@ def setUp(self): is_identified=True, ) - _create_event( - distinct_id=self.distinct_id_one, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": ["SyntaxError"], - "$exception_list": [ - { - "type": "SyntaxError", - "value": "this is the same error message", - } - ], - }, - ) - _create_event( - distinct_id=self.distinct_id_one, - event="$exception", - team=self.team, - properties={"$exception_fingerprint": ["TypeError"], "$exception_list": [{"type": "TypeError"}]}, - ) - _create_event( - distinct_id=self.distinct_id_two, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": ["SyntaxError"], - "$exception_list": [{"type": "SyntaxError", "value": "this is the same error message"}], - }, - ) - _create_event( - distinct_id=self.distinct_id_two, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": ["custom_fingerprint"], - "$exception_list": [{"type": "SyntaxError", "value": "this is the same error message"}], - }, + self.create_events_and_issue( + issue_id=self.issue_one, + distinct_ids=[self.distinct_id_one, self.distinct_id_two], ) + self.create_events_and_issue(issue_id=self.issue_two, distinct_ids=[self.distinct_id_one]) + self.create_events_and_issue(issue_id=self.issue_three, distinct_ids=[self.distinct_id_two]) flush_persons_and_events() @@ -252,7 +239,7 @@ def test_column_names(self): team=self.team, query=ErrorTrackingQuery( kind="ErrorTrackingQuery", - fingerprint=None, + issueId=None, dateRange=DateRange(), filterTestAccounts=True, ), @@ -267,9 +254,7 @@ def test_column_names(self): "users", "last_seen", "first_seen", - "description", - "exception_type", - "fingerprint", + "id", ], ) @@ -277,7 +262,7 @@ def test_column_names(self): team=self.team, query=ErrorTrackingQuery( kind="ErrorTrackingQuery", - fingerprint=["SyntaxError"], + issueId=self.issue_one, dateRange=DateRange(), filterTestAccounts=True, ), @@ -292,40 +277,44 @@ def test_column_names(self): "users", "last_seen", "first_seen", - "description", - "exception_type", + "id", ], ) + @snapshot_clickhouse_queries + def test_issue_grouping(self): + runner = ErrorTrackingQueryRunner( + team=self.team, + query=ErrorTrackingQuery( + kind="ErrorTrackingQuery", + issueId=self.issue_one, + dateRange=DateRange(), + ), + ) + + results = self._calculate(runner)["results"] + # returns a single group with multiple errors + self.assertEqual(len(results), 1) + self.assertEqual(results[0]["id"], self.issue_one) + self.assertEqual(results[0]["occurrences"], 2) + @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_list": [{"type": "DatabaseNotFoundX", "value": "this is the same error message"}], - }, + self.create_events_and_issue( + issue_id="01936e81-b0ce-7b56-8497-791e505b0d0c", + distinct_ids=[self.distinct_id_one], + exception_list=[{"type": "DatabaseNotFoundX", "value": "this is the same error message"}], ) - _create_event( - distinct_id=self.distinct_id_one, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": ["DatabaseNotFoundY"], - "$exception_list": [{"type": "DatabaseNotFoundY", "value": "this is the same error message"}], - }, + self.create_events_and_issue( + issue_id="01936e81-f5ce-79b1-99f1-f0e9675fcfef", + distinct_ids=[self.distinct_id_one], + exception_list=[{"type": "DatabaseNotFoundY", "value": "this is the same error message"}], ) - _create_event( - distinct_id=self.distinct_id_two, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": ["xyz"], - "$exception_list": [{"type": "xyz", "value": "this is the same error message"}], - }, + self.create_events_and_issue( + issue_id="01936e82-241e-7e27-b47d-6659c54eb0be", + distinct_ids=[self.distinct_id_two], + exception_list=[{"type": "xyz", "value": "this is the same error message"}], ) flush_persons_and_events() @@ -333,22 +322,22 @@ def test_search_query(self): team=self.team, query=ErrorTrackingQuery( kind="ErrorTrackingQuery", - fingerprint=None, + issueId=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"]) + results = sorted(self._calculate(runner)["results"], key=lambda x: x["id"]) self.assertEqual(len(results), 2) - self.assertEqual(results[0]["fingerprint"], ["DatabaseNotFoundX"]) + self.assertEqual(results[0]["id"], "01936e81-b0ce-7b56-8497-791e505b0d0c") 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]["id"], "01936e81-f5ce-79b1-99f1-f0e9675fcfef") self.assertEqual(results[1]["occurrences"], 1) self.assertEqual(results[1]["sessions"], 1) self.assertEqual(results[1]["users"], 1) @@ -358,7 +347,7 @@ def test_empty_search_query(self): team=self.team, query=ErrorTrackingQuery( kind="ErrorTrackingQuery", - fingerprint=None, + issueId=None, dateRange=DateRange(), filterTestAccounts=False, searchQuery="probs not found", @@ -372,36 +361,28 @@ def test_empty_search_query(self): @snapshot_clickhouse_queries def test_search_query_with_multiple_search_items(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_list": [ - { - "type": "DatabaseNotFoundX", - "value": "this is the same error message", - "stack_trace": {"frames": SAMPLE_STACK_TRACE}, - } - ], - }, + self.create_events_and_issue( + issue_id="01936e81-b0ce-7b56-8497-791e505b0d0c", + distinct_ids=[self.distinct_id_one], + exception_list=[ + { + "type": "DatabaseNotFoundX", + "value": "this is the same error message", + "stack_trace": {"frames": SAMPLE_STACK_TRACE}, + } + ], ) - _create_event( - distinct_id=self.distinct_id_two, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": ["DatabaseNotFoundY"], - "$exception_list": [ - { - "type": "DatabaseNotFoundY", - "value": "this is the same error message", - "stack_trace": {"frames": SAMPLE_STACK_TRACE}, - } - ], - }, + self.create_events_and_issue( + issue_id="01936e81-f5ce-79b1-99f1-f0e9675fcfef", + distinct_ids=[self.distinct_id_two], + exception_list=[ + { + "type": "DatabaseNotFoundY", + "value": "this is the same error message", + "stack_trace": {"frames": SAMPLE_STACK_TRACE}, + } + ], ) flush_persons_and_events() @@ -409,7 +390,7 @@ def test_search_query_with_multiple_search_items(self): team=self.team, query=ErrorTrackingQuery( kind="ErrorTrackingQuery", - fingerprint=None, + issueId=None, dateRange=DateRange(), filterTestAccounts=True, searchQuery="databasenotfoundX clickhouse/client/execute.py", @@ -419,143 +400,18 @@ def test_search_query_with_multiple_search_items(self): results = self._calculate(runner)["results"] self.assertEqual(len(results), 1) - self.assertEqual(results[0]["fingerprint"], ["DatabaseNotFoundX"]) + self.assertEqual(results[0]["id"], "01936e81-b0ce-7b56-8497-791e505b0d0c") self.assertEqual(results[0]["occurrences"], 1) self.assertEqual(results[0]["sessions"], 1) self.assertEqual(results[0]["users"], 1) - @snapshot_clickhouse_queries - def test_search_query_with_null_characters(self): - fingerprint_with_null_bytes = [ - "SyntaxError", - "Cannot use 'in' operator to search for 'wireframes' in \x1f\x8b\x08\x00\x94\x0cýf\x00\x03ì½é\x96\"¹\x920ø*Lö¹SY\x1dA\x00Î\x9e÷Ô\x9df\r\x88\x00Ø", - ] - exception_type_with_null_bytes = "SyntaxError\x00" - exception_message_with_null_bytes = "this is the same error message\x00" - exception_stack_trace_with_null_bytes = { - "frames": [ - { - "filename": "file.py\x00", - "lineno": 1, - "colno": 1, - "function": "function\x00", - "extra": "Cannot use 'in' operator to search for 'wireframes' in \x1f\x8b\x08\x00\x94\x0cýf\x00\x03ì½é\x96\"¹\x920ø*Lö¹SY\x1dA\x00Î\x9e÷Ô\x9df\r\x88\x00Ø", - } - ] - } - with freeze_time("2021-01-10 12:11:00"): - _create_event( - distinct_id=self.distinct_id_one, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": fingerprint_with_null_bytes, - "$exception_list": [ - { - "type": exception_type_with_null_bytes, - "value": exception_message_with_null_bytes, - "stack_trace": exception_stack_trace_with_null_bytes, - } - ], - }, - ) - flush_persons_and_events() - - runner = ErrorTrackingQueryRunner( - team=self.team, - query=ErrorTrackingQuery( - kind="ErrorTrackingQuery", - searchQuery="wireframe", - dateRange=DateRange(date_from="2021-01-10", date_to="2021-01-11"), - ), - ) - - results = self._calculate(runner)["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["fingerprint"], fingerprint_with_null_bytes) - self.assertEqual(results[0]["occurrences"], 1) - - # TODO: Searching for null characters doesn't work, probs because of how clickhouse handles this. Should it work??? - runner = ErrorTrackingQueryRunner( - team=self.team, - query=ErrorTrackingQuery( - kind="ErrorTrackingQuery", - searchQuery="f\x00\x03ì½é", - dateRange=DateRange(date_from="2021-01-10", date_to="2021-01-11"), - ), - ) - results = self._calculate(runner)["results"] - self.assertEqual(len(results), 0) - - @snapshot_clickhouse_queries - def test_fingerprints(self): - runner = ErrorTrackingQueryRunner( - team=self.team, - query=ErrorTrackingQuery( - kind="ErrorTrackingQuery", - fingerprint=["SyntaxError"], - dateRange=DateRange(), - ), - ) - - results = self._calculate(runner)["results"] - # returns a single group with multiple errors - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["fingerprint"], ["SyntaxError"]) - self.assertEqual(results[0]["occurrences"], 2) - - @snapshot_clickhouse_queries - def test_fingerprints_with_null_characters(self): - fingerprint_with_null_bytes = [ - "SyntaxError", - "Cannot use 'in' operator to search for 'wireframes' in \x1f\x8b\x08\x00\x94\x0cýf\x00\x03ì½é\x96\"\x00Ø", - ] - exception_type_with_null_bytes = "SyntaxError\x00" - exception_message_with_null_bytes = "this is the same error message\x00" - exception_stack_trace_with_null_bytes = { - "frames": [{"filename": "file.py\x00", "lineno": 1, "colno": 1, "function": "function\x00"}] - } - with freeze_time("2020-01-10 12:11:00"): - _create_event( - distinct_id=self.distinct_id_one, - event="$exception", - team=self.team, - properties={ - "$exception_fingerprint": fingerprint_with_null_bytes, - "$exception_list": [ - { - "type": exception_type_with_null_bytes, - "value": exception_message_with_null_bytes, - "stack_trace": exception_stack_trace_with_null_bytes, - } - ], - }, - ) - flush_persons_and_events() - - runner = ErrorTrackingQueryRunner( - team=self.team, - query=ErrorTrackingQuery( - kind="ErrorTrackingQuery", - fingerprint=fingerprint_with_null_bytes, - dateRange=DateRange(), - ), - ) - - results = self._calculate(runner)["results"] - self.assertEqual(len(results), 1) - self.assertEqual(results[0]["fingerprint"], fingerprint_with_null_bytes) - self.assertEqual(results[0]["occurrences"], 1) - def test_only_returns_exception_events(self): with freeze_time("2020-01-10 12:11:00"): _create_event( distinct_id=self.distinct_id_one, event="$pageview", team=self.team, - properties={ - "$exception_fingerprint": ["SyntaxError"], - }, + properties={"$exception_issue_id": self.issue_one}, ) flush_persons_and_events() diff --git a/posthog/schema.py b/posthog/schema.py index 11d9b36c5aedd..1b9ba09ef7b01 100644 --- a/posthog/schema.py +++ b/posthog/schema.py @@ -746,17 +746,16 @@ class Status(StrEnum): PENDING_RELEASE = "pending_release" -class ErrorTrackingGroup(BaseModel): +class ErrorTrackingIssue(BaseModel): model_config = ConfigDict( extra="forbid", ) assignee: Optional[float] = None description: Optional[str] = None - exception_type: Optional[str] = None - fingerprint: list[str] first_seen: AwareDatetime + id: str last_seen: AwareDatetime - merged_fingerprints: list[list[str]] + name: Optional[str] = None occurrences: float sessions: float status: Status @@ -2719,7 +2718,7 @@ class QueryResponseAlternative15(BaseModel): query_status: Optional[QueryStatus] = Field( default=None, description="Query status indicates whether next to the provided data, a query is still running." ) - results: list[ErrorTrackingGroup] + results: list[ErrorTrackingIssue] timings: Optional[list[QueryTiming]] = Field( default=None, description="Measured timings for different parts of the query generation process" ) @@ -2931,7 +2930,7 @@ class QueryResponseAlternative27(BaseModel): query_status: Optional[QueryStatus] = Field( default=None, description="Query status indicates whether next to the provided data, a query is still running." ) - results: list[ErrorTrackingGroup] + results: list[ErrorTrackingIssue] timings: Optional[list[QueryTiming]] = Field( default=None, description="Measured timings for different parts of the query generation process" ) @@ -3948,7 +3947,7 @@ class CachedErrorTrackingQueryResponse(BaseModel): query_status: Optional[QueryStatus] = Field( default=None, description="Query status indicates whether next to the provided data, a query is still running." ) - results: list[ErrorTrackingGroup] + results: list[ErrorTrackingIssue] timezone: str timings: Optional[list[QueryTiming]] = Field( default=None, description="Measured timings for different parts of the query generation process" @@ -4828,7 +4827,7 @@ class Response9(BaseModel): query_status: Optional[QueryStatus] = Field( default=None, description="Query status indicates whether next to the provided data, a query is still running." ) - results: list[ErrorTrackingGroup] + results: list[ErrorTrackingIssue] timings: Optional[list[QueryTiming]] = Field( default=None, description="Measured timings for different parts of the query generation process" ) @@ -5012,7 +5011,7 @@ class ErrorTrackingQueryResponse(BaseModel): query_status: Optional[QueryStatus] = Field( default=None, description="Query status indicates whether next to the provided data, a query is still running." ) - results: list[ErrorTrackingGroup] + results: list[ErrorTrackingIssue] timings: Optional[list[QueryTiming]] = Field( default=None, description="Measured timings for different parts of the query generation process" ) @@ -6143,7 +6142,7 @@ class ErrorTrackingQuery(BaseModel): dateRange: DateRange filterGroup: Optional[PropertyGroupFilter] = None filterTestAccounts: Optional[bool] = None - fingerprint: Optional[list[str]] = None + issueId: Optional[str] = None kind: Literal["ErrorTrackingQuery"] = "ErrorTrackingQuery" limit: Optional[int] = None modifiers: Optional[HogQLQueryModifiers] = Field(