123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- import {browserHistory} from 'react-router';
- import {
- generateSuspectSpansResponse,
- initializeData as _initializeData,
- } from 'sentry-test/performance/initializePerformanceData';
- import {
- act,
- render,
- screen,
- userEvent,
- waitFor,
- within,
- } from 'sentry-test/reactTestingLibrary';
- import ProjectsStore from 'sentry/stores/projectsStore';
- import SpanDetails from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails';
- import {spanDetailsRouteWithQuery} from 'sentry/views/performance/transactionSummary/transactionSpans/spanDetails/utils';
- function initializeData(settings) {
- const data = _initializeData(settings);
- act(() => void ProjectsStore.loadInitialData(data.organization.projects));
- return data;
- }
- describe('Performance > Transaction Spans > Span Summary', function () {
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/projects/',
- body: [],
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events/',
- body: {data: [{'count()': 1}]},
- });
- });
- afterEach(function () {
- MockApiClient.clearMockResponses();
- ProjectsStore.reset();
- // need to typecast to any to be able to call mockReset
- (browserHistory.push as any).mockReset();
- });
- describe('Without Span Data', function () {
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-performance/',
- body: [],
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans/',
- body: [],
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-stats/',
- body: {
- 'percentileArray(spans_exclusive_time, 0.50)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.75)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.95)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.99)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- },
- });
- });
- it('renders empty when missing project param', function () {
- const data = initializeData({query: {transaction: 'transaction'}});
- const {container} = render(
- <SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />,
- {organization: data.organization}
- );
- expect(container).toBeEmptyDOMElement();
- });
- it('renders empty when missing transaction param', function () {
- const data = initializeData({query: {project: '1'}});
- const {container} = render(
- <SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />,
- {organization: data.organization}
- );
- expect(container).toBeEmptyDOMElement();
- });
- it('renders no data when empty response', async function () {
- const data = initializeData({
- features: ['performance-view'],
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(
- await screen.findByText('No results found for your query')
- ).toBeInTheDocument();
- expect(await screen.findByText('Self Time Breakdown')).toBeInTheDocument();
- });
- });
- describe('With Bad Span Data', function () {
- it('filters examples missing spans', async function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-performance/',
- body: generateSuspectSpansResponse(),
- });
- // just want to get one span in the response
- const badExamples = [generateSuspectSpansResponse({examplesOnly: true})[0]];
- for (const example of badExamples[0].examples) {
- // make sure that the spans array is empty
- example.spans = [];
- }
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans/',
- body: badExamples,
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-stats/',
- body: {
- 'percentileArray(spans_exclusive_time, 0.50)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.75)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.95)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.99)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- },
- });
- const data = initializeData({
- features: ['performance-view'],
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(await screen.findByText('Event ID')).toBeInTheDocument();
- expect(await screen.findByText('Timestamp')).toBeInTheDocument();
- expect(await screen.findByText('Span Duration')).toBeInTheDocument();
- expect(await screen.findByText('Count')).toBeInTheDocument();
- expect(await screen.findByText('Cumulative Duration')).toBeInTheDocument();
- });
- });
- describe('With Span Data', function () {
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-performance/',
- body: generateSuspectSpansResponse(),
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans/',
- body: generateSuspectSpansResponse({examplesOnly: true}),
- });
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-stats/',
- body: {
- 'percentileArray(spans_exclusive_time, 0.50)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.75)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.95)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- 'percentileArray(spans_exclusive_time, 0.99)': {
- data: [
- [0, [{count: 0}]],
- [10, [{count: 0}]],
- ],
- order: 2,
- start: 0,
- end: 10,
- },
- },
- });
- });
- it('renders header elements', async function () {
- const data = initializeData({
- features: ['performance-view'],
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(await screen.findByText('Span Summary')).toBeInTheDocument();
- const operationNameHeader = await screen.findByTestId('header-operation-name');
- expect(
- await within(operationNameHeader).findByText('Span Operation')
- ).toBeInTheDocument();
- // TODO: add an expect for the span description here
- expect(
- await within(operationNameHeader).findByTestId('operation-name')
- ).toHaveTextContent('op');
- const percentilesHeader = await screen.findByTestId('header-percentiles');
- expect(
- await within(percentilesHeader).findByText('Self Time Percentiles')
- ).toBeInTheDocument();
- const p50Section = await within(percentilesHeader).findByTestId('section-p50');
- expect(await within(p50Section).findByText('1.00ms')).toBeInTheDocument();
- expect(await within(p50Section).findByText('p50')).toBeInTheDocument();
- const p75Section = await within(percentilesHeader).findByTestId('section-p75');
- expect(await within(p75Section).findByText('2.00ms')).toBeInTheDocument();
- expect(await within(p75Section).findByText('p75')).toBeInTheDocument();
- const p95Section = await within(percentilesHeader).findByTestId('section-p95');
- expect(await within(p95Section).findByText('3.00ms')).toBeInTheDocument();
- expect(await within(p95Section).findByText('p95')).toBeInTheDocument();
- const p99Section = await within(percentilesHeader).findByTestId('section-p99');
- expect(await within(p99Section).findByText('4.00ms')).toBeInTheDocument();
- expect(await within(p99Section).findByText('p99')).toBeInTheDocument();
- const frequencyHeader = await screen.findByTestId('header-frequency');
- expect(await within(frequencyHeader).findByText('100%')).toBeInTheDocument();
- expect(
- await within(frequencyHeader).findByText((_content, element) =>
- Boolean(
- element &&
- element.tagName === 'DIV' &&
- element.textContent === '1.00 times per event'
- )
- )
- ).toBeInTheDocument();
- const totalExclusiveTimeHeader = await screen.findByTestId(
- 'header-total-exclusive-time'
- );
- expect(
- await within(totalExclusiveTimeHeader).findByText('5.00ms')
- ).toBeInTheDocument();
- expect(
- await within(totalExclusiveTimeHeader).findByText((_content, element) =>
- Boolean(
- element && element.tagName === 'DIV' && element.textContent === '1 events'
- )
- )
- ).toBeInTheDocument();
- });
- it('renders timeseries chart', async function () {
- const data = initializeData({
- features: ['performance-view'],
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(await screen.findByText('Self Time Breakdown')).toBeInTheDocument();
- });
- it('renders table headers', async function () {
- const data = initializeData({
- features: ['performance-view'],
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(await screen.findByText('Event ID')).toBeInTheDocument();
- expect(await screen.findByText('Timestamp')).toBeInTheDocument();
- expect(await screen.findByText('Span Duration')).toBeInTheDocument();
- expect(await screen.findByText('Count')).toBeInTheDocument();
- expect(await screen.findByText('Cumulative Duration')).toBeInTheDocument();
- });
- describe('With histogram view feature flag enabled', function () {
- const FEATURES = ['performance-view', 'performance-span-histogram-view'];
- beforeEach(function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/recent-searches/',
- method: 'GET',
- body: [],
- });
- });
- it('renders a search bar', async function () {
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const searchBarNode = await screen.findByPlaceholderText('Filter Transactions');
- expect(searchBarNode).toBeInTheDocument();
- });
- it('disables reset button when no min or max query parameters were set', async function () {
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const resetButton = await screen.findByRole('button', {
- name: /reset view/i,
- });
- expect(resetButton).toBeInTheDocument();
- expect(resetButton).toBeDisabled();
- });
- it('enables reset button when min and max are set', async function () {
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', min: '10', max: '100'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const resetButton = await screen.findByRole('button', {
- name: /reset view/i,
- });
- expect(resetButton).toBeEnabled();
- });
- it('clears min and max query parameters when reset button is clicked', async function () {
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', min: '10', max: '100'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const resetButton = await screen.findByRole('button', {
- name: /reset view/i,
- });
- await userEvent.click(resetButton);
- expect(browserHistory.push).toHaveBeenCalledWith(
- expect.not.objectContaining({min: expect.any(String), max: expect.any(String)})
- );
- });
- it('does not add aggregate filters to the query', async function () {
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const searchBarNode = await screen.findByPlaceholderText('Filter Transactions');
- await userEvent.click(searchBarNode);
- await userEvent.paste('count():>3');
- expect(searchBarNode).toHaveTextContent('count():>3');
- expect(browserHistory.push).not.toHaveBeenCalled();
- });
- it('renders a display toggle that changes a chart view between timeseries and histogram by pushing it to the browser history', async function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-histogram/',
- body: [
- {bin: 0, count: 0},
- {bin: 10, count: 2},
- {bin: 20, count: 4},
- ],
- });
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(await screen.findByTestId('total-value')).toBeInTheDocument();
- const chartTitleNodes = await screen.findAllByText('Self Time Breakdown');
- expect(chartTitleNodes[0]).toBeInTheDocument();
- const displayToggle = await screen.findByTestId('display-toggle');
- expect(displayToggle).toBeInTheDocument();
- expect(await within(displayToggle).findByRole('button')).toHaveTextContent(
- 'Self Time Breakdown'
- );
- await userEvent.click(await within(displayToggle).findByRole('button'));
- await userEvent.click(
- await within(displayToggle).findByRole('option', {
- name: 'Self Time Distribution',
- })
- );
- expect(browserHistory.push).toHaveBeenCalledWith(
- expect.objectContaining({
- query: {
- display: 'histogram',
- project: '1',
- transaction: 'transaction',
- },
- })
- );
- });
- it('renders a histogram when display is set to histogram', async function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-histogram/',
- body: [
- {bin: 0, count: 0},
- {bin: 10, count: 2},
- {bin: 20, count: 4},
- ],
- });
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', display: 'histogram'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const displayToggle = await screen.findByTestId('display-toggle');
- expect(await within(displayToggle).findByRole('button')).toHaveTextContent(
- 'Self Time Distribution'
- );
- const nodes = await screen.findAllByText('Self Time Distribution');
- expect(nodes[0]).toBeInTheDocument();
- });
- it('gracefully handles error response', async function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-histogram/',
- statusCode: 400,
- });
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', display: 'histogram'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- expect(await screen.findByTestId('histogram-error-panel')).toBeInTheDocument();
- });
- it('gracefully renders empty histogram when empty buckets are received', async function () {
- MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-histogram/',
- body: [
- {bin: 0, count: 0},
- {bin: 10, count: 0},
- {bin: 20, count: 0},
- ],
- });
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', display: 'histogram'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- const nodes = await screen.findAllByText('Self Time Distribution');
- expect(nodes[0]).toBeInTheDocument();
- });
- it('sends min and max to span example query', async function () {
- const mock = MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans/',
- body: {},
- });
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', min: '10', max: '120'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- await waitFor(() => {
- expect(mock).toHaveBeenLastCalledWith(
- '/organizations/org-slug/events-spans/',
- expect.objectContaining({
- query: expect.objectContaining({
- min_exclusive_time: '10',
- max_exclusive_time: '120',
- }),
- })
- );
- });
- });
- it('sends min and max to suspect spans query', async function () {
- const mock = MockApiClient.addMockResponse({
- url: '/organizations/org-slug/events-spans-performance/',
- body: {},
- });
- const data = initializeData({
- features: FEATURES,
- query: {project: '1', transaction: 'transaction', min: '10', max: '120'},
- });
- render(<SpanDetails params={{spanSlug: 'op:aaaaaaaa'}} {...data} />, {
- context: data.routerContext,
- organization: data.organization,
- });
- await waitFor(() => {
- expect(mock).toHaveBeenLastCalledWith(
- '/organizations/org-slug/events-spans-performance/',
- expect.objectContaining({
- query: expect.objectContaining({
- min_exclusive_time: '10',
- max_exclusive_time: '120',
- }),
- })
- );
- });
- });
- });
- });
- });
- describe('spanDetailsRouteWithQuery', function () {
- it('should encode slashes in span op', function () {
- const target = spanDetailsRouteWithQuery({
- orgSlug: 'org-slug',
- transaction: 'transaction',
- query: {},
- spanSlug: {op: 'o/p', group: 'aaaaaaaaaaaaaaaa'},
- projectID: '1',
- });
- expect(target).toEqual(
- expect.objectContaining({
- pathname:
- '/organizations/org-slug/performance/summary/spans/o%2Fp:aaaaaaaaaaaaaaaa/',
- })
- );
- });
- });
|