import {initializeData as _initializeData} from 'sentry-test/performance/initializePerformanceData';
import {
act,
render,
screen,
userEvent,
waitFor,
within,
} from 'sentry-test/reactTestingLibrary';
import * as AnchorLinkManager from 'sentry/components/events/interfaces/spans/anchorLinkManager';
import TraceView from 'sentry/components/events/interfaces/spans/traceView';
import {spanTargetHash} from 'sentry/components/events/interfaces/spans/utils';
import WaterfallModel from 'sentry/components/events/interfaces/spans/waterfallModel';
import ProjectsStore from 'sentry/stores/projectsStore';
import {EntryType, EventTransaction} from 'sentry/types';
import {QuickTraceContext} from 'sentry/utils/performance/quickTrace/quickTraceContext';
import QuickTraceQuery from 'sentry/utils/performance/quickTrace/quickTraceQuery';
function initializeData(settings) {
const data = _initializeData(settings);
act(() => void ProjectsStore.loadInitialData(data.organization.projects));
return data;
}
function generateSampleEvent(): EventTransaction {
const event = {
id: '2b658a829a21496b87fd1f14a61abf65',
eventID: '2b658a829a21496b87fd1f14a61abf65',
title: '/organizations/:orgId/discover/results/',
type: 'transaction',
startTimestamp: 1622079935.86141,
endTimestamp: 1622079940.032905,
contexts: {
trace: {
trace_id: '8cbbc19c0f54447ab702f00263262726',
span_id: 'a000000000000000',
op: 'pageload',
status: 'unknown',
type: 'trace',
},
},
entries: [
{
data: [],
type: EntryType.SPANS,
},
],
} as EventTransaction;
return event;
}
function generateSampleSpan(
description: string | null,
op: string | null,
span_id: string,
parent_span_id: string,
event: EventTransaction
) {
const span = {
start_timestamp: 1000,
timestamp: 2000,
description,
op,
span_id,
parent_span_id,
trace_id: '8cbbc19c0f54447ab702f00263262726',
status: 'ok',
tags: {
'http.status_code': '200',
},
data: {},
};
event.entries[0].data.push(span);
return span;
}
describe('TraceView', () => {
afterEach(() => {
MockApiClient.clearMockResponses();
});
describe('Autogrouped spans tests', () => {
it('should render siblings with the same op and description as a grouped span in the minimap and span tree', async () => {
const data = initializeData({
features: ['performance-autogroup-sibling-spans'],
});
const event = generateSampleEvent();
generateSampleSpan(
'group me',
'http',
'b000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'a000000000000000',
event
);
const waterfallModel = new WaterfallModel(event);
render(
);
expect(await screen.findByTestId('minimap-sibling-group-bar')).toBeInTheDocument();
expect(await screen.findByTestId('span-row-2')).toHaveTextContent('Autogrouped');
expect(screen.queryByTestId('span-row-3')).not.toBeInTheDocument();
});
it('should expand grouped siblings when clicked, and then regroup when clicked again', async () => {
// eslint-disable-next-line no-console
jest.spyOn(console, 'error').mockImplementation(jest.fn());
const data = initializeData({
features: ['performance-autogroup-sibling-spans'],
});
const event = generateSampleEvent();
generateSampleSpan(
'group me',
'http',
'b000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'a000000000000000',
event
);
const waterfallModel = new WaterfallModel(event);
render(
);
const groupedSiblingsSpan = await screen.findByText('Autogrouped — http —');
userEvent.click(groupedSiblingsSpan);
await waitFor(() =>
expect(screen.queryByText('Autogrouped — http —')).not.toBeInTheDocument()
);
for (let i = 1; i < 7; i++) {
expect(await screen.findByTestId(`span-row-${i}`)).toBeInTheDocument();
}
const regroupButton = await screen.findByText('Regroup');
expect(regroupButton).toBeInTheDocument();
userEvent.click(regroupButton);
await waitFor(() =>
expect(screen.queryByTestId('span-row-6')).not.toBeInTheDocument()
);
expect(await screen.findByText('Autogrouped — http —')).toBeInTheDocument();
});
it("should not group sibling spans that don't have the same op or description", async () => {
const data = initializeData({
features: ['performance-autogroup-sibling-spans'],
});
const event = generateSampleEvent();
generateSampleSpan('test', 'http', 'b000000000000000', 'a000000000000000', event);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'ff00000000000000',
'a000000000000000',
event
);
generateSampleSpan('test', 'http', 'fff0000000000000', 'a000000000000000', event);
const waterfallModel = new WaterfallModel(event);
render(
);
expect(await screen.findByText('group me')).toBeInTheDocument();
expect(await screen.findAllByText('test')).toHaveLength(2);
});
it('should autogroup similar nested spans', async () => {
const data = initializeData({});
const event = generateSampleEvent();
generateSampleSpan(
'group me',
'http',
'b000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'b000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'c000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'd000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'e000000000000000',
event
);
const waterfallModel = new WaterfallModel(event);
render(
);
const grouped = await screen.findByText('group me');
expect(grouped).toBeInTheDocument();
});
it('should expand/collapse only the sibling group that is clicked, even if multiple groups have the same op and description', async () => {
const data = initializeData({features: ['performance-autogroup-sibling-spans']});
const event = generateSampleEvent();
generateSampleSpan(
'group me',
'http',
'b000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'a000000000000000',
event
);
generateSampleSpan('not me', 'http', 'aa00000000000000', 'a000000000000000', event);
generateSampleSpan(
'group me',
'http',
'bb00000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'cc00000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'dd00000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'ee00000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'ff00000000000000',
'a000000000000000',
event
);
const waterfallModel = new WaterfallModel(event);
render(
);
expect(screen.queryAllByText('group me')).toHaveLength(2);
const firstGroup = screen.queryAllByText('Autogrouped — http —')[0];
userEvent.click(firstGroup);
expect(await screen.findAllByText('group me')).toHaveLength(6);
const secondGroup = await screen.findByText('Autogrouped — http —');
userEvent.click(secondGroup);
expect(await screen.findAllByText('group me')).toHaveLength(10);
const firstRegroup = screen.queryAllByText('Regroup')[0];
userEvent.click(firstRegroup);
expect(await screen.findAllByText('group me')).toHaveLength(6);
const secondRegroup = await screen.findByText('Regroup');
userEvent.click(secondRegroup);
expect(await screen.findAllByText('group me')).toHaveLength(2);
});
it('should allow expanding of embedded transactions', async () => {
const {organization, project, location} = initializeData({
features: ['unified-span-view'],
});
const event = generateSampleEvent();
generateSampleSpan(
'parent span',
'db',
'b000000000000000',
'a000000000000000',
event
);
const waterfallModel = new WaterfallModel(event);
const eventsTraceMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events-trace/${event.contexts.trace?.trace_id}/`,
method: 'GET',
statusCode: 200,
body: [
event,
{
errors: [],
event_id: '998d7e2c304c45729545e4434e2967cb',
generation: 1,
parent_event_id: '2b658a829a21496b87fd1f14a61abf65',
parent_span_id: 'b000000000000000',
project_id: project.id,
project_slug: project.slug,
span_id: '8596e2795f88471d',
transaction:
'/api/0/organizations/{organization_slug}/events/{project_slug}:{event_id}/',
'transaction.duration': 159,
'transaction.op': 'http.server',
},
],
});
const eventsTraceLightMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events-trace-light/${event.contexts.trace?.trace_id}/`,
method: 'GET',
statusCode: 200,
body: [
event,
{
errors: [],
event_id: '998d7e2c304c45729545e4434e2967cb',
generation: 1,
parent_event_id: '2b658a829a21496b87fd1f14a61abf65',
parent_span_id: 'b000000000000000',
project_id: project.id,
project_slug: project.slug,
span_id: '8596e2795f88471d',
transaction:
'/api/0/organizations/{organization_slug}/events/{project_slug}:{event_id}/',
'transaction.duration': 159,
'transaction.op': 'http.server',
},
],
});
const embeddedEvent = {
...generateSampleEvent(),
id: '998d7e2c304c45729545e4434e2967cb',
eventID: '998d7e2c304c45729545e4434e2967cb',
};
embeddedEvent.contexts.trace!.span_id = 'a111111111111111';
const embeddedSpan = generateSampleSpan(
'i am embedded :)',
'test',
'b111111111111111',
'b000000000000000',
embeddedEvent
);
embeddedSpan.trace_id = '8cbbc19c0f54447ab702f00263262726';
const fetchEmbeddedTransactionMock = MockApiClient.addMockResponse({
url: `/organizations/${organization.slug}/events/${project.slug}:998d7e2c304c45729545e4434e2967cb/`,
method: 'GET',
statusCode: 200,
body: embeddedEvent,
});
render(
{results => (
)}
);
expect(eventsTraceMock).toHaveBeenCalled();
expect(eventsTraceLightMock).toHaveBeenCalled();
const embeddedTransactionBadge = await screen.findByTestId(
'embedded-transaction-badge'
);
expect(embeddedTransactionBadge).toBeInTheDocument();
userEvent.click(embeddedTransactionBadge);
expect(fetchEmbeddedTransactionMock).toHaveBeenCalled();
expect(await screen.findByText(/i am embedded :\)/i)).toBeInTheDocument();
});
it('should correctly render sibling autogroup text when op and/or description is not provided', async () => {
const data = initializeData({
features: ['performance-autogroup-sibling-spans'],
});
const event1 = generateSampleEvent();
generateSampleSpan(
'group me',
null,
'b000000000000000',
'a000000000000000',
event1
);
generateSampleSpan(
'group me',
null,
'c000000000000000',
'a000000000000000',
event1
);
generateSampleSpan(
'group me',
null,
'd000000000000000',
'a000000000000000',
event1
);
generateSampleSpan(
'group me',
null,
'e000000000000000',
'a000000000000000',
event1
);
generateSampleSpan(
'group me',
null,
'f000000000000000',
'a000000000000000',
event1
);
const {rerender} = render(
);
expect(await screen.findByTestId('span-row-2')).toHaveTextContent(
/Autogrouped — group me/
);
const event2 = generateSampleEvent();
generateSampleSpan(null, 'http', 'b000000000000000', 'a000000000000000', event2);
generateSampleSpan(null, 'http', 'c000000000000000', 'a000000000000000', event2);
generateSampleSpan(null, 'http', 'd000000000000000', 'a000000000000000', event2);
generateSampleSpan(null, 'http', 'e000000000000000', 'a000000000000000', event2);
generateSampleSpan(null, 'http', 'f000000000000000', 'a000000000000000', event2);
rerender(
);
expect(await screen.findByTestId('span-row-2')).toHaveTextContent(
/Autogrouped — http/
);
const event3 = generateSampleEvent();
generateSampleSpan(null, null, 'b000000000000000', 'a000000000000000', event3);
generateSampleSpan(null, null, 'c000000000000000', 'a000000000000000', event3);
generateSampleSpan(null, null, 'd000000000000000', 'a000000000000000', event3);
generateSampleSpan(null, null, 'e000000000000000', 'a000000000000000', event3);
generateSampleSpan(null, null, 'f000000000000000', 'a000000000000000', event3);
rerender(
);
expect(await screen.findByTestId('span-row-2')).toHaveTextContent(
/Autogrouped — siblings/
);
});
it('should automatically expand a sibling span group and select a span if it is anchored', async () => {
const data = initializeData({
features: ['performance-autogroup-sibling-spans'],
});
const event = generateSampleEvent();
generateSampleSpan(
'group me',
'http',
'b000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'a000000000000000',
event
);
// Manually set the hash here, the AnchorLinkManager is expected to automatically expand the group and scroll to the span with this id
location.hash = spanTargetHash('c000000000000000');
const waterfallModel = new WaterfallModel(event);
render(
);
expect(await screen.findByText(/c000000000000000/i)).toBeInTheDocument();
location.hash = '';
});
it('should automatically expand a descendant span group and select a span if it is anchored', async () => {
const data = initializeData({});
const event = generateSampleEvent();
generateSampleSpan(
'group me',
'http',
'b000000000000000',
'a000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'c000000000000000',
'b000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'd000000000000000',
'c000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'e000000000000000',
'd000000000000000',
event
);
generateSampleSpan(
'group me',
'http',
'f000000000000000',
'e000000000000000',
event
);
location.hash = spanTargetHash('d000000000000000');
const waterfallModel = new WaterfallModel(event);
render(
);
expect(await screen.findByText(/d000000000000000/i)).toBeInTheDocument();
location.hash = '';
});
});
it('should merge web vitals labels if they are too close together', () => {
const data = initializeData({});
const event = generateSampleEvent();
generateSampleSpan('browser', 'test1', 'b000000000000000', 'a000000000000000', event);
generateSampleSpan('browser', 'test2', 'c000000000000000', 'a000000000000000', event);
generateSampleSpan('browser', 'test3', 'd000000000000000', 'a000000000000000', event);
generateSampleSpan('browser', 'test4', 'e000000000000000', 'a000000000000000', event);
generateSampleSpan('browser', 'test5', 'f000000000000000', 'a000000000000000', event);
event.measurements = {
fcp: {value: 1000},
fp: {value: 1050},
lcp: {value: 1100},
};
const waterfallModel = new WaterfallModel(event);
render(
);
const labelContainer = screen.getByText(/fcp/i).parentElement?.parentElement;
expect(labelContainer).toBeInTheDocument();
expect(within(labelContainer!).getByText(/fcp/i)).toBeInTheDocument();
expect(within(labelContainer!).getByText(/fp/i)).toBeInTheDocument();
expect(within(labelContainer!).getByText(/lcp/i)).toBeInTheDocument();
});
it('should not merge web vitals labels if they are spaced away from each other', () => {
const data = initializeData({});
const event = generateSampleEvent();
generateSampleSpan('browser', 'test1', 'b000000000000000', 'a000000000000000', event);
event.startTimestamp = 1;
event.endTimestamp = 100;
event.measurements = {
fcp: {value: 858.3002090454102, unit: 'millisecond'},
lcp: {value: 1000363.800048828125, unit: 'millisecond'},
};
const waterfallModel = new WaterfallModel(event);
render(
);
const fcpLabelContainer = screen.getByText(/fcp/i).parentElement?.parentElement;
expect(fcpLabelContainer).toBeInTheDocument();
// LCP should not be merged along with FCP. We expect it to be in a separate element
expect(within(fcpLabelContainer!).queryByText(/lcp/i)).not.toBeInTheDocument();
const lcpLabelContainer = screen.getByText(/lcp/i).parentElement?.parentElement;
expect(lcpLabelContainer).toBeInTheDocument();
});
});