From 5b34cac8976bc25058f474da13f4c7884869da13 Mon Sep 17 00:00:00 2001 From: Matt Quinn Date: Tue, 14 Apr 2026 12:56:05 -0400 Subject: [PATCH] profile pages with EAP --- .../events/interfaces/spans/spanTreeModel.tsx | 4 +- .../events/interfaces/spans/utils.tsx | 5 +- .../profiling/continuousProfileHeader.tsx | 19 +- .../flamegraph/continuousFlamegraph.tsx | 69 ++--- .../profiling/flamegraph/flamegraph.spec.tsx | 35 ++- .../profiling/flamegraph/flamegraph.tsx | 78 ++--- .../flamegraphDrawer/flamegraphDrawer.tsx | 10 +- .../flamegraphDrawer/profileDetails.tsx | 82 ++--- .../flamegraph/flamegraphSpanTooltip.tsx | 8 +- .../profiling/flamegraph/flamegraphSpans.tsx | 51 ++-- .../flamegraph/flamegraphSpansContextMenu.tsx | 11 +- .../flamegraphToolbar/flamegraphSearch.tsx | 5 +- .../components/profiling/profileHeader.tsx | 16 +- static/app/utils/profiling/colors/utils.tsx | 16 +- .../utils/profiling/hooks/useTransaction.tsx | 63 ++++ .../profiling/renderers/spansRenderer.tsx | 13 +- .../profiling/renderers/spansTextRenderer.tsx | 5 +- static/app/utils/profiling/spanChart.spec.tsx | 282 ++++++++---------- static/app/utils/profiling/spanChart.tsx | 50 ++-- static/app/utils/profiling/spanTree.spec.tsx | 250 ++++++++-------- static/app/utils/profiling/spanTree.tsx | 169 ++++++----- static/app/views/insights/types.tsx | 9 + .../profiling/continuousProfileProvider.tsx | 16 +- .../app/views/profiling/profileFlamechart.tsx | 10 +- .../app/views/profiling/profilesProvider.tsx | 5 +- .../profiling/transactionProfileProvider.tsx | 37 ++- 26 files changed, 686 insertions(+), 632 deletions(-) create mode 100644 static/app/utils/profiling/hooks/useTransaction.tsx diff --git a/static/app/components/events/interfaces/spans/spanTreeModel.tsx b/static/app/components/events/interfaces/spans/spanTreeModel.tsx index 1894714678ff56..85bee5f6d308fd 100644 --- a/static/app/components/events/interfaces/spans/spanTreeModel.tsx +++ b/static/app/components/events/interfaces/spans/spanTreeModel.tsx @@ -25,7 +25,7 @@ import { getSpanID, getSpanOperation, groupShouldBeHidden, - isEventFromBrowserJavaScriptSDK, + isBrowserJavaScriptSDKName, isOrphanSpan, parseTrace, SpanSubTimingMark, @@ -155,7 +155,7 @@ export class SpanTreeModel { ): EnhancedProcessedSpanType | undefined { // hide gap spans (i.e. "missing instrumentation" spans) for browser js transactions, // since they're not useful to indicate - const shouldIncludeGap = !isEventFromBrowserJavaScriptSDK(event); + const shouldIncludeGap = !isBrowserJavaScriptSDKName(event.sdk?.name); const isValidGap = shouldIncludeGap && diff --git a/static/app/components/events/interfaces/spans/utils.tsx b/static/app/components/events/interfaces/spans/utils.tsx index 34625c4a442603..5ca7cf3093e2d7 100644 --- a/static/app/components/events/interfaces/spans/utils.tsx +++ b/static/app/components/events/interfaces/spans/utils.tsx @@ -575,10 +575,7 @@ function sortSpans(firstSpan: SpanType, secondSpan: SpanType) { return 1; } -export function isEventFromBrowserJavaScriptSDK( - event: EventTransaction | AggregateEventTransaction -): boolean { - const sdkName = event.sdk?.name; +export function isBrowserJavaScriptSDKName(sdkName: string | null | undefined): boolean { if (!sdkName) { return false; } diff --git a/static/app/components/profiling/continuousProfileHeader.tsx b/static/app/components/profiling/continuousProfileHeader.tsx index 7b0ef4ac20ca16..ef8d2f2749e2a8 100644 --- a/static/app/components/profiling/continuousProfileHeader.tsx +++ b/static/app/components/profiling/continuousProfileHeader.tsx @@ -8,19 +8,24 @@ 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 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'; +import type {SpanResponse} from 'sentry/views/insights/types'; +import {SpanFields} from 'sentry/views/insights/types'; import {TopBar} from 'sentry/views/navigation/topBar'; import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature'; interface ContinuousProfileHeader { - transaction: Event | null; + transactionSpan: + | Pick + | undefined; } -export function ContinuousProfileHeader({transaction}: ContinuousProfileHeader) { +export function ContinuousProfileHeader({ + transactionSpan: transaction, +}: ContinuousProfileHeader) { const location = useLocation(); const organization = useOrganization(); const hasPageFrameFeature = useHasPageFrameFeature(); @@ -30,11 +35,11 @@ export function ContinuousProfileHeader({transaction}: ContinuousProfileHeader) return [{type: 'landing', payload: {query: {}}}]; }, []); - const transactionTarget = transaction?.id + const transactionTarget = transaction ? generateLinkToEventInTraceView({ - timestamp: transaction.endTimestamp ?? '', - eventId: transaction.id, - traceSlug: transaction.contexts?.trace?.trace_id ?? '', + timestamp: transaction[SpanFields.PRECISE_FINISH_TS], + targetId: transaction[SpanFields.SPAN_ID], + traceSlug: transaction[SpanFields.TRACE], location, organization, }) diff --git a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx index 5bd412554dabca..512c46347ea3eb 100644 --- a/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx +++ b/static/app/components/profiling/flamegraph/continuousFlamegraph.tsx @@ -22,8 +22,6 @@ import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraph/f import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView'; import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap'; import {t} from 'sentry/locale'; -import type {EntrySpans, EventTransaction} from 'sentry/types/event'; -import {EntryType} from 'sentry/types/event'; import {defined} from 'sentry/utils'; import { CanvasPoolManager, @@ -49,6 +47,7 @@ import { initializeFlamegraphRenderer, useResizeCanvasObserver, } from 'sentry/utils/profiling/gl/utils'; +import type {TransactionSpan} from 'sentry/utils/profiling/hooks/useTransaction'; import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; import type {Profile} from 'sentry/utils/profiling/profile/profile'; import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D'; @@ -66,6 +65,7 @@ import { } from 'sentry/utils/profiling/units/units'; import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio'; import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious'; +import {SpanFields} from 'sentry/views/insights/types'; import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider'; import { useProfiles, @@ -82,39 +82,21 @@ import {FlamegraphUIFrames} from './flamegraphUIFrames'; const PROFILE_TYPE = 'continuous profile' as const; -function collectAllSpanEntriesFromTransaction( - transaction: EventTransaction -): EntrySpans['data'] { - if (!transaction.entries.length) { - return []; - } - - const spans = transaction.entries.filter( - (e): e is EntrySpans => e.type === EntryType.SPANS - ); - - let allSpans: EntrySpans['data'] = []; - - for (const span of spans) { - allSpans = allSpans.concat(span.data); - } - - return allSpans; -} - function getMaxConfigSpace( profileGroup: ProfileGroup, - transaction: EventTransaction | null, + transactionSpan: TransactionSpan | undefined, unit: ProfilingFormatterUnit | string, [start, end]: readonly [number, number] | readonly [null, null] ): Rect { const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration)); const spaceDuration = start !== null && end !== null ? end - start : 0; - if (transaction) { + if (transactionSpan) { // TODO: Adjust the alignment based on the profile's timestamp if it does // not match the transaction's start timestamp - const transactionDuration = transaction.endTimestamp - transaction.startTimestamp; + const transactionDuration = + transactionSpan[SpanFields.PRECISE_FINISH_TS] - + transactionSpan[SpanFields.PRECISE_START_TS]; // On most platforms, profile duration < transaction duration, however // there is one beloved platform where that is not true; android. // Hence, we should take the max of the two to ensure both the transaction @@ -144,16 +126,16 @@ function getProfileOffset( } function getTransactionOffset( - transaction: EventTransaction | null, + transactionSpan: TransactionSpan | undefined, profileTimestamp: number, startedAtMs: number | null ): Rect { - if (!transaction || !startedAtMs) { + if (!transactionSpan || !startedAtMs) { return Rect.Empty(); } return new Rect( - transaction.startTimestamp * 1e3 - profileTimestamp - startedAtMs, + transactionSpan[SpanFields.PRECISE_START_TS] * 1e3 - profileTimestamp - startedAtMs, 0, 0, 0 @@ -256,7 +238,7 @@ export function ContinuousFlamegraph(): ReactElement { const profiles = useProfiles(); const profileGroup = useProfileGroup(); - const segment = useProfileTransaction(); + const transactionResult = useProfileTransaction(); const profileTimestamp = useMemo(() => { return ( @@ -335,19 +317,15 @@ export function ContinuousFlamegraph(): ReactElement { }, [profileGroup, flamegraphProfiles.threadId]); const spanTree = useMemo(() => { - if (segment.type === 'empty') { - return null; - } - - if (segment.type === 'resolved' && segment.data) { + if (!transactionResult.isPending && transactionResult.data.transactionSpan) { return new SpanTree( - segment.data, - collectAllSpanEntriesFromTransaction(segment.data) + transactionResult.data.transactionSpan, + transactionResult.data.childSpans ); } return LOADING_OR_FALLBACK_SPAN_TREE; - }, [segment]); + }, [transactionResult]); const spanChart = useMemo(() => { if (!profile || !spanTree) { @@ -358,12 +336,12 @@ export function ContinuousFlamegraph(): ReactElement { unit: profile.unit, configSpace: getMaxConfigSpace( profileGroup, - segment.type === 'resolved' ? segment.data : null, + transactionResult.data.transactionSpan, profile.unit, configSpaceQueryParam ), }); - }, [spanTree, profile, profileGroup, segment, configSpaceQueryParam]); + }, [spanTree, profile, profileGroup, transactionResult, configSpaceQueryParam]); const flamegraph = useMemo(() => { if (typeof flamegraphProfiles.threadId !== 'number') { @@ -392,7 +370,7 @@ export function ContinuousFlamegraph(): ReactElement { sort: sorting, configSpace: getMaxConfigSpace( profileGroup, - segment.type === 'resolved' ? segment.data : null, + transactionResult.data.transactionSpan, profile.unit, configSpaceQueryParam ), @@ -407,7 +385,7 @@ export function ContinuousFlamegraph(): ReactElement { sorting, flamegraphProfiles.threadId, view, - segment, + transactionResult, configSpaceQueryParam, ]); @@ -692,7 +670,7 @@ export function ContinuousFlamegraph(): ReactElement { flamegraphCanvas, flamegraphTheme, profile, - segment, + transactionResult, configSpaceQueryParam, ] ); @@ -884,7 +862,7 @@ export function ContinuousFlamegraph(): ReactElement { barHeight: flamegraphTheme.SIZES.SPANS_BAR_HEIGHT, depthOffset: flamegraphTheme.SIZES.SPANS_DEPTH_OFFSET, configSpaceTransform: getTransactionOffset( - segment.type === 'resolved' ? segment.data : null, + transactionResult.data.transactionSpan, profileTimestamp, configSpaceQueryParam[0] ), @@ -906,7 +884,7 @@ export function ContinuousFlamegraph(): ReactElement { flamegraphTheme.SIZES, profileTimestamp, configSpaceQueryParam, - segment, + transactionResult, ] ); @@ -1554,7 +1532,7 @@ export function ContinuousFlamegraph(): ReactElement { setSpansCanvasRef={setSpansCanvasRef} canvasPoolManager={canvasPoolManager} spansView={spansView} - spansRequestState={segment} + spansRequestState={transactionResult} /> ) : null } @@ -1596,7 +1574,6 @@ export function ContinuousFlamegraph(): ReactElement { } flamegraphDrawer={ { beforeEach(() => { const project = ProjectFixture({slug: 'foo-project'}); act(() => ProjectsStore.loadInitialData([project])); - setWindowLocation('http://localhost/'); }); it('renders a missing profile', async () => { @@ -99,6 +97,11 @@ describe('Flamegraph', () => { statusCode: 404, }); + MockApiClient.addMockResponse({ + url: '/organizations/org-slug/events/', + body: {data: []}, + }); + render(, { initialRouterConfig: { location: { @@ -128,8 +131,8 @@ describe('Flamegraph', () => { }); MockApiClient.addMockResponse({ - url: `/projects/org-slug/foo-project/events/${flamechart.transaction.id}/`, - statusCode: 404, + url: '/organizations/org-slug/events/', + body: {data: []}, }); render(, { @@ -162,18 +165,21 @@ describe('Flamegraph', () => { }); MockApiClient.addMockResponse({ - url: `/projects/org-slug/foo-project/events/${flamechart.transaction.id}/`, - statusCode: 404, + url: '/organizations/org-slug/events/', + body: {data: []}, }); - setWindowLocation( - 'http://localhost/?colorCoding=by+library&query=&sorting=alphabetical&tid=0&view=bottom+up' - ); - render(, { initialRouterConfig: { location: { pathname: '/explore/profiling/profile/foo-project/profile-id/flamegraph/', + query: { + colorCoding: 'by library', + query: '', + sorting: 'alphabetical', + tid: '0', + view: 'bottom up', + }, }, route: '/explore/profiling/profile/:projectId/:eventId/', children: [ @@ -202,16 +208,17 @@ describe('Flamegraph', () => { }); MockApiClient.addMockResponse({ - url: `/projects/org-slug/foo-project/events/${flamechart.transaction.id}/`, - statusCode: 404, + url: '/organizations/org-slug/events/', + body: {data: []}, }); - setWindowLocation('http://localhost/?query=profiling+transaction'); - render(, { initialRouterConfig: { location: { pathname: '/explore/profiling/profile/foo-project/profile-id/flamegraph/', + query: { + query: 'profiling transaction', + }, }, route: '/explore/profiling/profile/:projectId/:eventId/', children: [ diff --git a/static/app/components/profiling/flamegraph/flamegraph.tsx b/static/app/components/profiling/flamegraph/flamegraph.tsx index ac91793b2781c4..0eff4441fd0844 100644 --- a/static/app/components/profiling/flamegraph/flamegraph.tsx +++ b/static/app/components/profiling/flamegraph/flamegraph.tsx @@ -22,9 +22,6 @@ import {FlamegraphViewSelectMenu} from 'sentry/components/profiling/flamegraph/f import {FlamegraphZoomView} from 'sentry/components/profiling/flamegraph/flamegraphZoomView'; import {FlamegraphZoomViewMinimap} from 'sentry/components/profiling/flamegraph/flamegraphZoomViewMinimap'; import {t} from 'sentry/locale'; -import type {RequestState} from 'sentry/types/core'; -import type {EntrySpans, EventTransaction} from 'sentry/types/event'; -import {EntryType} from 'sentry/types/event'; import {defined} from 'sentry/utils'; import { CanvasPoolManager, @@ -50,6 +47,10 @@ import { initializeFlamegraphRenderer, useResizeCanvasObserver, } from 'sentry/utils/profiling/gl/utils'; +import type { + TransactionResult, + TransactionSpan, +} from 'sentry/utils/profiling/hooks/useTransaction'; import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; import {FlamegraphRenderer2D} from 'sentry/utils/profiling/renderers/flamegraphRenderer2D'; import {FlamegraphRendererWebGL} from 'sentry/utils/profiling/renderers/flamegraphRendererWebGL'; @@ -63,6 +64,7 @@ import type {ProfilingFormatterUnit} from 'sentry/utils/profiling/units/units'; import {formatTo, fromNanoJoulesToWatts} from 'sentry/utils/profiling/units/units'; import {useDevicePixelRatio} from 'sentry/utils/useDevicePixelRatio'; import {useMemoWithPrevious} from 'sentry/utils/useMemoWithPrevious'; +import {SpanFields} from 'sentry/views/insights/types'; import {useProfileGroup} from 'sentry/views/profiling/profileGroupProvider'; import { useProfiles, @@ -81,44 +83,24 @@ const PROFILE_TYPE = 'transaction profile' as const; function getMaxConfigSpace( profileGroup: ProfileGroup, - transaction: EventTransaction | null, + transactionSpan: TransactionSpan | undefined, unit: ProfilingFormatterUnit | string ): Rect { - if (transaction) { + if (transactionSpan) { // TODO: Adjust the alignment based on the profile's timestamp if it does // not match the transaction's start timestamp - const transactionDuration = transaction.endTimestamp - transaction.startTimestamp; + const transactionDuration = + transactionSpan[SpanFields.PRECISE_FINISH_TS] - + transactionSpan[SpanFields.PRECISE_START_TS]; return new Rect(0, 0, formatTo(transactionDuration, 'seconds', unit), 0); } - // We have a transaction, so we should do our best to align the profile - // with the transaction's timeline. - const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration)); // No transaction was found, so best we can do is align it to the starting // position of the profiles - find the max of profile durations + const maxProfileDuration = Math.max(...profileGroup.profiles.map(p => p.duration)); return new Rect(0, 0, maxProfileDuration, 0); } -function collectAllSpanEntriesFromTransaction( - transaction: EventTransaction -): EntrySpans['data'] { - if (!transaction.entries.length) { - return []; - } - - const spans = transaction.entries.filter( - (e): e is EntrySpans => e.type === EntryType.SPANS - ); - - let allSpans: EntrySpans['data'] = []; - - for (const span of spans) { - allSpans = allSpans.concat(span.data); - } - - return allSpans; -} - function convertProfileMeasurementsToUIFrames( measurement: ProfileGroup['measurements']['slow_frame_renders'] ): UIFrameMeasurements | undefined { @@ -181,12 +163,13 @@ function findLongestMatchingFrame( function computeProfileOffset( profileStart: string | undefined, flamegraph: FlamegraphModel, - transaction: RequestState + transactionResult: TransactionResult ): number { let offset = flamegraph.profile.startedAt; - const transactionStart = - transaction.type === 'resolved' ? (transaction.data?.startTimestamp ?? null) : null; + const transactionStart = transactionResult.isPending + ? null + : (transactionResult.data.transactionSpan?.[SpanFields.PRECISE_START_TS] ?? null); if ( defined(transactionStart) && @@ -213,7 +196,7 @@ const noopFormatDuration = () => ''; function Flamegraph(): ReactElement { const devicePixelRatio = useDevicePixelRatio(); - const profiledTransaction = useProfileTransaction(); + const transactionResult = useProfileTransaction(); const dispatch = useDispatchFlamegraphState(); const profiles = useProfiles(); @@ -286,15 +269,15 @@ function Flamegraph(): ReactElement { }, [profileGroup, flamegraphProfiles.threadId]); const spanTree = useMemo(() => { - if (profiledTransaction.type === 'resolved' && profiledTransaction.data) { + if (!transactionResult.isPending && transactionResult.data.transactionSpan) { return new SpanTree( - profiledTransaction.data, - collectAllSpanEntriesFromTransaction(profiledTransaction.data) + transactionResult.data.transactionSpan, + transactionResult.data.childSpans ); } return LOADING_OR_FALLBACK_SPAN_TREE; - }, [profiledTransaction]); + }, [transactionResult]); const spanChart = useMemo(() => { if (!profile) { @@ -305,11 +288,11 @@ function Flamegraph(): ReactElement { unit: profile.unit, configSpace: getMaxConfigSpace( profileGroup, - profiledTransaction.type === 'resolved' ? profiledTransaction.data : null, + transactionResult.data.transactionSpan, profile.unit ), }); - }, [spanTree, profile, profileGroup, profiledTransaction]); + }, [spanTree, profile, profileGroup, transactionResult]); const flamegraph = useMemo(() => { if (typeof flamegraphProfiles.threadId !== 'number') { @@ -324,10 +307,7 @@ function Flamegraph(): ReactElement { // Wait for the transaction to finish loading, regardless of the results. // Otherwise, the rendered profile will probably shift once the transaction loads. - if ( - profiledTransaction.type === 'loading' || - profiledTransaction.type === 'initial' - ) { + if (transactionResult.isPending) { return LOADING_OR_FALLBACK_FLAMEGRAPH; } @@ -347,7 +327,7 @@ function Flamegraph(): ReactElement { sort: sorting, configSpace: getMaxConfigSpace( profileGroup, - profiledTransaction.type === 'resolved' ? profiledTransaction.data : null, + transactionResult.data.transactionSpan, profile.unit ), }); @@ -358,7 +338,7 @@ function Flamegraph(): ReactElement { }, [ profile, profileGroup, - profiledTransaction, + transactionResult, sorting, flamegraphProfiles.threadId, view, @@ -369,9 +349,9 @@ function Flamegraph(): ReactElement { computeProfileOffset( profileGroup.metadata.timestamp, flamegraph, - profiledTransaction + transactionResult ), - [flamegraph, profiledTransaction, profileGroup.metadata.timestamp] + [flamegraph, transactionResult, profileGroup.metadata.timestamp] ); const uiFrames = useMemo(() => { @@ -1504,7 +1484,7 @@ function Flamegraph(): ReactElement { setSpansCanvasRef={setSpansCanvasRef} canvasPoolManager={canvasPoolManager} spansView={spansView} - spansRequestState={profiledTransaction} + spansRequestState={transactionResult} /> ) : null } @@ -1546,7 +1526,7 @@ function Flamegraph(): ReactElement { } flamegraphDrawer={ string; profileGroup: ProfileGroup; - profileTransaction: ReturnType | null; referenceNode: FlamegraphFrame; rootNodes: FlamegraphFrame[]; onResize?: MouseEventHandler; onResizeReset?: MouseEventHandler; + transactionSpan?: TransactionSpan; } const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerProps) { @@ -268,11 +268,7 @@ const FlamegraphDrawer = memo(function FlamegraphDrawer(props: FlamegraphDrawerP /> {props.profileGroup.type === 'transaction' ? ( diff --git a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx index 70edbe4941b219..dc418ddd1cf5ae 100644 --- a/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphDrawer/profileDetails.tsx @@ -10,13 +10,12 @@ import {DateTime} from 'sentry/components/dateTime'; import ProjectBadge from 'sentry/components/idBadge/projectBadge'; import {Version} from 'sentry/components/version'; import {t} from 'sentry/locale'; -import type {EventTransaction} from 'sentry/types/event'; -import {DeviceContextKey} from 'sentry/types/event'; import type {Organization} from 'sentry/types/organization'; import type {Project} from 'sentry/types/project'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import type {FlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/flamegraphStateProvider/reducers/flamegraphPreferences'; import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences'; +import type {TransactionSpan} from 'sentry/utils/profiling/hooks/useTransaction'; import type {ProfileGroup} from 'sentry/utils/profiling/profile/importProfile'; import {makeFormatter} from 'sentry/utils/profiling/units/units'; import {useLocation} from 'sentry/utils/useLocation'; @@ -27,6 +26,7 @@ import {useResizableDrawer} from 'sentry/utils/useResizableDrawer'; import {formatVersion} from 'sentry/utils/versions/formatVersion'; import {QuickContextHoverWrapper} from 'sentry/views/discover/table/quickContext/quickContextWrapper'; import {ContextType} from 'sentry/views/discover/table/quickContext/utils'; +import {SpanFields} from 'sentry/views/insights/types'; import {makeProjectsPathname} from 'sentry/views/projects/pathname'; import {makeReleasesPathname} from 'sentry/views/releases/utils/pathnames'; @@ -53,7 +53,7 @@ function renderValue( interface ProfileDetailsProps { profileGroup: ProfileGroup; projectId: string; - transaction: EventTransaction | null; + transactionSpan: TransactionSpan | null; } export function ProfileDetails(props: ProfileDetailsProps) { @@ -153,29 +153,29 @@ export function ProfileDetails(props: ProfileDetailsProps) { /> - {!props.transaction && detailsTab === 'environment' && ( + {!props.transactionSpan && detailsTab === 'environment' && ( )} - {!props.transaction && detailsTab === 'transaction' && ( + {!props.transactionSpan && detailsTab === 'transaction' && ( )} - {props.transaction && detailsTab === 'environment' && ( + {props.transactionSpan && detailsTab === 'environment' && ( )} - {props.transaction && detailsTab === 'transaction' && ( + {props.transactionSpan && detailsTab === 'transaction' && ( )} @@ -184,15 +184,13 @@ export function ProfileDetails(props: ProfileDetailsProps) { function TransactionDeviceDetails({ profileGroup, - transaction, + transactionSpan, }: { profileGroup: ProfileGroup; - transaction: EventTransaction; + transactionSpan: TransactionSpan; }) { const deviceDetails = useMemo(() => { const profileMetadata = profileGroup.metadata; - const deviceContext = transaction.contexts.device; - const osContext = transaction.contexts.os; const details: Array<{ key: string; @@ -202,13 +200,13 @@ function TransactionDeviceDetails({ { key: 'model', label: t('Model'), - value: deviceContext?.[DeviceContextKey.MODEL] ?? profileMetadata.deviceModel, + value: transactionSpan[SpanFields.DEVICE_MODEL] || profileMetadata.deviceModel, }, { key: 'manufacturer', label: t('Manufacturer'), value: - deviceContext?.[DeviceContextKey.MANUFACTURER] ?? + transactionSpan[SpanFields.DEVICE_MANUFACTURER] || profileMetadata.deviceManufacturer, }, { @@ -219,12 +217,12 @@ function TransactionDeviceDetails({ { key: 'name', label: t('OS'), - value: osContext?.name ?? profileMetadata.deviceOSName, + value: transactionSpan[SpanFields.OS_NAME] || profileMetadata.deviceOSName, }, { key: 'version', label: t('OS Version'), - value: osContext?.version ?? profileMetadata.deviceOSVersion, + value: profileMetadata.deviceOSVersion, }, { key: 'locale', @@ -234,7 +232,7 @@ function TransactionDeviceDetails({ ]; return details; - }, [profileGroup, transaction]); + }, [profileGroup, transactionSpan]); return ( @@ -252,24 +250,25 @@ function TransactionEventDetails({ organization, profileGroup, project, - transaction, + transactionSpan: transaction, }: { organization: Organization; profileGroup: ProfileGroup; project: Project | undefined; - transaction: EventTransaction; + transactionSpan: TransactionSpan; }) { const location = useLocation(); const transactionDetails = useMemo(() => { const profileMetadata = profileGroup.metadata; - const traceSlug = transaction.contexts?.trace?.trace_id ?? ''; + const traceSlug = transaction[SpanFields.TRACE] ?? ''; + const eventId = transaction[SpanFields.TRANSACTION_EVENT_ID]; const transactionTarget = - transaction.id && project && organization + eventId && project && organization ? generateLinkToEventInTraceView({ - eventId: transaction.id, + eventId, traceSlug, - timestamp: transaction.endTimestamp, + timestamp: transaction[SpanFields.PRECISE_FINISH_TS], location, organization, }) @@ -284,15 +283,15 @@ function TransactionEventDetails({ key: 'transaction', label: t('Transaction'), value: transactionTarget ? ( - {transaction.title} + {transaction[SpanFields.SPAN_DESCRIPTION]} ) : ( - transaction.title + transaction[SpanFields.SPAN_DESCRIPTION] ), }, { key: 'timestamp', label: t('Timestamp'), - value: , + value: , }, { key: 'project', @@ -302,28 +301,28 @@ function TransactionEventDetails({ { key: 'release', label: t('Release'), - value: transaction.release && ( + value: transaction[SpanFields.RELEASE] && ( - + ), }, { key: 'environment', label: t('Environment'), - value: - transaction.tags.find(({key}) => key === 'environment')?.value ?? - profileMetadata.environment, + value: transaction[SpanFields.ENVIRONMENT] || profileMetadata.environment, }, { key: 'duration', label: t('Duration'), value: msFormatter( - (transaction.endTimestamp - transaction.startTimestamp) * 1000 + (transaction[SpanFields.PRECISE_FINISH_TS] - + transaction[SpanFields.PRECISE_START_TS]) * + 1000 ), }, { @@ -369,15 +368,15 @@ function ProfileEventDetails({ organization, profileGroup, project, - transaction, + transactionSpan, }: { organization: Organization; profileGroup: ProfileGroup; project: Project | undefined; - transaction: EventTransaction | null; + transactionSpan: TransactionSpan | null; }) { const location = useLocation(); - const traceSlug = transaction?.contexts?.trace?.trace_id ?? ''; + const traceSlug = transactionSpan?.[SpanFields.TRACE] ?? ''; return ( {Object.entries(PROFILE_DETAILS_KEY).map(([label, key]) => { @@ -400,12 +399,13 @@ function ProfileEventDetails({ } } if (key === 'transactionName') { + const eventId = transactionSpan?.[SpanFields.TRANSACTION_EVENT_ID]; const transactionTarget = - project?.slug && transaction?.id && organization + project?.slug && eventId && organization ? generateLinkToEventInTraceView({ traceSlug, - eventId: transaction.id, - timestamp: transaction.endTimestamp, + eventId, + timestamp: transactionSpan[SpanFields.PRECISE_FINISH_TS], location, organization, }) diff --git a/static/app/components/profiling/flamegraph/flamegraphSpanTooltip.tsx b/static/app/components/profiling/flamegraph/flamegraphSpanTooltip.tsx index 11c61e384ca690..9f50bdd7edeb99 100644 --- a/static/app/components/profiling/flamegraph/flamegraphSpanTooltip.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphSpanTooltip.tsx @@ -9,6 +9,7 @@ import {formatColorForSpan} from 'sentry/utils/profiling/gl/utils'; import type {SpanChartRenderer2D} from 'sentry/utils/profiling/renderers/spansRenderer'; import type {SpanChart, SpanChartNode} from 'sentry/utils/profiling/spanChart'; import {Rect} from 'sentry/utils/profiling/speedscope'; +import {SpanFields} from 'sentry/views/insights/types'; import { FlamegraphTooltipColorIndicator, @@ -51,7 +52,7 @@ export function FlamegraphSpanTooltip({ - {hoveredNode.node.span.op ? `${t('op')}:${hoveredNode.node.span.op} ` : null} - {hoveredNode.node.span.status - ? `${t('status')}:${hoveredNode.node.span.status}` + {hoveredNode.node.span[SpanFields.SPAN_OP] + ? `${t('op')}:${hoveredNode.node.span[SpanFields.SPAN_OP]} ` : null} diff --git a/static/app/components/profiling/flamegraph/flamegraphSpans.tsx b/static/app/components/profiling/flamegraph/flamegraphSpans.tsx index e27993b61e988b..68e7145bf3c7f3 100644 --- a/static/app/components/profiling/flamegraph/flamegraphSpans.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphSpans.tsx @@ -9,7 +9,6 @@ import {normalizeDateTimeParams} from 'sentry/components/pageFilters/parse'; import {usePageFilters} from 'sentry/components/pageFilters/usePageFilters'; import {SpansContextMenu} from 'sentry/components/profiling/flamegraph/flamegraphSpansContextMenu'; import {t} from 'sentry/locale'; -import type {RequestState} from 'sentry/types/core'; import type {CanvasPoolManager} from 'sentry/utils/profiling/canvasScheduler'; import {useCanvasScheduler} from 'sentry/utils/profiling/canvasScheduler'; import type {CanvasView} from 'sentry/utils/profiling/canvasView'; @@ -28,6 +27,7 @@ import type {SpanChart, SpanChartNode} from 'sentry/utils/profiling/spanChart'; import {Rect} from 'sentry/utils/profiling/speedscope'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; +import {SpanFields} from 'sentry/views/insights/types'; import {TraceViewSources} from 'sentry/views/performance/newTraceDetails/traceHeader/breadcrumbs'; import {getTraceDetailsUrl} from 'sentry/views/performance/traceDetails/utils'; @@ -49,7 +49,7 @@ interface FlamegraphSpansProps { spanChart: SpanChart; spansCanvas: FlamegraphCanvas | null; spansCanvasRef: HTMLCanvasElement | null; - spansRequestState: RequestState; + spansRequestState: {isError: boolean; isPending: boolean}; spansView: CanvasView | null; } @@ -136,7 +136,7 @@ export function FlamegraphSpans({ return undefined; } - if (spansRequestState.type !== 'resolved') { + if (spansRequestState.isPending || spansRequestState.isError) { return undefined; } const clearCanvas = () => { @@ -181,7 +181,8 @@ export function FlamegraphSpans({ spansView, spansTextRenderer, flamegraphSearch.results.spans, - spansRequestState.type, + spansRequestState.isPending, + spansRequestState.isError, ]); // When spans render, check for span_id presence in qs. @@ -199,10 +200,10 @@ export function FlamegraphSpans({ } const span = spanChart.spans.find(s => { - if ('span_id' in s.node.span && s.node.span.span_id === span_id) { + if (s.node.span[SpanFields.SPAN_ID] === span_id) { return true; } - if ('event_id' in s.node.span && s.node.span.event_id === span_id) { + if (s.node.span[SpanFields.TRANSACTION_EVENT_ID] === span_id) { return true; } return false; @@ -391,7 +392,8 @@ export function FlamegraphSpans({ }); const onCopyDescription = useCallback(() => { - const value = hoveredNodeOnContextMenuOpen.current?.node.span.description ?? ''; + const value = + hoveredNodeOnContextMenuOpen.current?.node.span[SpanFields.SPAN_DESCRIPTION] ?? ''; if (!value) { addErrorMessage(t('Event description value is empty.')); return; @@ -408,7 +410,8 @@ export function FlamegraphSpans({ }, [hoveredNodeOnContextMenuOpen]); const onCopyOperation = useCallback(() => { - const value = hoveredNodeOnContextMenuOpen.current?.node.span.op ?? ''; + const value = + hoveredNodeOnContextMenuOpen.current?.node.span[SpanFields.SPAN_OP] ?? ''; if (!value) { addErrorMessage(t('Event operation value is empty.')); return; @@ -426,8 +429,8 @@ export function FlamegraphSpans({ const onCopyEventId = useCallback(() => { const value = - hoveredNodeOnContextMenuOpen.current?.node.span.event_id ?? - hoveredNodeOnContextMenuOpen.current?.node.span.span_id ?? + hoveredNodeOnContextMenuOpen.current?.node.span[SpanFields.TRANSACTION_EVENT_ID] ?? + hoveredNodeOnContextMenuOpen.current?.node.span[SpanFields.SPAN_ID] ?? ''; if (!value) { addErrorMessage(t('Event ID value is empty.')); @@ -453,17 +456,23 @@ export function FlamegraphSpans({ const nodePath: Record = {}; - if (node.span.op === 'transaction' && node.span.event_id) { + if ( + node.span[SpanFields.SPAN_OP] === 'transaction' && + node.span[SpanFields.TRANSACTION_EVENT_ID] + ) { // If the user clicks on a transaction, we can directly use the event_id - nodePath.eventId = node.span.event_id; - } else if (node.span.span_id) { + nodePath.eventId = node.span[SpanFields.TRANSACTION_EVENT_ID]; + } else if (node.span[SpanFields.SPAN_ID]) { // If the user clicks on a span, we need to traverse up the tree to find the transaction so that // the trace view knows which transaction to load and what span to point to. - nodePath.spanId = node.span.span_id; + nodePath.spanId = node.span[SpanFields.SPAN_ID]; let parent = node.parent; while (parent) { - if (parent.span.op === 'transaction' && parent.span.event_id) { - nodePath.eventId = parent.span.event_id; + if ( + parent.span[SpanFields.SPAN_OP] === 'transaction' && + parent.span[SpanFields.TRANSACTION_EVENT_ID] + ) { + nodePath.eventId = parent.span[SpanFields.TRANSACTION_EVENT_ID]; break; } parent = parent.parent; @@ -471,11 +480,11 @@ export function FlamegraphSpans({ } const link = getTraceDetailsUrl({ - traceSlug: node.span.trace_id, + traceSlug: node.span[SpanFields.TRACE], dateSelection: normalizeDateTimeParams(pageFilters.selection.datetime), location, organization, - timestamp: node.span.timestamp, + timestamp: node.span[SpanFields.PRECISE_FINISH_TS], source: TraceViewSources.PROFILING_FLAMEGRAPH, ...nodePath, }); @@ -504,13 +513,13 @@ export function FlamegraphSpans({ cursor={lastInteraction === 'pan' ? 'grabbing' : 'default'} /> {/* transaction loads after profile, so we want to show loading even if it's in initial state */} - {spansRequestState.type === 'loading' || spansRequestState.type === 'initial' ? ( + {spansRequestState.isPending ? ( - ) : spansRequestState.type === 'errored' ? ( + ) : spansRequestState.isError ? ( {t('No associated transaction found')} - ) : spansRequestState.type === 'resolved' && spanChart.spans.length < 1 ? ( + ) : spanChart.spans.length < 1 ? ( {t('Transaction has no spans')} diff --git a/static/app/components/profiling/flamegraph/flamegraphSpansContextMenu.tsx b/static/app/components/profiling/flamegraph/flamegraphSpansContextMenu.tsx index fac2e5787bbaa1..f58fef453d55c2 100644 --- a/static/app/components/profiling/flamegraph/flamegraphSpansContextMenu.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphSpansContextMenu.tsx @@ -11,12 +11,13 @@ import {IconCopy, IconOpen} from 'sentry/icons'; import {t, tct} from 'sentry/locale'; import type {useContextMenu} from 'sentry/utils/profiling/hooks/useContextMenu'; import type {SpanChartNode} from 'sentry/utils/profiling/spanChart'; +import {SpanFields} from 'sentry/views/insights/types'; function getNodeType(node: SpanChartNode | null) { if (!node) { return ''; } - if (node.node.span.op === 'transaction') { + if (node.node.span[SpanFields.SPAN_OP] === 'transaction') { return t('Transaction'); } return t('Span'); @@ -63,8 +64,8 @@ export function SpansContextMenu(props: SpansContextMenuProps) { { @@ -78,7 +79,7 @@ export function SpansContextMenu(props: SpansContextMenuProps) { {tct('Copy Event ID', {type: title})} { props.onCopyDescription(); @@ -91,7 +92,7 @@ export function SpansContextMenu(props: SpansContextMenuProps) { {tct('Copy [type] Description', {type: title})} { props.onCopyOperation(); diff --git a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx index d266dd4d03ff72..f1508cee2bd6a8 100644 --- a/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx +++ b/static/app/components/profiling/flamegraph/flamegraphToolbar/flamegraphSearch.tsx @@ -17,6 +17,7 @@ import {memoizeByReference} from 'sentry/utils/profiling/profile/utils'; import type {SpanChart, SpanChartNode} from 'sentry/utils/profiling/spanChart'; import {parseRegExp} from 'sentry/utils/profiling/validators/regExp'; import {fzf} from 'sentry/utils/search/fzf'; +import {SpanFields} from 'sentry/views/insights/types'; function isFlamegraphFrame( frame: FlamegraphFrame | SpanChartNode @@ -53,7 +54,7 @@ function searchSpanFzf( return match; } - matches.set(span.node.span.span_id, { + matches.set(span.node.span[SpanFields.SPAN_ID], { span, match: match.matches, }); @@ -105,7 +106,7 @@ function searchSpanRegExp( return match; } - matches.set(span.node.span.span_id, { + matches.set(span.node.span[SpanFields.SPAN_ID], { span, match: [match], }); diff --git a/static/app/components/profiling/profileHeader.tsx b/static/app/components/profiling/profileHeader.tsx index bd412bf6c5e3f1..9e6d8b2602dfd9 100644 --- a/static/app/components/profiling/profileHeader.tsx +++ b/static/app/components/profiling/profileHeader.tsx @@ -7,12 +7,12 @@ import {FeedbackButton} from 'sentry/components/feedbackButton/feedbackButton'; import * as Layout from 'sentry/components/layouts/thirds'; import {ProfilingBreadcrumbs} from 'sentry/components/profiling/profilingBreadcrumbs'; import {t} from 'sentry/locale'; -import type {Event} from 'sentry/types/event'; import {trackAnalytics} from 'sentry/utils/analytics'; import {generateLinkToEventInTraceView} from 'sentry/utils/discover/urls'; import {isSchema, isSentrySampledProfile} from 'sentry/utils/profiling/guards/profile'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; +import type {SpanResponse} from 'sentry/views/insights/types'; import {TopBar} from 'sentry/views/navigation/topBar'; import {useHasPageFrameFeature} from 'sentry/views/navigation/useHasPageFrameFeature'; import {useProfiles} from 'sentry/views/profiling/profilesProvider'; @@ -31,10 +31,12 @@ function getTransactionName(input: Profiling.ProfileInput): string { interface ProfileHeaderProps { eventId: string; projectId: string; - transaction: Event | null; + transactionSpan: + | Pick + | undefined; } -function ProfileHeader({transaction, projectId, eventId}: ProfileHeaderProps) { +function ProfileHeader({transactionSpan, projectId, eventId}: ProfileHeaderProps) { const location = useLocation(); const organization = useOrganization(); const profiles = useProfiles(); @@ -45,11 +47,11 @@ function ProfileHeader({transaction, projectId, eventId}: ProfileHeaderProps) { const profileId = eventId ?? ''; const projectSlug = projectId ?? ''; - const transactionTarget = transaction?.id + const transactionTarget = transactionSpan?.span_id ? generateLinkToEventInTraceView({ - timestamp: transaction.endTimestamp ?? '', - eventId: transaction.id, - traceSlug: transaction.contexts?.trace?.trace_id ?? '', + timestamp: transactionSpan['precise.finish_ts'], + targetId: transactionSpan.span_id, + traceSlug: transactionSpan.trace, location, organization, }) diff --git a/static/app/utils/profiling/colors/utils.tsx b/static/app/utils/profiling/colors/utils.tsx index 98631dd92cc76b..b6035eae3019dd 100644 --- a/static/app/utils/profiling/colors/utils.tsx +++ b/static/app/utils/profiling/colors/utils.tsx @@ -4,7 +4,8 @@ import type { FlamegraphTheme, } from 'sentry/utils/profiling/flamegraph/flamegraphTheme'; import type {FlamegraphFrame} from 'sentry/utils/profiling/flamegraphFrame'; -import type {SpanChart, SpanChartNode} from 'sentry/utils/profiling/spanChart'; +import type {SpanChart} from 'sentry/utils/profiling/spanChart'; +import {SpanFields} from 'sentry/views/insights/types'; function uniqueCountBy( arr: readonly T[], @@ -384,12 +385,12 @@ export function makeColorMapByFrequency( export function makeSpansColorMapByOpAndDescription( spans: ReadonlyArray, colorBucket: FlamegraphTheme['COLORS']['COLOR_BUCKET'] -): Map { - const colors = new Map(); - const uniqueSpans = uniqueBy(spans, s => s.node.span.op ?? ''); +): Map { + const colors = new Map(); + const uniqueSpans = uniqueBy(spans, s => s.node.span[SpanFields.SPAN_OP] ?? ''); for (let i = 0; i < uniqueSpans.length; i++) { - const key = uniqueSpans[i]!.node.span.op ?? ''; + const key = uniqueSpans[i]!.node.span[SpanFields.SPAN_OP] ?? ''; if (key === 'missing span instrumentation') { continue; } @@ -397,7 +398,10 @@ export function makeSpansColorMapByOpAndDescription( } for (const span of spans) { - colors.set(span.node.span.span_id, colors.get(span.node.span.op ?? '')!); + colors.set( + span.node.span[SpanFields.SPAN_ID], + colors.get(span.node.span[SpanFields.SPAN_OP] ?? '')! + ); } return colors; diff --git a/static/app/utils/profiling/hooks/useTransaction.tsx b/static/app/utils/profiling/hooks/useTransaction.tsx new file mode 100644 index 00000000000000..b1ca8d5c216e0d --- /dev/null +++ b/static/app/utils/profiling/hooks/useTransaction.tsx @@ -0,0 +1,63 @@ +import {useMemo} from 'react'; + +import {MutableSearch} from 'sentry/utils/tokenizeSearch'; +import {useSpans} from 'sentry/views/insights/common/queries/useDiscover'; +import {SpanFields} from 'sentry/views/insights/types'; + +export type TransactionResult = ReturnType; +export type TransactionData = TransactionResult['data']; +export type TransactionSpan = NonNullable; + +export function useTransaction(transactionId?: string, traceId?: string) { + const search = useMemo(() => { + if (!transactionId || !traceId) { + return undefined; + } + const s = new MutableSearch(''); + s.setFilterValues(SpanFields.TRACE, [traceId]); + s.setFilterValues('transaction.event_id', [transactionId]); + return s; + }, [transactionId, traceId]); + + const result = useSpans( + { + search, + fields: [ + SpanFields.TRACE, + SpanFields.SPAN_ID, + SpanFields.IS_TRANSACTION, + SpanFields.SPAN_OP, + SpanFields.SPAN_DESCRIPTION, + SpanFields.PRECISE_START_TS, + SpanFields.PRECISE_FINISH_TS, + SpanFields.SPAN_SELF_TIME, + SpanFields.SDK_NAME, + SpanFields.TRACE_PARENT_SPAN, + SpanFields.TRANSACTION_EVENT_ID, + SpanFields.RELEASE, + SpanFields.ENVIRONMENT, + SpanFields.OS_NAME, + SpanFields.DEVICE_MODEL, + SpanFields.DEVICE_MANUFACTURER, + ], + sorts: [ + { + field: SpanFields.PRECISE_START_TS, + kind: 'asc', + }, + ], + queryWithoutPageFilters: true, + enabled: !!search, + }, + 'api.profiles.transaction' + ); + const transactionSpan = result.data.find(row => row[SpanFields.IS_TRANSACTION]); + const childSpans = result.data.filter(row => !row[SpanFields.IS_TRANSACTION]); + return { + ...result, + data: { + childSpans, + transactionSpan, + }, + }; +} diff --git a/static/app/utils/profiling/renderers/spansRenderer.tsx b/static/app/utils/profiling/renderers/spansRenderer.tsx index 5352d46ac3eb5b..354e1e5df9ac65 100644 --- a/static/app/utils/profiling/renderers/spansRenderer.tsx +++ b/static/app/utils/profiling/renderers/spansRenderer.tsx @@ -6,6 +6,7 @@ import type {FlamegraphTheme} from 'sentry/utils/profiling/flamegraph/flamegraph import {getContext, resizeCanvasToDisplaySize} from 'sentry/utils/profiling/gl/utils'; import type {SpanChart, SpanChartNode} from 'sentry/utils/profiling/spanChart'; import {Rect} from 'sentry/utils/profiling/speedscope'; +import {SpanFields} from 'sentry/views/insights/types'; // Convert color component from 0-1 to 0-255 range function colorComponentsToRgba(color: number[] | undefined): string { @@ -94,11 +95,12 @@ export class SpanChartRenderer2D { } getColorForFrame(span: SpanChartNode): number[] | CanvasPattern { - if (span.node.span.op === 'missing-instrumentation') { + if (span.node.span[SpanFields.SPAN_OP] === 'missing-instrumentation') { return this.pattern; } return ( - this.colors.get(span.node.span.span_id) ?? this.theme.COLORS.FRAME_FALLBACK_COLOR + this.colors.get(span.node.span[SpanFields.SPAN_ID]) ?? + this.theme.COLORS.FRAME_FALLBACK_COLOR ); } @@ -183,13 +185,14 @@ export class SpanChartRenderer2D { ); const color = - this.colors.get(span.node.span.span_id) ?? this.theme.COLORS.SPAN_FALLBACK_COLOR; + this.colors.get(span.node.span[SpanFields.SPAN_ID]) ?? + this.theme.COLORS.SPAN_FALLBACK_COLOR; // Reset any transforms that may have been applied before. // If we dont do it, it sometimes causes the canvas to be drawn with a translation this.context.setTransform(1, 0, 0, 1, 0, 0); - if (span.node.span.op === 'missing span instrumentation') { + if (span.node.span[SpanFields.SPAN_OP] === 'missing span instrumentation') { this.context.beginPath(); this.context.rect( rect.x + BORDER_WIDTH / 2, @@ -204,7 +207,7 @@ export class SpanChartRenderer2D { this.context.beginPath(); this.context.fillStyle = - this.isSearching && !this.searchResults.has(span.node.span.span_id) + this.isSearching && !this.searchResults.has(span.node.span[SpanFields.SPAN_ID]) ? colorComponentsToRgba(this.theme.COLORS.FRAME_FALLBACK_COLOR) : colorComponentsToRgba(color); diff --git a/static/app/utils/profiling/renderers/spansTextRenderer.tsx b/static/app/utils/profiling/renderers/spansTextRenderer.tsx index 6fcc5021e1a066..561c5383a1acad 100644 --- a/static/app/utils/profiling/renderers/spansTextRenderer.tsx +++ b/static/app/utils/profiling/renderers/spansTextRenderer.tsx @@ -15,6 +15,7 @@ import type {Rect} from 'sentry/utils/profiling/speedscope'; import {findRangeBinarySearch} from 'sentry/utils/profiling/speedscope'; import {trimTextCenter} from 'sentry/utils/string/trimTextCenter'; import {ELLIPSIS} from 'sentry/utils/string/unicode'; +import {SpanFields} from 'sentry/views/insights/types'; class SpansTextRenderer extends TextRenderer { spanChart: SpanChart; @@ -138,7 +139,9 @@ class SpansTextRenderer extends TextRenderer { ); if (HAS_SEARCH_RESULTS) { - const frameResults = flamegraphSearchResults.get(span.node.span.span_id); + const frameResults = flamegraphSearchResults.get( + span.node.span[SpanFields.SPAN_ID] + ); if (frameResults) { this.context.fillStyle = HIGHLIGHT_BACKGROUND_COLOR; diff --git a/static/app/utils/profiling/spanChart.spec.tsx b/static/app/utils/profiling/spanChart.spec.tsx index 1a51cba3bb5358..9d6b0c6e87af0a 100644 --- a/static/app/utils/profiling/spanChart.spec.tsx +++ b/static/app/utils/profiling/spanChart.spec.tsx @@ -1,51 +1,33 @@ -import type {EntrySpans, EventTransaction} from 'sentry/types/event'; -import {EventOrGroupType} from 'sentry/types/event'; import {SpanChart} from 'sentry/utils/profiling/spanChart'; +import type {SpanNodeData, TransactionSpanData} from 'sentry/utils/profiling/spanTree'; import {SpanTree} from 'sentry/utils/profiling/spanTree'; +import {SpanFields} from 'sentry/views/insights/types'; import {Rect} from './speedscope'; -function s(partial: Partial): EntrySpans['data'][0] { +function s(partial: Partial = {}): SpanNodeData { return { - timestamp: 0, - start_timestamp: 0, - exclusive_time: 0, - description: '', - op: '', - span_id: '', - parent_span_id: '', - trace_id: '', - hash: '', - data: {}, + [SpanFields.PRECISE_FINISH_TS]: 0, + [SpanFields.PRECISE_START_TS]: 0, + [SpanFields.SPAN_DESCRIPTION]: '', + [SpanFields.SPAN_OP]: '', + [SpanFields.SPAN_ID]: '', + [SpanFields.TRACE_PARENT_SPAN]: '', + [SpanFields.TRACE]: '', + [SpanFields.TRANSACTION_EVENT_ID]: '', ...partial, }; } -function txn(partial: Partial): EventTransaction { +function txn(partial: Partial = {}): TransactionSpanData { return { - id: '', - projectID: '', - user: {}, - contexts: {}, - entries: [], - errors: [], - dateCreated: '', - startTimestamp: Date.now(), - endTimestamp: Date.now() + 1000, - title: '', - type: EventOrGroupType.TRANSACTION, - culprit: '', - dist: null, - eventID: '', - fingerprints: [], - dateReceived: new Date().toISOString(), - message: '', - metadata: {}, - size: 0, - tags: [], - occurrence: null, - location: '', - crashFile: null, + [SpanFields.SPAN_DESCRIPTION]: '', + [SpanFields.PRECISE_START_TS]: Date.now(), + [SpanFields.PRECISE_FINISH_TS]: Date.now() + 1000, + [SpanFields.SPAN_ID]: '', + [SpanFields.SPAN_SELF_TIME]: 0, + [SpanFields.TRACE]: '', + [SpanFields.SDK_NAME]: '', ...partial, }; } @@ -55,49 +37,49 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 0, - endTimestamp: start + 1, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 0, + [SpanFields.PRECISE_FINISH_TS]: start + 1, }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 1, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 1, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 0.5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 0.5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '3', - parent_span_id: '2', - timestamp: start + 0.2, - start_timestamp: start, + [SpanFields.SPAN_ID]: '3', + [SpanFields.TRACE_PARENT_SPAN]: '2', + [SpanFields.PRECISE_FINISH_TS]: start + 0.2, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '4', - parent_span_id: '1', - timestamp: start + 1, - start_timestamp: start + 0.5, + [SpanFields.SPAN_ID]: '4', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 1, + [SpanFields.PRECISE_START_TS]: start + 0.5, }), ] ); - expect(tree.root.children[0]!.children[1]!.span.span_id).toBe('4'); + expect(tree.root.children[0]!.children[1]!.span[SpanFields.SPAN_ID]).toBe('4'); const chart = new SpanChart(tree); chart.forEachSpanOfTree(chart.spanTrees[0]!, 0, span => { - if (span.node.span.span_id === '1') { + if (span.node.span[SpanFields.SPAN_ID] === '1') { expect(span.depth).toBe(1); - } else if (span.node.span.span_id === '2') { + } else if (span.node.span[SpanFields.SPAN_ID] === '2') { expect(span.depth).toBe(2); - } else if (span.node.span.span_id === '3') { + } else if (span.node.span[SpanFields.SPAN_ID] === '3') { expect(span.depth).toBe(3); - } else if (span.node.span.span_id === '4') { + } else if (span.node.span[SpanFields.SPAN_ID] === '4') { expect(span.depth).toBe(2); } }); @@ -107,28 +89,28 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 0, - endTimestamp: start + 10, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 0, + [SpanFields.PRECISE_FINISH_TS]: start + 10, }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '3', - parent_span_id: '2', - timestamp: start + 1, - start_timestamp: start, + [SpanFields.SPAN_ID]: '3', + [SpanFields.TRACE_PARENT_SPAN]: '2', + [SpanFields.PRECISE_FINISH_TS]: start + 1, + [SpanFields.PRECISE_START_TS]: start, }), ] ); @@ -141,34 +123,34 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 0, - endTimestamp: start + 1, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 0, + [SpanFields.PRECISE_FINISH_TS]: start + 1, }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 1, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 1, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 0.5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 0.5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '3', - parent_span_id: '2', - timestamp: start + 0.2, - start_timestamp: start, + [SpanFields.SPAN_ID]: '3', + [SpanFields.TRACE_PARENT_SPAN]: '2', + [SpanFields.PRECISE_FINISH_TS]: start + 0.2, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '4', - parent_span_id: '3', - timestamp: start + 0.1, - start_timestamp: start, + [SpanFields.SPAN_ID]: '4', + [SpanFields.TRACE_PARENT_SPAN]: '3', + [SpanFields.PRECISE_FINISH_TS]: start + 0.1, + [SpanFields.PRECISE_START_TS]: start, }), ] ); @@ -181,28 +163,28 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 0, - endTimestamp: start + 10, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 0, + [SpanFields.PRECISE_FINISH_TS]: start + 10, }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '3', - parent_span_id: '2', - timestamp: start + 1, - start_timestamp: start, + [SpanFields.SPAN_ID]: '3', + [SpanFields.TRACE_PARENT_SPAN]: '2', + [SpanFields.PRECISE_FINISH_TS]: start + 1, + [SpanFields.PRECISE_START_TS]: start, }), ] ); @@ -215,22 +197,22 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 5, - endTimestamp: start + 10, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 5, + [SpanFields.PRECISE_FINISH_TS]: start + 10, }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start + 6, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start + 6, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 9, - start_timestamp: start + 8, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 9, + [SpanFields.PRECISE_START_TS]: start + 8, }), ] ); @@ -247,16 +229,16 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 1, - endTimestamp: start + 11, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 1, + [SpanFields.PRECISE_FINISH_TS]: start + 11, }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 4, - start_timestamp: start + 2, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 4, + [SpanFields.PRECISE_START_TS]: start + 2, }), ] ); @@ -274,25 +256,25 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 0, - endTimestamp: start + 10, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 0, + [SpanFields.PRECISE_FINISH_TS]: start + 10, }), [ // If a span belongs to nothing, we dont render it, // for now we only render spans that ultimately belong // to the transaction when the parent_span_id is followed s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: undefined, - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), ] ); @@ -305,25 +287,25 @@ describe('spanChart', () => { const start = Date.now(); const tree = new SpanTree( txn({ - contexts: {trace: {span_id: 'root'}}, - startTimestamp: start + 0, - endTimestamp: start + 10, + [SpanFields.SPAN_ID]: 'root', + [SpanFields.PRECISE_START_TS]: start + 0, + [SpanFields.PRECISE_FINISH_TS]: start + 10, }), [ // These two spans overlap and as first is inserted, // the 2nd span can no longer be inserted and the parent_span_id // edge is no longer satisfied s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), ] ); diff --git a/static/app/utils/profiling/spanChart.tsx b/static/app/utils/profiling/spanChart.tsx index 4dff813d7afe66..8597626420a26d 100644 --- a/static/app/utils/profiling/spanChart.tsx +++ b/static/app/utils/profiling/spanChart.tsx @@ -5,6 +5,7 @@ import { makeFormatTo, makeTimelineFormatter, } from 'sentry/utils/profiling/units/units'; +import {SpanFields} from 'sentry/views/insights/types'; import type {Profile} from './profile/profile'; @@ -71,26 +72,32 @@ class SpanChart { const previousTreeNode = orphanTree.root; let previous: SpanChartNode | null = null; for (const span of tree.orphanedSpans) { - const duration = span.timestamp - span.start_timestamp; - const start = span.start_timestamp - tree.root.span.start_timestamp; + const duration = + span[SpanFields.PRECISE_FINISH_TS] - span[SpanFields.PRECISE_START_TS]; + const start = + span[SpanFields.PRECISE_START_TS] - tree.root.span[SpanFields.PRECISE_START_TS]; const end = start + duration; const spanFitsInPreviousRow = - previous && previous.node.span.timestamp < span.start_timestamp; + previous && + previous.node.span[SpanFields.PRECISE_FINISH_TS] < + span[SpanFields.PRECISE_START_TS]; const depth = spanFitsInPreviousRow ? this.depth : Math.max(this.depth, this.depth + 1); this.depth = depth; + const op = span[SpanFields.SPAN_OP]; + const description = span[SpanFields.SPAN_DESCRIPTION]; const spanChartNode: SpanChartNode = { duration: this.toFinalUnit(duration), start: this.toFinalUnit(start), end: this.toFinalUnit(end), text: - span.op && span.description - ? span.op + ': ' + span.description - : span.op || span.description || '', + op && description + ? op + ': ' + description + : op || description || '', node: new SpanTreeNode(span), depth, parent: this.root, @@ -104,8 +111,10 @@ class SpanChart { previousTreeNode.children.push( new SpanTreeNode({ ...span, - start_timestamp: previous!.node.span.timestamp, - timestamp: previous!.node.span.timestamp + duration, + [SpanFields.PRECISE_START_TS]: + previous!.node.span[SpanFields.PRECISE_FINISH_TS], + [SpanFields.PRECISE_FINISH_TS]: + previous!.node.span[SpanFields.PRECISE_FINISH_TS] + duration, }) ); } else { @@ -113,8 +122,9 @@ class SpanChart { orphanTree.root.children.push( new SpanTreeNode({ ...span, - start_timestamp: tree.root.span.start_timestamp, - timestamp: tree.root.span.start_timestamp + duration, + [SpanFields.PRECISE_START_TS]: tree.root.span[SpanFields.PRECISE_START_TS], + [SpanFields.PRECISE_FINISH_TS]: + tree.root.span[SpanFields.PRECISE_START_TS] + duration, }) ); } @@ -124,8 +134,8 @@ class SpanChart { } const duration = this.toFinalUnit( - Math.max(...this.spanTrees.map(t => t.root.span.timestamp)) - - Math.min(...this.spanTrees.map(t => t.root.span.start_timestamp)) + Math.max(...this.spanTrees.map(t => t.root.span[SpanFields.PRECISE_FINISH_TS])) - + Math.min(...this.spanTrees.map(t => t.root.span[SpanFields.PRECISE_START_TS])) ); this.configSpace = @@ -140,7 +150,7 @@ class SpanChart { depthOffset: number, cb: (node: SpanChartNode) => void ): number { - const transactionStart = tree.root.span.start_timestamp; + const transactionStart = tree.root.span[SpanFields.PRECISE_START_TS]; // We only want to collect the root most node once const queue: Array<[SpanChartNode | null, SpanTreeNode]> = @@ -156,22 +166,26 @@ class SpanChart { while (children_at_depth-- !== 0) { const [parent, node] = queue.shift()!; - const duration = node.span.timestamp - node.span.start_timestamp; - const start = node.span.start_timestamp - transactionStart; + const duration = + node.span[SpanFields.PRECISE_FINISH_TS] - + node.span[SpanFields.PRECISE_START_TS]; + const start = node.span[SpanFields.PRECISE_START_TS] - transactionStart; const end = start + duration; if (duration <= 0) { continue; } + const op = node.span[SpanFields.SPAN_OP]; + const description = node.span[SpanFields.SPAN_DESCRIPTION]; const spanChartNode: SpanChartNode = { duration: this.toFinalUnit(duration), start: this.toFinalUnit(start), end: this.toFinalUnit(end), text: - node.span.op && node.span.description - ? node.span.op + ': ' + node.span.description - : node.span.op || node.span.description || '', + op && description + ? op + ': ' + description + : op || description || '', node, depth: depth + depthOffset, parent, diff --git a/static/app/utils/profiling/spanTree.spec.tsx b/static/app/utils/profiling/spanTree.spec.tsx index d5d3124f8318f8..af2a0f87982d7d 100644 --- a/static/app/utils/profiling/spanTree.spec.tsx +++ b/static/app/utils/profiling/spanTree.spec.tsx @@ -1,49 +1,30 @@ -import type {EntrySpans, EventTransaction} from 'sentry/types/event'; -import {EventOrGroupType} from 'sentry/types/event'; +import type {SpanNodeData, TransactionSpanData} from 'sentry/utils/profiling/spanTree'; +import {SpanTree} from 'sentry/utils/profiling/spanTree'; +import {SpanFields} from 'sentry/views/insights/types'; -import {SpanTree} from './spanTree'; - -function s(partial: Partial): EntrySpans['data'][0] { +function s(partial: Partial = {}): SpanNodeData { return { - timestamp: 0, - start_timestamp: 0, - exclusive_time: 0, - description: '', - op: '', - span_id: '', - parent_span_id: '', - trace_id: '', - hash: '', - data: {}, + [SpanFields.PRECISE_FINISH_TS]: 0, + [SpanFields.PRECISE_START_TS]: 0, + [SpanFields.SPAN_DESCRIPTION]: '', + [SpanFields.SPAN_OP]: '', + [SpanFields.SPAN_ID]: '', + [SpanFields.TRACE_PARENT_SPAN]: '', + [SpanFields.TRACE]: '', + [SpanFields.TRANSACTION_EVENT_ID]: '', ...partial, }; } -function txn(partial: Partial): EventTransaction { +function txn(partial: Partial = {}): TransactionSpanData { return { - id: '', - projectID: '', - user: {}, - contexts: {}, - entries: [], - errors: [], - dateCreated: '', - startTimestamp: Date.now(), - endTimestamp: Date.now() + 1000, - title: '', - type: EventOrGroupType.TRANSACTION, - culprit: '', - dist: null, - eventID: '', - fingerprints: [], - dateReceived: new Date().toISOString(), - message: '', - metadata: {}, - size: 0, - tags: [], - occurrence: null, - location: '', - crashFile: null, + [SpanFields.SPAN_DESCRIPTION]: '', + [SpanFields.PRECISE_START_TS]: Date.now(), + [SpanFields.PRECISE_FINISH_TS]: Date.now() + 1000, + [SpanFields.SPAN_ID]: '', + [SpanFields.SPAN_SELF_TIME]: 0, + [SpanFields.TRACE]: '', + [SpanFields.SDK_NAME]: '', ...partial, }; } @@ -51,77 +32,81 @@ function txn(partial: Partial): EventTransaction { describe('SpanTree', () => { it('initializes the root to txn', () => { const transaction = txn({ - title: 'transaction root', - startTimestamp: Date.now(), - endTimestamp: Date.now() + 1000, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: Date.now(), + [SpanFields.PRECISE_FINISH_TS]: Date.now() + 1000, }); const tree = new SpanTree(transaction, []); - expect(tree.root.span.start_timestamp).toBe(transaction.startTimestamp); - expect(tree.root.span.timestamp).toBe(transaction.endTimestamp); + expect(tree.root.span[SpanFields.PRECISE_START_TS]).toBe( + transaction[SpanFields.PRECISE_START_TS] + ); + expect(tree.root.span[SpanFields.PRECISE_FINISH_TS]).toBe( + transaction[SpanFields.PRECISE_FINISH_TS] + ); }); it('appends to parent that contains span', () => { const start = Date.now(); const tree = new SpanTree( txn({ - title: 'transaction root', - startTimestamp: start, - endTimestamp: start + 10, - contexts: {trace: {span_id: 'root'}}, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: start, + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.SPAN_ID]: 'root', }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 5, + [SpanFields.PRECISE_START_TS]: start, }), ] ); expect(tree.orphanedSpans).toHaveLength(0); - expect(tree.root.children[0]!.span.span_id).toBe('1'); - expect(tree.root.children[0]!.children[0]!.span.span_id).toBe('2'); + expect(tree.root.children[0]!.span[SpanFields.SPAN_ID]).toBe('1'); + expect(tree.root.children[0]!.children[0]!.span[SpanFields.SPAN_ID]).toBe('2'); }); it('checks for span overlaps that contains span', () => { const start = Date.now(); const tree = new SpanTree( txn({ - title: 'transaction root', - startTimestamp: start, - endTimestamp: start + 10, - contexts: {trace: {span_id: 'root'}}, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: start, + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.SPAN_ID]: 'root', }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: '1', - timestamp: start + 5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '3', - parent_span_id: '1', - timestamp: start + 6, - start_timestamp: start + 1, + [SpanFields.SPAN_ID]: '3', + [SpanFields.TRACE_PARENT_SPAN]: '1', + [SpanFields.PRECISE_FINISH_TS]: start + 6, + [SpanFields.PRECISE_START_TS]: start + 1, }), ] ); expect(tree.orphanedSpans).toHaveLength(1); - expect(tree.root.children[0]!.span.span_id).toBe('1'); - expect(tree.root.children[0]!.children[0]!.span.span_id).toBe('2'); + expect(tree.root.children[0]!.span[SpanFields.SPAN_ID]).toBe('1'); + expect(tree.root.children[0]!.children[0]!.span[SpanFields.SPAN_ID]).toBe('2'); expect(tree.root.children[0]!.children[1]).toBeUndefined(); }); @@ -129,108 +114,115 @@ describe('SpanTree', () => { const start = Date.now(); const tree = new SpanTree( txn({ - title: 'transaction root', - startTimestamp: start, - endTimestamp: start + 10, - contexts: {trace: {span_id: 'root'}}, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: start, + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.SPAN_ID]: 'root', }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: 'root', - timestamp: start + 10, - start_timestamp: start + 6, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.PRECISE_START_TS]: start + 6, }), ] ); expect(tree.orphanedSpans).toHaveLength(0); - expect(tree.root.children[0]!.span.span_id).toBe('1'); - expect(tree.root.children[1]!.span.op).toBe('missing span instrumentation'); - expect(tree.root.children[2]!.span.span_id).toBe('2'); + expect(tree.root.children[0]!.span[SpanFields.SPAN_ID]).toBe('1'); + expect(tree.root.children[1]!.span[SpanFields.SPAN_OP]).toBe( + 'missing span instrumentation' + ); + expect(tree.root.children[2]!.span[SpanFields.SPAN_ID]).toBe('2'); }); it('does not create missing instrumentation if elapsed < threshold', () => { const start = Date.now(); const tree = new SpanTree( txn({ - title: 'transaction root', - startTimestamp: start, - endTimestamp: start + 10, - contexts: {trace: {span_id: 'root'}}, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: start, + [SpanFields.PRECISE_FINISH_TS]: start + 10, + [SpanFields.SPAN_ID]: 'root', }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 5, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 5, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: 'root', - timestamp: start + 10, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 10, // There is only 50ms difference here, 100ms is the threshold - start_timestamp: start + 5.05, + [SpanFields.PRECISE_START_TS]: start + 5.05, }), ] ); - expect(tree.root.children[0]!.span.span_id).toBe('1'); - expect(tree.root.children[1]!.span.span_id).toBe('2'); + expect(tree.root.children[0]!.span[SpanFields.SPAN_ID]).toBe('1'); + expect(tree.root.children[1]!.span[SpanFields.SPAN_ID]).toBe('2'); }); it('pushes consecutive span', () => { const start = Date.now(); const tree = new SpanTree( txn({ - title: 'transaction root', - startTimestamp: start, - endTimestamp: start + 1000, - contexts: {trace: {span_id: 'root'}}, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: start, + [SpanFields.PRECISE_FINISH_TS]: start + 1000, + [SpanFields.SPAN_ID]: 'root', }), [ s({ - span_id: '1', - parent_span_id: 'root', - timestamp: start + 0.05, - start_timestamp: start, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 0.05, + [SpanFields.PRECISE_START_TS]: start, }), s({ - span_id: '2', - parent_span_id: 'root', - timestamp: start + 0.08, - start_timestamp: start + 0.05, + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: start + 0.08, + [SpanFields.PRECISE_START_TS]: start + 0.05, }), ] ); expect(tree.orphanedSpans).toHaveLength(0); - expect(tree.root.children[0]!.span.span_id).toBe('1'); - expect(tree.root.children[1]!.span.span_id).toBe('2'); + expect(tree.root.children[0]!.span[SpanFields.SPAN_ID]).toBe('1'); + expect(tree.root.children[1]!.span[SpanFields.SPAN_ID]).toBe('2'); }); it('marks span as orphaned if parent_id does not match', () => { const tree = new SpanTree( txn({ - title: 'transaction root', - startTimestamp: Date.now(), - endTimestamp: Date.now() + 1000, - contexts: {trace: {span_id: 'root'}}, + [SpanFields.SPAN_DESCRIPTION]: 'transaction root', + [SpanFields.PRECISE_START_TS]: Date.now(), + [SpanFields.PRECISE_FINISH_TS]: Date.now() + 1000, + [SpanFields.SPAN_ID]: 'root', }), [ - s({span_id: '1', parent_span_id: 'root', timestamp: 1, start_timestamp: 0}), s({ - span_id: '2', - parent_span_id: 'orphaned', - timestamp: 1.1, - start_timestamp: 0.1, + [SpanFields.SPAN_ID]: '1', + [SpanFields.TRACE_PARENT_SPAN]: 'root', + [SpanFields.PRECISE_FINISH_TS]: 1, + [SpanFields.PRECISE_START_TS]: 0, + }), + s({ + [SpanFields.SPAN_ID]: '2', + [SpanFields.TRACE_PARENT_SPAN]: 'orphaned', + [SpanFields.PRECISE_FINISH_TS]: 1.1, + [SpanFields.PRECISE_START_TS]: 0.1, }), ] ); - expect(tree.orphanedSpans[0]!.span_id).toBe('2'); + expect(tree.orphanedSpans[0]![SpanFields.SPAN_ID]).toBe('2'); }); }); diff --git a/static/app/utils/profiling/spanTree.tsx b/static/app/utils/profiling/spanTree.tsx index a3863d1819cd27..8ca8453dad154a 100644 --- a/static/app/utils/profiling/spanTree.tsx +++ b/static/app/utils/profiling/spanTree.tsx @@ -1,116 +1,115 @@ import {uuid4} from '@sentry/core'; -import type {RawSpanType} from 'sentry/components/events/interfaces/spans/types'; -import {isEventFromBrowserJavaScriptSDK} from 'sentry/components/events/interfaces/spans/utils'; +import {isBrowserJavaScriptSDKName} from 'sentry/components/events/interfaces/spans/utils'; import {t} from 'sentry/locale'; -import type {EventTransaction} from 'sentry/types/event'; -import {EventOrGroupType} from 'sentry/types/event'; +import type {SpanResponse} from 'sentry/views/insights/types'; +import {SpanFields} from 'sentry/views/insights/types'; + +export type TransactionSpanData = Pick< + SpanResponse, + | 'span.description' + | 'precise.start_ts' + | 'precise.finish_ts' + | 'span_id' + | 'span.self_time' + | 'trace' + | 'sdk.name' +>; + +export type SpanNodeData = Pick< + SpanResponse, + | 'span.description' + | 'span.op' + | 'precise.start_ts' + | 'precise.finish_ts' + | 'span_id' + | 'trace' + | 'trace.parent_span' + | 'transaction.event_id' +>; // Empty transaction to use as a default value with duration of 1 second -const EmptyEventTransaction: EventTransaction = { - id: '', - projectID: '', - user: {}, - contexts: {}, - entries: [], - errors: [], - dateCreated: '', - startTimestamp: Date.now(), - endTimestamp: Date.now() + 1000, - title: '', - type: EventOrGroupType.TRANSACTION, - culprit: '', - dist: null, - eventID: '', - fingerprints: [], - dateReceived: new Date().toISOString(), - message: '', - metadata: {}, - size: 0, - tags: [], - occurrence: null, - location: '', - crashFile: null, +const EmptyTransactionSpan: TransactionSpanData = { + [SpanFields.SPAN_DESCRIPTION]: '', + [SpanFields.PRECISE_START_TS]: Date.now(), + [SpanFields.PRECISE_FINISH_TS]: Date.now() + 1000, + [SpanFields.SPAN_ID]: '', + [SpanFields.SPAN_SELF_TIME]: 0, + [SpanFields.TRACE]: '', + [SpanFields.SDK_NAME]: '', }; -function sortByStartTimeAndDuration(a: SpanType, b: SpanType) { - return a.start_timestamp - b.start_timestamp; -} - -interface SpanType extends RawSpanType { - event_id?: string; +function sortByStartTimeAndDuration(a: SpanNodeData, b: SpanNodeData) { + return a[SpanFields.PRECISE_START_TS] - b[SpanFields.PRECISE_START_TS]; } export class SpanTreeNode { parent?: SpanTreeNode | null = null; - span: SpanType; + span: SpanNodeData; children: SpanTreeNode[] = []; - constructor(span: SpanType, parent?: SpanTreeNode | null) { + constructor(span: SpanNodeData, parent?: SpanTreeNode | null) { this.span = span; this.parent = parent; } - static Root(partial: Partial = {}): SpanTreeNode { + static Root(partial: Partial = {}): SpanTreeNode { return new SpanTreeNode( { - description: 'root', - op: 'root', - start_timestamp: 0, - exclusive_time: 0, - timestamp: Number.MAX_SAFE_INTEGER, - parent_span_id: '', - data: {}, - span_id: '', - trace_id: '', - hash: '', + [SpanFields.SPAN_DESCRIPTION]: 'root', + [SpanFields.SPAN_OP]: 'root', + [SpanFields.PRECISE_START_TS]: 0, + [SpanFields.PRECISE_FINISH_TS]: Number.MAX_SAFE_INTEGER, + [SpanFields.TRACE_PARENT_SPAN]: '', + [SpanFields.SPAN_ID]: '', + [SpanFields.TRACE]: '', + [SpanFields.TRANSACTION_EVENT_ID]: '', ...partial, }, null ); } - contains(span: SpanType) { + contains(span: SpanNodeData) { return ( - this.span.start_timestamp <= span.start_timestamp && - this.span.timestamp >= span.timestamp + this.span[SpanFields.PRECISE_START_TS] <= span[SpanFields.PRECISE_START_TS] && + this.span[SpanFields.PRECISE_FINISH_TS] >= span[SpanFields.PRECISE_FINISH_TS] ); } } class SpanTree { root: SpanTreeNode; - orphanedSpans: SpanType[] = []; - transaction: EventTransaction; + orphanedSpans: SpanNodeData[] = []; + transaction: TransactionSpanData; injectMissingInstrumentationSpans = true; - constructor(transaction: EventTransaction, spans: SpanType[]) { + constructor(transaction: TransactionSpanData, spans: SpanNodeData[]) { this.transaction = transaction; - this.injectMissingInstrumentationSpans = - !isEventFromBrowserJavaScriptSDK(transaction); + this.injectMissingInstrumentationSpans = !isBrowserJavaScriptSDKName( + transaction[SpanFields.SDK_NAME] + ); this.root = SpanTreeNode.Root({ - description: transaction.title, - start_timestamp: transaction.startTimestamp, - timestamp: transaction.endTimestamp, - exclusive_time: transaction.contexts?.trace?.exclusive_time ?? undefined, - span_id: transaction.contexts?.trace?.span_id ?? undefined, - event_id: transaction.eventID, - parent_span_id: undefined, - trace_id: transaction.contexts?.trace?.trace_id ?? undefined, - op: 'transaction', + [SpanFields.SPAN_DESCRIPTION]: transaction[SpanFields.SPAN_DESCRIPTION], + [SpanFields.PRECISE_START_TS]: transaction[SpanFields.PRECISE_START_TS], + [SpanFields.PRECISE_FINISH_TS]: transaction[SpanFields.PRECISE_FINISH_TS], + [SpanFields.SPAN_ID]: transaction[SpanFields.SPAN_ID], + [SpanFields.TRANSACTION_EVENT_ID]: transaction[SpanFields.SPAN_ID], + [SpanFields.TRACE]: transaction[SpanFields.TRACE], + [SpanFields.SPAN_OP]: 'transaction', }); this.buildCollapsedSpanTree(spans); } - static Empty = new SpanTree(EmptyEventTransaction, []); + static Empty = new SpanTree(EmptyTransactionSpan, []); isEmpty(): boolean { return this === SpanTree.Empty; } - buildCollapsedSpanTree(spans: SpanType[]) { + buildCollapsedSpanTree(spans: SpanNodeData[]) { const spansSortedByStartTime = [...spans].sort(sortByStartTimeAndDuration); const MISSING_INSTRUMENTATION_THRESHOLD_S = 0.1; @@ -120,7 +119,10 @@ class SpanTree { while (parent.contains(span)) { let nextParent: SpanTreeNode | null = null; for (const child of parent.children) { - if (child.span.op !== 'missing instrumentation' && child.contains(span)) { + if ( + child.span[SpanFields.SPAN_OP] !== 'missing instrumentation' && + child.contains(span) + ) { nextParent = child; break; } @@ -131,29 +133,34 @@ class SpanTree { parent = nextParent; } - if (parent.span.span_id === span.parent_span_id) { + if (parent.span[SpanFields.SPAN_ID] === span[SpanFields.TRACE_PARENT_SPAN]) { // If the missing instrumentation threshold is exceeded, add a span to // indicate that there is a gap in instrumentation. We can rely on this check // because the spans are sorted by start time, so we know that we will not be - // updating anything before span.start_timestamp. + // updating anything before span start timestamp. if ( this.injectMissingInstrumentationSpans && parent.children.length > 0 && - span.start_timestamp - - parent.children[parent.children.length - 1]!.span.timestamp > + span[SpanFields.PRECISE_START_TS] - + parent.children[parent.children.length - 1]!.span[ + SpanFields.PRECISE_FINISH_TS + ] > MISSING_INSTRUMENTATION_THRESHOLD_S ) { parent.children.push( new SpanTreeNode( { - description: t('Missing span instrumentation'), - op: 'missing span instrumentation', - start_timestamp: - parent.children[parent.children.length - 1]!.span.timestamp, - timestamp: span.start_timestamp, - span_id: uuid4(), - data: {}, - trace_id: span.trace_id, + [SpanFields.SPAN_DESCRIPTION]: t('Missing span instrumentation'), + [SpanFields.SPAN_OP]: 'missing span instrumentation', + [SpanFields.PRECISE_START_TS]: + parent.children[parent.children.length - 1]!.span[ + SpanFields.PRECISE_FINISH_TS + ], + [SpanFields.PRECISE_FINISH_TS]: span[SpanFields.PRECISE_START_TS], + [SpanFields.SPAN_ID]: uuid4(), + [SpanFields.TRACE]: span[SpanFields.TRACE], + [SpanFields.TRACE_PARENT_SPAN]: '', + [SpanFields.TRANSACTION_EVENT_ID]: '', }, parent ) @@ -164,7 +171,9 @@ class SpanTree { let start = parent.children.length - 1; while (start >= 0) { const child = parent.children[start]!; - if (span.start_timestamp < child.span.timestamp) { + if ( + span[SpanFields.PRECISE_START_TS] < child.span[SpanFields.PRECISE_FINISH_TS] + ) { foundOverlap = true; break; } diff --git a/static/app/views/insights/types.tsx b/static/app/views/insights/types.tsx index fd0e08f3faab46..965da69094ff05 100644 --- a/static/app/views/insights/types.tsx +++ b/static/app/views/insights/types.tsx @@ -54,6 +54,7 @@ export enum SpanFields { TRANSACTION_SPAN_ID = 'transaction.span_id', SPAN_SELF_TIME = 'span.self_time', TRACE = 'trace', + TRACE_PARENT_SPAN = 'trace.parent_span', PROFILE_ID = 'profile_id', PROFILEID = 'profile.id', REPLAYID = 'replayId', @@ -70,6 +71,7 @@ export enum SpanFields { BROWSER_NAME = 'browser.name', ENVIRONMENT = 'environment', ORIGIN_TRANSACTION = 'origin.transaction', + TRANSACTION_EVENT_ID = 'transaction.event_id', TRANSACTION_METHOD = 'transaction.method', TRANSACTION_OP = 'transaction.op', PROFILER_ID = 'profiler.id', @@ -148,6 +150,8 @@ export enum SpanFields { FROZEN_FRAMES_RATE = 'measurements.frames_frozen_rate', SLOW_FRAMES_RATE = 'measurements.frames_slow_rate', DEVICE_CLASS = 'device.class', + DEVICE_MODEL = 'device.model', + DEVICE_MANUFACTURER = 'device.manufacturer', APP_START_COLD = 'measurements.app_start_cold', APP_START_WARM = 'measurements.app_start_warm', MOBILE_FRAMES_DELAY = 'mobile.frames_delay', @@ -293,6 +297,7 @@ export type NonNullableStringFields = | SpanFields.MCP_RESOURCE_URI | SpanFields.MCP_PROMPT_NAME | SpanFields.TRACE + | SpanFields.TRACE_PARENT_SPAN | SpanFields.PROFILEID | SpanFields.PROFILE_ID | SpanFields.REPLAYID @@ -310,6 +315,8 @@ export type NonNullableStringFields = | SpanFields.SDK_NAME | SpanFields.SDK_VERSION | SpanFields.DEVICE_CLASS + | SpanFields.DEVICE_MODEL + | SpanFields.DEVICE_MANUFACTURER | SpanFields.SPAN_ACTION | SpanFields.SPAN_DOMAIN | SpanFields.MESSAGING_MESSAGE_BODY_SIZE @@ -325,8 +332,10 @@ export type NonNullableStringFields = | SpanFields.SPAN_SYSTEM | SpanFields.TIMESTAMP | SpanFields.TRANSACTION + | SpanFields.TRANSACTION_EVENT_ID | SpanFields.TRANSACTION_METHOD | SpanFields.RELEASE + | SpanFields.ENVIRONMENT | SpanFields.OS_NAME | SpanFields.SPAN_STATUS_CODE | SpanFields.SPAN_AI_PIPELINE_GROUP diff --git a/static/app/views/profiling/continuousProfileProvider.tsx b/static/app/views/profiling/continuousProfileProvider.tsx index fd05600c909cd8..73330576e1b534 100644 --- a/static/app/views/profiling/continuousProfileProvider.tsx +++ b/static/app/views/profiling/continuousProfileProvider.tsx @@ -3,8 +3,7 @@ import {Outlet} from 'react-router-dom'; import {ContinuousProfileHeader} from 'sentry/components/profiling/continuousProfileHeader'; import type {RequestState} from 'sentry/types/core'; -import type {EventTransaction} from 'sentry/types/event'; -import {useSentryEvent} from 'sentry/utils/profiling/hooks/useSentryEvent'; +import {useTransaction} from 'sentry/utils/profiling/hooks/useTransaction'; import {decodeScalar} from 'sentry/utils/queryString'; import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; @@ -50,12 +49,7 @@ export default function ProfileAndTransactionProvider(): React.ReactElement { type: eventId ? 'initial' : 'empty', }); - const profileTransaction = useSentryEvent( - organization.slug, - projectSlug, - eventId, - !eventId // disable if no event id - ); + const transactionResult = useTransaction(eventId || undefined); return ( - + diff --git a/static/app/views/profiling/profileFlamechart.tsx b/static/app/views/profiling/profileFlamechart.tsx index 6c43383b1d6380..8abd03a0327085 100644 --- a/static/app/views/profiling/profileFlamechart.tsx +++ b/static/app/views/profiling/profileFlamechart.tsx @@ -1,6 +1,6 @@ import {useEffect, useMemo, useRef} from 'react'; import styled from '@emotion/styled'; -import * as qs from 'query-string'; +import type * as qs from 'query-string'; import {Stack} from '@sentry/scraps/layout'; @@ -23,6 +23,7 @@ import {FlamegraphThemeProvider} from 'sentry/utils/profiling/flamegraph/flamegr import {useFlamegraphPreferences} from 'sentry/utils/profiling/flamegraph/hooks/useFlamegraphPreferences'; import {useCurrentProjectFromRouteParam} from 'sentry/utils/profiling/hooks/useCurrentProjectFromRouteParam'; import {useLocalStorageState} from 'sentry/utils/useLocalStorageState'; +import {useLocation} from 'sentry/utils/useLocation'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {ProfileGroupProvider} from 'sentry/views/profiling/profileGroupProvider'; @@ -60,8 +61,9 @@ function ProfileFlamegraph(): React.ReactElement { export default function ProfileFlamegraphWrapper() { const organization = useOrganization(); const profiles = useProfiles(); - const profiledTransaction = useProfileTransaction(); + const transactionResult = useProfileTransaction(); const params = useParams(); + const location = useLocation(); const [storedPreferences] = useLocalStorageState>( FLAMEGRAPH_LOCALSTORAGE_PREFERENCES_KEY, @@ -77,7 +79,7 @@ export default function ProfileFlamegraphWrapper() { const initialFlamegraphPreferencesState = useMemo((): DeepPartial => { const queryStringState = decodeFlamegraphStateFromQueryParams( - qs.parse(window.location.search) + location.query as qs.ParsedQuery ); return { @@ -113,7 +115,7 @@ export default function ProfileFlamegraphWrapper() { - {profiles.type === 'loading' || profiledTransaction.type === 'loading' ? ( + {profiles.type === 'loading' || transactionResult.isPending ? ( diff --git a/static/app/views/profiling/profilesProvider.tsx b/static/app/views/profiling/profilesProvider.tsx index 0c119d876caf7c..55e5704a0ae1c5 100644 --- a/static/app/views/profiling/profilesProvider.tsx +++ b/static/app/views/profiling/profilesProvider.tsx @@ -3,9 +3,9 @@ import * as Sentry from '@sentry/react'; import type {Client} from 'sentry/api'; 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 type {TransactionResult} from 'sentry/utils/profiling/hooks/useTransaction'; import {useApi} from 'sentry/utils/useApi'; import {useProjects} from 'sentry/utils/useProjects'; @@ -60,8 +60,7 @@ export function useProfiles() { return context; } -export const ProfileTransactionContext = - createContext | null>(null); +export const ProfileTransactionContext = createContext(null); export function useProfileTransaction() { const context = useContext(ProfileTransactionContext); diff --git a/static/app/views/profiling/transactionProfileProvider.tsx b/static/app/views/profiling/transactionProfileProvider.tsx index d46d1bc6f47c4f..48e2357ebe966d 100644 --- a/static/app/views/profiling/transactionProfileProvider.tsx +++ b/static/app/views/profiling/transactionProfileProvider.tsx @@ -3,23 +3,31 @@ import {Outlet} from 'react-router-dom'; import {ProfileHeader} from 'sentry/components/profiling/profileHeader'; import type {RequestState} from 'sentry/types/core'; -import type {EventTransaction} from 'sentry/types/event'; import {isSchema, isSentrySampledProfile} from 'sentry/utils/profiling/guards/profile'; -import {useSentryEvent} from 'sentry/utils/profiling/hooks/useSentryEvent'; +import {useTransaction} from 'sentry/utils/profiling/hooks/useTransaction'; import {useOrganization} from 'sentry/utils/useOrganization'; import {useParams} from 'sentry/utils/useParams'; import {LayoutPageWithHiddenFooter} from 'sentry/views/profiling/layoutPageWithHiddenFooter'; import {ProfileTransactionContext, TransactionProfileProvider} from './profilesProvider'; -function getTransactionId(input: Profiling.ProfileInput): string | null { +function getIdentifiers(input: Profiling.ProfileInput): { + traceId?: string; + transactionId?: string; +} { if (isSchema(input)) { - return input.metadata.transactionID; + return { + transactionId: input.metadata.transactionID || undefined, + traceId: input.metadata.traceID || undefined, + }; } if (isSentrySampledProfile(input)) { - return input.transaction.id; + return { + transactionId: input.transaction.id || undefined, + traceId: input.transaction.trace_id || undefined, + }; } - return null; + return {transactionId: undefined, traceId: undefined}; } export default function ProfileAndTransactionProvider(): React.ReactElement { @@ -32,11 +40,12 @@ export default function ProfileAndTransactionProvider(): React.ReactElement { type: 'initial', }); - const profileTransaction = useSentryEvent( - organization.slug, - projectSlug, - profile.type === 'resolved' ? getTransactionId(profile.data) : null - ); + const {transactionId, traceId} = + profile.type === 'resolved' + ? getIdentifiers(profile.data) + : {transactionId: undefined, traceId: undefined}; + + const transactionResult = useTransaction(transactionId, traceId); return ( - +