diff --git a/CHANGELOG.md b/CHANGELOG.md index b414c179f0..600c120a45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ ## Unreleased +### Features + +- Add thread information to spans ([#4579](https://github.com/getsentry/sentry-react-native/pull/4579)) + ### Fixes - Considers the `SENTRY_DISABLE_AUTO_UPLOAD` and `SENTRY_DISABLE_NATIVE_DEBUG_UPLOAD` environment variables in the configuration of the Sentry Android Gradle Plugin for Expo plugin ([#4583](https://github.com/getsentry/sentry-react-native/pull/4583)) diff --git a/packages/core/src/js/tracing/integrations/appStart.ts b/packages/core/src/js/tracing/integrations/appStart.ts index 0f96557d4c..572039f30e 100644 --- a/packages/core/src/js/tracing/integrations/appStart.ts +++ b/packages/core/src/js/tracing/integrations/appStart.ts @@ -1,4 +1,4 @@ -/* eslint-disable complexity */ +/* eslint-disable complexity, max-lines */ import type { Client, Event, Integration, SpanJSON, TransactionEvent } from '@sentry/core'; import { getCapturedScopesOnSpan, @@ -26,6 +26,7 @@ import { } from '../ops'; import { SPAN_ORIGIN_AUTO_APP_START, SPAN_ORIGIN_MANUAL_APP_START } from '../origin'; import { SEMANTIC_ATTRIBUTE_SENTRY_OP } from '../semanticAttributes'; +import { setMainThreadInfo } from '../span'; import { createChildSpanJSON, createSpanJSON, getBundleStartTimestampMs } from '../utils'; const INTEGRATION_NAME = 'AppStart'; @@ -384,15 +385,17 @@ function createJSExecutionStartSpan( function convertNativeSpansToSpanJSON(parentSpan: SpanJSON, nativeSpans: NativeAppStartResponse['spans']): SpanJSON[] { return nativeSpans.map(span => { if (span.description === 'UIKit init') { - return createUIKitSpan(parentSpan, span); + return setMainThreadInfo(createUIKitSpan(parentSpan, span)); } - return createChildSpanJSON(parentSpan, { - description: span.description, - start_timestamp: span.start_timestamp_ms / 1000, - timestamp: span.end_timestamp_ms / 1000, - origin: SPAN_ORIGIN_AUTO_APP_START, - }); + return setMainThreadInfo( + createChildSpanJSON(parentSpan, { + description: span.description, + start_timestamp: span.start_timestamp_ms / 1000, + timestamp: span.end_timestamp_ms / 1000, + origin: SPAN_ORIGIN_AUTO_APP_START, + }), + ); }); } diff --git a/packages/core/src/js/tracing/reactnativetracing.ts b/packages/core/src/js/tracing/reactnativetracing.ts index d9ada0ff9d..30beeaec07 100644 --- a/packages/core/src/js/tracing/reactnativetracing.ts +++ b/packages/core/src/js/tracing/reactnativetracing.ts @@ -5,7 +5,7 @@ import { getClient } from '@sentry/core'; import { isWeb } from '../utils/environment'; import { getDevServer } from './../integrations/debugsymbolicatorutils'; -import { addDefaultOpForSpanFrom, defaultIdleOptions } from './span'; +import { addDefaultOpForSpanFrom, addThreadInfoToSpan, defaultIdleOptions } from './span'; export const INTEGRATION_NAME = 'ReactNativeTracing'; @@ -119,6 +119,7 @@ export const reactNativeTracingIntegration = ( const setup = (client: Client): void => { addDefaultOpForSpanFrom(client); + addThreadInfoToSpan(client); instrumentOutgoingRequests(client, { traceFetch: finalOptions.traceFetch, diff --git a/packages/core/src/js/tracing/span.ts b/packages/core/src/js/tracing/span.ts index b44425691c..d7909d8d6b 100644 --- a/packages/core/src/js/tracing/span.ts +++ b/packages/core/src/js/tracing/span.ts @@ -1,4 +1,4 @@ -import type { Client, Scope, Span, StartSpanOptions } from '@sentry/core'; +import type { Client, Scope, Span, SpanJSON, StartSpanOptions } from '@sentry/core'; import { generatePropagationContext, getActiveSpan, @@ -154,3 +154,28 @@ export function addDefaultOpForSpanFrom(client: Client): void { } }); } + +export const SPAN_THREAD_NAME = 'thread.name'; +export const SPAN_THREAD_NAME_MAIN = 'main'; +export const SPAN_THREAD_NAME_JAVASCRIPT = 'javascript'; + +/** + * Adds Javascript thread info to spans. + * Ref: https://reactnative.dev/architecture/threading-model + */ +export function addThreadInfoToSpan(client: Client): void { + client.on('spanStart', (span: Span) => { + if (!spanToJSON(span).data?.[SPAN_THREAD_NAME]) { + span.setAttribute(SPAN_THREAD_NAME, SPAN_THREAD_NAME_JAVASCRIPT); + } + }); +} + +/** + * Sets the Main thread info to the span. + */ +export function setMainThreadInfo(spanJSON: SpanJSON): SpanJSON { + spanJSON.data = spanJSON.data || {}; + spanJSON.data[SPAN_THREAD_NAME] = SPAN_THREAD_NAME_MAIN; + return spanJSON; +} diff --git a/packages/core/test/tracing/integrations/appStart.test.ts b/packages/core/test/tracing/integrations/appStart.test.ts index 4337e3e2b3..17709730dd 100644 --- a/packages/core/test/tracing/integrations/appStart.test.ts +++ b/packages/core/test/tracing/integrations/appStart.test.ts @@ -27,6 +27,7 @@ import { setRootComponentCreationTimestampMs, } from '../../../src/js/tracing/integrations/appStart'; import { SPAN_ORIGIN_AUTO_APP_START, SPAN_ORIGIN_MANUAL_APP_START } from '../../../src/js/tracing/origin'; +import { SPAN_THREAD_NAME, SPAN_THREAD_NAME_MAIN } from '../../../src/js/tracing/span'; import { getTimeOriginMilliseconds } from '../../../src/js/tracing/utils'; import { RN_GLOBAL_OBJ } from '../../../src/js/utils/worldwide'; import { NATIVE } from '../../../src/js/wrapper'; @@ -252,6 +253,7 @@ describe('App Start Integration', () => { data: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: appStartRootSpan!.op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_MAIN, }, }), ); @@ -612,6 +614,7 @@ describe('App Start Integration', () => { data: { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: appStartRootSpan!.op, [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: SPAN_ORIGIN_AUTO_APP_START, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_MAIN, }, }), ); diff --git a/packages/core/test/tracing/reactnativenavigation.test.ts b/packages/core/test/tracing/reactnativenavigation.test.ts index 09acbaa25f..ebc264d91f 100644 --- a/packages/core/test/tracing/reactnativenavigation.test.ts +++ b/packages/core/test/tracing/reactnativenavigation.test.ts @@ -32,6 +32,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '../../src/js/tracing/semanticAttributes'; +import { SPAN_THREAD_NAME, SPAN_THREAD_NAME_JAVASCRIPT } from '../../src/js/tracing/span'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; interface MockEventsRegistry extends EventsRegistry { @@ -87,6 +88,7 @@ describe('React Native Navigation Instrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -131,6 +133,7 @@ describe('React Native Navigation Instrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -206,6 +209,7 @@ describe('React Native Navigation Instrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -294,6 +298,7 @@ describe('React Native Navigation Instrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -342,6 +347,7 @@ describe('React Native Navigation Instrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), diff --git a/packages/core/test/tracing/reactnavigation.test.ts b/packages/core/test/tracing/reactnavigation.test.ts index 5c8b805e9d..e37c313c20 100644 --- a/packages/core/test/tracing/reactnavigation.test.ts +++ b/packages/core/test/tracing/reactnavigation.test.ts @@ -20,7 +20,7 @@ import { SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE, SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, } from '../../src/js/tracing/semanticAttributes'; -import { DEFAULT_NAVIGATION_SPAN_NAME } from '../../src/js/tracing/span'; +import { DEFAULT_NAVIGATION_SPAN_NAME, SPAN_THREAD_NAME, SPAN_THREAD_NAME_JAVASCRIPT } from '../../src/js/tracing/span'; import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; import { getDefaultTestClientOptions, TestClient } from '../mocks/client'; import { NATIVE } from '../mockWrapper'; @@ -83,6 +83,7 @@ describe('ReactNavigationInstrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -192,6 +193,7 @@ describe('ReactNavigationInstrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -229,6 +231,7 @@ describe('ReactNavigationInstrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), @@ -268,6 +271,7 @@ describe('ReactNavigationInstrumentation', () => { [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'navigation', [SEMANTIC_ATTRIBUTE_SENTRY_SAMPLE_RATE]: 1, [SEMANTIC_ATTRIBUTE_SENTRY_IDLE_SPAN_FINISH_REASON]: 'idleTimeout', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, }), }), diff --git a/packages/core/test/tracing/reactnavigation.ttid.test.tsx b/packages/core/test/tracing/reactnavigation.ttid.test.tsx index a0245cff12..c34c1271cb 100644 --- a/packages/core/test/tracing/reactnavigation.ttid.test.tsx +++ b/packages/core/test/tracing/reactnavigation.ttid.test.tsx @@ -16,6 +16,7 @@ import { startSpanManual } from '../../src/js'; import { TimeToFullDisplay, TimeToInitialDisplay } from '../../src/js/tracing'; import { _setAppStartEndTimestampMs } from '../../src/js/tracing/integrations/appStart'; import { SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION, SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY } from '../../src/js/tracing/origin'; +import { SPAN_THREAD_NAME, SPAN_THREAD_NAME_JAVASCRIPT } from '../../src/js/tracing/span'; import { isHermesEnabled, notWeb } from '../../src/js/utils/environment'; import { createSentryFallbackEventEmitter } from '../../src/js/utils/sentryeventemitterfallback'; import { RN_GLOBAL_OBJ } from '../../src/js/utils/worldwide'; @@ -80,6 +81,7 @@ describe('React Navigation - TTID', () => { data: { 'sentry.op': 'ui.load.initial_display', 'sentry.origin': SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'New Screen initial display', op: 'ui.load.initial_display', @@ -110,6 +112,7 @@ describe('React Navigation - TTID', () => { data: { 'sentry.op': 'ui.load.initial_display', 'sentry.origin': SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'New Screen initial display', op: 'ui.load.initial_display', @@ -146,6 +149,7 @@ describe('React Navigation - TTID', () => { data: { 'sentry.op': 'ui.load.initial_display', 'sentry.origin': SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'New Screen initial display', op: 'ui.load.initial_display', @@ -203,6 +207,7 @@ describe('React Navigation - TTID', () => { 'sentry.op': 'navigation.processing', 'sentry.origin': SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION, 'sentry.source': 'custom', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'Navigation dispatch to screen New Screen mounted', op: 'navigation.processing', @@ -231,6 +236,7 @@ describe('React Navigation - TTID', () => { 'sentry.op': 'navigation.processing', 'sentry.origin': SPAN_ORIGIN_AUTO_NAVIGATION_REACT_NAVIGATION, 'sentry.source': 'custom', + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'Navigation dispatch to screen Initial Screen mounted', op: 'navigation.processing', @@ -261,6 +267,7 @@ describe('React Navigation - TTID', () => { data: { 'sentry.op': 'ui.load.initial_display', 'sentry.origin': SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'Initial Screen initial display', op: 'ui.load.initial_display', @@ -295,6 +302,7 @@ describe('React Navigation - TTID', () => { data: { 'sentry.op': 'ui.load.full_display', 'sentry.origin': SPAN_ORIGIN_MANUAL_UI_TIME_TO_DISPLAY, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'Time To Full Display', op: 'ui.load.full_display', @@ -371,6 +379,7 @@ describe('React Navigation - TTID', () => { data: { 'sentry.op': 'ui.load.initial_display', 'sentry.origin': SPAN_ORIGIN_AUTO_UI_TIME_TO_DISPLAY, + [SPAN_THREAD_NAME]: SPAN_THREAD_NAME_JAVASCRIPT, }, description: 'New Screen initial display', op: 'ui.load.initial_display',