123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- import {useTheme} from '@emotion/react';
- import styled from '@emotion/styled';
- import type {BarChartSeries} from 'sentry/components/charts/barChart';
- import MiniBarChart from 'sentry/components/charts/miniBarChart';
- import Count from 'sentry/components/count';
- import * as SidebarSection from 'sentry/components/sidebarSection';
- import {t} from 'sentry/locale';
- import type {Group, Release, TimeseriesValue} from 'sentry/types';
- import {getFormattedDate} from 'sentry/utils/dates';
- import {formatVersion} from 'sentry/utils/formatters';
- import type {Theme} from 'sentry/utils/theme';
- /**
- * Stats are provided indexed by statsPeriod strings.
- */
- type StatsGroup = Record<string, TimeseriesValue[]>;
- interface Props {
- group: Group;
- statsPeriod: string;
- title: string;
- className?: string;
- environment?: string;
- environmentLabel?: string;
- environmentStats?: StatsGroup;
- firstSeen?: string;
- lastSeen?: string;
- release?: Release;
- releaseStats?: StatsGroup;
- }
- type Marker = {
- color: string;
- displayValue: string | number | Date;
- name: string;
- value: string | number | Date;
- };
- export function getGroupReleaseChartMarkers(
- theme: Theme,
- stats: TimeseriesValue[],
- firstSeen?: string,
- lastSeen?: string
- ): BarChartSeries['markPoint'] {
- const markers: Marker[] = [];
- // Get the timestamp of the first point.
- const firstGraphTime = stats[0][0] * 1000;
- const firstSeenX = new Date(firstSeen ?? 0).getTime();
- const lastSeenX = new Date(lastSeen ?? 0).getTime();
- const difference = lastSeenX - firstSeenX;
- const oneHourMs = 1000 * 60 * 60;
- if (
- firstSeen &&
- stats.length > 2 &&
- firstSeenX >= firstGraphTime &&
- // Don't show first seen if the markers are too close together
- difference > oneHourMs
- ) {
- // Find the first bucket that would contain our first seen event
- const firstBucket = stats.findIndex(([time]) => time * 1000 > firstSeenX);
- let bucketStart: number | undefined;
- if (firstBucket > 0) {
- // The size of the data interval in ms
- const halfBucketSize = ((stats[1][0] - stats[0][0]) * 1000) / 2;
- // Display the marker in front of the first bucket
- bucketStart = stats[firstBucket - 1][0] * 1000 - halfBucketSize;
- }
- markers.push({
- name: t('First seen'),
- value: bucketStart ?? firstSeenX,
- displayValue: firstSeenX,
- color: theme.pink300,
- });
- }
- if (lastSeen && lastSeenX >= firstGraphTime) {
- markers.push({
- name: t('Last seen'),
- value: lastSeenX,
- displayValue: lastSeenX,
- color: theme.green300,
- });
- }
- const markerTooltip = {
- show: true,
- trigger: 'item',
- formatter: ({data}) => {
- const time = getFormattedDate(data.displayValue, 'MMM D, YYYY LT', {
- local: true,
- });
- return [
- '<div class="tooltip-series">',
- `<div><span class="tooltip-label"><strong>${data.name}</strong></span></div>`,
- '</div>',
- `<div class="tooltip-date">${time}</div>`,
- '</div>',
- '<div class="tooltip-arrow"></div>',
- ].join('');
- },
- };
- return {
- data: markers.map(marker => ({
- name: marker.name,
- coord: [marker.value, 0],
- tooltip: markerTooltip,
- displayValue: marker.displayValue,
- symbol: 'circle',
- symbolSize: 8,
- itemStyle: {
- color: marker.color,
- borderColor: theme.background,
- },
- })),
- };
- }
- function GroupReleaseChart(props: Props) {
- const {
- group,
- lastSeen,
- firstSeen,
- statsPeriod,
- release,
- releaseStats,
- environment,
- environmentLabel,
- environmentStats,
- title,
- } = props;
- const theme = useTheme();
- const stats = group.stats[statsPeriod];
- const environmentPeriodStats = environmentStats?.[statsPeriod];
- if (!stats || !stats.length || !environmentPeriodStats) {
- return null;
- }
- const series: BarChartSeries[] = [];
- if (environment) {
- // Add all events.
- series.push({
- seriesName: t('Events'),
- data: stats.map(point => ({name: point[0] * 1000, value: point[1]})),
- });
- }
- series.push({
- seriesName: t('Events in %s', environmentLabel),
- data: environmentStats[statsPeriod].map(point => ({
- name: point[0] * 1000,
- value: point[1],
- })),
- });
- if (release && releaseStats) {
- series.push({
- seriesName: t('Events in release %s', formatVersion(release.version)),
- data: releaseStats[statsPeriod].map(point => ({
- name: point[0] * 1000,
- value: point[1],
- })),
- });
- }
- const totalSeries =
- environment && environmentStats ? environmentStats[statsPeriod] : stats;
- const totalEvents = totalSeries.reduce((acc, current) => acc + current[1], 0);
- series[0].markPoint = getGroupReleaseChartMarkers(theme, stats, firstSeen, lastSeen);
- return (
- <SidebarSection.Wrap>
- <SidebarSection.Title>{title}</SidebarSection.Title>
- <SidebarSection.Content>
- <EventNumber>
- <Count value={totalEvents} />
- </EventNumber>
- <MiniBarChart
- isGroupedByDate
- showTimeInTooltip
- showMarkLineLabel
- height={42}
- colors={environment ? undefined : [theme.purple300, theme.purple300]}
- series={series}
- grid={{
- top: 6,
- bottom: 4,
- left: 4,
- right: 4,
- }}
- />
- </SidebarSection.Content>
- </SidebarSection.Wrap>
- );
- }
- const EventNumber = styled('div')`
- line-height: 1;
- font-size: ${p => p.theme.fontSizeExtraLarge};
- `;
- export default GroupReleaseChart;
|