diff --git a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java index 6c3e7fedf..d1140d543 100644 --- a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java +++ b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/NativeBugsnagPerformanceImpl.java @@ -1,24 +1,29 @@ package com.bugsnag.reactnative.performance; -import android.content.Context; import android.content.pm.PackageInfo; -import android.content.pm.PackageManager; import android.os.Build; -import androidx.annotation.NonNull; + import androidx.annotation.Nullable; import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; -import com.facebook.react.bridge.WritableArray; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import java.security.SecureRandom; + import com.bugsnag.android.performance.BugsnagPerformance; +import com.bugsnag.android.performance.SpanOptions; + +import com.bugsnag.android.performance.internal.BugsnagClock; +import com.bugsnag.android.performance.internal.EncodingUtils; +import com.bugsnag.android.performance.internal.SpanFactory; +import com.bugsnag.android.performance.internal.SpanImpl; import com.bugsnag.android.performance.internal.processing.ImmutableConfig; class NativeBugsnagPerformanceImpl { - + static final String MODULE_NAME = "BugsnagReactNativePerformance"; - + private final ReactApplicationContext reactContext; private final SecureRandom random = new SecureRandom(); @@ -29,15 +34,11 @@ public NativeBugsnagPerformanceImpl(ReactApplicationContext reactContext) { this.reactContext = reactContext; try { - Class.forName("com.bugsnag.android.performance.BugsnagPerformance"); - Class.forName("com.bugsnag.android.performance.internal.InstrumentedAppState"); + BugsnagPerformance.INSTANCE.getInstrumentedAppState$internal().getConfig$internal(); isNativePerformanceAvailable = true; } catch (LinkageError e) { - // do nothing, class found but is incompatible - } - catch (ClassNotFoundException e) { - // do nothing, Android Performance SDK is not installed + // do nothing, Android Performance SDK is not installed or is incompatible } } @@ -107,11 +108,11 @@ public WritableMap getNativeConfiguration() { WritableMap result = Arguments.createMap(); result.putString("apiKey", nativeConfig.getApiKey()); - result.putString("endpoint", nativeConfig.getEndpoint()); + result.putString("endpoint", nativeConfig.getEndpoint()); result.putString("releaseStage", nativeConfig.getReleaseStage()); result.putString("serviceName", nativeConfig.getServiceName()); result.putInt("attributeCountLimit", nativeConfig.getAttributeCountLimit()); - result.putInt("attriubuteStringValueLimit", nativeConfig.getAttributeStringValueLimit()); + result.putInt("attributeStringValueLimit", nativeConfig.getAttributeStringValueLimit()); result.putInt("attributeArrayLengthLimit", nativeConfig.getAttributeArrayLengthLimit()); var appVersion = nativeConfig.getAppVersion(); @@ -130,9 +131,67 @@ public WritableMap getNativeConfiguration() { } return result; - } + } @Nullable + public WritableMap startNativeSpan(String name, ReadableMap options) { + if (!isNativePerformanceAvailable) { + return null; + } + + SpanOptions spanOptions = readableMapToSpanOptions(options); + SpanFactory spanFactory = BugsnagPerformance.INSTANCE.getInstrumentedAppState$internal().getSpanFactory(); + SpanImpl nativeSpan = spanFactory.createCustomSpan(name, spanOptions); + + nativeSpan.getAttributes().getEntries$internal().clear(); + + WritableMap span = nativeSpanToJsSpan(nativeSpan); + return span; + } + + private WritableMap nativeSpanToJsSpan(SpanImpl nativeSpan) { + WritableMap span = Arguments.createMap(); + span.putString("name", nativeSpan.getName()); + span.putString("id", EncodingUtils.toHexString(nativeSpan.getSpanId())); + span.putString("traceId", EncodingUtils.toHexString(nativeSpan.getTraceId())); + + long unixNanoStartTime = BugsnagClock.INSTANCE.elapsedNanosToUnixTime(nativeSpan.getStartTime$internal()); + span.putDouble("startTime", (double)unixNanoStartTime); + + long parentSpanId = nativeSpan.getParentSpanId(); + if (parentSpanId != 0L) { + span.putString("parentSpanId", EncodingUtils.toHexString(parentSpanId)); + } + + return span; + } + + private SpanOptions readableMapToSpanOptions(ReadableMap jsOptions) { + SpanOptions spanOptions = SpanOptions.DEFAULTS + .setFirstClass(true) + .makeCurrentContext(false) + .within(null); + + if (jsOptions.hasKey("startTime")) { + double startTime = jsOptions.getDouble("startTime"); + long nativeStartTime = BugsnagClock.INSTANCE.unixNanoTimeToElapsedRealtime((long)startTime); + spanOptions = spanOptions.startTime(nativeStartTime); + } + + ReadableMap parentContext = null; + if (jsOptions.hasKey("parentContext") && (parentContext = jsOptions.getMap("parentContext")) != null) { + ReactNativeSpanContext nativeParentContext = new ReactNativeSpanContext( + parentContext.getString("id"), + parentContext.getString("traceId") + ); + + spanOptions = spanOptions.within(nativeParentContext); + } + + return spanOptions; + } + + @Nullable private String abiToArchitecture(@Nullable String abi) { if (abi == null) { return null; diff --git a/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/ReactNativeSpanContext.java b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/ReactNativeSpanContext.java new file mode 100644 index 000000000..6dc536d59 --- /dev/null +++ b/packages/platforms/react-native/android/src/main/java/com/bugsnag/android/performance/ReactNativeSpanContext.java @@ -0,0 +1,45 @@ +package com.bugsnag.reactnative.performance; + +import androidx.annotation.NonNull; +import com.bugsnag.android.performance.SpanContext; +import java.util.UUID; +import java.util.concurrent.Callable; + +class ReactNativeSpanContext implements SpanContext { + private static final int HEX_RADIX = 16; + private static final int TRACE_ID_MIDPOINT = 16; + + private final long nativeSpanId; + private final UUID nativeTraceId; + + ReactNativeSpanContext(String spanId, String traceId) { + nativeSpanId = Long.parseUnsignedLong(spanId, HEX_RADIX); + nativeTraceId = new UUID( + Long.parseUnsignedLong(traceId.substring(0, TRACE_ID_MIDPOINT), HEX_RADIX), + Long.parseUnsignedLong(traceId.substring(TRACE_ID_MIDPOINT), HEX_RADIX) + ); + } + + @Override + public long getSpanId() { + return nativeSpanId; + } + + @NonNull + @Override + public UUID getTraceId() { + return nativeTraceId; + } + + @NonNull + @Override + public Runnable wrap(@NonNull Runnable runnable) { + return SpanContext.DefaultImpls.wrap(this, runnable); + } + + @NonNull + @Override + public Callable wrap(@NonNull Callable callable) { + return SpanContext.DefaultImpls.wrap(this, callable); + } +} diff --git a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java index e816856c4..2f30c9391 100644 --- a/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java +++ b/packages/platforms/react-native/android/src/newarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java @@ -4,6 +4,7 @@ import com.bugsnag.reactnative.performance.NativeBugsnagPerformanceSpec; import com.facebook.react.bridge.Promise; import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; public class BugsnagReactNativePerformance extends NativeBugsnagPerformanceSpec { @@ -44,5 +45,9 @@ public boolean isNativePerformanceAvailable() { public WritableMap getNativeConfiguration() { return impl.getNativeConfiguration(); } + + @Override public WritableMap startNativeSpan(String name, ReadableMap options) { + return impl.startNativeSpan(name, options); + } } diff --git a/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java b/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java index 831355e02..6cb22e1cd 100644 --- a/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java +++ b/packages/platforms/react-native/android/src/oldarch/java/com/bugsnag/android/performance/BugsnagReactNativePerformance.java @@ -4,6 +4,7 @@ import com.facebook.react.bridge.ReactApplicationContext; import com.facebook.react.bridge.ReactContextBaseJavaModule; import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.ReadableMap; import com.facebook.react.bridge.WritableMap; import java.util.Map; @@ -46,4 +47,9 @@ public boolean isNativePerformanceAvailable() { public WritableMap getNativeConfiguration() { return impl.getNativeConfiguration(); } + + @ReactMethod(isBlockingSynchronousMethod = true) + public WritableMap startNativeSpan(String name, ReadableMap options) { + return impl.startNativeSpan(name, options); + } } diff --git a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts index b6a7c86d3..1c9ab7cc4 100644 --- a/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts +++ b/packages/platforms/react-native/lib/NativeBugsnagPerformance.ts @@ -1,7 +1,7 @@ +/* eslint-disable @typescript-eslint/consistent-type-definitions */ import type { TurboModule } from 'react-native/Libraries/TurboModule/RCTExport' import { TurboModuleRegistry } from 'react-native' -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type DeviceInfo = { arch: string | undefined model: string | undefined @@ -10,26 +10,44 @@ export type DeviceInfo = { bundleIdentifier: string | undefined } -// eslint-disable-next-line @typescript-eslint/consistent-type-definitions export type NativeConfiguration = { apiKey: string endpoint: string - samplingProbability?: number - appVersion?: string + samplingProbability: number | undefined + appVersion: string | undefined releaseStage: string - enabledReleaseStages?: string[] + enabledReleaseStages: string[] | undefined serviceName: string attributeCountLimit: number attributeStringValueLimit: number attributeArrayLengthLimit: number } +export type ParentContext = { + id: string + traceId: string +} + +export type NativeSpanOptions = { + startTime: number | undefined + parentContext: ParentContext | null +} + +export type NativeSpan = { + name: string + id: string + traceId: string + startTime: number + parentSpanId: string +} + export interface Spec extends TurboModule { getDeviceInfo: () => DeviceInfo requestEntropy: () => string requestEntropyAsync: () => Promise isNativePerformanceAvailable: () => boolean getNativeConfiguration: () => NativeConfiguration | null + startNativeSpan: (name: string, options: NativeSpanOptions) => NativeSpan } export default TurboModuleRegistry.get(