Repository: getsentry/sentry
Pull request: feat(replays): added links to console timestamps so the scrubber can jump to those times
Closes #34824 

Screenshots: 
As illustrated in the video, you can click on the timestamps and you'll jump to that point in the video.

https://user-images.githubusercontent.com/7014871/169597180-caff2eaa-3b65-49b2-b1cf-1fabce8e979a.mov



<!-- Describe your PR here. -->



<!--

  Sentry employees and contractors can delete or ignore the following.

-->

### Legal Boilerplate

Look, I get it. The entity doing business as "Sentry" was incorporated in the State of Delaware in 2015 as Functional Software, Inc. and is gonna need some rights from me in order to utilize my contributions in this here PR. So here's the deal: I retain all rights, title and interest in and to my contributions, and by keeping this boilerplate intact I confirm that Sentry can use, modify, copy, and redistribute my contributions, under Sentry's choice of terms.


Problem statement:
Link timestamps within the console view to the replay player.
### Problem Statement

Link timestamps within the console view to the replay player. Right now you can see a relative timestamp but you can't jump to it on the player. 

### Solution Brainstorm

_No response_

Hints from issue discussion:
Routing to @getsentry/emerging-technology for [triage](https://develop.sentry.dev/processing-tickets/#3-triage). ⏲️

Commit message: refactor: consolidated making the relative time to a variable

Patch to review:
diff --git a/static/app/views/replays/detail/console/consoleMessage.tsx b/static/app/views/replays/detail/console/consoleMessage.tsx
index a1d8246bb2ab15..87303a8b7fe326 100644
--- a/static/app/views/replays/detail/console/consoleMessage.tsx
+++ b/static/app/views/replays/detail/console/consoleMessage.tsx
@@ -66,12 +66,18 @@ function MessageFormatter({breadcrumb}: MessageFormatterProps) {
 
 interface ConsoleMessageProps extends MessageFormatterProps {
   isLast: boolean;
+  onClick: () => void;
+  onMouseOut: () => void;
+  onMouseOver: () => void;
   relativeTimestamp: string;
 }
 function ConsoleMessage({
   breadcrumb,
   isLast,
   relativeTimestamp = '',
+  onClick,
+  onMouseOver,
+  onMouseOut,
 }: ConsoleMessageProps) {
   const ICONS = {
     error: <IconClose isCircled size="xs" />,
@@ -88,7 +94,13 @@ function ConsoleMessage({
       </Message>
       <ConsoleTimestamp isLast={isLast} level={breadcrumb.level}>
         <Hovercard body={`${t('Relative Time')}: ${relativeTimestamp}`}>
-          <DateTime date={breadcrumb.timestamp} timeOnly />
+          <DateTime
+            timeOnly
+            date={breadcrumb.timestamp}
+            onClick={onClick}
+            onMouseOver={onMouseOver}
+            onMouseOut={onMouseOut}
+          />
         </Hovercard>
       </ConsoleTimestamp>
     </Fragment>
@@ -110,6 +122,7 @@ const Common = styled('div')<{isLast: boolean; level: string}>`
 const ConsoleTimestamp = styled(Common)<{isLast: boolean; level: string}>`
   padding: ${space(1)};
   border-left: 1px solid ${p => p.theme.innerBorder};
+  cursor: pointer;
 `;
 
 const Icon = styled(Common)`
diff --git a/static/app/views/replays/detail/console/index.tsx b/static/app/views/replays/detail/console/index.tsx
index 99067571dd4bea..90a9dc309ab211 100644
--- a/static/app/views/replays/detail/console/index.tsx
+++ b/static/app/views/replays/detail/console/index.tsx
@@ -4,7 +4,7 @@ import debounce from 'lodash/debounce';
 
 import CompactSelect from 'sentry/components/forms/compactSelect';
 import {Panel} from 'sentry/components/panels';
-import {showPlayerTime} from 'sentry/components/replays/utils';
+import {relativeTimeInMs, showPlayerTime} from 'sentry/components/replays/utils';
 import SearchBar from 'sentry/components/searchBar';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
@@ -15,13 +15,20 @@ import ConsoleMessage from './consoleMessage';
 
 interface Props {
   breadcrumbs: BreadcrumbTypeDefault[];
+  setCurrentHoverTime: (time: undefined | number) => void;
+  setCurrentTime: (time: number) => void;
   startTimestamp: number;
 }
 
 const getDistinctLogLevels = breadcrumbs =>
   Array.from(new Set<string>(breadcrumbs.map(breadcrumb => breadcrumb.level)));
 
-function Console({breadcrumbs, startTimestamp = 0}: Props) {
+function Console({
+  breadcrumbs,
+  startTimestamp = 0,
+  setCurrentTime,
+  setCurrentHoverTime,
+}: Props) {
   const [searchTerm, setSearchTerm] = useState('');
   const [logLevel, setLogLevel] = useState<BreadcrumbLevelType[]>([]);
   const handleSearch = debounce(query => setSearchTerm(query), 150);
@@ -59,17 +66,26 @@ function Console({breadcrumbs, startTimestamp = 0}: Props) {
 
       {filteredBreadcrumbs.length > 0 ? (
         <ConsoleTable>
-          {filteredBreadcrumbs.map((breadcrumb, i) => (
-            <ConsoleMessage
-              relativeTimestamp={showPlayerTime(
-                breadcrumb.timestamp || '',
-                startTimestamp
-              )}
-              key={i}
-              isLast={i === breadcrumbs.length - 1}
-              breadcrumb={breadcrumb}
-            />
-          ))}
+          {filteredBreadcrumbs.map((breadcrumb, i) => {
+            const diff = relativeTimeInMs(breadcrumb.timestamp || '', startTimestamp);
+            const onClick = () => setCurrentTime(diff);
+            const onMouseOver = () => setCurrentHoverTime(diff);
+            const onMouseOut = () => setCurrentHoverTime(undefined);
+            return (
+              <ConsoleMessage
+                onClick={onClick}
+                onMouseOver={onMouseOver}
+                onMouseOut={onMouseOut}
+                relativeTimestamp={showPlayerTime(
+                  breadcrumb.timestamp || '',
+                  startTimestamp
+                )}
+                key={i}
+                isLast={i === breadcrumbs.length - 1}
+                breadcrumb={breadcrumb}
+              />
+            );
+          })}
         </ConsoleTable>
       ) : (
         <StyledEmptyMessage title={t('No results found.')} />
diff --git a/static/app/views/replays/detail/focusArea.tsx b/static/app/views/replays/detail/focusArea.tsx
index 8a2f4afdff5d18..5da0cbdea41327 100644
--- a/static/app/views/replays/detail/focusArea.tsx
+++ b/static/app/views/replays/detail/focusArea.tsx
@@ -55,6 +55,8 @@ function FocusArea({replay}: Props) {
       ]);
       return (
         <Console
+          setCurrentHoverTime={setCurrentHoverTime}
+          setCurrentTime={setCurrentTime}
           breadcrumbs={consoleMessages ?? []}
           startTimestamp={event?.startTimestamp}
         />


Code review comments:
diff_hunk: @@ -59,17 +66,26 @@ function Console({breadcrumbs, startTimestamp = 0}: Props) {
 
       {filteredBreadcrumbs.length > 0 ? (
         <ConsoleTable>
-          {filteredBreadcrumbs.map((breadcrumb, i) => (
-            <ConsoleMessage
-              relativeTimestamp={showPlayerTime(
-                breadcrumb.timestamp || '',
-                startTimestamp
-              )}
-              key={i}
-              isLast={i === breadcrumbs.length - 1}
-              breadcrumb={breadcrumb}
-            />
-          ))}
+          {filteredBreadcrumbs.map((breadcrumb, i) => {
+            const diff = relativeTimeInMs(breadcrumb.timestamp || '', startTimestamp);
+            const onClick = () => setCurrentTime(diff);
+            const onMouseOver = () => setCurrentHoverTime(diff);
+            const onMouseOut = () => setCurrentHoverTime(undefined);
+            return (
+              <ConsoleMessage
+                onClick={onClick}
+                onMouseOver={onMouseOver}
+                onMouseOut={onMouseOut}
+                relativeTimestamp={showPlayerTime(
+                  breadcrumb.timestamp || '',
+                  startTimestamp
+                )}
path: static/app/views/replays/detail/console/index.tsx
text: @user1:
We should probably have `startTimestamp` as a prop for `ConsoleMessage` and calculate relativeTimestamp inside of that component.

diff_hunk: @@ -59,17 +66,26 @@ function Console({breadcrumbs, startTimestamp = 0}: Props) {
 
       {filteredBreadcrumbs.length > 0 ? (
         <ConsoleTable>
-          {filteredBreadcrumbs.map((breadcrumb, i) => (
-            <ConsoleMessage
-              relativeTimestamp={showPlayerTime(
-                breadcrumb.timestamp || '',
-                startTimestamp
-              )}
-              key={i}
-              isLast={i === breadcrumbs.length - 1}
-              breadcrumb={breadcrumb}
-            />
-          ))}
+          {filteredBreadcrumbs.map((breadcrumb, i) => {
+            const diff = relativeTimeInMs(breadcrumb.timestamp || '', startTimestamp);
+            const onClick = () => setCurrentTime(diff);
+            const onMouseOver = () => setCurrentHoverTime(diff);
+            const onMouseOut = () => setCurrentHoverTime(undefined);
+            return (
+              <ConsoleMessage
+                onClick={onClick}
+                onMouseOver={onMouseOver}
+                onMouseOut={onMouseOut}
path: static/app/views/replays/detail/console/index.tsx
text: @user1:
This won't be great when Console re-renders. Instead of prop drilling here, should we `useReplayContext` inside of `<ConsoleMessage>`?

@author:
I'm good with that! Will adjust it.