From d10148cf0bc3bf203ba0781378956925cef68f4f Mon Sep 17 00:00:00 2001 From: Catherine Lee <55311782+c298lee@users.noreply.github.com> Date: Fri, 19 Jul 2024 11:46:50 -0400 Subject: [PATCH] ref(replay): Replay timeline gaps using timestamp (#74406) Refactors replay timeline gaps to use frame timestamps and styles `left` and `width` instead of frame columns. Using timestamps instead of the frame column makes it line up with breadcrumbs better. Switching to styling `left` and `width` instead of the range of frames also fixed the gap bug when zooming in on the timeline. Also modified the gap logic: When there's a background frame, a gap is created until the next frame that's not a background frame or error frame. In most cases it will be the foreground frame, but in cases of bad data it could be any other replay frame. Before: ![image](https://github.com/user-attachments/assets/51ad1b87-74f8-4c03-8274-b5409e5bac75) After: ![image](https://github.com/user-attachments/assets/bbf19677-99e9-46d3-96aa-036f086542dc) The gap is now a consistent `div` so there's no weird spots that the gap doesn't cover. Additionally, the gap ends as soon as there's a frame even if it isn't a foreground frame, which is more accurate. Relates to https://github.com/getsentry/sentry/issues/71665 --- .../replays/breadcrumbs/replayTimeline.tsx | 6 +- .../replays/breadcrumbs/timelineGaps.tsx | 106 +++++++++--------- static/app/utils/replays/replayReader.tsx | 8 -- 3 files changed, 54 insertions(+), 66 deletions(-) diff --git a/static/app/components/replays/breadcrumbs/replayTimeline.tsx b/static/app/components/replays/breadcrumbs/replayTimeline.tsx index 7e8dd4c955c47..db1060d10c0a1 100644 --- a/static/app/components/replays/breadcrumbs/replayTimeline.tsx +++ b/static/app/components/replays/breadcrumbs/replayTimeline.tsx @@ -40,7 +40,6 @@ export default function ReplayTimeline() { const durationMs = replay.getDurationMs(); const startTimestampMs = replay.getStartTimestampMs(); const chapterFrames = replay.getChapterFrames(); - const appFrames = replay.getAppFrames(); // timeline is in the middle const initialTranslate = 0.5 / timelineScale; @@ -74,9 +73,8 @@ export default function ReplayTimeline() { {organization.features.includes('session-replay-timeline-gap') ? ( ) : null} diff --git a/static/app/components/replays/breadcrumbs/timelineGaps.tsx b/static/app/components/replays/breadcrumbs/timelineGaps.tsx index 794f6db092c11..6efe8e4501971 100644 --- a/static/app/components/replays/breadcrumbs/timelineGaps.tsx +++ b/static/app/components/replays/breadcrumbs/timelineGaps.tsx @@ -1,88 +1,86 @@ +import {Fragment} from 'react'; import styled from '@emotion/styled'; -import * as Timeline from 'sentry/components/replays/breadcrumbs/timeline'; -import {getFramesByColumn} from 'sentry/components/replays/utils'; import {Tooltip} from 'sentry/components/tooltip'; import {t} from 'sentry/locale'; +import toPercent from 'sentry/utils/number/toPercent'; import { isBackgroundFrame, - isForegroundFrame, + isErrorFrame, type ReplayFrame, } from 'sentry/utils/replays/types'; interface Props { durationMs: number; frames: ReplayFrame[]; - totalFrames: number; - width: number; + startTimestampMs: number; } // create gaps in the timeline by finding all columns between a background frame and foreground frame // or background frame to end of replay -export default function TimelineGaps({durationMs, frames, totalFrames, width}: Props) { - const markerWidth = totalFrames < 200 ? 4 : totalFrames < 500 ? 6 : 10; - const totalColumns = Math.floor(width / markerWidth); - const framesByCol = getFramesByColumn(durationMs, frames, totalColumns); +export default function TimelineGaps({durationMs, startTimestampMs, frames}: Props) { + const ranges: Array<{left: string; width: string}> = []; - // returns all numbers in the range, exclusive of start and inclusive of stop - const range = (start, stop) => - Array.from({length: stop - start}, (_, i) => start + i + 1); + let start = -1; + let end = -1; - const gapCol: number[] = []; - - const gapFrames = framesByCol.entries(); - let currFrame = gapFrames.next(); - - while (!currFrame.done) { - let start = -1; - let end = -1; - - // iterate through all frames until we have a start (background frame) and end (foreground frame) of gap or no more frames - while ((start === -1 || end === -1) && !currFrame.done) { - const [column, colFrame] = currFrame.value; - for (const frame of colFrame) { - // only considered start of gap if background frame hasn't been found yet - if (start === -1 && isBackgroundFrame(frame)) { - start = column; - } - // gap only ends if background frame has been found - if (start !== -1 && isForegroundFrame(frame)) { - end = column; - } - } - currFrame = gapFrames.next(); + for (const currFrame of frames) { + // only considered start of gap if background frame hasn't been found yet + if (start === -1 && isBackgroundFrame(currFrame)) { + start = currFrame.timestampMs - startTimestampMs; + } + // gap only ends if a frame that's not a background frame or error frame has been found + if (start !== -1 && !isBackgroundFrame(currFrame) && !isErrorFrame(currFrame)) { + end = currFrame.timestampMs - startTimestampMs; } - // create gap if we found have start (background frame) and end (foreground frame) + // create gap if we found have start (background frame) and end (another frame) if (start !== -1 && end !== -1) { - gapCol.push(...range(start, end)); - } - // if we have start but no end, that means we have a gap until end of replay - if (start !== -1 && end === -1) { - gapCol.push(...range(start, totalColumns)); + ranges.push({ + left: toPercent(start / durationMs), + width: toPercent((end - start) / durationMs), + }); + start = -1; + end = -1; } } + // create gap if we still have start (background frame) until end of replay + if (start !== -1) { + ranges.push({ + left: toPercent(start / durationMs), + width: toPercent((durationMs - start) / durationMs), + }); + } + + // TODO: Fix tooltip position to follow mouse (it currently goes off the timeline when zoomed too much) return ( - - {gapCol.map(column => ( - - - - - - ))} - + + {ranges.map(rangeCss => { + return ( + + + + + + ); + })} + ); } -const Column = styled(Timeline.Col)<{column: number}>` - grid-column: ${p => p.column}; - line-height: 14px; +const Range = styled('div')` + position: absolute; `; -const Gap = styled('span')` +const Gap = styled('div')` background: ${p => p.theme.gray400}; opacity: 16%; height: 20px; + width: 100%; `; diff --git a/static/app/utils/replays/replayReader.tsx b/static/app/utils/replays/replayReader.tsx index 80699955c698f..f5d8b2d12830c 100644 --- a/static/app/utils/replays/replayReader.tsx +++ b/static/app/utils/replays/replayReader.tsx @@ -35,10 +35,8 @@ import { BreadcrumbCategories, EventType, IncrementalSource, - isBackgroundFrame, isDeadClick, isDeadRageClick, - isForegroundFrame, isPaintFrame, isWebVitalFrame, } from 'sentry/utils/replays/types'; @@ -555,12 +553,6 @@ export default class ReplayReader { return []; }); - getAppFrames = memoize(() => { - return this._sortedBreadcrumbFrames.filter( - frame => isBackgroundFrame(frame) || isForegroundFrame(frame) - ); - }); - getVideoEvents = () => this._videoEvents; getPaintFrames = memoize(() => this._sortedSpanFrames.filter(isPaintFrame));