Skip to content

Commit c5132c6

Browse files
s1gr1dandrewshie-sentry
authored andcommitted
feat(issue-details): Add runtime label for JS issues (#88312)
Add "Frontend" and "Backend" to quickly see the runtime. ![image](https://github.com/user-attachments/assets/38e0ab8c-bb67-44fa-ae88-68b08e757031) ![image](https://github.com/user-attachments/assets/7f904bc4-225c-497d-8c83-530c874ab6ba) part of #85732
1 parent a241f69 commit c5132c6

File tree

5 files changed

+143
-3
lines changed

5 files changed

+143
-3
lines changed

static/app/components/events/contexts/index.spec.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ describe('getOrderedContextItems', function () {
3535
expect(aliasOrder[0]).toBe('response');
3636
expect(aliasOrder[1]).toBe('feedback');
3737
expect(aliasOrder[2]).toBe('user');
38-
expect(aliasOrder[3]).toBe('runtime');
39-
expect(aliasOrder[4]).toBe('os');
38+
expect(aliasOrder[3]).toBe('browser');
39+
expect(aliasOrder[4]).toBe('runtime');
40+
expect(aliasOrder[5]).toBe('os');
4041
});
4142

4243
it('does not fail with missing context items', function () {

static/app/components/events/contexts/index.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,20 @@ export function getOrderedContextItems(event: Event): ContextItem[] {
3636

3737
// hide `flags` in the contexts section since we display this
3838
// info in the feature flag section below
39-
const {feedback, response, runtime, os, flags: _, ...otherContexts} = contexts ?? {};
39+
const {
40+
feedback,
41+
response,
42+
browser,
43+
runtime,
44+
os,
45+
flags: _,
46+
...otherContexts
47+
} = contexts ?? {};
4048
const orderedContext: Array<[ContextItem['alias'], ContextValue]> = [
4149
['response', response],
4250
['feedback', feedback],
4351
['user', {...userContext, ...(customUserData as any)}],
52+
['browser', browser],
4453
['runtime', runtime],
4554
['os', os],
4655
...Object.entries(otherContexts),

static/app/components/events/highlights/highlightsIconSummary.tsx

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import ScreenshotModal, {
1515
modalCss,
1616
} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/modal';
1717
import {SCREENSHOT_NAMES} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/utils';
18+
import {getRuntimeLabelAndTooltip} from 'sentry/components/events/highlights/util';
19+
import {Text} from 'sentry/components/replays/virtualizedGrid/bodyCell';
1820
import {ScrollCarousel} from 'sentry/components/scrollCarousel';
1921
import {Tooltip} from 'sentry/components/tooltip';
2022
import Version from 'sentry/components/version';
@@ -106,10 +108,23 @@ export function HighlightsIconSummary({event, group}: HighlightsIconSummaryProps
106108
const releaseTag = event.tags?.find(tag => tag.key === 'release');
107109
const environmentTag = event.tags?.find(tag => tag.key === 'environment');
108110

111+
const runtimeInfo = getRuntimeLabelAndTooltip(event);
112+
109113
return items.length || screenshot ? (
110114
<Fragment>
111115
<IconBar>
112116
<ScrollCarousel gap={2} aria-label={t('Icon highlights')}>
117+
{runtimeInfo && (
118+
<Fragment>
119+
<Tooltip title={runtimeInfo.tooltip} isHoverable>
120+
<StyledRuntimeText>{runtimeInfo.label}</StyledRuntimeText>
121+
</Tooltip>
122+
<DividerWrapper>
123+
<Divider />
124+
</DividerWrapper>
125+
</Fragment>
126+
)}
127+
113128
{screenshot && group && (
114129
<Fragment>
115130
<ScreenshotButton
@@ -262,3 +277,13 @@ const StyledVersion = styled(Version)`
262277
const ScreenshotButton = styled(Button)`
263278
font-weight: normal;
264279
`;
280+
281+
const DividerWrapper = styled('div')`
282+
display: flex;
283+
align-items: center;
284+
font-size: 1.25rem;
285+
`;
286+
287+
const StyledRuntimeText = styled(Text)`
288+
padding: ${space(0.5)} 0;
289+
`;

static/app/components/events/highlights/util.spec.tsx

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
EMPTY_HIGHLIGHT_DEFAULT,
88
getHighlightContextData,
99
getHighlightTagData,
10+
getRuntimeLabelAndTooltip,
1011
} from 'sentry/components/events/highlights/util';
1112

1213
import {TEST_EVENT_CONTEXTS, TEST_EVENT_TAGS} from './testUtils';
@@ -84,3 +85,67 @@ describe('getHighlightTagData', function () {
8485
expect(missingTagHighlightFromEvent?.value).toBe(EMPTY_HIGHLIGHT_DEFAULT);
8586
});
8687
});
88+
89+
describe('getRuntimeLabel', function () {
90+
it('returns null for non-JavaScript SDK events', function () {
91+
const event = EventFixture({
92+
sdk: {name: 'python'},
93+
});
94+
95+
expect(getRuntimeLabelAndTooltip(event)).toBeNull();
96+
});
97+
98+
it('returns null for javascript issues without context information', function () {
99+
const event = EventFixture({
100+
sdk: {name: 'javascript'},
101+
});
102+
103+
expect(getRuntimeLabelAndTooltip(event)).toBeNull();
104+
});
105+
106+
it('returns inferred runtime from browser context', function () {
107+
const frontendEvent = EventFixture({
108+
sdk: {name: 'javascript'},
109+
contexts: {
110+
browser: {name: 'Chrome'},
111+
},
112+
});
113+
114+
expect(getRuntimeLabelAndTooltip(frontendEvent)?.label).toBe('Frontend');
115+
expect(getRuntimeLabelAndTooltip(frontendEvent)?.tooltip).toBe(
116+
'Error from Chrome browser'
117+
);
118+
});
119+
120+
it.each([
121+
['node', 'Error from Node.js Server Runtime'],
122+
['bun', 'Error from Bun Server Runtime'],
123+
['deno', 'Error from Deno Server Runtime'],
124+
['cloudflare', 'Error from Cloudflare Workers'],
125+
['vercel-edge', 'Error from Vercel Edge Runtime'],
126+
])(
127+
'returns correct runtime label and tooltip for %s runtime',
128+
(runtimeName, expectedTooltip) => {
129+
const event = EventFixture({
130+
sdk: {name: 'javascript'},
131+
contexts: {
132+
runtime: {name: runtimeName},
133+
browser: {name: 'Chrome'}, // Backend events might also have 'browser'
134+
},
135+
});
136+
137+
const result = getRuntimeLabelAndTooltip(event);
138+
expect(result?.label).toBe('Backend');
139+
expect(result?.tooltip).toBe(expectedTooltip);
140+
}
141+
);
142+
143+
it('returns null when no runtime can be determined', function () {
144+
const event = EventFixture({
145+
sdk: {name: 'javascript'},
146+
contexts: {}, // No browser or runtime context
147+
});
148+
149+
expect(getRuntimeLabelAndTooltip(event)).toBeNull();
150+
});
151+
});

static/app/components/events/highlights/util.tsx

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
getFormattedContextData,
1111
} from 'sentry/components/events/contexts/utils';
1212
import type {TagTreeContent} from 'sentry/components/events/eventTags/eventTagsTree';
13+
import {t} from 'sentry/locale';
1314
import type {Event, EventTag} from 'sentry/types/event';
1415
import type {KeyValueListData} from 'sentry/types/group';
1516
import type {Organization} from 'sentry/types/organization';
@@ -170,3 +171,42 @@ export function getHighlightTagData({
170171
originalTag: tagMap[tagKey]?.tag ?? {key: tagKey, value: EMPTY_HIGHLIGHT_DEFAULT},
171172
}));
172173
}
174+
175+
/**
176+
* Returns a label that can be used in the Event Highlight Summary.
177+
* Currently only used for JavaScript-based events.
178+
*/
179+
export function getRuntimeLabelAndTooltip(
180+
event: Event
181+
): {label: string; tooltip: string} | null {
182+
if (!event.sdk?.name.includes('javascript')) {
183+
return null;
184+
}
185+
186+
// eslint-disable-next-line default-case
187+
switch (event.contexts.runtime?.name || '') {
188+
case 'node':
189+
return {label: t('Backend'), tooltip: t('Error from Node.js Server Runtime')};
190+
case 'bun':
191+
return {label: t('Backend'), tooltip: t('Error from Bun Server Runtime')};
192+
case 'deno':
193+
return {label: t('Backend'), tooltip: t('Error from Deno Server Runtime')};
194+
case 'cloudflare':
195+
return {label: t('Backend'), tooltip: t('Error from Cloudflare Workers')};
196+
case 'vercel-edge':
197+
return {label: t('Backend'), tooltip: t('Error from Vercel Edge Runtime')};
198+
}
199+
200+
if (event.contexts.runtime?.name) {
201+
// Browser events don't have a runtime context
202+
return {label: t('Backend'), tooltip: t('Error from Server, Edge or Worker Runtime')};
203+
}
204+
205+
if (event.contexts.browser) {
206+
// Backend events might also have a browser context
207+
const browserName = event.contexts.browser.name || 'browser';
208+
return {label: t('Frontend'), tooltip: t('Error from %s browser', browserName)};
209+
}
210+
211+
return null;
212+
}

0 commit comments

Comments
 (0)