@goliapkg/sentori-react-native 1.1.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/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;
@@ -17,6 +36,21 @@ export type Config = {
17
36
  * session-trail buffer and uploads it as a `sessionTrail`
18
37
  * attachment. Defaults to false. */
19
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;
20
54
  };
21
55
 
22
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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/init.ts CHANGED
@@ -1,4 +1,6 @@
1
- import { setConfig } from './config';
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';
@@ -128,6 +130,16 @@ export type InitOptions = {
128
130
  * via `sentori.captureMessage()`. `null` / absent → keep all. */
129
131
  messages?: null | number;
130
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;
131
143
  };
132
144
 
133
145
  const DEFAULT_INGEST_URL = 'https://ingest.sentori.golia.jp';
@@ -140,6 +152,11 @@ export const init = (options: InitOptions): void => {
140
152
  throw new Error('Sentori: release is required');
141
153
  }
142
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
+
143
160
  const env =
144
161
  options.environment ??
145
162
  (typeof __DEV__ !== 'undefined' && __DEV__ ? 'dev' : 'prod');
@@ -316,8 +333,44 @@ export const init = (options: InitOptions): void => {
316
333
  void markLaunchCompleted(getBundleInfo()?.id ?? null);
317
334
  }, 2_000);
318
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
+ }
319
368
  };
320
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
+
321
374
  /**
322
375
  * Phase 42 sub-E.05: shape of each entry in the native crash JSON's
323
376
  * `_pendingAttachments` array. Mirrors what
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
- // eslint-disable-next-line no-console
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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 && typeof __DEV__ !== 'undefined' && __DEV__) {
315
- // eslint-disable-next-line no-console
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
103
- // eslint-disable-next-line no-console
104
- console.warn(
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
127
- // eslint-disable-next-line no-console
128
- console.warn(
129
- '[sentori] replay: scheduled tick period=', period, 'ms keyframe=', _keyframeIntervalMs, 'ms',
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 (typeof __DEV__ !== 'undefined' && __DEV__ && !_firstTickLogged) {
161
- // eslint-disable-next-line no-console
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- // eslint-disable-next-line no-console
309
- console.warn(
310
- '[sentori] replay tick: native returned',
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 ticks so far: ${_emptyTickCount})`,
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
- // eslint-disable-next-line no-console
330
- console.warn(
331
- `[sentori] replay tick: thin result nodes=${nodeCount} sizeBytes=${snapshotBytes} (thin ticks so far: ${_thinTickCount})`,
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
- // eslint-disable-next-line no-console
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
- if (typeof __DEV__ !== 'undefined' && __DEV__) {
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
  };