@@ -15,7 +15,7 @@ import type {
1515 IntegrationFn ,
1616 OsContext ,
1717} from '@sentry/core' ;
18- import { defineIntegration } from '@sentry/core' ;
18+ import { defineIntegration , safeSetSpanJSONAttributes } from '@sentry/core' ;
1919
2020export const readFileAsync = promisify ( readFile ) ;
2121export const readDirAsync = promisify ( readdir ) ;
@@ -42,8 +42,6 @@ interface ContextOptions {
4242}
4343
4444const _nodeContextIntegration = ( ( options : ContextOptions = { } ) => {
45- let cachedContext : Promise < Contexts > | undefined ;
46-
4745 const _options = {
4846 app : true ,
4947 os : true ,
@@ -53,13 +51,56 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => {
5351 ...options ,
5452 } ;
5553
56- /** Add contexts to the event. Caches the context so we only look it up once. */
57- async function addContext ( event : Event ) : Promise < Event > {
58- if ( cachedContext === undefined ) {
59- cachedContext = _getContexts ( ) ;
54+ // Compute contexts eagerly (shared between tx and span paths)
55+ const appContext = _options . app ? getAppContext ( ) : undefined ;
56+ const deviceContext = _options . device ? getDeviceContext ( _options . device ) : undefined ;
57+ const cultureContext = _options . culture ? getCultureContext ( ) : undefined ;
58+ const cloudResourceContext = _options . cloudResource ? getCloudResourceContext ( ) : undefined ;
59+ const osContextPromise = _options . os ? getOsContext ( ) : undefined ;
60+
61+ // Map static context data to span attributes
62+ const cachedSpanAttributes : Record < string , unknown > = {
63+ 'process.runtime.engine.name' : 'v8' ,
64+ 'process.runtime.engine.version' : process . versions . v8 ,
65+ ...contextsToSpanAttributes ( {
66+ app : appContext ,
67+ device : deviceContext ,
68+ culture : cultureContext ,
69+ cloud_resource : cloudResourceContext ,
70+ } ) ,
71+ } ;
72+
73+ if ( osContextPromise ) {
74+ osContextPromise
75+ . then ( osCtx => Object . assign ( cachedSpanAttributes , contextsToSpanAttributes ( { os : osCtx } ) ) )
76+ . catch ( ( ) => {
77+ // Ignore - os attributes will be undefined
78+ } ) ;
79+ }
80+
81+ // Build contexts for event processing (reuses same data, awaits async OS context)
82+ const contextsPromise : Promise < Contexts > = ( async ( ) => {
83+ const contexts : Contexts = { } ;
84+ if ( osContextPromise ) {
85+ contexts . os = await osContextPromise ;
86+ }
87+ if ( appContext ) {
88+ contexts . app = appContext ;
6089 }
90+ if ( deviceContext ) {
91+ contexts . device = deviceContext ;
92+ }
93+ if ( cultureContext ) {
94+ contexts . culture = cultureContext ;
95+ }
96+ if ( cloudResourceContext ) {
97+ contexts . cloud_resource = cloudResourceContext ;
98+ }
99+ return contexts ;
100+ } ) ( ) ;
61101
62- const updatedContext = _updateContext ( await cachedContext ) ;
102+ async function addContext ( event : Event ) : Promise < Event > {
103+ const updatedContext = _updateContext ( await contextsPromise ) ;
63104
64105 // TODO(v11): conditional with `sendDefaultPii` here?
65106 event . contexts = {
@@ -74,42 +115,15 @@ const _nodeContextIntegration = ((options: ContextOptions = {}) => {
74115 return event ;
75116 }
76117
77- /** Get the contexts from node. */
78- async function _getContexts ( ) : Promise < Contexts > {
79- const contexts : Contexts = { } ;
80-
81- if ( _options . os ) {
82- contexts . os = await getOsContext ( ) ;
83- }
84-
85- if ( _options . app ) {
86- contexts . app = getAppContext ( ) ;
87- }
88-
89- if ( _options . device ) {
90- contexts . device = getDeviceContext ( _options . device ) ;
91- }
92-
93- if ( _options . culture ) {
94- const culture = getCultureContext ( ) ;
95-
96- if ( culture ) {
97- contexts . culture = culture ;
98- }
99- }
100-
101- if ( _options . cloudResource ) {
102- contexts . cloud_resource = getCloudResourceContext ( ) ;
103- }
104-
105- return contexts ;
106- }
107-
108118 return {
109119 name : INTEGRATION_NAME ,
110120 processEvent ( event ) {
111121 return addContext ( event ) ;
112122 } ,
123+ processSegmentSpan ( span ) {
124+ safeSetSpanJSONAttributes ( span , cachedSpanAttributes ) ;
125+ safeSetSpanJSONAttributes ( span , getDynamicSpanAttributes ( appContext , deviceContext ) ) ;
126+ } ,
113127 } ;
114128} ) satisfies IntegrationFn ;
115129
@@ -142,6 +156,98 @@ function _updateContext(contexts: Contexts): Contexts {
142156 return contexts ;
143157}
144158
159+ export function contextsToSpanAttributes ( contexts : Contexts ) : Record < string , unknown > {
160+ const attrs : Record < string , unknown > = { } ;
161+
162+ const { app, device, os : osCtx , culture, cloud_resource } = contexts ;
163+
164+ if ( app ) {
165+ if ( app . app_start_time ) {
166+ attrs [ 'app.start_time' ] = app . app_start_time ;
167+ }
168+ }
169+
170+ if ( device ) {
171+ if ( device . arch ) {
172+ attrs [ 'device.archs' ] = [ device . arch ] ;
173+ }
174+ if ( device . boot_time ) {
175+ attrs [ 'device.boot_time' ] = device . boot_time ;
176+ }
177+ if ( device . memory_size != null ) {
178+ attrs [ 'device.memory_size' ] = device . memory_size ;
179+ }
180+ if ( device . processor_count != null ) {
181+ attrs [ 'device.processor_count' ] = device . processor_count ;
182+ }
183+ if ( device . cpu_description ) {
184+ attrs [ 'device.cpu_description' ] = device . cpu_description ;
185+ }
186+ if ( device . processor_frequency != null ) {
187+ attrs [ 'device.processor_frequency' ] = device . processor_frequency ;
188+ }
189+ }
190+
191+ if ( osCtx ) {
192+ if ( osCtx . name ) {
193+ attrs [ 'os.name' ] = osCtx . name ;
194+ }
195+ if ( osCtx . version ) {
196+ attrs [ 'os.version' ] = osCtx . version ;
197+ }
198+ if ( osCtx . kernel_version ) {
199+ attrs [ 'os.kernel_version' ] = osCtx . kernel_version ;
200+ }
201+ if ( osCtx . build ) {
202+ attrs [ 'os.build' ] = osCtx . build ;
203+ }
204+ }
205+
206+ if ( culture ) {
207+ if ( culture . locale ) {
208+ attrs [ 'culture.locale' ] = culture . locale ;
209+ }
210+ if ( culture . timezone ) {
211+ attrs [ 'culture.timezone' ] = culture . timezone ;
212+ }
213+ }
214+
215+ // CloudResourceContext already uses dot-notation keys matching span attribute conventions
216+ if ( cloud_resource ) {
217+ for ( const [ key , value ] of Object . entries ( cloud_resource ) ) {
218+ if ( value != null ) {
219+ attrs [ key ] = value ;
220+ }
221+ }
222+ }
223+
224+ return attrs ;
225+ }
226+
227+ export function getDynamicSpanAttributes (
228+ appContext : AppContext | undefined ,
229+ deviceContext : DeviceContext | undefined ,
230+ ) : Record < string , unknown > {
231+ const attrs : Record < string , unknown > = { } ;
232+
233+ if ( appContext ) {
234+ attrs [ 'app.memory' ] = process . memoryUsage ( ) . rss ;
235+ if ( typeof ( process as ProcessWithCurrentValues ) . availableMemory === 'function' ) {
236+ const freeMemory = ( process as ProcessWithCurrentValues ) . availableMemory ?.( ) ;
237+ if ( freeMemory != null ) {
238+ attrs [ 'app.free_memory' ] = freeMemory ;
239+ }
240+ }
241+ }
242+
243+ // Only include if memory tracking was initially enabled (indicated by free_memory being set)
244+ if ( deviceContext ?. free_memory != null ) {
245+ attrs [ 'device.free_memory' ] = os . freemem ( ) ;
246+ }
247+
248+ return attrs ;
249+ }
250+
145251/**
146252 * Returns the operating system context.
147253 *
0 commit comments