@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.
- package/MIGRATION.md +131 -0
- package/README.md +40 -30
- package/android/build.gradle +21 -0
- package/android/src/main/java/com/sentori/SentoriCrashHandler.kt +29 -0
- package/android/src/main/java/com/sentori/SentoriModule.kt +8 -0
- package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +36 -4
- package/android/src/test/java/com/sentori/SentoriCrashHandlerTest.kt +7 -2
- package/android/src/test/resources/robolectric.properties +10 -0
- package/ios/SentoriCrashHandler.swift +48 -4
- package/ios/SentoriModule.swift +12 -0
- package/ios/SentoriScreenshotCapture.swift +34 -3
- package/lib/config.d.ts +17 -47
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js +3 -0
- package/lib/config.js.map +1 -1
- package/lib/device.d.ts +5 -0
- package/lib/device.d.ts.map +1 -0
- package/lib/device.js +29 -0
- package/lib/device.js.map +1 -0
- package/lib/emit-hooks.d.ts +7 -0
- package/lib/emit-hooks.d.ts.map +1 -0
- package/lib/emit-hooks.js +24 -0
- package/lib/emit-hooks.js.map +1 -0
- package/lib/error-boundary.d.ts.map +1 -1
- package/lib/error-boundary.js +1 -1
- package/lib/error-boundary.js.map +1 -1
- package/lib/feature-flags.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.d.ts +1 -1
- package/lib/handlers/dev-symbolicate.d.ts.map +1 -1
- package/lib/handlers/dev-symbolicate.js +2 -0
- package/lib/handlers/dev-symbolicate.js.map +1 -1
- package/lib/handlers/global.js +1 -1
- package/lib/handlers/global.js.map +1 -1
- package/lib/handlers/lifecycle.js +3 -3
- package/lib/handlers/lifecycle.js.map +1 -1
- package/lib/handlers/network.d.ts +2 -2
- package/lib/handlers/network.d.ts.map +1 -1
- package/lib/handlers/network.js +69 -187
- package/lib/handlers/network.js.map +1 -1
- package/lib/handlers/promise.js +1 -1
- package/lib/handlers/promise.js.map +1 -1
- package/lib/handlers/screenshot.d.ts.map +1 -1
- package/lib/handlers/screenshot.js +3 -12
- package/lib/handlers/screenshot.js.map +1 -1
- package/lib/index.d.ts +32 -109
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +54 -103
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +3 -150
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +100 -261
- package/lib/init.js.map +1 -1
- package/lib/long-task-monitor.d.ts +1 -7
- package/lib/long-task-monitor.d.ts.map +1 -1
- package/lib/long-task-monitor.js +37 -58
- package/lib/long-task-monitor.js.map +1 -1
- package/lib/mask.d.ts +5 -13
- package/lib/mask.d.ts.map +1 -1
- package/lib/mask.js +29 -30
- package/lib/mask.js.map +1 -1
- package/lib/mobile-vitals.d.ts +3 -31
- package/lib/mobile-vitals.d.ts.map +1 -1
- package/lib/mobile-vitals.js +22 -61
- package/lib/mobile-vitals.js.map +1 -1
- package/lib/native-pending.d.ts +3 -0
- package/lib/native-pending.d.ts.map +1 -0
- package/lib/native-pending.js +64 -0
- package/lib/native-pending.js.map +1 -0
- package/lib/native.d.ts +4 -11
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +23 -0
- package/lib/native.js.map +1 -1
- package/lib/navigation.d.ts +3 -17
- package/lib/navigation.d.ts.map +1 -1
- package/lib/navigation.js +32 -133
- package/lib/navigation.js.map +1 -1
- package/lib/netinfo.d.ts +1 -1
- package/lib/netinfo.d.ts.map +1 -1
- package/lib/netinfo.js +2 -2
- package/lib/netinfo.js.map +1 -1
- package/lib/push.d.ts.map +1 -1
- package/lib/push.js +2 -4
- package/lib/push.js.map +1 -1
- package/lib/rage-tap.d.ts.map +1 -1
- package/lib/rage-tap.js +56 -23
- package/lib/rage-tap.js.map +1 -1
- package/lib/replay-screens.d.ts +15 -0
- package/lib/replay-screens.d.ts.map +1 -0
- package/lib/replay-screens.js +87 -0
- package/lib/replay-screens.js.map +1 -0
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +3 -24
- package/lib/replay.js.map +1 -1
- package/lib/responsiveness-detector.d.ts +18 -0
- package/lib/responsiveness-detector.d.ts.map +1 -0
- package/lib/responsiveness-detector.js +55 -0
- package/lib/responsiveness-detector.js.map +1 -0
- package/lib/scope.d.ts +7 -0
- package/lib/scope.d.ts.map +1 -0
- package/lib/scope.js +41 -0
- package/lib/scope.js.map +1 -0
- package/lib/transport.d.ts +19 -92
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +75 -288
- package/lib/transport.js.map +1 -1
- package/lib/verbs.d.ts +16 -0
- package/lib/verbs.d.ts.map +1 -0
- package/lib/verbs.js +153 -0
- package/lib/verbs.js.map +1 -0
- package/package.json +4 -3
- package/src/__tests__/iron-rule.test.ts +128 -0
- package/src/__tests__/replay-screens.test.ts +63 -0
- package/src/__tests__/responsiveness-detector.test.ts +74 -0
- package/src/__tests__/screenshot.test.ts +0 -2
- package/src/__tests__/transport.test.ts +66 -213
- package/src/__tests__/verbs.test.ts +91 -0
- package/src/config.ts +20 -47
- package/src/device.ts +36 -0
- package/src/emit-hooks.ts +30 -0
- package/src/error-boundary.tsx +1 -1
- package/src/handlers/dev-symbolicate.ts +2 -1
- package/src/handlers/global.ts +1 -1
- package/src/handlers/lifecycle.ts +3 -3
- package/src/handlers/network.ts +95 -211
- package/src/handlers/promise.ts +1 -1
- package/src/handlers/screenshot.ts +3 -11
- package/src/index.ts +84 -215
- package/src/init.ts +115 -449
- package/src/long-task-monitor.ts +42 -64
- package/src/mask.ts +29 -31
- package/src/mobile-vitals.ts +22 -82
- package/src/native-pending.ts +92 -0
- package/src/native.ts +33 -0
- package/src/navigation.ts +34 -142
- package/src/netinfo.ts +3 -3
- package/src/push.ts +2 -5
- package/src/rage-tap.tsx +72 -25
- package/src/replay-screens.ts +95 -0
- package/src/replay.ts +3 -20
- package/src/responsiveness-detector.ts +66 -0
- package/src/scope.ts +50 -0
- package/src/transport.ts +84 -371
- package/src/verbs.ts +198 -0
- package/lib/breadcrumbs.d.ts +0 -18
- package/lib/breadcrumbs.d.ts.map +0 -1
- package/lib/breadcrumbs.js +0 -42
- package/lib/breadcrumbs.js.map +0 -1
- package/lib/bundle-info.d.ts +0 -12
- package/lib/bundle-info.d.ts.map +0 -1
- package/lib/bundle-info.js +0 -73
- package/lib/bundle-info.js.map +0 -1
- package/lib/capture.d.ts +0 -140
- package/lib/capture.d.ts.map +0 -1
- package/lib/capture.js +0 -511
- package/lib/capture.js.map +0 -1
- package/lib/compat/sentry.d.ts +0 -126
- package/lib/compat/sentry.d.ts.map +0 -1
- package/lib/compat/sentry.js +0 -332
- package/lib/compat/sentry.js.map +0 -1
- package/lib/control-channel.d.ts +0 -6
- package/lib/control-channel.d.ts.map +0 -1
- package/lib/control-channel.js +0 -96
- package/lib/control-channel.js.map +0 -1
- package/lib/expo-compat.d.ts +0 -270
- package/lib/expo-compat.d.ts.map +0 -1
- package/lib/expo-compat.js +0 -500
- package/lib/expo-compat.js.map +0 -1
- package/lib/feedback-widget.d.ts +0 -35
- package/lib/feedback-widget.d.ts.map +0 -1
- package/lib/feedback-widget.js +0 -193
- package/lib/feedback-widget.js.map +0 -1
- package/lib/feedback.d.ts +0 -2
- package/lib/feedback.d.ts.map +0 -1
- package/lib/feedback.js +0 -17
- package/lib/feedback.js.map +0 -1
- package/lib/heartbeat.d.ts +0 -9
- package/lib/heartbeat.d.ts.map +0 -1
- package/lib/heartbeat.js +0 -138
- package/lib/heartbeat.js.map +0 -1
- package/lib/index.js.bak +0 -64
- package/lib/install-id.d.ts +0 -17
- package/lib/install-id.d.ts.map +0 -1
- package/lib/install-id.js +0 -125
- package/lib/install-id.js.map +0 -1
- package/lib/lifecycle.d.ts +0 -28
- package/lib/lifecycle.d.ts.map +0 -1
- package/lib/lifecycle.js +0 -73
- package/lib/lifecycle.js.map +0 -1
- package/lib/measure.d.ts +0 -4
- package/lib/measure.d.ts.map +0 -1
- package/lib/measure.js +0 -25
- package/lib/measure.js.map +0 -1
- package/lib/metrics.d.ts +0 -24
- package/lib/metrics.d.ts.map +0 -1
- package/lib/metrics.js +0 -81
- package/lib/metrics.js.map +0 -1
- package/lib/pre-crash-sentinel.d.ts +0 -12
- package/lib/pre-crash-sentinel.d.ts.map +0 -1
- package/lib/pre-crash-sentinel.js +0 -116
- package/lib/pre-crash-sentinel.js.map +0 -1
- package/lib/report-security.d.ts +0 -40
- package/lib/report-security.d.ts.map +0 -1
- package/lib/report-security.js +0 -159
- package/lib/report-security.js.map +0 -1
- package/lib/runtime-metrics-fps.d.ts +0 -19
- package/lib/runtime-metrics-fps.d.ts.map +0 -1
- package/lib/runtime-metrics-fps.js +0 -128
- package/lib/runtime-metrics-fps.js.map +0 -1
- package/lib/runtime-metrics-heap.d.ts +0 -13
- package/lib/runtime-metrics-heap.d.ts.map +0 -1
- package/lib/runtime-metrics-heap.js +0 -67
- package/lib/runtime-metrics-heap.js.map +0 -1
- package/lib/runtime-metrics-network.d.ts +0 -23
- package/lib/runtime-metrics-network.d.ts.map +0 -1
- package/lib/runtime-metrics-network.js +0 -99
- package/lib/runtime-metrics-network.js.map +0 -1
- package/lib/runtime-metrics.d.ts +0 -43
- package/lib/runtime-metrics.d.ts.map +0 -1
- package/lib/runtime-metrics.js +0 -115
- package/lib/runtime-metrics.js.map +0 -1
- package/lib/sample-profiler.d.ts +0 -21
- package/lib/sample-profiler.d.ts.map +0 -1
- package/lib/sample-profiler.js +0 -152
- package/lib/sample-profiler.js.map +0 -1
- package/lib/sentinel-context.d.ts +0 -4
- package/lib/sentinel-context.d.ts.map +0 -1
- package/lib/sentinel-context.js +0 -52
- package/lib/sentinel-context.js.map +0 -1
- package/lib/session-tracker.d.ts +0 -6
- package/lib/session-tracker.d.ts.map +0 -1
- package/lib/session-tracker.js +0 -50
- package/lib/session-tracker.js.map +0 -1
- package/lib/stack.d.ts +0 -3
- package/lib/stack.d.ts.map +0 -1
- package/lib/stack.js +0 -7
- package/lib/stack.js.map +0 -1
- package/lib/state-snapshots.d.ts +0 -33
- package/lib/state-snapshots.d.ts.map +0 -1
- package/lib/state-snapshots.js +0 -109
- package/lib/state-snapshots.js.map +0 -1
- package/lib/track.d.ts +0 -34
- package/lib/track.d.ts.map +0 -1
- package/lib/track.js +0 -107
- package/lib/track.js.map +0 -1
- package/lib/trail.d.ts +0 -12
- package/lib/trail.d.ts.map +0 -1
- package/lib/trail.js +0 -28
- package/lib/trail.js.map +0 -1
- package/lib/trust-score.d.ts +0 -20
- package/lib/trust-score.d.ts.map +0 -1
- package/lib/trust-score.js +0 -151
- package/lib/trust-score.js.map +0 -1
- package/lib/types.d.ts +0 -2
- package/lib/types.d.ts.map +0 -1
- package/lib/types.js +0 -4
- package/lib/types.js.map +0 -1
- package/lib/uuid.d.ts +0 -2
- package/lib/uuid.d.ts.map +0 -1
- package/lib/uuid.js +0 -3
- package/lib/uuid.js.map +0 -1
- package/src/__tests__/before-send.test.ts +0 -72
- package/src/__tests__/breadcrumbs.test.ts +0 -44
- package/src/__tests__/capture-replay.test.ts +0 -150
- package/src/__tests__/compat-sentry.test.ts +0 -121
- package/src/__tests__/control-channel.test.ts +0 -111
- package/src/__tests__/install-id.test.ts +0 -60
- package/src/__tests__/long-task-monitor.test.ts +0 -59
- package/src/__tests__/measure.test.ts +0 -45
- package/src/__tests__/navigation.test.ts +0 -175
- package/src/__tests__/push.test.ts +0 -238
- package/src/__tests__/report-security.test.ts +0 -106
- package/src/__tests__/runtime-metrics-instruments.test.ts +0 -109
- package/src/__tests__/runtime-metrics-network.test.ts +0 -78
- package/src/__tests__/sample-profiler.test.ts +0 -59
- package/src/__tests__/stack.test.ts +0 -45
- package/src/__tests__/state-snapshots.test.ts +0 -119
- package/src/__tests__/tracing.test.ts +0 -201
- package/src/__tests__/track.test.ts +0 -91
- package/src/__tests__/uuid.test.ts +0 -41
- package/src/breadcrumbs.ts +0 -57
- package/src/bundle-info.ts +0 -95
- package/src/capture.ts +0 -660
- package/src/compat/sentry.ts +0 -490
- package/src/control-channel.ts +0 -98
- package/src/expo-compat.ts +0 -698
- package/src/feedback-widget.tsx +0 -316
- package/src/feedback.ts +0 -21
- package/src/heartbeat.ts +0 -157
- package/src/install-id.ts +0 -146
- package/src/lifecycle.ts +0 -76
- package/src/measure.ts +0 -28
- package/src/metrics.ts +0 -94
- package/src/pre-crash-sentinel.ts +0 -140
- package/src/report-security.ts +0 -165
- package/src/runtime-metrics-fps.ts +0 -137
- package/src/runtime-metrics-heap.ts +0 -78
- package/src/runtime-metrics-network.ts +0 -103
- package/src/runtime-metrics.ts +0 -122
- package/src/sample-profiler.ts +0 -169
- package/src/sentinel-context.ts +0 -60
- package/src/session-tracker.ts +0 -53
- package/src/stack.ts +0 -10
- package/src/state-snapshots.ts +0 -133
- package/src/track.ts +0 -123
- package/src/trail.ts +0 -32
- package/src/trust-score.ts +0 -176
- package/src/types.ts +0 -21
- package/src/uuid.ts +0 -2
package/src/handlers/network.ts
CHANGED
|
@@ -1,27 +1,35 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
6
|
-
|
|
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
|
|
24
|
-
// every
|
|
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 = (
|
|
56
|
-
(
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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,
|
|
96
|
-
|
|
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
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
// ──
|
|
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 })
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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('
|
|
168
|
+
this.addEventListener('load', () => finish());
|
|
169
|
+
this.addEventListener('error', () => finish('error'));
|
|
170
|
+
this.addEventListener('timeout', () => finish('error'));
|
|
242
171
|
this.addEventListener('abort', () => {
|
|
243
|
-
|
|
244
|
-
|
|
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
|
-
|
|
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
|
-
// ──
|
|
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
|
-
|
|
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.
|
|
325
|
-
* `undefined` on any failure mode
|
|
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
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
package/src/handlers/promise.ts
CHANGED
|
@@ -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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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);
|