@coralogix/react-native-plugin 0.12.0 → 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,9 @@
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
+
1
7
  ## 0.12.0 (2026-10-06)
2
8
 
3
9
  ### 🚀 Features
package/README.md CHANGED
@@ -246,6 +246,23 @@ await CoralogixRum.init({
246
246
  });
247
247
  ```
248
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
+
249
266
  ### Exclude From Sampling
250
267
 
251
268
  By default, when a session is sampled out (via `sessionSampleRate`), nothing is
@@ -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
  }
@@ -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.12.0";
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
  }
package/index.esm.js CHANGED
@@ -1138,7 +1138,7 @@ function stopJsRefreshRateSampler() {
1138
1138
  appStateSub = null;
1139
1139
  }
1140
1140
 
1141
- var version = "0.12.0";
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
  }
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)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.12.0",
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>;