breadcrumbs.tsx 12 KB

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