Repository: getsentry/sentry
Pull request: feat(replay): Add a breadcrumb for `replay.mutations`
This breadcrumb is sent when we detect a large number of mutations (currently 500, but will be increased). This *can* have an impact on end users. We want to start with a breadcrumb first and then eventually be more proactive about skipping processing of mutations.

![image](https://user-images.githubusercontent.com/79684/226481028-cb58a43b-cc72-4763-9d4d-9c440f3c38e6.png)


Closes #46057


Problem statement:
Add breadcrumb for large mutations
We should show a better UI for breadcrumbs with category `replay.mutations`. There is a count of the # of mutations that will be processed in `data.count`.

Icon = warning?
Title = Replay
Description = A large mutation has been detected (data.count). This can slow down the Replay SDK and impact your customers.

Hints from issue discussion:

Commit message: use t()

Patch to review:
diff --git a/static/app/components/replays/breadcrumbs/utils.tsx b/static/app/components/replays/breadcrumbs/utils.tsx
index e6b6e54a7d6327..d696e4da27ff98 100644
--- a/static/app/components/replays/breadcrumbs/utils.tsx
+++ b/static/app/components/replays/breadcrumbs/utils.tsx
@@ -3,17 +3,23 @@ import {IconWarning} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
 
+// Replay SDK can send `data` that does not conform to our issue/event breadcrumbs
+type MaybeCrumbData = null | Record<string, unknown>;
+
 /**
  * Generate breadcrumb descriptions based on type
  */
 export function getDescription(crumb: Crumb) {
-  if (typeof crumb.data === 'object' && crumb.data !== null && 'action' in crumb.data) {
-    switch (crumb.data.action) {
+  const crumbData: MaybeCrumbData = crumb.data as MaybeCrumbData;
+
+  if (typeof crumbData === 'object' && crumbData !== null && 'action' in crumbData) {
+    switch (crumbData.action) {
       case 'largest-contentful-paint':
-        if (crumb.data?.value !== undefined) {
-          return `${Math.round(crumb.data.value)}ms`;
+        if (typeof crumbData?.value === 'number') {
+          return `${Math.round(crumbData.value)}ms`;
         }
-        if (crumb.data?.duration !== undefined) {
+
+        if (crumbData?.duration !== undefined) {
           // this means user is using an old SDK where LCP values are not
           // always correct. Prompt them to upgrade
           return (
@@ -32,11 +38,18 @@ export function getDescription(crumb: Crumb) {
     }
   }
 
+  if (crumb.category === 'replay.mutations') {
+    return t(
+      'A large number of mutations was detected (%s). This can slow down the Replay SDK and impact your customers.',
+      crumbData?.count
+    );
+  }
+
   switch (crumb.type) {
     case BreadcrumbType.NAVIGATION:
-      return `${crumb.data?.to ?? ''}`;
+      return `${crumbData?.to ?? ''}`;
     case BreadcrumbType.DEFAULT:
-      return JSON.stringify(crumb.data);
+      return JSON.stringify(crumbData);
     default:
       return crumb.message || '';
   }
@@ -60,6 +73,9 @@ export function getTitle(crumb: Crumb) {
   if (type === 'ui') {
     return `User ${action || ''}`;
   }
+  if (type === 'replay') {
+    return `Replay`;
+  }
   return `${type} ${action || ''}`;
 }
 
diff --git a/static/app/utils/replays/replayDataUtils.tsx b/static/app/utils/replays/replayDataUtils.tsx
index 9eeab027ee5043..db0c42c800baf4 100644
--- a/static/app/utils/replays/replayDataUtils.tsx
+++ b/static/app/utils/replays/replayDataUtils.tsx
@@ -216,9 +216,22 @@ export function breadcrumbFactory(
 
   const rawCrumbsWithTimestamp: RawCrumb[] = rawCrumbs
     .filter(crumb => {
-      return !UNWANTED_CRUMB_CATEGORIES.includes(crumb.category || '');
+      return (
+        !UNWANTED_CRUMB_CATEGORIES.includes(crumb.category || '') &&
+        // Explicitly include replay breadcrumbs to ensure we have valid UI for them
+        (!crumb.category?.startsWith('replay') || crumb.category === 'replay.mutations')
+      );
     })
     .map(crumb => {
+      if (crumb.category === 'replay.mutations') {
+        return {
+          ...crumb,
+          type: BreadcrumbType.WARNING,
+          level: BreadcrumbLevelType.WARNING,
+          timestamp: new Date(crumb.timestamp * 1000).toISOString(),
+        };
+      }
+
       return {
         ...crumb,
         type: BreadcrumbType.DEFAULT,


Code review comments:
diff_hunk: @@ -3,17 +3,23 @@ import {IconWarning} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
 
+// Replay SDK can send `data` that does not conform to our issue/event breadcrumbs
+type MaybeCrumbData = null | Record<string, unknown>;
+
 /**
  * Generate breadcrumb descriptions based on type
  */
 export function getDescription(crumb: Crumb) {
-  if (typeof crumb.data === 'object' && crumb.data !== null && 'action' in crumb.data) {
-    switch (crumb.data.action) {
+  const crumbData: MaybeCrumbData = crumb.data as MaybeCrumbData;
+
+  if (typeof crumbData === 'object' && crumbData !== null && 'action' in crumbData) {
path: static/app/components/replays/breadcrumbs/utils.tsx
text: @user1:
`typeof crumbData === 'object' && crumbData !== null`

thanks javascript for making null an object 🤯 💥  🔫 

@user2:
ranks high on the list of biggest language mistakes of JS!

@user2:
This can be written with slighly less bytes as:

```js
if( crumbData && typeof crumbData === 'object' && 'action' in crumbData)
```

diff_hunk: @@ -3,17 +3,23 @@ import {IconWarning} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {BreadcrumbType, Crumb} from 'sentry/types/breadcrumbs';
 
+// Replay SDK can send `data` that does not conform to our issue/event breadcrumbs
+type MaybeCrumbData = null | Record<string, unknown>;
+
 /**
  * Generate breadcrumb descriptions based on type
  */
 export function getDescription(crumb: Crumb) {
-  if (typeof crumb.data === 'object' && crumb.data !== null && 'action' in crumb.data) {
-    switch (crumb.data.action) {
+  const crumbData: MaybeCrumbData = crumb.data as MaybeCrumbData;
+
+  if (typeof crumbData === 'object' && crumbData !== null && 'action' in crumbData) {
path: static/app/components/replays/breadcrumbs/utils.tsx
text: @author:
```suggestion
  if (crumbData && typeof crumbData === 'object' && 'action' in crumbData) {
```