utils.spec.tsx 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  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. flattenFrames,
  8. formatTime,
  9. getFramesByColumn,
  10. showPlayerTime,
  11. } from 'sentry/components/replays/utils';
  12. import hydrateErrors from 'sentry/utils/replays/hydrateErrors';
  13. import hydrateSpans from 'sentry/utils/replays/hydrateSpans';
  14. const SECOND = 1000;
  15. describe('formatTime', () => {
  16. it.each([
  17. ['seconds', 15 * 1000, '00:15'],
  18. ['minutes', 2.5 * 60 * 1000, '02:30'],
  19. ['hours', 75 * 60 * 1000, '01:15:00'],
  20. ])('should format a %s long duration into a string', (_desc, duration, expected) => {
  21. expect(formatTime(duration)).toEqual(expected);
  22. });
  23. });
  24. describe('countColumns', () => {
  25. it('should divide 27s by 2700px to find twentyseven 1s columns, with some fraction remaining', () => {
  26. // 2700 allows for up to 27 columns at 100px wide.
  27. // That is what we'd need if we were to render at `1s` granularity, so we can.
  28. const width = 2700;
  29. const duration = 27 * SECOND;
  30. const minWidth = 100;
  31. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  32. expect(timespan).toBe(1 * SECOND);
  33. expect(cols).toBe(27);
  34. expect(remaining).toBe(0);
  35. });
  36. it('should divide 27s by 2699px to find five 5s columns, with some fraction remaining', () => {
  37. // 2699px allows for up to 26 columns at 100px wide, with 99px leftover.
  38. // That is less than the 27 cols we'd need if we were to render at `1s` granularity.
  39. // So instead we get 5 cols (wider than 100px) at 5s granularity, and some extra space is remaining.
  40. const width = 2699;
  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 600px to find five 5s columns, with some fraction column remaining', () => {
  49. // 600px allows for 6 columns at 100px wide to fix within it
  50. // That allows us to get 5 cols (100px wide) at 5s granularity, and an extra 100px for the remainder
  51. const width = 600;
  52. const duration = 27 * SECOND;
  53. const minWidth = 100;
  54. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  55. expect(timespan).toBe(5 * SECOND);
  56. expect(cols).toBe(5);
  57. expect(remaining).toBe(0.4);
  58. });
  59. it('should divide 27s by 599px to find five 2s columns, with some fraction column remaining', () => {
  60. // 599px allows for 5 columns at 100px wide, and 99px remaining.
  61. // That allows us to get 2 cols (100px wide) at 10s granularity, and an extra px for the remainder
  62. const width = 599;
  63. const duration = 27 * SECOND;
  64. const minWidth = 100;
  65. const {timespan, cols, remaining} = countColumns(duration, width, minWidth);
  66. expect(timespan).toBe(10 * SECOND);
  67. expect(cols).toBe(2);
  68. expect(remaining).toBe(0.7);
  69. });
  70. });
  71. describe('getFramesByColumn', () => {
  72. const durationMs = 25710; // milliseconds
  73. const [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5] = hydrateErrors(
  74. ReplayRecordFixture({
  75. started_at: new Date('2022-04-14T14:19:47.326000Z'),
  76. }),
  77. [
  78. RawReplayErrorFixture({
  79. timestamp: new Date('2022-04-14T14:19:47.326000Z'),
  80. }),
  81. RawReplayErrorFixture({
  82. timestamp: new Date('2022-04-14T14:19:49.249000Z'),
  83. }),
  84. RawReplayErrorFixture({
  85. timestamp: new Date('2022-04-14T14:19:51.512000Z'),
  86. }),
  87. RawReplayErrorFixture({
  88. timestamp: new Date('2022-04-14T14:19:57.326000Z'),
  89. }),
  90. RawReplayErrorFixture({
  91. timestamp: new Date('2022-04-14T14:20:13.036000Z'),
  92. }),
  93. ]
  94. );
  95. it('should return an empty list when no crumbs exist', () => {
  96. const columnCount = 3;
  97. const columns = getFramesByColumn(durationMs, [], columnCount);
  98. const expectedEntries = [];
  99. expect(columns).toEqual(new Map(expectedEntries));
  100. });
  101. it('should put a crumbs in the first and last buckets', () => {
  102. const columnCount = 3;
  103. const columns = getFramesByColumn(durationMs, [CRUMB_1, CRUMB_5], columnCount);
  104. expect(columns).toEqual(
  105. new Map([
  106. [1, [CRUMB_1]],
  107. [3, [CRUMB_5]],
  108. ])
  109. );
  110. });
  111. it('should group crumbs by bucket', () => {
  112. // 6 columns gives is 5s granularity
  113. const columnCount = 6;
  114. const columns = getFramesByColumn(
  115. durationMs,
  116. [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5],
  117. columnCount
  118. );
  119. expect(columns).toEqual(
  120. new Map([
  121. [1, [CRUMB_1, CRUMB_2, CRUMB_3]],
  122. [2, [CRUMB_4]],
  123. [6, [CRUMB_5]],
  124. ])
  125. );
  126. });
  127. });
  128. describe('flattenFrames', () => {
  129. it('should return an empty array if there ar eno spans', () => {
  130. expect(flattenFrames([])).toStrictEqual([]);
  131. });
  132. it('should return the FlattenedSpanRange for a single span', () => {
  133. const frames = hydrateSpans(ReplayRecordFixture(), [
  134. ReplayRequestFrameFixture({
  135. op: 'resource.fetch',
  136. startTimestamp: new Date(10000),
  137. endTimestamp: new Date(30000),
  138. }),
  139. ]);
  140. expect(flattenFrames(frames)).toStrictEqual([
  141. {
  142. duration: 20000,
  143. endTimestamp: 30000,
  144. frameCount: 1,
  145. startTimestamp: 10000,
  146. },
  147. ]);
  148. });
  149. it('should return two non-overlapping spans', () => {
  150. const frames = hydrateSpans(ReplayRecordFixture(), [
  151. ReplayRequestFrameFixture({
  152. op: 'resource.fetch',
  153. startTimestamp: new Date(10000),
  154. endTimestamp: new Date(30000),
  155. }),
  156. ReplayRequestFrameFixture({
  157. op: 'resource.fetch',
  158. startTimestamp: new Date(60000),
  159. endTimestamp: new Date(90000),
  160. }),
  161. ]);
  162. expect(flattenFrames(frames)).toStrictEqual([
  163. {
  164. duration: 20000,
  165. endTimestamp: 30000,
  166. frameCount: 1,
  167. startTimestamp: 10000,
  168. },
  169. {
  170. duration: 30000,
  171. endTimestamp: 90000,
  172. frameCount: 1,
  173. startTimestamp: 60000,
  174. },
  175. ]);
  176. });
  177. it('should merge two overlapping spans', () => {
  178. const frames = hydrateSpans(ReplayRecordFixture(), [
  179. ReplayRequestFrameFixture({
  180. op: 'resource.fetch',
  181. startTimestamp: new Date(10000),
  182. endTimestamp: new Date(30000),
  183. }),
  184. ReplayRequestFrameFixture({
  185. op: 'resource.fetch',
  186. startTimestamp: new Date(20000),
  187. endTimestamp: new Date(40000),
  188. }),
  189. ]);
  190. expect(flattenFrames(frames)).toStrictEqual([
  191. {
  192. duration: 30000,
  193. endTimestamp: 40000,
  194. frameCount: 2,
  195. startTimestamp: 10000,
  196. },
  197. ]);
  198. });
  199. it('should merge overlapping spans that are not first in the list', () => {
  200. const frames = hydrateSpans(ReplayRecordFixture(), [
  201. ReplayRequestFrameFixture({
  202. op: 'resource.fetch',
  203. startTimestamp: new Date(0),
  204. endTimestamp: new Date(1000),
  205. }),
  206. ReplayRequestFrameFixture({
  207. op: 'resource.fetch',
  208. startTimestamp: new Date(10000),
  209. endTimestamp: new Date(30000),
  210. }),
  211. ReplayRequestFrameFixture({
  212. op: 'resource.fetch',
  213. startTimestamp: new Date(20000),
  214. endTimestamp: new Date(40000),
  215. }),
  216. ]);
  217. expect(flattenFrames(frames)).toStrictEqual([
  218. {
  219. duration: 1000,
  220. endTimestamp: 1000,
  221. frameCount: 1,
  222. startTimestamp: 0,
  223. },
  224. {
  225. duration: 30000,
  226. endTimestamp: 40000,
  227. frameCount: 2,
  228. startTimestamp: 10000,
  229. },
  230. ]);
  231. });
  232. const diffMs = 1652309918676;
  233. describe('showPlayerTime', () => {
  234. it('returns time formatted for player', () => {
  235. expect(showPlayerTime('2022-05-11T23:04:27.576000Z', diffMs)).toEqual('05:48');
  236. });
  237. it('returns 0:00 if timestamp is malformed', () => {
  238. expect(showPlayerTime('20223:04:27.576000Z', diffMs)).toEqual('00:00');
  239. });
  240. });
  241. describe('divide', () => {
  242. it('divides numbers safely', () => {
  243. expect(divide(81, 9)).toEqual(9);
  244. });
  245. it('dividing by zero returns zero', () => {
  246. expect(divide(81, 0)).toEqual(0);
  247. });
  248. });
  249. });