import {Component, Fragment} from 'react'; import {InjectedRouter} from 'react-router'; import {withTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {Location} from 'history'; import {Client} from 'app/api'; import GuideAnchor from 'app/components/assistant/guideAnchor'; import EventsChart from 'app/components/charts/eventsChart'; import {ChartContainer, HeaderTitleLegend} from 'app/components/charts/styles'; import {Panel} from 'app/components/panels'; import QuestionTooltip from 'app/components/questionTooltip'; import {PlatformKey} from 'app/data/platformCategories'; import {t} from 'app/locale'; import {GlobalSelection, Organization, ReleaseMeta} from 'app/types'; import {Series} from 'app/types/echarts'; import {trackAnalyticsEvent} from 'app/utils/analytics'; import {WebVital} from 'app/utils/discover/fields'; import {decodeScalar} from 'app/utils/queryString'; import {Theme} from 'app/utils/theme'; import {getTermHelp, PERFORMANCE_TERM} from 'app/views/performance/data'; import ReleaseStatsRequest from '../releaseStatsRequest'; import HealthChartContainer from './healthChartContainer'; import ReleaseChartControls, { EventType, PERFORMANCE_AXIS, YAxis, } from './releaseChartControls'; import {getReleaseEventView} from './utils'; type Props = { releaseMeta: ReleaseMeta; selection: GlobalSelection; platform: PlatformKey; yAxis: YAxis; eventType: EventType; vitalType: WebVital; onYAxisChange: (yAxis: YAxis) => void; onEventTypeChange: (eventType: EventType) => void; onVitalTypeChange: (vitalType: WebVital) => void; router: InjectedRouter; organization: Organization; hasHealthData: boolean; location: Location; api: Client; version: string; hasDiscover: boolean; hasPerformance: boolean; theme: Theme; defaultStatsPeriod: string; projectSlug: string; }; class ReleaseChartContainer extends Component { componentDidMount() { const {organization, yAxis, platform} = this.props; trackAnalyticsEvent({ eventKey: `release_detail.display_chart`, eventName: `Release Detail: Display Chart`, organization_id: parseInt(organization.id, 10), display: yAxis, platform, }); } /** * This returns an array with 3 colors, one for each of * 1. This Release * 2. Other Releases * 3. Releases (the markers) */ getTransactionsChartColors(): [string, string, string] { const {yAxis, theme} = this.props; switch (yAxis) { case YAxis.FAILED_TRANSACTIONS: return [theme.red300, theme.red100, theme.purple300]; default: return [theme.purple300, theme.purple100, theme.purple300]; } } getChartTitle() { const {yAxis, organization} = this.props; switch (yAxis) { case YAxis.SESSIONS: return { title: t('Session Count'), help: t('The number of sessions in a given period.'), }; case YAxis.USERS: return { title: t('User Count'), help: t('The number of users in a given period.'), }; case YAxis.SESSION_DURATION: return {title: t('Session Duration')}; case YAxis.CRASH_FREE: return {title: t('Crash Free Rate')}; case YAxis.FAILED_TRANSACTIONS: return { title: t('Failure Count'), help: getTermHelp(organization, PERFORMANCE_TERM.FAILURE_RATE), }; case YAxis.COUNT_DURATION: return {title: t('Slow Duration Count')}; case YAxis.COUNT_VITAL: return {title: t('Slow Vital Count')}; case YAxis.EVENTS: default: return {title: t('Event Count')}; } } cloneSeriesAsZero(series: Series): Series { return { ...series, data: series.data.map(point => ({ ...point, value: 0, })), }; } /** * The top events endpoint used to generate these series is not guaranteed to return a series * for both the current release and the other releases. This happens when there is insufficient * data. In these cases, the endpoint will return a single zerofilled series for the current * release. * * This is problematic as we want to show both series even if one is empty. To deal with this, * we clone the non empty series (to preserve the timestamps) with value 0 (to represent the * lack of data). */ seriesTransformer = (series: Series[]): Series[] => { let current: Series | null = null; let others: Series | null = null; const allSeries: Series[] = []; series.forEach(s => { if (s.seriesName === 'current' || s.seriesName === t('This Release')) { current = s; } else if (s.seriesName === 'others' || s.seriesName === t('Other Releases')) { others = s; } else { allSeries.push(s); } }); if (current !== null && others === null) { others = this.cloneSeriesAsZero(current); } else if (current === null && others !== null) { current = this.cloneSeriesAsZero(others); } if (others !== null) { others.seriesName = t('Other Releases'); allSeries.unshift(others); } if (current !== null) { current.seriesName = t('This Release'); allSeries.unshift(current); } return allSeries; }; renderStackedChart() { const { location, router, organization, api, releaseMeta, yAxis, eventType, vitalType, selection, version, } = this.props; const {projects, environments, datetime} = selection; const {start, end, period, utc} = datetime; const eventView = getReleaseEventView( selection, version, yAxis, eventType, vitalType, organization ); const apiPayload = eventView.getEventsAPIPayload(location); const colors = this.getTransactionsChartColors(); const {title, help} = this.getChartTitle(); const releaseQueryExtra = { showTransactions: location.query.showTransactions, eventType, vitalType, yAxis, }; return ( {title} {help && } } legendOptions={{right: 10, top: 0}} chartOptions={{grid: {left: '10px', right: '10px', top: '40px', bottom: '0px'}}} /> ); } renderHealthChart( loading: boolean, reloading: boolean, errored: boolean, chartData: Series[] ) { const {selection, yAxis, router, platform} = this.props; const {title, help} = this.getChartTitle(); return ( ); } render() { const { yAxis, eventType, vitalType, hasDiscover, hasHealthData, hasPerformance, onYAxisChange, onEventTypeChange, onVitalTypeChange, organization, defaultStatsPeriod, api, version, selection, location, projectSlug, } = this.props; return ( {({loading, reloading, errored, chartData, chartSummary}) => ( {((hasDiscover || hasPerformance) && yAxis === YAxis.EVENTS) || (hasPerformance && PERFORMANCE_AXIS.includes(yAxis)) ? this.renderStackedChart() : this.renderHealthChart(loading, reloading, errored, chartData)} )} ); } } export default withTheme(ReleaseChartContainer); const AnchorWrapper = styled('div')` height: 0; width: 0; margin-left: 50%; `;