utils.tsx 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735
  1. import {browserHistory} from 'react-router';
  2. import {Location} from 'history';
  3. import isNumber from 'lodash/isNumber';
  4. import isString from 'lodash/isString';
  5. import set from 'lodash/set';
  6. import moment from 'moment';
  7. import {EntryType, EventTransaction} from 'sentry/types/event';
  8. import {assert} from 'sentry/types/utils';
  9. import {WEB_VITAL_DETAILS} from 'sentry/utils/performance/vitals/constants';
  10. import {getPerformanceTransaction} from 'sentry/utils/performanceForSentry';
  11. import {
  12. EnhancedSpan,
  13. GapSpanType,
  14. OrphanSpanType,
  15. OrphanTreeDepth,
  16. ParsedTraceType,
  17. ProcessedSpanType,
  18. RawSpanType,
  19. SpanEntry,
  20. SpanType,
  21. TraceContextType,
  22. TreeDepthType,
  23. } from './types';
  24. export const isValidSpanID = (maybeSpanID: any) =>
  25. isString(maybeSpanID) && maybeSpanID.length > 0;
  26. export const setSpansOnTransaction = (spanCount: number) => {
  27. const transaction = getPerformanceTransaction();
  28. if (!transaction || spanCount === 0) {
  29. return;
  30. }
  31. const spanCountGroups = [10, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1001];
  32. const spanGroup = spanCountGroups.find(g => spanCount <= g) || -1;
  33. transaction.setTag('ui.spanCount', spanCount);
  34. transaction.setTag('ui.spanCount.grouped', `<=${spanGroup}`);
  35. };
  36. export type SpanBoundsType = {endTimestamp: number; startTimestamp: number};
  37. export type SpanGeneratedBoundsType =
  38. | {isSpanVisibleInView: boolean; type: 'TRACE_TIMESTAMPS_EQUAL'}
  39. | {isSpanVisibleInView: boolean; type: 'INVALID_VIEW_WINDOW'}
  40. | {
  41. isSpanVisibleInView: boolean;
  42. start: number;
  43. type: 'TIMESTAMPS_EQUAL';
  44. width: number;
  45. }
  46. | {
  47. end: number;
  48. isSpanVisibleInView: boolean;
  49. start: number;
  50. type: 'TIMESTAMPS_REVERSED';
  51. }
  52. | {
  53. end: number;
  54. isSpanVisibleInView: boolean;
  55. start: number;
  56. type: 'TIMESTAMPS_STABLE';
  57. };
  58. export type SpanViewBoundsType = {
  59. isSpanVisibleInView: boolean;
  60. left: undefined | number;
  61. warning: undefined | string;
  62. width: undefined | number;
  63. };
  64. const normalizeTimestamps = (spanBounds: SpanBoundsType): SpanBoundsType => {
  65. const {startTimestamp, endTimestamp} = spanBounds;
  66. if (startTimestamp > endTimestamp) {
  67. return {startTimestamp: endTimestamp, endTimestamp: startTimestamp};
  68. }
  69. return spanBounds;
  70. };
  71. export enum TimestampStatus {
  72. Stable,
  73. Reversed,
  74. Equal,
  75. }
  76. export const parseSpanTimestamps = (spanBounds: SpanBoundsType): TimestampStatus => {
  77. const startTimestamp: number = spanBounds.startTimestamp;
  78. const endTimestamp: number = spanBounds.endTimestamp;
  79. if (startTimestamp < endTimestamp) {
  80. return TimestampStatus.Stable;
  81. }
  82. if (startTimestamp === endTimestamp) {
  83. return TimestampStatus.Equal;
  84. }
  85. return TimestampStatus.Reversed;
  86. };
  87. // given the start and end trace timestamps, and the view window, we want to generate a function
  88. // that'll output the relative %'s for the width and placements relative to the left-hand side.
  89. //
  90. // The view window (viewStart and viewEnd) are percentage values (between 0% and 100%), they correspond to the window placement
  91. // between the start and end trace timestamps.
  92. export const boundsGenerator = (bounds: {
  93. // unix timestamp
  94. traceEndTimestamp: number;
  95. traceStartTimestamp: number;
  96. // in [0, 1]
  97. viewEnd: number;
  98. // unix timestamp
  99. viewStart: number; // in [0, 1]
  100. }) => {
  101. const {viewStart, viewEnd} = bounds;
  102. const {startTimestamp: traceStartTimestamp, endTimestamp: traceEndTimestamp} =
  103. normalizeTimestamps({
  104. startTimestamp: bounds.traceStartTimestamp,
  105. endTimestamp: bounds.traceEndTimestamp,
  106. });
  107. // viewStart and viewEnd are percentage values (%) of the view window relative to the left
  108. // side of the trace view minimap
  109. // invariant: viewStart <= viewEnd
  110. // duration of the entire trace in seconds
  111. const traceDuration = traceEndTimestamp - traceStartTimestamp;
  112. const viewStartTimestamp = traceStartTimestamp + viewStart * traceDuration;
  113. const viewEndTimestamp = traceEndTimestamp - (1 - viewEnd) * traceDuration;
  114. const viewDuration = viewEndTimestamp - viewStartTimestamp;
  115. return (spanBounds: SpanBoundsType): SpanGeneratedBoundsType => {
  116. // TODO: alberto.... refactor so this is impossible 😠
  117. if (traceDuration <= 0) {
  118. return {
  119. type: 'TRACE_TIMESTAMPS_EQUAL',
  120. isSpanVisibleInView: true,
  121. };
  122. }
  123. if (viewDuration <= 0) {
  124. return {
  125. type: 'INVALID_VIEW_WINDOW',
  126. isSpanVisibleInView: true,
  127. };
  128. }
  129. const {startTimestamp, endTimestamp} = normalizeTimestamps(spanBounds);
  130. const timestampStatus = parseSpanTimestamps(spanBounds);
  131. const start = (startTimestamp - viewStartTimestamp) / viewDuration;
  132. const end = (endTimestamp - viewStartTimestamp) / viewDuration;
  133. const isSpanVisibleInView = end > 0 && start < 1;
  134. switch (timestampStatus) {
  135. case TimestampStatus.Equal: {
  136. return {
  137. type: 'TIMESTAMPS_EQUAL',
  138. start,
  139. width: 1,
  140. // a span bar is visible even if they're at the extreme ends of the view selection.
  141. // these edge cases are:
  142. // start == end == 0, and
  143. // start == end == 1
  144. isSpanVisibleInView: end >= 0 && start <= 1,
  145. };
  146. }
  147. case TimestampStatus.Reversed: {
  148. return {
  149. type: 'TIMESTAMPS_REVERSED',
  150. start,
  151. end,
  152. isSpanVisibleInView,
  153. };
  154. }
  155. case TimestampStatus.Stable: {
  156. return {
  157. type: 'TIMESTAMPS_STABLE',
  158. start,
  159. end,
  160. isSpanVisibleInView,
  161. };
  162. }
  163. default: {
  164. const _exhaustiveCheck: never = timestampStatus;
  165. return _exhaustiveCheck;
  166. }
  167. }
  168. };
  169. };
  170. export function generateRootSpan(trace: ParsedTraceType): RawSpanType {
  171. const rootSpan: RawSpanType = {
  172. trace_id: trace.traceID,
  173. span_id: trace.rootSpanID,
  174. parent_span_id: trace.parentSpanID,
  175. start_timestamp: trace.traceStartTimestamp,
  176. timestamp: trace.traceEndTimestamp,
  177. op: trace.op,
  178. description: trace.description,
  179. data: {},
  180. status: trace.rootSpanStatus,
  181. hash: trace.hash,
  182. exclusive_time: trace.exclusiveTime,
  183. };
  184. return rootSpan;
  185. }
  186. // start and end are assumed to be unix timestamps with fractional seconds
  187. export function getTraceDateTimeRange(input: {end: number; start: number}): {
  188. end: string;
  189. start: string;
  190. } {
  191. const start = moment
  192. .unix(input.start)
  193. .subtract(12, 'hours')
  194. .utc()
  195. .format('YYYY-MM-DDTHH:mm:ss.SSS');
  196. const end = moment
  197. .unix(input.end)
  198. .add(12, 'hours')
  199. .utc()
  200. .format('YYYY-MM-DDTHH:mm:ss.SSS');
  201. return {
  202. start,
  203. end,
  204. };
  205. }
  206. export function isGapSpan(span: ProcessedSpanType): span is GapSpanType {
  207. if ('type' in span) {
  208. return span.type === 'gap';
  209. }
  210. return false;
  211. }
  212. export function isOrphanSpan(span: ProcessedSpanType): span is OrphanSpanType {
  213. if ('type' in span) {
  214. if (span.type === 'orphan') {
  215. return true;
  216. }
  217. if (span.type === 'gap') {
  218. return span.isOrphan;
  219. }
  220. }
  221. return false;
  222. }
  223. export function getSpanID(span: ProcessedSpanType, defaultSpanID: string = ''): string {
  224. if (isGapSpan(span)) {
  225. return defaultSpanID;
  226. }
  227. return span.span_id;
  228. }
  229. export function getSpanOperation(span: ProcessedSpanType): string | undefined {
  230. if (isGapSpan(span)) {
  231. return undefined;
  232. }
  233. return span.op;
  234. }
  235. export function getSpanTraceID(span: ProcessedSpanType): string {
  236. if (isGapSpan(span)) {
  237. return 'gap-span';
  238. }
  239. return span.trace_id;
  240. }
  241. export function getSpanParentSpanID(span: ProcessedSpanType): string | undefined {
  242. if (isGapSpan(span)) {
  243. return 'gap-span';
  244. }
  245. return span.parent_span_id;
  246. }
  247. export function getTraceContext(
  248. event: Readonly<EventTransaction>
  249. ): TraceContextType | undefined {
  250. return event?.contexts?.trace;
  251. }
  252. export function parseTrace(event: Readonly<EventTransaction>): ParsedTraceType {
  253. const spanEntry = event.entries.find((entry: SpanEntry | any): entry is SpanEntry => {
  254. return entry.type === EntryType.SPANS;
  255. });
  256. const spans: Array<RawSpanType> = spanEntry?.data ?? [];
  257. const traceContext = getTraceContext(event);
  258. const traceID = (traceContext && traceContext.trace_id) || '';
  259. const rootSpanID = (traceContext && traceContext.span_id) || '';
  260. const rootSpanOpName = (traceContext && traceContext.op) || 'transaction';
  261. const description = traceContext && traceContext.description;
  262. const parentSpanID = traceContext && traceContext.parent_span_id;
  263. const rootSpanStatus = traceContext && traceContext.status;
  264. const hash = traceContext && traceContext.hash;
  265. const exclusiveTime = traceContext && traceContext.exclusive_time;
  266. if (!spanEntry || spans.length <= 0) {
  267. return {
  268. op: rootSpanOpName,
  269. childSpans: {},
  270. traceStartTimestamp: event.startTimestamp,
  271. traceEndTimestamp: event.endTimestamp,
  272. traceID,
  273. rootSpanID,
  274. rootSpanStatus,
  275. parentSpanID,
  276. spans: [],
  277. description,
  278. hash,
  279. exclusiveTime,
  280. };
  281. }
  282. // any span may be a parent of another span
  283. const potentialParents = new Set(
  284. spans.map(span => {
  285. return span.span_id;
  286. })
  287. );
  288. // the root transaction span is a parent of all other spans
  289. potentialParents.add(rootSpanID);
  290. // we reduce spans to become an object mapping span ids to their children
  291. const init: ParsedTraceType = {
  292. op: rootSpanOpName,
  293. childSpans: {},
  294. traceStartTimestamp: event.startTimestamp,
  295. traceEndTimestamp: event.endTimestamp,
  296. traceID,
  297. rootSpanID,
  298. rootSpanStatus,
  299. parentSpanID,
  300. spans,
  301. description,
  302. hash,
  303. exclusiveTime,
  304. };
  305. const reduced: ParsedTraceType = spans.reduce((acc, inputSpan) => {
  306. let span: SpanType = inputSpan;
  307. const parentSpanId = getSpanParentSpanID(span);
  308. const hasParent = parentSpanId && potentialParents.has(parentSpanId);
  309. if (!isValidSpanID(parentSpanId) || !hasParent) {
  310. // this span is considered an orphan with respect to the spans within this transaction.
  311. // although the span is an orphan, it's still a descendant of this transaction,
  312. // so we set its parent span id to be the root transaction span's id
  313. span.parent_span_id = rootSpanID;
  314. span = {
  315. type: 'orphan',
  316. ...span,
  317. } as OrphanSpanType;
  318. }
  319. assert(span.parent_span_id);
  320. // get any span children whose parent_span_id is equal to span.parent_span_id,
  321. // otherwise start with an empty array
  322. const spanChildren: Array<SpanType> = acc.childSpans[span.parent_span_id] ?? [];
  323. spanChildren.push(span);
  324. set(acc.childSpans, span.parent_span_id, spanChildren);
  325. // set trace start & end timestamps based on given span's start and end timestamps
  326. if (!acc.traceStartTimestamp || span.start_timestamp < acc.traceStartTimestamp) {
  327. acc.traceStartTimestamp = span.start_timestamp;
  328. }
  329. // establish trace end timestamp
  330. const hasEndTimestamp = isNumber(span.timestamp);
  331. if (!acc.traceEndTimestamp) {
  332. if (hasEndTimestamp) {
  333. acc.traceEndTimestamp = span.timestamp;
  334. return acc;
  335. }
  336. acc.traceEndTimestamp = span.start_timestamp;
  337. return acc;
  338. }
  339. if (hasEndTimestamp && span.timestamp! > acc.traceEndTimestamp) {
  340. acc.traceEndTimestamp = span.timestamp;
  341. return acc;
  342. }
  343. if (span.start_timestamp > acc.traceEndTimestamp) {
  344. acc.traceEndTimestamp = span.start_timestamp;
  345. }
  346. return acc;
  347. }, init);
  348. // sort span children
  349. Object.values(reduced.childSpans).forEach(spanChildren => {
  350. spanChildren.sort(sortSpans);
  351. });
  352. return reduced;
  353. }
  354. function sortSpans(firstSpan: SpanType, secondSpan: SpanType) {
  355. // orphan spans come after non-orphan spans.
  356. if (isOrphanSpan(firstSpan) && !isOrphanSpan(secondSpan)) {
  357. // sort secondSpan before firstSpan
  358. return 1;
  359. }
  360. if (!isOrphanSpan(firstSpan) && isOrphanSpan(secondSpan)) {
  361. // sort firstSpan before secondSpan
  362. return -1;
  363. }
  364. // sort spans by their start timestamp in ascending order
  365. if (firstSpan.start_timestamp < secondSpan.start_timestamp) {
  366. // sort firstSpan before secondSpan
  367. return -1;
  368. }
  369. if (firstSpan.start_timestamp === secondSpan.start_timestamp) {
  370. return 0;
  371. }
  372. // sort secondSpan before firstSpan
  373. return 1;
  374. }
  375. export function isOrphanTreeDepth(
  376. treeDepth: TreeDepthType
  377. ): treeDepth is OrphanTreeDepth {
  378. if (typeof treeDepth === 'number') {
  379. return false;
  380. }
  381. return treeDepth?.type === 'orphan';
  382. }
  383. export function unwrapTreeDepth(treeDepth: TreeDepthType): number {
  384. if (isOrphanTreeDepth(treeDepth)) {
  385. return treeDepth.depth;
  386. }
  387. return treeDepth;
  388. }
  389. export function isEventFromBrowserJavaScriptSDK(event: EventTransaction): boolean {
  390. const sdkName = event.sdk?.name;
  391. if (!sdkName) {
  392. return false;
  393. }
  394. // based on https://github.com/getsentry/sentry-javascript/blob/master/packages/browser/src/version.ts
  395. return [
  396. 'sentry.javascript.browser',
  397. 'sentry.javascript.react',
  398. 'sentry.javascript.gatsby',
  399. 'sentry.javascript.ember',
  400. 'sentry.javascript.vue',
  401. 'sentry.javascript.angular',
  402. 'sentry.javascript.nextjs',
  403. 'sentry.javascript.electron',
  404. ].includes(sdkName.toLowerCase());
  405. }
  406. // Durationless ops from: https://github.com/getsentry/sentry-javascript/blob/0defcdcc2dfe719343efc359d58c3f90743da2cd/packages/apm/src/integrations/tracing.ts#L629-L688
  407. // PerformanceMark: Duration is 0 as per https://developer.mozilla.org/en-US/docs/Web/API/PerformanceMark
  408. // PerformancePaintTiming: Duration is 0 as per https://developer.mozilla.org/en-US/docs/Web/API/PerformancePaintTiming
  409. export const durationlessBrowserOps = ['mark', 'paint'];
  410. type Measurements = {
  411. [name: string]: number | undefined;
  412. };
  413. type VerticalMark = {
  414. failedThreshold: boolean;
  415. marks: Measurements;
  416. };
  417. function hasFailedThreshold(marks: Measurements): boolean {
  418. const names = Object.keys(marks);
  419. const records = Object.values(WEB_VITAL_DETAILS).filter(vital =>
  420. names.includes(vital.slug)
  421. );
  422. return records.some(record => {
  423. const value = marks[record.slug];
  424. if (typeof value === 'number' && typeof record.poorThreshold === 'number') {
  425. return value >= record.poorThreshold;
  426. }
  427. return false;
  428. });
  429. }
  430. export function getMeasurements(event: EventTransaction): Map<number, VerticalMark> {
  431. if (!event.measurements) {
  432. return new Map();
  433. }
  434. const measurements = Object.keys(event.measurements)
  435. .filter(name => name.startsWith('mark.'))
  436. .map(name => {
  437. const slug = name.slice('mark.'.length);
  438. const associatedMeasurement = event.measurements![slug];
  439. return {
  440. name,
  441. timestamp: event.measurements![name].value,
  442. value: associatedMeasurement ? associatedMeasurement.value : undefined,
  443. };
  444. });
  445. const mergedMeasurements = new Map<number, VerticalMark>();
  446. measurements.forEach(measurement => {
  447. const name = measurement.name.slice('mark.'.length);
  448. const value = measurement.value;
  449. if (mergedMeasurements.has(measurement.timestamp)) {
  450. const verticalMark = mergedMeasurements.get(measurement.timestamp) as VerticalMark;
  451. verticalMark.marks = {
  452. ...verticalMark.marks,
  453. [name]: value,
  454. };
  455. if (!verticalMark.failedThreshold) {
  456. verticalMark.failedThreshold = hasFailedThreshold(verticalMark.marks);
  457. }
  458. mergedMeasurements.set(measurement.timestamp, verticalMark);
  459. return;
  460. }
  461. const marks = {
  462. [name]: value,
  463. };
  464. mergedMeasurements.set(measurement.timestamp, {
  465. marks,
  466. failedThreshold: hasFailedThreshold(marks),
  467. });
  468. });
  469. return mergedMeasurements;
  470. }
  471. export function getMeasurementBounds(
  472. timestamp: number,
  473. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  474. ): SpanViewBoundsType {
  475. const bounds = generateBounds({
  476. startTimestamp: timestamp,
  477. endTimestamp: timestamp,
  478. });
  479. switch (bounds.type) {
  480. case 'TRACE_TIMESTAMPS_EQUAL':
  481. case 'INVALID_VIEW_WINDOW': {
  482. return {
  483. warning: undefined,
  484. left: undefined,
  485. width: undefined,
  486. isSpanVisibleInView: bounds.isSpanVisibleInView,
  487. };
  488. }
  489. case 'TIMESTAMPS_EQUAL': {
  490. return {
  491. warning: undefined,
  492. left: bounds.start,
  493. width: 0.00001,
  494. isSpanVisibleInView: bounds.isSpanVisibleInView,
  495. };
  496. }
  497. case 'TIMESTAMPS_REVERSED': {
  498. return {
  499. warning: undefined,
  500. left: bounds.start,
  501. width: bounds.end - bounds.start,
  502. isSpanVisibleInView: bounds.isSpanVisibleInView,
  503. };
  504. }
  505. case 'TIMESTAMPS_STABLE': {
  506. return {
  507. warning: void 0,
  508. left: bounds.start,
  509. width: bounds.end - bounds.start,
  510. isSpanVisibleInView: bounds.isSpanVisibleInView,
  511. };
  512. }
  513. default: {
  514. const _exhaustiveCheck: never = bounds;
  515. return _exhaustiveCheck;
  516. }
  517. }
  518. }
  519. export function scrollToSpan(
  520. spanId: string,
  521. scrollToHash: (hash: string) => void,
  522. location: Location
  523. ) {
  524. return (e: React.MouseEvent<Element>) => {
  525. // do not use the default anchor behaviour
  526. // because it will be hidden behind the minimap
  527. e.preventDefault();
  528. const hash = spanTargetHash(spanId);
  529. scrollToHash(hash);
  530. // TODO(txiao): This is causing a rerender of the whole page,
  531. // which can be slow.
  532. //
  533. // make sure to update the location
  534. browserHistory.push({
  535. ...location,
  536. hash,
  537. });
  538. };
  539. }
  540. export function spanTargetHash(spanId: string): string {
  541. return `#span-${spanId}`;
  542. }
  543. export function getSiblingGroupKey(span: SpanType, occurrence?: number): string {
  544. if (occurrence !== undefined) {
  545. return `${span.op}.${span.description}.${occurrence}`;
  546. }
  547. return `${span.op}.${span.description}`;
  548. }
  549. export function getSpanGroupTimestamps(spanGroup: EnhancedSpan[]) {
  550. return spanGroup.reduce(
  551. (acc, spanGroupItem) => {
  552. const {start_timestamp, timestamp} = spanGroupItem.span;
  553. let newStartTimestamp = acc.startTimestamp;
  554. let newEndTimestamp = acc.endTimestamp;
  555. if (start_timestamp < newStartTimestamp) {
  556. newStartTimestamp = start_timestamp;
  557. }
  558. if (newEndTimestamp < timestamp) {
  559. newEndTimestamp = timestamp;
  560. }
  561. return {
  562. startTimestamp: newStartTimestamp,
  563. endTimestamp: newEndTimestamp,
  564. };
  565. },
  566. {
  567. startTimestamp: spanGroup[0].span.start_timestamp,
  568. endTimestamp: spanGroup[0].span.timestamp,
  569. }
  570. );
  571. }
  572. export function getSpanGroupBounds(
  573. spanGroup: EnhancedSpan[],
  574. generateBounds: (bounds: SpanBoundsType) => SpanGeneratedBoundsType
  575. ): SpanViewBoundsType {
  576. const {startTimestamp, endTimestamp} = getSpanGroupTimestamps(spanGroup);
  577. const bounds = generateBounds({
  578. startTimestamp,
  579. endTimestamp,
  580. });
  581. switch (bounds.type) {
  582. case 'TRACE_TIMESTAMPS_EQUAL':
  583. case 'INVALID_VIEW_WINDOW': {
  584. return {
  585. warning: void 0,
  586. left: void 0,
  587. width: void 0,
  588. isSpanVisibleInView: bounds.isSpanVisibleInView,
  589. };
  590. }
  591. case 'TIMESTAMPS_EQUAL': {
  592. return {
  593. warning: void 0,
  594. left: bounds.start,
  595. width: 0.00001,
  596. isSpanVisibleInView: bounds.isSpanVisibleInView,
  597. };
  598. }
  599. case 'TIMESTAMPS_REVERSED':
  600. case 'TIMESTAMPS_STABLE': {
  601. return {
  602. warning: void 0,
  603. left: bounds.start,
  604. width: bounds.end - bounds.start,
  605. isSpanVisibleInView: bounds.isSpanVisibleInView,
  606. };
  607. }
  608. default: {
  609. const _exhaustiveCheck: never = bounds;
  610. return _exhaustiveCheck;
  611. }
  612. }
  613. }