@drakkar.software/sunglasses-react-native 0.11.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,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
  */
@@ -227,4 +228,104 @@ interface SunglassesErrorBoundaryProps {
227
228
  */
228
229
  declare function SunglassesErrorBoundary(props: SunglassesErrorBoundaryProps): React$1.ReactElement;
229
230
 
230
- 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,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
  */
@@ -227,4 +228,104 @@ interface SunglassesErrorBoundaryProps {
227
228
  */
228
229
  declare function SunglassesErrorBoundary(props: SunglassesErrorBoundaryProps): React$1.ReactElement;
229
230
 
230
- 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,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();
@@ -114,8 +185,8 @@ var import_react3 = require("react");
114
185
  function useExpoRouterScreenTracking(client, options = {}) {
115
186
  let pathname;
116
187
  try {
117
- const { usePathname } = require("expo-router");
118
- pathname = usePathname();
188
+ const { usePathname: usePathname2 } = require("expo-router");
189
+ pathname = usePathname2();
119
190
  } catch {
120
191
  return;
121
192
  }
@@ -209,11 +280,15 @@ var import_react6 = require("react");
209
280
 
210
281
  // src/expoRouterCompat.ts
211
282
  var _useGlobalSearchParams = null;
283
+ var _usePathname = null;
212
284
  try {
213
- ({ useGlobalSearchParams: _useGlobalSearchParams } = require("expo-router"));
285
+ const expoRouter = require("expo-router");
286
+ _useGlobalSearchParams = expoRouter.useGlobalSearchParams;
287
+ _usePathname = expoRouter.usePathname;
214
288
  } catch {
215
289
  }
216
290
  var useGlobalSearchParams = _useGlobalSearchParams;
291
+ var usePathname = _usePathname;
217
292
 
218
293
  // src/useExpoRouterUtmCapture.ts
219
294
  function useExpoRouterUtmCapture(client) {
@@ -237,7 +312,7 @@ function useExpoRouterUtmCapture(client) {
237
312
 
238
313
  // src/SunglassesErrorBoundary.tsx
239
314
  var import_react7 = __toESM(require("react"));
240
- var import_sunglasses_core2 = require("@drakkar.software/sunglasses-core");
315
+ var import_sunglasses_core3 = require("@drakkar.software/sunglasses-core");
241
316
  var import_jsx_runtime2 = require("react/jsx-runtime");
242
317
  var ErrorBoundaryInner = class extends import_react7.default.Component {
243
318
  constructor() {
@@ -249,7 +324,7 @@ var ErrorBoundaryInner = class extends import_react7.default.Component {
249
324
  }
250
325
  componentDidCatch(error) {
251
326
  const { client, config } = this.props;
252
- (0, import_sunglasses_core2.captureException)(client, error, { handled: true, ...config });
327
+ (0, import_sunglasses_core3.captureException)(client, error, { handled: true, ...config });
253
328
  }
254
329
  render() {
255
330
  if (this.state.hasError) return this.props.fallback ?? null;
@@ -267,19 +342,109 @@ function SunglassesErrorBoundary(props) {
267
342
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(ErrorBoundaryInner, { ...props, client });
268
343
  }
269
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
+
270
430
  // src/index.ts
271
- var import_sunglasses_core3 = require("@drakkar.software/sunglasses-core");
431
+ var import_sunglasses_core6 = require("@drakkar.software/sunglasses-core");
272
432
  // Annotate the CommonJS export names for ESM import in node:
273
433
  0 && (module.exports = {
274
434
  SunglassesCore,
275
435
  SunglassesErrorBoundary,
436
+ SunglassesGlobalErrorBoundary,
276
437
  SunglassesProvider,
438
+ attachUnhandledRejectionHandler,
277
439
  captureDeepLinkUtmParams,
278
440
  captureException,
279
441
  patchConsole,
442
+ publishGlobalError,
443
+ subscribeGlobalError,
280
444
  useExpoRouterScreenTracking,
281
445
  useExpoRouterUtmCapture,
282
446
  useLinkingUtmCapture,
283
447
  useNavigationScreenTracking,
284
- useSunglasses
448
+ useSunglasses,
449
+ wrapExpoRouterErrorBoundary
285
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, 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));
@@ -75,8 +141,8 @@ import { useEffect as useEffect2 } from "react";
75
141
  function useExpoRouterScreenTracking(client, options = {}) {
76
142
  let pathname;
77
143
  try {
78
- const { usePathname } = __require("expo-router");
79
- pathname = usePathname();
144
+ const { usePathname: usePathname2 } = __require("expo-router");
145
+ pathname = usePathname2();
80
146
  } catch {
81
147
  return;
82
148
  }
@@ -170,11 +236,15 @@ import { useEffect as useEffect5 } from "react";
170
236
 
171
237
  // src/expoRouterCompat.ts
172
238
  var _useGlobalSearchParams = null;
239
+ var _usePathname = null;
173
240
  try {
174
- ({ useGlobalSearchParams: _useGlobalSearchParams } = __require("expo-router"));
241
+ const expoRouter = __require("expo-router");
242
+ _useGlobalSearchParams = expoRouter.useGlobalSearchParams;
243
+ _usePathname = expoRouter.usePathname;
175
244
  } catch {
176
245
  }
177
246
  var useGlobalSearchParams = _useGlobalSearchParams;
247
+ var usePathname = _usePathname;
178
248
 
179
249
  // src/useExpoRouterUtmCapture.ts
180
250
  function useExpoRouterUtmCapture(client) {
@@ -198,7 +268,7 @@ function useExpoRouterUtmCapture(client) {
198
268
 
199
269
  // src/SunglassesErrorBoundary.tsx
200
270
  import React2, { useContext as useContext2 } from "react";
201
- import { captureException as captureException2 } from "@drakkar.software/sunglasses-core";
271
+ import { captureException as captureException3 } from "@drakkar.software/sunglasses-core";
202
272
  import { jsx as jsx2 } from "react/jsx-runtime";
203
273
  var ErrorBoundaryInner = class extends React2.Component {
204
274
  constructor() {
@@ -210,7 +280,7 @@ var ErrorBoundaryInner = class extends React2.Component {
210
280
  }
211
281
  componentDidCatch(error) {
212
282
  const { client, config } = this.props;
213
- captureException2(client, error, { handled: true, ...config });
283
+ captureException3(client, error, { handled: true, ...config });
214
284
  }
215
285
  render() {
216
286
  if (this.state.hasError) return this.props.fallback ?? null;
@@ -228,18 +298,114 @@ function SunglassesErrorBoundary(props) {
228
298
  return /* @__PURE__ */ jsx2(ErrorBoundaryInner, { ...props, client });
229
299
  }
230
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
+
231
386
  // src/index.ts
232
- import { SunglassesCore, captureException as captureException3, patchConsole as patchConsole2 } 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";
233
394
  export {
234
395
  SunglassesCore,
235
396
  SunglassesErrorBoundary,
397
+ SunglassesGlobalErrorBoundary,
236
398
  SunglassesProvider,
399
+ attachUnhandledRejectionHandler,
237
400
  captureDeepLinkUtmParams,
238
- captureException3 as captureException,
401
+ captureException6 as captureException,
239
402
  patchConsole2 as patchConsole,
403
+ publishGlobalError3 as publishGlobalError,
404
+ subscribeGlobalError2 as subscribeGlobalError,
240
405
  useExpoRouterScreenTracking,
241
406
  useExpoRouterUtmCapture,
242
407
  useLinkingUtmCapture,
243
408
  useNavigationScreenTracking,
244
- useSunglasses
409
+ useSunglasses,
410
+ wrapExpoRouterErrorBoundary
245
411
  };
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.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.11.0"
19
+ "@drakkar.software/sunglasses-core": "0.12.0"
20
20
  },
21
21
  "peerDependencies": {
22
22
  "expo-router": ">=3.0.0",