From 84fa06c715ed7c441733cb67c908649965baeccc Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 4 Jul 2025 09:35:59 -0400 Subject: [PATCH 01/13] ref(discover): add missing renderers to special_fields --- static/app/utils/discover/fieldRenderers.tsx | 149 ++++++++++++++----- static/app/utils/discover/styles.tsx | 6 + 2 files changed, 118 insertions(+), 37 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 824cc372299bee..2a4ae7de56dedb 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -13,6 +13,7 @@ import {deviceNameMapper} from 'sentry/components/deviceName'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import Duration from 'sentry/components/duration'; +import {ContextIcon} from 'sentry/components/events/contexts/contextIcon'; import FileSize from 'sentry/components/fileSize'; import BadgeDisplayName from 'sentry/components/idBadge/badgeDisplayName'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; @@ -45,6 +46,7 @@ import { SPAN_OP_BREAKDOWN_FIELDS, SPAN_OP_RELATIVE_BREAKDOWN_FIELD, } from 'sentry/utils/discover/fields'; +import ViewReplayLink from 'sentry/utils/discover/viewReplayLink'; import {getShortEventId} from 'sentry/utils/events'; import {formatRate} from 'sentry/utils/formatters'; import getDynamicText from 'sentry/utils/getDynamicText'; @@ -63,16 +65,20 @@ import {ResponseStatusCodeCell} from 'sentry/views/insights/common/components/ta import {StarredSegmentCell} from 'sentry/views/insights/common/components/tableCells/starredSegmentCell'; import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell'; import {SpanFields, SpanMetricsField} from 'sentry/views/insights/types'; +import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; import { filterToLocationQuery, SpanOperationBreakdownFilter, stringToFilter, } from 'sentry/views/performance/transactionSummary/filter'; +import {makeProjectsPathname} from 'sentry/views/projects/pathname'; import {ADOPTION_STAGE_LABELS} from 'sentry/views/releases/utils'; +import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; import ArrayValue from './arrayValue'; import { BarContainer, + BrowserIconContainer, Container, FieldDateTime, FieldShortId, @@ -367,35 +373,6 @@ type SpecialField = { sortField: string | null; }; -type SpecialFields = { - adoption_stage: SpecialField; - 'apdex()': SpecialField; - attachments: SpecialField; - 'count_unique(user)': SpecialField; - device: SpecialField; - 'error.handled': SpecialField; - id: SpecialField; - [SpanFields.IS_STARRED_TRANSACTION]: SpecialField; - issue: SpecialField; - 'issue.id': SpecialField; - minidump: SpecialField; - 'performance_score(measurements.score.total)': SpecialField; - 'profile.id': SpecialField; - project: SpecialField; - release: SpecialField; - replayId: SpecialField; - 'span.description': SpecialField; - 'span.status_code': SpecialField; - span_id: SpecialField; - team_key_transaction: SpecialField; - 'timestamp.to_day': SpecialField; - 'timestamp.to_hour': SpecialField; - trace: SpecialField; - 'trend_percentage()': SpecialField; - user: SpecialField; - 'user.display': SpecialField; -}; - const DownloadCount = styled('span')` padding-left: ${space(0.75)}; `; @@ -409,7 +386,7 @@ const RightAlignedContainer = styled('span')` * "Special fields" either do not map 1:1 to an single column in the event database, * or they require custom UI formatting that can't be handled by the datatype formatters. */ -const SPECIAL_FIELDS: SpecialFields = { +const SPECIAL_FIELDS: Record = { // This is a custom renderer for a field outside discover // TODO - refactor code and remove from this file or add ability to query for attachments in Discover 'apdex()': { @@ -538,13 +515,25 @@ const SPECIAL_FIELDS: SpecialFields = { }, trace: { sortField: 'trace', - renderFunc: data => { + renderFunc: (data, {organization, location}) => { const id: string | unknown = data?.trace; if (typeof id !== 'string') { return emptyValue; } - return {getShortEventId(id)}; + const target = getTraceDetailsUrl({ + traceSlug: data.trace, + timestamp: data.timestamp, + organization, + dateSelection: {statsPeriod: undefined, start: undefined, end: undefined}, + location, + }); + + return ( + + {getShortEventId(id)} + + ); }, }, 'issue.id': { @@ -565,13 +554,24 @@ const SPECIAL_FIELDS: SpecialFields = { }, replayId: { sortField: 'replayId', - renderFunc: data => { + renderFunc: (data, {organization}) => { const replayId = data?.replayId; if (typeof replayId !== 'string' || !replayId) { return emptyValue; } - return {getShortEventId(replayId)}; + const target = makeReplaysPathname({ + path: `/${replayId}/`, + organization, + }); + + return ( + + + {getShortEventId(replayId)} + + + ); }, }, 'profile.id': { @@ -649,6 +649,31 @@ const SPECIAL_FIELDS: SpecialFields = { ); }, }, + project_id: { + sortField: 'project_id', + renderFunc: (data, {organization}) => { + const projectId = data.project_id; + // TODO: add projects to baggage to avoid using deprecated component + return ( + + + {({projects}) => { + const project = projects.find(p => p.id === projectId?.toString()); + if (!project) { + return emptyValue; + } + const target = makeProjectsPathname({ + path: `/${project?.slug}/?project=${projectId}/`, + organization, + }); + + return {projectId}; + }} + + + ); + }, + }, user: { sortField: 'user', renderFunc: data => { @@ -843,6 +868,56 @@ const SPECIAL_FIELDS: SpecialFields = { ); }, }, + alert_id: { + sortField: 'alert_id', + renderFunc: data => { + const alertId = data.alert_id; + if (!alertId) { + return {emptyValue}; + } + + return ( + + {alertId} + + ); + }, + }, + browser: { + sortField: 'browser', + renderFunc: data => { + const browser = data.browser; + if (!browser) { + return {emptyStringValue}; + } + + // also includes the version--don't want to pass that + const browserAsList = browser.split(); + + return ( + + + {browser} + + ); + }, + }, + 'browser.name': { + sortField: 'browser.name', + renderFunc: data => { + const browserName: string = data['browser.name']; + if (!browserName) { + return {emptyStringValue}; + } + + return ( + + + {browserName} + + ); + }, + }, }; type SpecialFunctionFieldRenderer = ( @@ -951,8 +1026,8 @@ export function getSortField( field: string, tableMeta: MetaType | undefined ): string | null { - if (SPECIAL_FIELDS.hasOwnProperty(field)) { - return SPECIAL_FIELDS[field as keyof typeof SPECIAL_FIELDS].sortField; + if (Object.hasOwn(SPECIAL_FIELDS, field)) { + return SPECIAL_FIELDS[field]!.sortField; } if (!tableMeta) { @@ -971,7 +1046,7 @@ export function getSortField( } const fieldType = tableMeta[field]; - if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) { + if (Object.hasOwn(FIELD_FORMATTERS, fieldType)) { return FIELD_FORMATTERS[fieldType as keyof typeof FIELD_FORMATTERS].isSortable ? field : null; diff --git a/static/app/utils/discover/styles.tsx b/static/app/utils/discover/styles.tsx index 1ae2f3c4c159e6..979e73a00d722c 100644 --- a/static/app/utils/discover/styles.tsx +++ b/static/app/utils/discover/styles.tsx @@ -62,3 +62,9 @@ export const UserIcon = styled(IconUser)` margin-left: ${space(1)}; color: ${p => p.theme.gray400}; `; + +export const BrowserIconContainer = styled('div')` + display: flex; + gap: ${space(1)}; + width: max-content; +`; From 08a2d8fde2faf0a63780d09a05988bd5cb40c547 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Tue, 8 Jul 2025 16:15:11 -0400 Subject: [PATCH 02/13] add more missing fields, limit max width for some fields --- static/app/utils/discover/fieldRenderers.tsx | 84 ++++++++++++++----- .../views/dashboards/datasetConfig/spans.tsx | 6 +- 2 files changed, 66 insertions(+), 24 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 2a4ae7de56dedb..adec57d834818a 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -21,6 +21,7 @@ import UserBadge from 'sentry/components/idBadge/userBadge'; import ExternalLink from 'sentry/components/links/externalLink'; import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar'; import {pickBarColor} from 'sentry/components/performance/waterfall/utils'; +import TimeSince from 'sentry/components/timeSince'; import UserMisery from 'sentry/components/userMisery'; import Version from 'sentry/components/version'; import {IconDownload} from 'sentry/icons'; @@ -46,6 +47,7 @@ import { SPAN_OP_BREAKDOWN_FIELDS, SPAN_OP_RELATIVE_BREAKDOWN_FIELD, } from 'sentry/utils/discover/fields'; +import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import ViewReplayLink from 'sentry/utils/discover/viewReplayLink'; import {getShortEventId} from 'sentry/utils/events'; import {formatRate} from 'sentry/utils/formatters'; @@ -328,11 +330,11 @@ export const FIELD_FORMATTERS: FieldFormatters = { if (isUrl(value)) { return ( - + {value} - + ); } @@ -340,7 +342,7 @@ export const FIELD_FORMATTERS: FieldFormatters = { if (value && typeof value === 'string') { return ( - {nullableValue(value)} + {nullableValue(value)} ); } @@ -470,13 +472,27 @@ const SPECIAL_FIELDS: Record = { }, id: { sortField: 'id', - renderFunc: data => { + renderFunc: (data, {organization, location}) => { const id: string | unknown = data?.id; if (typeof id !== 'string') { - return null; + return {emptyStringValue}; } + const target = generateLinkToEventInTraceView({ + projectSlug: data.project, + traceSlug: data.trace, + timestamp: data.timestamp, + targetId: data['transaction.span_id'], + eventId: undefined, + organization, + location, + spanId: id, + }); - return {getShortEventId(id)}; + return ( + + {getShortEventId(id)} + + ); }, }, span_id: { @@ -502,13 +518,13 @@ const SPECIAL_FIELDS: Record = { showOnlyOnOverflow maxWidth={400} > - + {isUrl(value) ? ( {value} ) : ( nullableValue(value) )} - + ); }, @@ -820,6 +836,21 @@ const SPECIAL_FIELDS: Record = { ), }, + timestamp: { + sortField: 'timestamp', + renderFunc: data => { + const timestamp = data.timestamp; + if (!timestamp) { + return {emptyStringValue}; + } + const date = new Date(data.timestamp); + return ( + + + + ); + }, + }, 'timestamp.to_hour': { sortField: 'timestamp.to_hour', renderFunc: data => ( @@ -868,21 +899,6 @@ const SPECIAL_FIELDS: Record = { ); }, }, - alert_id: { - sortField: 'alert_id', - renderFunc: data => { - const alertId = data.alert_id; - if (!alertId) { - return {emptyValue}; - } - - return ( - - {alertId} - - ); - }, - }, browser: { sortField: 'browser', renderFunc: data => { @@ -918,6 +934,22 @@ const SPECIAL_FIELDS: Record = { ); }, }, + 'os.name': { + sortField: 'os.name', + renderFunc: data => { + const osName: string = data['os.name']; + if (!osName) { + return {emptyStringValue}; + } + + return ( + + + {osName} + + ); + }, + }, }; type SpecialFunctionFieldRenderer = ( @@ -1186,6 +1218,12 @@ const StyledProjectBadge = styled(ProjectBadge)` } `; +// Use this for fields that may be extremely wide +export const OverflowContainer = styled('div')` + max-width: 500px; + ${p => p.theme.overflowEllipsis}; +`; + /** * Get the field renderer for the named field and metadata * diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index d6d9194c1c3aab..93edb2736b2c21 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -35,6 +35,7 @@ import { import { getTableSortOptions, getTimeseriesSortOptions, + renderTraceAsLinkable, transformEventsResponseToTable, } from 'sentry/views/dashboards/datasetConfig/errorsAndTransactions'; import {getSeriesRequestData} from 'sentry/views/dashboards/datasetConfig/utils/getSeriesRequestData'; @@ -170,7 +171,10 @@ export const SpansConfig: DatasetConfig< transformTable: transformEventsResponseToTable, transformSeries: transformEventsResponseToSeries, filterAggregateParams, - getCustomFieldRenderer: (field, meta, _organization) => { + getCustomFieldRenderer: (field, meta, widget, _organization) => { + if (field === 'trace') { + return renderTraceAsLinkable(widget); + } return getFieldRenderer(field, meta, false); }, }; From 51c8131d14344fbe4bf22b5d5b690b7398e35ab2 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Tue, 8 Jul 2025 16:57:49 -0400 Subject: [PATCH 03/13] remove browser renderer for just browser.name --- static/app/utils/discover/fieldRenderers.tsx | 21 +------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index adec57d834818a..f6949bdd54c4d2 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -899,25 +899,6 @@ const SPECIAL_FIELDS: Record = { ); }, }, - browser: { - sortField: 'browser', - renderFunc: data => { - const browser = data.browser; - if (!browser) { - return {emptyStringValue}; - } - - // also includes the version--don't want to pass that - const browserAsList = browser.split(); - - return ( - - - {browser} - - ); - }, - }, 'browser.name': { sortField: 'browser.name', renderFunc: data => { @@ -1220,7 +1201,7 @@ const StyledProjectBadge = styled(ProjectBadge)` // Use this for fields that may be extremely wide export const OverflowContainer = styled('div')` - max-width: 500px; + max-width: 1000px; ${p => p.theme.overflowEllipsis}; `; From 89f02ed391f6edb503bb6d0d01c0b31b08ce5284 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Wed, 9 Jul 2025 15:03:31 -0400 Subject: [PATCH 04/13] add better parsing for os and browser name --- static/app/utils/discover/fieldRenderers.tsx | 56 +++++++++++++++++--- static/app/utils/discover/styles.tsx | 2 +- 2 files changed, 50 insertions(+), 8 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index f6949bdd54c4d2..b0682a847193e6 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -80,11 +80,11 @@ import {makeReplaysPathname} from 'sentry/views/replays/pathnames'; import ArrayValue from './arrayValue'; import { BarContainer, - BrowserIconContainer, Container, FieldDateTime, FieldShortId, FlexContainer, + IconContainer, NumberContainer, OverflowFieldShortId, OverflowLink, @@ -907,11 +907,32 @@ const SPECIAL_FIELDS: Record = { return {emptyStringValue}; } + const formattedName = browserName.split(' ').join('-').toLocaleLowerCase(); return ( - - + + {browserName} - + + ); + }, + }, + browser: { + sortField: 'browser', + renderFunc: data => { + const browser: string = data.browser; + if (!browser) { + return {emptyStringValue}; + } + + const broswerArray = browser.split(' '); + broswerArray.pop(); + const formattedName = broswerArray.join('-').toLocaleLowerCase(); + + return ( + + + {browser} + ); }, }, @@ -923,11 +944,32 @@ const SPECIAL_FIELDS: Record = { return {emptyStringValue}; } + const formattedName = osName.split(' ').join('-').toLocaleLowerCase(); return ( - - + + {osName} - + + ); + }, + }, + os: { + sortField: 'os', + renderFunc: data => { + const os: string = data.os; + if (!os) { + return {emptyStringValue}; + } + + const osArray = os.split(' '); + osArray.pop(); + const formattedName = osArray.join('-').toLocaleLowerCase(); + + return ( + + + {os} + ); }, }, diff --git a/static/app/utils/discover/styles.tsx b/static/app/utils/discover/styles.tsx index 979e73a00d722c..2bfd9d2aba1ce6 100644 --- a/static/app/utils/discover/styles.tsx +++ b/static/app/utils/discover/styles.tsx @@ -63,7 +63,7 @@ export const UserIcon = styled(IconUser)` color: ${p => p.theme.gray400}; `; -export const BrowserIconContainer = styled('div')` +export const IconContainer = styled('div')` display: flex; gap: ${space(1)}; width: max-content; From ae9924405827f063855222bd39d2ed20186e09ce Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Wed, 9 Jul 2025 16:49:05 -0400 Subject: [PATCH 05/13] more clean up --- .../utils/dashboards/issueFieldRenderers.tsx | 14 +++-- static/app/utils/discover/fieldRenderers.tsx | 53 ++++++++++++------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/static/app/utils/dashboards/issueFieldRenderers.tsx b/static/app/utils/dashboards/issueFieldRenderers.tsx index 72777ce29891a9..05f282db9dc36a 100644 --- a/static/app/utils/dashboards/issueFieldRenderers.tsx +++ b/static/app/utils/dashboards/issueFieldRenderers.tsx @@ -21,6 +21,8 @@ import {Container, FieldShortId, OverflowLink} from 'sentry/utils/discover/style import {SavedQueryDatasets} from 'sentry/utils/discover/types'; import {hasDatasetSelector} from 'sentry/views/dashboards/utils'; import {FieldKey} from 'sentry/views/dashboards/widgetBuilder/issueWidget/fields'; +import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper'; +import {ContextType} from 'sentry/views/discover/table/quickContext/utils'; /** * Types, functions and definitions for rendering fields in discover results. @@ -79,9 +81,15 @@ const SPECIAL_FIELDS: SpecialFields = { return ( - - - + + + + + ); }, diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index b0682a847193e6..4ab89c955945ca 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -64,9 +64,10 @@ import {ContextType} from 'sentry/views/discover/table/quickContext/utils'; import {PerformanceBadge} from 'sentry/views/insights/browser/webVitals/components/performanceBadge'; import {PercentChangeCell} from 'sentry/views/insights/common/components/tableCells/percentChangeCell'; import {ResponseStatusCodeCell} from 'sentry/views/insights/common/components/tableCells/responseStatusCodeCell'; +import {SpanDescriptionCell} from 'sentry/views/insights/common/components/tableCells/spanDescriptionCell'; import {StarredSegmentCell} from 'sentry/views/insights/common/components/tableCells/starredSegmentCell'; import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell'; -import {SpanFields, SpanMetricsField} from 'sentry/views/insights/types'; +import {ModuleName, SpanFields, SpanMetricsField} from 'sentry/views/insights/types'; import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; import { filterToLocationQuery, @@ -151,6 +152,9 @@ const missingUserMisery = tct( ), } ); +const userAgentLocking = t( + 'This OS locks newer versions to the this version in the user-agent HTTP header. The exact OS version is unknown.' +); export function nullableValue(value: string | null): string | React.ReactElement { switch (value) { @@ -510,23 +514,29 @@ const SPECIAL_FIELDS: Record = { sortField: 'span.description', renderFunc: data => { const value = data['span.description']; + const op: string = data['span.op']; - return ( - - - {isUrl(value) ? ( - {value} - ) : ( - nullableValue(value) - )} - - - ); + if (!op || !(op === ModuleName.DB || op === ModuleName.RESOURCE)) { + return ( + + + {isUrl(value) ? ( + {value} + ) : ( + nullableValue(value) + )} + + + ); + } + + // TODO: Figure this out + return ; }, }, trace: { @@ -962,13 +972,20 @@ const SPECIAL_FIELDS: Record = { } const osArray = os.split(' '); + const hasUserAgentLocking = osArray[osArray.length - 1]?.includes('>='); osArray.pop(); const formattedName = osArray.join('-').toLocaleLowerCase(); return ( - {os} + {hasUserAgentLocking ? ( + + {os} + + ) : ( + os + )} ); }, From 1f087519bd0ad841a699840a1a995beeeac3e044 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 10 Jul 2025 10:24:02 -0400 Subject: [PATCH 06/13] add user agent locking message, span.description --- static/app/utils/discover/fieldRenderers.tsx | 76 ++++++++++++-------- static/app/utils/discover/styles.tsx | 2 +- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 4ab89c955945ca..fa6e75b90ed8b6 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -153,7 +153,7 @@ const missingUserMisery = tct( } ); const userAgentLocking = t( - 'This OS locks newer versions to the this version in the user-agent HTTP header. The exact OS version is unknown.' + 'This OS locks newer versions to this version in the user-agent HTTP header. The exact OS version is unknown.' ); export function nullableValue(value: string | null): string | React.ReactElement { @@ -334,11 +334,11 @@ export const FIELD_FORMATTERS: FieldFormatters = { if (isUrl(value)) { return ( - + {value} - + ); } @@ -346,7 +346,7 @@ export const FIELD_FORMATTERS: FieldFormatters = { if (value && typeof value === 'string') { return ( - {nullableValue(value)} + {nullableValue(value)} ); } @@ -481,12 +481,16 @@ const SPECIAL_FIELDS: Record = { if (typeof id !== 'string') { return {emptyStringValue}; } + + if (!data.trace) { + return {getShortEventId(id)}; + } + const target = generateLinkToEventInTraceView({ projectSlug: data.project, traceSlug: data.trace, timestamp: data.timestamp, targetId: data['transaction.span_id'], - eventId: undefined, organization, location, spanId: id, @@ -515,28 +519,48 @@ const SPECIAL_FIELDS: Record = { renderFunc: data => { const value = data['span.description']; const op: string = data['span.op']; + const projectId = + typeof data.project_id === 'number' + ? data.project_id + : parseInt(data.project_id, 10) || -1; + const spanGroup: string | undefined = data['span.group']; - if (!op || !(op === ModuleName.DB || op === ModuleName.RESOURCE)) { + if (op === ModuleName.DB) { return ( - - - {isUrl(value) ? ( - {value} - ) : ( - nullableValue(value) - )} - - + ); } - - // TODO: Figure this out - return ; + if (op === ModuleName.RESOURCE) { + return ( + + ); + } + return ( + + + {isUrl(value) ? ( + {value} + ) : ( + nullableValue(value) + )} + + + ); }, }, trace: { @@ -1258,12 +1282,6 @@ const StyledProjectBadge = styled(ProjectBadge)` } `; -// Use this for fields that may be extremely wide -export const OverflowContainer = styled('div')` - max-width: 1000px; - ${p => p.theme.overflowEllipsis}; -`; - /** * Get the field renderer for the named field and metadata * diff --git a/static/app/utils/discover/styles.tsx b/static/app/utils/discover/styles.tsx index 2bfd9d2aba1ce6..763705503ded87 100644 --- a/static/app/utils/discover/styles.tsx +++ b/static/app/utils/discover/styles.tsx @@ -66,5 +66,5 @@ export const UserIcon = styled(IconUser)` export const IconContainer = styled('div')` display: flex; gap: ${space(1)}; - width: max-content; + width: 100%; `; From 181cd6fe0a94b3839dd5c92947cdb7a87800ffb2 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 10 Jul 2025 11:42:51 -0400 Subject: [PATCH 07/13] fix span id error --- static/app/utils/discover/fieldRenderers.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index fa6e75b90ed8b6..f4148367b1b8fd 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -487,7 +487,6 @@ const SPECIAL_FIELDS: Record = { } const target = generateLinkToEventInTraceView({ - projectSlug: data.project, traceSlug: data.trace, timestamp: data.timestamp, targetId: data['transaction.span_id'], From 3ba8b9cc6d8ce0fd3aa945bcd83fa54aee297681 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 10 Jul 2025 12:17:41 -0400 Subject: [PATCH 08/13] fix broken unit tests --- static/app/views/discover/table/tableView.tsx | 2 +- static/app/views/explore/tables/fieldRenderer.tsx | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index 250a490ec1a200..ea37526765bcbe 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -379,7 +379,7 @@ function TableView(props: TableViewProps) { const idLink = ( - {cell} + {getShortEventId(dataRow[columnKey])} ); diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx index 5ae471eb329355..83933460c50867 100644 --- a/static/app/views/explore/tables/fieldRenderer.tsx +++ b/static/app/views/explore/tables/fieldRenderer.tsx @@ -146,7 +146,7 @@ function BaseExploreFieldRenderer({ source: TraceViewSources.TRACES, }); - rendered = {rendered}; + rendered = {data[field]}; } if (['id', 'span_id', 'transaction.id'].includes(field)) { @@ -162,7 +162,9 @@ function BaseExploreFieldRenderer({ source: TraceViewSources.TRACES, }); - rendered = {rendered}; + rendered = ( + {spanId ? getShortEventId(data[field]) : data[field]} + ); } if (field === 'profile.id') { From 811bebd1cacc8efc387b90c716c5339bacd7c30c Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Thu, 10 Jul 2025 14:19:38 -0400 Subject: [PATCH 09/13] refactoring, fix tests --- static/app/utils/discover/fieldRenderers.tsx | 44 +++---------------- static/app/utils/discover/styles.tsx | 3 +- .../views/dashboards/datasetConfig/spans.tsx | 29 +++++++++++- static/app/views/discover/table/tableView.tsx | 2 +- .../views/explore/tables/fieldRenderer.tsx | 6 +-- .../performance/transactionEvents.spec.tsx | 4 +- 6 files changed, 42 insertions(+), 46 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index f4148367b1b8fd..96695d7fc40ac3 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -47,7 +47,6 @@ import { SPAN_OP_BREAKDOWN_FIELDS, SPAN_OP_RELATIVE_BREAKDOWN_FIELD, } from 'sentry/utils/discover/fields'; -import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import ViewReplayLink from 'sentry/utils/discover/viewReplayLink'; import {getShortEventId} from 'sentry/utils/events'; import {formatRate} from 'sentry/utils/formatters'; @@ -68,7 +67,6 @@ import {SpanDescriptionCell} from 'sentry/views/insights/common/components/table import {StarredSegmentCell} from 'sentry/views/insights/common/components/tableCells/starredSegmentCell'; import {TimeSpentCell} from 'sentry/views/insights/common/components/tableCells/timeSpentCell'; import {ModuleName, SpanFields, SpanMetricsField} from 'sentry/views/insights/types'; -import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; import { filterToLocationQuery, SpanOperationBreakdownFilter, @@ -143,7 +141,9 @@ const EmptyValueContainer = styled('span')` color: ${p => p.theme.subText}; `; const emptyValue = {t('(no value)')}; -const emptyStringValue = {t('(empty string)')}; +export const emptyStringValue = ( + {t('(empty string)')} +); const missingUserMisery = tct( 'We were unable to calculate User Misery. A likely cause of this is that the user was not set. [link:Read the docs]', { @@ -476,30 +476,12 @@ const SPECIAL_FIELDS: Record = { }, id: { sortField: 'id', - renderFunc: (data, {organization, location}) => { + renderFunc: data => { const id: string | unknown = data?.id; if (typeof id !== 'string') { return {emptyStringValue}; } - - if (!data.trace) { - return {getShortEventId(id)}; - } - - const target = generateLinkToEventInTraceView({ - traceSlug: data.trace, - timestamp: data.timestamp, - targetId: data['transaction.span_id'], - organization, - location, - spanId: id, - }); - - return ( - - {getShortEventId(id)} - - ); + return {getShortEventId(id)}; }, }, span_id: { @@ -564,25 +546,13 @@ const SPECIAL_FIELDS: Record = { }, trace: { sortField: 'trace', - renderFunc: (data, {organization, location}) => { + renderFunc: data => { const id: string | unknown = data?.trace; if (typeof id !== 'string') { return emptyValue; } - const target = getTraceDetailsUrl({ - traceSlug: data.trace, - timestamp: data.timestamp, - organization, - dateSelection: {statsPeriod: undefined, start: undefined, end: undefined}, - location, - }); - - return ( - - {getShortEventId(id)} - - ); + return {getShortEventId(id)}; }, }, 'issue.id': { diff --git a/static/app/utils/discover/styles.tsx b/static/app/utils/discover/styles.tsx index 763705503ded87..31dff1513cbe18 100644 --- a/static/app/utils/discover/styles.tsx +++ b/static/app/utils/discover/styles.tsx @@ -66,5 +66,6 @@ export const UserIcon = styled(IconUser)` export const IconContainer = styled('div')` display: flex; gap: ${space(1)}; - width: 100%; + overflow: hidden; + text-overflow: ellipsis; `; diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index 93edb2736b2c21..8852df9fd4a2fe 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -2,6 +2,7 @@ import pickBy from 'lodash/pickBy'; import {doEventsRequest} from 'sentry/actionCreators/events'; import type {Client} from 'sentry/api'; +import {Link} from 'sentry/components/core/link'; import type {PageFilters} from 'sentry/types/core'; import type {TagCollection} from 'sentry/types/group'; import type { @@ -13,7 +14,8 @@ import type { import toArray from 'sentry/utils/array/toArray'; import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements'; import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery'; -import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; +import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers'; +import {emptyStringValue, getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {Aggregation, QueryFieldValue} from 'sentry/utils/discover/fields'; import { type DiscoverQueryExtras, @@ -21,6 +23,8 @@ import { doDiscoverQuery, } from 'sentry/utils/discover/genericDiscoverQuery'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; +import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; +import {getShortEventId} from 'sentry/utils/events'; import { AggregationKey, ALLOWED_EXPLORE_VISUALIZE_AGGREGATES, @@ -172,6 +176,9 @@ export const SpansConfig: DatasetConfig< transformSeries: transformEventsResponseToSeries, filterAggregateParams, getCustomFieldRenderer: (field, meta, widget, _organization) => { + if (field === 'id') { + return renderEventInTraceView; + } if (field === 'trace') { return renderTraceAsLinkable(widget); } @@ -356,3 +363,23 @@ function filterSeriesSortOptions(columns: Set) { return columns.has(option.value.meta.name); }; } + +function renderEventInTraceView( + data: any, + {location, organization}: RenderFunctionBaggage +) { + const spanId: string = data.id; + if (!spanId) { + return emptyStringValue; + } + const target = generateLinkToEventInTraceView({ + traceSlug: data.trace, + timestamp: data.timestamp, + targetId: data['transaction.span_id'], + organization, + location, + spanId, + }); + + return {getShortEventId(spanId)}; +} diff --git a/static/app/views/discover/table/tableView.tsx b/static/app/views/discover/table/tableView.tsx index ea37526765bcbe..250a490ec1a200 100644 --- a/static/app/views/discover/table/tableView.tsx +++ b/static/app/views/discover/table/tableView.tsx @@ -379,7 +379,7 @@ function TableView(props: TableViewProps) { const idLink = ( - {getShortEventId(dataRow[columnKey])} + {cell} ); diff --git a/static/app/views/explore/tables/fieldRenderer.tsx b/static/app/views/explore/tables/fieldRenderer.tsx index 83933460c50867..5ae471eb329355 100644 --- a/static/app/views/explore/tables/fieldRenderer.tsx +++ b/static/app/views/explore/tables/fieldRenderer.tsx @@ -146,7 +146,7 @@ function BaseExploreFieldRenderer({ source: TraceViewSources.TRACES, }); - rendered = {data[field]}; + rendered = {rendered}; } if (['id', 'span_id', 'transaction.id'].includes(field)) { @@ -162,9 +162,7 @@ function BaseExploreFieldRenderer({ source: TraceViewSources.TRACES, }); - rendered = ( - {spanId ? getShortEventId(data[field]) : data[field]} - ); + rendered = {rendered}; } if (field === 'profile.id') { diff --git a/static/app/views/performance/transactionEvents.spec.tsx b/static/app/views/performance/transactionEvents.spec.tsx index c5edb33c1056cf..6fbf2401db77f0 100644 --- a/static/app/views/performance/transactionEvents.spec.tsx +++ b/static/app/views/performance/transactionEvents.spec.tsx @@ -237,7 +237,7 @@ describe('Performance > TransactionSummary', function () { expect(tableFirstRowColumns[2]).toHaveTextContent('(no value)'); expect(tableFirstRowColumns[3]).toHaveTextContent('400.00ms'); expect(tableFirstRowColumns[4]).toHaveTextContent('1234'); - expect(tableFirstRowColumns[5]).toHaveTextContent('May 21, 2020 3:31:18 PM UTC'); + expect(tableFirstRowColumns[5]).toHaveTextContent('in 3y'); ProjectsStore.reset(); }); @@ -268,7 +268,7 @@ describe('Performance > TransactionSummary', function () { expect(tableFirstRowColumns[3]).toHaveTextContent('200'); expect(tableFirstRowColumns[4]).toHaveTextContent('400.00ms'); expect(tableFirstRowColumns[5]).toHaveTextContent('1234'); - expect(tableFirstRowColumns[6]).toHaveTextContent('May 21, 2020 3:31:18 PM UTC'); + expect(tableFirstRowColumns[6]).toHaveTextContent('in 3y'); ProjectsStore.reset(); }); From 5b653ac705ff878b4f631a1c8197f4bd943b18c7 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 11 Jul 2025 13:21:41 -0400 Subject: [PATCH 10/13] address comments --- static/app/utils/discover/fieldRenderers.tsx | 127 ++++++++++-------- .../datasetConfig/errorsAndTransactions.tsx | 10 +- .../views/dashboards/datasetConfig/spans.tsx | 5 +- .../performance/transactionEvents.spec.tsx | 4 +- 4 files changed, 84 insertions(+), 62 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 96695d7fc40ac3..62fb5ea0c48243 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -21,7 +21,6 @@ import UserBadge from 'sentry/components/idBadge/userBadge'; import ExternalLink from 'sentry/components/links/externalLink'; import {RowRectangle} from 'sentry/components/performance/waterfall/rowBar'; import {pickBarColor} from 'sentry/components/performance/waterfall/utils'; -import TimeSince from 'sentry/components/timeSince'; import UserMisery from 'sentry/components/userMisery'; import Version from 'sentry/components/version'; import {IconDownload} from 'sentry/icons'; @@ -153,7 +152,7 @@ const missingUserMisery = tct( } ); const userAgentLocking = t( - 'This OS locks newer versions to this version in the user-agent HTTP header. The exact OS version is unknown.' + 'This operating system does not provide detailed version information in the User-Agent HTTP header. The exact operating system version is unknown.' ); export function nullableValue(value: string | null): string | React.ReactElement { @@ -498,25 +497,15 @@ const SPECIAL_FIELDS: Record = { 'span.description': { sortField: 'span.description', renderFunc: data => { - const value = data['span.description']; - const op: string = data['span.op']; + const value = data[SpanFields.SPAN_DESCRIPTION]; + const op: string = data[SpanFields.SPAN_OP]; const projectId = - typeof data.project_id === 'number' - ? data.project_id - : parseInt(data.project_id, 10) || -1; - const spanGroup: string | undefined = data['span.group']; + typeof data[SpanFields.PROJECT_ID] === 'number' + ? data[SpanFields.PROJECT_ID] + : parseInt(data[SpanFields.PROJECT_ID], 10) || -1; + const spanGroup: string | undefined = data[SpanFields.SPAN_GROUP]; - if (op === ModuleName.DB) { - return ( - - ); - } - if (op === ModuleName.RESOURCE) { + if (op === ModuleName.DB || op === ModuleName.RESOURCE) { return ( = { /> ); } + return ( = { }, project_id: { sortField: 'project_id', - renderFunc: (data, {organization}) => { + renderFunc: (data, baggage) => { const projectId = data.project_id; - // TODO: add projects to baggage to avoid using deprecated component - return ( - - - {({projects}) => { - const project = projects.find(p => p.id === projectId?.toString()); - if (!project) { - return emptyValue; - } - const target = makeProjectsPathname({ - path: `/${project?.slug}/?project=${projectId}/`, - organization, - }); - - return {projectId}; - }} - - - ); + return getProjectIdLink(projectId, baggage); + }, + }, + 'project.id': { + sortField: 'project.id', + renderFunc: (data, baggage) => { + const projectId = data['project.id']; + return getProjectIdLink(projectId, baggage); }, }, user: { @@ -849,7 +828,9 @@ const SPECIAL_FIELDS: Record = { const date = new Date(data.timestamp); return ( - + + + ); }, @@ -910,10 +891,9 @@ const SPECIAL_FIELDS: Record = { return {emptyStringValue}; } - const formattedName = browserName.split(' ').join('-').toLocaleLowerCase(); return ( - + {getContextIcon(browserName, false)} {browserName} ); @@ -927,13 +907,9 @@ const SPECIAL_FIELDS: Record = { return {emptyStringValue}; } - const broswerArray = browser.split(' '); - broswerArray.pop(); - const formattedName = broswerArray.join('-').toLocaleLowerCase(); - return ( - + {getContextIcon(browser, true)} {browser} ); @@ -947,10 +923,9 @@ const SPECIAL_FIELDS: Record = { return {emptyStringValue}; } - const formattedName = osName.split(' ').join('-').toLocaleLowerCase(); return ( - + {getContextIcon(osName, false)} {osName} ); @@ -964,14 +939,11 @@ const SPECIAL_FIELDS: Record = { return {emptyStringValue}; } - const osArray = os.split(' '); - const hasUserAgentLocking = osArray[osArray.length - 1]?.includes('>='); - osArray.pop(); - const formattedName = osArray.join('-').toLocaleLowerCase(); + const hasUserAgentLocking = os.includes('>='); return ( - + {getContextIcon(os, true)} {hasUserAgentLocking ? ( {os} @@ -985,6 +957,53 @@ const SPECIAL_FIELDS: Record = { }, }; +/** + * Returns an icon component for fields that use logo icons + * @param value the text to map to an icon + * @param dropVersion drops the last part of the value (the version) + */ +const getContextIcon = (value: string, dropVersion?: boolean) => { + const valueArray = value.split(' '); + + // Some fields have the number version attached, so it needs to be removed + if (dropVersion) { + valueArray.pop(); + } + + const formattedValue = valueArray.join('-').toLocaleLowerCase(); + + return ; +}; + +const getProjectIdLink = ( + projectId: number | string | undefined, + {organization}: RenderFunctionBaggage +) => { + if (!projectId) { + return {emptyValue}; + } + const parsedId = typeof projectId === 'string' ? parseInt(projectId, 10) : projectId; + // TODO: Component has been deprecated in favour of hook, need to refactor this later + return ( + + + {({projects}) => { + const project = projects.find(p => p.id === parsedId?.toString()); + if (!project) { + return emptyValue; + } + const target = makeProjectsPathname({ + path: `/${project?.slug}/?project=${parsedId}/`, + organization, + }); + + return {parsedId}; + }} + + + ); +}; + type SpecialFunctionFieldRenderer = ( fieldName: string ) => (data: EventData, baggage: RenderFunctionBaggage) => React.ReactNode; diff --git a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx index 48c245e2376f33..2ce9a1a899a6e6 100644 --- a/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx +++ b/static/app/views/dashboards/datasetConfig/errorsAndTransactions.tsx @@ -19,12 +19,12 @@ import {defined} from 'sentry/utils'; import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements'; import {getTimeStampFromTableDateField} from 'sentry/utils/dates'; import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery'; -import type {MetaType} from 'sentry/utils/discover/eventView'; +import type {EventData, MetaType} from 'sentry/utils/discover/eventView'; import type { FieldFormatterRenderFunctionPartial, RenderFunctionBaggage, } from 'sentry/utils/discover/fieldRenderers'; -import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; +import {emptyStringValue, getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {AggregationOutputType, QueryFieldValue} from 'sentry/utils/discover/fields'; import { errorsAndTransactionsAggregateFunctionOutputType, @@ -360,7 +360,7 @@ function getSeriesResultType( } export function renderEventIdAsLinkable( - data: any, + data: EventData, {eventView, organization}: RenderFunctionBaggage ) { const id: string | unknown = data?.id; @@ -387,12 +387,12 @@ export function renderEventIdAsLinkable( export function renderTraceAsLinkable(widget?: Widget) { return function ( - data: any, + data: EventData, {eventView, organization, location}: RenderFunctionBaggage ) { const id: string | unknown = data?.trace; if (!eventView || typeof id !== 'string') { - return null; + return emptyStringValue; } const dateSelection = eventView.normalizeDateSelection(location); const target = getTraceDetailsUrl({ diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index 8852df9fd4a2fe..efeed7a6c97432 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -14,6 +14,7 @@ import type { import toArray from 'sentry/utils/array/toArray'; import type {CustomMeasurementCollection} from 'sentry/utils/customMeasurements/customMeasurements'; import type {EventsTableData, TableData} from 'sentry/utils/discover/discoverQuery'; +import type {EventData} from 'sentry/utils/discover/eventView'; import type {RenderFunctionBaggage} from 'sentry/utils/discover/fieldRenderers'; import {emptyStringValue, getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {Aggregation, QueryFieldValue} from 'sentry/utils/discover/fields'; @@ -51,6 +52,7 @@ import type {FieldValueOption} from 'sentry/views/discover/table/queryField'; import {FieldValueKind} from 'sentry/views/discover/table/types'; import {generateFieldOptions} from 'sentry/views/discover/utils'; import type {SamplingMode} from 'sentry/views/explore/hooks/useProgressiveQuery'; +import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; const DEFAULT_WIDGET_QUERY: WidgetQuery = { name: '', @@ -365,7 +367,7 @@ function filterSeriesSortOptions(columns: Set) { } function renderEventInTraceView( - data: any, + data: EventData, {location, organization}: RenderFunctionBaggage ) { const spanId: string = data.id; @@ -379,6 +381,7 @@ function renderEventInTraceView( organization, location, spanId, + source: TraceViewSources.DASHBOARDS, }); return {getShortEventId(spanId)}; diff --git a/static/app/views/performance/transactionEvents.spec.tsx b/static/app/views/performance/transactionEvents.spec.tsx index 6fbf2401db77f0..c5edb33c1056cf 100644 --- a/static/app/views/performance/transactionEvents.spec.tsx +++ b/static/app/views/performance/transactionEvents.spec.tsx @@ -237,7 +237,7 @@ describe('Performance > TransactionSummary', function () { expect(tableFirstRowColumns[2]).toHaveTextContent('(no value)'); expect(tableFirstRowColumns[3]).toHaveTextContent('400.00ms'); expect(tableFirstRowColumns[4]).toHaveTextContent('1234'); - expect(tableFirstRowColumns[5]).toHaveTextContent('in 3y'); + expect(tableFirstRowColumns[5]).toHaveTextContent('May 21, 2020 3:31:18 PM UTC'); ProjectsStore.reset(); }); @@ -268,7 +268,7 @@ describe('Performance > TransactionSummary', function () { expect(tableFirstRowColumns[3]).toHaveTextContent('200'); expect(tableFirstRowColumns[4]).toHaveTextContent('400.00ms'); expect(tableFirstRowColumns[5]).toHaveTextContent('1234'); - expect(tableFirstRowColumns[6]).toHaveTextContent('in 3y'); + expect(tableFirstRowColumns[6]).toHaveTextContent('May 21, 2020 3:31:18 PM UTC'); ProjectsStore.reset(); }); From 7c0f760be620fee3da1363f57374fd06e17bb4e6 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 11 Jul 2025 13:54:23 -0400 Subject: [PATCH 11/13] fix broken tests --- static/app/utils/discover/fieldRenderers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index 62fb5ea0c48243..e200a899b111c3 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -659,14 +659,14 @@ const SPECIAL_FIELDS: Record = { }, }, project_id: { - sortField: 'project_id', + sortField: null, renderFunc: (data, baggage) => { const projectId = data.project_id; return getProjectIdLink(projectId, baggage); }, }, 'project.id': { - sortField: 'project.id', + sortField: null, renderFunc: (data, baggage) => { const projectId = data['project.id']; return getProjectIdLink(projectId, baggage); From 02d580819a0addab1e85513e8f3cbd32e39e5471 Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 11 Jul 2025 14:21:06 -0400 Subject: [PATCH 12/13] address cursor bugs, fix unit tests again --- static/app/utils/discover/eventView.spec.tsx | 96 +++++++++---------- static/app/utils/discover/fieldRenderers.tsx | 28 +++--- .../views/dashboards/datasetConfig/spans.tsx | 4 +- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/static/app/utils/discover/eventView.spec.tsx b/static/app/utils/discover/eventView.spec.tsx index 6d708f34e60996..4bb5199845c51d 100644 --- a/static/app/utils/discover/eventView.spec.tsx +++ b/static/app/utils/discover/eventView.spec.tsx @@ -1184,7 +1184,7 @@ describe('EventView.generateQueryStringObject()', function () { name: 'best query', fields: [ {field: 'count()', width: 123}, - {field: 'project.id', width: 456}, + {field: 'issue', width: 456}, ], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', @@ -1203,7 +1203,7 @@ describe('EventView.generateQueryStringObject()', function () { const expected = { id: '1234', name: 'best query', - field: ['count()', 'project.id'], + field: ['count()', 'issue'], widths: ['123', '456'], sort: '-count', query: 'event.type:error', @@ -1659,7 +1659,7 @@ describe('EventView.toNewQuery()', function () { name: 'best query', fields: [ {field: 'count()', width: 123}, - {field: 'project.id', width: 456}, + {field: 'issue', width: 456}, ], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', @@ -1681,7 +1681,7 @@ describe('EventView.toNewQuery()', function () { version: 2, id: '1234', name: 'best query', - fields: ['count()', 'project.id'], + fields: ['count()', 'issue'], widths: ['123', '456'], orderby: '-count', query: 'event.type:error', @@ -1713,7 +1713,7 @@ describe('EventView.toNewQuery()', function () { version: 2, id: '1234', name: 'best query', - fields: ['count()', 'project.id'], + fields: ['count()', 'issue'], widths: ['123', '456'], orderby: '-count', projects: [42], @@ -1744,7 +1744,7 @@ describe('EventView.toNewQuery()', function () { version: 2, id: '1234', name: 'best query', - fields: ['count()', 'project.id'], + fields: ['count()', 'issue'], widths: ['123', '456'], orderby: '-count', projects: [42], @@ -1765,7 +1765,7 @@ describe('EventView.isValid()', function () { it('event view is valid when there is at least one field', function () { const eventView = new EventView({ ...REQUIRED_CONSTRUCTOR_PROPS, - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: [], project: [], }); @@ -1791,7 +1791,7 @@ describe('EventView.getWidths()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, fields: [ {field: 'count()', width: COL_WIDTH_UNDEFINED}, - {field: 'project.id', width: 2020}, + {field: 'issue', width: 2020}, {field: 'title', width: COL_WIDTH_UNDEFINED}, {field: 'time', width: 420}, {field: 'lcp', width: 69}, @@ -1817,12 +1817,12 @@ describe('EventView.getFields()', function () { it('returns fields', function () { const eventView = new EventView({ ...REQUIRED_CONSTRUCTOR_PROPS, - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: [], project: [], }); - expect(eventView.getFields()).toEqual(['count()', 'project.id']); + expect(eventView.getFields()).toEqual(['count()', 'issue']); }); }); @@ -1832,7 +1832,7 @@ describe('EventView.numOfColumns()', function () { const eventView = new EventView({ ...REQUIRED_CONSTRUCTOR_PROPS, - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: [], project: [], }); @@ -1893,7 +1893,7 @@ describe('EventView.clone()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -1928,7 +1928,7 @@ describe('EventView.withColumns()', function () { name: 'best query', fields: [ {field: 'count()', width: 30}, - {field: 'project.id', width: 99}, + {field: 'issue', width: 99}, {field: 'failure_count()', width: 30}, ], yAxis: 'failure_count()', @@ -1946,7 +1946,7 @@ describe('EventView.withColumns()', function () { const newView = eventView.withColumns([ {kind: 'field', field: 'title'}, {kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]}, - {kind: 'field', field: 'project.id'}, + {kind: 'field', field: 'issue'}, {kind: 'field', field: 'culprit'}, ]); // Views should be different. @@ -1954,7 +1954,7 @@ describe('EventView.withColumns()', function () { expect(newView.fields).toEqual([ {field: 'title', width: COL_WIDTH_UNDEFINED}, {field: 'count()', width: COL_WIDTH_UNDEFINED}, - {field: 'project.id', width: COL_WIDTH_UNDEFINED}, + {field: 'issue', width: COL_WIDTH_UNDEFINED}, {field: 'culprit', width: COL_WIDTH_UNDEFINED}, ]); }); @@ -1976,14 +1976,14 @@ describe('EventView.withColumns()', function () { it('inherits widths from existing columns when names match', function () { const newView = eventView.withColumns([ {kind: 'function', function: [AggregationKey.COUNT, '', undefined, undefined]}, - {kind: 'field', field: 'project.id'}, + {kind: 'field', field: 'issue'}, {kind: 'field', field: 'title'}, {kind: 'field', field: 'time'}, ]); expect(newView.fields).toEqual([ {field: 'count()', width: 30}, - {field: 'project.id', width: 99}, + {field: 'issue', width: 99}, {field: 'title', width: COL_WIDTH_UNDEFINED}, {field: 'time', width: COL_WIDTH_UNDEFINED}, ]); @@ -2016,12 +2016,12 @@ describe('EventView.withColumns()', function () { it('updates yAxis if column is dropped', function () { const newView = eventView.withColumns([ {kind: 'field', field: 'count()'}, - {kind: 'field', field: 'project.id'}, + {kind: 'field', field: 'issue'}, ]); expect(newView.fields).toEqual([ {field: 'count()', width: 30}, - {field: 'project.id', width: 99}, + {field: 'issue', width: 99}, ]); expect(eventView.yAxis).toBe('failure_count()'); @@ -2036,7 +2036,7 @@ describe('EventView.withNewColumn()', function () { name: 'best query', fields: [ {field: 'count()', width: 30}, - {field: 'project.id', width: 99}, + {field: 'issue', width: 99}, ], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', @@ -2118,7 +2118,7 @@ describe('EventView.withResizedColumn()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2146,7 +2146,7 @@ describe('EventView.withUpdatedColumn()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2335,7 +2335,7 @@ describe('EventView.withUpdatedColumn()', function () { // this column is expected to be non-sortable const newColumn: Column = { kind: 'field', - field: 'project.id', + field: 'issue', }; const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta); @@ -2347,7 +2347,7 @@ describe('EventView.withUpdatedColumn()', function () { const nextState = { ...state, sorts: [{field: 'title', kind: 'desc'}], - fields: [{field: 'project.id'}, {field: 'title'}], + fields: [{field: 'issue'}, {field: 'title'}], }; expect(eventView2).toMatchObject(nextState); @@ -2364,7 +2364,7 @@ describe('EventView.withUpdatedColumn()', function () { // this column is expected to be non-sortable const newColumn: Column = { kind: 'field', - field: 'project.id', + field: 'issue', }; const eventView2 = eventView.withUpdatedColumn(0, newColumn, meta); @@ -2376,7 +2376,7 @@ describe('EventView.withUpdatedColumn()', function () { const nextState = { ...state, sorts: [], - fields: [{field: 'project.id'}], + fields: [{field: 'issue'}], }; expect(eventView2).toMatchObject(nextState); @@ -2389,7 +2389,7 @@ describe('EventView.withDeletedColumn()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2454,7 +2454,7 @@ describe('EventView.withDeletedColumn()', function () { const nextState = { ...state, - // we expect sorts to be empty since project.id is non-sortable + // we expect sorts to be empty since issue is non-sortable sorts: [], fields: [state.fields[1]], }; @@ -2465,7 +2465,7 @@ describe('EventView.withDeletedColumn()', function () { it('has a remaining sortable column', function () { const modifiedState: ConstructorParameters[0] = { ...state, - fields: [{field: 'count()'}, {field: 'project.id'}, {field: 'title'}], + fields: [{field: 'count()'}, {field: 'issue'}, {field: 'title'}], }; const eventView = new EventView(modifiedState); @@ -2478,7 +2478,7 @@ describe('EventView.withDeletedColumn()', function () { const nextState = { ...state, sorts: [{field: 'title', kind: 'desc'}], - fields: [{field: 'project.id'}, {field: 'title'}], + fields: [{field: 'issue'}, {field: 'title'}], }; expect(eventView2).toMatchObject(nextState); @@ -2539,7 +2539,7 @@ describe('EventView.getSorts()', function () { it('returns fields', function () { const eventView = new EventView({ ...REQUIRED_CONSTRUCTOR_PROPS, - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), project: [], }); @@ -2611,7 +2611,7 @@ describe('EventView.sortForField()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2638,7 +2638,7 @@ describe('EventView.sortForField()', function () { it('returns undefined when selected field is not sorted', function () { const field = { - field: 'project.id', + field: 'issue', }; expect(eventView.sortForField(field, meta)).toBeUndefined(); @@ -2646,7 +2646,7 @@ describe('EventView.sortForField()', function () { it('returns undefined when no meta is provided', function () { const field = { - field: 'project.id', + field: 'issue', }; expect(eventView.sortForField(field, undefined)).toBeUndefined(); @@ -2658,7 +2658,7 @@ describe('EventView.sortOnField()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2839,7 +2839,7 @@ describe('EventView.isEqualTo()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2870,7 +2870,7 @@ describe('EventView.isEqualTo()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2894,7 +2894,7 @@ describe('EventView.isEqualTo()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2910,7 +2910,7 @@ describe('EventView.isEqualTo()', function () { const differences = { id: '12', name: 'new query', - fields: [{field: 'project.id'}, {field: 'count()'}], + fields: [{field: 'issue'}, {field: 'count()'}], sorts: [{field: AggregationKey.COUNT, kind: 'asc'}], query: 'event.type:transaction', project: [24], @@ -2938,7 +2938,7 @@ describe('EventView.isEqualTo()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -2977,7 +2977,7 @@ describe('EventView.getResultsViewUrlTarget()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -3033,7 +3033,7 @@ describe('EventView.getResultsViewShortUrlTarget()', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -3098,7 +3098,7 @@ describe('EventView.getPerformanceTransactionEventsViewUrlTarget()', function () ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -3564,7 +3564,7 @@ describe('isAPIPayloadSimilar', function () { ...REQUIRED_CONSTRUCTOR_PROPS, id: '1234', name: 'best query', - fields: [{field: 'count()'}, {field: 'project.id'}], + fields: [{field: 'count()'}, {field: 'issue'}], sorts: generateSorts([AggregationKey.COUNT]), query: 'event.type:error', project: [42], @@ -3759,7 +3759,7 @@ describe('isAPIPayloadSimilar', function () { const equationField = {field: 'equation|failure_count() / count()'}; const otherEquationField = {field: 'equation|failure_count() / 2'}; state.fields = [ - {field: 'project.id'}, + {field: 'issue'}, {field: 'count()'}, equationField, otherEquationField, @@ -3770,7 +3770,7 @@ describe('isAPIPayloadSimilar', function () { state.fields = [ equationField, - {field: 'project.id'}, + {field: 'issue'}, {field: 'count()'}, otherEquationField, ]; @@ -3787,7 +3787,7 @@ describe('isAPIPayloadSimilar', function () { const equationField = {field: 'equation|failure_count() / count()'}; const otherEquationField = {field: 'equation|failure_count() / 2'}; state.fields = [ - {field: 'project.id'}, + {field: 'issue'}, {field: 'count()'}, equationField, otherEquationField, @@ -3797,7 +3797,7 @@ describe('isAPIPayloadSimilar', function () { const thisAPIPayload = thisEventView.getEventsAPIPayload(location); state.fields = [ - {field: 'project.id'}, + {field: 'issue'}, {field: 'count()'}, otherEquationField, equationField, diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index e200a899b111c3..f0050ee2af33ce 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -658,15 +658,16 @@ const SPECIAL_FIELDS: Record = { ); }, }, + // Two different project ID fields are being used right now. `project_id` is shared between all datasets, but `project.id` is the new one used in spans project_id: { - sortField: null, + sortField: 'project_id', renderFunc: (data, baggage) => { const projectId = data.project_id; return getProjectIdLink(projectId, baggage); }, }, 'project.id': { - sortField: null, + sortField: 'project.id', renderFunc: (data, baggage) => { const projectId = data['project.id']; return getProjectIdLink(projectId, baggage); @@ -886,8 +887,8 @@ const SPECIAL_FIELDS: Record = { 'browser.name': { sortField: 'browser.name', renderFunc: data => { - const browserName: string = data['browser.name']; - if (!browserName) { + const browserName = data['browser.name']; + if (typeof browserName !== 'string') { return {emptyStringValue}; } @@ -902,8 +903,8 @@ const SPECIAL_FIELDS: Record = { browser: { sortField: 'browser', renderFunc: data => { - const browser: string = data.browser; - if (!browser) { + const browser = data.browser; + if (typeof browser !== 'string') { return {emptyStringValue}; } @@ -918,8 +919,8 @@ const SPECIAL_FIELDS: Record = { 'os.name': { sortField: 'os.name', renderFunc: data => { - const osName: string = data['os.name']; - if (!osName) { + const osName = data['os.name']; + if (osName !== 'string') { return {emptyStringValue}; } @@ -934,8 +935,8 @@ const SPECIAL_FIELDS: Record = { os: { sortField: 'os', renderFunc: data => { - const os: string = data.os; - if (!os) { + const os = data.os; + if (typeof os !== 'string') { return {emptyStringValue}; } @@ -979,11 +980,12 @@ const getProjectIdLink = ( projectId: number | string | undefined, {organization}: RenderFunctionBaggage ) => { - if (!projectId) { + const parsedId = typeof projectId === 'string' ? parseInt(projectId, 10) : projectId; + if (!parsedId || isNaN(parsedId)) { return {emptyValue}; } - const parsedId = typeof projectId === 'string' ? parseInt(projectId, 10) : projectId; - // TODO: Component has been deprecated in favour of hook, need to refactor this later + + // TODO: Component has been deprecated in favour of hook, need to refactor this return ( diff --git a/static/app/views/dashboards/datasetConfig/spans.tsx b/static/app/views/dashboards/datasetConfig/spans.tsx index efeed7a6c97432..53b7ebf986c09a 100644 --- a/static/app/views/dashboards/datasetConfig/spans.tsx +++ b/static/app/views/dashboards/datasetConfig/spans.tsx @@ -370,8 +370,8 @@ function renderEventInTraceView( data: EventData, {location, organization}: RenderFunctionBaggage ) { - const spanId: string = data.id; - if (!spanId) { + const spanId = data.id; + if (!spanId || typeof spanId !== 'string') { return emptyStringValue; } const target = generateLinkToEventInTraceView({ From 2b38268f3be37b8c7254e7a2b3aee87990fec15d Mon Sep 17 00:00:00 2001 From: lzhao-sentry Date: Fri, 11 Jul 2025 14:41:03 -0400 Subject: [PATCH 13/13] add typeof check to os name --- static/app/utils/discover/fieldRenderers.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/app/utils/discover/fieldRenderers.tsx b/static/app/utils/discover/fieldRenderers.tsx index f0050ee2af33ce..7ab9251036d568 100644 --- a/static/app/utils/discover/fieldRenderers.tsx +++ b/static/app/utils/discover/fieldRenderers.tsx @@ -920,7 +920,7 @@ const SPECIAL_FIELDS: Record = { sortField: 'os.name', renderFunc: data => { const osName = data['os.name']; - if (osName !== 'string') { + if (typeof osName !== 'string') { return {emptyStringValue}; }