customMetricsEventData.tsx 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. import {Fragment, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import {LinkButton} from 'sentry/components/button';
  4. import {EventDataSection} from 'sentry/components/events/eventDataSection';
  5. import KeyValueList from 'sentry/components/events/interfaces/keyValueList';
  6. import {
  7. MetricsSummary,
  8. MetricsSummaryItem,
  9. } from 'sentry/components/events/interfaces/spans/types';
  10. import Link from 'sentry/components/links/link';
  11. import {normalizeDateTimeString} from 'sentry/components/organizations/pageFilters/parse';
  12. import Pill from 'sentry/components/pill';
  13. import Pills from 'sentry/components/pills';
  14. import TextOverflow from 'sentry/components/textOverflow';
  15. import {t} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import {MRI, Organization} from 'sentry/types';
  18. import {
  19. formatMetricUsingUnit,
  20. getDdmUrl,
  21. getDefaultMetricOp,
  22. getReadableMetricType,
  23. MetricDisplayType,
  24. } from 'sentry/utils/metrics';
  25. import {hasDDMFeature} from 'sentry/utils/metrics/features';
  26. import {formatMRI, parseMRI} from 'sentry/utils/metrics/mri';
  27. import useOrganization from 'sentry/utils/useOrganization';
  28. function flattenMetricsSummary(
  29. metricsSummary: MetricsSummary
  30. ): {item: MetricsSummaryItem; key: string; mri: MRI}[] {
  31. return Object.entries(metricsSummary).flatMap(([mri, items]) =>
  32. items.map((item, index) => ({item, mri, key: `${mri}${index}`}))
  33. );
  34. }
  35. function tagToQuery(tagKey: string, tagValue: string) {
  36. return `${tagKey}:"${tagValue}"`;
  37. }
  38. const HALF_HOUR_IN_MS = 30 * 60 * 1000;
  39. export function CustomMetricsEventData({
  40. metricsSummary,
  41. startTimestamp,
  42. }: {
  43. metricsSummary: MetricsSummary;
  44. startTimestamp: number;
  45. }) {
  46. const organization = useOrganization();
  47. const metricsSummaryEntries = flattenMetricsSummary(metricsSummary);
  48. const widgetStart = new Date(startTimestamp * 1000 - HALF_HOUR_IN_MS);
  49. const widgetEnd = new Date(startTimestamp * 1000 + HALF_HOUR_IN_MS);
  50. if (!hasDDMFeature(organization) || metricsSummaryEntries.length === 0) {
  51. return null;
  52. }
  53. return (
  54. <EventDataSection type="custom-metrics" title={t('Emitted Metrics')}>
  55. {metricsSummaryEntries.map(({mri, item, key}) => {
  56. return (
  57. <Fragment key={key}>
  58. <KeyValueList
  59. shouldSort={false}
  60. data={[
  61. {
  62. key: 'name',
  63. subject: t('Name'),
  64. value: <TextOverflow>{formatMRI(mri)}</TextOverflow>,
  65. actionButton: (
  66. <LinkButton
  67. size="xs"
  68. to={getDdmUrl(organization.slug, {
  69. start: normalizeDateTimeString(widgetStart),
  70. end: normalizeDateTimeString(widgetEnd),
  71. widgets: [
  72. {
  73. mri,
  74. displayType: MetricDisplayType.LINE,
  75. op: getDefaultMetricOp(mri),
  76. query: Object.entries(item.tags ?? {})
  77. .map(([tagKey, tagValue]) => tagToQuery(tagKey, tagValue))
  78. .join(' '),
  79. },
  80. ],
  81. })}
  82. >
  83. {t('Open in Metrics')}
  84. </LinkButton>
  85. ),
  86. },
  87. {
  88. key: 'stats',
  89. subject: t('Stats'),
  90. value: <MetricStats item={item} mri={mri} />,
  91. },
  92. item.tags && Object.keys(item.tags).length > 0
  93. ? {
  94. key: 'tags',
  95. subject: t('Tags'),
  96. value: (
  97. <Tags tags={item.tags} organization={organization} mri={mri} />
  98. ),
  99. }
  100. : null,
  101. ].filter((row): row is Exclude<typeof row, null> => Boolean(row))}
  102. />
  103. </Fragment>
  104. );
  105. })}
  106. </EventDataSection>
  107. );
  108. }
  109. function MetricStats({mri, item}: {item: MetricsSummaryItem; mri: MRI}) {
  110. const parsedMRI = parseMRI(mri);
  111. const unit = parsedMRI?.unit ?? 'none';
  112. const type = parsedMRI?.type ?? 'c';
  113. const typeLine = t(`Type: %s`, getReadableMetricType(type));
  114. // We use formatMetricUsingUnit with unit 'none' to ensure uniform number formatting
  115. const countLine = t(`Count: %s`, formatMetricUsingUnit(item.count, 'none'));
  116. // For counters the other stats offer little value, so we only show type and count
  117. if (type === 'c' || !item.count) {
  118. return (
  119. <pre>
  120. {typeLine}
  121. <br />
  122. {countLine}
  123. </pre>
  124. );
  125. }
  126. // If there is only one value, min, max, avg and sum are all the same
  127. if (item.count <= 1) {
  128. return (
  129. <pre>
  130. {typeLine}
  131. <br />
  132. {t('Value: %s', formatMetricUsingUnit(item.sum, unit))}
  133. </pre>
  134. );
  135. }
  136. return (
  137. <pre>
  138. {typeLine}
  139. <br />
  140. {countLine}
  141. <br />
  142. {t('Sum: %s', formatMetricUsingUnit(item.sum, unit))}
  143. <br />
  144. {t('Min: %s', formatMetricUsingUnit(item.min, unit))}
  145. <br />
  146. {t('Max: %s', formatMetricUsingUnit(item.max, unit))}
  147. <br />
  148. {t(
  149. 'Avg: %s',
  150. formatMetricUsingUnit(item.sum && item.count && item.sum / item.count, unit)
  151. )}
  152. </pre>
  153. );
  154. }
  155. function Tags({
  156. tags,
  157. organization,
  158. mri,
  159. }: {
  160. mri: MRI;
  161. organization: Organization;
  162. tags: Record<string, string>;
  163. }) {
  164. const [showingAll, setShowingAll] = useState(false);
  165. const renderedTags = Object.entries(tags).slice(0, showingAll ? undefined : 5);
  166. const renderText = showingAll ? t('Show less') : t('Show more') + '...';
  167. return (
  168. <StyledPills>
  169. {renderedTags.map(([tagKey, tagValue]) => (
  170. <StyledPill key={tagKey} name={tagKey}>
  171. <Link
  172. to={getDdmUrl(organization.slug, {
  173. widgets: [
  174. {
  175. mri,
  176. displayType: MetricDisplayType.LINE,
  177. op: getDefaultMetricOp(mri),
  178. query: tagToQuery(tagKey, tagValue),
  179. },
  180. ],
  181. })}
  182. >
  183. {tagValue}
  184. </Link>
  185. </StyledPill>
  186. ))}
  187. {Object.entries(tags).length > 5 && (
  188. <ShowMore onClick={() => setShowingAll(prev => !prev)}>{renderText}</ShowMore>
  189. )}
  190. </StyledPills>
  191. );
  192. }
  193. const StyledPills = styled(Pills)`
  194. padding-top: ${space(1)};
  195. `;
  196. const StyledPill = styled(Pill)`
  197. width: min-content;
  198. `;
  199. const ShowMore = styled('a')`
  200. white-space: nowrap;
  201. align-self: center;
  202. margin-bottom: ${space(1)};
  203. padding: ${space(0.5)} ${space(0.5)};
  204. `;