@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.
Files changed (61) hide show
  1. package/Dash0Mobile.podspec +29 -0
  2. package/README.md +117 -0
  3. package/android/build.gradle +68 -0
  4. package/android/src/main/AndroidManifest.xml +1 -0
  5. package/android/src/main/java/com/dash0/mobile/reactnative/BridgeCallSink.kt +67 -0
  6. package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobileModule.kt +198 -0
  7. package/android/src/main/java/com/dash0/mobile/reactnative/Dash0MobilePackage.kt +14 -0
  8. package/android/src/main/java/com/dash0/mobile/reactnative/OTelMobileCallSink.kt +208 -0
  9. package/android/src/test/java/com/dash0/mobile/reactnative/Dash0MobileModuleTest.kt +413 -0
  10. package/ios/BridgeCallSink.swift +83 -0
  11. package/ios/Dash0MobileBridgeDispatcher.swift +142 -0
  12. package/ios/OTelMobileCallSink.swift +262 -0
  13. package/ios/RCTDash0MobileModule.m +28 -0
  14. package/ios/RCTDash0MobileModule.swift +104 -0
  15. package/ios/Tests/Dash0MobileBridgeDispatcherTests.swift +341 -0
  16. package/lib/src/NativeDash0Mobile.d.ts +27 -0
  17. package/lib/src/NativeDash0Mobile.d.ts.map +1 -0
  18. package/lib/src/NativeDash0Mobile.js +19 -0
  19. package/lib/src/bridge/NativeBridge.d.ts +38 -0
  20. package/lib/src/bridge/NativeBridge.d.ts.map +1 -0
  21. package/lib/src/bridge/NativeBridge.js +95 -0
  22. package/lib/src/bridge/types.d.ts +166 -0
  23. package/lib/src/bridge/types.d.ts.map +1 -0
  24. package/lib/src/bridge/types.js +10 -0
  25. package/lib/src/index.d.ts +35 -0
  26. package/lib/src/index.d.ts.map +1 -0
  27. package/lib/src/index.js +408 -0
  28. package/lib/src/instrumentation/errors.d.ts +14 -0
  29. package/lib/src/instrumentation/errors.d.ts.map +1 -0
  30. package/lib/src/instrumentation/errors.js +65 -0
  31. package/lib/src/instrumentation/fetch.d.ts +16 -0
  32. package/lib/src/instrumentation/fetch.d.ts.map +1 -0
  33. package/lib/src/instrumentation/fetch.js +75 -0
  34. package/lib/src/instrumentation/navigation.d.ts +19 -0
  35. package/lib/src/instrumentation/navigation.d.ts.map +1 -0
  36. package/lib/src/instrumentation/navigation.js +39 -0
  37. package/lib/src/instrumentation/touch.d.ts +12 -0
  38. package/lib/src/instrumentation/touch.d.ts.map +1 -0
  39. package/lib/src/instrumentation/touch.js +18 -0
  40. package/lib/src/instrumentation/unhandledRejection.d.ts +9 -0
  41. package/lib/src/instrumentation/unhandledRejection.d.ts.map +1 -0
  42. package/lib/src/instrumentation/unhandledRejection.js +52 -0
  43. package/lib/src/instrumentation/xhr.d.ts +14 -0
  44. package/lib/src/instrumentation/xhr.d.ts.map +1 -0
  45. package/lib/src/instrumentation/xhr.js +88 -0
  46. package/lib/src/otel-compat.d.ts +67 -0
  47. package/lib/src/otel-compat.d.ts.map +1 -0
  48. package/lib/src/otel-compat.js +84 -0
  49. package/package.json +72 -0
  50. package/react-native.config.js +17 -0
  51. package/src/NativeDash0Mobile.ts +29 -0
  52. package/src/bridge/NativeBridge.ts +101 -0
  53. package/src/bridge/types.ts +188 -0
  54. package/src/index.ts +456 -0
  55. package/src/instrumentation/errors.ts +84 -0
  56. package/src/instrumentation/fetch.ts +93 -0
  57. package/src/instrumentation/navigation.ts +52 -0
  58. package/src/instrumentation/touch.ts +32 -0
  59. package/src/instrumentation/unhandledRejection.ts +75 -0
  60. package/src/instrumentation/xhr.ts +125 -0
  61. 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
+ }