vimp-engine 0.35.0 → 0.35.1
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/package.json
CHANGED
|
@@ -210,6 +210,7 @@ export default class ClientReportsView {
|
|
|
210
210
|
}
|
|
211
211
|
|
|
212
212
|
details.appendChild(this._line(`User agent: ${report.userAgent ?? '—'}`));
|
|
213
|
+
details.appendChild(this._line(`Role: ${report.role ?? '—'}; page: ${report.page ?? '—'}`));
|
|
213
214
|
details.appendChild(
|
|
214
215
|
this._line(
|
|
215
216
|
`First seen: ${report.firstSeen ?? '—'}; last seen: ${report.lastSeen ?? '—'}` +
|
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
// Репортёр не должен стать новым источником падений и шума: каждая
|
|
7
7
|
// публичная функция глушит свои исключения, повтор известной ошибки — только
|
|
8
8
|
// счётчик, разных ошибок за сессию не больше maxKeysPerSession, отправка —
|
|
9
|
-
// пачками с
|
|
10
|
-
// осознанно: там шум и случайные данные в
|
|
9
|
+
// пачками с дельтами счётчиков: первая через 2 с, следующие не чаще раза в
|
|
10
|
+
// 10 с. console.* не перехватывается осознанно: там шум и случайные данные в
|
|
11
|
+
// аргументах.
|
|
11
12
|
|
|
12
13
|
// обрезки — те же числа, что у приёма на боксе (stage_2.md, контракт)
|
|
13
14
|
const MAX_MESSAGE = 500;
|
|
@@ -125,10 +126,13 @@ function isExtensionUrl(value) {
|
|
|
125
126
|
// очередь переполнена — fetch с keepalive. Ответ не читается
|
|
126
127
|
function defaultSend(url, json) {
|
|
127
128
|
if (typeof navigator !== 'undefined' && navigator.sendBeacon) {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
129
|
+
try {
|
|
130
|
+
if (navigator.sendBeacon(url, new Blob([json], { type: 'application/json' }))) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
// старые Chromium бросали SecurityError на Blob с application/json
|
|
135
|
+
// (crbug.com/490015) — тогда запасной fetch
|
|
132
136
|
}
|
|
133
137
|
}
|
|
134
138
|
|
|
@@ -151,7 +155,9 @@ function noop() {}
|
|
|
151
155
|
* @param {Function} [opts.send] - (url, json) => void; по умолчанию beacon/fetch
|
|
152
156
|
* @param {Function} [opts.now]
|
|
153
157
|
* @param {number} [opts.maxKeysPerSession=50]
|
|
154
|
-
* @param {number} [opts.flushDelayMs=2000]
|
|
158
|
+
* @param {number} [opts.flushDelayMs=2000] - задержка первой отправки
|
|
159
|
+
* @param {number} [opts.minIntervalMs=10000] - интервал между отправками по
|
|
160
|
+
* таймеру; прямой flush() его не ждёт
|
|
155
161
|
* @returns {{ capture, warn, flush, setContext, install }}
|
|
156
162
|
*/
|
|
157
163
|
export function createDiagnostics({
|
|
@@ -161,6 +167,7 @@ export function createDiagnostics({
|
|
|
161
167
|
now = Date.now,
|
|
162
168
|
maxKeysPerSession = 50,
|
|
163
169
|
flushDelayMs = 2000,
|
|
170
|
+
minIntervalMs = 10000,
|
|
164
171
|
} = {}) {
|
|
165
172
|
if (!url) {
|
|
166
173
|
return {
|
|
@@ -181,6 +188,7 @@ export function createDiagnostics({
|
|
|
181
188
|
|
|
182
189
|
let capWarned = false;
|
|
183
190
|
let timer = null;
|
|
191
|
+
let lastSentAt = null;
|
|
184
192
|
|
|
185
193
|
// ошибка внутри самого репортёра не должна порождать новый отчёт
|
|
186
194
|
let busy = false;
|
|
@@ -220,9 +228,16 @@ export function createDiagnostics({
|
|
|
220
228
|
}
|
|
221
229
|
|
|
222
230
|
function schedule() {
|
|
223
|
-
if (timer
|
|
224
|
-
|
|
231
|
+
if (timer !== null) {
|
|
232
|
+
return;
|
|
225
233
|
}
|
|
234
|
+
|
|
235
|
+
// первая отправка — через flushDelayMs, следующие — не чаще
|
|
236
|
+
// minIntervalMs: бокс режет приём лимитом запросов с адреса, и 429
|
|
237
|
+
// молча съедал бы приросты счётчиков непрерывно повторяющейся ошибки
|
|
238
|
+
const sinceLast = lastSentAt === null ? Infinity : now() - lastSentAt;
|
|
239
|
+
|
|
240
|
+
timer = setTimeout(flush, Math.max(flushDelayMs, minIntervalMs - sinceLast));
|
|
226
241
|
}
|
|
227
242
|
|
|
228
243
|
function guarded(fn) {
|
|
@@ -371,14 +386,25 @@ export function createDiagnostics({
|
|
|
371
386
|
}
|
|
372
387
|
}
|
|
373
388
|
|
|
389
|
+
if (items.length > 0) {
|
|
390
|
+
lastSentAt = now();
|
|
391
|
+
}
|
|
392
|
+
|
|
374
393
|
for (let i = 0; i < items.length; i += MAX_BATCH_ITEMS) {
|
|
375
394
|
sendBatch(items.slice(i, i + MAX_BATCH_ITEMS));
|
|
376
395
|
}
|
|
377
396
|
});
|
|
378
397
|
}
|
|
379
398
|
|
|
399
|
+
// смена контекста (игра, роль): накопленное уходит СО СВОИМ контекстом —
|
|
400
|
+
// иначе отчёт игры A, отправленный уже под игрой B, получил бы на боксе
|
|
401
|
+
// отпечаток и source maps игры B
|
|
380
402
|
function setContext(patch) {
|
|
381
403
|
try {
|
|
404
|
+
if (Object.keys(patch).some(key => ctx[key] !== patch[key])) {
|
|
405
|
+
flush();
|
|
406
|
+
}
|
|
407
|
+
|
|
382
408
|
Object.assign(ctx, patch);
|
|
383
409
|
} catch {
|
|
384
410
|
// контекст — вспомогательный, его сбой отчёты не останавливает
|
|
@@ -20,6 +20,24 @@ const SWAP_QUEUE_LIMIT = 2000;
|
|
|
20
20
|
// предел ожидания ответа Worker'а на отладочный запрос (dump/запись)
|
|
21
21
|
const DEBUG_TIMEOUT = 5000;
|
|
22
22
|
|
|
23
|
+
// ErrorEvent Worker'а в главном потоке: `error` там null (исключение между
|
|
24
|
+
// потоками не клонируется) — есть только message и filename/lineno/colno.
|
|
25
|
+
// Кадр собирается из них: без него у отчёта нет верхнего кадра, то есть ни
|
|
26
|
+
// различимого отпечатка, ни расшифровки по source map. Пустой message
|
|
27
|
+
// (Worker не загрузился) бокс отбросил бы — отсюда запасной текст
|
|
28
|
+
function workerErrorReport(event) {
|
|
29
|
+
if (typeof event?.error?.stack === 'string') {
|
|
30
|
+
return event.error;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const message = event?.message || 'Worker error';
|
|
34
|
+
const stack = event?.filename
|
|
35
|
+
? `${message}\n at ${event.filename}:${event.lineno ?? 0}:${event.colno ?? 0}`
|
|
36
|
+
: null;
|
|
37
|
+
|
|
38
|
+
return { message, stack };
|
|
39
|
+
}
|
|
40
|
+
|
|
23
41
|
export default class HostController {
|
|
24
42
|
/**
|
|
25
43
|
* @param {Object} room - настройки комнаты (имя/карта/лимит/таймеры).
|
|
@@ -85,7 +103,7 @@ export default class HostController {
|
|
|
85
103
|
// браузера остаётся как есть
|
|
86
104
|
_watchWorkerErrors(worker) {
|
|
87
105
|
worker.onerror = event =>
|
|
88
|
-
this._diagnostics?.capture(event
|
|
106
|
+
this._diagnostics?.capture(workerErrorReport(event), {
|
|
89
107
|
source: 'host-worker',
|
|
90
108
|
kind: 'worker',
|
|
91
109
|
});
|
|
@@ -342,27 +360,20 @@ export default class HostController {
|
|
|
342
360
|
if (msg.type === 'ready') {
|
|
343
361
|
this._finishSwap();
|
|
344
362
|
} else if (msg.type === 'error') {
|
|
345
|
-
this.
|
|
363
|
+
this._reportWorkerMessage(msg, 'error');
|
|
346
364
|
this._abortSwap(msg.message);
|
|
347
365
|
} else if (msg.type === 'diagnostic') {
|
|
348
|
-
this.
|
|
366
|
+
this._reportWorkerMessage(msg, msg.kind);
|
|
349
367
|
}
|
|
350
368
|
}
|
|
351
369
|
|
|
352
|
-
// сбой
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
kind: 'error',
|
|
357
|
-
});
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
// необработанный reject внутри Worker'а (host.worker.js) — только в
|
|
361
|
-
// журнал: откат эстафеты завязан на 'error', не на него
|
|
362
|
-
_reportWorkerDiagnostic(msg) {
|
|
370
|
+
// сбой Worker'а, присланный сообщением ('error' — провал init,
|
|
371
|
+
// 'diagnostic' — необработанный reject): стек — самого Worker'а, а не
|
|
372
|
+
// главного потока
|
|
373
|
+
_reportWorkerMessage(msg, kind) {
|
|
363
374
|
this._diagnostics?.capture(
|
|
364
|
-
{ message: msg.message, stack: msg.stack },
|
|
365
|
-
{ source: 'host-worker', kind
|
|
375
|
+
{ message: msg.message, stack: msg.stack ?? null },
|
|
376
|
+
{ source: 'host-worker', kind },
|
|
366
377
|
);
|
|
367
378
|
}
|
|
368
379
|
|
|
@@ -426,12 +437,12 @@ export default class HostController {
|
|
|
426
437
|
break;
|
|
427
438
|
|
|
428
439
|
case 'error':
|
|
429
|
-
this.
|
|
440
|
+
this._reportWorkerMessage(msg, 'error');
|
|
430
441
|
this._onError?.(msg);
|
|
431
442
|
break;
|
|
432
443
|
|
|
433
444
|
case 'diagnostic':
|
|
434
|
-
this.
|
|
445
|
+
this._reportWorkerMessage(msg, msg.kind);
|
|
435
446
|
break;
|
|
436
447
|
|
|
437
448
|
case 'map_changed':
|
package/src/config/master.js
CHANGED
|
@@ -175,7 +175,9 @@ export default {
|
|
|
175
175
|
// известных отпечатков бюджет не тратят
|
|
176
176
|
newFingerprintsPerMinute: 60,
|
|
177
177
|
// ключ — адрес, для IPv6 — подсеть /64 (lib/clientIp.js → rateLimitKey)
|
|
178
|
-
|
|
178
|
+
// клиент шлёт не чаще раза в 10 с — запас на несколько вкладок за одним
|
|
179
|
+
// адресом (NAT); главная защита — бюджет новых отпечатков
|
|
180
|
+
rateLimit: { limit: 30, windowMs: 60000 },
|
|
179
181
|
bodyLimit: '16kb',
|
|
180
182
|
maxItemsPerRequest: 10,
|
|
181
183
|
},
|
package/src/host/host.worker.js
CHANGED
|
@@ -178,6 +178,9 @@ self.onmessage = async event => {
|
|
|
178
178
|
self.postMessage({
|
|
179
179
|
type: 'error',
|
|
180
180
|
message: e && e.message ? e.message : String(e),
|
|
181
|
+
// стек Worker'а — для журнала клиентских ошибок: в главном потоке его
|
|
182
|
+
// уже не восстановить
|
|
183
|
+
stack: e && typeof e.stack === 'string' ? e.stack : null,
|
|
181
184
|
});
|
|
182
185
|
}
|
|
183
186
|
break;
|