Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions frontend/snapshots.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
74 changes: 74 additions & 0 deletions products/error_tracking/frontend/ErrorTracking.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
ErrorTrackingQueryResponse,
ErrorTrackingReleasesQueryResponse,
NodeKind,
TrendsQueryResponse,
} from '~/queries/schema/schema-general'

import { errorTrackingQueryResponse, errorTrackingTypeIssue } from './__mocks__/error_tracking_query'
Expand Down Expand Up @@ -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<string, number[]> = {
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',
Expand Down Expand Up @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div>
Expand All @@ -25,6 +26,12 @@ export function ErrorTrackingInsights(): JSX.Element {
query={exceptionVolumeQuery}
chartKey="exception_volume"
/>
<ChartCard
title="Issues created"
description="New issues per day"
query={issuesCreatedQuery}
chartKey="issues_created"
/>
<ChartCard
title="Affected users"
description="Unique users experiencing exceptions"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ describe('errorTrackingInsightsLogic', () => {
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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import {
buildAffectedUsersQuery,
buildCrashFreeSessionsQuery,
buildExceptionVolumeQuery,
buildIssuesCreatedQuery,
InsightQueryFilters,
} from './queries'

Expand Down Expand Up @@ -71,6 +72,7 @@ export interface errorTrackingInsightsLogicValues {
exceptionVolumeQuery: InsightVizNode<TrendsQuery>
insightQueryFilters: InsightQueryFilters
insightsFilterGroup: UniversalFiltersGroup
issuesCreatedQuery: InsightVizNode<TrendsQuery>
summaryStats: InsightsSummaryStats | null
summaryStatsLoading: boolean
}
Expand Down Expand Up @@ -137,6 +139,10 @@ export interface errorTrackingInsightsLogicMeta {
effectiveDateRange: DateRange,
insightQueryFilters: InsightQueryFilters
) => InsightVizNode<TrendsQuery>
issuesCreatedQuery: (
effectiveDateRange: DateRange,
insightQueryFilters: InsightQueryFilters
) => InsightVizNode<TrendsQuery>
affectedUsersQuery: (
effectiveDateRange: DateRange,
insightQueryFilters: InsightQueryFilters
Expand Down Expand Up @@ -218,6 +224,11 @@ export const errorTrackingInsightsLogic = kea<errorTrackingInsightsLogicType>([
(dateRange: DateRange, filters: InsightQueryFilters): InsightVizNode<TrendsQuery> =>
buildExceptionVolumeQuery(dateRange, filters),
],
issuesCreatedQuery: [
(s) => [s.effectiveDateRange, s.insightQueryFilters],
(dateRange: DateRange, filters: InsightQueryFilters): InsightVizNode<TrendsQuery> =>
buildIssuesCreatedQuery(dateRange, filters),
],
affectedUsersQuery: [
(s) => [s.effectiveDateRange, s.insightQueryFilters],
(dateRange: DateRange, filters: InsightQueryFilters): InsightVizNode<TrendsQuery> =>
Expand Down
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand All @@ -13,11 +19,28 @@ 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,
})
expect(buildCrashFreeSessionsQuery(dateRange, filters).source.tags).toEqual({
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)',
})
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -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[]
Expand Down Expand Up @@ -49,6 +49,38 @@ export function buildExceptionVolumeQuery(
}
}

export function buildIssuesCreatedQuery(
dateRange: DateRange,
{ properties, filterTestAccounts }: InsightQueryFilters
): InsightVizNode<TrendsQuery> {
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)',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting filter but if it works, I love it 🚀

Comment thread
hpouillot marked this conversation as resolved.
},
],
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
Expand Down
29 changes: 28 additions & 1 deletion rust/cymbal/tests/event.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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<DateTime<Utc>> = 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);
Expand Down
Loading