utils.spec.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  1. import {Event} from 'sentry/types/event';
  2. import {TraceFull} from 'sentry/utils/performance/quickTrace/types';
  3. import {
  4. flattenRelevantPaths,
  5. parseQuickTrace,
  6. } from 'sentry/utils/performance/quickTrace/utils';
  7. type Position = {
  8. generation: number;
  9. offset: number;
  10. };
  11. function hexCharFor(x: number): string {
  12. x = x % 16;
  13. if (x < 10) {
  14. return String(x);
  15. }
  16. return String.fromCharCode('a'.charCodeAt(0) + x - 10);
  17. }
  18. function generateId(prefix: string, {generation, offset}: Position) {
  19. const s = `${hexCharFor(generation)}${hexCharFor(offset)}`;
  20. return `${prefix}:${Array(7).join(s)}`;
  21. }
  22. function generateEventId({generation, offset}: Position) {
  23. return generateId('e', {generation, offset});
  24. }
  25. function generateSpanId({generation, offset}: Position) {
  26. return generateId('s', {generation, offset});
  27. }
  28. function generateTransactionName({generation, offset}: Position) {
  29. return `transaction-${generation}-${offset}`;
  30. }
  31. function generateProjectSlug({generation, offset}: Position) {
  32. const c = hexCharFor(generation);
  33. return `project-${c.toUpperCase()}-${offset}`;
  34. }
  35. function computePosition(index: number) {
  36. index += 1;
  37. const generation = Math.floor(Math.log2(index));
  38. const offset = index - 2 ** generation;
  39. return {generation, offset};
  40. }
  41. function generateTransactionLite({
  42. generation,
  43. offset,
  44. }: {
  45. generation: number;
  46. offset: number;
  47. }) {
  48. const position = {generation, offset};
  49. const parentPosition = {
  50. generation: generation - 1,
  51. offset: Math.floor(offset / 2),
  52. };
  53. return {
  54. event_id: generateEventId(position),
  55. generation,
  56. span_id: generateSpanId(position),
  57. transaction: generateTransactionName(position),
  58. 'transaction.duration': 0,
  59. project_id: generation, // just use generation as project id
  60. project_slug: generateProjectSlug(position),
  61. parent_event_id: generation <= 0 ? null : generateEventId(parentPosition),
  62. parent_span_id: generation <= 0 ? null : generateSpanId(parentPosition),
  63. performance_issues: [],
  64. errors: [],
  65. };
  66. }
  67. function generateTransaction(opts: {depth: number; index: number}): TraceFull {
  68. const {index, depth = -1} = opts;
  69. const {generation, offset} = computePosition(index);
  70. return {
  71. ...generateTransactionLite({generation, offset}),
  72. errors: [],
  73. children: Array(depth <= 0 || generation >= depth - 1 ? 0 : 2)
  74. .fill(null)
  75. .map((_, i) =>
  76. generateTransaction({
  77. index: 2 * index + i + 1,
  78. depth,
  79. })
  80. ),
  81. /**
  82. * These timestamps aren't used in tests here, just adding them to pass
  83. * the type checking.
  84. */
  85. 'transaction.duration': 0,
  86. };
  87. }
  88. function generateTrace(depth = 1): TraceFull {
  89. if (depth < 1) {
  90. throw new Error('Minimum depth is 1!');
  91. }
  92. return generateTransaction({
  93. depth,
  94. index: 0,
  95. });
  96. }
  97. function generateEventSelector(position: Position, eventType: string): Event {
  98. return {id: generateEventId(position), type: eventType} as Event;
  99. }
  100. describe('Quick Trace Utils', function () {
  101. describe('flattenRelevantPaths', function () {
  102. it('flattens trace without the expected event', function () {
  103. const trace = generateTrace(1);
  104. const current = {id: 'you cant find me'} as Event;
  105. expect(() => flattenRelevantPaths(current, trace)).toThrow(
  106. 'No relevant path exists!'
  107. );
  108. });
  109. it('flattens a single transaction trace', function () {
  110. const trace = generateTrace(1);
  111. const current = generateEventSelector({generation: 0, offset: 0}, 'transaction');
  112. const relevantPath = flattenRelevantPaths(current, trace);
  113. expect(relevantPath).toMatchObject([
  114. generateTransactionLite({generation: 0, offset: 0}),
  115. ]);
  116. });
  117. it('flattens trace from the leaf', function () {
  118. const trace = generateTrace(3);
  119. const current = generateEventSelector({generation: 2, offset: 3}, 'transaction');
  120. const relevantPath = flattenRelevantPaths(current, trace);
  121. expect(relevantPath).toMatchObject([
  122. generateTransactionLite({generation: 0, offset: 0}),
  123. generateTransactionLite({generation: 1, offset: 1}),
  124. generateTransactionLite({generation: 2, offset: 3}),
  125. ]);
  126. });
  127. it('flattens trace from the middle', function () {
  128. const trace = generateTrace(3);
  129. const current = generateEventSelector({generation: 1, offset: 1}, 'transaction');
  130. const relevantPath = flattenRelevantPaths(current, trace);
  131. expect(relevantPath).toMatchObject([
  132. generateTransactionLite({generation: 0, offset: 0}),
  133. generateTransactionLite({generation: 1, offset: 1}),
  134. generateTransactionLite({generation: 2, offset: 2}),
  135. generateTransactionLite({generation: 2, offset: 3}),
  136. ]);
  137. });
  138. it('flattens trace from the root', function () {
  139. const trace = generateTrace(3);
  140. const current = generateEventSelector({generation: 0, offset: 0}, 'transaction');
  141. const relevantPath = flattenRelevantPaths(current, trace);
  142. expect(relevantPath).toMatchObject([
  143. generateTransactionLite({generation: 0, offset: 0}),
  144. generateTransactionLite({generation: 1, offset: 0}),
  145. generateTransactionLite({generation: 1, offset: 1}),
  146. generateTransactionLite({generation: 2, offset: 0}),
  147. generateTransactionLite({generation: 2, offset: 1}),
  148. generateTransactionLite({generation: 2, offset: 2}),
  149. generateTransactionLite({generation: 2, offset: 3}),
  150. ]);
  151. });
  152. });
  153. describe('parseQuickTrace', function () {
  154. const organization = TestStubs.Organization();
  155. it('parses empty trace', function () {
  156. const current = generateEventSelector({generation: 0, offset: 0}, 'transaction');
  157. expect(() =>
  158. parseQuickTrace({type: 'empty', trace: []}, current, organization)
  159. ).toThrow('Current event not in trace navigator');
  160. });
  161. describe('partial trace', function () {
  162. it('parses correctly without the expected event', function () {
  163. const relevantPath = [generateTransactionLite({generation: 0, offset: 0})];
  164. const current = generateEventSelector({generation: 1, offset: 0}, 'transaction');
  165. expect(() =>
  166. parseQuickTrace({type: 'partial', trace: relevantPath}, current, organization)
  167. ).toThrow('Current event not in trace navigator');
  168. });
  169. it('parses only the current event', function () {
  170. const relevantPath = [generateTransactionLite({generation: 0, offset: 0})];
  171. const current = generateEventSelector({generation: 0, offset: 0}, 'transaction');
  172. const parsedQuickTrace = parseQuickTrace(
  173. {type: 'partial', trace: relevantPath},
  174. current,
  175. organization
  176. );
  177. expect(parsedQuickTrace).toEqual({
  178. root: null,
  179. ancestors: null,
  180. parent: null,
  181. current: generateTransactionLite({generation: 0, offset: 0}),
  182. children: [],
  183. descendants: null,
  184. });
  185. });
  186. it('parses current with only parent', function () {
  187. const relevantPath = [
  188. generateTransactionLite({generation: 0, offset: 0}),
  189. generateTransactionLite({generation: 1, offset: 0}),
  190. ];
  191. const current = generateEventSelector({generation: 1, offset: 0}, 'transaction');
  192. const parsedQuickTrace = parseQuickTrace(
  193. {type: 'partial', trace: relevantPath},
  194. current,
  195. organization
  196. );
  197. expect(parsedQuickTrace).toEqual({
  198. root: null,
  199. ancestors: null,
  200. parent: generateTransactionLite({generation: 0, offset: 0}),
  201. current: generateTransactionLite({generation: 1, offset: 0}),
  202. children: [],
  203. descendants: null,
  204. });
  205. });
  206. it('parses current with only root', function () {
  207. const relevantPath = [
  208. generateTransactionLite({generation: 0, offset: 0}),
  209. generateTransactionLite({generation: 2, offset: 0}),
  210. ];
  211. const current = generateEventSelector({generation: 2, offset: 0}, 'transaction');
  212. const parsedQuickTrace = parseQuickTrace(
  213. {type: 'partial', trace: relevantPath},
  214. current,
  215. organization
  216. );
  217. expect(parsedQuickTrace).toEqual({
  218. root: generateTransactionLite({generation: 0, offset: 0}),
  219. ancestors: null,
  220. parent: null,
  221. current: generateTransactionLite({generation: 2, offset: 0}),
  222. children: [],
  223. descendants: null,
  224. });
  225. });
  226. it('parses current with only children', function () {
  227. const relevantPath = [
  228. generateTransactionLite({generation: 0, offset: 0}),
  229. generateTransactionLite({generation: 1, offset: 0}),
  230. generateTransactionLite({generation: 1, offset: 1}),
  231. ];
  232. const current = generateEventSelector({generation: 0, offset: 0}, 'transaction');
  233. const parsedQuickTrace = parseQuickTrace(
  234. {type: 'partial', trace: relevantPath},
  235. current,
  236. organization
  237. );
  238. expect(parsedQuickTrace).toEqual({
  239. root: null,
  240. ancestors: null,
  241. parent: null,
  242. current: generateTransactionLite({generation: 0, offset: 0}),
  243. children: [
  244. generateTransactionLite({generation: 1, offset: 0}),
  245. generateTransactionLite({generation: 1, offset: 1}),
  246. ],
  247. descendants: null,
  248. });
  249. });
  250. it('parses current with parent and children', function () {
  251. const relevantPath = [
  252. generateTransactionLite({generation: 0, offset: 0}),
  253. generateTransactionLite({generation: 1, offset: 1}),
  254. generateTransactionLite({generation: 2, offset: 2}),
  255. ];
  256. const current = generateEventSelector({generation: 1, offset: 1}, 'transaction');
  257. const parsedQuickTrace = parseQuickTrace(
  258. {type: 'partial', trace: relevantPath},
  259. current,
  260. organization
  261. );
  262. expect(parsedQuickTrace).toEqual({
  263. root: null,
  264. ancestors: null,
  265. parent: generateTransactionLite({generation: 0, offset: 0}),
  266. current: generateTransactionLite({generation: 1, offset: 1}),
  267. children: [generateTransactionLite({generation: 2, offset: 2})],
  268. descendants: null,
  269. });
  270. });
  271. it('parses current with root and children', function () {
  272. const relevantPath = [
  273. generateTransactionLite({generation: 0, offset: 0}),
  274. generateTransactionLite({generation: 2, offset: 2}),
  275. generateTransactionLite({generation: 3, offset: 4}),
  276. generateTransactionLite({generation: 3, offset: 5}),
  277. ];
  278. const current = generateEventSelector({generation: 2, offset: 2}, 'transaction');
  279. const parsedQuickTrace = parseQuickTrace(
  280. {type: 'partial', trace: relevantPath},
  281. current,
  282. organization
  283. );
  284. expect(parsedQuickTrace).toEqual({
  285. root: generateTransactionLite({generation: 0, offset: 0}),
  286. ancestors: null,
  287. parent: null,
  288. current: generateTransactionLite({generation: 2, offset: 2}),
  289. children: [
  290. generateTransactionLite({generation: 3, offset: 4}),
  291. generateTransactionLite({generation: 3, offset: 5}),
  292. ],
  293. descendants: null,
  294. });
  295. });
  296. });
  297. describe('full trace', function () {
  298. it('parses the full trace', function () {
  299. const trace = generateTrace(6);
  300. const current = generateEventSelector({generation: 3, offset: 0}, 'transaction');
  301. const relevantPath = flattenRelevantPaths(current, trace);
  302. const parsedQuickTrace = parseQuickTrace(
  303. {type: 'full', trace: relevantPath},
  304. current,
  305. organization
  306. );
  307. expect(parsedQuickTrace).toMatchObject({
  308. root: generateTransactionLite({generation: 0, offset: 0}),
  309. ancestors: [generateTransactionLite({generation: 1, offset: 0})],
  310. parent: generateTransactionLite({generation: 2, offset: 0}),
  311. current: generateTransactionLite({generation: 3, offset: 0}),
  312. children: [
  313. generateTransactionLite({generation: 4, offset: 0}),
  314. generateTransactionLite({generation: 4, offset: 1}),
  315. ],
  316. descendants: [
  317. generateTransactionLite({generation: 5, offset: 0}),
  318. generateTransactionLite({generation: 5, offset: 1}),
  319. generateTransactionLite({generation: 5, offset: 2}),
  320. generateTransactionLite({generation: 5, offset: 3}),
  321. ],
  322. });
  323. });
  324. it('parses full trace without ancestors', function () {
  325. const trace = generateTrace(5);
  326. const current = generateEventSelector({generation: 2, offset: 0}, 'transaction');
  327. const relevantPath = flattenRelevantPaths(current, trace);
  328. const parsedQuickTrace = parseQuickTrace(
  329. {type: 'full', trace: relevantPath},
  330. current,
  331. organization
  332. );
  333. expect(parsedQuickTrace).toMatchObject({
  334. root: generateTransactionLite({generation: 0, offset: 0}),
  335. ancestors: [],
  336. parent: generateTransactionLite({generation: 1, offset: 0}),
  337. current: generateTransactionLite({generation: 2, offset: 0}),
  338. children: [
  339. generateTransactionLite({generation: 3, offset: 0}),
  340. generateTransactionLite({generation: 3, offset: 1}),
  341. ],
  342. descendants: [
  343. generateTransactionLite({generation: 4, offset: 0}),
  344. generateTransactionLite({generation: 4, offset: 1}),
  345. generateTransactionLite({generation: 4, offset: 2}),
  346. generateTransactionLite({generation: 4, offset: 3}),
  347. ],
  348. });
  349. });
  350. it('parses full trace without descendants', function () {
  351. const trace = generateTrace(5);
  352. const current = generateEventSelector({generation: 3, offset: 0}, 'transaction');
  353. const relevantPath = flattenRelevantPaths(current, trace);
  354. const parsedQuickTrace = parseQuickTrace(
  355. {type: 'full', trace: relevantPath},
  356. current,
  357. organization
  358. );
  359. expect(parsedQuickTrace).toMatchObject({
  360. root: generateTransactionLite({generation: 0, offset: 0}),
  361. ancestors: [generateTransactionLite({generation: 1, offset: 0})],
  362. parent: generateTransactionLite({generation: 2, offset: 0}),
  363. current: generateTransactionLite({generation: 3, offset: 0}),
  364. children: [
  365. generateTransactionLite({generation: 4, offset: 0}),
  366. generateTransactionLite({generation: 4, offset: 1}),
  367. ],
  368. descendants: [],
  369. });
  370. });
  371. it('parses full trace without children descendants', function () {
  372. const trace = generateTrace(4);
  373. const current = generateEventSelector({generation: 3, offset: 0}, 'transaction');
  374. const relevantPath = flattenRelevantPaths(current, trace);
  375. const parsedQuickTrace = parseQuickTrace(
  376. {type: 'full', trace: relevantPath},
  377. current,
  378. organization
  379. );
  380. expect(parsedQuickTrace).toMatchObject({
  381. root: generateTransactionLite({generation: 0, offset: 0}),
  382. ancestors: [generateTransactionLite({generation: 1, offset: 0})],
  383. parent: generateTransactionLite({generation: 2, offset: 0}),
  384. current: generateTransactionLite({generation: 3, offset: 0}),
  385. children: [],
  386. descendants: [],
  387. });
  388. });
  389. });
  390. });
  391. });