@genesislcap/foundation-utils 15.50.3 → 15.51.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,177 @@
1
+ import type { ConsolaReporter } from '@genesislcap/foundation-logger';
2
+ /**
3
+ * Severity of a monitoring event or breadcrumb.
4
+ * @public
5
+ */
6
+ export type ErrorMonitoringLevel = 'fatal' | 'error' | 'warning' | 'info' | 'debug';
7
+ /**
8
+ * Context sent with a captured exception or message.
9
+ * @public
10
+ */
11
+ export interface ErrorMonitoringContext {
12
+ level?: ErrorMonitoringLevel;
13
+ tags?: Record<string, string>;
14
+ extra?: Record<string, unknown>;
15
+ }
16
+ /**
17
+ * A breadcrumb: a record of something that happened before an event.
18
+ * @public
19
+ */
20
+ export interface ErrorMonitoringBreadcrumb {
21
+ category?: string;
22
+ level?: string;
23
+ message?: string;
24
+ data?: Record<string, unknown>;
25
+ }
26
+ /**
27
+ * Extra information an SDK passes with a breadcrumb, such as the DOM event or the console arguments.
28
+ * @public
29
+ */
30
+ export type ErrorMonitoringBreadcrumbHint = Record<string, unknown>;
31
+ /**
32
+ * Options {@link setupErrorMonitoring} passes to the client's `init`.
33
+ *
34
+ * @remarks
35
+ * The names match Sentry's `init` options. Any other keys from `initOptions` are passed through.
36
+ * @public
37
+ */
38
+ export interface ErrorMonitoringInitOptions {
39
+ dsn: string;
40
+ environment: string;
41
+ /** Drops the console breadcrumbs foundation loggers write; see {@link isFoundationLoggerConsoleBreadcrumb}. */
42
+ beforeBreadcrumb?: <B extends ErrorMonitoringBreadcrumb>(breadcrumb: B, hint?: ErrorMonitoringBreadcrumbHint) => B | null;
43
+ [key: string]: unknown;
44
+ }
45
+ /**
46
+ * Extra options for the client's `init`, merged into what {@link setupErrorMonitoring} passes.
47
+ * @public
48
+ */
49
+ export interface ErrorMonitoringExtraInitOptions {
50
+ /** Runs after the built-in filter, for breadcrumbs it keeps, with the SDK's hint. */
51
+ beforeBreadcrumb?: (breadcrumb: ErrorMonitoringBreadcrumb, hint?: ErrorMonitoringBreadcrumbHint) => ErrorMonitoringBreadcrumb | null;
52
+ [key: string]: unknown;
53
+ }
54
+ /**
55
+ * An error-monitoring SDK, or an adapter for one. The method names and shapes match the Sentry
56
+ * browser SDKs, so `import * as Sentry from '@sentry/react'` can be passed as is.
57
+ * @public
58
+ */
59
+ export interface ErrorMonitoringClient {
60
+ captureException(error: unknown, context?: ErrorMonitoringContext): unknown;
61
+ captureMessage(message: string, context?: ErrorMonitoringContext): unknown;
62
+ addBreadcrumb?(breadcrumb: ErrorMonitoringBreadcrumb & {
63
+ level?: ErrorMonitoringLevel;
64
+ }): unknown;
65
+ init?(options: ErrorMonitoringInitOptions): unknown;
66
+ }
67
+ /**
68
+ * Picks the monitoring environment from the page's location, or returns `undefined` to leave
69
+ * monitoring off.
70
+ * @public
71
+ */
72
+ export type ErrorMonitoringEnvironmentResolver = (location: Location) => string | undefined;
73
+ /**
74
+ * Options for {@link setupErrorMonitoring}.
75
+ * @public
76
+ */
77
+ export interface ErrorMonitoringOptions {
78
+ /** The DSN events are sent to. Without one, monitoring stays off. */
79
+ dsn: string | undefined;
80
+ /**
81
+ * The environment events are tagged with, or a function that picks it from the page's location.
82
+ * Defaults to the page's hostname, and leaves monitoring off on local hosts: `localhost`,
83
+ * `*.localhost`, `*.local`, `0.0.0.0`, `host.docker.internal`, and loopback, private, link-local
84
+ * and shared (`100.64.0.0/10`, as Tailscale uses) IP addresses, IPv4-mapped IPv6 included.
85
+ */
86
+ environment?: string | ErrorMonitoringEnvironmentResolver;
87
+ /** Extra options merged into what the client's `init` receives, e.g. Sentry's `dataCollection`. */
88
+ initOptions?: ErrorMonitoringExtraInitOptions;
89
+ /** Initialises the client in place of calling `client.init`. */
90
+ init?: (options: ErrorMonitoringInitOptions) => unknown;
91
+ /** Records non-error foundation logs as breadcrumbs. Defaults to `true`. */
92
+ breadcrumbs?: boolean;
93
+ }
94
+ /**
95
+ * Creates a global log reporter that sends foundation logger output to a monitoring client.
96
+ *
97
+ * @remarks
98
+ * No logged data is sent: a report carries only the logger name, the level, the log's message (its
99
+ * first argument, when that is a string) and, for errors, the exception. Objects and any further
100
+ * arguments are left out, so credentials, personal or business data passed to a logger never
101
+ * reach the monitoring service. `error` and `fatal` logs become events: an exception when an
102
+ * `Error` was logged, with the message as `extra.message`, otherwise a message event. A logged
103
+ * `Error` with its own fields or a cause is sent as a copy that keeps only its name, message, stack
104
+ * and `Error` cause. Other logs become breadcrumbs unless `breadcrumbs` is `false`. A failing client
105
+ * never breaks logging.
106
+ *
107
+ * This covers only what the reporter sends. SDKs that record console calls, such as the Sentry
108
+ * browser SDKs by default, also record foundation logger output as console breadcrumbs with the
109
+ * logged arguments as data. When wiring a client by hand, drop those in the SDK's
110
+ * `beforeBreadcrumb` with {@link isFoundationLoggerConsoleBreadcrumb}.
111
+ *
112
+ * {@link setupErrorMonitoring} does both for you; use this directly to wire a client by hand.
113
+ *
114
+ * @param client - The monitoring client.
115
+ * @param options - Set `breadcrumbs: false` to skip breadcrumbs.
116
+ * @public
117
+ */
118
+ export declare const createErrorMonitoringReporter: (client: ErrorMonitoringClient, { breadcrumbs }?: Pick<ErrorMonitoringOptions, "breadcrumbs">) => ConsolaReporter;
119
+ /**
120
+ * True for a console breadcrumb written by a foundation logger.
121
+ *
122
+ * @remarks
123
+ * consola's browser output starts with a `%c` badge (after a timestamp, when timestamps are on)
124
+ * styled as a pill, and SDKs that record console calls join the arguments with `String()`, so these
125
+ * breadcrumbs arrive as CSS and `[object Object]`, and they carry the logged arguments as data. The
126
+ * reporter from {@link createErrorMonitoringReporter} records the same logs' messages without the
127
+ * data. Other `%c`-styled console output doesn't match.
128
+ * @public
129
+ */
130
+ export declare const isFoundationLoggerConsoleBreadcrumb: (breadcrumb: ErrorMonitoringBreadcrumb) => boolean;
131
+ /**
132
+ * Sets up error monitoring for a Genesis app in one call.
133
+ *
134
+ * @remarks
135
+ * Without a DSN, or when no environment applies (by default on local hosts such as `localhost` or
136
+ * a private IP address), it does nothing and resolves `false`. The environment defaults to the
137
+ * page's hostname, so one image promoted across environments reports each under its own name; pass
138
+ * a function to map hostnames to names such as `uat`. Otherwise it:
139
+ *
140
+ * - initialises the client (`client.init`, or `options.init`) with the DSN, environment, a
141
+ * `beforeBreadcrumb` that drops the SDK's console breadcrumbs for foundation logger output (they
142
+ * carry the logged arguments), and any `initOptions`;
143
+ * - registers a global log reporter ({@link createErrorMonitoringReporter}), so errors logged by
144
+ * every foundation logger, including those Genesis packages create at import time, are reported,
145
+ * and other logs become breadcrumbs. Only log messages and exceptions are sent, never logged data.
146
+ *
147
+ * Logs written while an asynchronous `init` runs (the last 100) are reported once it finishes. If
148
+ * `init` throws or rejects, for example when a lazily loaded SDK fails to download, it warns on the
149
+ * console and resolves `false` rather than rejecting.
150
+ *
151
+ * Errors caught by the foundation-react-utils `AppErrorBoundary` and `TileErrorBoundary` are logged
152
+ * with their reference ID and component stack in the message, so they are reported too without an
153
+ * `onError`.
154
+ * Pair them with `onCaughtError` on the React root.
155
+ *
156
+ * Calling it again replaces the previous reporter, or removes it if the new call has no DSN or
157
+ * environment. When calls overlap, only the last one takes effect, and the earlier ones resolve
158
+ * `false`.
159
+ *
160
+ * @example
161
+ * ```ts
162
+ * import * as Sentry from '@sentry/react';
163
+ * import { setupErrorMonitoring } from '@genesislcap/foundation-utils';
164
+ *
165
+ * setupErrorMonitoring(Sentry, {
166
+ * dsn: 'https://<key>@<org>.ingest.sentry.io/<project>',
167
+ * environment: ({ hostname }) => ({ 'app-uat.example.com': 'uat', 'app.example.com': 'prod' })[hostname],
168
+ * });
169
+ * ```
170
+ *
171
+ * @param client - The monitoring SDK (for example the Sentry namespace) or an adapter for one.
172
+ * @param options - The DSN, the environment, and extra init options.
173
+ * @returns Whether this call enabled monitoring.
174
+ * @public
175
+ */
176
+ export declare function setupErrorMonitoring(client: ErrorMonitoringClient, options: ErrorMonitoringOptions): Promise<boolean>;
177
+ //# sourceMappingURL=error-monitoring.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-monitoring.d.ts","sourceRoot":"","sources":["../../../src/error-monitoring/error-monitoring.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export * from './error-monitoring';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/error-monitoring/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
@@ -6,6 +6,7 @@ export * from './directives';
6
6
  export * from './encoding';
7
7
  export * from './env';
8
8
  export * from './error';
9
+ export * from './error-monitoring';
9
10
  export * from './feature-flags';
10
11
  export * from './formatters';
11
12
  export * from './logger';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,QAAQ,CAAC;AACvB,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC;AAC3B,cAAc,OAAO,CAAC;AACtB,cAAc,SAAS,CAAC;AACxB,cAAc,oBAAoB,CAAC;AACnC,cAAc,iBAAiB,CAAC;AAChC,cAAc,cAAc,CAAC;AAC7B,cAAc,UAAU,CAAC;AACzB,cAAc,WAAW,CAAC;AAC1B,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,WAAW,CAAC;AAC1B,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,eAAe,CAAC;AAC9B,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,cAAc,CAAC"}
@@ -0,0 +1,259 @@
1
+ import { __awaiter } from "tslib";
2
+ import { addGlobalLogReporter } from '@genesislcap/foundation-logger';
3
+ const REPORTED_LOG_TYPES = new Set(['error', 'fatal']);
4
+ const MAX_BUFFERED_LOGS = 100;
5
+ const LOCAL_HOSTNAMES = new Set(['localhost', '0.0.0.0', 'host.docker.internal']);
6
+ const LOCAL_HOSTNAME_SUFFIXES = ['.localhost', '.local'];
7
+ const IPV4 = /^\d{1,3}(\.\d{1,3}){3}$/;
8
+ const LOCAL_IPV4 = /^(127|10|0)\.|^192\.168\.|^169\.254\.|^172\.(1[6-9]|2\d|3[01])\.|^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./;
9
+ const LOCAL_IPV6 = /^\[(::1?\]|f[cd]|fe[89ab])/i;
10
+ const BITS_PER_BYTE = 8;
11
+ const BYTE_MASK = 0xff;
12
+ const IPV4_MAPPED_IPV6 = /^\[::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})\]$/i;
13
+ const mappedIpv4 = (hostname) => {
14
+ const groups = IPV4_MAPPED_IPV6.exec(hostname);
15
+ if (!groups) {
16
+ return undefined;
17
+ }
18
+ const [high, low] = [parseInt(groups[1], 16), parseInt(groups[2], 16)];
19
+ return [high >> BITS_PER_BYTE, high & BYTE_MASK, low >> BITS_PER_BYTE, low & BYTE_MASK].join('.');
20
+ };
21
+ const isLocalHostname = (hostname) => {
22
+ var _a;
23
+ const host = (_a = mappedIpv4(hostname)) !== null && _a !== void 0 ? _a : hostname.replace(/\.$/, '');
24
+ return (LOCAL_HOSTNAMES.has(host) ||
25
+ LOCAL_HOSTNAME_SUFFIXES.some((suffix) => host.endsWith(suffix)) ||
26
+ (IPV4.test(host) && LOCAL_IPV4.test(host)) ||
27
+ LOCAL_IPV6.test(host));
28
+ };
29
+ const hostnameEnvironment = ({ hostname }) => isLocalHostname(hostname) ? undefined : hostname;
30
+ const resolveEnvironment = (environment = hostnameEnvironment) => {
31
+ if (typeof environment === 'string') {
32
+ return nonEmptyString(environment);
33
+ }
34
+ return typeof location === 'undefined' ? undefined : nonEmptyString(environment(location));
35
+ };
36
+ const CONSOLA_BADGE = /^(?:\d{2}:\d{2}:\d{2}\.\d{3} )?%c[^%]*(?:%c |$)/;
37
+ const CONSOLA_BADGE_STYLE = /border-radius: 0\.5em;[\s\S]*padding: 2px 0\.5em;/;
38
+ /** consola log types mapped to monitoring severities; anything unlisted is `info`. */
39
+ const BREADCRUMB_LEVELS = {
40
+ warn: 'warning',
41
+ debug: 'debug',
42
+ trace: 'debug',
43
+ verbose: 'debug',
44
+ };
45
+ const nonEmptyString = (value) => typeof value === 'string' && value ? value : undefined;
46
+ const MAX_MESSAGE_LENGTH = 2000;
47
+ const MAX_CAUSE_DEPTH = 5;
48
+ const logMessage = ([first]) => {
49
+ if (first instanceof Error) {
50
+ return `${first.name}: ${first.message}`;
51
+ }
52
+ return typeof first === 'string'
53
+ ? nonEmptyString(first.trim().slice(0, MAX_MESSAGE_LENGTH))
54
+ : undefined;
55
+ };
56
+ /**
57
+ * The error itself, or, when it has its own fields or a cause, a copy with the same prototype that
58
+ * keeps only its name, message, stack and an `Error` cause, so integrations that read an error's
59
+ * fields find nothing else.
60
+ */
61
+ const withoutOwnFields = (error, depth = 0) => {
62
+ const cause = Reflect.get(error, 'cause');
63
+ if (Object.keys(error).length === 0 && cause === undefined) {
64
+ return error;
65
+ }
66
+ const fields = {
67
+ name: error.name,
68
+ message: error.message,
69
+ stack: error.stack,
70
+ };
71
+ if (cause instanceof Error && depth < MAX_CAUSE_DEPTH) {
72
+ fields.cause = withoutOwnFields(cause, depth + 1);
73
+ }
74
+ const copy = Object.create(Object.getPrototypeOf(error));
75
+ for (const [key, value] of Object.entries(fields)) {
76
+ Object.defineProperty(copy, key, { value, writable: true, configurable: true });
77
+ }
78
+ return copy;
79
+ };
80
+ /**
81
+ * Creates a global log reporter that sends foundation logger output to a monitoring client.
82
+ *
83
+ * @remarks
84
+ * No logged data is sent: a report carries only the logger name, the level, the log's message (its
85
+ * first argument, when that is a string) and, for errors, the exception. Objects and any further
86
+ * arguments are left out, so credentials, personal or business data passed to a logger never
87
+ * reach the monitoring service. `error` and `fatal` logs become events: an exception when an
88
+ * `Error` was logged, with the message as `extra.message`, otherwise a message event. A logged
89
+ * `Error` with its own fields or a cause is sent as a copy that keeps only its name, message, stack
90
+ * and `Error` cause. Other logs become breadcrumbs unless `breadcrumbs` is `false`. A failing client
91
+ * never breaks logging.
92
+ *
93
+ * This covers only what the reporter sends. SDKs that record console calls, such as the Sentry
94
+ * browser SDKs by default, also record foundation logger output as console breadcrumbs with the
95
+ * logged arguments as data. When wiring a client by hand, drop those in the SDK's
96
+ * `beforeBreadcrumb` with {@link isFoundationLoggerConsoleBreadcrumb}.
97
+ *
98
+ * {@link setupErrorMonitoring} does both for you; use this directly to wire a client by hand.
99
+ *
100
+ * @param client - The monitoring client.
101
+ * @param options - Set `breadcrumbs: false` to skip breadcrumbs.
102
+ * @public
103
+ */
104
+ export const createErrorMonitoringReporter = (client, { breadcrumbs = true } = {}) => ({
105
+ log: ({ type, tag, args }) => {
106
+ var _a, _b;
107
+ try {
108
+ const message = logMessage(args);
109
+ if (!REPORTED_LOG_TYPES.has(type)) {
110
+ if (breadcrumbs) {
111
+ (_a = client.addBreadcrumb) === null || _a === void 0 ? void 0 : _a.call(client, {
112
+ category: tag || 'logger',
113
+ level: (_b = BREADCRUMB_LEVELS[type]) !== null && _b !== void 0 ? _b : 'info',
114
+ message,
115
+ });
116
+ }
117
+ return;
118
+ }
119
+ const context = Object.assign({ level: type === 'fatal' ? 'fatal' : 'error', tags: { logger: tag || 'unknown' } }, (message ? { extra: { message } } : {}));
120
+ const error = args.find((arg) => arg instanceof Error);
121
+ if (error) {
122
+ client.captureException(withoutOwnFields(error), context);
123
+ return;
124
+ }
125
+ client.captureMessage(`${tag || 'unknown'}: ${message !== null && message !== void 0 ? message : '[no message]'}`, context);
126
+ }
127
+ catch (_c) { }
128
+ },
129
+ });
130
+ /**
131
+ * True for a console breadcrumb written by a foundation logger.
132
+ *
133
+ * @remarks
134
+ * consola's browser output starts with a `%c` badge (after a timestamp, when timestamps are on)
135
+ * styled as a pill, and SDKs that record console calls join the arguments with `String()`, so these
136
+ * breadcrumbs arrive as CSS and `[object Object]`, and they carry the logged arguments as data. The
137
+ * reporter from {@link createErrorMonitoringReporter} records the same logs' messages without the
138
+ * data. Other `%c`-styled console output doesn't match.
139
+ * @public
140
+ */
141
+ export const isFoundationLoggerConsoleBreadcrumb = (breadcrumb) => {
142
+ var _a;
143
+ const args = (_a = breadcrumb.data) === null || _a === void 0 ? void 0 : _a.arguments;
144
+ if (breadcrumb.category !== 'console' || !Array.isArray(args)) {
145
+ return false;
146
+ }
147
+ const [first, style] = args;
148
+ return (typeof first === 'string' &&
149
+ CONSOLA_BADGE.test(first) &&
150
+ typeof style === 'string' &&
151
+ CONSOLA_BADGE_STYLE.test(style));
152
+ };
153
+ let removeActiveReporter;
154
+ let latestSetup = 0;
155
+ const bufferLogs = () => {
156
+ const logs = [];
157
+ const stop = addGlobalLogReporter({
158
+ log: (...log) => {
159
+ logs.push(log);
160
+ if (logs.length > MAX_BUFFERED_LOGS) {
161
+ logs.shift();
162
+ }
163
+ },
164
+ });
165
+ return { logs, stop };
166
+ };
167
+ /**
168
+ * Sets up error monitoring for a Genesis app in one call.
169
+ *
170
+ * @remarks
171
+ * Without a DSN, or when no environment applies (by default on local hosts such as `localhost` or
172
+ * a private IP address), it does nothing and resolves `false`. The environment defaults to the
173
+ * page's hostname, so one image promoted across environments reports each under its own name; pass
174
+ * a function to map hostnames to names such as `uat`. Otherwise it:
175
+ *
176
+ * - initialises the client (`client.init`, or `options.init`) with the DSN, environment, a
177
+ * `beforeBreadcrumb` that drops the SDK's console breadcrumbs for foundation logger output (they
178
+ * carry the logged arguments), and any `initOptions`;
179
+ * - registers a global log reporter ({@link createErrorMonitoringReporter}), so errors logged by
180
+ * every foundation logger, including those Genesis packages create at import time, are reported,
181
+ * and other logs become breadcrumbs. Only log messages and exceptions are sent, never logged data.
182
+ *
183
+ * Logs written while an asynchronous `init` runs (the last 100) are reported once it finishes. If
184
+ * `init` throws or rejects, for example when a lazily loaded SDK fails to download, it warns on the
185
+ * console and resolves `false` rather than rejecting.
186
+ *
187
+ * Errors caught by the foundation-react-utils `AppErrorBoundary` and `TileErrorBoundary` are logged
188
+ * with their reference ID and component stack in the message, so they are reported too without an
189
+ * `onError`.
190
+ * Pair them with `onCaughtError` on the React root.
191
+ *
192
+ * Calling it again replaces the previous reporter, or removes it if the new call has no DSN or
193
+ * environment. When calls overlap, only the last one takes effect, and the earlier ones resolve
194
+ * `false`.
195
+ *
196
+ * @example
197
+ * ```ts
198
+ * import * as Sentry from '@sentry/react';
199
+ * import { setupErrorMonitoring } from '@genesislcap/foundation-utils';
200
+ *
201
+ * setupErrorMonitoring(Sentry, {
202
+ * dsn: 'https://<key>@<org>.ingest.sentry.io/<project>',
203
+ * environment: ({ hostname }) => ({ 'app-uat.example.com': 'uat', 'app.example.com': 'prod' })[hostname],
204
+ * });
205
+ * ```
206
+ *
207
+ * @param client - The monitoring SDK (for example the Sentry namespace) or an adapter for one.
208
+ * @param options - The DSN, the environment, and extra init options.
209
+ * @returns Whether this call enabled monitoring.
210
+ * @public
211
+ */
212
+ export function setupErrorMonitoring(client, options) {
213
+ return __awaiter(this, void 0, void 0, function* () {
214
+ var _a;
215
+ const { initOptions = {}, init, breadcrumbs = true } = options;
216
+ latestSetup += 1;
217
+ const setup = latestSetup;
218
+ const isSuperseded = () => setup !== latestSetup;
219
+ removeActiveReporter === null || removeActiveReporter === void 0 ? void 0 : removeActiveReporter();
220
+ removeActiveReporter = undefined;
221
+ const dsn = nonEmptyString(options.dsn);
222
+ const environment = resolveEnvironment(options.environment);
223
+ if (!dsn || !environment) {
224
+ return false;
225
+ }
226
+ const buffer = bufferLogs();
227
+ try {
228
+ const appBeforeBreadcrumb = initOptions.beforeBreadcrumb;
229
+ const beforeBreadcrumb = (breadcrumb, hint) => {
230
+ if (isFoundationLoggerConsoleBreadcrumb(breadcrumb)) {
231
+ return null;
232
+ }
233
+ return appBeforeBreadcrumb ? appBeforeBreadcrumb(breadcrumb, hint) : breadcrumb;
234
+ };
235
+ const resolvedInitOptions = Object.assign(Object.assign({}, initOptions), { dsn,
236
+ environment,
237
+ beforeBreadcrumb });
238
+ try {
239
+ yield (init ? init(resolvedInitOptions) : (_a = client.init) === null || _a === void 0 ? void 0 : _a.call(client, resolvedInitOptions));
240
+ }
241
+ catch (error) {
242
+ console.warn('Error monitoring is off: the client failed to initialise.', error);
243
+ return false;
244
+ }
245
+ if (isSuperseded()) {
246
+ return false;
247
+ }
248
+ const reporter = createErrorMonitoringReporter(client, { breadcrumbs });
249
+ for (const log of buffer.logs) {
250
+ reporter.log(...log);
251
+ }
252
+ removeActiveReporter = addGlobalLogReporter(reporter);
253
+ return true;
254
+ }
255
+ finally {
256
+ buffer.stop();
257
+ }
258
+ });
259
+ }
@@ -0,0 +1 @@
1
+ export * from './error-monitoring';
package/dist/esm/index.js CHANGED
@@ -6,6 +6,7 @@ export * from './directives';
6
6
  export * from './encoding';
7
7
  export * from './env';
8
8
  export * from './error';
9
+ export * from './error-monitoring';
9
10
  export * from './feature-flags';
10
11
  export * from './formatters';
11
12
  export * from './logger';