@goliapkg/sentori-react-native 1.0.0 → 1.2.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/lib/breadcrumbs.d.ts.map +1 -1
- package/lib/breadcrumbs.js +2 -5
- package/lib/breadcrumbs.js.map +1 -1
- package/lib/capture.d.ts +64 -1
- package/lib/capture.d.ts.map +1 -1
- package/lib/capture.js +122 -50
- package/lib/capture.js.map +1 -1
- package/lib/config.d.ts +38 -0
- package/lib/config.d.ts.map +1 -1
- package/lib/config.js.map +1 -1
- package/lib/heartbeat.d.ts.map +1 -1
- package/lib/heartbeat.js +2 -4
- package/lib/heartbeat.js.map +1 -1
- package/lib/index.d.ts +18 -4
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +15 -4
- package/lib/index.js.map +1 -1
- package/lib/init.d.ts +14 -0
- package/lib/init.d.ts.map +1 -1
- package/lib/init.js +42 -0
- package/lib/init.js.map +1 -1
- package/lib/lifecycle.d.ts +28 -0
- package/lib/lifecycle.d.ts.map +1 -0
- package/lib/lifecycle.js +73 -0
- package/lib/lifecycle.js.map +1 -0
- package/lib/metrics.d.ts +18 -1
- package/lib/metrics.d.ts.map +1 -1
- package/lib/metrics.js +19 -2
- package/lib/metrics.js.map +1 -1
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +10 -29
- package/lib/native.js.map +1 -1
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +12 -33
- package/lib/replay.js.map +1 -1
- package/lib/transport.d.ts.map +1 -1
- package/lib/transport.js +4 -13
- package/lib/transport.js.map +1 -1
- package/package.json +3 -3
- package/src/breadcrumbs.ts +2 -5
- package/src/capture.ts +170 -46
- package/src/config.ts +37 -0
- package/src/heartbeat.ts +3 -4
- package/src/index.ts +35 -2
- package/src/init.ts +58 -1
- package/src/lifecycle.ts +76 -0
- package/src/metrics.ts +19 -1
- package/src/native.ts +11 -35
- package/src/replay.ts +25 -44
- package/src/transport.ts +4 -16
package/src/config.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
import type { LogLevel } from '@goliapkg/sentori-core';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional structured signal handed to `onReady` after init
|
|
5
|
+
* completes. Host wires the callback if they want to know the SDK
|
|
6
|
+
* is live (alternative to scanning console).
|
|
7
|
+
*/
|
|
8
|
+
export type ReadyInfo = {
|
|
9
|
+
/** npm version string of @goliapkg/sentori-react-native */
|
|
10
|
+
sdkVersion: string;
|
|
11
|
+
/** Milliseconds between RN cold-start signal and SDK init
|
|
12
|
+
* completion. May be undefined if native module wasn't bound. */
|
|
13
|
+
coldStartMs?: number;
|
|
14
|
+
/** Native module status. `bound: false` means screenshot /
|
|
15
|
+
* wireframe / native crash capture won't fire — useful for
|
|
16
|
+
* host to know if e.g. they forgot to autolink. */
|
|
17
|
+
native: { bound: boolean; methods: string[] };
|
|
18
|
+
};
|
|
19
|
+
|
|
1
20
|
export type Config = {
|
|
2
21
|
token: string;
|
|
3
22
|
release: string;
|
|
@@ -10,10 +29,28 @@ export type Config = {
|
|
|
10
29
|
* `null` = keep everything (default). */
|
|
11
30
|
errorSampleRate: null | number;
|
|
12
31
|
traceSampleRate: null | number;
|
|
32
|
+
/** v2.0 — sampling rate for `kind: 'message'` events emitted via
|
|
33
|
+
* `captureMessage`. `null` = keep all (default). */
|
|
34
|
+
messageSampleRate: null | number;
|
|
13
35
|
/** Phase 46: when true, every `captureException` seals the
|
|
14
36
|
* session-trail buffer and uploads it as a `sessionTrail`
|
|
15
37
|
* attachment. Defaults to false. */
|
|
16
38
|
sessionTrailEnabled: boolean;
|
|
39
|
+
/** v2.3 — Sentori console output gate.
|
|
40
|
+
*
|
|
41
|
+
* Default `warn`: SDK is silent on host's console unless
|
|
42
|
+
* something is genuinely broken (transport sustained failure,
|
|
43
|
+
* native module not found, internal SDK exception). No
|
|
44
|
+
* per-tick / per-init / per-breadcrumb noise.
|
|
45
|
+
*
|
|
46
|
+
* Set `'silent'` for absolute silence (e.g. CI smoke runs);
|
|
47
|
+
* set `'info'` or `'debug'` when debugging Sentori itself. */
|
|
48
|
+
logLevel?: LogLevel;
|
|
49
|
+
/** v2.3 — fires once after init completes. Use this to know the
|
|
50
|
+
* SDK is live instead of scanning the console. `info` carries
|
|
51
|
+
* the native-module bind status + cold-start timing. Host
|
|
52
|
+
* wraps any host-side logging here. */
|
|
53
|
+
onReady?: (info: ReadyInfo) => void;
|
|
17
54
|
};
|
|
18
55
|
|
|
19
56
|
let _config: Config | null = null;
|
package/src/heartbeat.ts
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
// (transport-batched); the heartbeat exists *during* the session to
|
|
20
20
|
// signal presence.
|
|
21
21
|
|
|
22
|
+
import { logger } from '@goliapkg/sentori-core';
|
|
23
|
+
|
|
22
24
|
import { getConfig } from './config';
|
|
23
25
|
import { getUser } from './capture';
|
|
24
26
|
import { getLastRoute } from './navigation';
|
|
@@ -134,10 +136,7 @@ async function send(): Promise<void> {
|
|
|
134
136
|
method: 'POST',
|
|
135
137
|
});
|
|
136
138
|
} catch (e) {
|
|
137
|
-
|
|
138
|
-
// eslint-disable-next-line no-console
|
|
139
|
-
console.warn('[sentori] heartbeat failed (best-effort)', e);
|
|
140
|
-
}
|
|
139
|
+
logger.debug('heartbeat', 'failed (best-effort, normal on offline)', e);
|
|
141
140
|
}
|
|
142
141
|
}
|
|
143
142
|
|
package/src/index.ts
CHANGED
|
@@ -3,9 +3,12 @@ import { addBreadcrumb } from './breadcrumbs';
|
|
|
3
3
|
import {
|
|
4
4
|
captureError,
|
|
5
5
|
captureException,
|
|
6
|
+
captureMessage,
|
|
6
7
|
captureStep,
|
|
7
8
|
getUser,
|
|
8
9
|
sendUserFeedback,
|
|
10
|
+
setTag,
|
|
11
|
+
setTags,
|
|
9
12
|
setUser,
|
|
10
13
|
} from './capture';
|
|
11
14
|
import { ErrorBoundary } from './error-boundary';
|
|
@@ -24,9 +27,15 @@ import {
|
|
|
24
27
|
type TimeToFullDisplayHandle,
|
|
25
28
|
} from './mobile-vitals';
|
|
26
29
|
import { bindState, recordState, unbindState } from './state-snapshots';
|
|
27
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
startMoment,
|
|
32
|
+
startSpan,
|
|
33
|
+
startTrace,
|
|
34
|
+
withScopedSpan,
|
|
35
|
+
} from '@goliapkg/sentori-core';
|
|
28
36
|
import { getInstallId } from './install-id';
|
|
29
37
|
import { flushMetrics, recordMetric } from './metrics';
|
|
38
|
+
import { close, flush } from './lifecycle';
|
|
30
39
|
import { linkFederatedIdentity, reportPinMismatch, reportSecurity } from './report-security';
|
|
31
40
|
import { flushTrack, track } from './track';
|
|
32
41
|
import { queryTrustScore } from './trust-score';
|
|
@@ -42,8 +51,11 @@ export const sentori = {
|
|
|
42
51
|
addBreadcrumb,
|
|
43
52
|
setUser,
|
|
44
53
|
getUser,
|
|
54
|
+
setTag,
|
|
55
|
+
setTags,
|
|
45
56
|
captureError,
|
|
46
57
|
captureException,
|
|
58
|
+
captureMessage,
|
|
47
59
|
captureStep,
|
|
48
60
|
sendUserFeedback,
|
|
49
61
|
recordMetric,
|
|
@@ -57,6 +69,9 @@ export const sentori = {
|
|
|
57
69
|
linkFederatedIdentity,
|
|
58
70
|
measureFn,
|
|
59
71
|
startMoment,
|
|
72
|
+
startSpan,
|
|
73
|
+
startTrace,
|
|
74
|
+
withScopedSpan,
|
|
60
75
|
bindState,
|
|
61
76
|
recordState,
|
|
62
77
|
unbindState,
|
|
@@ -74,6 +89,8 @@ export const sentori = {
|
|
|
74
89
|
startSession,
|
|
75
90
|
endSession,
|
|
76
91
|
markSessionCrashed,
|
|
92
|
+
flush,
|
|
93
|
+
close,
|
|
77
94
|
};
|
|
78
95
|
|
|
79
96
|
export default sentori;
|
|
@@ -83,11 +100,27 @@ export { addBreadcrumb } from './breadcrumbs';
|
|
|
83
100
|
export {
|
|
84
101
|
captureError,
|
|
85
102
|
captureException,
|
|
103
|
+
captureMessage,
|
|
86
104
|
captureStep,
|
|
87
105
|
getUser,
|
|
88
106
|
sendUserFeedback,
|
|
107
|
+
setTag,
|
|
108
|
+
setTags,
|
|
89
109
|
setUser,
|
|
90
110
|
} from './capture';
|
|
111
|
+
export {
|
|
112
|
+
startMoment,
|
|
113
|
+
startSpan,
|
|
114
|
+
startTrace,
|
|
115
|
+
withScopedSpan,
|
|
116
|
+
type SpanContextLike,
|
|
117
|
+
type StartSpanOptions,
|
|
118
|
+
} from '@goliapkg/sentori-core';
|
|
119
|
+
export { close, flush } from './lifecycle';
|
|
120
|
+
export type {
|
|
121
|
+
CaptureMessageOptions,
|
|
122
|
+
MessageLevel,
|
|
123
|
+
} from '@goliapkg/sentori-core';
|
|
91
124
|
export { ErrorBoundary } from './error-boundary';
|
|
92
125
|
export { FeedbackButton, type FeedbackButtonHandle, type FeedbackButtonProps } from './feedback-widget';
|
|
93
126
|
export {
|
|
@@ -117,7 +150,7 @@ export {
|
|
|
117
150
|
markTimeToFullDisplay,
|
|
118
151
|
type TimeToFullDisplayHandle,
|
|
119
152
|
} from './mobile-vitals';
|
|
120
|
-
export { MomentHandle, type MomentProperties
|
|
153
|
+
export { MomentHandle, type MomentProperties } from '@goliapkg/sentori-core';
|
|
121
154
|
export {
|
|
122
155
|
bindState,
|
|
123
156
|
recordState,
|
package/src/init.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { setLogLevel } from '@goliapkg/sentori-core';
|
|
2
|
+
|
|
3
|
+
import { type ReadyInfo, setConfig } from './config';
|
|
2
4
|
import { installGlobalHandler } from './handlers/global';
|
|
3
5
|
import { installLifecycleHandler } from './handlers/lifecycle';
|
|
4
6
|
import { installPromiseHandler } from './handlers/promise';
|
|
@@ -124,7 +126,20 @@ export type InitOptions = {
|
|
|
124
126
|
sampling?: {
|
|
125
127
|
errors?: null | number;
|
|
126
128
|
traces?: null | number;
|
|
129
|
+
/** v2.0 — sampling rate for `kind: 'message'` events emitted
|
|
130
|
+
* via `sentori.captureMessage()`. `null` / absent → keep all. */
|
|
131
|
+
messages?: null | number;
|
|
127
132
|
};
|
|
133
|
+
/** v2.3 — Sentori SDK's own console output gate. Default `'warn'`:
|
|
134
|
+
* SDK is silent unless something is genuinely broken (transport
|
|
135
|
+
* sustained failure, native module not found, internal SDK
|
|
136
|
+
* exception). Set to `'silent'` for absolute silence; bump to
|
|
137
|
+
* `'info'` / `'debug'` when debugging Sentori itself. */
|
|
138
|
+
logLevel?: import('@goliapkg/sentori-core').LogLevel;
|
|
139
|
+
/** v2.3 — fires once after init completes. Use this to know the
|
|
140
|
+
* SDK is live instead of scanning the console. The `ReadyInfo`
|
|
141
|
+
* carries native-module bind status + cold-start timing. */
|
|
142
|
+
onReady?: (info: ReadyInfo) => void;
|
|
128
143
|
};
|
|
129
144
|
|
|
130
145
|
const DEFAULT_INGEST_URL = 'https://ingest.sentori.golia.jp';
|
|
@@ -137,6 +152,11 @@ export const init = (options: InitOptions): void => {
|
|
|
137
152
|
throw new Error('Sentori: release is required');
|
|
138
153
|
}
|
|
139
154
|
|
|
155
|
+
// v2.3 — set log level FIRST so any startup-time logger calls
|
|
156
|
+
// are gated correctly. Default 'warn' from logger.ts; an explicit
|
|
157
|
+
// host setting overrides.
|
|
158
|
+
setLogLevel(options.logLevel);
|
|
159
|
+
|
|
140
160
|
const env =
|
|
141
161
|
options.environment ??
|
|
142
162
|
(typeof __DEV__ !== 'undefined' && __DEV__ ? 'dev' : 'prod');
|
|
@@ -163,6 +183,7 @@ export const init = (options: InitOptions): void => {
|
|
|
163
183
|
screenshotsEnabled: options.capture?.screenshot === true,
|
|
164
184
|
errorSampleRate: options.sampling?.errors ?? null,
|
|
165
185
|
traceSampleRate: options.sampling?.traces ?? null,
|
|
186
|
+
messageSampleRate: options.sampling?.messages ?? null,
|
|
166
187
|
sessionTrailEnabled: options.capture?.sessionTrail === true,
|
|
167
188
|
});
|
|
168
189
|
|
|
@@ -312,8 +333,44 @@ export const init = (options: InitOptions): void => {
|
|
|
312
333
|
void markLaunchCompleted(getBundleInfo()?.id ?? null);
|
|
313
334
|
}, 2_000);
|
|
314
335
|
}
|
|
336
|
+
|
|
337
|
+
// v2.3 — onReady callback. Fires after setConfig + native bind +
|
|
338
|
+
// transport start are all settled. The drain-pending work is
|
|
339
|
+
// still in flight (it's async) but the SDK is ready to accept
|
|
340
|
+
// new captures. Host wires this to know the SDK is live without
|
|
341
|
+
// scanning the console. Wrapped in try/catch — host callback
|
|
342
|
+
// throwing must not propagate (NEVER rule).
|
|
343
|
+
if (options.onReady) {
|
|
344
|
+
const nativeMod = (() => {
|
|
345
|
+
try {
|
|
346
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
347
|
+
const { native } = require('./native');
|
|
348
|
+
const n = native?.();
|
|
349
|
+
return n ?? null;
|
|
350
|
+
} catch {
|
|
351
|
+
return null;
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
const info: ReadyInfo = {
|
|
355
|
+
sdkVersion: SDK_VERSION,
|
|
356
|
+
coldStartMs: coldMs ?? undefined,
|
|
357
|
+
native: {
|
|
358
|
+
bound: !!nativeMod,
|
|
359
|
+
methods: nativeMod ? Object.keys(nativeMod).sort() : [],
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
try {
|
|
363
|
+
options.onReady(info);
|
|
364
|
+
} catch {
|
|
365
|
+
// Host's onReady threw. NEVER rule — swallow.
|
|
366
|
+
}
|
|
367
|
+
}
|
|
315
368
|
};
|
|
316
369
|
|
|
370
|
+
// Bumped on each SDK release; surfaced in onReady payload + the
|
|
371
|
+
// future Sentry-compat layer's identification string.
|
|
372
|
+
const SDK_VERSION = '2.3.0';
|
|
373
|
+
|
|
317
374
|
/**
|
|
318
375
|
* Phase 42 sub-E.05: shape of each entry in the native crash JSON's
|
|
319
376
|
* `_pendingAttachments` array. Mirrors what
|
package/src/lifecycle.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// v2.0 W3 — top-level lifecycle: flush / close.
|
|
2
|
+
//
|
|
3
|
+
// Sentry / OTel parity: a single `flush(timeoutMs?)` that drains
|
|
4
|
+
// every in-flight buffer (events / metrics / track), gated by a
|
|
5
|
+
// timeout so a slow network doesn't hang the host's shutdown path.
|
|
6
|
+
//
|
|
7
|
+
// Use before short-lived process exit (CLI / serverless function /
|
|
8
|
+
// fixture cleanup) to ensure pending captures land before the
|
|
9
|
+
// process dies.
|
|
10
|
+
|
|
11
|
+
import { flush as flushTransport } from './transport';
|
|
12
|
+
import { flushMetrics } from './metrics';
|
|
13
|
+
import { flushTrack } from './track';
|
|
14
|
+
import { reportInternal } from '@goliapkg/sentori-core';
|
|
15
|
+
|
|
16
|
+
let _closed = false;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Force-flush every pending Sentori buffer (events, metrics,
|
|
20
|
+
* track). Returns when the flush settles or the timeout fires —
|
|
21
|
+
* whichever happens first.
|
|
22
|
+
*
|
|
23
|
+
* Never rejects: per the NEVER rule, individual flush failures
|
|
24
|
+
* are silently absorbed (and self-reported via the internal
|
|
25
|
+
* circuit-breaker), and the resolved promise's value is undefined.
|
|
26
|
+
*
|
|
27
|
+
* await sentori.flush(5_000) // wait up to 5 s, then move on
|
|
28
|
+
* process.exit(0)
|
|
29
|
+
*/
|
|
30
|
+
export async function flush(timeoutMs: number = 5_000): Promise<void> {
|
|
31
|
+
if (_closed) return;
|
|
32
|
+
try {
|
|
33
|
+
const drainAll = Promise.all([
|
|
34
|
+
flushTransport().catch((err) => {
|
|
35
|
+
reportInternal('flush.transport', err);
|
|
36
|
+
}),
|
|
37
|
+
flushMetrics().catch((err) => {
|
|
38
|
+
reportInternal('flush.metrics', err);
|
|
39
|
+
}),
|
|
40
|
+
flushTrack().catch((err) => {
|
|
41
|
+
reportInternal('flush.track', err);
|
|
42
|
+
}),
|
|
43
|
+
]).then(() => undefined);
|
|
44
|
+
const timer = new Promise<void>((resolve) => setTimeout(resolve, timeoutMs));
|
|
45
|
+
await Promise.race([drainAll, timer]);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
// Belt-and-braces: even though each branch already has its own
|
|
48
|
+
// catch, the wrapping race shouldn't be able to surface a
|
|
49
|
+
// failure to the host either. NEVER rule.
|
|
50
|
+
reportInternal('flush', err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Flush + shut down. After `close()`, further capture* calls remain
|
|
56
|
+
* silent no-ops via `_closed` gate. Idempotent — re-calling is safe.
|
|
57
|
+
*
|
|
58
|
+
* await sentori.close()
|
|
59
|
+
* // SDK is asleep; no more events go out.
|
|
60
|
+
*/
|
|
61
|
+
export async function close(timeoutMs?: number): Promise<void> {
|
|
62
|
+
await flush(timeoutMs);
|
|
63
|
+
_closed = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Test hook — reset the shutdown latch so unit tests don't have to
|
|
67
|
+
* re-init the whole SDK between cases. */
|
|
68
|
+
export function __resetLifecycleForTests(): void {
|
|
69
|
+
_closed = false;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** Internal — capture* implementations check this to short-circuit
|
|
73
|
+
* after `close()`. */
|
|
74
|
+
export function isClosed(): boolean {
|
|
75
|
+
return _closed;
|
|
76
|
+
}
|
package/src/metrics.ts
CHANGED
|
@@ -27,16 +27,34 @@ const FLUSH_INTERVAL_MS = 30_000;
|
|
|
27
27
|
let _buf: Point[] = [];
|
|
28
28
|
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
29
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Record a numeric metric point. Cheap to call from a render hook —
|
|
32
|
+
* pushes into a 500-slot ring drained every 30 s (or on overflow).
|
|
33
|
+
*
|
|
34
|
+
* v2.0 — accepts an optional `parent: SpanContextLike` so a metric
|
|
35
|
+
* can be correlated to a span. The dashboard span-detail view joins
|
|
36
|
+
* metric points by `tags.span_id` / `tags.trace_id`.
|
|
37
|
+
*
|
|
38
|
+
* const span = sentori.startSpan({ name: 'db.query users' })
|
|
39
|
+
* sentori.recordMetric('db.query.duration_ms', 42, undefined, { parent: span })
|
|
40
|
+
* span.end({ status: 'ok' })
|
|
41
|
+
*/
|
|
30
42
|
export function recordMetric(
|
|
31
43
|
name: string,
|
|
32
44
|
value: number,
|
|
33
45
|
tags?: Record<string, string>,
|
|
46
|
+
opts?: { parent?: { spanId: string; traceId: string } },
|
|
34
47
|
): void {
|
|
35
48
|
if (!isInitialized()) return;
|
|
36
49
|
if (typeof name !== 'string' || name.length === 0 || name.length > 200) return;
|
|
37
50
|
if (typeof value !== 'number' || !Number.isFinite(value)) return;
|
|
38
51
|
if (tags && Object.keys(tags).length > 20) return;
|
|
39
|
-
|
|
52
|
+
// Merge span-context tags so the dashboard can join metric points
|
|
53
|
+
// to the span that produced them without a separate schema column.
|
|
54
|
+
const finalTags: Record<string, string> | undefined = opts?.parent
|
|
55
|
+
? { ...(tags ?? {}), span_id: opts.parent.spanId, trace_id: opts.parent.traceId }
|
|
56
|
+
: tags;
|
|
57
|
+
_buf.push({ name, tags: finalTags, ts: new Date().toISOString(), value });
|
|
40
58
|
if (_buf.length >= MAX_BUFFER) {
|
|
41
59
|
void flushMetrics();
|
|
42
60
|
}
|
package/src/native.ts
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* this keeps the SDK usable in pure-JS environments (jest, bun test, web).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import { logger } from '@goliapkg/sentori-core'
|
|
8
|
+
|
|
7
9
|
declare const __DEV__: boolean | undefined
|
|
8
10
|
|
|
9
11
|
type SentoriNativeModule = {
|
|
@@ -139,15 +141,11 @@ function native(): SentoriNativeModule | null {
|
|
|
139
141
|
// distinguish "pod is stale" from "method exists but throws
|
|
140
142
|
// at runtime" in one log line.
|
|
141
143
|
const keys = Object.keys(_native as object).sort()
|
|
142
|
-
|
|
143
|
-
console.warn('[sentori] native module bound; exposed methods:', keys.join(', ') || '(none)')
|
|
144
|
+
logger.debug('native', 'module bound; methods:', keys.join(', ') || '(none)')
|
|
144
145
|
}
|
|
145
146
|
} catch (e) {
|
|
146
147
|
_native = null
|
|
147
|
-
|
|
148
|
-
// eslint-disable-next-line no-console
|
|
149
|
-
console.warn('[sentori] requireNativeModule("Sentori") threw', e)
|
|
150
|
-
}
|
|
148
|
+
logger.error('native', 'requireNativeModule("Sentori") threw', e)
|
|
151
149
|
}
|
|
152
150
|
return _native
|
|
153
151
|
}
|
|
@@ -292,37 +290,21 @@ export async function captureNativeScreenshotWithMask(
|
|
|
292
290
|
): Promise<null | { base64: string; mediaType: string }> {
|
|
293
291
|
const n = native()
|
|
294
292
|
if (!n) {
|
|
295
|
-
|
|
296
|
-
// eslint-disable-next-line no-console
|
|
297
|
-
console.warn(
|
|
298
|
-
'[sentori] native module not bound — requireNativeModule("Sentori") threw',
|
|
299
|
-
)
|
|
300
|
-
}
|
|
293
|
+
logger.warn('native', 'module not bound — requireNativeModule("Sentori") threw')
|
|
301
294
|
return null
|
|
302
295
|
}
|
|
303
296
|
if (!n.captureScreenshotWithMask) {
|
|
304
|
-
|
|
305
|
-
// eslint-disable-next-line no-console
|
|
306
|
-
console.warn(
|
|
307
|
-
'[sentori] native.captureScreenshotWithMask missing — pod install may be stale',
|
|
308
|
-
)
|
|
309
|
-
}
|
|
297
|
+
logger.warn('native', 'captureScreenshotWithMask missing — pod install may be stale')
|
|
310
298
|
return null
|
|
311
299
|
}
|
|
312
300
|
try {
|
|
313
301
|
const r = await n.captureScreenshotWithMask(maskedIds)
|
|
314
|
-
if (!r
|
|
315
|
-
|
|
316
|
-
console.warn(
|
|
317
|
-
'[sentori] native screenshot returned null — no key window / render failed',
|
|
318
|
-
)
|
|
302
|
+
if (!r) {
|
|
303
|
+
logger.debug('native', 'screenshot returned null — no key window / render failed')
|
|
319
304
|
}
|
|
320
305
|
return r
|
|
321
306
|
} catch (e) {
|
|
322
|
-
|
|
323
|
-
// eslint-disable-next-line no-console
|
|
324
|
-
console.warn('[sentori] native screenshot threw', e)
|
|
325
|
-
}
|
|
307
|
+
logger.warn('native', 'screenshot threw', e)
|
|
326
308
|
return null
|
|
327
309
|
}
|
|
328
310
|
}
|
|
@@ -413,10 +395,7 @@ export function probeNativeWireframe(): {
|
|
|
413
395
|
windowCount: typeof r?.windowCount === 'number' ? r.windowCount : 0,
|
|
414
396
|
}
|
|
415
397
|
} catch (e) {
|
|
416
|
-
|
|
417
|
-
// eslint-disable-next-line no-console
|
|
418
|
-
console.warn('[sentori] probeWireframe threw', e)
|
|
419
|
-
}
|
|
398
|
+
logger.warn('native', 'probeWireframe threw', e)
|
|
420
399
|
return {
|
|
421
400
|
available: false,
|
|
422
401
|
lastDepthMax: 0,
|
|
@@ -472,10 +451,7 @@ export function probeNativeScreenshot(): {
|
|
|
472
451
|
const lastPath = typeof raw.lastPath === 'string' ? raw.lastPath : 'unknown'
|
|
473
452
|
return { available: true, lastPath, raw }
|
|
474
453
|
} catch (e) {
|
|
475
|
-
|
|
476
|
-
// eslint-disable-next-line no-console
|
|
477
|
-
console.warn('[sentori] probeScreenshot threw', e)
|
|
478
|
-
}
|
|
454
|
+
logger.warn('native', 'probeScreenshot threw', e)
|
|
479
455
|
return { available: false, lastPath: 'native.threw', raw: {} }
|
|
480
456
|
}
|
|
481
457
|
}
|
package/src/replay.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
//
|
|
16
16
|
// Wire schema: docs/replay-encoding-v2.md.
|
|
17
17
|
|
|
18
|
-
import { startSpan } from '@goliapkg/sentori-core';
|
|
18
|
+
import { logger, startSpan } from '@goliapkg/sentori-core';
|
|
19
19
|
|
|
20
20
|
import { getRegisteredMaskQuery } from './mask';
|
|
21
21
|
import { describeWireframeNative } from './native';
|
|
@@ -99,22 +99,16 @@ export function startReplay(opts: ReplayOptions): void {
|
|
|
99
99
|
if (opts.mode !== 'wireframe') return;
|
|
100
100
|
const info = describeWireframeNative();
|
|
101
101
|
if (!info.bound) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
'[sentori] replay: Sentori native module not bound (expo-modules-core) — replay attachments will stay empty',
|
|
106
|
-
);
|
|
107
|
-
}
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
111
|
-
// eslint-disable-next-line no-console
|
|
112
|
-
console.warn(
|
|
113
|
-
'[sentori] replay: starting',
|
|
114
|
-
'bound=', info.bound,
|
|
115
|
-
'hasCaptureWireframe=', info.hasCaptureWireframe,
|
|
102
|
+
logger.warn(
|
|
103
|
+
'replay',
|
|
104
|
+
'native module not bound (expo-modules-core); replay attachments will stay empty',
|
|
116
105
|
);
|
|
106
|
+
return;
|
|
117
107
|
}
|
|
108
|
+
logger.debug(
|
|
109
|
+
'replay',
|
|
110
|
+
'starting; bound=', info.bound, 'hasCaptureWireframe=', info.hasCaptureWireframe,
|
|
111
|
+
);
|
|
118
112
|
_running = true;
|
|
119
113
|
_nativeMod = loadNativeReplay();
|
|
120
114
|
_keyframeIntervalMs = opts.keyframeMs ?? KEYFRAME_INTERVAL_MS;
|
|
@@ -123,12 +117,10 @@ export function startReplay(opts: ReplayOptions): void {
|
|
|
123
117
|
_timer = setInterval(() => {
|
|
124
118
|
captureTick();
|
|
125
119
|
}, period);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
);
|
|
131
|
-
}
|
|
120
|
+
logger.debug(
|
|
121
|
+
'replay',
|
|
122
|
+
'scheduled; tick period=', period, 'ms keyframe=', _keyframeIntervalMs, 'ms',
|
|
123
|
+
);
|
|
132
124
|
}
|
|
133
125
|
|
|
134
126
|
export function stopReplay(): void {
|
|
@@ -157,9 +149,8 @@ const THIN_RESULT_NODES = 6;
|
|
|
157
149
|
|
|
158
150
|
function captureTick(): void {
|
|
159
151
|
if (!_running) return;
|
|
160
|
-
if (
|
|
161
|
-
|
|
162
|
-
console.warn('[sentori] replay tick: FIRST INVOCATION');
|
|
152
|
+
if (!_firstTickLogged) {
|
|
153
|
+
logger.debug('replay', 'tick: first invocation');
|
|
163
154
|
_firstTickLogged = true;
|
|
164
155
|
}
|
|
165
156
|
let tickSpan: ReturnType<typeof startSpan> | null = null;
|
|
@@ -181,10 +172,7 @@ function captureTick(): void {
|
|
|
181
172
|
try {
|
|
182
173
|
snapshot = JSON.parse(snapshotJson) as NativeFrame;
|
|
183
174
|
} catch (e) {
|
|
184
|
-
|
|
185
|
-
// eslint-disable-next-line no-console
|
|
186
|
-
console.warn('[sentori] replay tick: native JSON parse failed', e);
|
|
187
|
-
}
|
|
175
|
+
logger.warn('replay', 'tick: native JSON parse failed', e);
|
|
188
176
|
tickSpan?.finish({ status: 'error' });
|
|
189
177
|
return;
|
|
190
178
|
}
|
|
@@ -201,10 +189,7 @@ function captureTick(): void {
|
|
|
201
189
|
} catch (e) {
|
|
202
190
|
if (e instanceof Error) tickSpan?.setTag('error.message', e.message);
|
|
203
191
|
tickSpan?.finish({ status: 'error' });
|
|
204
|
-
|
|
205
|
-
// eslint-disable-next-line no-console
|
|
206
|
-
console.warn('[sentori] replay tick: threw', e);
|
|
207
|
-
}
|
|
192
|
+
logger.warn('replay', 'tick threw', e);
|
|
208
193
|
}
|
|
209
194
|
}
|
|
210
195
|
|
|
@@ -302,18 +287,17 @@ export function computeDelta(prev: Map<string, Node>, curr: Map<string, Node>):
|
|
|
302
287
|
}
|
|
303
288
|
|
|
304
289
|
function handleEmptyTick(snapshot: unknown): void {
|
|
305
|
-
if (typeof __DEV__ === 'undefined' || !__DEV__) return;
|
|
306
290
|
_emptyTickCount += 1;
|
|
307
291
|
if (_emptyTickCount === 1 || _emptyTickCount === _emptyTickLogStride) {
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
'
|
|
292
|
+
logger.debug(
|
|
293
|
+
'replay',
|
|
294
|
+
'tick empty — native returned',
|
|
311
295
|
snapshot === null
|
|
312
296
|
? 'null'
|
|
313
297
|
: typeof snapshot === 'string'
|
|
314
298
|
? `empty (length=${snapshot.length})`
|
|
315
299
|
: typeof snapshot,
|
|
316
|
-
`(empty
|
|
300
|
+
`(empty so far: ${_emptyTickCount})`,
|
|
317
301
|
);
|
|
318
302
|
_emptyTickLogStride = Math.max(_emptyTickLogStride * 10, 10);
|
|
319
303
|
}
|
|
@@ -326,9 +310,9 @@ function diagnosticForTick(snapshot: NativeFrame, snapshotBytes: number): void {
|
|
|
326
310
|
if (isThin) {
|
|
327
311
|
_thinTickCount += 1;
|
|
328
312
|
if (_thinTickCount === 1 || _thinTickCount === _thinTickLogStride) {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`
|
|
313
|
+
logger.debug(
|
|
314
|
+
'replay',
|
|
315
|
+
`tick thin: nodes=${nodeCount} sizeBytes=${snapshotBytes} (thin so far: ${_thinTickCount})`,
|
|
332
316
|
);
|
|
333
317
|
_thinTickLogStride = Math.max(_thinTickLogStride * 10, 10);
|
|
334
318
|
}
|
|
@@ -337,10 +321,7 @@ function diagnosticForTick(snapshot: NativeFrame, snapshotBytes: number): void {
|
|
|
337
321
|
_thinTickLogStride = 1;
|
|
338
322
|
}
|
|
339
323
|
if (_okTickCount === 1) {
|
|
340
|
-
|
|
341
|
-
console.warn(
|
|
342
|
-
`[sentori] replay tick: first ok — nodes=${nodeCount} sizeBytes=${snapshotBytes}`,
|
|
343
|
-
);
|
|
324
|
+
logger.debug('replay', `first ok tick — nodes=${nodeCount} sizeBytes=${snapshotBytes}`);
|
|
344
325
|
}
|
|
345
326
|
}
|
|
346
327
|
|
package/src/transport.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { addBreadcrumb, drainSpans } from '@goliapkg/sentori-core';
|
|
1
|
+
import { addBreadcrumb, drainSpans, logger } from '@goliapkg/sentori-core';
|
|
2
2
|
|
|
3
3
|
import { getConfig } from './config';
|
|
4
4
|
import { isLiveMode } from './control-channel';
|
|
@@ -430,15 +430,11 @@ export const uploadAttachment = async (
|
|
|
430
430
|
// doesn't have to guess between 413/422/500. Pre-rc.6 only
|
|
431
431
|
// the breadcrumb carried the reason; logcat only saw the
|
|
432
432
|
// generic `upload returned null` line.
|
|
433
|
-
|
|
434
|
-
// eslint-disable-next-line no-console
|
|
435
|
-
console.warn(
|
|
436
|
-
'[sentori] attachment upload non-2xx',
|
|
433
|
+
logger.debug('transport', 'attachment upload non-2xx',
|
|
437
434
|
'eventId=', eventId,
|
|
438
435
|
'kind=', kind,
|
|
439
436
|
'status=', resp.status,
|
|
440
437
|
);
|
|
441
|
-
}
|
|
442
438
|
return null;
|
|
443
439
|
}
|
|
444
440
|
const j = (await resp.json().catch(() => null)) as null | {
|
|
@@ -449,15 +445,11 @@ export const uploadAttachment = async (
|
|
|
449
445
|
};
|
|
450
446
|
if (!j || !j.refId) {
|
|
451
447
|
noteAttachmentFailure(eventId, kind, 'bad_response_body');
|
|
452
|
-
|
|
453
|
-
// eslint-disable-next-line no-console
|
|
454
|
-
console.warn(
|
|
455
|
-
'[sentori] attachment upload bad-response-body',
|
|
448
|
+
logger.debug('transport', 'attachment upload bad-response-body',
|
|
456
449
|
'eventId=', eventId,
|
|
457
450
|
'kind=', kind,
|
|
458
451
|
'status=', resp.status,
|
|
459
452
|
);
|
|
460
|
-
}
|
|
461
453
|
return null;
|
|
462
454
|
}
|
|
463
455
|
return {
|
|
@@ -470,15 +462,11 @@ export const uploadAttachment = async (
|
|
|
470
462
|
} catch (e) {
|
|
471
463
|
const reason = e instanceof Error ? `fetch_${e.name}` : 'fetch_unknown';
|
|
472
464
|
noteAttachmentFailure(eventId, kind, reason);
|
|
473
|
-
|
|
474
|
-
// eslint-disable-next-line no-console
|
|
475
|
-
console.warn(
|
|
476
|
-
'[sentori] attachment upload fetch threw',
|
|
465
|
+
logger.debug('transport', 'attachment upload fetch threw',
|
|
477
466
|
'eventId=', eventId,
|
|
478
467
|
'kind=', kind,
|
|
479
468
|
'reason=', reason,
|
|
480
469
|
);
|
|
481
|
-
}
|
|
482
470
|
return null;
|
|
483
471
|
}
|
|
484
472
|
};
|