import { MockSpan, ProblemSpan, TransactionEventBuilder, } from 'sentry-test/performance/utils'; import {render, screen} from 'sentry-test/reactTestingLibrary'; import {EntryType, IssueType} from 'sentry/types'; import { extractQueryParameters, extractSpanURLString, SpanEvidenceKeyValueList, } from './spanEvidenceKeyValueList'; describe('SpanEvidenceKeyValueList', () => { describe('N+1 Database Queries', () => { const builder = new TransactionEventBuilder('a1', '/'); const parentSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 0.2, op: 'http.server', problemSpan: ProblemSpan.PARENT, }); parentSpan.addChild({ startTimestamp: 0.01, endTimestamp: 2.1, op: 'db', description: 'SELECT * FROM books', problemSpan: ProblemSpan.OFFENDER, }); parentSpan.addChild({ startTimestamp: 2.1, endTimestamp: 4.0, op: 'db', description: 'SELECT * FROM books', problemSpan: ProblemSpan.OFFENDER, }); builder.addSpan(parentSpan); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Parent Span'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.parent-span') ).toHaveTextContent('http.server'); expect(screen.getByRole('cell', {name: 'Repeating Spans (2)'})).toBeInTheDocument(); expect( screen.getByTestId(/span-evidence-key-value-list.repeating-spans/) ).toHaveTextContent('db - SELECT * FROM books'); expect( screen.queryByTestId('span-evidence-key-value-list.') ).not.toBeInTheDocument(); expect(screen.queryByRole('cell', {name: 'Parameter'})).not.toBeInTheDocument(); expect( screen.queryByTestId('span-evidence-key-value-list.problem-parameters') ).not.toBeInTheDocument(); }); }); describe('MN+1 Database Queries', () => { const builder = new TransactionEventBuilder('a1', '/'); const parentSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 0.2, op: 'http.server', problemSpan: ProblemSpan.PARENT, }); parentSpan.addChild({ startTimestamp: 0.01, endTimestamp: 2.1, op: 'db', description: 'SELECT * FROM books', problemSpan: ProblemSpan.OFFENDER, }); parentSpan.addChild({ startTimestamp: 2.1, endTimestamp: 4.0, op: 'db.sql.active_record', description: 'SELECT * FROM books WHERE id = %s', problemSpan: ProblemSpan.OFFENDER, }); builder.addSpan(parentSpan); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Parent Span'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.parent-span') ).toHaveTextContent('http.server'); expect(screen.getByRole('cell', {name: 'Repeating Spans (2)'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.repeating-spans-2') ).toHaveTextContent('db - SELECT * FROM books'); expect(screen.getByTestId('span-evidence-key-value-list.')).toHaveTextContent( 'db.sql.active_record - SELECT * FROM books WHERE id = %s' ); expect(screen.queryByRole('cell', {name: 'Parameter'})).not.toBeInTheDocument(); expect( screen.queryByTestId('span-evidence-key-value-list.problem-parameters') ).not.toBeInTheDocument(); }); }); describe('Consecutive DB Queries', () => { const builder = new TransactionEventBuilder( 'a1', '/', IssueType.PERFORMANCE_CONSECUTIVE_DB_QUERIES ); const parentSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 0.65, op: 'http.server', problemSpan: ProblemSpan.PARENT, }); parentSpan.addChild({ startTimestamp: 0.1, endTimestamp: 0.2, op: 'db', description: 'SELECT * FROM USERS LIMIT 100', problemSpan: ProblemSpan.CAUSE, }); parentSpan.addChild({ startTimestamp: 0.2, endTimestamp: 0.4, op: 'db', description: 'SELECT COUNT(*) FROM USERS', problemSpan: [ProblemSpan.CAUSE, ProblemSpan.OFFENDER], }); parentSpan.addChild({ startTimestamp: 0.4, endTimestamp: 0.6, op: 'db', description: 'SELECT COUNT(*) FROM ITEMS', problemSpan: [ProblemSpan.CAUSE, ProblemSpan.OFFENDER], }); builder.addSpan(parentSpan); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Starting Span'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.starting-span') ).toHaveTextContent('db - SELECT * FROM USERS LIMIT 100'); expect(screen.queryAllByRole('cell', {name: 'Parallelizable Spans'}).length).toBe( 1 ); const parallelizableSpanKeyValue = screen.getByTestId( 'span-evidence-key-value-list.parallelizable-spans' ); expect(parallelizableSpanKeyValue).toHaveTextContent( 'db - SELECT COUNT(*) FROM USERS' ); expect(parallelizableSpanKeyValue).toHaveTextContent( 'db - SELECT COUNT(*) FROM ITEMS' ); expect( screen.getByTestId('span-evidence-key-value-list.duration-impact') ).toHaveTextContent('46% (300ms/650ms)'); }); }); describe('N+1 API Calls', () => { const builder = new TransactionEventBuilder( 'a1', '/', IssueType.PERFORMANCE_N_PLUS_ONE_API_CALLS ); const parentSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 200, op: 'pageload', problemSpan: ProblemSpan.PARENT, }); parentSpan.addChild({ startTimestamp: 10, endTimestamp: 2100, op: 'http.client', description: 'GET /book/?book_id=7&sort=up', problemSpan: ProblemSpan.OFFENDER, }); parentSpan.addChild({ startTimestamp: 10, endTimestamp: 2100, op: 'http.client', description: 'GET /book/?book_id=8&sort=down', problemSpan: ProblemSpan.OFFENDER, }); builder.addSpan(parentSpan); builder.addEntry( TestStubs.EventEntry({ type: EntryType.REQUEST, data: { url: 'http://some.service.io', }, }) ); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Repeating Spans (2)'})).toBeInTheDocument(); expect( screen.getByTestId(/span-evidence-key-value-list.repeating-spans/) ).toHaveTextContent('/book/[Parameters]'); expect(screen.queryByRole('cell', {name: 'Parameters'})).toBeInTheDocument(); const parametersKeyValue = screen.getByTestId( 'span-evidence-key-value-list.parameters' ); expect(parametersKeyValue).toHaveTextContent('book_id:{7,8}'); expect(parametersKeyValue).toHaveTextContent('sort:{up,down}'); }); describe('extractSpanURLString', () => { it('Tries to pull a URL from the span data', () => { expect( extractSpanURLString({ span_id: 'a', data: { url: 'http://service.io', }, })?.toString() ).toEqual('http://service.io/'); }); it('Pulls out a relative URL if a base is provided', () => { expect( extractSpanURLString( { span_id: 'a', data: { url: '/item', }, }, 'http://service.io' )?.toString() ).toEqual('http://service.io/item'); }); it('Falls back to span description if URL is faulty', () => { expect( extractSpanURLString({ span_id: 'a', description: 'GET http://service.io/item', data: { url: '/item', }, })?.toString() ).toEqual('http://service.io/item'); }); }); describe('extractQueryParameters', () => { it('If the URLs have no parameters or are malformed, returns nothing', () => { const URLs = [ new URL('http://service.io/items'), new URL('http://service.io/values'), ]; expect(extractQueryParameters(URLs)).toEqual({}); }); it('If the URLs have one changing parameter, returns it and its values', () => { const URLs = [ new URL('http://service.io/items?id=4'), new URL('http://service.io/items?id=5'), new URL('http://service.io/items?id=6'), ]; expect(extractQueryParameters(URLs)).toEqual({ id: ['4', '5', '6'], }); }); it('If the URLs have multiple changing parameters, returns them and their values', () => { const URLs = [ new URL('http://service.io/items?id=4&sort=down&filter=none'), new URL('http://service.io/items?id=5&sort=up&filter=none'), new URL('http://service.io/items?id=6&sort=up&filter=none'), ]; expect(extractQueryParameters(URLs)).toEqual({ id: ['4', '5', '6'], sort: ['down', 'up'], filter: ['none'], }); }); }); }); describe('Slow DB Span', () => { const builder = new TransactionEventBuilder( 'a1', '/', IssueType.PERFORMANCE_SLOW_DB_QUERY ); const parentSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 200, op: 'pageload', problemSpan: ProblemSpan.PARENT, }); parentSpan.addChild({ startTimestamp: 10, endTimestamp: 10100, op: 'db', description: 'SELECT pokemon FROM pokedex', problemSpan: ProblemSpan.OFFENDER, }); builder.addSpan(parentSpan); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Slow DB Query'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.slow-db-query') ).toHaveTextContent('SELECT pokemon FROM pokedex'); expect(screen.getByRole('cell', {name: 'Duration Impact'})).toBeInTheDocument(); }); }); describe('Render Blocking Asset', () => { const builder = new TransactionEventBuilder( 'a1', '/', IssueType.PERFORMANCE_RENDER_BLOCKING_ASSET, { duration: 3, fcp: 2500, } ); const offenderSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 1.0, op: 'resource.script', description: 'https://example.com/resource.js', problemSpan: ProblemSpan.OFFENDER, }); builder.addSpan(offenderSpan); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Slow Resource Span'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.slow-resource-span') ).toHaveTextContent('resource.script - https://example.com/resource.js'); expect(screen.getByRole('cell', {name: 'FCP Delay'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.fcp-delay') ).toHaveTextContent('1s (40% of 2.50s)'); expect(screen.getByRole('cell', {name: 'Duration Impact'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.duration-impact') ).toHaveTextContent('33% (1s/3.00s'); }); }); describe('Uncompressed Asset', () => { const builder = new TransactionEventBuilder( 'a1', '/', IssueType.PERFORMANCE_UNCOMPRESSED_ASSET, { duration: 0.931, // in seconds } ); const offenderSpan = new MockSpan({ startTimestamp: 0, endTimestamp: 0.487, // in seconds op: 'resource.script', description: 'https://example.com/resource.js', problemSpan: ProblemSpan.OFFENDER, data: { 'Encoded Body Size': 31041901, }, }); builder.addSpan(offenderSpan); it('Renders relevant fields', () => { render(); expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.transaction') ).toHaveTextContent('/'); expect(screen.getByRole('cell', {name: 'Slow Resource Span'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.slow-resource-span') ).toHaveTextContent('resource.script - https://example.com/resource.js'); expect(screen.getByRole('cell', {name: 'Asset Size'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.asset-size') ).toHaveTextContent('29.6 MiB (31041901 B)'); expect(screen.getByRole('cell', {name: 'Duration Impact'})).toBeInTheDocument(); expect( screen.getByTestId('span-evidence-key-value-list.duration-impact') ).toHaveTextContent('52% (487ms/931ms)'); }); }); });