spanEvidenceKeyValueList.spec.tsx 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. import {
  2. MockSpan,
  3. ProblemSpan,
  4. TransactionEventBuilder,
  5. } from 'sentry-test/performance/utils';
  6. import {render, screen} from 'sentry-test/reactTestingLibrary';
  7. import {EntryType, IssueType} from 'sentry/types';
  8. import {
  9. extractQueryParameters,
  10. extractSpanURLString,
  11. SpanEvidenceKeyValueList,
  12. } from './spanEvidenceKeyValueList';
  13. describe('SpanEvidenceKeyValueList', () => {
  14. describe('N+1 Database Queries', () => {
  15. const builder = new TransactionEventBuilder('a1', '/');
  16. const parentSpan = new MockSpan({
  17. startTimestamp: 0,
  18. endTimestamp: 0.2,
  19. op: 'http.server',
  20. problemSpan: ProblemSpan.PARENT,
  21. });
  22. parentSpan.addChild({
  23. startTimestamp: 0.01,
  24. endTimestamp: 2.1,
  25. op: 'db',
  26. description: 'SELECT * FROM books',
  27. problemSpan: ProblemSpan.OFFENDER,
  28. });
  29. parentSpan.addChild({
  30. startTimestamp: 2.1,
  31. endTimestamp: 4.0,
  32. op: 'db',
  33. description: 'SELECT * FROM books',
  34. problemSpan: ProblemSpan.OFFENDER,
  35. });
  36. builder.addSpan(parentSpan);
  37. it('Renders relevant fields', () => {
  38. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  39. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  40. expect(
  41. screen.getByTestId('span-evidence-key-value-list.transaction')
  42. ).toHaveTextContent('/');
  43. expect(screen.getByRole('cell', {name: 'Parent Span'})).toBeInTheDocument();
  44. expect(
  45. screen.getByTestId('span-evidence-key-value-list.parent-span')
  46. ).toHaveTextContent('http.server');
  47. expect(screen.getByRole('cell', {name: 'Repeating Spans (2)'})).toBeInTheDocument();
  48. expect(
  49. screen.getByTestId(/span-evidence-key-value-list.repeating-spans/)
  50. ).toHaveTextContent('db - SELECT * FROM books');
  51. expect(
  52. screen.queryByTestId('span-evidence-key-value-list.')
  53. ).not.toBeInTheDocument();
  54. expect(screen.queryByRole('cell', {name: 'Parameter'})).not.toBeInTheDocument();
  55. expect(
  56. screen.queryByTestId('span-evidence-key-value-list.problem-parameters')
  57. ).not.toBeInTheDocument();
  58. });
  59. });
  60. describe('MN+1 Database Queries', () => {
  61. const builder = new TransactionEventBuilder('a1', '/');
  62. const parentSpan = new MockSpan({
  63. startTimestamp: 0,
  64. endTimestamp: 0.2,
  65. op: 'http.server',
  66. problemSpan: ProblemSpan.PARENT,
  67. });
  68. parentSpan.addChild({
  69. startTimestamp: 0.01,
  70. endTimestamp: 2.1,
  71. op: 'db',
  72. description: 'SELECT * FROM books',
  73. problemSpan: ProblemSpan.OFFENDER,
  74. });
  75. parentSpan.addChild({
  76. startTimestamp: 2.1,
  77. endTimestamp: 4.0,
  78. op: 'db.sql.active_record',
  79. description: 'SELECT * FROM books WHERE id = %s',
  80. problemSpan: ProblemSpan.OFFENDER,
  81. });
  82. builder.addSpan(parentSpan);
  83. it('Renders relevant fields', () => {
  84. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  85. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  86. expect(
  87. screen.getByTestId('span-evidence-key-value-list.transaction')
  88. ).toHaveTextContent('/');
  89. expect(screen.getByRole('cell', {name: 'Parent Span'})).toBeInTheDocument();
  90. expect(
  91. screen.getByTestId('span-evidence-key-value-list.parent-span')
  92. ).toHaveTextContent('http.server');
  93. expect(screen.getByRole('cell', {name: 'Repeating Spans (2)'})).toBeInTheDocument();
  94. expect(
  95. screen.getByTestId('span-evidence-key-value-list.repeating-spans-2')
  96. ).toHaveTextContent('db - SELECT * FROM books');
  97. expect(screen.getByTestId('span-evidence-key-value-list.')).toHaveTextContent(
  98. 'db.sql.active_record - SELECT * FROM books WHERE id = %s'
  99. );
  100. expect(screen.queryByRole('cell', {name: 'Parameter'})).not.toBeInTheDocument();
  101. expect(
  102. screen.queryByTestId('span-evidence-key-value-list.problem-parameters')
  103. ).not.toBeInTheDocument();
  104. });
  105. });
  106. describe('Consecutive DB Queries', () => {
  107. const builder = new TransactionEventBuilder(
  108. 'a1',
  109. '/',
  110. IssueType.PERFORMANCE_CONSECUTIVE_DB_QUERIES
  111. );
  112. const parentSpan = new MockSpan({
  113. startTimestamp: 0,
  114. endTimestamp: 0.65,
  115. op: 'http.server',
  116. problemSpan: ProblemSpan.PARENT,
  117. });
  118. parentSpan.addChild({
  119. startTimestamp: 0.1,
  120. endTimestamp: 0.2,
  121. op: 'db',
  122. description: 'SELECT * FROM USERS LIMIT 100',
  123. problemSpan: ProblemSpan.CAUSE,
  124. });
  125. parentSpan.addChild({
  126. startTimestamp: 0.2,
  127. endTimestamp: 0.4,
  128. op: 'db',
  129. description: 'SELECT COUNT(*) FROM USERS',
  130. problemSpan: [ProblemSpan.CAUSE, ProblemSpan.OFFENDER],
  131. });
  132. parentSpan.addChild({
  133. startTimestamp: 0.4,
  134. endTimestamp: 0.6,
  135. op: 'db',
  136. description: 'SELECT COUNT(*) FROM ITEMS',
  137. problemSpan: [ProblemSpan.CAUSE, ProblemSpan.OFFENDER],
  138. });
  139. builder.addSpan(parentSpan);
  140. it('Renders relevant fields', () => {
  141. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  142. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  143. expect(
  144. screen.getByTestId('span-evidence-key-value-list.transaction')
  145. ).toHaveTextContent('/');
  146. expect(screen.getByRole('cell', {name: 'Starting Span'})).toBeInTheDocument();
  147. expect(
  148. screen.getByTestId('span-evidence-key-value-list.starting-span')
  149. ).toHaveTextContent('db - SELECT * FROM USERS LIMIT 100');
  150. expect(screen.queryAllByRole('cell', {name: 'Parallelizable Spans'}).length).toBe(
  151. 1
  152. );
  153. const parallelizableSpanKeyValue = screen.getByTestId(
  154. 'span-evidence-key-value-list.parallelizable-spans'
  155. );
  156. expect(parallelizableSpanKeyValue).toHaveTextContent(
  157. 'db - SELECT COUNT(*) FROM USERS'
  158. );
  159. expect(parallelizableSpanKeyValue).toHaveTextContent(
  160. 'db - SELECT COUNT(*) FROM ITEMS'
  161. );
  162. expect(
  163. screen.getByTestId('span-evidence-key-value-list.duration-impact')
  164. ).toHaveTextContent('46% (300ms/650ms)');
  165. });
  166. });
  167. describe('N+1 API Calls', () => {
  168. const builder = new TransactionEventBuilder(
  169. 'a1',
  170. '/',
  171. IssueType.PERFORMANCE_N_PLUS_ONE_API_CALLS
  172. );
  173. const parentSpan = new MockSpan({
  174. startTimestamp: 0,
  175. endTimestamp: 200,
  176. op: 'pageload',
  177. problemSpan: ProblemSpan.PARENT,
  178. });
  179. parentSpan.addChild({
  180. startTimestamp: 10,
  181. endTimestamp: 2100,
  182. op: 'http.client',
  183. description: 'GET /book/?book_id=7&sort=up',
  184. problemSpan: ProblemSpan.OFFENDER,
  185. });
  186. parentSpan.addChild({
  187. startTimestamp: 10,
  188. endTimestamp: 2100,
  189. op: 'http.client',
  190. description: 'GET /book/?book_id=8&sort=down',
  191. problemSpan: ProblemSpan.OFFENDER,
  192. });
  193. builder.addSpan(parentSpan);
  194. builder.addEntry(
  195. TestStubs.EventEntry({
  196. type: EntryType.REQUEST,
  197. data: {
  198. url: 'http://some.service.io',
  199. },
  200. })
  201. );
  202. it('Renders relevant fields', () => {
  203. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  204. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  205. expect(
  206. screen.getByTestId('span-evidence-key-value-list.transaction')
  207. ).toHaveTextContent('/');
  208. expect(screen.getByRole('cell', {name: 'Repeating Spans (2)'})).toBeInTheDocument();
  209. expect(
  210. screen.getByTestId(/span-evidence-key-value-list.repeating-spans/)
  211. ).toHaveTextContent('/book/[Parameters]');
  212. expect(screen.queryByRole('cell', {name: 'Parameters'})).toBeInTheDocument();
  213. const parametersKeyValue = screen.getByTestId(
  214. 'span-evidence-key-value-list.parameters'
  215. );
  216. expect(parametersKeyValue).toHaveTextContent('book_id:{7,8}');
  217. expect(parametersKeyValue).toHaveTextContent('sort:{up,down}');
  218. });
  219. describe('extractSpanURLString', () => {
  220. it('Tries to pull a URL from the span data', () => {
  221. expect(
  222. extractSpanURLString({
  223. span_id: 'a',
  224. data: {
  225. url: 'http://service.io',
  226. },
  227. })?.toString()
  228. ).toEqual('http://service.io/');
  229. });
  230. it('Pulls out a relative URL if a base is provided', () => {
  231. expect(
  232. extractSpanURLString(
  233. {
  234. span_id: 'a',
  235. data: {
  236. url: '/item',
  237. },
  238. },
  239. 'http://service.io'
  240. )?.toString()
  241. ).toEqual('http://service.io/item');
  242. });
  243. it('Falls back to span description if URL is faulty', () => {
  244. expect(
  245. extractSpanURLString({
  246. span_id: 'a',
  247. description: 'GET http://service.io/item',
  248. data: {
  249. url: '/item',
  250. },
  251. })?.toString()
  252. ).toEqual('http://service.io/item');
  253. });
  254. });
  255. describe('extractQueryParameters', () => {
  256. it('If the URLs have no parameters or are malformed, returns nothing', () => {
  257. const URLs = [
  258. new URL('http://service.io/items'),
  259. new URL('http://service.io/values'),
  260. ];
  261. expect(extractQueryParameters(URLs)).toEqual({});
  262. });
  263. it('If the URLs have one changing parameter, returns it and its values', () => {
  264. const URLs = [
  265. new URL('http://service.io/items?id=4'),
  266. new URL('http://service.io/items?id=5'),
  267. new URL('http://service.io/items?id=6'),
  268. ];
  269. expect(extractQueryParameters(URLs)).toEqual({
  270. id: ['4', '5', '6'],
  271. });
  272. });
  273. it('If the URLs have multiple changing parameters, returns them and their values', () => {
  274. const URLs = [
  275. new URL('http://service.io/items?id=4&sort=down&filter=none'),
  276. new URL('http://service.io/items?id=5&sort=up&filter=none'),
  277. new URL('http://service.io/items?id=6&sort=up&filter=none'),
  278. ];
  279. expect(extractQueryParameters(URLs)).toEqual({
  280. id: ['4', '5', '6'],
  281. sort: ['down', 'up'],
  282. filter: ['none'],
  283. });
  284. });
  285. });
  286. });
  287. describe('Slow DB Span', () => {
  288. const builder = new TransactionEventBuilder(
  289. 'a1',
  290. '/',
  291. IssueType.PERFORMANCE_SLOW_DB_QUERY
  292. );
  293. const parentSpan = new MockSpan({
  294. startTimestamp: 0,
  295. endTimestamp: 200,
  296. op: 'pageload',
  297. problemSpan: ProblemSpan.PARENT,
  298. });
  299. parentSpan.addChild({
  300. startTimestamp: 10,
  301. endTimestamp: 10100,
  302. op: 'db',
  303. description: 'SELECT pokemon FROM pokedex',
  304. problemSpan: ProblemSpan.OFFENDER,
  305. });
  306. builder.addSpan(parentSpan);
  307. it('Renders relevant fields', () => {
  308. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  309. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  310. expect(
  311. screen.getByTestId('span-evidence-key-value-list.transaction')
  312. ).toHaveTextContent('/');
  313. expect(screen.getByRole('cell', {name: 'Slow DB Query'})).toBeInTheDocument();
  314. expect(
  315. screen.getByTestId('span-evidence-key-value-list.slow-db-query')
  316. ).toHaveTextContent('SELECT pokemon FROM pokedex');
  317. expect(screen.getByRole('cell', {name: 'Duration Impact'})).toBeInTheDocument();
  318. });
  319. });
  320. describe('Render Blocking Asset', () => {
  321. const builder = new TransactionEventBuilder(
  322. 'a1',
  323. '/',
  324. IssueType.PERFORMANCE_RENDER_BLOCKING_ASSET,
  325. {
  326. duration: 3,
  327. fcp: 2500,
  328. }
  329. );
  330. const offenderSpan = new MockSpan({
  331. startTimestamp: 0,
  332. endTimestamp: 1.0,
  333. op: 'resource.script',
  334. description: 'https://example.com/resource.js',
  335. problemSpan: ProblemSpan.OFFENDER,
  336. });
  337. builder.addSpan(offenderSpan);
  338. it('Renders relevant fields', () => {
  339. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  340. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  341. expect(
  342. screen.getByTestId('span-evidence-key-value-list.transaction')
  343. ).toHaveTextContent('/');
  344. expect(screen.getByRole('cell', {name: 'Slow Resource Span'})).toBeInTheDocument();
  345. expect(
  346. screen.getByTestId('span-evidence-key-value-list.slow-resource-span')
  347. ).toHaveTextContent('resource.script - https://example.com/resource.js');
  348. expect(screen.getByRole('cell', {name: 'FCP Delay'})).toBeInTheDocument();
  349. expect(
  350. screen.getByTestId('span-evidence-key-value-list.fcp-delay')
  351. ).toHaveTextContent('1s (40% of 2.50s)');
  352. expect(screen.getByRole('cell', {name: 'Duration Impact'})).toBeInTheDocument();
  353. expect(
  354. screen.getByTestId('span-evidence-key-value-list.duration-impact')
  355. ).toHaveTextContent('33% (1s/3.00s');
  356. });
  357. });
  358. describe('Uncompressed Asset', () => {
  359. const builder = new TransactionEventBuilder(
  360. 'a1',
  361. '/',
  362. IssueType.PERFORMANCE_UNCOMPRESSED_ASSET,
  363. {
  364. duration: 0.931, // in seconds
  365. }
  366. );
  367. const offenderSpan = new MockSpan({
  368. startTimestamp: 0,
  369. endTimestamp: 0.487, // in seconds
  370. op: 'resource.script',
  371. description: 'https://example.com/resource.js',
  372. problemSpan: ProblemSpan.OFFENDER,
  373. data: {
  374. 'Encoded Body Size': 31041901,
  375. },
  376. });
  377. builder.addSpan(offenderSpan);
  378. it('Renders relevant fields', () => {
  379. render(<SpanEvidenceKeyValueList event={builder.getEvent()} />);
  380. expect(screen.getByRole('cell', {name: 'Transaction'})).toBeInTheDocument();
  381. expect(
  382. screen.getByTestId('span-evidence-key-value-list.transaction')
  383. ).toHaveTextContent('/');
  384. expect(screen.getByRole('cell', {name: 'Slow Resource Span'})).toBeInTheDocument();
  385. expect(
  386. screen.getByTestId('span-evidence-key-value-list.slow-resource-span')
  387. ).toHaveTextContent('resource.script - https://example.com/resource.js');
  388. expect(screen.getByRole('cell', {name: 'Asset Size'})).toBeInTheDocument();
  389. expect(
  390. screen.getByTestId('span-evidence-key-value-list.asset-size')
  391. ).toHaveTextContent('29.6 MiB (31041901 B)');
  392. expect(screen.getByRole('cell', {name: 'Duration Impact'})).toBeInTheDocument();
  393. expect(
  394. screen.getByTestId('span-evidence-key-value-list.duration-impact')
  395. ).toHaveTextContent('52% (487ms/931ms)');
  396. });
  397. });
  398. });