utils.spec.tsx 15 KB

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