diff --git a/static/app/utils/performanceForSentry.tsx b/static/app/utils/performanceForSentry.tsx index 91a7f46336c49d..c9f0f82ad09518 100644 --- a/static/app/utils/performanceForSentry.tsx +++ b/static/app/utils/performanceForSentry.tsx @@ -309,3 +309,82 @@ export const VisuallyCompleteWithData = ({ ); }; + +interface OpAssetMeasurementDefinition { + key: string; +} + +const OP_ASSET_MEASUREMENT_MAP: Record = { + 'resource.script': { + key: 'script', + }, + 'resource.css': { + key: 'css', + }, + 'resource.link': { + key: 'link', + }, + 'resource.img': { + key: 'img', + }, +}; +const ASSET_MEASUREMENT_ALL = 'allResources'; + +export const MeasureAssetsOnTransaction = ({children}: {children: ReactNode}) => { + useEffect(() => { + try { + const transaction: any = getCurrentSentryReactTransaction(); // Using any to override types for private api. + if (!transaction) { + return; + } + + transaction.registerBeforeFinishCallback((t: Transaction) => { + const spans: any[] = (t as any).spanRecorder?.spans; + const measurements = (t as any)._measurements; + + if (!spans) { + return; + } + + if (measurements[ASSET_MEASUREMENT_ALL]) { + return; + } + + let allTransfered = 0; + let allEncoded = 0; + let allCount = 0; + + for (const [op, definition] of Object.entries(OP_ASSET_MEASUREMENT_MAP)) { + const filtered = spans.filter(s => s.op === op); + const count = filtered.length; + const transfered = filtered.reduce( + (acc, curr) => acc + curr.data['Transfer Size'] ?? 0, + 0 + ); + const encoded = filtered.reduce( + (acc, curr) => acc + curr.data['Encoded Body Size'] ?? 0, + 0 + ); + + if (op === 'resource.script') { + t.setMeasurement(`assets.${definition.key}.encoded`, encoded, ''); + t.setMeasurement(`assets.${definition.key}.transfer`, transfered, ''); + t.setMeasurement(`assets.${definition.key}.count`, count, ''); + } + + allCount += count; + allTransfered += transfered; + allEncoded += encoded; + } + + t.setMeasurement(`${ASSET_MEASUREMENT_ALL}.encoded`, allEncoded, ''); + t.setMeasurement(`${ASSET_MEASUREMENT_ALL}.transfer`, allTransfered, ''); + t.setMeasurement(`${ASSET_MEASUREMENT_ALL}.count`, allCount, ''); + }); + } catch (_) { + // Defensive catch since this code is auxiliary. + } + }, []); + + return {children}; +}; diff --git a/static/app/views/performance/content.tsx b/static/app/views/performance/content.tsx index 42e42e35c075cf..49e044fb2962d7 100644 --- a/static/app/views/performance/content.tsx +++ b/static/app/views/performance/content.tsx @@ -13,6 +13,7 @@ import {PageFilters, Project} from 'sentry/types'; import trackAdvancedAnalyticsEvent from 'sentry/utils/analytics/trackAdvancedAnalyticsEvent'; import {MEPSettingProvider} from 'sentry/utils/performance/contexts/metricsEnhancedSetting'; import {PerformanceEventViewProvider} from 'sentry/utils/performance/contexts/performanceEventViewContext'; +import {MeasureAssetsOnTransaction} from 'sentry/utils/performanceForSentry'; import useApi from 'sentry/utils/useApi'; import useOrganization from 'sentry/utils/useOrganization'; import usePrevious from 'sentry/utils/usePrevious'; @@ -159,24 +160,26 @@ function PerformanceContent({selection, location, demoMode}: Props) { }, }} > - - handleTrendsClick({ - location, - organization, - projectPlatforms: getSelectedProjectPlatforms(location, projects), - }) - } - onboardingProject={onboardingProject} - organization={organization} - location={location} - projects={projects} - selection={selection} - withStaticFilters={withStaticFilters} - /> + + + handleTrendsClick({ + location, + organization, + projectPlatforms: getSelectedProjectPlatforms(location, projects), + }) + } + onboardingProject={onboardingProject} + organization={organization} + location={location} + projects={projects} + selection={selection} + withStaticFilters={withStaticFilters} + /> +