@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.
- package/dist/custom-elements.json +537 -405
- package/dist/dts/error-monitoring/error-monitoring.d.ts +177 -0
- package/dist/dts/error-monitoring/error-monitoring.d.ts.map +1 -0
- package/dist/dts/error-monitoring/index.d.ts +2 -0
- package/dist/dts/error-monitoring/index.d.ts.map +1 -0
- package/dist/dts/index.d.ts +1 -0
- package/dist/dts/index.d.ts.map +1 -1
- package/dist/esm/error-monitoring/error-monitoring.js +259 -0
- package/dist/esm/error-monitoring/index.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/foundation-utils.api.json +1307 -99
- package/dist/foundation-utils.d.ts +188 -0
- package/package.json +9 -9
|
@@ -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 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/error-monitoring/index.ts"],"names":[],"mappings":"AAAA,cAAc,oBAAoB,CAAC"}
|
package/dist/dts/index.d.ts
CHANGED
package/dist/dts/index.d.ts.map
CHANGED
|
@@ -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