Skip to content

feat(replay): Show the correct level of each issue in Replay Details breadcrumbs #97552

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 11, 2025
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
16 changes: 7 additions & 9 deletions static/app/components/events/eventReplay/index.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import {EventFixture} from 'sentry-fixture/event';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
import {RawReplayErrorFixture} from 'sentry-fixture/replay/error';
import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb';
import {ReplayErrorFixture} from 'sentry-fixture/replayError';
import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';

import {render, screen} from 'sentry-test/reactTestingLibrary';
Expand All @@ -15,7 +15,7 @@ import {
useReplayOnboardingSidebarPanel,
} from 'sentry/utils/replays/hooks/useReplayOnboarding';
import ReplayReader from 'sentry/utils/replays/replayReader';
import type {ReplayError} from 'sentry/views/replays/types';
import type {RawReplayError} from 'sentry/utils/replays/types';

jest.mock('sentry/utils/replays/hooks/useReplayOnboarding');
jest.mock('sentry/utils/replays/hooks/useLoadReplayReader');
Expand All @@ -32,25 +32,23 @@ jest.mock(
const mockEventTimestamp = new Date('2022-09-22T16:59:41Z');
const mockReplayId = '761104e184c64d439ee1014b72b4d83b';

const mockErrors: ReplayError[] = [
ReplayErrorFixture({
const mockErrors: RawReplayError[] = [
RawReplayErrorFixture({
id: '1',
issue: 'JAVASCRIPT-101',
'issue.id': 101,
'error.value': ['Something bad happened.'],
'error.type': ['error'],
'project.name': 'javascript',
timestamp: mockEventTimestamp.toISOString(),
timestamp: mockEventTimestamp,
title: 'Something bad happened.',
}),
ReplayErrorFixture({
RawReplayErrorFixture({
id: '2',
issue: 'JAVASCRIPT-102',
'issue.id': 102,
'error.value': ['Something bad happened 2.'],
'error.type': ['error'],
'project.name': 'javascript',
timestamp: mockEventTimestamp.toISOString(),
timestamp: mockEventTimestamp,
title: 'Something bad happened 2.',
}),
];
Expand Down
2 changes: 1 addition & 1 deletion static/app/components/replays/breadcrumbs/errorTitle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function CrumbErrorTitle({frame}: {frame: ErrorFrame}) {

return (
<Fragment>
Error:{' '}
{frame.data.level || 'Error'}:{' '}
<Link
to={`/organizations/${organization.slug}/issues/${frame.data.groupId}/events/${frame.data.eventId}/#replay`}
>
Expand Down
26 changes: 14 additions & 12 deletions static/app/components/replays/header/errorCounts.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
import {ReplayErrorFixture} from 'sentry-fixture/replayError';
import {RawReplayErrorFixture} from 'sentry-fixture/replay/error';
import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';

import {render, screen} from 'sentry-test/reactTestingLibrary';
Expand All @@ -11,7 +11,7 @@ import ProjectsStore from 'sentry/stores/projectsStore';
const replayRecord = ReplayRecordFixture();
const organization = OrganizationFixture();

const baseErrorProps = {id: '1', issue: '', timestamp: new Date().toISOString()};
const baseErrorProps = {id: '1', issue: '', timestamp: new Date()};

describe('ErrorCounts', () => {
beforeEach(() => {
Expand Down Expand Up @@ -43,7 +43,9 @@ describe('ErrorCounts', () => {
});

it('should render an icon & count when all errors come from a single project', async () => {
const errors = [ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-js-app'})];
const errors = [
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-js-app'}),
];

render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
organization,
Expand All @@ -63,9 +65,9 @@ describe('ErrorCounts', () => {

it('should render an icon & count with links when there are errors in two unique projects', async () => {
const errors = [
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-js-app'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-js-app'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
];

render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
Expand Down Expand Up @@ -93,12 +95,12 @@ describe('ErrorCounts', () => {

it('should render multiple icons, but a single count and link, when there are errors in three or more projects', async () => {
const errors = [
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-js-app'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-node-service'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-node-service'}),
ReplayErrorFixture({...baseErrorProps, 'project.name': 'my-node-service'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-js-app'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-py-backend'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-node-service'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-node-service'}),
RawReplayErrorFixture({...baseErrorProps, 'project.name': 'my-node-service'}),
];

render(<ErrorCounts replayErrors={errors} replayRecord={replayRecord} />, {
Expand Down
5 changes: 3 additions & 2 deletions static/app/components/replays/header/errorCounts.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {t, tn} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Project} from 'sentry/types/project';
import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
import type {RawReplayError} from 'sentry/utils/replays/types';
import {useLocation} from 'sentry/utils/useLocation';
import type {HydratedReplayRecord, ReplayError} from 'sentry/views/replays/types';
import type {HydratedReplayRecord} from 'sentry/views/replays/types';

type Props = {
replayErrors: ReplayError[];
replayErrors: RawReplayError[];
replayRecord: HydratedReplayRecord;
};

Expand Down
5 changes: 3 additions & 2 deletions static/app/components/replays/header/replayMetaData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import {space} from 'sentry/styles/space';
import EventView from 'sentry/utils/discover/eventView';
import getRouteStringFromRoutes from 'sentry/utils/getRouteStringFromRoutes';
import {TabKey} from 'sentry/utils/replays/hooks/useActiveReplayTab';
import type {RawReplayError} from 'sentry/utils/replays/types';
import {useLocation} from 'sentry/utils/useLocation';
import {useRoutes} from 'sentry/utils/useRoutes';
import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
import type {ReplayRecord} from 'sentry/views/replays/types';

interface Props {
replayErrors: ReplayError[];
replayErrors: RawReplayError[];
replayRecord: ReplayRecord;
showDeadRageClicks?: boolean;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import {useMemo} from 'react';
import countBy from 'lodash/countBy';

import type {RawReplayError} from 'sentry/utils/replays/types';
import useProjects from 'sentry/utils/useProjects';
import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
import type {ReplayRecord} from 'sentry/views/replays/types';

type Props = {
replayErrors: ReplayError[];
replayErrors: RawReplayError[];
replayRecord: ReplayRecord;
};

Expand Down
10 changes: 5 additions & 5 deletions static/app/utils/replays/getDiffTimestamps.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ import {
IncrementalSource,
isHydrationErrorFrame,
type RawBreadcrumbFrame,
type RawReplayError,
} from 'sentry/utils/replays/types';
import type {ReplayError} from 'sentry/views/replays/types';

const START_DATE = new Date('2022-06-15T00:40:00.000Z');
const INIT_DATE = new Date('2022-06-15T00:40:00.100Z');
Expand Down Expand Up @@ -62,7 +62,7 @@ const RRWEB_EVENTS = [
}),
];

function getMockReplay(rrwebEvents: any[], errors: ReplayError[]) {
function getMockReplay(rrwebEvents: any[], errors: RawReplayError[]) {
const attachments = [...rrwebEvents];
const replay = ReplayReader.factory({
replayRecord,
Expand All @@ -77,7 +77,7 @@ function getMockReplay(rrwebEvents: any[], errors: ReplayError[]) {
function getMockReplayWithCrumbFrame(
rrwebEvents: any[],
crumbFrame: RawBreadcrumbFrame,
errors: ReplayError[]
errors: RawReplayError[]
) {
const attachments = [...rrwebEvents];

Expand Down Expand Up @@ -155,7 +155,7 @@ describe('getReplayDiffOffsetsFromEvent', () => {
});
const errorEvent = EventFixture({dateCreated: ERROR_DATE.toISOString()});
const {replay} = getMockReplayWithCrumbFrame(RRWEB_EVENTS, rawHydrationCrumbFrame, [
errorEvent as any as ReplayError,
errorEvent as any as RawReplayError,
]);

const [hydratedHydrationCrumbFrame] = hydrateBreadcrumbs(replayRecord, [
Expand All @@ -170,7 +170,7 @@ describe('getReplayDiffOffsetsFromEvent', () => {

it('should get offsets when no hydration breadcrumb exists', () => {
const errorEvent = EventFixture({dateCreated: ERROR_DATE.toISOString()});
const {replay} = getMockReplay(RRWEB_EVENTS, [errorEvent as any as ReplayError]);
const {replay} = getMockReplay(RRWEB_EVENTS, [errorEvent as any as RawReplayError]);

expect(getReplayDiffOffsetsFromEvent(replay!, errorEvent)).toEqual({
frameOrEvent: errorEvent,
Expand Down
22 changes: 11 additions & 11 deletions static/app/utils/replays/hooks/useReplayData.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type {ReactNode} from 'react';
import {duration} from 'moment-timezone';
import {RawReplayErrorFixture} from 'sentry-fixture/replay/error';
import {
ReplayConsoleEventFixture,
ReplayNavigateEventFixture,
} from 'sentry-fixture/replay/helpers';
import {RRWebInitFrameEventsFixture} from 'sentry-fixture/replay/rrweb';
import {ReplayErrorFixture} from 'sentry-fixture/replayError';
import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';

import {initializeOrg} from 'sentry-test/initializeOrg';
Expand Down Expand Up @@ -278,31 +278,31 @@ describe('useReplayData', () => {
});

const mockErrorResponse1 = [
ReplayErrorFixture({
RawReplayErrorFixture({
id: ERROR_IDS[0]!,
issue: 'JAVASCRIPT-123E',
timestamp: startedAt.toISOString(),
timestamp: startedAt,
}),
];
const mockErrorResponse2 = [
ReplayErrorFixture({
RawReplayErrorFixture({
id: ERROR_IDS[1]!,
issue: 'JAVASCRIPT-789Z',
timestamp: startedAt.toISOString(),
timestamp: startedAt,
}),
];
const mockErrorResponse3 = [
ReplayErrorFixture({
RawReplayErrorFixture({
id: ERROR_IDS[0]!,
issue: 'JAVASCRIPT-123E',
timestamp: startedAt.toISOString(),
timestamp: startedAt,
}),
];
const mockErrorResponse4 = [
ReplayErrorFixture({
RawReplayErrorFixture({
id: ERROR_IDS[1]!,
issue: 'JAVASCRIPT-789Z',
timestamp: startedAt.toISOString(),
timestamp: startedAt,
}),
];

Expand Down Expand Up @@ -418,10 +418,10 @@ describe('useReplayData', () => {
timestamp: startedAt,
});
const mockErrorResponse = [
ReplayErrorFixture({
RawReplayErrorFixture({
id: ERROR_IDS[0]!,
issue: 'JAVASCRIPT-123E',
timestamp: startedAt.toISOString(),
timestamp: startedAt,
}),
];

Expand Down
11 changes: 6 additions & 5 deletions static/app/utils/replays/hooks/useReplayData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,9 @@ import {useApiQuery, useQueryClient} from 'sentry/utils/queryClient';
import useFeedbackEvents from 'sentry/utils/replays/hooks/useFeedbackEvents';
import {useReplayProjectSlug} from 'sentry/utils/replays/hooks/useReplayProjectSlug';
import {mapResponseToReplayRecord} from 'sentry/utils/replays/replayDataUtils';
import type {RawReplayError} from 'sentry/utils/replays/types';
import type RequestError from 'sentry/utils/requestError/requestError';
import type {ReplayError, ReplayRecord} from 'sentry/views/replays/types';
import type {ReplayRecord} from 'sentry/views/replays/types';

type Options = {
/**
Expand Down Expand Up @@ -41,7 +42,7 @@ type Options = {
interface Result {
attachmentError: undefined | RequestError[];
attachments: unknown[];
errors: ReplayError[];
errors: RawReplayError[];
fetchError: undefined | RequestError;
isError: boolean;
isPending: boolean;
Expand Down Expand Up @@ -200,7 +201,7 @@ function useReplayData({
pages: errorPages,
status: fetchErrorsStatus,
getLastResponseHeader: lastErrorsResponseHeader,
} = useFetchParallelPages<{data: ReplayError[]}>({
} = useFetchParallelPages<{data: RawReplayError[]}>({
enabled: enableErrors,
hits: replayRecord?.count_errors ?? 0,
getQueryKey: getErrorsQueryKey,
Expand All @@ -214,15 +215,15 @@ function useReplayData({
(!replayRecord?.count_errors || Boolean(links.next?.results)) &&
fetchErrorsStatus === 'success';
const {pages: extraErrorPages, status: fetchExtraErrorsStatus} =
useFetchSequentialPages<{data: ReplayError[]}>({
useFetchSequentialPages<{data: RawReplayError[]}>({
enabled: enableExtraErrors,
initialCursor: links.next?.cursor,
getQueryKey: getErrorsQueryKey,
perPage: errorsPerPage,
});

const {pages: platformErrorPages, status: fetchPlatformErrorsStatus} =
useFetchSequentialPages<{data: ReplayError[]}>({
useFetchSequentialPages<{data: RawReplayError[]}>({
enabled: true,
getQueryKey: getPlatformErrorsQueryKey,
perPage: errorsPerPage,
Expand Down
3 changes: 3 additions & 0 deletions static/app/utils/replays/hydrateErrors.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('hydrateErrors', () => {
groupShortId: 'JS-374',
label: '',
labels: [],
level: 'Error',
projectSlug: 'javascript',
},
message: 'A Redirect with :orgId param on customer domain',
Expand All @@ -41,6 +42,7 @@ describe('hydrateErrors', () => {
groupShortId: 'JS-374',
label: '',
labels: [],
level: 'Error',
projectSlug: 'javascript',
},
message: 'A Redirect with :orgId param on customer domain',
Expand All @@ -57,6 +59,7 @@ describe('hydrateErrors', () => {
groupShortId: 'JS-374',
label: '',
labels: [],
level: 'Error',
projectSlug: 'javascript',
},
message: 'A Redirect with :orgId param on customer domain',
Expand Down
2 changes: 2 additions & 0 deletions static/app/utils/replays/hydrateErrors.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export default function hydrateErrors(
(Array.isArray(e['error.type']) ? e['error.type'][0] : e['error.type']) ??
'',
labels: toArray(e['error.type']).filter(Boolean),
level: e.level,
projectSlug: e['project.name'],
},
message:
Expand All @@ -64,6 +65,7 @@ export default function hydrateErrors(
label:
(Array.isArray(e['error.type']) ? e['error.type'][0] : e['error.type']) ?? '',
labels: toArray(e['error.type']).filter(defined),
level: e.level,
projectSlug: e['project.name'],
},
message: e.title,
Expand Down
Loading
Loading