uptimeChecksGrid.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import {useTheme} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import {Tag} from 'sentry/components/core/badge/tag';
  4. import {DateTime} from 'sentry/components/dateTime';
  5. import Duration from 'sentry/components/duration';
  6. import type {GridColumnOrder} from 'sentry/components/gridEditable';
  7. import GridEditable from 'sentry/components/gridEditable';
  8. import ExternalLink from 'sentry/components/links/externalLink';
  9. import Link from 'sentry/components/links/link';
  10. import Placeholder from 'sentry/components/placeholder';
  11. import {Tooltip} from 'sentry/components/tooltip';
  12. import {t, tct} from 'sentry/locale';
  13. import {space} from 'sentry/styles/space';
  14. import {getShortEventId} from 'sentry/utils/events';
  15. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  16. import type {UptimeCheck, UptimeRule} from 'sentry/views/alerts/rules/uptime/types';
  17. import {useEAPSpans} from 'sentry/views/insights/common/queries/useDiscover';
  18. import {
  19. reasonToText,
  20. statusToText,
  21. tickStyle,
  22. } from 'sentry/views/insights/uptime/timelineConfig';
  23. type Props = {
  24. uptimeChecks: UptimeCheck[];
  25. uptimeRule: UptimeRule;
  26. };
  27. /**
  28. * This value is used when a trace was not recorded since the field is required.
  29. * It will never link to trace, so omit the row to avoid confusion.
  30. */
  31. const EMPTY_TRACE = '00000000000000000000000000000000';
  32. export function UptimeChecksGrid({uptimeRule, uptimeChecks}: Props) {
  33. const traceIds = uptimeChecks?.map(check => check.traceId) ?? [];
  34. const {data: spanCounts, isPending: spanCountLoading} = useEAPSpans(
  35. {
  36. limit: 10,
  37. enabled: traceIds.length > 0,
  38. search: new MutableSearch('').addDisjunctionFilterValues('trace', traceIds),
  39. fields: ['trace', 'count()'],
  40. },
  41. 'api.uptime-checks-grid'
  42. );
  43. const traceSpanCounts = spanCountLoading
  44. ? undefined
  45. : Object.fromEntries(
  46. traceIds.map(traceId => [
  47. traceId,
  48. Number(spanCounts.find(row => row.trace === traceId)?.['count()'] ?? 0),
  49. ])
  50. );
  51. return (
  52. <GridEditable
  53. emptyMessage={t('No matching uptime checks found')}
  54. data={uptimeChecks}
  55. columnOrder={[
  56. {key: 'timestamp', width: 150, name: t('Timestamp')},
  57. {key: 'checkStatus', width: 250, name: t('Status')},
  58. {key: 'httpStatusCode', width: 100, name: t('HTTP Code')},
  59. {key: 'durationMs', width: 110, name: t('Duration')},
  60. {key: 'regionName', width: 200, name: t('Region')},
  61. {key: 'traceId', width: 100, name: t('Trace')},
  62. ]}
  63. columnSortBy={[]}
  64. grid={{
  65. renderHeadCell: (col: GridColumnOrder) => <Cell>{col.name}</Cell>,
  66. renderBodyCell: (column, dataRow) => (
  67. <CheckInBodyCell
  68. column={column}
  69. uptimeRule={uptimeRule}
  70. check={dataRow}
  71. spanCount={traceSpanCounts?.[dataRow.traceId]}
  72. />
  73. ),
  74. }}
  75. />
  76. );
  77. }
  78. function CheckInBodyCell({
  79. check,
  80. column,
  81. spanCount,
  82. uptimeRule,
  83. }: {
  84. check: UptimeCheck;
  85. column: GridColumnOrder<keyof UptimeCheck>;
  86. spanCount: number | undefined;
  87. uptimeRule: UptimeRule;
  88. }) {
  89. const theme = useTheme();
  90. const {
  91. timestamp,
  92. scheduledCheckTime,
  93. durationMs,
  94. checkStatus,
  95. httpStatusCode,
  96. checkStatusReason,
  97. traceId,
  98. } = check;
  99. if (check[column.key] === undefined) {
  100. return <Cell />;
  101. }
  102. switch (column.key) {
  103. case 'timestamp': {
  104. return (
  105. <TimeCell>
  106. <Tooltip
  107. maxWidth={300}
  108. isHoverable
  109. title={t('Checked at %s', <DateTime date={timestamp} seconds />)}
  110. >
  111. <DateTime date={scheduledCheckTime} timeZone />
  112. </Tooltip>
  113. </TimeCell>
  114. );
  115. }
  116. case 'durationMs':
  117. return (
  118. <Cell>
  119. <Duration seconds={durationMs / 1000} abbreviation exact />
  120. </Cell>
  121. );
  122. case 'httpStatusCode': {
  123. if (httpStatusCode === null) {
  124. return <Cell style={{color: theme.subText}}>{t('None')}</Cell>;
  125. }
  126. return <Cell>{httpStatusCode}</Cell>;
  127. }
  128. case 'checkStatus': {
  129. const colorKey = tickStyle[checkStatus].labelColor ?? 'textColor';
  130. return (
  131. <Cell style={{color: theme[colorKey] as string}}>
  132. {statusToText[checkStatus]}{' '}
  133. {checkStatusReason &&
  134. tct('([reason])', {
  135. reason: reasonToText[checkStatusReason](check),
  136. })}
  137. </Cell>
  138. );
  139. }
  140. case 'traceId': {
  141. if (traceId === EMPTY_TRACE) {
  142. return <Cell />;
  143. }
  144. const learnMore = (
  145. <ExternalLink href="https://docs.sentry.io/product/alerts/uptime-monitoring/uptime-tracing/" />
  146. );
  147. const badge =
  148. spanCount === undefined ? (
  149. <Placeholder height="20px" width="70px" />
  150. ) : spanCount === 0 ? (
  151. <Tooltip
  152. isHoverable
  153. title={
  154. uptimeRule.traceSampling
  155. ? tct(
  156. 'No spans found in this trace. Configure your SDKs to see correlated spans across services. [learnMore:Learn more].',
  157. {learnMore}
  158. )
  159. : tct(
  160. 'Span sampling is disabled. Enable sampling to collect trace data. [learnMore:Learn more].',
  161. {learnMore}
  162. )
  163. }
  164. >
  165. <Tag type="default">{t('0 spans')}</Tag>
  166. </Tooltip>
  167. ) : (
  168. <Tag type="info">{t('%s spans', spanCount)}</Tag>
  169. );
  170. return (
  171. <TraceCell>
  172. {spanCount ? (
  173. <Link to={`/performance/trace/${traceId}/`}>{getShortEventId(traceId)}</Link>
  174. ) : (
  175. getShortEventId(traceId)
  176. )}
  177. {badge}
  178. </TraceCell>
  179. );
  180. }
  181. default:
  182. return <Cell>{check[column.key]}</Cell>;
  183. }
  184. }
  185. const Cell = styled('div')`
  186. display: flex;
  187. align-items: center;
  188. text-align: left;
  189. gap: ${space(1)};
  190. `;
  191. const TimeCell = styled(Cell)`
  192. color: ${p => p.theme.subText};
  193. text-decoration: underline;
  194. text-decoration-style: dotted;
  195. `;
  196. const TraceCell = styled(Cell)`
  197. display: grid;
  198. grid-template-columns: 65px max-content;
  199. gap: ${space(1)};
  200. `;