Repository: getsentry/sentry
Pull request: feat(devtool): Refresh issue and feedback panels on page nav
Refresh the issue and feedback panels on page navigation using `startNavigationSpan`. This also gives the transaction name in the callback so we don't need to get the name from the scope. 
**Caveat** is that this only works if perf is enabled and using browser tracing instrumentation

https://github.com/user-attachments/assets/f14b04d0-be75-40a4-a351-d44f7e652d8b

closes https://github.com/getsentry/sentry-toolbar/issues/77

Problem statement:
[DevToolbar] Open Issue and Feedback panels should respond and update themselves when the current transaction name changes
In the toolbar PoC on sentry.io once a panel is open the data inside it won't really change... Instead the panel should always show data relevant to the current url in the browser. So as the url changes, or as the transaction name changes from `/issues` to `/issues/:issueId` we should be re-rendering the open panel to match.

It might be better to respond to `transactionName` changes instead of looking at `window.location` because window.location can trigger updates whenever query params change, which is too frequent! However I don't have more specific implementation ideas right now.

Hints from issue discussion:

Commit message: try using startNavigationSpan

Patch to review:
diff --git a/static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx b/static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx
index 4033ace888136b..80c05b1d03b01a 100644
--- a/static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx
+++ b/static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx
@@ -1,8 +1,9 @@
-import {useMemo} from 'react';
+import {useEffect, useMemo, useState} from 'react';
 import {css} from '@emotion/react';
 
 import ActorAvatar from 'sentry/components/avatar/actorAvatar';
 import AnalyticsProvider from 'sentry/components/devtoolbar/components/analyticsProvider';
+import {useScopeAndClient} from 'sentry/components/devtoolbar/hooks/useSentryClientAndScope';
 import ProjectBadge from 'sentry/components/idBadge/projectBadge';
 import Placeholder from 'sentry/components/placeholder';
 import TextOverflow from 'sentry/components/textOverflow';
@@ -11,7 +12,7 @@ import {IconChat, IconFatal, IconImage, IconMegaphone, IconPlay} from 'sentry/ic
 import useReplayCount from 'sentry/utils/replayCount/useReplayCount';
 
 import useConfiguration from '../../hooks/useConfiguration';
-import useCurrentTransactionName from '../../hooks/useCurrentTransactionName';
+import toSearchTerm from '../../hooks/useCurrentTransactionName';
 import {useSDKFeedbackButton} from '../../hooks/useSDKFeedbackButton';
 import useVisibility from '../../hooks/useVisibility';
 import {
@@ -49,7 +50,20 @@ export default function FeedbackPanel() {
       [setVisible]
     )
   );
-  const transactionName = useCurrentTransactionName();
+
+  const {scope, client} = useScopeAndClient();
+  const [transactionName, setTransactionName] = useState(
+    scope?.getScopeData().transactionName
+  );
+
+  useEffect(() => {
+    if (client) {
+      client.on('startNavigationSpan', options => {
+        setTransactionName(toSearchTerm(options.name));
+      });
+    }
+  }, [client]);
+
   const queryResult = useInfiniteFeedbackList({
     query: `url:*${transactionName}`,
   });
diff --git a/static/app/components/devtoolbar/components/issues/issuesPanel.tsx b/static/app/components/devtoolbar/components/issues/issuesPanel.tsx
index 6a89ccf042b959..290b8e22f908c1 100644
--- a/static/app/components/devtoolbar/components/issues/issuesPanel.tsx
+++ b/static/app/components/devtoolbar/components/issues/issuesPanel.tsx
@@ -1,7 +1,10 @@
+import {useEffect, useState} from 'react';
+
 import IssueListItem from 'sentry/components/devtoolbar/components/issueListItem';
+import {useScopeAndClient} from 'sentry/components/devtoolbar/hooks/useSentryClientAndScope';
 import Placeholder from 'sentry/components/placeholder';
 
-import useCurrentTransactionName from '../../hooks/useCurrentTransactionName';
+import toSearchTerm from '../../hooks/useCurrentTransactionName';
 import {listItemPlaceholderWrapperCss} from '../../styles/listItem';
 import {panelDescCss, panelInsetContentCss, panelSectionCss} from '../../styles/panel';
 import {resetFlexColumnCss} from '../../styles/reset';
@@ -13,7 +16,20 @@ import PanelLayout from '../panelLayout';
 import useInfiniteIssuesList from './useInfiniteIssuesList';
 
 export default function IssuesPanel() {
-  const transactionName = useCurrentTransactionName();
+  const {scope, client} = useScopeAndClient();
+  const [transactionName, setTransactionName] = useState(
+    scope?.getScopeData().transactionName
+  );
+
+  useEffect(() => {
+    if (client) {
+      client.on('startNavigationSpan', options => {
+        setTransactionName(toSearchTerm(options.name));
+      });
+    }
+  }, [client]);
+
+  // Fetch issues based on the updated transaction name
   const queryResult = useInfiniteIssuesList({
     query: `url:*${transactionName}`,
   });
diff --git a/static/app/components/devtoolbar/hooks/useCurrentTransactionName.test.tsx b/static/app/components/devtoolbar/hooks/useCurrentTransactionName.test.tsx
index c5fd2a04dea85f..d725e8b27fab8e 100644
--- a/static/app/components/devtoolbar/hooks/useCurrentTransactionName.test.tsx
+++ b/static/app/components/devtoolbar/hooks/useCurrentTransactionName.test.tsx
@@ -1,4 +1,4 @@
-import {toSearchTerm} from 'sentry/components/devtoolbar/hooks/useCurrentTransactionName';
+import toSearchTerm from 'sentry/components/devtoolbar/hooks/useCurrentTransactionName';
 
 describe('getSearchTerm', () => {
   it.each([
diff --git a/static/app/components/devtoolbar/hooks/useCurrentTransactionName.tsx b/static/app/components/devtoolbar/hooks/useCurrentTransactionName.tsx
index 4e0cae30767bf8..fe479c6eb30975 100644
--- a/static/app/components/devtoolbar/hooks/useCurrentTransactionName.tsx
+++ b/static/app/components/devtoolbar/hooks/useCurrentTransactionName.tsx
@@ -1,78 +1,4 @@
-import type {Scope} from '@sentry/types';
-
-type V8Carrier = {
-  stack: {
-    getScope?: () => Scope;
-  };
-};
-
-type LegacyCarrier = {
-  /** pre-v8 way of accessing scope (v7 and earlier) */
-  hub?: {
-    getScope?: () => Scope;
-  };
-};
-
-type VersionedCarrier = {version: string} & Record<Exclude<string, 'version'>, V8Carrier>;
-
-type WindowWithSentry = Window & {
-  __SENTRY__?: LegacyCarrier & VersionedCarrier;
-};
-
-export default function useCurrentTransactionName() {
-  const scope = getScope();
-  const scopeData = scope?.getScopeData();
-
-  const search = toSearchTerm(scopeData?.transactionName);
-
-  return search;
-}
-
-function getScope() {
-  const sentryCarrier = (window as WindowWithSentry).__SENTRY__;
-  const sentryScope = sentryCarrier && getSentryScope(sentryCarrier);
-
-  if (!sentryScope) {
-    // using console log for now, will change this when moving to dev tool bar repo
-    // eslint-disable-next-line no-console
-    console.log(
-      "Couldn't find a Sentry SDK scope. Make sure you're using a Sentry SDK with version 7.x or 8.x"
-    );
-  }
-
-  return sentryScope;
-}
-
-/**
- * Accesses the `window.__SENTRY__` carrier object and tries to get the Sentry scope
- * from it. This function supports all carrier object structures from v7 to all versions
- * of v8.
- */
-function getSentryScope(
-  sentryCarrier: LegacyCarrier & VersionedCarrier
-): Scope | undefined {
-  // 8.6.0+ way to get the scope
-  if (sentryCarrier.version) {
-    const versionedCarrier = sentryCarrier[sentryCarrier.version];
-    const scope =
-      typeof versionedCarrier?.stack?.getScope === 'function'
-        ? versionedCarrier?.stack?.getScope?.()
-        : undefined;
-    return scope;
-  }
-
-  // pre-8.6.0 (+v7) way to get the scope
-  if (sentryCarrier.hub) {
-    const hub = sentryCarrier.hub;
-    if (typeof hub.getScope === 'function') {
-      return hub.getScope();
-    }
-  }
-
-  return undefined;
-}
-
-export function toSearchTerm(transaction) {
+export default function toSearchTerm(transaction) {
   // finds dynamic parts of transaction name to change into search term
   let modifiedTransaction = transaction;
 
diff --git a/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx b/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx
new file mode 100644
index 00000000000000..b95f283e1c4dd7
--- /dev/null
+++ b/static/app/components/devtoolbar/hooks/useSentryClientAndScope.tsx
@@ -0,0 +1,97 @@
+import type {Client, Scope} from '@sentry/types';
+
+type V8Carrier = {
+  stack: {
+    getScope?: () => Scope;
+  };
+};
+
+type LegacyCarrier = {
+  /** pre-v8 way of accessing scope (v7 and earlier) */
+  hub?: {
+    getClient?: () => Client;
+    getScope?: () => Scope;
+  };
+};
+
+type VersionedCarrier = {version: string} & Record<Exclude<string, 'version'>, V8Carrier>;
+
+type WindowWithSentry = Window & {
+  __SENTRY__?: LegacyCarrier & VersionedCarrier;
+};
+
+export function useScopeAndClient() {
+  const sentryCarrier = (window as WindowWithSentry).__SENTRY__;
+  const sentryScope = sentryCarrier && getSentryScope(sentryCarrier);
+  const sentryClient = sentryCarrier && getSentryClient(sentryCarrier);
+
+  if (!sentryScope || !sentryClient) {
+    // using console log for now, will change this when moving to dev tool bar repo
+    // eslint-disable-next-line no-console
+    console.log(
+      "Couldn't find a Sentry SDK scope or client. Make sure you're using a Sentry SDK with version 7.x or 8.x"
+    );
+  }
+
+  return {scope: sentryScope, client: sentryClient};
+}
+
+/**
+ * Accesses the `window.__SENTRY__` carrier object and tries to get the Sentry scope
+ * from it. This function supports all carrier object structures from v7 to all versions
+ * of v8.
+ */
+function getSentryScope(
+  sentryCarrier: LegacyCarrier & VersionedCarrier
+): Scope | undefined {
+  // 8.6.0+ way to get the scope
+  if (sentryCarrier.version) {
+    const versionedCarrier = sentryCarrier[sentryCarrier.version];
+    const scope =
+      typeof versionedCarrier?.stack?.getScope === 'function'
+        ? versionedCarrier?.stack?.getScope?.()
+        : undefined;
+    return scope;
+  }
+
+  // pre-8.6.0 (+v7) way to get the scope
+  if (sentryCarrier.hub) {
+    const hub = sentryCarrier.hub;
+    if (typeof hub.getScope === 'function') {
+      return hub.getScope();
+    }
+  }
+
+  return undefined;
+}
+
+/**
+ * Accesses the `window.__SENTRY__` carrier object and tries to get the Sentry client
+ * from it. This function supports all carrier object structures from v7 to all versions
+ * of v8.
+ */
+function getSentryClient(
+  sentryCarrier: LegacyCarrier & VersionedCarrier
+): Client | undefined {
+  // 8.6.0+ way to get the client
+  if (sentryCarrier.version) {
+    const versionedCarrier = sentryCarrier[sentryCarrier.version];
+    const scope =
+      typeof versionedCarrier?.stack?.getScope === 'function'
+        ? versionedCarrier?.stack?.getScope?.()
+        : undefined;
+    if (typeof scope?.getClient === 'function') {
+      return scope.getClient();
+    }
+  }
+
+  // pre-8.6.0 (+v7) way to get the client
+  if (sentryCarrier.hub) {
+    const hub = sentryCarrier.hub;
+    if (typeof hub.getClient === 'function') {
+      return hub.getClient();
+    }
+  }
+
+  return undefined;
+}


Code review comments:
diff_hunk: @@ -13,7 +16,20 @@ import PanelLayout from '../panelLayout';
 import useInfiniteIssuesList from './useInfiniteIssuesList';
 
 export default function IssuesPanel() {
-  const transactionName = useCurrentTransactionName();
+  const {scope, client} = useScopeAndClient();
+  const [transactionName, setTransactionName] = useState(
+    scope?.getScopeData().transactionName
path: static/app/components/devtoolbar/components/issues/issuesPanel.tsx
text: @user1:
need to call `toSearchTerm()` in all the places... probably better to have a `const transactionName = scope?.getScopeData().transactionName` separate from a `const transactionSearchTerm = toSearchTerm(transactionName)` local variable
.

diff_hunk: @@ -1,78 +1,4 @@
-import type {Scope} from '@sentry/types';
-
-type V8Carrier = {
-  stack: {
-    getScope?: () => Scope;
-  };
-};
-
-type LegacyCarrier = {
-  /** pre-v8 way of accessing scope (v7 and earlier) */
-  hub?: {
-    getScope?: () => Scope;
-  };
-};
-
-type VersionedCarrier = {version: string} & Record<Exclude<string, 'version'>, V8Carrier>;
-
-type WindowWithSentry = Window & {
-  __SENTRY__?: LegacyCarrier & VersionedCarrier;
-};
-
-export default function useCurrentTransactionName() {
-  const scope = getScope();
-  const scopeData = scope?.getScopeData();
-
-  const search = toSearchTerm(scopeData?.transactionName);
-
-  return search;
-}
-
-function getScope() {
-  const sentryCarrier = (window as WindowWithSentry).__SENTRY__;
-  const sentryScope = sentryCarrier && getSentryScope(sentryCarrier);
-
-  if (!sentryScope) {
-    // using console log for now, will change this when moving to dev tool bar repo
-    // eslint-disable-next-line no-console
-    console.log(
-      "Couldn't find a Sentry SDK scope. Make sure you're using a Sentry SDK with version 7.x or 8.x"
-    );
-  }
-
-  return sentryScope;
-}
-
-/**
- * Accesses the `window.__SENTRY__` carrier object and tries to get the Sentry scope
- * from it. This function supports all carrier object structures from v7 to all versions
- * of v8.
- */
-function getSentryScope(
-  sentryCarrier: LegacyCarrier & VersionedCarrier
-): Scope | undefined {
-  // 8.6.0+ way to get the scope
-  if (sentryCarrier.version) {
-    const versionedCarrier = sentryCarrier[sentryCarrier.version];
-    const scope =
-      typeof versionedCarrier?.stack?.getScope === 'function'
-        ? versionedCarrier?.stack?.getScope?.()
-        : undefined;
-    return scope;
-  }
-
-  // pre-8.6.0 (+v7) way to get the scope
-  if (sentryCarrier.hub) {
-    const hub = sentryCarrier.hub;
-    if (typeof hub.getScope === 'function') {
-      return hub.getScope();
-    }
-  }
-
-  return undefined;
-}
-
-export function toSearchTerm(transaction) {
+export default function toSearchTerm(transaction) {
path: static/app/components/devtoolbar/hooks/useCurrentTransactionName.tsx
text: @user1:
```suggestion
export default function transactionNameToSearchTerm(transaction) {
```
Feels a little wordy but maybe?

diff_hunk: @@ -49,7 +50,20 @@ export default function FeedbackPanel() {
       [setVisible]
     )
   );
-  const transactionName = useCurrentTransactionName();
+
+  const {scope, client} = useScopeAndClient();
+  const [transactionName, setTransactionName] = useState(
+    scope?.getScopeData().transactionName
+  );
+
+  useEffect(() => {
+    if (client) {
+      client.on('startNavigationSpan', options => {
+        setTransactionName(toSearchTerm(options.name));
+      });
+    }
+  }, [client]);
path: static/app/components/devtoolbar/components/feedback/feedbackPanel.tsx
text: @user1:
We should return a cleanup fn that cleans up the listener

@user2:
is the listener the return value of client.on?

@author:
how do you clean up the listener?

diff_hunk: @@ -1,78 +1,4 @@
-import type {Scope} from '@sentry/types';
-
-type V8Carrier = {
-  stack: {
-    getScope?: () => Scope;
-  };
-};
-
-type LegacyCarrier = {
-  /** pre-v8 way of accessing scope (v7 and earlier) */
-  hub?: {
-    getScope?: () => Scope;
-  };
-};
-
-type VersionedCarrier = {version: string} & Record<Exclude<string, 'version'>, V8Carrier>;
-
-type WindowWithSentry = Window & {
-  __SENTRY__?: LegacyCarrier & VersionedCarrier;
-};
-
-export default function useCurrentTransactionName() {
-  const scope = getScope();
-  const scopeData = scope?.getScopeData();
-
-  const search = toSearchTerm(scopeData?.transactionName);
-
-  return search;
-}
-
-function getScope() {
-  const sentryCarrier = (window as WindowWithSentry).__SENTRY__;
-  const sentryScope = sentryCarrier && getSentryScope(sentryCarrier);
-
-  if (!sentryScope) {
-    // using console log for now, will change this when moving to dev tool bar repo
-    // eslint-disable-next-line no-console
-    console.log(
-      "Couldn't find a Sentry SDK scope. Make sure you're using a Sentry SDK with version 7.x or 8.x"
-    );
-  }
-
-  return sentryScope;
-}
-
-/**
- * Accesses the `window.__SENTRY__` carrier object and tries to get the Sentry scope
- * from it. This function supports all carrier object structures from v7 to all versions
- * of v8.
- */
-function getSentryScope(
-  sentryCarrier: LegacyCarrier & VersionedCarrier
-): Scope | undefined {
-  // 8.6.0+ way to get the scope
-  if (sentryCarrier.version) {
-    const versionedCarrier = sentryCarrier[sentryCarrier.version];
-    const scope =
-      typeof versionedCarrier?.stack?.getScope === 'function'
-        ? versionedCarrier?.stack?.getScope?.()
-        : undefined;
-    return scope;
-  }
-
-  // pre-8.6.0 (+v7) way to get the scope
-  if (sentryCarrier.hub) {
-    const hub = sentryCarrier.hub;
-    if (typeof hub.getScope === 'function') {
-      return hub.getScope();
-    }
-  }
-
-  return undefined;
-}
-
-export function toSearchTerm(transaction) {
+export default function toSearchTerm(transaction) {
path: static/app/components/devtoolbar/hooks/useCurrentTransactionName.tsx
text: @user1:
This is just a util now not a hook, maybe we could rename + move the file?

diff_hunk: @@ -13,7 +16,20 @@ import PanelLayout from '../panelLayout';
 import useInfiniteIssuesList from './useInfiniteIssuesList';
 
 export default function IssuesPanel() {
-  const transactionName = useCurrentTransactionName();
+  const {scope, client} = useScopeAndClient();
+  const [transactionName, setTransactionName] = useState(
+    scope?.getScopeData().transactionName
path: static/app/components/devtoolbar/components/issues/issuesPanel.tsx
text: @user1:
Do we need to call `toSearchTerm` on this?

@author:
oh good catch, ty!