import {Fragment} from 'react'; import * as qs from 'query-string'; import {getInterval} from 'sentry/components/charts/utils'; import type {GridColumnHeader} from 'sentry/components/gridEditable'; import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable'; import SortLink from 'sentry/components/gridEditable/sortLink'; import Link from 'sentry/components/links/link'; import type {CursorHandler} from 'sentry/components/pagination'; import Pagination from 'sentry/components/pagination'; import {t} from 'sentry/locale'; import type {NewQuery} from 'sentry/types/organization'; import {defined} from 'sentry/utils'; import {browserHistory} from 'sentry/utils/browserHistory'; import type {TableDataRow} from 'sentry/utils/discover/discoverQuery'; import type {MetaType} from 'sentry/utils/discover/eventView'; import EventView, {isFieldSortable} from 'sentry/utils/discover/eventView'; import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers'; import type {Sort} from 'sentry/utils/discover/fields'; import {fieldAlignment} from 'sentry/utils/discover/fields'; import {DiscoverDatasets} from 'sentry/utils/discover/types'; import {decodeList, decodeScalar, decodeSorts} from 'sentry/utils/queryString'; import {MutableSearch} from 'sentry/utils/tokenizeSearch'; import {useLocation} from 'sentry/utils/useLocation'; import useOrganization from 'sentry/utils/useOrganization'; import usePageFilters from 'sentry/utils/usePageFilters'; import { PRIMARY_RELEASE_ALIAS, SECONDARY_RELEASE_ALIAS, } from 'sentry/views/insights/common/components/releaseSelector'; import {PercentChangeCell} from 'sentry/views/insights/common/components/tableCells/percentChangeCell'; import {OverflowEllipsisTextContainer} from 'sentry/views/insights/common/components/textAlign'; import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/insights/common/utils/constants'; import {appendReleaseFilters} from 'sentry/views/insights/common/utils/releaseComparison'; import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL'; import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters'; import {APP_START_SPANS} from 'sentry/views/insights/mobile/appStarts/components/spanOpSelector'; import { COLD_START_TYPE, WARM_START_TYPE, } from 'sentry/views/insights/mobile/appStarts/components/startTypeSelector'; import useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject'; import {useTableQuery} from 'sentry/views/insights/mobile/screenload/components/tables/screensTable'; import {MobileCursors} from 'sentry/views/insights/mobile/screenload/constants'; import {SpanMetricsField, type SubregionCode} from 'sentry/views/insights/types'; const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_GROUP, SPAN_OP, PROJECT_ID} = SpanMetricsField; type Props = { primaryRelease?: string; secondaryRelease?: string; transaction?: string; }; export function SpanOperationTable({ transaction, primaryRelease, secondaryRelease, }: Props) { const moduleURL = useModuleURL('app_start'); const location = useLocation(); const {selection} = usePageFilters(); const organization = useOrganization(); const {isProjectCrossPlatform, selectedPlatform} = useCrossPlatformProject(); const cursor = decodeScalar(location.query?.[MobileCursors.SPANS_TABLE]); const spanOp = decodeScalar(location.query[SpanMetricsField.SPAN_OP]) ?? ''; const subregions = decodeList( location.query[SpanMetricsField.USER_GEO_SUBREGION] ) as SubregionCode[]; const startType = decodeScalar(location.query[SpanMetricsField.APP_START_TYPE]) ?? COLD_START_TYPE; const deviceClass = decodeScalar(location.query[SpanMetricsField.DEVICE_CLASS]) ?? ''; const searchQuery = new MutableSearch([ // Exclude root level spans because they're comprised of nested operations '!span.description:"Cold Start"', '!span.description:"Warm Start"', // Exclude this span because we can get TTID contributing spans instead '!span.description:"Initial Frame Render"', 'has:span.description', 'transaction.op:ui.load', `transaction:${transaction}`, `has:ttid`, `${SpanMetricsField.APP_START_TYPE}:${ startType || `[${COLD_START_TYPE},${WARM_START_TYPE}]` }`, `${SpanMetricsField.SPAN_OP}:${spanOp ? spanOp : `[${APP_START_SPANS.join(',')}]`}`, ...(spanOp ? [`${SpanMetricsField.SPAN_OP}:${spanOp}`] : []), ...(deviceClass ? [`${SpanMetricsField.DEVICE_CLASS}:${deviceClass}`] : []), ...(subregions.length > 0 ? [`${SpanMetricsField.USER_GEO_SUBREGION}:[${subregions.join(',')}]`] : []), ]); if (isProjectCrossPlatform) { searchQuery.addFilterValue('os.name', selectedPlatform); } const queryStringPrimary = appendReleaseFilters( searchQuery, primaryRelease, secondaryRelease ); const sort = decodeSorts(location.query[QueryParameterNames.SPANS_SORT])[0] ?? { kind: 'desc', field: `avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`, }; const newQuery: NewQuery = { name: '', fields: [ PROJECT_ID, SPAN_OP, SPAN_GROUP, SPAN_DESCRIPTION, `avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`, `avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`, `avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`, `sum(${SPAN_SELF_TIME})`, ], query: queryStringPrimary, dataset: DiscoverDatasets.SPANS_METRICS, version: 2, projects: selection.projects, interval: getInterval(selection.datetime, STARFISH_CHART_INTERVAL_FIDELITY), }; const eventView = EventView.fromNewQueryWithLocation(newQuery, location); eventView.sorts = [sort]; const {data, isPending, pageLinks} = useTableQuery({ eventView, enabled: true, referrer: 'api.starfish.mobile-spartup-span-table', cursor, }); const columnNameMap = { [SPAN_OP]: t('Operation'), [SPAN_DESCRIPTION]: t('Span Description'), [`avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`]: t( 'Avg Duration (%s)', PRIMARY_RELEASE_ALIAS ), [`avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`]: t( 'Avg Duration (%s)', SECONDARY_RELEASE_ALIAS ), [`avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`]: t('Change'), }; function renderBodyCell(column, row): React.ReactNode { if (!data?.meta || !data?.meta.fields) { return row[column.key]; } if (column.key === SPAN_DESCRIPTION) { const label = row[SpanMetricsField.SPAN_DESCRIPTION]; const pathname = `${moduleURL}/spans/`; const query = { ...location.query, transaction, spanOp: row[SpanMetricsField.SPAN_OP], spanGroup: row[SpanMetricsField.SPAN_GROUP], spanDescription: row[SpanMetricsField.SPAN_DESCRIPTION], appStartType: row[SpanMetricsField.APP_START_TYPE], }; return ( {label} ); } if (data.meta.fields[column.key] === 'percent_change') { return ( ); } const renderer = getFieldRenderer(column.key, data?.meta.fields, false); const rendered = renderer(row, { location, organization, unit: data?.meta.units?.[column.key], }); return rendered; } function renderHeadCell( column: GridColumnHeader, tableMeta?: MetaType ): React.ReactNode { const fieldType = tableMeta?.fields?.[column.key]; const alignment = fieldAlignment(column.key as string, fieldType); const field = { field: column.key as string, width: column.width, }; function generateSortLink() { if (!tableMeta) { return undefined; } let newSortDirection: Sort['kind'] = 'desc'; if (sort?.field === column.key) { if (sort.kind === 'desc') { newSortDirection = 'asc'; } } function getNewSort() { return `${newSortDirection === 'desc' ? '-' : ''}${column.key}`; } return { ...location, query: {...location.query, [QueryParameterNames.SPANS_SORT]: getNewSort()}, }; } const canSort = isFieldSortable(field, tableMeta?.fields, true); const sortLink = ( ); return sortLink; } const columnSortBy = eventView.getSorts(); const handleCursor: CursorHandler = (newCursor, pathname, query) => { browserHistory.push({ pathname, query: {...query, [MobileCursors.SPANS_TABLE]: newCursor}, }); }; return ( { return {key: col, name: columnNameMap[col] ?? col, width: COL_WIDTH_UNDEFINED}; })} columnSortBy={columnSortBy} grid={{ renderHeadCell: column => renderHeadCell(column, data?.meta), renderBodyCell, }} /> ); }