toolbarSuggestedQueries.tsx 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import {useMemo} from 'react';
  2. import styled from '@emotion/styled';
  3. import Tag from 'sentry/components/badge/tag';
  4. import Panel from 'sentry/components/panels/panel';
  5. import {ALL_ACCESS_PROJECTS} from 'sentry/constants/pageFilters';
  6. import {
  7. backend,
  8. frontend,
  9. mobile,
  10. PlatformCategory,
  11. serverless,
  12. } from 'sentry/data/platformCategories';
  13. import {IconClose} from 'sentry/icons/iconClose';
  14. import {t} from 'sentry/locale';
  15. import {space} from 'sentry/styles/space';
  16. import type {PageFilters} from 'sentry/types/core';
  17. import type {Project} from 'sentry/types/project';
  18. import {defined} from 'sentry/utils';
  19. import useDismissAlert from 'sentry/utils/useDismissAlert';
  20. import {useLocation} from 'sentry/utils/useLocation';
  21. import useOrganization from 'sentry/utils/useOrganization';
  22. import usePageFilters from 'sentry/utils/usePageFilters';
  23. import useProjects from 'sentry/utils/useProjects';
  24. import {
  25. newExploreTarget,
  26. type SuggestedQuery,
  27. } from 'sentry/views/explore/contexts/pageParamsContext';
  28. import {Mode} from 'sentry/views/explore/contexts/pageParamsContext/mode';
  29. import {ChartType} from 'sentry/views/insights/common/components/chart';
  30. import {ToolbarHeader, ToolbarHeaderButton, ToolbarLabel, ToolbarSection} from './styles';
  31. interface ToolbarSuggestedQueriesProps {}
  32. export function ToolbarSuggestedQueries(props: ToolbarSuggestedQueriesProps) {
  33. const organization = useOrganization();
  34. const {dismiss, isDismissed} = useDismissAlert({
  35. key: `${organization.id}:metrics-empty-state-dismissed`,
  36. expirationDays: 30,
  37. });
  38. if (isDismissed) {
  39. return null;
  40. }
  41. return <ToolbarSuggestedQueriesInner {...props} dismiss={dismiss} />;
  42. }
  43. interface ToolbarSuggestedQueriesInnerProps extends ToolbarSuggestedQueriesProps {
  44. dismiss: () => void;
  45. }
  46. function ToolbarSuggestedQueriesInner({dismiss}: ToolbarSuggestedQueriesInnerProps) {
  47. const {selection} = usePageFilters();
  48. const {projects} = useProjects();
  49. const suggestedQueries: SuggestedQuery[] = useMemo(() => {
  50. const counters = {
  51. [PlatformCategory.FRONTEND]: 0,
  52. [PlatformCategory.MOBILE]: 0,
  53. [PlatformCategory.BACKEND]: 0,
  54. };
  55. for (const project of getSelectedProjectsList(selection.projects, projects)) {
  56. if (!defined(project.platform)) {
  57. continue;
  58. }
  59. if (frontend.includes(project.platform)) {
  60. counters[PlatformCategory.FRONTEND] += 1;
  61. } else if (mobile.includes(project.platform)) {
  62. counters[PlatformCategory.MOBILE] += 1;
  63. } else if (backend.includes(project.platform)) {
  64. counters[PlatformCategory.BACKEND] += 1;
  65. } else if (serverless.includes(project.platform)) {
  66. // consider serverless as a type of backend platform
  67. counters[PlatformCategory.BACKEND] += 1;
  68. }
  69. }
  70. const platforms = [
  71. PlatformCategory.FRONTEND,
  72. PlatformCategory.MOBILE,
  73. PlatformCategory.BACKEND,
  74. ]
  75. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  76. .filter(k => counters[k] > 0)
  77. // @ts-expect-error TS(7053): Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
  78. .sort((a, b) => counters[b] - counters[a]);
  79. return getSuggestedQueries(platforms);
  80. }, [selection, projects]);
  81. return (
  82. <ToolbarSection data-test-id="section-suggested-queries">
  83. <StyledPanel>
  84. <ToolbarHeader>
  85. <ToolbarLabel underlined={false}>{t('Suggested Queries')}</ToolbarLabel>
  86. <ToolbarHeaderButton
  87. size="zero"
  88. onClick={dismiss}
  89. borderless
  90. aria-label={t('Dismiss Suggested Queries')}
  91. icon={<IconClose />}
  92. />
  93. </ToolbarHeader>
  94. <div>
  95. {t("Feeling like a newb? Been there, done that. Here's a few to get you goin.")}
  96. </div>
  97. <SuggestedQueriesContainer>
  98. {suggestedQueries.map(suggestedQuery => (
  99. <SuggestedQueryLink
  100. key={suggestedQuery.title}
  101. suggestedQuery={suggestedQuery}
  102. />
  103. ))}
  104. </SuggestedQueriesContainer>
  105. </StyledPanel>
  106. </ToolbarSection>
  107. );
  108. }
  109. interface SuggestedQueryLinkProps {
  110. suggestedQuery: SuggestedQuery;
  111. }
  112. function SuggestedQueryLink({suggestedQuery}: SuggestedQueryLinkProps) {
  113. const location = useLocation();
  114. const target = useMemo(
  115. () => newExploreTarget(location, suggestedQuery),
  116. [location, suggestedQuery]
  117. );
  118. return (
  119. <Tag to={target} icon={null} type="info">
  120. {suggestedQuery.title}
  121. </Tag>
  122. );
  123. }
  124. function getSelectedProjectsList(
  125. selectedProjects: PageFilters['projects'],
  126. projects: Project[]
  127. ): Project[] {
  128. if (
  129. selectedProjects[0] === ALL_ACCESS_PROJECTS || // all projects
  130. selectedProjects.length === 0 // my projects
  131. ) {
  132. return projects;
  133. }
  134. const projectIds = new Set(selectedProjects.map(String));
  135. return projects.filter(project => projectIds.has(project.id));
  136. }
  137. function getSuggestedQueries(platforms: PlatformCategory[], maxQueries = 5) {
  138. const frontendQueries: SuggestedQuery[] = [
  139. {
  140. title: t('Worst LCPs'),
  141. fields: [
  142. 'id',
  143. 'project',
  144. 'span.op',
  145. 'span.description',
  146. 'span.duration',
  147. 'measurements.lcp',
  148. ],
  149. groupBys: ['span.description'],
  150. mode: Mode.AGGREGATE,
  151. query: 'span.op:[pageload,navigation]',
  152. sortBys: [{field: 'avg(measurements.lcp)', kind: 'desc'}],
  153. visualizes: [
  154. {chartType: ChartType.LINE, yAxes: ['p50(measurements.lcp)']},
  155. {chartType: ChartType.LINE, yAxes: ['avg(measurements.lcp)']},
  156. ],
  157. },
  158. {
  159. title: t('Biggest Assets'),
  160. fields: [
  161. 'id',
  162. 'project',
  163. 'span.op',
  164. 'span.description',
  165. 'span.duration',
  166. 'http.response_transfer_size',
  167. 'timestamp',
  168. ],
  169. groupBys: ['span.description'],
  170. mode: Mode.AGGREGATE,
  171. query: 'span.op:[resource.css,resource.img,resource.script]',
  172. sortBys: [{field: 'p75(http.response_transfer_size)', kind: 'desc'}],
  173. visualizes: [
  174. {chartType: ChartType.LINE, yAxes: ['p75(http.response_transfer_size)']},
  175. {chartType: ChartType.LINE, yAxes: ['p90(http.response_transfer_size)']},
  176. ],
  177. },
  178. {
  179. title: t('Top Pageloads'),
  180. fields: [
  181. 'id',
  182. 'project',
  183. 'span.op',
  184. 'span.description',
  185. 'span.duration',
  186. 'timestamp',
  187. ],
  188. groupBys: ['span.description'],
  189. mode: Mode.AGGREGATE,
  190. query: 'span.op:[pageload,navigation]',
  191. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  192. visualizes: [
  193. {chartType: ChartType.LINE, yAxes: ['avg(span.duration)']},
  194. {chartType: ChartType.LINE, yAxes: ['p50(span.duration)']},
  195. ],
  196. },
  197. ];
  198. const backendQueries: SuggestedQuery[] = [
  199. {
  200. title: t('Slowest Server Calls'),
  201. fields: [
  202. 'id',
  203. 'project',
  204. 'span.op',
  205. 'span.description',
  206. 'span.duration',
  207. 'timestamp',
  208. ],
  209. groupBys: ['span.description'],
  210. mode: Mode.AGGREGATE,
  211. query: 'span.op:http.server',
  212. sortBys: [{field: 'p75(span.duration)', kind: 'desc'}],
  213. visualizes: [
  214. {chartType: ChartType.LINE, yAxes: ['p75(span.duration)']},
  215. {chartType: ChartType.LINE, yAxes: ['p90(span.duration)']},
  216. ],
  217. },
  218. ];
  219. const mobileQueries: SuggestedQuery[] = [
  220. {
  221. title: t('Top Screenloads'),
  222. fields: [
  223. 'id',
  224. 'project',
  225. 'span.op',
  226. 'span.description',
  227. 'span.duration',
  228. 'timestamp',
  229. ],
  230. groupBys: ['span.description'],
  231. mode: Mode.AGGREGATE,
  232. query: 'span.op:ui.load',
  233. sortBys: [{field: 'count(span.duration)', kind: 'desc'}],
  234. visualizes: [{chartType: ChartType.LINE, yAxes: ['count(span.duration)']}],
  235. },
  236. ];
  237. const allQueries: Partial<Record<PlatformCategory, SuggestedQuery[]>> = {
  238. [PlatformCategory.FRONTEND]: frontendQueries,
  239. [PlatformCategory.BACKEND]: backendQueries,
  240. [PlatformCategory.MOBILE]: mobileQueries,
  241. };
  242. const genericQueries: SuggestedQuery[] = [
  243. {
  244. title: t('Slowest Ops'),
  245. fields: [
  246. 'id',
  247. 'project',
  248. 'span.op',
  249. 'span.description',
  250. 'span.duration',
  251. 'timestamp',
  252. ],
  253. groupBys: ['span.op'],
  254. mode: Mode.AGGREGATE,
  255. query: '',
  256. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  257. visualizes: [
  258. {chartType: ChartType.LINE, yAxes: ['avg(span.duration)']},
  259. {chartType: ChartType.LINE, yAxes: ['p50(span.duration)']},
  260. ],
  261. },
  262. {
  263. title: t('Database Latency'),
  264. fields: [
  265. 'id',
  266. 'project',
  267. 'span.op',
  268. 'span.description',
  269. 'span.duration',
  270. 'db.system',
  271. ],
  272. groupBys: ['span.op', 'db.system'],
  273. mode: Mode.AGGREGATE,
  274. query: 'span.op:db',
  275. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  276. visualizes: [
  277. {chartType: ChartType.LINE, yAxes: ['avg(span.duration)']},
  278. {chartType: ChartType.LINE, yAxes: ['p50(span.duration)']},
  279. ],
  280. },
  281. ];
  282. const queries: SuggestedQuery[] = [];
  283. for (const platform of platforms) {
  284. for (const query of allQueries[platform] || []) {
  285. queries.push(query);
  286. if (queries.length >= maxQueries) {
  287. return queries;
  288. }
  289. }
  290. }
  291. for (const query of genericQueries) {
  292. queries.push(query);
  293. if (queries.length >= maxQueries) {
  294. return queries;
  295. }
  296. }
  297. return queries;
  298. }
  299. const StyledPanel = styled(Panel)`
  300. padding: ${space(2)};
  301. background: linear-gradient(
  302. 269.35deg,
  303. ${p => p.theme.backgroundTertiary} 0.32%,
  304. rgba(245, 243, 247, 0) 99.69%
  305. );
  306. `;
  307. const SuggestedQueriesContainer = styled('div')`
  308. display: flex;
  309. flex-wrap: wrap;
  310. gap: ${space(1)};
  311. margin-top: ${space(1)};
  312. `;