@coralogix/react-native-plugin 0.12.0 → 0.13.1
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 +12 -0
- package/CxSdk.podspec +3 -3
- package/README.md +17 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/com/cxsdk/CxSdkModule.kt +6 -0
- package/android/src/main/java/com/cxsdk/HybridColdStart.kt +70 -0
- package/index.cjs.js +4 -3
- package/index.esm.js +4 -3
- package/ios/CxSdk.mm +49 -0
- package/ios/CxSdk.swift +24 -1
- package/package.json +1 -1
- package/src/index.d.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## 0.13.1 (2026-10-12)
|
|
2
|
+
|
|
3
|
+
### ⬆️ Dependency Updates
|
|
4
|
+
|
|
5
|
+
- Native iOS SDK bumped to **2.19.1**, which fixes a crash that could occur when `setLabels` or `setUserContext` was called while events were being sent
|
|
6
|
+
|
|
7
|
+
## 0.13.0 (2026-10-07)
|
|
8
|
+
|
|
9
|
+
### 🚀 Features
|
|
10
|
+
|
|
11
|
+
- 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.
|
|
12
|
+
|
|
1
13
|
## 0.12.0 (2026-10-06)
|
|
2
14
|
|
|
3
15
|
### 🚀 Features
|
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.19.
|
|
20
|
-
s.dependency 'CoralogixInternal','2.19.
|
|
21
|
-
s.dependency 'SessionReplay','2.19.
|
|
19
|
+
s.dependency 'Coralogix','2.19.1'
|
|
20
|
+
s.dependency 'CoralogixInternal','2.19.1'
|
|
21
|
+
s.dependency 'SessionReplay','2.19.1'
|
|
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
|
@@ -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.
|
|
1143
|
+
var version = "0.13.1";
|
|
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.
|
|
1141
|
+
var version = "0.13.1";
|
|
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
|
-
|
|
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
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>;
|