@coralogix/react-native-plugin 0.3.3 → 0.5.0

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/CHANGELOG.md CHANGED
@@ -1,3 +1,26 @@
1
+ ## 0.5.0 (2026-05-17)
2
+
3
+ ### 🚀 Features
4
+
5
+ - bridge startTimeMeasure / endTimeMeasure to native (CX-40525)
6
+ - expose excludeFromSampling on JS config + native bridges
7
+
8
+ ## Unreleased
9
+
10
+ ### 🚀 Features
11
+
12
+ - Bridge `CoralogixRum.startTimeMeasure(name, labels?)` / `CoralogixRum.endTimeMeasure(name)` to the native iOS and Android SDKs as a fire-and-forget pass-through ([CX-40525](https://coralogix.atlassian.net/browse/CX-40525)). The JS side keeps no state — native owns the in-flight registry.
13
+
14
+ ## 0.4.0 (2026-05-03)
15
+
16
+ ### 🚀 Features
17
+
18
+ - Custom Spans & Traces Exporter (CX-36055)
19
+
20
+ ### 🩹 Fixes
21
+
22
+ - import CoralogixIgnoredInstrument in index.ts for rollup build
23
+
1
24
  ## 0.3.3 (2026-04-05)
2
25
 
3
26
  ### 🩹 Fixes
package/CxSdk.podspec CHANGED
@@ -16,9 +16,9 @@ Pod::Spec.new do |s|
16
16
 
17
17
  s.source_files = "ios/**/*.{h,m,mm,swift}"
18
18
 
19
- s.dependency 'Coralogix','2.4.1'
20
- s.dependency 'CoralogixInternal','2.4.1'
21
- s.dependency 'SessionReplay','2.4.1'
19
+ s.dependency 'Coralogix','2.6.4'
20
+ s.dependency 'CoralogixInternal','2.6.4'
21
+ s.dependency 'SessionReplay','2.6.4'
22
22
 
23
23
  # Use install_modules_dependencies helper to install the dependencies if React Native version >=0.71.0.
24
24
  # See https://github.com/facebook/react-native/blob/febf6b7f33fdb4904669f99d795eba4c0f95d7bf/scripts/cocoapods/new_architecture.rb#L79.
package/README.md CHANGED
@@ -156,6 +156,25 @@ await CoralogixRum.init({
156
156
  });
157
157
  ```
158
158
 
159
+ ### Exclude From Sampling
160
+
161
+ By default, when a session is sampled out (via `sessionSampleRate`), nothing is
162
+ emitted for that session. Use `excludeFromSampling` to keep emitting specific
163
+ event categories even when the session is sampled out.
164
+
165
+ ```javascript
166
+ import { CoralogixRum } from '@coralogix/react-native-plugin';
167
+
168
+ await CoralogixRum.init({
169
+ // ...
170
+ sessionSampleRate: 10,
171
+ excludeFromSampling: ['errors', 'logs'], // always emitted, even for sampled-out sessions
172
+ });
173
+ ```
174
+
175
+ Allowed values: `'errors'`, `'logs'`, `'network'`, `'userInteractions'`,
176
+ `'mobileVitals'`, `'customSpan'`, `'customMeasurement'`.
177
+
159
178
  ### Ignore Errors
160
179
 
161
180
  The ignoreErrors option allows you to exclude errors that meet specific criteria.
@@ -184,6 +203,126 @@ await CoralogixRum.init({
184
203
  });
185
204
  ```
186
205
 
206
+ ### Custom Spans
207
+
208
+ Create manual RUM spans to instrument custom flows in your application. Requires `traceParentInHeader.enabled: true`.
209
+
210
+ **Prerequisite:**
211
+ ```javascript
212
+ await CoralogixRum.init({
213
+ // ...
214
+ traceParentInHeader: { enabled: true },
215
+ });
216
+ ```
217
+
218
+ **Basic usage — global span with a child:**
219
+ ```javascript
220
+ import { CoralogixRum } from '@coralogix/react-native-plugin';
221
+
222
+ const tracer = CoralogixRum.getCustomTracer();
223
+
224
+ const globalSpan = await tracer.startGlobalSpan('checkout', { step: 'start' });
225
+ if (!globalSpan) return; // another global span is already active
226
+
227
+ const childSpan = await globalSpan.startCustomSpan('validate-cart');
228
+ await childSpan?.endSpan();
229
+
230
+ await globalSpan.endSpan();
231
+ ```
232
+
233
+ **Linking network requests with `withContext`:**
234
+
235
+ When you call `fetch` inside `withContext`, the network request is automatically linked to the active global span's trace.
236
+
237
+ ```javascript
238
+ const globalSpan = await tracer.startGlobalSpan('checkout');
239
+
240
+ await globalSpan.withContext(async () => {
241
+ await fetch('https://api.example.com/cart'); // linked to globalSpan's traceId
242
+ });
243
+
244
+ await globalSpan.endSpan();
245
+ ```
246
+
247
+ **`ignoredInstruments` — exclude auto-instrumentation from the trace:**
248
+
249
+ Pass instrument names to prevent network requests, errors, or interactions fired during this tracer's spans from being linked to your custom trace.
250
+
251
+ ```javascript
252
+ const tracer = CoralogixRum.getCustomTracer(['networkRequests', 'userInteractions', 'errors']);
253
+ ```
254
+
255
+ > **Note:** Only one global span may be active at a time. `startGlobalSpan` returns `null` if a global span is already open.
256
+
257
+ ### Time Measurement
258
+
259
+ Measure the duration of arbitrary flows with a pair of `startTimeMeasure(name, labels?)` / `endTimeMeasure(name)` calls. The native SDK records start/end timestamps and reports the delta as a custom-measurement span (milliseconds).
260
+
261
+ ```javascript
262
+ import { CoralogixRum } from '@coralogix/react-native-plugin';
263
+
264
+ CoralogixRum.startTimeMeasure('checkout', { flow: 'checkout' });
265
+
266
+ await validateCart();
267
+ await charge();
268
+ await confirm();
269
+
270
+ CoralogixRum.endTimeMeasure('checkout');
271
+ ```
272
+
273
+ **Behaviour:**
274
+
275
+ - Both calls are fire-and-forget — they cross the bridge directly into native and return immediately.
276
+ - The JS side keeps no state; the native SDK owns the in-flight registry.
277
+ - **You are responsible for pairing every `startTimeMeasure(name, labels?)` with exactly one `endTimeMeasure(name)`.** Leaked starts persist in memory until `CoralogixRum.shutdown()`.
278
+ - Unmatched `endTimeMeasure` calls are dropped silently by the native SDK.
279
+ - Calling `startTimeMeasure` again with an open `name` overwrites the previous start.
280
+
281
+ ### Traces Exporter
282
+
283
+ Receive OTLP-formatted trace batches from the native SDK in your JavaScript code. Use this to forward spans to an OTLP-compatible backend (e.g. Jaeger, custom collector) alongside Coralogix.
284
+
285
+ ```javascript
286
+ await CoralogixRum.init({
287
+ // ...
288
+ tracesExporter: (data) => {
289
+ // data.resource_spans contains OTLP JSON-format span data
290
+ sendToMyOtlpBackend(JSON.stringify(data));
291
+ },
292
+ });
293
+ ```
294
+
295
+ The callback receives a `TraceExporterData` object following the OTLP JSON format:
296
+
297
+ ```typescript
298
+ {
299
+ resource_spans: [
300
+ {
301
+ resource: { attributes: [{ key: string, value: { string_value: string } }] },
302
+ scope_spans: [
303
+ {
304
+ scope: { name: string, version?: string },
305
+ spans: [
306
+ {
307
+ trace_id: string,
308
+ span_id: string,
309
+ parent_span_id?: string,
310
+ name: string,
311
+ start_time_unix_nano: string,
312
+ end_time_unix_nano: string,
313
+ attributes: [{ key: string, value: {...} }],
314
+ status: { code: string },
315
+ }
316
+ ]
317
+ }
318
+ ]
319
+ }
320
+ ]
321
+ }
322
+ ```
323
+
324
+ > **Note:** The callback fires once per native export batch, which typically contains several spans rather than one per span.
325
+
187
326
  ### beforeSend
188
327
 
189
328
  Enable event access and modification before sending to Coralogix, supporting content modification, and event discarding.
@@ -61,6 +61,15 @@ android {
61
61
  "generated/jni"
62
62
  ]
63
63
  }
64
+ test {
65
+ java.srcDirs += ["src/test/kotlin"]
66
+ }
67
+ }
68
+
69
+ testOptions {
70
+ unitTests.all {
71
+ useJUnitPlatform()
72
+ }
64
73
  }
65
74
  }
66
75
 
@@ -75,7 +84,10 @@ dependencies {
75
84
  implementation "com.facebook.react:react-android"
76
85
  implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
77
86
 
78
- implementation "com.coralogix:android-sdk:2.9.5"
87
+ implementation "com.coralogix:android-sdk:2.12.0"
88
+
89
+ testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
90
+ testImplementation "org.mockito:mockito-core:5.12.0"
79
91
  }
80
92
 
81
93
  react {
@@ -5,6 +5,11 @@ import android.os.Handler
5
5
  import android.os.Looper
6
6
  import android.util.Log
7
7
  import com.coralogix.android.sdk.CoralogixRum
8
+ import com.coralogix.android.sdk.customspans.CoralogixCustomSpan
9
+ import com.coralogix.android.sdk.customspans.CoralogixGlobalSpan
10
+ import com.coralogix.android.sdk.customspans.CoralogixIgnoredInstrument
11
+ import com.coralogix.android.sdk.model.ExcludableInstrumentation
12
+ import com.coralogix.android.sdk.traceexporter.CoralogixTraceExporterData
8
13
  import com.coralogix.android.sdk.internal.features.instrumentations.error.CoralogixErrorDecorator
9
14
  import com.coralogix.android.sdk.internal.features.instrumentations.network.NetworkRequestDetails
10
15
  import com.coralogix.android.sdk.internal.infrastructure.threaddump.CoralogixJsStackFrame
@@ -39,10 +44,14 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
39
44
  import org.json.JSONArray
40
45
  import org.json.JSONObject
41
46
  import com.facebook.react.uimanager.UIManagerModule
47
+ import java.util.concurrent.ConcurrentHashMap
42
48
 
43
49
  class CxSdkModule(reactContext: ReactApplicationContext) :
44
50
  ReactContextBaseJavaModule(reactContext), RUMClient, SessionReplayClient {
45
51
 
52
+ private val globalSpans = ConcurrentHashMap<String, CoralogixGlobalSpan>()
53
+ private val customSpans = ConcurrentHashMap<String, CoralogixCustomSpan>()
54
+
46
55
  override fun getName(): String {
47
56
  return NAME
48
57
  }
@@ -134,6 +143,16 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
134
143
  CoralogixRum.sendCustomMeasurement(name, value)
135
144
  }
136
145
 
146
+ @ReactMethod
147
+ override fun startTimeMeasure(name: String, labels: ReadableMap?) {
148
+ CoralogixRum.startTimeMeasure(name, labels?.toStringAnyMap() ?: emptyMap())
149
+ }
150
+
151
+ @ReactMethod
152
+ override fun endTimeMeasure(name: String) {
153
+ CoralogixRum.endTimeMeasure(name)
154
+ }
155
+
137
156
  @ReactMethod
138
157
  override fun reportError(details: ReadableMap) {
139
158
  val errorType = details.getString("error_type").orEmpty()
@@ -290,10 +309,73 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
290
309
  }
291
310
  // endregion
292
311
 
312
+ // region - Custom Spans
313
+
314
+ @ReactMethod
315
+ override fun startGlobalSpan(name: String, labels: ReadableMap?, ignoredInstruments: ReadableArray?, promise: Promise) {
316
+ val ignoredSet = ignoredInstruments?.toIgnoredInstrumentSet() ?: emptySet()
317
+ val tracer = CoralogixRum.getCustomTracer(ignoredSet)
318
+ if (tracer == null) {
319
+ promise.resolve(null)
320
+ return
321
+ }
322
+ val globalSpan = tracer.startGlobalSpan(name, labels?.toStringAnyMap())
323
+ if (globalSpan == null) {
324
+ promise.resolve(null)
325
+ return
326
+ }
327
+ globalSpans[globalSpan.spanId] = globalSpan
328
+ val result = Arguments.createMap()
329
+ result.putString("spanId", globalSpan.spanId)
330
+ result.putString("traceId", globalSpan.traceId)
331
+ promise.resolve(result)
332
+ }
333
+
334
+ @ReactMethod
335
+ override fun startCustomSpan(parentSpanId: String, name: String, labels: ReadableMap?, promise: Promise) {
336
+ val globalSpan = globalSpans[parentSpanId]
337
+ if (globalSpan == null) {
338
+ promise.resolve(null)
339
+ return
340
+ }
341
+ val customSpan = globalSpan.startCustomSpan(name, labels?.toStringAnyMap())
342
+ val otelIds = extractOtelIds(customSpan)
343
+ if (otelIds == null) {
344
+ Log.e(NAME, "startCustomSpan: failed to extract OTel IDs via reflection — resolving null")
345
+ promise.resolve(null)
346
+ return
347
+ }
348
+ customSpans[otelIds.first] = customSpan
349
+ val result = Arguments.createMap()
350
+ result.putString("spanId", otelIds.first)
351
+ result.putString("traceId", otelIds.second)
352
+ promise.resolve(result)
353
+ }
354
+
355
+
356
+ @ReactMethod
357
+ override fun endSpan(spanId: String, promise: Promise) {
358
+ globalSpans.remove(spanId)?.let {
359
+ it.endSpan()
360
+ promise.resolve(null)
361
+ return
362
+ }
363
+ customSpans.remove(spanId)?.let {
364
+ it.endSpan()
365
+ promise.resolve(null)
366
+ return
367
+ }
368
+ Log.w(NAME, "endSpan: unknown spanId $spanId")
369
+ promise.resolve(null)
370
+ }
371
+
372
+ // endregion
373
+
293
374
  // region - utils
294
375
  override fun invalidate() {
295
376
  super.invalidate()
296
-
377
+ globalSpans.clear()
378
+ customSpans.clear()
297
379
  Handler(Looper.getMainLooper()).post {
298
380
  Log.d("CxSdkModule", "Bridge destroyed — shutting down CoralogixRum")
299
381
  CoralogixRum.shutdown()
@@ -354,6 +436,13 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
354
436
  getArray("networkExtraConfig")?.toNetworkCaptureRuleList() ?: emptyList()
355
437
  else emptyList()
356
438
 
439
+ val excludeFromSampling = if (hasKey("excludeFromSampling") && !isNull("excludeFromSampling"))
440
+ getArray("excludeFromSampling")?.toExcludeFromSamplingList() ?: emptyList()
441
+ else emptyList()
442
+
443
+ val sessionSampleRate =
444
+ if (hasKey("sessionSampleRate") && !isNull("sessionSampleRate")) getInt("sessionSampleRate") else 100
445
+
357
446
  return CoralogixOptions(
358
447
  applicationName = applicationName,
359
448
  coralogixDomain = coralogixDomain,
@@ -367,13 +456,23 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
367
456
  mobileVitalsOptions = getMap("mobileVitals")?.toMobileVitalsOptions() ?: mapOf(),
368
457
  ignoreUrls = getArray("ignoreUrls")?.handleStringOrRegexList() ?: listOf(),
369
458
  ignoreErrors = getArray("ignoreErrors")?.handleStringOrRegexList() ?: listOf(),
370
- sessionSampleRate = 100,
459
+ sessionSampleRate = sessionSampleRate,
460
+ excludeFromSampling = excludeFromSampling,
371
461
  traceParentInHeader = traceParentInHeaderConfig,
372
462
  debug = if (hasKey("debug")) getBoolean("debug") else false,
373
463
  proxyUrl = getString("proxyUrl"),
374
464
  beforeSendCallback = if (hasKey("hasBeforeSend") && getBoolean("hasBeforeSend")) ::beforeSendCallback else null,
375
465
  collectIPData = collectIpData,
376
- networkCaptureConfig = networkExtraConfig
466
+ networkCaptureConfig = networkExtraConfig,
467
+ tracesExporter = if (hasKey("hasTracesExporter") && getBoolean("hasTracesExporter")) {
468
+ { data: CoralogixTraceExporterData ->
469
+ if (reactApplicationContext.hasActiveReactInstance()) {
470
+ reactApplicationContext
471
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
472
+ .emit("onTracesExport", data.toJson())
473
+ }
474
+ }
475
+ } else null
377
476
  )
378
477
  }
379
478
 
@@ -523,8 +622,9 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
523
622
  "AP1" -> CoralogixDomain.AP1
524
623
  "AP2" -> CoralogixDomain.AP2
525
624
  "AP3" -> CoralogixDomain.AP3
625
+ "US3" -> CoralogixDomain.US3
526
626
  "STAGING" -> CoralogixDomain.STAGING
527
- else -> throw IllegalArgumentException("Invalid coralogixDomain: $this. Must be one of [EU1, EU2, US1, US2, AP1, AP2, AP3, STAGING]")
627
+ else -> throw IllegalArgumentException("Invalid coralogixDomain: $this. Must be one of [EU1, EU2, US1, US2, US3, AP1, AP2, AP3, STAGING]")
528
628
  }
529
629
  }
530
630
 
@@ -758,6 +858,36 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
758
858
  return result
759
859
  }
760
860
 
861
+ private fun ReadableArray.toIgnoredInstrumentSet(): Set<CoralogixIgnoredInstrument> {
862
+ val set = mutableSetOf<CoralogixIgnoredInstrument>()
863
+ for (i in 0 until size()) {
864
+ when (getString(i)) {
865
+ "networkRequests" -> set.add(CoralogixIgnoredInstrument.NETWORK_REQUESTS)
866
+ "userInteractions" -> set.add(CoralogixIgnoredInstrument.USER_INTERACTIONS)
867
+ "errors" -> set.add(CoralogixIgnoredInstrument.ERRORS)
868
+ }
869
+ }
870
+ return set
871
+ }
872
+
873
+ private fun ReadableArray.toExcludeFromSamplingList(): List<ExcludableInstrumentation> {
874
+ val set = LinkedHashSet<ExcludableInstrumentation>()
875
+ for (i in 0 until size()) {
876
+ val value = getString(i)
877
+ when (value) {
878
+ "errors" -> set.add(ExcludableInstrumentation.Errors)
879
+ "logs" -> set.add(ExcludableInstrumentation.Logs)
880
+ "network" -> set.add(ExcludableInstrumentation.Network)
881
+ "userInteractions" -> set.add(ExcludableInstrumentation.UserInteractions)
882
+ "mobileVitals" -> set.add(ExcludableInstrumentation.MobileVitals)
883
+ "customSpan" -> set.add(ExcludableInstrumentation.CustomSpan)
884
+ "customMeasurement" -> set.add(ExcludableInstrumentation.CustomMeasurement)
885
+ else -> Log.w("CxSdkModule", "excludeFromSampling: unrecognized value '$value' — native enum may have drifted")
886
+ }
887
+ }
888
+ return set.toList()
889
+ }
890
+
761
891
  private fun ReadableArray.toHybridMetricList(): List<HybridMetric> {
762
892
  val out = ArrayList<HybridMetric>(size())
763
893
  for (i in 0 until size()) {
@@ -778,5 +908,22 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
778
908
 
779
909
  companion object {
780
910
  const val NAME = "CxSdk"
911
+
912
+ // TODO: replace reflection with a public accessor once the Coralogix Android SDK exposes one —
913
+ // 'getSpan$library_release' is a Kotlin-internal name that may be renamed in a future SDK version.
914
+ internal fun extractOtelIds(child: CoralogixCustomSpan): Pair<String, String>? =
915
+ extractOtelIds(child, "getSpan\$library_release")
916
+
917
+ internal fun extractOtelIds(child: CoralogixCustomSpan, accessorName: String): Pair<String, String>? = try {
918
+ val span = child.javaClass.getMethod(accessorName).invoke(child) ?: return null
919
+ val ctx = span.javaClass.getMethod("getSpanContext").invoke(span) ?: return null
920
+ val spanId = ctx.javaClass.getMethod("getSpanId").invoke(ctx) as? String
921
+ val traceId = ctx.javaClass.getMethod("getTraceId").invoke(ctx) as? String
922
+ if (spanId.isNullOrEmpty() || traceId.isNullOrEmpty()) null
923
+ else Pair(spanId, traceId)
924
+ } catch (e: Throwable) {
925
+ Log.w(NAME, "extractOtelIds: reflection failed — $e")
926
+ null
927
+ }
781
928
  }
782
929
  }
@@ -16,10 +16,17 @@ interface RUMClient {
16
16
  fun log(severity: Int, message: String, data: ReadableMap, labels: ReadableMap)
17
17
  fun reportNetworkRequest(requestDetails: ReadableMap)
18
18
  fun sendCustomMeasurement(measurement: ReadableMap)
19
+ fun startTimeMeasure(name: String, labels: ReadableMap?)
20
+ fun endTimeMeasure(name: String)
19
21
  fun reportError(details: ReadableMap)
20
22
  fun sendCxSpanData(results: ReadableArray)
21
23
  fun reportMobileVitalsMeasurement(type: String, value: Double, units: String)
22
24
  fun reportMobileVitalsMeasurementSet(type: String, metrics: ReadableArray)
23
25
  fun isCoralogixGradlePluginApplied(promise: Promise)
24
26
  fun shutdown(promise: Promise)
27
+
28
+ // custom spans
29
+ fun startGlobalSpan(name: String, labels: ReadableMap?, ignoredInstruments: ReadableArray?, promise: Promise)
30
+ fun startCustomSpan(parentSpanId: String, name: String, labels: ReadableMap?, promise: Promise)
31
+ fun endSpan(spanId: String, promise: Promise)
25
32
  }
@@ -0,0 +1,49 @@
1
+ package com.cxsdk
2
+
3
+ import com.coralogix.android.sdk.customspans.CoralogixCustomSpan
4
+ import io.opentelemetry.api.trace.Span
5
+ import io.opentelemetry.api.trace.SpanContext
6
+ import io.opentelemetry.api.trace.TraceFlags
7
+ import io.opentelemetry.api.trace.TraceState
8
+ import org.mockito.Mockito.mock
9
+ import org.mockito.Mockito.`when`
10
+ import kotlin.test.Test
11
+ import kotlin.test.assertEquals
12
+ import kotlin.test.assertNull
13
+
14
+ class ExtractOtelIdsTest {
15
+
16
+ @Test
17
+ fun `extractOtelIds returns correct spanId and traceId via reflection`() {
18
+ val spanId = "1234567890abcdef"
19
+ val traceId = "abcdef1234567890abcdef1234567890"
20
+
21
+ val mockSpan = mock(Span::class.java)
22
+ `when`(mockSpan.spanContext).thenReturn(
23
+ SpanContext.create(traceId, spanId, TraceFlags.getDefault(), TraceState.getDefault())
24
+ )
25
+
26
+ val result = CxSdkModule.extractOtelIds(CoralogixCustomSpan(mockSpan))
27
+
28
+ assertEquals(Pair(spanId, traceId), result)
29
+ }
30
+
31
+ @Test
32
+ fun `extractOtelIds returns null when spanContext returns null`() {
33
+ val mockSpan = mock(Span::class.java)
34
+ `when`(mockSpan.spanContext).thenReturn(null)
35
+
36
+ val result = CxSdkModule.extractOtelIds(CoralogixCustomSpan(mockSpan))
37
+
38
+ assertNull(result)
39
+ }
40
+
41
+ @Test
42
+ fun `extractOtelIds returns null when accessor method does not exist`() {
43
+ val mockSpan = mock(Span::class.java)
44
+
45
+ val result = CxSdkModule.extractOtelIds(CoralogixCustomSpan(mockSpan), "nonExistentMethod")
46
+
47
+ assertNull(result)
48
+ }
49
+ }