@goliapkg/sentori-react-native 0.2.0 → 0.3.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.
@@ -0,0 +1,227 @@
1
+ package com.sentori
2
+
3
+ import android.content.Context
4
+ import android.os.Build
5
+ import android.os.Handler
6
+ import android.os.Looper
7
+ import org.json.JSONArray
8
+ import org.json.JSONObject
9
+ import java.io.File
10
+ import java.text.SimpleDateFormat
11
+ import java.util.Date
12
+ import java.util.Locale
13
+ import java.util.TimeZone
14
+ import java.util.UUID
15
+ import java.util.concurrent.atomic.AtomicBoolean
16
+
17
+ /**
18
+ * ANR (Application Not Responding) detector.
19
+ *
20
+ * Lightweight watchdog: a worker thread posts a tick onto the main
21
+ * Looper every `intervalMs` and sleeps for `timeoutMs`. If the main
22
+ * thread didn't acknowledge the tick within that window we capture the
23
+ * main thread's stack trace as a Sentori event with `kind = "anr"`
24
+ * and write it to the same pending dir SentoriCrashHandler uses, so
25
+ * the next-launch drain delivers it.
26
+ *
27
+ * Design notes:
28
+ * - Single-shot per ANR: once we report, we wait for the main
29
+ * thread to recover before re-arming. Otherwise a 30 s freeze
30
+ * would dump six events.
31
+ * - We DON'T kill the process or rethrow. The OS does that on its
32
+ * own ANR threshold (~5 s for input-driven, longer otherwise).
33
+ * - Worker thread is a daemon so it doesn't keep the process alive.
34
+ * - Disabled in debug builds by default — the JS debugger pauses
35
+ * the main thread routinely and we don't want a flood. The host
36
+ * app can override via `SentoriAnrWatchdog.start(ctx, force = true)`.
37
+ */
38
+ object SentoriAnrWatchdog {
39
+
40
+ private const val DEFAULT_TIMEOUT_MS = 5_000L
41
+ private const val DEFAULT_INTERVAL_MS = 1_000L
42
+ private const val PENDING_DIR_NAME = "sentori/pending"
43
+
44
+ @Volatile private var running = AtomicBoolean(false)
45
+ @Volatile private var thread: Thread? = null
46
+ @Volatile private var appCtx: Context? = null
47
+
48
+ /**
49
+ * Start the watchdog. Idempotent — calling start() twice is a
50
+ * no-op. Pass `force = true` to enable in debug builds.
51
+ */
52
+ @JvmStatic
53
+ @JvmOverloads
54
+ fun start(
55
+ context: Context,
56
+ timeoutMs: Long = DEFAULT_TIMEOUT_MS,
57
+ intervalMs: Long = DEFAULT_INTERVAL_MS,
58
+ force: Boolean = false,
59
+ ) {
60
+ if (!force && isDebuggable(context)) return
61
+ if (running.getAndSet(true)) return
62
+ appCtx = context.applicationContext
63
+
64
+ val mainHandler = Handler(Looper.getMainLooper())
65
+ val watchdogThread = Thread {
66
+ val tick = MainTick()
67
+ while (running.get()) {
68
+ tick.armed = true
69
+ mainHandler.post(tick)
70
+ try {
71
+ Thread.sleep(timeoutMs)
72
+ } catch (_: InterruptedException) {
73
+ return@Thread
74
+ }
75
+ if (tick.armed) {
76
+ // Main thread is wedged — capture once, then wait
77
+ // for the tick to land before arming again.
78
+ captureAnr()
79
+ while (running.get() && tick.armed) {
80
+ try {
81
+ Thread.sleep(intervalMs)
82
+ } catch (_: InterruptedException) {
83
+ return@Thread
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ watchdogThread.name = "Sentori-ANR-Watchdog"
90
+ watchdogThread.isDaemon = true
91
+ watchdogThread.start()
92
+ thread = watchdogThread
93
+ }
94
+
95
+ @JvmStatic
96
+ fun stop() {
97
+ running.set(false)
98
+ thread?.interrupt()
99
+ thread = null
100
+ }
101
+
102
+ private fun captureAnr() {
103
+ val ctx = appCtx ?: return
104
+ try {
105
+ val mainStack = Looper.getMainLooper().thread.stackTrace
106
+ val event = buildAnrEvent(ctx, mainStack)
107
+ val dir = File(ctx.filesDir, PENDING_DIR_NAME)
108
+ if (!dir.exists()) dir.mkdirs()
109
+ val file = File(dir, "${uuid()}.json")
110
+ file.writeText(event.toString())
111
+ } catch (_: Throwable) {
112
+ // never throw from inside the watchdog — losing one
113
+ // capture beats killing the worker thread.
114
+ }
115
+ }
116
+
117
+ private fun buildAnrEvent(ctx: Context, mainStack: Array<StackTraceElement>): JSONObject {
118
+ val cfg = configMap(ctx)
119
+ val release = cfg["release"] ?: "unknown"
120
+ val environment = cfg["environment"] ?: "prod"
121
+
122
+ val device = JSONObject().apply {
123
+ put("os", "android")
124
+ put("osVersion", Build.VERSION.RELEASE)
125
+ put("model", "${Build.MANUFACTURER} ${Build.MODEL}")
126
+ }
127
+ val app = JSONObject().apply {
128
+ put("version", appVersion(ctx))
129
+ put("build", appBuild(ctx))
130
+ }
131
+
132
+ val frames = JSONArray()
133
+ for (f in mainStack) {
134
+ frames.put(
135
+ JSONObject().apply {
136
+ put("function", "${f.className}.${f.methodName}")
137
+ put("file", f.fileName ?: "<unknown>")
138
+ put("line", f.lineNumber.coerceAtLeast(0))
139
+ put("inApp", isInApp(f.className))
140
+ },
141
+ )
142
+ }
143
+
144
+ val error = JSONObject().apply {
145
+ put("type", "ApplicationNotResponding")
146
+ put("message", "Main thread blocked for ≥ ${DEFAULT_TIMEOUT_MS} ms")
147
+ put("stack", frames)
148
+ put("cause", JSONObject.NULL)
149
+ }
150
+
151
+ return JSONObject().apply {
152
+ put("id", uuid())
153
+ put("timestamp", iso8601Now())
154
+ put("kind", "anr")
155
+ put("platform", "android")
156
+ put("release", release)
157
+ put("environment", environment)
158
+ put("device", device)
159
+ put("app", app)
160
+ put("user", JSONObject.NULL)
161
+ put("tags", JSONObject().apply { put("source", "sentori.anrWatchdog") })
162
+ put("breadcrumbs", JSONArray())
163
+ put("error", error)
164
+ put("fingerprint", JSONArray())
165
+ put("traceId", JSONObject.NULL)
166
+ put("spanId", JSONObject.NULL)
167
+ }
168
+ }
169
+
170
+ private fun configMap(ctx: Context): Map<String, String> {
171
+ val prefs = ctx.getSharedPreferences("sentori", Context.MODE_PRIVATE)
172
+ val out = mutableMapOf<String, String>()
173
+ for ((k, v) in prefs.all) if (v is String) out[k] = v
174
+ return out
175
+ }
176
+
177
+ private fun isInApp(cls: String): Boolean {
178
+ if (cls.startsWith("android.")) return false
179
+ if (cls.startsWith("androidx.")) return false
180
+ if (cls.startsWith("java.")) return false
181
+ if (cls.startsWith("javax.")) return false
182
+ if (cls.startsWith("kotlin.")) return false
183
+ if (cls.startsWith("kotlinx.")) return false
184
+ if (cls.startsWith("com.facebook.react.")) return false
185
+ if (cls.startsWith("com.android.")) return false
186
+ if (cls.startsWith("dalvik.")) return false
187
+ if (cls.startsWith("sun.")) return false
188
+ return true
189
+ }
190
+
191
+ private fun iso8601Now(): String {
192
+ val f = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US)
193
+ f.timeZone = TimeZone.getTimeZone("UTC")
194
+ return f.format(Date())
195
+ }
196
+
197
+ private fun uuid(): String = UUID.randomUUID().toString().lowercase(Locale.US)
198
+
199
+ private fun appVersion(ctx: Context): String =
200
+ try {
201
+ val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
202
+ pi.versionName ?: "0.0.0"
203
+ } catch (_: Throwable) {
204
+ "0.0.0"
205
+ }
206
+
207
+ private fun appBuild(ctx: Context): String =
208
+ try {
209
+ val pi = ctx.packageManager.getPackageInfo(ctx.packageName, 0)
210
+ pi.longVersionCode.toString()
211
+ } catch (_: Throwable) {
212
+ "0"
213
+ }
214
+
215
+ private fun isDebuggable(ctx: Context): Boolean =
216
+ (ctx.applicationInfo.flags and android.content.pm.ApplicationInfo.FLAG_DEBUGGABLE) != 0
217
+
218
+ /** Posted to the main thread every poll. `armed` is flipped to
219
+ * false once it actually runs. */
220
+ private class MainTick : Runnable {
221
+ @Volatile var armed = true
222
+
223
+ override fun run() {
224
+ armed = false
225
+ }
226
+ }
227
+ }
@@ -8,6 +8,8 @@ import expo.modules.kotlin.modules.ModuleDefinition
8
8
  * as the iOS module:
9
9
  * - setConfig({ token, release, environment })
10
10
  * - drainPending() -> List<String> (JSON bodies)
11
+ * - startAnrWatchdog({ timeoutMs?, intervalMs?, force? })
12
+ * - stopAnrWatchdog()
11
13
  */
12
14
  class SentoriModule : Module() {
13
15
  override fun definition() = ModuleDefinition {
@@ -26,6 +28,22 @@ class SentoriModule : Module() {
26
28
  SentoriCrashHandler.consumePending()
27
29
  }
28
30
 
31
+ // Watchdog is opt-in from JS so the host app picks the
32
+ // trade-off — stricter detection vs noise from the Metro
33
+ // debugger pausing the main thread. Pass `force: true` to
34
+ // run in debug builds.
35
+ Function("startAnrWatchdog") { options: Map<String, Any?>? ->
36
+ val ctx = appContext.reactContext ?: return@Function
37
+ val timeoutMs = (options?.get("timeoutMs") as? Number)?.toLong() ?: 5_000L
38
+ val intervalMs = (options?.get("intervalMs") as? Number)?.toLong() ?: 1_000L
39
+ val force = (options?.get("force") as? Boolean) ?: false
40
+ SentoriAnrWatchdog.start(ctx, timeoutMs, intervalMs, force)
41
+ }
42
+
43
+ Function("stopAnrWatchdog") {
44
+ SentoriAnrWatchdog.stop()
45
+ }
46
+
29
47
  // Dev-only helper — schedules an uncaught RuntimeException after
30
48
  // a tick so the JS bridge has time to return; the crash is then
31
49
  // captured by SentoriCrashHandler and written to
package/lib/index.d.ts CHANGED
@@ -13,6 +13,6 @@ export { init, init as initSentori } from './init';
13
13
  export { addBreadcrumb } from './breadcrumbs';
14
14
  export { setUser, getUser, captureError, captureException } from './capture';
15
15
  export { ErrorBoundary } from './error-boundary';
16
- export { triggerNativeCrash } from './native';
16
+ export { startAnrWatchdog, stopAnrWatchdog, triggerNativeCrash, } from './native';
17
17
  export type { Event, SentoriError, Frame, Breadcrumb, BreadcrumbType, Device, DeviceOS, App, User, Tags, EventKind, Platform, } from './types';
18
18
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,eAAO,MAAM,OAAO;;;;;;;;CAQnB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC;AAE9C,YAAY,EACV,KAAK,EACL,YAAY,EACZ,KAAK,EACL,UAAU,EACV,cAAc,EACd,MAAM,EACN,QAAQ,EACR,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,MAAM,SAAS,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,eAAO,MAAM,OAAO;;;;;;;;CAQnB,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC;AAElB,YAAY,EACV,KAAK,EACL,YAAY,EACZ,KAAK,EACL,UAAU,EACV,cAAc,EACd,MAAM,EACN,QAAQ,EACR,GAAG,EACH,IAAI,EACJ,IAAI,EACJ,SAAS,EACT,QAAQ,GACT,MAAM,SAAS,CAAC"}
package/lib/index.js CHANGED
@@ -16,5 +16,5 @@ export { init, init as initSentori } from './init';
16
16
  export { addBreadcrumb } from './breadcrumbs';
17
17
  export { setUser, getUser, captureError, captureException } from './capture';
18
18
  export { ErrorBoundary } from './error-boundary';
19
- export { triggerNativeCrash } from './native';
19
+ export { startAnrWatchdog, stopAnrWatchdog, triggerNativeCrash, } from './native';
20
20
  //# sourceMappingURL=index.js.map
package/lib/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI;IACJ,aAAa;IACb,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,aAAa;CACd,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EAAE,kBAAkB,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAEjD,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI;IACJ,aAAa;IACb,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,aAAa;CACd,CAAC;AAEF,eAAe,OAAO,CAAC;AAEvB,OAAO,EAAE,IAAI,EAAE,IAAI,IAAI,WAAW,EAAE,MAAM,QAAQ,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,WAAW,CAAC;AAC7E,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,MAAM,UAAU,CAAC"}
package/lib/native.d.ts CHANGED
@@ -20,4 +20,20 @@ export declare function drainNativePending(): Promise<string[]>;
20
20
  * No-op when the native module isn't installed (jest, bun test, web).
21
21
  */
22
22
  export declare function triggerNativeCrash(): void;
23
+ /**
24
+ * Phase 22 sub-D: start the Android ANR watchdog.
25
+ *
26
+ * startAnrWatchdog() // default 5s/1s, prod-only
27
+ * startAnrWatchdog({ force: true }) // include debug builds
28
+ * startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
29
+ *
30
+ * Returns silently on iOS / web / jest. iOS hang detection (sub-E)
31
+ * will hook the same JS function once landed.
32
+ */
33
+ export declare function startAnrWatchdog(options?: {
34
+ force?: boolean;
35
+ intervalMs?: number;
36
+ timeoutMs?: number;
37
+ }): void;
38
+ export declare function stopAnrWatchdog(): void;
23
39
  //# sourceMappingURL=native.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA4BH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,GAAG,IAAI,CAMP;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAQ5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAMzC"}
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAwCH,wBAAgB,eAAe,CAAC,MAAM,EAAE;IACtC,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,MAAM,CAAA;IACf,KAAK,EAAE,MAAM,CAAA;CACd,GAAG,IAAI,CAMP;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC,CAQ5D;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,IAAI,IAAI,CAMzC;AAED;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,CAAC,EAAE;IACzC,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,SAAS,CAAC,EAAE,MAAM,CAAA;CACnB,GAAG,IAAI,CAMP;AAED,wBAAgB,eAAe,IAAI,IAAI,CAMtC"}
package/lib/native.js CHANGED
@@ -53,4 +53,30 @@ export function triggerNativeCrash() {
53
53
  // never throw from a debugging helper
54
54
  }
55
55
  }
56
+ /**
57
+ * Phase 22 sub-D: start the Android ANR watchdog.
58
+ *
59
+ * startAnrWatchdog() // default 5s/1s, prod-only
60
+ * startAnrWatchdog({ force: true }) // include debug builds
61
+ * startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
62
+ *
63
+ * Returns silently on iOS / web / jest. iOS hang detection (sub-E)
64
+ * will hook the same JS function once landed.
65
+ */
66
+ export function startAnrWatchdog(options) {
67
+ try {
68
+ native()?.startAnrWatchdog?.(options);
69
+ }
70
+ catch {
71
+ // never throw from init helpers
72
+ }
73
+ }
74
+ export function stopAnrWatchdog() {
75
+ try {
76
+ native()?.stopAnrWatchdog?.();
77
+ }
78
+ catch {
79
+ // ignore
80
+ }
81
+ }
56
82
  //# sourceMappingURL=native.js.map
package/lib/native.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAaH,IAAI,OAA+C,CAAA;AAEnD,SAAS,MAAM;IACb,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,OAAO,CAAA;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAEvC,CAAA;QACD,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAsB,SAAS,CAAC,CAAA;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,IAAI,CAAA;IAChB,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAI/B;IACC,IAAI,CAAC;QACH,MAAM,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,sBAAsB;IACxB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,CAAC,GAAG,MAAM,EAAE,CAAA;IAClB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAA;IACjB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,CAAC,YAAY,EAAE,CAAA;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,EAAE,sBAAsB,EAAE,EAAE,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAyBH,IAAI,OAA+C,CAAA;AAEnD,SAAS,MAAM;IACb,IAAI,OAAO,KAAK,SAAS;QAAE,OAAO,OAAO,CAAA;IACzC,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,OAAO,CAAC,mBAAmB,CAEvC,CAAA;QACD,OAAO,GAAG,IAAI,CAAC,mBAAmB,CAAsB,SAAS,CAAC,CAAA;IACpE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,GAAG,IAAI,CAAA;IAChB,CAAC;IACD,OAAO,OAAO,CAAA;AAChB,CAAC;AAED,MAAM,UAAU,eAAe,CAAC,MAI/B;IACC,IAAI,CAAC;QACH,MAAM,EAAE,EAAE,SAAS,CAAC,MAAM,CAAC,CAAA;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,sBAAsB;IACxB,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,kBAAkB;IACtC,MAAM,CAAC,GAAG,MAAM,EAAE,CAAA;IAClB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAA;IACjB,IAAI,CAAC;QACH,OAAO,MAAM,CAAC,CAAC,YAAY,EAAE,CAAA;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAA;IACX,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,kBAAkB;IAChC,IAAI,CAAC;QACH,MAAM,EAAE,EAAE,sBAAsB,EAAE,EAAE,CAAA;IACtC,CAAC;IAAC,MAAM,CAAC;QACP,sCAAsC;IACxC,CAAC;AACH,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAIhC;IACC,IAAI,CAAC;QACH,MAAM,EAAE,EAAE,gBAAgB,EAAE,CAAC,OAAO,CAAC,CAAA;IACvC,CAAC;IAAC,MAAM,CAAC;QACP,gCAAgC;IAClC,CAAC;AACH,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,IAAI,CAAC;QACH,MAAM,EAAE,EAAE,eAAe,EAAE,EAAE,CAAA;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,SAAS;IACX,CAAC;AACH,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
@@ -64,6 +64,6 @@
64
64
  "access": "public"
65
65
  },
66
66
  "dependencies": {
67
- "@goliapkg/sentori-core": "0.1.0"
67
+ "@goliapkg/sentori-core": "0.2.0"
68
68
  }
69
69
  }
package/src/index.ts CHANGED
@@ -19,7 +19,11 @@ export { init, init as initSentori } from './init';
19
19
  export { addBreadcrumb } from './breadcrumbs';
20
20
  export { setUser, getUser, captureError, captureException } from './capture';
21
21
  export { ErrorBoundary } from './error-boundary';
22
- export { triggerNativeCrash } from './native';
22
+ export {
23
+ startAnrWatchdog,
24
+ stopAnrWatchdog,
25
+ triggerNativeCrash,
26
+ } from './native';
23
27
 
24
28
  export type {
25
29
  Event,
package/src/native.ts CHANGED
@@ -11,6 +11,18 @@ type SentoriNativeModule = {
11
11
  release: string
12
12
  token: string
13
13
  }) => void
14
+ /**
15
+ * Phase 22 sub-D: opt-in Android ANR watchdog. Posts a tick to the
16
+ * main looper every `intervalMs`; if not acknowledged within
17
+ * `timeoutMs`, captures the main-thread stack as an `anr` event.
18
+ * No-op on iOS today — iOS hang detection lands in sub-E.
19
+ */
20
+ startAnrWatchdog?: (options?: {
21
+ force?: boolean
22
+ intervalMs?: number
23
+ timeoutMs?: number
24
+ }) => void
25
+ stopAnrWatchdog?: () => void
14
26
  /** Dev-only — example app uses this to verify the crash flow. */
15
27
  triggerTestNativeCrash?: () => void
16
28
  }
@@ -69,3 +81,33 @@ export function triggerNativeCrash(): void {
69
81
  // never throw from a debugging helper
70
82
  }
71
83
  }
84
+
85
+ /**
86
+ * Phase 22 sub-D: start the Android ANR watchdog.
87
+ *
88
+ * startAnrWatchdog() // default 5s/1s, prod-only
89
+ * startAnrWatchdog({ force: true }) // include debug builds
90
+ * startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
91
+ *
92
+ * Returns silently on iOS / web / jest. iOS hang detection (sub-E)
93
+ * will hook the same JS function once landed.
94
+ */
95
+ export function startAnrWatchdog(options?: {
96
+ force?: boolean
97
+ intervalMs?: number
98
+ timeoutMs?: number
99
+ }): void {
100
+ try {
101
+ native()?.startAnrWatchdog?.(options)
102
+ } catch {
103
+ // never throw from init helpers
104
+ }
105
+ }
106
+
107
+ export function stopAnrWatchdog(): void {
108
+ try {
109
+ native()?.stopAnrWatchdog?.()
110
+ } catch {
111
+ // ignore
112
+ }
113
+ }