utils.spec.tsx 11 KB

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