useTraceMeta.tsx 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172
  1. import {useMemo} from 'react';
  2. import {useQuery} from '@tanstack/react-query';
  3. import type {Location} from 'history';
  4. import * as qs from 'query-string';
  5. import type {Client} from 'sentry/api';
  6. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  7. import {DEFAULT_STATS_PERIOD} from 'sentry/constants';
  8. import type {PageFilters} from 'sentry/types/core';
  9. import type {Organization} from 'sentry/types/organization';
  10. import type {TraceMeta} from 'sentry/utils/performance/quickTrace/types';
  11. import {decodeScalar} from 'sentry/utils/queryString';
  12. import useApi from 'sentry/utils/useApi';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import usePageFilters from 'sentry/utils/usePageFilters';
  15. type TraceMetaQueryParams =
  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. function getMetaQueryParams(
  27. query: Location['query'],
  28. filters: Partial<PageFilters> = {}
  29. ): TraceMetaQueryParams {
  30. const normalizedParams = normalizeDateTimeParams(query, {
  31. allowAbsolutePageDatetime: true,
  32. });
  33. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  34. const timestamp = decodeScalar(normalizedParams.timestamp);
  35. if (timestamp) {
  36. return {timestamp, demo: decodeScalar(normalizedParams.demo)};
  37. }
  38. return {
  39. statsPeriod: (statsPeriod || filters?.datetime?.period) ?? DEFAULT_STATS_PERIOD,
  40. demo: decodeScalar(normalizedParams.demo),
  41. };
  42. }
  43. async function fetchSingleTraceMetaNew(
  44. api: Client,
  45. organization: Organization,
  46. traceSlug: string,
  47. queryParams: any
  48. ) {
  49. const data = await api.requestPromise(
  50. `/organizations/${organization.slug}/events-trace-meta/${traceSlug}/`,
  51. {
  52. method: 'GET',
  53. data: queryParams,
  54. }
  55. );
  56. return data;
  57. }
  58. async function fetchTraceMetaInBatches(
  59. traceIds: string[],
  60. api: Client,
  61. organization: Organization,
  62. queryParams: TraceMetaQueryParams
  63. ) {
  64. const clonedTraceIds = [...traceIds];
  65. const metaResults: TraceMeta = {
  66. errors: 0,
  67. performance_issues: 0,
  68. projects: 0,
  69. transactions: 0,
  70. };
  71. const apiErrors: Error[] = [];
  72. while (clonedTraceIds.length > 0) {
  73. const batch = clonedTraceIds.splice(0, 3);
  74. const results = await Promise.allSettled(
  75. batch.map(slug => {
  76. return fetchSingleTraceMetaNew(api, organization, slug, queryParams);
  77. })
  78. );
  79. const updatedData = results.reduce(
  80. (acc, result) => {
  81. if (result.status === 'fulfilled') {
  82. const {errors, performance_issues, projects, transactions} = result.value;
  83. acc.errors += errors;
  84. acc.performance_issues += performance_issues;
  85. acc.projects = Math.max(acc.projects, projects);
  86. acc.transactions += transactions;
  87. } else {
  88. apiErrors.push(new Error(result.reason));
  89. }
  90. return acc;
  91. },
  92. {...metaResults}
  93. );
  94. metaResults.errors = updatedData.errors;
  95. metaResults.performance_issues = updatedData.performance_issues;
  96. metaResults.projects = Math.max(updatedData.projects, metaResults.projects);
  97. metaResults.transactions = updatedData.transactions;
  98. }
  99. return {metaResults, apiErrors};
  100. }
  101. export type TraceMetaQueryResults = {
  102. data: TraceMeta | undefined;
  103. errors: Error[];
  104. isLoading: boolean;
  105. };
  106. export function useTraceMeta(traceSlugs: string[]): TraceMetaQueryResults {
  107. const filters = usePageFilters();
  108. const api = useApi();
  109. const organization = useOrganization();
  110. const queryParams = useMemo(() => {
  111. const query = qs.parse(location.search);
  112. return getMetaQueryParams(query, filters.selection);
  113. // eslint-disable-next-line react-hooks/exhaustive-deps
  114. }, []);
  115. const mode = queryParams.demo ? 'demo' : undefined;
  116. const {data, isLoading} = useQuery<
  117. {
  118. apiErrors: Error[];
  119. metaResults: TraceMeta;
  120. },
  121. Error
  122. >(
  123. ['traceData', traceSlugs],
  124. () => fetchTraceMetaInBatches(traceSlugs, api, organization, queryParams),
  125. {
  126. enabled: traceSlugs.length > 0,
  127. }
  128. );
  129. // When projects don't have performance set up, we allow them to view a sample transaction.
  130. // The backend creates the sample transaction, however the trace is created async, so when the
  131. // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
  132. // When this happens, we assemble a fake trace response to only include the transaction that had already been
  133. // created and stored already so that the users can visualize in the context of a trace.
  134. // The trace meta query has to reflect this by returning a single transaction and project.
  135. if (mode === 'demo') {
  136. return {
  137. data: {
  138. errors: 0,
  139. performance_issues: 0,
  140. projects: 1,
  141. transactions: 1,
  142. },
  143. isLoading: false,
  144. errors: [],
  145. };
  146. }
  147. return {
  148. data: data?.metaResults,
  149. isLoading,
  150. errors: data?.apiErrors || [],
  151. };
  152. }