@coralogix/react-native-plugin 0.11.2 → 0.11.3

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,10 @@
1
+ ## 0.11.3 (2026-09-26)
2
+
3
+ ### 🩹 Fixes
4
+
5
+ - Fixed a crash on iOS when calling `setLabels`. Labels with non-string values (such as numbers or booleans) are now preserved instead of being dropped.
6
+ - `log` now handles missing or incorrectly typed arguments gracefully instead of crashing your app.
7
+
1
8
  ## 0.11.2 (2026-09-19)
2
9
 
3
10
  ### ⬆️ Dependency Updates
@@ -109,9 +109,14 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
109
109
  }
110
110
 
111
111
  @ReactMethod
112
- override fun setLabels(labelsMap: ReadableMap) {
113
- val labels = labelsMap.toStringAnyMap()
114
- CoralogixRum.setLabels(labels)
112
+ override fun setLabels(labelsMap: ReadableMap?) {
113
+ // Nullable: a JS null would fail Kotlin's null check and crash the app.
114
+ if (labelsMap == null) {
115
+ Log.w("CxSdkModule", "setLabels: labels is null, ignoring")
116
+ return
117
+ }
118
+
119
+ CoralogixRum.setLabels(labelsMap.toStringAnyMap())
115
120
  }
116
121
 
117
122
  @ReactMethod
@@ -126,8 +131,23 @@ class CxSdkModule(reactContext: ReactApplicationContext) :
126
131
  }
127
132
 
128
133
  @ReactMethod
129
- override fun log(severity: Int, message: String, data: ReadableMap, labels: ReadableMap) {
130
- CoralogixRum.log(severity.toCoralogixLogSeverity(), message, data.toStringAnyMap(), labels.toStringAnyMap())
134
+ override fun log(severity: Int, message: String?, data: ReadableMap?, labels: ReadableMap?) {
135
+ // Nullable: a JS null would fail Kotlin's null check and crash the app.
136
+ // Non-string messages fail earlier, in RN's parsing; `toSafeMessage` (JS) covers them.
137
+
138
+ // Drop unknown severities, like iOS, rather than invent one.
139
+ val cxSeverity = runCatching { severity.toCoralogixLogSeverity() }.getOrNull()
140
+ if (cxSeverity == null) {
141
+ Log.w("CxSdkModule", "log: invalid severity $severity, dropping log. Must be one of [1, 2, 3, 4, 5, 6]")
142
+ return
143
+ }
144
+
145
+ CoralogixRum.log(
146
+ cxSeverity,
147
+ message ?: "",
148
+ data?.toStringAnyMap() ?: emptyMap(),
149
+ labels?.toStringAnyMap() ?: emptyMap()
150
+ )
131
151
  }
132
152
 
133
153
  @ReactMethod
@@ -10,10 +10,10 @@ interface RUMClient {
10
10
  fun getUserContext(promise: Promise)
11
11
  fun setApplicationContext(appContextMap: ReadableMap)
12
12
  fun getSessionId(promise: Promise)
13
- fun setLabels(labelsMap: ReadableMap)
13
+ fun setLabels(labelsMap: ReadableMap?)
14
14
  fun getLabels(promise: Promise)
15
15
  fun setViewContext(viewContext: String)
16
- fun log(severity: Int, message: String, data: ReadableMap, labels: ReadableMap)
16
+ fun log(severity: Int, message: String?, data: ReadableMap?, labels: ReadableMap?)
17
17
  fun reportNetworkRequest(requestDetails: ReadableMap)
18
18
  fun sendCustomMeasurement(measurement: ReadableMap)
19
19
  fun startTimeMeasure(name: String, labels: ReadableMap?)
package/index.cjs.js CHANGED
@@ -1140,7 +1140,7 @@ function stopJsRefreshRateSampler() {
1140
1140
  appStateSub = null;
1141
1141
  }
1142
1142
 
1143
- var version = "0.11.2";
1143
+ var version = "0.11.3";
1144
1144
  var pkg = {
1145
1145
  version: version};
1146
1146
 
@@ -1741,17 +1741,121 @@ let initPromise;
1741
1741
  // `CoralogixRum.isInited` be true while native has no RUM behind it.
1742
1742
  let nativeOwnsSamplingRoll = false;
1743
1743
  let fetchInstrumentation;
1744
+
1745
+ /**
1746
+ * Whether RN can send `value` as a dictionary; never throws. RN sends an object's
1747
+ * enumerable properties, so plain objects and class instances with fields work,
1748
+ * while `Map`, `Set` and `Date` (none enumerable) would arrive as `{}`.
1749
+ */
1750
+ function isPlainObjectArgument(value) {
1751
+ try {
1752
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1753
+ return false;
1754
+ }
1755
+ const proto = Object.getPrototypeOf(value);
1756
+ if (proto === Object.prototype || proto === null) {
1757
+ return true;
1758
+ }
1759
+ for (const _key in value) {
1760
+ return true;
1761
+ }
1762
+ return false;
1763
+ } catch (_unused) {
1764
+ return false;
1765
+ }
1766
+ }
1767
+
1768
+ /** Whether `severity` is one of the `CoralogixLogSeverity` values (1–6). */
1769
+ function isValidSeverity(severity) {
1770
+ return Number.isInteger(severity) && severity >= 1 && severity <= 6;
1771
+ }
1772
+
1773
+ /** Type name for warnings; never throws. */
1774
+ function describeArgument(value) {
1775
+ if (value === null) {
1776
+ return 'null';
1777
+ }
1778
+ try {
1779
+ var _Object$getPrototypeO;
1780
+ if (Array.isArray(value)) {
1781
+ return 'array';
1782
+ }
1783
+ const name = typeof value === 'object' ? (_Object$getPrototypeO = Object.getPrototypeOf(value)) == null || (_Object$getPrototypeO = _Object$getPrototypeO.constructor) == null ? void 0 : _Object$getPrototypeO.name : undefined;
1784
+ // Minified builds rename classes to one letter, which would only confuse.
1785
+ return name && name !== 'Object' && name.length > 1 ? name : typeof value;
1786
+ } catch (_unused2) {
1787
+ return 'an uninspectable value';
1788
+ }
1789
+ }
1744
1790
  function toSafeRecord(name, value) {
1745
1791
  if (value == null) {
1746
1792
  return {};
1747
1793
  }
1748
- if (typeof value !== 'object' || Array.isArray(value)) {
1749
- logger.warn(`CoralogixRum.log: \`${name}\` must be a plain object; received ${Array.isArray(value) ? 'array' : typeof value}. Ignoring.`);
1794
+ if (!isPlainObjectArgument(value)) {
1795
+ logger.warn(`CoralogixRum.log: \`${name}\` must be a plain object; received ${describeArgument(value)}. Ignoring.`);
1750
1796
  return {};
1751
1797
  }
1752
1798
  return value;
1753
1799
  }
1754
1800
 
1801
+ /** Stringifies any value without throwing (`JSON.stringify` and `String()` both can). */
1802
+ function stringifyUnknown(value) {
1803
+ try {
1804
+ const json = JSON.stringify(value);
1805
+ if (typeof json === 'string') {
1806
+ return json;
1807
+ }
1808
+ } catch (_unused3) {
1809
+ // fall through to String()
1810
+ }
1811
+ try {
1812
+ return String(value);
1813
+ } catch (_unused4) {
1814
+ return '[unserializable]';
1815
+ }
1816
+ }
1817
+
1818
+ /**
1819
+ * Readable text for an `Error` or a function, which `JSON.stringify` renders as `"{}"`
1820
+ * or source code. Returns undefined for anything else; never throws.
1821
+ */
1822
+ function describeSpecialMessage(value) {
1823
+ try {
1824
+ if (value instanceof Error) {
1825
+ return value.stack || String(value);
1826
+ }
1827
+ if (typeof value === 'function') {
1828
+ return `[function ${value.name || 'anonymous'}]`;
1829
+ }
1830
+ } catch (_unused5) {
1831
+ // fall through to stringifyUnknown
1832
+ }
1833
+ return undefined;
1834
+ }
1835
+
1836
+ /**
1837
+ * Coerces `message` to a string before the bridge. On Android a non-string throws
1838
+ * inside RN's argument parsing, before native code runs, so only JS can guard it.
1839
+ */
1840
+ function toSafeMessage(value) {
1841
+ if (typeof value === 'string') {
1842
+ return value;
1843
+ }
1844
+ if (value == null) {
1845
+ logger.warn(`CoralogixRum.log: \`message\` must be a string; received ${value === null ? 'null' : 'undefined'}. Sending an empty message.`);
1846
+ return '';
1847
+ }
1848
+ let coerced;
1849
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
1850
+ coerced = String(value);
1851
+ } else {
1852
+ var _describeSpecialMessa;
1853
+ coerced = (_describeSpecialMessa = describeSpecialMessage(value)) != null ? _describeSpecialMessa : stringifyUnknown(value);
1854
+ }
1855
+ logger.warn(`CoralogixRum.log: \`message\` must be a string; received ${describeArgument(value)}. Coerced to a string.`);
1856
+ return coerced;
1857
+ }
1858
+
1755
1859
  /**
1756
1860
  * The real init work. Runs at most once per in-flight/finished init: `CoralogixRum.init`
1757
1861
  * memoizes the promise this returns, so concurrent callers share this single run.
@@ -1961,6 +2065,13 @@ const CoralogixRum = {
1961
2065
  logger.debug('CoralogixRum must be initiated before setting labels');
1962
2066
  return;
1963
2067
  }
2068
+
2069
+ // Native reads this as a dictionary unchecked, so a non-object crashes the app.
2070
+ // Return instead of sending `{}`, which would wipe existing labels.
2071
+ if (!isPlainObjectArgument(labels)) {
2072
+ logger.warn(`CoralogixRum.setLabels: \`labels\` must be a plain object; received ${describeArgument(labels)}. Ignoring.`);
2073
+ return;
2074
+ }
1964
2075
  CxSdk.setLabels(labels);
1965
2076
  },
1966
2077
  getLabels: async () => {
@@ -1982,9 +2093,16 @@ const CoralogixRum = {
1982
2093
  logger.debug('CoralogixRum must be initiated before using log');
1983
2094
  return;
1984
2095
  }
2096
+
2097
+ // A non-integer severity crashes Android while RN parses the arguments.
2098
+ if (!isValidSeverity(severity)) {
2099
+ logger.warn(`CoralogixRum.log: \`severity\` must be a CoralogixLogSeverity (1-6); received ${describeArgument(severity)}. Dropping the log.`);
2100
+ return;
2101
+ }
2102
+ const safeMessage = toSafeMessage(message);
1985
2103
  const safeData = toSafeRecord('data', data);
1986
2104
  const safeLabels = toSafeRecord('labels', labels);
1987
- CxSdk.log(severity, message, safeData, safeLabels);
2105
+ CxSdk.log(severity, safeMessage, safeData, safeLabels);
1988
2106
  },
1989
2107
  debug(message, data) {
1990
2108
  this.log(CoralogixLogSeverity.Debug, message, data);
package/index.esm.js CHANGED
@@ -1138,7 +1138,7 @@ function stopJsRefreshRateSampler() {
1138
1138
  appStateSub = null;
1139
1139
  }
1140
1140
 
1141
- var version = "0.11.2";
1141
+ var version = "0.11.3";
1142
1142
  var pkg = {
1143
1143
  version: version};
1144
1144
 
@@ -1739,17 +1739,121 @@ let initPromise;
1739
1739
  // `CoralogixRum.isInited` be true while native has no RUM behind it.
1740
1740
  let nativeOwnsSamplingRoll = false;
1741
1741
  let fetchInstrumentation;
1742
+
1743
+ /**
1744
+ * Whether RN can send `value` as a dictionary; never throws. RN sends an object's
1745
+ * enumerable properties, so plain objects and class instances with fields work,
1746
+ * while `Map`, `Set` and `Date` (none enumerable) would arrive as `{}`.
1747
+ */
1748
+ function isPlainObjectArgument(value) {
1749
+ try {
1750
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
1751
+ return false;
1752
+ }
1753
+ const proto = Object.getPrototypeOf(value);
1754
+ if (proto === Object.prototype || proto === null) {
1755
+ return true;
1756
+ }
1757
+ for (const _key in value) {
1758
+ return true;
1759
+ }
1760
+ return false;
1761
+ } catch (_unused) {
1762
+ return false;
1763
+ }
1764
+ }
1765
+
1766
+ /** Whether `severity` is one of the `CoralogixLogSeverity` values (1–6). */
1767
+ function isValidSeverity(severity) {
1768
+ return Number.isInteger(severity) && severity >= 1 && severity <= 6;
1769
+ }
1770
+
1771
+ /** Type name for warnings; never throws. */
1772
+ function describeArgument(value) {
1773
+ if (value === null) {
1774
+ return 'null';
1775
+ }
1776
+ try {
1777
+ var _Object$getPrototypeO;
1778
+ if (Array.isArray(value)) {
1779
+ return 'array';
1780
+ }
1781
+ const name = typeof value === 'object' ? (_Object$getPrototypeO = Object.getPrototypeOf(value)) == null || (_Object$getPrototypeO = _Object$getPrototypeO.constructor) == null ? void 0 : _Object$getPrototypeO.name : undefined;
1782
+ // Minified builds rename classes to one letter, which would only confuse.
1783
+ return name && name !== 'Object' && name.length > 1 ? name : typeof value;
1784
+ } catch (_unused2) {
1785
+ return 'an uninspectable value';
1786
+ }
1787
+ }
1742
1788
  function toSafeRecord(name, value) {
1743
1789
  if (value == null) {
1744
1790
  return {};
1745
1791
  }
1746
- if (typeof value !== 'object' || Array.isArray(value)) {
1747
- logger.warn(`CoralogixRum.log: \`${name}\` must be a plain object; received ${Array.isArray(value) ? 'array' : typeof value}. Ignoring.`);
1792
+ if (!isPlainObjectArgument(value)) {
1793
+ logger.warn(`CoralogixRum.log: \`${name}\` must be a plain object; received ${describeArgument(value)}. Ignoring.`);
1748
1794
  return {};
1749
1795
  }
1750
1796
  return value;
1751
1797
  }
1752
1798
 
1799
+ /** Stringifies any value without throwing (`JSON.stringify` and `String()` both can). */
1800
+ function stringifyUnknown(value) {
1801
+ try {
1802
+ const json = JSON.stringify(value);
1803
+ if (typeof json === 'string') {
1804
+ return json;
1805
+ }
1806
+ } catch (_unused3) {
1807
+ // fall through to String()
1808
+ }
1809
+ try {
1810
+ return String(value);
1811
+ } catch (_unused4) {
1812
+ return '[unserializable]';
1813
+ }
1814
+ }
1815
+
1816
+ /**
1817
+ * Readable text for an `Error` or a function, which `JSON.stringify` renders as `"{}"`
1818
+ * or source code. Returns undefined for anything else; never throws.
1819
+ */
1820
+ function describeSpecialMessage(value) {
1821
+ try {
1822
+ if (value instanceof Error) {
1823
+ return value.stack || String(value);
1824
+ }
1825
+ if (typeof value === 'function') {
1826
+ return `[function ${value.name || 'anonymous'}]`;
1827
+ }
1828
+ } catch (_unused5) {
1829
+ // fall through to stringifyUnknown
1830
+ }
1831
+ return undefined;
1832
+ }
1833
+
1834
+ /**
1835
+ * Coerces `message` to a string before the bridge. On Android a non-string throws
1836
+ * inside RN's argument parsing, before native code runs, so only JS can guard it.
1837
+ */
1838
+ function toSafeMessage(value) {
1839
+ if (typeof value === 'string') {
1840
+ return value;
1841
+ }
1842
+ if (value == null) {
1843
+ logger.warn(`CoralogixRum.log: \`message\` must be a string; received ${value === null ? 'null' : 'undefined'}. Sending an empty message.`);
1844
+ return '';
1845
+ }
1846
+ let coerced;
1847
+ if (typeof value === 'number' || typeof value === 'boolean' || typeof value === 'bigint') {
1848
+ coerced = String(value);
1849
+ } else {
1850
+ var _describeSpecialMessa;
1851
+ coerced = (_describeSpecialMessa = describeSpecialMessage(value)) != null ? _describeSpecialMessa : stringifyUnknown(value);
1852
+ }
1853
+ logger.warn(`CoralogixRum.log: \`message\` must be a string; received ${describeArgument(value)}. Coerced to a string.`);
1854
+ return coerced;
1855
+ }
1856
+
1753
1857
  /**
1754
1858
  * The real init work. Runs at most once per in-flight/finished init: `CoralogixRum.init`
1755
1859
  * memoizes the promise this returns, so concurrent callers share this single run.
@@ -1959,6 +2063,13 @@ const CoralogixRum = {
1959
2063
  logger.debug('CoralogixRum must be initiated before setting labels');
1960
2064
  return;
1961
2065
  }
2066
+
2067
+ // Native reads this as a dictionary unchecked, so a non-object crashes the app.
2068
+ // Return instead of sending `{}`, which would wipe existing labels.
2069
+ if (!isPlainObjectArgument(labels)) {
2070
+ logger.warn(`CoralogixRum.setLabels: \`labels\` must be a plain object; received ${describeArgument(labels)}. Ignoring.`);
2071
+ return;
2072
+ }
1962
2073
  CxSdk.setLabels(labels);
1963
2074
  },
1964
2075
  getLabels: async () => {
@@ -1980,9 +2091,16 @@ const CoralogixRum = {
1980
2091
  logger.debug('CoralogixRum must be initiated before using log');
1981
2092
  return;
1982
2093
  }
2094
+
2095
+ // A non-integer severity crashes Android while RN parses the arguments.
2096
+ if (!isValidSeverity(severity)) {
2097
+ logger.warn(`CoralogixRum.log: \`severity\` must be a CoralogixLogSeverity (1-6); received ${describeArgument(severity)}. Dropping the log.`);
2098
+ return;
2099
+ }
2100
+ const safeMessage = toSafeMessage(message);
1983
2101
  const safeData = toSafeRecord('data', data);
1984
2102
  const safeLabels = toSafeRecord('labels', labels);
1985
- CxSdk.log(severity, message, safeData, safeLabels);
2103
+ CxSdk.log(severity, safeMessage, safeData, safeLabels);
1986
2104
  },
1987
2105
  debug(message, data) {
1988
2106
  this.log(CoralogixLogSeverity.Debug, message, data);
package/ios/CxSdk.swift CHANGED
@@ -79,11 +79,15 @@ class CxSdk: RCTEventEmitter {
79
79
  }
80
80
 
81
81
  @objc(setLabels:withResolver:withRejecter:)
82
- func setLabels(labelsMap: NSDictionary,
82
+ func setLabels(labelsMap: NSDictionary?,
83
83
  resolve:RCTPromiseResolveBlock,
84
84
  reject:RCTPromiseRejectBlock) -> Void {
85
- let labels = labelsMap as? [String: String] ?? [String: String]()
86
- resolve(coralogixRum?.set(labels: labels))
85
+ // Ignore nil or a non-dictionary, as Android does, rather than wipe the labels.
86
+ guard let labelsMap = labelsMap, labelsMap.isKind(of: NSDictionary.self) else {
87
+ resolve(nil)
88
+ return
89
+ }
90
+ resolve(coralogixRum?.set(labels: bridgedDictionary(labelsMap)))
87
91
  }
88
92
 
89
93
  @objc(getLabels:withRejecter:)
@@ -128,18 +132,19 @@ class CxSdk: RCTEventEmitter {
128
132
 
129
133
  @objc(log:message:data:labels:withResolver:withRejecter:)
130
134
  func log(severity: Int,
131
- message: String,
132
- data: NSDictionary,
133
- labels: NSDictionary,
135
+ message: NSString?,
136
+ data: NSDictionary?,
137
+ labels: NSDictionary?,
134
138
  resolve:RCTPromiseResolveBlock,
135
139
  reject:RCTPromiseRejectBlock) -> Void {
136
140
 
137
141
  guard let cxLogSeverity = CoralogixLogSeverity(rawValue: Int(severity)) else {
138
142
  return
139
143
  }
140
- let data = data as? [String: Any] ?? [String: Any]()
141
- let labels = labels as? [String: Any] ?? [String: Any]()
142
- coralogixRum?.log(severity:cxLogSeverity, message: message, data: data, labels: labels)
144
+ coralogixRum?.log(severity: cxLogSeverity,
145
+ message: bridgedString(message),
146
+ data: bridgedDictionary(data),
147
+ labels: bridgedDictionary(labels))
143
148
  resolve("log success")
144
149
  }
145
150
 
@@ -832,6 +837,30 @@ class CxSdk: RCTEventEmitter {
832
837
  return set
833
838
  }
834
839
 
840
+ /// Reads a bridged dictionary without trusting its type: release builds skip RN's
841
+ /// type check, so a JS array/string/number can arrive here. Must be `isKind(of:)` —
842
+ /// `as? NSDictionary` gets optimised away. Values pass through as `Any`, like Android.
843
+ private func bridgedDictionary(_ value: NSDictionary?) -> [String: Any] {
844
+ guard let object = value, object.isKind(of: NSDictionary.self) else {
845
+ return [String: Any]()
846
+ }
847
+
848
+ var result = [String: Any]()
849
+ object.enumerateKeysAndObjects { key, value, _ in
850
+ guard let name = key as? String else { return }
851
+ result[name] = value
852
+ }
853
+ return result
854
+ }
855
+
856
+ /// Same for strings: a Swift `String` parameter would force-cast an `NSNumber` and crash.
857
+ private func bridgedString(_ value: NSString?) -> String {
858
+ guard let object = value, object.isKind(of: NSString.self) else {
859
+ return ""
860
+ }
861
+ return object as String
862
+ }
863
+
835
864
  private func toAnyDict(_ dict: NSDictionary?) -> [String: Any]? {
836
865
  guard let dict = dict, dict.count > 0 else { return nil }
837
866
  var result = [String: Any]()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coralogix/react-native-plugin",
3
- "version": "0.11.2",
3
+ "version": "0.11.3",
4
4
  "description": "Official Coralogix React Native plugin",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Coralogix",