mirror of
https://github.com/hanzoai/sentinel.git
synced 2026-09-29 22:34:33 +00:00
174 lines
5.1 KiB
TypeScript
174 lines
5.1 KiB
TypeScript
import {createContext, useContext, useEffect, useState} from 'react';
|
|
import * as Sentry from '@sentry/react';
|
|
|
|
import type {Client} from 'sentry/api';
|
|
import {ProfileHeader} from 'sentry/components/profiling/profileHeader';
|
|
import {t} from 'sentry/locale';
|
|
import type {EventTransaction, Organization, Project} from 'sentry/types';
|
|
import type {RequestState} from 'sentry/types/core';
|
|
import {isSchema, isSentrySampledProfile} from 'sentry/utils/profiling/guards/profile';
|
|
import {useSentryEvent} from 'sentry/utils/profiling/hooks/useSentryEvent';
|
|
import useApi from 'sentry/utils/useApi';
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
import {useParams} from 'sentry/utils/useParams';
|
|
|
|
function fetchFlamegraphs(
|
|
api: Client,
|
|
eventId: string,
|
|
projectSlug: Project['slug'],
|
|
orgSlug: Organization['slug']
|
|
): Promise<Profiling.ProfileInput> {
|
|
return api
|
|
.requestPromise(
|
|
`/projects/${orgSlug}/${projectSlug}/profiling/profiles/${eventId}/`,
|
|
{
|
|
method: 'GET',
|
|
includeAllArgs: true,
|
|
}
|
|
)
|
|
.then(([data]) => data);
|
|
}
|
|
|
|
function getTransactionId(input: Profiling.ProfileInput): string | null {
|
|
if (isSchema(input)) {
|
|
return input.metadata.transactionID;
|
|
}
|
|
if (isSentrySampledProfile(input)) {
|
|
return input.transaction.id;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
interface FlamegraphViewProps {
|
|
children: React.ReactNode;
|
|
}
|
|
|
|
type ProfileProviderValue = RequestState<Profiling.ProfileInput>;
|
|
type SetProfileProviderValue = React.Dispatch<
|
|
React.SetStateAction<RequestState<Profiling.ProfileInput>>
|
|
>;
|
|
export const ProfileContext = createContext<ProfileProviderValue | null>(null);
|
|
const SetProfileProvider = createContext<SetProfileProviderValue | null>(null);
|
|
|
|
export function useProfiles() {
|
|
const context = useContext(ProfileContext);
|
|
if (!context) {
|
|
throw new Error('useProfiles was called outside of ProfileProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export function useSetProfiles() {
|
|
const context = useContext(SetProfileProvider);
|
|
if (!context) {
|
|
throw new Error('useSetProfiles was called outside of SetProfileProvider');
|
|
}
|
|
return context;
|
|
}
|
|
|
|
export const ProfileTransactionContext =
|
|
createContext<RequestState<EventTransaction | null> | null>(null);
|
|
|
|
export function useProfileTransaction() {
|
|
const context = useContext(ProfileTransactionContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
'useProfileTransaction was called outside of ProfileTransactionContext'
|
|
);
|
|
}
|
|
return context;
|
|
}
|
|
|
|
function ProfilesAndTransactionProvider(props: FlamegraphViewProps): React.ReactElement {
|
|
const organization = useOrganization();
|
|
const params = useParams();
|
|
|
|
const [profiles, setProfiles] = useState<RequestState<Profiling.ProfileInput>>({
|
|
type: 'initial',
|
|
});
|
|
|
|
const profileTransaction = useSentryEvent<EventTransaction>(
|
|
organization.slug,
|
|
params.projectId,
|
|
profiles.type === 'resolved' ? getTransactionId(profiles.data) : null
|
|
);
|
|
|
|
return (
|
|
<ProfilesProvider
|
|
onUpdateProfiles={setProfiles}
|
|
orgSlug={organization.slug}
|
|
profileId={params.eventId}
|
|
projectSlug={params.projectId}
|
|
>
|
|
<SetProfileProvider.Provider value={setProfiles}>
|
|
<ProfileTransactionContext.Provider value={profileTransaction}>
|
|
<ProfileHeader
|
|
eventId={params.eventId}
|
|
projectId={params.projectId}
|
|
transaction={
|
|
profileTransaction.type === 'resolved' ? profileTransaction.data : null
|
|
}
|
|
/>
|
|
{props.children}
|
|
</ProfileTransactionContext.Provider>
|
|
</SetProfileProvider.Provider>
|
|
</ProfilesProvider>
|
|
);
|
|
}
|
|
|
|
interface ProfilesProviderProps {
|
|
children: React.ReactNode;
|
|
orgSlug: Organization['slug'];
|
|
profileId: string;
|
|
projectSlug: Project['slug'];
|
|
onUpdateProfiles?: (any) => void;
|
|
}
|
|
|
|
export function ProfilesProvider({
|
|
children,
|
|
onUpdateProfiles,
|
|
orgSlug,
|
|
projectSlug,
|
|
profileId,
|
|
}: ProfilesProviderProps) {
|
|
const api = useApi();
|
|
|
|
const [profiles, setProfiles] = useState<RequestState<Profiling.ProfileInput>>({
|
|
type: 'initial',
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (!profileId || !projectSlug || !orgSlug) {
|
|
return undefined;
|
|
}
|
|
|
|
setProfiles({type: 'loading'});
|
|
|
|
fetchFlamegraphs(api, profileId, projectSlug, orgSlug)
|
|
.then(p => {
|
|
setProfiles({type: 'resolved', data: p});
|
|
onUpdateProfiles?.({type: 'resolved', data: p});
|
|
})
|
|
.catch(err => {
|
|
// XXX: our API client mock implementation does not mimick the real
|
|
// implementation, so we need to check for an empty object here. #sad
|
|
const isEmptyObject = err.toString() === '[object Object]';
|
|
const message = isEmptyObject
|
|
? t('Error: Unable to load profiles')
|
|
: err.toString();
|
|
|
|
setProfiles({type: 'errored', error: message});
|
|
onUpdateProfiles?.({type: 'errored', error: message});
|
|
Sentry.captureException(err);
|
|
});
|
|
|
|
return () => {
|
|
api.clear();
|
|
};
|
|
}, [api, onUpdateProfiles, orgSlug, projectSlug, profileId]);
|
|
|
|
return <ProfileContext.Provider value={profiles}>{children}</ProfileContext.Provider>;
|
|
}
|
|
|
|
export default ProfilesAndTransactionProvider;
|