spansTable.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. import {Fragment} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import {Location} from 'history';
  4. import GridEditable, {
  5. COL_WIDTH_UNDEFINED,
  6. GridColumnHeader,
  7. } from 'sentry/components/gridEditable';
  8. import Pagination, {CursorHandler} from 'sentry/components/pagination';
  9. import {Organization} from 'sentry/types';
  10. import {EventsMetaType} from 'sentry/utils/discover/eventView';
  11. import {getFieldRenderer} from 'sentry/utils/discover/fieldRenderers';
  12. import {VisuallyCompleteWithData} from 'sentry/utils/performanceForSentry';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import {useLocation} from 'sentry/utils/useLocation';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import {renderHeadCell} from 'sentry/views/starfish/components/tableCells/renderHeadCell';
  17. import {SpanDescriptionCell} from 'sentry/views/starfish/components/tableCells/spanDescriptionCell';
  18. import {useSpanList} from 'sentry/views/starfish/queries/useSpanList';
  19. import {ModuleName, SpanMetricsField} from 'sentry/views/starfish/types';
  20. import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
  21. import {DataTitles, getThroughputTitle} from 'sentry/views/starfish/views/spans/types';
  22. import type {ValidSort} from 'sentry/views/starfish/views/spans/useModuleSort';
  23. type Row = {
  24. 'avg(span.self_time)': number;
  25. 'http_error_count()': number;
  26. 'span.description': string;
  27. 'span.domain': Array<string>;
  28. 'span.group': string;
  29. 'span.op': string;
  30. 'spm()': number;
  31. 'time_spent_percentage()': number;
  32. };
  33. type Column = GridColumnHeader<keyof Row>;
  34. type Props = {
  35. moduleName: ModuleName;
  36. sort: ValidSort;
  37. columnOrder?: Column[];
  38. endpoint?: string;
  39. limit?: number;
  40. method?: string;
  41. spanCategory?: string;
  42. };
  43. const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_GROUP, SPAN_OP, PROJECT_ID, SPAN_DOMAIN} =
  44. SpanMetricsField;
  45. export default function SpansTable({
  46. moduleName,
  47. sort,
  48. columnOrder,
  49. spanCategory,
  50. endpoint,
  51. method,
  52. limit = 25,
  53. }: Props) {
  54. const location = useLocation();
  55. const organization = useOrganization();
  56. const cursor = decodeScalar(location.query?.[QueryParameterNames.SPANS_CURSOR]);
  57. const {isLoading, data, meta, pageLinks} = useSpanList(
  58. moduleName ?? ModuleName.ALL,
  59. endpoint,
  60. method,
  61. spanCategory,
  62. [sort],
  63. limit,
  64. 'api.starfish.use-span-list',
  65. cursor
  66. );
  67. const handleCursor: CursorHandler = (newCursor, pathname, query) => {
  68. browserHistory.push({
  69. pathname,
  70. query: {...query, [QueryParameterNames.SPANS_CURSOR]: newCursor},
  71. });
  72. };
  73. const shouldTrackVCD = Boolean(endpoint);
  74. return (
  75. <Fragment>
  76. <VisuallyCompleteWithData
  77. id="SpansTable"
  78. hasData={(data?.length ?? 0) > 0}
  79. isLoading={isLoading}
  80. disabled={shouldTrackVCD}
  81. >
  82. <GridEditable
  83. isLoading={isLoading}
  84. data={data as Row[]}
  85. columnOrder={columnOrder ?? getColumns(moduleName, spanCategory)}
  86. columnSortBy={[
  87. {
  88. key: sort.field,
  89. order: sort.kind,
  90. },
  91. ]}
  92. grid={{
  93. renderHeadCell: column =>
  94. renderHeadCell({
  95. column,
  96. sort,
  97. location,
  98. sortParameterName: QueryParameterNames.SPANS_SORT,
  99. }),
  100. renderBodyCell: (column, row) =>
  101. renderBodyCell(
  102. column,
  103. row,
  104. moduleName,
  105. meta,
  106. location,
  107. organization,
  108. endpoint,
  109. method
  110. ),
  111. }}
  112. location={location}
  113. />
  114. <Pagination pageLinks={pageLinks} onCursor={handleCursor} />
  115. </VisuallyCompleteWithData>
  116. </Fragment>
  117. );
  118. }
  119. function renderBodyCell(
  120. column: Column,
  121. row: Row,
  122. moduleName: ModuleName,
  123. meta: EventsMetaType | undefined,
  124. location: Location,
  125. organization: Organization,
  126. endpoint?: string,
  127. endpointMethod?: string
  128. ) {
  129. if (column.key === SPAN_DESCRIPTION) {
  130. return (
  131. <SpanDescriptionCell
  132. moduleName={moduleName}
  133. description={row[SPAN_DESCRIPTION]}
  134. group={row[SPAN_GROUP]}
  135. projectId={row[PROJECT_ID]}
  136. endpoint={endpoint}
  137. endpointMethod={endpointMethod}
  138. />
  139. );
  140. }
  141. if (!meta || !meta?.fields) {
  142. return row[column.key];
  143. }
  144. const renderer = getFieldRenderer(column.key, meta.fields, false);
  145. const rendered = renderer(row, {
  146. location,
  147. organization,
  148. unit: meta.units?.[column.key],
  149. });
  150. return rendered;
  151. }
  152. function getDomainHeader(moduleName: ModuleName) {
  153. if (moduleName === ModuleName.HTTP) {
  154. return 'Host';
  155. }
  156. if (moduleName === ModuleName.DB) {
  157. return 'Table';
  158. }
  159. return 'Domain';
  160. }
  161. function getDescriptionHeader(moduleName: ModuleName, spanCategory?: string) {
  162. if (moduleName === ModuleName.HTTP) {
  163. return 'URL Request';
  164. }
  165. if (moduleName === ModuleName.DB) {
  166. return 'Query Description';
  167. }
  168. if (spanCategory === 'cache') {
  169. return 'Cache Query';
  170. }
  171. if (spanCategory === 'serialize') {
  172. return 'Serializer';
  173. }
  174. if (spanCategory === 'middleware') {
  175. return 'Middleware';
  176. }
  177. if (spanCategory === 'app') {
  178. return 'Application Task';
  179. }
  180. if (moduleName === 'other') {
  181. return 'Requests';
  182. }
  183. return 'Description';
  184. }
  185. function getColumns(moduleName: ModuleName, spanCategory?: string): Column[] {
  186. const description = getDescriptionHeader(moduleName, spanCategory);
  187. const domain = getDomainHeader(moduleName);
  188. const order = [
  189. // We don't show the operation selector in specific modules, so there's no
  190. // point having that column
  191. [ModuleName.ALL, ModuleName.OTHER].includes(moduleName)
  192. ? {
  193. key: SPAN_OP,
  194. name: 'Operation',
  195. width: 120,
  196. }
  197. : undefined,
  198. {
  199. key: SPAN_DESCRIPTION,
  200. name: description,
  201. width: COL_WIDTH_UNDEFINED,
  202. },
  203. ...(moduleName !== ModuleName.ALL && moduleName !== ModuleName.DB
  204. ? [
  205. {
  206. key: SPAN_DOMAIN,
  207. name: domain,
  208. width: COL_WIDTH_UNDEFINED,
  209. } as Column,
  210. ]
  211. : []),
  212. {
  213. key: 'spm()',
  214. name: getThroughputTitle(moduleName),
  215. width: COL_WIDTH_UNDEFINED,
  216. },
  217. {
  218. key: `avg(${SPAN_SELF_TIME})`,
  219. name: DataTitles.avg,
  220. width: COL_WIDTH_UNDEFINED,
  221. },
  222. ...(moduleName === ModuleName.HTTP
  223. ? [
  224. {
  225. key: 'http_error_count()',
  226. name: DataTitles.errorCount,
  227. width: COL_WIDTH_UNDEFINED,
  228. } as Column,
  229. ]
  230. : []),
  231. {
  232. key: 'time_spent_percentage()',
  233. name: DataTitles.timeSpent,
  234. width: COL_WIDTH_UNDEFINED,
  235. },
  236. ];
  237. return order.filter((item): item is NonNullable<Column> => Boolean(item));
  238. }