utils.spec.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355
  1. import {RawReplayErrorFixture} from 'sentry-fixture/replay/error';
  2. import {ReplayRequestFrameFixture} from 'sentry-fixture/replay/replaySpanFrameData';
  3. import {ReplayRecordFixture} from 'sentry-fixture/replayRecord';
  4. import {
  5. countColumns,
  6. findVideoSegmentIndex,
  7. flattenFrames,
  8. getFramesByColumn,
  9. } from 'sentry/components/replays/utils';
  10. import hydrateErrors from 'sentry/utils/replays/hydrateErrors';
  11. import hydrateSpans from 'sentry/utils/replays/hydrateSpans';
  12. const SECOND = 1000;
  13. describe('countColumns', () => {
  14. it('should divide 27s by 2700px to find twentyseven 1s columns, with some fraction remaining', () => {
  15. // 2700 allows for up to 27 columns at 100px wide.
  16. // That is what we'd need if we were to render at `1s` granularity, so we can.
  17. const width = 2700;
  18. const duration = 27 * SECOND;
  19. const minWidth = 100;
  20. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  21. expect(timespan).toBe(1 * SECOND);
  22. expect(cols).toBe(27);
  23. expect(remaining).toBe(0);
  24. });
  25. it('should divide 27s by 2699px to find five 5s columns, with some fraction remaining', () => {
  26. // 2699px allows for up to 26 columns at 100px wide, with 99px leftover.
  27. // That is less than the 27 cols we'd need if we were to render at `1s` granularity.
  28. // So instead we get 5 cols (wider than 100px) at 5s granularity, and some extra space is remaining.
  29. const width = 2699;
  30. const duration = 27 * SECOND;
  31. const minWidth = 100;
  32. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  33. expect(timespan).toBe(5 * SECOND);
  34. expect(cols).toBe(5);
  35. expect(remaining).toBe(0.4);
  36. });
  37. it('should divide 27s by 600px to find five 5s columns, with some fraction column remaining', () => {
  38. // 600px allows for 6 columns at 100px wide to fix within it
  39. // That allows us to get 5 cols (100px wide) at 5s granularity, and an extra 100px for the remainder
  40. const width = 600;
  41. const duration = 27 * SECOND;
  42. const minWidth = 100;
  43. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  44. expect(timespan).toBe(5 * SECOND);
  45. expect(cols).toBe(5);
  46. expect(remaining).toBe(0.4);
  47. });
  48. it('should divide 27s by 599px to find five 2s columns, with some fraction column remaining', () => {
  49. // 599px allows for 5 columns at 100px wide, and 99px remaining.
  50. // That allows us to get 2 cols (100px wide) at 10s granularity, and an extra px for the remainder
  51. const width = 599;
  52. const duration = 27 * SECOND;
  53. const minWidth = 100;
  54. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  55. expect(timespan).toBe(10 * SECOND);
  56. expect(cols).toBe(2);
  57. expect(remaining).toBe(0.7);
  58. });
  59. });
  60. describe('getFramesByColumn', () => {
  61. const durationMs = 25710; // milliseconds
  62. const {
  63. errorFrames: [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5],
  64. } = hydrateErrors(
  65. ReplayRecordFixture({
  66. started_at: new Date('2022-04-14T14:19:47.326000Z'),
  67. }),
  68. [
  69. RawReplayErrorFixture({
  70. timestamp: new Date('2022-04-14T14:19:47.326000Z'),
  71. }),
  72. RawReplayErrorFixture({
  73. timestamp: new Date('2022-04-14T14:19:49.249000Z'),
  74. }),
  75. RawReplayErrorFixture({
  76. timestamp: new Date('2022-04-14T14:19:51.512000Z'),
  77. }),
  78. RawReplayErrorFixture({
  79. timestamp: new Date('2022-04-14T14:19:57.326000Z'),
  80. }),
  81. RawReplayErrorFixture({
  82. timestamp: new Date('2022-04-14T14:20:13.036000Z'),
  83. }),
  84. ]
  85. );
  86. it('should return an empty list when no crumbs exist', () => {
  87. const columnCount = 3;
  88. const columns = getFramesByColumn(durationMs, [], columnCount);
  89. const expectedEntries = [];
  90. expect(columns).toEqual(new Map(expectedEntries));
  91. });
  92. it('should put a crumbs in the first and last buckets', () => {
  93. const columnCount = 3;
  94. const columns = getFramesByColumn(durationMs, [CRUMB_1!, CRUMB_5!], columnCount);
  95. expect(columns).toEqual(
  96. new Map([
  97. [1, [CRUMB_1!]],
  98. [3, [CRUMB_5!]],
  99. ])
  100. );
  101. });
  102. it('should group crumbs by bucket', () => {
  103. // 6 columns gives is 5s granularity
  104. const columnCount = 6;
  105. const columns = getFramesByColumn(
  106. durationMs,
  107. [CRUMB_1!, CRUMB_2!, CRUMB_3!, CRUMB_4!, CRUMB_5!],
  108. columnCount
  109. );
  110. expect(columns).toEqual(
  111. new Map([
  112. [1, [CRUMB_1, CRUMB_2, CRUMB_3]],
  113. [2, [CRUMB_4]],
  114. [6, [CRUMB_5]],
  115. ])
  116. );
  117. });
  118. });
  119. describe('flattenFrames', () => {
  120. it('should return an empty array if there ar eno spans', () => {
  121. expect(flattenFrames([])).toStrictEqual([]);
  122. });
  123. it('should return the FlattenedSpanRange for a single span', () => {
  124. const frames = hydrateSpans(ReplayRecordFixture(), [
  125. ReplayRequestFrameFixture({
  126. op: 'resource.fetch',
  127. startTimestamp: new Date(10000),
  128. endTimestamp: new Date(30000),
  129. }),
  130. ]);
  131. expect(flattenFrames(frames)).toStrictEqual([
  132. {
  133. duration: 20000,
  134. endTimestamp: 30000,
  135. frameCount: 1,
  136. startTimestamp: 10000,
  137. },
  138. ]);
  139. });
  140. it('should return two non-overlapping spans', () => {
  141. const frames = hydrateSpans(ReplayRecordFixture(), [
  142. ReplayRequestFrameFixture({
  143. op: 'resource.fetch',
  144. startTimestamp: new Date(10000),
  145. endTimestamp: new Date(30000),
  146. }),
  147. ReplayRequestFrameFixture({
  148. op: 'resource.fetch',
  149. startTimestamp: new Date(60000),
  150. endTimestamp: new Date(90000),
  151. }),
  152. ]);
  153. expect(flattenFrames(frames)).toStrictEqual([
  154. {
  155. duration: 20000,
  156. endTimestamp: 30000,
  157. frameCount: 1,
  158. startTimestamp: 10000,
  159. },
  160. {
  161. duration: 30000,
  162. endTimestamp: 90000,
  163. frameCount: 1,
  164. startTimestamp: 60000,
  165. },
  166. ]);
  167. });
  168. it('should merge two overlapping spans', () => {
  169. const frames = hydrateSpans(ReplayRecordFixture(), [
  170. ReplayRequestFrameFixture({
  171. op: 'resource.fetch',
  172. startTimestamp: new Date(10000),
  173. endTimestamp: new Date(30000),
  174. }),
  175. ReplayRequestFrameFixture({
  176. op: 'resource.fetch',
  177. startTimestamp: new Date(20000),
  178. endTimestamp: new Date(40000),
  179. }),
  180. ]);
  181. expect(flattenFrames(frames)).toStrictEqual([
  182. {
  183. duration: 30000,
  184. endTimestamp: 40000,
  185. frameCount: 2,
  186. startTimestamp: 10000,
  187. },
  188. ]);
  189. });
  190. it('should merge overlapping spans that are not first in the list', () => {
  191. const frames = hydrateSpans(ReplayRecordFixture(), [
  192. ReplayRequestFrameFixture({
  193. op: 'resource.fetch',
  194. startTimestamp: new Date(0),
  195. endTimestamp: new Date(1000),
  196. }),
  197. ReplayRequestFrameFixture({
  198. op: 'resource.fetch',
  199. startTimestamp: new Date(10000),
  200. endTimestamp: new Date(30000),
  201. }),
  202. ReplayRequestFrameFixture({
  203. op: 'resource.fetch',
  204. startTimestamp: new Date(20000),
  205. endTimestamp: new Date(40000),
  206. }),
  207. ]);
  208. expect(flattenFrames(frames)).toStrictEqual([
  209. {
  210. duration: 1000,
  211. endTimestamp: 1000,
  212. frameCount: 1,
  213. startTimestamp: 0,
  214. },
  215. {
  216. duration: 30000,
  217. endTimestamp: 40000,
  218. frameCount: 2,
  219. startTimestamp: 10000,
  220. },
  221. ]);
  222. });
  223. });
  224. describe('findVideoSegmentIndex', () => {
  225. const segments = [
  226. {
  227. id: 0,
  228. timestamp: 0,
  229. duration: 5000,
  230. },
  231. // no gap
  232. {
  233. id: 1,
  234. timestamp: 5000,
  235. duration: 5000,
  236. },
  237. {
  238. id: 2,
  239. timestamp: 10_001,
  240. duration: 5000,
  241. },
  242. // 5 second gap
  243. {
  244. id: 3,
  245. timestamp: 20_000,
  246. duration: 5000,
  247. },
  248. // 5 second gap
  249. {
  250. id: 4,
  251. timestamp: 30_000,
  252. duration: 5000,
  253. },
  254. {
  255. id: 5,
  256. timestamp: 35_002,
  257. duration: 5000,
  258. },
  259. ];
  260. const trackList = segments.map(
  261. ({timestamp}, index) => [timestamp, index] as [ts: number, index: number]
  262. );
  263. it.each([
  264. ['matches starting timestamp', 0, 0],
  265. ['matches ending timestamp', 5000, 0],
  266. ['is inside of a segment (between timestamps)', 7500, 1],
  267. ['matches ending timestamp', 15_001, 2],
  268. ['is not inside of a segment', 16_000, 2],
  269. ['matches starting timestamp', 20_000, 3],
  270. ['is not inside of a segment', 27_500, 3],
  271. ['is not inside of a segment', 29_000, 3],
  272. ['is inside of a segment', 34_999, 4],
  273. ['is inside of a segment', 40_002, 5],
  274. ['is after the last segment', 50_000, 5],
  275. ])(
  276. 'should find correct segment when target timestamp %s (%s)',
  277. (_desc, targetTimestamp, expected) => {
  278. expect(findVideoSegmentIndex(trackList, segments, targetTimestamp)).toEqual(
  279. expected
  280. );
  281. }
  282. );
  283. it('returns first segment if target timestamp is before the first segment when there is only a single attachment', () => {
  284. const segments2 = [
  285. {
  286. id: 0,
  287. timestamp: 5000,
  288. duration: 5000,
  289. },
  290. ];
  291. const trackList2 = segments2.map(
  292. ({timestamp}, index) => [timestamp, index] as [ts: number, index: number]
  293. );
  294. expect(findVideoSegmentIndex(trackList2, segments2, 1000)).toBe(-1);
  295. });
  296. it('returns first segment if target timestamp is before the first segment', () => {
  297. const segments2 = [
  298. {
  299. id: 0,
  300. timestamp: 5000,
  301. duration: 5000,
  302. },
  303. {
  304. id: 1,
  305. timestamp: 10000,
  306. duration: 5000,
  307. },
  308. {
  309. id: 2,
  310. timestamp: 15000,
  311. duration: 5000,
  312. },
  313. {
  314. id: 3,
  315. timestamp: 25000,
  316. duration: 5000,
  317. },
  318. ];
  319. const trackList2 = segments2.map(
  320. ({timestamp}, index) => [timestamp, index] as [ts: number, index: number]
  321. );
  322. expect(findVideoSegmentIndex(trackList2, segments2, 1000)).toBe(-1);
  323. });
  324. });