breadcrumbs.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  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_SCREENS,
  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(
  90. organization.slug,
  91. view,
  92. true
  93. );
  94. crumbs.push({
  95. label: (view && DOMAIN_VIEW_TITLES[view]) || t('Performance'),
  96. to: getBreadCrumbTarget(performanceUrl, location.query, organization),
  97. });
  98. switch (location.query.tab) {
  99. case Tab.EVENTS:
  100. crumbs.push({
  101. label: t('Transaction Summary'),
  102. to: getBreadCrumbTarget(`${transactionSummaryUrl}`, location.query, organization),
  103. });
  104. break;
  105. case Tab.TAGS:
  106. crumbs.push({
  107. label: t('Tags'),
  108. to: getBreadCrumbTarget(
  109. `${transactionSummaryUrl}/tags`,
  110. location.query,
  111. organization
  112. ),
  113. });
  114. break;
  115. case Tab.SPANS:
  116. crumbs.push({
  117. label: t('Spans'),
  118. to: getBreadCrumbTarget(
  119. `${transactionSummaryUrl}/spans`,
  120. location.query,
  121. organization
  122. ),
  123. });
  124. const {spanSlug} = location.query;
  125. if (spanSlug) {
  126. crumbs.push({
  127. label: t('Span Summary'),
  128. to: getBreadCrumbTarget(
  129. `${transactionSummaryUrl}/spans/${spanSlug}`,
  130. location.query,
  131. organization
  132. ),
  133. });
  134. }
  135. break;
  136. case Tab.AGGREGATE_WATERFALL:
  137. crumbs.push({
  138. label: t('Transaction Summary'),
  139. to: getBreadCrumbTarget(
  140. `${transactionSummaryUrl}/aggregateWaterfall`,
  141. location.query,
  142. organization
  143. ),
  144. });
  145. break;
  146. default:
  147. crumbs.push({
  148. label: t('Transaction Summary'),
  149. to: getBreadCrumbTarget(`${transactionSummaryUrl}`, location.query, organization),
  150. });
  151. break;
  152. }
  153. crumbs.push({
  154. label: t('Trace View'),
  155. });
  156. return crumbs;
  157. }
  158. function getIssuesBreadCrumbs(organization: Organization, location: Location) {
  159. const crumbs: Crumb[] = [];
  160. crumbs.push({
  161. label: t('Issues'),
  162. to: getBreadCrumbTarget(`issues`, location.query, organization),
  163. });
  164. if (location.query.groupId) {
  165. crumbs.push({
  166. label: t('Issue Details'),
  167. to: getBreadCrumbTarget(
  168. `issues/${location.query.groupId}`,
  169. location.query,
  170. organization
  171. ),
  172. });
  173. }
  174. crumbs.push({
  175. label: t('Trace View'),
  176. });
  177. return crumbs;
  178. }
  179. function getDashboardsBreadCrumbs(organization: Organization, location: Location) {
  180. const crumbs: Crumb[] = [];
  181. crumbs.push({
  182. label: t('Dashboards'),
  183. to: getBreadCrumbTarget('dashboards', location.query, organization),
  184. });
  185. if (location.query.dashboardId) {
  186. crumbs.push({
  187. label: t('Widgets Legend'),
  188. to: getBreadCrumbTarget(
  189. `dashboard/${location.query.dashboardId}`,
  190. location.query,
  191. organization
  192. ),
  193. });
  194. if (location.query.widgetId) {
  195. crumbs.push({
  196. label: t('Widget'),
  197. to: getBreadCrumbTarget(
  198. `dashboard/${location.query.dashboardId}/widget/${location.query.widgetId}/`,
  199. location.query,
  200. organization
  201. ),
  202. });
  203. }
  204. }
  205. crumbs.push({
  206. label: t('Trace View'),
  207. });
  208. return crumbs;
  209. }
  210. function getInsightsModuleBreadcrumbs(
  211. location: Location,
  212. organization: Organization,
  213. moduleURLBuilder: URLBuilder,
  214. view?: DomainView
  215. ) {
  216. const crumbs: Crumb[] = [];
  217. if (view && DOMAIN_VIEW_TITLES[view]) {
  218. crumbs.push({
  219. label: DOMAIN_VIEW_BASE_TITLE,
  220. to: undefined,
  221. });
  222. crumbs.push({
  223. label: DOMAIN_VIEW_TITLES[view],
  224. to: getBreadCrumbTarget(
  225. `${DOMAIN_VIEW_BASE_URL}/${view}/`,
  226. location.query,
  227. organization
  228. ),
  229. });
  230. } else {
  231. crumbs.push({
  232. label: t('Insights'),
  233. });
  234. }
  235. let moduleName: RoutableModuleNames | undefined = undefined;
  236. if (
  237. typeof location.query.source === 'string' &&
  238. TRACE_SOURCE_TO_INSIGHTS_MODULE[
  239. location.query.source as keyof typeof TRACE_SOURCE_TO_INSIGHTS_MODULE
  240. ]
  241. ) {
  242. moduleName = TRACE_SOURCE_TO_INSIGHTS_MODULE[
  243. location.query.source as keyof typeof TRACE_SOURCE_TO_INSIGHTS_MODULE
  244. ] as RoutableModuleNames;
  245. crumbs.push({
  246. label: MODULE_TITLES[moduleName],
  247. to: getBreadCrumbTarget(
  248. `${moduleURLBuilder(moduleName, view)}/`,
  249. location.query,
  250. organization
  251. ),
  252. });
  253. }
  254. switch (moduleName) {
  255. case ModuleName.HTTP:
  256. crumbs.push({
  257. label: t('Domain Summary'),
  258. to: getBreadCrumbTarget(
  259. `${moduleURLBuilder(moduleName, view)}/domains`,
  260. location.query,
  261. organization
  262. ),
  263. });
  264. break;
  265. case ModuleName.DB:
  266. if (location.query.groupId) {
  267. crumbs.push({
  268. label: t('Query Summary'),
  269. to: getBreadCrumbTarget(
  270. `${moduleURLBuilder(moduleName, view)}/spans/span/${location.query.groupId}`,
  271. location.query,
  272. organization
  273. ),
  274. });
  275. } else {
  276. crumbs.push({
  277. label: t('Query Summary'),
  278. });
  279. }
  280. break;
  281. case ModuleName.RESOURCE:
  282. if (location.query.groupId) {
  283. crumbs.push({
  284. label: t('Asset Summary'),
  285. to: getBreadCrumbTarget(
  286. `${moduleURLBuilder(moduleName)}/spans/span/${location.query.groupId}`,
  287. location.query,
  288. organization
  289. ),
  290. });
  291. } else {
  292. crumbs.push({
  293. label: t('Asset Summary'),
  294. });
  295. }
  296. break;
  297. case ModuleName.APP_START:
  298. crumbs.push({
  299. label: t('Screen Summary'),
  300. to: getBreadCrumbTarget(
  301. `${moduleURLBuilder(moduleName, view)}/spans`,
  302. location.query,
  303. organization
  304. ),
  305. });
  306. break;
  307. case ModuleName.SCREEN_LOAD:
  308. crumbs.push({
  309. label: t('Screen Summary'),
  310. to: getBreadCrumbTarget(
  311. `${moduleURLBuilder(moduleName, view)}/spans`,
  312. location.query,
  313. organization
  314. ),
  315. });
  316. break;
  317. case ModuleName.VITAL:
  318. crumbs.push({
  319. label: t('Page Overview'),
  320. to: getBreadCrumbTarget(
  321. `${moduleURLBuilder(moduleName, view)}/overview`,
  322. location.query,
  323. organization
  324. ),
  325. });
  326. break;
  327. case ModuleName.QUEUE:
  328. crumbs.push({
  329. label: t('Destination Summary'),
  330. to: getBreadCrumbTarget(
  331. `${moduleURLBuilder(moduleName, view)}/destination`,
  332. location.query,
  333. organization
  334. ),
  335. });
  336. break;
  337. case ModuleName.AI:
  338. if (location.query.groupId) {
  339. crumbs.push({
  340. label: t('Pipeline Summary'),
  341. to: getBreadCrumbTarget(
  342. `${moduleURLBuilder(moduleName, view)}/pipeline-type/${location.query.groupId}`,
  343. location.query,
  344. organization
  345. ),
  346. });
  347. }
  348. break;
  349. case ModuleName.CACHE:
  350. default:
  351. break;
  352. }
  353. crumbs.push({
  354. label: t('Trace View'),
  355. });
  356. return crumbs;
  357. }
  358. export function getTraceViewBreadcrumbs(
  359. organization: Organization,
  360. location: Location,
  361. moduleUrlBuilder: URLBuilder,
  362. view?: DomainView
  363. ): Crumb[] {
  364. if (
  365. typeof location.query.source === 'string' &&
  366. TRACE_SOURCE_TO_INSIGHTS_MODULE[
  367. location.query.source as keyof typeof TRACE_SOURCE_TO_INSIGHTS_MODULE
  368. ]
  369. ) {
  370. return getInsightsModuleBreadcrumbs(location, organization, moduleUrlBuilder, view);
  371. }
  372. switch (location.query.source) {
  373. case TraceViewSources.TRACES:
  374. return [
  375. {
  376. label: t('Traces'),
  377. to: getBreadCrumbTarget(`traces`, location.query, organization),
  378. },
  379. {
  380. label: t('Trace View'),
  381. },
  382. ];
  383. case TraceViewSources.DISCOVER:
  384. return [
  385. {
  386. label: t('Discover'),
  387. to: getBreadCrumbTarget(`discover/homepage`, location.query, organization),
  388. },
  389. {
  390. label: t('Trace View'),
  391. },
  392. ];
  393. case TraceViewSources.METRICS:
  394. return [
  395. {
  396. label: t('Metrics'),
  397. to: getBreadCrumbTarget(`metrics`, location.query, organization),
  398. },
  399. {
  400. label: t('Trace View'),
  401. },
  402. ];
  403. case TraceViewSources.FEEDBACK_DETAILS:
  404. return [
  405. {
  406. label: t('User Feedback'),
  407. to: getBreadCrumbTarget(`feedback`, location.query, organization),
  408. },
  409. {
  410. label: t('Trace View'),
  411. },
  412. ];
  413. case TraceViewSources.DASHBOARDS:
  414. return getDashboardsBreadCrumbs(organization, location);
  415. case TraceViewSources.ISSUE_DETAILS:
  416. return getIssuesBreadCrumbs(organization, location);
  417. case TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY:
  418. return getPerformanceBreadCrumbs(organization, location, view);
  419. default:
  420. return [{label: t('Trace View')}];
  421. }
  422. }