@goliapkg/sentori-react-native 0.6.0 → 0.7.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@goliapkg/sentori-react-native",
3
- "version": "0.6.0",
4
- "description": "Sentori SDK for React Native JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
3
+ "version": "0.7.0",
4
+ "description": "Sentori SDK for React Native \u2014 JS-layer error capture, native crash handlers (iOS / Android), batched transport, fetch + react-navigation tracing.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://sentori.golia.jp",
7
7
  "repository": {
@@ -69,6 +69,6 @@
69
69
  "access": "public"
70
70
  },
71
71
  "dependencies": {
72
- "@goliapkg/sentori-core": "0.5.0"
72
+ "@goliapkg/sentori-core": "0.6.0"
73
73
  }
74
74
  }
package/src/capture.ts CHANGED
@@ -1,13 +1,18 @@
1
+ import { sealTrail, shouldSample } from '@goliapkg/sentori-core';
2
+
1
3
  import { addBreadcrumb, getBreadcrumbs } from './breadcrumbs';
2
4
  import { getConfig, isInitialized } from './config';
3
5
  import { symbolicateErrorViaMetro } from './handlers/dev-symbolicate';
4
6
  import { captureScreenshot } from './handlers/screenshot';
5
7
  import { markSessionErrored } from './session-tracker';
6
8
  import { parseStack } from './stack';
9
+ import { getTrailBuffer } from './trail';
7
10
  import { enqueue, uploadAttachment } from './transport';
8
11
  import { uuidV7 } from './uuid';
9
12
  import type { App, AttachmentMeta, Device, Event, SentoriError, Tags, User } from './types';
10
13
 
14
+ export { captureStep, __resetTrailForTests } from './trail';
15
+
11
16
  declare const __DEV__: boolean | undefined;
12
17
 
13
18
  let _user: User | null = null;
@@ -60,6 +65,14 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
60
65
  const config = getConfig();
61
66
  if (!config) return;
62
67
 
68
+ // Phase 44 sub-B: client-side sampling. Skip the whole pipeline
69
+ // (no screenshot capture either) when the sample dice come up
70
+ // wrong. Default rate = null = keep, so existing callers unaffected.
71
+ if (!shouldSample(config.errorSampleRate)) {
72
+ addBreadcrumb({ type: 'custom', data: { reason: 'sampled-out', kind: 'error' } });
73
+ return;
74
+ }
75
+
63
76
  const event: Event = {
64
77
  id: uuidV7(),
65
78
  timestamp: new Date().toISOString(),
@@ -97,11 +110,49 @@ export const captureError = (error: Error, extras?: CaptureExtras): void => {
97
110
  if (wantScreenshot) {
98
111
  await captureAndAttachScreenshot(event);
99
112
  }
113
+ const trail = getTrailBuffer();
114
+ if (config.sessionTrailEnabled && trail.size() > 0) {
115
+ await captureAndAttachSessionTrail(event);
116
+ }
100
117
  enqueue(event);
101
118
  };
102
119
  void pipeline();
103
120
  };
104
121
 
122
+ /**
123
+ * Phase 46 — seal the trail buffer, upload it as a `sessionTrail`
124
+ * attachment, attach the ref. Best-effort: any failure leaves a
125
+ * breadcrumb and lets the event ship without the trail.
126
+ *
127
+ * The trail is **always cleared** after `captureException`, even if
128
+ * upload fails — we don't want a stale 30-step buffer leaking into
129
+ * the next crash's trail.
130
+ */
131
+ async function captureAndAttachSessionTrail(event: Event): Promise<void> {
132
+ const trail = getTrailBuffer();
133
+ const payload = sealTrail(trail);
134
+ trail.clear();
135
+ const json = JSON.stringify(payload);
136
+ // base64 the JSON for the `data:` URI multipart bridge (same
137
+ // trick the screenshot path uses).
138
+ const base64 =
139
+ typeof globalThis.btoa === 'function'
140
+ ? globalThis.btoa(unescape(encodeURIComponent(json)))
141
+ : Buffer.from(json, 'utf-8').toString('base64');
142
+ const attachment = await uploadAttachment(
143
+ event.id,
144
+ 'sessionTrail',
145
+ { base64, mediaType: 'application/json' },
146
+ { source: 'js' },
147
+ );
148
+ if (!attachment) {
149
+ addBreadcrumb({ type: 'custom', data: { reason: 'session-trail-upload-failed' } });
150
+ return;
151
+ }
152
+ if (!event.attachments) event.attachments = [];
153
+ event.attachments.push(attachment);
154
+ }
155
+
105
156
  export const captureException = captureError;
106
157
 
107
158
  /** Phase 42 sub-D.08: per-session screenshot quota gate. */
package/src/config.ts CHANGED
@@ -6,6 +6,14 @@ export type Config = {
6
6
  enabled: boolean;
7
7
  /** Phase 42 sub-D.07: opt-in screenshot capture on captureException. */
8
8
  screenshotsEnabled: boolean;
9
+ /** Phase 44 sub-B: per-event-class sampling rates 0..1.
10
+ * `null` = keep everything (default). */
11
+ errorSampleRate: null | number;
12
+ traceSampleRate: null | number;
13
+ /** Phase 46: when true, every `captureException` seals the
14
+ * session-trail buffer and uploads it as a `sessionTrail`
15
+ * attachment. Defaults to false. */
16
+ sessionTrailEnabled: boolean;
9
17
  };
10
18
 
11
19
  let _config: Config | null = null;
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { init } from './init';
2
2
  import { addBreadcrumb } from './breadcrumbs';
3
- import { setUser, getUser, captureError, captureException } from './capture';
3
+ import { setUser, getUser, captureError, captureException, captureStep } from './capture';
4
4
  import { ErrorBoundary } from './error-boundary';
5
5
  import { MaskRegion, setMaskedNode, unsetMaskedNode } from './mask';
6
6
  import {
@@ -16,6 +16,7 @@ export const sentori = {
16
16
  getUser,
17
17
  captureError,
18
18
  captureException,
19
+ captureStep,
19
20
  ErrorBoundary,
20
21
  MaskRegion,
21
22
  setMaskedNode,
@@ -29,7 +30,13 @@ export default sentori;
29
30
 
30
31
  export { init, init as initSentori } from './init';
31
32
  export { addBreadcrumb } from './breadcrumbs';
32
- export { setUser, getUser, captureError, captureException } from './capture';
33
+ export {
34
+ captureError,
35
+ captureException,
36
+ captureStep,
37
+ getUser,
38
+ setUser,
39
+ } from './capture';
33
40
  export { ErrorBoundary } from './error-boundary';
34
41
  export { MaskRegion, setMaskedNode, unsetMaskedNode } from './mask';
35
42
  export {
package/src/init.ts CHANGED
@@ -5,8 +5,13 @@ import { installPromiseHandler } from './handlers/promise';
5
5
  import { installNetworkHandler } from './handlers/network';
6
6
  import { drainNativePending, setNativeConfig } from './native';
7
7
  import { startSession } from './session-tracker';
8
- import { drainOfflineQueue, enqueue, startTransport } from './transport';
9
- import type { Event } from './types';
8
+ import {
9
+ drainOfflineQueue,
10
+ enqueue,
11
+ startTransport,
12
+ uploadAttachment,
13
+ } from './transport';
14
+ import type { AttachmentKind, AttachmentMeta, AttachmentSource, Event } from './types';
10
15
 
11
16
  declare const __DEV__: boolean | undefined;
12
17
 
@@ -33,6 +38,21 @@ export type InitOptions = {
33
38
  * installed and `<MaskRegion>` placed over any sensitive UI. The
34
39
  * image is webp q=70 480 px max, < 100 KB typical. */
35
40
  screenshot?: boolean;
41
+ /** Phase 46: record the last N steps (route changes, custom
42
+ * breadcrumbs) leading up to a crash. On `captureException`
43
+ * the buffer is sealed and uploaded as a `sessionTrail`
44
+ * attachment. Defaults to false. */
45
+ sessionTrail?: boolean;
46
+ };
47
+ /** Phase 44 sub-B: client-side sampling. Each rate is `[0, 1]`;
48
+ * absent / null keeps everything. Defaults to 1.0 for both
49
+ * (no drop). Set traces to e.g. 0.1 once the app's at user
50
+ * volume to keep ingest budget under control without changing
51
+ * the server-side quota. Decisions are made per-event for
52
+ * errors and per-trace (all spans together) for traces. */
53
+ sampling?: {
54
+ errors?: null | number;
55
+ traces?: null | number;
36
56
  };
37
57
  };
38
58
 
@@ -57,6 +77,9 @@ export const init = (options: InitOptions): void => {
57
77
  ingestUrl: options.ingestUrl ?? DEFAULT_INGEST_URL,
58
78
  enabled: true,
59
79
  screenshotsEnabled: options.capture?.screenshot === true,
80
+ errorSampleRate: options.sampling?.errors ?? null,
81
+ traceSampleRate: options.sampling?.traces ?? null,
82
+ sessionTrailEnabled: options.capture?.sessionTrail === true,
60
83
  });
61
84
 
62
85
  // Tell the native crash handler about the config so the JSON it writes
@@ -85,10 +108,33 @@ export const init = (options: InitOptions): void => {
85
108
  // - native crashes from <Documents>/sentori/pending/*.json
86
109
  // - JS transport offline queue from AsyncStorage
87
110
  drainNativePending()
88
- .then((items) => {
111
+ .then(async (items) => {
89
112
  for (const json of items) {
90
113
  try {
91
- enqueue(JSON.parse(json) as Event);
114
+ const event = JSON.parse(json) as Event & {
115
+ _pendingAttachments?: PendingAttachment[];
116
+ };
117
+ // Phase 42 sub-E.05 / F.09: the native crash handler couldn't
118
+ // upload attachments at crash time (the app was dying); it
119
+ // base64-encoded them into `_pendingAttachments` instead.
120
+ // On next launch we upload each before enqueueing the event,
121
+ // so the dashboard sees the refs in `event.attachments[]`.
122
+ if (event._pendingAttachments && event._pendingAttachments.length > 0) {
123
+ for (const p of event._pendingAttachments) {
124
+ const meta = await uploadAttachment(
125
+ event.id,
126
+ p.kind,
127
+ { base64: p.base64, mediaType: p.mediaType },
128
+ { source: p.source },
129
+ );
130
+ if (meta) {
131
+ if (!event.attachments) event.attachments = [];
132
+ event.attachments.push(meta);
133
+ }
134
+ }
135
+ delete event._pendingAttachments;
136
+ }
137
+ enqueue(event);
92
138
  } catch {
93
139
  // skip malformed
94
140
  }
@@ -97,3 +143,20 @@ export const init = (options: InitOptions): void => {
97
143
  .catch(() => {});
98
144
  drainOfflineQueue().catch(() => {});
99
145
  };
146
+
147
+ /**
148
+ * Phase 42 sub-E.05: shape of each entry in the native crash JSON's
149
+ * `_pendingAttachments` array. Mirrors what
150
+ * `SentoriCrashHandler.write` writes on iOS and (sub-F) what
151
+ * `SentoriCrashWriter` writes on Android.
152
+ */
153
+ type PendingAttachment = {
154
+ base64: string;
155
+ kind: AttachmentKind;
156
+ mediaType: string;
157
+ source: AttachmentSource;
158
+ };
159
+
160
+ // Keep AttachmentMeta in the imports — it's part of the public type
161
+ // surface re-exported from this module's bundle.
162
+ export type { AttachmentMeta };
package/src/navigation.ts CHANGED
@@ -24,6 +24,8 @@ import { useEffect, useRef } from 'react';
24
24
 
25
25
  import { setActiveSpan, startSpan, type SpanHandle } from '@goliapkg/sentori-core';
26
26
 
27
+ import { captureStep } from './trail';
28
+
27
29
  /** Minimal contract: anything with `addListener('state', cb)` and
28
30
  * `getCurrentRoute()` works. The real @react-navigation/native
29
31
  * NavigationContainer ref matches this shape. */
@@ -72,6 +74,12 @@ export function useTraceNavigation(navigationRef: NavigationRefLike): void {
72
74
  openSpanRef.current = span;
73
75
  setActiveSpan(span);
74
76
  lastRouteRef.current = to;
77
+ // Phase 46 — record the screen transition into the session trail.
78
+ // No-op when sessionTrail isn't enabled (the buffer just grows
79
+ // unbounded until cleared by captureException, but stays cheap).
80
+ captureStep(`screen:${to}`, {
81
+ breadcrumb: { type: 'navigation', message: from ? `${from} → ${to}` : to },
82
+ });
75
83
  };
76
84
 
77
85
  // Open a span for the initial screen so its requests are grouped
package/src/trail.ts ADDED
@@ -0,0 +1,32 @@
1
+ // Phase 46 — singleton TrailBuffer for the RN SDK.
2
+ //
3
+ // Kept in its own module so callers (including navigation.ts, which
4
+ // is intentionally lightweight) can record steps without pulling in
5
+ // capture.ts → handlers/screenshot.ts → react-native. The buffer is
6
+ // drained inside capture.ts when an event captures and
7
+ // `sessionTrailEnabled` is on.
8
+
9
+ import { TrailBuffer, type TrailStep } from '@goliapkg/sentori-core';
10
+
11
+ const _trail = new TrailBuffer(30);
12
+
13
+ /**
14
+ * Phase 46 — record a step into the session-trail buffer. The buffer
15
+ * is a fixed-size FIFO (default 30 steps); pushing past capacity
16
+ * drops the oldest entry. Steps are only uploaded if
17
+ * `init({ capture: { sessionTrail: true } })` is on AND a
18
+ * `captureException` follows.
19
+ */
20
+ export const captureStep = (label: string, opts?: Partial<TrailStep>): void => {
21
+ _trail.push({
22
+ ts: Date.now(),
23
+ label,
24
+ ...(opts ?? {}),
25
+ });
26
+ };
27
+
28
+ export const getTrailBuffer = (): TrailBuffer => _trail;
29
+
30
+ export const __resetTrailForTests = (): void => {
31
+ _trail.clear();
32
+ };