profilesProvider.tsx 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. import {createContext, useContext, useLayoutEffect, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import type {Client} from 'sentry/api';
  4. import {t} from 'sentry/locale';
  5. import type {RequestState} from 'sentry/types/core';
  6. import type {EventTransaction} from 'sentry/types/event';
  7. import type {Organization} from 'sentry/types/organization';
  8. import type {Project} from 'sentry/types/project';
  9. import useApi from 'sentry/utils/useApi';
  10. import useProjects from 'sentry/utils/useProjects';
  11. function fetchFlamegraphs(
  12. api: Client,
  13. eventId: string,
  14. projectSlug: Project['slug'],
  15. orgSlug: Organization['slug']
  16. ): Promise<Profiling.ProfileInput> {
  17. return api
  18. .requestPromise(
  19. `/projects/${orgSlug}/${projectSlug}/profiling/profiles/${eventId}/`,
  20. {
  21. method: 'GET',
  22. includeAllArgs: true,
  23. }
  24. )
  25. .then(([data]) => data);
  26. }
  27. function fetchContinuousProfileFlamegraph(
  28. api: Client,
  29. query: ContinuousProfileQueryParams,
  30. projectSlug: Project['slug'],
  31. orgSlug: Organization['slug']
  32. ): Promise<Profiling.ProfileInput> {
  33. return api
  34. .requestPromise(`/organizations/${orgSlug}/profiling/chunks/`, {
  35. method: 'GET',
  36. query: {
  37. ...query,
  38. project: projectSlug,
  39. },
  40. includeAllArgs: true,
  41. })
  42. .then(([data]) => data.chunk);
  43. }
  44. type ProfileProviderValue = RequestState<Profiling.ProfileInput>;
  45. export const ProfileContext = createContext<ProfileProviderValue | null>(null);
  46. export function useProfiles() {
  47. const context = useContext(ProfileContext);
  48. if (!context) {
  49. throw new Error('useProfiles was called outside of ProfileProvider');
  50. }
  51. return context;
  52. }
  53. export const ProfileTransactionContext =
  54. createContext<RequestState<EventTransaction | null> | null>(null);
  55. export function useProfileTransaction() {
  56. const context = useContext(ProfileTransactionContext);
  57. if (!context) {
  58. throw new Error(
  59. 'useProfileTransaction was called outside of ProfileTransactionContext'
  60. );
  61. }
  62. return context;
  63. }
  64. interface ProfilesProviderProps {
  65. children: React.ReactNode;
  66. orgSlug: Organization['slug'];
  67. profileMeta: string | ContinuousProfileQueryParams;
  68. projectSlug: Project['slug'];
  69. }
  70. export function ProfilesProvider({
  71. children,
  72. orgSlug,
  73. profileMeta,
  74. projectSlug,
  75. }: ProfilesProviderProps) {
  76. const [profile, setProfile] = useState<RequestState<Profiling.ProfileInput>>({
  77. type: 'initial',
  78. });
  79. if (isContinuousProfileQueryParams(profileMeta)) {
  80. return (
  81. <ContinuousProfileProvider
  82. orgSlug={orgSlug}
  83. projectSlug={projectSlug}
  84. profileMeta={profileMeta}
  85. profile={profile}
  86. setProfile={setProfile}
  87. >
  88. {children}
  89. </ContinuousProfileProvider>
  90. );
  91. }
  92. return (
  93. <TransactionProfileProvider
  94. orgSlug={orgSlug}
  95. projectSlug={projectSlug}
  96. profileId={profileMeta}
  97. profile={profile}
  98. setProfile={setProfile}
  99. >
  100. {children}
  101. </TransactionProfileProvider>
  102. );
  103. }
  104. interface TransactionProfileProviderProps {
  105. children: React.ReactNode;
  106. orgSlug: Organization['slug'];
  107. profile: RequestState<Profiling.ProfileInput>;
  108. profileId: string;
  109. projectSlug: Project['slug'];
  110. setProfile: (profiles: RequestState<Profiling.ProfileInput>) => void;
  111. }
  112. export function TransactionProfileProvider({
  113. children,
  114. orgSlug,
  115. profile,
  116. projectSlug,
  117. profileId,
  118. setProfile,
  119. }: TransactionProfileProviderProps) {
  120. const api = useApi();
  121. useLayoutEffect(() => {
  122. if (!profileId || !projectSlug || !orgSlug) {
  123. return undefined;
  124. }
  125. setProfile({type: 'loading'});
  126. fetchFlamegraphs(api, profileId, projectSlug, orgSlug)
  127. .then(p => {
  128. setProfile({type: 'resolved', data: p});
  129. })
  130. .catch(err => {
  131. // XXX: our API client mock implementation does not mimick the real
  132. // implementation, so we need to check for an empty object here. #sad
  133. const isEmptyObject = err.toString() === '[object Object]';
  134. const message = isEmptyObject
  135. ? t('Error: Unable to load profiles')
  136. : err.toString();
  137. setProfile({type: 'errored', error: message});
  138. Sentry.captureException(err);
  139. });
  140. return () => {
  141. api.clear();
  142. };
  143. }, [api, orgSlug, projectSlug, profileId, setProfile]);
  144. return <ProfileContext.Provider value={profile}>{children}</ProfileContext.Provider>;
  145. }
  146. export interface ContinuousProfileQueryParams {
  147. end: string;
  148. profiler_id: string;
  149. start: string;
  150. }
  151. function isContinuousProfileQueryParams(val: any): val is ContinuousProfileQueryParams {
  152. return (
  153. typeof val === 'object' &&
  154. typeof val.start === 'string' &&
  155. typeof val.end === 'string' &&
  156. typeof val.profiler_id === 'string'
  157. );
  158. }
  159. interface ContinuousProfileProviderProps {
  160. children: React.ReactNode;
  161. orgSlug: Organization['slug'];
  162. profile: RequestState<Profiling.ProfileInput>;
  163. profileMeta: ContinuousProfileQueryParams | null;
  164. projectSlug: Project['slug'];
  165. setProfile: (profile: RequestState<Profiling.ProfileInput>) => void;
  166. }
  167. export function ContinuousProfileProvider({
  168. children,
  169. orgSlug,
  170. profile,
  171. profileMeta,
  172. projectSlug,
  173. setProfile,
  174. }: ContinuousProfileProviderProps) {
  175. const api = useApi();
  176. const {projects} = useProjects();
  177. useLayoutEffect(() => {
  178. if (!profileMeta) {
  179. Sentry.captureMessage(
  180. 'Failed to fetch continuous profile - invalid chunk parameters.'
  181. );
  182. return undefined;
  183. }
  184. const project = projects.find(p => p.slug === projectSlug);
  185. if (!project) {
  186. Sentry.captureMessage('Failed to fetch continuous profile - project not found.');
  187. return undefined;
  188. }
  189. setProfile({type: 'loading'});
  190. fetchContinuousProfileFlamegraph(api, profileMeta, project.id, orgSlug)
  191. .then(p => {
  192. setProfile({type: 'resolved', data: p});
  193. })
  194. .catch(err => {
  195. setProfile({type: 'errored', error: 'Failed to fetch profiles'});
  196. Sentry.captureException(err);
  197. });
  198. return () => api.clear();
  199. }, [api, profileMeta, orgSlug, projectSlug, projects, setProfile]);
  200. return <ProfileContext.Provider value={profile}>{children}</ProfileContext.Provider>;
  201. }