Browse Source

feat(replay): Add logging to track replay data in the browser (#50533)

This adds logging to Replay Details page, very similar to pageview, but
it's "data loaded" because we have to wait a bit before we can log.

The idea is that we want to know how many, and what type, of errors are
associated with the replay that is being viewed.
This will let us create charts where we compare:
- How many times is the Replay Details page loaded with a valid replay
- Of those times, how many instances have different types of errors,
like backend or frontend

Fixes https://github.com/getsentry/sentry/issues/50500
Ryan Albrecht 1 year ago
parent
commit
616865a1d0

+ 1 - 15
static/app/components/events/eventReplay/index.tsx

@@ -6,7 +6,7 @@ import {Organization} from 'sentry/types';
 import {Event} from 'sentry/types/event';
 import {useHasOrganizationSentAnyReplayEvents} from 'sentry/utils/replays/hooks/useReplayOnboarding';
 import {projectCanLinkToReplay} from 'sentry/utils/replays/projectSupportsReplay';
-import useProjects from 'sentry/utils/useProjects';
+import useProjectFromSlug from 'sentry/utils/useProjectFromSlug';
 
 type Props = {
   event: Event;
@@ -15,20 +15,6 @@ type Props = {
   replayId: undefined | string;
 };
 
-function useProjectFromSlug({
-  organization,
-  projectSlug,
-}: {
-  organization: Organization;
-  projectSlug: string;
-}) {
-  const {fetching, projects} = useProjects({
-    slugs: [projectSlug],
-    orgId: organization.slug,
-  });
-  return fetching ? undefined : projects[0];
-}
-
 export default function EventReplay({replayId, organization, projectSlug, event}: Props) {
   const hasReplaysFeature = organization.features.includes('session-replay');
   const {hasOrgSentReplays, fetching} = useHasOrganizationSentAnyReplayEvents();

+ 8 - 0
static/app/utils/analytics/replayAnalyticsEvents.tsx

@@ -2,6 +2,13 @@ import type {LayoutKey} from 'sentry/utils/replays/hooks/useReplayLayout';
 import {Output} from 'sentry/views/replays/detail/network/details/getOutputType';
 
 export type ReplayEventParameters = {
+  'replay.details-data-loaded': {
+    be_errors: number;
+    fe_errors: number;
+    project_platform: string;
+    replay_errors: number;
+    total_errors: number;
+  };
   'replay.details-layout-changed': {
     chosen_layout: LayoutKey;
     default_layout: LayoutKey;
@@ -87,6 +94,7 @@ export type ReplayEventParameters = {
 export type ReplayEventKey = keyof ReplayEventParameters;
 
 export const replayEventMap: Record<ReplayEventKey, string | null> = {
+  'replay.details-data-loaded': 'Replay Details Data Loaded',
   'replay.details-layout-changed': 'Changed Replay Details Layout',
   'replay.details-network-panel-closed': 'Closed Replay Network Details Panel',
   'replay.details-network-panel-opened': 'Opened Replay Network Details Panel',

+ 40 - 0
static/app/utils/replays/hooks/useLogReplayDataLoaded.tsx

@@ -0,0 +1,40 @@
+import {useEffect} from 'react';
+
+import {trackAnalytics} from 'sentry/utils/analytics';
+import type useReplayData from 'sentry/utils/replays/hooks/useReplayData';
+import useOrganization from 'sentry/utils/useOrganization';
+import useProjectFromSlug from 'sentry/utils/useProjectFromSlug';
+
+interface Props
+  extends Pick<
+    ReturnType<typeof useReplayData>,
+    'fetchError' | 'fetching' | 'projectSlug' | 'replay'
+  > {}
+
+function useLogReplayDataLoaded({fetchError, fetching, projectSlug, replay}: Props) {
+  const organization = useOrganization();
+  const project = useProjectFromSlug({
+    organization,
+    projectSlug: projectSlug ?? undefined,
+  });
+
+  useEffect(() => {
+    if (fetching || fetchError || !replay || !project) {
+      return;
+    }
+    const feErrorIds = replay.getReplay().error_ids || [];
+    const allErrors = replay.getRawErrors();
+    const beErrorCount = allErrors.filter(error => !feErrorIds.includes(error.id)).length;
+
+    trackAnalytics('replay.details-data-loaded', {
+      organization,
+      be_errors: beErrorCount,
+      fe_errors: feErrorIds.length,
+      project_platform: project.platform!,
+      replay_errors: 0,
+      total_errors: allErrors.length,
+    });
+  }, [organization, project, fetchError, fetching, projectSlug, replay]);
+}
+
+export default useLogReplayDataLoaded;

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

@@ -90,6 +90,8 @@ export default class ReplayReader {
       replayRecord.finished_at.getTime() - replayRecord.started_at.getTime()
     );
 
+    this.rawErrors = errors;
+
     this.sortedSpans = spansFactory(spans);
     this.breadcrumbs = breadcrumbFactory(
       replayRecord,
@@ -105,6 +107,7 @@ export default class ReplayReader {
     this.replayRecord = replayRecord;
   }
 
+  private rawErrors: ReplayError[];
   private sortedSpans: ReplaySpan[];
   private replayRecord: ReplayRecord;
   private rrwebEvents: RecordingEvent[];
@@ -146,6 +149,8 @@ export default class ReplayReader {
     this.breadcrumbs.filter(crumb => ['console', 'issue'].includes(crumb.category || ''))
   );
 
+  getRawErrors = memoize(() => this.rawErrors);
+
   getNonConsoleCrumbs = memoize(() =>
     this.breadcrumbs.filter(crumb => crumb.category !== 'console')
   );

+ 18 - 0
static/app/utils/useProjectFromSlug.tsx

@@ -0,0 +1,18 @@
+import {Organization} from 'sentry/types';
+import useProjects from 'sentry/utils/useProjects';
+
+function useProjectFromSlug({
+  organization,
+  projectSlug,
+}: {
+  organization: Organization;
+  projectSlug: undefined | string;
+}) {
+  const {fetching, projects} = useProjects({
+    slugs: projectSlug ? [projectSlug] : undefined,
+    orgId: organization.slug,
+  });
+  return fetching ? undefined : projects[0];
+}
+
+export default useProjectFromSlug;

+ 3 - 0
static/app/views/replays/details.tsx

@@ -14,6 +14,7 @@ import {t} from 'sentry/locale';
 import useInitialTimeOffsetMs, {
   TimeOffsetLocationQueryParams,
 } from 'sentry/utils/replays/hooks/useInitialTimeOffsetMs';
+import useLogReplayDataLoaded from 'sentry/utils/replays/hooks/useLogReplayDataLoaded';
 import useReplayData from 'sentry/utils/replays/hooks/useReplayData';
 import useReplayLayout from 'sentry/utils/replays/hooks/useReplayLayout';
 import useReplayPageview from 'sentry/utils/replays/hooks/useReplayPageview';
@@ -51,6 +52,8 @@ function ReplayDetails({params: {replaySlug}}: Props) {
     orgSlug,
   });
 
+  useLogReplayDataLoaded({fetching, fetchError, replay, projectSlug});
+
   const initialTimeOffsetMs = useInitialTimeOffsetMs({
     orgSlug,
     projectSlug,