releaseChart.tsx 5.4 KB

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