123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- import {PureComponent} from 'react';
- import isEqual from 'lodash/isEqual';
- import omitBy from 'lodash/omitBy';
- import {doEventsRequest} from 'sentry/actionCreators/events';
- import {addErrorMessage} from 'sentry/actionCreators/indicator';
- import {Client} from 'sentry/api';
- import LoadingPanel from 'sentry/components/charts/loadingPanel';
- import {
- canIncludePreviousPeriod,
- getPreviousSeriesName,
- isMultiSeriesStats,
- } from 'sentry/components/charts/utils';
- import {t} from 'sentry/locale';
- import {
- DateString,
- EventsStats,
- EventsStatsData,
- MultiSeriesEventsStats,
- OrganizationSummary,
- } from 'sentry/types';
- import {Series, SeriesDataUnit} from 'sentry/types/echarts';
- import {defined} from 'sentry/utils';
- import {DURATION_UNITS, SIZE_UNITS} from 'sentry/utils/discover/fieldRenderers';
- import {
- AggregationOutputType,
- getAggregateAlias,
- stripEquationPrefix,
- } from 'sentry/utils/discover/fields';
- import {QueryBatching} from 'sentry/utils/performance/contexts/genericQueryBatcher';
- export type TimeSeriesData = {
- allTimeseriesData?: EventsStatsData;
- comparisonTimeseriesData?: Series[];
- originalPreviousTimeseriesData?: EventsStatsData | null;
- originalTimeseriesData?: EventsStatsData;
- previousTimeseriesData?: Series[] | null;
- timeAggregatedData?: Series | {};
- timeframe?: {end: number; start: number};
- // timeseries data
- timeseriesData?: Series[];
- timeseriesResultsTypes?: Record<string, AggregationOutputType>;
- timeseriesTotals?: {count: number};
- };
- type LoadingStatus = {
- /**
- * Whether there was an error retrieving data
- */
- errored: boolean;
- loading: boolean;
- reloading: boolean;
- errorMessage?: string;
- };
- // Can hold additional data from the root an events stat object (eg. start, end, order, isMetricsData).
- interface AdditionalSeriesInfo {
- isMetricsData?: boolean;
- }
- export type RenderProps = LoadingStatus &
- TimeSeriesData & {
- results?: Series[]; // Chart with multiple series.
- seriesAdditionalInfo?: Record<string, AdditionalSeriesInfo>;
- };
- type DefaultProps = {
- /**
- * Include data for previous period
- */
- includePrevious: boolean;
- /**
- * Transform the response data to be something ingestible by charts
- */
- includeTransformedData: boolean;
- /**
- * Interval to group results in
- *
- * e.g. 1d, 1h, 1m, 1s
- */
- interval: string;
- /**
- * number of rows to return
- */
- limit: number;
- /**
- * The query string to search events by
- */
- query: string;
- /**
- * Time delta for comparing intervals of alert metrics, in seconds
- */
- comparisonDelta?: number;
- /**
- * Absolute end date for query
- */
- end?: DateString;
- /**
- * Relative time period for query.
- *
- * Use `start` and `end` for absolute dates.
- *
- * e.g. 24h, 7d, 30d
- */
- period?: string | null;
- /**
- * Absolute start date for query
- */
- start?: DateString;
- };
- type EventsRequestPartialProps = {
- /**
- * API client instance
- */
- api: Client;
- children: (renderProps: RenderProps) => React.ReactNode;
- organization: OrganizationSummary;
- /**
- * Whether or not to include the last partial bucket. This happens for example when the
- * current time is 11:26 and the last bucket ranges from 11:25-11:30. This means that
- * the last bucket contains 1 minute worth of data while the rest contains 5 minutes.
- *
- * This flag indicates whether or not this last bucket should be included in the result.
- */
- partial: boolean;
- /**
- * Discover needs confirmation to run >30 day >10 project queries,
- * optional and when not passed confirmation is not required.
- */
- confirmedQuery?: boolean;
- /**
- * Name used for display current series dataset tooltip
- */
- currentSeriesNames?: string[];
- /**
- * Optional callback to further process raw events request response data
- */
- dataLoadedCallback?: (any: EventsStats | MultiSeriesEventsStats | null) => void;
- /**
- * List of environments to query
- */
- environment?: Readonly<string[]>;
- /**
- * Is query out of retention
- */
- expired?: boolean;
- /**
- * List of fields to group with when doing a topEvents request.
- */
- field?: string[];
- /**
- * Allows overriding the pathname.
- */
- generatePathname?: (org: OrganizationSummary) => string;
- /**
- * Hide error toast (used for pages which also query eventsV2). Stops error appearing as a toast.
- */
- hideError?: boolean;
- /**
- * Initial loading state
- */
- loading?: boolean;
- /**
- * Query name used for displaying error toast if it is out of retention
- */
- name?: string;
- /**
- * A way to control error if error handling is not owned by the toast.
- */
- onError?: (error: string) => void;
- /**
- * How to order results when getting top events.
- */
- orderby?: string;
- previousSeriesNames?: string[];
- /**
- * List of project ids to query
- */
- project?: Readonly<number[]>;
- /**
- * A container for query batching data and functions.
- */
- queryBatching?: QueryBatching;
- /**
- * Extra query parameters to be added.
- */
- queryExtras?: Record<string, string>;
- /**
- * A unique name for what's triggering this request, see organization_events_stats for an allowlist
- */
- referrer?: string;
- /**
- * Should loading be shown.
- */
- showLoading?: boolean;
- /**
- * List of team ids to query
- */
- team?: Readonly<string | string[]>;
- /**
- * The number of top results to get. When set a multi-series result will be returned
- * in the `results` child render function.
- */
- topEvents?: number;
- /**
- * Whether or not to zerofill results
- */
- withoutZerofill?: boolean;
- /**
- * The yAxis being plotted. If multiple yAxis are requested,
- * the child render function will be called with `results`
- */
- yAxis?: string | string[];
- };
- type TimeAggregationProps =
- | {includeTimeAggregation: true; timeAggregationSeriesName: string}
- | {includeTimeAggregation?: false; timeAggregationSeriesName?: undefined};
- export type EventsRequestProps = DefaultProps &
- TimeAggregationProps &
- EventsRequestPartialProps;
- type EventsRequestState = {
- errored: boolean;
- fetchedWithPrevious: boolean;
- reloading: boolean;
- timeseriesData: null | EventsStats | MultiSeriesEventsStats;
- errorMessage?: string;
- };
- const propNamesToIgnore = [
- 'api',
- 'children',
- 'organization',
- 'loading',
- 'queryBatching',
- 'generatePathname',
- ];
- const omitIgnoredProps = (props: EventsRequestProps) =>
- omitBy(props, (_value, key) => propNamesToIgnore.includes(key));
- class EventsRequest extends PureComponent<EventsRequestProps, EventsRequestState> {
- static defaultProps: DefaultProps = {
- period: undefined,
- start: null,
- end: null,
- interval: '1d',
- comparisonDelta: undefined,
- limit: 15,
- query: '',
- includePrevious: true,
- includeTransformedData: true,
- };
- state: EventsRequestState = {
- reloading: !!this.props.loading,
- errored: false,
- timeseriesData: null,
- fetchedWithPrevious: false,
- };
- componentDidMount() {
- this.fetchData();
- }
- componentDidUpdate(prevProps: EventsRequestProps) {
- if (isEqual(omitIgnoredProps(prevProps), omitIgnoredProps(this.props))) {
- return;
- }
- this.fetchData();
- }
- componentWillUnmount() {
- this.unmounting = true;
- }
- private unmounting: boolean = false;
- fetchData = async () => {
- const {api, confirmedQuery, onError, expired, name, hideError, ...props} = this.props;
- let timeseriesData: EventsStats | MultiSeriesEventsStats | null = null;
- if (confirmedQuery === false) {
- return;
- }
- this.setState(state => ({
- reloading: state.timeseriesData !== null,
- errored: false,
- errorMessage: undefined,
- }));
- let errorMessage;
- if (expired) {
- errorMessage = t(
- '%s has an invalid date range. Please try a more recent date range.',
- name
- );
- addErrorMessage(errorMessage, {append: true});
- this.setState({
- errored: true,
- errorMessage,
- });
- } else {
- try {
- api.clear();
- timeseriesData = await doEventsRequest(api, props);
- } catch (resp) {
- if (resp && resp.responseJSON && resp.responseJSON.detail) {
- errorMessage = resp.responseJSON.detail;
- } else {
- errorMessage = t('Error loading chart data');
- }
- if (!hideError) {
- addErrorMessage(errorMessage);
- }
- if (onError) {
- onError(errorMessage);
- }
- this.setState({
- errored: true,
- errorMessage,
- });
- }
- }
- if (this.unmounting) {
- return;
- }
- this.setState({
- reloading: false,
- timeseriesData,
- fetchedWithPrevious: props.includePrevious,
- });
- if (props.dataLoadedCallback) {
- props.dataLoadedCallback(timeseriesData);
- }
- };
- /**
- * Retrieves dataset for the current period (since data can potentially
- * contain previous period's data), as well as the previous period if
- * possible.
- *
- * Returns `null` if data does not exist
- */
- getData = (
- data: EventsStatsData = []
- ): {current: EventsStatsData; previous: EventsStatsData | null} => {
- const {fetchedWithPrevious} = this.state;
- const {period, includePrevious} = this.props;
- const hasPreviousPeriod =
- fetchedWithPrevious || canIncludePreviousPeriod(includePrevious, period);
- // Take the floor just in case, but data should always be divisible by 2
- const dataMiddleIndex = Math.floor(data.length / 2);
- return {
- current: hasPreviousPeriod ? data.slice(dataMiddleIndex) : data,
- previous: hasPreviousPeriod ? data.slice(0, dataMiddleIndex) : null,
- };
- };
- // This aggregates all values per `timestamp`
- calculateTotalsPerTimestamp(
- data: EventsStatsData,
- getName: (
- timestamp: number,
- countArray: {count: number}[],
- i: number
- ) => number = timestamp => timestamp * 1000
- ): SeriesDataUnit[] {
- return data.map(([timestamp, countArray], i) => ({
- name: getName(timestamp, countArray, i),
- value: countArray.reduce((acc, {count}) => acc + count, 0),
- }));
- }
- /**
- * Get previous period data, but transform timestamps so that data fits unto
- * the current period's data axis
- */
- transformPreviousPeriodData(
- current: EventsStatsData,
- previous: EventsStatsData | null,
- seriesName?: string
- ): Series | null {
- // Need the current period data array so we can take the timestamp
- // so we can be sure the data lines up
- if (!previous) {
- return null;
- }
- return {
- seriesName: seriesName ?? 'Previous',
- data: this.calculateTotalsPerTimestamp(
- previous,
- (_timestamp, _countArray, i) => current[i][0] * 1000
- ),
- stack: 'previous',
- };
- }
- /**
- * Aggregate all counts for each time stamp
- */
- transformAggregatedTimeseries(data: EventsStatsData, seriesName: string = ''): Series {
- return {
- seriesName,
- data: this.calculateTotalsPerTimestamp(data),
- };
- }
- /**
- * Transforms query response into timeseries data to be used in a chart
- */
- transformTimeseriesData(
- data: EventsStatsData,
- meta: EventsStats['meta'],
- seriesName?: string
- ): Series[] {
- let scale = 1;
- if (seriesName) {
- const unit = meta?.units?.[getAggregateAlias(seriesName)];
- // Scale series values to milliseconds or bytes depending on units from meta
- scale = (unit && (DURATION_UNITS[unit] ?? SIZE_UNITS[unit])) ?? 1;
- }
- return [
- {
- seriesName: seriesName || 'Current',
- data: data.map(([timestamp, countsForTimestamp]) => ({
- name: timestamp * 1000,
- value: countsForTimestamp.reduce((acc, {count}) => acc + count, 0) * scale,
- })),
- },
- ];
- }
- /**
- * Transforms comparisonCount in query response into timeseries data to be used in a comparison chart for change alerts
- */
- transformComparisonTimeseriesData(data: EventsStatsData): Series[] {
- return [
- {
- seriesName: 'comparisonCount()',
- data: data.map(([timestamp, countsForTimestamp]) => ({
- name: timestamp * 1000,
- value: countsForTimestamp.reduce(
- (acc, {comparisonCount}) => acc + (comparisonCount ?? 0),
- 0
- ),
- })),
- },
- ];
- }
- processData(response: EventsStats, seriesIndex: number = 0, seriesName?: string) {
- const {data, isMetricsData, totals, meta} = response;
- const {
- includeTransformedData,
- includeTimeAggregation,
- timeAggregationSeriesName,
- currentSeriesNames,
- previousSeriesNames,
- comparisonDelta,
- } = this.props;
- const {current, previous} = this.getData(data);
- const transformedData = includeTransformedData
- ? this.transformTimeseriesData(
- current,
- meta,
- seriesName ?? currentSeriesNames?.[seriesIndex]
- )
- : [];
- const transformedComparisonData =
- includeTransformedData && comparisonDelta
- ? this.transformComparisonTimeseriesData(current)
- : [];
- const previousData = includeTransformedData
- ? this.transformPreviousPeriodData(
- current,
- previous,
- (seriesName ? getPreviousSeriesName(seriesName) : undefined) ??
- previousSeriesNames?.[seriesIndex]
- )
- : null;
- const timeAggregatedData = includeTimeAggregation
- ? this.transformAggregatedTimeseries(current, timeAggregationSeriesName || '')
- : {};
- const timeframe =
- response.start && response.end
- ? !previous
- ? {
- start: response.start * 1000,
- end: response.end * 1000,
- }
- : {
- // Find the midpoint of start & end since previous includes 2x data
- start: (response.start + response.end) * 500,
- end: response.end * 1000,
- }
- : undefined;
- const processedData = {
- data: transformedData,
- comparisonData: transformedComparisonData,
- allData: data,
- originalData: current,
- totals,
- isMetricsData,
- originalPreviousData: previous,
- previousData,
- timeAggregatedData,
- timeframe,
- };
- return processedData;
- }
- render() {
- const {children, showLoading, ...props} = this.props;
- const {topEvents, yAxis} = this.props;
- const {timeseriesData, reloading, errored, errorMessage} = this.state;
- // Is "loading" if data is null
- const loading = this.props.loading || timeseriesData === null;
- if (showLoading && loading) {
- return <LoadingPanel data-test-id="events-request-loading" />;
- }
- if (isMultiSeriesStats(timeseriesData, defined(topEvents))) {
- // Convert multi-series results into chartable series. Multi series results
- // are created when multiple yAxis are used or a topEvents request is made.
- // Convert the timeseries data into a multi-series result set.
- // As the server will have replied with a map like:
- // {[titleString: string]: EventsStats}
- let timeframe: {end: number; start: number} | undefined = undefined;
- const seriesAdditionalInfo: Record<string, AdditionalSeriesInfo> = {};
- const sortedTimeseriesData = Object.keys(timeseriesData)
- .map(
- (
- seriesName: string,
- index: number
- ): [number, Series, Series | null, AdditionalSeriesInfo] => {
- const seriesData: EventsStats = timeseriesData[seriesName];
- const processedData = this.processData(
- seriesData,
- index,
- stripEquationPrefix(seriesName)
- );
- if (!timeframe) {
- timeframe = processedData.timeframe;
- }
- if (processedData.isMetricsData) {
- seriesAdditionalInfo[seriesName] = {
- isMetricsData: processedData.isMetricsData,
- };
- }
- return [
- seriesData.order || 0,
- processedData.data[0],
- processedData.previousData,
- {isMetricsData: processedData.isMetricsData},
- ];
- }
- )
- .sort((a, b) => a[0] - b[0]);
- const timeseriesResultsTypes: Record<string, AggregationOutputType> = {};
- Object.keys(timeseriesData).forEach(key => {
- const fieldsMeta = timeseriesData[key].meta?.fields[getAggregateAlias(key)];
- if (fieldsMeta) {
- timeseriesResultsTypes[key] = fieldsMeta;
- }
- });
- const results: Series[] = sortedTimeseriesData.map(item => {
- return item[1];
- });
- const previousTimeseriesData: Series[] | undefined = sortedTimeseriesData.some(
- item => item[2] === null
- )
- ? undefined
- : sortedTimeseriesData.map(item => {
- return item[2] as Series;
- });
- return children({
- loading,
- reloading,
- errored,
- errorMessage,
- results,
- timeframe,
- previousTimeseriesData,
- seriesAdditionalInfo,
- timeseriesResultsTypes,
- // sometimes we want to reference props that were given to EventsRequest
- ...props,
- });
- }
- if (timeseriesData) {
- const yAxisKey = yAxis && (typeof yAxis === 'string' ? yAxis : yAxis[0]);
- const yAxisFieldType =
- yAxisKey && timeseriesData.meta?.fields[getAggregateAlias(yAxisKey)];
- const timeseriesResultsTypes = yAxisFieldType
- ? {[yAxisKey]: yAxisFieldType}
- : undefined;
- const {
- data: transformedTimeseriesData,
- comparisonData: transformedComparisonTimeseriesData,
- allData: allTimeseriesData,
- originalData: originalTimeseriesData,
- totals: timeseriesTotals,
- originalPreviousData: originalPreviousTimeseriesData,
- previousData: previousTimeseriesData,
- timeAggregatedData,
- timeframe,
- isMetricsData,
- } = this.processData(timeseriesData);
- const seriesAdditionalInfo = {
- [this.props.currentSeriesNames?.[0] ?? 'current']: {isMetricsData},
- };
- return children({
- loading,
- reloading,
- errored,
- errorMessage,
- // meta data,
- seriesAdditionalInfo,
- // timeseries data
- timeseriesData: transformedTimeseriesData,
- comparisonTimeseriesData: transformedComparisonTimeseriesData,
- allTimeseriesData,
- originalTimeseriesData,
- timeseriesTotals,
- originalPreviousTimeseriesData,
- previousTimeseriesData: previousTimeseriesData
- ? [previousTimeseriesData]
- : previousTimeseriesData,
- timeAggregatedData,
- timeframe,
- timeseriesResultsTypes,
- // sometimes we want to reference props that were given to EventsRequest
- ...props,
- });
- }
- return children({
- loading,
- reloading,
- errored,
- errorMessage,
- ...props,
- });
- }
- }
- export default EventsRequest;
|