Repository: getsentry/sentry
Pull request: fix(replays): search is more robust now that we look inside of breadcrumb data.
Closes #34805 

<img width="986" alt="image" src="https://user-images.githubusercontent.com/7014871/169409873-7ce69a99-0dfb-4a48-be4c-106f7886ad59.png">
<img width="493" alt="image" src="https://user-images.githubusercontent.com/7014871/169409893-a0938150-2859-400a-a947-bf9b9e43f890.png">
<img width="986" alt="image" src="https://user-images.githubusercontent.com/7014871/169409941-5bc83d8a-04a5-4b4f-8d6d-3b77f454cfee.png">

Created a more robust search function that accounts for all of what's shown within the console window.
<!-- 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:
Fix the replay details console search to look thru every aspect of the console log.
### Problem Statement

Search doesn't work quite right as certain parts of the message aren't found. Need to fix that so search works like a user would expect.

### 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: made the filter function a first class function that just filters the breadcrumbs

Patch to review:
diff --git a/static/app/views/replays/detail/console/index.tsx b/static/app/views/replays/detail/console/index.tsx
index 99067571dd4bea..3b2fdd8e1a2532 100644
--- a/static/app/views/replays/detail/console/index.tsx
+++ b/static/app/views/replays/detail/console/index.tsx
@@ -21,20 +21,33 @@ interface Props {
 const getDistinctLogLevels = breadcrumbs =>
   Array.from(new Set<string>(breadcrumbs.map(breadcrumb => breadcrumb.level)));
 
+export const filterBreadcrumbs = (
+  breadcrumbs: BreadcrumbTypeDefault[],
+  searchTerm: string,
+  logLevel: Array<string>
+) => {
+  if (!searchTerm && logLevel.length === 0) {
+    return breadcrumbs;
+  }
+  return breadcrumbs.filter(breadcrumb => {
+    const normalizedSearchTerm = searchTerm.toLowerCase();
+    const doesMatch = JSON.stringify(breadcrumb.data)
+      .toLowerCase()
+      .includes(normalizedSearchTerm);
+    if (logLevel.length > 0) {
+      return doesMatch && logLevel.includes(breadcrumb.level);
+    }
+    return doesMatch;
+  });
+};
+
 function Console({breadcrumbs, startTimestamp = 0}: Props) {
   const [searchTerm, setSearchTerm] = useState('');
   const [logLevel, setLogLevel] = useState<BreadcrumbLevelType[]>([]);
   const handleSearch = debounce(query => setSearchTerm(query), 150);
 
   const filteredBreadcrumbs = useMemo(
-    () =>
-      !searchTerm && logLevel.length === 0
-        ? breadcrumbs
-        : breadcrumbs.filter(
-            breadcrumb =>
-              breadcrumb.message?.toLowerCase().includes(searchTerm) &&
-              logLevel.includes(breadcrumb.level)
-          ),
+    () => filterBreadcrumbs(breadcrumbs, searchTerm, logLevel),
     [logLevel, searchTerm, breadcrumbs]
   );
 
diff --git a/tests/js/spec/views/replays/detail/console/index.spec.jsx b/tests/js/spec/views/replays/detail/console/index.spec.jsx
new file mode 100644
index 00000000000000..5cdea9ee4b1c21
--- /dev/null
+++ b/tests/js/spec/views/replays/detail/console/index.spec.jsx
@@ -0,0 +1,106 @@
+import {filterBreadcrumbs} from 'sentry/views/replays/detail/console';
+
+const breadcrumbs = [
+  {
+    type: 'default',
+    timestamp: '2022-05-11T23:00:45.094000Z',
+    level: 'info',
+    message: 'longtask - does not exist [object PerformanceLongTaskTiming]',
+    category: 'console',
+    data: {
+      arguments: [
+        'longtask - does not exist',
+        {
+          attribution: ['[object TaskAttributionTiming]'],
+          duration: 76,
+          entryType: 'longtask',
+          name: 'self',
+          startTime: 3741,
+        },
+      ],
+      logger: 'console',
+    },
+    event_id: null,
+  },
+  {
+    type: 'default',
+    timestamp: '2022-05-11T23:00:45.093000Z',
+    level: 'info',
+    message: 'event - does not exist [object PerformanceEventTiming]',
+    category: 'console',
+    data: {
+      arguments: [
+        'event - does not exist',
+        {
+          cancelable: true,
+          duration: 160,
+          entryType: 'event',
+          name: 'keyup',
+          processingEnd: 505,
+          processingStart: 505,
+          startTime: 347.90000009536743,
+        },
+      ],
+      logger: 'console',
+    },
+    event_id: null,
+  },
+  {
+    type: 'default',
+    timestamp: '2022-05-11T23:04:27.576000Z',
+    level: 'error',
+    message:
+      'The above error occurred in the <TestButton> component:\n\n    at TestButton (webpack-internal:///./app/views/userFeedback/index.tsx:224:76)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at ButtonBar (webpack-internal:///./app/components/buttonBar.tsx:30:5)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at ErrorBoundary (webpack-internal:///./app/components/errorBoundary.tsx:45:5)\n    at div\n    at NoProjectMessage (webpack-internal:///./app/components/noProjectMessage.tsx:45:5)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at Container (webpack-internal:///./app/components/organizations/pageFilters/container.tsx:66:5)\n    at eval (webpack-internal:///../node_modules/create-react-class/factory.js:821:37)\n    at _class (webpack-internal:///./app/utils/withOrganization.tsx:24:19)\n    at DocumentTitle\n    at SideEffect (webpack-internal:///../node_modules/react-side-effect/lib/index.js:74:27)\n    at SentryDocumentTitle (webpack-internal:///./app/components/sentryDocumentTitle.tsx:22:5)\n    at OrganizationUserFeedback (webpack-internal:///./app/views/userFeedback/index.tsx:73:1)\n    at Profiler (webpack-internal:///../node_modules/@sentry/react/esm/profiler.js:83:28)\n    at profiler(OrganizationUserFeedback)\n    at _class (webpack-internal:///./app/utils/withOrganization.tsx:24:19)\n    at LazyLoad (webpack-internal:///./app/components/lazyLoad.tsx:39:5)\n    at ErrorHandler (webpack-internal:///./app/utils/errorHandler.tsx:24:7)\n    at ErrorBoundary (webpack-internal:///./app/components/errorBoundary.tsx:45:5)\n    at OrganizationDetailsBody (webpack-internal:///./app/views/organizationDetails/body.tsx:131:5)\n    at _class (webpack-internal:///./app/utils/withOrganization.tsx:24:19)\n    at div\n    at DocumentTitle\n    at SideEffect (webpack-internal:///../node_modules/react-side-effect/lib/index.js:74:27)\n    at SentryDocumentTitle (webpack-internal:///./app/components/sentryDocumentTitle.tsx:22:5)\n    at OrganizationContextContainer (webpack-internal:///./app/views/organizationContextContainer.tsx:157:5)\n    at Profiler (webpack-internal:///../node_modules/@sentry/react/esm/profiler.js:83:28)\n    at profiler(OrganizationContextContainer)\n    at WithOrganizations (webpack-internal:///./app/utils/withOrganizations.tsx:27:7)\n    at WithApi (webpack-internal:///./app/utils/withApi.tsx:35:12)\n    at OrganizationDetails (webpack-internal:///./app/views/organizationDetails/index.tsx:27:5)\n    at ErrorHandler (webpack-internal:///./app/utils/errorHandler.tsx:24:7)\n    at ErrorBoundary (webpack-internal:///./app/components/errorBoundary.tsx:45:5)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at App (webpack-internal:///./app/views/app/index.tsx:72:5)\n    at ErrorHandler (webpack-internal:///./app/utils/errorHandler.tsx:24:7)\n    at eval (webpack-internal:///../node_modules/create-react-class/factory.js:821:37)\n    at eval (webpack-internal:///../node_modules/create-react-class/factory.js:821:37)\n    at PersistedStoreProvider (webpack-internal:///./app/stores/persistedStore.tsx:59:76)\n    at ThemeProvider (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:79:64)\n    at ThemeAndStyleProvider (webpack-internal:///./app/components/themeAndStyleProvider.tsx:45:5)\n    at Main\n\nReact will try to recreate this component tree from scratch using the error boundary you provided, ErrorBoundary.',
+    category: 'console',
+    data: {
+      arguments: [
+        'The above error occurred in the <TestButton> component:\n\n    at TestButton (webpack-internal:///./app/views/userFeedback/index.tsx:224:76)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at ButtonBar (webpack-internal:///./app/components/buttonBar.tsx:30:5)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at ErrorBoundary (webpack-internal:///./app/components/errorBoundary.tsx:45:5)\n    at div\n    at NoProjectMessage (webpack-internal:///./app/components/noProjectMessage.tsx:45:5)\n    at div\n    at eval (webpack-internal:///../node_modules/@emotion/react/dist/emotion-element-a8309070.browser.esm.js:45:66)\n    at Container (webpack-internal:///./app/components/organizations/pageFilters/container.tsx:66:5)\n    at eval (webpack-internal:///../node_modules/create-react-class/factory.js:821:37)\n    at _class (webpack-internal:///./app/utils/withOrganization.tsx:24:19)\n    at DocumentTitle\n    at SideEffect (webpack-internal:///../node_modules/react-side-effect/lib/index.js:74:27)\n    at SentryDocumentTitle (webpack-internal:///./app/components/sentryDocumentTitle.tsx:22:5)\n    at OrganizationUserFeedback (webpack-internal:///./app/views/userFeedback/index.tsx:73:1)\n    at Profiler (webpack-internal:///../node_modules/@sentry/react/esm/profiler.js:83:28)\n    at profiler(OrganizationUserFeedback)\n    at _class (webpack-internal:///./app/utils/withOrganization.tsx:24:19)\n    at LazyLoad (webpack-internal:///./app/components/lazyLoad.tsx:39:5)\n    at ErrorHandler (webpack-internal:///./app/utils/errorHandler.tsx:24:7)\n    at ErrorBoundary (webpack-internal:///./app/components/errorBoundary.tsx:45:5)\n    at OrganizationDetailsBody (webpack-internal:///./app/views/organizationDetails/body.tsx:131:5)\n    at _class (webpack-internal:///./app/utils/withOrganization.tsx:24:19)\n    at div\n    at DocumentTitle\n    at SideEffect (webpack-internal:///../node_modules/...',
+      ],
+    },
+    event_id: null,
+  },
+  {
+    type: 'default',
+    timestamp: '2022-05-11T23:05:51.531000Z',
+    level: 'warning',
+    message:
+      'Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n* Move code with side effects to componentDidMount, and set initial state in the constructor.\n* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\nPlease update the following components: %s Router, RouterContext',
+    category: 'console',
+    data: {
+      arguments: [
+        'Warning: componentWillMount has been renamed, and is not recommended for use. See https://reactjs.org/link/unsafe-component-lifecycles for details.\n\n* Move code with side effects to componentDidMount, and set initial state in the constructor.\n* Rename componentWillMount to UNSAFE_componentWillMount to suppress this warning in non-strict mode. In React 18.x, only the UNSAFE_ name will work. To rename all deprecated lifecycles to their new names, you can run `npx react-codemod rename-unsafe-lifecycles` in your project source folder.\n\nPlease update the following components: %s',
+        'Router, RouterContext',
+        'find me',
+      ],
+      logger: 'console',
+    },
+    event_id: null,
+  },
+];
+
+describe('replays/details/console', () => {
+  it('filters console breadcrumbs by log level', () => {
+    const result = filterBreadcrumbs(breadcrumbs, '', ['error', 'warning']);
+    expect(result.length).toEqual(2);
+  });
+
+  it('filters console breadcrumbs by search term', () => {
+    const result = filterBreadcrumbs(breadcrumbs, 'comp', []);
+    expect(result.length).toEqual(2);
+  });
+
+  it('filters console breadcrumbs by search term where match is within data.arguments', () => {
+    const result = filterBreadcrumbs(breadcrumbs, 'find me', []);
+    expect(result.length).toEqual(1);
+  });
+
+  it('filters console breadcrumbs by search term and log level', () => {
+    const result = filterBreadcrumbs(breadcrumbs, 'tEs', ['error']);
+    expect(result.length).toEqual(1);
+  });
+
+  it('does not filter if no searchTerm or logLevel filters are present', () => {
+    const result = filterBreadcrumbs(breadcrumbs, '', []);
+    expect(result.length).toEqual(4);
+  });
+});


Code review comments:
diff_hunk: @@ -21,20 +21,33 @@ interface Props {
 const getDistinctLogLevels = breadcrumbs =>
   Array.from(new Set<string>(breadcrumbs.map(breadcrumb => breadcrumb.level)));
 
+export const filterBreadcrumbs = (
+  breadcrumbs: BreadcrumbTypeDefault[],
+  searchTerm: string,
+  logLevel: Array<string>
+) => {
+  if (!searchTerm && logLevel.length === 0) {
+    return breadcrumbs;
+  }
+  return breadcrumbs.filter(breadcrumb => {
+    const normalizedSearchTerm = searchTerm.toLowerCase();
path: static/app/views/replays/detail/console/index.tsx
text: @user1:
```suggestion
  }
  const normalizedSearchTerm = searchTerm.toLowerCase();
  return breadcrumbs.filter(breadcrumb => {
```