@goliapkg/sentori-react-native 4.0.0 → 5.1.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.
Files changed (308) hide show
  1. package/MIGRATION.md +131 -0
  2. package/README.md +40 -30
  3. package/android/build.gradle +21 -0
  4. package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
  5. package/android/src/main/java/com/sentori/SentoriModule.kt +8 -0
  6. package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +36 -4
  7. package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
  8. package/android/src/test/resources/robolectric.properties +10 -0
  9. package/ios/SentoriCrashHandler.swift +48 -4
  10. package/ios/SentoriModule.swift +12 -0
  11. package/ios/SentoriScreenshotCapture.swift +34 -3
  12. package/lib/config.d.ts +17 -47
  13. package/lib/config.d.ts.map +1 -1
  14. package/lib/config.js +3 -0
  15. package/lib/config.js.map +1 -1
  16. package/lib/device.d.ts +5 -0
  17. package/lib/device.d.ts.map +1 -0
  18. package/lib/device.js +29 -0
  19. package/lib/device.js.map +1 -0
  20. package/lib/emit-hooks.d.ts +7 -0
  21. package/lib/emit-hooks.d.ts.map +1 -0
  22. package/lib/emit-hooks.js +24 -0
  23. package/lib/emit-hooks.js.map +1 -0
  24. package/lib/error-boundary.d.ts.map +1 -1
  25. package/lib/error-boundary.js +1 -1
  26. package/lib/error-boundary.js.map +1 -1
  27. package/lib/feature-flags.d.ts.map +1 -1
  28. package/lib/handlers/dev-symbolicate.d.ts +1 -1
  29. package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
  30. package/lib/handlers/dev-symbolicate.js +2 -0
  31. package/lib/handlers/dev-symbolicate.js.map +1 -1
  32. package/lib/handlers/global.js +1 -1
  33. package/lib/handlers/global.js.map +1 -1
  34. package/lib/handlers/lifecycle.js +3 -3
  35. package/lib/handlers/lifecycle.js.map +1 -1
  36. package/lib/handlers/network.d.ts +2 -2
  37. package/lib/handlers/network.d.ts.map +1 -1
  38. package/lib/handlers/network.js +69 -187
  39. package/lib/handlers/network.js.map +1 -1
  40. package/lib/handlers/promise.js +1 -1
  41. package/lib/handlers/promise.js.map +1 -1
  42. package/lib/handlers/screenshot.d.ts.map +1 -1
  43. package/lib/handlers/screenshot.js +3 -12
  44. package/lib/handlers/screenshot.js.map +1 -1
  45. package/lib/index.d.ts +32 -109
  46. package/lib/index.d.ts.map +1 -1
  47. package/lib/index.js +54 -103
  48. package/lib/index.js.map +1 -1
  49. package/lib/init.d.ts +3 -150
  50. package/lib/init.d.ts.map +1 -1
  51. package/lib/init.js +100 -261
  52. package/lib/init.js.map +1 -1
  53. package/lib/long-task-monitor.d.ts +1 -7
  54. package/lib/long-task-monitor.d.ts.map +1 -1
  55. package/lib/long-task-monitor.js +37 -58
  56. package/lib/long-task-monitor.js.map +1 -1
  57. package/lib/mask.d.ts +5 -13
  58. package/lib/mask.d.ts.map +1 -1
  59. package/lib/mask.js +29 -30
  60. package/lib/mask.js.map +1 -1
  61. package/lib/mobile-vitals.d.ts +3 -31
  62. package/lib/mobile-vitals.d.ts.map +1 -1
  63. package/lib/mobile-vitals.js +22 -61
  64. package/lib/mobile-vitals.js.map +1 -1
  65. package/lib/native-pending.d.ts +3 -0
  66. package/lib/native-pending.d.ts.map +1 -0
  67. package/lib/native-pending.js +64 -0
  68. package/lib/native-pending.js.map +1 -0
  69. package/lib/native.d.ts +4 -11
  70. package/lib/native.d.ts.map +1 -1
  71. package/lib/native.js +23 -0
  72. package/lib/native.js.map +1 -1
  73. package/lib/navigation.d.ts +3 -17
  74. package/lib/navigation.d.ts.map +1 -1
  75. package/lib/navigation.js +32 -133
  76. package/lib/navigation.js.map +1 -1
  77. package/lib/netinfo.d.ts +1 -1
  78. package/lib/netinfo.d.ts.map +1 -1
  79. package/lib/netinfo.js +2 -2
  80. package/lib/netinfo.js.map +1 -1
  81. package/lib/push.d.ts.map +1 -1
  82. package/lib/push.js +2 -4
  83. package/lib/push.js.map +1 -1
  84. package/lib/rage-tap.d.ts.map +1 -1
  85. package/lib/rage-tap.js +56 -23
  86. package/lib/rage-tap.js.map +1 -1
  87. package/lib/replay-screens.d.ts +15 -0
  88. package/lib/replay-screens.d.ts.map +1 -0
  89. package/lib/replay-screens.js +87 -0
  90. package/lib/replay-screens.js.map +1 -0
  91. package/lib/replay.d.ts.map +1 -1
  92. package/lib/replay.js +3 -24
  93. package/lib/replay.js.map +1 -1
  94. package/lib/responsiveness-detector.d.ts +18 -0
  95. package/lib/responsiveness-detector.d.ts.map +1 -0
  96. package/lib/responsiveness-detector.js +55 -0
  97. package/lib/responsiveness-detector.js.map +1 -0
  98. package/lib/scope.d.ts +7 -0
  99. package/lib/scope.d.ts.map +1 -0
  100. package/lib/scope.js +41 -0
  101. package/lib/scope.js.map +1 -0
  102. package/lib/transport.d.ts +19 -92
  103. package/lib/transport.d.ts.map +1 -1
  104. package/lib/transport.js +75 -288
  105. package/lib/transport.js.map +1 -1
  106. package/lib/verbs.d.ts +16 -0
  107. package/lib/verbs.d.ts.map +1 -0
  108. package/lib/verbs.js +153 -0
  109. package/lib/verbs.js.map +1 -0
  110. package/package.json +4 -3
  111. package/src/__tests__/iron-rule.test.ts +128 -0
  112. package/src/__tests__/replay-screens.test.ts +63 -0
  113. package/src/__tests__/responsiveness-detector.test.ts +74 -0
  114. package/src/__tests__/screenshot.test.ts +0 -2
  115. package/src/__tests__/transport.test.ts +66 -213
  116. package/src/__tests__/verbs.test.ts +91 -0
  117. package/src/config.ts +20 -47
  118. package/src/device.ts +36 -0
  119. package/src/emit-hooks.ts +30 -0
  120. package/src/error-boundary.tsx +1 -1
  121. package/src/handlers/dev-symbolicate.ts +2 -1
  122. package/src/handlers/global.ts +1 -1
  123. package/src/handlers/lifecycle.ts +3 -3
  124. package/src/handlers/network.ts +95 -211
  125. package/src/handlers/promise.ts +1 -1
  126. package/src/handlers/screenshot.ts +3 -11
  127. package/src/index.ts +84 -215
  128. package/src/init.ts +115 -449
  129. package/src/long-task-monitor.ts +42 -64
  130. package/src/mask.ts +29 -31
  131. package/src/mobile-vitals.ts +22 -82
  132. package/src/native-pending.ts +92 -0
  133. package/src/native.ts +33 -0
  134. package/src/navigation.ts +34 -142
  135. package/src/netinfo.ts +3 -3
  136. package/src/push.ts +2 -5
  137. package/src/rage-tap.tsx +72 -25
  138. package/src/replay-screens.ts +95 -0
  139. package/src/replay.ts +3 -20
  140. package/src/responsiveness-detector.ts +66 -0
  141. package/src/scope.ts +50 -0
  142. package/src/transport.ts +84 -371
  143. package/src/verbs.ts +198 -0
  144. package/lib/breadcrumbs.d.ts +0 -18
  145. package/lib/breadcrumbs.d.ts.map +0 -1
  146. package/lib/breadcrumbs.js +0 -42
  147. package/lib/breadcrumbs.js.map +0 -1
  148. package/lib/bundle-info.d.ts +0 -12
  149. package/lib/bundle-info.d.ts.map +0 -1
  150. package/lib/bundle-info.js +0 -73
  151. package/lib/bundle-info.js.map +0 -1
  152. package/lib/capture.d.ts +0 -140
  153. package/lib/capture.d.ts.map +0 -1
  154. package/lib/capture.js +0 -511
  155. package/lib/capture.js.map +0 -1
  156. package/lib/compat/sentry.d.ts +0 -126
  157. package/lib/compat/sentry.d.ts.map +0 -1
  158. package/lib/compat/sentry.js +0 -332
  159. package/lib/compat/sentry.js.map +0 -1
  160. package/lib/control-channel.d.ts +0 -6
  161. package/lib/control-channel.d.ts.map +0 -1
  162. package/lib/control-channel.js +0 -96
  163. package/lib/control-channel.js.map +0 -1
  164. package/lib/expo-compat.d.ts +0 -270
  165. package/lib/expo-compat.d.ts.map +0 -1
  166. package/lib/expo-compat.js +0 -500
  167. package/lib/expo-compat.js.map +0 -1
  168. package/lib/feedback-widget.d.ts +0 -35
  169. package/lib/feedback-widget.d.ts.map +0 -1
  170. package/lib/feedback-widget.js +0 -193
  171. package/lib/feedback-widget.js.map +0 -1
  172. package/lib/feedback.d.ts +0 -2
  173. package/lib/feedback.d.ts.map +0 -1
  174. package/lib/feedback.js +0 -17
  175. package/lib/feedback.js.map +0 -1
  176. package/lib/heartbeat.d.ts +0 -9
  177. package/lib/heartbeat.d.ts.map +0 -1
  178. package/lib/heartbeat.js +0 -138
  179. package/lib/heartbeat.js.map +0 -1
  180. package/lib/index.js.bak +0 -64
  181. package/lib/install-id.d.ts +0 -17
  182. package/lib/install-id.d.ts.map +0 -1
  183. package/lib/install-id.js +0 -125
  184. package/lib/install-id.js.map +0 -1
  185. package/lib/lifecycle.d.ts +0 -28
  186. package/lib/lifecycle.d.ts.map +0 -1
  187. package/lib/lifecycle.js +0 -73
  188. package/lib/lifecycle.js.map +0 -1
  189. package/lib/measure.d.ts +0 -4
  190. package/lib/measure.d.ts.map +0 -1
  191. package/lib/measure.js +0 -25
  192. package/lib/measure.js.map +0 -1
  193. package/lib/metrics.d.ts +0 -24
  194. package/lib/metrics.d.ts.map +0 -1
  195. package/lib/metrics.js +0 -81
  196. package/lib/metrics.js.map +0 -1
  197. package/lib/pre-crash-sentinel.d.ts +0 -12
  198. package/lib/pre-crash-sentinel.d.ts.map +0 -1
  199. package/lib/pre-crash-sentinel.js +0 -116
  200. package/lib/pre-crash-sentinel.js.map +0 -1
  201. package/lib/report-security.d.ts +0 -40
  202. package/lib/report-security.d.ts.map +0 -1
  203. package/lib/report-security.js +0 -159
  204. package/lib/report-security.js.map +0 -1
  205. package/lib/runtime-metrics-fps.d.ts +0 -19
  206. package/lib/runtime-metrics-fps.d.ts.map +0 -1
  207. package/lib/runtime-metrics-fps.js +0 -128
  208. package/lib/runtime-metrics-fps.js.map +0 -1
  209. package/lib/runtime-metrics-heap.d.ts +0 -13
  210. package/lib/runtime-metrics-heap.d.ts.map +0 -1
  211. package/lib/runtime-metrics-heap.js +0 -67
  212. package/lib/runtime-metrics-heap.js.map +0 -1
  213. package/lib/runtime-metrics-network.d.ts +0 -23
  214. package/lib/runtime-metrics-network.d.ts.map +0 -1
  215. package/lib/runtime-metrics-network.js +0 -99
  216. package/lib/runtime-metrics-network.js.map +0 -1
  217. package/lib/runtime-metrics.d.ts +0 -43
  218. package/lib/runtime-metrics.d.ts.map +0 -1
  219. package/lib/runtime-metrics.js +0 -115
  220. package/lib/runtime-metrics.js.map +0 -1
  221. package/lib/sample-profiler.d.ts +0 -21
  222. package/lib/sample-profiler.d.ts.map +0 -1
  223. package/lib/sample-profiler.js +0 -152
  224. package/lib/sample-profiler.js.map +0 -1
  225. package/lib/sentinel-context.d.ts +0 -4
  226. package/lib/sentinel-context.d.ts.map +0 -1
  227. package/lib/sentinel-context.js +0 -52
  228. package/lib/sentinel-context.js.map +0 -1
  229. package/lib/session-tracker.d.ts +0 -6
  230. package/lib/session-tracker.d.ts.map +0 -1
  231. package/lib/session-tracker.js +0 -50
  232. package/lib/session-tracker.js.map +0 -1
  233. package/lib/stack.d.ts +0 -3
  234. package/lib/stack.d.ts.map +0 -1
  235. package/lib/stack.js +0 -7
  236. package/lib/stack.js.map +0 -1
  237. package/lib/state-snapshots.d.ts +0 -33
  238. package/lib/state-snapshots.d.ts.map +0 -1
  239. package/lib/state-snapshots.js +0 -109
  240. package/lib/state-snapshots.js.map +0 -1
  241. package/lib/track.d.ts +0 -34
  242. package/lib/track.d.ts.map +0 -1
  243. package/lib/track.js +0 -107
  244. package/lib/track.js.map +0 -1
  245. package/lib/trail.d.ts +0 -12
  246. package/lib/trail.d.ts.map +0 -1
  247. package/lib/trail.js +0 -28
  248. package/lib/trail.js.map +0 -1
  249. package/lib/trust-score.d.ts +0 -20
  250. package/lib/trust-score.d.ts.map +0 -1
  251. package/lib/trust-score.js +0 -151
  252. package/lib/trust-score.js.map +0 -1
  253. package/lib/types.d.ts +0 -2
  254. package/lib/types.d.ts.map +0 -1
  255. package/lib/types.js +0 -4
  256. package/lib/types.js.map +0 -1
  257. package/lib/uuid.d.ts +0 -2
  258. package/lib/uuid.d.ts.map +0 -1
  259. package/lib/uuid.js +0 -3
  260. package/lib/uuid.js.map +0 -1
  261. package/src/__tests__/before-send.test.ts +0 -72
  262. package/src/__tests__/breadcrumbs.test.ts +0 -44
  263. package/src/__tests__/capture-replay.test.ts +0 -150
  264. package/src/__tests__/compat-sentry.test.ts +0 -121
  265. package/src/__tests__/control-channel.test.ts +0 -111
  266. package/src/__tests__/install-id.test.ts +0 -60
  267. package/src/__tests__/long-task-monitor.test.ts +0 -59
  268. package/src/__tests__/measure.test.ts +0 -45
  269. package/src/__tests__/navigation.test.ts +0 -175
  270. package/src/__tests__/push.test.ts +0 -238
  271. package/src/__tests__/report-security.test.ts +0 -106
  272. package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
  273. package/src/__tests__/runtime-metrics-network.test.ts +0 -78
  274. package/src/__tests__/sample-profiler.test.ts +0 -59
  275. package/src/__tests__/stack.test.ts +0 -45
  276. package/src/__tests__/state-snapshots.test.ts +0 -119
  277. package/src/__tests__/tracing.test.ts +0 -201
  278. package/src/__tests__/track.test.ts +0 -91
  279. package/src/__tests__/uuid.test.ts +0 -41
  280. package/src/breadcrumbs.ts +0 -57
  281. package/src/bundle-info.ts +0 -95
  282. package/src/capture.ts +0 -660
  283. package/src/compat/sentry.ts +0 -490
  284. package/src/control-channel.ts +0 -98
  285. package/src/expo-compat.ts +0 -698
  286. package/src/feedback-widget.tsx +0 -316
  287. package/src/feedback.ts +0 -21
  288. package/src/heartbeat.ts +0 -157
  289. package/src/install-id.ts +0 -146
  290. package/src/lifecycle.ts +0 -76
  291. package/src/measure.ts +0 -28
  292. package/src/metrics.ts +0 -94
  293. package/src/pre-crash-sentinel.ts +0 -140
  294. package/src/report-security.ts +0 -165
  295. package/src/runtime-metrics-fps.ts +0 -137
  296. package/src/runtime-metrics-heap.ts +0 -78
  297. package/src/runtime-metrics-network.ts +0 -103
  298. package/src/runtime-metrics.ts +0 -122
  299. package/src/sample-profiler.ts +0 -169
  300. package/src/sentinel-context.ts +0 -60
  301. package/src/session-tracker.ts +0 -53
  302. package/src/stack.ts +0 -10
  303. package/src/state-snapshots.ts +0 -133
  304. package/src/track.ts +0 -123
  305. package/src/trail.ts +0 -32
  306. package/src/trust-score.ts +0 -176
  307. package/src/types.ts +0 -21
  308. package/src/uuid.ts +0 -2
@@ -1,27 +1,35 @@
1
- import { normalizeUrl, startSpan } from '@goliapkg/sentori-core';
1
+ // Network instrumentation — v1 role: feed the signal ring (`http`
2
+ // signals) and detect the slow_api warn scenario (design.md §3,
3
+ // category C: 「转圈不完」server-side flavour). The span/breadcrumb
4
+ // machinery this file used to drive is gone with the APM vocabulary.
5
+ //
6
+ // Mini-spec (slow_api): a completed request slower than 3 s emits
7
+ // one `warn`, scenario `slow_api`, surface = current screen, with
8
+ // method + scrubbed URL + duration. Per-endpoint cooldown of 60 s so
9
+ // one slow backend doesn't flood ingest with identical warns.
10
+ //
11
+ // Everything here is wrapped so a bug in our patch can never break
12
+ // the host's networking (failure isolation): on any internal error
13
+ // the original fetch/XHR behaviour wins.
14
+
15
+ import { normalizeUrl, pushSignal } from '@goliapkg/sentori-core';
2
16
 
3
- import { addBreadcrumb } from '../breadcrumbs';
4
17
  import { getConfig } from '../config';
5
- // v2.1 W2 — bytes counters drive the runtime.network.{sent,received}
6
- // metrics. Cheap two-add per request, no allocation.
7
- import {
8
- estimateRequestBytes,
9
- estimateResponseBytes,
10
- recordNetworkBytes,
11
- } from '../runtime-metrics-network';
18
+ import { currentScreen } from '../navigation';
19
+ import { warnDetected } from '../verbs';
12
20
 
13
21
  let _installed = false;
14
22
  let _graphqlEnabled = true;
15
23
 
16
24
  const AUTH_PARAMS = ['token', 'key', 'password', 'secret', 'access_token'];
17
-
18
- // v0.9.0 #11 — cap on body size we'll parse for `operationName`.
19
- // 8 KB is generous for any sensible GraphQL request and keeps the
20
- // hot-path JSON.parse bounded.
21
25
  const GQL_BODY_MAX_BYTES = 8 * 1024;
26
+ const SLOW_API_MS = 3_000;
27
+ const SLOW_API_COOLDOWN_MS = 60_000;
28
+
29
+ const _slowApiLastWarn = new Map<string, number>();
22
30
 
23
- // Requests to our own ingest endpoint shouldn't be traced — otherwise
24
- // every span upload spawns another http.client span, and so on.
31
+ // Requests to our own ingest endpoint are never observed — otherwise
32
+ // every batch upload would signal itself, and so on.
25
33
  const isIngestUrl = (url: string): boolean => {
26
34
  const base = getConfig()?.ingestUrl;
27
35
  return !!base && url.startsWith(base);
@@ -39,6 +47,35 @@ export const installNetworkHandler = (opts?: { graphql?: boolean }): void => {
39
47
  export const __resetNetworkHandlerForTests = (): void => {
40
48
  _installed = false;
41
49
  _graphqlEnabled = true;
50
+ _slowApiLastWarn.clear();
51
+ };
52
+
53
+ /** One completed (or failed) request: ring signal + slow_api check. */
54
+ const observe = (
55
+ method: string,
56
+ url: string,
57
+ status: number | 'error',
58
+ durationMs: number,
59
+ gqlOp?: string,
60
+ ): void => {
61
+ try {
62
+ const endpoint = gqlOp ? `graphql/${gqlOp}` : normalizeUrl(url);
63
+ pushSignal('http', { method, url: endpoint, status, ms: durationMs });
64
+
65
+ if (durationMs <= SLOW_API_MS) return;
66
+ if (getConfig()?.detect.slowApi !== true) return;
67
+ const now = Date.now();
68
+ const last = _slowApiLastWarn.get(endpoint) ?? 0;
69
+ if (now - last < SLOW_API_COOLDOWN_MS) return;
70
+ _slowApiLastWarn.set(endpoint, now);
71
+ warnDetected(
72
+ 'slow_api',
73
+ { screen: currentScreen(), element: endpoint },
74
+ { method, endpoint, durationMs, status },
75
+ );
76
+ } catch {
77
+ // Observation must never leak into the host's request path.
78
+ }
42
79
  };
43
80
 
44
81
  // ── fetch ──────────────────────────────────────────────────────────
@@ -48,123 +85,52 @@ function patchFetch(): void {
48
85
  const original = globalThis.fetch;
49
86
 
50
87
  globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
51
- const start = Date.now();
52
88
  const url = extractUrl(input);
53
89
  if (isIngestUrl(url)) return original(input, init);
90
+ const start = Date.now();
54
91
  const scrubbed = scrubUrl(url);
55
- const method = (init?.method ??
56
- (typeof input !== 'string' && 'method' in (input as Request)
57
- ? (input as Request).method
58
- : 'GET')) as string;
59
-
60
- // v0.9.0 #11 — GraphQL operation auto-naming. Inspect the request
61
- // body cheaply (string only, < 8 KB) when the request looks like
62
- // GraphQL (URL contains /graphql or content-type hints it). On
63
- // success we override the span name to `graphql/<operationName>`
64
- // and ride along `gql.operation` on the breadcrumb so the dashboard
65
- // can group + filter by operation instead of by the useless
66
- // `POST /graphql` line.
92
+ const method = (
93
+ (init?.method ??
94
+ (typeof input !== 'string' && 'method' in (input as Request)
95
+ ? (input as Request).method
96
+ : 'GET')) as string
97
+ ).toUpperCase();
67
98
  const gqlOp = _graphqlEnabled
68
99
  ? extractGraphqlOpFromInit(init, input, url)
69
100
  : undefined;
70
101
 
71
- // Phase 35 sub-C: also open an http.client span so the request
72
- // shows up in the trace waterfall. Breadcrumbs stay — they're
73
- // attached to error events at capture time and serve a different
74
- // surface (the "last 100 things" timeline on the issue page).
75
- const span = startSpan('http.client', {
76
- name: gqlOp
77
- ? `graphql/${gqlOp}`
78
- : `${method.toUpperCase()} ${normalizeUrl(scrubbed)}`,
79
- tags: gqlOp
80
- ? {
81
- 'http.method': method.toUpperCase(),
82
- 'http.url': scrubbed,
83
- 'gql.operation': gqlOp,
84
- }
85
- : { 'http.method': method.toUpperCase(), 'http.url': scrubbed },
86
- });
87
-
88
- // Inject traceparent header on outbound requests.
89
- const reqInit: RequestInit = { ...(init ?? {}) };
90
- const headers = mergeHeaders(input, init);
91
- headers.set('traceparent', toTraceparent(span.traceId, span.spanId));
92
- reqInit.headers = headers;
93
-
94
102
  try {
95
- const resp = await original(input, reqInit);
96
- span.setTag('http.status', String(resp.status));
97
- span.finish({ status: resp.status >= 400 ? 'error' : 'ok' });
98
- // v2.1 W2 — bytes accounting. Sent estimated from
99
- // init.body; received read from response Content-Length
100
- // header (0 when missing / chunked — undercount-safe).
101
- recordNetworkBytes(estimateRequestBytes(init), estimateResponseBytes(resp.headers));
102
- addBreadcrumb({
103
- type: 'net',
104
- data: gqlOp
105
- ? {
106
- method,
107
- url: scrubbed,
108
- status: resp.status,
109
- durationMs: Date.now() - start,
110
- 'gql.operation': gqlOp,
111
- }
112
- : {
113
- method,
114
- url: scrubbed,
115
- status: resp.status,
116
- durationMs: Date.now() - start,
117
- },
118
- });
103
+ const resp = await original(input, init);
104
+ observe(method, scrubbed, resp.status, Date.now() - start, gqlOp);
119
105
  return resp;
120
106
  } catch (e) {
121
- const isAbort = isAbortError(e);
122
- if (e instanceof Error) span.setTag('error.message', e.message);
123
- span.finish({ status: isAbort ? 'cancelled' : 'error' });
124
- addBreadcrumb({
125
- type: 'net',
126
- data: {
127
- method,
128
- url: scrubbed,
129
- status: 0,
130
- durationMs: Date.now() - start,
131
- error: String(e),
132
- ...(gqlOp ? { 'gql.operation': gqlOp } : {}),
133
- },
134
- });
107
+ if (!isAbortError(e)) {
108
+ observe(method, scrubbed, 'error', Date.now() - start, gqlOp);
109
+ }
135
110
  throw e;
136
111
  }
137
112
  }) as typeof fetch;
138
113
  }
139
114
 
140
- // ── XMLHttpRequest ─────────────────────────────────────────────────
141
- //
142
- // React Native's XHR is a native polyfill, not built on fetch — so
143
- // patching `globalThis.fetch` alone misses every axios / older-style
144
- // request. axios on RN uses its `xhr` adapter by default. We patch
145
- // the prototype's `open` + `send` so the instance carries the span
146
- // from `send()` to `loadend`.
115
+ // ── XHR ────────────────────────────────────────────────────────────
147
116
 
148
117
  type TracedXhr = XMLHttpRequest & {
149
118
  __sentoriMethod?: string;
150
119
  __sentoriUrl?: string;
151
- __sentoriSpan?: ReturnType<typeof startSpan>;
152
120
  __sentoriStart?: number;
153
121
  __sentoriGqlOp?: string;
154
122
  };
155
123
 
156
124
  function patchXhr(): void {
157
- const XHR = (globalThis as { XMLHttpRequest?: typeof XMLHttpRequest }).XMLHttpRequest;
125
+ const XHR = (globalThis as { XMLHttpRequest?: typeof XMLHttpRequest })
126
+ .XMLHttpRequest;
158
127
  if (typeof XHR !== 'function') return;
159
- const proto = XHR.prototype as XMLHttpRequest & {
160
- __sentoriPatched?: boolean;
161
- };
128
+ const proto = XHR.prototype as XMLHttpRequest & { __sentoriPatched?: boolean };
162
129
  if (proto.__sentoriPatched) return;
163
130
  proto.__sentoriPatched = true;
164
131
 
165
132
  const originalOpen = proto.open;
166
133
  const originalSend = proto.send;
167
- const originalSetHeader = proto.setRequestHeader;
168
134
 
169
135
  proto.open = function (
170
136
  this: TracedXhr,
@@ -178,106 +144,40 @@ function patchXhr(): void {
178
144
  return originalOpen.call(this, method, url, ...rest);
179
145
  };
180
146
 
181
- proto.send = function (this: TracedXhr, body?: Document | XMLHttpRequestBodyInit | null): void {
147
+ proto.send = function (
148
+ this: TracedXhr,
149
+ body?: Document | XMLHttpRequestBodyInit | null,
150
+ ): void {
182
151
  if (isIngestUrl(this.__sentoriUrl ?? '')) return originalSend.call(this, body);
183
152
  const method = this.__sentoriMethod ?? 'GET';
184
153
  const url = scrubUrl(this.__sentoriUrl ?? '');
185
- // v0.9.0 #11 — GraphQL operation auto-naming on XHR.
186
- const gqlOp = _graphqlEnabled
154
+ this.__sentoriGqlOp = _graphqlEnabled
187
155
  ? extractGraphqlOpFromXhr(body, this.__sentoriUrl ?? '')
188
156
  : undefined;
189
- this.__sentoriGqlOp = gqlOp;
190
- const span = startSpan('http.client', {
191
- name: gqlOp ? `graphql/${gqlOp}` : `${method} ${normalizeUrl(url)}`,
192
- tags: gqlOp
193
- ? {
194
- 'http.method': method,
195
- 'http.url': url,
196
- 'gql.operation': gqlOp,
197
- }
198
- : { 'http.method': method, 'http.url': url },
199
- });
200
- this.__sentoriSpan = span;
201
157
  this.__sentoriStart = Date.now();
202
158
 
203
- // setRequestHeader must be called between open() and send(); we're
204
- // inside send() before the underlying call, so this is legal.
205
- try {
206
- originalSetHeader.call(this, 'traceparent', toTraceparent(span.traceId, span.spanId));
207
- } catch {
208
- // Some XHR polyfills are strict about header timing; if it
209
- // rejects, drop the header rather than fail the request.
210
- }
211
-
212
- const finish = () => {
213
- const s = this.__sentoriSpan;
214
- if (!s) return;
215
- this.__sentoriSpan = undefined;
216
- const status = this.status;
217
- s.setTag('http.status', String(status));
218
- // status 0 means network error / aborted / CORS block — treat
219
- // as error. The `abort` event handler below downgrades aborts.
220
- s.finish({ status: status === 0 || status >= 400 ? 'error' : 'ok' });
221
- const op = this.__sentoriGqlOp;
222
- addBreadcrumb({
223
- type: 'net',
224
- data: op
225
- ? {
226
- method,
227
- url,
228
- status,
229
- durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
230
- 'gql.operation': op,
231
- }
232
- : {
233
- method,
234
- url,
235
- status,
236
- durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
237
- },
238
- });
159
+ const finish = (statusOverride?: 'error') => {
160
+ const start = this.__sentoriStart;
161
+ if (start === undefined) return;
162
+ this.__sentoriStart = undefined;
163
+ const status =
164
+ statusOverride ?? (this.status === 0 ? ('error' as const) : this.status);
165
+ observe(method, url, status, Date.now() - start, this.__sentoriGqlOp);
239
166
  };
240
167
 
241
- this.addEventListener('loadend', finish);
168
+ this.addEventListener('load', () => finish());
169
+ this.addEventListener('error', () => finish('error'));
170
+ this.addEventListener('timeout', () => finish('error'));
242
171
  this.addEventListener('abort', () => {
243
- const s = this.__sentoriSpan;
244
- if (!s) return;
245
- this.__sentoriSpan = undefined;
246
- s.finish({ status: 'cancelled' });
247
- const op = this.__sentoriGqlOp;
248
- addBreadcrumb({
249
- type: 'net',
250
- data: {
251
- method,
252
- url,
253
- status: 0,
254
- durationMs: Date.now() - (this.__sentoriStart ?? Date.now()),
255
- error: 'aborted',
256
- ...(op ? { 'gql.operation': op } : {}),
257
- },
258
- });
172
+ // Deliberate cancellation is not a network event worth noting.
173
+ this.__sentoriStart = undefined;
259
174
  });
260
175
 
261
176
  return originalSend.call(this, body);
262
177
  };
263
178
  }
264
179
 
265
- function mergeHeaders(input: RequestInfo | URL, init?: RequestInit): Headers {
266
- const out = new Headers();
267
- if (typeof input !== 'string' && !(input instanceof URL)) {
268
- (input as Request).headers.forEach((v, k) => out.set(k, v));
269
- }
270
- if (init?.headers) {
271
- new Headers(init.headers).forEach((v, k) => out.set(k, v));
272
- }
273
- return out;
274
- }
275
-
276
- function toTraceparent(traceId: string, spanId: string): string {
277
- const trace = traceId.replace(/-/g, '').toLowerCase();
278
- const parent = spanId.replace(/-/g, '').toLowerCase().slice(0, 16);
279
- return `00-${trace}-${parent}-01`;
280
- }
180
+ // ── helpers ────────────────────────────────────────────────────────
281
181
 
282
182
  function isAbortError(err: unknown): boolean {
283
183
  if (typeof err !== 'object' || err === null) return false;
@@ -306,30 +206,19 @@ const scrubUrl = (url: string): string => {
306
206
  }
307
207
  };
308
208
 
309
- // ── v0.9.0 #11 — GraphQL operation extraction ─────────────────────
310
- //
311
- // Cheap, sync, never throws. Two callers (fetch + xhr) feed in
312
- // whatever they have on hand; both end up calling `parseGqlOpName`.
209
+ // ── GraphQL operation extraction — cheap, sync, never throws ──────
313
210
 
314
211
  function lookGraphqlish(url: string, contentType?: string): boolean {
315
- if (contentType) {
316
- if (contentType.includes('graphql')) return true;
317
- // application/json is too generic to gate on alone, but combined
318
- // with a `/graphql` path it's a strong hint.
319
- }
320
- if (url.includes('/graphql')) return true;
321
- return false;
212
+ if (contentType?.includes('graphql')) return true;
213
+ return url.includes('/graphql');
322
214
  }
323
215
 
324
- /** Pull `operationName` out of a JSON body or a raw query body. Returns
325
- * `undefined` on any failure mode. Cap at GQL_BODY_MAX_BYTES so a
216
+ /** Pull `operationName` out of a JSON body or a raw query body.
217
+ * Returns `undefined` on any failure mode; body capped at 8 KB so a
326
218
  * hostile / oversize body never lands in `JSON.parse`. */
327
219
  export function parseGqlOpName(body: string): string | undefined {
328
220
  if (typeof body !== 'string' || body.length === 0) return undefined;
329
221
  if (body.length > GQL_BODY_MAX_BYTES) return undefined;
330
- // First char `{` or `[` → JSON path. Most clients (Apollo, urql,
331
- // Relay) send `{"query":"…","operationName":"…","variables":{…}}`
332
- // or an array of such objects (batched).
333
222
  const first = body.charCodeAt(0);
334
223
  if (first === 0x7b /* { */ || first === 0x5b /* [ */) {
335
224
  try {
@@ -340,8 +229,6 @@ export function parseGqlOpName(body: string): string | undefined {
340
229
  if (typeof name === 'string' && name.length > 0 && name.length <= 200) {
341
230
  return name;
342
231
  }
343
- // No operationName — try to sniff the `query` string for
344
- // `query Foo {…}` / `mutation Bar {…}` / `subscription Baz {…}`.
345
232
  const q = (candidate as { query?: unknown }).query;
346
233
  if (typeof q === 'string') return parseQueryStringOpName(q);
347
234
  }
@@ -350,17 +237,14 @@ export function parseGqlOpName(body: string): string | undefined {
350
237
  }
351
238
  return undefined;
352
239
  }
353
- // `application/graphql` body is the bare query string — no JSON wrapper.
354
240
  return parseQueryStringOpName(body);
355
241
  }
356
242
 
357
243
  function parseQueryStringOpName(query: string): string | undefined {
358
- // Skip leading whitespace + comments. We only need the first non-trivial
359
- // top-level operation keyword to extract a name; nested operations are
360
- // a non-issue because GraphQL forbids them.
361
- const m = /^\s*(?:#[^\n]*\n\s*)*(query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(
362
- query,
363
- );
244
+ const m =
245
+ /^\s*(?:#[^\n]*\n\s*)*(query|mutation|subscription)\s+([A-Za-z_][A-Za-z0-9_]*)/.exec(
246
+ query,
247
+ );
364
248
  return m?.[2];
365
249
  }
366
250
 
@@ -1,6 +1,6 @@
1
1
  import { coerceError } from '@goliapkg/sentori-core';
2
2
 
3
- import { captureError } from '../capture';
3
+ import { error as captureError } from '../verbs';
4
4
 
5
5
  type RejectionTracker = (opts: {
6
6
  allRejections: boolean;
@@ -25,7 +25,6 @@
25
25
  // - On any failure we silently return null. The error event still
26
26
  // goes to the server; the user just doesn't see a thumbnail.
27
27
 
28
- import { getRegisteredMaskQuery } from '../mask';
29
28
  import { captureNativeScreenshotWithMask } from '../native';
30
29
 
31
30
  /** What `captureScreenshot()` hands back when it succeeds. */
@@ -51,16 +50,9 @@ export async function captureScreenshot(): Promise<ScreenshotBlob | null> {
51
50
  // the host never called `registerMaskQuery`, no mask is applied
52
51
  // and the full screenshot ships — sane default: SDK does nothing
53
52
  // unless told to.
54
- const query = getRegisteredMaskQuery();
55
- let maskedIds: string[] = [];
56
- if (query) {
57
- try {
58
- maskedIds = query();
59
- } catch {
60
- // A throwing query is the host's bug, not ours; skip mask
61
- // rather than skip the screenshot.
62
- maskedIds = [];
63
- }
53
+ // Masking will return with a v1 privacy pass; nothing masks today.
54
+ const maskedIds: string[] = [];
55
+ {
64
56
  }
65
57
 
66
58
  const result = await captureNativeScreenshotWithMask(maskedIds);