useTrace.tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218
  1. import {useMemo} from 'react';
  2. import type {Location} from 'history';
  3. import * as qs from 'query-string';
  4. import type {Client} from 'sentry/api';
  5. import {normalizeDateTimeParams} from 'sentry/components/organizations/pageFilters/parse';
  6. import type {EventTransaction, PageFilters} from 'sentry/types';
  7. import type {
  8. TraceFullDetailed,
  9. TraceSplitResults,
  10. } from 'sentry/utils/performance/quickTrace/types';
  11. import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
  12. import {decodeScalar} from 'sentry/utils/queryString';
  13. import useOrganization from 'sentry/utils/useOrganization';
  14. import usePageFilters from 'sentry/utils/usePageFilters';
  15. import {useParams} from 'sentry/utils/useParams';
  16. export function fetchTrace(
  17. api: Client,
  18. params: {
  19. orgSlug: string;
  20. query: string;
  21. traceId: string;
  22. }
  23. ): Promise<TraceSplitResults<TraceFullDetailed>> {
  24. return api.requestPromise(
  25. `/organizations/${params.orgSlug}/events-trace/${params.traceId}/?${params.query}`
  26. );
  27. }
  28. const DEFAULT_TIMESTAMP_LIMIT = 10_000;
  29. const DEFAULT_LIMIT = 1_000;
  30. export function getTraceQueryParams(
  31. query: Location['query'],
  32. filters: Partial<PageFilters> = {},
  33. options: {limit?: number} = {}
  34. ): {
  35. eventId: string | undefined;
  36. limit: number;
  37. timestamp: string | undefined;
  38. useSpans: number;
  39. demo?: string | undefined;
  40. pageEnd?: string | undefined;
  41. pageStart?: string | undefined;
  42. statsPeriod?: string | undefined;
  43. } {
  44. const normalizedParams = normalizeDateTimeParams(query, {
  45. allowAbsolutePageDatetime: true,
  46. });
  47. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  48. const demo = decodeScalar(normalizedParams.demo);
  49. const timestamp = decodeScalar(normalizedParams.timestamp);
  50. let decodedLimit: string | number | undefined =
  51. options.limit ?? decodeScalar(normalizedParams.limit);
  52. if (typeof decodedLimit === 'string') {
  53. decodedLimit = parseInt(decodedLimit, 10);
  54. }
  55. const eventId = decodeScalar(normalizedParams.eventId);
  56. if (timestamp) {
  57. decodedLimit = decodedLimit ?? DEFAULT_TIMESTAMP_LIMIT;
  58. } else {
  59. decodedLimit = decodedLimit ?? DEFAULT_LIMIT;
  60. }
  61. const limit = decodedLimit;
  62. const otherParams: Record<string, string | string[] | undefined | null> = {
  63. end: normalizedParams.pageEnd,
  64. start: normalizedParams.pageStart,
  65. statsPeriod: statsPeriod || filters.datetime?.period,
  66. };
  67. // We prioritize timestamp over statsPeriod as it makes the query more specific, faster
  68. // and not prone to time drift issues.
  69. if (timestamp) {
  70. delete otherParams.statsPeriod;
  71. }
  72. const queryParams = {
  73. ...otherParams,
  74. demo,
  75. limit,
  76. timestamp,
  77. eventId,
  78. useSpans: 1,
  79. };
  80. for (const key in queryParams) {
  81. if (
  82. queryParams[key] === '' ||
  83. queryParams[key] === null ||
  84. queryParams[key] === undefined
  85. ) {
  86. delete queryParams[key];
  87. }
  88. }
  89. return queryParams;
  90. }
  91. function parseDemoEventSlug(
  92. demoEventSlug: string | undefined
  93. ): {event_id: string; project_slug: string} | null {
  94. if (!demoEventSlug) {
  95. return null;
  96. }
  97. const [project_slug, event_id] = demoEventSlug.split(':');
  98. return {project_slug, event_id};
  99. }
  100. function makeTraceFromTransaction(
  101. event: EventTransaction | undefined
  102. ): TraceSplitResults<TraceFullDetailed> | undefined {
  103. if (!event) {
  104. return undefined;
  105. }
  106. const traceContext = event.contexts?.trace;
  107. const transaction = {
  108. event_id: event.eventID,
  109. generation: 0,
  110. parent_event_id: '',
  111. parent_span_id: traceContext?.parent_span_id ?? '',
  112. performance_issues: [],
  113. project_id: Number(event.projectID),
  114. project_slug: event.projectSlug ?? '',
  115. span_id: traceContext?.span_id ?? '',
  116. timestamp: event.endTimestamp,
  117. transaction: event.title,
  118. 'transaction.duration': (event.endTimestamp - event.startTimestamp) * 1000,
  119. errors: [],
  120. children: [],
  121. start_timestamp: event.startTimestamp,
  122. 'transaction.op': traceContext?.op ?? '',
  123. 'transaction.status': traceContext?.status ?? '',
  124. measurements: event.measurements ?? {},
  125. tags: [],
  126. };
  127. return {transactions: [transaction], orphan_errors: []};
  128. }
  129. function useDemoTrace(
  130. demo: string | undefined,
  131. organization: {slug: string}
  132. ): UseApiQueryResult<TraceSplitResults<TraceFullDetailed> | undefined, any> {
  133. const demoEventSlug = parseDemoEventSlug(demo);
  134. // When projects don't have performance set up, we allow them to view a demo transaction.
  135. // The backend creates the demo transaction, however the trace is created async, so when the
  136. // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
  137. // When this happens, we assemble a fake trace response to only include the transaction that had already been
  138. // created and stored already so that the users can visualize in the context of a trace.
  139. const demoEventQuery = useApiQuery<EventTransaction>(
  140. [
  141. `/organizations/${organization.slug}/events/${demoEventSlug?.project_slug}:${demoEventSlug?.event_id}/`,
  142. {
  143. query: {
  144. referrer: 'trace-view',
  145. },
  146. },
  147. ],
  148. {
  149. staleTime: Infinity,
  150. enabled: !!demoEventSlug,
  151. }
  152. );
  153. // Without the useMemo, the trace from the transformed response will be re-created on every render,
  154. // causing the trace view to re-render as we interact with it.
  155. const data = useMemo(() => {
  156. return makeTraceFromTransaction(demoEventQuery.data);
  157. }, [demoEventQuery.data]);
  158. // Casting here since the 'select' option is not available in the useApiQuery hook to transform the data
  159. // from EventTransaction to TraceSplitResults<TraceFullDetailed>
  160. return {...demoEventQuery, data} as UseApiQueryResult<
  161. TraceSplitResults<TraceFullDetailed> | undefined,
  162. any
  163. >;
  164. }
  165. type UseTraceParams = {
  166. limit?: number;
  167. };
  168. const DEFAULT_OPTIONS = {};
  169. export function useTrace(
  170. options: Partial<UseTraceParams> = DEFAULT_OPTIONS
  171. ): UseApiQueryResult<TraceSplitResults<TraceFullDetailed> | undefined, any> {
  172. const filters = usePageFilters();
  173. const organization = useOrganization();
  174. const params = useParams<{traceSlug?: string}>();
  175. const queryParams = useMemo(() => {
  176. const query = qs.parse(location.search);
  177. return getTraceQueryParams(query, filters.selection, options);
  178. // eslint-disable-next-line react-hooks/exhaustive-deps
  179. }, [options]);
  180. const mode = queryParams.demo ? 'demo' : undefined;
  181. const demoTrace = useDemoTrace(queryParams.demo, organization);
  182. const traceQuery = useApiQuery<TraceSplitResults<TraceFullDetailed>>(
  183. [
  184. `/organizations/${organization.slug}/events-trace/${params.traceSlug ?? ''}/`,
  185. {query: queryParams},
  186. ],
  187. {
  188. staleTime: Infinity,
  189. enabled: !!params.traceSlug && !!organization.slug && mode !== 'demo',
  190. }
  191. );
  192. return mode === 'demo' ? demoTrace : traceQuery;
  193. }