@steve31415/baselib 3.23.0 → 3.25.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/log-browser.d.ts +33 -1
- package/dist/log-browser.js +126 -4
- package/package.json +1 -1
package/dist/log-browser.d.ts
CHANGED
|
@@ -10,6 +10,38 @@ export interface BrowserLoggerOptions {
|
|
|
10
10
|
/** Also mirror events to the devtools console (default false). */
|
|
11
11
|
console?: boolean;
|
|
12
12
|
}
|
|
13
|
+
/** One random id per page load, stamped on every browser event as
|
|
14
|
+
* `page_id`. Exported so an app can send the same id to its server (e.g. a
|
|
15
|
+
* socket's client id) and join the two sides in one query. */
|
|
16
|
+
export declare const pageId: string;
|
|
17
|
+
/**
|
|
18
|
+
* A coarse `<os>-<browser>` label from a user-agent string — enough to tell
|
|
19
|
+
* Steve's devices apart in a query, deliberately nothing finer (no versions,
|
|
20
|
+
* no model). os: android | ios | mac | windows | chromeos | linux | other;
|
|
21
|
+
* browser: edge | opera | firefox | chrome | safari | other. An iPad asking for
|
|
22
|
+
* the desktop site reports itself as a Mac; pass `touchPoints`
|
|
23
|
+
* (`navigator.maxTouchPoints`) to tell it apart.
|
|
24
|
+
*/
|
|
25
|
+
export declare function deviceLabel(userAgent: string, touchPoints?: number): string;
|
|
13
26
|
export declare function createBrowserLogger(opts?: BrowserLoggerOptions): Logger;
|
|
14
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Make the shared `logger` forward to `instance`. Call once at boot, right
|
|
29
|
+
* after createBrowserLogger; tests install a recorder.
|
|
30
|
+
*/
|
|
31
|
+
export declare function installBrowserLogger(instance: Logger | null): void;
|
|
32
|
+
/**
|
|
33
|
+
* The page's logger, importable from any module — no threading a Logger
|
|
34
|
+
* through props and constructors. It forwards to the instance given to
|
|
35
|
+
* installBrowserLogger; before that (or in a test that installs none)
|
|
36
|
+
* events go to the devtools console rather than being lost.
|
|
37
|
+
*/
|
|
38
|
+
export declare const logger: Logger;
|
|
39
|
+
/**
|
|
40
|
+
* Route uncaught errors, unhandled rejections, and failed resource loads
|
|
41
|
+
* (`<img>`, `<script>`, `<link>`, media) into the logger. A resource load
|
|
42
|
+
* failure fires a non-bubbling `error` on its element, which only a
|
|
43
|
+
* capturing window listener sees; it logs WARN while the browser reports
|
|
44
|
+
* itself online and INFO while offline (expected then), once per URL per
|
|
45
|
+
* page so a re-rendering broken element cannot flood the dataset.
|
|
46
|
+
*/
|
|
15
47
|
export declare function installGlobalErrorHandlers(logger: Logger): void;
|
package/dist/log-browser.js
CHANGED
|
@@ -10,8 +10,61 @@
|
|
|
10
10
|
//
|
|
11
11
|
// The token is ingest-only and dataset-scoped by design — visible to any
|
|
12
12
|
// signed-in user, able to do nothing but add log events.
|
|
13
|
+
//
|
|
14
|
+
// Every event also carries two top-level fields saying where it came from,
|
|
15
|
+
// because browser events have no request to join on (2026-10-03, radar: telling
|
|
16
|
+
// a phone put down from a sleeping Mac from a server fault took the
|
|
17
|
+
// load-balancer log): `page_id`, one random id per page load, and `device`, a
|
|
18
|
+
// coarse `<os>-<browser>` label such as `mac-chrome` or `android-chrome`.
|
|
13
19
|
import { LogShipper, makeEvents } from './log-core.js';
|
|
14
20
|
export { serializeError } from './log-core.js';
|
|
21
|
+
/** One random id per page load, stamped on every browser event as
|
|
22
|
+
* `page_id`. Exported so an app can send the same id to its server (e.g. a
|
|
23
|
+
* socket's client id) and join the two sides in one query. */
|
|
24
|
+
export const pageId = newPageId();
|
|
25
|
+
function newPageId() {
|
|
26
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function')
|
|
27
|
+
return crypto.randomUUID();
|
|
28
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 12)}`;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* A coarse `<os>-<browser>` label from a user-agent string — enough to tell
|
|
32
|
+
* Steve's devices apart in a query, deliberately nothing finer (no versions,
|
|
33
|
+
* no model). os: android | ios | mac | windows | chromeos | linux | other;
|
|
34
|
+
* browser: edge | opera | firefox | chrome | safari | other. An iPad asking for
|
|
35
|
+
* the desktop site reports itself as a Mac; pass `touchPoints`
|
|
36
|
+
* (`navigator.maxTouchPoints`) to tell it apart.
|
|
37
|
+
*/
|
|
38
|
+
export function deviceLabel(userAgent, touchPoints = 0) {
|
|
39
|
+
const ua = userAgent;
|
|
40
|
+
const os = /Android/.test(ua)
|
|
41
|
+
? 'android'
|
|
42
|
+
: /iPhone|iPad|iPod/.test(ua)
|
|
43
|
+
? 'ios'
|
|
44
|
+
: /Macintosh|Mac OS X/.test(ua)
|
|
45
|
+
? touchPoints > 1
|
|
46
|
+
? 'ios'
|
|
47
|
+
: 'mac'
|
|
48
|
+
: /Windows/.test(ua)
|
|
49
|
+
? 'windows'
|
|
50
|
+
: /CrOS/.test(ua)
|
|
51
|
+
? 'chromeos'
|
|
52
|
+
: /Linux/.test(ua)
|
|
53
|
+
? 'linux'
|
|
54
|
+
: 'other';
|
|
55
|
+
const browser = /Edg(e|A|iOS)?\//.test(ua)
|
|
56
|
+
? 'edge'
|
|
57
|
+
: /OPR\/|OPiOS\//.test(ua)
|
|
58
|
+
? 'opera'
|
|
59
|
+
: /Firefox\/|FxiOS\//.test(ua)
|
|
60
|
+
? 'firefox'
|
|
61
|
+
: /Chrome\/|CriOS\//.test(ua)
|
|
62
|
+
? 'chrome'
|
|
63
|
+
: /Safari\//.test(ua)
|
|
64
|
+
? 'safari'
|
|
65
|
+
: 'other';
|
|
66
|
+
return `${os}-${browser}`;
|
|
67
|
+
}
|
|
15
68
|
function metaContent(name) {
|
|
16
69
|
return document.querySelector(`meta[name="${name}"]`)?.content || undefined;
|
|
17
70
|
}
|
|
@@ -68,7 +121,12 @@ export function createBrowserLogger(opts = {}) {
|
|
|
68
121
|
flush: () => shipper?.flush() ?? Promise.resolve(),
|
|
69
122
|
};
|
|
70
123
|
}
|
|
71
|
-
const logger = build({
|
|
124
|
+
const logger = build({
|
|
125
|
+
...(buildId ? { build_id: buildId } : {}),
|
|
126
|
+
page_id: pageId,
|
|
127
|
+
device: deviceLabel(navigator.userAgent, navigator.maxTouchPoints ?? 0),
|
|
128
|
+
...opts.context,
|
|
129
|
+
});
|
|
72
130
|
// Ship what's pending whenever the page may be going away.
|
|
73
131
|
const flushNow = () => void logger.flush();
|
|
74
132
|
addEventListener('pagehide', flushNow);
|
|
@@ -79,11 +137,75 @@ export function createBrowserLogger(opts = {}) {
|
|
|
79
137
|
});
|
|
80
138
|
return logger;
|
|
81
139
|
}
|
|
82
|
-
|
|
140
|
+
let installed = null;
|
|
141
|
+
/**
|
|
142
|
+
* Make the shared `logger` forward to `instance`. Call once at boot, right
|
|
143
|
+
* after createBrowserLogger; tests install a recorder.
|
|
144
|
+
*/
|
|
145
|
+
export function installBrowserLogger(instance) {
|
|
146
|
+
installed = instance;
|
|
147
|
+
}
|
|
148
|
+
const CONSOLE_METHOD = { debug: 'debug', info: 'log', warn: 'warn', error: 'error' };
|
|
149
|
+
function forwardingLogger(context) {
|
|
150
|
+
const emit = (level, message, meta) => {
|
|
151
|
+
// Resolved per call, so a module-scope `logger.with(...)` taken before
|
|
152
|
+
// boot still ships once the app installs its logger.
|
|
153
|
+
if (installed)
|
|
154
|
+
(context ? installed.with(context) : installed)[level](message, meta);
|
|
155
|
+
else
|
|
156
|
+
console[CONSOLE_METHOD[level]](message, meta ?? '');
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
debug: (m, meta) => emit('debug', m, meta),
|
|
160
|
+
info: (m, meta) => emit('info', m, meta),
|
|
161
|
+
warn: (m, meta) => emit('warn', m, meta),
|
|
162
|
+
error: (m, meta) => emit('error', m, meta),
|
|
163
|
+
with: (extra) => forwardingLogger({ ...context, ...extra }),
|
|
164
|
+
flush: () => installed?.flush() ?? Promise.resolve(),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* The page's logger, importable from any module — no threading a Logger
|
|
169
|
+
* through props and constructors. It forwards to the instance given to
|
|
170
|
+
* installBrowserLogger; before that (or in a test that installs none)
|
|
171
|
+
* events go to the devtools console rather than being lost.
|
|
172
|
+
*/
|
|
173
|
+
export const logger = forwardingLogger(undefined);
|
|
174
|
+
const MAX_RESOURCE_URL = 500;
|
|
175
|
+
function resourceUrl(target) {
|
|
176
|
+
// The resolved (absolute) URL, as the browser requested it.
|
|
177
|
+
const element = target;
|
|
178
|
+
const url = element.currentSrc || element.src || element.href || '';
|
|
179
|
+
return url.length > MAX_RESOURCE_URL ? `${url.slice(0, MAX_RESOURCE_URL)}…` : url;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Route uncaught errors, unhandled rejections, and failed resource loads
|
|
183
|
+
* (`<img>`, `<script>`, `<link>`, media) into the logger. A resource load
|
|
184
|
+
* failure fires a non-bubbling `error` on its element, which only a
|
|
185
|
+
* capturing window listener sees; it logs WARN while the browser reports
|
|
186
|
+
* itself online and INFO while offline (expected then), once per URL per
|
|
187
|
+
* page so a re-rendering broken element cannot flood the dataset.
|
|
188
|
+
*/
|
|
83
189
|
export function installGlobalErrorHandlers(logger) {
|
|
190
|
+
const failedResources = new Set();
|
|
84
191
|
addEventListener('error', (e) => {
|
|
85
|
-
|
|
86
|
-
|
|
192
|
+
const target = e.target;
|
|
193
|
+
if (typeof Element !== 'undefined' && target instanceof Element) {
|
|
194
|
+
const url = resourceUrl(target);
|
|
195
|
+
if (failedResources.has(url))
|
|
196
|
+
return;
|
|
197
|
+
failedResources.add(url);
|
|
198
|
+
const online = navigator.onLine !== false;
|
|
199
|
+
logger[online ? 'warn' : 'info']('resource failed to load', {
|
|
200
|
+
element: target.tagName.toLowerCase(),
|
|
201
|
+
url,
|
|
202
|
+
online,
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const event = e;
|
|
207
|
+
logger.error('uncaught error', { error: event.error ?? event.message });
|
|
208
|
+
}, true);
|
|
87
209
|
addEventListener('unhandledrejection', (e) => {
|
|
88
210
|
logger.error('unhandled rejection', { error: e.reason });
|
|
89
211
|
});
|
package/package.json
CHANGED