@drakkar.software/sunglasses-react-native 0.11.0 → 0.12.1

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/dist/index.d.mts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React$1 from 'react';
2
2
  import { ISunglassesClient, AutoCaptureErrorsOptions, ScreenTrackingOptions, CaptureExceptionOptions } from '@drakkar.software/sunglasses-core';
3
- export { AutoCaptureErrorsOptions, CaptureExceptionOptions, ConsentStatus, ConsoleCaptureOptions, ISunglassesClient, ScreenTrackingOptions, SunglassesConfig, SunglassesCore, SunglassesEvent, captureException, patchConsole } from '@drakkar.software/sunglasses-core';
3
+ export { AutoCaptureErrorsOptions, CaptureExceptionOptions, ConsentStatus, ConsoleCaptureOptions, GlobalErrorInfo, GlobalErrorListener, ISunglassesClient, ScreenTrackingOptions, SunglassesConfig, SunglassesCore, SunglassesEvent, captureException, patchConsole, publishGlobalError, subscribeGlobalError } from '@drakkar.software/sunglasses-core';
4
4
 
5
5
  interface SunglassesProviderProps {
6
6
  /** An initialized ISunglassesClient (from SunglassesCore.create()). */
@@ -10,10 +10,11 @@ interface SunglassesProviderProps {
10
10
  * (`$error_handled: false`).
11
11
  *
12
12
  * - `true` installs a global `ErrorUtils` handler (the previous handler is
13
- * preserved and still invoked).
14
- * - An options object additionally lets you toggle `globalHandlers` and opt
15
- * into `console` capture (`console.error` / `console.warn`), plus configure
16
- * truncation / stack inclusion / ignore patterns.
13
+ * preserved and still invoked) and an unhandled promise rejection handler.
14
+ * - An options object additionally lets you toggle `globalHandlers` and
15
+ * `unhandledRejections`, opt into `console` capture (`console.error` /
16
+ * `console.warn`), plus configure truncation / stack inclusion / ignore
17
+ * patterns.
17
18
  *
18
19
  * Default: off.
19
20
  */
@@ -63,26 +64,6 @@ declare function SunglassesProvider({ client, autoCaptureErrors, children, }: Su
63
64
  */
64
65
  declare function useSunglasses(): ISunglassesClient;
65
66
 
66
- /**
67
- * Expo Router screen tracking hook.
68
- *
69
- * Tracks screen changes by observing the `pathname` from Expo Router.
70
- * Place this hook inside your root `_layout.tsx`.
71
- *
72
- * **Requires**: `expo-router` to be installed in your project.
73
- *
74
- * @example
75
- * ```tsx
76
- * // app/_layout.tsx
77
- * import { useExpoRouterScreenTracking } from '@drakkar.software/sunglasses-react-native';
78
- *
79
- * export default function RootLayout() {
80
- * const client = useSunglasses();
81
- * useExpoRouterScreenTracking(client);
82
- * return <Stack />;
83
- * }
84
- * ```
85
- */
86
67
  declare function useExpoRouterScreenTracking(client: ISunglassesClient, options?: Pick<ScreenTrackingOptions, 'screenNameMapper'>): void;
87
68
 
88
69
  interface NavigationState {
@@ -227,4 +208,104 @@ interface SunglassesErrorBoundaryProps {
227
208
  */
228
209
  declare function SunglassesErrorBoundary(props: SunglassesErrorBoundaryProps): React$1.ReactElement;
229
210
 
230
- export { SunglassesErrorBoundary, type SunglassesErrorBoundaryProps, SunglassesProvider, type SunglassesProviderProps, captureDeepLinkUtmParams, useExpoRouterScreenTracking, useExpoRouterUtmCapture, useLinkingUtmCapture, useNavigationScreenTracking, useSunglasses };
211
+ interface SunglassesGlobalErrorBoundaryProps {
212
+ /**
213
+ * SunGlasses client. Optional — defaults to the client provided by the
214
+ * nearest `<SunglassesProvider>`.
215
+ */
216
+ client?: ISunglassesClient;
217
+ /** Rendered when an error is caught. Defaults to rendering nothing. */
218
+ fallback?: React$1.ReactNode;
219
+ /** Error capture configuration forwarded to `captureException`. */
220
+ config?: CaptureExceptionOptions;
221
+ /**
222
+ * Also render the fallback for non-fatal global errors (e.g. `ErrorUtils`
223
+ * errors reported as non-fatal). Default: `false`.
224
+ */
225
+ includeNonFatalGlobalErrors?: boolean;
226
+ /**
227
+ * Also render the fallback for unhandled promise rejections. Off by default
228
+ * because many apps prefer to surface rejections as toasts or inline errors
229
+ * rather than as a full-screen fallback. Default: `false`.
230
+ */
231
+ includeUnhandledRejections?: boolean;
232
+ children: React$1.ReactNode;
233
+ }
234
+ /**
235
+ * A superset of `SunglassesErrorBoundary` that renders a fallback UI for fatal
236
+ * non-render errors (uncaught `ErrorUtils` errors and, optionally, unhandled
237
+ * rejections) in addition to the render-phase errors a normal error boundary
238
+ * catches.
239
+ *
240
+ * Render-phase errors are captured here as `$error` events
241
+ * (`$error_handled: true`). Global errors are captured by the provider's
242
+ * `autoCaptureErrors` handlers and merely surfaced here as a fallback — so the
243
+ * global fallback requires `autoCaptureErrors` to be enabled on the
244
+ * `<SunglassesProvider>`. No event is captured twice.
245
+ *
246
+ * @example
247
+ * ```tsx
248
+ * <SunglassesProvider client={client} autoCaptureErrors>
249
+ * <SunglassesGlobalErrorBoundary fallback={<ErrorScreen />}>
250
+ * <App />
251
+ * </SunglassesGlobalErrorBoundary>
252
+ * </SunglassesProvider>
253
+ * ```
254
+ */
255
+ declare function SunglassesGlobalErrorBoundary(props: SunglassesGlobalErrorBoundaryProps): React$1.ReactElement;
256
+
257
+ /**
258
+ * Props Expo Router passes to a route-level `ErrorBoundary` export.
259
+ */
260
+ interface ExpoRouterErrorBoundaryProps {
261
+ /** The render error caught by Expo Router for this route. */
262
+ error: Error;
263
+ /** Re-mounts the route's components to retry rendering. */
264
+ retry: () => Promise<void>;
265
+ }
266
+ interface WrapExpoRouterErrorBoundaryOptions {
267
+ /**
268
+ * SunGlasses client. Optional — defaults to the client provided by the
269
+ * nearest `<SunglassesProvider>`.
270
+ */
271
+ client?: ISunglassesClient;
272
+ /** Error capture configuration forwarded to `captureException`. */
273
+ config?: CaptureExceptionOptions;
274
+ }
275
+ /**
276
+ * Wrap an Expo Router route-level `ErrorBoundary` so render errors that reach it
277
+ * are also captured as SunGlasses `$error` events (`$error_handled: true`) with
278
+ * route context (`$route_path`, `$route_name`). The original boundary still
279
+ * renders unchanged.
280
+ *
281
+ * The client is read from the nearest `<SunglassesProvider>` by default; pass
282
+ * `options.client` to override. Each distinct error is captured once.
283
+ *
284
+ * @example
285
+ * ```tsx
286
+ * // app/_layout.tsx
287
+ * import { ErrorBoundary as ExpoErrorBoundary } from 'expo-router';
288
+ * import { wrapExpoRouterErrorBoundary } from '@drakkar.software/sunglasses-react-native';
289
+ *
290
+ * export const ErrorBoundary = wrapExpoRouterErrorBoundary(ExpoErrorBoundary);
291
+ * ```
292
+ */
293
+ declare function wrapExpoRouterErrorBoundary<P extends ExpoRouterErrorBoundaryProps>(Boundary: React$1.ComponentType<P>, options?: WrapExpoRouterErrorBoundaryOptions): React$1.ComponentType<P>;
294
+
295
+ /**
296
+ * Attach an unhandled promise rejection handler that captures rejections as
297
+ * `$error` events (`$error_handled: false`) and publishes them to the global
298
+ * error bus.
299
+ *
300
+ * Tries React Native's bundled rejection tracker first
301
+ * (`promise/setimmediate/rejection-tracking`, the same mechanism Sentry uses),
302
+ * then falls back to a global `unhandledrejection` listener / `onunhandledrejection`
303
+ * hook for engines that expose one. Never throws.
304
+ *
305
+ * @param client - SunGlasses client instance.
306
+ * @param options - Capture configuration forwarded to `captureException`.
307
+ * @returns A cleanup function that detaches the handler.
308
+ */
309
+ declare function attachUnhandledRejectionHandler(client: ISunglassesClient, options?: CaptureExceptionOptions): () => void;
310
+
311
+ export { type ExpoRouterErrorBoundaryProps, SunglassesErrorBoundary, type SunglassesErrorBoundaryProps, SunglassesGlobalErrorBoundary, type SunglassesGlobalErrorBoundaryProps, SunglassesProvider, type SunglassesProviderProps, type WrapExpoRouterErrorBoundaryOptions, attachUnhandledRejectionHandler, captureDeepLinkUtmParams, useExpoRouterScreenTracking, useExpoRouterUtmCapture, useLinkingUtmCapture, useNavigationScreenTracking, useSunglasses, wrapExpoRouterErrorBoundary };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import React$1 from 'react';
2
2
  import { ISunglassesClient, AutoCaptureErrorsOptions, ScreenTrackingOptions, CaptureExceptionOptions } from '@drakkar.software/sunglasses-core';
3
- export { AutoCaptureErrorsOptions, CaptureExceptionOptions, ConsentStatus, ConsoleCaptureOptions, ISunglassesClient, ScreenTrackingOptions, SunglassesConfig, SunglassesCore, SunglassesEvent, captureException, patchConsole } from '@drakkar.software/sunglasses-core';
3
+ export { AutoCaptureErrorsOptions, CaptureExceptionOptions, ConsentStatus, ConsoleCaptureOptions, GlobalErrorInfo, GlobalErrorListener, ISunglassesClient, ScreenTrackingOptions, SunglassesConfig, SunglassesCore, SunglassesEvent, captureException, patchConsole, publishGlobalError, subscribeGlobalError } from '@drakkar.software/sunglasses-core';
4
4
 
5
5
  interface SunglassesProviderProps {
6
6
  /** An initialized ISunglassesClient (from SunglassesCore.create()). */
@@ -10,10 +10,11 @@ interface SunglassesProviderProps {
10
10
  * (`$error_handled: false`).
11
11
  *
12
12
  * - `true` installs a global `ErrorUtils` handler (the previous handler is
13
- * preserved and still invoked).
14
- * - An options object additionally lets you toggle `globalHandlers` and opt
15
- * into `console` capture (`console.error` / `console.warn`), plus configure
16
- * truncation / stack inclusion / ignore patterns.
13
+ * preserved and still invoked) and an unhandled promise rejection handler.
14
+ * - An options object additionally lets you toggle `globalHandlers` and
15
+ * `unhandledRejections`, opt into `console` capture (`console.error` /
16
+ * `console.warn`), plus configure truncation / stack inclusion / ignore
17
+ * patterns.
17
18
  *
18
19
  * Default: off.
19
20
  */
@@ -63,26 +64,6 @@ declare function SunglassesProvider({ client, autoCaptureErrors, children, }: Su
63
64
  */
64
65
  declare function useSunglasses(): ISunglassesClient;
65
66
 
66
- /**
67
- * Expo Router screen tracking hook.
68
- *
69
- * Tracks screen changes by observing the `pathname` from Expo Router.
70
- * Place this hook inside your root `_layout.tsx`.
71
- *
72
- * **Requires**: `expo-router` to be installed in your project.
73
- *
74
- * @example
75
- * ```tsx
76
- * // app/_layout.tsx
77
- * import { useExpoRouterScreenTracking } from '@drakkar.software/sunglasses-react-native';
78
- *
79
- * export default function RootLayout() {
80
- * const client = useSunglasses();
81
- * useExpoRouterScreenTracking(client);
82
- * return <Stack />;
83
- * }
84
- * ```
85
- */
86
67
  declare function useExpoRouterScreenTracking(client: ISunglassesClient, options?: Pick<ScreenTrackingOptions, 'screenNameMapper'>): void;
87
68
 
88
69
  interface NavigationState {
@@ -227,4 +208,104 @@ interface SunglassesErrorBoundaryProps {
227
208
  */
228
209
  declare function SunglassesErrorBoundary(props: SunglassesErrorBoundaryProps): React$1.ReactElement;
229
210
 
230
- export { SunglassesErrorBoundary, type SunglassesErrorBoundaryProps, SunglassesProvider, type SunglassesProviderProps, captureDeepLinkUtmParams, useExpoRouterScreenTracking, useExpoRouterUtmCapture, useLinkingUtmCapture, useNavigationScreenTracking, useSunglasses };
211
+ interface SunglassesGlobalErrorBoundaryProps {
212
+ /**
213
+ * SunGlasses client. Optional — defaults to the client provided by the
214
+ * nearest `<SunglassesProvider>`.
215
+ */
216
+ client?: ISunglassesClient;
217
+ /** Rendered when an error is caught. Defaults to rendering nothing. */
218
+ fallback?: React$1.ReactNode;
219
+ /** Error capture configuration forwarded to `captureException`. */
220
+ config?: CaptureExceptionOptions;
221
+ /**
222
+ * Also render the fallback for non-fatal global errors (e.g. `ErrorUtils`
223
+ * errors reported as non-fatal). Default: `false`.
224
+ */
225
+ includeNonFatalGlobalErrors?: boolean;
226
+ /**
227
+ * Also render the fallback for unhandled promise rejections. Off by default
228
+ * because many apps prefer to surface rejections as toasts or inline errors
229
+ * rather than as a full-screen fallback. Default: `false`.
230
+ */
231
+ includeUnhandledRejections?: boolean;
232
+ children: React$1.ReactNode;
233
+ }
234
+ /**
235
+ * A superset of `SunglassesErrorBoundary` that renders a fallback UI for fatal
236
+ * non-render errors (uncaught `ErrorUtils` errors and, optionally, unhandled
237
+ * rejections) in addition to the render-phase errors a normal error boundary
238
+ * catches.
239
+ *
240
+ * Render-phase errors are captured here as `$error` events
241
+ * (`$error_handled: true`). Global errors are captured by the provider's
242
+ * `autoCaptureErrors` handlers and merely surfaced here as a fallback — so the
243
+ * global fallback requires `autoCaptureErrors` to be enabled on the
244
+ * `<SunglassesProvider>`. No event is captured twice.
245
+ *
246
+ * @example
247
+ * ```tsx
248
+ * <SunglassesProvider client={client} autoCaptureErrors>
249
+ * <SunglassesGlobalErrorBoundary fallback={<ErrorScreen />}>
250
+ * <App />
251
+ * </SunglassesGlobalErrorBoundary>
252
+ * </SunglassesProvider>
253
+ * ```
254
+ */
255
+ declare function SunglassesGlobalErrorBoundary(props: SunglassesGlobalErrorBoundaryProps): React$1.ReactElement;
256
+
257
+ /**
258
+ * Props Expo Router passes to a route-level `ErrorBoundary` export.
259
+ */
260
+ interface ExpoRouterErrorBoundaryProps {
261
+ /** The render error caught by Expo Router for this route. */
262
+ error: Error;
263
+ /** Re-mounts the route's components to retry rendering. */
264
+ retry: () => Promise<void>;
265
+ }
266
+ interface WrapExpoRouterErrorBoundaryOptions {
267
+ /**
268
+ * SunGlasses client. Optional — defaults to the client provided by the
269
+ * nearest `<SunglassesProvider>`.
270
+ */
271
+ client?: ISunglassesClient;
272
+ /** Error capture configuration forwarded to `captureException`. */
273
+ config?: CaptureExceptionOptions;
274
+ }
275
+ /**
276
+ * Wrap an Expo Router route-level `ErrorBoundary` so render errors that reach it
277
+ * are also captured as SunGlasses `$error` events (`$error_handled: true`) with
278
+ * route context (`$route_path`, `$route_name`). The original boundary still
279
+ * renders unchanged.
280
+ *
281
+ * The client is read from the nearest `<SunglassesProvider>` by default; pass
282
+ * `options.client` to override. Each distinct error is captured once.
283
+ *
284
+ * @example
285
+ * ```tsx
286
+ * // app/_layout.tsx
287
+ * import { ErrorBoundary as ExpoErrorBoundary } from 'expo-router';
288
+ * import { wrapExpoRouterErrorBoundary } from '@drakkar.software/sunglasses-react-native';
289
+ *
290
+ * export const ErrorBoundary = wrapExpoRouterErrorBoundary(ExpoErrorBoundary);
291
+ * ```
292
+ */
293
+ declare function wrapExpoRouterErrorBoundary<P extends ExpoRouterErrorBoundaryProps>(Boundary: React$1.ComponentType<P>, options?: WrapExpoRouterErrorBoundaryOptions): React$1.ComponentType<P>;
294
+
295
+ /**
296
+ * Attach an unhandled promise rejection handler that captures rejections as
297
+ * `$error` events (`$error_handled: false`) and publishes them to the global
298
+ * error bus.
299
+ *
300
+ * Tries React Native's bundled rejection tracker first
301
+ * (`promise/setimmediate/rejection-tracking`, the same mechanism Sentry uses),
302
+ * then falls back to a global `unhandledrejection` listener / `onunhandledrejection`
303
+ * hook for engines that expose one. Never throws.
304
+ *
305
+ * @param client - SunGlasses client instance.
306
+ * @param options - Capture configuration forwarded to `captureException`.
307
+ * @returns A cleanup function that detaches the handler.
308
+ */
309
+ declare function attachUnhandledRejectionHandler(client: ISunglassesClient, options?: CaptureExceptionOptions): () => void;
310
+
311
+ export { type ExpoRouterErrorBoundaryProps, SunglassesErrorBoundary, type SunglassesErrorBoundaryProps, SunglassesGlobalErrorBoundary, type SunglassesGlobalErrorBoundaryProps, SunglassesProvider, type SunglassesProviderProps, type WrapExpoRouterErrorBoundaryOptions, attachUnhandledRejectionHandler, captureDeepLinkUtmParams, useExpoRouterScreenTracking, useExpoRouterUtmCapture, useLinkingUtmCapture, useNavigationScreenTracking, useSunglasses, wrapExpoRouterErrorBoundary };
package/dist/index.js CHANGED
@@ -30,24 +30,29 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- SunglassesCore: () => import_sunglasses_core3.SunglassesCore,
33
+ SunglassesCore: () => import_sunglasses_core6.SunglassesCore,
34
34
  SunglassesErrorBoundary: () => SunglassesErrorBoundary,
35
+ SunglassesGlobalErrorBoundary: () => SunglassesGlobalErrorBoundary,
35
36
  SunglassesProvider: () => SunglassesProvider,
37
+ attachUnhandledRejectionHandler: () => attachUnhandledRejectionHandler,
36
38
  captureDeepLinkUtmParams: () => captureDeepLinkUtmParams,
37
- captureException: () => import_sunglasses_core3.captureException,
38
- patchConsole: () => import_sunglasses_core3.patchConsole,
39
+ captureException: () => import_sunglasses_core6.captureException,
40
+ patchConsole: () => import_sunglasses_core6.patchConsole,
41
+ publishGlobalError: () => import_sunglasses_core6.publishGlobalError,
42
+ subscribeGlobalError: () => import_sunglasses_core6.subscribeGlobalError,
39
43
  useExpoRouterScreenTracking: () => useExpoRouterScreenTracking,
40
44
  useExpoRouterUtmCapture: () => useExpoRouterUtmCapture,
41
45
  useLinkingUtmCapture: () => useLinkingUtmCapture,
42
46
  useNavigationScreenTracking: () => useNavigationScreenTracking,
43
- useSunglasses: () => useSunglasses
47
+ useSunglasses: () => useSunglasses,
48
+ wrapExpoRouterErrorBoundary: () => wrapExpoRouterErrorBoundary
44
49
  });
45
50
  module.exports = __toCommonJS(index_exports);
46
51
 
47
52
  // src/SunglassesProvider.tsx
48
53
  var import_react2 = require("react");
49
54
  var import_react_native = require("react-native");
50
- var import_sunglasses_core = require("@drakkar.software/sunglasses-core");
55
+ var import_sunglasses_core2 = require("@drakkar.software/sunglasses-core");
51
56
 
52
57
  // src/context.ts
53
58
  var import_react = require("react");
@@ -62,6 +67,68 @@ function useSunglasses() {
62
67
  return client;
63
68
  }
64
69
 
70
+ // src/unhandledRejections.ts
71
+ var import_sunglasses_core = require("@drakkar.software/sunglasses-core");
72
+
73
+ // src/rejectionTracking.ts
74
+ var _tracking = null;
75
+ try {
76
+ const required = require("promise/setimmediate/rejection-tracking");
77
+ const resolved = required.default ?? required;
78
+ if (resolved && typeof resolved.enable === "function") {
79
+ _tracking = resolved;
80
+ }
81
+ } catch {
82
+ }
83
+ var rejectionTracking = _tracking;
84
+
85
+ // src/unhandledRejections.ts
86
+ function reasonOf(event) {
87
+ if (event && typeof event === "object" && "reason" in event) {
88
+ return event.reason;
89
+ }
90
+ return event;
91
+ }
92
+ function attachUnhandledRejectionHandler(client, options = {}) {
93
+ const onUnhandled = (error) => {
94
+ (0, import_sunglasses_core.captureException)(client, error, { handled: false, ...options });
95
+ (0, import_sunglasses_core.publishGlobalError)({ error, fatal: false, kind: "rejection" });
96
+ };
97
+ const tracking = rejectionTracking;
98
+ if (tracking) {
99
+ tracking.enable({
100
+ allRejections: true,
101
+ onUnhandled: (_id, error) => onUnhandled(error),
102
+ onHandled: () => {
103
+ }
104
+ });
105
+ return () => {
106
+ try {
107
+ tracking.disable?.();
108
+ } catch {
109
+ }
110
+ };
111
+ }
112
+ const g = globalThis;
113
+ if (typeof g.addEventListener === "function" && typeof g.removeEventListener === "function") {
114
+ const listener = (event) => onUnhandled(reasonOf(event));
115
+ g.addEventListener("unhandledrejection", listener);
116
+ return () => g.removeEventListener?.("unhandledrejection", listener);
117
+ }
118
+ if ("onunhandledrejection" in g) {
119
+ const previous = g.onunhandledrejection ?? null;
120
+ g.onunhandledrejection = (event) => {
121
+ onUnhandled(reasonOf(event));
122
+ previous?.(event);
123
+ };
124
+ return () => {
125
+ g.onunhandledrejection = previous;
126
+ };
127
+ }
128
+ return () => {
129
+ };
130
+ }
131
+
65
132
  // src/SunglassesProvider.tsx
66
133
  var import_jsx_runtime = require("react/jsx-runtime");
67
134
  function SunglassesProvider({
@@ -82,16 +149,20 @@ function SunglassesProvider({
82
149
  if (options.globalHandlers !== false && typeof ErrorUtils !== "undefined" && ErrorUtils.setGlobalHandler) {
83
150
  const previous = ErrorUtils.getGlobalHandler?.();
84
151
  ErrorUtils.setGlobalHandler((error, isFatal) => {
85
- (0, import_sunglasses_core.captureException)(client, error, { handled: false, ...options });
152
+ (0, import_sunglasses_core2.captureException)(client, error, { handled: false, ...options });
153
+ (0, import_sunglasses_core2.publishGlobalError)({ error, fatal: isFatal !== false, kind: "error" });
86
154
  previous?.(error, isFatal);
87
155
  });
88
156
  cleanups.push(() => {
89
157
  if (previous) ErrorUtils.setGlobalHandler?.(previous);
90
158
  });
91
159
  }
160
+ if (options.unhandledRejections !== false) {
161
+ cleanups.push(attachUnhandledRejectionHandler(client, options));
162
+ }
92
163
  if (options.console) {
93
164
  const consoleOptions = typeof options.console === "object" ? options.console : {};
94
- cleanups.push((0, import_sunglasses_core.patchConsole)(client, consoleOptions));
165
+ cleanups.push((0, import_sunglasses_core2.patchConsole)(client, consoleOptions));
95
166
  }
96
167
  return () => {
97
168
  for (const cleanup of cleanups) cleanup();
@@ -111,14 +182,26 @@ function SunglassesProvider({
111
182
 
112
183
  // src/useExpoRouterScreenTracking.ts
113
184
  var import_react3 = require("react");
185
+
186
+ // src/expoRouterCompat.ts
187
+ var _useGlobalSearchParams = null;
188
+ var _usePathname = null;
189
+ try {
190
+ const expoRouter = require("expo-router");
191
+ _useGlobalSearchParams = expoRouter.useGlobalSearchParams;
192
+ _usePathname = expoRouter.usePathname;
193
+ } catch {
194
+ }
195
+ var useGlobalSearchParams = _useGlobalSearchParams;
196
+ var usePathname = _usePathname;
197
+
198
+ // src/useExpoRouterScreenTracking.ts
199
+ function _noopPathname() {
200
+ return "";
201
+ }
202
+ var _impl = usePathname ?? _noopPathname;
114
203
  function useExpoRouterScreenTracking(client, options = {}) {
115
- let pathname;
116
- try {
117
- const { usePathname } = require("expo-router");
118
- pathname = usePathname();
119
- } catch {
120
- return;
121
- }
204
+ const pathname = _impl();
122
205
  const { screenNameMapper } = options;
123
206
  (0, import_react3.useEffect)(() => {
124
207
  if (!pathname) return;
@@ -206,16 +289,6 @@ function useLinkingUtmCapture(client) {
206
289
 
207
290
  // src/useExpoRouterUtmCapture.ts
208
291
  var import_react6 = require("react");
209
-
210
- // src/expoRouterCompat.ts
211
- var _useGlobalSearchParams = null;
212
- try {
213
- ({ useGlobalSearchParams: _useGlobalSearchParams } = require("expo-router"));
214
- } catch {
215
- }
216
- var useGlobalSearchParams = _useGlobalSearchParams;
217
-
218
- // src/useExpoRouterUtmCapture.ts
219
292
  function useExpoRouterUtmCapture(client) {
220
293
  if (!useGlobalSearchParams) return;
221
294
  const params = useGlobalSearchParams();
@@ -237,7 +310,7 @@ function useExpoRouterUtmCapture(client) {
237
310
 
238
311
  // src/SunglassesErrorBoundary.tsx
239
312
  var import_react7 = __toESM(require("react"));
240
- var import_sunglasses_core2 = require("@drakkar.software/sunglasses-core");
313
+ var import_sunglasses_core3 = require("@drakkar.software/sunglasses-core");
241
314
  var import_jsx_runtime2 = require("react/jsx-runtime");
242
315
  var ErrorBoundaryInner = class extends import_react7.default.Component {
243
316
  constructor() {
@@ -249,7 +322,7 @@ var ErrorBoundaryInner = class extends import_react7.default.Component {
249
322
  }
250
323
  componentDidCatch(error) {
251
324
  const { client, config } = this.props;
252
- (0, import_sunglasses_core2.captureException)(client, error, { handled: true, ...config });
325
+ (0, import_sunglasses_core3.captureException)(client, error, { handled: true, ...config });
253
326
  }
254
327
  render() {
255
328
  if (this.state.hasError) return this.props.fallback ?? null;
@@ -267,19 +340,110 @@ function SunglassesErrorBoundary(props) {
267
340
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ErrorBoundaryInner, { ...props, client });
268
341
  }
269
342
 
343
+ // src/SunglassesGlobalErrorBoundary.tsx
344
+ var import_react8 = __toESM(require("react"));
345
+ var import_sunglasses_core4 = require("@drakkar.software/sunglasses-core");
346
+ var import_jsx_runtime3 = require("react/jsx-runtime");
347
+ var GlobalErrorBoundaryInner = class extends import_react8.default.Component {
348
+ constructor() {
349
+ super(...arguments);
350
+ this.state = { hasError: false };
351
+ }
352
+ static getDerivedStateFromError() {
353
+ return { hasError: true };
354
+ }
355
+ componentDidMount() {
356
+ if (typeof import_sunglasses_core4.subscribeGlobalError !== "function") return;
357
+ this.unsubscribe = (0, import_sunglasses_core4.subscribeGlobalError)((info) => this.handleGlobalError(info));
358
+ }
359
+ componentWillUnmount() {
360
+ this.unsubscribe?.();
361
+ }
362
+ componentDidCatch(error) {
363
+ const { client, config } = this.props;
364
+ (0, import_sunglasses_core4.captureException)(client, error, { handled: true, ...config });
365
+ }
366
+ /**
367
+ * React to a global error published by the provider's auto-capture handlers.
368
+ * The provider already captured it, so we only decide whether to show the
369
+ * fallback — we never re-capture here.
370
+ */
371
+ handleGlobalError(info) {
372
+ if (this.state.hasError) return;
373
+ const { includeNonFatalGlobalErrors, includeUnhandledRejections } = this.props;
374
+ const shouldShow = info.kind === "rejection" ? includeUnhandledRejections === true : info.fatal || includeNonFatalGlobalErrors === true;
375
+ if (shouldShow) this.setState({ hasError: true });
376
+ }
377
+ render() {
378
+ if (this.state.hasError) return this.props.fallback ?? null;
379
+ return this.props.children;
380
+ }
381
+ };
382
+ function SunglassesGlobalErrorBoundary(props) {
383
+ const contextClient = (0, import_react8.useContext)(SunglassesContext);
384
+ const client = props.client ?? contextClient;
385
+ if (client === null) {
386
+ throw new Error(
387
+ "[SunGlasses] <SunglassesGlobalErrorBoundary> must be inside a <SunglassesProvider> or receive a `client` prop."
388
+ );
389
+ }
390
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(GlobalErrorBoundaryInner, { ...props, client });
391
+ }
392
+
393
+ // src/wrapExpoRouterErrorBoundary.tsx
394
+ var import_react9 = require("react");
395
+ var import_sunglasses_core5 = require("@drakkar.software/sunglasses-core");
396
+ var import_jsx_runtime4 = require("react/jsx-runtime");
397
+ function useExpoRouterPathname() {
398
+ if (!usePathname) return void 0;
399
+ return usePathname();
400
+ }
401
+ function wrapExpoRouterErrorBoundary(Boundary, options = {}) {
402
+ function WrappedExpoRouterErrorBoundary(props) {
403
+ const contextClient = (0, import_react9.useContext)(SunglassesContext);
404
+ const client = options.client ?? contextClient;
405
+ const pathname = useExpoRouterPathname();
406
+ const lastCaptured = (0, import_react9.useRef)(null);
407
+ const { error } = props;
408
+ (0, import_react9.useEffect)(() => {
409
+ if (!client || !error || lastCaptured.current === error) return;
410
+ lastCaptured.current = error;
411
+ const routeProps = {};
412
+ if (pathname) {
413
+ routeProps.$route_path = pathname;
414
+ routeProps.$route_name = pathname;
415
+ }
416
+ (0, import_sunglasses_core5.captureException)(client, error, {
417
+ handled: true,
418
+ ...options.config,
419
+ properties: { ...routeProps, ...options.config?.properties }
420
+ });
421
+ }, [client, error, pathname]);
422
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(Boundary, { ...props });
423
+ }
424
+ const name = Boundary.displayName || Boundary.name || "ErrorBoundary";
425
+ WrappedExpoRouterErrorBoundary.displayName = `wrapExpoRouterErrorBoundary(${name})`;
426
+ return WrappedExpoRouterErrorBoundary;
427
+ }
428
+
270
429
  // src/index.ts
271
- var import_sunglasses_core3 = require("@drakkar.software/sunglasses-core");
430
+ var import_sunglasses_core6 = require("@drakkar.software/sunglasses-core");
272
431
  // Annotate the CommonJS export names for ESM import in node:
273
432
  0 && (module.exports = {
274
433
  SunglassesCore,
275
434
  SunglassesErrorBoundary,
435
+ SunglassesGlobalErrorBoundary,
276
436
  SunglassesProvider,
437
+ attachUnhandledRejectionHandler,
277
438
  captureDeepLinkUtmParams,
278
439
  captureException,
279
440
  patchConsole,
441
+ publishGlobalError,
442
+ subscribeGlobalError,
280
443
  useExpoRouterScreenTracking,
281
444
  useExpoRouterUtmCapture,
282
445
  useLinkingUtmCapture,
283
446
  useNavigationScreenTracking,
284
- useSunglasses
447
+ useSunglasses,
448
+ wrapExpoRouterErrorBoundary
285
449
  });
package/dist/index.mjs CHANGED
@@ -8,7 +8,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
8
8
  // src/SunglassesProvider.tsx
9
9
  import { useEffect } from "react";
10
10
  import { AppState } from "react-native";
11
- import { captureException, patchConsole } from "@drakkar.software/sunglasses-core";
11
+ import { captureException as captureException2, patchConsole, publishGlobalError as publishGlobalError2 } from "@drakkar.software/sunglasses-core";
12
12
 
13
13
  // src/context.ts
14
14
  import { createContext, useContext } from "react";
@@ -23,6 +23,68 @@ function useSunglasses() {
23
23
  return client;
24
24
  }
25
25
 
26
+ // src/unhandledRejections.ts
27
+ import { captureException, publishGlobalError } from "@drakkar.software/sunglasses-core";
28
+
29
+ // src/rejectionTracking.ts
30
+ var _tracking = null;
31
+ try {
32
+ const required = __require("promise/setimmediate/rejection-tracking");
33
+ const resolved = required.default ?? required;
34
+ if (resolved && typeof resolved.enable === "function") {
35
+ _tracking = resolved;
36
+ }
37
+ } catch {
38
+ }
39
+ var rejectionTracking = _tracking;
40
+
41
+ // src/unhandledRejections.ts
42
+ function reasonOf(event) {
43
+ if (event && typeof event === "object" && "reason" in event) {
44
+ return event.reason;
45
+ }
46
+ return event;
47
+ }
48
+ function attachUnhandledRejectionHandler(client, options = {}) {
49
+ const onUnhandled = (error) => {
50
+ captureException(client, error, { handled: false, ...options });
51
+ publishGlobalError({ error, fatal: false, kind: "rejection" });
52
+ };
53
+ const tracking = rejectionTracking;
54
+ if (tracking) {
55
+ tracking.enable({
56
+ allRejections: true,
57
+ onUnhandled: (_id, error) => onUnhandled(error),
58
+ onHandled: () => {
59
+ }
60
+ });
61
+ return () => {
62
+ try {
63
+ tracking.disable?.();
64
+ } catch {
65
+ }
66
+ };
67
+ }
68
+ const g = globalThis;
69
+ if (typeof g.addEventListener === "function" && typeof g.removeEventListener === "function") {
70
+ const listener = (event) => onUnhandled(reasonOf(event));
71
+ g.addEventListener("unhandledrejection", listener);
72
+ return () => g.removeEventListener?.("unhandledrejection", listener);
73
+ }
74
+ if ("onunhandledrejection" in g) {
75
+ const previous = g.onunhandledrejection ?? null;
76
+ g.onunhandledrejection = (event) => {
77
+ onUnhandled(reasonOf(event));
78
+ previous?.(event);
79
+ };
80
+ return () => {
81
+ g.onunhandledrejection = previous;
82
+ };
83
+ }
84
+ return () => {
85
+ };
86
+ }
87
+
26
88
  // src/SunglassesProvider.tsx
27
89
  import { jsx } from "react/jsx-runtime";
28
90
  function SunglassesProvider({
@@ -43,13 +105,17 @@ function SunglassesProvider({
43
105
  if (options.globalHandlers !== false && typeof ErrorUtils !== "undefined" && ErrorUtils.setGlobalHandler) {
44
106
  const previous = ErrorUtils.getGlobalHandler?.();
45
107
  ErrorUtils.setGlobalHandler((error, isFatal) => {
46
- captureException(client, error, { handled: false, ...options });
108
+ captureException2(client, error, { handled: false, ...options });
109
+ publishGlobalError2({ error, fatal: isFatal !== false, kind: "error" });
47
110
  previous?.(error, isFatal);
48
111
  });
49
112
  cleanups.push(() => {
50
113
  if (previous) ErrorUtils.setGlobalHandler?.(previous);
51
114
  });
52
115
  }
116
+ if (options.unhandledRejections !== false) {
117
+ cleanups.push(attachUnhandledRejectionHandler(client, options));
118
+ }
53
119
  if (options.console) {
54
120
  const consoleOptions = typeof options.console === "object" ? options.console : {};
55
121
  cleanups.push(patchConsole(client, consoleOptions));
@@ -72,14 +138,26 @@ function SunglassesProvider({
72
138
 
73
139
  // src/useExpoRouterScreenTracking.ts
74
140
  import { useEffect as useEffect2 } from "react";
141
+
142
+ // src/expoRouterCompat.ts
143
+ var _useGlobalSearchParams = null;
144
+ var _usePathname = null;
145
+ try {
146
+ const expoRouter = __require("expo-router");
147
+ _useGlobalSearchParams = expoRouter.useGlobalSearchParams;
148
+ _usePathname = expoRouter.usePathname;
149
+ } catch {
150
+ }
151
+ var useGlobalSearchParams = _useGlobalSearchParams;
152
+ var usePathname = _usePathname;
153
+
154
+ // src/useExpoRouterScreenTracking.ts
155
+ function _noopPathname() {
156
+ return "";
157
+ }
158
+ var _impl = usePathname ?? _noopPathname;
75
159
  function useExpoRouterScreenTracking(client, options = {}) {
76
- let pathname;
77
- try {
78
- const { usePathname } = __require("expo-router");
79
- pathname = usePathname();
80
- } catch {
81
- return;
82
- }
160
+ const pathname = _impl();
83
161
  const { screenNameMapper } = options;
84
162
  useEffect2(() => {
85
163
  if (!pathname) return;
@@ -167,16 +245,6 @@ function useLinkingUtmCapture(client) {
167
245
 
168
246
  // src/useExpoRouterUtmCapture.ts
169
247
  import { useEffect as useEffect5 } from "react";
170
-
171
- // src/expoRouterCompat.ts
172
- var _useGlobalSearchParams = null;
173
- try {
174
- ({ useGlobalSearchParams: _useGlobalSearchParams } = __require("expo-router"));
175
- } catch {
176
- }
177
- var useGlobalSearchParams = _useGlobalSearchParams;
178
-
179
- // src/useExpoRouterUtmCapture.ts
180
248
  function useExpoRouterUtmCapture(client) {
181
249
  if (!useGlobalSearchParams) return;
182
250
  const params = useGlobalSearchParams();
@@ -198,7 +266,7 @@ function useExpoRouterUtmCapture(client) {
198
266
 
199
267
  // src/SunglassesErrorBoundary.tsx
200
268
  import React2, { useContext as useContext2 } from "react";
201
- import { captureException as captureException2 } from "@drakkar.software/sunglasses-core";
269
+ import { captureException as captureException3 } from "@drakkar.software/sunglasses-core";
202
270
  import { jsx as jsx2 } from "react/jsx-runtime";
203
271
  var ErrorBoundaryInner = class extends React2.Component {
204
272
  constructor() {
@@ -210,7 +278,7 @@ var ErrorBoundaryInner = class extends React2.Component {
210
278
  }
211
279
  componentDidCatch(error) {
212
280
  const { client, config } = this.props;
213
- captureException2(client, error, { handled: true, ...config });
281
+ captureException3(client, error, { handled: true, ...config });
214
282
  }
215
283
  render() {
216
284
  if (this.state.hasError) return this.props.fallback ?? null;
@@ -228,18 +296,115 @@ function SunglassesErrorBoundary(props) {
228
296
  return /* @__PURE__ */ jsx2(ErrorBoundaryInner, { ...props, client });
229
297
  }
230
298
 
299
+ // src/SunglassesGlobalErrorBoundary.tsx
300
+ import React3, { useContext as useContext3 } from "react";
301
+ import { captureException as captureException4, subscribeGlobalError } from "@drakkar.software/sunglasses-core";
302
+ import { jsx as jsx3 } from "react/jsx-runtime";
303
+ var GlobalErrorBoundaryInner = class extends React3.Component {
304
+ constructor() {
305
+ super(...arguments);
306
+ this.state = { hasError: false };
307
+ }
308
+ static getDerivedStateFromError() {
309
+ return { hasError: true };
310
+ }
311
+ componentDidMount() {
312
+ if (typeof subscribeGlobalError !== "function") return;
313
+ this.unsubscribe = subscribeGlobalError((info) => this.handleGlobalError(info));
314
+ }
315
+ componentWillUnmount() {
316
+ this.unsubscribe?.();
317
+ }
318
+ componentDidCatch(error) {
319
+ const { client, config } = this.props;
320
+ captureException4(client, error, { handled: true, ...config });
321
+ }
322
+ /**
323
+ * React to a global error published by the provider's auto-capture handlers.
324
+ * The provider already captured it, so we only decide whether to show the
325
+ * fallback — we never re-capture here.
326
+ */
327
+ handleGlobalError(info) {
328
+ if (this.state.hasError) return;
329
+ const { includeNonFatalGlobalErrors, includeUnhandledRejections } = this.props;
330
+ const shouldShow = info.kind === "rejection" ? includeUnhandledRejections === true : info.fatal || includeNonFatalGlobalErrors === true;
331
+ if (shouldShow) this.setState({ hasError: true });
332
+ }
333
+ render() {
334
+ if (this.state.hasError) return this.props.fallback ?? null;
335
+ return this.props.children;
336
+ }
337
+ };
338
+ function SunglassesGlobalErrorBoundary(props) {
339
+ const contextClient = useContext3(SunglassesContext);
340
+ const client = props.client ?? contextClient;
341
+ if (client === null) {
342
+ throw new Error(
343
+ "[SunGlasses] <SunglassesGlobalErrorBoundary> must be inside a <SunglassesProvider> or receive a `client` prop."
344
+ );
345
+ }
346
+ return /* @__PURE__ */ jsx3(GlobalErrorBoundaryInner, { ...props, client });
347
+ }
348
+
349
+ // src/wrapExpoRouterErrorBoundary.tsx
350
+ import { useContext as useContext4, useEffect as useEffect6, useRef as useRef2 } from "react";
351
+ import { captureException as captureException5 } from "@drakkar.software/sunglasses-core";
352
+ import { jsx as jsx4 } from "react/jsx-runtime";
353
+ function useExpoRouterPathname() {
354
+ if (!usePathname) return void 0;
355
+ return usePathname();
356
+ }
357
+ function wrapExpoRouterErrorBoundary(Boundary, options = {}) {
358
+ function WrappedExpoRouterErrorBoundary(props) {
359
+ const contextClient = useContext4(SunglassesContext);
360
+ const client = options.client ?? contextClient;
361
+ const pathname = useExpoRouterPathname();
362
+ const lastCaptured = useRef2(null);
363
+ const { error } = props;
364
+ useEffect6(() => {
365
+ if (!client || !error || lastCaptured.current === error) return;
366
+ lastCaptured.current = error;
367
+ const routeProps = {};
368
+ if (pathname) {
369
+ routeProps.$route_path = pathname;
370
+ routeProps.$route_name = pathname;
371
+ }
372
+ captureException5(client, error, {
373
+ handled: true,
374
+ ...options.config,
375
+ properties: { ...routeProps, ...options.config?.properties }
376
+ });
377
+ }, [client, error, pathname]);
378
+ return /* @__PURE__ */ jsx4(Boundary, { ...props });
379
+ }
380
+ const name = Boundary.displayName || Boundary.name || "ErrorBoundary";
381
+ WrappedExpoRouterErrorBoundary.displayName = `wrapExpoRouterErrorBoundary(${name})`;
382
+ return WrappedExpoRouterErrorBoundary;
383
+ }
384
+
231
385
  // src/index.ts
232
- import { SunglassesCore, captureException as captureException3, patchConsole as patchConsole2 } from "@drakkar.software/sunglasses-core";
386
+ import {
387
+ SunglassesCore,
388
+ captureException as captureException6,
389
+ patchConsole as patchConsole2,
390
+ publishGlobalError as publishGlobalError3,
391
+ subscribeGlobalError as subscribeGlobalError2
392
+ } from "@drakkar.software/sunglasses-core";
233
393
  export {
234
394
  SunglassesCore,
235
395
  SunglassesErrorBoundary,
396
+ SunglassesGlobalErrorBoundary,
236
397
  SunglassesProvider,
398
+ attachUnhandledRejectionHandler,
237
399
  captureDeepLinkUtmParams,
238
- captureException3 as captureException,
400
+ captureException6 as captureException,
239
401
  patchConsole2 as patchConsole,
402
+ publishGlobalError3 as publishGlobalError,
403
+ subscribeGlobalError2 as subscribeGlobalError,
240
404
  useExpoRouterScreenTracking,
241
405
  useExpoRouterUtmCapture,
242
406
  useLinkingUtmCapture,
243
407
  useNavigationScreenTracking,
244
- useSunglasses
408
+ useSunglasses,
409
+ wrapExpoRouterErrorBoundary
245
410
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drakkar.software/sunglasses-react-native",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "React Native / Expo provider and hooks for SunGlasses event tracking",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -16,7 +16,7 @@
16
16
  "dist"
17
17
  ],
18
18
  "dependencies": {
19
- "@drakkar.software/sunglasses-core": "0.11.0"
19
+ "@drakkar.software/sunglasses-core": "0.12.0"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "expo-router": ">=3.0.0",