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));