@goliapkg/sentori-react-native 0.9.11 → 1.0.0-rc.2
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/android/src/main/java/com/sentori/SentoriForegroundActivity.kt +145 -0
- package/android/src/main/java/com/sentori/SentoriModule.kt +13 -0
- package/android/src/main/java/com/sentori/SentoriReplayCapture.kt +41 -18
- package/android/src/main/java/com/sentori/SentoriScreenshotCapture.kt +72 -36
- package/ios/SentoriModule.swift +15 -0
- package/ios/SentoriReplayCapture.swift +103 -9
- package/ios/SentoriScreenshotCapture.swift +69 -3
- package/lib/index.d.ts +2 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -1
- package/lib/index.js.bak +64 -0
- package/lib/index.js.map +1 -1
- package/lib/native.d.ts +57 -0
- package/lib/native.d.ts.map +1 -1
- package/lib/native.js +99 -0
- package/lib/native.js.map +1 -1
- package/lib/replay.d.ts.map +1 -1
- package/lib/replay.js +75 -8
- package/lib/replay.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +3 -0
- package/src/native.ts +136 -0
- package/src/replay.ts +75 -7
package/src/native.ts
CHANGED
|
@@ -67,6 +67,31 @@ type SentoriNativeModule = {
|
|
|
67
67
|
* snapshot string or null on failure.
|
|
68
68
|
*/
|
|
69
69
|
captureWireframe?: (maskedIds: string[]) => null | string
|
|
70
|
+
/**
|
|
71
|
+
* v0.9.12 — diagnostic readout for the wireframe path. Cheap
|
|
72
|
+
* synchronous call that returns the path the last `captureWireframe`
|
|
73
|
+
* tick took plus scene/window counts at that moment. Used by the
|
|
74
|
+
* example app's debug button and the Insight verify flow to tell
|
|
75
|
+
* "no window resolvable" from "window walked but tree empty" without
|
|
76
|
+
* shipping a new pod.
|
|
77
|
+
*/
|
|
78
|
+
probeWireframe?: () => {
|
|
79
|
+
lastPath: string
|
|
80
|
+
lastNodes: number
|
|
81
|
+
sceneCount?: number
|
|
82
|
+
windowCount?: number
|
|
83
|
+
trackedSource?: string
|
|
84
|
+
trackedActivity?: string
|
|
85
|
+
decorViewFound?: boolean
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* v1.0.0-rc.2 — diagnostic mirror of probeWireframe for the
|
|
89
|
+
* screenshot path. Returned shape is best-effort cross-platform —
|
|
90
|
+
* Android carries `trackedActivity` / `decorViewFound` / dims,
|
|
91
|
+
* iOS carries `windowFound` / `rootViewControllerFound` / `bounds*`.
|
|
92
|
+
* Callers should treat unknown keys as missing.
|
|
93
|
+
*/
|
|
94
|
+
probeScreenshot?: () => Record<string, unknown>
|
|
70
95
|
/**
|
|
71
96
|
* Phase 22 sub-D / sub-E: cross-platform main-thread watchdog.
|
|
72
97
|
* Android: 5 s / 1 s defaults (matches the OS ANR threshold).
|
|
@@ -295,10 +320,121 @@ export async function captureNativeScreenshotWithMask(
|
|
|
295
320
|
export function describeWireframeNative(): {
|
|
296
321
|
bound: boolean
|
|
297
322
|
hasCaptureWireframe: boolean
|
|
323
|
+
hasProbeWireframe: boolean
|
|
298
324
|
} {
|
|
299
325
|
const n = native()
|
|
300
326
|
return {
|
|
301
327
|
bound: n !== null,
|
|
302
328
|
hasCaptureWireframe: Boolean(n?.captureWireframe),
|
|
329
|
+
hasProbeWireframe: Boolean(n?.probeWireframe),
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* v0.9.12 — JS entry to the native `probeWireframe` diagnostic. Safe
|
|
335
|
+
* to call before the first replay tick — returns the not-yet-called
|
|
336
|
+
* sentinel. When the ring stays empty, this is the single call that
|
|
337
|
+
* answers "why" without redeploying the pod.
|
|
338
|
+
*
|
|
339
|
+
* path meaning
|
|
340
|
+
* ─────────────── ───────────────────────────────────────────────
|
|
341
|
+
* none(not-yet…) captureWireframe has never run yet
|
|
342
|
+
* scene.fg.key iOS: resolved via foregroundActive scene's key window
|
|
343
|
+
* scene.fg.first iOS: foregroundActive scene's first window (no key)
|
|
344
|
+
* scene.fgi.first iOS: foregroundInactive scene mid-transition
|
|
345
|
+
* scene.any.first iOS: had to fall back to any window
|
|
346
|
+
* legacy.first iOS: legacy UIApplication.windows path
|
|
347
|
+
* none iOS: no UIWindow reachable at the tick instant
|
|
348
|
+
* activity.null Android: no resumed Activity registered
|
|
349
|
+
* decorView.null Android: activity has no decor view yet
|
|
350
|
+
* root.zero-size Android: decorView size <= 0 (mid-layout)
|
|
351
|
+
* activity.resumed Android: ok
|
|
352
|
+
*/
|
|
353
|
+
export function probeNativeWireframe(): {
|
|
354
|
+
available: boolean
|
|
355
|
+
lastNodes: number
|
|
356
|
+
lastPath: string
|
|
357
|
+
sceneCount: number
|
|
358
|
+
windowCount: number
|
|
359
|
+
} {
|
|
360
|
+
const n = native()
|
|
361
|
+
if (!n || typeof n.probeWireframe !== 'function') {
|
|
362
|
+
return {
|
|
363
|
+
available: false,
|
|
364
|
+
lastNodes: 0,
|
|
365
|
+
lastPath: 'native.unavailable',
|
|
366
|
+
sceneCount: 0,
|
|
367
|
+
windowCount: 0,
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
try {
|
|
371
|
+
const r = n.probeWireframe()
|
|
372
|
+
return {
|
|
373
|
+
available: true,
|
|
374
|
+
lastNodes: typeof r?.lastNodes === 'number' ? r.lastNodes : 0,
|
|
375
|
+
lastPath: typeof r?.lastPath === 'string' ? r.lastPath : 'unknown',
|
|
376
|
+
sceneCount: typeof r?.sceneCount === 'number' ? r.sceneCount : 0,
|
|
377
|
+
windowCount: typeof r?.windowCount === 'number' ? r.windowCount : 0,
|
|
378
|
+
}
|
|
379
|
+
} catch (e) {
|
|
380
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
381
|
+
// eslint-disable-next-line no-console
|
|
382
|
+
console.warn('[sentori] probeWireframe threw', e)
|
|
383
|
+
}
|
|
384
|
+
return {
|
|
385
|
+
available: false,
|
|
386
|
+
lastNodes: 0,
|
|
387
|
+
lastPath: 'native.threw',
|
|
388
|
+
sceneCount: 0,
|
|
389
|
+
windowCount: 0,
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* v1.0.0-rc.2 — JS entry to the `probeScreenshot` native diagnostic.
|
|
396
|
+
* Same shape contract as [probeNativeWireframe]: returns a flat
|
|
397
|
+
* key/value bag the consumer can ship back as-is when screenshot
|
|
398
|
+
* capture returns null.
|
|
399
|
+
*
|
|
400
|
+
* path meaning
|
|
401
|
+
* ───────────────────────── ───────────────────────────────────────
|
|
402
|
+
* none(not-yet-called) captureScreenshot has never run
|
|
403
|
+
* ok capture succeeded
|
|
404
|
+
* activity.null Android: foreground tracker had no Activity
|
|
405
|
+
* window.null Android/iOS: Activity/scene has no window
|
|
406
|
+
* decorView.null Android: window had no decor view
|
|
407
|
+
* decorView.zero-size Android: decorView size <= 0 (mid-layout)
|
|
408
|
+
* api.unsupported Android: API < 24 (no PixelCopy)
|
|
409
|
+
* pixelCopy.notSuccess Android: PixelCopy completed but reported failure
|
|
410
|
+
* pixelCopy.threw:<class> Android: PixelCopy threw mid-request
|
|
411
|
+
* render.failed iOS: UIGraphicsImageRenderer returned nil
|
|
412
|
+
* empty iOS: walked tree but no view+screenshot output
|
|
413
|
+
*
|
|
414
|
+
* On Android the result also carries `trackedSource` (lifecycle.created /
|
|
415
|
+
* lifecycle.resumed / reflection.activityThread / manual.setActivity)
|
|
416
|
+
* so callers can tell whether the SDK back-filled via reflection.
|
|
417
|
+
*/
|
|
418
|
+
export function probeNativeScreenshot(): {
|
|
419
|
+
available: boolean
|
|
420
|
+
lastPath: string
|
|
421
|
+
raw: Record<string, unknown>
|
|
422
|
+
} {
|
|
423
|
+
const n = native()
|
|
424
|
+
if (!n || typeof n.probeScreenshot !== 'function') {
|
|
425
|
+
return { available: false, lastPath: 'native.unavailable', raw: {} }
|
|
426
|
+
}
|
|
427
|
+
try {
|
|
428
|
+
const r = n.probeScreenshot()
|
|
429
|
+
const raw =
|
|
430
|
+
r && typeof r === 'object' && !Array.isArray(r) ? (r as Record<string, unknown>) : {}
|
|
431
|
+
const lastPath = typeof raw.lastPath === 'string' ? raw.lastPath : 'unknown'
|
|
432
|
+
return { available: true, lastPath, raw }
|
|
433
|
+
} catch (e) {
|
|
434
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
435
|
+
// eslint-disable-next-line no-console
|
|
436
|
+
console.warn('[sentori] probeScreenshot threw', e)
|
|
437
|
+
}
|
|
438
|
+
return { available: false, lastPath: 'native.threw', raw: {} }
|
|
303
439
|
}
|
|
304
440
|
}
|
package/src/replay.ts
CHANGED
|
@@ -37,6 +37,26 @@ let _ring: string[] = [];
|
|
|
37
37
|
let _timer: ReturnType<typeof setInterval> | null = null;
|
|
38
38
|
let _running = false;
|
|
39
39
|
|
|
40
|
+
/**
|
|
41
|
+
* v0.9.13 — frame-level delta encoding: when the new snapshot matches
|
|
42
|
+
* the last one byte-for-byte (static UI, no animation, off-screen
|
|
43
|
+
* app), skip pushing it. The ring stays meaningful (one frame =
|
|
44
|
+
* one *change*), the attachment shrinks proportionally, and a real
|
|
45
|
+
* idle phase no longer evicts a useful pre-error frame.
|
|
46
|
+
*
|
|
47
|
+
* We only check against the most-recently-pushed snapshot, not the
|
|
48
|
+
* whole ring — that's cheap (one string comparison per tick) and
|
|
49
|
+
* catches the dominant case (idle screens). True content changes
|
|
50
|
+
* fall through and push as before.
|
|
51
|
+
*
|
|
52
|
+
* Budget verification on the iOS showcase (apps/ios-showcase): 60
|
|
53
|
+
* frames at ~120 bytes each → ≈ 7 KB raw NDJSON, well under the
|
|
54
|
+
* 500 KB attachment cap. Heavier RN apps with 200+ visible nodes
|
|
55
|
+
* per frame can land in the 400 KB band; future work in v1.x adds
|
|
56
|
+
* native gzip on upload if real-world traffic ever pushes the cap.
|
|
57
|
+
*/
|
|
58
|
+
let _lastPushed: null | string = null;
|
|
59
|
+
|
|
40
60
|
/** Native module ref, resolved once on first start. Caching here
|
|
41
61
|
* avoids the cost of `requireNativeModule('Sentori')` on every
|
|
42
62
|
* capture tick (Metro's require cache makes this cheap, but the
|
|
@@ -83,7 +103,20 @@ export function startReplay(opts: ReplayOptions): void {
|
|
|
83
103
|
_timer = setInterval(() => {
|
|
84
104
|
captureTick();
|
|
85
105
|
}, period);
|
|
86
|
-
|
|
106
|
+
// v0.9.12 — Insight 2026-05-17 report: 0.9.11 emitted the
|
|
107
|
+
// "starting bound=true" line then went silent. Root cause was the
|
|
108
|
+
// `.unref?.()` call that used to live here. Hermes 0.81 doesn't
|
|
109
|
+
// ship a Timer object with the Node-style `unref` method, and the
|
|
110
|
+
// optional-chained call ended up dereferencing a `prototype`
|
|
111
|
+
// property on `undefined` — throwing synchronously inside
|
|
112
|
+
// startReplay, which RN's bridge swallowed silently. Net effect:
|
|
113
|
+
// setInterval registered, captureTick never invoked. Drop the
|
|
114
|
+
// call; replay tick lifecycle is bound to the app process, no
|
|
115
|
+
// event-loop tweak needed.
|
|
116
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
117
|
+
// eslint-disable-next-line no-console
|
|
118
|
+
console.warn('[sentori] replay: scheduled tick period=', period, 'ms');
|
|
119
|
+
}
|
|
87
120
|
}
|
|
88
121
|
|
|
89
122
|
export function stopReplay(): void {
|
|
@@ -95,20 +128,49 @@ export function stopReplay(): void {
|
|
|
95
128
|
_nativeMod = null;
|
|
96
129
|
_emptyTickCount = 0;
|
|
97
130
|
_emptyTickLogStride = 1;
|
|
131
|
+
_firstTickLogged = false;
|
|
98
132
|
}
|
|
99
133
|
|
|
100
134
|
let _emptyTickCount = 0;
|
|
101
135
|
let _emptyTickLogStride = 1;
|
|
136
|
+
let _firstTickLogged = false;
|
|
102
137
|
|
|
103
138
|
function captureTick(): void {
|
|
104
139
|
if (!_running) return;
|
|
105
|
-
|
|
140
|
+
// v0.9.12 — UNCONDITIONAL first-tick log. Proves the setInterval
|
|
141
|
+
// callback is firing at all, before any other code that could
|
|
142
|
+
// throw. 0.9.11's diagnostic was inside a `else if (snapshot==null)`
|
|
143
|
+
// branch that could only surface AFTER the native call returned;
|
|
144
|
+
// useless when the bug is that the tick body never enters.
|
|
145
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__ && !_firstTickLogged) {
|
|
146
|
+
// eslint-disable-next-line no-console
|
|
147
|
+
console.warn('[sentori] replay tick: FIRST INVOCATION');
|
|
148
|
+
_firstTickLogged = true;
|
|
149
|
+
}
|
|
150
|
+
// 0.9.11 called startSpan OUTSIDE the catch block. If
|
|
151
|
+
// `@goliapkg/sentori-core` failed to initialise (or startSpan
|
|
152
|
+
// threw for any other reason on the first tick) the whole tick
|
|
153
|
+
// callback died silently. Wrap so worst case is "no span for this
|
|
154
|
+
// tick" not "no ticks for the session".
|
|
155
|
+
let tickSpan: ReturnType<typeof startSpan> | null = null;
|
|
156
|
+
try {
|
|
157
|
+
tickSpan = startSpan('sentori.replay.tick', { name: 'tick' });
|
|
158
|
+
} catch {
|
|
159
|
+
// never fatal
|
|
160
|
+
}
|
|
106
161
|
try {
|
|
107
162
|
const maskIds = readMaskIds();
|
|
108
163
|
const snapshot = _nativeMod?.captureWireframe?.(maskIds);
|
|
109
164
|
if (typeof snapshot === 'string' && snapshot.length > 0) {
|
|
110
|
-
|
|
111
|
-
|
|
165
|
+
// v0.9.13 — skip pushing if the frame is identical to the last
|
|
166
|
+
// pushed one. See _lastPushed comment for the rationale.
|
|
167
|
+
if (snapshot !== _lastPushed) {
|
|
168
|
+
_ring.push(snapshot);
|
|
169
|
+
_lastPushed = snapshot;
|
|
170
|
+
while (_ring.length > RING_SIZE) {
|
|
171
|
+
_ring.shift();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
112
174
|
_emptyTickCount = 0;
|
|
113
175
|
_emptyTickLogStride = 1;
|
|
114
176
|
} else if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
@@ -132,10 +194,14 @@ function captureTick(): void {
|
|
|
132
194
|
_emptyTickLogStride = Math.max(_emptyTickLogStride * 10, 10);
|
|
133
195
|
}
|
|
134
196
|
}
|
|
135
|
-
tickSpan
|
|
197
|
+
tickSpan?.finish({ status: 'ok' });
|
|
136
198
|
} catch (e) {
|
|
137
|
-
if (e instanceof Error) tickSpan
|
|
138
|
-
tickSpan
|
|
199
|
+
if (e instanceof Error) tickSpan?.setTag('error.message', e.message);
|
|
200
|
+
tickSpan?.finish({ status: 'error' });
|
|
201
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
202
|
+
// eslint-disable-next-line no-console
|
|
203
|
+
console.warn('[sentori] replay tick: threw', e);
|
|
204
|
+
}
|
|
139
205
|
}
|
|
140
206
|
}
|
|
141
207
|
|
|
@@ -172,10 +238,12 @@ export function drainReplay(): string {
|
|
|
172
238
|
if (_ring.length === 0) return '';
|
|
173
239
|
const out = _ring.join('\n');
|
|
174
240
|
_ring = [];
|
|
241
|
+
_lastPushed = null;
|
|
175
242
|
return out;
|
|
176
243
|
}
|
|
177
244
|
|
|
178
245
|
export function __resetReplayForTests(): void {
|
|
179
246
|
stopReplay();
|
|
180
247
|
_ring = [];
|
|
248
|
+
_lastPushed = null;
|
|
181
249
|
}
|