From 27af9eca1e470e85fcd43bf8ebd9e9f1a9abe676 Mon Sep 17 00:00:00 2001 From: Hugues Pouillot Date: Fri, 28 Aug 2026 09:58:39 +0200 Subject: [PATCH 1/4] feat(error-tracking): chart issues created per day Add an Issues created chart to the Error tracking Insights tab. Count unique issues on their first exception while preserving the existing date, property, and test-account filters. Generated-By: PostHog Desktop Task-Id: e5e0d821-a9c7-4b96-9ade-55c378a59fbc --- .../tabs/insights/ErrorTrackingInsights.tsx | 9 +++++- .../errorTrackingInsightsLogic.test.ts | 1 + .../insights/errorTrackingInsightsLogic.ts | 11 +++++++ .../tabs/insights/queries.test.ts | 25 ++++++++++++++- .../tabs/insights/queries.ts | 32 ++++++++++++++++++- 5 files changed, 75 insertions(+), 3 deletions(-) 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..58cad02bdbb3 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,36 @@ 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, + 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 From 018b77a42d2a0fb6ad9cae4614577e5d7b34bada Mon Sep 17 00:00:00 2001 From: Hugues Pouillot Date: Fri, 28 Aug 2026 09:58:41 +0200 Subject: [PATCH 2/4] test(error-tracking): add insights visual snapshot Add a stable Insights tab Storybook story with mocked summary and trend data so visual review covers the new Issues created chart. Document why the first-exception timestamp comparison identifies issue creation. Generated-By: PostHog Desktop Task-Id: e5e0d821-a9c7-4b96-9ade-55c378a59fbc --- .../frontend/ErrorTracking.stories.tsx | 74 +++++++++++++++++++ .../tabs/insights/queries.ts | 2 + 2 files changed, 76 insertions(+) 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/queries.ts b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts index 58cad02bdbb3..1cb511ce4048 100644 --- a/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts +++ b/products/error_tracking/frontend/scenes/ErrorTrackingScene/tabs/insights/queries.ts @@ -64,6 +64,8 @@ export function buildIssuesCreatedQuery( 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)', }, ], From 802de7cc1a48401404e2777cc1277af00b73f2fe Mon Sep 17 00:00:00 2001 From: Hugues Pouillot Date: Fri, 28 Aug 2026 09:58:43 +0200 Subject: [PATCH 3/4] chore(visual): update storybook baselines 2 updated Run: 71d79e03-3be6-4f6a-99cb-0c41d9a94656 Co-authored-by: hpouillot <3455883+hpouillot@users.noreply.github.com> --- frontend/snapshots.yml | 4 ++++ 1 file changed, 4 insertions(+) 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: From ab44d30ef142a3f7ab793c45206e4d0257139bd2 Mon Sep 17 00:00:00 2001 From: Hugues Pouillot Date: Fri, 28 Aug 2026 09:58:45 +0200 Subject: [PATCH 4/4] test(error-tracking): verify issue first-seen timestamp Add an ingestion regression test proving that a new fingerprint stores the originating exception event timestamp as first_seen, including when that timestamp differs from the database creation time. Generated-By: PostHog Desktop Task-Id: e5e0d821-a9c7-4b96-9ade-55c378a59fbc --- rust/cymbal/tests/event.rs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) 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);