@coralogix/react-native-plugin 0.11.3 → 0.13.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,15 @@
1
+ ## 0.13.0 (2026-10-07)
2
+
3
+ ### 🚀 Features
4
+
5
+ - New `cold_hybrid` mobile vital: the time from process start until your React content is first on screen, reported once per launch next to the native `cold` start. Turning off `mobileVitals.cold` turns off both. On Android it isn't reported in bridgeless mode.
6
+
7
+ ## 0.12.0 (2026-10-06)
8
+
9
+ ### 🚀 Features
10
+
11
+ - Added optional `account_id` and `account_name` to `UserContext`.
12
+
1
13
  ## 0.11.3 (2026-09-26)
2
14
 
3
15
  ### 🩹 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.18.4'
20
- s.dependency 'CoralogixInternal','2.18.4'
21
- s.dependency 'SessionReplay','2.18.4'
19
+ s.dependency 'Coralogix','2.19.0'
20
+ s.dependency 'CoralogixInternal','2.19.0'
21
+ s.dependency 'SessionReplay','2.19.0'
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
@@ -48,6 +48,8 @@ CoralogixRum.setUserContext({
48
48
  user_id: '123',
49
49
  user_name: 'name',
50
50
  user_email: 'user@email.com',
51
+ account_id: 'acc-42', // optional, from 0.12.0
52
+ account_name: 'Acme Corp', // optional, from 0.12.0
51
53
  user_metadata: {
52
54
  role: 'admin',
53
55
  // ...
@@ -244,6 +246,23 @@ await CoralogixRum.init({
244
246
  });
245
247
  ```
246
248
 
249
+ #### Cold start
250
+
251
+ Two cold-start values are reported per user-visible launch:
252
+
253
+ - `cold` — measured by the native SDK: process start to the first native frame.
254
+ - `cold_hybrid` — measured by the plugin: process start to the first frame with React content
255
+ on screen. This is the launch time your users actually see, so it is always at least `cold`.
256
+
257
+ Setting `mobileVitals.cold` (or `instrumentations.mobile_vitals`) to `false` disables both.
258
+ Launches that start in the background (headless JS, background fetch, push) and prewarmed iOS
259
+ launches do not report `cold_hybrid`.
260
+
261
+ > [!NOTE]
262
+ > On Android, `cold_hybrid` is not reported in bridgeless mode (the New Architecture default
263
+ > since React Native 0.74): React Native only emits the content-appeared marker it relies on
264
+ > when the app runs with the bridge.
265
+
247
266
  ### Exclude From Sampling
248
267
 
249
268
  By default, when a session is sampled out (via `sessionSampleRate`), nothing is
@@ -87,7 +87,7 @@ dependencies {
87
87
  // 2.22.0 is the first release exposing View.isCoralogixMasked() and
88
88
  // View.isInsideCoralogixMaskedSubtree(), which the masked-interaction walk in
89
89
  // reportUserInteraction reads instead of reimplementing the SDK's mask rule here.
90
- implementation "com.coralogix:android-sdk:2.22.0"
90
+ implementation "com.coralogix:android-sdk:2.23.0"
91
91
 
92
92
  testImplementation "org.jetbrains.kotlin:kotlin-test:$kotlin_version"
93
93
  testImplementation "org.mockito:mockito-core:5.12.0"
@@ -1,2 +1,8 @@
1
1
  <manifest xmlns:android="http://schemas.android.com/apk/res/android">
2
+ <application>
3
+ <provider
4
+ android:name="com.cxsdk.HybridColdStartProvider"
5
+ android:authorities="${applicationId}.cxsdk-hybrid-cold-start"
6
+ android:exported="false" />
7
+ </application>
2
8
  </manifest>
@@ -71,12 +71,18 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
71
71
  config.getString("frameworkVersion") ?: ""
72
72
  else ""
73
73
 
74
+ val reportHybridColdStart =
75
+ config.hasKey("reportHybridColdStart") && config.getBoolean("reportHybridColdStart")
76
+
74
77
  Handler(Looper.getMainLooper()).post {
75
78
  CoralogixRum.initialize(
76
79
  application = application,
77
80
  options = options,
78
81
  framework = Framework.HybridFramework.ReactNative(rnPluginVersion)
79
82
  )
83
+ if (reportHybridColdStart) {
84
+ HybridColdStart.onSdkInitialized()
85
+ }
80
86
 
81
87
  promise.resolve(true)
82
88
  }
@@ -833,7 +839,9 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
833
839
  userId = getString("user_id") ?: "",
834
840
  username = getString("user_name") ?: "",
835
841
  email = getString("user_email") ?: "",
836
- metadata = getMap("user_metadata")?.toStringMap() ?: mapOf()
842
+ metadata = getMap("user_metadata")?.toStringMap() ?: mapOf(),
843
+ accountId = getString("account_id"),
844
+ accountName = getString("account_name")
837
845
  )
838
846
 
839
847
  private fun ReadableMap.toInstrumentationMap(): Map<Instrumentation, Boolean> {
@@ -970,6 +978,8 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
970
978
  map.putString("user_name", username)
971
979
  map.putString("user_email", email)
972
980
  map.putMap("user_metadata", metadata.toWritableMap())
981
+ accountId?.let { map.putString("account_id", it) }
982
+ accountName?.let { map.putString("account_name", it) }
973
983
 
974
984
  return map
975
985
  }
@@ -0,0 +1,70 @@
1
+ package com.cxsdk
2
+
3
+ import android.app.ActivityManager
4
+ import android.content.ContentProvider
5
+ import android.content.ContentValues
6
+ import android.net.Uri
7
+ import android.os.Process
8
+ import android.os.SystemClock
9
+ import com.coralogix.android.sdk.CoralogixRum
10
+ import com.facebook.react.bridge.ReactMarker
11
+ import com.facebook.react.bridge.ReactMarkerConstants
12
+
13
+ /**
14
+ * Hybrid cold start: process start until React Native first puts content on screen.
15
+ * The marker fires before (or racing with) the JS `init` call, so the listener is
16
+ * registered from a ContentProvider at process start. Everything runs on the main
17
+ * thread: the marker comes from ReactRootView.onViewAdded and the module posts
18
+ * initialize to the main looper.
19
+ *
20
+ * React Native arms CONTENT_APPEARED only when it runs the app through the bridge,
21
+ * so bridgeless apps report nothing here.
22
+ */
23
+ internal object HybridColdStart {
24
+ const val TYPE = "cold_hybrid"
25
+
26
+ private var measuredMs: Long? = null
27
+ private var sdkReady = false
28
+
29
+ private val listener = object : ReactMarker.MarkerListener {
30
+ override fun logMarker(name: ReactMarkerConstants, tag: String?, instanceKey: Int) {
31
+ if (name != ReactMarkerConstants.CONTENT_APPEARED) return
32
+ ReactMarker.removeListener(this)
33
+ measuredMs = SystemClock.uptimeMillis() - Process.getStartUptimeMillis()
34
+ reportIfReady()
35
+ }
36
+ }
37
+
38
+ fun start() = ReactMarker.addListener(listener)
39
+
40
+ fun onSdkInitialized() {
41
+ sdkReady = true
42
+ reportIfReady()
43
+ }
44
+
45
+ private fun reportIfReady() {
46
+ val ms = measuredMs ?: return
47
+ if (!sdkReady) return
48
+ measuredMs = null
49
+ CoralogixRum.reportMobileVitalsMeasurement(TYPE, ms.toDouble(), "ms")
50
+ }
51
+ }
52
+
53
+ class HybridColdStartProvider : ContentProvider() {
54
+ override fun onCreate(): Boolean {
55
+ // A process started for headless JS, a broadcast or a service is not a user-visible
56
+ // launch; content appearing later in it would measure idle time, not a cold start.
57
+ val info = ActivityManager.RunningAppProcessInfo()
58
+ ActivityManager.getMyMemoryState(info)
59
+ if (info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND) {
60
+ HybridColdStart.start()
61
+ }
62
+ return true
63
+ }
64
+
65
+ override fun query(uri: Uri, p: Array<String>?, s: String?, a: Array<String>?, o: String?) = null
66
+ override fun getType(uri: Uri) = null
67
+ override fun insert(uri: Uri, values: ContentValues?) = null
68
+ override fun delete(uri: Uri, s: String?, a: Array<String>?) = 0
69
+ override fun update(uri: Uri, v: ContentValues?, s: String?, a: Array<String>?) = 0
70
+ }
package/index.cjs.js CHANGED
@@ -1140,7 +1140,7 @@ function stopJsRefreshRateSampler() {
1140
1140
  appStateSub = null;
1141
1141
  }
1142
1142
 
1143
- var version = "0.11.3";
1143
+ var version = "0.13.0";
1144
1144
  var pkg = {
1145
1145
  version: version};
1146
1146
 
@@ -1865,7 +1865,7 @@ async function runInit(options,
1865
1865
  // which can call `init()` again, and a shared flag would let that nested call answer for
1866
1866
  // this one.
1867
1867
  progress) {
1868
- var _options$debug, _resolvedOptions$excl, _resolvedOptions$excl2;
1868
+ var _options$debug, _resolvedOptions$excl, _resolvedOptions$excl2, _finalOptions$instrum, _finalOptions$mobileV;
1869
1869
  // Configure the logger before anything that can warn: the `public_key` check below, and
1870
1870
  // resolving the options, which normalizes `sessionSampleRate` and warns about an invalid
1871
1871
  // rate. Setting it any later swallowed those warnings on the first init — exactly when
@@ -1932,7 +1932,8 @@ progress) {
1932
1932
  frameworkVersion: pkg.version,
1933
1933
  networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined,
1934
1934
  hasBeforeSend: !!finalOptions.beforeSend,
1935
- hasTracesExporter: !!finalOptions.tracesExporter
1935
+ hasTracesExporter: !!finalOptions.tracesExporter,
1936
+ reportHybridColdStart: ((_finalOptions$instrum = finalOptions.instrumentations) == null ? void 0 : _finalOptions$instrum.mobile_vitals) !== false && ((_finalOptions$mobileV = finalOptions.mobileVitals) == null ? void 0 : _finalOptions$mobileV.cold) !== false
1936
1937
  }));
1937
1938
  isInited = true;
1938
1939
  }
@@ -2053,12 +2054,16 @@ const CoralogixRum = {
2053
2054
  if (!result || typeof result !== 'object') {
2054
2055
  return undefined;
2055
2056
  }
2056
- return {
2057
+ return _extends({
2057
2058
  user_id: result.user_id || '',
2058
2059
  user_name: result.user_name || '',
2059
2060
  user_email: result.user_email || '',
2060
2061
  user_metadata: result.user_metadata || {}
2061
- };
2062
+ }, result.account_id && {
2063
+ account_id: result.account_id
2064
+ }, result.account_name && {
2065
+ account_name: result.account_name
2066
+ });
2062
2067
  },
2063
2068
  setLabels: labels => {
2064
2069
  if (!isInited) {
package/index.esm.js CHANGED
@@ -1138,7 +1138,7 @@ function stopJsRefreshRateSampler() {
1138
1138
  appStateSub = null;
1139
1139
  }
1140
1140
 
1141
- var version = "0.11.3";
1141
+ var version = "0.13.0";
1142
1142
  var pkg = {
1143
1143
  version: version};
1144
1144
 
@@ -1863,7 +1863,7 @@ async function runInit(options,
1863
1863
  // which can call `init()` again, and a shared flag would let that nested call answer for
1864
1864
  // this one.
1865
1865
  progress) {
1866
- var _options$debug, _resolvedOptions$excl, _resolvedOptions$excl2;
1866
+ var _options$debug, _resolvedOptions$excl, _resolvedOptions$excl2, _finalOptions$instrum, _finalOptions$mobileV;
1867
1867
  // Configure the logger before anything that can warn: the `public_key` check below, and
1868
1868
  // resolving the options, which normalizes `sessionSampleRate` and warns about an invalid
1869
1869
  // rate. Setting it any later swallowed those warnings on the first init — exactly when
@@ -1930,7 +1930,8 @@ progress) {
1930
1930
  frameworkVersion: pkg.version,
1931
1931
  networkExtraConfig: finalOptions.networkExtraConfig ? serializeNetworkCaptureRules(finalOptions.networkExtraConfig) : undefined,
1932
1932
  hasBeforeSend: !!finalOptions.beforeSend,
1933
- hasTracesExporter: !!finalOptions.tracesExporter
1933
+ hasTracesExporter: !!finalOptions.tracesExporter,
1934
+ reportHybridColdStart: ((_finalOptions$instrum = finalOptions.instrumentations) == null ? void 0 : _finalOptions$instrum.mobile_vitals) !== false && ((_finalOptions$mobileV = finalOptions.mobileVitals) == null ? void 0 : _finalOptions$mobileV.cold) !== false
1934
1935
  }));
1935
1936
  isInited = true;
1936
1937
  }
@@ -2051,12 +2052,16 @@ const CoralogixRum = {
2051
2052
  if (!result || typeof result !== 'object') {
2052
2053
  return undefined;
2053
2054
  }
2054
- return {
2055
+ return _extends({
2055
2056
  user_id: result.user_id || '',
2056
2057
  user_name: result.user_name || '',
2057
2058
  user_email: result.user_email || '',
2058
2059
  user_metadata: result.user_metadata || {}
2059
- };
2060
+ }, result.account_id && {
2061
+ account_id: result.account_id
2062
+ }, result.account_name && {
2063
+ account_name: result.account_name
2064
+ });
2060
2065
  },
2061
2066
  setLabels: labels => {
2062
2067
  if (!isInited) {
package/ios/CxSdk.mm CHANGED
@@ -1,5 +1,8 @@
1
1
  #import <React/RCTBridgeModule.h>
2
2
  #import <React/RCTEventEmitter.h>
3
+ #import <React/RCTRootView.h>
4
+ #import <UIKit/UIKit.h>
5
+ #import <sys/sysctl.h>
3
6
 
4
7
  @interface RCT_EXTERN_MODULE(CxSdk, RCTEventEmitter)
5
8
 
@@ -139,3 +142,49 @@ RCT_EXTERN_METHOD(endSpan:(NSString *)spanId
139
142
  }
140
143
 
141
144
  @end
145
+
146
+ @interface CxSdk (HybridColdStart)
147
+ + (void)recordHybridColdStart:(double)ms;
148
+ @end
149
+
150
+ // Records process start -> first React content on screen. RCTContentDidAppearNotification
151
+ // is posted by both RCTRootView and Fabric's RCTSurfaceHostingProxyRootView, before or
152
+ // racing with the JS init call, so the observer is added in +load. Must stay trivial:
153
+ // it runs in every process of every consuming app.
154
+ @interface CxHybridColdStartRecorder : NSObject
155
+ @end
156
+
157
+ @implementation CxHybridColdStartRecorder
158
+
159
+ + (void)load
160
+ {
161
+ // Prewarmed processes start long before the user launches the app.
162
+ if ([NSProcessInfo.processInfo.environment[@"ActivePrewarm"] isEqualToString:@"1"]) {
163
+ return;
164
+ }
165
+
166
+ __block id observer = [NSNotificationCenter.defaultCenter
167
+ addObserverForName:RCTContentDidAppearNotification
168
+ object:nil
169
+ queue:NSOperationQueue.mainQueue
170
+ usingBlock:^(NSNotification *note) {
171
+ [NSNotificationCenter.defaultCenter removeObserver:observer];
172
+ observer = nil;
173
+ // Background launches (fetch, push, location) are not a user-visible cold start.
174
+ if (UIApplication.sharedApplication.applicationState == UIApplicationStateBackground) {
175
+ return;
176
+ }
177
+ struct kinfo_proc info;
178
+ size_t size = sizeof(info);
179
+ int mib[] = {CTL_KERN, KERN_PROC, KERN_PROC_PID, getpid()};
180
+ if (sysctl(mib, 4, &info, &size, NULL, 0) != 0) {
181
+ return;
182
+ }
183
+ struct timeval start = info.kp_proc.p_starttime;
184
+ double ms = [NSDate date].timeIntervalSince1970 * 1000.0 -
185
+ (start.tv_sec * 1000.0 + start.tv_usec / 1000.0);
186
+ [CxSdk recordHybridColdStart:ms];
187
+ }];
188
+ }
189
+
190
+ @end
package/ios/CxSdk.swift CHANGED
@@ -24,6 +24,22 @@ class CxSdk: RCTEventEmitter {
24
24
  return false
25
25
  }
26
26
 
27
+ // Hybrid cold start, measured by CxHybridColdStartRecorder in CxSdk.mm. Main thread only:
28
+ // the content-appeared notification is posted on main and initialize hops there.
29
+ private static var hybridColdStartMs: Double?
30
+ private static weak var hybridColdStartRum: CoralogixRum?
31
+
32
+ @objc static func recordHybridColdStart(_ ms: Double) {
33
+ hybridColdStartMs = ms
34
+ reportHybridColdStartIfReady()
35
+ }
36
+
37
+ private static func reportHybridColdStartIfReady() {
38
+ guard let ms = hybridColdStartMs, let rum = hybridColdStartRum else { return }
39
+ hybridColdStartMs = nil
40
+ rum.reportMobileVitalsMeasurement(type: "cold_hybrid", value: ms, units: "ms")
41
+ }
42
+
27
43
  override func supportedEvents() -> [String]! {
28
44
  return ["onBeforeSend", "onTracesExport"]
29
45
  }
@@ -54,7 +70,14 @@ class CxSdk: RCTEventEmitter {
54
70
 
55
71
  let rnPluginVersion = parameter["frameworkVersion"] as? String ?? ""
56
72
 
57
- self.coralogixRum = CoralogixRum(options: options, sdkFramework: .reactNative(version: rnPluginVersion))
73
+ let rum = CoralogixRum(options: options, sdkFramework: .reactNative(version: rnPluginVersion))
74
+ self.coralogixRum = rum
75
+ if parameter["reportHybridColdStart"] as? Bool ?? false {
76
+ DispatchQueue.main.async {
77
+ CxSdk.hybridColdStartRum = rum
78
+ CxSdk.reportHybridColdStartIfReady()
79
+ }
80
+ }
58
81
  resolve("initialize success")
59
82
  } catch let error as CxSdkError {
60
83
  reject("CX_SDK_ERROR", error.localizedDescription, error)
@@ -606,7 +629,9 @@ class CxSdk: RCTEventEmitter {
606
629
  let coralogixUser = UserContext(userId: userContext?["user_id"] as? String ?? "",
607
630
  userName: userContext?["user_name"] as? String ?? "",
608
631
  userEmail:userContext?["user_email"] as? String ?? "",
609
- userMetadata: userMetadata)
632
+ userMetadata: userMetadata,
633
+ accountId: userContext?["account_id"] as? String,
634
+ accountName: userContext?["account_name"] as? String)
610
635
  let ignoreUrls = (parameter["ignoreUrls"] as? [Any])?.compactMap { $0 as? String } ?? []
611
636
  let ignoreError = (parameter["ignoreErrors"] as? [Any])?.compactMap { $0 as? String } ?? []
612
637
  let rawNetworkExtraConfig = parameter["networkExtraConfig"] as? [[String: Any]]
@@ -716,7 +741,9 @@ class CxSdk: RCTEventEmitter {
716
741
  return UserContext(userId: parameter["user_id"] as? String ?? "",
717
742
  userName: parameter["user_name"] as? String ?? "",
718
743
  userEmail:parameter["user_email"] as? String ?? "",
719
- userMetadata: userMetadata)
744
+ userMetadata: userMetadata,
745
+ accountId: parameter["account_id"] as? String,
746
+ accountName: parameter["account_name"] as? String)
720
747
  }
721
748
 
722
749
  private func mobileVitalsType(from string: String) -> CoralogixExporterOptions.MobileVitalsType? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.11.3",
3
+ "version": "0.13.0",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",
package/src/index.d.ts CHANGED
@@ -17,6 +17,7 @@ export interface CxSdkClient {
17
17
  frameworkVersion?: string;
18
18
  hasBeforeSend?: boolean;
19
19
  hasTracesExporter?: boolean;
20
+ reportHybridColdStart?: boolean;
20
21
  }): Promise<boolean>;
21
22
  setUserContext(userContext: UserContextConfig): void;
22
23
  getUserContext(): Promise<UserContextConfig>;
@@ -2,6 +2,8 @@ export interface UserContextConfig {
2
2
  user_id: string;
3
3
  user_name: string;
4
4
  user_email?: string;
5
+ account_id?: string;
6
+ account_name?: string;
5
7
  user_metadata?: {
6
8
  [key: string]: any;
7
9
  };