querysub 0.377.0 → 0.379.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.
Files changed (29) hide show
  1. package/.cursorrules +2 -0
  2. package/bin/error-watch-public.js +7 -0
  3. package/bin/error-watch.js +6 -0
  4. package/package.json +7 -4
  5. package/src/-f-node-discovery/NodeDiscovery.ts +7 -0
  6. package/src/-g-core-values/NodeCapabilities.ts +28 -14
  7. package/src/3-path-functions/PathFunctionRunnerMain.ts +0 -4
  8. package/src/diagnostics/MachineThreadInfo.tsx +33 -10
  9. package/src/diagnostics/logs/IndexedLogs/IndexedLogs.ts +24 -1
  10. package/src/diagnostics/logs/IndexedLogs/LogViewer3.tsx +6 -2
  11. package/src/diagnostics/logs/diskLogger.ts +26 -27
  12. package/src/diagnostics/logs/diskShimConsoleLogs.ts +4 -0
  13. package/src/diagnostics/logs/errorNotifications2/ErrorNotificationPage.tsx +505 -0
  14. package/src/diagnostics/logs/errorNotifications2/ErrorWarning.tsx +32 -0
  15. package/src/diagnostics/logs/errorNotifications2/errorNotifications.ts +629 -0
  16. package/src/diagnostics/logs/errorNotifications2/errorWatchEntry.ts +13 -0
  17. package/src/diagnostics/logs/errorNotifications2/errorWatcher.ts +168 -0
  18. package/src/diagnostics/logs/errorNotifications2/logWatcher.ts +104 -0
  19. package/src/diagnostics/logs/errorNotifications2/openRouterHelper.ts +77 -0
  20. package/src/diagnostics/logs/lifeCycleAnalysis/lifeCycles.tsx +17 -19
  21. package/src/diagnostics/managementPages.tsx +12 -2
  22. package/src/server.ts +0 -8
  23. package/src/user-implementation/SecurityPage.tsx +6 -0
  24. package/src/user-implementation/userData.ts +6 -1
  25. package/test.ts +16 -6
  26. package/test2.ts +20 -0
  27. package/src/diagnostics/logs/errorNotifications2/errorNotifications2.ts +0 -9
  28. package/src/diagnostics/logs/lifeCycleAnalysis/test.ts +0 -0
  29. /package/src/library-components/{errorNotifications.tsx → uncaughtToast.tsx} +0 -0
@@ -0,0 +1,505 @@
1
+ import { qreact } from "../../../4-dom/qreact";
2
+ import { t } from "../../../2-proxy/schema2";
3
+ import { css } from "typesafecss";
4
+ import { LogDatum } from "../diskLogger";
5
+ import { sort, timeInDay } from "socket-function/src/misc";
6
+ import { formatDateTime, formatNumber, formatTime } from "socket-function/src/formatting/format";
7
+ import { Querysub } from "../../../4-querysub/QuerysubController";
8
+ import { InputLabel } from "../../../library-components/InputLabel";
9
+ import { isPublic } from "../../../config";
10
+ import { MachineThreadInfo } from "../../MachineThreadInfo";
11
+ import { ErrorWarning } from "./ErrorWarning";
12
+ import { getErrorNotificationsManager } from "./errorWatcher";
13
+ import { createMatchesPattern } from "../IndexedLogs/bufferSearchFindMatcher";
14
+ import { cacheLimited } from "socket-function/src/caching";
15
+ import { managementPageURL } from "../../managementPages";
16
+ import { searchTextURL, readProductionLogsURL } from "../IndexedLogs/LogViewerParams";
17
+ import { startTimeParam, endTimeParam } from "../TimeRangeSelector";
18
+ import { timeInHour } from "socket-function/src/misc";
19
+ import { ATag, URLOverride } from "../../../library-components/ATag";
20
+ import { Button } from "../../../library-components/Button";
21
+
22
+ let getMatcher = cacheLimited(100, (pattern: string) =>
23
+ createMatchesPattern(Buffer.from(pattern), false)
24
+ );
25
+
26
+ export class LogDatumRenderer extends qreact.Component<{
27
+ datum: LogDatum;
28
+ inlineMode?: boolean;
29
+ hideSuppression?: boolean;
30
+ pattern?: string;
31
+ }> {
32
+ state = t.state({
33
+ expanded: t.atomic<boolean>(false),
34
+ });
35
+
36
+ manager = getErrorNotificationsManager();
37
+
38
+ render() {
39
+ let datum = this.props.datum;
40
+ let mainMessage = datum.param0 && String(datum.param0) || "(no message)";
41
+
42
+ let matchingSuppressions = [];
43
+ if (!this.manager.state.isLoading && !this.props.hideSuppression) {
44
+ for (let suppression of this.manager.state.suppressionEntries) {
45
+ let matcher = getMatcher(suppression.pattern);
46
+ let errorBuffer = Buffer.from(JSON.stringify(datum));
47
+ if (matcher(errorBuffer)) {
48
+ matchingSuppressions.push(suppression);
49
+ }
50
+ }
51
+ }
52
+
53
+ let timedOutMatches = matchingSuppressions.filter(s => datum.time > s.timeout);
54
+ let shouldBeSuppressed = matchingSuppressions.filter(s => datum.time <= s.timeout);
55
+
56
+ let logViewerValues: URLOverride[] | undefined;
57
+ {
58
+ logViewerValues = [
59
+ managementPageURL.getOverride("LogViewer3"),
60
+ readProductionLogsURL.getOverride(isPublic()),
61
+ startTimeParam.getOverride(datum.time - timeInHour),
62
+ endTimeParam.getOverride(datum.time + timeInHour),
63
+ ];
64
+
65
+ let searchText = this.props.pattern && this.props.pattern || timedOutMatches[0]?.pattern || "";
66
+ if (datum.__threadId) {
67
+ if (searchText) {
68
+ searchText += "&";
69
+ }
70
+ searchText += `"__threadId":"${datum.__threadId}"`;
71
+ }
72
+ if (searchText) {
73
+ logViewerValues.push(searchTextURL.getOverride(searchText));
74
+ }
75
+ }
76
+
77
+ return <div className={
78
+ css.vbox(4)
79
+ + (!this.props.inlineMode && css.maxWidth("80vw"))
80
+ + (this.props.inlineMode && css.maxWidth("calc(100vw - 90px)"))
81
+ }>
82
+ <div
83
+ className={
84
+ css.hbox(8).maxWidth("100%")
85
+ }
86
+ >
87
+ {logViewerValues && (
88
+ <ATag values={logViewerValues}>
89
+ View Logs
90
+ </ATag>
91
+ )}
92
+ <div
93
+ className={
94
+ css.hbox(8)
95
+ + (!this.props.inlineMode && css.button)
96
+ }
97
+ onClick={!this.props.inlineMode && (() => {
98
+ this.state.expanded = !this.state.expanded;
99
+ }) || undefined}
100
+ >
101
+ {!this.props.inlineMode && <span>{this.state.expanded && "▼" || "▶"}</span>}
102
+ <div>{formatDateTime(datum.time)}</div>
103
+ </div>
104
+ {datum.__machineId && <MachineThreadInfo machineId={datum.__machineId} threadId={datum.__threadId} />}
105
+ <span className={css.ellipsis.flexFillWidth.colorhsl(0, 50, 50).boldStyle}>{mainMessage}</span>
106
+ </div>
107
+
108
+ {!this.props.inlineMode && this.state.expanded && (
109
+ <div className={css.vbox(4).pad2(8).hsl(0, 0, 98).bord2(0, 0, 85)}>
110
+ {Object.entries(datum).map(([key, value]) => (
111
+ <div key={key} className={css.hbox(8)}>
112
+ <strong className={css.minWidth(120)}>{key}:</strong>
113
+ <pre className={css.flexGrow(1).margin(0)}>{JSON.stringify(value, undefined, 2)}</pre>
114
+ </div>
115
+ ))}
116
+ </div>
117
+ )}
118
+
119
+ {timedOutMatches.length > 0 && (
120
+ <div className={css.hbox(8).pad2(8).bord2(200, 50, 60).hsl(200, 50, 95)}>
121
+ <span>
122
+ <span className={css.colorhsl(0, 0, 50)}>(expired {formatTime(Date.now() - timedOutMatches[0].timeout)} AGO)</span>
123
+ {" "}
124
+ <span className={css.colorhsl(0, 0, 0).boldStyle}>{timedOutMatches[0].pattern}</span>
125
+ {timedOutMatches[0].notes && <span className={css.colorhsl(210, 80, 40)}> ({timedOutMatches[0].notes})</span>}
126
+ </span>
127
+ <Button
128
+ hue={30}
129
+ onClick={(e) => {
130
+ if (this.props.inlineMode) {
131
+ e.preventDefault();
132
+ e.stopPropagation();
133
+ }
134
+ let timeout = Date.now() + timeInDay * 2;
135
+ Querysub.onCommitFinished(async () => {
136
+ await this.manager.updateTimeout(timedOutMatches[0].id, timeout);
137
+ });
138
+ }}
139
+ >
140
+ Fixed
141
+ </Button>
142
+ <Button
143
+ hue={120}
144
+ onClick={(e) => {
145
+ if (this.props.inlineMode) {
146
+ e.preventDefault();
147
+ e.stopPropagation();
148
+ }
149
+ Querysub.onCommitFinished(async () => {
150
+ await this.manager.updateTimeout(timedOutMatches[0].id, Infinity);
151
+ });
152
+ }}
153
+ >
154
+ Not a Bug
155
+ </Button>
156
+ </div>
157
+ )}
158
+
159
+ {shouldBeSuppressed.length > 0 && timedOutMatches.length === 0 && (
160
+ <div className={css.pad2(8).bord2(30, 50, 60).hsl(30, 50, 95)}>
161
+ <span>
162
+ <span className={css.colorhsl(0, 0, 50)}>(for {formatTime(Date.now() - shouldBeSuppressed[0].lastUpdatedTime)})</span>
163
+ {" "}
164
+ <span className={css.colorhsl(0, 80, 30)}>{shouldBeSuppressed[0].pattern}</span>
165
+ {shouldBeSuppressed[0].notes && <span className={css.colorhsl(210, 80, 40)}> ({shouldBeSuppressed[0].notes})</span>}
166
+ </span>
167
+ </div>
168
+ )}
169
+ </div>;
170
+ }
171
+ }
172
+
173
+ export class ErrorNotificationPage extends qreact.Component {
174
+ state = t.state({
175
+ newFixedPattern: t.atomic<string>(""),
176
+ newNotABugPattern: t.atomic<string>(""),
177
+ testErrorMessage: t.atomic<string>("example error"),
178
+ showingAllErrors: t.atomic<boolean>(false),
179
+ });
180
+
181
+ manager = getErrorNotificationsManager();
182
+
183
+ render() {
184
+ if (this.manager.state.isLoading) {
185
+ return <div className={css.vbox(16).pad2(16)}>
186
+ <h2>Error Notifications</h2>
187
+ <div>Loading error notifications...</div>
188
+ </div>;
189
+ }
190
+
191
+ if (this.manager.state.initError) {
192
+ return <div className={css.vbox(16).pad2(16)}>
193
+ <h2>Error Notifications</h2>
194
+ {!isPublic() && <h1>
195
+ IMPORTANT! You MUST run `yarn error-watch` to get errors notifications in the dev environment.
196
+ </h1>}
197
+ <div className={css.pad2(12).bord2(0, 80, 50).hsl(0, 80, 95).colorhsl(0, 80, 30)}>
198
+ <strong>Error loading error notifications:</strong>
199
+ <pre className={css.pad2(8).hsl(0, 0, 98).bord2(0, 0, 85)}>
200
+ {this.manager.state.initError}
201
+ </pre>
202
+ </div>
203
+ </div>;
204
+ }
205
+
206
+ let sortedErrors = [...this.manager.state.unmatchedErrors];
207
+ sort(sortedErrors, x => x.time);
208
+ let displayedErrors = this.state.showingAllErrors && sortedErrors || sortedErrors.slice(0, 5);
209
+
210
+ let sortedSuppressions = [...this.manager.state.suppressionEntries];
211
+ sort(sortedSuppressions, x => -x.lastUpdatedTime);
212
+
213
+ return <div className={css.vbox(16).pad2(16).fillWidth}>
214
+ <h2>Error Notifications</h2>
215
+
216
+ <div className={css.hbox(12).pad2(12).bord2(30, 50, 60).hsl(30, 50, 95)}>
217
+ <InputLabel
218
+ label="Test Error Message"
219
+ className={css.width(600)}
220
+ value={this.state.testErrorMessage}
221
+ onChangeValue={(value) => {
222
+ this.state.testErrorMessage = value;
223
+ }}
224
+ />
225
+ <Button
226
+ hue={30}
227
+ onClick={() => {
228
+ let message = this.state.testErrorMessage;
229
+ Querysub.onCommitFinished(async () => {
230
+ await this.manager.logTestError(message);
231
+ });
232
+ }}
233
+ >
234
+ Log Test Error
235
+ </Button>
236
+ </div>
237
+
238
+ <div className={css.vbox(12)}>
239
+ <h3>Unmatched Errors ({this.manager.state.unmatchedErrors.length})</h3>
240
+ {displayedErrors.length === 0 && <div>No unmatched errors</div>}
241
+ <div className={css.vbox(0)}>
242
+ {displayedErrors.map((error, idx) => (
243
+ <LogDatumRenderer key={idx} datum={error} />
244
+ ))}
245
+ </div>
246
+ {sortedErrors.length > 5 && (
247
+ <Button
248
+ hue={200}
249
+ onClick={() => {
250
+ this.state.showingAllErrors = !this.state.showingAllErrors;
251
+ }}
252
+ >
253
+ {this.state.showingAllErrors && "Show Less" || `Show More (${sortedErrors.length - 5} more)`}
254
+ </Button>
255
+ )}
256
+ </div>
257
+
258
+ <div className={css.vbox(12)}>
259
+ <h3>Suppressions ({sortedSuppressions.length})</h3>
260
+
261
+ <div className={css.vbox(8).pad2(12).bord2(120, 50, 60).hsl(120, 50, 95)}>
262
+ <InputLabel
263
+ label="Fixed"
264
+ value={this.state.newFixedPattern}
265
+ onChangeValue={(value) => {
266
+ this.state.newFixedPattern = value;
267
+ }}
268
+ onKeyDown={e => {
269
+ if (e.key === "Enter") {
270
+ let pattern = e.currentTarget.value.trim();
271
+ if (pattern) {
272
+ this.state.newFixedPattern = pattern;
273
+ let timeout = Date.now() + timeInDay * 2;
274
+ Querysub.onCommitFinished(async () => {
275
+ await this.manager.addSuppression(pattern, timeout);
276
+ Querysub.commit(() => {
277
+ this.state.newFixedPattern = "";
278
+ });
279
+ });
280
+ }
281
+ }
282
+ }}
283
+ className={css.width(500)}
284
+ />
285
+ <InputLabel
286
+ label="Not a Bug"
287
+ value={this.state.newNotABugPattern}
288
+ onChangeValue={(value) => {
289
+ this.state.newNotABugPattern = value;
290
+ }}
291
+ onKeyDown={e => {
292
+ if (e.key === "Enter") {
293
+ let pattern = e.currentTarget.value.trim();
294
+ if (pattern) {
295
+ this.state.newNotABugPattern = pattern;
296
+ Querysub.onCommitFinished(async () => {
297
+ await this.manager.addSuppression(pattern, Infinity);
298
+ Querysub.commit(() => {
299
+ this.state.newNotABugPattern = "";
300
+ });
301
+ });
302
+ }
303
+ }
304
+ }}
305
+ className={css.width(500)}
306
+ />
307
+ </div>
308
+
309
+ <div className={css.vbox(12)}>
310
+ {sortedSuppressions.map((suppression) => {
311
+ let matchData = this.manager.state.suppressionMatches.get(suppression.id);
312
+
313
+ return <SuppressionItem
314
+ key={suppression.id}
315
+ suppression={suppression}
316
+ matchData={matchData}
317
+ />;
318
+ })}
319
+ </div>
320
+ </div>
321
+ </div>;
322
+ }
323
+ }
324
+
325
+ class SuppressionItem extends qreact.Component<{
326
+ suppression: {
327
+ id: string;
328
+ notes?: string;
329
+ pattern: string;
330
+ timeout: number;
331
+ createdTime: number;
332
+ lastUpdatedTime: number;
333
+ };
334
+ matchData: {
335
+ history: Map<number, { count: number }>;
336
+ exampleIndex: number;
337
+ examples: LogDatum[];
338
+ } | undefined;
339
+ }> {
340
+ state = t.state({
341
+ showingAllExamples: t.atomic<boolean>(false),
342
+ patternValue: t.atomic<string>(""),
343
+ notesValue: t.atomic<string>(""),
344
+ });
345
+
346
+ manager = getErrorNotificationsManager();
347
+
348
+ componentDidMount() {
349
+ this.state.patternValue = this.props.suppression.pattern;
350
+ this.state.notesValue = this.props.suppression.notes ?? "";
351
+ }
352
+
353
+ componentDidUpdate(prevProps: any) {
354
+ if (prevProps.suppression.pattern !== this.props.suppression.pattern) {
355
+ this.state.patternValue = this.props.suppression.pattern;
356
+ }
357
+ if (prevProps.suppression.notes !== this.props.suppression.notes) {
358
+ this.state.notesValue = this.props.suppression.notes ?? "";
359
+ }
360
+ }
361
+
362
+ render() {
363
+ let suppression = this.props.suppression;
364
+ let matchData = this.props.matchData;
365
+ let totalCount = 0;
366
+ if (matchData) {
367
+ for (let [chunk, data] of matchData.history) {
368
+ totalCount += data.count;
369
+ }
370
+ }
371
+
372
+ let timeoutText: preact.ComponentChild;
373
+ if (suppression.timeout === Infinity) {
374
+ timeoutText = "Never expires";
375
+ } else if (suppression.timeout > Date.now()) {
376
+ timeoutText = <span>Expires <b>{formatTime(suppression.timeout - Date.now())}</b></span>;
377
+ } else {
378
+ timeoutText = <span>Expired <b>{formatTime(Date.now() - suppression.timeout)}</b> ago</span>;
379
+ }
380
+
381
+ return <div key={suppression.id} className={css.pad2(12).bord2(200, 30, 70).hsl(200, 30, 95).vbox(8)}>
382
+ <div className={css.hbox(12)}>
383
+ <ATag
384
+ values={[
385
+ managementPageURL.getOverride("LogViewer3"),
386
+ searchTextURL.getOverride(suppression.pattern),
387
+ readProductionLogsURL.getOverride(true),
388
+ ]}
389
+ >
390
+ View Logs
391
+ </ATag>
392
+ <Button
393
+ hue={30}
394
+ onClick={() => {
395
+ let timeout = Date.now() + timeInDay * 2;
396
+ Querysub.onCommitFinished(async () => {
397
+ await this.manager.updateTimeout(suppression.id, timeout);
398
+ });
399
+ }}
400
+ >
401
+ Fixed
402
+ </Button>
403
+ <Button
404
+ hue={120}
405
+ onClick={() => {
406
+ Querysub.onCommitFinished(async () => {
407
+ await this.manager.updateTimeout(suppression.id, Infinity);
408
+ });
409
+ }}
410
+ >
411
+ Not a Bug
412
+ </Button>
413
+ <Button
414
+ hue={30}
415
+ onClick={() => {
416
+ let timeout = Date.now();
417
+ Querysub.onCommitFinished(async () => {
418
+ await this.manager.updateTimeout(suppression.id, timeout);
419
+ });
420
+ }}
421
+ >
422
+ Expire Now
423
+ </Button>
424
+ <Button
425
+ hue={0}
426
+ onClick={() => {
427
+ if (!confirm(`Delete this suppression?`)) return;
428
+
429
+ Querysub.onCommitFinished(async () => {
430
+ await this.manager.deleteSuppression(suppression.id);
431
+ });
432
+ }}
433
+ >
434
+ Delete
435
+ </Button>
436
+ <div className={css.hbox(20)}>
437
+ <span><b>{formatNumber(totalCount)}</b> Matches</span>
438
+ <span>Updated <b>{formatTime(Date.now() - suppression.lastUpdatedTime)}</b> AGO</span>
439
+ <span>{timeoutText}</span>
440
+ </div>
441
+ </div>
442
+
443
+ <div className={css.hbox(20)}>
444
+ <InputLabel
445
+ label="Pattern"
446
+ value={this.state.patternValue}
447
+ onChangeValue={(value) => {
448
+ this.state.patternValue = value;
449
+ }}
450
+ onKeyDown={e => {
451
+ if (e.key === "Enter") {
452
+ let pattern = e.currentTarget.value.trim();
453
+ if (!pattern) {
454
+ alert("Pattern cannot be empty");
455
+ return;
456
+ }
457
+ this.state.patternValue = pattern;
458
+ Querysub.onCommitFinished(async () => {
459
+ await this.manager.updateSuppression(suppression.id, pattern);
460
+ });
461
+ }
462
+ }}
463
+ className={css.width(500)}
464
+ />
465
+ <InputLabel
466
+ label="Reason"
467
+ value={this.state.notesValue}
468
+ onChangeValue={(value) => {
469
+ this.state.notesValue = value;
470
+ }}
471
+ onKeyDown={e => {
472
+ if (e.key === "Enter") {
473
+ let notes = e.currentTarget.value.trim();
474
+ this.state.notesValue = notes;
475
+ Querysub.onCommitFinished(async () => {
476
+ await this.manager.updateNotes(suppression.id, notes && notes || undefined);
477
+ });
478
+ }
479
+ }}
480
+ className={css.width(500)}
481
+ />
482
+ </div>
483
+
484
+ {matchData && matchData.examples.length > 0 && (
485
+ <div className={css.hbox(8)}>
486
+ {matchData.examples.length > 1 && (
487
+ <Button
488
+ hue={200}
489
+ onClick={() => {
490
+ this.state.showingAllExamples = !this.state.showingAllExamples;
491
+ }}
492
+ >
493
+ {this.state.showingAllExamples && "Show Less" || `Show More (${matchData.examples.length - 1} more)`}
494
+ </Button>
495
+ )}
496
+ <div className={css.vbox(4)}>
497
+ {(this.state.showingAllExamples && matchData.examples || matchData.examples.slice(0, 1)).map((example, idx) => (
498
+ <LogDatumRenderer key={idx} hideSuppression datum={example} pattern={suppression.pattern} />
499
+ ))}
500
+ </div>
501
+ </div>
502
+ )}
503
+ </div>;
504
+ }
505
+ }
@@ -0,0 +1,32 @@
1
+ import { qreact } from "../../../4-dom/qreact";
2
+ import { t } from "../../../2-proxy/schema2";
3
+ import { css } from "typesafecss";
4
+ import { Querysub } from "../../../4-querysub/QuerysubController";
5
+ import { ATag } from "../../../library-components/ATag";
6
+ import { managementPageURL } from "../../managementPages";
7
+ import { LogDatumRenderer } from "./ErrorNotificationPage";
8
+ import { getErrorNotificationsManager } from "./errorWatcher";
9
+
10
+ export class ErrorWarning extends qreact.Component {
11
+ manager = getErrorNotificationsManager();
12
+
13
+ render() {
14
+ if (this.manager.state.isLoading) {
15
+ return undefined;
16
+ }
17
+
18
+ if (this.manager.state.unmatchedErrors.length === 0) {
19
+ return undefined;
20
+ }
21
+
22
+ let firstError = this.manager.state.unmatchedErrors[0];
23
+
24
+ return <div className={css.hbox(4).alignItems("start").hsl(0, 0, 90).bord2(0, 0, 85).pad2(4, 2).hslcolor(0, 0, 0)}>
25
+ <ATag className={css.paddingTop(5).flexShrink0} values={[managementPageURL.getOverride("ErrorNotificationPage")]}>
26
+ Suppress
27
+ </ATag>
28
+ <div className={css.paddingTop(5)}>|</div>
29
+ <LogDatumRenderer datum={firstError} inlineMode />
30
+ </div>;
31
+ }
32
+ }