customMetricsEventData.tsx 6.7 KB

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