useTraceMeta.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import {useMemo} from 'react';
  2. import type {Location} from 'history';
  3. import * as qs from 'query-string';
  4. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  5. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  6. import type {PageFilters} from 'sentry/types/core';
  7. import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
  8. import {type ApiQueryKey, useApiQueries} from 'sentry/utils/queryClient';
  9. import {decodeScalar} from 'sentry/utils/queryString';
  10. import useOrganization from 'sentry/utils/useOrganization';
  11. import usePageFilters from 'sentry/utils/usePageFilters';
  12. function getMetaQueryParams(
  13. query: Location['query'],
  14. filters: Partial<PageFilters> = {}
  15. ):
  16. | {
  17. // demo has the format ${projectSlug}:${eventId}
  18. // used to query a demo transaction event from the backend.
  19. demo: string | undefined;
  20. statsPeriod: string;
  21. }
  22. | {
  23. demo: string | undefined;
  24. timestamp: string;
  25. } {
  26. const normalizedParams = normalizeDateTimeParams(query, {
  27. allowAbsolutePageDatetime: true,
  28. });
  29. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  30. const timestamp = decodeScalar(normalizedParams.timestamp);
  31. if (timestamp) {
  32. return {timestamp, demo: decodeScalar(normalizedParams.demo)};
  33. }
  34. return {
  35. statsPeriod: (statsPeriod || filters?.datetime?.period) ?? DEFAULT_STATS_PERIOD,
  36. demo: decodeScalar(normalizedParams.demo),
  37. };
  38. }
  39. function getTraceMetaQueryKey(
  40. traceSlug: string,
  41. orgSlug: string,
  42. queryParams:
  43. | {
  44. // demo has the format ${projectSlug}:${eventId}
  45. // used to query a demo transaction event from the backend.
  46. demo: string | undefined;
  47. statsPeriod: string;
  48. }
  49. | {
  50. demo: string | undefined;
  51. timestamp: string;
  52. }
  53. ): ApiQueryKey {
  54. return [
  55. `/organizations/${orgSlug}/events-trace-meta/${traceSlug}/`,
  56. {query: queryParams},
  57. ];
  58. }
  59. export type TraceMetaQueryResults = {
  60. data: TraceMeta;
  61. isLoading: boolean;
  62. isRefetching: boolean;
  63. refetch: () => void;
  64. };
  65. export function useTraceMeta(traceSlugs: string[]): {
  66. data: TraceMeta;
  67. isLoading: boolean;
  68. isRefetching: boolean;
  69. refetch: () => void;
  70. } {
  71. const filters = usePageFilters();
  72. const organization = useOrganization();
  73. const queryParams = useMemo(() => {
  74. const query = qs.parse(location.search);
  75. return getMetaQueryParams(query, filters.selection);
  76. // eslint-disable-next-line react-hooks/exhaustive-deps
  77. }, []);
  78. const mode = queryParams.demo ? 'demo' : undefined;
  79. const queryKeys = useMemo(() => {
  80. return traceSlugs.map(traceSlug =>
  81. getTraceMetaQueryKey(traceSlug, organization.slug, queryParams)
  82. );
  83. }, [traceSlugs, organization, queryParams]);
  84. const results = useApiQueries<TraceMeta>(queryKeys, {
  85. enabled: traceSlugs.length > 0 && !!organization.slug && mode !== 'demo',
  86. staleTime: Infinity,
  87. });
  88. const mergedTraceMeta = useMemo(() => {
  89. const mergedResult: {
  90. data: TraceMeta;
  91. isLoading: boolean;
  92. isRefetching: boolean;
  93. refetch: () => void;
  94. } = {
  95. data: {
  96. errors: 0,
  97. performance_issues: 0,
  98. projects: 0,
  99. transactions: 0,
  100. },
  101. isLoading: false,
  102. isRefetching: false,
  103. refetch: () => {
  104. results.forEach(result => result.refetch());
  105. },
  106. };
  107. for (const metaResult of results) {
  108. mergedResult.isLoading ||= metaResult.isLoading;
  109. mergedResult.isRefetching ||= metaResult.isRefetching;
  110. mergedResult.data.errors += metaResult.data?.errors ?? 0;
  111. mergedResult.data.performance_issues += metaResult.data?.performance_issues ?? 0;
  112. // TODO: We want the count of unique projects, not the sum of all projects. When there are multiple traces, taking the best guess by using the max
  113. // project count accross all traces for now. We don't use the project count for anything yet, so this is fine for now.
  114. mergedResult.data.projects += Math.max(
  115. metaResult.data?.projects ?? 0,
  116. mergedResult.data.projects
  117. );
  118. mergedResult.data.transactions += metaResult.data?.transactions ?? 0;
  119. }
  120. return mergedResult;
  121. }, [results]);
  122. // When projects don't have performance set up, we allow them to view a sample transaction.
  123. // The backend creates the sample transaction, however the trace is created async, so when the
  124. // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
  125. // When this happens, we assemble a fake trace response to only include the transaction that had already been
  126. // created and stored already so that the users can visualize in the context of a trace.
  127. // The trace meta query has to reflect this by returning a single transaction and project.
  128. if (mode === 'demo') {
  129. return {
  130. data: {
  131. errors: 0,
  132. performance_issues: 0,
  133. projects: 1,
  134. transactions: 1,
  135. },
  136. isLoading: false,
  137. isRefetching: false,
  138. refetch: () => {},
  139. };
  140. }
  141. return mergedTraceMeta;
  142. }