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

1308 lines
38 KiB
TypeScript

import type {InjectedRouter} from 'react-router';
import {browserHistory} from 'react-router';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';
import {TeamFixture} from 'sentry-fixture/team';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {makeTestQueryClient} from 'sentry-test/queryClient';
import {
render,
renderGlobalModal,
screen,
userEvent,
waitFor,
} from 'sentry-test/reactTestingLibrary';
import OrganizationStore from 'sentry/stores/organizationStore';
import ProjectsStore from 'sentry/stores/projectsStore';
import TeamStore from 'sentry/stores/teamStore';
import type {Project} from 'sentry/types';
import {DiscoverDatasets} from 'sentry/utils/discover/types';
import {MetricsCardinalityProvider} from 'sentry/utils/performance/contexts/metricsCardinality';
import {
MEPSetting,
MEPState,
} from 'sentry/utils/performance/contexts/metricsEnhancedSetting';
import {QueryClientProvider} from 'sentry/utils/queryClient';
import TransactionSummary from 'sentry/views/performance/transactionSummary/transactionOverview';
import {RouteContext} from 'sentry/views/routeContext';
const teams = [
TeamFixture({id: '1', slug: 'team1', name: 'Team 1'}),
TeamFixture({id: '2', slug: 'team2', name: 'Team 2'}),
];
function initializeData({
features: additionalFeatures = [],
query = {},
project: prj,
projects,
}: {
features?: string[];
project?: Project;
projects?: Project[];
query?: Record<string, any>;
} = {}) {
const features = ['discover-basic', 'performance-view', ...additionalFeatures];
const project = prj ?? ProjectFixture({teams});
const organization = OrganizationFixture({
features,
projects: projects ? projects : [project],
});
const initialData = initializeOrg({
organization,
router: {
location: {
query: {
transaction: '/performance',
project: project.id,
transactionCursor: '1:0:0',
...query,
},
},
},
});
ProjectsStore.loadInitialData(initialData.organization.projects);
TeamStore.loadInitialData(teams, false, null);
return initialData;
}
function TestComponent({
router,
...props
}: React.ComponentProps<typeof TransactionSummary> & {
router: InjectedRouter<Record<string, string>, any>;
}) {
if (!props.organization) {
throw new Error('Missing organization');
}
return (
<QueryClientProvider client={makeTestQueryClient()}>
<RouteContext.Provider value={{router, ...router}}>
<MetricsCardinalityProvider
organization={props.organization}
location={props.location}
>
<TransactionSummary {...props} />
</MetricsCardinalityProvider>
</RouteContext.Provider>
</QueryClientProvider>
);
}
describe('Performance > TransactionSummary', function () {
let eventStatsMock: jest.Mock;
beforeEach(function () {
// eslint-disable-next-line no-console
jest.spyOn(console, 'error').mockImplementation(jest.fn());
MockApiClient.clearMockResponses();
MockApiClient.addMockResponse({
url: '/organizations/org-slug/projects/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/tags/user.email/values/',
body: [],
});
eventStatsMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-stats/',
body: {data: [[123, []]]},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/releases/stats/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/?limit=5&project=2&query=is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/users/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/recent-searches/',
method: 'POST',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/sdk-updates/',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/prompts-activity/',
body: {},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-facets-performance/',
body: {},
});
// Events Mock totals for the sidebar and other summary data
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
'count()': 'number',
'apdex()': 'number',
'count_miserable_user()': 'number',
'user_misery()': 'number',
'count_unique_user()': 'number',
'p95()': 'number',
'failure_rate()': 'number',
'tpm()': 'number',
project_threshold_config: 'string',
},
},
data: [
{
'count()': 2,
'apdex()': 0.6,
'count_miserable_user()': 122,
'user_misery()': 0.114,
'count_unique_user()': 1,
'p95()': 750.123,
'failure_rate()': 1,
'tpm()': 1,
project_threshold_config: ['duration', 300],
},
],
},
match: [
(_url, options) => {
return options.query?.field?.includes('p95()');
},
],
});
// [Metrics Enhanced] Events Mock totals for the sidebar and other summary data
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
'count()': 'number',
'apdex()': 'number',
'count_miserable_user()': 'number',
'user_misery()': 'number',
'count_unique_user()': 'number',
'p95()': 'number',
'failure_rate()': 'number',
'tpm()': 'number',
project_threshold_config: 'string',
},
isMetricsData: true,
},
data: [
{
'count()': 200,
'apdex()': 0.5,
'count_miserable_user()': 120,
'user_misery()': 0.1,
'count_unique_user()': 100,
'p95()': 731.3132,
'failure_rate()': 1,
'tpm()': 100,
project_threshold_config: ['duration', 300],
},
],
},
match: [
(_url, options) => {
const isMetricsEnhanced =
options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED;
return options.query?.field?.includes('p95()') && isMetricsEnhanced;
},
],
});
// Events Mock unfiltered totals for percentage calculations
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
'tpm()': 'number',
},
},
data: [
{
'tpm()': 1,
},
],
},
match: [
(_url, options) => {
return (
options.query?.field?.includes('tpm()') &&
!options.query?.field?.includes('p95()')
);
},
],
});
// Events Mock count totals for histogram percentage calculations
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
'count()': 'number',
},
},
data: [
{
'count()': 2,
},
],
},
match: [
(_url, options) => {
return (
options.query?.field?.includes('count()') &&
!options.query?.field?.includes('p95()')
);
},
],
});
// Events Transaction list response
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
headers: {
Link:
'<http://localhost/api/0/organizations/org-slug/events/?cursor=2:0:0>; rel="next"; results="true"; cursor="2:0:0",' +
'<http://localhost/api/0/organizations/org-slug/events/?cursor=1:0:0>; rel="previous"; results="false"; cursor="1:0:0"',
},
body: {
meta: {
fields: {
id: 'string',
'user.display': 'string',
'transaction.duration': 'duration',
'project.id': 'integer',
timestamp: 'date',
},
},
data: [
{
id: 'deadbeef',
'user.display': '[email protected]',
'transaction.duration': 400,
'project.id': 2,
timestamp: '2020-05-21T15:31:18+00:00',
},
],
},
match: [
(_url, options) => {
return options.query?.field?.includes('user.display');
},
],
});
// Events Mock totals for status breakdown
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
'transaction.status': 'string',
'count()': 'number',
},
},
data: [
{
'count()': 2,
'transaction.status': 'ok',
},
],
},
match: [
(_url, options) => {
return options.query?.field?.includes('transaction.status');
},
],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-facets/',
body: [
{
key: 'release',
topValues: [{count: 3, value: 'abcd123', name: 'abcd123'}],
},
{
key: 'environment',
topValues: [
{count: 2, value: 'dev', name: 'dev'},
{count: 1, value: 'prod', name: 'prod'},
],
},
{
key: 'foo',
topValues: [
{count: 2, value: 'bar', name: 'bar'},
{count: 1, value: 'baz', name: 'baz'},
],
},
{
key: 'user',
topValues: [
{count: 2, value: 'id:100', name: '100'},
{count: 1, value: 'id:101', name: '101'},
],
},
],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/project-transaction-threshold-override/',
method: 'GET',
body: {
threshold: '800',
metric: 'lcp',
},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-vitals/',
body: {
'measurements.fcp': {
poor: 3,
meh: 100,
good: 47,
total: 150,
p75: 1500,
},
'measurements.lcp': {
poor: 2,
meh: 38,
good: 40,
total: 80,
p75: 2750,
},
'measurements.fid': {
poor: 2,
meh: 53,
good: 5,
total: 60,
p75: 1000,
},
'measurements.cls': {
poor: 3,
meh: 10,
good: 4,
total: 17,
p75: 0.2,
},
},
});
MockApiClient.addMockResponse({
method: 'GET',
url: `/organizations/org-slug/key-transactions-list/`,
body: teams.map(({id}) => ({
team: id,
count: 0,
keyed: [],
})),
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-has-measurements/',
body: {measurements: false},
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-spans-performance/',
body: [
{
op: 'ui.long-task',
group: 'c777169faad84eb4',
description: 'Main UI thread blocked',
frequency: 713,
count: 9040,
avgOccurrences: null,
sumExclusiveTime: 1743893.9822921753,
p50ExclusiveTime: null,
p75ExclusiveTime: 244.9998779296875,
p95ExclusiveTime: null,
p99ExclusiveTime: null,
},
],
});
MockApiClient.addMockResponse({
url: `/projects/org-slug/project-slug/profiling/functions/`,
body: {functions: []},
});
MockApiClient.addMockResponse({
method: 'GET',
url: `/organizations/org-slug/metrics-compatibility/`,
body: {
compatible_projects: [],
incompatible_projecs: [],
},
});
MockApiClient.addMockResponse({
method: 'GET',
url: `/organizations/org-slug/metrics-compatibility-sums/`,
body: {
sum: {
metrics: 100,
metrics_null: 0,
metrics_unparam: 0,
},
},
});
jest.spyOn(MEPSetting, 'get').mockImplementation(() => MEPState.AUTO);
});
afterEach(function () {
MockApiClient.clearMockResponses();
ProjectsStore.reset();
jest.clearAllMocks();
});
describe('with events', function () {
it('renders basic UI elements', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
// It shows the header
await screen.findByText('Transaction Summary');
expect(screen.getByRole('heading', {name: '/performance'})).toBeInTheDocument();
// It shows a chart
expect(
screen.getByRole('button', {name: 'Display Duration Breakdown'})
).toBeInTheDocument();
// It shows a searchbar
expect(screen.getByLabelText('Search events')).toBeInTheDocument();
// It shows a table
expect(screen.getByTestId('transactions-table')).toBeInTheDocument();
// Ensure open in discover button exists.
expect(screen.getByTestId('transaction-events-open')).toBeInTheDocument();
// Ensure open issues button exists.
expect(screen.getByRole('button', {name: 'Open in Issues'})).toBeInTheDocument();
// Ensure transaction filter button exists
expect(
screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
).toBeInTheDocument();
// Ensure create alert from discover is hidden without metric alert
expect(
screen.queryByRole('button', {name: 'Create Alert'})
).not.toBeInTheDocument();
// Ensure status breakdown exists
expect(screen.getByText('Status Breakdown')).toBeInTheDocument();
});
it('renders feature flagged UI elements', function () {
const {organization, router, routerContext} = initializeData({
features: ['incidents'],
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
// Ensure create alert from discover is shown with metric alerts
expect(screen.getByRole('button', {name: 'Create Alert'})).toBeInTheDocument();
});
it('renders Web Vitals widget', async function () {
const {organization, router, routerContext} = initializeData({
project: ProjectFixture({teams, platform: 'javascript'}),
query: {
query:
'transaction.duration:<15m transaction.op:pageload event.type:transaction transaction:/organizations/:orgId/issues/',
},
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
// It renders the web vitals widget
await screen.findByRole('heading', {name: 'Web Vitals'});
const vitalStatues = screen.getAllByTestId('vital-status');
expect(vitalStatues).toHaveLength(3);
expect(vitalStatues[0]).toHaveTextContent('31%');
expect(vitalStatues[1]).toHaveTextContent('65%');
expect(vitalStatues[2]).toHaveTextContent('3%');
});
it('renders sidebar widgets', async function () {
const {organization, router, routerContext} = initializeData({});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
// Renders Apdex widget
await screen.findByRole('heading', {name: 'Apdex'});
expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
// Renders Failure Rate widget
expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
// Renders TPM widget
expect(
screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
).toBeInTheDocument();
expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
'100%'
);
});
it('renders project picker modal when no url does not have project id', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
project: 'string',
'count()': 'number',
},
},
data: [
{
'count()': 2,
project: 'proj-slug-1',
},
{
'count()': 3,
project: 'proj-slug-2',
},
],
},
match: [
(_url, options) => {
return options.query?.field?.includes('project');
},
],
});
const projects = [
ProjectFixture({
slug: 'proj-slug-1',
id: '1',
name: 'Project Name 1',
}),
ProjectFixture({
slug: 'proj-slug-2',
id: '2',
name: 'Project Name 2',
}),
];
OrganizationStore.onUpdate(OrganizationFixture({slug: 'org-slug'}), {
replace: true,
});
const {organization, router, routerContext} = initializeData({projects});
const spy = jest.spyOn(router, 'replace');
// Ensure project id is not in path
delete router.location.query.project;
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{context: routerContext, organization}
);
renderGlobalModal();
const firstProjectOption = await screen.findByText('proj-slug-1');
expect(firstProjectOption).toBeInTheDocument();
expect(screen.getByText('proj-slug-2')).toBeInTheDocument();
expect(screen.getByText('My Projects')).toBeInTheDocument();
await userEvent.click(firstProjectOption);
expect(spy).toHaveBeenCalledWith(
'/organizations/org-slug/performance/summary/?transaction=/performance&statsPeriod=14d&referrer=performance-transaction-summary&transactionCursor=1:0:0&project=1'
);
});
it('fetches transaction threshold', function () {
const {organization, router, routerContext} = initializeData();
const getTransactionThresholdMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/project-transaction-threshold-override/',
method: 'GET',
body: {
threshold: '800',
metric: 'lcp',
},
});
const getProjectThresholdMock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
method: 'GET',
body: {
threshold: '200',
metric: 'duration',
},
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
expect(getProjectThresholdMock).not.toHaveBeenCalled();
});
it('fetches project transaction threshdold', async function () {
const {organization, router, routerContext} = initializeData();
const getTransactionThresholdMock = MockApiClient.addMockResponse({
url: '/organizations/org-slug/project-transaction-threshold-override/',
method: 'GET',
statusCode: 404,
});
const getProjectThresholdMock = MockApiClient.addMockResponse({
url: '/projects/org-slug/project-slug/transaction-threshold/configure/',
method: 'GET',
body: {
threshold: '200',
metric: 'duration',
},
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
expect(getTransactionThresholdMock).toHaveBeenCalledTimes(1);
expect(getProjectThresholdMock).toHaveBeenCalledTimes(1);
});
it('triggers a navigation on search', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
// Fill out the search box, and submit it.
await userEvent.type(
screen.getByLabelText('Search events'),
'user.email:uhoh*{enter}'
);
// Check the navigation.
expect(browserHistory.push).toHaveBeenCalledTimes(1);
expect(browserHistory.push).toHaveBeenCalledWith({
pathname: undefined,
query: {
transaction: '/performance',
project: '2',
statsPeriod: '14d',
query: 'user.email:uhoh*',
transactionCursor: '1:0:0',
},
});
});
it('can mark a transaction as key', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
const mockUpdate = MockApiClient.addMockResponse({
url: `/organizations/org-slug/key-transactions/`,
method: 'POST',
body: {},
});
await screen.findByRole('button', {name: 'Star for Team'});
// Click the key transaction button
await userEvent.click(screen.getByRole('button', {name: 'Star for Team'}));
await userEvent.click(screen.getByRole('option', {name: '#team1'}));
// Ensure request was made.
expect(mockUpdate).toHaveBeenCalled();
});
it('triggers a navigation on transaction filter', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
await waitFor(() => {
expect(screen.queryByTestId('loading-indicator')).not.toBeInTheDocument();
});
// Open the transaction filter dropdown
await userEvent.click(
screen.getByRole('button', {name: 'Filter Slow Transactions (p95)'})
);
await userEvent.click(screen.getAllByText('Slow Transactions (p95)')[1]);
// Check the navigation.
expect(browserHistory.push).toHaveBeenCalledWith({
pathname: undefined,
query: {
transaction: '/performance',
project: '2',
showTransactions: 'slow',
transactionCursor: undefined,
},
});
});
it('renders pagination buttons', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
expect(await screen.findByLabelText('Previous')).toBeInTheDocument();
// Click the 'next' button
await userEvent.click(screen.getByLabelText('Next'));
// Check the navigation.
expect(browserHistory.push).toHaveBeenCalledWith({
pathname: undefined,
query: {
transaction: '/performance',
project: '2',
transactionCursor: '2:0:0',
},
});
});
it('forwards conditions to related issues', async function () {
const issueGet = MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
body: [],
});
const {organization, router, routerContext} = initializeData({
query: {query: 'tag:value'},
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
expect(issueGet).toHaveBeenCalled();
});
it('does not forward event type to related issues', async function () {
const issueGet = MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/?limit=5&project=2&query=tag%3Avalue%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
body: [],
match: [
(_, options) => {
// event.type must NOT be in the query params
return !options.query?.query?.includes('event.type');
},
],
});
const {organization, router, routerContext} = initializeData({
query: {query: 'tag:value event.type:transaction'},
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
expect(issueGet).toHaveBeenCalled();
});
it('renders the suspect spans table if the feature is enabled', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events-spans-performance/',
body: [],
});
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
expect(await screen.findByText('Suspect Spans')).toBeInTheDocument();
});
it('adds search condition on transaction status when clicking on status breakdown', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByTestId('status-ok');
await userEvent.click(screen.getByTestId('status-ok'));
expect(browserHistory.push).toHaveBeenCalledTimes(1);
expect(browserHistory.push).toHaveBeenCalledWith(
expect.objectContaining({
query: expect.objectContaining({
query: expect.stringContaining('transaction.status:ok'),
}),
})
);
});
it('appends tag value to existing query when clicked', async function () {
const {organization, router, routerContext} = initializeData();
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Tag Summary');
await userEvent.click(
await screen.findByLabelText(
'environment, dev, 100% of all events. View events with this tag value.'
)
);
await userEvent.click(
await screen.findByLabelText(
'foo, bar, 100% of all events. View events with this tag value.'
)
);
expect(router.push).toHaveBeenCalledTimes(2);
expect(router.push).toHaveBeenNthCalledWith(1, {
query: {
project: '2',
query: 'tags[environment]:dev',
transaction: '/performance',
transactionCursor: '1:0:0',
},
});
expect(router.push).toHaveBeenNthCalledWith(2, {
query: {
project: '2',
query: 'foo:bar',
transaction: '/performance',
transactionCursor: '1:0:0',
},
});
});
it('does not use MEP dataset for stats query without features', async function () {
const {organization, router, routerContext} = initializeData({
query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
features: [''], // No 'dynamic-sampling' feature to indicate it can use metrics dataset or metrics enhanced.
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
await screen.findByRole('heading', {name: 'Apdex'});
expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
expect(eventStatsMock).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({
query: expect.objectContaining({
environment: [],
interval: '30m',
partial: '1',
project: [2],
query:
'transaction.op:pageload event.type:transaction transaction:/performance',
referrer: 'api.performance.transaction-summary.duration-chart',
statsPeriod: '14d',
yAxis: [
'p50(transaction.duration)',
'p75(transaction.duration)',
'p95(transaction.duration)',
'p99(transaction.duration)',
'p100(transaction.duration)',
'avg(transaction.duration)',
],
}),
})
);
});
it('uses MEP dataset for stats query', async function () {
const {organization, router, routerContext} = initializeData({
query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
features: ['dynamic-sampling', 'mep-rollout-flag'],
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
// Renders Apdex widget
await screen.findByRole('heading', {name: 'Apdex'});
expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5');
expect(eventStatsMock).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({
query: expect.objectContaining({
query:
'transaction.op:pageload event.type:transaction transaction:/performance',
dataset: 'metricsEnhanced',
}),
})
);
// Renders Failure Rate widget
expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
// Renders TPM widget
expect(
screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
).toBeInTheDocument();
expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
'100%'
);
expect(
screen.queryByTestId('search-metrics-fallback-warning')
).not.toBeInTheDocument();
});
it('does not use MEP dataset for stats query if cardinality fallback fails', async function () {
MockApiClient.addMockResponse({
method: 'GET',
url: `/organizations/org-slug/metrics-compatibility-sums/`,
body: {
sum: {
metrics: 100,
metrics_null: 100,
metrics_unparam: 0,
},
},
});
const {organization, router, routerContext} = initializeData({
query: {query: 'transaction.op:pageload'}, // transaction.op is covered by the metrics dataset
features: ['dynamic-sampling', 'mep-rollout-flag'],
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
// Renders Apdex widget
await screen.findByRole('heading', {name: 'Apdex'});
expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.6');
expect(eventStatsMock).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({
query: expect.objectContaining({
query:
'transaction.op:pageload event.type:transaction transaction:/performance',
}),
})
);
// Renders TPM widget
expect(
screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
).toBeInTheDocument();
expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
'100%'
);
});
it('uses MEP dataset for stats query and shows fallback warning', async function () {
MockApiClient.addMockResponse({
url: '/organizations/org-slug/issues/?limit=5&project=2&query=has%3Anot-compatible%20is%3Aunresolved%20transaction%3A%2Fperformance&sort=new&statsPeriod=14d',
body: [],
});
MockApiClient.addMockResponse({
url: '/organizations/org-slug/events/',
body: {
meta: {
fields: {
'count()': 'number',
'apdex()': 'number',
'count_miserable_user()': 'number',
'user_misery()': 'number',
'count_unique_user()': 'number',
'p95()': 'number',
'failure_rate()': 'number',
'tpm()': 'number',
project_threshold_config: 'string',
},
isMetricsData: false, // The total response is setting the metrics fallback behaviour.
},
data: [
{
'count()': 200,
'apdex()': 0.5,
'count_miserable_user()': 120,
'user_misery()': 0.1,
'count_unique_user()': 100,
'p95()': 731.3132,
'failure_rate()': 1,
'tpm()': 100,
project_threshold_config: ['duration', 300],
},
],
},
match: [
(_url, options) => {
const isMetricsEnhanced =
options.query?.dataset === DiscoverDatasets.METRICS_ENHANCED;
return (
options.query?.field?.includes('p95()') &&
isMetricsEnhanced &&
options.query?.query?.includes('not-compatible')
);
},
],
});
const {organization, router, routerContext} = initializeData({
query: {query: 'transaction.op:pageload has:not-compatible'}, // Adds incompatible w/ metrics tag
features: ['dynamic-sampling', 'mep-rollout-flag'],
});
render(
<TestComponent
organization={organization}
router={router}
location={router.location}
/>,
{
context: routerContext,
organization,
}
);
await screen.findByText('Transaction Summary');
// Renders Apdex widget
await screen.findByRole('heading', {name: 'Apdex'});
expect(await screen.findByTestId('apdex-summary-value')).toHaveTextContent('0.5');
expect(eventStatsMock).toHaveBeenNthCalledWith(
1,
expect.anything(),
expect.objectContaining({
query: expect.objectContaining({
query:
'transaction.op:pageload has:not-compatible event.type:transaction transaction:/performance',
dataset: 'metricsEnhanced',
}),
})
);
// Renders Failure Rate widget
expect(screen.getByRole('heading', {name: 'Failure Rate'})).toBeInTheDocument();
expect(screen.getByTestId('failure-rate-summary-value')).toHaveTextContent('100%');
// Renders TPM widget
expect(
screen.getByRole('heading', {name: 'Percentage of Total Transactions'})
).toBeInTheDocument();
expect(screen.getByTestId('count-percentage-summary-value')).toHaveTextContent(
'100%'
);
expect(screen.getByTestId('search-metrics-fallback-warning')).toBeInTheDocument();
});
});
});