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;
`;