@goliapkg/sentori-react-native 6.1.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.
Files changed (46) hide show
  1. package/SentoriReactNative.podspec +11 -4
  2. package/android/build.gradle +15 -1
  3. package/android/src/main/java-core/com/sentori/Sentori.kt +244 -0
  4. package/android/src/main/{java → java-core}/com/sentori/SentoriAnrWatchdog.kt +3 -0
  5. package/android/src/main/java-core/com/sentori/SentoriConfig.kt +61 -0
  6. package/android/src/main/{java → java-core}/com/sentori/SentoriCrashHandler.kt +3 -0
  7. package/android/src/main/java-core/com/sentori/SentoriDevice.kt +61 -0
  8. package/android/src/main/{java → java-core}/com/sentori/SentoriFirebaseMessagingService.kt +3 -0
  9. package/android/src/main/{java → java-core}/com/sentori/SentoriForegroundActivity.kt +3 -0
  10. package/android/src/main/java-core/com/sentori/SentoriIdentity.kt +101 -0
  11. package/android/src/main/{java → java-core}/com/sentori/SentoriMobileVitals.kt +3 -0
  12. package/android/src/main/{java → java-core}/com/sentori/SentoriNativeExceptionBridge.kt +3 -0
  13. package/android/src/main/{java → java-core}/com/sentori/SentoriNativeSignals.kt +3 -0
  14. package/android/src/main/java-core/com/sentori/SentoriPush.kt +359 -0
  15. package/android/src/main/{java → java-core}/com/sentori/SentoriPushNotifications.kt +3 -0
  16. package/android/src/main/{java → java-core}/com/sentori/SentoriReplayCapture.kt +3 -0
  17. package/android/src/main/java-core/com/sentori/SentoriScope.kt +60 -0
  18. package/android/src/main/{java → java-core}/com/sentori/SentoriScreenshotCapture.kt +3 -0
  19. package/android/src/main/java-core/com/sentori/SentoriSignalRing.kt +80 -0
  20. package/android/src/main/java-core/com/sentori/SentoriTransport.kt +383 -0
  21. package/android/src/test/{java → java-core}/com/sentori/SentoriCrashHandlerTest.kt +3 -0
  22. package/android/src/test/java-core/com/sentori/SentoriIdentityTest.kt +107 -0
  23. package/android/src/test/java-core/com/sentori/SentoriLiveServerTest.kt +106 -0
  24. package/android/src/test/java-core/com/sentori/SentoriPushTest.kt +151 -0
  25. package/android/src/test/{java → java-core}/com/sentori/SentoriScreenshotCaptureTest.kt +3 -0
  26. package/android/src/test/java-core/com/sentori/SentoriVerbsTest.kt +185 -0
  27. package/ios/{SentoriCrashHandler.swift → core/SentoriCrashHandler.swift} +16 -0
  28. package/ios/{SentoriHangWatchdog.swift → core/SentoriHangWatchdog.swift} +3 -0
  29. package/ios/{SentoriMobileVitals.swift → core/SentoriMobileVitals.swift} +3 -0
  30. package/ios/{SentoriNativeExceptionBridge.swift → core/SentoriNativeExceptionBridge.swift} +3 -0
  31. package/ios/core/SentoriPush.swift +340 -0
  32. package/ios/{SentoriPushNotifications.swift → core/SentoriPushNotifications.swift} +42 -0
  33. package/ios/{SentoriReplayCapture.swift → core/SentoriReplayCapture.swift} +3 -0
  34. package/ios/{SentoriScreenshotCapture.swift → core/SentoriScreenshotCapture.swift} +14 -0
  35. package/ios/{SentoriThreadSampler.swift → core/SentoriThreadSampler.swift} +3 -0
  36. package/lib/push.d.ts +7 -6
  37. package/lib/push.d.ts.map +1 -1
  38. package/lib/push.js +8 -0
  39. package/lib/push.js.map +1 -1
  40. package/lib/transport.js +1 -1
  41. package/package.json +1 -1
  42. package/src/push.ts +14 -6
  43. package/src/transport.ts +1 -1
  44. package/ios/Tests/SentoriCrashHandlerTests.swift +0 -59
  45. package/ios/Tests/SentoriScreenshotCaptureTests.swift +0 -59
  46. 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/SentoriMobileVitals.swift
3
+ // Run `node scripts/sync-native-core.mjs` after editing it.
1
4
  import Foundation
2
5
  import UIKit
3
6
  import QuartzCore
@@ -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