Skip to content

Commit

Permalink
ref(profiling): First pass to consolidate profile providers (#83274)
Browse files Browse the repository at this point in the history
This makes the 2 profile provider components more similar in preparation
to merge them into 1 soon.
  • Loading branch information
Zylphrex authored Jan 13, 2025
1 parent 3f72251 commit 6ebe933
Show file tree
Hide file tree
Showing 4 changed files with 157 additions and 89 deletions.
45 changes: 43 additions & 2 deletions static/app/components/profiling/continuousProfileHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,56 @@
import {useMemo} from 'react';
import {useCallback, useMemo} from 'react';
import styled from '@emotion/styled';

import {LinkButton} from 'sentry/components/button';
import FeedbackWidgetButton from 'sentry/components/feedback/widget/feedbackWidgetButton';
import * as Layout from 'sentry/components/layouts/thirds';
import type {ProfilingBreadcrumbsProps} from 'sentry/components/profiling/profilingBreadcrumbs';
import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs';
import {t} from 'sentry/locale';
import {space} from 'sentry/styles/space';
import type {Event} from 'sentry/types/event';
import {trackAnalytics} from 'sentry/utils/analytics';
import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';

export function ContinuousProfileHeader() {
interface ContinuousProfileHeader {
projectId: string;
transaction: Event | null;
}

export function ContinuousProfileHeader({
transaction,
projectId,
}: ContinuousProfileHeader) {
const location = useLocation();
const organization = useOrganization();

// @TODO add breadcrumbs when other views are implemented
const breadCrumbs = useMemo((): ProfilingBreadcrumbsProps['trails'] => {
return [{type: 'landing', payload: {query: {}}}];
}, []);

const projectSlug = projectId ?? '';

const transactionTarget = transaction?.id
? generateLinkToEventInTraceView({
timestamp: transaction.endTimestamp ?? '',
eventId: transaction.id,
projectSlug,
traceSlug: transaction.contexts?.trace?.trace_id ?? '',
location,
organization,
})
: null;

const handleGoToTransaction = useCallback(() => {
trackAnalytics('profiling_views.go_to_transaction', {
organization,
source: 'transaction_details',
});
}, [organization]);

return (
<SmallerLayoutHeader>
<SmallerHeaderContent>
Expand All @@ -24,6 +60,11 @@ export function ContinuousProfileHeader() {
</SmallerHeaderContent>
<StyledHeaderActions>
<FeedbackWidgetButton />
{transactionTarget && (
<LinkButton size="sm" onClick={handleGoToTransaction} to={transactionTarget}>
{t('Go to Transaction')}
</LinkButton>
)}
</StyledHeaderActions>
</SmallerLayoutHeader>
);
Expand Down
58 changes: 32 additions & 26 deletions static/app/views/profiling/continuousProfileProvider.spec.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import * as Sentry from '@sentry/react';
import {uuid4} from '@sentry/utils';
import * as qs from 'query-string';
import {OrganizationFixture} from 'sentry-fixture/organization';
import {ProjectFixture} from 'sentry-fixture/project';

import {initializeOrg} from 'sentry-test/initializeOrg';
import {render, waitFor} from 'sentry-test/reactTestingLibrary';

import ProjectsStore from 'sentry/stores/projectsStore';
Expand All @@ -12,20 +10,22 @@ import ContinuosProfileProvider from './continuousProfileProvider';

describe('ContinuousProfileProvider', () => {
beforeEach(() => {
window.location.search = '';
MockApiClient.clearMockResponses();
});
it('fetches chunk', async () => {
const project = ProjectFixture();
const organization = OrganizationFixture();
ProjectsStore.loadInitialData([project]);

window.location.search = qs.stringify({
start: new Date().toISOString(),
end: new Date().toISOString(),
profilerId: uuid4(),
eventId: '1',
const {organization, project, router} = initializeOrg({
router: {
location: {
query: {
start: new Date().toISOString(),
end: new Date().toISOString(),
profilerId: uuid4(),
eventId: '1',
},
},
},
});
ProjectsStore.loadInitialData([project]);

const captureMessage = jest.spyOn(Sentry, 'captureMessage');
const chunkRequest = MockApiClient.addMockResponse({
Expand All @@ -34,14 +34,12 @@ describe('ContinuousProfileProvider', () => {
});

MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.id}/events/1/`,
url: `/projects/${organization.slug}/${project.slug}/events/1/`,
body: {},
});

render(<ContinuosProfileProvider>{null}</ContinuosProfileProvider>, {
router: {
params: {orgId: organization.slug, projectId: project.slug},
},
router,
organization,
});

Expand All @@ -55,25 +53,33 @@ describe('ContinuousProfileProvider', () => {
[new Date().toISOString(), undefined, uuid4()],
[new Date().toISOString(), new Date().toISOString(), undefined],
]) {
const project = ProjectFixture();
const organization = OrganizationFixture();
window.location.search = qs.stringify({start, end, profilerId, eventId: '1'});
const {organization, project, router} = initializeOrg({
// params: {orgId: organization.slug, projectId: project.slug},
router: {
location: {
query: {
start,
end,
profilerId,
eventId: '1',
},
},
},
});
MockApiClient.addMockResponse({
url: `/projects/${organization.slug}/${project.id}/events/1/`,
url: `/projects/${organization.slug}/${project.slug}/events/1/`,
body: {},
});
const captureMessage = jest.spyOn(Sentry, 'captureMessage');
render(<ContinuosProfileProvider>{null}</ContinuosProfileProvider>, {
router: {
params: {orgId: OrganizationFixture().slug, projectId: ProjectFixture().slug},
},
organization: OrganizationFixture(),
router,
organization,
});

await waitFor(() =>
expect(captureMessage).toHaveBeenCalledWith(
expect.stringContaining(
'Failed to fetch continuous profile - invalid query parameters.'
'Failed to fetch continuous profile - invalid chunk parameters.'
)
)
);
Expand Down
133 changes: 76 additions & 57 deletions static/app/views/profiling/continuousProfileProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
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';
Expand All @@ -9,7 +8,9 @@ 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 {decodeScalar} from 'sentry/utils/queryString';
import useApi from 'sentry/utils/useApi';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import {useParams} from 'sentry/utils/useParams';
import useProjects from 'sentry/utils/useProjects';
Expand Down Expand Up @@ -68,101 +69,119 @@ 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');
interface FlamegraphViewProps {
children: React.ReactNode;
}

if (!start || !end || !profiler_id) {
return null;
}
function ProfilesAndTransactionProvider(props: FlamegraphViewProps): React.ReactElement {
const organization = useOrganization();
const params = useParams();
const location = useLocation();

if (!isValidDate(start) || !isValidDate(end)) {
return null;
}
const projectSlug = params.projectId!;

const profileMeta = useMemo(() => {
const start = decodeScalar(location.query.start);
const end = decodeScalar(location.query.end);
const profilerId = decodeScalar(location.query.profilerId);

if (!start || !end || !profilerId) {
return null;
}

if (!isValidDate(start) || !isValidDate(end)) {
return null;
}

return {
start,
end,
profiler_id,
};
return {
start,
end,
profiler_id: profilerId,
};
}, [location.query.start, location.query.end, location.query.profilerId]);

const profileTransaction = useSentryEvent<EventTransaction>(
organization.slug,
projectSlug!,
decodeScalar(location.query.eventId) || null
);

return (
<ProfilesProvider
orgSlug={organization.slug}
profileMeta={profileMeta}
projectSlug={projectSlug}
>
<ContinuousProfileSegmentContext.Provider value={profileTransaction}>
<ContinuousProfileHeader
projectId={projectSlug}
transaction={
profileTransaction.type === 'resolved' ? profileTransaction.data : null
}
/>
{props.children}
</ContinuousProfileSegmentContext.Provider>
</ProfilesProvider>
);
}

interface ContinuousFlamegraphViewProps {
interface ProfilesProviderProps {
children: React.ReactNode;
orgSlug: Organization['slug'];
profileMeta: ContinuousProfileQueryParams | null;
projectSlug: Project['slug'];
onUpdateProfiles?: (profiles: RequestState<Profiling.ProfileInput>) => void;
}

function ContinuousProfileProvider(
props: ContinuousFlamegraphViewProps
): React.ReactElement {
export function ProfilesProvider({
children,
onUpdateProfiles,
orgSlug,
profileMeta,
projectSlug,
}: ProfilesProviderProps) {
const api = useApi();
const params = useParams();
const organization = useOrganization();
const projects = useProjects();
const {projects} = useProjects();

const [profiles, setProfiles] = useState<RequestState<Profiling.ProfileInput>>({
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) {
if (!profileMeta) {
Sentry.captureMessage(
'Failed to fetch continuous profile - invalid query parameters.'
'Failed to fetch continuous profile - invalid chunk parameters.'
);
return undefined;
}

const project = projects.find(p => p.slug === projectSlug);
if (!project) {
Sentry.captureMessage('Failed to fetch continuous profile - project not found.');
return undefined;
}

setProfiles({type: 'loading'});

fetchContinuousProfileFlamegraph(api, chunkParams, project.id, organization.slug)
fetchContinuousProfileFlamegraph(api, profileMeta, project.id, orgSlug)
.then(p => {
setProfiles({type: 'resolved', data: p});
onUpdateProfiles?.({type: 'resolved', data: p});
})
.catch(err => {
setProfiles({type: 'errored', error: 'Failed to fetch profiles'});
onUpdateProfiles?.({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<EventTransaction>(
organization.slug,
eventPayload.project?.id ?? '',
eventPayload.eventId
);
}, [api, onUpdateProfiles, profileMeta, orgSlug, projectSlug, projects]);

return (
<ContinuousProfileContext.Provider value={profiles}>
<ContinuousProfileSegmentContext.Provider value={profileTransaction}>
<ContinuousProfileHeader />
{props.children}
</ContinuousProfileSegmentContext.Provider>
{children}
</ContinuousProfileContext.Provider>
);
}

export default ContinuousProfileProvider;
export default ProfilesAndTransactionProvider;
Loading

0 comments on commit 6ebe933

Please sign in to comment.