breadcrumbs.tsx 10 KB

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