@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.
- package/ios/PRIVACY_AND_REVIEW.md +122 -0
- package/ios/SentoriHangWatchdog.swift +244 -0
- package/ios/SentoriModule.swift +19 -0
- package/ios/SentoriThreadSampler.swift +149 -0
- package/ios/Tests/SentoriThreadSamplerTests.swift +96 -0
- package/lib/capture.d.ts.map +1 -1
- package/lib/capture.js +4 -0
- package/lib/capture.js.map +1 -1
- package/lib/handlers/lifecycle.d.ts +3 -0
- package/lib/handlers/lifecycle.d.ts.map +1 -0
- package/lib/handlers/lifecycle.js +48 -0
- package/lib/handlers/lifecycle.js.map +1 -0
- package/lib/index.d.ts +4 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +5 -0
- package/lib/index.js.map +1 -1
- package/lib/native.d.ts +5 -4
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +5 -4
- package/lib/native.js.map +1 -1
- package/lib/session-tracker.d.ts +6 -0
- package/lib/session-tracker.d.ts.map +1 -0
- package/lib/session-tracker.js +50 -0
- package/lib/session-tracker.js.map +1 -0
- package/lib/transport.d.ts +8 -0
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +23 -0
- package/lib/transport.js.map +1 -1
- package/package.json +2 -2
- package/src/capture.ts +4 -0
- package/src/handlers/lifecycle.ts +54 -0
- package/src/index.ts +13 -0
- package/src/native.ts +11 -8
- package/src/session-tracker.ts +53 -0
- package/src/transport.ts +27 -0
|
@@ -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
|
+
}
|
package/ios/SentoriModule.swift
CHANGED
|
@@ -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
|
+
}
|
package/lib/capture.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"capture.d.ts","sourceRoot":"","sources":["../src/capture.ts"],"names":[],"mappings":"
|
|
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;
|
package/lib/capture.js.map
CHANGED
|
@@ -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 @@
|
|
|
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
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,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;
|
|
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
|
|
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() //
|
|
27
|
+
* startAnrWatchdog() // platform defaults, prod-only
|
|
27
28
|
* startAnrWatchdog({ force: true }) // include debug builds
|
|
28
29
|
* startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
|
|
29
30
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
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;
|
package/lib/native.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"native.d.ts","sourceRoot":"","sources":["../src/native.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
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
|
|
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() //
|
|
60
|
+
* startAnrWatchdog() // platform defaults, prod-only
|
|
60
61
|
* startAnrWatchdog({ force: true }) // include debug builds
|
|
61
62
|
* startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
|
|
62
63
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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;
|
|
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"}
|
package/lib/transport.d.ts
CHANGED
|
@@ -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
|
package/lib/transport.d.ts.map
CHANGED
|
@@ -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
|
package/lib/transport.js.map
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "Sentori SDK for React Native
|
|
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:
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
|
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() //
|
|
91
|
+
* startAnrWatchdog() // platform defaults, prod-only
|
|
89
92
|
* startAnrWatchdog({ force: true }) // include debug builds
|
|
90
93
|
* startAnrWatchdog({ timeoutMs: 3000 }) // tighter threshold
|
|
91
94
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
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
|
+
};
|