import { countColumns, divide, flattenSpans, formatTime, getCrumbsByColumn, relativeTimeInMs, showPlayerTime, } from 'sentry/components/replays/utils'; import {BreadcrumbLevelType, BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs'; import type {ReplaySpan} from 'sentry/views/replays/types'; const SECOND = 1000; function createSpan(span: Partial): ReplaySpan { return { data: {}, ...span, } as ReplaySpan; } function createCrumb({timestamp}: Pick): Crumb { return { timestamp, color: 'white', description: 'crumb description', id: 1, type: BreadcrumbType.DEFAULT, data: {}, level: BreadcrumbLevelType.DEBUG, }; } describe('formatTime', () => { it.each([ ['seconds', 15 * 1000, '00:15'], ['minutes', 2.5 * 60 * 1000, '02:30'], ['hours', 75 * 60 * 1000, '01:15:00'], ])('should format a %s long duration into a string', (_desc, duration, expected) => { expect(formatTime(duration)).toEqual(expected); }); }); describe('countColumns', () => { it('should divide 27s by 2700px to find twentyseven 1s columns, with some fraction remaining', () => { // 2700 allows for up to 27 columns at 100px wide. // That is what we'd need if we were to render at `1s` granularity, so we can. const width = 2700; const duration = 27 * SECOND; const minWidth = 100; const {timespan, cols, remaining} = countColumns(duration, width, minWidth); expect(timespan).toBe(1 * SECOND); expect(cols).toBe(27); expect(remaining).toBe(0); }); it('should divide 27s by 2699px to find five 5s columns, with some fraction remaining', () => { // 2699px allows for up to 26 columns at 100px wide, with 99px leftover. // That is less than the 27 cols we'd need if we were to render at `1s` granularity. // So instead we get 5 cols (wider than 100px) at 5s granularity, and some extra space is remaining. const width = 2699; const duration = 27 * SECOND; const minWidth = 100; const {timespan, cols, remaining} = countColumns(duration, width, minWidth); expect(timespan).toBe(5 * SECOND); expect(cols).toBe(5); expect(remaining).toBe(0.4); }); it('should divide 27s by 600px to find five 5s columns, with some fraction column remaining', () => { // 600px allows for 6 columns at 100px wide to fix within it // That allows us to get 5 cols (100px wide) at 5s granularity, and an extra 100px for the remainder const width = 600; const duration = 27 * SECOND; const minWidth = 100; const {timespan, cols, remaining} = countColumns(duration, width, minWidth); expect(timespan).toBe(5 * SECOND); expect(cols).toBe(5); expect(remaining).toBe(0.4); }); it('should divide 27s by 599px to find five 2s columns, with some fraction column remaining', () => { // 599px allows for 5 columns at 100px wide, and 99px remaining. // That allows us to get 2 cols (100px wide) at 10s granularity, and an extra px for the remainder const width = 599; const duration = 27 * SECOND; const minWidth = 100; const {timespan, cols, remaining} = countColumns(duration, width, minWidth); expect(timespan).toBe(10 * SECOND); expect(cols).toBe(2); expect(remaining).toBe(0.7); }); }); describe('getCrumbsByColumn', () => { const startTimestamp = 1649945987.326; // seconds const duration = 25710; // milliseconds const CRUMB_1 = createCrumb({timestamp: '2022-04-14T14:19:47.326000Z'}); const CRUMB_2 = createCrumb({timestamp: '2022-04-14T14:19:49.249000Z'}); const CRUMB_3 = createCrumb({timestamp: '2022-04-14T14:19:51.512000Z'}); const CRUMB_4 = createCrumb({timestamp: '2022-04-14T14:19:57.326000Z'}); const CRUMB_5 = createCrumb({timestamp: '2022-04-14T14:20:13.036000Z'}); it('should return an empty list when no crumbs exist', () => { const columnCount = 3; const columns = getCrumbsByColumn(startTimestamp, duration, [], columnCount); const expectedEntries = []; expect(columns).toEqual(new Map(expectedEntries)); }); it('should put a crumbs in the first and last buckets', () => { const columnCount = 3; const columns = getCrumbsByColumn( startTimestamp, duration, [CRUMB_1, CRUMB_5], columnCount ); expect(columns).toEqual( new Map([ [1, [CRUMB_1]], [3, [CRUMB_5]], ]) ); }); it('should group crumbs by bucket', () => { // 6 columns gives is 5s granularity const columnCount = 6; const columns = getCrumbsByColumn( startTimestamp, duration, [CRUMB_1, CRUMB_2, CRUMB_3, CRUMB_4, CRUMB_5], columnCount ); expect(columns).toEqual( new Map([ [1, [CRUMB_1, CRUMB_2, CRUMB_3]], [2, [CRUMB_4]], [6, [CRUMB_5]], ]) ); }); }); describe('flattenSpans', () => { it('should return an empty array if there ar eno spans', () => { expect(flattenSpans([])).toStrictEqual([]); }); it('should return the FlattenedSpanRange for a single span', () => { const span = createSpan({ op: 'span', startTimestamp: 10, endTimestamp: 30, }); expect(flattenSpans([span])).toStrictEqual([ { duration: 20000, endTimestamp: 30000, spanCount: 1, startTimestamp: 10000, }, ]); }); it('should return two non-overlapping spans', () => { const span1 = createSpan({ op: 'span1', startTimestamp: 10, endTimestamp: 30, }); const span2 = createSpan({ op: 'span2', startTimestamp: 60, endTimestamp: 90, }); expect(flattenSpans([span1, span2])).toStrictEqual([ { duration: 20000, endTimestamp: 30000, spanCount: 1, startTimestamp: 10000, }, { duration: 30000, endTimestamp: 90000, spanCount: 1, startTimestamp: 60000, }, ]); }); it('should merge two overlapping spans', () => { const span1 = createSpan({ op: 'span1', data: {}, startTimestamp: 10, endTimestamp: 30, }); const span2 = createSpan({ op: 'span2', startTimestamp: 20, endTimestamp: 40, }); expect(flattenSpans([span1, span2])).toStrictEqual([ { duration: 30000, endTimestamp: 40000, spanCount: 2, startTimestamp: 10000, }, ]); }); it('should merge overlapping spans that are not first in the list', () => { const span0 = createSpan({ op: 'span0', startTimestamp: 0, endTimestamp: 1, }); const span1 = createSpan({ op: 'span1', startTimestamp: 10, endTimestamp: 30, }); const span2 = createSpan({ op: 'span2', startTimestamp: 20, endTimestamp: 40, }); expect(flattenSpans([span0, span1, span2])).toStrictEqual([ { duration: 1000, endTimestamp: 1000, spanCount: 1, startTimestamp: 0, }, { duration: 30000, endTimestamp: 40000, spanCount: 2, startTimestamp: 10000, }, ]); }); describe('relativeTimeinMs', () => { it('returns relative time in MS', () => { expect(relativeTimeInMs('2022-05-11T23:04:27.576000Z', 347.90000009536743)).toEqual( 1652309919676 ); }); it('returns invalid date if date string is malformed', () => { expect(relativeTimeInMs('202223:04:27.576000Z', 347.90000009536743)).toEqual(NaN); }); }); describe('showPlayerTime', () => { it('returns time formatted for player', () => { expect(showPlayerTime('2022-05-11T23:04:27.576000Z', 1652309918.676)).toEqual( '05:48' ); }); it('returns 0:00 if timestamp is malformed', () => { expect(showPlayerTime('20223:04:27.576000Z', 1652309918.676)).toEqual('00:00'); }); }); describe('divide', () => { it('divides numbers safely', () => { expect(divide(81, 9)).toEqual(9); }); it('dividing by zero returns zero', () => { expect(divide(81, 0)).toEqual(0); }); }); });