@@ -54,7 +54,8 @@ export const createNativeFramesIntegrations = (enable: boolean | undefined): Int
5454} ;
5555
5656/**
57- * Instrumentation to add native slow/frozen frames measurements onto transactions.
57+ * Instrumentation to add native slow/frozen frames measurements onto transactions
58+ * and frame data (frames.total, frames.slow, frames.frozen) onto all spans.
5859 */
5960export const nativeFramesIntegration = ( ) : Integration => {
6061 /** The native frames at the finish time of the most recent span. */
@@ -81,13 +82,11 @@ export const nativeFramesIntegration = (): Integration => {
8182 client . on ( 'spanEnd' , fetchEndFramesForSpan ) ;
8283 } ;
8384
84- const fetchStartFramesForSpan = ( rootSpan : Span ) : void => {
85- if ( ! isRootSpan ( rootSpan ) ) {
86- return ;
87- }
85+ const fetchStartFramesForSpan = ( span : Span ) : void => {
86+ const spanId = span . spanContext ( ) . spanId ;
87+ const spanType = isRootSpan ( span ) ? 'root' : 'child' ;
88+ debug . log ( `[ ${ INTEGRATION_NAME } ] Fetching frames for ${ spanType } span start ( ${ spanId } ).` ) ;
8889
89- const spanId = rootSpan . spanContext ( ) . spanId ;
90- debug . log ( `[${ INTEGRATION_NAME } ] Fetching frames for root span start (${ spanId } ).` ) ;
9190 _spanToNativeFramesAtStartMap . set (
9291 spanId ,
9392 new Promise < NativeFramesResponse | null > ( resolve => {
@@ -101,17 +100,26 @@ export const nativeFramesIntegration = (): Integration => {
101100 ) ;
102101 } ;
103102
104- const fetchEndFramesForSpan = ( span : Span ) : void => {
103+ /**
104+ * Fetches end frames for a span and attaches frame data as span attributes.
105+ *
106+ * Note: This makes one native bridge call per span end. While this creates O(n) calls
107+ * for n spans, it's necessary for accuracy. Frame counts are cumulative and continuously
108+ * incrementing, so each span needs the exact frame count at its end time. Caching would
109+ * produce incorrect deltas. The native bridge calls are async and non-blocking.
110+ */
111+ const fetchEndFramesForSpan = async ( span : Span ) : Promise < void > => {
105112 const timestamp = timestampInSeconds ( ) ;
106113 const spanId = span . spanContext ( ) . spanId ;
114+ const hasStartFrames = _spanToNativeFramesAtStartMap . has ( spanId ) ;
107115
108- if ( isRootSpan ( span ) ) {
109- const hasStartFrames = _spanToNativeFramesAtStartMap . has ( spanId ) ;
110- if ( ! hasStartFrames ) {
111- // We don't have start frames, won't be able to calculate the difference.
112- return ;
113- }
116+ if ( ! hasStartFrames ) {
117+ // We don't have start frames, won't be able to calculate the difference.
118+ return ;
119+ }
114120
121+ if ( isRootSpan ( span ) ) {
122+ // Root spans: Store end frames for transaction measurements (backward compatibility)
115123 debug . log ( `[${ INTEGRATION_NAME } ] Fetch frames for root span end (${ spanId } ).` ) ;
116124 _spanToNativeFramesAtEndMap . set (
117125 spanId ,
@@ -129,17 +137,45 @@ export const nativeFramesIntegration = (): Integration => {
129137 } ) ;
130138 } ) ,
131139 ) ;
132- return undefined ;
133- } else {
134- debug . log ( `[${ INTEGRATION_NAME } ] Fetch frames for child span end (${ spanId } ).` ) ;
135- fetchNativeFrames ( )
136- . then ( frames => {
137- _lastChildSpanEndFrames = {
138- timestamp,
139- nativeFrames : frames ,
140- } ;
141- } )
142- . catch ( error => debug . log ( `[${ INTEGRATION_NAME } ] Error while fetching native frames.` , error ) ) ;
140+ }
141+
142+ // All spans (root and child): Attach frame data as span attributes
143+ try {
144+ const startFrames = await _spanToNativeFramesAtStartMap . get ( spanId ) ;
145+ if ( ! startFrames ) {
146+ debug . log ( `[${ INTEGRATION_NAME } ] No start frames found for span ${ spanId } , skipping frame data.` ) ;
147+ return ;
148+ }
149+
150+ // NOTE: For root spans, this is the second call to fetchNativeFrames() for the same span.
151+ // The calls are very close together (microseconds apart), so inconsistency is minimal.
152+ // Future optimization: reuse the first call's promise to avoid redundant native bridge call.
153+ const endFrames = await fetchNativeFrames ( ) ;
154+
155+ // Calculate deltas
156+ const totalFrames = endFrames . totalFrames - startFrames . totalFrames ;
157+ const slowFrames = endFrames . slowFrames - startFrames . slowFrames ;
158+ const frozenFrames = endFrames . frozenFrames - startFrames . frozenFrames ;
159+
160+ // Only attach if we have meaningful data
161+ if ( totalFrames > 0 || slowFrames > 0 || frozenFrames > 0 ) {
162+ span . setAttribute ( 'frames.total' , totalFrames ) ;
163+ span . setAttribute ( 'frames.slow' , slowFrames ) ;
164+ span . setAttribute ( 'frames.frozen' , frozenFrames ) ;
165+ debug . log (
166+ `[${ INTEGRATION_NAME } ] Attached frame data to span ${ spanId } : total=${ totalFrames } , slow=${ slowFrames } , frozen=${ frozenFrames } ` ,
167+ ) ;
168+ }
169+
170+ // Update last child span end frames for root span fallback logic
171+ if ( ! isRootSpan ( span ) ) {
172+ _lastChildSpanEndFrames = {
173+ timestamp,
174+ nativeFrames : endFrames ,
175+ } ;
176+ }
177+ } catch ( error ) {
178+ debug . log ( `[${ INTEGRATION_NAME } ] Error while capturing end frames for span ${ spanId } .` , error ) ;
143179 }
144180 } ;
145181
@@ -233,21 +269,37 @@ export const nativeFramesIntegration = (): Integration => {
233269
234270function fetchNativeFrames ( ) : Promise < NativeFramesResponse > {
235271 return new Promise < NativeFramesResponse > ( ( resolve , reject ) => {
272+ let settled = false ;
273+
274+ const timeoutId = setTimeout ( ( ) => {
275+ if ( ! settled ) {
276+ settled = true ;
277+ reject ( 'Fetching native frames took too long. Dropping frames.' ) ;
278+ }
279+ } , FETCH_FRAMES_TIMEOUT_MS ) ;
280+
236281 NATIVE . fetchNativeFrames ( )
237282 . then ( value => {
283+ if ( settled ) {
284+ return ;
285+ }
286+ clearTimeout ( timeoutId ) ;
287+ settled = true ;
288+
238289 if ( ! value ) {
239290 reject ( 'Native frames response is null.' ) ;
240291 return ;
241292 }
242293 resolve ( value ) ;
243294 } )
244295 . then ( undefined , error => {
296+ if ( settled ) {
297+ return ;
298+ }
299+ clearTimeout ( timeoutId ) ;
300+ settled = true ;
245301 reject ( error ) ;
246302 } ) ;
247-
248- setTimeout ( ( ) => {
249- reject ( 'Fetching native frames took too long. Dropping frames.' ) ;
250- } , FETCH_FRAMES_TIMEOUT_MS ) ;
251303 } ) ;
252304}
253305
0 commit comments