import {createContext, useContext, useLayoutEffect, useMemo, useState} from 'react'; import * as Sentry from '@sentry/react'; import * as qs from 'query-string'; import type {Client} from 'sentry/api'; import {ContinuousProfileHeader} from 'sentry/components/profiling/continuousProfileHeader'; import type {RequestState} from 'sentry/types/core'; import type {EventTransaction} from 'sentry/types/event'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; 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'; import useProjects from 'sentry/utils/useProjects'; interface ContinuousProfileQueryParams { end: string; profiler_id: string; start: string; } function fetchContinuousProfileFlamegraph( api: Client, query: ContinuousProfileQueryParams, projectSlug: Project['slug'], orgSlug: Organization['slug'] ): Promise { return api .requestPromise(`/organizations/${orgSlug}/profiling/chunks/`, { method: 'GET', query: { ...query, project: projectSlug, }, includeAllArgs: true, }) .then(([data]) => data.chunk); } type ContinuousProfileProviderValue = RequestState; export const ContinuousProfileContext = createContext(null); export function useContinuousProfile() { const context = useContext(ContinuousProfileContext); if (!context) { throw new Error('useContinuousProfile was called outside of ProfileProvider'); } return context; } type ContinuousProfileSegmentProviderValue = RequestState; export const ContinuousProfileSegmentContext = createContext(null); export function useContinuousProfileSegment() { const context = useContext(ContinuousProfileSegmentContext); if (!context) { throw new Error( 'useContinuousProfileSegment was called outside of ContinuousProfileSegmentProvider' ); } return context; } function isValidDate(date: string): boolean { return !isNaN(Date.parse(date)); } function getContinuousChunkQueryParams( query: string ): ContinuousProfileQueryParams | null { const queryString = new URLSearchParams(query); const start = queryString.get('start'); const end = queryString.get('end'); const profiler_id = queryString.get('profilerId'); if (!start || !end || !profiler_id) { return null; } if (!isValidDate(start) || !isValidDate(end)) { return null; } return { start, end, profiler_id, }; } interface ContinuousFlamegraphViewProps { children: React.ReactNode; } function ContinuousProfileProvider( props: ContinuousFlamegraphViewProps ): React.ReactElement { const api = useApi(); const params = useParams(); const organization = useOrganization(); const projects = useProjects(); const [profiles, setProfiles] = useState>({ type: 'initial', }); useLayoutEffect(() => { if (!params.projectId) { return undefined; } const chunkParams = getContinuousChunkQueryParams(window.location.search); const project = projects.projects.find(p => p.slug === params.projectId); if (!chunkParams) { Sentry.captureMessage( 'Failed to fetch continuous profile - invalid query parameters.' ); return undefined; } if (!project) { Sentry.captureMessage('Failed to fetch continuous profile - project not found.'); return undefined; } setProfiles({type: 'loading'}); fetchContinuousProfileFlamegraph(api, chunkParams, project.id, organization.slug) .then(p => { setProfiles({type: 'resolved', data: p}); }) .catch(err => { setProfiles({type: 'errored', error: 'Failed to fetch profiles'}); Sentry.captureException(err); }); return () => api.clear(); }, [api, organization.slug, projects.projects, params.projectId]); const eventPayload = useMemo(() => { const query = qs.parse(window.location.search); return { project: projects.projects.find(p => p.slug === params.projectId), eventId: query.eventId as string, }; }, [projects, params.projectId]); const profileTransaction = useSentryEvent( organization.slug, eventPayload.project?.id ?? '', eventPayload.eventId ); return ( {props.children} ); } export default ContinuousProfileProvider;