@coralogix/react-native-plugin 0.3.3 → 0.4.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,13 @@
1
+ ## 0.4.0 (2026-05-03)
2
+
3
+ ### 🚀 Features
4
+
5
+ - Custom Spans & Traces Exporter (CX-36055)
6
+
7
+ ### 🩹 Fixes
8
+
9
+ - import CoralogixIgnoredInstrument in index.ts for rollup build
10
+
1
11
  ## 0.3.3 (2026-04-05)
2
12
 
3
13
  ### 🩹 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.2'
20
+ s.dependency 'CoralogixInternal','2.6.2'
21
+ s.dependency 'SessionReplay','2.6.2'
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
@@ -184,6 +184,102 @@ await CoralogixRum.init({
184
184
  });
185
185
  ```
186
186
 
187
+ ### Custom Spans
188
+
189
+ Create manual RUM spans to instrument custom flows in your application. Requires `traceParentInHeader.enabled: true`.
190
+
191
+ **Prerequisite:**
192
+ ```javascript
193
+ await CoralogixRum.init({
194
+ // ...
195
+ traceParentInHeader: { enabled: true },
196
+ });
197
+ ```
198
+
199
+ **Basic usage — global span with a child:**
200
+ ```javascript
201
+ import { CoralogixRum } from '@coralogix/react-native-plugin';
202
+
203
+ const tracer = CoralogixRum.getCustomTracer();
204
+
205
+ const globalSpan = await tracer.startGlobalSpan('checkout', { step: 'start' });
206
+ if (!globalSpan) return; // another global span is already active
207
+
208
+ const childSpan = await globalSpan.startCustomSpan('validate-cart');
209
+ await childSpan?.endSpan();
210
+
211
+ await globalSpan.endSpan();
212
+ ```
213
+
214
+ **Linking network requests with `withContext`:**
215
+
216
+ When you call `fetch` inside `withContext`, the network request is automatically linked to the active global span's trace.
217
+
218
+ ```javascript
219
+ const globalSpan = await tracer.startGlobalSpan('checkout');
220
+
221
+ await globalSpan.withContext(async () => {
222
+ await fetch('https://api.example.com/cart'); // linked to globalSpan's traceId
223
+ });
224
+
225
+ await globalSpan.endSpan();
226
+ ```
227
+
228
+ **`ignoredInstruments` — exclude auto-instrumentation from the trace:**
229
+
230
+ Pass instrument names to prevent network requests, errors, or interactions fired during this tracer's spans from being linked to your custom trace.
231
+
232
+ ```javascript
233
+ const tracer = CoralogixRum.getCustomTracer(['networkRequests', 'userInteractions', 'errors']);
234
+ ```
235
+
236
+ > **Note:** Only one global span may be active at a time. `startGlobalSpan` returns `null` if a global span is already open.
237
+
238
+ ### Traces Exporter
239
+
240
+ 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.
241
+
242
+ ```javascript
243
+ await CoralogixRum.init({
244
+ // ...
245
+ tracesExporter: (data) => {
246
+ // data.resource_spans contains OTLP JSON-format span data
247
+ sendToMyOtlpBackend(JSON.stringify(data));
248
+ },
249
+ });
250
+ ```
251
+
252
+ The callback receives a `TraceExporterData` object following the OTLP JSON format:
253
+
254
+ ```typescript
255
+ {
256
+ resource_spans: [
257
+ {
258
+ resource: { attributes: [{ key: string, value: { string_value: string } }] },
259
+ scope_spans: [
260
+ {
261
+ scope: { name: string, version?: string },
262
+ spans: [
263
+ {
264
+ trace_id: string,
265
+ span_id: string,
266
+ parent_span_id?: string,
267
+ name: string,
268
+ start_time_unix_nano: string,
269
+ end_time_unix_nano: string,
270
+ attributes: [{ key: string, value: {...} }],
271
+ status: { code: string },
272
+ }
273
+ ]
274
+ }
275
+ ]
276
+ }
277
+ ]
278
+ }
279
+ ```
280
+
281
+ > **Note:** The callback fires once per native export batch, which typically contains several spans rather than one per span.
282
+
187
283
  ### beforeSend
188
284
 
189
285
  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.11.2"
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,10 @@ 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.traceexporter.CoralogixTraceExporterData
8
12
  import com.coralogix.android.sdk.internal.features.instrumentations.error.CoralogixErrorDecorator
9
13
  import com.coralogix.android.sdk.internal.features.instrumentations.network.NetworkRequestDetails
10
14
  import com.coralogix.android.sdk.internal.infrastructure.threaddump.CoralogixJsStackFrame
@@ -39,10 +43,14 @@ import com.facebook.react.modules.core.DeviceEventManagerModule
39
43
  import org.json.JSONArray
40
44
  import org.json.JSONObject
41
45
  import com.facebook.react.uimanager.UIManagerModule
46
+ import java.util.concurrent.ConcurrentHashMap
42
47
 
43
48
  class CxSdkModule(reactContext: ReactApplicationContext) :
44
49
  ReactContextBaseJavaModule(reactContext), RUMClient, SessionReplayClient {
45
50
 
51
+ private val globalSpans = ConcurrentHashMap<String, CoralogixGlobalSpan>()
52
+ private val customSpans = ConcurrentHashMap<String, CoralogixCustomSpan>()
53
+
46
54
  override fun getName(): String {
47
55
  return NAME
48
56
  }
@@ -290,10 +298,73 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
290
298
  }
291
299
  // endregion
292
300
 
301
+ // region - Custom Spans
302
+
303
+ @ReactMethod
304
+ override fun startGlobalSpan(name: String, labels: ReadableMap?, ignoredInstruments: ReadableArray?, promise: Promise) {
305
+ val ignoredSet = ignoredInstruments?.toIgnoredInstrumentSet() ?: emptySet()
306
+ val tracer = CoralogixRum.getCustomTracer(ignoredSet)
307
+ if (tracer == null) {
308
+ promise.resolve(null)
309
+ return
310
+ }
311
+ val globalSpan = tracer.startGlobalSpan(name, labels?.toStringAnyMap())
312
+ if (globalSpan == null) {
313
+ promise.resolve(null)
314
+ return
315
+ }
316
+ globalSpans[globalSpan.spanId] = globalSpan
317
+ val result = Arguments.createMap()
318
+ result.putString("spanId", globalSpan.spanId)
319
+ result.putString("traceId", globalSpan.traceId)
320
+ promise.resolve(result)
321
+ }
322
+
323
+ @ReactMethod
324
+ override fun startCustomSpan(parentSpanId: String, name: String, labels: ReadableMap?, promise: Promise) {
325
+ val globalSpan = globalSpans[parentSpanId]
326
+ if (globalSpan == null) {
327
+ promise.resolve(null)
328
+ return
329
+ }
330
+ val customSpan = globalSpan.startCustomSpan(name, labels?.toStringAnyMap())
331
+ val otelIds = extractOtelIds(customSpan)
332
+ if (otelIds == null) {
333
+ Log.e(NAME, "startCustomSpan: failed to extract OTel IDs via reflection — resolving null")
334
+ promise.resolve(null)
335
+ return
336
+ }
337
+ customSpans[otelIds.first] = customSpan
338
+ val result = Arguments.createMap()
339
+ result.putString("spanId", otelIds.first)
340
+ result.putString("traceId", otelIds.second)
341
+ promise.resolve(result)
342
+ }
343
+
344
+
345
+ @ReactMethod
346
+ override fun endSpan(spanId: String, promise: Promise) {
347
+ globalSpans.remove(spanId)?.let {
348
+ it.endSpan()
349
+ promise.resolve(null)
350
+ return
351
+ }
352
+ customSpans.remove(spanId)?.let {
353
+ it.endSpan()
354
+ promise.resolve(null)
355
+ return
356
+ }
357
+ Log.w(NAME, "endSpan: unknown spanId $spanId")
358
+ promise.resolve(null)
359
+ }
360
+
361
+ // endregion
362
+
293
363
  // region - utils
294
364
  override fun invalidate() {
295
365
  super.invalidate()
296
-
366
+ globalSpans.clear()
367
+ customSpans.clear()
297
368
  Handler(Looper.getMainLooper()).post {
298
369
  Log.d("CxSdkModule", "Bridge destroyed — shutting down CoralogixRum")
299
370
  CoralogixRum.shutdown()
@@ -373,7 +444,16 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
373
444
  proxyUrl = getString("proxyUrl"),
374
445
  beforeSendCallback = if (hasKey("hasBeforeSend") && getBoolean("hasBeforeSend")) ::beforeSendCallback else null,
375
446
  collectIPData = collectIpData,
376
- networkCaptureConfig = networkExtraConfig
447
+ networkCaptureConfig = networkExtraConfig,
448
+ tracesExporter = if (hasKey("hasTracesExporter") && getBoolean("hasTracesExporter")) {
449
+ { data: CoralogixTraceExporterData ->
450
+ if (reactApplicationContext.hasActiveReactInstance()) {
451
+ reactApplicationContext
452
+ .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
453
+ .emit("onTracesExport", data.toJson())
454
+ }
455
+ }
456
+ } else null
377
457
  )
378
458
  }
379
459
 
@@ -523,8 +603,9 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
523
603
  "AP1" -> CoralogixDomain.AP1
524
604
  "AP2" -> CoralogixDomain.AP2
525
605
  "AP3" -> CoralogixDomain.AP3
606
+ "US3" -> CoralogixDomain.US3
526
607
  "STAGING" -> CoralogixDomain.STAGING
527
- else -> throw IllegalArgumentException("Invalid coralogixDomain: $this. Must be one of [EU1, EU2, US1, US2, AP1, AP2, AP3, STAGING]")
608
+ else -> throw IllegalArgumentException("Invalid coralogixDomain: $this. Must be one of [EU1, EU2, US1, US2, US3, AP1, AP2, AP3, STAGING]")
528
609
  }
529
610
  }
530
611
 
@@ -758,6 +839,18 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
758
839
  return result
759
840
  }
760
841
 
842
+ private fun ReadableArray.toIgnoredInstrumentSet(): Set<CoralogixIgnoredInstrument> {
843
+ val set = mutableSetOf<CoralogixIgnoredInstrument>()
844
+ for (i in 0 until size()) {
845
+ when (getString(i)) {
846
+ "networkRequests" -> set.add(CoralogixIgnoredInstrument.NETWORK_REQUESTS)
847
+ "userInteractions" -> set.add(CoralogixIgnoredInstrument.USER_INTERACTIONS)
848
+ "errors" -> set.add(CoralogixIgnoredInstrument.ERRORS)
849
+ }
850
+ }
851
+ return set
852
+ }
853
+
761
854
  private fun ReadableArray.toHybridMetricList(): List<HybridMetric> {
762
855
  val out = ArrayList<HybridMetric>(size())
763
856
  for (i in 0 until size()) {
@@ -778,5 +871,22 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
778
871
 
779
872
  companion object {
780
873
  const val NAME = "CxSdk"
874
+
875
+ // TODO: replace reflection with a public accessor once the Coralogix Android SDK exposes one —
876
+ // 'getSpan$library_release' is a Kotlin-internal name that may be renamed in a future SDK version.
877
+ internal fun extractOtelIds(child: CoralogixCustomSpan): Pair<String, String>? =
878
+ extractOtelIds(child, "getSpan\$library_release")
879
+
880
+ internal fun extractOtelIds(child: CoralogixCustomSpan, accessorName: String): Pair<String, String>? = try {
881
+ val span = child.javaClass.getMethod(accessorName).invoke(child) ?: return null
882
+ val ctx = span.javaClass.getMethod("getSpanContext").invoke(span) ?: return null
883
+ val spanId = ctx.javaClass.getMethod("getSpanId").invoke(ctx) as? String
884
+ val traceId = ctx.javaClass.getMethod("getTraceId").invoke(ctx) as? String
885
+ if (spanId.isNullOrEmpty() || traceId.isNullOrEmpty()) null
886
+ else Pair(spanId, traceId)
887
+ } catch (e: Throwable) {
888
+ Log.w(NAME, "extractOtelIds: reflection failed — $e")
889
+ null
890
+ }
781
891
  }
782
892
  }
@@ -22,4 +22,9 @@ interface RUMClient {
22
22
  fun reportMobileVitalsMeasurementSet(type: String, metrics: ReadableArray)
23
23
  fun isCoralogixGradlePluginApplied(promise: Promise)
24
24
  fun shutdown(promise: Promise)
25
+
26
+ // custom spans
27
+ fun startGlobalSpan(name: String, labels: ReadableMap?, ignoredInstruments: ReadableArray?, promise: Promise)
28
+ fun startCustomSpan(parentSpanId: String, name: String, labels: ReadableMap?, promise: Promise)
29
+ fun endSpan(spanId: String, promise: Promise)
25
30
  }
@@ -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
+ }
package/index.cjs.js CHANGED
@@ -27,6 +27,14 @@ let CoralogixLogSeverity = /*#__PURE__*/function (CoralogixLogSeverity) {
27
27
  return CoralogixLogSeverity;
28
28
  }({});
29
29
 
30
+ /**
31
+ * Instrumentation names that can be excluded from a custom tracer's span context.
32
+ * Pass one or more values to `getCustomTracer()` to prevent those event types from
33
+ * being linked to spans created by that tracer.
34
+ */
35
+
36
+ /** OTLP JSON-format trace data delivered to the `tracesExporter` callback. */
37
+
30
38
  const ERROR_INSTRUMENTATION_NAME = 'errors';
31
39
  const ERROR_INSTRUMENTATION_VERSION = '1';
32
40
 
@@ -523,7 +531,7 @@ function stopJsRefreshRateSampler() {
523
531
  appStateSub = null;
524
532
  }
525
533
 
526
- var version = "0.3.3";
534
+ var version = "0.4.0";
527
535
  var pkg = {
528
536
  version: version};
529
537
 
@@ -561,6 +569,128 @@ class Logger {
561
569
  }
562
570
  const logger = new Logger();
563
571
 
572
+ let activeGlobalSpanId = null;
573
+ const spanTraceIds = new Map();
574
+ let ignoredInstruments = new Set();
575
+ const CustomSpanRegistry = {
576
+ getActive: () => activeGlobalSpanId,
577
+ setActive: id => {
578
+ activeGlobalSpanId = id;
579
+ },
580
+ clearActive: () => {
581
+ activeGlobalSpanId = null;
582
+ },
583
+ registerSpan: (spanId, traceId) => {
584
+ spanTraceIds.set(spanId, traceId);
585
+ },
586
+ unregisterSpan: spanId => {
587
+ spanTraceIds.delete(spanId);
588
+ },
589
+ getTraceId: spanId => spanTraceIds.get(spanId),
590
+ setIgnoredInstruments: set => {
591
+ ignoredInstruments = set;
592
+ },
593
+ getIgnoredInstruments: () => ignoredInstruments,
594
+ clear: () => {
595
+ activeGlobalSpanId = null;
596
+ spanTraceIds.clear();
597
+ ignoredInstruments.clear();
598
+ }
599
+ };
600
+
601
+ /** A child span nested under a {@link CoralogixGlobalSpan}. */
602
+ class CoralogixCustomSpan {
603
+ constructor(spanId, traceId, bridge) {
604
+ this.spanId = void 0;
605
+ this.traceId = void 0;
606
+ this.bridge = void 0;
607
+ this.spanId = spanId;
608
+ this.traceId = traceId;
609
+ this.bridge = bridge;
610
+ }
611
+ async endSpan() {
612
+ CustomSpanRegistry.unregisterSpan(this.spanId);
613
+ await this.bridge.endSpan(this.spanId);
614
+ }
615
+ }
616
+
617
+ /** A root-level custom span. All child spans and `withContext` network calls share its traceId. */
618
+ class CoralogixGlobalSpan {
619
+ constructor(spanId, traceId, bridge) {
620
+ this.spanId = void 0;
621
+ this.traceId = void 0;
622
+ this.bridge = void 0;
623
+ this.spanId = spanId;
624
+ this.traceId = traceId;
625
+ this.bridge = bridge;
626
+ }
627
+
628
+ /** Creates a child span nested under this global span. */
629
+ async startCustomSpan(name, labels) {
630
+ const raw = await this.bridge.startCustomSpan(this.spanId, name, labels != null ? labels : null);
631
+ if (!raw) return null;
632
+ CustomSpanRegistry.registerSpan(raw.spanId, raw.traceId);
633
+ return new CoralogixCustomSpan(raw.spanId, raw.traceId, this.bridge);
634
+ }
635
+
636
+ /**
637
+ * Runs fn() with this span set as the active global span for the duration of
638
+ * the returned promise. The active span ID remains set throughout the full
639
+ * async chain — including code after intermediate awaits — because we await
640
+ * fn() before clearing. Only detached callbacks (e.g. setTimeout) that fire
641
+ * after withContext resolves won't be linked.
642
+ */
643
+ async withContext(fn) {
644
+ const prev = CustomSpanRegistry.getActive();
645
+ CustomSpanRegistry.setActive(this.spanId);
646
+ try {
647
+ return await fn();
648
+ } finally {
649
+ if (prev !== null) {
650
+ CustomSpanRegistry.setActive(prev);
651
+ } else {
652
+ CustomSpanRegistry.clearActive();
653
+ }
654
+ }
655
+ }
656
+ async endSpan() {
657
+ CustomSpanRegistry.unregisterSpan(this.spanId);
658
+ if (CustomSpanRegistry.getActive() === this.spanId) {
659
+ CustomSpanRegistry.clearActive();
660
+ }
661
+ await this.bridge.endSpan(this.spanId);
662
+ }
663
+ }
664
+
665
+ /**
666
+ * Entry point for manual RUM tracing. Obtain an instance via `CoralogixRum.getCustomTracer()`.
667
+ * The `ignoredInstruments` list controls which auto-instrumented events are NOT linked to spans
668
+ * created by this tracer (e.g. passing `['networkRequests']` keeps network spans independent).
669
+ */
670
+ class CoralogixCustomTracer {
671
+ constructor(ignoredInstruments, bridge) {
672
+ this.ignoredInstruments = void 0;
673
+ this.bridge = void 0;
674
+ this.ignoredInstruments = ignoredInstruments;
675
+ this.bridge = bridge;
676
+ }
677
+
678
+ /**
679
+ * Starts a new global (root) span. Returns `null` if a global span is already active —
680
+ * only one global span may exist at a time across the entire app.
681
+ */
682
+ async startGlobalSpan(name, labels) {
683
+ const raw = await this.bridge.startGlobalSpan(name, labels != null ? labels : null, this.ignoredInstruments);
684
+ if (!raw) {
685
+ logger.debug('CoralogixCustomTracer: startGlobalSpan returned null — check that the SDK is initialized, traceParentInHeader is enabled, and no other global span is already active');
686
+ return null;
687
+ }
688
+ CustomSpanRegistry.registerSpan(raw.spanId, raw.traceId);
689
+ CustomSpanRegistry.setIgnoredInstruments(new Set(this.ignoredInstruments));
690
+ return new CoralogixGlobalSpan(raw.spanId, raw.traceId, this.bridge);
691
+ }
692
+ }
693
+
564
694
  const CORALOGIX_LOGS_URL_SUFFIX = '/browser/v1beta/logs';
565
695
  const CORALOGIX_RECORDING_URL_SUFFIX = '/sessionrecording';
566
696
  const OPTIONS_DEFAULTS = {
@@ -838,6 +968,7 @@ let CoralogixDomain = /*#__PURE__*/function (CoralogixDomain) {
838
968
  CoralogixDomain["AP1"] = "AP1";
839
969
  CoralogixDomain["AP2"] = "AP2";
840
970
  CoralogixDomain["AP3"] = "AP3";
971
+ CoralogixDomain["US3"] = "US3";
841
972
  CoralogixDomain["STAGING"] = "staging";
842
973
  return CoralogixDomain;
843
974
  }({});
@@ -847,6 +978,7 @@ const LINKING_ERROR = `The package 'cx-plugin' doesn't seem to be linked. Make s
847
978
  default: ''
848
979
  }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n';
849
980
  let beforeSendCallback;
981
+ let tracesExporterSubscription;
850
982
  let _deregisterInstrumentations;
851
983
  const CxSdk = reactNative.NativeModules.CxSdk ? reactNative.NativeModules.CxSdk : createErrorProxy(LINKING_ERROR);
852
984
  function createErrorProxy(errorMessage) {
@@ -886,10 +1018,28 @@ const CoralogixRum = {
886
1018
  } else {
887
1019
  finalOptions = resolvedOptions;
888
1020
  }
1021
+ if (finalOptions.tracesExporter) {
1022
+ const callback = finalOptions.tracesExporter;
1023
+ tracesExporterSubscription = eventEmitter.addListener('onTracesExport', jsonString => {
1024
+ let data;
1025
+ try {
1026
+ data = JSON.parse(jsonString);
1027
+ } catch (e) {
1028
+ logger.warn('Error parsing onTracesExport payload:', e);
1029
+ return;
1030
+ }
1031
+ try {
1032
+ callback(data);
1033
+ } catch (e) {
1034
+ logger.warn('tracesExporter callback threw:', e);
1035
+ }
1036
+ });
1037
+ }
889
1038
  await CxSdk.initialize(_extends({}, finalOptions, {
890
1039
  frameworkVersion: pkg.version,
891
1040
  networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined,
892
- hasBeforeSend: !!finalOptions.beforeSend
1041
+ hasBeforeSend: !!finalOptions.beforeSend,
1042
+ hasTracesExporter: !!finalOptions.tracesExporter
893
1043
  }));
894
1044
  isInited = true;
895
1045
  },
@@ -901,6 +1051,10 @@ const CoralogixRum = {
901
1051
  if (subscription) {
902
1052
  subscription.remove();
903
1053
  }
1054
+ if (tracesExporterSubscription) {
1055
+ tracesExporterSubscription.remove();
1056
+ tracesExporterSubscription = undefined;
1057
+ }
904
1058
  isInited = false;
905
1059
  _deregisterInstrumentations == null || _deregisterInstrumentations();
906
1060
  _deregisterInstrumentations = undefined;
@@ -992,6 +1146,19 @@ const CoralogixRum = {
992
1146
  reportNetworkRequest: details => {
993
1147
  CxSdk.reportNetworkRequest(details);
994
1148
  },
1149
+ /**
1150
+ * Returns a tracer for creating manual custom spans.
1151
+ * Pass `ignoredInstruments` to prevent specific auto-instrumented event types from being
1152
+ * linked to spans created by this tracer.
1153
+ */
1154
+ getCustomTracer(ignoredInstruments = []) {
1155
+ const bridge = {
1156
+ startGlobalSpan: (name, labels, instruments) => CxSdk.startGlobalSpan(name, labels, instruments),
1157
+ startCustomSpan: (parentSpanId, name, labels) => CxSdk.startCustomSpan(parentSpanId, name, labels),
1158
+ endSpan: spanId => CxSdk.endSpan(spanId)
1159
+ };
1160
+ return new CoralogixCustomTracer(ignoredInstruments, bridge);
1161
+ },
995
1162
  sendCustomMeasurement: measurement => {
996
1163
  if (!isInited) {
997
1164
  logger.debug('CoralogixRum must be initiated before sending custom measurements');
@@ -1221,7 +1388,10 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1221
1388
  }
1222
1389
  });
1223
1390
 
1391
+ exports.CoralogixCustomSpan = CoralogixCustomSpan;
1392
+ exports.CoralogixCustomTracer = CoralogixCustomTracer;
1224
1393
  exports.CoralogixDomain = CoralogixDomain;
1394
+ exports.CoralogixGlobalSpan = CoralogixGlobalSpan;
1225
1395
  exports.CoralogixLogSeverity = CoralogixLogSeverity;
1226
1396
  exports.CoralogixRum = CoralogixRum;
1227
1397
  exports.CxSdk = CxSdk;
package/index.esm.js CHANGED
@@ -25,6 +25,14 @@ let CoralogixLogSeverity = /*#__PURE__*/function (CoralogixLogSeverity) {
25
25
  return CoralogixLogSeverity;
26
26
  }({});
27
27
 
28
+ /**
29
+ * Instrumentation names that can be excluded from a custom tracer's span context.
30
+ * Pass one or more values to `getCustomTracer()` to prevent those event types from
31
+ * being linked to spans created by that tracer.
32
+ */
33
+
34
+ /** OTLP JSON-format trace data delivered to the `tracesExporter` callback. */
35
+
28
36
  const ERROR_INSTRUMENTATION_NAME = 'errors';
29
37
  const ERROR_INSTRUMENTATION_VERSION = '1';
30
38
 
@@ -521,7 +529,7 @@ function stopJsRefreshRateSampler() {
521
529
  appStateSub = null;
522
530
  }
523
531
 
524
- var version = "0.3.3";
532
+ var version = "0.4.0";
525
533
  var pkg = {
526
534
  version: version};
527
535
 
@@ -559,6 +567,128 @@ class Logger {
559
567
  }
560
568
  const logger = new Logger();
561
569
 
570
+ let activeGlobalSpanId = null;
571
+ const spanTraceIds = new Map();
572
+ let ignoredInstruments = new Set();
573
+ const CustomSpanRegistry = {
574
+ getActive: () => activeGlobalSpanId,
575
+ setActive: id => {
576
+ activeGlobalSpanId = id;
577
+ },
578
+ clearActive: () => {
579
+ activeGlobalSpanId = null;
580
+ },
581
+ registerSpan: (spanId, traceId) => {
582
+ spanTraceIds.set(spanId, traceId);
583
+ },
584
+ unregisterSpan: spanId => {
585
+ spanTraceIds.delete(spanId);
586
+ },
587
+ getTraceId: spanId => spanTraceIds.get(spanId),
588
+ setIgnoredInstruments: set => {
589
+ ignoredInstruments = set;
590
+ },
591
+ getIgnoredInstruments: () => ignoredInstruments,
592
+ clear: () => {
593
+ activeGlobalSpanId = null;
594
+ spanTraceIds.clear();
595
+ ignoredInstruments.clear();
596
+ }
597
+ };
598
+
599
+ /** A child span nested under a {@link CoralogixGlobalSpan}. */
600
+ class CoralogixCustomSpan {
601
+ constructor(spanId, traceId, bridge) {
602
+ this.spanId = void 0;
603
+ this.traceId = void 0;
604
+ this.bridge = void 0;
605
+ this.spanId = spanId;
606
+ this.traceId = traceId;
607
+ this.bridge = bridge;
608
+ }
609
+ async endSpan() {
610
+ CustomSpanRegistry.unregisterSpan(this.spanId);
611
+ await this.bridge.endSpan(this.spanId);
612
+ }
613
+ }
614
+
615
+ /** A root-level custom span. All child spans and `withContext` network calls share its traceId. */
616
+ class CoralogixGlobalSpan {
617
+ constructor(spanId, traceId, bridge) {
618
+ this.spanId = void 0;
619
+ this.traceId = void 0;
620
+ this.bridge = void 0;
621
+ this.spanId = spanId;
622
+ this.traceId = traceId;
623
+ this.bridge = bridge;
624
+ }
625
+
626
+ /** Creates a child span nested under this global span. */
627
+ async startCustomSpan(name, labels) {
628
+ const raw = await this.bridge.startCustomSpan(this.spanId, name, labels != null ? labels : null);
629
+ if (!raw) return null;
630
+ CustomSpanRegistry.registerSpan(raw.spanId, raw.traceId);
631
+ return new CoralogixCustomSpan(raw.spanId, raw.traceId, this.bridge);
632
+ }
633
+
634
+ /**
635
+ * Runs fn() with this span set as the active global span for the duration of
636
+ * the returned promise. The active span ID remains set throughout the full
637
+ * async chain — including code after intermediate awaits — because we await
638
+ * fn() before clearing. Only detached callbacks (e.g. setTimeout) that fire
639
+ * after withContext resolves won't be linked.
640
+ */
641
+ async withContext(fn) {
642
+ const prev = CustomSpanRegistry.getActive();
643
+ CustomSpanRegistry.setActive(this.spanId);
644
+ try {
645
+ return await fn();
646
+ } finally {
647
+ if (prev !== null) {
648
+ CustomSpanRegistry.setActive(prev);
649
+ } else {
650
+ CustomSpanRegistry.clearActive();
651
+ }
652
+ }
653
+ }
654
+ async endSpan() {
655
+ CustomSpanRegistry.unregisterSpan(this.spanId);
656
+ if (CustomSpanRegistry.getActive() === this.spanId) {
657
+ CustomSpanRegistry.clearActive();
658
+ }
659
+ await this.bridge.endSpan(this.spanId);
660
+ }
661
+ }
662
+
663
+ /**
664
+ * Entry point for manual RUM tracing. Obtain an instance via `CoralogixRum.getCustomTracer()`.
665
+ * The `ignoredInstruments` list controls which auto-instrumented events are NOT linked to spans
666
+ * created by this tracer (e.g. passing `['networkRequests']` keeps network spans independent).
667
+ */
668
+ class CoralogixCustomTracer {
669
+ constructor(ignoredInstruments, bridge) {
670
+ this.ignoredInstruments = void 0;
671
+ this.bridge = void 0;
672
+ this.ignoredInstruments = ignoredInstruments;
673
+ this.bridge = bridge;
674
+ }
675
+
676
+ /**
677
+ * Starts a new global (root) span. Returns `null` if a global span is already active —
678
+ * only one global span may exist at a time across the entire app.
679
+ */
680
+ async startGlobalSpan(name, labels) {
681
+ const raw = await this.bridge.startGlobalSpan(name, labels != null ? labels : null, this.ignoredInstruments);
682
+ if (!raw) {
683
+ logger.debug('CoralogixCustomTracer: startGlobalSpan returned null — check that the SDK is initialized, traceParentInHeader is enabled, and no other global span is already active');
684
+ return null;
685
+ }
686
+ CustomSpanRegistry.registerSpan(raw.spanId, raw.traceId);
687
+ CustomSpanRegistry.setIgnoredInstruments(new Set(this.ignoredInstruments));
688
+ return new CoralogixGlobalSpan(raw.spanId, raw.traceId, this.bridge);
689
+ }
690
+ }
691
+
562
692
  const CORALOGIX_LOGS_URL_SUFFIX = '/browser/v1beta/logs';
563
693
  const CORALOGIX_RECORDING_URL_SUFFIX = '/sessionrecording';
564
694
  const OPTIONS_DEFAULTS = {
@@ -836,6 +966,7 @@ let CoralogixDomain = /*#__PURE__*/function (CoralogixDomain) {
836
966
  CoralogixDomain["AP1"] = "AP1";
837
967
  CoralogixDomain["AP2"] = "AP2";
838
968
  CoralogixDomain["AP3"] = "AP3";
969
+ CoralogixDomain["US3"] = "US3";
839
970
  CoralogixDomain["STAGING"] = "staging";
840
971
  return CoralogixDomain;
841
972
  }({});
@@ -845,6 +976,7 @@ const LINKING_ERROR = `The package 'cx-plugin' doesn't seem to be linked. Make s
845
976
  default: ''
846
977
  }) + '- You rebuilt the app after installing the package\n' + '- You are not using Expo Go\n';
847
978
  let beforeSendCallback;
979
+ let tracesExporterSubscription;
848
980
  let _deregisterInstrumentations;
849
981
  const CxSdk = NativeModules.CxSdk ? NativeModules.CxSdk : createErrorProxy(LINKING_ERROR);
850
982
  function createErrorProxy(errorMessage) {
@@ -884,10 +1016,28 @@ const CoralogixRum = {
884
1016
  } else {
885
1017
  finalOptions = resolvedOptions;
886
1018
  }
1019
+ if (finalOptions.tracesExporter) {
1020
+ const callback = finalOptions.tracesExporter;
1021
+ tracesExporterSubscription = eventEmitter.addListener('onTracesExport', jsonString => {
1022
+ let data;
1023
+ try {
1024
+ data = JSON.parse(jsonString);
1025
+ } catch (e) {
1026
+ logger.warn('Error parsing onTracesExport payload:', e);
1027
+ return;
1028
+ }
1029
+ try {
1030
+ callback(data);
1031
+ } catch (e) {
1032
+ logger.warn('tracesExporter callback threw:', e);
1033
+ }
1034
+ });
1035
+ }
887
1036
  await CxSdk.initialize(_extends({}, finalOptions, {
888
1037
  frameworkVersion: pkg.version,
889
1038
  networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined,
890
- hasBeforeSend: !!finalOptions.beforeSend
1039
+ hasBeforeSend: !!finalOptions.beforeSend,
1040
+ hasTracesExporter: !!finalOptions.tracesExporter
891
1041
  }));
892
1042
  isInited = true;
893
1043
  },
@@ -899,6 +1049,10 @@ const CoralogixRum = {
899
1049
  if (subscription) {
900
1050
  subscription.remove();
901
1051
  }
1052
+ if (tracesExporterSubscription) {
1053
+ tracesExporterSubscription.remove();
1054
+ tracesExporterSubscription = undefined;
1055
+ }
902
1056
  isInited = false;
903
1057
  _deregisterInstrumentations == null || _deregisterInstrumentations();
904
1058
  _deregisterInstrumentations = undefined;
@@ -990,6 +1144,19 @@ const CoralogixRum = {
990
1144
  reportNetworkRequest: details => {
991
1145
  CxSdk.reportNetworkRequest(details);
992
1146
  },
1147
+ /**
1148
+ * Returns a tracer for creating manual custom spans.
1149
+ * Pass `ignoredInstruments` to prevent specific auto-instrumented event types from being
1150
+ * linked to spans created by this tracer.
1151
+ */
1152
+ getCustomTracer(ignoredInstruments = []) {
1153
+ const bridge = {
1154
+ startGlobalSpan: (name, labels, instruments) => CxSdk.startGlobalSpan(name, labels, instruments),
1155
+ startCustomSpan: (parentSpanId, name, labels) => CxSdk.startCustomSpan(parentSpanId, name, labels),
1156
+ endSpan: spanId => CxSdk.endSpan(spanId)
1157
+ };
1158
+ return new CoralogixCustomTracer(ignoredInstruments, bridge);
1159
+ },
993
1160
  sendCustomMeasurement: measurement => {
994
1161
  if (!isInited) {
995
1162
  logger.debug('CoralogixRum must be initiated before sending custom measurements');
@@ -1219,4 +1386,4 @@ const subscription = eventEmitter.addListener('onBeforeSend', events => {
1219
1386
  }
1220
1387
  });
1221
1388
 
1222
- export { CoralogixDomain, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack };
1389
+ export { CoralogixCustomSpan, CoralogixCustomTracer, CoralogixDomain, CoralogixGlobalSpan, CoralogixLogSeverity, CoralogixRum, CxSdk, SessionReplay, attachReactNavigationObserver, parseErrorStack };
package/ios/CxSdk.mm CHANGED
@@ -106,6 +106,22 @@ RCT_EXTERN_METHOD(maskViewByTag:(nonnull NSNumber *)viewTag
106
106
  withResolver:(RCTPromiseResolveBlock)resolve
107
107
  withRejecter:(RCTPromiseRejectBlock)reject)
108
108
 
109
+ RCT_EXTERN_METHOD(startGlobalSpan:(NSString *)name
110
+ labels:(NSDictionary *)labels
111
+ ignoredInstruments:(NSArray *)ignoredInstruments
112
+ withResolver:(RCTPromiseResolveBlock)resolve
113
+ withRejecter:(RCTPromiseRejectBlock)reject)
114
+
115
+ RCT_EXTERN_METHOD(startCustomSpan:(NSString *)parentSpanId
116
+ name:(NSString *)name
117
+ labels:(NSDictionary *)labels
118
+ withResolver:(RCTPromiseResolveBlock)resolve
119
+ withRejecter:(RCTPromiseRejectBlock)reject)
120
+
121
+ RCT_EXTERN_METHOD(endSpan:(NSString *)spanId
122
+ withResolver:(RCTPromiseResolveBlock)resolve
123
+ withRejecter:(RCTPromiseRejectBlock)reject)
124
+
109
125
  + (BOOL)requiresMainQueueSetup
110
126
  {
111
127
  return NO;
package/ios/CxSdk.swift CHANGED
@@ -1,5 +1,6 @@
1
1
  import Foundation
2
2
  import Coralogix
3
+ import CoralogixInternal
3
4
  import SessionReplay
4
5
  import React
5
6
 
@@ -11,6 +12,9 @@ enum CxSdkError: Error {
11
12
  @objc(CxSdk)
12
13
  class CxSdk: RCTEventEmitter {
13
14
  var coralogixRum: CoralogixRum?
15
+ // Accessed only from the RN methodQueue (serial); no explicit locking required.
16
+ private var globalSpans: [String: CoralogixGlobalSpan] = [:]
17
+ private var customSpans: [String: CoralogixCustomSpan] = [:]
14
18
 
15
19
  override init() {
16
20
  super.init()
@@ -21,7 +25,7 @@ class CxSdk: RCTEventEmitter {
21
25
  }
22
26
 
23
27
  override func supportedEvents() -> [String]! {
24
- return ["onBeforeSend"]
28
+ return ["onBeforeSend", "onTracesExport"]
25
29
  }
26
30
 
27
31
  @objc(initialize:withResolver:withRejecter:)
@@ -39,6 +43,15 @@ class CxSdk: RCTEventEmitter {
39
43
 
40
44
  var options = try self.toCoralogixOptions(parameter: parameter)
41
45
  options.beforeSendCallBack = beforeSendCallBack
46
+
47
+ let hasTracesExporter = parameter["hasTracesExporter"] as? Bool ?? false
48
+ if hasTracesExporter {
49
+ options.tracesExporter = { [weak self] data in
50
+ guard let jsonString = data.jsonString else { return }
51
+ self?.sendEvent(withName: "onTracesExport", body: jsonString)
52
+ }
53
+ }
54
+
42
55
  let rnPluginVersion = parameter["frameworkVersion"] as? String ?? ""
43
56
 
44
57
  self.coralogixRum = CoralogixRum(options: options, sdkFramework: .reactNative(version: rnPluginVersion))
@@ -195,6 +208,8 @@ class CxSdk: RCTEventEmitter {
195
208
  @objc(shutdown:withRejecter:)
196
209
  func shutdown(resolve:RCTPromiseResolveBlock,
197
210
  reject:RCTPromiseRejectBlock) -> Void {
211
+ globalSpans.removeAll()
212
+ customSpans.removeAll()
198
213
  coralogixRum?.shutdown()
199
214
  resolve("shutdown success")
200
215
  }
@@ -600,6 +615,78 @@ class CxSdk: RCTEventEmitter {
600
615
  }
601
616
  }
602
617
 
618
+ // MARK: - Custom Spans
619
+
620
+ @objc(startGlobalSpan:labels:ignoredInstruments:withResolver:withRejecter:)
621
+ func startGlobalSpan(name: String,
622
+ labels: NSDictionary?,
623
+ ignoredInstruments: NSArray?,
624
+ resolve: RCTPromiseResolveBlock,
625
+ reject: RCTPromiseRejectBlock) {
626
+ guard let rum = coralogixRum else { resolve(nil); return }
627
+ let ignored = toIgnoredInstrumentSet(ignoredInstruments)
628
+ guard let tracer = rum.getCustomTracer(ignoredInstruments: ignored) else { resolve(nil); return }
629
+ let labelsDict = toAnyDict(labels)
630
+ guard let globalSpan = tracer.startGlobalSpan(name: name, labels: labelsDict) else { resolve(nil); return }
631
+ let spanId = globalSpan.spanId
632
+ let traceId = globalSpan.traceId
633
+ globalSpans[spanId] = globalSpan
634
+ resolve(["spanId": spanId, "traceId": traceId])
635
+ }
636
+
637
+ @objc(startCustomSpan:name:labels:withResolver:withRejecter:)
638
+ func startCustomSpan(parentSpanId: String,
639
+ name: String,
640
+ labels: NSDictionary?,
641
+ resolve: RCTPromiseResolveBlock,
642
+ reject: RCTPromiseRejectBlock) {
643
+ guard let globalSpan = globalSpans[parentSpanId] else { resolve(nil); return }
644
+ let labelsDict = toAnyDict(labels)
645
+ let customSpan = globalSpan.startCustomSpan(name: name, labels: labelsDict)
646
+ let spanId = customSpan.span.context.spanId.hexString
647
+ let traceId = globalSpan.traceId
648
+ guard !spanId.isEmpty else { resolve(nil); return }
649
+ customSpans[spanId] = customSpan
650
+ resolve(["spanId": spanId, "traceId": traceId])
651
+ }
652
+
653
+ @objc(endSpan:withResolver:withRejecter:)
654
+ func endSpan(spanId: String,
655
+ resolve: RCTPromiseResolveBlock,
656
+ reject: RCTPromiseRejectBlock) {
657
+ if let globalSpan = globalSpans.removeValue(forKey: spanId) {
658
+ globalSpan.endSpan()
659
+ } else if let customSpan = customSpans.removeValue(forKey: spanId) {
660
+ customSpan.endSpan()
661
+ }
662
+ resolve(nil)
663
+ }
664
+
665
+ private func toIgnoredInstrumentSet(_ array: NSArray?) -> Set<CoralogixIgnoredInstrument> {
666
+ guard let array = array else { return [] }
667
+ var set = Set<CoralogixIgnoredInstrument>()
668
+ for item in array {
669
+ switch item as? String {
670
+ case "networkRequests": set.insert(.networkRequests)
671
+ case "userInteractions": set.insert(.userInteractions)
672
+ case "errors": set.insert(.errors)
673
+ default: break
674
+ }
675
+ }
676
+ return set
677
+ }
678
+
679
+ private func toAnyDict(_ dict: NSDictionary?) -> [String: Any]? {
680
+ guard let dict = dict, dict.count > 0 else { return nil }
681
+ var result = [String: Any]()
682
+ for (key, value) in dict {
683
+ if let k = key as? String {
684
+ result[k] = value
685
+ }
686
+ }
687
+ return result.isEmpty ? nil : result
688
+ }
689
+
603
690
  private func getCoralogixDomain(domain: String) -> CoralogixDomain {
604
691
  switch domain.uppercased() {
605
692
  case "EU1":
@@ -616,6 +703,8 @@ class CxSdk: RCTEventEmitter {
616
703
  return .AP2
617
704
  case "AP3":
618
705
  return .AP3
706
+ case "US3":
707
+ return .US3
619
708
  case "STAGING":
620
709
  return .STG
621
710
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.3.3",
3
+ "version": "0.4.0",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",
@@ -0,0 +1,9 @@
1
+ import type { CustomSpanBridge } from './CustomSpanBridge';
2
+ /** A child span nested under a {@link CoralogixGlobalSpan}. */
3
+ export declare class CoralogixCustomSpan {
4
+ readonly spanId: string;
5
+ readonly traceId: string;
6
+ private readonly bridge;
7
+ constructor(spanId: string, traceId: string, bridge: CustomSpanBridge);
8
+ endSpan(): Promise<void>;
9
+ }
@@ -0,0 +1,18 @@
1
+ import type { CoralogixIgnoredInstrument } from '../model/Types';
2
+ import { CoralogixGlobalSpan } from './CoralogixGlobalSpan';
3
+ import type { CustomSpanBridge } from './CustomSpanBridge';
4
+ /**
5
+ * Entry point for manual RUM tracing. Obtain an instance via `CoralogixRum.getCustomTracer()`.
6
+ * The `ignoredInstruments` list controls which auto-instrumented events are NOT linked to spans
7
+ * created by this tracer (e.g. passing `['networkRequests']` keeps network spans independent).
8
+ */
9
+ export declare class CoralogixCustomTracer {
10
+ private readonly ignoredInstruments;
11
+ private readonly bridge;
12
+ constructor(ignoredInstruments: CoralogixIgnoredInstrument[], bridge: CustomSpanBridge);
13
+ /**
14
+ * Starts a new global (root) span. Returns `null` if a global span is already active —
15
+ * only one global span may exist at a time across the entire app.
16
+ */
17
+ startGlobalSpan(name: string, labels?: Record<string, any>): Promise<CoralogixGlobalSpan | null>;
18
+ }
@@ -0,0 +1,20 @@
1
+ import { CoralogixCustomSpan } from './CoralogixCustomSpan';
2
+ import type { CustomSpanBridge } from './CustomSpanBridge';
3
+ /** A root-level custom span. All child spans and `withContext` network calls share its traceId. */
4
+ export declare class CoralogixGlobalSpan {
5
+ readonly spanId: string;
6
+ readonly traceId: string;
7
+ private readonly bridge;
8
+ constructor(spanId: string, traceId: string, bridge: CustomSpanBridge);
9
+ /** Creates a child span nested under this global span. */
10
+ startCustomSpan(name: string, labels?: Record<string, any>): Promise<CoralogixCustomSpan | null>;
11
+ /**
12
+ * Runs fn() with this span set as the active global span for the duration of
13
+ * the returned promise. The active span ID remains set throughout the full
14
+ * async chain — including code after intermediate awaits — because we await
15
+ * fn() before clearing. Only detached callbacks (e.g. setTimeout) that fire
16
+ * after withContext resolves won't be linked.
17
+ */
18
+ withContext<T>(fn: () => Promise<T>): Promise<T>;
19
+ endSpan(): Promise<void>;
20
+ }
@@ -0,0 +1,11 @@
1
+ export interface CustomSpanBridge {
2
+ startGlobalSpan(name: string, labels: Record<string, any> | null, ignoredInstruments: string[]): Promise<{
3
+ spanId: string;
4
+ traceId: string;
5
+ } | null>;
6
+ startCustomSpan(parentSpanId: string, name: string, labels: Record<string, any> | null): Promise<{
7
+ spanId: string;
8
+ traceId: string;
9
+ } | null>;
10
+ endSpan(spanId: string): Promise<void>;
11
+ }
@@ -0,0 +1,12 @@
1
+ import type { CoralogixIgnoredInstrument } from '../model/Types';
2
+ export declare const CustomSpanRegistry: {
3
+ getActive: () => string | null;
4
+ setActive: (id: string) => void;
5
+ clearActive: () => void;
6
+ registerSpan: (spanId: string, traceId: string) => void;
7
+ unregisterSpan: (spanId: string) => void;
8
+ getTraceId: (spanId: string) => string | undefined;
9
+ setIgnoredInstruments: (set: Set<CoralogixIgnoredInstrument>) => void;
10
+ getIgnoredInstruments: () => Set<CoralogixIgnoredInstrument>;
11
+ clear: () => void;
12
+ };
package/src/index.d.ts CHANGED
@@ -10,6 +10,7 @@ interface CxSdkClient {
10
10
  initialize(options: CoralogixBrowserSdkConfig & {
11
11
  frameworkVersion?: string;
12
12
  hasBeforeSend?: boolean;
13
+ hasTracesExporter?: boolean;
13
14
  }): Promise<boolean>;
14
15
  setUserContext(userContext: UserContextConfig): void;
15
16
  getUserContext(): Promise<UserContextConfig>;
@@ -38,6 +39,15 @@ interface CxSdkClient {
38
39
  captureScreenshot(): void;
39
40
  maskViewByTag(viewTag: number): void;
40
41
  reportUserInteraction(interaction: InteractionContext): void;
42
+ startGlobalSpan(name: string, labels: Record<string, any> | null, ignoredInstruments: string[]): Promise<{
43
+ spanId: string;
44
+ traceId: string;
45
+ } | null>;
46
+ startCustomSpan(parentSpanId: string, name: string, labels: Record<string, any> | null): Promise<{
47
+ spanId: string;
48
+ traceId: string;
49
+ } | null>;
50
+ endSpan(spanId: string): Promise<void>;
41
51
  }
42
52
  export declare const CxSdk: CxSdkClient;
43
53
  export declare const CoralogixRum: CoralogixOtelWebType;
@@ -47,9 +57,12 @@ export { type ApplicationContextConfig } from './model/ApplicationContextConfig'
47
57
  export { type ViewContextConfig } from './model/ViewContextConfig';
48
58
  export { CoralogixDomain } from './model/CoralogixDomain';
49
59
  export { type UserContextConfig } from './model/UserContextConfig';
50
- export { type CoralogixBrowserSdkConfig, type NetworkCaptureRule, CoralogixLogSeverity, } from './model/Types';
60
+ export { type CoralogixBrowserSdkConfig, type CoralogixIgnoredInstrument, type NetworkCaptureRule, type TraceExporterData, CoralogixLogSeverity, } from './model/Types';
51
61
  export { type CoralogixOtelWebOptionsInstrumentations } from './model/CoralogixOtelWebOptionsInstrumentations';
52
62
  export { type CustomMeasurement } from './model/CustomMeasurement';
53
63
  export { type NetworkRequestDetails } from './model/NetworkRequestDetails';
54
64
  export { type CoralogixMobileVitals } from './model/CoralogixMobileVitals';
55
65
  export { attachReactNavigationObserver } from './instrumentations/navigation/NavigationInstrumentation';
66
+ export { CoralogixCustomTracer } from './custom-spans/CoralogixCustomTracer';
67
+ export { CoralogixGlobalSpan } from './custom-spans/CoralogixGlobalSpan';
68
+ export { CoralogixCustomSpan } from './custom-spans/CoralogixCustomSpan';
@@ -6,5 +6,6 @@ export declare enum CoralogixDomain {
6
6
  AP1 = "AP1",
7
7
  AP2 = "AP2",
8
8
  AP3 = "AP3",
9
+ US3 = "US3",
9
10
  STAGING = "staging"
10
11
  }
@@ -1,11 +1,12 @@
1
1
  import type { SendLog } from './SendLog';
2
2
  import type { CoralogixRumLabels } from './CoralogixRumLabels';
3
- import { CoralogixBrowserSdkConfig } from './Types';
3
+ import { CoralogixBrowserSdkConfig, CoralogixIgnoredInstrument } from './Types';
4
4
  import { UserContextConfig } from './UserContextConfig';
5
5
  import { ViewContextConfig } from './ViewContextConfig';
6
6
  import { ApplicationContextConfig } from './ApplicationContextConfig';
7
7
  import { CustomMeasurement } from './CustomMeasurement';
8
8
  import { NetworkRequestDetails } from './NetworkRequestDetails';
9
+ import { CoralogixCustomTracer } from '../custom-spans/CoralogixCustomTracer';
9
10
  export interface CoralogixOtelWebType extends SendLog {
10
11
  /**
11
12
  * Init CoralogixRum.
@@ -46,5 +47,10 @@ export interface CoralogixOtelWebType extends SendLog {
46
47
  * @param details
47
48
  */
48
49
  reportNetworkRequest: (details: NetworkRequestDetails) => void;
50
+ /**
51
+ * Returns a tracer for creating custom spans. Instruments listed in
52
+ * ignoredInstruments will not be auto-parented to the active global span.
53
+ */
54
+ getCustomTracer: (ignoredInstruments?: CoralogixIgnoredInstrument[]) => CoralogixCustomTracer;
49
55
  readonly isInited: boolean;
50
56
  }
@@ -218,12 +218,24 @@ export interface CoralogixBrowserSdkConfig {
218
218
  sampleIntervalMs?: number;
219
219
  };
220
220
  mobileVitals?: CoralogixMobileVitals;
221
+ /** Callback invoked with each OTLP trace batch exported by the native SDK. */
222
+ tracesExporter?: (data: TraceExporterData) => void;
221
223
  }
222
224
  export type HybridMetric = {
223
225
  name: string;
224
226
  value: number;
225
227
  units: string;
226
228
  };
229
+ /**
230
+ * Instrumentation names that can be excluded from a custom tracer's span context.
231
+ * Pass one or more values to `getCustomTracer()` to prevent those event types from
232
+ * being linked to spans created by that tracer.
233
+ */
234
+ export type CoralogixIgnoredInstrument = 'networkRequests' | 'userInteractions' | 'errors';
235
+ /** OTLP JSON-format trace data delivered to the `tracesExporter` callback. */
236
+ export interface TraceExporterData {
237
+ resource_spans: Record<string, unknown>[];
238
+ }
227
239
  export interface InteractionContext {
228
240
  type: 'click' | 'scroll' | 'swipe';
229
241
  /** Required. Element identifier: accessibilityLabel ?? componentName for clicks; 'ScrollView' for scroll/swipe. */