breadcrumbs.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450
  1. import type {Location} from 'history';
  2. import omit from 'lodash/omit';
  3. import type {Crumb} from 'sentry/components/breadcrumbs';
  4. import {t} from 'sentry/locale';
  5. import type {Organization} from 'sentry/types/organization';
  6. import normalizeUrl from 'sentry/utils/url/normalizeUrl';
  7. import type {
  8. RoutableModuleNames,
  9. URLBuilder,
  10. } from 'sentry/views/insights/common/utils/useModuleURL';
  11. import {
  12. DOMAIN_VIEW_BASE_TITLE,
  13. DOMAIN_VIEW_BASE_URL,
  14. } from 'sentry/views/insights/pages/settings';
  15. import {DOMAIN_VIEW_TITLES} from 'sentry/views/insights/pages/types';
  16. import type {DomainView} from 'sentry/views/insights/pages/useFilters';
  17. import {ModuleName} from 'sentry/views/insights/types';
  18. import {getTransactionSummaryBaseUrl} from 'sentry/views/performance/transactionSummary/utils';
  19. import {getPerformanceBaseUrl} from 'sentry/views/performance/utils';
  20. import Tab from '../../transactionSummary/tabs';
  21. export const enum TraceViewSources {
  22. TRACES = 'traces',
  23. METRICS = 'metrics',
  24. DISCOVER = 'discover',
  25. PROFILING_FLAMEGRAPH = 'profiling_flamegraph',
  26. REQUESTS_MODULE = 'requests_module',
  27. QUERIES_MODULE = 'queries_module',
  28. ASSETS_MODULE = 'assets_module',
  29. APP_STARTS_MODULE = 'app_starts_module',
  30. SCREEN_LOADS_MODULE = 'screen_loads_module',
  31. WEB_VITALS_MODULE = 'web_vitals_module',
  32. CACHES_MODULE = 'caches_module',
  33. QUEUES_MODULE = 'queues_module',
  34. LLM_MODULE = 'llm_module',
  35. SCREEN_LOAD_MODULE = 'screen_load_module',
  36. MOBILE_SCREENS_MODULE = 'mobile_screens_module',
  37. SCREEN_RENDERING_MODULE = 'screen_rendering_module',
  38. PERFORMANCE_TRANSACTION_SUMMARY = 'performance_transaction_summary',
  39. ISSUE_DETAILS = 'issue_details',
  40. DASHBOARDS = 'dashboards',
  41. FEEDBACK_DETAILS = 'feedback_details',
  42. }
  43. // Ideally every new entry to ModuleName, would require a new source to be added here so we don't miss any.
  44. const TRACE_SOURCE_TO_INSIGHTS_MODULE: Partial<Record<TraceViewSources, ModuleName>> = {
  45. app_starts_module: ModuleName.APP_START,
  46. assets_module: ModuleName.RESOURCE,
  47. caches_module: ModuleName.CACHE,
  48. llm_module: ModuleName.AI,
  49. queries_module: ModuleName.DB,
  50. requests_module: ModuleName.HTTP,
  51. screen_loads_module: ModuleName.SCREEN_LOAD,
  52. web_vitals_module: ModuleName.VITAL,
  53. queues_module: ModuleName.QUEUE,
  54. screen_load_module: ModuleName.SCREEN_LOAD,
  55. screen_rendering_module: ModuleName.SCREEN_RENDERING,
  56. mobile_screens_module: ModuleName.MOBILE_VITALS,
  57. };
  58. export const TRACE_SOURCE_TO_NON_INSIGHT_ROUTES: Partial<
  59. Record<TraceViewSources, string>
  60. > = {
  61. traces: 'traces',
  62. metrics: 'metrics',
  63. discover: 'discover',
  64. profiling_flamegraph: 'profiling',
  65. performance_transaction_summary: 'traces',
  66. issue_details: 'issues',
  67. feedback_details: 'feedback',
  68. dashboards: 'dashboards',
  69. };
  70. function getBreadCrumbTarget(
  71. path: string,
  72. query: Location['query'],
  73. organization: Organization
  74. ) {
  75. return {
  76. pathname: normalizeUrl(`/organizations/${organization.slug}/${path}`),
  77. // Remove traceView specific query parameters that are not needed when navigating back.
  78. query: {...omit(query, ['node', 'fov', 'timestamp', 'eventId'])},
  79. };
  80. }
  81. function getPerformanceBreadCrumbs(
  82. organization: Organization,
  83. location: Location,
  84. view?: DomainView
  85. ) {
  86. const crumbs: Crumb[] = [];
  87. const hasPerfLandingRemovalFlag = organization.features.includes(
  88. 'insights-performance-landing-removal'
  89. );
  90. const performanceUrl = getPerformanceBaseUrl(organization.slug, view, true);
  91. const transactionSummaryUrl = getTransactionSummaryBaseUrl(organization, view, true);
  92. if (!view && hasPerfLandingRemovalFlag) {
  93. crumbs.push({
  94. label: DOMAIN_VIEW_BASE_TITLE,
  95. to: undefined,
  96. });
  97. } else {
  98. crumbs.push({
  99. label: (view && DOMAIN_VIEW_TITLES[view]) || t('Performance'),
  100. to: getBreadCrumbTarget(performanceUrl, location.query, organization),
  101. });
  102. }
  103. switch (location.query.tab) {
  104. case Tab.EVENTS:
  105. crumbs.push({
  106. label: t('Transaction Summary'),
  107. to: getBreadCrumbTarget(`${transactionSummaryUrl}`, location.query, organization),
  108. });
  109. break;
  110. case Tab.TAGS:
  111. crumbs.push({
  112. label: t('Tags'),
  113. to: getBreadCrumbTarget(
  114. `${transactionSummaryUrl}/tags`,
  115. location.query,
  116. organization
  117. ),
  118. });
  119. break;
  120. case Tab.SPANS: {
  121. crumbs.push({
  122. label: t('Spans'),
  123. to: getBreadCrumbTarget(
  124. `${transactionSummaryUrl}/spans`,
  125. location.query,
  126. organization
  127. ),
  128. });
  129. const {spanSlug} = location.query;
  130. if (spanSlug) {
  131. crumbs.push({
  132. label: t('Span Summary'),
  133. to: getBreadCrumbTarget(
  134. `${transactionSummaryUrl}/spans/${spanSlug}`,
  135. location.query,
  136. organization
  137. ),
  138. });
  139. }
  140. break;
  141. }
  142. case Tab.AGGREGATE_WATERFALL:
  143. crumbs.push({
  144. label: t('Transaction Summary'),
  145. to: getBreadCrumbTarget(
  146. `${transactionSummaryUrl}/aggregateWaterfall`,
  147. location.query,
  148. organization
  149. ),
  150. });
  151. break;
  152. default:
  153. crumbs.push({
  154. label: t('Transaction Summary'),
  155. to: getBreadCrumbTarget(`${transactionSummaryUrl}`, location.query, organization),
  156. });
  157. break;
  158. }
  159. crumbs.push({
  160. label: t('Trace View'),
  161. });
  162. return crumbs;
  163. }
  164. function getIssuesBreadCrumbs(organization: Organization, location: Location) {
  165. const crumbs: Crumb[] = [];
  166. crumbs.push({
  167. label: t('Issues'),
  168. to: getBreadCrumbTarget(`issues`, location.query, organization),
  169. });
  170. if (location.query.groupId) {
  171. crumbs.push({
  172. label: t('Issue Details'),
  173. to: getBreadCrumbTarget(
  174. `issues/${location.query.groupId}`,
  175. location.query,
  176. organization
  177. ),
  178. });
  179. }
  180. crumbs.push({
  181. label: t('Trace View'),
  182. });
  183. return crumbs;
  184. }
  185. function getDashboardsBreadCrumbs(organization: Organization, location: Location) {
  186. const crumbs: Crumb[] = [];
  187. crumbs.push({
  188. label: t('Dashboards'),
  189. to: getBreadCrumbTarget('dashboards', location.query, organization),
  190. });
  191. if (location.query.dashboardId) {
  192. crumbs.push({
  193. label: t('Widgets Legend'),
  194. to: getBreadCrumbTarget(
  195. `dashboard/${location.query.dashboardId}`,
  196. location.query,
  197. organization
  198. ),
  199. });
  200. if (location.query.widgetId) {
  201. crumbs.push({
  202. label: t('Widget'),
  203. to: getBreadCrumbTarget(
  204. `dashboard/${location.query.dashboardId}/widget/${location.query.widgetId}/`,
  205. location.query,
  206. organization
  207. ),
  208. });
  209. }
  210. }
  211. crumbs.push({
  212. label: t('Trace View'),
  213. });
  214. return crumbs;
  215. }
  216. function getInsightsModuleBreadcrumbs(
  217. location: Location,
  218. organization: Organization,
  219. moduleURLBuilder: URLBuilder,
  220. view?: DomainView
  221. ) {
  222. const crumbs: Crumb[] = [];
  223. if (view && DOMAIN_VIEW_TITLES[view]) {
  224. crumbs.push({
  225. label: DOMAIN_VIEW_TITLES[view],
  226. to: getBreadCrumbTarget(
  227. `${DOMAIN_VIEW_BASE_URL}/${view}/`,
  228. location.query,
  229. organization
  230. ),
  231. });
  232. } else {
  233. crumbs.push({
  234. label: t('Insights'),
  235. });
  236. }
  237. let moduleName: RoutableModuleNames | undefined = undefined;
  238. if (
  239. typeof location.query.source === 'string' &&
  240. TRACE_SOURCE_TO_INSIGHTS_MODULE[
  241. location.query.source as keyof typeof TRACE_SOURCE_TO_INSIGHTS_MODULE
  242. ]
  243. ) {
  244. moduleName = TRACE_SOURCE_TO_INSIGHTS_MODULE[
  245. location.query.source as keyof typeof TRACE_SOURCE_TO_INSIGHTS_MODULE
  246. ] as RoutableModuleNames;
  247. }
  248. switch (moduleName) {
  249. case ModuleName.HTTP:
  250. crumbs.push({
  251. label: t('Domain Summary'),
  252. to: getBreadCrumbTarget(
  253. `${moduleURLBuilder(moduleName, view)}/domains`,
  254. location.query,
  255. organization
  256. ),
  257. });
  258. break;
  259. case ModuleName.DB:
  260. if (location.query.groupId) {
  261. crumbs.push({
  262. label: t('Query Summary'),
  263. to: getBreadCrumbTarget(
  264. `${moduleURLBuilder(moduleName, view)}/spans/span/${location.query.groupId}`,
  265. location.query,
  266. organization
  267. ),
  268. });
  269. } else {
  270. crumbs.push({
  271. label: t('Query Summary'),
  272. });
  273. }
  274. break;
  275. case ModuleName.RESOURCE:
  276. if (location.query.groupId) {
  277. crumbs.push({
  278. label: t('Asset Summary'),
  279. to: getBreadCrumbTarget(
  280. `${moduleURLBuilder(moduleName)}/spans/span/${location.query.groupId}`,
  281. location.query,
  282. organization
  283. ),
  284. });
  285. } else {
  286. crumbs.push({
  287. label: t('Asset Summary'),
  288. });
  289. }
  290. break;
  291. case ModuleName.APP_START:
  292. crumbs.push({
  293. label: t('Screen Summary'),
  294. to: getBreadCrumbTarget(
  295. `${moduleURLBuilder(moduleName, view)}/spans`,
  296. location.query,
  297. organization
  298. ),
  299. });
  300. break;
  301. case ModuleName.SCREEN_LOAD:
  302. crumbs.push({
  303. label: t('Screen Summary'),
  304. to: getBreadCrumbTarget(
  305. `${moduleURLBuilder(moduleName, view)}/spans`,
  306. location.query,
  307. organization
  308. ),
  309. });
  310. break;
  311. case ModuleName.VITAL:
  312. crumbs.push({
  313. label: t('Page Overview'),
  314. to: getBreadCrumbTarget(
  315. `${moduleURLBuilder(moduleName, view)}/overview`,
  316. location.query,
  317. organization
  318. ),
  319. });
  320. break;
  321. case ModuleName.QUEUE:
  322. crumbs.push({
  323. label: t('Destination Summary'),
  324. to: getBreadCrumbTarget(
  325. `${moduleURLBuilder(moduleName, view)}/destination`,
  326. location.query,
  327. organization
  328. ),
  329. });
  330. break;
  331. case ModuleName.AI:
  332. if (location.query.groupId) {
  333. crumbs.push({
  334. label: t('Pipeline Summary'),
  335. to: getBreadCrumbTarget(
  336. `${moduleURLBuilder(moduleName, view)}/pipeline-type/${location.query.groupId}`,
  337. location.query,
  338. organization
  339. ),
  340. });
  341. }
  342. break;
  343. case ModuleName.CACHE:
  344. default:
  345. break;
  346. }
  347. crumbs.push({
  348. label: t('Trace View'),
  349. });
  350. return crumbs;
  351. }
  352. export function getTraceViewBreadcrumbs(
  353. organization: Organization,
  354. location: Location,
  355. moduleUrlBuilder: URLBuilder,
  356. view?: DomainView
  357. ): Crumb[] {
  358. if (
  359. typeof location.query.source === 'string' &&
  360. TRACE_SOURCE_TO_INSIGHTS_MODULE[
  361. location.query.source as keyof typeof TRACE_SOURCE_TO_INSIGHTS_MODULE
  362. ]
  363. ) {
  364. return getInsightsModuleBreadcrumbs(location, organization, moduleUrlBuilder, view);
  365. }
  366. switch (location.query.source) {
  367. case TraceViewSources.TRACES:
  368. return [
  369. {
  370. label: t('Traces'),
  371. to: getBreadCrumbTarget(`traces`, location.query, organization),
  372. },
  373. {
  374. label: t('Trace View'),
  375. },
  376. ];
  377. case TraceViewSources.DISCOVER:
  378. return [
  379. {
  380. label: t('Discover'),
  381. to: getBreadCrumbTarget(`discover/homepage`, location.query, organization),
  382. },
  383. {
  384. label: t('Trace View'),
  385. },
  386. ];
  387. case TraceViewSources.METRICS:
  388. return [
  389. {
  390. label: t('Metrics'),
  391. to: getBreadCrumbTarget(`metrics`, location.query, organization),
  392. },
  393. {
  394. label: t('Trace View'),
  395. },
  396. ];
  397. case TraceViewSources.FEEDBACK_DETAILS:
  398. return [
  399. {
  400. label: t('User Feedback'),
  401. to: getBreadCrumbTarget(`feedback`, location.query, organization),
  402. },
  403. {
  404. label: t('Trace View'),
  405. },
  406. ];
  407. case TraceViewSources.DASHBOARDS:
  408. return getDashboardsBreadCrumbs(organization, location);
  409. case TraceViewSources.ISSUE_DETAILS:
  410. return getIssuesBreadCrumbs(organization, location);
  411. case TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY:
  412. return getPerformanceBreadCrumbs(organization, location, view);
  413. default:
  414. return [{label: t('Trace View')}];
  415. }
  416. }