diff --git a/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx b/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx index 7efb9a5fd69723..3db0ea462a26be 100644 --- a/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx +++ b/static/app/views/explore/metrics/hooks/useTraceTelemetry.tsx @@ -2,8 +2,11 @@ import {useMemo} from 'react'; import {MutableSearch} from 'sentry/components/searchSyntax/mutableSearch'; import type {NewQuery} from 'sentry/types/organization'; +import {useDiscoverQuery, type TableDataRow} from 'sentry/utils/discover/discoverQuery'; import EventView from 'sentry/utils/discover/eventView'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; +import {useLocation} from 'sentry/utils/useLocation'; +import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import {useSpansQuery} from 'sentry/views/insights/common/queries/useSpansQuery'; @@ -13,6 +16,7 @@ interface UseTraceTelemetryOptions { } interface TraceTelemetryData { + errorsCount: number; logsCount: number; spansCount: number; trace: string; @@ -27,7 +31,35 @@ export function useTraceTelemetry({ enabled, traceIds, }: UseTraceTelemetryOptions): TraceTelemetryResult { + const organization = useOrganization(); const {selection} = usePageFilters(); + const location = useLocation(); + + // Query for error count + const errorsEventView = useMemo(() => { + const traceFilter = new MutableSearch('').addFilterValueList('trace', traceIds); + const discoverQuery: NewQuery = { + id: undefined, + name: 'Error Count', + fields: ['trace', 'count()'], + orderby: '-count', + query: traceFilter.formatString(), + version: 2, + dataset: DiscoverDatasets.ERRORS, + }; + return EventView.fromNewQueryWithPageFilters(discoverQuery, selection); + }, [traceIds, selection]); + + const errorsResult = useDiscoverQuery({ + eventView: errorsEventView, + limit: traceIds.length, + referrer: 'api.explore.trace-errors-count', + orgSlug: organization.slug, + location, + options: { + enabled: enabled && errorsEventView !== null, + }, + }); // Query for spans count const spansEventView = useMemo(() => { @@ -90,6 +122,7 @@ export function useTraceTelemetry({ trace: traceId, spansCount: 0, logsCount: 0, + errorsCount: 0, }); }); @@ -115,11 +148,22 @@ export function useTraceTelemetry({ }); } + // Populate errors count + if (errorsResult.data) { + errorsResult.data.data.forEach((row: TableDataRow) => { + const traceId = row.trace as string; + const count = row['count()'] as number; + if (dataMap.has(traceId)) { + dataMap.get(traceId)!.errorsCount = count; + } + }); + } + return dataMap; - }, [traceIds, spansResult.data, logsResult.data]); + }, [traceIds, spansResult.data, logsResult.data, errorsResult.data]); return { data: telemetryData, - isLoading: spansResult.isPending || logsResult.isPending, + isLoading: spansResult.isPending || logsResult.isPending || errorsResult.isPending, }; } diff --git a/static/app/views/explore/metrics/metricInfoTabs/samplesTab.tsx b/static/app/views/explore/metrics/metricInfoTabs/samplesTab.tsx index e377963bc18fe9..ac6598a48cf323 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/samplesTab.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/samplesTab.tsx @@ -1,4 +1,5 @@ import {useMemo, useRef} from 'react'; +import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Flex} from '@sentry/scraps/layout'; @@ -10,6 +11,7 @@ import LoadingIndicator from 'sentry/components/loadingIndicator'; import LoadingMask from 'sentry/components/loadingMask'; import type {Alignments} from 'sentry/components/tables/gridEditable/sortLink'; import {GridBodyCell, GridHeadCell} from 'sentry/components/tables/gridEditable/styles'; +import {IconFire, IconSpan, IconTerminal} from 'sentry/icons'; import {IconArrow} from 'sentry/icons/iconArrow'; import {IconWarning} from 'sentry/icons/iconWarning'; import {t} from 'sentry/locale'; @@ -26,6 +28,10 @@ import { TableStatus, useTableStyles, } from 'sentry/views/explore/components/table'; +import {LogAttributesRendererMap} from 'sentry/views/explore/logs/fieldRenderers'; +import {getLogColors} from 'sentry/views/explore/logs/styles'; +import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; +import {SeverityLevel} from 'sentry/views/explore/logs/utils'; import {useMetricSamplesTable} from 'sentry/views/explore/metrics/hooks/useMetricSamplesTable'; import {useTraceTelemetry} from 'sentry/views/explore/metrics/hooks/useTraceTelemetry'; import {Table} from 'sentry/views/explore/multiQueryMode/components/miniTable'; @@ -53,7 +59,7 @@ export function SamplesTab({metricName}: SamplesTabProps) { enabled: Boolean(metricName), limit: RESULT_LIMIT, metricName, - fields: ['timestamp', 'trace', 'value'], + fields: ['timestamp', 'value', 'trace'], }); // Extract trace IDs from the result @@ -74,13 +80,8 @@ export function SamplesTab({metricName}: SamplesTabProps) { const sorts = useQueryParamsSortBys(); const setSorts = useSetQueryParamsSortBys(); - // Add telemetry columns to the fields list - const displayFields = useMemo(() => { - return [...fields, 'logs', 'spans']; - }, [fields]); - const tableRef = useRef(null); - const {initialTableStyles} = useTableStyles(displayFields, tableRef, { + const {initialTableStyles} = useTableStyles(fields, tableRef, { minimumColumnWidth: 50, }); @@ -90,8 +91,103 @@ export function SamplesTab({metricName}: SamplesTabProps) { trace: t('Trace'), value: t('Value'), timestamp: t('Timestamp'), - logs: t('Logs'), - spans: t('Spans'), + }; + + const theme = useTheme(); + + const renderTraceCell = (row: any, traceId: string, telemetry: any) => { + const timestamp = row.timestamp as number; + const target = getTraceDetailsUrl({ + organization, + traceSlug: traceId, + dateSelection: { + start: selection.datetime.start, + end: selection.datetime.end, + statsPeriod: selection.datetime.period, + }, + timestamp: timestamp / 1000, + location, + source: TraceViewSources.TRACES, + }); + + return ( + + + {getShortEventId(traceId)} + + + + {telemetry?.errorsCount ?? 0} + + + + {telemetry?.logsCount ?? 0} + + + + {telemetry?.spansCount ?? 0} + + + ); + }; + + const renderTimestampCell = (field: string, row: any, originalFieldIndex: number) => { + const customRenderer = LogAttributesRendererMap[OurLogKnownFieldKey.TIMESTAMP]; + + if (!customRenderer) { + return ( + + ); + } + + return customRenderer({ + item: { + fieldKey: field, + value: row[field], + }, + extra: { + attributes: row, + attributeTypes: meta.fields ?? {}, + highlightTerms: [], + logColors: getLogColors(SeverityLevel.INFO, theme), + location, + organization, + theme, + }, + }); + }; + + const renderDefaultCell = (field: string, row: any, originalFieldIndex: number) => { + return ( + + ); + }; + + const renderFieldCell = (field: string, row: any, traceId: string, telemetry: any) => { + const originalFieldIndex = fields.indexOf(field); + + if (originalFieldIndex === -1) { + return null; + } + + switch (field) { + case 'trace': + return renderTraceCell(row, traceId, telemetry); + case 'timestamp': + return renderTimestampCell(field, row, originalFieldIndex); + default: + return renderDefaultCell(field, row, originalFieldIndex); + } }; return ( @@ -100,33 +196,21 @@ export function SamplesTab({metricName}: SamplesTabProps) { - {displayFields.map((field, i) => { + {fields.map((field, i) => { const label = fieldLabels[field] ?? field; const fieldType = meta.fields?.[field]; const align = fieldAlignment(field, fieldType); - // Don't allow sorting on telemetry fields - const isTelemetryField = field === 'logs' || field === 'spans'; - const direction = isTelemetryField - ? undefined - : sorts.find(s => s.field === field)?.kind; + const direction = sorts.find(s => s.field === field)?.kind; function updateSort() { - if (isTelemetryField) { - return; - } const kind = direction === 'desc' ? 'asc' : 'desc'; setSorts([{field, kind}]); } return ( - + {label} @@ -160,64 +244,11 @@ export function SamplesTab({metricName}: SamplesTabProps) { return ( - {displayFields.map((field, j) => { - if (field === 'trace') { - const timestamp = row.timestamp as number; - const target = getTraceDetailsUrl({ - organization, - traceSlug: traceId, - dateSelection: { - start: selection.datetime.start, - end: selection.datetime.end, - statsPeriod: selection.datetime.period, - }, - timestamp: timestamp / 1000, - location, - source: TraceViewSources.TRACES, - }); - - return ( - - - {getShortEventId(traceId)} - - - ); - } - - if (field === 'logs') { - return ( - - {telemetry?.logsCount ?? 0} - - ); - } - - if (field === 'spans') { - return ( - - {telemetry?.spansCount ?? 0} - - ); - } - - // Find the index in original fields array - const originalFieldIndex = fields.indexOf(field); - if (originalFieldIndex === -1) { - return null; - } - - return ( - - - - ); - })} + {fields.map((field, j) => ( + + {renderFieldCell(field, row, traceId, telemetry)} + + ))} ); })