@goliapkg/sentori-react-native 0.3.1 → 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).
@@ -27,13 +27,21 @@ import Foundation
27
27
  private static var timer: DispatchSourceTimer?
28
28
  private static let lock = NSLock()
29
29
 
30
- /// Start the watchdog. Idempotent.
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`.
31
34
  @objc public static func start(timeoutMs: Int, intervalMs: Int, force: Bool) {
32
35
  lock.lock()
33
36
  defer { lock.unlock() }
34
37
  if running { return }
35
38
  if isDebug() && !force { return }
36
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
+
37
45
  let q = DispatchQueue(label: "com.sentori.hangWatchdog", qos: .utility)
38
46
  let t = DispatchSource.makeTimerSource(queue: q)
39
47
  let interval = DispatchTimeInterval.milliseconds(intervalMs)
@@ -97,33 +105,50 @@ import Foundation
97
105
  let release = (cfg["release"] as? String) ?? "unknown"
98
106
  let environment = (cfg["environment"] as? String) ?? "prod"
99
107
 
100
- // Thread.callStackSymbols on the main thread is what we want;
101
- // cross-thread inspection requires Mach APIs that App Store
102
- // review tends to flag. Posting onto main blocks if main is
103
- // wedged we want the wedged stack, but we can't get it
104
- // without main cooperation. Best-effort: capture this thread's
105
- // stack (the watchdog) which is at least informative about
106
- // the timing path. Phase 22 sub-F gets a proper main-thread
107
- // stack via thread_state_t when we sit down with mach.
108
- let frames = Thread.callStackSymbols.map { sym -> [String: Any] in
109
- let parts = sym.split(
110
- separator: " ", omittingEmptySubsequences: true
111
- ).map(String.init)
112
- let module = parts.count > 1 ? parts[1] : "<unknown>"
113
- let function =
114
- parts.count > 3
115
- ? parts.dropFirst(3).joined(separator: " ")
116
- : "<anonymous>"
117
- return [
118
- "function": function,
119
- "file": module,
120
- "line": 0,
121
- "inApp": !module.contains("UIKit")
122
- && !module.contains("Foundation")
123
- && !module.contains("CoreFoundation")
124
- && !module.contains("libsystem")
125
- && !module.contains("libobjc"),
126
- ]
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"
127
152
  }
128
153
 
129
154
  let event: [String: Any] = [
@@ -140,7 +165,7 @@ import Foundation
140
165
  ],
141
166
  "app": appInfo(),
142
167
  "user": NSNull(),
143
- "tags": ["source": "sentori.hangWatchdog"],
168
+ "tags": ["source": stackSource],
144
169
  "breadcrumbs": [Any](),
145
170
  "error": [
146
171
  "type": "ApplicationNotResponding",
@@ -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":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAGrC,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAChC,MAAM,UAAU,GAAG,EAAE,CAAC;AACtB,MAAM,SAAS,GAAG,CAAC,CAAC;AACpB,MAAM,WAAW,GAAG,kBAAkB,CAAC;AACvC,MAAM,aAAa,GAAG,IAAI,CAAC;AAE3B,IAAI,MAAM,GAAY,EAAE,CAAC;AACzB,IAAI,WAAW,GAAyC,IAAI,CAAC;AAC7D,IAAI,QAAQ,GAAG,KAAK,CAAC;AAErB,MAAM,WAAW,GAAG,OAAO,CAAC;AAE5B,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAY,EAAQ,EAAE;IAC5C,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACnB,IAAI,MAAM,CAAC,MAAM,IAAI,UAAU,EAAE,CAAC;QAChC,KAAK,KAAK,EAAE,CAAC;IACf,CAAC;SAAM,IAAI,CAAC,WAAW,EAAE,CAAC;QACxB,WAAW,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,WAAW,GAAG,IAAI,CAAC;YACnB,KAAK,KAAK,EAAE,CAAC;QACf,CAAC,EAAE,iBAAiB,CAAC,CAAC;IACxB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,cAAc,GAAG,GAAS,EAAE;IACvC,QAAQ,GAAG,IAAI,CAAC;AAClB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,KAAK,GAAG,KAAK,IAAmB,EAAE;IAC7C,IAAI,CAAC,QAAQ;QAAE,OAAO;IACtB,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO;IAEhC,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;IAC3B,IAAI,CAAC,MAAM;QAAE,OAAO;IAEpB,MAAM,KAAK,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAC9C,IAAI,WAAW,EAAE,CAAC;QAChB,YAAY,CAAC,WAAW,CAAC,CAAC;QAC1B,WAAW,GAAG,IAAI,CAAC;IACrB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,aAAa,CAAC,KAAK,EAAE,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7D,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,aAAa,GAAG,KAAK,EACzB,MAAe,EACf,SAAiB,EACjB,KAAa,EACE,EAAE;IACjB,IAAI,OAAO,GAAG,CAAC,CAAC;IAChB,IAAI,OAAO,GAAG,IAAI,CAAC;IACnB,OAAO,IAAI,EAAE,CAAC;QACZ,IAAI,CAAC;YACH,MAAM,QAAQ,CAAC,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;YACzC,OAAO;QACT,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,EAAE,CAAC;YACV,IAAI,OAAO,IAAI,SAAS;gBAAE,MAAM,CAAC,CAAC;YAClC,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;YACrB,OAAO,IAAI,CAAC,CAAC;QACf,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,QAAQ,GAAG,KAAK,EACpB,MAAe,EACf,SAAiB,EACjB,KAAa,EACE,EAAE;IACjB,MAAM,GAAG,GACP,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,SAAS,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS,kBAAkB,CAAC;IAClF,MAAM,IAAI,GAAG,MAAM,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC;IAE1D,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;QAC5B,MAAM,EAAE,MAAM;QACd,OAAO,EAAE;YACP,cAAc,EAAE,kBAAkB;YAClC,aAAa,EAAE,UAAU,KAAK,EAAE;YAChC,aAAa,EAAE,gBAAgB,WAAW,EAAE;SAC7C;QACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;KAC3B,CAAC,CAAC;IAEH,IAAI,IAAI,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;QACxB,IAAI,YAAY,GAAG,IAAI,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,IAAI,EAAE,CAA8B,CAAC;YAC3D,IAAI,OAAO,CAAC,CAAC,YAAY,KAAK,QAAQ;gBAAE,YAAY,GAAG,CAAC,CAAC,YAAY,CAAC;QACxE,CAAC;QAAC,MAAM,CAAC;YACP,0BAA0B;QAC5B,CAAC;QACD,MAAM,KAAK,CAAC,YAAY,CAAC,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,IAAI,CAAC,MAAM,IAAI,GAAG,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,UAAU,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC3C,CAAC;IACD,mDAAmD;AACrD,CAAC,CAAC;AAEF,MAAM,KAAK,GAAG,CAAC,EAAU,EAAiB,EAAE,CAC1C,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;AAQxC,MAAM,eAAe,GAAG,KAAK,IAAsC,EAAE;IACnE,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,MAAM,CACvB,2CAA2C,CAC5C,CAAkC,CAAC;QACpC,OAAO,GAAG,CAAC,OAAO,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,OAAO,GAAG,KAAK,EAAE,MAAe,EAAiB,EAAE;IACvD,MAAM,YAAY,GAAG,MAAM,eAAe,EAAE,CAAC;IAC7C,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACzD,MAAM,IAAI,GAAY,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3D,MAAM,MAAM,GAAG,CAAC,GAAG,IAAI,EAAE,GAAG,MAAM,CAAC,CAAC,KAAK,CAAC,CAAC,aAAa,CAAC,CAAC;QAC1D,MAAM,YAAY,CAAC,OAAO,CAAC,WAAW,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;IAClE,CAAC;IAAC,MAAM,CAAC;QACP,cAAc;IAChB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,IAAmB,EAAE;IACzD,MAAM,YAAY,GAAG,MAAM,eAAe,EAAE,CAAC;IAC7C,IAAI,CAAC,YAAY;QAAE,OAAO;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;QACpD,IAAI,CAAC,GAAG;YAAE,OAAO;QACjB,MAAM,YAAY,CAAC,UAAU,CAAC,WAAW,CAAC,CAAC;QAC3C,MAAM,MAAM,GAAY,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QACxC,KAAK,MAAM,CAAC,IAAI,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QACvC,MAAM,KAAK,EAAE,CAAC;IAChB,CAAC;IAAC,MAAM,CAAC;QACP,cAAc;IAChB,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,eAAe,GAAG,GAAS,EAAE;IACxC,MAAM,GAAG,EAAE,CAAC;IACZ,IAAI,WAAW;QAAE,YAAY,CAAC,WAAW,CAAC,CAAC;IAC3C,WAAW,GAAG,IAAI,CAAC;IACnB,QAAQ,GAAG,KAAK,CAAC;AACnB,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG,GAAqB,EAAE,CAAC,MAAM,CAAC"}
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.1",
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
+ };