utils.spec.tsx 7.9 KB

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