content.tsx 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329
  1. import {Fragment, useMemo} from 'react';
  2. import {browserHistory} from 'react-router';
  3. import styled from '@emotion/styled';
  4. import type {Location} from 'history';
  5. import omit from 'lodash/omit';
  6. import MarkArea from 'sentry/components/charts/components/markArea';
  7. import MarkLine from 'sentry/components/charts/components/markLine';
  8. import type {LineChartSeries} from 'sentry/components/charts/lineChart';
  9. import SearchBar from 'sentry/components/events/searchBar';
  10. import * as Layout from 'sentry/components/layouts/thirds';
  11. import {DatePageFilter} from 'sentry/components/organizations/datePageFilter';
  12. import {EnvironmentPageFilter} from 'sentry/components/organizations/environmentPageFilter';
  13. import PageFilterBar from 'sentry/components/organizations/pageFilterBar';
  14. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  15. import {t} from 'sentry/locale';
  16. import {space} from 'sentry/styles/space';
  17. import type {Organization} from 'sentry/types';
  18. import {defined} from 'sentry/utils';
  19. import type EventView from 'sentry/utils/discover/eventView';
  20. import type {
  21. AnomalyInfo,
  22. AnomalyPayload,
  23. ChildrenProps,
  24. } from 'sentry/utils/performance/anomalies/anomaliesQuery';
  25. import AnomaliesQuery from 'sentry/utils/performance/anomalies/anomaliesQuery';
  26. import {decodeScalar} from 'sentry/utils/queryString';
  27. import theme from 'sentry/utils/theme';
  28. import {GenericPerformanceWidget} from '../../landing/widgets/components/performanceWidget';
  29. import {WidgetEmptyStateWarning} from '../../landing/widgets/components/selectableList';
  30. import type {QueryDefinition, WidgetDataResult} from '../../landing/widgets/types';
  31. import {
  32. PerformanceWidgetSetting,
  33. WIDGET_DEFINITIONS,
  34. } from '../../landing/widgets/widgetDefinitions';
  35. import type {SetStateAction} from '../types';
  36. import AnomaliesTable from './anomaliesTable';
  37. import {AnomalyChart} from './anomalyChart';
  38. type Props = {
  39. eventView: EventView;
  40. location: Location;
  41. organization: Organization;
  42. projectId: string;
  43. setError: SetStateAction<string | undefined>;
  44. transactionName: string;
  45. };
  46. type AnomaliesSectionProps = Props & {
  47. queryData: ChildrenProps;
  48. };
  49. const anomalyAreaName = (anomaly: AnomalyInfo) => `#${anomaly.id}`;
  50. const transformAnomalyToArea = (
  51. anomaly: AnomalyInfo
  52. ): [{name: string; xAxis: number}, {xAxis: number}] => [
  53. {name: anomalyAreaName(anomaly), xAxis: anomaly.start},
  54. {xAxis: anomaly.end},
  55. ];
  56. const transformAnomalyData = (
  57. _: any,
  58. results: {data: AnomalyPayload; error: null | string; isLoading: boolean}
  59. ) => {
  60. const data: LineChartSeries[] = [];
  61. const resultData = results.data;
  62. if (!resultData) {
  63. return {
  64. isLoading: results.isLoading,
  65. isErrored: !!results.error,
  66. data: undefined,
  67. hasData: false,
  68. loading: results.isLoading,
  69. };
  70. }
  71. data.push({
  72. seriesName: 'tpm()',
  73. data: resultData.y.data.map(([name, [{count}]]) => ({
  74. name,
  75. value: count,
  76. })),
  77. });
  78. data.push({
  79. seriesName: 'tpm() lower bound',
  80. data: resultData.yhat_lower.data.map(([name, [{count}]]) => ({
  81. name,
  82. value: count,
  83. })),
  84. });
  85. data.push({
  86. seriesName: 'tpm() upper bound',
  87. data: resultData.yhat_upper.data.map(([name, [{count}]]) => ({
  88. name,
  89. value: count,
  90. })),
  91. });
  92. const anomalies = results.data.anomalies;
  93. const highConfidenceAreas = anomalies
  94. .filter(a => a.confidence === 'high')
  95. .map(transformAnomalyToArea);
  96. const highConfidenceLines = anomalies
  97. .filter(a => a.confidence === 'high')
  98. .map(area => ({xAxis: area.start, name: anomalyAreaName(area)}));
  99. const lowConfidenceAreas = anomalies
  100. .filter(a => a.confidence === 'low')
  101. .map(transformAnomalyToArea);
  102. const lowConfidenceLines = anomalies
  103. .filter(a => a.confidence === 'low')
  104. .map(area => ({xAxis: area.start, name: anomalyAreaName(area)}));
  105. data.push({
  106. seriesName: 'High Confidence',
  107. color: theme.red300,
  108. data: [],
  109. silent: true,
  110. markLine: MarkLine({
  111. animation: false,
  112. lineStyle: {color: theme.red300, type: 'solid', width: 1, opacity: 1.0},
  113. data: highConfidenceLines,
  114. label: {
  115. show: true,
  116. rotate: 90,
  117. color: theme.red300,
  118. position: 'insideEndBottom',
  119. fontSize: '10',
  120. offset: [5, 5],
  121. formatter: obj => `${(obj.data as any).name}`,
  122. },
  123. }),
  124. markArea: MarkArea({
  125. itemStyle: {
  126. color: theme.red300,
  127. opacity: 0.2,
  128. },
  129. label: {
  130. show: false,
  131. },
  132. data: highConfidenceAreas,
  133. }),
  134. });
  135. data.push({
  136. seriesName: 'Low Confidence',
  137. color: theme.yellow200,
  138. data: [],
  139. markLine: MarkLine({
  140. animation: false,
  141. lineStyle: {color: theme.yellow200, type: 'solid', width: 1, opacity: 1.0},
  142. data: lowConfidenceLines,
  143. label: {
  144. show: true,
  145. rotate: 90,
  146. color: theme.yellow300,
  147. position: 'insideEndBottom',
  148. fontSize: '10',
  149. offset: [5, 5],
  150. formatter: obj => `${(obj.data as any).name}`,
  151. },
  152. }),
  153. markArea: MarkArea({
  154. itemStyle: {
  155. color: theme.yellow200,
  156. opacity: 0.2,
  157. },
  158. label: {
  159. show: false,
  160. },
  161. data: lowConfidenceAreas,
  162. }),
  163. });
  164. return {
  165. isLoading: results.isLoading,
  166. isErrored: !!results.error,
  167. data,
  168. hasData: true,
  169. loading: results.isLoading,
  170. };
  171. };
  172. type AnomalyData = WidgetDataResult & ReturnType<typeof transformAnomalyData>;
  173. type DataType = {
  174. chart: AnomalyData;
  175. };
  176. function Anomalies(props: AnomaliesSectionProps) {
  177. const height = 250;
  178. const chartColor = theme.charts.colors[0];
  179. const chart = useMemo<QueryDefinition<DataType, WidgetDataResult>>(() => {
  180. return {
  181. fields: '',
  182. component: provided => <Fragment>{provided.children(props.queryData)}</Fragment>,
  183. transform: transformAnomalyData,
  184. };
  185. }, [props.queryData]);
  186. return (
  187. <GenericPerformanceWidget<DataType>
  188. {...props}
  189. title={t('Transaction Count')}
  190. titleTooltip={t(
  191. 'Represents transaction count across time, with added visualizations to highlight anomalies in your data.'
  192. )}
  193. fields={['']}
  194. chartSetting={PerformanceWidgetSetting.TPM_AREA}
  195. chartDefinition={WIDGET_DEFINITIONS[PerformanceWidgetSetting.TPM_AREA]}
  196. Subtitle={() => <div />}
  197. HeaderActions={() => <div />}
  198. EmptyComponent={WidgetEmptyStateWarning}
  199. Queries={{
  200. chart,
  201. }}
  202. Visualizations={[
  203. {
  204. component: provided => {
  205. const data =
  206. provided.widgetData.chart.data?.map(series => {
  207. if (series.seriesName !== 'tpm()') {
  208. series.lineStyle = {type: 'dashed', color: chartColor, width: 1.5};
  209. }
  210. if (series.seriesName === 'score') {
  211. series.lineStyle = {color: theme.red400};
  212. }
  213. return series;
  214. }) ?? [];
  215. return (
  216. <AnomalyChart
  217. {...provided}
  218. data={data}
  219. height={height}
  220. statsPeriod={undefined}
  221. start={null}
  222. end={null}
  223. />
  224. );
  225. },
  226. height,
  227. },
  228. ]}
  229. />
  230. );
  231. }
  232. function AnomaliesContent(props: Props) {
  233. const {location, organization, eventView} = props;
  234. const query = decodeScalar(location.query.query, '');
  235. function handleChange(key: string) {
  236. return function (value: string | undefined) {
  237. const queryParams = normalizeDateTimeParams({
  238. ...(location.query || {}),
  239. [key]: value,
  240. });
  241. // do not propagate pagination when making a new search
  242. const toOmit = ['cursor'];
  243. if (!defined(value)) {
  244. toOmit.push(key);
  245. }
  246. const searchQueryParams = omit(queryParams, toOmit);
  247. browserHistory.push({
  248. ...location,
  249. query: searchQueryParams,
  250. });
  251. };
  252. }
  253. return (
  254. <Layout.Main fullWidth>
  255. <FilterActions>
  256. <PageFilterBar condensed>
  257. <EnvironmentPageFilter />
  258. <DatePageFilter />
  259. </PageFilterBar>
  260. <SearchBar
  261. organization={organization}
  262. projectIds={eventView.project}
  263. query={query}
  264. fields={eventView.fields}
  265. onSearch={handleChange('query')}
  266. />
  267. </FilterActions>
  268. <AnomaliesQuery
  269. organization={organization}
  270. location={location}
  271. eventView={eventView}
  272. >
  273. {queryData => (
  274. <Fragment>
  275. <AnomaliesWrapper>
  276. <Anomalies {...props} queryData={queryData} />
  277. </AnomaliesWrapper>
  278. <AnomaliesTable
  279. anomalies={queryData.data?.anomalies}
  280. {...props}
  281. isLoading={queryData.isLoading}
  282. />
  283. </Fragment>
  284. )}
  285. </AnomaliesQuery>
  286. </Layout.Main>
  287. );
  288. }
  289. const FilterActions = styled('div')`
  290. display: grid;
  291. gap: ${space(2)};
  292. margin-bottom: ${space(2)};
  293. @media (min-width: ${p => p.theme.breakpoints.small}) {
  294. grid-template-columns: auto 1fr;
  295. }
  296. `;
  297. const AnomaliesWrapper = styled('div')`
  298. margin-bottom: ${space(2)};
  299. `;
  300. export default AnomaliesContent;