From c8b9fcf28975ede78755c7fbbd285d514e17a104 Mon Sep 17 00:00:00 2001 From: Kev Date: Tue, 18 Nov 2025 13:50:34 -0500 Subject: [PATCH 1/2] fix(ourlogs): Add error event to log list On issues details, this will add the error event into the logs list to give context as to where the error happened in time. --- .../events/ourlogs/ourlogsDrawer.tsx | 63 ++++++++- .../events/ourlogs/ourlogsSection.spec.tsx | 51 ++++++++ static/app/views/explore/logs/styles.tsx | 43 ++++++- .../explore/logs/tables/logsInfiniteTable.tsx | 70 +++++++++- .../explore/logs/tables/logsTableRow.tsx | 120 +++++++++++------- static/app/views/explore/logs/utils.tsx | 53 ++++++++ .../metricInfoTabs/metricsSamplesTable.tsx | 6 +- 7 files changed, 351 insertions(+), 55 deletions(-) diff --git a/static/app/components/events/ourlogs/ourlogsDrawer.tsx b/static/app/components/events/ourlogs/ourlogsDrawer.tsx index bf3c91525c14ef..d4ebef5efc6be5 100644 --- a/static/app/components/events/ourlogs/ourlogsDrawer.tsx +++ b/static/app/components/events/ourlogs/ourlogsDrawer.tsx @@ -1,7 +1,11 @@ -import {useRef} from 'react'; +import {useMemo, useRef} from 'react'; import styled from '@emotion/styled'; +import moment from 'moment-timezone'; + +import {Flex} from '@sentry/scraps/layout'; import {ProjectAvatar} from 'sentry/components/core/avatar/projectAvatar'; +import {LinkButton} from 'sentry/components/core/button/linkButton'; import { CrumbContainer, EventDrawerBody, @@ -17,18 +21,23 @@ import {space} from 'sentry/styles/space'; import type {Event} from 'sentry/types/event'; import type {Group} from 'sentry/types/group'; import type {Project} from 'sentry/types/project'; +import {getUtcDateString} from 'sentry/utils/dates'; import {getShortEventId} from 'sentry/utils/events'; +import useOrganization from 'sentry/utils/useOrganization'; import { TraceItemSearchQueryBuilder, useSearchQueryBuilderProps, } from 'sentry/views/explore/components/traceItemSearchQueryBuilder'; import {useTraceItemAttributes} from 'sentry/views/explore/contexts/traceItemAttributeContext'; import {LogsInfiniteTable} from 'sentry/views/explore/logs/tables/logsInfiniteTable'; +import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types'; +import {getLogsUrl} from 'sentry/views/explore/logs/utils'; import { useQueryParamsSearch, useSetQueryParamsQuery, } from 'sentry/views/explore/queryParams/context'; import {TraceItemDataset} from 'sentry/views/explore/types'; +import {getEventEnvironment} from 'sentry/views/issueDetails/utils'; interface LogIssueDrawerProps { event: Event; @@ -45,6 +54,7 @@ export function OurlogsDrawer({ group, embeddedOptions, }: LogIssueDrawerProps) { + const organization = useOrganization(); const setLogsQuery = useSetQueryParamsQuery(); const logsSearch = useQueryParamsSearch(); @@ -68,6 +78,45 @@ export function OurlogsDrawer({ ); const containerRef = useRef(null); + const additionalData = useMemo( + () => ({ + event, + }), + [event] + ); + + const exploreUrl = useMemo(() => { + const traceId = event.contexts.trace?.trace_id; + if (!traceId) { + return null; + } + + const eventTimestamp = event.dateCreated || event.dateReceived; + if (!eventTimestamp) { + return null; + } + + const eventMoment = moment(eventTimestamp); + const start = getUtcDateString(eventMoment.clone().subtract(1, 'day')); + const end = getUtcDateString(eventMoment.clone().add(1, 'day')); + const environment = getEventEnvironment(event); + + return getLogsUrl({ + organization, + selection: { + projects: [parseInt(project.id, 10)], + environments: environment ? [environment] : [], + datetime: { + start, + end, + period: null, + utc: null, + }, + }, + query: `${OurLogKnownFieldKey.TRACE_ID}:${traceId}`, + }); + }, [event, organization, project.id]); + return ( @@ -88,7 +137,16 @@ export function OurlogsDrawer({ /> - + + + + + {exploreUrl && ( + + {t('Open in explore')} + + )} + @@ -96,6 +154,7 @@ export function OurlogsDrawer({ embedded scrollContainer={containerRef} embeddedOptions={embeddedOptions} + additionalData={additionalData} /> diff --git a/static/app/components/events/ourlogs/ourlogsSection.spec.tsx b/static/app/components/events/ourlogs/ourlogsSection.spec.tsx index 65d16812ffea58..65e4adc9bc52af 100644 --- a/static/app/components/events/ourlogs/ourlogsSection.spec.tsx +++ b/static/app/components/events/ourlogs/ourlogsSection.spec.tsx @@ -28,6 +28,7 @@ jest.mock('@tanstack/react-virtual', () => { {key: '3', index: 2, start: 100, end: 150, lane: 0}, ]), getTotalSize: jest.fn().mockReturnValue(150), + scrollToIndex: jest.fn(), options: { scrollMargin: 0, }, @@ -42,6 +43,7 @@ jest.mock('@tanstack/react-virtual', () => { {key: '3', index: 2, start: 100, end: 150, lane: 0}, ]), getTotalSize: jest.fn().mockReturnValue(150), + scrollToIndex: jest.fn(), options: { scrollMargin: 0, }, @@ -155,6 +157,19 @@ describe('OurlogsSection', () => { }, }); + MockApiClient.addMockResponse({ + url: `/projects/${organization.slug}/${project.slug}/trace-items/${logId}/`, + method: 'GET', + body: { + itemId: logId, + timestamp: '2025-04-03T15:50:10+00:00', + attributes: [ + {name: 'severity', type: 'str', value: 'error'}, + {name: 'special_field', type: 'str', value: 'special value'}, + ], + }, + }); + MockApiClient.addMockResponse({ url: `/organizations/${organization.slug}/trace-items/attributes/`, method: 'GET', @@ -233,4 +248,40 @@ describe('OurlogsSection', () => { expect(within(aside).getByTestId('tree-key-severity')).toBeInTheDocument(); expect(within(aside).getByTestId('tree-key-severity')).toHaveTextContent('severity'); }); + + it('renders Open in explore button with correct URL when trace_id exists', async () => { + render(, { + organization: OrganizationFixture({ + features: ['ourlogs-enabled', 'visibility-explore-view'], + }), + initialRouterConfig: { + location: { + pathname: `/organizations/${organization.slug}/issues/${group.id}/`, + query: { + project: project.id, + }, + }, + }, + }); + + await waitFor(() => { + expect(screen.getByText(/i am a log/)).toBeInTheDocument(); + }); + + await userEvent.click(screen.getByText(/i am a log/)); + + const aside = screen.getByRole('complementary', {name: 'logs drawer'}); + expect(aside).toBeInTheDocument(); + + const openInExploreButton = within(aside).getByRole('button', { + name: 'Open in explore', + }); + expect(openInExploreButton).toBeInTheDocument(); + expect(openInExploreButton).toHaveAttribute('target', '_blank'); + + const href = openInExploreButton.getAttribute('href'); + expect(href).toBe( + '/organizations/org-slug/explore/logs/?end=2019-03-21T00%3A00%3A00&environment=dev&logsQuery=trace%3A00000000000000000000000000000000&project=2&start=2019-03-19T00%3A00%3A00' + ); + }); }); diff --git a/static/app/views/explore/logs/styles.tsx b/static/app/views/explore/logs/styles.tsx index 27d7aaad59d0ab..f85530c2440839 100644 --- a/static/app/views/explore/logs/styles.tsx +++ b/static/app/views/explore/logs/styles.tsx @@ -2,6 +2,8 @@ import type {Theme} from '@emotion/react'; import {css} from '@emotion/react'; import styled from '@emotion/styled'; +import {Flex} from '@sentry/scraps/layout'; + import {Button} from 'sentry/components/core/button'; import {HighlightComponent} from 'sentry/components/highlight'; import PageFilterBar from 'sentry/components/organizations/pageFilterBar'; @@ -43,6 +45,20 @@ export const LogTableRow = styled(TableRow)` } } + .log-table-row-pseudo-row-chevron-replacement { + width: 23px; + height: 24px; + } + + &[data-row-highlighted='true']:not(thead > &) { + background-color: ${p => p.theme.yellow100}; + color: ${p => p.theme.red300}; + + &:hover { + background-color: ${p => p.theme.yellow200}; + } + } + &.beforeHoverTime + &.afterHoverTime:before { border-top: 1px solid ${p => p.theme.purple200}; content: ''; @@ -156,7 +172,7 @@ export const DetailsWrapper = styled('tr')` display: grid; border-top: 1px solid ${p => p.theme.border}; border-bottom: 1px solid ${p => p.theme.border}; - z-index: ${2 /* place above the grid resizing lines */}; + z-index: ${1 /* place above the grid resizing lines */}; `; export const DetailsContent = styled(StyledPanel)` @@ -480,3 +496,28 @@ export const LogsFilterSection = styled('div')` grid-template-columns: minmax(300px, auto) 1fr min-content; } `; + +export const TraceIconStyleWrapper = styled(Flex)` + width: 18px; + height: 18px; + + .TraceIcon { + background-color: ${p => p.theme.red300}; + position: absolute; + transform: translate(-50%, -50%) scaleX(var(--inverse-span-scale)) translateZ(0); + width: 18px; + height: 18px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + margin-right: -2px; + } + + .TraceIcon svg { + width: 12px; + height: 12px; + fill: #ffffff; + } +`; diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index f00248cd8c62f0..dc7e25d1dbf410 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -17,6 +17,7 @@ import {GridResizer} from 'sentry/components/tables/gridEditable/styles'; import {IconArrow, IconWarning} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import {space} from 'sentry/styles/space'; +import type {Event} from 'sentry/types/event'; import type {TagCollection} from 'sentry/types/group'; import {defined} from 'sentry/utils'; import { @@ -51,12 +52,15 @@ import { type OurLogsResponseItem, } from 'sentry/views/explore/logs/types'; import { + createPseudoLogResponseItem, getDynamicLogsNextFetchThreshold, getLogBodySearchTerms, getLogRowTimestampMillis, getTableHeaderLabel, + isRegularLogResponseItem, logsFieldAlignment, quantizeTimestampToMinutes, + type LogTableRowItem, } from 'sentry/views/explore/logs/utils'; import type {ReplayEmbeddedTableOptions} from 'sentry/views/explore/logs/utils/logsReplayUtils'; import { @@ -68,6 +72,9 @@ import { import {EmptyStateText} from 'sentry/views/explore/tables/tracesTable/styles'; type LogsTableProps = { + additionalData?: { + event?: Event; + }; allowPagination?: boolean; embedded?: boolean; embeddedOptions?: { @@ -100,6 +107,7 @@ export function LogsInfiniteTable({ scrollContainer, embeddedStyling, embeddedOptions, + additionalData, }: LogsTableProps) { const theme = useTheme(); const fields = useQueryParamsFields(); @@ -123,8 +131,50 @@ export function LogsInfiniteTable({ resumeAutoFetch, } = useLogsPageDataQueryResult(); - // Use filtered items if provided, otherwise use original data - const data = localOnlyItemFilters?.filteredItems ?? originalData; + const baseData = localOnlyItemFilters?.filteredItems ?? originalData; + + const pseudoRowIndex = useMemo(() => { + if ( + !additionalData?.event || + !baseData || + baseData.length === 0 || + isPending || + isError + ) { + return -1; + } + const event = additionalData.event; + const eventTimestamp = new Date(event.dateCreated || new Date()).getTime(); + const index = baseData.findIndex( + row => + isRegularLogResponseItem(row) && getLogRowTimestampMillis(row) < eventTimestamp + ); + return index === -1 ? baseData.length : index; // If the event is older than all the data, add it to the end. + }, [additionalData, baseData, isPending, isError]); + + const data: LogTableRowItem[] = useMemo(() => { + if ( + !additionalData?.event || + !baseData || + baseData.length === 0 || + isPending || + isError || + pseudoRowIndex === -1 + ) { + return baseData || []; + } + + const newData: LogTableRowItem[] = [...baseData]; + newData.splice( + pseudoRowIndex, + 0, + createPseudoLogResponseItem( + additionalData.event, + additionalData.event.projectID || '' + ) + ); + return newData; + }, [baseData, additionalData, isPending, isError, pseudoRowIndex]); // Calculate quantized start and end times for replay links const {logStart, logEnd} = useMemo(() => { @@ -132,7 +182,10 @@ export function LogsInfiniteTable({ return {logStart: undefined, logEnd: undefined}; } - const timestamps = data.map(row => getLogRowTimestampMillis(row)).filter(Boolean); + const timestamps = data + .filter((row): row is OurLogsResponseItem => isRegularLogResponseItem(row)) + .map(row => getLogRowTimestampMillis(row)) + .filter(Boolean); if (timestamps.length === 0) { return {logStart: undefined, logEnd: undefined}; } @@ -229,6 +282,17 @@ export function LogsInfiniteTable({ [virtualizer] ); + useEffect(() => { + if (pseudoRowIndex !== -1 && scrollContainer?.current) { + setTimeout(() => { + containerVirtualizer.scrollToIndex(pseudoRowIndex, { + behavior: 'smooth', + align: 'center', + }); + }, 100); + } + }, [pseudoRowIndex, containerVirtualizer, scrollContainer]); + const hasReplay = !!embeddedOptions?.replay; const replayJumpButtons = useJumpButtons({ diff --git a/static/app/views/explore/logs/tables/logsTableRow.tsx b/static/app/views/explore/logs/tables/logsTableRow.tsx index d7f24754d0757c..c7127f462f348a 100644 --- a/static/app/views/explore/logs/tables/logsTableRow.tsx +++ b/static/app/views/explore/logs/tables/logsTableRow.tsx @@ -1,5 +1,5 @@ import type {ComponentProps, SyntheticEvent} from 'react'; -import { +import React, { Fragment, memo, useCallback, @@ -12,6 +12,8 @@ import {useTheme} from '@emotion/react'; import classNames from 'classnames'; import omit from 'lodash/omit'; +import {Flex} from '@sentry/scraps/layout'; + import {Button} from 'sentry/components/core/button'; import {EmptyStreamWrapper} from 'sentry/components/emptyStateWarning'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; @@ -46,7 +48,6 @@ import { import type {TraceItemDetailsResponse} from 'sentry/views/explore/hooks/useTraceItemDetails'; import {useFetchTraceItemDetailsOnHover} from 'sentry/views/explore/hooks/useTraceItemDetails'; import { - AlwaysPresentLogFields, DEFAULT_TRACE_ITEM_HOVER_TIMEOUT, DEFAULT_TRACE_ITEM_HOVER_TIMEOUT_WITH_AUTO_REFRESH, HiddenLogDetailFields, @@ -74,6 +75,7 @@ import { LogTableBodyCell, LogTableRow, StyledChevronButton, + TraceIconStyleWrapper, } from 'sentry/views/explore/logs/styles'; import { OurLogKnownFieldKey, @@ -86,7 +88,10 @@ import { getLogRowItem, getLogRowTimestampMillis, getLogSeverityLevel, + isPseudoLogResponseItem, + isRegularLogResponseItem, ourlogToJson, + type LogTableRowItem, } from 'sentry/views/explore/logs/utils'; import type {ReplayEmbeddedTableOptions} from 'sentry/views/explore/logs/utils/logsReplayUtils'; import { @@ -94,9 +99,10 @@ import { useQueryParamsFields, } from 'sentry/views/explore/queryParams/context'; import {TraceItemDataset} from 'sentry/views/explore/types'; +import {TraceIcons} from 'sentry/views/performance/newTraceDetails/traceIcons'; type LogsRowProps = { - dataRow: OurLogsResponseItem; + dataRow: LogTableRowItem; highlightTerms: string[]; meta: EventsMetaType | undefined; sharedHoverTimeoutRef: React.MutableRefObject; @@ -111,9 +117,6 @@ type LogsRowProps = { logEnd?: string; logStart?: string; onCollapse?: (logItemId: string) => void; - /** - * This should only be used in embedded views since we won't be opening the details. - */ onEmbeddedRowClick?: (logItemId: string, event: React.MouseEvent) => void; onExpand?: (logItemId: string) => void; onExpandHeight?: (logItemId: string, estimatedHeight: number) => void; @@ -203,6 +206,7 @@ export const LogRowContent = memo(function LogRowContent({ const analyticsPageSource = useLogsAnalyticsPageSource(); const [_expanded, setExpanded] = useState(false); const expanded = isExpanded ?? _expanded; + const isPseudoRow = isPseudoLogResponseItem(dataRow); function toggleExpanded() { if (onExpand) { @@ -239,9 +243,8 @@ export const LogRowContent = memo(function LogRowContent({ const severityNumber = dataRow[OurLogKnownFieldKey.SEVERITY_NUMBER]; const severityText = dataRow[OurLogKnownFieldKey.SEVERITY]; - const projectId: (typeof AlwaysPresentLogFields)[1] = - dataRow[OurLogKnownFieldKey.PROJECT_ID]; - const project = projects.projects.find(p => p.id === '' + projectId); + const projectId = dataRow[OurLogKnownFieldKey.PROJECT_ID]; + const project = projects.projects.find(p => p.id === String(projectId)); const projectSlug = project?.slug ?? ''; const level = getLogSeverityLevel( @@ -268,7 +271,7 @@ export const LogRowContent = memo(function LogRowContent({ useFullSeverityText: false, location, organization, - attributes: dataRow, + attributes: dataRow as OurLogsResponseItem, attributeTypes: meta?.fields ?? {}, theme, projectSlug, @@ -281,16 +284,18 @@ export const LogRowContent = memo(function LogRowContent({ logEnd, }; - const rowInteractProps: ComponentProps = blockRowExpanding - ? onEmbeddedRowClick - ? {onClick, isClickable: true} - : {} - : { - ...hoverProps, - onPointerUp, - onTouchEnd: onPointerUp, - isClickable: true, - }; + const rowInteractProps: ComponentProps = isPseudoRow + ? {isClickable: false} + : blockRowExpanding + ? onEmbeddedRowClick + ? {onClick, isClickable: true} + : {} + : { + ...hoverProps, + onPointerUp, + onTouchEnd: onPointerUp, + isClickable: true, + }; const buttonSize = 'xs'; const chevronIcon = ( @@ -300,7 +305,8 @@ export const LogRowContent = memo(function LogRowContent({ let replayTimeClasses = {}; if ( embeddedOptions?.replay?.displayReplayTimeIndicator && - embeddedOptions.replay.timestampRelativeTo + embeddedOptions.replay.timestampRelativeTo && + isRegularLogResponseItem(dataRow) ) { const logTimestamp = getLogRowTimestampMillis(dataRow); const offsetMs = logTimestamp - embeddedOptions.replay.timestampRelativeTo; @@ -323,6 +329,7 @@ export const LogRowContent = memo(function LogRowContent({ { @@ -334,7 +341,9 @@ export const LogRowContent = memo(function LogRowContent({ > - {blockRowExpanding ? null : shouldRenderHoverElements ? ( + {isPseudoRow ? ( + + ) : blockRowExpanding ? null : shouldRenderHoverElements ? ( } aria-label={t('Toggle trace details')} @@ -346,12 +355,26 @@ export const LogRowContent = memo(function LogRowContent({ ) : ( {chevronIcon} )} - - {project ? : null} + {isPseudoRow ? ( + + +
+ +
+
+
+ ) : ( + + + {project ? ( + + ) : null} + + )}
{fields?.map(field => { - const value = dataRow[field]; + const value = (dataRow as OurLogsResponseItem)[field]; if (!defined(value)) { return ; @@ -359,7 +382,7 @@ export const LogRowContent = memo(function LogRowContent({ const renderedField = ( - {LogBodyRenderer({ - item: getLogRowItem(OurLogKnownFieldKey.MESSAGE, dataRow, meta), - extra: { - highlightTerms, - logColors, - wrapBody: true, - location, - organization, - projectSlug, - attributes, - attributeTypes, - meta, - theme, - traceItemMeta: data?.meta, - }, - })} + {isRegularLogResponseItem(dataRow) ? ( + LogBodyRenderer({ + item: getLogRowItem(OurLogKnownFieldKey.MESSAGE, dataRow, meta), + extra: { + highlightTerms, + logColors, + wrapBody: true, + location, + organization, + projectSlug, + attributes, + attributeTypes, + meta, + theme, + traceItemMeta: data?.meta, + }, + }) + ) : ( + {String(dataRow[OurLogKnownFieldKey.MESSAGE] ?? '')} + )} @@ -568,7 +596,7 @@ function LogRowDetails({ ); } -function LogRowDetailsFilterActions({tableDataRow}: {tableDataRow: OurLogsResponseItem}) { +function LogRowDetailsFilterActions({tableDataRow}: {tableDataRow: LogTableRowItem}) { const addSearchFilter = useAddSearchFilter(); return ( @@ -610,7 +638,7 @@ function LogRowDetailsActions({ tableDataRow, }: { fullLogDataResult: UseApiQueryResult; - tableDataRow: OurLogsResponseItem; + tableDataRow: LogTableRowItem; }) { const {data, isPending, isError} = fullLogDataResult; const isFrozen = useLogsFrozenIsFrozen(); diff --git a/static/app/views/explore/logs/utils.tsx b/static/app/views/explore/logs/utils.tsx index 29dc25d79bdd6d..82dd2ced446c0a 100644 --- a/static/app/views/explore/logs/utils.tsx +++ b/static/app/views/explore/logs/utils.tsx @@ -6,6 +6,7 @@ import * as qs from 'query-string'; import type {ApiResult} from 'sentry/api'; import {t} from 'sentry/locale'; import type {PageFilters} from 'sentry/types/core'; +import type {Event} from 'sentry/types/event'; import type {TagCollection} from 'sentry/types/group'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; @@ -574,3 +575,55 @@ export const logOnceFactory = (logSeverity: 'info' | 'warn') => { }; }; }; + +export interface PseudoLogResponseItem { + [OurLogKnownFieldKey.ID]: string; + [OurLogKnownFieldKey.MESSAGE]: string; + [OurLogKnownFieldKey.SEVERITY]: 'ERROR'; + [OurLogKnownFieldKey.SEVERITY_NUMBER]: 17; + [OurLogKnownFieldKey.TRACE_ID]: string; + [OurLogKnownFieldKey.SPAN_ID]: string; + [OurLogKnownFieldKey.ORGANIZATION_ID]: number; + [OurLogKnownFieldKey.PROJECT_ID]: string; + [OurLogKnownFieldKey.TIMESTAMP]: string; + [OurLogKnownFieldKey.TIMESTAMP_PRECISE]: string | number; + __isPseudoRow: true; + __originalEvent: Event; +} + +export type LogTableRowItem = OurLogsResponseItem | PseudoLogResponseItem; + +export function isPseudoLogResponseItem( + item: LogTableRowItem +): item is PseudoLogResponseItem { + return '__isPseudoRow' in item && item.__isPseudoRow === true; +} + +export function isRegularLogResponseItem( + item: LogTableRowItem +): item is OurLogsResponseItem { + return !isPseudoLogResponseItem(item); +} + +export function createPseudoLogResponseItem( + event: Event, + projectId: string +): PseudoLogResponseItem { + const timestamp = event.dateCreated || new Date().toISOString(); + const timestampPrecise = new Date(timestamp).getTime() * 1_000_000; + + return { + [OurLogKnownFieldKey.ID]: `pseudo-${event.eventID}`, + [OurLogKnownFieldKey.MESSAGE]: event.title || event.message || 'Error Event', + [OurLogKnownFieldKey.SEVERITY]: 'ERROR', + [OurLogKnownFieldKey.SEVERITY_NUMBER]: 17, + [OurLogKnownFieldKey.TRACE_ID]: event.contexts?.trace?.trace_id || '', + [OurLogKnownFieldKey.SPAN_ID]: event.contexts?.trace?.span_id || '', + [OurLogKnownFieldKey.PROJECT_ID]: projectId, + [OurLogKnownFieldKey.TIMESTAMP]: timestamp, + [OurLogKnownFieldKey.TIMESTAMP_PRECISE]: timestampPrecise, + // Observed timestamp can be added later if needed per the event received time. + __isPseudoRow: true, + __originalEvent: event, + } as PseudoLogResponseItem; +} diff --git a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx index 899b18e86eef2c..d46cc9cff7c648 100644 --- a/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx +++ b/static/app/views/explore/metrics/metricInfoTabs/metricsSamplesTable.tsx @@ -81,12 +81,12 @@ export function MetricsSamplesTable({ {isFetching && } - {error ? ( + {!overrideTableData?.length && error ? ( - ) : data?.length ? ( - (overrideTableData ?? data).map((row, i) => ( + ) : overrideTableData?.length || data?.length ? ( + (overrideTableData ?? data ?? []).map((row, i) => ( Date: Wed, 19 Nov 2025 11:55:24 -0500 Subject: [PATCH 2/2] knip --- .../app/views/explore/logs/tables/logsInfiniteTable.tsx | 9 +++------ static/app/views/explore/logs/utils.tsx | 2 +- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx index dc7e25d1dbf410..af947e45cd9253 100644 --- a/static/app/views/explore/logs/tables/logsInfiniteTable.tsx +++ b/static/app/views/explore/logs/tables/logsInfiniteTable.tsx @@ -178,14 +178,11 @@ export function LogsInfiniteTable({ // Calculate quantized start and end times for replay links const {logStart, logEnd} = useMemo(() => { - if (!data || data.length === 0) { + if (!baseData || baseData.length === 0) { return {logStart: undefined, logEnd: undefined}; } - const timestamps = data - .filter((row): row is OurLogsResponseItem => isRegularLogResponseItem(row)) - .map(row => getLogRowTimestampMillis(row)) - .filter(Boolean); + const timestamps = baseData.map(row => getLogRowTimestampMillis(row)).filter(Boolean); if (timestamps.length === 0) { return {logStart: undefined, logEnd: undefined}; } @@ -203,7 +200,7 @@ export function LogsInfiniteTable({ logStart: new Date(quantizedStart).toISOString(), logEnd: new Date(quantizedEnd).toISOString(), }; - }, [data]); + }, [baseData]); const tableRef = useRef(null); const tableBodyRef = useRef(null); diff --git a/static/app/views/explore/logs/utils.tsx b/static/app/views/explore/logs/utils.tsx index 82dd2ced446c0a..54f64587597b70 100644 --- a/static/app/views/explore/logs/utils.tsx +++ b/static/app/views/explore/logs/utils.tsx @@ -576,7 +576,7 @@ export const logOnceFactory = (logSeverity: 'info' | 'warn') => { }; }; -export interface PseudoLogResponseItem { +interface PseudoLogResponseItem { [OurLogKnownFieldKey.ID]: string; [OurLogKnownFieldKey.MESSAGE]: string; [OurLogKnownFieldKey.SEVERITY]: 'ERROR';