@goliapkg/sentori-react-native 0.3.0 → 0.4.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,122 @@
1
+ # iOS Main Thread Sampler — Privacy & App Store Review Notes
2
+
3
+ > **Status:** Phase 29 sub-A step 1. Documents the Mach + pthread + dyld
4
+ > APIs the upcoming `SentoriThreadSampler.swift` will call, the App
5
+ > Store review risk for each, and the privacy boundary. Written before
6
+ > the Swift implementation lands so we can rule API choices in or out
7
+ > before writing code we'd later need to rip out.
8
+
9
+ ## Why we need this
10
+
11
+ `SentoriHangWatchdog.swift` currently captures
12
+ `Thread.callStackSymbols` from the watchdog thread itself, not from
13
+ main, because main is wedged when we want to sample it. The captured
14
+ stack therefore points into our own timer machinery, which is useless
15
+ for diagnosing the user's hang. (The file's own comment at line 100
16
+ flags this as a stop-gap.)
17
+
18
+ To get the actual wedged main-thread frames we have to walk a remote
19
+ thread's frame pointer chain — main is alive but not running our
20
+ code, so we resolve it via Mach + pthread APIs. This document checks
21
+ those APIs against App Store Review Guideline 2.5.1 ("Apps must use
22
+ only public APIs") before we commit to them.
23
+
24
+ ## API inventory
25
+
26
+ ### Public, App Store-safe
27
+
28
+ | API | Header | Purpose | Risk |
29
+ |---|---|---|---|
30
+ | `pthread_main_np()` | `<pthread.h>` | bool: am I on main? Sanity-check before sampling. | none |
31
+ | `pthread_self()` | `<pthread.h>` | get current pthread for `pthread_mach_thread_np`. | none |
32
+ | `pthread_mach_thread_np(pthread_t)` | `<pthread.h>` | pthread → mach port. The `_np` suffix is Apple's mark for "non-portable extension"; it's a public API, just not POSIX-portable. | none |
33
+ | `mach_task_self()` | `<mach/mach.h>` | this process's task port. We pass our own task only; we never look up another. | none |
34
+ | `thread_get_state(thread, ARM_THREAD_STATE64, ...)` | `<mach/thread_act.h>` | read main thread's PC / FP / SP / LR. Same call sentry-cocoa, Firebase Crashlytics, and Bugsnag use. | low — public Mach API; reviewers expect to see it from crash-reporter SDKs. |
35
+ | `vm_read_overwrite(self_task, addr, size, dst, ...)` | `<mach/vm_map.h>` | safe (no SIGSEGV) read of own-process memory. We restrict to `mach_task_self()` and small reads (each frame is two pointers). | low — public; flagged only when used to read other processes' memory. |
36
+ | `_dyld_image_count()` / `_dyld_get_image_header()` / `_dyld_get_image_vmaddr_slide()` / `_dyld_get_image_uuid()` | `<mach-o/dyld.h>` | LC_UUID for dSYM matching, ASLR slide for offset calc. Public dyld API. | none |
37
+
38
+ ### Explicitly NOT used (private API risk)
39
+
40
+ | API | Why we don't use it |
41
+ |---|---|
42
+ | `_pthread_main_thread_np` (underscore prefix) | private alias of `pthread_main_np()`; underscore prefix in Apple SDK = SPI, rejection-grade. |
43
+ | `task_threads(other_task, ...)` cross-task | requires `task-port` entitlement and is reviewer-flagged. We only ever look at our own task. |
44
+ | `task_for_pid` | gated by entitlement; not appropriate for our same-process use. |
45
+ | `__platform_call_*` / `kdebug_trace` / signal hooking | private. |
46
+ | `_dyld_register_func_for_*` introspection beyond UUID | not needed for stack walking. |
47
+
48
+ ## Why this is App Store safe
49
+
50
+ Direct prior art shipping the same call set, no review issues:
51
+
52
+ - **sentry-cocoa** — uses `thread_get_state` + `vm_read_overwrite` for
53
+ slow-frame and ANR sampling, in millions of apps.
54
+ - **Firebase Crashlytics** — same primitives for ANR + hang capture.
55
+ - **Bugsnag**, **Embrace** — likewise.
56
+ - **Apple's MetricKit** (`MXCallStackTree`) walks call stacks via the
57
+ same public Darwin primitives. Apple themselves consume this
58
+ surface.
59
+
60
+ Apple's stance: Review Guideline 2.5.1 forbids non-public APIs, but
61
+ "non-public" means undocumented / underscore-prefixed / not in the
62
+ public SDK. All calls in the public-safe table above are documented
63
+ in Apple's developer reference and shipped in `<mach/...>` /
64
+ `<pthread.h>` / `<mach-o/dyld.h>` headers that come with Xcode by
65
+ default.
66
+
67
+ ## Privacy considerations
68
+
69
+ What we capture per hang event:
70
+
71
+ - Up to 64 PC values (program counter addresses) from the main
72
+ thread's frame pointer chain.
73
+ - The `LC_UUID` of each loaded image and its ASLR vmaddr slide, so
74
+ the server can match PCs back to a dSYM (Phase 22 sub-B field).
75
+ - Hang duration in milliseconds (already captured today).
76
+
77
+ What we explicitly do NOT capture:
78
+
79
+ - Other processes' memory. We only pass `mach_task_self()` to
80
+ `vm_read_overwrite`.
81
+ - Function arguments, local variables, or register contents — PC
82
+ only, never the rest of the `arm_thread_state64_t` struct.
83
+ - Heap content, NSString contents, user-typed text, PII.
84
+ - Continuous-rate samples. Sampling fires only on a detected hang
85
+ (≥ 2s main-thread block) and is one-shot per hang (see watchdog
86
+ `reportedThisHang` flag at `SentoriHangWatchdog.swift:54`).
87
+
88
+ Symbolication happens **server-side** against the uploaded dSYM
89
+ (`server/src/symbolicate.rs`). On-device,
90
+ `frames[].instructionAddress` is an ASLR-slid pointer with no
91
+ semantic content until paired with the dSYM.
92
+
93
+ For the user-facing privacy doc (what data Sentori collects and why),
94
+ see `docs/legal/privacy.md`.
95
+
96
+ ## Rejection contingency
97
+
98
+ If Apple Review ever rejects with reference to `vm_read_overwrite` or
99
+ `thread_get_state`:
100
+
101
+ 1. Confirm sentry-cocoa / Firebase / Bugsnag are still shipping the
102
+ same call set. They're the canary; rejection there means a policy
103
+ change everyone needs to handle.
104
+ 2. Switch to `backtrace()` from `<execinfo.h>` — pure libc, walks
105
+ only the *current* thread, which is the watchdog thread, not
106
+ main. Lower fidelity (back where we started), zero risk.
107
+ 3. Last resort: ship without main-thread sampler; emit hang events
108
+ with empty `frames[]` and `tags.source = "sentori.hangWatchdog.no-sampler"`
109
+ so the dashboard can flag the gap. Feature degrades, doesn't
110
+ break.
111
+
112
+ ## Implementation references
113
+
114
+ - `sdk/react-native/ios/SentoriThreadSampler.swift` — to be created in
115
+ Phase 29 sub-A step 2: `captureMainThreadFrames(maxFrames: Int = 64) -> [(pc: UInt64, fp: UInt64)]`
116
+ - `sdk/react-native/ios/SentoriHangWatchdog.swift` — current
117
+ `Thread.callStackSymbols` capture (line 108) replaced by sampler
118
+ call in step 4.
119
+ - `server/src/symbolicate.rs` — dSYM lookup + frame resolution
120
+ (existing, Phase 22 sub-B).
121
+ - `docs/protocol.md` — `frames[].instructionAddress` + `debugId` +
122
+ `arch` field definitions (existing, Phase 22 sub-B).
@@ -0,0 +1,244 @@
1
+ import Foundation
2
+
3
+ /// iOS hang detector — mirrors the Android ANR watchdog (Phase 22 sub-D).
4
+ ///
5
+ /// A background thread posts a tick onto the main queue every
6
+ /// `intervalMs` and waits `timeoutMs` for it to run. If the main run
7
+ /// loop didn't drain the tick in time we capture the main thread's
8
+ /// call stack and write a Sentori event with `kind = "anr"` (the
9
+ /// dashboard already groups Android ANR + iOS hang under that kind —
10
+ /// the user-visible distinction lives in `tags.source`).
11
+ ///
12
+ /// Single-shot per hang: once we report, we wait for the tick to
13
+ /// land before re-arming so a 30-second freeze doesn't dump six
14
+ /// events. Daemon thread (DispatchSourceTimer in a background
15
+ /// queue) so it can't keep the process alive on shutdown.
16
+ ///
17
+ /// Disabled in DEBUG builds by default — the Xcode debugger
18
+ /// pauses the main thread routinely and we don't want a flood. The
19
+ /// host app can override via `start(force: true)`.
20
+ @objc public final class SentoriHangWatchdog: NSObject {
21
+
22
+ private static let pendingDirName = "sentori/pending"
23
+ private static let configKey = "com.sentori.config"
24
+
25
+ private static var running: Bool = false
26
+ private static var queue: DispatchQueue?
27
+ private static var timer: DispatchSourceTimer?
28
+ private static let lock = NSLock()
29
+
30
+ /// Start the watchdog. Idempotent. **Must be called from the main
31
+ /// thread** so the sampler can capture main's mach port; if called
32
+ /// from a background thread the sampler stays uninstalled and the
33
+ /// watchdog falls back to `Thread.callStackSymbols`.
34
+ @objc public static func start(timeoutMs: Int, intervalMs: Int, force: Bool) {
35
+ lock.lock()
36
+ defer { lock.unlock() }
37
+ if running { return }
38
+ if isDebug() && !force { return }
39
+
40
+ // Capture main's mach port for the Phase 29 sub-A sampler. No-op
41
+ // if we're not on main; sampler will then return [] and we
42
+ // gracefully fall back at capture time.
43
+ SentoriThreadSampler.installMainThreadHandle()
44
+
45
+ let q = DispatchQueue(label: "com.sentori.hangWatchdog", qos: .utility)
46
+ let t = DispatchSource.makeTimerSource(queue: q)
47
+ let interval = DispatchTimeInterval.milliseconds(intervalMs)
48
+ let timeoutNs = UInt64(max(0, timeoutMs)) * NSEC_PER_MSEC
49
+
50
+ // The "tick" path: post a Bool flip onto the main queue and
51
+ // record the time we did so. Read both fields in the worker
52
+ // tick to decide whether the main loop is alive.
53
+ let state = HangState()
54
+
55
+ t.schedule(deadline: .now() + interval, repeating: interval)
56
+ t.setEventHandler {
57
+ // If the previous tick is still pending after the timeout
58
+ // window, the main thread is wedged. Capture once, then
59
+ // hold off until the main loop catches up.
60
+ if state.armed.value, let armedAt = state.armedAt.value {
61
+ let elapsedNs = DispatchTime.now().uptimeNanoseconds &- armedAt.uptimeNanoseconds
62
+ if elapsedNs >= timeoutNs && !state.reportedThisHang.value {
63
+ state.reportedThisHang.value = true
64
+ captureHang(durationMs: Int(elapsedNs / NSEC_PER_MSEC))
65
+ }
66
+ return
67
+ }
68
+
69
+ // Main is responsive — re-arm.
70
+ state.armed.value = true
71
+ state.armedAt.value = DispatchTime.now()
72
+ state.reportedThisHang.value = false
73
+ DispatchQueue.main.async {
74
+ state.armed.value = false
75
+ state.armedAt.value = nil
76
+ }
77
+ }
78
+ t.resume()
79
+ timer = t
80
+ queue = q
81
+ running = true
82
+ }
83
+
84
+ @objc public static func stop() {
85
+ lock.lock()
86
+ defer { lock.unlock() }
87
+ timer?.cancel()
88
+ timer = nil
89
+ queue = nil
90
+ running = false
91
+ }
92
+
93
+ private static func isDebug() -> Bool {
94
+ #if DEBUG
95
+ return true
96
+ #else
97
+ return false
98
+ #endif
99
+ }
100
+
101
+ // MARK: - capture
102
+
103
+ private static func captureHang(durationMs: Int) {
104
+ let cfg = UserDefaults.standard.dictionary(forKey: configKey) ?? [:]
105
+ let release = (cfg["release"] as? String) ?? "unknown"
106
+ let environment = (cfg["environment"] as? String) ?? "prod"
107
+
108
+ // Phase 29 sub-A: try the Mach-based main-thread sampler first.
109
+ // It walks main's frame pointer chain via thread_get_state +
110
+ // vm_read_overwrite (see PRIVACY_AND_REVIEW.md). Returns [] on
111
+ // non-arm64 platforms or if installMainThreadHandle was never
112
+ // called from main; we then fall back to this thread's own
113
+ // stack — biased toward dispatch machinery but better than
114
+ // nothing.
115
+ let pcs = SentoriThreadSampler.captureMainThreadFrames(maxFrames: 64)
116
+ let frames: [[String: Any]]
117
+ let stackSource: String
118
+ if !pcs.isEmpty {
119
+ frames = pcs.map { pc -> [String: Any] in
120
+ return [
121
+ "function": "<unsymbolicated>",
122
+ "file": "<unknown>",
123
+ "line": 0,
124
+ "instructionAddress": String(format: "0x%llx", pc.uint64Value),
125
+ "arch": "arm64",
126
+ "inApp": true,
127
+ ]
128
+ }
129
+ stackSource = "sentori.hangWatchdog.sampler"
130
+ } else {
131
+ frames = Thread.callStackSymbols.map { sym -> [String: Any] in
132
+ let parts = sym.split(
133
+ separator: " ", omittingEmptySubsequences: true
134
+ ).map(String.init)
135
+ let module = parts.count > 1 ? parts[1] : "<unknown>"
136
+ let function =
137
+ parts.count > 3
138
+ ? parts.dropFirst(3).joined(separator: " ")
139
+ : "<anonymous>"
140
+ return [
141
+ "function": function,
142
+ "file": module,
143
+ "line": 0,
144
+ "inApp": !module.contains("UIKit")
145
+ && !module.contains("Foundation")
146
+ && !module.contains("CoreFoundation")
147
+ && !module.contains("libsystem")
148
+ && !module.contains("libobjc"),
149
+ ]
150
+ }
151
+ stackSource = "sentori.hangWatchdog.no-sampler"
152
+ }
153
+
154
+ let event: [String: Any] = [
155
+ "id": UUID().uuidString.lowercased(),
156
+ "timestamp": iso8601(Date()),
157
+ "kind": "anr",
158
+ "platform": "ios",
159
+ "release": release,
160
+ "environment": environment,
161
+ "device": [
162
+ "os": "ios",
163
+ "osVersion": osVersion(),
164
+ "model": deviceModel(),
165
+ ],
166
+ "app": appInfo(),
167
+ "user": NSNull(),
168
+ "tags": ["source": stackSource],
169
+ "breadcrumbs": [Any](),
170
+ "error": [
171
+ "type": "ApplicationNotResponding",
172
+ "message": "Main thread blocked for ≥ \(durationMs) ms",
173
+ "stack": frames,
174
+ "cause": NSNull(),
175
+ ],
176
+ "fingerprint": [String](),
177
+ "traceId": NSNull(),
178
+ "spanId": NSNull(),
179
+ ]
180
+
181
+ guard
182
+ let docs = FileManager.default.urls(
183
+ for: .documentDirectory, in: .userDomainMask
184
+ ).first
185
+ else { return }
186
+ let dir = docs.appendingPathComponent(pendingDirName)
187
+ try? FileManager.default.createDirectory(
188
+ at: dir, withIntermediateDirectories: true)
189
+ let url = dir.appendingPathComponent(
190
+ "\(UUID().uuidString.lowercased()).json")
191
+ if let data = try? JSONSerialization.data(
192
+ withJSONObject: event, options: [])
193
+ {
194
+ try? data.write(to: url)
195
+ }
196
+ }
197
+
198
+ private static func iso8601(_ date: Date) -> String {
199
+ let f = ISO8601DateFormatter()
200
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
201
+ return f.string(from: date)
202
+ }
203
+
204
+ private static func osVersion() -> String {
205
+ let v = ProcessInfo.processInfo.operatingSystemVersion
206
+ return "\(v.majorVersion).\(v.minorVersion).\(v.patchVersion)"
207
+ }
208
+
209
+ private static func deviceModel() -> String {
210
+ var s = utsname()
211
+ uname(&s)
212
+ return withUnsafePointer(to: &s.machine) { ptr in
213
+ ptr.withMemoryRebound(to: CChar.self, capacity: 1) {
214
+ String(validatingUTF8: $0) ?? "unknown"
215
+ }
216
+ }
217
+ }
218
+
219
+ private static func appInfo() -> [String: Any] {
220
+ let info = Bundle.main.infoDictionary ?? [:]
221
+ var d: [String: Any] = [
222
+ "version": (info["CFBundleShortVersionString"] as? String)
223
+ ?? "0.0.0"
224
+ ]
225
+ if let build = info["CFBundleVersion"] as? String {
226
+ d["build"] = build
227
+ }
228
+ return d
229
+ }
230
+ }
231
+
232
+ /// Mutable boxes shared between the watchdog tick and the main-queue
233
+ /// ack. Class so the closures can mutate without `inout` and so the
234
+ /// references survive across the `setEventHandler` capture.
235
+ private final class HangState {
236
+ let armed = Box(false)
237
+ let armedAt = Box<DispatchTime?>(nil)
238
+ let reportedThisHang = Box(false)
239
+ }
240
+
241
+ private final class Box<T> {
242
+ var value: T
243
+ init(_ v: T) { self.value = v }
244
+ }
@@ -24,6 +24,25 @@ public class SentoriModule: Module {
24
24
  return SentoriCrashHandler.consumePending()
25
25
  }
26
26
 
27
+ // Phase 22 sub-E: opt-in iOS hang watchdog. Same JS function
28
+ // name as Android (sub-D) so the host app calls
29
+ // `startAnrWatchdog(...)` once, both platforms react.
30
+ // Defaults: 2 s timeout, 1 s tick interval, debug-build off.
31
+ Function("startAnrWatchdog") { (options: [String: Any]?) in
32
+ let timeoutMs = (options?["timeoutMs"] as? Int) ?? 2000
33
+ let intervalMs = (options?["intervalMs"] as? Int) ?? 1000
34
+ let force = (options?["force"] as? Bool) ?? false
35
+ SentoriHangWatchdog.start(
36
+ timeoutMs: timeoutMs,
37
+ intervalMs: intervalMs,
38
+ force: force
39
+ )
40
+ }
41
+
42
+ Function("stopAnrWatchdog") {
43
+ SentoriHangWatchdog.stop()
44
+ }
45
+
27
46
  // Dev-only helper used by the example app to verify the
28
47
  // crash-write / drain round-trip without writing native code in
29
48
  // the host app. Schedules a real NSException after a tick so
@@ -0,0 +1,149 @@
1
+ import Foundation
2
+
3
+ /// Captures the main thread's program-counter chain for hang reporting.
4
+ ///
5
+ /// Phase 29 sub-A. Used by `SentoriHangWatchdog` to fill an `anr` event's
6
+ /// `frames[].instructionAddress`. Server-side symbolicates against the
7
+ /// uploaded dSYM (Phase 22 sub-B). API surface and App Store review notes
8
+ /// are in `PRIVACY_AND_REVIEW.md`.
9
+ ///
10
+ /// arm64-only. On non-arm64 (Intel Mac simulators) `captureMainThreadFrames`
11
+ /// returns an empty array; the watchdog falls back to its previous
12
+ /// `Thread.callStackSymbols` path.
13
+ @objc public final class SentoriThreadSampler: NSObject {
14
+
15
+ /// Mach port for the main thread, captured once at SDK init time from
16
+ /// main. 0 = uninstalled (or non-arm64 platform).
17
+ private static var mainThreadHandle: thread_t = 0
18
+
19
+ /// Captures the main thread's mach port. Must be called from the main
20
+ /// thread, exactly once during SDK init. Idempotent — second + later
21
+ /// calls are no-ops.
22
+ @objc public static func installMainThreadHandle() {
23
+ guard pthread_main_np() != 0 else { return }
24
+ if mainThreadHandle != 0 { return }
25
+ mainThreadHandle = pthread_mach_thread_np(pthread_self())
26
+ }
27
+
28
+ /// Walks the main thread's frame pointer chain and returns up to
29
+ /// `maxFrames` PCs.
30
+ ///
31
+ /// - PC[0] = current main-thread PC (from `thread_get_state`).
32
+ /// - PC[i>0] = saved LR (return address) `i` frames up the stack.
33
+ ///
34
+ /// Returns an empty array if:
35
+ /// * the main handle isn't installed (caller forgot
36
+ /// `installMainThreadHandle` from main),
37
+ /// * the platform isn't arm64,
38
+ /// * `thread_get_state` or `vm_read_overwrite` fails,
39
+ /// * the caller is itself on main (we don't sample our own thread).
40
+ ///
41
+ /// Bridged to Objective-C as `[NSNumber]` of unsigned 64-bit PCs.
42
+ @objc public static func captureMainThreadFrames(maxFrames: Int = 64) -> [NSNumber] {
43
+ #if arch(arm64)
44
+ guard mainThreadHandle != 0, maxFrames > 0 else { return [] }
45
+ // Don't sample ourselves — we'd race with our own register state.
46
+ if pthread_main_np() != 0 { return [] }
47
+
48
+ var state = arm_thread_state64_t()
49
+ // ARM_THREAD_STATE64_COUNT is a C macro that doesn't make the
50
+ // Swift import; compute it the same way the macro does
51
+ // (sizeof(arm_thread_state64_t) / sizeof(uint32_t)).
52
+ var stateCount = mach_msg_type_number_t(
53
+ MemoryLayout<arm_thread_state64_t>.size / MemoryLayout<UInt32>.size
54
+ )
55
+
56
+ let kr: kern_return_t = withUnsafeMutablePointer(to: &state) { sp in
57
+ sp.withMemoryRebound(to: natural_t.self, capacity: Int(stateCount)) { ptr in
58
+ thread_get_state(
59
+ mainThreadHandle,
60
+ ARM_THREAD_STATE64,
61
+ ptr,
62
+ &stateCount
63
+ )
64
+ }
65
+ }
66
+ guard kr == KERN_SUCCESS else { return [] }
67
+
68
+ // The intrinsics __darwin_arm_thread_state64_get_{pc,fp} also
69
+ // don't import cleanly into Swift. Reinterpret the struct as
70
+ // a UInt64 array and pick out registers by ABI index, which is
71
+ // identical between arm64 and arm64e (only the field names
72
+ // differ; the raw byte layout is the same):
73
+ // __x[0..28] = indices 0..28
74
+ // __fp = index 29
75
+ // __lr = index 30
76
+ // __sp = index 31
77
+ // __pc = index 32
78
+ // __cpsr / __pad = index 33 (packed)
79
+ // PAC bits, if any (arm64e), are stripped by stripPAC below.
80
+ let regs = withUnsafeBytes(of: state) { raw -> (pc: UInt64, fp: UInt64) in
81
+ let base = raw.baseAddress!.assumingMemoryBound(to: UInt64.self)
82
+ return (pc: base[32], fp: base[29])
83
+ }
84
+
85
+ var frames: [UInt64] = []
86
+ if regs.pc != 0 {
87
+ frames.append(stripPAC(regs.pc))
88
+ }
89
+
90
+ var fp = regs.fp
91
+ let task = mach_task_self_
92
+
93
+ while frames.count < maxFrames && fp != 0 {
94
+ // ARM64 frame layout (per AAPCS64): [saved fp][saved lr] at
95
+ // offsets 0 and 8 from the current frame pointer.
96
+ var nextFP: UInt64 = 0
97
+ var savedLR: UInt64 = 0
98
+
99
+ guard readWord(task: task, addr: fp, into: &nextFP) else { break }
100
+ guard readWord(task: task, addr: fp &+ 8, into: &savedLR) else { break }
101
+
102
+ // Sanity: FP must walk up the user stack (always increasing).
103
+ // Bail on chain corruption; still return what we have.
104
+ if nextFP <= fp { break }
105
+
106
+ frames.append(stripPAC(savedLR))
107
+ fp = nextFP
108
+ }
109
+
110
+ return frames.map { NSNumber(value: $0) }
111
+ #else
112
+ return []
113
+ #endif
114
+ }
115
+
116
+ // MARK: - arm64 helpers
117
+
118
+ #if arch(arm64)
119
+ /// Strips the pointer authentication code (PAC) from a saved LR.
120
+ /// On arm64 (non-arm64e) PAC bits aren't set; this is a no-op mask.
121
+ /// On arm64e stack-saved LRs may carry a signing tag in the high
122
+ /// bits; user-space PCs fit in the low 47 bits, so we mask above.
123
+ private static func stripPAC(_ pc: UInt64) -> UInt64 {
124
+ return pc & 0x0000_007F_FFFF_FFFF
125
+ }
126
+
127
+ /// `vm_read_overwrite` wrapper: read 8 bytes from `addr` in the given
128
+ /// task into `dst`. Returns true iff the read succeeded with a full
129
+ /// word transferred.
130
+ private static func readWord(
131
+ task: mach_port_t,
132
+ addr: UInt64,
133
+ into dst: inout UInt64
134
+ ) -> Bool {
135
+ var bytesRead: vm_size_t = 0
136
+ return withUnsafeMutablePointer(to: &dst) { ptr -> Bool in
137
+ let dstAddr = vm_address_t(UInt(bitPattern: UnsafeMutableRawPointer(ptr)))
138
+ let kr = vm_read_overwrite(
139
+ task,
140
+ vm_address_t(addr),
141
+ vm_size_t(MemoryLayout<UInt64>.size),
142
+ dstAddr,
143
+ &bytesRead
144
+ )
145
+ return kr == KERN_SUCCESS && bytesRead == MemoryLayout<UInt64>.size
146
+ }
147
+ }
148
+ #endif
149
+ }
@@ -0,0 +1,96 @@
1
+ // Phase 29 sub-A step 3: XCTest coverage for SentoriThreadSampler.
2
+ //
3
+ // Run via Xcode (target → SentoriTests, ⌘U) or via xcodebuild from the
4
+ // iOS host:
5
+ // xcodebuild test \
6
+ // -scheme SentoriTests \
7
+ // -destination 'platform=iOS Simulator,name=iPhone 15'
8
+ //
9
+ // On Apple Silicon Mac the simulator runs the arm64 slice and the
10
+ // sampler can walk frames; on Intel Mac the simulator slice is x86_64
11
+ // and the sampler returns []. Both paths are asserted below.
12
+
13
+ import XCTest
14
+
15
+ @testable import SentoriCrashHandler
16
+
17
+ final class SentoriThreadSamplerTests: XCTestCase {
18
+
19
+ override func setUp() {
20
+ super.setUp()
21
+ // Capture the main pthread → mach port mapping. setUp runs on
22
+ // the test runner's main queue, which is the main thread.
23
+ SentoriThreadSampler.installMainThreadHandle()
24
+ }
25
+
26
+ /// Background → sampler → main: should collect a non-trivial frame
27
+ /// chain on arm64 simulators.
28
+ func testCaptureFromBackgroundReturnsAtLeastFiveFrames() {
29
+ let exp = expectation(description: "background sample")
30
+ DispatchQueue.global(qos: .userInitiated).async {
31
+ let frames = SentoriThreadSampler.captureMainThreadFrames(maxFrames: 64)
32
+ #if arch(arm64)
33
+ XCTAssertGreaterThanOrEqual(
34
+ frames.count, 5,
35
+ "expected ≥ 5 main-thread frames on arm64; got \(frames.count)"
36
+ )
37
+ if !frames.isEmpty {
38
+ XCTAssertGreaterThan(
39
+ frames[0].uint64Value, 0,
40
+ "first PC must be non-zero"
41
+ )
42
+ }
43
+ XCTAssertLessThanOrEqual(
44
+ frames.count, 64,
45
+ "must respect maxFrames cap"
46
+ )
47
+ #else
48
+ // Intel simulator: sampler returns empty by design.
49
+ XCTAssertEqual(frames.count, 0)
50
+ #endif
51
+ exp.fulfill()
52
+ }
53
+ wait(for: [exp], timeout: 5.0)
54
+ }
55
+
56
+ /// Sampling from main itself must refuse — would race with our own
57
+ /// register state.
58
+ func testCaptureFromMainReturnsEmpty() {
59
+ let frames = SentoriThreadSampler.captureMainThreadFrames(maxFrames: 64)
60
+ XCTAssertEqual(
61
+ frames.count, 0,
62
+ "sampling from main must return [] (would race with own state)"
63
+ )
64
+ }
65
+
66
+ /// `installMainThreadHandle` must be safe to call repeatedly.
67
+ func testInstallIsIdempotent() {
68
+ SentoriThreadSampler.installMainThreadHandle()
69
+ SentoriThreadSampler.installMainThreadHandle()
70
+ SentoriThreadSampler.installMainThreadHandle()
71
+
72
+ let exp = expectation(description: "still works after re-install")
73
+ DispatchQueue.global().async {
74
+ let frames = SentoriThreadSampler.captureMainThreadFrames(maxFrames: 16)
75
+ #if arch(arm64)
76
+ XCTAssertGreaterThan(
77
+ frames.count, 0,
78
+ "re-installs must not break the captured handle"
79
+ )
80
+ #endif
81
+ exp.fulfill()
82
+ }
83
+ wait(for: [exp], timeout: 2.0)
84
+ }
85
+
86
+ /// `maxFrames: 0` returns empty even on arm64.
87
+ func testZeroMaxFramesReturnsEmpty() {
88
+ let exp = expectation(description: "zero max")
89
+ DispatchQueue.global().async {
90
+ let frames = SentoriThreadSampler.captureMainThreadFrames(maxFrames: 0)
91
+ XCTAssertEqual(frames.count, 0)
92
+ exp.fulfill()
93
+ }
94
+ wait(for: [exp], timeout: 2.0)
95
+ }
96
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAoC,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAI5E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,IAAI,GAAG,IAAI,KAAG,IAE3C,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,IAAI,GAAG,IAAa,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,KAAK,EAAE,SAAS,aAAa,KAAG,IAsBnE,CAAC;AAEF,eAAO,MAAM,gBAAgB,UAxBO,KAAK,WAAW,aAAa,KAAG,IAwBxB,CAAC"}
1
+ {"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAoC,IAAI,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAI5E;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,OAAO,GAAI,MAAM,IAAI,GAAG,IAAI,KAAG,IAE3C,CAAC;AAEF,eAAO,MAAM,OAAO,QAAO,IAAI,GAAG,IAAa,CAAC;AAEhD,MAAM,MAAM,aAAa,GAAG;IAC1B,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,IAAI,CAAC,EAAE,IAAI,CAAC;IACZ,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB,CAAC;AAEF,eAAO,MAAM,YAAY,GAAI,OAAO,KAAK,EAAE,SAAS,aAAa,KAAG,IAyBnE,CAAC;AAEF,eAAO,MAAM,gBAAgB,UA3BO,KAAK,WAAW,aAAa,KAAG,IA2BxB,CAAC"}
package/lib/capture.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getConfig, isInitialized } from './config';
2
2
  import { getBreadcrumbs } from './breadcrumbs';
3
+ import { markSessionErrored } from './session-tracker';
3
4
  import { parseStack } from './stack';
4
5
  import { enqueue } from './transport';
5
6
  import { uuidV7 } from './uuid';
@@ -41,6 +42,9 @@ export const captureError = (error, extras) => {
41
42
  error: errorToObject(error),
42
43
  fingerprint: extras?.fingerprint,
43
44
  };
45
+ // Phase 26 sub-B: a captured error promotes the current session to
46
+ // `errored` so the next AppState=background ping reports unhealthy.
47
+ markSessionErrored();
44
48
  enqueue(event);
45
49
  };
46
50
  export const captureException = captureError;
@@ -1 +1 @@
1
- {"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"capture.js","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare const installLifecycleHandler: () => void;
2
+ export declare const __uninstallLifecycleForTests: () => void;
3
+ //# sourceMappingURL=lifecycle.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.d.ts","sourceRoot":"","sources":["../../src/handlers/lifecycle.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,uBAAuB,QAAO,IAqB1C,CAAC;AAEF,eAAO,MAAM,4BAA4B,QAAO,IAI/C,CAAC"}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Phase 26 sub-B: AppState binding.
3
+ *
4
+ * Subscribes to AppState transitions:
5
+ * - active → start a fresh session (after a previous background end)
6
+ * - background / inactive → end the current session
7
+ *
8
+ * RN's AppState fires `inactive` on iOS during multitasking peek; we
9
+ * end on it because that's effectively a background and the user may
10
+ * not return. If they do, `active` starts a new one — the on-the-wire
11
+ * session count goes up by one, which matches "the user opened the app
12
+ * twice". Sentry historically did the opposite (treat inactive as
13
+ * still alive), but that lets a swiped-away session never end.
14
+ */
15
+ import { endSession, startSession } from '../session-tracker';
16
+ let _installed = false;
17
+ let _subscription = null;
18
+ export const installLifecycleHandler = () => {
19
+ if (_installed)
20
+ return;
21
+ _installed = true;
22
+ let AppState;
23
+ try {
24
+ // RN ships AppState; in test / non-RN host the require throws and
25
+ // we silently no-op.
26
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
27
+ AppState = require('react-native').AppState;
28
+ }
29
+ catch {
30
+ return;
31
+ }
32
+ if (!AppState || typeof AppState.addEventListener !== 'function')
33
+ return;
34
+ _subscription = AppState.addEventListener('change', (state) => {
35
+ if (state === 'active') {
36
+ startSession();
37
+ }
38
+ else if (state === 'background' || state === 'inactive') {
39
+ endSession();
40
+ }
41
+ });
42
+ };
43
+ export const __uninstallLifecycleForTests = () => {
44
+ _subscription?.remove();
45
+ _subscription = null;
46
+ _installed = false;
47
+ };
48
+ //# sourceMappingURL=lifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lifecycle.js","sourceRoot":"","sources":["../../src/handlers/lifecycle.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAE9D,IAAI,UAAU,GAAG,KAAK,CAAC;AACvB,IAAI,aAAa,GAAkC,IAAI,CAAC;AASxD,MAAM,CAAC,MAAM,uBAAuB,GAAG,GAAS,EAAE;IAChD,IAAI,UAAU;QAAE,OAAO;IACvB,UAAU,GAAG,IAAI,CAAC;IAClB,IAAI,QAAkC,CAAC;IACvC,IAAI,CAAC;QACH,kEAAkE;QAClE,qBAAqB;QACrB,iEAAiE;QACjE,QAAQ,GAAI,OAAO,CAAC,cAAc,CAAiC,CAAC,QAAQ,CAAC;IAC/E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IACD,IAAI,CAAC,QAAQ,IAAI,OAAO,QAAQ,CAAC,gBAAgB,KAAK,UAAU;QAAE,OAAO;IAEzE,aAAa,GAAG,QAAQ,CAAC,gBAAgB,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,EAAE;QAC5D,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;YACvB,YAAY,EAAE,CAAC;QACjB,CAAC;aAAM,IAAI,KAAK,KAAK,YAAY,IAAI,KAAK,KAAK,UAAU,EAAE,CAAC;YAC1D,UAAU,EAAE,CAAC;QACf,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,4BAA4B,GAAG,GAAS,EAAE;IACrD,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,aAAa,GAAG,IAAI,CAAC;IACrB,UAAU,GAAG,KAAK,CAAC;AACrB,CAAC,CAAC"}
package/lib/index.d.ts CHANGED
@@ -7,6 +7,9 @@ export declare const sentori: {
7
7
  captureError: (error: Error, extras?: import("./capture").CaptureExtras) => void;
8
8
  captureException: (error: Error, extras?: import("./capture").CaptureExtras) => void;
9
9
  ErrorBoundary: typeof ErrorBoundary;
10
+ startSession: () => void;
11
+ endSession: (status?: "exited") => void;
12
+ markSessionCrashed: () => void;
10
13
  };
11
14
  export default sentori;
12
15
  export { init, init as initSentori } from './init';
@@ -14,5 +17,6 @@ export { addBreadcrumb } from './breadcrumbs';
14
17
  export { setUser, getUser, captureError, captureException } from './capture';
15
18
  export { ErrorBoundary } from './error-boundary';
16
19
  export { startAnrWatchdog, stopAnrWatchdog, triggerNativeCrash, } from './native';
20
+ export { endSession, markSessionCrashed, startSession, } from './session-tracker';
17
21
  export type { Event, SentoriError, Frame, Breadcrumb, BreadcrumbType, Device, DeviceOS, App, User, Tags, EventKind, Platform, } from './types';
18
22
  //# 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,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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAOjD,eAAO,MAAM,OAAO;;;;;;;;;;;CAWnB,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;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,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
@@ -2,6 +2,7 @@ import { init } from './init';
2
2
  import { addBreadcrumb } from './breadcrumbs';
3
3
  import { setUser, getUser, captureError, captureException } from './capture';
4
4
  import { ErrorBoundary } from './error-boundary';
5
+ import { endSession, markSessionCrashed, startSession, } from './session-tracker';
5
6
  export const sentori = {
6
7
  init,
7
8
  addBreadcrumb,
@@ -10,6 +11,9 @@ export const sentori = {
10
11
  captureError,
11
12
  captureException,
12
13
  ErrorBoundary,
14
+ startSession,
15
+ endSession,
16
+ markSessionCrashed,
13
17
  };
14
18
  export default sentori;
15
19
  export { init, init as initSentori } from './init';
@@ -17,4 +21,5 @@ export { addBreadcrumb } from './breadcrumbs';
17
21
  export { setUser, getUser, captureError, captureException } from './capture';
18
22
  export { ErrorBoundary } from './error-boundary';
19
23
  export { startAnrWatchdog, stopAnrWatchdog, triggerNativeCrash, } from './native';
24
+ export { endSession, markSessionCrashed, startSession, } from './session-tracker';
20
25
  //# 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,EACL,gBAAgB,EAChB,eAAe,EACf,kBAAkB,GACnB,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;AACjD,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC;AAE3B,MAAM,CAAC,MAAM,OAAO,GAAG;IACrB,IAAI;IACJ,aAAa;IACb,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,aAAa;IACb,YAAY;IACZ,UAAU;IACV,kBAAkB;CACnB,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;AAClB,OAAO,EACL,UAAU,EACV,kBAAkB,EAClB,YAAY,GACb,MAAM,mBAAmB,CAAC"}
package/lib/native.d.ts CHANGED
@@ -21,14 +21,15 @@ export declare function drainNativePending(): Promise<string[]>;
21
21
  */
22
22
  export declare function triggerNativeCrash(): void;
23
23
  /**
24
- * Phase 22 sub-D: start the Android ANR watchdog.
24
+ * Phase 22 sub-D / sub-E: cross-platform main-thread watchdog.
25
+ * Single JS call covers both Android ANR and iOS hang detection.
25
26
  *
26
- * startAnrWatchdog() // default 5s/1s, prod-only
27
+ * startAnrWatchdog() // platform defaults, prod-only
27
28
  * startAnrWatchdog({ force: true }) // include debug builds
28
29
  * startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
29
30
  *
30
- * Returns silently on iOS / web / jest. iOS hang detection (sub-E)
31
- * will hook the same JS function once landed.
31
+ * Defaults: Android 5 s / 1 s tick; iOS 2 s / 1 s tick. Returns
32
+ * silently on web / jest / unsupported runtimes.
32
33
  */
33
34
  export declare function startAnrWatchdog(options?: {
34
35
  force?: boolean;
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA0CH,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;;;;;;;;;;GAUG;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
@@ -54,14 +54,15 @@ export function triggerNativeCrash() {
54
54
  }
55
55
  }
56
56
  /**
57
- * Phase 22 sub-D: start the Android ANR watchdog.
57
+ * Phase 22 sub-D / sub-E: cross-platform main-thread watchdog.
58
+ * Single JS call covers both Android ANR and iOS hang detection.
58
59
  *
59
- * startAnrWatchdog() // default 5s/1s, prod-only
60
+ * startAnrWatchdog() // platform defaults, prod-only
60
61
  * startAnrWatchdog({ force: true }) // include debug builds
61
62
  * startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
62
63
  *
63
- * Returns silently on iOS / web / jest. iOS hang detection (sub-E)
64
- * will hook the same JS function once landed.
64
+ * Defaults: Android 5 s / 1 s tick; iOS 2 s / 1 s tick. Returns
65
+ * silently on web / jest / unsupported runtimes.
65
66
  */
66
67
  export function startAnrWatchdog(options) {
67
68
  try {
package/lib/native.js.map CHANGED
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"native.js","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AA2BH,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;;;;;;;;;;GAUG;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"}
@@ -0,0 +1,6 @@
1
+ export declare const startSession: () => void;
2
+ export declare const endSession: (status?: "exited") => void;
3
+ export declare const markSessionErrored: () => void;
4
+ export declare const markSessionCrashed: () => void;
5
+ export declare const __resetSessionForTests: () => void;
6
+ //# sourceMappingURL=session-tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-tracker.d.ts","sourceRoot":"","sources":["../src/session-tracker.ts"],"names":[],"mappings":"AA0BA,eAAO,MAAM,YAAY,QAAO,IAS/B,CAAC;AAEF,eAAO,MAAM,UAAU,GAAI,SAAS,QAAQ,KAAG,IAG9C,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,IAErC,CAAC;AAEF,eAAO,MAAM,kBAAkB,QAAO,IAErC,CAAC;AAEF,eAAO,MAAM,sBAAsB,QAAO,IAEzC,CAAC"}
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Phase 26 sub-B: RN session tracker glue.
3
+ *
4
+ * Mirrors the JS SDK's session-tracker but sends through the RN
5
+ * transport. AppState binding lives in `handlers/lifecycle.ts`; this
6
+ * file is just the singleton + the start/end/markErrored/markCrashed
7
+ * surface.
8
+ */
9
+ import { SessionTracker } from '@goliapkg/sentori-core';
10
+ import { getConfig } from './config';
11
+ import { getUser } from './capture';
12
+ import { sendSessionPing } from './transport';
13
+ let _tracker = null;
14
+ const tracker = () => {
15
+ if (_tracker)
16
+ return _tracker;
17
+ _tracker = new SessionTracker((ping) => {
18
+ const cfg = getConfig();
19
+ if (!cfg)
20
+ return;
21
+ void sendSessionPing(cfg.ingestUrl, cfg.token, ping);
22
+ });
23
+ return _tracker;
24
+ };
25
+ export const startSession = () => {
26
+ const cfg = getConfig();
27
+ if (!cfg)
28
+ return;
29
+ const user = getUser();
30
+ tracker().start({
31
+ environment: cfg.environment,
32
+ release: cfg.release,
33
+ userId: user?.id ?? null,
34
+ });
35
+ };
36
+ export const endSession = (status) => {
37
+ if (!_tracker)
38
+ return;
39
+ _tracker.end(status);
40
+ };
41
+ export const markSessionErrored = () => {
42
+ _tracker?.markErrored();
43
+ };
44
+ export const markSessionCrashed = () => {
45
+ _tracker?.markCrashed();
46
+ };
47
+ export const __resetSessionForTests = () => {
48
+ _tracker = null;
49
+ };
50
+ //# sourceMappingURL=session-tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-tracker.js","sourceRoot":"","sources":["../src/session-tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AACH,OAAO,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAExD,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AACrC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAE9C,IAAI,QAAQ,GAA0B,IAAI,CAAC;AAE3C,MAAM,OAAO,GAAG,GAAmB,EAAE;IACnC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAC9B,QAAQ,GAAG,IAAI,cAAc,CAAC,CAAC,IAAI,EAAE,EAAE;QACrC,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;QACxB,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,KAAK,eAAe,CAAC,GAAG,CAAC,SAAS,EAAE,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;IACvD,CAAC,CAAC,CAAC;IACH,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG,GAAS,EAAE;IACrC,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC;IACxB,IAAI,CAAC,GAAG;QAAE,OAAO;IACjB,MAAM,IAAI,GAAG,OAAO,EAAE,CAAC;IACvB,OAAO,EAAE,CAAC,KAAK,CAAC;QACd,WAAW,EAAE,GAAG,CAAC,WAAW;QAC5B,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,MAAM,EAAE,IAAI,EAAE,EAAE,IAAI,IAAI;KACzB,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,MAAiB,EAAQ,EAAE;IACpD,IAAI,CAAC,QAAQ;QAAE,OAAO;IACtB,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACvB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAS,EAAE;IAC3C,QAAQ,EAAE,WAAW,EAAE,CAAC;AAC1B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,GAAS,EAAE;IAC3C,QAAQ,EAAE,WAAW,EAAE,CAAC;AAC1B,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,sBAAsB,GAAG,GAAS,EAAE;IAC/C,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC,CAAC"}
@@ -5,4 +5,12 @@ export declare const flush: () => Promise<void>;
5
5
  export declare const drainOfflineQueue: () => Promise<void>;
6
6
  export declare const __resetForTests: () => void;
7
7
  export declare const __peekQueue: () => readonly Event[];
8
+ /**
9
+ * Phase 26 sub-B: session ping transport. Best-effort; we don't queue
10
+ * pings the way we queue events because they fire on background and
11
+ * AsyncStorage writes during background can be killed by the OS. If
12
+ * the network's down, the ping is lost — the session counters tolerate
13
+ * this.
14
+ */
15
+ export declare const sendSessionPing: (ingestUrl: string, token: string, ping: unknown) => Promise<void>;
8
16
  //# sourceMappingURL=transport.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAcrC,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,KAAG,IAUtC,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAEjC,CAAC;AAEF,eAAO,MAAM,KAAK,QAAa,OAAO,CAAC,IAAI,CAkB1C,CAAC;AA4FF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAatD,CAAC;AAEF,eAAO,MAAM,eAAe,QAAO,IAKlC,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,SAAS,KAAK,EAAY,CAAC"}
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAcrC,eAAO,MAAM,OAAO,GAAI,OAAO,KAAK,KAAG,IAUtC,CAAC;AAEF,eAAO,MAAM,cAAc,QAAO,IAEjC,CAAC;AAEF,eAAO,MAAM,KAAK,QAAa,OAAO,CAAC,IAAI,CAkB1C,CAAC;AA4FF,eAAO,MAAM,iBAAiB,QAAa,OAAO,CAAC,IAAI,CAatD,CAAC;AAEF,eAAO,MAAM,eAAe,QAAO,IAKlC,CAAC;AAEF,eAAO,MAAM,WAAW,QAAO,SAAS,KAAK,EAAY,CAAC;AAE1D;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,GAC1B,WAAW,MAAM,EACjB,OAAO,MAAM,EACb,MAAM,OAAO,KACZ,OAAO,CAAC,IAAI,CAcd,CAAC"}
package/lib/transport.js CHANGED
@@ -140,4 +140,27 @@ export const __resetForTests = () => {
140
140
  _started = false;
141
141
  };
142
142
  export const __peekQueue = () => _queue;
143
+ /**
144
+ * Phase 26 sub-B: session ping transport. Best-effort; we don't queue
145
+ * pings the way we queue events because they fire on background and
146
+ * AsyncStorage writes during background can be killed by the OS. If
147
+ * the network's down, the ping is lost — the session counters tolerate
148
+ * this.
149
+ */
150
+ export const sendSessionPing = async (ingestUrl, token, ping) => {
151
+ try {
152
+ await fetch(`${ingestUrl}/v1/sessions`, {
153
+ body: JSON.stringify(ping),
154
+ headers: {
155
+ Authorization: `Bearer ${token}`,
156
+ 'Content-Type': 'application/json',
157
+ 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
158
+ },
159
+ method: 'POST',
160
+ });
161
+ }
162
+ catch {
163
+ // best-effort
164
+ }
165
+ };
143
166
  //# sourceMappingURL=transport.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "0.3.0",
4
- "description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport.",
3
+ "version": "0.4.0",
4
+ "description": "Sentori SDK for React Native JS-layer error capture, native crash handlers (iOS / Android), batched transport.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
7
7
  "repository": {
package/src/capture.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { getConfig, isInitialized } from './config';
2
2
  import { getBreadcrumbs } from './breadcrumbs';
3
+ import { markSessionErrored } from './session-tracker';
3
4
  import { parseStack } from './stack';
4
5
  import { enqueue } from './transport';
5
6
  import { uuidV7 } from './uuid';
@@ -52,6 +53,9 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
52
53
  fingerprint: extras?.fingerprint,
53
54
  };
54
55
 
56
+ // Phase 26 sub-B: a captured error promotes the current session to
57
+ // `errored` so the next AppState=background ping reports unhealthy.
58
+ markSessionErrored();
55
59
  enqueue(event);
56
60
  };
57
61
 
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Phase 26 sub-B: AppState binding.
3
+ *
4
+ * Subscribes to AppState transitions:
5
+ * - active → start a fresh session (after a previous background end)
6
+ * - background / inactive → end the current session
7
+ *
8
+ * RN's AppState fires `inactive` on iOS during multitasking peek; we
9
+ * end on it because that's effectively a background and the user may
10
+ * not return. If they do, `active` starts a new one — the on-the-wire
11
+ * session count goes up by one, which matches "the user opened the app
12
+ * twice". Sentry historically did the opposite (treat inactive as
13
+ * still alive), but that lets a swiped-away session never end.
14
+ */
15
+ import { endSession, startSession } from '../session-tracker';
16
+
17
+ let _installed = false;
18
+ let _subscription: { remove: () => void } | null = null;
19
+
20
+ type AppStateLike = {
21
+ addEventListener: (
22
+ event: 'change',
23
+ handler: (state: string) => void
24
+ ) => { remove: () => void };
25
+ };
26
+
27
+ export const installLifecycleHandler = (): void => {
28
+ if (_installed) return;
29
+ _installed = true;
30
+ let AppState: AppStateLike | undefined;
31
+ try {
32
+ // RN ships AppState; in test / non-RN host the require throws and
33
+ // we silently no-op.
34
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
35
+ AppState = (require('react-native') as { AppState?: AppStateLike }).AppState;
36
+ } catch {
37
+ return;
38
+ }
39
+ if (!AppState || typeof AppState.addEventListener !== 'function') return;
40
+
41
+ _subscription = AppState.addEventListener('change', (state) => {
42
+ if (state === 'active') {
43
+ startSession();
44
+ } else if (state === 'background' || state === 'inactive') {
45
+ endSession();
46
+ }
47
+ });
48
+ };
49
+
50
+ export const __uninstallLifecycleForTests = (): void => {
51
+ _subscription?.remove();
52
+ _subscription = null;
53
+ _installed = false;
54
+ };
package/src/index.ts CHANGED
@@ -2,6 +2,11 @@ import { init } from './init';
2
2
  import { addBreadcrumb } from './breadcrumbs';
3
3
  import { setUser, getUser, captureError, captureException } from './capture';
4
4
  import { ErrorBoundary } from './error-boundary';
5
+ import {
6
+ endSession,
7
+ markSessionCrashed,
8
+ startSession,
9
+ } from './session-tracker';
5
10
 
6
11
  export const sentori = {
7
12
  init,
@@ -11,6 +16,9 @@ export const sentori = {
11
16
  captureError,
12
17
  captureException,
13
18
  ErrorBoundary,
19
+ startSession,
20
+ endSession,
21
+ markSessionCrashed,
14
22
  };
15
23
 
16
24
  export default sentori;
@@ -24,6 +32,11 @@ export {
24
32
  stopAnrWatchdog,
25
33
  triggerNativeCrash,
26
34
  } from './native';
35
+ export {
36
+ endSession,
37
+ markSessionCrashed,
38
+ startSession,
39
+ } from './session-tracker';
27
40
 
28
41
  export type {
29
42
  Event,
package/src/native.ts CHANGED
@@ -12,10 +12,12 @@ type SentoriNativeModule = {
12
12
  token: string
13
13
  }) => void
14
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.
15
+ * Phase 22 sub-D / sub-E: cross-platform main-thread watchdog.
16
+ * Android: 5 s / 1 s defaults (matches the OS ANR threshold).
17
+ * iOS: 2 s / 1 s (more aggressive — iOS has no system-level
18
+ * watchdog signal we can lean on, so we surface stutter Apple's
19
+ * own runtime never flags).
20
+ * Reports a `kind = "anr"` event when the main thread is wedged.
19
21
  */
20
22
  startAnrWatchdog?: (options?: {
21
23
  force?: boolean
@@ -83,14 +85,15 @@ export function triggerNativeCrash(): void {
83
85
  }
84
86
 
85
87
  /**
86
- * Phase 22 sub-D: start the Android ANR watchdog.
88
+ * Phase 22 sub-D / sub-E: cross-platform main-thread watchdog.
89
+ * Single JS call covers both Android ANR and iOS hang detection.
87
90
  *
88
- * startAnrWatchdog() // default 5s/1s, prod-only
91
+ * startAnrWatchdog() // platform defaults, prod-only
89
92
  * startAnrWatchdog({ force: true }) // include debug builds
90
93
  * startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
91
94
  *
92
- * Returns silently on iOS / web / jest. iOS hang detection (sub-E)
93
- * will hook the same JS function once landed.
95
+ * Defaults: Android 5 s / 1 s tick; iOS 2 s / 1 s tick. Returns
96
+ * silently on web / jest / unsupported runtimes.
94
97
  */
95
98
  export function startAnrWatchdog(options?: {
96
99
  force?: boolean
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Phase 26 sub-B: RN session tracker glue.
3
+ *
4
+ * Mirrors the JS SDK's session-tracker but sends through the RN
5
+ * transport. AppState binding lives in `handlers/lifecycle.ts`; this
6
+ * file is just the singleton + the start/end/markErrored/markCrashed
7
+ * surface.
8
+ */
9
+ import { SessionTracker } from '@goliapkg/sentori-core';
10
+
11
+ import { getConfig } from './config';
12
+ import { getUser } from './capture';
13
+ import { sendSessionPing } from './transport';
14
+
15
+ let _tracker: null | SessionTracker = null;
16
+
17
+ const tracker = (): SessionTracker => {
18
+ if (_tracker) return _tracker;
19
+ _tracker = new SessionTracker((ping) => {
20
+ const cfg = getConfig();
21
+ if (!cfg) return;
22
+ void sendSessionPing(cfg.ingestUrl, cfg.token, ping);
23
+ });
24
+ return _tracker;
25
+ };
26
+
27
+ export const startSession = (): void => {
28
+ const cfg = getConfig();
29
+ if (!cfg) return;
30
+ const user = getUser();
31
+ tracker().start({
32
+ environment: cfg.environment,
33
+ release: cfg.release,
34
+ userId: user?.id ?? null,
35
+ });
36
+ };
37
+
38
+ export const endSession = (status?: 'exited'): void => {
39
+ if (!_tracker) return;
40
+ _tracker.end(status);
41
+ };
42
+
43
+ export const markSessionErrored = (): void => {
44
+ _tracker?.markErrored();
45
+ };
46
+
47
+ export const markSessionCrashed = (): void => {
48
+ _tracker?.markCrashed();
49
+ };
50
+
51
+ export const __resetSessionForTests = (): void => {
52
+ _tracker = null;
53
+ };
package/src/transport.ts CHANGED
@@ -162,3 +162,30 @@ export const __resetForTests = (): void => {
162
162
  };
163
163
 
164
164
  export const __peekQueue = (): readonly Event[] => _queue;
165
+
166
+ /**
167
+ * Phase 26 sub-B: session ping transport. Best-effort; we don't queue
168
+ * pings the way we queue events because they fire on background and
169
+ * AsyncStorage writes during background can be killed by the OS. If
170
+ * the network's down, the ping is lost — the session counters tolerate
171
+ * this.
172
+ */
173
+ export const sendSessionPing = async (
174
+ ingestUrl: string,
175
+ token: string,
176
+ ping: unknown
177
+ ): Promise<void> => {
178
+ try {
179
+ await fetch(`${ingestUrl}/v1/sessions`, {
180
+ body: JSON.stringify(ping),
181
+ headers: {
182
+ Authorization: `Bearer ${token}`,
183
+ 'Content-Type': 'application/json',
184
+ 'Sentori-Sdk': `react-native/${SDK_VERSION}`,
185
+ },
186
+ method: 'POST',
187
+ });
188
+ } catch {
189
+ // best-effort
190
+ }
191
+ };