Skip to content

Commit

Permalink
feat(react-native): support for starting native spans on android (#536)
Browse files Browse the repository at this point in the history
* feat(react-native): support for starting native spans on android

* refactor(react-native): add ReactNativeSpanContext class

* refactor(react-native): use constants for hex radix and trace id midpoint values

* fix(react-native): update `isNativePerformanceAvailable` runtime check
  • Loading branch information
yousif-bugsnag authored Dec 2, 2024
1 parent d890a1c commit ab5a293
Show file tree
Hide file tree
Showing 5 changed files with 153 additions and 20 deletions.
Original file line number Diff line number Diff line change
@@ -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();
Expand All @@ -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
}
}

Expand Down Expand Up @@ -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();
Expand All @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <T> Callable<T> wrap(@NonNull Callable<T> callable) {
return SpanContext.DefaultImpls.wrap(this, callable);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
28 changes: 23 additions & 5 deletions packages/platforms/react-native/lib/NativeBugsnagPerformance.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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<string>
isNativePerformanceAvailable: () => boolean
getNativeConfiguration: () => NativeConfiguration | null
startNativeSpan: (name: string, options: NativeSpanOptions) => NativeSpan
}

export default TurboModuleRegistry.get<Spec>(
Expand Down

0 comments on commit ab5a293

Please sign in to comment.