screenLoadSpansTable.tsx 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401
  1. import {Fragment, useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as qs from 'query-string';
  4. import {getInterval} from 'sentry/components/charts/utils';
  5. import type {GridColumnHeader} from 'sentry/components/gridEditable';
  6. import GridEditable, {COL_WIDTH_UNDEFINED} from 'sentry/components/gridEditable';
  7. import SortLink from 'sentry/components/gridEditable/sortLink';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import Link from 'sentry/components/links/link';
  10. import type {CursorHandler} from 'sentry/components/pagination';
  11. import Pagination from 'sentry/components/pagination';
  12. import {Tooltip} from 'sentry/components/tooltip';
  13. import {t, tct} from 'sentry/locale';
  14. import type {NewQuery} from 'sentry/types/organization';
  15. import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
  16. import type {MetaType} from 'sentry/utils/discover/eventView';
  17. import EventView, {isFieldSortable} from 'sentry/utils/discover/eventView';
  18. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  19. import type {Sort} from 'sentry/utils/discover/fields';
  20. import {fieldAlignment} from 'sentry/utils/discover/fields';
  21. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  22. import {decodeScalar, decodeSorts} from 'sentry/utils/queryString';
  23. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  24. import {useLocation} from 'sentry/utils/useLocation';
  25. import {useNavigate} from 'sentry/utils/useNavigate';
  26. import useOrganization from 'sentry/utils/useOrganization';
  27. import usePageFilters from 'sentry/utils/usePageFilters';
  28. import {
  29. PRIMARY_RELEASE_ALIAS,
  30. SECONDARY_RELEASE_ALIAS,
  31. } from 'sentry/views/insights/common/components/releaseSelector';
  32. import {OverflowEllipsisTextContainer} from 'sentry/views/insights/common/components/textAlign';
  33. import {useTTFDConfigured} from 'sentry/views/insights/common/queries/useHasTtfdConfigured';
  34. import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/insights/common/utils/constants';
  35. import {appendReleaseFilters} from 'sentry/views/insights/common/utils/releaseComparison';
  36. import {useModuleURL} from 'sentry/views/insights/common/utils/useModuleURL';
  37. import {QueryParameterNames} from 'sentry/views/insights/common/views/queryParameters';
  38. import useCrossPlatformProject from 'sentry/views/insights/mobile/common/queries/useCrossPlatformProject';
  39. import {
  40. SpanOpSelector,
  41. TTID_CONTRIBUTING_SPAN_OPS,
  42. } from 'sentry/views/insights/mobile/screenload/components/spanOpSelector';
  43. import {useTableQuery} from 'sentry/views/insights/mobile/screenload/components/tables/screensTable';
  44. import {MobileCursors} from 'sentry/views/insights/mobile/screenload/constants';
  45. import {MODULE_DOC_LINK} from 'sentry/views/insights/mobile/screenload/settings';
  46. import {isModuleEnabled} from 'sentry/views/insights/pages/utils';
  47. import {ModuleName, SpanMetricsField} from 'sentry/views/insights/types';
  48. const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_GROUP, SPAN_OP, PROJECT_ID} =
  49. SpanMetricsField;
  50. type Props = {
  51. primaryRelease?: string;
  52. secondaryRelease?: string;
  53. transaction?: string;
  54. };
  55. export function ScreenLoadSpansTable({
  56. transaction,
  57. primaryRelease,
  58. secondaryRelease,
  59. }: Props) {
  60. const organization = useOrganization();
  61. const isMobileScreensEnabled = isModuleEnabled(ModuleName.MOBILE_VITALS, organization);
  62. const moduleURL = useModuleURL(
  63. isMobileScreensEnabled ? ModuleName.MOBILE_VITALS : ModuleName.SCREEN_LOAD
  64. );
  65. const baseURL = isMobileScreensEnabled
  66. ? `${moduleURL}/details/`
  67. : `${moduleURL}/spans/`;
  68. const navigate = useNavigate();
  69. const location = useLocation();
  70. const {selection} = usePageFilters();
  71. const cursor = decodeScalar(location.query?.[MobileCursors.SPANS_TABLE]);
  72. const {isProjectCrossPlatform, selectedPlatform} = useCrossPlatformProject();
  73. const spanOp = decodeScalar(location.query[SpanMetricsField.SPAN_OP]) ?? '';
  74. const {hasTTFD, isPending: hasTTFDLoading} = useTTFDConfigured([
  75. `transaction:"${transaction}"`,
  76. ]);
  77. const queryStringPrimary = useMemo(() => {
  78. const searchQuery = new MutableSearch([
  79. 'transaction.op:ui.load',
  80. `transaction:${transaction}`,
  81. 'has:span.description',
  82. ...(spanOp
  83. ? [`${SpanMetricsField.SPAN_OP}:${spanOp}`]
  84. : [`span.op:[${TTID_CONTRIBUTING_SPAN_OPS.join(',')}]`]),
  85. ]);
  86. if (isProjectCrossPlatform) {
  87. searchQuery.addFilterValue('os.name', selectedPlatform);
  88. }
  89. return appendReleaseFilters(searchQuery, primaryRelease, secondaryRelease);
  90. }, [
  91. isProjectCrossPlatform,
  92. primaryRelease,
  93. secondaryRelease,
  94. selectedPlatform,
  95. spanOp,
  96. transaction,
  97. ]);
  98. const sort = decodeSorts(location.query[QueryParameterNames.SPANS_SORT])[0] ?? {
  99. kind: 'desc',
  100. field: 'time_spent_percentage()',
  101. };
  102. const newQuery: NewQuery = {
  103. name: '',
  104. fields: [
  105. PROJECT_ID,
  106. SPAN_OP,
  107. SPAN_GROUP,
  108. SPAN_DESCRIPTION,
  109. `avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`,
  110. `avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`,
  111. 'ttid_contribution_rate()',
  112. 'ttfd_contribution_rate()',
  113. 'count()',
  114. 'time_spent_percentage()',
  115. `sum(${SPAN_SELF_TIME})`,
  116. ],
  117. query: queryStringPrimary,
  118. dataset: DiscoverDatasets.SPANS_METRICS,
  119. version: 2,
  120. projects: selection.projects,
  121. interval: getInterval(selection.datetime, STARFISH_CHART_INTERVAL_FIDELITY),
  122. };
  123. const eventView = EventView.fromNewQueryWithLocation(newQuery, location);
  124. eventView.sorts = [sort];
  125. const {data, isPending, pageLinks} = useTableQuery({
  126. eventView,
  127. enabled: true,
  128. referrer: 'api.starfish.mobile-span-table',
  129. cursor,
  130. });
  131. const columnNameMap = {
  132. [SPAN_OP]: t('Operation'),
  133. [SPAN_DESCRIPTION]: t('Span Description'),
  134. 'count()': t('Total Count'),
  135. affects: hasTTFD ? t('Affects') : t('Affects TTID'),
  136. 'time_spent_percentage()': t('Total Time Spent'),
  137. [`avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`]: t(
  138. 'Avg Duration (%s)',
  139. PRIMARY_RELEASE_ALIAS
  140. ),
  141. [`avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`]: t(
  142. 'Avg Duration (%s)',
  143. SECONDARY_RELEASE_ALIAS
  144. ),
  145. };
  146. function renderBodyCell(column: any, row: any): React.ReactNode {
  147. if (!data?.meta || !data?.meta.fields) {
  148. return row[column.key];
  149. }
  150. if (column.key === SPAN_DESCRIPTION) {
  151. const label = row[SpanMetricsField.SPAN_DESCRIPTION];
  152. const query = {
  153. ...location.query,
  154. transaction,
  155. spanGroup: row[SpanMetricsField.SPAN_GROUP],
  156. spanDescription: row[SpanMetricsField.SPAN_DESCRIPTION],
  157. };
  158. return (
  159. <OverflowEllipsisTextContainer>
  160. <Link to={`${baseURL}?${qs.stringify(query)}`}>{label}</Link>
  161. </OverflowEllipsisTextContainer>
  162. );
  163. }
  164. if (column.key === 'affects' && hasTTFD) {
  165. const ttid_contribution_rate = row['ttid_contribution_rate()']
  166. ? parseFloat(row['ttid_contribution_rate()'])
  167. : 0;
  168. const ttfd_contribution_rate = row['ttfd_contribution_rate()']
  169. ? parseFloat(row['ttfd_contribution_rate()'])
  170. : 0;
  171. if (!isNaN(ttid_contribution_rate) && ttid_contribution_rate > 0.99) {
  172. const tooltipValue = tct(
  173. 'This span always ends before TTID and TTFD and may affect initial and final display. [link: Learn more.]',
  174. {
  175. link: (
  176. <ExternalLink href={`${MODULE_DOC_LINK}#ttid-and-ttfd-affecting-spans`} />
  177. ),
  178. }
  179. );
  180. return (
  181. <Tooltip isHoverable title={tooltipValue} showUnderline>
  182. <Container>{t('TTID, TTFD')}</Container>
  183. </Tooltip>
  184. );
  185. }
  186. if (!isNaN(ttfd_contribution_rate) && ttfd_contribution_rate > 0.99) {
  187. const tooltipValue = tct(
  188. 'This span always ends before TTFD and may affect final display. [link: Learn more.]',
  189. {
  190. link: (
  191. <ExternalLink href={`${MODULE_DOC_LINK}#ttid-and-ttfd-affecting-spans`} />
  192. ),
  193. }
  194. );
  195. return (
  196. <Tooltip isHoverable title={tooltipValue} showUnderline>
  197. <Container>{t('TTFD')}</Container>
  198. </Tooltip>
  199. );
  200. }
  201. const tooltipValue = tct(
  202. 'This span may not be contributing to TTID or TTFD. [link: Learn more.]',
  203. {
  204. link: (
  205. <ExternalLink href={`${MODULE_DOC_LINK}#ttid-and-ttfd-affecting-spans`} />
  206. ),
  207. }
  208. );
  209. return (
  210. <Tooltip isHoverable title={tooltipValue}>
  211. <Container>{'--'}</Container>
  212. </Tooltip>
  213. );
  214. }
  215. if (column.key === 'affects') {
  216. const ttid_contribution_rate = row['ttid_contribution_rate()']
  217. ? parseFloat(row['ttid_contribution_rate()'])
  218. : 0;
  219. if (!isNaN(ttid_contribution_rate) && ttid_contribution_rate > 0.99) {
  220. const tooltipValue = tct(
  221. 'This span always ends before TTID and may affect initial display. [link: Learn more.]',
  222. {
  223. link: (
  224. <ExternalLink href={`${MODULE_DOC_LINK}#ttid-and-ttfd-affecting-spans`} />
  225. ),
  226. }
  227. );
  228. return (
  229. <Tooltip isHoverable title={tooltipValue} showUnderline>
  230. <Container>{t('Yes')}</Container>
  231. </Tooltip>
  232. );
  233. }
  234. const tooltipValue = tct(
  235. 'This span may not affect initial display. [link: Learn more.]',
  236. {
  237. link: (
  238. <ExternalLink href={`${MODULE_DOC_LINK}#ttid-and-ttfd-affecting-spans`} />
  239. ),
  240. }
  241. );
  242. return (
  243. <Tooltip isHoverable title={tooltipValue} showUnderline>
  244. <Container>{t('No')}</Container>
  245. </Tooltip>
  246. );
  247. }
  248. const renderer = getFieldRenderer(column.key, data?.meta.fields, false);
  249. const rendered = renderer(row, {
  250. location,
  251. organization,
  252. unit: data?.meta.units?.[column.key],
  253. });
  254. return rendered;
  255. }
  256. function renderHeadCell(
  257. column: GridColumnHeader,
  258. tableMeta?: MetaType
  259. ): React.ReactNode {
  260. const fieldType = tableMeta?.fields?.[column.key];
  261. let alignment = fieldAlignment(column.key as string, fieldType);
  262. if (column.key === 'affects') {
  263. alignment = 'right';
  264. }
  265. const field = {
  266. field: column.key as string,
  267. width: column.width,
  268. };
  269. const affectsIsCurrentSort =
  270. column.key === 'affects' &&
  271. (sort?.field === 'ttid_contribution_rate()' ||
  272. sort?.field === 'ttfd_contribution_rate()');
  273. function generateSortLink() {
  274. if (!tableMeta) {
  275. return undefined;
  276. }
  277. let newSortDirection: Sort['kind'] = 'desc';
  278. if (sort?.field === column.key) {
  279. if (sort.kind === 'desc') {
  280. newSortDirection = 'asc';
  281. }
  282. }
  283. function getNewSort() {
  284. if (column.key === 'affects') {
  285. if (sort?.field === 'ttid_contribution_rate()') {
  286. return '-ttfd_contribution_rate()';
  287. }
  288. return '-ttid_contribution_rate()';
  289. }
  290. return `${newSortDirection === 'desc' ? '-' : ''}${column.key}`;
  291. }
  292. return {
  293. ...location,
  294. query: {...location.query, [QueryParameterNames.SPANS_SORT]: getNewSort()},
  295. };
  296. }
  297. const canSort =
  298. column.key === 'affects' || isFieldSortable(field, tableMeta?.fields, true);
  299. const sortLink = (
  300. <SortLink
  301. align={alignment}
  302. title={column.name}
  303. direction={
  304. affectsIsCurrentSort
  305. ? sort?.field === 'ttid_contribution_rate()'
  306. ? 'desc'
  307. : 'asc'
  308. : sort?.field === column.key
  309. ? sort.kind
  310. : undefined
  311. }
  312. canSort={canSort}
  313. generateSortLink={generateSortLink}
  314. />
  315. );
  316. return sortLink;
  317. }
  318. const columnSortBy = eventView.getSorts();
  319. const handleCursor: CursorHandler = (newCursor, pathname, query) => {
  320. navigate({
  321. pathname,
  322. query: {...query, [MobileCursors.SPANS_TABLE]: newCursor},
  323. });
  324. };
  325. return (
  326. <Fragment>
  327. <SpanOpSelector
  328. primaryRelease={primaryRelease}
  329. transaction={transaction}
  330. secondaryRelease={secondaryRelease}
  331. />
  332. <GridEditable
  333. isLoading={isPending || hasTTFDLoading}
  334. data={data?.data as TableDataRow[]}
  335. columnOrder={[
  336. String(SPAN_OP),
  337. String(SPAN_DESCRIPTION),
  338. `avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`,
  339. `avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`,
  340. ...(organization.features.includes('insights-initial-modules')
  341. ? ['affects']
  342. : []),
  343. ...['count()', 'time_spent_percentage()'],
  344. ].map(col => {
  345. return {key: col, name: columnNameMap[col] ?? col, width: COL_WIDTH_UNDEFINED};
  346. })}
  347. columnSortBy={columnSortBy}
  348. grid={{
  349. renderHeadCell: column => renderHeadCell(column, data?.meta),
  350. renderBodyCell,
  351. }}
  352. />
  353. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  354. </Fragment>
  355. );
  356. }
  357. const Container = styled('div')`
  358. ${p => p.theme.overflowEllipsis};
  359. text-align: right;
  360. `;