breadcrumbs.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  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. 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. PERFORMANCE_TRANSACTION_SUMMARY_PROFILES = 'performance_transaction_summary_profiles',
  40. ISSUE_DETAILS = 'issue_details',
  41. }
  42. // Ideally every new entry to ModuleName, would require a new source to be added here so we don't miss any.
  43. const TRACE_SOURCE_TO_MODULE: Partial<Record<TraceViewSources, ModuleName>> = {
  44. app_starts_module: ModuleName.APP_START,
  45. assets_module: ModuleName.RESOURCE,
  46. caches_module: ModuleName.CACHE,
  47. llm_module: ModuleName.AI,
  48. queries_module: ModuleName.DB,
  49. requests_module: ModuleName.HTTP,
  50. screen_loads_module: ModuleName.SCREEN_LOAD,
  51. web_vitals_module: ModuleName.VITAL,
  52. queues_module: ModuleName.QUEUE,
  53. screen_load_module: ModuleName.SCREEN_LOAD,
  54. screen_rendering_module: ModuleName.SCREEN_RENDERING,
  55. mobile_screens_module: ModuleName.MOBILE_SCREENS,
  56. };
  57. function getBreadCrumbTarget(
  58. path: string,
  59. query: Location['query'],
  60. organization: Organization
  61. ) {
  62. return {
  63. pathname: normalizeUrl(`/organizations/${organization.slug}/${path}`),
  64. // Remove traceView specific query parameters that are not needed when navigating back.
  65. query: {...omit(query, ['node', 'fov', 'timestamp', 'eventId'])},
  66. };
  67. }
  68. function getPerformanceBreadCrumbs(
  69. organization: Organization,
  70. location: Location,
  71. view?: DomainView
  72. ) {
  73. const crumbs: Crumb[] = [];
  74. const performanceUrl = getPerformanceBaseUrl(organization.slug, view, true);
  75. const transactionSummaryUrl = getTransactionSummaryBaseUrl(
  76. organization.slug,
  77. view,
  78. true
  79. );
  80. if (view) {
  81. crumbs.push({
  82. label: DOMAIN_VIEW_BASE_TITLE,
  83. to: undefined,
  84. });
  85. }
  86. crumbs.push({
  87. label: (view && DOMAIN_VIEW_TITLES[view]) || t('Performance'),
  88. to: getBreadCrumbTarget(performanceUrl, location.query, organization),
  89. });
  90. switch (location.query.tab) {
  91. case Tab.EVENTS:
  92. crumbs.push({
  93. label: t('All Events'),
  94. to: getBreadCrumbTarget(
  95. `${transactionSummaryUrl}/events`,
  96. location.query,
  97. organization
  98. ),
  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 getInsightsModuleBreadcrumbs(
  176. location: Location,
  177. organization: Organization,
  178. moduleURLBuilder: URLBuilder,
  179. view?: DomainView
  180. ) {
  181. const crumbs: Crumb[] = [];
  182. if (view && DOMAIN_VIEW_TITLES[view]) {
  183. crumbs.push({
  184. label: DOMAIN_VIEW_BASE_TITLE,
  185. to: undefined,
  186. });
  187. crumbs.push({
  188. label: DOMAIN_VIEW_TITLES[view],
  189. to: getBreadCrumbTarget(
  190. `${DOMAIN_VIEW_BASE_URL}/${view}/`,
  191. location.query,
  192. organization
  193. ),
  194. });
  195. } else {
  196. crumbs.push({
  197. label: t('Insights'),
  198. });
  199. }
  200. let moduleName: RoutableModuleNames | undefined = undefined;
  201. if (
  202. typeof location.query.source === 'string' &&
  203. TRACE_SOURCE_TO_MODULE[location.query.source]
  204. ) {
  205. moduleName = TRACE_SOURCE_TO_MODULE[location.query.source] as RoutableModuleNames;
  206. crumbs.push({
  207. label: MODULE_TITLES[moduleName],
  208. to: moduleURLBuilder(moduleName),
  209. });
  210. }
  211. switch (moduleName) {
  212. case ModuleName.HTTP:
  213. crumbs.push({
  214. label: t('Domain Summary'),
  215. to: getBreadCrumbTarget(
  216. `${moduleURLBuilder(moduleName, view)}/domains`,
  217. location.query,
  218. organization
  219. ),
  220. });
  221. break;
  222. case ModuleName.DB:
  223. if (location.query.groupId) {
  224. crumbs.push({
  225. label: t('Query Summary'),
  226. to: getBreadCrumbTarget(
  227. `${moduleURLBuilder(moduleName, view)}/spans/span/${location.query.groupId}`,
  228. location.query,
  229. organization
  230. ),
  231. });
  232. } else {
  233. crumbs.push({
  234. label: t('Query Summary'),
  235. });
  236. }
  237. break;
  238. case ModuleName.RESOURCE:
  239. if (location.query.groupId) {
  240. crumbs.push({
  241. label: t('Asset Summary'),
  242. to: getBreadCrumbTarget(
  243. `${moduleURLBuilder(moduleName)}/spans/span/${location.query.groupId}`,
  244. location.query,
  245. organization
  246. ),
  247. });
  248. } else {
  249. crumbs.push({
  250. label: t('Asset Summary'),
  251. });
  252. }
  253. break;
  254. case ModuleName.APP_START:
  255. crumbs.push({
  256. label: t('Screen Summary'),
  257. to: getBreadCrumbTarget(
  258. `${moduleURLBuilder(moduleName, view)}/spans`,
  259. location.query,
  260. organization
  261. ),
  262. });
  263. break;
  264. case ModuleName.SCREEN_LOAD:
  265. crumbs.push({
  266. label: t('Screen Summary'),
  267. to: getBreadCrumbTarget(
  268. `${moduleURLBuilder(moduleName, view)}/spans`,
  269. location.query,
  270. organization
  271. ),
  272. });
  273. break;
  274. case ModuleName.VITAL:
  275. crumbs.push({
  276. label: t('Page Overview'),
  277. to: getBreadCrumbTarget(
  278. `${moduleURLBuilder(moduleName, view)}/overview`,
  279. location.query,
  280. organization
  281. ),
  282. });
  283. break;
  284. case ModuleName.QUEUE:
  285. crumbs.push({
  286. label: t('Destination Summary'),
  287. to: getBreadCrumbTarget(
  288. `${moduleURLBuilder(moduleName, view)}/destination`,
  289. location.query,
  290. organization
  291. ),
  292. });
  293. break;
  294. case ModuleName.AI:
  295. if (location.query.groupId) {
  296. crumbs.push({
  297. label: t('Pipeline Summary'),
  298. to: getBreadCrumbTarget(
  299. `${moduleURLBuilder(moduleName, view)}/pipeline-type/${location.query.groupId}`,
  300. location.query,
  301. organization
  302. ),
  303. });
  304. }
  305. break;
  306. case ModuleName.CACHE:
  307. default:
  308. break;
  309. }
  310. crumbs.push({
  311. label: t('Trace View'),
  312. });
  313. return crumbs;
  314. }
  315. export function getTraceViewBreadcrumbs(
  316. organization: Organization,
  317. location: Location,
  318. moduleUrlBuilder: URLBuilder,
  319. view?: DomainView
  320. ): Crumb[] {
  321. if (
  322. typeof location.query.source === 'string' &&
  323. TRACE_SOURCE_TO_MODULE[location.query.source]
  324. ) {
  325. return getInsightsModuleBreadcrumbs(location, organization, moduleUrlBuilder, view);
  326. }
  327. switch (location.query.source) {
  328. case TraceViewSources.TRACES:
  329. return [
  330. {
  331. label: t('Traces'),
  332. to: getBreadCrumbTarget(`traces`, location.query, organization),
  333. },
  334. {
  335. label: t('Trace View'),
  336. },
  337. ];
  338. case TraceViewSources.DISCOVER:
  339. return [
  340. {
  341. label: t('Discover'),
  342. to: getBreadCrumbTarget(`discover/homepage`, location.query, organization),
  343. },
  344. {
  345. label: t('Trace View'),
  346. },
  347. ];
  348. case TraceViewSources.METRICS:
  349. return [
  350. {
  351. label: t('Metrics'),
  352. to: getBreadCrumbTarget(`metrics`, location.query, organization),
  353. },
  354. {
  355. label: t('Trace View'),
  356. },
  357. ];
  358. case TraceViewSources.ISSUE_DETAILS:
  359. return getIssuesBreadCrumbs(organization, location);
  360. case TraceViewSources.PERFORMANCE_TRANSACTION_SUMMARY:
  361. return getPerformanceBreadCrumbs(organization, location, view);
  362. default:
  363. return [{label: t('Trace View')}];
  364. }
  365. }