Просмотр исходного кода

feat(replays): Force a 'Replay Start' breadcrumb in the Chapters list (#34734)

Insert a new 'Replay Start' breadcrumb to the top of the Chapters list, with a tooltip to show the initial url from when the replay started.

There is no navigation event at this part of the replay because the navigation happened before the replay SDK got started up, so we didn't observe it. The SDK might've been started as soon as the page loaded (very likely) or it might've been started some long time later, so this special breadcrumb type marks the beginning of the replay dataset.

**NOTE THAT** the timestamp is the start of the dataset, and might not exactly represent the time the SDK was started. Instead it could be the timestamp of an earlier network activity that that we read from a browser api.

Fixes #34693
Ryan Albrecht 2 лет назад
Родитель
Сommit
3d8f08c1e1

+ 0 - 13
static/app/components/events/interfaces/breadcrumbs/utils.tsx

@@ -149,19 +149,6 @@ export function transformCrumbs(breadcrumbs: Array<RawCrumb>): Crumb[] {
   });
 }
 
-// In the future if we want to show more items at
-// the EventChapter, we should add to this array.
-const USER_ACTIONS = [
-  BreadcrumbType.USER,
-  BreadcrumbType.UI,
-  BreadcrumbType.ERROR,
-  BreadcrumbType.NAVIGATION,
-];
-
-export function onlyUserActions(crumbs: Crumb[]): Crumb[] {
-  return crumbs.filter(crumb => USER_ACTIONS.includes(crumb.type));
-}
-
 function moduleToCategory(module: string | null | undefined) {
   if (!module) {
     return undefined;

+ 5 - 0
static/app/components/replays/actionCategory.tsx

@@ -34,6 +34,11 @@ function getActionCategoryInfo(crumb: Crumb): ActionCategoryInfo {
         title: t('Error'),
         description: `${crumb.category}: ${crumb.message}`,
       };
+    case BreadcrumbType.INIT:
+      return {
+        title: t('Replay Start'),
+        description: crumb.data?.url,
+      };
     default:
       return {
         title: t('Default'),

+ 2 - 0
static/app/types/breadcrumbs.tsx

@@ -29,6 +29,7 @@ export enum BreadcrumbType {
   SESSION = 'session',
   TRANSACTION = 'transaction',
   CONSOLE = 'console',
+  INIT = 'init',
 }
 
 type BreadcrumbTypeBase = {
@@ -98,6 +99,7 @@ export type BreadcrumbTypeDefault = {
     | BreadcrumbType.WARNING
     | BreadcrumbType.ERROR
     | BreadcrumbType.DEFAULT
+    | BreadcrumbType.INIT
     | BreadcrumbType.SESSION
     | BreadcrumbType.SYSTEM
     | BreadcrumbType.SESSION

+ 2 - 3
static/app/utils/replays/getCurrentUrl.tsx

@@ -9,13 +9,12 @@ function findUrlTag(tags: EventTag[]) {
   return tags.find(tag => tag.key === 'url');
 }
 
-function getCurrentUrl(replay: ReplayReader, currentTime: number) {
+function getCurrentUrl(replay: ReplayReader, currentOffsetMS: number) {
   const event = replay.getEvent();
   const crumbs = replay.getRawCrumbs();
 
-  const currentOffsetMs = Math.floor(currentTime);
   const startTimestampMs = event.startTimestamp * 1000;
-  const currentTimeMs = startTimestampMs + currentOffsetMs;
+  const currentTimeMs = startTimestampMs + Math.floor(currentOffsetMS);
 
   const navigationCrumbs = transformCrumbs(crumbs).filter(
     crumb => crumb.type === BreadcrumbType.NAVIGATION

+ 20 - 5
static/app/utils/replays/replayDataUtils.tsx

@@ -3,8 +3,10 @@ import type {eventWithTime} from 'rrweb/typings/types';
 
 import {getVirtualCrumb} from 'sentry/components/events/interfaces/breadcrumbs/utils';
 import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types';
-import type {RawCrumb} from 'sentry/types/breadcrumbs';
-import {Entry, EntryType, Event} from 'sentry/types/event';
+import {t} from 'sentry/locale';
+import type {BreadcrumbTypeDefault, RawCrumb} from 'sentry/types/breadcrumbs';
+import {BreadcrumbLevelType, BreadcrumbType} from 'sentry/types/breadcrumbs';
+import {Entry, EntryType, Event, EventTag} from 'sentry/types/event';
 
 export function rrwebEventListFactory(
   startTimestampMS: number,
@@ -56,12 +58,25 @@ export function breadcrumbValuesFromEvents(events: Event[]) {
   return ([] as RawCrumb[]).concat(fromEntries).concat(fromEvents);
 }
 
-export function breadcrumbEntryFactory(_startTimestamp: number, rawCrumbs: RawCrumb[]) {
-  // TODO: insert init breadcrumb
+export function breadcrumbEntryFactory(
+  startTimestamp: number,
+  tags: EventTag[],
+  rawCrumbs: RawCrumb[]
+) {
+  const initBreadcrumb = {
+    type: BreadcrumbType.INIT,
+    timestamp: new Date(startTimestamp).toISOString(),
+    level: BreadcrumbLevelType.INFO,
+    action: 'replay-init',
+    message: t('Start recording'),
+    data: {
+      url: tags.find(tag => tag.key === 'url')?.value,
+    },
+  } as BreadcrumbTypeDefault;
 
   const stringified = rawCrumbs.map(value => JSON.stringify(value));
   const deduped = Array.from(new Set(stringified));
-  const values = deduped.map(value => JSON.parse(value));
+  const values = [initBreadcrumb].concat(deduped.map(value => JSON.parse(value)));
 
   values.sort((a, b) => +new Date(a?.timestamp || 0) - +new Date(b?.timestamp || 0));
 

+ 5 - 1
static/app/utils/replays/replayReader.tsx

@@ -54,7 +54,11 @@ export default class ReplayReader {
       rawSpanData
     );
 
-    this.breadcrumbEntry = breadcrumbEntryFactory(startTimestampMS, rawCrumbs);
+    this.breadcrumbEntry = breadcrumbEntryFactory(
+      startTimestampMS,
+      event.tags,
+      rawCrumbs
+    );
     this.spanEntry = spanEntryFactory(rawSpanData);
 
     this.rrwebEvents = rrwebEventListFactory(

+ 12 - 6
static/app/views/replays/detail/userActionsNavigator.tsx

@@ -2,10 +2,7 @@ import {Fragment, useCallback} from 'react';
 import styled from '@emotion/styled';
 
 import Type from 'sentry/components/events/interfaces/breadcrumbs/breadcrumb/type';
-import {
-  onlyUserActions,
-  transformCrumbs,
-} from 'sentry/components/events/interfaces/breadcrumbs/utils';
+import {transformCrumbs} from 'sentry/components/events/interfaces/breadcrumbs/utils';
 import {
   Panel as BasePanel,
   PanelBody as BasePanelBody,
@@ -19,7 +16,7 @@ import {useReplayContext} from 'sentry/components/replays/replayContext';
 import {relativeTimeInMs} from 'sentry/components/replays/utils';
 import {t} from 'sentry/locale';
 import space from 'sentry/styles/space';
-import {Crumb, RawCrumb} from 'sentry/types/breadcrumbs';
+import {BreadcrumbType, Crumb, RawCrumb} from 'sentry/types/breadcrumbs';
 import {EventTransaction} from 'sentry/types/event';
 import {getCurrentUserAction} from 'sentry/utils/replays/getCurrentUserAction';
 
@@ -49,12 +46,21 @@ type ContainerProps = {
   isSelected: boolean;
 };
 
+const USER_ACTIONS = [
+  BreadcrumbType.ERROR,
+  BreadcrumbType.INIT,
+  BreadcrumbType.NAVIGATION,
+  BreadcrumbType.UI,
+  BreadcrumbType.USER,
+];
+
 function UserActionsNavigator({event, crumbs}: Props) {
   const {setCurrentTime, setCurrentHoverTime, currentHoverTime, currentTime} =
     useReplayContext();
 
   const {startTimestamp} = event || {};
-  const userActionCrumbs = crumbs && onlyUserActions(transformCrumbs(crumbs));
+  const userActionCrumbs =
+    crumbs && transformCrumbs(crumbs).filter(crumb => USER_ACTIONS.includes(crumb.type));
   const isLoaded = userActionCrumbs && startTimestamp;
 
   const currentUserAction = getCurrentUserAction(