@goliapkg/sentori-react-native 6.2.0 → 6.2.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/SentoriReactNative.podspec +11 -4
- package/android/build.gradle +15 -1
- package/android/src/main/java-core/com/sentori/Sentori.kt +244 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriAnrWatchdog.kt +3 -0
- package/android/src/main/java-core/com/sentori/SentoriConfig.kt +61 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriCrashHandler.kt +3 -0
- package/android/src/main/java-core/com/sentori/SentoriDevice.kt +61 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriFirebaseMessagingService.kt +3 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriForegroundActivity.kt +3 -0
- package/android/src/main/java-core/com/sentori/SentoriIdentity.kt +101 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriMobileVitals.kt +3 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriNativeExceptionBridge.kt +3 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriNativeSignals.kt +3 -0
- package/android/src/main/java-core/com/sentori/SentoriPush.kt +359 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriPushNotifications.kt +3 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriReplayCapture.kt +3 -0
- package/android/src/main/java-core/com/sentori/SentoriScope.kt +60 -0
- package/android/src/main/{java → java-core}/com/sentori/SentoriScreenshotCapture.kt +3 -0
- package/android/src/main/java-core/com/sentori/SentoriSignalRing.kt +80 -0
- package/android/src/main/java-core/com/sentori/SentoriTransport.kt +383 -0
- package/android/src/test/{java → java-core}/com/sentori/SentoriCrashHandlerTest.kt +3 -0
- package/android/src/test/java-core/com/sentori/SentoriIdentityTest.kt +107 -0
- package/android/src/test/java-core/com/sentori/SentoriLiveServerTest.kt +106 -0
- package/android/src/test/java-core/com/sentori/SentoriPushTest.kt +151 -0
- package/android/src/test/{java → java-core}/com/sentori/SentoriScreenshotCaptureTest.kt +3 -0
- package/android/src/test/java-core/com/sentori/SentoriVerbsTest.kt +185 -0
- package/ios/{SentoriCrashHandler.swift → core/SentoriCrashHandler.swift} +16 -0
- package/ios/{SentoriHangWatchdog.swift → core/SentoriHangWatchdog.swift} +3 -0
- package/ios/{SentoriMobileVitals.swift → core/SentoriMobileVitals.swift} +3 -0
- package/ios/{SentoriNativeExceptionBridge.swift → core/SentoriNativeExceptionBridge.swift} +3 -0
- package/ios/core/SentoriPush.swift +340 -0
- package/ios/{SentoriPushNotifications.swift → core/SentoriPushNotifications.swift} +42 -0
- package/ios/{SentoriReplayCapture.swift → core/SentoriReplayCapture.swift} +3 -0
- package/ios/{SentoriScreenshotCapture.swift → core/SentoriScreenshotCapture.swift} +14 -0
- package/ios/{SentoriThreadSampler.swift → core/SentoriThreadSampler.swift} +3 -0
- package/lib/transport.js +1 -1
- package/package.json +1 -1
- package/src/transport.ts +1 -1
- package/ios/Tests/SentoriCrashHandlerTests.swift +0 -59
- package/ios/Tests/SentoriScreenshotCaptureTests.swift +0 -59
- package/ios/Tests/SentoriThreadSamplerTests.swift +0 -96
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// GENERATED MIRROR — do not edit.
|
|
2
|
+
// Source of truth: sdk/native/android/src/test/java/com/sentori/SentoriPushTest.kt
|
|
3
|
+
// Run `node scripts/sync-native-core.mjs` after editing it.
|
|
4
|
+
package com.sentori
|
|
5
|
+
|
|
6
|
+
import androidx.test.core.app.ApplicationProvider
|
|
7
|
+
import java.util.concurrent.CountDownLatch
|
|
8
|
+
import java.util.concurrent.TimeUnit
|
|
9
|
+
import org.junit.After
|
|
10
|
+
import org.junit.Assert.assertEquals
|
|
11
|
+
import org.junit.Assert.assertNotNull
|
|
12
|
+
import org.junit.Assert.assertNull
|
|
13
|
+
import org.junit.Assert.assertTrue
|
|
14
|
+
import org.junit.Before
|
|
15
|
+
import org.junit.Test
|
|
16
|
+
import org.junit.runner.RunWith
|
|
17
|
+
import org.robolectric.RobolectricTestRunner
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Push registration, as far as Robolectric can go.
|
|
21
|
+
*
|
|
22
|
+
* There is no FCM on the classpath here and no Google Play services,
|
|
23
|
+
* so the honest coverage is the shape of the failures and the shape of
|
|
24
|
+
* the request — not a real token. `scripts/android-live-ingest.sh`
|
|
25
|
+
* proves the ingest route against a real server; a device proves the
|
|
26
|
+
* rest, and nothing here pretends otherwise.
|
|
27
|
+
*
|
|
28
|
+
* Deliberately the same assertions as `SentoriPushTests.swift`.
|
|
29
|
+
*/
|
|
30
|
+
@RunWith(RobolectricTestRunner::class)
|
|
31
|
+
class SentoriPushTest {
|
|
32
|
+
|
|
33
|
+
private val context get() = ApplicationProvider.getApplicationContext<android.content.Context>()
|
|
34
|
+
|
|
35
|
+
@Before
|
|
36
|
+
fun setUp() {
|
|
37
|
+
SentoriConfig.resetForTests()
|
|
38
|
+
SentoriScope.clear()
|
|
39
|
+
SentoriPush.resetForTests()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@After
|
|
43
|
+
fun tearDown() {
|
|
44
|
+
SentoriConfig.resetForTests()
|
|
45
|
+
SentoriPush.resetForTests()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private fun registerBlocking(timeoutMs: Long = 1_000): SentoriPush.Result {
|
|
49
|
+
val latch = CountDownLatch(1)
|
|
50
|
+
var result: SentoriPush.Result? = null
|
|
51
|
+
SentoriPush.register(context, activity = null, timeoutMs = timeoutMs) {
|
|
52
|
+
result = it
|
|
53
|
+
latch.countDown()
|
|
54
|
+
}
|
|
55
|
+
assertTrue("register never called back", latch.await(30, TimeUnit.SECONDS))
|
|
56
|
+
return result!!
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@Test
|
|
60
|
+
fun registerBeforeStartReportsItRatherThanThrowing() {
|
|
61
|
+
val r = registerBlocking()
|
|
62
|
+
assertTrue("expected a failure before start, got $r", r is SentoriPush.Result.Failure)
|
|
63
|
+
assertEquals(
|
|
64
|
+
SentoriPush.Failure.NOT_INITIALISED,
|
|
65
|
+
(r as SentoriPush.Result.Failure).reason,
|
|
66
|
+
)
|
|
67
|
+
assertEquals("not-initialised", r.reason.reason)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Test
|
|
71
|
+
fun everyFailureHasTheSameNameAsTheOtherTwoSdks() {
|
|
72
|
+
// Same strings as `PushRegisterFailure` in React Native and
|
|
73
|
+
// `SentoriPush.Failure` in Swift, so one set of integration
|
|
74
|
+
// notes covers all three.
|
|
75
|
+
assertEquals("not-initialised", SentoriPush.Failure.NOT_INITIALISED.reason)
|
|
76
|
+
assertEquals("permission-denied", SentoriPush.Failure.PERMISSION_DENIED.reason)
|
|
77
|
+
assertEquals("no-transport", SentoriPush.Failure.NO_TRANSPORT.reason)
|
|
78
|
+
assertEquals("token-timeout", SentoriPush.Failure.TOKEN_TIMEOUT.reason)
|
|
79
|
+
assertEquals("server-rejected", SentoriPush.Failure.SERVER_REJECTED.reason)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@Test
|
|
83
|
+
fun registerWithoutFcmFailsWithoutThrowing() {
|
|
84
|
+
Sentori.start(
|
|
85
|
+
SentoriConfig(
|
|
86
|
+
token = "st_test",
|
|
87
|
+
ingestUrl = "http://127.0.0.1:9",
|
|
88
|
+
release = "app@1.0.0",
|
|
89
|
+
environment = "test",
|
|
90
|
+
),
|
|
91
|
+
context,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
// No Firebase on the classpath, so this ends at permission or
|
|
95
|
+
// at the token wait. Either is fine; reaching the end without
|
|
96
|
+
// a throw and without a hang is the assertion.
|
|
97
|
+
val r = registerBlocking()
|
|
98
|
+
assertTrue("Robolectric cannot actually register, got $r", r is SentoriPush.Result.Failure)
|
|
99
|
+
val reason = (r as SentoriPush.Result.Failure).reason
|
|
100
|
+
assertTrue(
|
|
101
|
+
"unexpected reason ${reason.reason}: ${r.message}",
|
|
102
|
+
reason in
|
|
103
|
+
listOf(
|
|
104
|
+
SentoriPush.Failure.PERMISSION_DENIED,
|
|
105
|
+
SentoriPush.Failure.NO_TRANSPORT,
|
|
106
|
+
SentoriPush.Failure.TOKEN_TIMEOUT,
|
|
107
|
+
),
|
|
108
|
+
)
|
|
109
|
+
assertNull("a failure must cache nothing", SentoriPush.cachedDeviceHandle(context))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@Test
|
|
113
|
+
fun unregisterWithNothingRegisteredIsANoOp() {
|
|
114
|
+
val latch = CountDownLatch(1)
|
|
115
|
+
var ok: Boolean? = null
|
|
116
|
+
SentoriPush.unregister(context) {
|
|
117
|
+
ok = it
|
|
118
|
+
latch.countDown()
|
|
119
|
+
}
|
|
120
|
+
assertTrue(latch.await(10, TimeUnit.SECONDS))
|
|
121
|
+
assertEquals("nothing to revoke", false, ok)
|
|
122
|
+
assertNull(SentoriPush.cachedDeviceHandle(context))
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
@Test
|
|
126
|
+
fun theRegistrationBodyIsTheOneTheServerAccepts() {
|
|
127
|
+
// The field names are the whole reason push never worked for a
|
|
128
|
+
// year: the RN SDK sent `provider` where the server reads
|
|
129
|
+
// `kind`, and parsed an `ipt_*` handle no server has ever
|
|
130
|
+
// returned. Pinned here rather than left to a reviewer.
|
|
131
|
+
SentoriScope.setUser("usr_123", null)
|
|
132
|
+
val body =
|
|
133
|
+
mapOf(
|
|
134
|
+
"kind" to "fcm",
|
|
135
|
+
"nativeToken" to "abcd",
|
|
136
|
+
"userKey" to SentoriScope.userKey,
|
|
137
|
+
)
|
|
138
|
+
val json = org.json.JSONObject(SentoriTransport.toJson(body).toString())
|
|
139
|
+
|
|
140
|
+
assertEquals("fcm", json.getString("kind"))
|
|
141
|
+
assertTrue("the server has no such field", !json.has("provider"))
|
|
142
|
+
// FCM is one host; an `env` here would be a claim about a
|
|
143
|
+
// sandbox/production split that does not exist.
|
|
144
|
+
assertTrue("FCM has no environment split", !json.has("env"))
|
|
145
|
+
assertEquals(
|
|
146
|
+
SentoriIdentity.hash("id", "usr_123"),
|
|
147
|
+
json.getString("userKey"),
|
|
148
|
+
)
|
|
149
|
+
assertNotNull(json)
|
|
150
|
+
}
|
|
151
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// GENERATED MIRROR — do not edit.
|
|
2
|
+
// Source of truth: sdk/native/android/src/test/java/com/sentori/SentoriScreenshotCaptureTest.kt
|
|
3
|
+
// Run `node scripts/sync-native-core.mjs` after editing it.
|
|
1
4
|
// Phase 42 sub-F.10 — Robolectric coverage for SentoriScreenshotCapture.
|
|
2
5
|
//
|
|
3
6
|
// Run via Gradle on the Android host:
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// GENERATED MIRROR — do not edit.
|
|
2
|
+
// Source of truth: sdk/native/android/src/test/java/com/sentori/SentoriVerbsTest.kt
|
|
3
|
+
// Run `node scripts/sync-native-core.mjs` after editing it.
|
|
4
|
+
package com.sentori
|
|
5
|
+
|
|
6
|
+
import org.junit.After
|
|
7
|
+
import org.junit.Assert.assertEquals
|
|
8
|
+
import org.junit.Assert.assertNotNull
|
|
9
|
+
import org.junit.Assert.assertNull
|
|
10
|
+
import org.junit.Assert.assertTrue
|
|
11
|
+
import org.junit.Before
|
|
12
|
+
import org.junit.Test
|
|
13
|
+
import org.junit.runner.RunWith
|
|
14
|
+
import org.robolectric.RobolectricTestRunner
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The five verbs, and the contract an integrator is entitled to:
|
|
18
|
+
* synchronous, an id back every time, never a throw, and a no-op
|
|
19
|
+
* before [Sentori.start] rather than a crash.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately the same assertions as `SentoriVerbsTests.swift`. The
|
|
22
|
+
* dashboard cannot tell which platform sent an event and should not
|
|
23
|
+
* have to, so the two suites disagreeing is itself the bug.
|
|
24
|
+
*/
|
|
25
|
+
@RunWith(RobolectricTestRunner::class)
|
|
26
|
+
class SentoriVerbsTest {
|
|
27
|
+
|
|
28
|
+
@Before
|
|
29
|
+
fun setUp() {
|
|
30
|
+
SentoriTransport.resetForTests()
|
|
31
|
+
SentoriConfig.resetForTests()
|
|
32
|
+
SentoriScope.clear()
|
|
33
|
+
SentoriSignalRing.clear()
|
|
34
|
+
SentoriDevice.resetForTests()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
@After
|
|
38
|
+
fun tearDown() {
|
|
39
|
+
SentoriTransport.resetForTests()
|
|
40
|
+
SentoriConfig.resetForTests()
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
private fun started() {
|
|
44
|
+
// Port 9 is discard: nothing listens, so a send fails without
|
|
45
|
+
// waiting on DNS.
|
|
46
|
+
Sentori.start(
|
|
47
|
+
SentoriConfig(
|
|
48
|
+
token = "st_test",
|
|
49
|
+
ingestUrl = "http://127.0.0.1:9",
|
|
50
|
+
release = "app@1.0.0",
|
|
51
|
+
environment = "test",
|
|
52
|
+
)
|
|
53
|
+
)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private fun queued() = SentoriTransport.peekQueue()
|
|
57
|
+
|
|
58
|
+
@Test
|
|
59
|
+
fun everyVerbBeforeStartIsASilentNoOpThatStillReturnsAnId() {
|
|
60
|
+
val ids =
|
|
61
|
+
listOf(
|
|
62
|
+
Sentori.error(IllegalStateException("boom")),
|
|
63
|
+
Sentori.warn("checkout.slow"),
|
|
64
|
+
Sentori.trace("cart.opened"),
|
|
65
|
+
Sentori.assert("total.positive", false),
|
|
66
|
+
Sentori.probe("SEN-482"),
|
|
67
|
+
)
|
|
68
|
+
ids.forEach { assertEquals("every verb returns an event id", 36, it.length) }
|
|
69
|
+
assertTrue("and nothing is queued before init", queued().isEmpty())
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
@Test
|
|
73
|
+
fun errorCarriesTypeMessageStackAndTheSignalRing() {
|
|
74
|
+
started()
|
|
75
|
+
Sentori.pushSignal("nav", mapOf("to" to "/checkout"))
|
|
76
|
+
Sentori.error(IllegalStateException("boom"), mapOf("cartId" to "c_1"))
|
|
77
|
+
|
|
78
|
+
val e = queued().last()
|
|
79
|
+
assertEquals("error", e["kind"])
|
|
80
|
+
assertEquals("android", e["platform"])
|
|
81
|
+
assertEquals("app@1.0.0", e["release"])
|
|
82
|
+
|
|
83
|
+
@Suppress("UNCHECKED_CAST") val payload = e["payload"] as Map<String, Any?>
|
|
84
|
+
@Suppress("UNCHECKED_CAST") val err = payload["error"] as Map<String, Any?>
|
|
85
|
+
assertEquals("java.lang.IllegalStateException", err["type"])
|
|
86
|
+
assertEquals("boom", err["message"])
|
|
87
|
+
assertTrue("a stack with no frames symbolicates nothing", (err["stack"] as List<*>).isNotEmpty())
|
|
88
|
+
|
|
89
|
+
@Suppress("UNCHECKED_CAST") val data = payload["data"] as Map<String, Any?>
|
|
90
|
+
assertEquals("c_1", data["cartId"])
|
|
91
|
+
|
|
92
|
+
val signals = payload["signals"] as List<*>
|
|
93
|
+
assertEquals(1, signals.size)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
@Test
|
|
97
|
+
fun aPassingAssertIsNeverAnEvent() {
|
|
98
|
+
started()
|
|
99
|
+
Sentori.assert("total.positive", true)
|
|
100
|
+
Sentori.assert("total.positive", true)
|
|
101
|
+
assertTrue("passes aggregate; only failures are events", queued().isEmpty())
|
|
102
|
+
assertEquals(2, SentoriTransport.peekAssertStats().first()["passDelta"])
|
|
103
|
+
|
|
104
|
+
Sentori.assert("total.positive", false)
|
|
105
|
+
assertEquals(1, queued().size)
|
|
106
|
+
assertEquals("assert", queued().first()["kind"])
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
@Test
|
|
110
|
+
fun assertNeverStopsTheProgram() {
|
|
111
|
+
started()
|
|
112
|
+
Sentori.assert("impossible", false)
|
|
113
|
+
Sentori.assert("also impossible", false)
|
|
114
|
+
assertEquals("reached here, twice", 2, queued().size)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
@Test
|
|
118
|
+
fun quietTraceReachesTheRingAndNotTheQueue() {
|
|
119
|
+
started()
|
|
120
|
+
Sentori.trace("tick", quiet = true)
|
|
121
|
+
assertTrue("a quiet trace must stay affordable", queued().isEmpty())
|
|
122
|
+
assertEquals("but it is still context", 1, SentoriSignalRing.snapshot().size)
|
|
123
|
+
|
|
124
|
+
Sentori.trace("cart.opened")
|
|
125
|
+
assertEquals(1, queued().size)
|
|
126
|
+
assertEquals("cart.opened", queued().first()["name"])
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
@Test
|
|
130
|
+
fun userKeyRidesEventsOnlyAfterUserIsCalled() {
|
|
131
|
+
started()
|
|
132
|
+
Sentori.warn("before")
|
|
133
|
+
assertNull("no identity, no key — not an empty one", queued().last()["userKey"])
|
|
134
|
+
|
|
135
|
+
Sentori.user("usr_123", null)
|
|
136
|
+
Sentori.warn("after")
|
|
137
|
+
assertEquals(SentoriIdentity.hash("id", "usr_123"), queued().last()["userKey"])
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
@Test
|
|
141
|
+
fun contextMergesAndRidesEveryEvent() {
|
|
142
|
+
started()
|
|
143
|
+
Sentori.context(mapOf("tenant" to "acme"))
|
|
144
|
+
Sentori.context(mapOf("plan" to "pro"))
|
|
145
|
+
Sentori.probe("SEN-1")
|
|
146
|
+
|
|
147
|
+
@Suppress("UNCHECKED_CAST")
|
|
148
|
+
val payload = queued().last()["payload"] as Map<String, Any?>
|
|
149
|
+
@Suppress("UNCHECKED_CAST") val ctx = payload["context"] as Map<String, Any?>
|
|
150
|
+
assertEquals("acme", ctx["tenant"])
|
|
151
|
+
assertEquals("pro", ctx["plan"])
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@Test
|
|
155
|
+
fun eventIdsAreUuidV7AndSortByTime() {
|
|
156
|
+
val first = Sentori.newEventId()
|
|
157
|
+
Thread.sleep(10)
|
|
158
|
+
val second = Sentori.newEventId()
|
|
159
|
+
assertEquals(36, first.length)
|
|
160
|
+
assertEquals("version nibble", '7', first[14])
|
|
161
|
+
assertTrue("v7 ids sort by creation time as strings", first < second)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@Test
|
|
165
|
+
fun theQueueIsBoundedAndDropsOldestFirst() {
|
|
166
|
+
started()
|
|
167
|
+
// Not flushed: nothing drains to a dead port fast enough to
|
|
168
|
+
// race this, so it measures the cap rather than a timer.
|
|
169
|
+
repeat(2000) { Sentori.trace("t$it") }
|
|
170
|
+
assertTrue("an unbounded queue is a leak with a nicer name", queued().size <= 500)
|
|
171
|
+
assertEquals("t1999", queued().last()["name"])
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
@Test
|
|
175
|
+
fun garbageInDataDoesNotReachTheCaller() {
|
|
176
|
+
started()
|
|
177
|
+
val id =
|
|
178
|
+
Sentori.warn(
|
|
179
|
+
"bad",
|
|
180
|
+
mapOf("nan" to Double.NaN, "obj" to Any(), "nested" to mapOf("k" to Double.NaN)),
|
|
181
|
+
)
|
|
182
|
+
assertEquals(36, id.length)
|
|
183
|
+
assertNotNull(queued().last())
|
|
184
|
+
}
|
|
185
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// GENERATED MIRROR — do not edit.
|
|
2
|
+
// Source of truth: sdk/native/ios/Sources/Sentori/SentoriCrashHandler.swift
|
|
3
|
+
// Run `node scripts/sync-native-core.mjs` after editing it.
|
|
1
4
|
import Foundation
|
|
2
5
|
import MachO
|
|
3
6
|
|
|
@@ -64,6 +67,19 @@ import MachO
|
|
|
64
67
|
return UserDefaults.standard.dictionary(forKey: configKey) ?? [:]
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
/// Test-only seam onto `write`, which the real path reaches from
|
|
71
|
+
/// an `@convention(c)` handler an XCTest cannot raise safely.
|
|
72
|
+
///
|
|
73
|
+
/// The test that calls this was written against this name and has
|
|
74
|
+
/// never compiled: the sources were loose files in an Expo module
|
|
75
|
+
/// with no module to import, so nothing ever built the test target
|
|
76
|
+
/// and nothing reported that the member was missing. Internal
|
|
77
|
+
/// rather than public — `@testable import` reaches it, an app
|
|
78
|
+
/// does not.
|
|
79
|
+
internal static func persistForTesting(exception: NSException) {
|
|
80
|
+
write(exception: exception)
|
|
81
|
+
}
|
|
82
|
+
|
|
67
83
|
private static func write(exception: NSException) {
|
|
68
84
|
let cfg = config()
|
|
69
85
|
let release = (cfg["release"] as? String) ?? "unknown"
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// GENERATED MIRROR — do not edit.
|
|
2
|
+
// Source of truth: sdk/native/ios/Sources/Sentori/SentoriHangWatchdog.swift
|
|
3
|
+
// Run `node scripts/sync-native-core.mjs` after editing it.
|
|
1
4
|
import Foundation
|
|
2
5
|
|
|
3
6
|
/// iOS hang detector — mirrors the Android ANR watchdog (Phase 22 sub-D).
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// GENERATED MIRROR — do not edit.
|
|
2
|
+
// Source of truth: sdk/native/ios/Sources/Sentori/SentoriNativeExceptionBridge.swift
|
|
3
|
+
// Run `node scripts/sync-native-core.mjs` after editing it.
|
|
1
4
|
import Foundation
|
|
2
5
|
|
|
3
6
|
/// v0.9.5 #8 — partial-fix for the "TurboModule swallows NSException
|