toolbarSuggestedQueries.tsx 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  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. .filter(k => counters[k] > 0)
  76. .sort((a, b) => counters[b] - counters[a]);
  77. return getSuggestedQueries(platforms);
  78. }, [selection, projects]);
  79. return (
  80. <ToolbarSection data-test-id="section-suggested-queries">
  81. <StyledPanel>
  82. <ToolbarHeader>
  83. <ToolbarLabel underlined={false}>{t('Suggested Queries')}</ToolbarLabel>
  84. <ToolbarHeaderButton
  85. size="zero"
  86. onClick={dismiss}
  87. borderless
  88. aria-label={t('Dismiss Suggested Queries')}
  89. icon={<IconClose />}
  90. />
  91. </ToolbarHeader>
  92. <div>
  93. {t("Feeling like a newb? Been there, done that. Here's a few to get you goin.")}
  94. </div>
  95. <SuggestedQueriesContainer>
  96. {suggestedQueries.map(suggestedQuery => (
  97. <SuggestedQueryLink
  98. key={suggestedQuery.title}
  99. suggestedQuery={suggestedQuery}
  100. />
  101. ))}
  102. </SuggestedQueriesContainer>
  103. </StyledPanel>
  104. </ToolbarSection>
  105. );
  106. }
  107. interface SuggestedQueryLinkProps {
  108. suggestedQuery: SuggestedQuery;
  109. }
  110. function SuggestedQueryLink({suggestedQuery}: SuggestedQueryLinkProps) {
  111. const location = useLocation();
  112. const target = useMemo(
  113. () => newExploreTarget(location, suggestedQuery),
  114. [location, suggestedQuery]
  115. );
  116. return (
  117. <Tag to={target} icon={null} type="info">
  118. {suggestedQuery.title}
  119. </Tag>
  120. );
  121. }
  122. function getSelectedProjectsList(
  123. selectedProjects: PageFilters['projects'],
  124. projects: Project[]
  125. ): Project[] {
  126. if (
  127. selectedProjects[0] === ALL_ACCESS_PROJECTS || // all projects
  128. selectedProjects.length === 0 // my projects
  129. ) {
  130. return projects;
  131. }
  132. const projectIds = new Set(selectedProjects.map(String));
  133. return projects.filter(project => projectIds.has(project.id));
  134. }
  135. function getSuggestedQueries(platforms: PlatformCategory[], maxQueries = 5) {
  136. const frontendQueries: SuggestedQuery[] = [
  137. {
  138. title: t('Worst LCPs'),
  139. fields: [
  140. 'id',
  141. 'project',
  142. 'span.op',
  143. 'span.description',
  144. 'span.duration',
  145. 'measurements.lcp',
  146. ],
  147. groupBys: ['span.description'],
  148. mode: Mode.AGGREGATE,
  149. query: 'span.op:[pageload,navigation]',
  150. sortBys: [{field: 'avg(measurements.lcp)', kind: 'desc'}],
  151. visualizes: [
  152. {chartType: ChartType.LINE, yAxes: ['p50(measurements.lcp)']},
  153. {chartType: ChartType.LINE, yAxes: ['avg(measurements.lcp)']},
  154. ],
  155. },
  156. {
  157. title: t('Biggest Assets'),
  158. fields: [
  159. 'id',
  160. 'project',
  161. 'span.op',
  162. 'span.description',
  163. 'span.duration',
  164. 'http.response_transfer_size',
  165. 'timestamp',
  166. ],
  167. groupBys: ['span.description'],
  168. mode: Mode.AGGREGATE,
  169. query: 'span.op:[resource.css,resource.img,resource.script]',
  170. sortBys: [{field: 'p75(http.response_transfer_size)', kind: 'desc'}],
  171. visualizes: [
  172. {chartType: ChartType.LINE, yAxes: ['p75(http.response_transfer_size)']},
  173. {chartType: ChartType.LINE, yAxes: ['p90(http.response_transfer_size)']},
  174. ],
  175. },
  176. {
  177. title: t('Top Pageloads'),
  178. fields: [
  179. 'id',
  180. 'project',
  181. 'span.op',
  182. 'span.description',
  183. 'span.duration',
  184. 'timestamp',
  185. ],
  186. groupBys: ['span.description'],
  187. mode: Mode.AGGREGATE,
  188. query: 'span.op:[pageload,navigation]',
  189. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  190. visualizes: [
  191. {chartType: ChartType.LINE, yAxes: ['avg(span.duration)']},
  192. {chartType: ChartType.LINE, yAxes: ['p50(span.duration)']},
  193. ],
  194. },
  195. ];
  196. const backendQueries: SuggestedQuery[] = [
  197. {
  198. title: t('Slowest Server Calls'),
  199. fields: [
  200. 'id',
  201. 'project',
  202. 'span.op',
  203. 'span.description',
  204. 'span.duration',
  205. 'timestamp',
  206. ],
  207. groupBys: ['span.description'],
  208. mode: Mode.AGGREGATE,
  209. query: 'span.op:http.server',
  210. sortBys: [{field: 'p75(span.duration)', kind: 'desc'}],
  211. visualizes: [
  212. {chartType: ChartType.LINE, yAxes: ['p75(span.duration)']},
  213. {chartType: ChartType.LINE, yAxes: ['p90(span.duration)']},
  214. ],
  215. },
  216. ];
  217. const mobileQueries: SuggestedQuery[] = [
  218. {
  219. title: t('Top Screenloads'),
  220. fields: [
  221. 'id',
  222. 'project',
  223. 'span.op',
  224. 'span.description',
  225. 'span.duration',
  226. 'timestamp',
  227. ],
  228. groupBys: ['span.description'],
  229. mode: Mode.AGGREGATE,
  230. query: 'span.op:ui.load',
  231. sortBys: [{field: 'count(span.duration)', kind: 'desc'}],
  232. visualizes: [{chartType: ChartType.LINE, yAxes: ['count(span.duration)']}],
  233. },
  234. ];
  235. const allQueries: Partial<Record<PlatformCategory, SuggestedQuery[]>> = {
  236. [PlatformCategory.FRONTEND]: frontendQueries,
  237. [PlatformCategory.BACKEND]: backendQueries,
  238. [PlatformCategory.MOBILE]: mobileQueries,
  239. };
  240. const genericQueries: SuggestedQuery[] = [
  241. {
  242. title: t('Slowest Ops'),
  243. fields: [
  244. 'id',
  245. 'project',
  246. 'span.op',
  247. 'span.description',
  248. 'span.duration',
  249. 'timestamp',
  250. ],
  251. groupBys: ['span.op'],
  252. mode: Mode.AGGREGATE,
  253. query: '',
  254. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  255. visualizes: [
  256. {chartType: ChartType.LINE, yAxes: ['avg(span.duration)']},
  257. {chartType: ChartType.LINE, yAxes: ['p50(span.duration)']},
  258. ],
  259. },
  260. {
  261. title: t('Database Latency'),
  262. fields: [
  263. 'id',
  264. 'project',
  265. 'span.op',
  266. 'span.description',
  267. 'span.duration',
  268. 'db.system',
  269. ],
  270. groupBys: ['span.op', 'db.system'],
  271. mode: Mode.AGGREGATE,
  272. query: 'span.op:db',
  273. sortBys: [{field: 'avg(span.duration)', kind: 'desc'}],
  274. visualizes: [
  275. {chartType: ChartType.LINE, yAxes: ['avg(span.duration)']},
  276. {chartType: ChartType.LINE, yAxes: ['p50(span.duration)']},
  277. ],
  278. },
  279. ];
  280. const queries: SuggestedQuery[] = [];
  281. for (const platform of platforms) {
  282. for (const query of allQueries[platform] || []) {
  283. queries.push(query);
  284. if (queries.length >= maxQueries) {
  285. return queries;
  286. }
  287. }
  288. }
  289. for (const query of genericQueries) {
  290. queries.push(query);
  291. if (queries.length >= maxQueries) {
  292. return queries;
  293. }
  294. }
  295. return queries;
  296. }
  297. const StyledPanel = styled(Panel)`
  298. padding: ${space(2)};
  299. background: linear-gradient(
  300. 269.35deg,
  301. ${p => p.theme.backgroundTertiary} 0.32%,
  302. rgba(245, 243, 247, 0) 99.69%
  303. );
  304. `;
  305. const SuggestedQueriesContainer = styled('div')`
  306. display: flex;
  307. flex-wrap: wrap;
  308. gap: ${space(1)};
  309. margin-top: ${space(2)};
  310. `;