profilesProvider.tsx 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. import {createContext, useContext, useEffect, useState} from 'react';
  2. import * as Sentry from '@sentry/react';
  3. import type {Client} from 'sentry/api';
  4. import {ProfileHeader} from 'sentry/components/profiling/profileHeader';
  5. import {t} from 'sentry/locale';
  6. import type {EventTransaction, Organization, Project} from 'sentry/types';
  7. import type {RequestState} from 'sentry/types/core';
  8. import {isSchema, isSentrySampledProfile} from 'sentry/utils/profiling/guards/profile';
  9. import {useSentryEvent} from 'sentry/utils/profiling/hooks/useSentryEvent';
  10. import useApi from 'sentry/utils/useApi';
  11. import useOrganization from 'sentry/utils/useOrganization';
  12. import {useParams} from 'sentry/utils/useParams';
  13. function fetchFlamegraphs(
  14. api: Client,
  15. eventId: string,
  16. projectSlug: Project['slug'],
  17. orgSlug: Organization['slug']
  18. ): Promise<Profiling.ProfileInput> {
  19. return api
  20. .requestPromise(
  21. `/projects/${orgSlug}/${projectSlug}/profiling/profiles/${eventId}/`,
  22. {
  23. method: 'GET',
  24. includeAllArgs: true,
  25. }
  26. )
  27. .then(([data]) => data);
  28. }
  29. function getTransactionId(input: Profiling.ProfileInput): string | null {
  30. if (isSchema(input)) {
  31. return input.metadata.transactionID;
  32. }
  33. if (isSentrySampledProfile(input)) {
  34. return input.transaction.id;
  35. }
  36. return null;
  37. }
  38. interface FlamegraphViewProps {
  39. children: React.ReactNode;
  40. }
  41. type ProfileProviderValue = RequestState<Profiling.ProfileInput>;
  42. type SetProfileProviderValue = React.Dispatch<
  43. React.SetStateAction<RequestState<Profiling.ProfileInput>>
  44. >;
  45. export const ProfileContext = createContext<ProfileProviderValue | null>(null);
  46. const SetProfileProvider = createContext<SetProfileProviderValue | null>(null);
  47. export function useProfiles() {
  48. const context = useContext(ProfileContext);
  49. if (!context) {
  50. throw new Error('useProfiles was called outside of ProfileProvider');
  51. }
  52. return context;
  53. }
  54. export function useSetProfiles() {
  55. const context = useContext(SetProfileProvider);
  56. if (!context) {
  57. throw new Error('useSetProfiles was called outside of SetProfileProvider');
  58. }
  59. return context;
  60. }
  61. export const ProfileTransactionContext =
  62. createContext<RequestState<EventTransaction | null> | null>(null);
  63. export function useProfileTransaction() {
  64. const context = useContext(ProfileTransactionContext);
  65. if (!context) {
  66. throw new Error(
  67. 'useProfileTransaction was called outside of ProfileTransactionContext'
  68. );
  69. }
  70. return context;
  71. }
  72. function ProfilesAndTransactionProvider(props: FlamegraphViewProps): React.ReactElement {
  73. const organization = useOrganization();
  74. const params = useParams();
  75. const [profiles, setProfiles] = useState<RequestState<Profiling.ProfileInput>>({
  76. type: 'initial',
  77. });
  78. const profileTransaction = useSentryEvent<EventTransaction>(
  79. organization.slug,
  80. params.projectId,
  81. profiles.type === 'resolved' ? getTransactionId(profiles.data) : null
  82. );
  83. return (
  84. <ProfilesProvider
  85. onUpdateProfiles={setProfiles}
  86. orgSlug={organization.slug}
  87. profileId={params.eventId}
  88. projectSlug={params.projectId}
  89. >
  90. <SetProfileProvider.Provider value={setProfiles}>
  91. <ProfileTransactionContext.Provider value={profileTransaction}>
  92. <ProfileHeader
  93. eventId={params.eventId}
  94. projectId={params.projectId}
  95. transaction={
  96. profileTransaction.type === 'resolved' ? profileTransaction.data : null
  97. }
  98. />
  99. {props.children}
  100. </ProfileTransactionContext.Provider>
  101. </SetProfileProvider.Provider>
  102. </ProfilesProvider>
  103. );
  104. }
  105. interface ProfilesProviderProps {
  106. children: React.ReactNode;
  107. orgSlug: Organization['slug'];
  108. profileId: string;
  109. projectSlug: Project['slug'];
  110. onUpdateProfiles?: (any) => void;
  111. }
  112. export function ProfilesProvider({
  113. children,
  114. onUpdateProfiles,
  115. orgSlug,
  116. projectSlug,
  117. profileId,
  118. }: ProfilesProviderProps) {
  119. const api = useApi();
  120. const [profiles, setProfiles] = useState<RequestState<Profiling.ProfileInput>>({
  121. type: 'initial',
  122. });
  123. useEffect(() => {
  124. if (!profileId || !projectSlug || !orgSlug) {
  125. return undefined;
  126. }
  127. setProfiles({type: 'loading'});
  128. fetchFlamegraphs(api, profileId, projectSlug, orgSlug)
  129. .then(p => {
  130. setProfiles({type: 'resolved', data: p});
  131. onUpdateProfiles?.({type: 'resolved', data: p});
  132. })
  133. .catch(err => {
  134. // XXX: our API client mock implementation does not mimick the real
  135. // implementation, so we need to check for an empty object here. #sad
  136. const isEmptyObject = err.toString() === '[object Object]';
  137. const message = isEmptyObject
  138. ? t('Error: Unable to load profiles')
  139. : err.toString();
  140. setProfiles({type: 'errored', error: message});
  141. onUpdateProfiles?.({type: 'errored', error: message});
  142. Sentry.captureException(err);
  143. });
  144. return () => {
  145. api.clear();
  146. };
  147. }, [api, onUpdateProfiles, orgSlug, projectSlug, profileId]);
  148. return <ProfileContext.Provider value={profiles}>{children}</ProfileContext.Provider>;
  149. }
  150. export default ProfilesAndTransactionProvider;