Skip to content

feat(issue-details): Add runtime label for JS issues #88312

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 12 commits into from
Apr 14, 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
5 changes: 3 additions & 2 deletions static/app/components/events/contexts/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ describe('getOrderedContextItems', function () {
expect(aliasOrder[0]).toBe('response');
expect(aliasOrder[1]).toBe('feedback');
expect(aliasOrder[2]).toBe('user');
expect(aliasOrder[3]).toBe('runtime');
expect(aliasOrder[4]).toBe('os');
expect(aliasOrder[3]).toBe('browser');
expect(aliasOrder[4]).toBe('runtime');
expect(aliasOrder[5]).toBe('os');
});

it('does not fail with missing context items', function () {
Expand Down
11 changes: 10 additions & 1 deletion static/app/components/events/contexts/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,20 @@ export function getOrderedContextItems(event: Event): ContextItem[] {

// hide `flags` in the contexts section since we display this
// info in the feature flag section below
const {feedback, response, runtime, os, flags: _, ...otherContexts} = contexts ?? {};
const {
feedback,
response,
browser,
runtime,
os,
flags: _,
...otherContexts
} = contexts ?? {};
const orderedContext: Array<[ContextItem['alias'], ContextValue]> = [
['response', response],
['feedback', feedback],
['user', {...userContext, ...(customUserData as any)}],
['browser', browser],
['runtime', runtime],
['os', os],
...Object.entries(otherContexts),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import ScreenshotModal, {
modalCss,
} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/modal';
import {SCREENSHOT_NAMES} from 'sentry/components/events/eventTagsAndScreenshot/screenshot/utils';
import {getRuntimeLabelAndTooltip} from 'sentry/components/events/highlights/util';
import {Text} from 'sentry/components/replays/virtualizedGrid/bodyCell';
import {ScrollCarousel} from 'sentry/components/scrollCarousel';
import {Tooltip} from 'sentry/components/tooltip';
import Version from 'sentry/components/version';
Expand Down Expand Up @@ -106,10 +108,23 @@ export function HighlightsIconSummary({event, group}: HighlightsIconSummaryProps
const releaseTag = event.tags?.find(tag => tag.key === 'release');
const environmentTag = event.tags?.find(tag => tag.key === 'environment');

const runtimeInfo = getRuntimeLabelAndTooltip(event);

return items.length || screenshot ? (
<Fragment>
<IconBar>
<ScrollCarousel gap={2} aria-label={t('Icon highlights')}>
{runtimeInfo && (
<Fragment>
<Tooltip title={runtimeInfo.tooltip} isHoverable>
<StyledRuntimeText>{runtimeInfo.label}</StyledRuntimeText>
</Tooltip>
<DividerWrapper>
<Divider />
</DividerWrapper>
</Fragment>
)}

{screenshot && group && (
<Fragment>
<ScreenshotButton
Expand Down Expand Up @@ -262,3 +277,13 @@ const StyledVersion = styled(Version)`
const ScreenshotButton = styled(Button)`
font-weight: normal;
`;

const DividerWrapper = styled('div')`
display: flex;
align-items: center;
font-size: 1.25rem;
`;

const StyledRuntimeText = styled(Text)`
padding: ${space(0.5)} 0;
`;
65 changes: 65 additions & 0 deletions static/app/components/events/highlights/util.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
EMPTY_HIGHLIGHT_DEFAULT,
getHighlightContextData,
getHighlightTagData,
getRuntimeLabelAndTooltip,
} from 'sentry/components/events/highlights/util';

import {TEST_EVENT_CONTEXTS, TEST_EVENT_TAGS} from './testUtils';
Expand Down Expand Up @@ -84,3 +85,67 @@ describe('getHighlightTagData', function () {
expect(missingTagHighlightFromEvent?.value).toBe(EMPTY_HIGHLIGHT_DEFAULT);
});
});

describe('getRuntimeLabel', function () {
it('returns null for non-JavaScript SDK events', function () {
const event = EventFixture({
sdk: {name: 'python'},
});

expect(getRuntimeLabelAndTooltip(event)).toBeNull();
});

it('returns null for javascript issues without context information', function () {
const event = EventFixture({
sdk: {name: 'javascript'},
});

expect(getRuntimeLabelAndTooltip(event)).toBeNull();
});

it('returns inferred runtime from browser context', function () {
const frontendEvent = EventFixture({
sdk: {name: 'javascript'},
contexts: {
browser: {name: 'Chrome'},
},
});

expect(getRuntimeLabelAndTooltip(frontendEvent)?.label).toBe('Frontend');
expect(getRuntimeLabelAndTooltip(frontendEvent)?.tooltip).toBe(
'Error from Chrome browser'
);
});

it.each([
['node', 'Error from Node.js Server Runtime'],
['bun', 'Error from Bun Server Runtime'],
['deno', 'Error from Deno Server Runtime'],
['cloudflare', 'Error from Cloudflare Workers'],
['vercel-edge', 'Error from Vercel Edge Runtime'],
])(
'returns correct runtime label and tooltip for %s runtime',
(runtimeName, expectedTooltip) => {
const event = EventFixture({
sdk: {name: 'javascript'},
contexts: {
runtime: {name: runtimeName},
browser: {name: 'Chrome'}, // Backend events might also have 'browser'
},
});

const result = getRuntimeLabelAndTooltip(event);
expect(result?.label).toBe('Backend');
expect(result?.tooltip).toBe(expectedTooltip);
}
);

it('returns null when no runtime can be determined', function () {
const event = EventFixture({
sdk: {name: 'javascript'},
contexts: {}, // No browser or runtime context
});

expect(getRuntimeLabelAndTooltip(event)).toBeNull();
});
});
40 changes: 40 additions & 0 deletions static/app/components/events/highlights/util.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
getFormattedContextData,
} from 'sentry/components/events/contexts/utils';
import type {TagTreeContent} from 'sentry/components/events/eventTags/eventTagsTree';
import {t} from 'sentry/locale';
import type {Event, EventTag} from 'sentry/types/event';
import type {KeyValueListData} from 'sentry/types/group';
import type {Organization} from 'sentry/types/organization';
Expand Down Expand Up @@ -170,3 +171,42 @@ export function getHighlightTagData({
originalTag: tagMap[tagKey]?.tag ?? {key: tagKey, value: EMPTY_HIGHLIGHT_DEFAULT},
}));
}

/**
* Returns a label that can be used in the Event Highlight Summary.
* Currently only used for JavaScript-based events.
*/
export function getRuntimeLabelAndTooltip(
event: Event
): {label: string; tooltip: string} | null {
if (!event.sdk?.name.includes('javascript')) {
return null;
}

// eslint-disable-next-line default-case
switch (event.contexts.runtime?.name || '') {
case 'node':
return {label: t('Backend'), tooltip: t('Error from Node.js Server Runtime')};
case 'bun':
return {label: t('Backend'), tooltip: t('Error from Bun Server Runtime')};
case 'deno':
return {label: t('Backend'), tooltip: t('Error from Deno Server Runtime')};
case 'cloudflare':
return {label: t('Backend'), tooltip: t('Error from Cloudflare Workers')};
case 'vercel-edge':
return {label: t('Backend'), tooltip: t('Error from Vercel Edge Runtime')};
}

if (event.contexts.runtime?.name) {
// Browser events don't have a runtime context
return {label: t('Backend'), tooltip: t('Error from Server, Edge or Worker Runtime')};
Copy link
Member

Choose a reason for hiding this comment

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

actually, can we be more specific here - based on the runtime name? :D So if it is node / bun / deno, "Error from Server", if it is cloudflare "Error from Worker Runtime" if it is vercel-edge "Error from Edge", or something like this?

Thinking about this, maybe we can/should just remove the knownRuntimeType escape hatch and use this always, this would be kind of neat I guess... But no strong feelings!

}

if (event.contexts.browser) {
// Backend events might also have a browser context
const browserName = event.contexts.browser.name || 'browser';
return {label: t('Frontend'), tooltip: t('Error from %s browser', browserName)};
}

return null;
}
Loading