123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458 |
- import {ASAP} from 'downsample/methods/ASAP';
- import type {Location} from 'history';
- import moment from 'moment';
- import {getInterval} from 'sentry/components/charts/utils';
- import {wrapQueryInWildcards} from 'sentry/components/performance/searchBar';
- import {t} from 'sentry/locale';
- import type {Project} from 'sentry/types';
- import type {Series, SeriesDataUnit} from 'sentry/types/echarts';
- import type EventView from 'sentry/utils/discover/eventView';
- import type {AggregationKeyWithAlias, Field, Sort} from 'sentry/utils/discover/fields';
- import {generateFieldAsString} from 'sentry/utils/discover/fields';
- import {decodeScalar} from 'sentry/utils/queryString';
- import theme from 'sentry/utils/theme';
- import {MutableSearch} from 'sentry/utils/tokenizeSearch';
- import {platformToPerformanceType, ProjectPerformanceType} from '../utils';
- import type {
- NormalizedTrendsTransaction,
- TrendFunction,
- TrendParameter,
- TrendsTransaction,
- TrendView,
- } from './types';
- import {
- TrendChangeType,
- TrendFunctionField,
- TrendParameterColumn,
- TrendParameterLabel,
- } from './types';
- export const DEFAULT_TRENDS_STATS_PERIOD = '14d';
- export const DEFAULT_MAX_DURATION = '15min';
- export const TRENDS_FUNCTIONS: TrendFunction[] = [
- {
- label: 'p50',
- field: TrendFunctionField.P50,
- alias: 'percentile_range',
- legendLabel: 'p50',
- },
- {
- label: 'p75',
- field: TrendFunctionField.P75,
- alias: 'percentile_range',
- legendLabel: 'p75',
- },
- {
- label: 'p95',
- field: TrendFunctionField.P95,
- alias: 'percentile_range',
- legendLabel: 'p95',
- },
- {
- label: 'p99',
- field: TrendFunctionField.P99,
- alias: 'percentile_range',
- legendLabel: 'p99',
- },
- {
- label: 'average',
- field: TrendFunctionField.AVG,
- alias: 'avg_range',
- legendLabel: 'average',
- },
- ];
- export const TRENDS_PARAMETERS: TrendParameter[] = [
- {
- label: TrendParameterLabel.DURATION,
- column: TrendParameterColumn.DURATION,
- },
- {
- label: TrendParameterLabel.LCP,
- column: TrendParameterColumn.LCP,
- },
- {
- label: TrendParameterLabel.FCP,
- column: TrendParameterColumn.FCP,
- },
- {
- label: TrendParameterLabel.FID,
- column: TrendParameterColumn.FID,
- },
- {
- label: TrendParameterLabel.CLS,
- column: TrendParameterColumn.CLS,
- },
- {
- label: TrendParameterLabel.SPANS_HTTP,
- column: TrendParameterColumn.SPANS_HTTP,
- },
- {
- label: TrendParameterLabel.SPANS_DB,
- column: TrendParameterColumn.SPANS_DB,
- },
- {
- label: TrendParameterLabel.SPANS_BROWSER,
- column: TrendParameterColumn.SPANS_BROWSER,
- },
- {
- label: TrendParameterLabel.SPANS_RESOURCE,
- column: TrendParameterColumn.SPANS_RESOURCE,
- },
- ];
- export const trendToColor = {
- [TrendChangeType.IMPROVED]: {
- lighter: theme.green200,
- default: theme.green300,
- },
- [TrendChangeType.REGRESSION]: {
- lighter: theme.red200,
- default: theme.red300,
- },
- neutral: {
- lighter: theme.yellow200,
- default: theme.yellow300,
- },
- // TODO remove this once backend starts sending
- // TrendChangeType.IMPROVED as change type
- improvement: {
- lighter: theme.green200,
- default: theme.green300,
- },
- };
- export const trendSelectedQueryKeys = {
- [TrendChangeType.IMPROVED]: 'improvedSelected',
- [TrendChangeType.REGRESSION]: 'regressionSelected',
- };
- export const trendUnselectedSeries = {
- [TrendChangeType.IMPROVED]: 'improvedUnselectedSeries',
- [TrendChangeType.REGRESSION]: 'regressionUnselectedSeries',
- };
- export const trendCursorNames = {
- [TrendChangeType.IMPROVED]: 'improvedCursor',
- [TrendChangeType.REGRESSION]: 'regressionCursor',
- };
- const TOKEN_KEYS_SUPPORTED_IN_METRICS_TRENDS = ['transaction', 'tpm()'];
- export function resetCursors() {
- const cursors = {};
- Object.values(trendCursorNames).forEach(cursor => (cursors[cursor] = undefined)); // Resets both cursors
- return cursors;
- }
- export function getCurrentTrendFunction(
- location: Location,
- _trendFunctionField?: TrendFunctionField
- ): TrendFunction {
- const trendFunctionField =
- _trendFunctionField ?? decodeScalar(location?.query?.trendFunction);
- const trendFunction = TRENDS_FUNCTIONS.find(({field}) => field === trendFunctionField);
- return trendFunction || TRENDS_FUNCTIONS[0];
- }
- function getDefaultTrendParameter(
- projects: Project[],
- projectIds: Readonly<number[]>
- ): TrendParameter {
- const performanceType = platformToPerformanceType(projects, projectIds);
- const trendParameter = performanceTypeToTrendParameterLabel(performanceType);
- return trendParameter;
- }
- export function getCurrentTrendParameter(
- location: Location,
- projects: Project[],
- projectIds: Readonly<number[]>
- ): TrendParameter {
- const trendParameterLabel = decodeScalar(location?.query?.trendParameter);
- const trendParameter = TRENDS_PARAMETERS.find(
- ({label}) => label === trendParameterLabel
- );
- if (trendParameter) {
- return trendParameter;
- }
- const defaultTrendParameter = getDefaultTrendParameter(projects, projectIds);
- return defaultTrendParameter;
- }
- export function performanceTypeToTrendParameterLabel(
- performanceType: ProjectPerformanceType
- ): TrendParameter {
- switch (performanceType) {
- case ProjectPerformanceType.FRONTEND:
- return {
- label: TrendParameterLabel.LCP,
- column: TrendParameterColumn.LCP,
- };
- case ProjectPerformanceType.ANY:
- case ProjectPerformanceType.BACKEND:
- case ProjectPerformanceType.FRONTEND_OTHER:
- default:
- return {
- label: TrendParameterLabel.DURATION,
- column: TrendParameterColumn.DURATION,
- };
- }
- }
- export function generateTrendFunctionAsString(
- trendFunction: TrendFunctionField,
- trendParameter: string
- ): string {
- return generateFieldAsString({
- kind: 'function',
- function: [
- trendFunction as AggregationKeyWithAlias,
- trendParameter,
- undefined,
- undefined,
- ],
- });
- }
- export function transformDeltaSpread(from: number, to: number) {
- const fromSeconds = from / 1000;
- const toSeconds = to / 1000;
- const showDigits = from > 1000 || to > 1000 || from < 10 || to < 10; // Show digits consistently if either has them
- return {fromSeconds, toSeconds, showDigits};
- }
- export function getTrendProjectId(
- trend: NormalizedTrendsTransaction,
- projects?: Project[]
- ): string | undefined {
- if (!trend.project || !projects) {
- return undefined;
- }
- const transactionProject = projects.find(project => project.slug === trend.project);
- return transactionProject?.id;
- }
- export function modifyTrendView(
- trendView: TrendView,
- location: Location,
- trendsType: TrendChangeType,
- projects: Project[],
- canUseMetricsTrends: boolean = false
- ) {
- const trendFunction = getCurrentTrendFunction(location);
- const trendParameter = getCurrentTrendParameter(location, projects, trendView.project);
- const fields = ['transaction', 'project'].map(field => ({
- field,
- })) as Field[];
- const trendSort = {
- field: 'trend_percentage()',
- kind: 'asc',
- } as Sort;
- trendView.trendType = trendsType;
- if (trendsType === TrendChangeType.REGRESSION) {
- trendSort.kind = 'desc';
- }
- if (trendFunction && trendParameter) {
- trendView.trendFunction = generateTrendFunctionAsString(
- trendFunction.field,
- trendParameter.column
- );
- }
- if (!canUseMetricsTrends) {
- trendView.query = getLimitTransactionItems(trendView.query);
- } else {
- const query = new MutableSearch(trendView.query);
- if (query.freeText.length > 0) {
- const parsedFreeText = query.freeText.join(' ');
- // the query here is a user entered condition, no need to escape it
- query.setFilterValues('transaction', [wrapQueryInWildcards(parsedFreeText)], false);
- query.freeText = [];
- }
- query.tokens = query.tokens.filter(
- token => token.key && TOKEN_KEYS_SUPPORTED_IN_METRICS_TRENDS.includes(token.key)
- );
- trendView.query = query.formatString();
- }
- trendView.interval = getQueryInterval(location, trendView);
- trendView.sorts = [trendSort];
- trendView.fields = fields;
- }
- export function modifyTrendsViewDefaultPeriod(eventView: EventView, location: Location) {
- const {query} = location;
- const hasStartAndEnd = query.start && query.end;
- if (!query.statsPeriod && !hasStartAndEnd) {
- eventView.statsPeriod = DEFAULT_TRENDS_STATS_PERIOD;
- }
- return eventView;
- }
- function getQueryInterval(location: Location, eventView: TrendView) {
- const intervalFromQueryParam = decodeScalar(location?.query?.interval);
- const {start, end, statsPeriod} = eventView;
- const datetimeSelection = {
- start: start || null,
- end: end || null,
- period: statsPeriod,
- };
- const intervalFromSmoothing = getInterval(datetimeSelection, 'medium');
- return intervalFromQueryParam || intervalFromSmoothing;
- }
- export function transformValueDelta(value: number, trendType: TrendChangeType) {
- const absoluteValue = Math.abs(value);
- const changeLabel =
- trendType === TrendChangeType.REGRESSION ? t('slower') : t('faster');
- const seconds = absoluteValue / 1000;
- const fixedDigits = absoluteValue > 1000 || absoluteValue < 10 ? 1 : 0;
- return {seconds, fixedDigits, changeLabel};
- }
- /**
- * This will normalize the trends transactions while the current trend function and current data are out of sync
- * To minimize extra renders with missing results.
- */
- export function normalizeTrends(
- data: Array<TrendsTransaction>
- ): Array<NormalizedTrendsTransaction> {
- const received_at = moment(); // Adding the received time for the transaction so calls to get baseline always line up with the transaction
- return data.map(row => {
- return {
- ...row,
- received_at,
- transaction: row.transaction,
- } as NormalizedTrendsTransaction;
- });
- }
- export function getSelectedQueryKey(trendChangeType: TrendChangeType) {
- return trendSelectedQueryKeys[trendChangeType];
- }
- export function getUnselectedSeries(trendChangeType: TrendChangeType) {
- return trendUnselectedSeries[trendChangeType];
- }
- export function movingAverage(data, index, size) {
- return (
- data
- .slice(index - size, index)
- .map(a => a.value)
- .reduce((a, b) => a + b, 0) / size
- );
- }
- /**
- * This function applies defaults for trend and count percentage, and adds the confidence limit to the query
- */
- function getLimitTransactionItems(query: string) {
- const limitQuery = new MutableSearch(query);
- if (!limitQuery.hasFilter('count_percentage()')) {
- limitQuery.addFilterValues('count_percentage()', ['>0.25', '<4']);
- }
- if (!limitQuery.hasFilter('trend_percentage()')) {
- limitQuery.addFilterValues('trend_percentage()', ['>0%']);
- }
- if (!limitQuery.hasFilter('confidence()')) {
- limitQuery.addFilterValues('confidence()', ['>6']);
- }
- return limitQuery.formatString();
- }
- export const smoothTrend = (data: [number, number][], resolution = 100) => {
- return ASAP(data, resolution);
- };
- export const replaceSeriesName = (seriesName: string) => {
- return ['p50', 'p75'].find(aggregate => seriesName.includes(aggregate));
- };
- export const replaceSmoothedSeriesName = (seriesName: string) => {
- return `Smoothed ${['p50', 'p75'].find(aggregate => seriesName.includes(aggregate))}`;
- };
- export function transformEventStatsSmoothed(data?: Series[], seriesName?: string) {
- let minValue = Number.MAX_SAFE_INTEGER;
- let maxValue = 0;
- if (!data) {
- return {
- maxValue,
- minValue,
- smoothedResults: undefined,
- };
- }
- const smoothedResults: Series[] = [];
- for (const current of data) {
- const currentData = current.data;
- const resultData: SeriesDataUnit[] = [];
- const smoothed = smoothTrend(
- currentData.map(({name, value}) => [Number(name), value])
- );
- for (let i = 0; i < smoothed.length; i++) {
- const point = smoothed[i] as any;
- const value = point.y;
- resultData.push({
- name: point.x,
- value,
- });
- if (!isNaN(value)) {
- const rounded = Math.round(value);
- minValue = Math.min(rounded, minValue);
- maxValue = Math.max(rounded, maxValue);
- }
- }
- smoothedResults.push({
- seriesName: seriesName || current.seriesName || 'Current',
- data: resultData,
- lineStyle: current.lineStyle,
- color: current.color,
- });
- }
- return {
- minValue,
- maxValue,
- smoothedResults,
- };
- }
- export function modifyTransactionNameTrendsQuery(trendView: TrendView) {
- const query = new MutableSearch(trendView.query);
- query.setFilterValues('tpm()', ['>0.1']);
- trendView.query = query.formatString();
- }
- export function getTopTrendingEvents(location: Location) {
- return decodeScalar(location?.query?.topEvents);
- }
|