Repository: getsentry/sentry
Pull request: ref(similarity-embedding): Add analytics for similar issue diff
Any time a user clicks on a diff on any of the issues in the Similar issues tab, we should log the following in an analytic event:

- Project and Org ids
- Issue ids for both issues
- Error messages for both issues
- Stacktraces for both issues
- transactions for both issues

closes https://github.com/getsentry/sentry/issues/64557

Problem statement:
Add analytics to similar issues diff
- Any time a user clicks on a diff on any of the issues in the Similar issues tab, we should log the following in an analytic event
    - Project and Org ids
    - Issue ids for both issues
    - Error messages for both issues
    - Stacktraces for both issues
    - transactions for both issues

Hints from issue discussion:

Commit message: ref(similarity-embedding): Add analytics for similar issue diff

Patch to review:
diff --git a/static/app/components/issueDiff/index.spec.tsx b/static/app/components/issueDiff/index.spec.tsx
index e2b7c1cf64a958..e3c9bef4297862 100644
--- a/static/app/components/issueDiff/index.spec.tsx
+++ b/static/app/components/issueDiff/index.spec.tsx
@@ -1,17 +1,21 @@
 import {Entries123Base, Entries123Target} from 'sentry-fixture/entries';
+import {OrganizationFixture} from 'sentry-fixture/organization';
 import {ProjectFixture} from 'sentry-fixture/project';
 
 import {render, screen} from 'sentry-test/reactTestingLibrary';
 
 import {IssueDiff} from 'sentry/components/issueDiff';
+import {trackAnalytics} from 'sentry/utils/analytics';
 
 jest.mock('sentry/api');
+jest.mock('sentry/utils/analytics');
 
 describe('IssueDiff', function () {
   const entries123Target = Entries123Target();
   const entries123Base = Entries123Base();
   const api = new MockApiClient();
-  const project = ProjectFixture();
+  const organization = OrganizationFixture();
+  const project = ProjectFixture({features: ['similarity-embeddings']});
 
   beforeEach(function () {
     MockApiClient.addMockResponse({
@@ -67,10 +71,12 @@ describe('IssueDiff', function () {
         targetIssueId="target"
         orgId="org-slug"
         project={project}
+        organization={organization}
       />
     );
 
     expect(await screen.findByTestId('split-diff')).toBeInTheDocument();
+    expect(trackAnalytics).toHaveBeenCalled();
   });
 
   it('can diff message', async function () {
diff --git a/static/app/components/issueDiff/index.tsx b/static/app/components/issueDiff/index.tsx
index 77248f8e876db7..2b1ddb9cccb7bd 100644
--- a/static/app/components/issueDiff/index.tsx
+++ b/static/app/components/issueDiff/index.tsx
@@ -8,7 +8,8 @@ import LoadingIndicator from 'sentry/components/loadingIndicator';
 import type SplitDiff from 'sentry/components/splitDiff';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
-import type {Project} from 'sentry/types';
+import type {Organization, Project} from 'sentry/types';
+import {trackAnalytics} from 'sentry/utils/analytics';
 import getStacktraceBody from 'sentry/utils/getStacktraceBody';
 import withApi from 'sentry/utils/withApi';
 
@@ -27,6 +28,7 @@ type Props = {
   targetIssueId: string;
   baseEventId?: string;
   className?: string;
+  organization?: Organization;
   targetEventId?: string;
 };
 
@@ -55,28 +57,69 @@ class IssueDiff extends Component<Props, State> {
   }
 
   fetchData() {
-    const {baseIssueId, targetIssueId, baseEventId, targetEventId} = this.props;
+    const {
+      baseIssueId,
+      targetIssueId,
+      baseEventId,
+      targetEventId,
+      organization,
+      project,
+    } = this.props;
+    const hasSimilarityEmbeddingsFeature = project.features.includes(
+      'similarity-embeddings'
+    );
 
     // Fetch component and event data
-    Promise.all([
-      import('../splitDiff'),
-      this.fetchEventData(baseIssueId, baseEventId ?? 'latest'),
-      this.fetchEventData(targetIssueId, targetEventId ?? 'latest'),
-    ])
-      .then(([{default: SplitDiffAsync}, baseEvent, targetEvent]) => {
+    const asyncFetch = async () => {
+      try {
+        const [baseEventData, targetEventData] = await Promise.all([
+          this.fetchEvent(baseIssueId, baseEventId ?? 'latest'),
+          this.fetchEvent(targetIssueId, targetEventId ?? 'latest'),
+        ]);
+
+        const [{default: SplitDiffAsync}, baseEvent, targetEvent] = await Promise.all([
+          import('../splitDiff'),
+          getStacktraceBody(baseEventData),
+          getStacktraceBody(targetEventData),
+        ]);
+
         this.setState({
           SplitDiffAsync,
           baseEvent,
           targetEvent,
           loading: false,
         });
-      })
-      .catch(() => {
+        if (organization && hasSimilarityEmbeddingsFeature) {
+          trackAnalytics('issue_details.similar_issues.diff_clicked', {
+            organization,
+            project_id: baseEventData?.projectID,
+            group_id: baseEventData?.groupID,
+            error_message: baseEventData?.message
+              ? baseEventData.message
+              : baseEventData?.title,
+            stacktrace: baseEvent.join('/n '),
+            transaction: this.getTransaction(
+              baseEventData?.tags ? baseEventData.tags : []
+            ),
+            parent_group_id: targetEventData?.groupID,
+            parent_error_message: targetEventData?.message
+              ? targetEventData.message
+              : targetEventData?.title,
+            parent_stacktrace: targetEvent.join('/n '),
+            parent_transaction: this.getTransaction(
+              targetEventData?.tags ? targetEventData.tags : []
+            ),
+          });
+        }
+      } catch {
         addErrorMessage(t('Error loading events'));
-      });
+      }
+    };
+
+    asyncFetch();
   }
 
-  fetchEventData = async (issueId: string, eventId: string) => {
+  fetchEvent = async (issueId: string, eventId: string) => {
     const {orgId, project, api} = this.props;
 
     let paramEventId = eventId;
@@ -89,7 +132,16 @@ class IssueDiff extends Component<Props, State> {
     const event = await api.requestPromise(
       `/projects/${orgId}/${project.slug}/events/${paramEventId}/`
     );
-    return getStacktraceBody(event);
+    return event;
+  };
+
+  getTransaction = (tags: any[]) => {
+    for (let i = 0; i < tags.length; i++) {
+      if (tags[i].key === 'transaction') {
+        return tags[i].value;
+      }
+    }
+    return undefined;
   };
 
   render() {
diff --git a/static/app/components/modals/diffModal.tsx b/static/app/components/modals/diffModal.tsx
index 094670b4dbded4..e472aea915c71d 100644
--- a/static/app/components/modals/diffModal.tsx
+++ b/static/app/components/modals/diffModal.tsx
@@ -2,14 +2,16 @@ import {css} from '@emotion/react';
 
 import type {ModalRenderProps} from 'sentry/actionCreators/modal';
 import IssueDiff from 'sentry/components/issueDiff';
+import useOrganization from 'sentry/utils/useOrganization';
 
 type Props = ModalRenderProps & React.ComponentProps<typeof IssueDiff>;
 
 function DiffModal({className, Body, CloseButton, ...props}: Props) {
+  const organization = useOrganization();
   return (
     <Body>
       <CloseButton />
-      <IssueDiff className={className} {...props} />
+      <IssueDiff className={className} organization={organization} {...props} />
     </Body>
   );
 }
diff --git a/static/app/stores/groupingStore.tsx b/static/app/stores/groupingStore.tsx
index f38b143f4a8c45..738a332027a58a 100644
--- a/static/app/stores/groupingStore.tsx
+++ b/static/app/stores/groupingStore.tsx
@@ -47,7 +47,7 @@ type State = {
   unmergeState: Map<any, any>;
 };
 
-type ScoreMap = Record<string, number | null>;
+type ScoreMap = Record<string, number | null | string>;
 
 type ApiFingerprint = {
   id: string;
@@ -88,18 +88,20 @@ export type SimilarItem = {
   aggregate?: {
     exception: number;
     message: number;
+    shouldBeGrouped?: string;
   };
   score?: Record<string, number | null>;
   scoresByInterface?: {
     exception: Array<[string, number | null]>;
     message: Array<[string, any | null]>;
+    shouldBeGrouped?: Array<[string, string | null]>;
   };
 };
 
 type ResponseProcessors = {
   merged: (item: ApiFingerprint[]) => Fingerprint[];
   similar: (data: [Group, ScoreMap]) => {
-    aggregate: Record<string, number>;
+    aggregate: Record<string, number | string>;
     isBelowThreshold: boolean;
     issue: Group;
     score: ScoreMap;
@@ -328,8 +330,15 @@ const storeConfig: GroupingStoreDefinition = {
         return newItems;
       },
       similar: ([issue, scoreMap]) => {
+        // Check which similarity endpoint is being used
+        const hasSimilarityEmbeddingsFeature = requests[0]?.endpoint.includes(
+          'similar-issues-embeddings'
+        );
+
         // Hide items with a low scores
-        const isBelowThreshold = checkBelowThreshold(scoreMap);
+        const isBelowThreshold = hasSimilarityEmbeddingsFeature
+          ? false
+          : checkBelowThreshold(scoreMap);
 
         // List of scores indexed by interface (i.e., exception and message)
         // Note: for v2, the interface is always "similarity". When v2 is
@@ -357,8 +366,7 @@ const storeConfig: GroupingStoreDefinition = {
             const scores = allScores.filter(([, score]) => score !== null);
 
             const avg = scores.reduce((sum, [, score]) => sum + score, 0) / scores.length;
-
-            acc[interfaceName] = avg;
+            acc[interfaceName] = hasSimilarityEmbeddingsFeature ? scores[0][1] : avg;
             return acc;
           }, {});
 
diff --git a/static/app/utils/analytics/issueAnalyticsEvents.tsx b/static/app/utils/analytics/issueAnalyticsEvents.tsx
index 568183526199c6..6a53c5af16f7db 100644
--- a/static/app/utils/analytics/issueAnalyticsEvents.tsx
+++ b/static/app/utils/analytics/issueAnalyticsEvents.tsx
@@ -83,6 +83,17 @@ export type IssueEventParameters = {
   'issue_details.issue_status_docs_clicked': {};
   'issue_details.performance.autogrouped_siblings_toggle': {};
   'issue_details.performance.hidden_spans_expanded': {};
+  'issue_details.similar_issues.diff_clicked': {
+    error_message: string;
+    group_id: string;
+    parent_error_message: string;
+    parent_group_id: string;
+    parent_stacktrace: string;
+    parent_transaction: string;
+    project_id: string;
+    stacktrace: string;
+    transaction: string;
+  };
   'issue_details.sourcemap_wizard_copy': SourceMapWizardParam;
   'issue_details.sourcemap_wizard_dismiss': SourceMapWizardParam;
   'issue_details.sourcemap_wizard_learn_more': SourceMapWizardParam;
@@ -320,6 +331,8 @@ export const issueEventMap: Record<IssueEventKey, string | null> = {
   'device.classification.unclassified.ios.device':
     'Event from iOS device missing device.class',
   'device.classification.high.end.android.device': 'Event from high end Android device',
+  'issue_details.similar_issues.diff_clicked':
+    'Issue Details: Similar Issues: Diff Clicked',
   'issue_details.sourcemap_wizard_dismiss': 'Issue Details: Sourcemap Wizard Dismiss',
   'issue_details.sourcemap_wizard_copy': 'Issue Details: Sourcemap Wizard Copy',
   'issue_details.sourcemap_wizard_learn_more':
diff --git a/static/app/views/issueDetails/groupSimilarIssues/index.spec.tsx b/static/app/views/issueDetails/groupSimilarIssues/index.spec.tsx
index a7f3d152b98727..b26c3ebf49a0d9 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/index.spec.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/index.spec.tsx
@@ -1,5 +1,4 @@
 import {GroupsFixture} from 'sentry-fixture/groups';
-import {OrganizationFixture} from 'sentry-fixture/organization';
 import {ProjectFixture} from 'sentry-fixture/project';
 import {RouterContextFixture} from 'sentry-fixture/routerContextFixture';
 import {RouterFixture} from 'sentry-fixture/routerFixture';
@@ -147,16 +146,70 @@ describe('Issues Similar View', function () {
     );
     renderGlobalModal();
 
-    await userEvent.click(await screen.findByTestId('similar-item-row'));
+    await selectNthSimilarItem(0);
     expect(screen.getByText('Merge (1)')).toBeInTheDocument();
 
     // Correctly show "Merge (0)" when the item is un-clicked
-    await userEvent.click(await screen.findByTestId('similar-item-row'));
+    await selectNthSimilarItem(0);
     expect(screen.getByText('Merge (0)')).toBeInTheDocument();
   });
+});
+
+describe('Issues Similar Embeddings View', function () {
+  let mock;
+
+  const project = ProjectFixture({
+    features: ['similarity-view', 'similarity-embeddings'],
+  });
+
+  const routerContext = RouterContextFixture([
+    {
+      router: {
+        ...RouterFixture(),
+        params: {orgId: 'org-slug', projectId: 'project-slug', groupId: 'group-id'},
+      },
+    },
+  ]);
+
+  const similarEmbeddingsScores = [
+    {exception: 0.9987, message: 0.3748, shouldBeGrouped: 'Yes'},
+    {exception: 0.9985, message: 0.3738, shouldBeGrouped: 'Yes'},
+    {exception: 0.7384, message: 0.3743, shouldBeGrouped: 'No'},
+    {exception: 0.3849, message: 0.4738, shouldBeGrouped: 'No'},
+  ];
+
+  const mockData = {
+    simlarEmbeddings: GroupsFixture().map((issue, i) => [
+      issue,
+      similarEmbeddingsScores[i],
+    ]),
+  };
+
+  const router = RouterFixture();
 
-  it('renders all filtered issues with issues-similarity-embeddings flag', async function () {
-    const features = ['issues-similarity-embeddings'];
+  beforeEach(function () {
+    mock = MockApiClient.addMockResponse({
+      url: '/organizations/org-slug/issues/group-id/similar-issues-embeddings/?k=5&threshold=0.99',
+      body: mockData.simlarEmbeddings,
+    });
+  });
+
+  afterEach(() => {
+    MockApiClient.clearMockResponses();
+    jest.clearAllMocks();
+  });
+
+  const selectNthSimilarItem = async (index: number) => {
+    const items = await screen.findAllByTestId('similar-item-row');
+
+    const item = items.at(index);
+
+    expect(item).toBeDefined();
+
+    await userEvent.click(item!);
+  };
+
+  it('renders with mocked data', async function () {
     render(
       <GroupSimilarIssues
         project={project}
@@ -167,7 +220,7 @@ describe('Issues Similar View', function () {
         routes={router.routes}
         route={{}}
       />,
-      {context: routerContext, organization: OrganizationFixture({features})}
+      {context: routerContext}
     );
 
     expect(screen.getByTestId('loading-indicator')).toBeInTheDocument();
@@ -175,5 +228,70 @@ describe('Issues Similar View', function () {
     await waitFor(() => expect(mock).toHaveBeenCalled());
 
     expect(screen.queryByText('Show 3 issues below threshold')).not.toBeInTheDocument();
+    expect(screen.queryByText('Would Group')).toBeInTheDocument();
+  });
+
+  it('can merge and redirect to new parent', async function () {
+    const merge = MockApiClient.addMockResponse({
+      method: 'PUT',
+      url: '/projects/org-slug/project-slug/issues/',
+      body: {
+        merge: {children: ['123'], parent: '321'},
+      },
+    });
+
+    render(
+      <GroupSimilarIssues
+        project={project}
+        params={{orgId: 'org-slug', groupId: 'group-id'}}
+        location={router.location}
+        router={router}
+        routeParams={router.params}
+        routes={router.routes}
+        route={{}}
+      />,
+      {context: routerContext}
+    );
+    renderGlobalModal();
+
+    await selectNthSimilarItem(0);
+    await userEvent.click(await screen.findByRole('button', {name: 'Merge (1)'}));
+    await userEvent.click(screen.getByRole('button', {name: 'Confirm'}));
+
+    await waitFor(() => {
+      expect(merge).toHaveBeenCalledWith(
+        '/projects/org-slug/project-slug/issues/',
+        expect.objectContaining({
+          data: {merge: 1},
+        })
+      );
+    });
+
+    expect(MockNavigate).toHaveBeenCalledWith(
+      '/organizations/org-slug/issues/321/similar/'
+    );
+  });
+
+  it('correctly shows merge count', async function () {
+    render(
+      <GroupSimilarIssues
+        project={project}
+        params={{orgId: 'org-slug', groupId: 'group-id'}}
+        location={router.location}
+        router={router}
+        routeParams={router.params}
+        routes={router.routes}
+        route={{}}
+      />,
+      {context: routerContext}
+    );
+    renderGlobalModal();
+
+    await selectNthSimilarItem(0);
+    expect(screen.getByText('Merge (1)')).toBeInTheDocument();
+
+    // Correctly show "Merge (0)" when the item is un-clicked
+    await selectNthSimilarItem(0);
+    expect(screen.getByText('Merge (0)')).toBeInTheDocument();
   });
 });
diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx
index d29c56bae99ad0..7e608623dcb593 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/index.tsx
@@ -15,7 +15,6 @@ import GroupingStore from 'sentry/stores/groupingStore';
 import {space} from 'sentry/styles/space';
 import type {Project} from 'sentry/types';
 import {useNavigate} from 'sentry/utils/useNavigate';
-import useOrganization from 'sentry/utils/useOrganization';
 import usePrevious from 'sentry/utils/usePrevious';
 
 import List from './list';
@@ -49,9 +48,8 @@ function SimilarStackTrace({params, location, project}: Props) {
   const navigate = useNavigate();
   const prevLocationSearch = usePrevious(location.search);
   const hasSimilarityFeature = project.features.includes('similarity-view');
-  const organization = useOrganization();
-  const hasSimilarityEmbeddingsFeature = organization?.features?.includes(
-    'issues-similarity-embeddings'
+  const hasSimilarityEmbeddingsFeature = project.features.includes(
+    'similarity-embeddings'
   );
 
   const fetchData = useCallback(() => {
@@ -59,7 +57,17 @@ function SimilarStackTrace({params, location, project}: Props) {
 
     const reqs: Parameters<typeof GroupingStore.onFetch>[0] = [];
 
-    if (hasSimilarityFeature) {
+    if (hasSimilarityEmbeddingsFeature) {
+      reqs.push({
+        endpoint: `/organizations/${orgId}/issues/${groupId}/similar-issues-embeddings/?${qs.stringify(
+          {
+            k: 5,
+            threshold: 0.99,
+          }
+        )}`,
+        dataKey: 'similar',
+      });
+    } else if (hasSimilarityFeature) {
       reqs.push({
         endpoint: `/organizations/${orgId}/issues/${groupId}/similar/?${qs.stringify({
           ...location.query,
@@ -70,7 +78,13 @@ function SimilarStackTrace({params, location, project}: Props) {
     }
 
     GroupingStore.onFetch(reqs);
-  }, [location.query, groupId, orgId, hasSimilarityFeature]);
+  }, [
+    location.query,
+    groupId,
+    orgId,
+    hasSimilarityFeature,
+    hasSimilarityEmbeddingsFeature,
+  ]);
 
   const onGroupingChange = useCallback(
     ({
@@ -137,7 +151,8 @@ function SimilarStackTrace({params, location, project}: Props) {
   }, [params, location.query, items]);
 
   const hasSimilarItems =
-    hasSimilarityFeature && (items.similar.length > 0 || items.filtered.length > 0);
+    (hasSimilarityFeature || hasSimilarityEmbeddingsFeature) &&
+    (items.similar.length > 0 || items.filtered.length > 0);
 
   return (
     <Layout.Body>
@@ -171,7 +186,6 @@ function SimilarStackTrace({params, location, project}: Props) {
             onMerge={handleMerge}
             orgId={orgId}
             project={project}
-            organization={organization}
             groupId={groupId}
             pageLinks={items.pageLinks}
           />
@@ -183,7 +197,6 @@ function SimilarStackTrace({params, location, project}: Props) {
             onMerge={handleMerge}
             orgId={orgId}
             project={project}
-            organization={organization}
             groupId={groupId}
             pageLinks={items.pageLinks}
           />
diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/item.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/item.tsx
index fd8d02c2212dc2..373503a903c0fe 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/item.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/item.tsx
@@ -22,11 +22,11 @@ type Props = {
   groupId: Group['id'];
   issue: Group;
   orgId: Organization['id'];
-  organization: Organization;
   project: Project;
   aggregate?: {
     exception: number;
     message: number;
+    shouldBeGrouped?: string;
   };
   score?: Record<string, any>;
   scoresByInterface?: {
@@ -95,12 +95,14 @@ class Item extends Component<Props, State> {
   };
 
   render() {
-    const {aggregate, scoresByInterface, issue, organization} = this.props;
+    const {aggregate, scoresByInterface, issue, project} = this.props;
     const {visible, busy} = this.state;
-    const similarInterfaces = ['exception', 'message'];
-    const hasSimilarityEmbeddingsFeature = organization?.features?.includes(
-      'issues-similarity-embeddings'
+    const hasSimilarityEmbeddingsFeature = project.features.includes(
+      'similarity-embeddings'
     );
+    const similarInterfaces = hasSimilarityEmbeddingsFeature
+      ? ['exception', 'message', 'shouldBeGrouped']
+      : ['exception', 'message'];
 
     if (!visible) {
       return null;
@@ -141,10 +143,17 @@ class Item extends Component<Props, State> {
           {similarInterfaces.map(interfaceName => {
             const avgScore = aggregate?.[interfaceName];
             const scoreList = scoresByInterface?.[interfaceName] || [];
-            // Check for valid number (and not NaN)
-            const scoreValue =
-              typeof avgScore === 'number' && !Number.isNaN(avgScore) ? avgScore : 0;
 
+            // If hasSimilarityEmbeddingsFeature is on, avgScore can be a string
+            let scoreValue = avgScore;
+            if (
+              (typeof avgScore !== 'string' && hasSimilarityEmbeddingsFeature) ||
+              !hasSimilarityEmbeddingsFeature
+            ) {
+              // Check for valid number (and not NaN)
+              scoreValue =
+                typeof avgScore === 'number' && !Number.isNaN(avgScore) ? avgScore : 0;
+            }
             return (
               <Column key={interfaceName}>
                 {!hasSimilarityEmbeddingsFeature && (
@@ -154,7 +163,11 @@ class Item extends Component<Props, State> {
                     <ScoreBar vertical score={Math.round(scoreValue * 5)} />
                   </Hovercard>
                 )}
-                {hasSimilarityEmbeddingsFeature && <div>{scoreValue.toFixed(4)}</div>}
+                {hasSimilarityEmbeddingsFeature && (
+                  <div>
+                    {typeof scoreValue === 'number' ? scoreValue.toFixed(4) : scoreValue}
+                  </div>
+                )}
               </Column>
             );
           })}
@@ -181,8 +194,8 @@ const Columns = styled('div')`
   display: flex;
   align-items: center;
   flex-shrink: 0;
-  min-width: 300px;
-  width: 300px;
+  min-width: 350px;
+  width: 350px;
 `;
 
 const columnStyle = css`
diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx
index 6a548ec25851d1..015cd8053dc871 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/list.tsx
@@ -24,7 +24,6 @@ type Props = {
   items: Array<SimilarItem>;
   onMerge: () => void;
   orgId: Organization['id'];
-  organization: Organization;
   pageLinks: string | null;
   project: Project;
 } & DefaultProps;
@@ -45,7 +44,6 @@ function List({
   orgId,
   groupId,
   project,
-  organization,
   items,
   filteredItems = [],
   pageLinks,
@@ -56,8 +54,8 @@ function List({
   const hasHiddenItems = !!filteredItems.length;
   const hasResults = items.length > 0 || hasHiddenItems;
   const itemsWithFiltered = items.concat(showAllItems ? filteredItems : []);
-  const hasSimilarityEmbeddingsFeature = organization?.features?.includes(
-    'issues-similarity-embeddings'
+  const hasSimilarityEmbeddingsFeature = project.features.includes(
+    'similarity-embeddings'
   );
 
   if (!hasResults) {
@@ -72,7 +70,7 @@ function List({
         </Header>
       )}
       <Panel>
-        <Toolbar onMerge={onMerge} />
+        <Toolbar onMerge={onMerge} project={project} />
 
         <PanelBody>
           {itemsWithFiltered.map(item => (
@@ -81,7 +79,6 @@ function List({
               orgId={orgId}
               groupId={groupId}
               project={project}
-              organization={organization}
               {...item}
             />
           ))}
diff --git a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/toolbar.tsx b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/toolbar.tsx
index 3330a82ceda32d..6968b65d12a95b 100644
--- a/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/toolbar.tsx
+++ b/static/app/views/issueDetails/groupSimilarIssues/similarStackTrace/toolbar.tsx
@@ -8,9 +8,11 @@ import ToolbarHeader from 'sentry/components/toolbarHeader';
 import {t} from 'sentry/locale';
 import GroupingStore from 'sentry/stores/groupingStore';
 import {space} from 'sentry/styles/space';
+import type {Project} from 'sentry/types';
 
 type Props = {
   onMerge: () => void;
+  project?: Project;
 };
 
 const initialState = {
@@ -40,8 +42,11 @@ class SimilarToolbar extends Component<Props, State> {
   listener = GroupingStore.listen(this.onGroupChange, undefined);
 
   render() {
-    const {onMerge} = this.props;
+    const {onMerge, project} = this.props;
     const {mergeCount} = this.state;
+    const hasSimilarityEmbeddingsFeature = project?.features.includes(
+      'similarity-embeddings'
+    );
 
     return (
       <PanelHeader hasButtons>
@@ -59,6 +64,9 @@ class SimilarToolbar extends Component<Props, State> {
           <StyledToolbarHeader>{t('Events')}</StyledToolbarHeader>
           <StyledToolbarHeader>{t('Exception')}</StyledToolbarHeader>
           <StyledToolbarHeader>{t('Message')}</StyledToolbarHeader>
+          {hasSimilarityEmbeddingsFeature && (
+            <StyledToolbarHeader>{t('Would Group')}</StyledToolbarHeader>
+          )}
         </Columns>
       </PanelHeader>
     );
@@ -70,8 +78,8 @@ const Columns = styled('div')`
   display: flex;
   align-items: center;
   flex-shrink: 0;
-  min-width: 300px;
-  width: 300px;
+  min-width: 350px;
+  width: 350px;
 `;
 
 const StyledToolbarHeader = styled(ToolbarHeader)`


Code review comments:
diff_hunk: @@ -89,7 +132,16 @@ class IssueDiff extends Component<Props, State> {
     const event = await api.requestPromise(
       `/projects/${orgId}/${project.slug}/events/${paramEventId}/`
     );
-    return getStacktraceBody(event);
+    return event;
+  };
+
+  getTransaction = (tags: any[]) => {
+    for (let i = 0; i < tags.length; i++) {
+      if (tags[i].key === 'transaction') {
+        return tags[i].value;
+      }
+    }
+    return undefined;
path: static/app/components/issueDiff/index.tsx
text: @user1:
probably prefer a find here
```suggestion
    return tags.find(tag => tag.key === 'transaction')?.value;
```

diff_hunk: @@ -55,28 +57,69 @@ class IssueDiff extends Component<Props, State> {
   }
 
   fetchData() {
-    const {baseIssueId, targetIssueId, baseEventId, targetEventId} = this.props;
+    const {
+      baseIssueId,
+      targetIssueId,
+      baseEventId,
+      targetEventId,
+      organization,
+      project,
+    } = this.props;
+    const hasSimilarityEmbeddingsFeature = project.features.includes(
+      'similarity-embeddings'
+    );
 
     // Fetch component and event data
-    Promise.all([
-      import('../splitDiff'),
-      this.fetchEventData(baseIssueId, baseEventId ?? 'latest'),
-      this.fetchEventData(targetIssueId, targetEventId ?? 'latest'),
-    ])
-      .then(([{default: SplitDiffAsync}, baseEvent, targetEvent]) => {
+    const asyncFetch = async () => {
+      try {
+        const [baseEventData, targetEventData] = await Promise.all([
+          this.fetchEvent(baseIssueId, baseEventId ?? 'latest'),
+          this.fetchEvent(targetIssueId, targetEventId ?? 'latest'),
+        ]);
+
+        const [{default: SplitDiffAsync}, baseEvent, targetEvent] = await Promise.all([
+          import('../splitDiff'),
path: static/app/components/issueDiff/index.tsx
text: @user1:
lets declare the promise for `import('../splitDiff')` higher in the function which will start the request.
```ts
const splitdiffPromise = import('../splitDiff');
```

and then you can just await it on its own line after (it should already be fufilled), its kinda weird to mix regular requests and a component in a promise.all
```ts
const `{default: SplitDiffAsync} = await splitdiffPromise;
```