releaseChart.tsx 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. import {useTheme} from '@emotion/react';
  2. import styled from '@emotion/styled';
  3. import type {BarChartSeries} from 'sentry/components/charts/barChart';
  4. import MiniBarChart from 'sentry/components/charts/miniBarChart';
  5. import Count from 'sentry/components/count';
  6. import * as SidebarSection from 'sentry/components/sidebarSection';
  7. import {t} from 'sentry/locale';
  8. import type {Group, Release, TimeseriesValue} from 'sentry/types';
  9. import {getFormattedDate} from 'sentry/utils/dates';
  10. import {formatVersion} from 'sentry/utils/formatters';
  11. import type {Theme} from 'sentry/utils/theme';
  12. /**
  13. * Stats are provided indexed by statsPeriod strings.
  14. */
  15. type StatsGroup = Record<string, TimeseriesValue[]>;
  16. interface Props {
  17. group: Group;
  18. statsPeriod: string;
  19. title: string;
  20. className?: string;
  21. environment?: string;
  22. environmentLabel?: string;
  23. environmentStats?: StatsGroup;
  24. firstSeen?: string;
  25. lastSeen?: string;
  26. release?: Release;
  27. releaseStats?: StatsGroup;
  28. }
  29. type Marker = {
  30. color: string;
  31. displayValue: string | number | Date;
  32. name: string;
  33. value: string | number | Date;
  34. };
  35. export function getGroupReleaseChartMarkers(
  36. theme: Theme,
  37. stats: TimeseriesValue[],
  38. firstSeen?: string,
  39. lastSeen?: string
  40. ): BarChartSeries['markPoint'] {
  41. const markers: Marker[] = [];
  42. // Get the timestamp of the first point.
  43. const firstGraphTime = stats[0][0] * 1000;
  44. const firstSeenX = new Date(firstSeen ?? 0).getTime();
  45. const lastSeenX = new Date(lastSeen ?? 0).getTime();
  46. const difference = lastSeenX - firstSeenX;
  47. const oneHourMs = 1000 * 60 * 60;
  48. if (
  49. firstSeen &&
  50. stats.length > 2 &&
  51. firstSeenX >= firstGraphTime &&
  52. // Don't show first seen if the markers are too close together
  53. difference > oneHourMs
  54. ) {
  55. // Find the first bucket that would contain our first seen event
  56. const firstBucket = stats.findIndex(([time]) => time * 1000 > firstSeenX);
  57. let bucketStart: number | undefined;
  58. if (firstBucket > 0) {
  59. // The size of the data interval in ms
  60. const halfBucketSize = ((stats[1][0] - stats[0][0]) * 1000) / 2;
  61. // Display the marker in front of the first bucket
  62. bucketStart = stats[firstBucket - 1][0] * 1000 - halfBucketSize;
  63. }
  64. markers.push({
  65. name: t('First seen'),
  66. value: bucketStart ?? firstSeenX,
  67. displayValue: firstSeenX,
  68. color: theme.pink300,
  69. });
  70. }
  71. if (lastSeen && lastSeenX >= firstGraphTime) {
  72. markers.push({
  73. name: t('Last seen'),
  74. value: lastSeenX,
  75. displayValue: lastSeenX,
  76. color: theme.green300,
  77. });
  78. }
  79. const markerTooltip = {
  80. show: true,
  81. trigger: 'item',
  82. formatter: ({data}) => {
  83. const time = getFormattedDate(data.displayValue, 'MMM D, YYYY LT', {
  84. local: true,
  85. });
  86. return [
  87. '<div class="tooltip-series">',
  88. `<div><span class="tooltip-label"><strong>${data.name}</strong></span></div>`,
  89. '</div>',
  90. `<div class="tooltip-date">${time}</div>`,
  91. '</div>',
  92. '<div class="tooltip-arrow"></div>',
  93. ].join('');
  94. },
  95. };
  96. return {
  97. data: markers.map(marker => ({
  98. name: marker.name,
  99. coord: [marker.value, 0],
  100. tooltip: markerTooltip,
  101. displayValue: marker.displayValue,
  102. symbol: 'circle',
  103. symbolSize: 8,
  104. itemStyle: {
  105. color: marker.color,
  106. borderColor: theme.background,
  107. },
  108. })),
  109. };
  110. }
  111. function GroupReleaseChart(props: Props) {
  112. const {
  113. group,
  114. lastSeen,
  115. firstSeen,
  116. statsPeriod,
  117. release,
  118. releaseStats,
  119. environment,
  120. environmentLabel,
  121. environmentStats,
  122. title,
  123. } = props;
  124. const theme = useTheme();
  125. const stats = group.stats[statsPeriod];
  126. const environmentPeriodStats = environmentStats?.[statsPeriod];
  127. if (!stats || !stats.length || !environmentPeriodStats) {
  128. return null;
  129. }
  130. const series: BarChartSeries[] = [];
  131. if (environment) {
  132. // Add all events.
  133. series.push({
  134. seriesName: t('Events'),
  135. data: stats.map(point => ({name: point[0] * 1000, value: point[1]})),
  136. });
  137. }
  138. series.push({
  139. seriesName: t('Events in %s', environmentLabel),
  140. data: environmentStats[statsPeriod].map(point => ({
  141. name: point[0] * 1000,
  142. value: point[1],
  143. })),
  144. });
  145. if (release && releaseStats) {
  146. series.push({
  147. seriesName: t('Events in release %s', formatVersion(release.version)),
  148. data: releaseStats[statsPeriod].map(point => ({
  149. name: point[0] * 1000,
  150. value: point[1],
  151. })),
  152. });
  153. }
  154. const totalSeries =
  155. environment && environmentStats ? environmentStats[statsPeriod] : stats;
  156. const totalEvents = totalSeries.reduce((acc, current) => acc + current[1], 0);
  157. series[0].markPoint = getGroupReleaseChartMarkers(theme, stats, firstSeen, lastSeen);
  158. return (
  159. <SidebarSection.Wrap>
  160. <SidebarSection.Title>{title}</SidebarSection.Title>
  161. <SidebarSection.Content>
  162. <EventNumber>
  163. <Count value={totalEvents} />
  164. </EventNumber>
  165. <MiniBarChart
  166. isGroupedByDate
  167. showTimeInTooltip
  168. showMarkLineLabel
  169. height={42}
  170. colors={environment ? undefined : [theme.purple300, theme.purple300]}
  171. series={series}
  172. grid={{
  173. top: 6,
  174. bottom: 4,
  175. left: 4,
  176. right: 4,
  177. }}
  178. />
  179. </SidebarSection.Content>
  180. </SidebarSection.Wrap>
  181. );
  182. }
  183. const EventNumber = styled('div')`
  184. line-height: 1;
  185. font-size: ${p => p.theme.fontSizeExtraLarge};
  186. `;
  187. export default GroupReleaseChart;