screenLoadSpansTable.tsx 13 KB

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