@drakkar.software/sunglasses-adapter-sentry 0.1.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.
@@ -0,0 +1,156 @@
1
+ import { ErrorEventProperties, ISunglassesClient } from '@drakkar.software/sunglasses-core';
2
+ import React from 'react';
3
+
4
+ /** Minimal mirror of the Sentry `Event` shape we care about. */
5
+ interface SentryLikeEvent {
6
+ exception?: {
7
+ values?: Array<{
8
+ type?: string;
9
+ value?: string;
10
+ stacktrace?: {
11
+ frames?: Array<{
12
+ filename?: string;
13
+ lineno?: number;
14
+ function?: string;
15
+ }>;
16
+ };
17
+ }>;
18
+ };
19
+ level?: string;
20
+ tags?: Record<string, string | number | boolean>;
21
+ }
22
+ type SentryBeforeSendResult = SentryLikeEvent | null | Promise<SentryLikeEvent | null>;
23
+ /**
24
+ * Configuration for the Sentry → SunGlasses bridge.
25
+ */
26
+ interface SentryBridgeConfig {
27
+ /**
28
+ * Include stack frames in `$error_stack`. Default: `false` (privacy-safe).
29
+ * Stack frames may expose internal file paths and function names.
30
+ */
31
+ includeStack?: boolean;
32
+ /**
33
+ * Maximum number of stack frames to include when `includeStack` is `true`.
34
+ * Default: `5`. Sentry stores frames innermost-last; we take the last N.
35
+ */
36
+ maxStackFrames?: number;
37
+ /**
38
+ * Truncate error messages to this many characters. Default: `200`.
39
+ * Error messages sometimes contain user data ("User foo@bar.com not found").
40
+ */
41
+ maxMessageLength?: number;
42
+ /**
43
+ * Skip errors whose message matches any of these patterns.
44
+ * Matched errors are not captured by SunGlasses (Sentry still receives them).
45
+ * Pattern is tested against the raw (pre-truncation) message.
46
+ */
47
+ ignorePatterns?: RegExp[];
48
+ /**
49
+ * Optional transform applied before `client.capture()`.
50
+ * Receives typed `ErrorEventProperties`; return a (possibly extended) props
51
+ * object to capture, or `null` to skip capture entirely.
52
+ */
53
+ beforeCapture?: (props: ErrorEventProperties) => Record<string, unknown> | null;
54
+ /**
55
+ * When `true`, the bridge returns `null` from `beforeSend`, instructing Sentry
56
+ * not to transmit the event to its servers. Useful when you want Sentry purely
57
+ * as a local error capture/parsing engine with no data leaving the device.
58
+ *
59
+ * Compatible with omitting the DSN entirely — Sentry initialises fine without
60
+ * one, still attaches global error handlers, and still fires `beforeSend`.
61
+ *
62
+ * Default: `false`.
63
+ */
64
+ suppressSentrySend?: boolean;
65
+ }
66
+ /**
67
+ * Creates a Sentry `beforeSend` callback that captures errors as SunGlasses
68
+ * `$error` events. Works with `@sentry/browser`, `@sentry/react`, and
69
+ * `@sentry/react-native` — any Sentry SDK that supports `beforeSend`.
70
+ *
71
+ * @param client - SunGlasses client instance.
72
+ * @param config - Optional bridge configuration.
73
+ * @param originalBeforeSend - Existing `beforeSend` callback to chain after this one.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Mode A — both Sentry and SunGlasses receive errors
78
+ * Sentry.init({
79
+ * dsn: 'https://...',
80
+ * beforeSend: createSentryBeforeSend(client),
81
+ * });
82
+ *
83
+ * // Mode B — SunGlasses only, nothing sent to Sentry servers
84
+ * Sentry.init({
85
+ * beforeSend: createSentryBeforeSend(client, { suppressSentrySend: true }),
86
+ * });
87
+ * ```
88
+ */
89
+ declare function createSentryBeforeSend(client: ISunglassesClient, config?: SentryBridgeConfig, originalBeforeSend?: (event: SentryLikeEvent, hint: unknown) => SentryBeforeSendResult): (event: SentryLikeEvent, hint: unknown) => SentryBeforeSendResult;
90
+
91
+ /**
92
+ * Configuration for `SunglassesErrorBoundary`.
93
+ */
94
+ interface ErrorBoundaryConfig {
95
+ /**
96
+ * Include the stack trace in `$error_stack`. Default: `false` (privacy-safe).
97
+ * Stack traces may expose internal file paths and function names.
98
+ */
99
+ includeStack?: boolean;
100
+ /**
101
+ * Maximum number of stack frames to include when `includeStack` is `true`.
102
+ * Default: `5`.
103
+ */
104
+ maxStackFrames?: number;
105
+ /**
106
+ * Truncate error messages to this many characters. Default: `200`.
107
+ * Error messages sometimes contain user data.
108
+ */
109
+ maxMessageLength?: number;
110
+ /**
111
+ * Skip errors whose message matches any of these patterns.
112
+ * Pattern is tested against the raw (pre-truncation) message.
113
+ */
114
+ ignorePatterns?: RegExp[];
115
+ /**
116
+ * Optional transform applied before `client.capture()`.
117
+ * Receives typed `ErrorEventProperties`; return a (possibly extended) props
118
+ * object to capture, or `null` to skip capture entirely.
119
+ */
120
+ beforeCapture?: (props: ErrorEventProperties) => Record<string, unknown> | null;
121
+ }
122
+ interface Props {
123
+ /** SunGlasses client instance that will receive `$error` capture events. */
124
+ client: ISunglassesClient;
125
+ /** Rendered when an error is caught. Defaults to rendering nothing. */
126
+ fallback?: React.ReactNode;
127
+ /** Error capture configuration. */
128
+ config?: ErrorBoundaryConfig;
129
+ children: React.ReactNode;
130
+ }
131
+ interface State {
132
+ hasError: boolean;
133
+ }
134
+ /**
135
+ * React error boundary that captures render-phase errors as SunGlasses events.
136
+ *
137
+ * Fires `client.capture('$error', { ..., $error_handled: true })` when a
138
+ * descendant component throws during rendering. Complements the Sentry bridge
139
+ * (which covers unhandled global errors) by catching errors at component
140
+ * boundaries before they propagate to the global handler.
141
+ *
142
+ * @example
143
+ * ```tsx
144
+ * <SunglassesErrorBoundary client={client} fallback={<ErrorPage />}>
145
+ * <App />
146
+ * </SunglassesErrorBoundary>
147
+ * ```
148
+ */
149
+ declare class SunglassesErrorBoundary extends React.Component<Props, State> {
150
+ state: State;
151
+ static getDerivedStateFromError(): State;
152
+ componentDidCatch(error: Error): void;
153
+ render(): React.ReactNode;
154
+ }
155
+
156
+ export { type ErrorBoundaryConfig, type SentryBridgeConfig, SunglassesErrorBoundary, createSentryBeforeSend };
@@ -0,0 +1,156 @@
1
+ import { ErrorEventProperties, ISunglassesClient } from '@drakkar.software/sunglasses-core';
2
+ import React from 'react';
3
+
4
+ /** Minimal mirror of the Sentry `Event` shape we care about. */
5
+ interface SentryLikeEvent {
6
+ exception?: {
7
+ values?: Array<{
8
+ type?: string;
9
+ value?: string;
10
+ stacktrace?: {
11
+ frames?: Array<{
12
+ filename?: string;
13
+ lineno?: number;
14
+ function?: string;
15
+ }>;
16
+ };
17
+ }>;
18
+ };
19
+ level?: string;
20
+ tags?: Record<string, string | number | boolean>;
21
+ }
22
+ type SentryBeforeSendResult = SentryLikeEvent | null | Promise<SentryLikeEvent | null>;
23
+ /**
24
+ * Configuration for the Sentry → SunGlasses bridge.
25
+ */
26
+ interface SentryBridgeConfig {
27
+ /**
28
+ * Include stack frames in `$error_stack`. Default: `false` (privacy-safe).
29
+ * Stack frames may expose internal file paths and function names.
30
+ */
31
+ includeStack?: boolean;
32
+ /**
33
+ * Maximum number of stack frames to include when `includeStack` is `true`.
34
+ * Default: `5`. Sentry stores frames innermost-last; we take the last N.
35
+ */
36
+ maxStackFrames?: number;
37
+ /**
38
+ * Truncate error messages to this many characters. Default: `200`.
39
+ * Error messages sometimes contain user data ("User foo@bar.com not found").
40
+ */
41
+ maxMessageLength?: number;
42
+ /**
43
+ * Skip errors whose message matches any of these patterns.
44
+ * Matched errors are not captured by SunGlasses (Sentry still receives them).
45
+ * Pattern is tested against the raw (pre-truncation) message.
46
+ */
47
+ ignorePatterns?: RegExp[];
48
+ /**
49
+ * Optional transform applied before `client.capture()`.
50
+ * Receives typed `ErrorEventProperties`; return a (possibly extended) props
51
+ * object to capture, or `null` to skip capture entirely.
52
+ */
53
+ beforeCapture?: (props: ErrorEventProperties) => Record<string, unknown> | null;
54
+ /**
55
+ * When `true`, the bridge returns `null` from `beforeSend`, instructing Sentry
56
+ * not to transmit the event to its servers. Useful when you want Sentry purely
57
+ * as a local error capture/parsing engine with no data leaving the device.
58
+ *
59
+ * Compatible with omitting the DSN entirely — Sentry initialises fine without
60
+ * one, still attaches global error handlers, and still fires `beforeSend`.
61
+ *
62
+ * Default: `false`.
63
+ */
64
+ suppressSentrySend?: boolean;
65
+ }
66
+ /**
67
+ * Creates a Sentry `beforeSend` callback that captures errors as SunGlasses
68
+ * `$error` events. Works with `@sentry/browser`, `@sentry/react`, and
69
+ * `@sentry/react-native` — any Sentry SDK that supports `beforeSend`.
70
+ *
71
+ * @param client - SunGlasses client instance.
72
+ * @param config - Optional bridge configuration.
73
+ * @param originalBeforeSend - Existing `beforeSend` callback to chain after this one.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Mode A — both Sentry and SunGlasses receive errors
78
+ * Sentry.init({
79
+ * dsn: 'https://...',
80
+ * beforeSend: createSentryBeforeSend(client),
81
+ * });
82
+ *
83
+ * // Mode B — SunGlasses only, nothing sent to Sentry servers
84
+ * Sentry.init({
85
+ * beforeSend: createSentryBeforeSend(client, { suppressSentrySend: true }),
86
+ * });
87
+ * ```
88
+ */
89
+ declare function createSentryBeforeSend(client: ISunglassesClient, config?: SentryBridgeConfig, originalBeforeSend?: (event: SentryLikeEvent, hint: unknown) => SentryBeforeSendResult): (event: SentryLikeEvent, hint: unknown) => SentryBeforeSendResult;
90
+
91
+ /**
92
+ * Configuration for `SunglassesErrorBoundary`.
93
+ */
94
+ interface ErrorBoundaryConfig {
95
+ /**
96
+ * Include the stack trace in `$error_stack`. Default: `false` (privacy-safe).
97
+ * Stack traces may expose internal file paths and function names.
98
+ */
99
+ includeStack?: boolean;
100
+ /**
101
+ * Maximum number of stack frames to include when `includeStack` is `true`.
102
+ * Default: `5`.
103
+ */
104
+ maxStackFrames?: number;
105
+ /**
106
+ * Truncate error messages to this many characters. Default: `200`.
107
+ * Error messages sometimes contain user data.
108
+ */
109
+ maxMessageLength?: number;
110
+ /**
111
+ * Skip errors whose message matches any of these patterns.
112
+ * Pattern is tested against the raw (pre-truncation) message.
113
+ */
114
+ ignorePatterns?: RegExp[];
115
+ /**
116
+ * Optional transform applied before `client.capture()`.
117
+ * Receives typed `ErrorEventProperties`; return a (possibly extended) props
118
+ * object to capture, or `null` to skip capture entirely.
119
+ */
120
+ beforeCapture?: (props: ErrorEventProperties) => Record<string, unknown> | null;
121
+ }
122
+ interface Props {
123
+ /** SunGlasses client instance that will receive `$error` capture events. */
124
+ client: ISunglassesClient;
125
+ /** Rendered when an error is caught. Defaults to rendering nothing. */
126
+ fallback?: React.ReactNode;
127
+ /** Error capture configuration. */
128
+ config?: ErrorBoundaryConfig;
129
+ children: React.ReactNode;
130
+ }
131
+ interface State {
132
+ hasError: boolean;
133
+ }
134
+ /**
135
+ * React error boundary that captures render-phase errors as SunGlasses events.
136
+ *
137
+ * Fires `client.capture('$error', { ..., $error_handled: true })` when a
138
+ * descendant component throws during rendering. Complements the Sentry bridge
139
+ * (which covers unhandled global errors) by catching errors at component
140
+ * boundaries before they propagate to the global handler.
141
+ *
142
+ * @example
143
+ * ```tsx
144
+ * <SunglassesErrorBoundary client={client} fallback={<ErrorPage />}>
145
+ * <App />
146
+ * </SunglassesErrorBoundary>
147
+ * ```
148
+ */
149
+ declare class SunglassesErrorBoundary extends React.Component<Props, State> {
150
+ state: State;
151
+ static getDerivedStateFromError(): State;
152
+ componentDidCatch(error: Error): void;
153
+ render(): React.ReactNode;
154
+ }
155
+
156
+ export { type ErrorBoundaryConfig, type SentryBridgeConfig, SunglassesErrorBoundary, createSentryBeforeSend };
package/dist/index.js ADDED
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ SunglassesErrorBoundary: () => SunglassesErrorBoundary,
34
+ createSentryBeforeSend: () => createSentryBeforeSend
35
+ });
36
+ module.exports = __toCommonJS(index_exports);
37
+
38
+ // src/createSentryBeforeSend.ts
39
+ function createSentryBeforeSend(client, config = {}, originalBeforeSend) {
40
+ const {
41
+ includeStack = false,
42
+ maxStackFrames = 5,
43
+ maxMessageLength = 200,
44
+ ignorePatterns = [],
45
+ beforeCapture,
46
+ suppressSentrySend = false
47
+ } = config;
48
+ return (event, hint) => {
49
+ const sentryResult = originalBeforeSend ? originalBeforeSend(event, hint) : event;
50
+ const result = suppressSentrySend ? null : sentryResult;
51
+ const exc = event.exception?.values?.[0];
52
+ const rawMessage = exc?.value ?? "";
53
+ const message = rawMessage.slice(0, maxMessageLength);
54
+ if (!ignorePatterns.some((p) => p.test(rawMessage))) {
55
+ let props = {
56
+ $error_message: message,
57
+ $error_type: exc?.type ?? "Error",
58
+ $error_handled: false,
59
+ $error_level: event.level ?? "error"
60
+ };
61
+ if (includeStack && exc?.stacktrace?.frames) {
62
+ const frames = exc.stacktrace.frames.slice(-maxStackFrames).map((f) => `${f.function ?? "?"} (${f.filename ?? "?"}:${f.lineno ?? "?"})`).join("\n");
63
+ props = { ...props, $error_stack: frames };
64
+ }
65
+ if (beforeCapture) {
66
+ const transformed = beforeCapture(props);
67
+ if (!transformed) return result;
68
+ client.capture("$error", transformed);
69
+ } else {
70
+ client.capture("$error", props);
71
+ }
72
+ }
73
+ return result;
74
+ };
75
+ }
76
+
77
+ // src/SunglassesErrorBoundary.tsx
78
+ var import_react = __toESM(require("react"));
79
+ var SunglassesErrorBoundary = class extends import_react.default.Component {
80
+ constructor() {
81
+ super(...arguments);
82
+ this.state = { hasError: false };
83
+ }
84
+ static getDerivedStateFromError() {
85
+ return { hasError: true };
86
+ }
87
+ componentDidCatch(error) {
88
+ const { client, config = {} } = this.props;
89
+ const {
90
+ maxMessageLength = 200,
91
+ maxStackFrames = 5,
92
+ includeStack = false,
93
+ ignorePatterns = [],
94
+ beforeCapture
95
+ } = config;
96
+ const rawMessage = error.message;
97
+ const message = rawMessage.slice(0, maxMessageLength);
98
+ if (ignorePatterns.some((p) => p.test(rawMessage))) return;
99
+ let props = {
100
+ $error_message: message,
101
+ $error_type: error.name,
102
+ $error_handled: true,
103
+ $error_level: "error"
104
+ };
105
+ if (includeStack && error.stack) {
106
+ const frames = error.stack.split("\n").filter((line) => line.trim().startsWith("at ")).slice(0, maxStackFrames).join("\n");
107
+ props = { ...props, $error_stack: frames };
108
+ }
109
+ if (beforeCapture) {
110
+ const transformed = beforeCapture(props);
111
+ if (!transformed) return;
112
+ client.capture("$error", transformed);
113
+ } else {
114
+ client.capture("$error", props);
115
+ }
116
+ }
117
+ render() {
118
+ if (this.state.hasError) return this.props.fallback ?? null;
119
+ return this.props.children;
120
+ }
121
+ };
122
+ // Annotate the CommonJS export names for ESM import in node:
123
+ 0 && (module.exports = {
124
+ SunglassesErrorBoundary,
125
+ createSentryBeforeSend
126
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,88 @@
1
+ // src/createSentryBeforeSend.ts
2
+ function createSentryBeforeSend(client, config = {}, originalBeforeSend) {
3
+ const {
4
+ includeStack = false,
5
+ maxStackFrames = 5,
6
+ maxMessageLength = 200,
7
+ ignorePatterns = [],
8
+ beforeCapture,
9
+ suppressSentrySend = false
10
+ } = config;
11
+ return (event, hint) => {
12
+ const sentryResult = originalBeforeSend ? originalBeforeSend(event, hint) : event;
13
+ const result = suppressSentrySend ? null : sentryResult;
14
+ const exc = event.exception?.values?.[0];
15
+ const rawMessage = exc?.value ?? "";
16
+ const message = rawMessage.slice(0, maxMessageLength);
17
+ if (!ignorePatterns.some((p) => p.test(rawMessage))) {
18
+ let props = {
19
+ $error_message: message,
20
+ $error_type: exc?.type ?? "Error",
21
+ $error_handled: false,
22
+ $error_level: event.level ?? "error"
23
+ };
24
+ if (includeStack && exc?.stacktrace?.frames) {
25
+ const frames = exc.stacktrace.frames.slice(-maxStackFrames).map((f) => `${f.function ?? "?"} (${f.filename ?? "?"}:${f.lineno ?? "?"})`).join("\n");
26
+ props = { ...props, $error_stack: frames };
27
+ }
28
+ if (beforeCapture) {
29
+ const transformed = beforeCapture(props);
30
+ if (!transformed) return result;
31
+ client.capture("$error", transformed);
32
+ } else {
33
+ client.capture("$error", props);
34
+ }
35
+ }
36
+ return result;
37
+ };
38
+ }
39
+
40
+ // src/SunglassesErrorBoundary.tsx
41
+ import React from "react";
42
+ var SunglassesErrorBoundary = class extends React.Component {
43
+ constructor() {
44
+ super(...arguments);
45
+ this.state = { hasError: false };
46
+ }
47
+ static getDerivedStateFromError() {
48
+ return { hasError: true };
49
+ }
50
+ componentDidCatch(error) {
51
+ const { client, config = {} } = this.props;
52
+ const {
53
+ maxMessageLength = 200,
54
+ maxStackFrames = 5,
55
+ includeStack = false,
56
+ ignorePatterns = [],
57
+ beforeCapture
58
+ } = config;
59
+ const rawMessage = error.message;
60
+ const message = rawMessage.slice(0, maxMessageLength);
61
+ if (ignorePatterns.some((p) => p.test(rawMessage))) return;
62
+ let props = {
63
+ $error_message: message,
64
+ $error_type: error.name,
65
+ $error_handled: true,
66
+ $error_level: "error"
67
+ };
68
+ if (includeStack && error.stack) {
69
+ const frames = error.stack.split("\n").filter((line) => line.trim().startsWith("at ")).slice(0, maxStackFrames).join("\n");
70
+ props = { ...props, $error_stack: frames };
71
+ }
72
+ if (beforeCapture) {
73
+ const transformed = beforeCapture(props);
74
+ if (!transformed) return;
75
+ client.capture("$error", transformed);
76
+ } else {
77
+ client.capture("$error", props);
78
+ }
79
+ }
80
+ render() {
81
+ if (this.state.hasError) return this.props.fallback ?? null;
82
+ return this.props.children;
83
+ }
84
+ };
85
+ export {
86
+ SunglassesErrorBoundary,
87
+ createSentryBeforeSend
88
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@drakkar.software/sunglasses-adapter-sentry",
3
+ "version": "0.1.0",
4
+ "description": "Sentry bridge and React error boundary for SunGlasses — captures errors as analytics events",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.mjs",
12
+ "require": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@drakkar.software/sunglasses-core": "0.3.0"
20
+ },
21
+ "peerDependencies": {
22
+ "react": ">=17"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "react": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^18.3.12",
31
+ "happy-dom": "^15.11.7",
32
+ "react": "^18.3.1",
33
+ "react-dom": "^18.3.1",
34
+ "tsup": "^8.3.5",
35
+ "typescript": "^5.7.2",
36
+ "vitest": "^2.1.8",
37
+ "@drakkar.software/sunglasses-tsconfig": "0.1.0"
38
+ },
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format cjs,esm --dts --clean",
41
+ "dev": "tsup src/index.ts --format cjs,esm --dts --watch",
42
+ "typecheck": "tsc --noEmit",
43
+ "lint": "eslint src/",
44
+ "test": "vitest run",
45
+ "clean": "rm -rf dist .tsbuildinfo"
46
+ }
47
+ }