diff --git a/CHANGELOG.md b/CHANGELOG.md index 41849d7c25..376a5214cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Features + +- Send Source Maps Debug ID for symbolicated Profiles ([#3343](https://github.com/getsentry/sentry-react-native/pull/3343)) + ### Fixes - Add actual `activeThreadId` to Profiles ([#3338](https://github.com/getsentry/sentry-react-native/pull/3338)) diff --git a/src/js/profiling/convertHermesProfile.ts b/src/js/profiling/convertHermesProfile.ts index 36daa2ba9b..aa62146f00 100644 --- a/src/js/profiling/convertHermesProfile.ts +++ b/src/js/profiling/convertHermesProfile.ts @@ -2,7 +2,7 @@ import type { FrameId, StackId, ThreadCpuFrame, ThreadCpuSample, ThreadCpuStack, import { logger } from '@sentry/utils'; import type * as Hermes from './hermes'; -import { parseHermesJSStackFrame } from './hermes'; +import { DEFAULT_BUNDLE_NAME } from './hermes'; import { MAX_PROFILE_DURATION_MS } from './integration'; import type { RawThreadCpuProfile } from './types'; @@ -173,3 +173,39 @@ function mapStacks( hermesStackToSentryStackMap, }; } + +/** + * Parses Hermes StackFrame to Sentry StackFrame. + * For native frames only function name is returned, for Hermes bytecode the line and column are calculated. + */ +export function parseHermesJSStackFrame(frame: Hermes.StackFrame): ThreadCpuFrame { + if (frame.category !== 'JavaScript') { + // Native + if (frame.name === '[root]') { + return { function: frame.name, in_app: false }; + } + return { function: frame.name }; + } + + if (frame.funcVirtAddr !== undefined && frame.offset !== undefined) { + // Hermes Bytecode + return { + function: frame.name, + abs_path: DEFAULT_BUNDLE_NAME, + // https://github.com/krystofwoldrich/metro/blob/417e6f276ff9422af6039fc4d1bce41fcf7d9f46/packages/metro-symbolicate/src/Symbolication.js#L298-L301 + // Hermes lineno is hardcoded 1, currently only one bundle symbolication is supported by metro-symbolicate and thus by us. + lineno: 1, + // Hermes colno is 0-based, while Sentry is 1-based + colno: Number(frame.funcVirtAddr) + Number(frame.offset) + 1, + }; + } + + // JavaScript + const indexOfLeftParenthesis = frame.name.indexOf('('); + return { + function: indexOfLeftParenthesis !== -1 ? frame.name.substring(0, indexOfLeftParenthesis) || undefined : frame.name, + abs_path: DEFAULT_BUNDLE_NAME, + lineno: frame.line !== undefined ? Number(frame.line) : undefined, + colno: frame.column !== undefined ? Number(frame.column) : undefined, + }; +} diff --git a/src/js/profiling/hermes.ts b/src/js/profiling/hermes.ts index 65c6577586..f9c98eb127 100644 --- a/src/js/profiling/hermes.ts +++ b/src/js/profiling/hermes.ts @@ -53,51 +53,8 @@ export interface Profile { stackFrames: Record; } -export interface ParsedHermesStackFrame { - function: string; - file?: string; - lineno?: number; - colno?: number; -} - -const DEFAULT_BUNDLE_NAME = +export const DEFAULT_BUNDLE_NAME = Platform.OS === 'android' ? ANDROID_DEFAULT_BUNDLE_NAME : Platform.OS === 'ios' ? IOS_DEFAULT_BUNDLE_NAME : undefined; -const ANONYMOUS_FUNCTION_NAME = 'anonymous'; - -/** - * Parses Hermes StackFrame to Sentry StackFrame. - * For native frames only function name is returned, for Hermes bytecode the line and column are calculated. - */ -export function parseHermesJSStackFrame(frame: StackFrame): ParsedHermesStackFrame { - if (frame.category !== 'JavaScript') { - // Native - return { function: frame.name }; - } - - if (frame.funcVirtAddr !== undefined && frame.offset !== undefined) { - // Hermes Bytecode - return { - function: frame.name || ANONYMOUS_FUNCTION_NAME, - file: DEFAULT_BUNDLE_NAME, - // https://github.com/krystofwoldrich/metro/blob/417e6f276ff9422af6039fc4d1bce41fcf7d9f46/packages/metro-symbolicate/src/Symbolication.js#L298-L301 - // Hermes lineno is hardcoded 1, currently only one bundle symbolication is supported by metro-symbolicate and thus by us. - lineno: 1, - // Hermes colno is 0-based, while Sentry is 1-based - colno: Number(frame.funcVirtAddr) + Number(frame.offset) + 1, - }; - } - - // JavaScript - const indexOfLeftParenthesis = frame.name.indexOf('('); - return { - function: - (indexOfLeftParenthesis !== -1 && (frame.name.substring(0, indexOfLeftParenthesis) || ANONYMOUS_FUNCTION_NAME)) || - frame.name, - file: DEFAULT_BUNDLE_NAME, - lineno: frame.line !== undefined ? Number(frame.line) : undefined, - colno: frame.column !== undefined ? Number(frame.column) : undefined, - }; -} const MS_TO_NS: number = 1e6; diff --git a/src/js/profiling/types.ts b/src/js/profiling/types.ts index f03d8bcf6f..4880dd1f54 100644 --- a/src/js/profiling/types.ts +++ b/src/js/profiling/types.ts @@ -1,6 +1,7 @@ -import type { ThreadCpuProfile } from '@sentry/types'; +import type { ThreadCpuFrame, ThreadCpuProfile } from '@sentry/types'; export interface RawThreadCpuProfile extends ThreadCpuProfile { + frames: ThreadCpuFrame[]; profile_id?: string; active_thread_id: string; } diff --git a/src/js/profiling/utils.ts b/src/js/profiling/utils.ts index f67104d5bc..85230e4ef6 100644 --- a/src/js/profiling/utils.ts +++ b/src/js/profiling/utils.ts @@ -1,6 +1,7 @@ -import type { Envelope, Event, Profile } from '@sentry/types'; -import { forEachEnvelopeItem, logger } from '@sentry/utils'; +import type { DebugImage, Envelope, Event, Profile } from '@sentry/types'; +import { forEachEnvelopeItem, GLOBAL_OBJ, logger } from '@sentry/utils'; +import { DEFAULT_BUNDLE_NAME } from './hermes'; import type { RawThreadCpuProfile } from './types'; /** @@ -136,7 +137,7 @@ function createProfilePayload( const profile: Profile = { event_id: profile_id, timestamp: new Date(start_timestamp).toISOString(), - platform: 'node', + platform: 'javascript', version: '1', release: release, environment: environment, @@ -163,11 +164,47 @@ function createProfilePayload( trace_id: trace_id || '', active_thread_id: cpuProfile.active_thread_id, }, + debug_meta: { + images: getDebugMetadata(), + }, }; return profile; } +/** + * Returns debug meta images of the loaded bundle. + */ +export function getDebugMetadata(): DebugImage[] { + if (!DEFAULT_BUNDLE_NAME) { + return []; + } + + const debugIdMap = GLOBAL_OBJ._sentryDebugIds; + if (!debugIdMap) { + return []; + } + + const debugIdsKeys = Object.keys(debugIdMap); + if (!debugIdsKeys.length) { + return []; + } + + if (debugIdsKeys.length > 1) { + logger.warn( + '[Profiling] Multiple debug images found, but only one one bundle is supported. Using the first one...', + ); + } + + return [ + { + code_file: DEFAULT_BUNDLE_NAME, + debug_id: debugIdMap[debugIdsKeys[0]], + type: 'sourcemap', + }, + ]; +} + /** * Adds items to envelope if they are not already present - mutates the envelope. * @param envelope diff --git a/test/profiling/convertHermesProfile.test.ts b/test/profiling/convertHermesProfile.test.ts index 45fe3391f5..6e2ccfdd8e 100644 --- a/test/profiling/convertHermesProfile.test.ts +++ b/test/profiling/convertHermesProfile.test.ts @@ -84,23 +84,24 @@ describe('convert hermes profile to sentry profile', () => { frames: [ { function: '[root]', + in_app: false, }, { colno: 33, - file: 'app:///main.jsbundle', + abs_path: 'app:///main.jsbundle', function: 'fooA', lineno: 1610, }, { colno: 21, - file: 'app:///main.jsbundle', + abs_path: 'app:///main.jsbundle', function: 'fooB', lineno: 1616, }, { colno: 18, - file: 'app:///main.jsbundle', - function: 'anonymous', + abs_path: 'app:///main.jsbundle', + function: undefined, lineno: 1627, }, ], diff --git a/test/profiling/hermes.test.ts b/test/profiling/hermes.test.ts index 63ba25b66a..0d7852d4f0 100644 --- a/test/profiling/hermes.test.ts +++ b/test/profiling/hermes.test.ts @@ -1,5 +1,6 @@ -import type { ParsedHermesStackFrame } from '../../src/js/profiling/hermes'; -import { parseHermesJSStackFrame } from '../../src/js/profiling/hermes'; +import type { ThreadCpuFrame } from '@sentry/types'; + +import { parseHermesJSStackFrame } from '../../src/js/profiling/convertHermesProfile'; describe('hermes', () => { describe('parseHermesStackFrameName', () => { @@ -11,9 +12,9 @@ describe('hermes', () => { column: '33', category: 'JavaScript', }), - ).toEqual({ + ).toEqual({ function: 'fooA', - file: 'app:///main.jsbundle', + abs_path: 'app:///main.jsbundle', lineno: 1610, colno: 33, }); @@ -25,7 +26,7 @@ describe('hermes', () => { category: 'root', }), ).toEqual( - expect.objectContaining({ + expect.objectContaining({ function: '[root]', }), ); @@ -38,9 +39,8 @@ describe('hermes', () => { column: '33', category: 'JavaScript', }), - ).toEqual({ - function: 'anonymous', - file: 'app:///main.jsbundle', + ).toEqual({ + abs_path: 'app:///main.jsbundle', lineno: 1610, colno: 33, }); @@ -52,9 +52,9 @@ describe('hermes', () => { category: 'JavaScript', }), ).toEqual( - expect.objectContaining({ + expect.objectContaining({ function: 'fooA', - file: 'app:///main.jsbundle', + abs_path: 'app:///main.jsbundle', }), ); });