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