flamegraph.tsx 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. import {Fragment, useEffect, useMemo, useState} from 'react';
  2. import styled from '@emotion/styled';
  3. import * as Sentry from '@sentry/react';
  4. import {Location} from 'history';
  5. import {Client} from 'sentry/api';
  6. import Alert from 'sentry/components/alert';
  7. import * as Layout from 'sentry/components/layouts/thirds';
  8. import LoadingIndicator from 'sentry/components/loadingIndicator';
  9. import {Breadcrumb} from 'sentry/components/profiling/breadcrumb';
  10. import {Flamegraph} from 'sentry/components/profiling/flamegraph';
  11. import {ProfileDragDropImportProps} from 'sentry/components/profiling/profileDragDropImport';
  12. import SentryDocumentTitle from 'sentry/components/sentryDocumentTitle';
  13. import {t} from 'sentry/locale';
  14. import {Organization, Project} from 'sentry/types';
  15. import {Trace} from 'sentry/types/profiling/core';
  16. import {DeepPartial} from 'sentry/types/utils';
  17. import {
  18. decodeFlamegraphStateFromQueryParams,
  19. FlamegraphState,
  20. FlamegraphStateProvider,
  21. FlamegraphStateQueryParamSync,
  22. } from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider';
  23. import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegraphThemeProvider';
  24. import {importProfile, ProfileGroup} from 'sentry/utils/profiling/profile/importProfile';
  25. import {Profile} from 'sentry/utils/profiling/profile/profile';
  26. import useApi from 'sentry/utils/useApi';
  27. import {useLocation} from 'sentry/utils/useLocation';
  28. import useOrganization from 'sentry/utils/useOrganization';
  29. type InitialState = {type: 'initial'};
  30. type LoadingState = {type: 'loading'};
  31. type ResolvedState<T> = {
  32. data: T;
  33. type: 'resolved';
  34. };
  35. type ErroredState = {
  36. error: string;
  37. type: 'errored';
  38. };
  39. type RequestState<T> = InitialState | LoadingState | ResolvedState<T> | ErroredState;
  40. function fetchFlamegraphs(
  41. api: Client,
  42. eventId: string,
  43. projectId: Project['id'],
  44. organization: Organization
  45. ): Promise<ProfileGroup> {
  46. return api
  47. .requestPromise(
  48. `/projects/${organization.slug}/${projectId}/profiling/profiles/${eventId}/`,
  49. {
  50. method: 'GET',
  51. includeAllArgs: true,
  52. }
  53. )
  54. .then(([data]) => importProfile(data, eventId));
  55. }
  56. const LoadingGroup: ProfileGroup = {
  57. name: 'Loading',
  58. traceID: '',
  59. activeProfileIndex: 0,
  60. profiles: [Profile.Empty()],
  61. };
  62. interface FlamegraphViewProps {
  63. location: Location;
  64. params: {
  65. eventId?: Trace['id'];
  66. projectId?: Project['id'];
  67. };
  68. }
  69. function FlamegraphView(props: FlamegraphViewProps): React.ReactElement {
  70. const api = useApi();
  71. const organization = useOrganization();
  72. const location = useLocation();
  73. const [requestState, setRequestState] = useState<RequestState<ProfileGroup>>({
  74. type: 'initial',
  75. });
  76. const initialFlamegraphPreferencesState = useMemo((): DeepPartial<FlamegraphState> => {
  77. return decodeFlamegraphStateFromQueryParams(location.query);
  78. // We only want to decode this when our component mounts
  79. // eslint-disable-next-line react-hooks/exhaustive-deps
  80. }, []);
  81. useEffect(() => {
  82. if (!props.params.eventId || !props.params.projectId) {
  83. return undefined;
  84. }
  85. setRequestState({type: 'loading'});
  86. fetchFlamegraphs(api, props.params.eventId, props.params.projectId, organization)
  87. .then(importedFlamegraphs => {
  88. setRequestState({type: 'resolved', data: importedFlamegraphs});
  89. })
  90. .catch(err => {
  91. const message = err.toString() || t('Error: Unable to load profiles');
  92. setRequestState({type: 'errored', error: message});
  93. Sentry.captureException(err);
  94. });
  95. return () => {
  96. api.clear();
  97. };
  98. }, [props.params.eventId, props.params.projectId, api, organization]);
  99. const onImport: ProfileDragDropImportProps['onImport'] = profiles => {
  100. setRequestState({type: 'resolved', data: profiles});
  101. };
  102. return (
  103. <SentryDocumentTitle title={t('Profiling')} orgSlug={organization.slug}>
  104. <FlamegraphStateProvider initialState={initialFlamegraphPreferencesState}>
  105. <FlamegraphStateQueryParamSync />
  106. <Fragment>
  107. <Layout.Header>
  108. <Layout.HeaderContent>
  109. <Breadcrumb
  110. location={props.location}
  111. organization={organization}
  112. trails={[
  113. {type: 'profiling'},
  114. {
  115. type: 'flamegraph',
  116. payload: {
  117. interactionName:
  118. requestState.type === 'resolved' ? requestState.data.name : '',
  119. profileId: props.params.eventId ?? '',
  120. projectSlug: props.params.projectId ?? '',
  121. },
  122. },
  123. ]}
  124. />
  125. </Layout.HeaderContent>
  126. </Layout.Header>
  127. <FlamegraphThemeProvider>
  128. <FlamegraphContainer>
  129. {requestState.type === 'errored' ? (
  130. <Alert type="error" showIcon>
  131. {requestState.error}
  132. </Alert>
  133. ) : requestState.type === 'loading' ? (
  134. <Fragment>
  135. <Flamegraph onImport={onImport} profiles={LoadingGroup} />
  136. <LoadingIndicatorContainer>
  137. <LoadingIndicator />
  138. </LoadingIndicatorContainer>
  139. </Fragment>
  140. ) : requestState.type === 'resolved' ? (
  141. <Flamegraph onImport={onImport} profiles={requestState.data} />
  142. ) : null}
  143. </FlamegraphContainer>
  144. </FlamegraphThemeProvider>
  145. </Fragment>
  146. </FlamegraphStateProvider>
  147. </SentryDocumentTitle>
  148. );
  149. }
  150. const LoadingIndicatorContainer = styled('div')`
  151. position: absolute;
  152. display: flex;
  153. flex-direction: column;
  154. justify-content: center;
  155. width: 100%;
  156. height: 100%;
  157. `;
  158. const FlamegraphContainer = styled('div')`
  159. display: flex;
  160. flex-direction: column;
  161. flex: 1 1 100%;
  162. /*
  163. * The footer component is a sibling of this div.
  164. * Remove it so the flamegraph can take up the
  165. * entire screen.
  166. */
  167. ~ footer {
  168. display: none;
  169. }
  170. `;
  171. export default FlamegraphView;