import {Fragment, useMemo} from 'react'; import styled from '@emotion/styled'; 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 ExternalLink from 'sentry/components/links/externalLink'; import Link from 'sentry/components/links/link'; import type {CursorHandler} from 'sentry/components/pagination'; import Pagination from 'sentry/components/pagination'; import {Tooltip} from 'sentry/components/tooltip'; import {t, tct} from 'sentry/locale'; import type {NewQuery} from 'sentry/types/organization'; 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 {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 {OverflowEllipsisTextContainer} from 'sentry/views/insights/common/components/textAlign'; import {useTTFDConfigured} from 'sentry/views/insights/common/queries/useHasTtfdConfigured'; 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 useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject'; import { SpanOpSelector, TTID_CONTRIBUTING_SPAN_OPS, } from 'sentry/views/insights/mobile/screenload/components/spanOpSelector'; import {useTableQuery} from 'sentry/views/insights/mobile/screenload/components/tables/screensTable'; import {MobileCursors} from 'sentry/views/insights/mobile/screenload/constants'; import {MODULE_DOC_LINK} from 'sentry/views/insights/mobile/screenload/settings'; import {SpanMetricsField} 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 ScreenLoadSpansTable({ transaction, primaryRelease, secondaryRelease, }: Props) { const moduleURL = useModuleURL('screen_load'); const location = useLocation(); const {selection} = usePageFilters(); const organization = useOrganization(); const cursor = decodeScalar(location.query?.[MobileCursors.SPANS_TABLE]); const {isProjectCrossPlatform, selectedPlatform} = useCrossPlatformProject(); const spanOp = decodeScalar(location.query[SpanMetricsField.SPAN_OP]) ?? ''; const {hasTTFD, isPending: hasTTFDLoading} = useTTFDConfigured([ `transaction:"${transaction}"`, ]); const queryStringPrimary = useMemo(() => { const searchQuery = new MutableSearch([ 'transaction.op:ui.load', `transaction:${transaction}`, 'has:span.description', ...(spanOp ? [`${SpanMetricsField.SPAN_OP}:${spanOp}`] : [`span.op:[${TTID_CONTRIBUTING_SPAN_OPS.join(',')}]`]), ]); if (isProjectCrossPlatform) { searchQuery.addFilterValue('os.name', selectedPlatform); } return appendReleaseFilters(searchQuery, primaryRelease, secondaryRelease); }, [ isProjectCrossPlatform, primaryRelease, secondaryRelease, selectedPlatform, spanOp, transaction, ]); const sort = decodeSorts(location.query[QueryParameterNames.SPANS_SORT])[0] ?? { kind: 'desc', field: 'time_spent_percentage()', }; 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})`, 'ttid_contribution_rate()', 'ttfd_contribution_rate()', 'count()', 'time_spent_percentage()', `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-span-table', cursor, }); const columnNameMap = { [SPAN_OP]: t('Operation'), [SPAN_DESCRIPTION]: t('Span Description'), 'count()': t('Total Count'), affects: hasTTFD ? t('Affects') : t('Affects TTID'), 'time_spent_percentage()': t('Total Time Spent'), [`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 ), }; 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, spanGroup: row[SpanMetricsField.SPAN_GROUP], spanDescription: row[SpanMetricsField.SPAN_DESCRIPTION], }; return ( {label} ); } if (column.key === 'affects' && hasTTFD) { const ttid_contribution_rate = row['ttid_contribution_rate()'] ? parseFloat(row['ttid_contribution_rate()']) : 0; const ttfd_contribution_rate = row['ttfd_contribution_rate()'] ? parseFloat(row['ttfd_contribution_rate()']) : 0; if (!isNaN(ttid_contribution_rate) && ttid_contribution_rate > 0.99) { const tooltipValue = tct( 'This span always ends before TTID and TTFD and may affect initial and final display. [link: Learn more.]', { link: ( ), } ); return ( {t('TTID, TTFD')} ); } if (!isNaN(ttfd_contribution_rate) && ttfd_contribution_rate > 0.99) { const tooltipValue = tct( 'This span always ends before TTFD and may affect final display. [link: Learn more.]', { link: ( ), } ); return ( {t('TTFD')} ); } const tooltipValue = tct( 'This span may not be contributing to TTID or TTFD. [link: Learn more.]', { link: ( ), } ); return ( {'--'} ); } if (column.key === 'affects') { const ttid_contribution_rate = row['ttid_contribution_rate()'] ? parseFloat(row['ttid_contribution_rate()']) : 0; if (!isNaN(ttid_contribution_rate) && ttid_contribution_rate > 0.99) { const tooltipValue = tct( 'This span always ends before TTID and may affect initial display. [link: Learn more.]', { link: ( ), } ); return ( {t('Yes')} ); } const tooltipValue = tct( 'This span may not affect initial display. [link: Learn more.]', { link: ( ), } ); return ( {t('No')} ); } 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]; let alignment = fieldAlignment(column.key as string, fieldType); if (column.key === 'affects') { alignment = 'right'; } const field = { field: column.key as string, width: column.width, }; const affectsIsCurrentSort = column.key === 'affects' && (sort?.field === 'ttid_contribution_rate()' || sort?.field === 'ttfd_contribution_rate()'); function generateSortLink() { if (!tableMeta) { return undefined; } let newSortDirection: Sort['kind'] = 'desc'; if (sort?.field === column.key) { if (sort.kind === 'desc') { newSortDirection = 'asc'; } } function getNewSort() { if (column.key === 'affects') { if (sort?.field === 'ttid_contribution_rate()') { return '-ttfd_contribution_rate()'; } return '-ttid_contribution_rate()'; } return `${newSortDirection === 'desc' ? '-' : ''}${column.key}`; } return { ...location, query: {...location.query, [QueryParameterNames.SPANS_SORT]: getNewSort()}, }; } const canSort = column.key === 'affects' || 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, }} /> ); } const Container = styled('div')` ${p => p.theme.overflowEllipsis}; text-align: right; `;