useAnalytics.tsx 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  1. import {useEffect} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import {defined} from 'sentry/utils';
  4. import {trackAnalytics} from 'sentry/utils/analytics';
  5. import {dedupeArray} from 'sentry/utils/dedupeArray';
  6. import {DiscoverDatasets} from 'sentry/utils/discover/types';
  7. import {MutableSearch} from 'sentry/utils/tokenizeSearch';
  8. import useOrganization from 'sentry/utils/useOrganization';
  9. import {
  10. useExploreDataset,
  11. useExploreFields,
  12. useExploreQuery,
  13. useExploreTitle,
  14. useExploreVisualizes,
  15. } from 'sentry/views/explore/contexts/pageParamsContext';
  16. import type {Visualize} from 'sentry/views/explore/contexts/pageParamsContext/visualizes';
  17. import type {AggregatesTableResult} from 'sentry/views/explore/hooks/useExploreAggregatesTable';
  18. import type {SpansTableResult} from 'sentry/views/explore/hooks/useExploreSpansTable';
  19. import type {TracesTableResult} from 'sentry/views/explore/hooks/useExploreTracesTable';
  20. import type {ReadableExploreQueryParts} from 'sentry/views/explore/multiQueryMode/locationUtils';
  21. import {combineConfidenceForSeries} from 'sentry/views/explore/utils';
  22. import type {useSortedTimeSeries} from 'sentry/views/insights/common/queries/useSortedTimeSeries';
  23. import {usePerformanceSubscriptionDetails} from 'sentry/views/performance/newTraceDetails/traceTypeWarnings/usePerformanceSubscriptionDetails';
  24. const {info, fmt} = Sentry._experiment_log;
  25. export function useTrackAnalytics({
  26. queryType,
  27. aggregatesTableResult,
  28. spansTableResult,
  29. tracesTableResult,
  30. timeseriesResult,
  31. dataset,
  32. title,
  33. query,
  34. fields,
  35. visualizes,
  36. page_source,
  37. }: {
  38. aggregatesTableResult: AggregatesTableResult;
  39. dataset: DiscoverDatasets;
  40. fields: string[];
  41. page_source: 'explore' | 'compare';
  42. query: string;
  43. queryType: 'aggregate' | 'samples' | 'traces';
  44. spansTableResult: SpansTableResult;
  45. timeseriesResult: ReturnType<typeof useSortedTimeSeries>;
  46. visualizes: Visualize[];
  47. title?: string;
  48. tracesTableResult?: TracesTableResult;
  49. }) {
  50. const organization = useOrganization();
  51. const {
  52. data: {hasExceededPerformanceUsageLimit},
  53. isLoading: isLoadingSubscriptionDetails,
  54. } = usePerformanceSubscriptionDetails();
  55. const tableError =
  56. queryType === 'aggregate'
  57. ? (aggregatesTableResult.result.error?.message ?? '')
  58. : queryType === 'traces'
  59. ? (tracesTableResult?.result.error?.message ?? '')
  60. : (spansTableResult.result.error?.message ?? '');
  61. const chartError = timeseriesResult.error?.message ?? '';
  62. const query_status = tableError || chartError ? 'error' : 'success';
  63. useEffect(() => {
  64. if (
  65. queryType !== 'aggregate' ||
  66. aggregatesTableResult.result.isPending ||
  67. timeseriesResult.isPending ||
  68. isLoadingSubscriptionDetails
  69. ) {
  70. return;
  71. }
  72. const search = new MutableSearch(query);
  73. const columns = aggregatesTableResult.eventView.getColumns() as unknown as string[];
  74. trackAnalytics('trace.explorer.metadata', {
  75. organization,
  76. dataset,
  77. result_mode: 'aggregates',
  78. columns,
  79. columns_count: columns.length,
  80. query_status,
  81. result_length: aggregatesTableResult.result.data?.length || 0,
  82. result_missing_root: 0,
  83. user_queries: search.formatString(),
  84. user_queries_count: search.tokens.length,
  85. visualizes: visualizes.map(visualize => ({
  86. chartType: visualize.chartType,
  87. yAxes: visualize.yAxes,
  88. })),
  89. visualizes_count: visualizes.length,
  90. title: title || '',
  91. confidences: computeConfidence(visualizes, timeseriesResult.data),
  92. has_exceeded_performance_usage_limit: hasExceededPerformanceUsageLimit,
  93. page_source,
  94. });
  95. info(
  96. fmt`trace.explorer.metadata:
  97. organization: ${organization.slug}
  98. dataset: ${dataset}
  99. query: [omitted]
  100. visualizes: ${visualizes.map(v => v.chartType).join(', ')}
  101. title: ${title || ''}
  102. queryType: ${queryType}
  103. result_length: ${String(aggregatesTableResult.result.data?.length || 0)}
  104. user_queries: ${search.formatString()}
  105. user_queries_count: ${String(String(search.tokens).length)}
  106. visualizes_count: ${String(String(visualizes).length)}
  107. has_exceeded_performance_usage_limit: ${String(hasExceededPerformanceUsageLimit)}
  108. page_source: ${page_source}
  109. `,
  110. {isAnalytics: true}
  111. );
  112. }, [
  113. organization,
  114. dataset,
  115. fields,
  116. query,
  117. visualizes,
  118. title,
  119. queryType,
  120. aggregatesTableResult.result.isPending,
  121. aggregatesTableResult.result.status,
  122. aggregatesTableResult.result.data?.length,
  123. aggregatesTableResult.eventView,
  124. timeseriesResult.isPending,
  125. timeseriesResult.data,
  126. hasExceededPerformanceUsageLimit,
  127. isLoadingSubscriptionDetails,
  128. query_status,
  129. page_source,
  130. ]);
  131. useEffect(() => {
  132. if (
  133. queryType !== 'samples' ||
  134. spansTableResult.result.isPending ||
  135. timeseriesResult.isPending ||
  136. isLoadingSubscriptionDetails
  137. ) {
  138. return;
  139. }
  140. const search = new MutableSearch(query);
  141. trackAnalytics('trace.explorer.metadata', {
  142. organization,
  143. dataset,
  144. result_mode: 'span samples',
  145. columns: fields,
  146. columns_count: fields.length,
  147. query_status,
  148. result_length: spansTableResult.result.data?.length || 0,
  149. result_missing_root: 0,
  150. user_queries: search.formatString(),
  151. user_queries_count: search.tokens.length,
  152. visualizes,
  153. visualizes_count: visualizes.length,
  154. title: title || '',
  155. confidences: computeConfidence(visualizes, timeseriesResult.data),
  156. has_exceeded_performance_usage_limit: hasExceededPerformanceUsageLimit,
  157. page_source,
  158. });
  159. info(fmt`trace.explorer.metadata:
  160. organization: ${organization.slug}
  161. dataset: ${dataset}
  162. query: ${query}
  163. visualizes: ${visualizes.map(v => v.chartType).join(', ')}
  164. title: ${title || ''}
  165. queryType: ${queryType}
  166. result_length: ${String(spansTableResult.result.data?.length || 0)}
  167. user_queries: ${search.formatString()}
  168. user_queries_count: ${String(search.tokens.length)}
  169. visualizes_count: ${String(visualizes.length)}
  170. has_exceeded_performance_usage_limit: ${String(hasExceededPerformanceUsageLimit)}
  171. page_source: ${page_source}
  172. `);
  173. }, [
  174. organization,
  175. dataset,
  176. fields,
  177. query,
  178. visualizes,
  179. title,
  180. queryType,
  181. spansTableResult.result.isPending,
  182. spansTableResult.result.status,
  183. spansTableResult.result.data?.length,
  184. timeseriesResult.isPending,
  185. timeseriesResult.data,
  186. hasExceededPerformanceUsageLimit,
  187. isLoadingSubscriptionDetails,
  188. query_status,
  189. page_source,
  190. ]);
  191. const tracesTableResultDefined = defined(tracesTableResult);
  192. useEffect(() => {
  193. if (
  194. !tracesTableResultDefined ||
  195. queryType !== 'traces' ||
  196. tracesTableResult.result.isPending ||
  197. timeseriesResult.isPending ||
  198. isLoadingSubscriptionDetails
  199. ) {
  200. return;
  201. }
  202. const search = new MutableSearch(query);
  203. const columns = [
  204. 'trace id',
  205. 'trace root',
  206. 'total spans',
  207. 'timeline',
  208. 'root duration',
  209. 'timestamp',
  210. ];
  211. const resultMissingRoot =
  212. tracesTableResult?.result?.data?.data?.filter(trace => !defined(trace.name))
  213. .length ?? 0;
  214. trackAnalytics('trace.explorer.metadata', {
  215. organization,
  216. dataset,
  217. result_mode: 'trace samples',
  218. columns,
  219. columns_count: columns.length,
  220. query_status,
  221. result_length: tracesTableResult.result.data?.data?.length || 0,
  222. result_missing_root: resultMissingRoot,
  223. user_queries: search.formatString(),
  224. user_queries_count: search.tokens.length,
  225. visualizes,
  226. visualizes_count: visualizes.length,
  227. title: title || '',
  228. confidences: computeConfidence(visualizes, timeseriesResult.data),
  229. has_exceeded_performance_usage_limit: hasExceededPerformanceUsageLimit,
  230. page_source,
  231. });
  232. }, [
  233. organization,
  234. dataset,
  235. fields,
  236. query,
  237. visualizes,
  238. title,
  239. queryType,
  240. tracesTableResult?.result.isPending,
  241. tracesTableResult?.result.status,
  242. tracesTableResult?.result.data?.data,
  243. timeseriesResult.isPending,
  244. timeseriesResult.data,
  245. hasExceededPerformanceUsageLimit,
  246. isLoadingSubscriptionDetails,
  247. query_status,
  248. page_source,
  249. tracesTableResultDefined,
  250. ]);
  251. }
  252. export function useAnalytics({
  253. queryType,
  254. aggregatesTableResult,
  255. spansTableResult,
  256. tracesTableResult,
  257. timeseriesResult,
  258. }: {
  259. aggregatesTableResult: AggregatesTableResult;
  260. queryType: 'aggregate' | 'samples' | 'traces';
  261. spansTableResult: SpansTableResult;
  262. timeseriesResult: ReturnType<typeof useSortedTimeSeries>;
  263. tracesTableResult: TracesTableResult;
  264. }) {
  265. const dataset = useExploreDataset();
  266. const title = useExploreTitle();
  267. const query = useExploreQuery();
  268. const fields = useExploreFields();
  269. const visualizes = useExploreVisualizes();
  270. return useTrackAnalytics({
  271. queryType,
  272. aggregatesTableResult,
  273. spansTableResult,
  274. tracesTableResult,
  275. timeseriesResult,
  276. dataset,
  277. title,
  278. query,
  279. fields,
  280. visualizes,
  281. page_source: 'explore',
  282. });
  283. }
  284. export function useCompareAnalytics({
  285. query: queryParts,
  286. index,
  287. queryType,
  288. aggregatesTableResult,
  289. spansTableResult,
  290. timeseriesResult,
  291. }: {
  292. aggregatesTableResult: AggregatesTableResult;
  293. index: number;
  294. query: ReadableExploreQueryParts;
  295. queryType: 'aggregate' | 'samples' | 'traces';
  296. spansTableResult: SpansTableResult;
  297. timeseriesResult: ReturnType<typeof useSortedTimeSeries>;
  298. }) {
  299. const dataset = DiscoverDatasets.SPANS_EAP_RPC;
  300. const query = queryParts.query;
  301. const fields = queryParts.fields;
  302. const visualizes = queryParts.yAxes.map(yAxis => {
  303. return {
  304. chartType: queryParts.chartType,
  305. yAxes: [yAxis],
  306. label: String(index),
  307. } as Visualize;
  308. });
  309. return useTrackAnalytics({
  310. queryType,
  311. aggregatesTableResult,
  312. spansTableResult,
  313. timeseriesResult,
  314. dataset,
  315. query,
  316. fields,
  317. visualizes,
  318. page_source: 'compare',
  319. });
  320. }
  321. function computeConfidence(
  322. visualizes: Visualize[],
  323. data: ReturnType<typeof useSortedTimeSeries>['data']
  324. ) {
  325. return visualizes.map(visualize => {
  326. const dedupedYAxes = dedupeArray(visualize.yAxes);
  327. const series = dedupedYAxes.flatMap(yAxis => data[yAxis]).filter(defined);
  328. return String(combineConfidenceForSeries(series));
  329. });
  330. }