@barrysolomon/mobile-react-native 0.1.0-alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Dash0Mobile.podspec +29 -0
- package/README.md +117 -0
- package/android/build.gradle +68 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/BridgeCallSink.kt +67 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobileModule.kt +198 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobilePackage.kt +14 -0
- package/android/src/main/java/com/dash0/mobile/reactnative/OTelMobileCallSink.kt +208 -0
- package/android/src/test/java/com/dash0/mobile/reactnative/Dash0MobileModuleTest.kt +413 -0
- package/ios/BridgeCallSink.swift +83 -0
- package/ios/Dash0MobileBridgeDispatcher.swift +142 -0
- package/ios/OTelMobileCallSink.swift +262 -0
- package/ios/RCTDash0MobileModule.m +28 -0
- package/ios/RCTDash0MobileModule.swift +104 -0
- package/ios/Tests/Dash0MobileBridgeDispatcherTests.swift +341 -0
- package/lib/src/NativeDash0Mobile.d.ts +27 -0
- package/lib/src/NativeDash0Mobile.d.ts.map +1 -0
- package/lib/src/NativeDash0Mobile.js +19 -0
- package/lib/src/bridge/NativeBridge.d.ts +38 -0
- package/lib/src/bridge/NativeBridge.d.ts.map +1 -0
- package/lib/src/bridge/NativeBridge.js +95 -0
- package/lib/src/bridge/types.d.ts +166 -0
- package/lib/src/bridge/types.d.ts.map +1 -0
- package/lib/src/bridge/types.js +10 -0
- package/lib/src/index.d.ts +35 -0
- package/lib/src/index.d.ts.map +1 -0
- package/lib/src/index.js +408 -0
- package/lib/src/instrumentation/errors.d.ts +14 -0
- package/lib/src/instrumentation/errors.d.ts.map +1 -0
- package/lib/src/instrumentation/errors.js +65 -0
- package/lib/src/instrumentation/fetch.d.ts +16 -0
- package/lib/src/instrumentation/fetch.d.ts.map +1 -0
- package/lib/src/instrumentation/fetch.js +75 -0
- package/lib/src/instrumentation/navigation.d.ts +19 -0
- package/lib/src/instrumentation/navigation.d.ts.map +1 -0
- package/lib/src/instrumentation/navigation.js +39 -0
- package/lib/src/instrumentation/touch.d.ts +12 -0
- package/lib/src/instrumentation/touch.d.ts.map +1 -0
- package/lib/src/instrumentation/touch.js +18 -0
- package/lib/src/instrumentation/unhandledRejection.d.ts +9 -0
- package/lib/src/instrumentation/unhandledRejection.d.ts.map +1 -0
- package/lib/src/instrumentation/unhandledRejection.js +52 -0
- package/lib/src/instrumentation/xhr.d.ts +14 -0
- package/lib/src/instrumentation/xhr.d.ts.map +1 -0
- package/lib/src/instrumentation/xhr.js +88 -0
- package/lib/src/otel-compat.d.ts +67 -0
- package/lib/src/otel-compat.d.ts.map +1 -0
- package/lib/src/otel-compat.js +84 -0
- package/package.json +72 -0
- package/react-native.config.js +17 -0
- package/src/NativeDash0Mobile.ts +29 -0
- package/src/bridge/NativeBridge.ts +101 -0
- package/src/bridge/types.ts +188 -0
- package/src/index.ts +456 -0
- package/src/instrumentation/errors.ts +84 -0
- package/src/instrumentation/fetch.ts +93 -0
- package/src/instrumentation/navigation.ts +52 -0
- package/src/instrumentation/touch.ts +32 -0
- package/src/instrumentation/unhandledRejection.ts +75 -0
- package/src/instrumentation/xhr.ts +125 -0
- package/src/otel-compat.ts +159 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "Dash0Mobile"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = package["repository"]
|
|
10
|
+
s.license = "Apache-2.0"
|
|
11
|
+
s.author = { "Dash0" => "hello@dash0.com" }
|
|
12
|
+
|
|
13
|
+
s.platforms = { :ios => "15.0" }
|
|
14
|
+
s.source = { :path => "." }
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
16
|
+
# OTelMobileCallSink.swift is excluded from the pod because it depends on
|
|
17
|
+
# OTelMobileSDK, which in most RN apps is delivered via SwiftPM on the
|
|
18
|
+
# app's Xcode project — not through CocoaPods. The pod compiles without
|
|
19
|
+
# it; consumers copy OTelMobileCallSink.swift into their app target and
|
|
20
|
+
# call `Dash0MobileModule.installSink { OTelMobileCallSink() }` from
|
|
21
|
+
# AppDelegate to activate real telemetry.
|
|
22
|
+
s.exclude_files = [
|
|
23
|
+
"ios/Tests/**/*",
|
|
24
|
+
"ios/OTelMobileCallSink.swift",
|
|
25
|
+
]
|
|
26
|
+
s.swift_version = "5.9"
|
|
27
|
+
|
|
28
|
+
s.dependency "React-Core"
|
|
29
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# @dash0/mobile-react-native
|
|
2
|
+
|
|
3
|
+
Dash0 Mobile Observability SDK for React Native. Bridges JS/TS into the
|
|
4
|
+
existing native Android (`otel-android-mobile`) and iOS (`otel-ios-mobile`)
|
|
5
|
+
SDKs — native owns buffering, policy evaluation, OTLP export, and crash
|
|
6
|
+
recovery. JS stays thin.
|
|
7
|
+
|
|
8
|
+
**Status:** Complete and validated end-to-end in Dash0. Both Android and iOS
|
|
9
|
+
builds produce real binaries (140 MB APK, 239 MB .app) and telemetry lands in
|
|
10
|
+
Dash0 within ~3 s. All 4 platforms (Android native, iOS native, RN Android,
|
|
11
|
+
RN iOS) have a UAT matrix of 12/12 cells green. 83 Jest tests pass.
|
|
12
|
+
|
|
13
|
+
## Quickstart
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { Dash0Mobile } from '@dash0/mobile-react-native';
|
|
17
|
+
|
|
18
|
+
await Dash0Mobile.start({
|
|
19
|
+
serviceName: 'my-rn-app',
|
|
20
|
+
endpoint: 'https://ingress.us-west-2.aws.dash0.com/v1/logs',
|
|
21
|
+
authToken: process.env.DASH0_AUTH_TOKEN,
|
|
22
|
+
dataset: 'otel-mobile',
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
Dash0Mobile.log('cart.add_item', { 'shop.item_id': 'abc', qty: 2 });
|
|
26
|
+
|
|
27
|
+
await Dash0Mobile.span('checkout', async () => {
|
|
28
|
+
await doCheckout();
|
|
29
|
+
});
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Build & test
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
# Install dependencies (first time)
|
|
36
|
+
npm install
|
|
37
|
+
|
|
38
|
+
# Jest — bridge contract + auto-instrumentation (83 tests)
|
|
39
|
+
npm test
|
|
40
|
+
|
|
41
|
+
# Type-check
|
|
42
|
+
npx tsc --noEmit
|
|
43
|
+
|
|
44
|
+
# End-to-end (package + AstronomyShopRN demo, Jest mode — no simulator)
|
|
45
|
+
../../scripts/test/validate-rn-end-to-end.sh --mode=jest
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Layout
|
|
49
|
+
|
|
50
|
+
```text
|
|
51
|
+
src/
|
|
52
|
+
bridge/
|
|
53
|
+
types.ts # cross-repo seam — DO NOT change without coordinating
|
|
54
|
+
NativeBridge.ts # debounced batching marshaller (50 ms batch window)
|
|
55
|
+
instrumentation/
|
|
56
|
+
fetch.ts # fetch/XHR span auto-capture
|
|
57
|
+
xhr.ts # XHR span auto-capture
|
|
58
|
+
errors.ts # JS error log auto-capture
|
|
59
|
+
unhandledRejection.ts # unhandled promise rejection capture
|
|
60
|
+
navigation.ts # React Navigation screen-view auto-capture
|
|
61
|
+
touch.ts # tap event auto-capture
|
|
62
|
+
otel-compat.ts # OTel-API shim (third-party JS libs flow through bridge)
|
|
63
|
+
index.ts # public API
|
|
64
|
+
android/ # Kotlin ReactContextBaseJavaModule + BridgeCallSink
|
|
65
|
+
ios/ # Swift RCTDash0MobileModule + BridgeCallSink
|
|
66
|
+
__tests__/ # Jest — 83 tests across bridge + instrumentation
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Auto-instrumentation
|
|
70
|
+
|
|
71
|
+
Enabled by default when `Dash0Mobile.start()` is called. Opt out per signal:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
await Dash0Mobile.start({
|
|
75
|
+
// ...
|
|
76
|
+
autoCapture: {
|
|
77
|
+
network: false, // disable fetch/XHR spans
|
|
78
|
+
errors: false, // disable JS error + rejection logs
|
|
79
|
+
lifecycle: false, // disable AppState fg/bg
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Architecture
|
|
85
|
+
|
|
86
|
+
The JS layer is a thin marshaller with a 50 ms batching window. All buffering,
|
|
87
|
+
policy evaluation, export scheduling, and crash recovery happen inside the
|
|
88
|
+
native SDK on each platform:
|
|
89
|
+
|
|
90
|
+
```text
|
|
91
|
+
JS (fetch/XHR/errors/nav/tap)
|
|
92
|
+
↓ 50 ms batch window
|
|
93
|
+
NativeBridge.ts → NativeDash0Mobile → Android: OTelMobile (gRPC :4317)
|
|
94
|
+
→ iOS: OTelMobile (HTTP :4318)
|
|
95
|
+
↓
|
|
96
|
+
OTLP Collector → Dash0
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Transport note:** Android uses OTLP/gRPC on port 4317; iOS uses OTLP/HTTP on
|
|
100
|
+
port 4318. The shared `otel-config.json` uses a per-platform port rewrite at
|
|
101
|
+
startup — do not assume a single endpoint works for both.
|
|
102
|
+
|
|
103
|
+
## Test strategy
|
|
104
|
+
|
|
105
|
+
Three layers, all must pass on every PR:
|
|
106
|
+
|
|
107
|
+
1. **Jest** — `npm test` in this directory; contract + bridge + instrumentation
|
|
108
|
+
2. **Native unit** — `./gradlew :android:test` (Android) + `swift test` (iOS)
|
|
109
|
+
3. **Real-app E2E** — `scripts/test/validate-rn-end-to-end.sh` boots
|
|
110
|
+
AstronomyShopRN on iOS Simulator + Android emulator, queries Dash0 after 75 s
|
|
111
|
+
|
|
112
|
+
## Known limitations
|
|
113
|
+
|
|
114
|
+
- **Expo** — not supported without eject (bare workflow only). An Expo config
|
|
115
|
+
plugin is planned as a follow-up.
|
|
116
|
+
- **Realm / Amplify DataStore** — scoped in a separate epic.
|
|
117
|
+
- **Web / desktop RN targets** — not supported.
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// @dash0/mobile-react-native — Android native module build.
|
|
2
|
+
//
|
|
3
|
+
// When consumed via RN autolinking, the host app's buildscript owns AGP + the
|
|
4
|
+
// Kotlin plugin versions. We apply the plugins here but let the host provide
|
|
5
|
+
// them — hardcoding classpath versions here forced an old AGP/Kotlin combo
|
|
6
|
+
// that conflicted with host-provided stdlib.
|
|
7
|
+
|
|
8
|
+
// Default matches RN 0.85's Kotlin (2.1.20). Older RN consumers can override
|
|
9
|
+
// via -PDash0Mobile_kotlinVersion.
|
|
10
|
+
ext.kotlin_version = project.findProperty('Dash0Mobile_kotlinVersion') ?: '2.1.20'
|
|
11
|
+
ext.safeExtGet = { prop, fallback ->
|
|
12
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
apply plugin: 'com.android.library'
|
|
16
|
+
apply plugin: 'org.jetbrains.kotlin.android'
|
|
17
|
+
|
|
18
|
+
android {
|
|
19
|
+
namespace 'com.dash0.mobile.reactnative'
|
|
20
|
+
compileSdk safeExtGet('compileSdkVersion', 34)
|
|
21
|
+
|
|
22
|
+
defaultConfig {
|
|
23
|
+
minSdk safeExtGet('minSdkVersion', 26)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
compileOptions {
|
|
27
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
28
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
kotlinOptions {
|
|
32
|
+
jvmTarget = '17'
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
testOptions {
|
|
36
|
+
targetSdk safeExtGet('targetSdkVersion', 34)
|
|
37
|
+
unitTests {
|
|
38
|
+
includeAndroidResources = true
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lint {
|
|
43
|
+
targetSdk safeExtGet('targetSdkVersion', 34)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
repositories {
|
|
48
|
+
google()
|
|
49
|
+
mavenCentral()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
dependencies {
|
|
53
|
+
implementation "com.facebook.react:react-android:+"
|
|
54
|
+
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
|
55
|
+
|
|
56
|
+
// Depend on the existing native Android SDK. Must be `implementation`
|
|
57
|
+
// (not `compileOnly`) so Kotlin can resolve transitive config classes
|
|
58
|
+
// (TextInputConfig, BreadcrumbConfig, VitalsConfig, etc.) referenced
|
|
59
|
+
// through MobileConfig's constructor signature.
|
|
60
|
+
implementation 'io.opentelemetry.android:mobile:0.1.0-alpha'
|
|
61
|
+
implementation 'io.opentelemetry:opentelemetry-api:1.58.0'
|
|
62
|
+
|
|
63
|
+
testImplementation 'junit:junit:4.13.2'
|
|
64
|
+
testImplementation 'org.mockito:mockito-core:5.12.0'
|
|
65
|
+
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.4.0'
|
|
66
|
+
testImplementation 'io.opentelemetry.android:mobile:0.1.0-alpha'
|
|
67
|
+
testImplementation 'io.opentelemetry:opentelemetry-api:1.58.0'
|
|
68
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android" />
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Seam between the RN bridge and the native OTel Mobile SDK.
|
|
3
|
+
*
|
|
4
|
+
* The production implementation (`OTelMobileCallSink`) forwards calls into
|
|
5
|
+
* `io.opentelemetry.android.mobile.OTelMobile`. Tests provide a fake so we
|
|
6
|
+
* can assert forwarding behavior without standing up Android's Application,
|
|
7
|
+
* emulator, or the full OTel SDK.
|
|
8
|
+
*/
|
|
9
|
+
package com.dash0.mobile.reactnative
|
|
10
|
+
|
|
11
|
+
interface BridgeCallSink {
|
|
12
|
+
fun start(config: StartConfig)
|
|
13
|
+
fun emitLog(name: String, severity: Int, attributes: Map<String, Any?>, timeUnixNano: Long)
|
|
14
|
+
fun startSpan(spanId: String, parentSpanId: String?, name: String, spanKind: String, attributes: Map<String, Any?>, startTimeUnixNano: Long)
|
|
15
|
+
fun endSpan(spanId: String, status: String, statusMessage: String?, attributes: Map<String, Any?>, endTimeUnixNano: Long)
|
|
16
|
+
fun recordMetric(name: String, instrumentType: String, value: Double, attributes: Map<String, Any?>, timeUnixNano: Long)
|
|
17
|
+
fun flushWindow(minutes: Int)
|
|
18
|
+
fun shutdown()
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Synchronously drain every buffered telemetry record through the
|
|
22
|
+
* underlying SDK exporter, persisting any in-flight records to disk
|
|
23
|
+
* on export failure. Called by `Dash0MobileModule.dispatch`
|
|
24
|
+
* immediately after dispatching a FATAL-severity (severity ≥ 21)
|
|
25
|
+
* log emit, before continuing to the next payload in the batch.
|
|
26
|
+
*
|
|
27
|
+
* Mirrors the iOS [`BridgeCallSink.forceFlush`](../../../../../../../ios/BridgeCallSink.swift)
|
|
28
|
+
* contract introduced in commit `39bd258`. Default no-op so test
|
|
29
|
+
* fakes and lightweight non-RN consumers inherit safe behavior;
|
|
30
|
+
* production sinks override.
|
|
31
|
+
*
|
|
32
|
+
* Kept as a default method so existing implementations (test
|
|
33
|
+
* fakes from earlier RN-bridge work) compile without needing to
|
|
34
|
+
* be touched. Production [`OTelMobileCallSink.forceFlush`]
|
|
35
|
+
* overrides this to call [io.opentelemetry.android.mobile.buffering.MobileLogRecordProcessor.forceFlush].
|
|
36
|
+
*/
|
|
37
|
+
fun forceFlush() = Unit
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
data class StartConfig(
|
|
41
|
+
val serviceName: String,
|
|
42
|
+
val serviceVersion: String?,
|
|
43
|
+
val endpoint: String,
|
|
44
|
+
val authToken: String?,
|
|
45
|
+
val dataset: String?,
|
|
46
|
+
/**
|
|
47
|
+
* Extra resource attributes supplied by the JS caller. The RN bridge
|
|
48
|
+
* populates `telemetry.distro.name` / `telemetry.distro.version` by
|
|
49
|
+
* default; apps can add their own keys through `Dash0Mobile.start`.
|
|
50
|
+
*/
|
|
51
|
+
val extraResourceAttributes: Map<String, String>? = null,
|
|
52
|
+
/**
|
|
53
|
+
* Native-only auto-capture capability tokens the JS caller explicitly
|
|
54
|
+
* opted into. Default empty = no native auto-capture (RN apps get
|
|
55
|
+
* network/errors/lifecycle coverage from the JS-side shims).
|
|
56
|
+
*
|
|
57
|
+
* Supported tokens: "network", "errors", "lifecycle", "tap", "scroll",
|
|
58
|
+
* "textInput", "screen", "freeze", "vitals", "deviceStats".
|
|
59
|
+
*
|
|
60
|
+
* Today the Android MobileConfig's auto-capture is driven separately by
|
|
61
|
+
* the host app's `OTelMobileBuilder` — these tokens are accepted here
|
|
62
|
+
* for cross-platform bridge parity but not yet consumed. When the
|
|
63
|
+
* Android SDK gains a unified `autoCaptureOptions` on MobileConfig, the
|
|
64
|
+
* sink will translate them the same way the iOS sink does today.
|
|
65
|
+
*/
|
|
66
|
+
val nativeAutoCapture: List<String> = emptyList(),
|
|
67
|
+
)
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Dash0MobileModule — React Native bridge for @dash0/mobile-react-native.
|
|
3
|
+
*
|
|
4
|
+
* Responsibilities (kept deliberately thin):
|
|
5
|
+
* 1. Decode JS payloads (ReadableMap / ReadableArray) into native types.
|
|
6
|
+
* 2. Dispatch on the payload `kind` field.
|
|
7
|
+
* 3. Forward to a BridgeCallSink (production: OTelMobileCallSink).
|
|
8
|
+
*
|
|
9
|
+
* All buffering / policy / export / crash recovery lives in the existing
|
|
10
|
+
* io.opentelemetry.android.mobile.OTelMobile SDK — not here.
|
|
11
|
+
*/
|
|
12
|
+
package com.dash0.mobile.reactnative
|
|
13
|
+
|
|
14
|
+
import com.facebook.react.bridge.Promise
|
|
15
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
16
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
17
|
+
import com.facebook.react.bridge.ReactMethod
|
|
18
|
+
import com.facebook.react.bridge.ReadableArray
|
|
19
|
+
import com.facebook.react.bridge.ReadableMap
|
|
20
|
+
import com.facebook.react.bridge.ReadableType
|
|
21
|
+
|
|
22
|
+
class Dash0MobileModule internal constructor(
|
|
23
|
+
reactContext: ReactApplicationContext,
|
|
24
|
+
private val sink: BridgeCallSink,
|
|
25
|
+
) : ReactContextBaseJavaModule(reactContext) {
|
|
26
|
+
|
|
27
|
+
constructor(reactContext: ReactApplicationContext) :
|
|
28
|
+
this(reactContext, OTelMobileCallSink(reactContext.applicationContext))
|
|
29
|
+
|
|
30
|
+
override fun getName(): String = NAME
|
|
31
|
+
|
|
32
|
+
@ReactMethod
|
|
33
|
+
fun start(config: ReadableMap, promise: Promise) {
|
|
34
|
+
try {
|
|
35
|
+
sink.start(
|
|
36
|
+
StartConfig(
|
|
37
|
+
serviceName = config.getString("serviceName")
|
|
38
|
+
?: error("serviceName is required"),
|
|
39
|
+
serviceVersion = config.getStringOrNull("serviceVersion"),
|
|
40
|
+
endpoint = config.getString("endpoint")
|
|
41
|
+
?: error("endpoint is required"),
|
|
42
|
+
authToken = config.getStringOrNull("authToken"),
|
|
43
|
+
dataset = config.getStringOrNull("dataset"),
|
|
44
|
+
extraResourceAttributes = config
|
|
45
|
+
.getMapOrNull("extraResourceAttributes")
|
|
46
|
+
?.toStringMap(),
|
|
47
|
+
nativeAutoCapture = config
|
|
48
|
+
.getArrayOrNull("nativeAutoCapture")
|
|
49
|
+
?.toStringList()
|
|
50
|
+
?: emptyList(),
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
promise.resolve(null)
|
|
54
|
+
} catch (t: Throwable) {
|
|
55
|
+
promise.reject("Dash0Mobile.start", t)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@ReactMethod
|
|
60
|
+
fun emitBatch(payloads: ReadableArray, promise: Promise) {
|
|
61
|
+
try {
|
|
62
|
+
for (i in 0 until payloads.size()) {
|
|
63
|
+
val p = payloads.getMap(i) ?: continue
|
|
64
|
+
dispatch(p)
|
|
65
|
+
}
|
|
66
|
+
promise.resolve(null)
|
|
67
|
+
} catch (t: Throwable) {
|
|
68
|
+
promise.reject("Dash0Mobile.emitBatch", t)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@ReactMethod
|
|
73
|
+
fun flushWindow(minutes: Double, promise: Promise) {
|
|
74
|
+
try {
|
|
75
|
+
sink.flushWindow(minutes.toInt())
|
|
76
|
+
promise.resolve(null)
|
|
77
|
+
} catch (t: Throwable) {
|
|
78
|
+
promise.reject("Dash0Mobile.flushWindow", t)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@ReactMethod
|
|
83
|
+
fun shutdown(promise: Promise) {
|
|
84
|
+
try {
|
|
85
|
+
sink.shutdown()
|
|
86
|
+
promise.resolve(null)
|
|
87
|
+
} catch (t: Throwable) {
|
|
88
|
+
promise.reject("Dash0Mobile.shutdown", t)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private fun dispatch(p: ReadableMap) {
|
|
93
|
+
val kind = p.getString("kind") ?: return
|
|
94
|
+
val attrs = p.getMapOrNull("attributes")?.toAttributeMap() ?: emptyMap()
|
|
95
|
+
when (kind) {
|
|
96
|
+
"log" -> {
|
|
97
|
+
val severity = p.getInt("severity")
|
|
98
|
+
sink.emitLog(
|
|
99
|
+
name = p.getString("name") ?: return,
|
|
100
|
+
severity = severity,
|
|
101
|
+
attributes = attrs,
|
|
102
|
+
timeUnixNano = p.getStringAsLong("timeUnixNano"),
|
|
103
|
+
)
|
|
104
|
+
// FATAL-severity logs (OTel semconv 21..24) are the
|
|
105
|
+
// crash path. JS-side bypasses the 50ms debounce via
|
|
106
|
+
// emitSync, but the payload still sits in
|
|
107
|
+
// MobileLogRecordProcessor's RAM buffer waiting for the
|
|
108
|
+
// periodic flush. Eagerly drain BEFORE the next payload
|
|
109
|
+
// in the batch so the FATAL has a chance to reach disk
|
|
110
|
+
// (and from there OTLP) even if the next payload's
|
|
111
|
+
// dispatch path or the JS reporter terminates the
|
|
112
|
+
// process. Mirrors iOS dispatcher commit `39bd258`.
|
|
113
|
+
if (severity >= 21) {
|
|
114
|
+
sink.forceFlush()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
"spanStart" -> sink.startSpan(
|
|
118
|
+
spanId = p.getString("spanId") ?: return,
|
|
119
|
+
parentSpanId = p.getStringOrNull("parentSpanId"),
|
|
120
|
+
name = p.getString("name") ?: return,
|
|
121
|
+
spanKind = p.getString("spanKind") ?: "INTERNAL",
|
|
122
|
+
attributes = attrs,
|
|
123
|
+
startTimeUnixNano = p.getStringAsLong("startTimeUnixNano"),
|
|
124
|
+
)
|
|
125
|
+
"spanEnd" -> sink.endSpan(
|
|
126
|
+
spanId = p.getString("spanId") ?: return,
|
|
127
|
+
status = p.getString("status") ?: "UNSET",
|
|
128
|
+
statusMessage = p.getStringOrNull("statusMessage"),
|
|
129
|
+
attributes = attrs,
|
|
130
|
+
endTimeUnixNano = p.getStringAsLong("endTimeUnixNano"),
|
|
131
|
+
)
|
|
132
|
+
"metric" -> sink.recordMetric(
|
|
133
|
+
name = p.getString("name") ?: return,
|
|
134
|
+
instrumentType = p.getString("instrumentType") ?: "counter",
|
|
135
|
+
value = p.getDouble("value"),
|
|
136
|
+
attributes = attrs,
|
|
137
|
+
timeUnixNano = p.getStringAsLong("timeUnixNano"),
|
|
138
|
+
)
|
|
139
|
+
else -> Unit // unknown kinds are silently dropped (forward-compat)
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
companion object {
|
|
144
|
+
const val NAME = "Dash0Mobile"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── ReadableMap helpers ─────────────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
private fun ReadableMap.getStringOrNull(key: String): String? =
|
|
151
|
+
if (hasKey(key) && !isNull(key)) getString(key) else null
|
|
152
|
+
|
|
153
|
+
private fun ReadableMap.getMapOrNull(key: String): ReadableMap? =
|
|
154
|
+
if (hasKey(key) && !isNull(key)) getMap(key) else null
|
|
155
|
+
|
|
156
|
+
private fun ReadableMap.getArrayOrNull(key: String): ReadableArray? =
|
|
157
|
+
if (hasKey(key) && !isNull(key)) getArray(key) else null
|
|
158
|
+
|
|
159
|
+
private fun ReadableArray.toStringList(): List<String> {
|
|
160
|
+
val out = ArrayList<String>(size())
|
|
161
|
+
for (i in 0 until size()) {
|
|
162
|
+
if (getType(i) == ReadableType.String) {
|
|
163
|
+
getString(i)?.let { out.add(it) }
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return out
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
private fun ReadableMap.getStringAsLong(key: String): Long =
|
|
170
|
+
getStringOrNull(key)?.toLongOrNull() ?: 0L
|
|
171
|
+
|
|
172
|
+
private fun ReadableMap.toStringMap(): Map<String, String> {
|
|
173
|
+
val iter = keySetIterator()
|
|
174
|
+
val out = LinkedHashMap<String, String>()
|
|
175
|
+
while (iter.hasNextKey()) {
|
|
176
|
+
val k = iter.nextKey()
|
|
177
|
+
if (getType(k) == ReadableType.String) {
|
|
178
|
+
getString(k)?.let { out[k] = it }
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return out
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private fun ReadableMap.toAttributeMap(): Map<String, Any?> {
|
|
185
|
+
val iter = keySetIterator()
|
|
186
|
+
val out = LinkedHashMap<String, Any?>()
|
|
187
|
+
while (iter.hasNextKey()) {
|
|
188
|
+
val k = iter.nextKey()
|
|
189
|
+
out[k] = when (getType(k)) {
|
|
190
|
+
ReadableType.Null -> null
|
|
191
|
+
ReadableType.Boolean -> getBoolean(k)
|
|
192
|
+
ReadableType.Number -> getDouble(k)
|
|
193
|
+
ReadableType.String -> getString(k)
|
|
194
|
+
else -> null // RN bridge doesn't send nested Map/Array as attribute values
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return out
|
|
198
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
package com.dash0.mobile.reactnative
|
|
2
|
+
|
|
3
|
+
import com.facebook.react.ReactPackage
|
|
4
|
+
import com.facebook.react.bridge.NativeModule
|
|
5
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
6
|
+
import com.facebook.react.uimanager.ViewManager
|
|
7
|
+
|
|
8
|
+
class Dash0MobilePackage : ReactPackage {
|
|
9
|
+
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> =
|
|
10
|
+
listOf(Dash0MobileModule(reactContext))
|
|
11
|
+
|
|
12
|
+
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> =
|
|
13
|
+
emptyList()
|
|
14
|
+
}
|