breadcrumbs.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  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. PERFORMANCE_TRANSACTION_SUMMARY_PROFILES = 'performance_transaction_summary_profiles',
  41. ISSUE_DETAILS = 'issue_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_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_SCREENS,
  57. };
  58. function getBreadCrumbTarget(
  59. path: string,
  60. query: Location['query'],
  61. organization: Organization
  62. ) {
  63. return {
  64. pathname: normalizeUrl(`/organizations/${organization.slug}/${path}`),
  65. // Remove traceView specific query parameters that are not needed when navigating back.
  66. query: {...omit(query, ['node', 'fov', 'timestamp', 'eventId'])},
  67. };
  68. }
  69. function getPerformanceBreadCrumbs(
  70. organization: Organization,
  71. location: Location,
  72. view?: DomainView
  73. ) {
  74. const crumbs: Crumb[] = [];
  75. const performanceUrl = getPerformanceBaseUrl(organization.slug, view, true);
  76. const transactionSummaryUrl = getTransactionSummaryBaseUrl(
  77. organization.slug,
  78. view,
  79. true
  80. );
  81. if (view) {
  82. crumbs.push({
  83. label: DOMAIN_VIEW_BASE_TITLE,
  84. to: undefined,
  85. });
  86. }
  87. crumbs.push({
  88. label: (view && DOMAIN_VIEW_TITLES[view]) || t('Performance'),
  89. to: getBreadCrumbTarget(performanceUrl, location.query, organization),
  90. });
  91. switch (location.query.tab) {
  92. case Tab.EVENTS:
  93. crumbs.push({
  94. label: t('All Events'),
  95. to: getBreadCrumbTarget(
  96. `${transactionSummaryUrl}/events`,
  97. location.query,
  98. organization
  99. ),
  100. });
  101. break;
  102. case Tab.TAGS:
  103. crumbs.push({
  104. label: t('Tags'),
  105. to: getBreadCrumbTarget(
  106. `${transactionSummaryUrl}/tags`,
  107. location.query,
  108. organization
  109. ),
  110. });
  111. break;
  112. case Tab.SPANS:
  113. crumbs.push({
  114. label: t('Spans'),
  115. to: getBreadCrumbTarget(
  116. `${transactionSummaryUrl}/spans`,
  117. location.query,
  118. organization
  119. ),
  120. });
  121. const {spanSlug} = location.query;
  122. if (spanSlug) {
  123. crumbs.push({
  124. label: t('Span Summary'),
  125. to: getBreadCrumbTarget(
  126. `${transactionSummaryUrl}/spans/${spanSlug}`,
  127. location.query,
  128. organization
  129. ),
  130. });
  131. }
  132. break;
  133. case Tab.AGGREGATE_WATERFALL:
  134. crumbs.push({
  135. label: t('Transaction Summary'),
  136. to: getBreadCrumbTarget(
  137. `${transactionSummaryUrl}/aggregateWaterfall`,
  138. location.query,
  139. organization
  140. ),
  141. });
  142. break;
  143. default:
  144. crumbs.push({
  145. label: t('Transaction Summary'),
  146. to: getBreadCrumbTarget(`${transactionSummaryUrl}`, location.query, organization),
  147. });
  148. break;
  149. }
  150. crumbs.push({
  151. label: t('Trace View'),
  152. });
  153. return crumbs;
  154. }
  155. function getIssuesBreadCrumbs(organization: Organization, location: Location) {
  156. const crumbs: Crumb[] = [];
  157. crumbs.push({
  158. label: t('Issues'),
  159. to: getBreadCrumbTarget(`issues`, location.query, organization),
  160. });
  161. if (location.query.groupId) {
  162. crumbs.push({
  163. label: t('Issue Details'),
  164. to: getBreadCrumbTarget(
  165. `issues/${location.query.groupId}`,
  166. location.query,
  167. organization
  168. ),
  169. });
  170. }
  171. crumbs.push({
  172. label: t('Trace View'),
  173. });
  174. return crumbs;
  175. }
  176. function getInsightsModuleBreadcrumbs(
  177. location: Location,
  178. organization: Organization,
  179. moduleURLBuilder: URLBuilder,
  180. view?: DomainView
  181. ) {
  182. const crumbs: Crumb[] = [];
  183. if (view && DOMAIN_VIEW_TITLES[view]) {
  184. crumbs.push({
  185. label: DOMAIN_VIEW_BASE_TITLE,
  186. to: undefined,
  187. });
  188. crumbs.push({
  189. label: DOMAIN_VIEW_TITLES[view],
  190. to: getBreadCrumbTarget(
  191. `${DOMAIN_VIEW_BASE_URL}/${view}/`,
  192. location.query,
  193. organization
  194. ),
  195. });
  196. } else {
  197. crumbs.push({
  198. label: t('Insights'),
  199. });
  200. }
  201. let moduleName: RoutableModuleNames | undefined = undefined;
  202. if (
  203. typeof location.query.source === 'string' &&
  204. TRACE_SOURCE_TO_MODULE[location.query.source]
  205. ) {
  206. moduleName = TRACE_SOURCE_TO_MODULE[location.query.source] as RoutableModuleNames;
  207. crumbs.push({
  208. label: MODULE_TITLES[moduleName],
  209. to: moduleURLBuilder(moduleName),
  210. });
  211. }
  212. switch (moduleName) {
  213. case ModuleName.HTTP:
  214. crumbs.push({
  215. label: t('Domain Summary'),
  216. to: getBreadCrumbTarget(
  217. `${moduleURLBuilder(moduleName, view)}/domains`,
  218. location.query,
  219. organization
  220. ),
  221. });
  222. break;
  223. case ModuleName.DB:
  224. if (location.query.groupId) {
  225. crumbs.push({
  226. label: t('Query Summary'),
  227. to: getBreadCrumbTarget(
  228. `${moduleURLBuilder(moduleName, view)}/spans/span/${location.query.groupId}`,
  229. location.query,
  230. organization
  231. ),
  232. });
  233. } else {
  234. crumbs.push({
  235. label: t('Query Summary'),
  236. });
  237. }
  238. break;
  239. case ModuleName.RESOURCE:
  240. if (location.query.groupId) {
  241. crumbs.push({
  242. label: t('Asset Summary'),
  243. to: getBreadCrumbTarget(
  244. `${moduleURLBuilder(moduleName)}/spans/span/${location.query.groupId}`,
  245. location.query,
  246. organization
  247. ),
  248. });
  249. } else {
  250. crumbs.push({
  251. label: t('Asset Summary'),
  252. });
  253. }
  254. break;
  255. case ModuleName.APP_START:
  256. crumbs.push({
  257. label: t('Screen Summary'),
  258. to: getBreadCrumbTarget(
  259. `${moduleURLBuilder(moduleName, view)}/spans`,
  260. location.query,
  261. organization
  262. ),
  263. });
  264. break;
  265. case ModuleName.SCREEN_LOAD:
  266. crumbs.push({
  267. label: t('Screen Summary'),
  268. to: getBreadCrumbTarget(
  269. `${moduleURLBuilder(moduleName, view)}/spans`,
  270. location.query,
  271. organization
  272. ),
  273. });
  274. break;
  275. case ModuleName.VITAL:
  276. crumbs.push({
  277. label: t('Page Overview'),
  278. to: getBreadCrumbTarget(
  279. `${moduleURLBuilder(moduleName, view)}/overview`,
  280. location.query,
  281. organization
  282. ),
  283. });
  284. break;
  285. case ModuleName.QUEUE:
  286. crumbs.push({
  287. label: t('Destination Summary'),
  288. to: getBreadCrumbTarget(
  289. `${moduleURLBuilder(moduleName, view)}/destination`,
  290. location.query,
  291. organization
  292. ),
  293. });
  294. break;
  295. case ModuleName.AI:
  296. if (location.query.groupId) {
  297. crumbs.push({
  298. label: t('Pipeline Summary'),
  299. to: getBreadCrumbTarget(
  300. `${moduleURLBuilder(moduleName, view)}/pipeline-type/${location.query.groupId}`,
  301. location.query,
  302. organization
  303. ),
  304. });
  305. }
  306. break;
  307. case ModuleName.CACHE:
  308. default:
  309. break;
  310. }
  311. crumbs.push({
  312. label: t('Trace View'),
  313. });
  314. return crumbs;
  315. }
  316. export function getTraceViewBreadcrumbs(
  317. organization: Organization,
  318. location: Location,
  319. moduleUrlBuilder: URLBuilder,
  320. view?: DomainView
  321. ): Crumb[] {
  322. if (
  323. typeof location.query.source === 'string' &&
  324. TRACE_SOURCE_TO_MODULE[location.query.source]
  325. ) {
  326. return getInsightsModuleBreadcrumbs(location, organization, moduleUrlBuilder, view);
  327. }
  328. switch (location.query.source) {
  329. case TraceViewSources.TRACES:
  330. return [
  331. {
  332. label: t('Traces'),
  333. to: getBreadCrumbTarget(`traces`, location.query, organization),
  334. },
  335. {
  336. label: t('Trace View'),
  337. },
  338. ];
  339. case TraceViewSources.DISCOVER:
  340. return [
  341. {
  342. label: t('Discover'),
  343. to: getBreadCrumbTarget(`discover/homepage`, location.query, organization),
  344. },
  345. {
  346. label: t('Trace View'),
  347. },
  348. ];
  349. case TraceViewSources.METRICS:
  350. return [
  351. {
  352. label: t('Metrics'),
  353. to: getBreadCrumbTarget(`metrics`, location.query, organization),
  354. },
  355. {
  356. label: t('Trace View'),
  357. },
  358. ];
  359. case TraceViewSources.ISSUE_DETAILS:
  360. return getIssuesBreadCrumbs(organization, location);
  361. case TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY:
  362. return getPerformanceBreadCrumbs(organization, location, view);
  363. default:
  364. return [{label: t('Trace View')}];
  365. }
  366. }