useTrace.tsx 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221
  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 {PageFilters} from 'sentry/types/core';
  7. import type {EventTransaction} from 'sentry/types/event';
  8. import type {
  9. TraceFullDetailed,
  10. TraceSplitResults,
  11. } from 'sentry/utils/performance/quickTrace/types';
  12. import {useApiQuery, type UseApiQueryResult} from 'sentry/utils/queryClient';
  13. import {decodeScalar} from 'sentry/utils/queryString';
  14. import type RequestError from 'sentry/utils/requestError/requestError';
  15. import useOrganization from 'sentry/utils/useOrganization';
  16. import usePageFilters from 'sentry/utils/usePageFilters';
  17. import type {TraceTree} from '../traceModels/traceTree';
  18. export function fetchTrace(
  19. api: Client,
  20. params: {
  21. orgSlug: string;
  22. query: string;
  23. traceId: string;
  24. }
  25. ): Promise<TraceSplitResults<TraceFullDetailed>> {
  26. return api.requestPromise(
  27. `/organizations/${params.orgSlug}/events-trace/${params.traceId}/?${params.query}`
  28. );
  29. }
  30. const DEFAULT_TIMESTAMP_LIMIT = 10_000;
  31. const DEFAULT_LIMIT = 1_000;
  32. export function getTraceQueryParams(
  33. query: Location['query'],
  34. filters?: Partial<PageFilters>,
  35. options: {limit?: number; timestamp?: number} = {}
  36. ): {
  37. limit: number;
  38. targetId: string | undefined;
  39. timestamp: string | undefined;
  40. useSpans: number;
  41. demo?: string | undefined;
  42. pageEnd?: string | undefined;
  43. pageStart?: string | undefined;
  44. statsPeriod?: string | undefined;
  45. } {
  46. const normalizedParams = normalizeDateTimeParams(query, {
  47. allowAbsolutePageDatetime: true,
  48. });
  49. const statsPeriod = decodeScalar(normalizedParams.statsPeriod);
  50. const demo = decodeScalar(normalizedParams.demo);
  51. const timestamp = options.timestamp ?? decodeScalar(normalizedParams.timestamp);
  52. let limit = options.limit ?? decodeScalar(normalizedParams.limit);
  53. if (typeof limit === 'string') {
  54. limit = parseInt(limit, 10);
  55. }
  56. const targetId = decodeScalar(normalizedParams.targetId ?? normalizedParams.eventId);
  57. if (timestamp) {
  58. limit = limit ?? DEFAULT_TIMESTAMP_LIMIT;
  59. } else {
  60. limit = limit ?? DEFAULT_LIMIT;
  61. }
  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: timestamp?.toString(),
  77. targetId,
  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. sdk_name: event.sdk?.name ?? '',
  121. children: [],
  122. start_timestamp: event.startTimestamp,
  123. 'transaction.op': traceContext?.op ?? '',
  124. 'transaction.status': traceContext?.status ?? '',
  125. measurements: event.measurements ?? {},
  126. tags: [],
  127. };
  128. return {transactions: [transaction], orphan_errors: []};
  129. }
  130. function useDemoTrace(
  131. demo: string | undefined,
  132. organization: {slug: string}
  133. ): UseApiQueryResult<TraceSplitResults<TraceTree.Transaction>, RequestError> {
  134. const demoEventSlug = parseDemoEventSlug(demo);
  135. // When projects don't have performance set up, we allow them to view a demo transaction.
  136. // The backend creates the demo transaction, however the trace is created async, so when the
  137. // page loads, we cannot guarantee that querying the trace will succeed as it may not have been stored yet.
  138. // When this happens, we assemble a fake trace response to only include the transaction that had already been
  139. // created and stored already so that the users can visualize in the context of a trace.
  140. const demoEventQuery = useApiQuery<EventTransaction>(
  141. [
  142. `/organizations/${organization.slug}/events/${demoEventSlug?.project_slug}:${demoEventSlug?.event_id}/`,
  143. {
  144. query: {
  145. referrer: 'trace-view',
  146. },
  147. },
  148. ],
  149. {
  150. staleTime: Infinity,
  151. enabled: !!demoEventSlug,
  152. }
  153. );
  154. // Without the useMemo, the trace from the transformed response will be re-created on every render,
  155. // causing the trace view to re-render as we interact with it.
  156. const data = useMemo(() => {
  157. return makeTraceFromTransaction(demoEventQuery.data);
  158. }, [demoEventQuery.data]);
  159. // Casting here since the 'select' option is not available in the useApiQuery hook to transform the data
  160. // from EventTransaction to TraceSplitResults<TraceFullDetailed>
  161. return {...demoEventQuery, data} as UseApiQueryResult<
  162. TraceSplitResults<TraceTree.Transaction>,
  163. any
  164. >;
  165. }
  166. type UseTraceParams = {
  167. limit?: number;
  168. timestamp?: number;
  169. traceSlug?: string;
  170. };
  171. export function useTrace(
  172. options: UseTraceParams
  173. ): UseApiQueryResult<TraceSplitResults<TraceTree.Transaction>, RequestError> {
  174. const filters = usePageFilters();
  175. const organization = useOrganization();
  176. const queryParams = useMemo(() => {
  177. const query = qs.parse(location.search);
  178. return getTraceQueryParams(query, filters.selection, {
  179. limit: options.limit,
  180. timestamp: options.timestamp,
  181. });
  182. // eslint-disable-next-line react-hooks/exhaustive-deps
  183. }, [options.limit, options.timestamp]);
  184. const mode = queryParams.demo ? 'demo' : undefined;
  185. const demoTrace = useDemoTrace(queryParams.demo, organization);
  186. const traceQuery = useApiQuery<TraceSplitResults<TraceTree.Transaction>>(
  187. [
  188. `/organizations/${organization.slug}/events-trace/${options.traceSlug ?? ''}/`,
  189. {query: queryParams},
  190. ],
  191. {
  192. staleTime: Infinity,
  193. enabled: !!options.traceSlug && !!organization.slug && mode !== 'demo',
  194. }
  195. );
  196. return mode === 'demo' ? demoTrace : traceQuery;
  197. }