Files
2024-02-15 17:01:50 +00:00

683 lines
18 KiB
TypeScript

import {Fragment} from 'react';
import moment from 'moment';
import {initializeData} from 'sentry-test/performance/initializePerformanceData';
import {act, render, screen, waitFor} from 'sentry-test/reactTestingLibrary';
import ProjectsStore from 'sentry/stores/projectsStore';
import type {SuspectSpans} from 'sentry/utils/performance/suspectSpans/types';
import type {EventsResultsDataRow} from 'sentry/utils/profiling/hooks/types';
import {PerformanceChangeExplorer} from 'sentry/views/performance/trends/changeExplorer';
import type {FunctionsField} from 'sentry/views/performance/trends/changeExplorerUtils/functionsList';
import {FunctionsList} from 'sentry/views/performance/trends/changeExplorerUtils/functionsList';
import {
COLUMNS,
MetricsTable,
renderBodyCell,
} from 'sentry/views/performance/trends/changeExplorerUtils/metricsTable';
import {SpansList} from 'sentry/views/performance/trends/changeExplorerUtils/spansList';
import type {NormalizedTrendsTransaction} from 'sentry/views/performance/trends/types';
import {TrendChangeType, TrendFunctionField} from 'sentry/views/performance/trends/types';
import {TRENDS_PARAMETERS} from 'sentry/views/performance/trends/utils';
async function waitForMockCall(mock: jest.Mock) {
await waitFor(() => {
expect(mock).toHaveBeenCalled();
});
}
const transaction: NormalizedTrendsTransaction = {
aggregate_range_1: 78.2757131147541,
aggregate_range_2: 110.50465131578949,
breakpoint: 1687262400,
project: 'sentry',
transaction: 'sentry.tasks.store.save_event',
trend_difference: 32.22893820103539,
trend_percentage: 1.411736117354651,
count: 3459,
received_at: moment(1601251200000),
};
const spanResults: SuspectSpans = [
{
op: 'db',
group: '1',
description: 'span1',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '2',
description: 'span2',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '3',
description: 'span3',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '4',
description: 'span4',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '5',
description: 'span5',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '6',
description: 'span6',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '7',
description: 'span7',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '8',
description: 'span8',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '9',
description: 'span9',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '10',
description: 'span10',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
{
op: 'db',
group: '11',
description: 'span11',
frequency: 4,
count: 4,
avgOccurrences: undefined,
sumExclusiveTime: 345345,
p50ExclusiveTime: undefined,
p75ExclusiveTime: 25,
p95ExclusiveTime: undefined,
p99ExclusiveTime: undefined,
examples: [],
},
];
const functionResults: EventsResultsDataRow<FunctionsField>[] = [
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f1',
'p75()': 4239847,
package: 'p1',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f2',
'p75()': 4239847,
package: 'p2',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f3',
'p75()': 4239847,
package: 'p3',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f4',
'p75()': 4239847,
package: 'p4',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f5',
'p75()': 4239847,
package: 'p5',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f5',
'p75()': 4239847,
package: 'p5',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f7',
'p75()': 4239847,
package: 'p7',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f8',
'p75()': 4239847,
package: 'p8',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f9',
'p75()': 4239847,
package: 'p9',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f10',
'p75()': 4239847,
package: 'p10',
'sum()': 2304823908,
},
{
'count()': 234,
'examples()': ['serw8r9s', 'aeo4i2u38'],
function: 'f11',
'p75()': 4239847,
package: 'p11',
'sum()': 2304823908,
},
];
describe('Performance > Trends > Performance Change Explorer', function () {
let eventsMockBefore;
let spansMock;
beforeEach(function () {
eventsMockBefore = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
data: [
{
'p95()': 1010.9232499999998,
'p50()': 47.34580982348902,
'tps()': 3.7226926286168966,
'count()': 345,
'failure_rate()': 0.23498234,
'examples()': ['dkwj4w8sdjk', 'asdi389a8'],
},
],
meta: {
fields: {
'p95()': 'duration',
'950()': 'duration',
'tps()': 'number',
'count()': 'number',
'failure_rate()': 'number',
'examples()': 'Array',
},
units: {
'p95()': 'millisecond',
'p50()': 'millisecond',
'tps()': null,
'count()': null,
'failure_rate()': null,
'examples()': null,
},
isMetricsData: true,
tips: {},
dataset: 'metrics',
},
},
});
spansMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-spans-performance/',
body: [],
});
});
afterEach(function () {
MockApiClient.clearMockResponses();
act(() => ProjectsStore.reset());
});
it('renders basic UI elements', async function () {
const data = initializeData();
const statsData = {
['/organizations/:orgId/performance/']: {
data: [],
order: 0,
},
};
render(
<PerformanceChangeExplorer
collapsed={false}
transaction={transaction}
onClose={() => {}}
trendChangeType={TrendChangeType.REGRESSION}
trendFunction={TrendFunctionField.P50}
trendParameter={TRENDS_PARAMETERS[0]}
trendView={data.eventView}
statsData={statsData}
isLoading={false}
organization={data.organization}
projects={data.projects}
location={data.location}
/>,
{
context: data.routerContext,
organization: data.organization,
}
);
await waitForMockCall(eventsMockBefore);
await waitForMockCall(spansMock);
await waitFor(() => {
expect(screen.getByTestId('pce-header')).toBeInTheDocument();
expect(screen.getByTestId('pce-graph')).toBeInTheDocument();
expect(screen.getByTestId('grid-editable')).toBeInTheDocument();
expect(screen.getAllByTestId('pce-metrics-chart-row-metric')).toHaveLength(4);
expect(screen.getAllByTestId('pce-metrics-chart-row-before')).toHaveLength(4);
expect(screen.getAllByTestId('pce-metrics-chart-row-after')).toHaveLength(4);
expect(screen.getAllByTestId('pce-metrics-chart-row-change')).toHaveLength(4);
expect(screen.getByTestId('list-item')).toBeInTheDocument();
});
});
it('shows correct change notation for no change', async () => {
const data = initializeData();
render(
<MetricsTable
isLoading={false}
location={data.location}
trendFunction={TrendFunctionField.P50}
transaction={transaction}
trendView={data.eventView}
organization={data.organization}
/>
);
await waitForMockCall(eventsMockBefore);
await waitFor(() => {
expect(screen.getAllByText('3.7 ps')).toHaveLength(2);
expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
});
});
it('shows correct change notation for positive change', async () => {
const data = initializeData();
render(
<MetricsTable
isLoading={false}
location={data.location}
trendFunction={TrendFunctionField.P50}
transaction={transaction}
trendView={data.eventView}
organization={data.organization}
/>
);
await waitForMockCall(eventsMockBefore);
await waitFor(() => {
expect(screen.getAllByTestId('pce-metrics-text-before')[1]).toHaveTextContent(
'78.3 ms'
);
expect(screen.getAllByTestId('pce-metrics-text-after')[1]).toHaveTextContent(
'110.5 ms'
);
expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent(
'+41.2%'
);
});
});
it('shows correct change notation for negative change', async () => {
const data = initializeData();
const negativeTransaction = {
...transaction,
aggregate_range_1: 110.50465131578949,
aggregate_range_2: 78.2757131147541,
trend_percentage: 0.588263882645349,
};
render(
<MetricsTable
isLoading={false}
location={data.location}
trendFunction={TrendFunctionField.P50}
transaction={negativeTransaction}
trendView={data.eventView}
organization={data.organization}
/>
);
await waitForMockCall(eventsMockBefore);
await waitFor(() => {
expect(screen.getAllByTestId('pce-metrics-text-after')[1]).toHaveTextContent(
'78.3 ms'
);
expect(screen.getAllByTestId('pce-metrics-text-before')[1]).toHaveTextContent(
'110.5 ms'
);
expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent(
'-41.2%'
);
});
});
it('shows correct change notation for no results', async () => {
const data = initializeData();
const nullEventsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
data: [
{
'p95()': 1010.9232499999998,
'p50()': 47.34580982348902,
'count()': 345,
},
],
meta: {
fields: {
'p95()': 'duration',
'950()': 'duration',
'count()': 'number',
},
units: {
'p95()': 'millisecond',
'p50()': 'millisecond',
'count()': null,
},
isMetricsData: true,
tips: {},
dataset: 'metrics',
},
},
});
render(
<MetricsTable
isLoading={false}
location={data.location}
trendFunction={TrendFunctionField.P50}
transaction={transaction}
trendView={data.eventView}
organization={data.organization}
/>
);
await waitForMockCall(nullEventsMock);
await waitFor(() => {
expect(screen.getAllByTestId('pce-metrics-text-after')[0]).toHaveTextContent('-');
expect(screen.getAllByTestId('pce-metrics-text-before')[0]).toHaveTextContent('-');
expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
});
});
it('returns correct null formatting for change column', () => {
render(
<Fragment>
{renderBodyCell(COLUMNS.change, {
metric: null,
before: null,
after: null,
change: '0%',
})}
{renderBodyCell(COLUMNS.change, {
metric: null,
before: null,
after: null,
change: '+NaN%',
})}
{renderBodyCell(COLUMNS.change, {
metric: null,
before: null,
after: null,
change: '-',
})}
</Fragment>
);
expect(screen.getAllByTestId('pce-metrics-text-change')[0]).toHaveTextContent('-');
expect(screen.getAllByTestId('pce-metrics-text-change')[1]).toHaveTextContent('-');
expect(screen.getAllByTestId('pce-metrics-text-change')[2]).toHaveTextContent('-');
});
it('returns correct positive formatting for change column', () => {
render(
renderBodyCell(COLUMNS.change, {
metric: null,
before: null,
after: null,
change: '40.3%',
})
);
expect(screen.getByText('+40.3%')).toBeInTheDocument();
});
it('renders spans list with no results', async () => {
const data = initializeData();
const emptyEventsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {},
});
render(
<div>
<SpansList
location={data.location}
organization={data.organization}
trendView={data.eventView}
breakpoint={transaction.breakpoint!}
transaction={transaction}
trendChangeType={TrendChangeType.REGRESSION}
/>
<FunctionsList
location={data.location}
organization={data.organization}
trendView={data.eventView}
breakpoint={transaction.breakpoint!}
transaction={transaction}
trendChangeType={TrendChangeType.REGRESSION}
/>
</div>
);
await waitForMockCall(spansMock);
await waitForMockCall(emptyEventsMock);
await waitFor(() => {
expect(screen.getAllByTestId('empty-state')).toHaveLength(2);
expect(screen.getByTestId('spans-no-results')).toBeInTheDocument();
expect(screen.getByTestId('functions-no-results')).toBeInTheDocument();
});
});
it('renders spans list with error message', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-spans-performance/',
statusCode: 504,
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
statusCode: 504,
});
const data = initializeData();
render(
<div>
<SpansList
location={data.location}
organization={data.organization}
trendView={data.eventView}
breakpoint={transaction.breakpoint!}
transaction={transaction}
trendChangeType={TrendChangeType.REGRESSION}
/>
<FunctionsList
location={data.location}
organization={data.organization}
trendView={data.eventView}
breakpoint={transaction.breakpoint!}
transaction={transaction}
trendChangeType={TrendChangeType.REGRESSION}
/>
</div>
);
await waitFor(() => {
expect(screen.getByTestId('error-indicator-spans')).toBeInTheDocument();
expect(screen.getByTestId('error-indicator-functions')).toBeInTheDocument();
});
});
it('renders spans list with no changes message', async () => {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-spans-performance/',
body: spanResults,
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
data: functionResults,
meta: {},
},
});
const data = initializeData();
render(
<div>
<SpansList
location={data.location}
organization={data.organization}
trendView={data.eventView}
breakpoint={transaction.breakpoint!}
transaction={transaction}
trendChangeType={TrendChangeType.REGRESSION}
/>
<FunctionsList
location={data.location}
organization={data.organization}
trendView={data.eventView}
breakpoint={transaction.breakpoint!}
transaction={transaction}
trendChangeType={TrendChangeType.REGRESSION}
/>
</div>
);
await waitFor(() => {
expect(screen.getAllByTestId('empty-state')).toHaveLength(2);
expect(screen.getByTestId('spans-no-changes')).toBeInTheDocument();
expect(screen.getByTestId('functions-no-changes')).toBeInTheDocument();
});
});
});