diff --git a/frontend/snapshots.yml b/frontend/snapshots.yml index 05f227124c26..125934197e52 100644 --- a/frontend/snapshots.yml +++ b/frontend/snapshots.yml @@ -6460,6 +6460,10 @@ snapshots: hash: v1.k794b7964.361c63d8098b60ca1bbf69206dc8f6711320146663aaf32dbd6651168a0b600b.dMsSmC7-kkmsg2uY_UCUtBk-2KP8U8EC-CgTQBwgCkE scenes-app-errortracking--group-page-with-self-driving--light: hash: v1.k794b7964.4c24d565a6a126a64761040eb44a64d5a689fc04a8b566ccc5410147c19451fc.wuQskHSi8SlMXfFZuza565EKQMIw1RhLM-1UCclFPJ8 + scenes-app-errortracking--insights-page--dark: + hash: v1.k794b7964.53b21c283d6e02513197c47385305bcde043553c03a05f410c4fca51b8ac3c76.ZqqLe0fDujqr3-AQieKGpq9howa1_XaoLdPAoZC9ceI + scenes-app-errortracking--insights-page--light: + hash: v1.k794b7964.0da15fab41604af57c36492d1eeb1cfbe0524557126ab8e61b1f2bace0ddbd10.zQQ_qIbZM_7JnmZHvOIudQQ5rmFGKeuyNiRxvBC4WJk scenes-app-errortracking--list-page--dark: hash: v1.k794b7964.2fcf2311aa6f601214b532ccb22e83ef378b87ed1fd6705085b8276ca245ba6f.zLha5NipvTWdyRZqgMiowFL3N5MicRiuwrfuAa22y7c scenes-app-errortracking--list-page--light: diff --git a/products/error_tracking/frontend/ErrorTracking.stories.tsx b/products/error_tracking/frontend/ErrorTracking.stories.tsx index c173a9c3a3e8..51e99ca153ce 100644 --- a/products/error_tracking/frontend/ErrorTracking.stories.tsx +++ b/products/error_tracking/frontend/ErrorTracking.stories.tsx @@ -16,6 +16,7 @@ import { ErrorTrackingQueryResponse, ErrorTrackingReleasesQueryResponse, NodeKind, + TrendsQueryResponse, } from '~/queries/schema/schema-general' import { errorTrackingQueryResponse, errorTrackingTypeIssue } from './__mocks__/error_tracking_query' @@ -378,6 +379,52 @@ const STORY_SUMMARY_RESPONSE: ErrorTrackingQueryResponse = { }, ], } +const STORY_INSIGHT_DAYS = [ + '2024-07-02', + '2024-07-03', + '2024-07-04', + '2024-07-05', + '2024-07-06', + '2024-07-07', + '2024-07-08', +] +const STORY_INSIGHT_LABELS = [ + '2-Jul-2024', + '3-Jul-2024', + '4-Jul-2024', + '5-Jul-2024', + '6-Jul-2024', + '7-Jul-2024', + '8-Jul-2024', +] +const STORY_INSIGHT_DATA: Record = { + Exceptions: [18, 24, 16, 31, 22, 27, 19], + 'Issues created': [4, 6, 3, 8, 5, 7, 4], + 'Affected users': [12, 15, 11, 21, 14, 18, 13], + 'Crash-free sessions %': [98.8, 98.2, 99.1, 97.6, 98.5, 97.9, 98.7], +} + +function buildStoryInsightResponse(label: string): TrendsQueryResponse { + const data = STORY_INSIGHT_DATA[label] ?? [] + return { + results: [ + { + action: + label === 'Crash-free sessions %' + ? null + : { id: '$exception', type: 'events', name: label, order: 0 }, + order: 0, + label, + count: data.reduce((sum, value) => sum + value, 0), + aggregated_value: data.reduce((sum, value) => sum + value, 0), + data, + labels: STORY_INSIGHT_LABELS, + days: STORY_INSIGHT_DAYS, + }, + ], + } +} + const meta: Meta = { component: App, title: 'Scenes-App/ErrorTracking', @@ -459,6 +506,33 @@ export default meta type Story = StoryObj<{}> export const ListPage: Story = {} +export const InsightsPage: Story = { + parameters: { pageUrl: urls.errorTracking({ activeTab: 'insights' }) }, + decorators: [ + mswDecorator({ + post: { + '/api/environments/:team_id/query/:kind/': async ({ request }) => { + const body = (await request.json()) as { + query?: { + kind?: string + series?: { custom_name?: string }[] + trendsFilter?: { formulaNodes?: { custom_name?: string }[] } + } + } + if (body.query?.kind === NodeKind.HogQLQuery) { + return [200, { results: [[157, 76, 1240, 42]] }] + } + const label = + body.query?.trendsFilter?.formulaNodes?.[0]?.custom_name ?? + body.query?.series?.[0]?.custom_name ?? + '' + return [200, buildStoryInsightResponse(label)] + }, + }, + }), + ], +} + // An unresolved source maps recommendation renders the wizard banner above the // issue list without the sticky filters bar overlapping its bottom edge export const ListPageWithSourceMapsBanner: Story = { diff --git a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/ErrorTrackingInsights.tsx b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/ErrorTrackingInsights.tsx index 0af2c19f7e5c..2151674e96c4 100644 --- a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/ErrorTrackingInsights.tsx +++ b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/ErrorTrackingInsights.tsx @@ -8,7 +8,8 @@ import { InsightsFilters } from './InsightsFilters' import { SummaryStats } from './SummaryStats' export function ErrorTrackingInsights(): JSX.Element { - const { exceptionVolumeQuery, affectedUsersQuery, crashFreeSessionsQuery } = useValues(errorTrackingInsightsLogic) + const { exceptionVolumeQuery, issuesCreatedQuery, affectedUsersQuery, crashFreeSessionsQuery } = + useValues(errorTrackingInsightsLogic) return (
@@ -25,6 +26,12 @@ export function ErrorTrackingInsights(): JSX.Element { query={exceptionVolumeQuery} chartKey="exception_volume" /> + { expect(JSON.stringify(insights.values.exceptionVolumeQuery)).not.toContain( PropertyFilterType.ErrorTrackingIssue ) + expect(JSON.stringify(insights.values.issuesCreatedQuery)).not.toContain(PropertyFilterType.ErrorTrackingIssue) const lastSummaryStatsQuery = jest.mocked(api.query).mock.calls.at(-1)?.[0] as any expect(lastSummaryStatsQuery.filters.properties).toEqual(inner.values) diff --git a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/errorTrackingInsightsLogic.ts b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/errorTrackingInsightsLogic.ts index 0420cedce61d..356bccca5065 100644 --- a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/errorTrackingInsightsLogic.ts +++ b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/errorTrackingInsightsLogic.ts @@ -28,6 +28,7 @@ import { buildAffectedUsersQuery, buildCrashFreeSessionsQuery, buildExceptionVolumeQuery, + buildIssuesCreatedQuery, InsightQueryFilters, } from './queries' @@ -71,6 +72,7 @@ export interface errorTrackingInsightsLogicValues { exceptionVolumeQuery: InsightVizNode insightQueryFilters: InsightQueryFilters insightsFilterGroup: UniversalFiltersGroup + issuesCreatedQuery: InsightVizNode summaryStats: InsightsSummaryStats | null summaryStatsLoading: boolean } @@ -137,6 +139,10 @@ export interface errorTrackingInsightsLogicMeta { effectiveDateRange: DateRange, insightQueryFilters: InsightQueryFilters ) => InsightVizNode + issuesCreatedQuery: ( + effectiveDateRange: DateRange, + insightQueryFilters: InsightQueryFilters + ) => InsightVizNode affectedUsersQuery: ( effectiveDateRange: DateRange, insightQueryFilters: InsightQueryFilters @@ -218,6 +224,11 @@ export const errorTrackingInsightsLogic = kea([ (dateRange: DateRange, filters: InsightQueryFilters): InsightVizNode => buildExceptionVolumeQuery(dateRange, filters), ], + issuesCreatedQuery: [ + (s) => [s.effectiveDateRange, s.insightQueryFilters], + (dateRange: DateRange, filters: InsightQueryFilters): InsightVizNode => + buildIssuesCreatedQuery(dateRange, filters), + ], affectedUsersQuery: [ (s) => [s.effectiveDateRange, s.insightQueryFilters], (dateRange: DateRange, filters: InsightQueryFilters): InsightVizNode => diff --git a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.test.ts b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.test.ts index 036c4be5a31c..bc0e0af670de 100644 --- a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.test.ts +++ b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.test.ts @@ -1,6 +1,12 @@ import { ProductKey } from '~/queries/schema/schema-general' +import { HogQLMathType } from '~/types' -import { buildAffectedUsersQuery, buildCrashFreeSessionsQuery, buildExceptionVolumeQuery } from './queries' +import { + buildAffectedUsersQuery, + buildCrashFreeSessionsQuery, + buildExceptionVolumeQuery, + buildIssuesCreatedQuery, +} from './queries' describe('error tracking insights queries', () => { it('tags chart queries as error tracking', () => { @@ -13,6 +19,9 @@ describe('error tracking insights queries', () => { expect(buildExceptionVolumeQuery(dateRange, filters).source.tags).toEqual({ productKey: ProductKey.ERROR_TRACKING, }) + expect(buildIssuesCreatedQuery(dateRange, filters).source.tags).toEqual({ + productKey: ProductKey.ERROR_TRACKING, + }) expect(buildAffectedUsersQuery(dateRange, filters).source.tags).toEqual({ productKey: ProductKey.ERROR_TRACKING, }) @@ -20,4 +29,18 @@ describe('error tracking insights queries', () => { productKey: ProductKey.ERROR_TRACKING, }) }) + + it('counts each issue on its first exception', () => { + const query = buildIssuesCreatedQuery( + { date_from: '-7d', date_to: null }, + { properties: [], filterTestAccounts: false } + ) + + expect(query.source.series[0]).toMatchObject({ + event: '$exception', + custom_name: 'Issues created', + math: HogQLMathType.HogQL, + math_hogql: 'uniqIf(issue_id, timestamp = issue_first_seen)', + }) + }) }) diff --git a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts index 5f45b5e49bd9..1cb511ce4048 100644 --- a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts +++ b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts @@ -3,7 +3,7 @@ import { dateStringToDayJs } from 'lib/utils/dateFilters' import { urls } from 'scenes/urls' import { DateRange, InsightVizNode, NodeKind, ProductKey, TrendsQuery } from '~/queries/schema/schema-general' -import { AnyPropertyFilter, BaseMathType, ChartDisplayType, IntervalType } from '~/types' +import { AnyPropertyFilter, BaseMathType, ChartDisplayType, HogQLMathType, IntervalType } from '~/types' export interface InsightQueryFilters { properties: AnyPropertyFilter[] @@ -49,6 +49,38 @@ export function buildExceptionVolumeQuery( } } +export function buildIssuesCreatedQuery( + dateRange: DateRange, + { properties, filterTestAccounts }: InsightQueryFilters +): InsightVizNode { + const interval = getInterval(dateRange.date_from, dateRange.date_to) + return { + kind: NodeKind.InsightVizNode, + source: { + kind: NodeKind.TrendsQuery, + series: [ + { + kind: NodeKind.EventsNode, + event: '$exception', + custom_name: 'Issues created', + math: HogQLMathType.HogQL, + // Cymbal stores issue_first_seen from the same event timestamp when it creates the fingerprint, + // so equality selects the event that created the issue rather than its later occurrences. + math_hogql: 'uniqIf(issue_id, timestamp = issue_first_seen)', + }, + ], + interval, + dateRange, + trendsFilter: { display: ChartDisplayType.ActionsBar }, + filterTestAccounts, + properties, + tags: { productKey: ProductKey.ERROR_TRACKING }, + }, + showHeader: false, + showTable: false, + } +} + export function buildAffectedUsersQuery( dateRange: DateRange, { properties, filterTestAccounts }: InsightQueryFilters diff --git a/rust/cymbal/tests/event.rs b/rust/cymbal/tests/event.rs index 3687ec6ab374..c385c6ee853d 100644 --- a/rust/cymbal/tests/event.rs +++ b/rust/cymbal/tests/event.rs @@ -1,7 +1,7 @@ use std::{collections::HashMap, fs, sync::Arc}; use axum::{body::Body, http::Request}; -use chrono::Utc; +use chrono::{DateTime, Utc}; use common_types::error_tracking::FrameId; use cymbal::{ error::UnhandledError, @@ -833,6 +833,33 @@ async fn new_issue_uses_newest_fingerprint_version(db: PgPool) { ); } +#[sqlx::test(migrations = "./tests/test_migrations")] +async fn new_issue_stores_event_timestamp_as_fingerprint_first_seen(db: PgPool) { + let harness = TestHarness::new(db); + let mut input = resolved_stack_event("src/app.js"); + input.timestamp = "2020-02-03T04:05:06.789Z".to_string(); + + let (status, body): (_, SuccessResponse) = harness.post_event(&input).await; + assert!(status.is_success()); + + let event = body.first_event().as_ref().unwrap(); + let fingerprint = event.properties["$exception_fingerprint"] + .as_str() + .expect("fingerprint should be a string"); + let stored_first_seen: Option> = sqlx::query_scalar( + "SELECT first_seen FROM posthog_errortrackingissuefingerprintv2 WHERE team_id = 1 AND fingerprint = $1", + ) + .bind(fingerprint) + .fetch_one(&harness.db) + .await + .expect("first_seen should be queryable"); + + assert_eq!( + stored_first_seen, + Some(input.timestamp.parse().expect("timestamp should be valid")) + ); +} + #[sqlx::test(migrations = "./tests/test_migrations")] async fn existing_issue_under_older_version_wins(db: PgPool) { let harness = TestHarness::new(db);