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
87 changes: 87 additions & 0 deletions src/sentry/seer/agent/embed_widgets.generated.json
Original file line number Diff line number Diff line change
Expand Up @@ -790,6 +790,93 @@
}
]
},
{
"name": "log",
"description": "The ONLY way to reference a single log line (Explore > Logs). `id` is the log item ID exactly as the logs API returns it. Provide `traceId`, `projectId`, and `timestamp` whenever the API gave them to you — without them the embed has to scan a wider window to find the row. When referencing a SET of logs defined by a search, use the `logsQuery` embed instead. Inline: renders a compact link that opens the log row in Explore. Block: renders the log row with its severity, message, and timestamp — do NOT duplicate any of that as text. Set `view` to \"attributes\" to also render the full attribute list for the log, or to \"attribute\" together with `attribute` to break that one attribute down across matching logs. Leave `view` as \"summary\" unless the user asked about attributes. Never use a markdown link for log references.",
"level": ["inline", "block"],
"body": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"id": {
"type": "string",
"minLength": 1
},
"traceId": {
"type": "string",
"minLength": 1
},
"projectId": {
"anyOf": [
{
"type": "string"
},
{
"type": "number"
}
]
},
"timestamp": {
"type": "string",
"format": "date-time",
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
},
"view": {
"default": "summary",
"type": "string",
"enum": ["summary", "attributes", "attribute"]
},
"attribute": {
"description": "Required when view is \"attribute\". The attribute key to break down, e.g. \"severity\".",
"type": "string",
"minLength": 1
}
},
"required": ["id", "view"],
"additionalProperties": false
},
"examples": [
{
"label": "Inline",
"data": {
"id": "019bfe1c-4c1f-7e3d-9a2f-3e6b1a2c3d4e",
"traceId": "a1b2c3d4e5f678901234567890abcdef",
"projectId": "1",
"timestamp": "2026-08-25T16:37:12Z"
}
},
{
"label": "Block",
"data": {
"id": "019bfe1c-4c1f-7e3d-9a2f-3e6b1a2c3d4e",
"traceId": "a1b2c3d4e5f678901234567890abcdef",
"projectId": "1",
"timestamp": "2026-08-25T16:37:12Z"
}
},
{
"label": "All attributes",
"data": {
"id": "019bfe1c-4c1f-7e3d-9a2f-3e6b1a2c3d4e",
"traceId": "a1b2c3d4e5f678901234567890abcdef",
"projectId": "1",
"timestamp": "2026-08-25T16:37:12Z",
"view": "attributes"
}
},
{
"label": "Single attribute breakdown",
"data": {
"id": "019bfe1c-4c1f-7e3d-9a2f-3e6b1a2c3d4e",
"traceId": "a1b2c3d4e5f678901234567890abcdef",
"projectId": "1",
"timestamp": "2026-08-25T16:37:12Z",
"view": "attribute",
"attribute": "severity"
}
}
]
},
{
"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
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {LogFixture} from 'sentry-fixture/log';

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

import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';

import {LogEmbedStory} from './logEmbedStory';

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

const TIMESTAMP_MS = Date.UTC(2026, 7, 28, 16, 37, 12);

function createLog(id: string, overrides: Record<string, string> = {}) {
return LogFixture({
[OurLogKnownFieldKey.ID]: id,
[OurLogKnownFieldKey.PROJECT_ID]: '2',
[OurLogKnownFieldKey.ORGANIZATION_ID]: 3,
[OurLogKnownFieldKey.TRACE_ID]: 'a1b2c3d4e5f678901234567890abcdef',
[OurLogKnownFieldKey.TIMESTAMP_PRECISE]: String(
BigInt(TIMESTAMP_MS) * 1_000_000n
) as unknown as string,
...overrides,
});
}

describe('LogEmbedStory', () => {
it('renders every view of a recent log that carries a trace and project', async () => {
const untraced = createLog('019bfe1c-0000-7e3d-9a2f-000000000000', {
[OurLogKnownFieldKey.TRACE_ID]: '',
});
const log = createLog('019bfe1c-4c1f-7e3d-9a2f-3e6b1a2c3d4e');
const eventsRequest = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {data: [untraced, log], meta: {}},
match: [
MockApiClient.matchQuery({
dataset: 'ourlogs',
sort: '-timestamp',
statsPeriod: '7d',
per_page: 25,
}),
],
});

render(<LogEmbedStory />);

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

const [summary, attributes, attribute, idOnly] = variants as [
HTMLElement,
HTMLElement,
HTMLElement,
HTMLElement,
];

// The traceless row is skipped in favour of the one the details lookup can use.
for (const variant of variants) {
expect(variant).toHaveTextContent(log[OurLogKnownFieldKey.ID]);
expect(variant).not.toHaveTextContent(untraced[OurLogKnownFieldKey.ID]);
}

expect(summary).toHaveTextContent(log[OurLogKnownFieldKey.TRACE_ID]);
expect(summary).toHaveTextContent(new Date(TIMESTAMP_MS).toISOString());
expect(summary).not.toHaveTextContent('"view"');

expect(attributes).toHaveTextContent('"view":"attributes"');
expect(attribute).toHaveTextContent('"view":"attribute","attribute":"severity"');

// The id-only variant exercises the embed resolving trace and project itself.
expect(idOnly).not.toHaveTextContent(log[OurLogKnownFieldKey.TRACE_ID]);
});

it('explains itself when the organization has no logs', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {data: [], meta: {}},
});

render(<LogEmbedStory />);

expect(
await screen.findByText('No log is available for this organization.')
).toBeInTheDocument();
});
});
98 changes: 98 additions & 0 deletions static/app/components/seer/markdown/__stories__/logEmbedStory.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import {Fragment} from 'react';
import {useQuery} from '@tanstack/react-query';

import {Text} from '@sentry/scraps/text';

import {LoadingIndicator} from 'sentry/components/loadingIndicator';
import {ALL_ACCESS_PROJECTS} from 'sentry/components/pageFilters/constants';
import {apiOptions} from 'sentry/utils/api/apiOptions';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {useOrganization} from 'sentry/utils/useOrganization';
import {AlwaysPresentLogFields} from 'sentry/views/explore/logs/constants';
import {
OurLogKnownFieldKey,
type EventsLogsResult,
type OurLogsResponseItem,
} from 'sentry/views/explore/logs/types';
import {getLogRowTimestampMillis} from 'sentry/views/explore/logs/utils';

import {EmbedStory, EmbedVariant} from './embedStory';

const STORY_REFERRER = 'seer-log-embed-story';

/**
* Every log carries a severity, so the breakdown variant always has something
* to group by whichever row this organization happens to supply.
*/
const BREAKDOWN_ATTRIBUTE = OurLogKnownFieldKey.SEVERITY;

function toIdentity(log: OurLogsResponseItem) {
const timestampMs = getLogRowTimestampMillis(log);

return {
id: String(log[OurLogKnownFieldKey.ID]),
traceId: String(log[OurLogKnownFieldKey.TRACE_ID]),
projectId: String(log[OurLogKnownFieldKey.PROJECT_ID]),
timestamp: Number.isFinite(timestampMs)
? new Date(timestampMs).toISOString()
: String(log[OurLogKnownFieldKey.TIMESTAMP]),
};
}

export function LogEmbedStory() {
const organization = useOrganization();
const {data, isError, isPending} = useQuery(
apiOptions.as<EventsLogsResult>()('/organizations/$organizationIdOrSlug/events/', {
path: {organizationIdOrSlug: organization.slug},
query: {
dataset: DiscoverDatasets.OURLOGS,
field: AlwaysPresentLogFields,
project: [ALL_ACCESS_PROJECTS],
sort: `-${OurLogKnownFieldKey.TIMESTAMP}`,
statsPeriod: '7d',
per_page: 25,
referrer: STORY_REFERRER,
},
// Keep every variant on the page pointed at the same row while it is read.
staleTime: Infinity,
})
);

// The details lookup behind the block is addressed by trace and project, so
// prefer a row that already carries both over one the block has to resolve.
const log = data?.data.find(
row => row[OurLogKnownFieldKey.TRACE_ID] && row[OurLogKnownFieldKey.PROJECT_ID]
);
const identity = log ? toIdentity(log) : null;

return (
<EmbedStory name="log">
{isPending ? (
<LoadingIndicator />
) : isError ? (
<Text variant="muted">Unable to load a log example.</Text>
) : identity ? (
<Fragment>
<EmbedVariant name="log" label="Log" data={identity} />
<EmbedVariant
name="log"
label="All attributes"
data={{...identity, view: 'attributes'}}
/>
<EmbedVariant
name="log"
label="Single attribute breakdown"
data={{...identity, view: 'attribute', attribute: BREAKDOWN_ATTRIBUTE}}
/>
<EmbedVariant
name="log"
label="Id only (the embed resolves trace and project)"
data={{id: identity.id}}
/>
</Fragment>
) : (
<Text variant="muted">No log is available for this organization.</Text>
)}
</EmbedStory>
);
}
Loading
Loading