Skip to content
77 changes: 77 additions & 0 deletions src/sentry/seer/agent/embed_widgets.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -713,6 +713,83 @@
}
]
},
{
"name": "event",
"description": "The ONLY way to reference a single error event inside a Sentry issue. `id` is the 32-character event ID and `issueId` is the numeric group ID the event belongs to, both exactly as the events API returns them. Include the issue short ID as `shortId` when available. When referencing the issue as a whole rather than one of its events, use the `issue` embed instead. Inline: renders a compact link to the event. Block: renders the event with its title, message, culprit, and context — do NOT duplicate any of that as text. Set `view` to \"tags\" to also render the full tag list for the event, or to \"tag\" together with `tagKeys` to render how those tags are distributed across the issue -- pass every key the user asked about in one embed rather than repeating the embed per key, and keep it to a handful. Leave `view` as \"summary\" unless the user asked about tags. Never use a markdown link for event references.",
"level": ["inline", "block"],
"body": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"issueId": {
"type": "string",
"minLength": 1
},
"shortId": {
"type": "string",
"minLength": 1
},
"view": {
"default": "summary",
"type": "string",
"enum": ["summary", "tags", "tag"]
},
"tagKeys": {
"description": "Required when view is \"tag\". The tag keys to break down, e.g. [\"browser\", \"os\"].",
"type": "array",
"items": {
"type": "string",
"minLength": 1
}
}
},
"required": ["id", "issueId", "view"],
"additionalProperties": false
},
"examples": [
{
"label": "Event",
"data": {
"id": "8f2c1a9d7e6b4f30a1b2c3d4e5f60718",
"issueId": "5551212",
"shortId": "JAVASCRIPT-22SP"
}
},
{
"label": "All tags",
"data": {
"id": "8f2c1a9d7e6b4f30a1b2c3d4e5f60718",
"issueId": "5551212",
"shortId": "JAVASCRIPT-22SP",
"view": "tags"
}
},
{
"label": "Single tag breakdown",
"data": {
"id": "8f2c1a9d7e6b4f30a1b2c3d4e5f60718",
"issueId": "5551212",
"shortId": "JAVASCRIPT-22SP",
"view": "tag",
"tagKeys": ["browser"]
}
},
{
"label": "Several tag breakdowns",
"data": {
"id": "8f2c1a9d7e6b4f30a1b2c3d4e5f60718",
"issueId": "5551212",
"shortId": "JAVASCRIPT-22SP",
"view": "tag",
"tagKeys": ["browser", "os", "release"]
}
}
]
},
{
"name": "issuesQuery",
"description": "Link to the issue stream filtered by a search query. Use this when pointing the user at a SET of issues defined by a search rather than specific known issues — if you already have the short IDs, use the `issue` or `issues` embed instead. `query` uses issue search syntax, e.g. \"is:unresolved level:error\".",
Expand Down
11 changes: 9 additions & 2 deletions static/app/components/events/eventTags/eventTagsTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import styled from '@emotion/styled';

import {ErrorBoundary} from 'sentry/components/errorBoundary';
import {
type EventTagTreeRowConfig,
EventTagsTreeRow,
type EventTagsTreeRowProps,
} from 'sentry/components/events/eventTags/eventTagsTreeRow';
Expand Down Expand Up @@ -38,6 +39,8 @@ interface EventTagsTreeProps {
event: Event;
projectSlug: Project['slug'];
tags: EventTagWithMeta[];
/** Applied to every row; e.g. `disableActions` for read-only surfaces. */
config?: EventTagTreeRowConfig;
}

function addToTagTree({
Expand Down Expand Up @@ -106,13 +109,15 @@ function getTagTreeRows({
event,
project,
isLast,
config,
}: EventTagsTreeRowProps & {uniqueKey: string}): React.ReactNode[] {
const subtreeEntries = Array.from(content.subtree.entries());
const subtreeRows = subtreeEntries.reduce<React.ReactNode[]>(
(rows, [tag, tagContent], i) => {
const branchRows = getTagTreeRows({
event,
project,
config,
tagKey: tag,
content: tagContent,
spacerCount: spacerCount + 1,
Expand All @@ -134,6 +139,7 @@ function getTagTreeRows({
event={event}
project={project}
isLast={isLast}
config={config}
/>,
...subtreeRows,
];
Expand All @@ -148,6 +154,7 @@ function TagTreeColumns({
columnCount,
projectSlug,
event,
config,
}: EventTagsTreeProps & {columnCount: number}) {
const organization = useOrganization();
const {data: project, isPending} = useDetailedProject({
Expand All @@ -171,7 +178,7 @@ function TagTreeColumns({
// root parent so that we do not split up roots/branches when forming columns
const tagTreeRowGroups: React.ReactNode[][] = Array.from(tagTree.entries()).map(
([tagKey, content], i) =>
getTagTreeRows({tagKey, content, uniqueKey: `${i}`, project, event})
getTagTreeRows({tagKey, content, uniqueKey: `${i}`, project, event, config})
);
// Get the total number of TagTreeRow components to be rendered, and a goal size for each column
const tagTreeRowTotal = tagTreeRowGroups.reduce(
Expand Down Expand Up @@ -208,7 +215,7 @@ function TagTreeColumns({
{startIndex: 0, runningTotal: 0, columns: []}
);
return data.columns;
}, [columnCount, isPending, project, event, tags]);
}, [columnCount, isPending, project, event, tags, config]);

return <Fragment>{assembledColumns}</Fragment>;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils';
import {getSizeBuildPath} from 'sentry/views/preprod/utils/buildLinkUtils';

interface EventTagTreeRowConfig {
export interface EventTagTreeRowConfig {
// Omits the dropdown of actions applicable to this tag
disableActions?: boolean;
// Omit error styling from being displayed, even if context is invalid
Expand Down
11 changes: 10 additions & 1 deletion static/app/components/events/eventTags/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react';

import {EventTagCustomBanner} from 'sentry/components/events/eventTags/eventTagCustomBanner';
import {EventTagsTree} from 'sentry/components/events/eventTags/eventTagsTree';
import type {EventTagTreeRowConfig} from 'sentry/components/events/eventTags/eventTagsTreeRow';
import {associateTagsWithMeta, TagFilter} from 'sentry/components/events/eventTags/util';
import {AnnotatedText} from 'sentry/components/events/meta/annotatedText';
import type {Event, EventTagWithMeta} from 'sentry/types/event';
Expand All @@ -15,6 +16,8 @@ import {useOrganization} from 'sentry/utils/useOrganization';
type Props = {
event: Event;
projectSlug: Project['slug'];
/** Applied to every tag row; e.g. `disableActions` for read-only surfaces. */
config?: EventTagTreeRowConfig;
filteredTags?: EventTagWithMeta[];
tagFilter?: TagFilter;
};
Expand All @@ -25,6 +28,7 @@ export function EventTags({
event,
filteredTags,
projectSlug,
config,
tagFilter = TagFilter.ALL,
}: Props) {
const organization = useOrganization();
Expand Down Expand Up @@ -100,7 +104,12 @@ export function EventTags({

return (
<Fragment>
<EventTagsTree event={event} projectSlug={projectSlug} tags={filtered} />
<EventTagsTree
event={event}
projectSlug={projectSlug}
tags={filtered}
config={config}
/>
{hasCustomTagsBanner && <EventTagCustomBanner />}
</Fragment>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import {EventFixture} from 'sentry-fixture/event';
import {GroupFixture} from 'sentry-fixture/group';

import {render, screen} from 'sentry-test/reactTestingLibrary';

import {EventEmbedStory} from './eventEmbedStory';

jest.mock('sentry/components/seer/markdown', () => ({
SeerMarkdown: ({raw}: {raw: string}) => <div aria-label="Rendered markdown">{raw}</div>,
}));

const EVENT_ID = '8f2c1a9d7e6b4f30a1b2c3d4e5f60718';

describe('EventEmbedStory', () => {
it('resolves the latest event of a recent issue and breaks down a varying tag', async () => {
const issue = GroupFixture({id: '5551212', shortId: 'JAVASCRIPT-22SP'});
const issueRequest = MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/',
body: [issue],
match: [
MockApiClient.matchQuery({
project: '-1',
query: 'is:unresolved issue.category:error',
sort: 'freq',
statsPeriod: '14d',
limit: 1,
}),
],
});
const eventRequest = MockApiClient.addMockResponse({
url: `/organizations/org-slug/issues/${issue.id}/events/latest/`,
body: EventFixture({
id: EVENT_ID,
eventID: EVENT_ID,
groupID: issue.id,
tags: [
{key: 'level', value: 'error'},
{key: 'browser', value: 'Chrome'},
{key: 'os', value: 'macOS'},
],
}),
});

render(<EventEmbedStory />);

const variants = await screen.findAllByLabelText('Rendered markdown');
expect(variants).toHaveLength(4);

for (const variant of variants) {
expect(variant).toHaveTextContent(EVENT_ID);
expect(variant).toHaveTextContent(issue.id);
expect(variant).toHaveTextContent(issue.shortId);
}

expect(variants[1]).toHaveTextContent('"view":"tags"');
// `browser` and `os` are preferred over `level`, which is the same on every
// event and would draw a single full-width bar.
expect(variants[2]).toHaveTextContent('"view":"tag","tagKeys":["browser"]');
expect(variants[3]).toHaveTextContent('"view":"tag","tagKeys":["browser","os"]');

expect(issueRequest).toHaveBeenCalled();
expect(eventRequest).toHaveBeenCalled();
});

it('omits the multi-tag variant when the event carries only one usable tag', async () => {
const issue = GroupFixture({id: '5551212', shortId: 'JAVASCRIPT-22SP'});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/',
body: [issue],
});
MockApiClient.addMockResponse({
url: `/organizations/org-slug/issues/${issue.id}/events/latest/`,
body: EventFixture({
id: EVENT_ID,
eventID: EVENT_ID,
groupID: issue.id,
tags: [{key: 'browser', value: 'Chrome'}],
}),
});

render(<EventEmbedStory />);

const variants = await screen.findAllByLabelText('Rendered markdown');
expect(variants).toHaveLength(3);
expect(variants[2]).toHaveTextContent('"view":"tag","tagKeys":["browser"]');
});

it('falls back to a message when the organization has no error events', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/',
body: [],
});

render(<EventEmbedStory />);

expect(
await screen.findByText('No error event is available for this organization.')
).toBeInTheDocument();
expect(screen.queryByLabelText('Rendered markdown')).not.toBeInTheDocument();
});
});
Loading
Loading