peekinside 1.2.4 → 1.3.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/README.md +13 -2
- package/index.d.ts +44 -13
- package/package.json +10 -1
- package/src/PeekInside.js +93 -17
- package/src/alerts/rule-matcher.js +158 -0
- package/src/capture/exchange-recorder.js +5 -12
- package/src/capture/http-message.js +17 -0
- package/src/crypto.js +26 -2
- package/src/dumps/dump-capture.js +100 -0
- package/src/dumps/dump-record.js +57 -0
- package/src/dumps/dump-value.js +428 -0
- package/src/exceptions/exception-capture.js +5 -4
- package/src/exceptions/exception-record.js +11 -1
- package/src/exceptions/request-context.js +118 -10
- package/src/hub/ingest-payload.js +89 -8
- package/src/hub/project-log.js +71 -4
- package/src/hub/server.js +19 -8
- package/src/hub/spool-ingester.js +1 -0
- package/src/hub/store.js +44 -1
- package/src/instrumentation/index.js +48 -3
- package/src/instrumentation/jobs.js +114 -6
- package/src/instrumentation/loggers.js +78 -4
- package/src/integrations/fastify.js +18 -1
- package/src/integrations/hono.js +209 -0
- package/src/integrations/nest.js +277 -0
- package/src/integrations/server-binder.js +9 -6
- package/src/logs/log-capture.js +5 -5
- package/src/logs/log-record.js +2 -2
- package/src/record.js +6 -2
- package/src/runtime/request-counter.js +40 -0
- package/src/runtime/sampler.js +120 -6
- package/src/timeline/timeline.js +24 -3
- package/src/transport/batch.js +4 -1
- package/src/transport/cloud.js +34 -3
- package/src/transport/local.js +7 -5
- package/src/user.js +18 -7
package/src/runtime/sampler.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Samples the process now and then: memory (to spot leaks, as growth over hours
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* Samples the process now and then: memory (to spot leaks, as growth over hours, and how close the
|
|
3
|
+
* heap is to its limit), CPU, the event loop (its delay, and how busy it was), garbage collection,
|
|
4
|
+
* open handles and the requests it served. Samples are numbers only, and go to the dashboard with
|
|
5
|
+
* the other records: every 10 seconds to the local dashboard, every minute to PeekInside Cloud.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { performance } from 'perf_hooks';
|
|
8
|
+
import { PerformanceObserver, performance } from 'perf_hooks';
|
|
9
|
+
import v8 from 'v8';
|
|
8
10
|
import { RequestContext } from '../exceptions/request-context.js';
|
|
9
11
|
|
|
10
12
|
/**
|
|
@@ -18,8 +20,73 @@ import { RequestContext } from '../exceptions/request-context.js';
|
|
|
18
20
|
* @property {number|null} loopP50 - Event-loop delay since the last sample, ms
|
|
19
21
|
* @property {number|null} loopP99
|
|
20
22
|
* @property {number|null} loopMax
|
|
23
|
+
* @property {number|null} arrayBuffers - Bytes of ArrayBuffers and Buffers (part of external)
|
|
24
|
+
* @property {number|null} heapLimit - Bytes the heap may grow to before the process runs out of memory
|
|
25
|
+
* @property {number|null} elu - Percent of the time since the last sample the event loop was busy
|
|
26
|
+
* @property {number|null} gcCount - Garbage collections since the last sample
|
|
27
|
+
* @property {number|null} gcMs - Time they paused the process, ms
|
|
28
|
+
* @property {number|null} gcMax - The longest of them, ms
|
|
29
|
+
* @property {number|null} handles - Open handles and requests (sockets, timers, files) now
|
|
30
|
+
* @property {number|null} requests - Requests this process finished since the last sample
|
|
31
|
+
* @property {number|null} inFlight - Requests open when the sample was taken
|
|
21
32
|
*/
|
|
22
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Garbage collections since the last look: how many, and how long they paused the process.
|
|
36
|
+
*/
|
|
37
|
+
class GcWatch {
|
|
38
|
+
constructor() {
|
|
39
|
+
this.observer = null;
|
|
40
|
+
this.reset();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @returns {void}
|
|
45
|
+
*/
|
|
46
|
+
start() {
|
|
47
|
+
if (this.observer) return;
|
|
48
|
+
try {
|
|
49
|
+
this.observer = new PerformanceObserver((list) => {
|
|
50
|
+
for (const entry of list.getEntries()) {
|
|
51
|
+
this.count++;
|
|
52
|
+
this.ms += entry.duration;
|
|
53
|
+
if (entry.duration > this.max) this.max = entry.duration;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
// Outside any request: collections belong to the process, not to the request that started it.
|
|
57
|
+
RequestContext.outside(() => this.observer.observe({ entryTypes: ['gc'] }));
|
|
58
|
+
} catch (error) {
|
|
59
|
+
this.observer = null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @returns {void}
|
|
65
|
+
*/
|
|
66
|
+
stop() {
|
|
67
|
+
if (this.observer) this.observer.disconnect();
|
|
68
|
+
this.observer = null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The collections since the last call, or null when they can't be watched.
|
|
73
|
+
*
|
|
74
|
+
* @returns {{ count: number, ms: number, max: number }|null}
|
|
75
|
+
*/
|
|
76
|
+
take() {
|
|
77
|
+
if (!this.observer) return null;
|
|
78
|
+
const taken = { count: this.count, ms: round(this.ms), max: round(this.max) };
|
|
79
|
+
this.reset();
|
|
80
|
+
return taken;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
reset() {
|
|
84
|
+
this.count = 0;
|
|
85
|
+
this.ms = 0;
|
|
86
|
+
this.max = 0;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
23
90
|
/**
|
|
24
91
|
* Takes a sample every `intervalMs` and hands it over.
|
|
25
92
|
*/
|
|
@@ -29,14 +96,18 @@ export class RuntimeSampler {
|
|
|
29
96
|
* @param {number} options.intervalMs
|
|
30
97
|
* @param {(sample: RuntimeSample) => void} options.onSample
|
|
31
98
|
* @param {import('./loop-monitor.js').LoopMonitor} options.monitor - Measures the delay
|
|
99
|
+
* @param {import('./request-counter.js').RequestCounter} [options.requests] - Counts the requests served
|
|
32
100
|
*/
|
|
33
|
-
constructor({ intervalMs, onSample, monitor }) {
|
|
101
|
+
constructor({ intervalMs, onSample, monitor, requests = null }) {
|
|
34
102
|
this.intervalMs = intervalMs;
|
|
35
103
|
this.onSample = onSample;
|
|
36
104
|
this.monitor = monitor;
|
|
105
|
+
this.requests = requests;
|
|
37
106
|
this.timer = null;
|
|
107
|
+
this.gc = new GcWatch();
|
|
38
108
|
this.lastAt = performance.now();
|
|
39
109
|
this.lastCpu = process.cpuUsage();
|
|
110
|
+
this.lastElu = loopUtilization();
|
|
40
111
|
}
|
|
41
112
|
|
|
42
113
|
/**
|
|
@@ -46,6 +117,8 @@ export class RuntimeSampler {
|
|
|
46
117
|
if (this.timer) return this;
|
|
47
118
|
this.lastAt = performance.now();
|
|
48
119
|
this.lastCpu = process.cpuUsage();
|
|
120
|
+
this.lastElu = loopUtilization();
|
|
121
|
+
this.gc.start();
|
|
49
122
|
this.timer = RequestContext.outside(() => setInterval(() => this.take(), this.intervalMs));
|
|
50
123
|
if (typeof this.timer.unref === 'function') this.timer.unref();
|
|
51
124
|
return this;
|
|
@@ -57,6 +130,7 @@ export class RuntimeSampler {
|
|
|
57
130
|
stop() {
|
|
58
131
|
clearInterval(this.timer);
|
|
59
132
|
this.timer = null;
|
|
133
|
+
this.gc.stop();
|
|
60
134
|
}
|
|
61
135
|
|
|
62
136
|
/**
|
|
@@ -70,9 +144,14 @@ export class RuntimeSampler {
|
|
|
70
144
|
const elapsed = Math.max(1, now - this.lastAt);
|
|
71
145
|
const cpu = process.cpuUsage(this.lastCpu);
|
|
72
146
|
const delay = this.monitor ? this.monitor.delaySince(this.lastAt) : null;
|
|
147
|
+
const elu = loopUtilization();
|
|
148
|
+
const busy = elu && this.lastElu ? performance.eventLoopUtilization(elu, this.lastElu).utilization : null;
|
|
73
149
|
this.lastAt = now;
|
|
74
150
|
this.lastCpu = process.cpuUsage();
|
|
151
|
+
this.lastElu = elu;
|
|
75
152
|
const memory = process.memoryUsage();
|
|
153
|
+
const gc = this.gc.take();
|
|
154
|
+
const traffic = this.requests ? this.requests.take() : null;
|
|
76
155
|
const sample = {
|
|
77
156
|
at: Date.now(),
|
|
78
157
|
rss: memory.rss,
|
|
@@ -82,7 +161,16 @@ export class RuntimeSampler {
|
|
|
82
161
|
cpu: Math.round(((cpu.user + cpu.system) / 1000 / elapsed) * 1000) / 10,
|
|
83
162
|
loopP50: delay ? delay.p50 : null,
|
|
84
163
|
loopP99: delay ? delay.p99 : null,
|
|
85
|
-
loopMax: delay ? delay.max : null
|
|
164
|
+
loopMax: delay ? delay.max : null,
|
|
165
|
+
arrayBuffers: Number.isFinite(memory.arrayBuffers) ? memory.arrayBuffers : null,
|
|
166
|
+
heapLimit: heapLimit(),
|
|
167
|
+
elu: Number.isFinite(busy) ? Math.round(busy * 1000) / 10 : null,
|
|
168
|
+
gcCount: gc ? gc.count : null,
|
|
169
|
+
gcMs: gc ? gc.ms : null,
|
|
170
|
+
gcMax: gc ? gc.max : null,
|
|
171
|
+
handles: openHandles(),
|
|
172
|
+
requests: traffic ? traffic.finished : null,
|
|
173
|
+
inFlight: traffic ? traffic.open : null
|
|
86
174
|
};
|
|
87
175
|
this.onSample(sample);
|
|
88
176
|
return sample;
|
|
@@ -91,3 +179,29 @@ export class RuntimeSampler {
|
|
|
91
179
|
}
|
|
92
180
|
}
|
|
93
181
|
}
|
|
182
|
+
|
|
183
|
+
// How busy the loop has been since the process started (Node.js 14.10 and later).
|
|
184
|
+
function loopUtilization() {
|
|
185
|
+
return typeof performance.eventLoopUtilization === 'function' ? performance.eventLoopUtilization() : null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function heapLimit() {
|
|
189
|
+
try {
|
|
190
|
+
return v8.getHeapStatistics().heap_size_limit;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Sockets, timers, files and other resources keeping the loop alive (Node.js 17 and later).
|
|
197
|
+
function openHandles() {
|
|
198
|
+
try {
|
|
199
|
+
return typeof process.getActiveResourcesInfo === 'function' ? process.getActiveResourcesInfo().length : null;
|
|
200
|
+
} catch (error) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function round(value) {
|
|
206
|
+
return Math.round(value * 10) / 10;
|
|
207
|
+
}
|
package/src/timeline/timeline.js
CHANGED
|
@@ -37,7 +37,7 @@ export const TIMELINE_DEFAULTS = Object.freeze({
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
// Spans listed per request, by type.
|
|
40
|
-
const SPAN_CAPS = Object.freeze({ query: 500, http: 100, log: 200, measure: 100, exception: 10, block: 20 });
|
|
40
|
+
const SPAN_CAPS = Object.freeze({ query: 500, http: 100, log: 200, measure: 100, exception: 10, block: 20, dump: 50 });
|
|
41
41
|
const MAX_STATEMENTS = 200;
|
|
42
42
|
// Executions per statement whose parameters are kept.
|
|
43
43
|
const PARAMS_PER_STATEMENT = 20;
|
|
@@ -121,7 +121,7 @@ export class Timeline {
|
|
|
121
121
|
this.origin = origin;
|
|
122
122
|
this.closed = false;
|
|
123
123
|
this.spans = [];
|
|
124
|
-
this.listed = { query: 0, http: 0, log: 0, measure: 0, exception: 0, block: 0 };
|
|
124
|
+
this.listed = { query: 0, http: 0, log: 0, measure: 0, exception: 0, block: 0, dump: 0 };
|
|
125
125
|
this.dropped = 0;
|
|
126
126
|
this.statements = [];
|
|
127
127
|
this.byFingerprint = new Map();
|
|
@@ -263,7 +263,7 @@ export class Timeline {
|
|
|
263
263
|
*
|
|
264
264
|
* @param {'debug'|'info'|'warn'|'error'} level
|
|
265
265
|
* @param {string} message - Formatted as it was written
|
|
266
|
-
* @param {'console'|'pino'|'winston'} [source='console']
|
|
266
|
+
* @param {'console'|'pino'|'winston'|'nest'} [source='console']
|
|
267
267
|
* @returns {void}
|
|
268
268
|
*/
|
|
269
269
|
log(level, message, source = 'console') {
|
|
@@ -282,6 +282,27 @@ export class Timeline {
|
|
|
282
282
|
}
|
|
283
283
|
}
|
|
284
284
|
|
|
285
|
+
/**
|
|
286
|
+
* Records a dump made during the request (local dashboard only): its label and a preview of the
|
|
287
|
+
* value; the Dumps page has the value itself.
|
|
288
|
+
*
|
|
289
|
+
* @param {{ id: string, label: string|null, summary: { preview: string } }} record - A DumpRecord
|
|
290
|
+
* @returns {void}
|
|
291
|
+
*/
|
|
292
|
+
dump(record) {
|
|
293
|
+
if (this.closed) return;
|
|
294
|
+
try {
|
|
295
|
+
if (this.listed.dump < SPAN_CAPS.dump) {
|
|
296
|
+
this.listed.dump++;
|
|
297
|
+
this.spans.push({ type: 'dump', start: this._offset(performance.now()), id: record.id, label: record.label, preview: cut(record.summary.preview, MAX_LOG) });
|
|
298
|
+
} else {
|
|
299
|
+
this.dropped++;
|
|
300
|
+
}
|
|
301
|
+
} catch (error) {
|
|
302
|
+
// The dump is still on the Dumps page.
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
285
306
|
/**
|
|
286
307
|
* Records an exception recorded during the request.
|
|
287
308
|
*
|
package/src/transport/batch.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { LOG_PREFIX } from '../config.js';
|
|
14
14
|
import { ExceptionRecord } from '../exceptions/exception-record.js';
|
|
15
15
|
import { RequestContext } from '../exceptions/request-context.js';
|
|
16
|
+
import { DumpRecord } from '../dumps/dump-record.js';
|
|
16
17
|
import { JobRunRecord } from '../jobs/job-run-record.js';
|
|
17
18
|
import { LogRecord } from '../logs/log-record.js';
|
|
18
19
|
import { RequestRecord } from '../record.js';
|
|
@@ -170,13 +171,15 @@ export class BatchTransport {
|
|
|
170
171
|
const exceptions = [];
|
|
171
172
|
const jobs = [];
|
|
172
173
|
const logs = [];
|
|
174
|
+
const dumps = [];
|
|
173
175
|
for (const record of records) {
|
|
174
176
|
if (record instanceof ExceptionRecord) exceptions.push(record);
|
|
175
177
|
else if (record instanceof JobRunRecord) jobs.push(record);
|
|
176
178
|
else if (record instanceof LogRecord) logs.push(record);
|
|
179
|
+
else if (record instanceof DumpRecord) dumps.push(record);
|
|
177
180
|
else requests.push(record);
|
|
178
181
|
}
|
|
179
|
-
return { requests, exceptions, jobs, logs };
|
|
182
|
+
return { requests, exceptions, jobs, logs, dumps };
|
|
180
183
|
}
|
|
181
184
|
|
|
182
185
|
/**
|
package/src/transport/cloud.js
CHANGED
|
@@ -16,10 +16,12 @@ import { settings } from '../config.js';
|
|
|
16
16
|
import { ApiKey, Sealer, SyncSealer } from '../crypto.js';
|
|
17
17
|
import { RequestContext } from '../exceptions/request-context.js';
|
|
18
18
|
import { ExceptionRecord } from '../exceptions/exception-record.js';
|
|
19
|
+
import { DumpRecord } from '../dumps/dump-record.js';
|
|
19
20
|
import { JobRunRecord } from '../jobs/job-run-record.js';
|
|
20
21
|
import { LogRecord } from '../logs/log-record.js';
|
|
21
22
|
import { RequestRecord } from '../record.js';
|
|
22
23
|
import { Spool } from '../spool.js';
|
|
24
|
+
import { RuleMatcher } from '../alerts/rule-matcher.js';
|
|
23
25
|
import { BatchTransport } from './batch.js';
|
|
24
26
|
|
|
25
27
|
const FLUSH_INTERVAL_MS = 1000;
|
|
@@ -86,7 +88,7 @@ export class CloudTransport extends BatchTransport {
|
|
|
86
88
|
constructor({ apiKey, environment, release = null, version }) {
|
|
87
89
|
const key = ApiKey.parse(apiKey);
|
|
88
90
|
if (!key) {
|
|
89
|
-
throw new Error(
|
|
91
|
+
throw new Error(ApiKey.problem(apiKey));
|
|
90
92
|
}
|
|
91
93
|
super({
|
|
92
94
|
environment: RequestRecord.normalizeEnvironment(environment, 'production'),
|
|
@@ -121,12 +123,25 @@ export class CloudTransport extends BatchTransport {
|
|
|
121
123
|
|
|
122
124
|
this._resendTimer = null;
|
|
123
125
|
this._resending = null;
|
|
126
|
+
// Alert rules on sealed fields, checked here before records are sealed (started with the first batch).
|
|
127
|
+
this.rules = new RuleMatcher({ fetchRules: () => this._fetchRules() });
|
|
124
128
|
// Set when records that didn't fit in the last POST wait for the next one.
|
|
125
129
|
this._more = false;
|
|
126
130
|
// Batches spooled by an earlier run that crashed.
|
|
127
131
|
this._scheduleResend(SPOOL_RESEND_DELAY_MS);
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
/**
|
|
135
|
+
* Queues a record for sending. Dumps are for the local dashboard: PeekInside Cloud never gets them.
|
|
136
|
+
*
|
|
137
|
+
* @param {Object} record
|
|
138
|
+
* @returns {void}
|
|
139
|
+
*/
|
|
140
|
+
enqueueRecord(record) {
|
|
141
|
+
if (record instanceof DumpRecord) return;
|
|
142
|
+
super.enqueueRecord(record);
|
|
143
|
+
}
|
|
144
|
+
|
|
130
145
|
/**
|
|
131
146
|
* Stops the background timer and the flush at exit. Sending itself stays possible: drain()
|
|
132
147
|
* still sends what is queued.
|
|
@@ -137,6 +152,7 @@ export class CloudTransport extends BatchTransport {
|
|
|
137
152
|
clearInterval(this.timer);
|
|
138
153
|
clearTimeout(this._resendTimer);
|
|
139
154
|
this._resendTimer = null;
|
|
155
|
+
this.rules.stop();
|
|
140
156
|
super.stop();
|
|
141
157
|
}
|
|
142
158
|
|
|
@@ -189,6 +205,7 @@ export class CloudTransport extends BatchTransport {
|
|
|
189
205
|
// even a large one, or it would block the queue.
|
|
190
206
|
async _pack(batch, sealer) {
|
|
191
207
|
const lists = { events: [], exceptions: [], jobs: [], logs: [] };
|
|
208
|
+
const alerts = [];
|
|
192
209
|
let size = 32;
|
|
193
210
|
let count = 0;
|
|
194
211
|
for (const record of batch) {
|
|
@@ -198,8 +215,10 @@ export class CloudTransport extends BatchTransport {
|
|
|
198
215
|
size += length;
|
|
199
216
|
count++;
|
|
200
217
|
lists[listFor(record)].push(sealed);
|
|
218
|
+
// Which alert rules it met on what only this server can read: ids, never the content.
|
|
219
|
+
alerts.push(...this.rules.matches(record));
|
|
201
220
|
}
|
|
202
|
-
return { ...lists, rest: batch.slice(count) };
|
|
221
|
+
return { ...lists, alerts, rest: batch.slice(count) };
|
|
203
222
|
}
|
|
204
223
|
|
|
205
224
|
// Records that didn't fit go back to the front of the queue, in order, and go out right away.
|
|
@@ -213,11 +232,12 @@ export class CloudTransport extends BatchTransport {
|
|
|
213
232
|
async _deliver(batch) {
|
|
214
233
|
// A fresh ephemeral key per batch: one key agreement, a new IV per message.
|
|
215
234
|
const sealer = await Sealer.create(this.apiKey.publicKey, 'event');
|
|
216
|
-
const { events, exceptions, jobs, logs, rest } = await this._pack(batch, sealer);
|
|
235
|
+
const { events, exceptions, jobs, logs, alerts, rest } = await this._pack(batch, sealer);
|
|
217
236
|
const body = { events };
|
|
218
237
|
if (exceptions.length) body.exceptions = exceptions;
|
|
219
238
|
if (jobs.length) body.jobs = jobs;
|
|
220
239
|
if (logs.length) body.logs = logs;
|
|
240
|
+
if (alerts.length) body.alerts = alerts;
|
|
221
241
|
const samples = this.takeSamples();
|
|
222
242
|
if (samples.length) {
|
|
223
243
|
body.instance = { ...this.instance, environment: this.environment, release: this.release };
|
|
@@ -234,6 +254,8 @@ export class CloudTransport extends BatchTransport {
|
|
|
234
254
|
const sent = batch.length - rest.length;
|
|
235
255
|
|
|
236
256
|
if (response.ok) {
|
|
257
|
+
// PeekInside Cloud answers: from now on, the alert rules it holds are checked here too.
|
|
258
|
+
this.rules.start();
|
|
237
259
|
this._requeue(rest);
|
|
238
260
|
return true;
|
|
239
261
|
}
|
|
@@ -287,6 +309,15 @@ export class CloudTransport extends BatchTransport {
|
|
|
287
309
|
console.error(line);
|
|
288
310
|
}
|
|
289
311
|
|
|
312
|
+
// The project's alert rules on sealed fields, from beside the ingest endpoint.
|
|
313
|
+
async _fetchRules() {
|
|
314
|
+
if (this.exiting) throw new Error('exiting');
|
|
315
|
+
// Never after a redirect: the API key goes only where batches go (see _post).
|
|
316
|
+
const response = await fetch(`${this.endpoint}/alert-rules`, { headers: this._headers(), redirect: 'manual', signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
317
|
+
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
|
318
|
+
return response.json();
|
|
319
|
+
}
|
|
320
|
+
|
|
290
321
|
_headers() {
|
|
291
322
|
return {
|
|
292
323
|
'content-type': 'application/json',
|
package/src/transport/local.js
CHANGED
|
@@ -182,14 +182,15 @@ export class LocalTransport extends BatchTransport {
|
|
|
182
182
|
const records = fatal ? [...this.sending, ...this.queue.splice(0)] : [];
|
|
183
183
|
for (const exchange of exchanges) records.push(RequestRecord.fromExchange(exchange, this.environment, this.release));
|
|
184
184
|
records.push(...exceptions);
|
|
185
|
-
const { requests, exceptions: errors, jobs, logs } = BatchTransport.split(records);
|
|
185
|
+
const { requests, exceptions: errors, jobs, logs, dumps } = BatchTransport.split(records);
|
|
186
186
|
Spool.local(settings.dataDir(), this.project.id).write({
|
|
187
187
|
project: this.project,
|
|
188
188
|
instance: this.instance,
|
|
189
189
|
requests: requests.slice(-MAX_SPOOLED_REQUESTS),
|
|
190
190
|
exceptions: errors.slice(-MAX_SPOOLED_EXCEPTIONS),
|
|
191
191
|
jobs: jobs.slice(-MAX_SPOOLED_EXCEPTIONS),
|
|
192
|
-
logs: logs.slice(-MAX_SPOOLED_REQUESTS)
|
|
192
|
+
logs: logs.slice(-MAX_SPOOLED_REQUESTS),
|
|
193
|
+
dumps: dumps.slice(-MAX_SPOOLED_EXCEPTIONS)
|
|
193
194
|
});
|
|
194
195
|
} catch (error) {
|
|
195
196
|
// A data folder that can't be written loses the records, never the app.
|
|
@@ -207,13 +208,14 @@ export class LocalTransport extends BatchTransport {
|
|
|
207
208
|
}
|
|
208
209
|
const blocked = this.blocked.take();
|
|
209
210
|
const samples = this.takeSamples();
|
|
210
|
-
const { requests, exceptions, jobs, logs } = BatchTransport.split(batch);
|
|
211
|
+
const { requests, exceptions, jobs, logs, dumps } = BatchTransport.split(batch);
|
|
211
212
|
try {
|
|
212
213
|
const answer = await this.hub.request('POST', '/api/ingest', {
|
|
213
|
-
body: { project: this.project, instance: this.instance, requests, exceptions, jobs, logs, samples, blocked: blocked.map(([, entry]) => entry) }
|
|
214
|
+
body: { project: this.project, instance: this.instance, requests, exceptions, jobs, logs, dumps, samples, blocked: blocked.map(([, entry]) => entry) }
|
|
214
215
|
});
|
|
215
216
|
if (exceptions.length && answer.acceptedExceptions === undefined) this._noteOutdatedHub();
|
|
216
217
|
else if ((jobs.length || logs.length) && answer.acceptedJobs === undefined) this._noteOutdatedHub();
|
|
218
|
+
else if (dumps.length && answer.acceptedDumps === undefined) this._noteOutdatedHub();
|
|
217
219
|
return true;
|
|
218
220
|
} catch (error) {
|
|
219
221
|
if (error.code === 'NOT_HUB') {
|
|
@@ -238,7 +240,7 @@ export class LocalTransport extends BatchTransport {
|
|
|
238
240
|
}
|
|
239
241
|
|
|
240
242
|
// A hub started before this version keeps running until it is restarted, and ignores what it
|
|
241
|
-
// doesn't know (exceptions, job runs, log lines).
|
|
243
|
+
// doesn't know (exceptions, job runs, log lines, dumps).
|
|
242
244
|
_noteOutdatedHub() {
|
|
243
245
|
if (this.outdatedHubNoted) return;
|
|
244
246
|
this.outdatedHubNoted = true;
|
package/src/user.js
CHANGED
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
const MAX_ID = 128;
|
|
10
10
|
const MAX_EMAIL = 254;
|
|
11
11
|
const MAX_NAME = 128;
|
|
12
|
+
const MAX_PHONE = 32;
|
|
12
13
|
|
|
13
14
|
/**
|
|
14
15
|
* @typedef {Object} User
|
|
15
16
|
* @property {string|null} id
|
|
16
17
|
* @property {string|null} email
|
|
17
18
|
* @property {string|null} name
|
|
19
|
+
* @property {string|null} phone
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
/**
|
|
@@ -22,14 +24,15 @@ const MAX_NAME = 128;
|
|
|
22
24
|
*/
|
|
23
25
|
export class UserInfo {
|
|
24
26
|
/**
|
|
25
|
-
* Accepts `{ id, email, name }` (also `_id`, `userId
|
|
26
|
-
* libraries have them), or just
|
|
27
|
+
* Accepts `{ id, email, name, phone }` (also `_id`, `userId`, `username`, `phoneNumber`,
|
|
28
|
+
* `phone_number` and `mobile`, as user objects of common libraries and apps have them), or just
|
|
29
|
+
* an id. Anything else is no user.
|
|
27
30
|
*
|
|
28
31
|
* @param {*} user
|
|
29
32
|
* @returns {User|null} null when it names nobody
|
|
30
33
|
*
|
|
31
34
|
* @example
|
|
32
|
-
* UserInfo.normalize({ id: 42,
|
|
35
|
+
* UserInfo.normalize({ id: 42, phone: '+91 98765 43210' }) // { id: '42', email: null, name: null, phone: '+91 98765 43210' }
|
|
33
36
|
*/
|
|
34
37
|
static normalize(user) {
|
|
35
38
|
if (user === null || user === undefined) return null;
|
|
@@ -37,25 +40,27 @@ export class UserInfo {
|
|
|
37
40
|
let id;
|
|
38
41
|
let email;
|
|
39
42
|
let name;
|
|
43
|
+
let phone;
|
|
40
44
|
try {
|
|
41
45
|
id = firstDefined(user.id, user._id, user.userId);
|
|
42
46
|
email = user.email;
|
|
43
47
|
name = firstDefined(user.name, user.username);
|
|
48
|
+
phone = firstDefined(user.phone, user.phoneNumber, user.phone_number, user.mobile);
|
|
44
49
|
} catch (error) {
|
|
45
50
|
return null;
|
|
46
51
|
}
|
|
47
|
-
const clean = { id: identifier(id), email: label(email, MAX_EMAIL), name: label(name, MAX_NAME) };
|
|
48
|
-
return clean.id || clean.email || clean.name ? clean : null;
|
|
52
|
+
const clean = { id: identifier(id), email: label(email, MAX_EMAIL), name: label(name, MAX_NAME), phone: phoneNumber(phone) };
|
|
53
|
+
return clean.id || clean.email || clean.name || clean.phone ? clean : null;
|
|
49
54
|
}
|
|
50
55
|
|
|
51
56
|
/**
|
|
52
|
-
* One string that tells users apart: the id, else the email, else the name.
|
|
57
|
+
* One string that tells users apart: the id, else the email, else the phone, else the name.
|
|
53
58
|
*
|
|
54
59
|
* @param {User|null} user
|
|
55
60
|
* @returns {string|null}
|
|
56
61
|
*/
|
|
57
62
|
static key(user) {
|
|
58
|
-
return user ? user.id || user.email || user.name || null : null;
|
|
63
|
+
return user ? user.id || user.email || user.phone || user.name || null : null;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
|
|
@@ -76,6 +81,12 @@ function identifier(value) {
|
|
|
76
81
|
return null;
|
|
77
82
|
}
|
|
78
83
|
|
|
84
|
+
// As the app wrote it ("+91 98765 43210"); numbers (and bigints) as their digits.
|
|
85
|
+
function phoneNumber(value) {
|
|
86
|
+
if (typeof value === 'number' || typeof value === 'bigint') return label(String(value), MAX_PHONE);
|
|
87
|
+
return label(value, MAX_PHONE);
|
|
88
|
+
}
|
|
89
|
+
|
|
79
90
|
function label(value, max) {
|
|
80
91
|
if (typeof value !== 'string') return null;
|
|
81
92
|
const clean = value.trim().slice(0, max);
|