querysub 0.327.0 → 0.329.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/bin/error-email.js +8 -0
- package/bin/error-im.js +8 -0
- package/package.json +4 -3
- package/src/-a-archives/archivesBackBlaze.ts +20 -0
- package/src/-a-archives/archivesCborT.ts +52 -0
- package/src/-a-archives/archivesDisk.ts +5 -5
- package/src/-a-archives/archivesJSONT.ts +19 -5
- package/src/-a-archives/archivesLimitedCache.ts +118 -7
- package/src/-a-archives/archivesPrivateFileSystem.ts +3 -0
- package/src/-g-core-values/NodeCapabilities.ts +26 -11
- package/src/0-path-value-core/auditLogs.ts +4 -2
- package/src/2-proxy/PathValueProxyWatcher.ts +7 -0
- package/src/3-path-functions/PathFunctionRunner.ts +2 -2
- package/src/4-querysub/Querysub.ts +1 -1
- package/src/5-diagnostics/GenericFormat.tsx +2 -2
- package/src/config.ts +15 -3
- package/src/deployManager/machineApplyMainCode.ts +10 -8
- package/src/deployManager/machineSchema.ts +4 -3
- package/src/deployManager/setupMachineMain.ts +3 -2
- package/src/diagnostics/logs/FastArchiveAppendable.ts +86 -53
- package/src/diagnostics/logs/FastArchiveController.ts +11 -2
- package/src/diagnostics/logs/FastArchiveViewer.tsx +205 -48
- package/src/diagnostics/logs/LogViewer2.tsx +78 -34
- package/src/diagnostics/logs/TimeRangeSelector.tsx +8 -0
- package/src/diagnostics/logs/diskLogGlobalContext.ts +5 -4
- package/src/diagnostics/logs/diskLogger.ts +70 -23
- package/src/diagnostics/logs/errorNotifications/ErrorDigestPage.tsx +409 -0
- package/src/diagnostics/logs/errorNotifications/ErrorNotificationController.ts +94 -67
- package/src/diagnostics/logs/errorNotifications/ErrorSuppressionUI.tsx +37 -3
- package/src/diagnostics/logs/errorNotifications/ErrorWarning.tsx +50 -16
- package/src/diagnostics/logs/errorNotifications/errorDigestEmail.tsx +174 -0
- package/src/diagnostics/logs/errorNotifications/errorDigests.tsx +291 -0
- package/src/diagnostics/logs/errorNotifications/errorLoopEntry.tsx +7 -0
- package/src/diagnostics/logs/errorNotifications/errorWatchEntry.tsx +185 -68
- package/src/diagnostics/logs/lifeCycleAnalysis/spec.md +10 -19
- package/src/diagnostics/managementPages.tsx +33 -15
- package/src/email_ims_notifications/discord.tsx +203 -0
- package/src/{email → email_ims_notifications}/postmark.tsx +3 -3
- package/src/fs.ts +9 -0
- package/src/functional/SocketChannel.ts +9 -0
- package/src/functional/throttleRender.ts +134 -0
- package/src/library-components/ATag.tsx +2 -2
- package/src/library-components/SyncedController.ts +3 -3
- package/src/misc.ts +18 -0
- package/src/misc2.ts +106 -0
- package/src/user-implementation/SecurityPage.tsx +11 -5
- package/src/user-implementation/userData.ts +57 -23
- package/testEntry2.ts +14 -5
- package/src/user-implementation/setEmailKey.ts +0 -25
- /package/src/{email → email_ims_notifications}/sendgrid.tsx +0 -0
|
@@ -6,6 +6,8 @@ import { timeInMinute } from "socket-function/src/misc";
|
|
|
6
6
|
import { formatTime } from "socket-function/src/formatting/format";
|
|
7
7
|
import { addEpsilons } from "../../bits";
|
|
8
8
|
import { FileMetadata } from "./FastArchiveController";
|
|
9
|
+
import { getPathStr2 } from "../../path";
|
|
10
|
+
import { isPublic } from "../../config";
|
|
9
11
|
// IMPORTANT! We can't have any real imports here, because we are depended on so early in startup!
|
|
10
12
|
|
|
11
13
|
if (isNode()) {
|
|
@@ -35,7 +37,25 @@ export type LogDatum = Record<string, unknown> & {
|
|
|
35
37
|
/** Dynamically set when matching recent errors only. */
|
|
36
38
|
__matchedOutdatedSuppressionKey?: string;
|
|
37
39
|
};
|
|
40
|
+
export function getLogHash(obj: LogDatum) {
|
|
41
|
+
return getPathStr2(obj.__threadId || "", obj.time.toString());
|
|
42
|
+
}
|
|
43
|
+
export function getLogFile(obj: LogDatum) {
|
|
44
|
+
let logType = obj.param0 || "";
|
|
45
|
+
if (obj.__FILE__) {
|
|
46
|
+
logType = String(obj.__FILE__);
|
|
47
|
+
}
|
|
48
|
+
if (obj[LOG_LINE_LIMIT_ID]) {
|
|
49
|
+
logType += "::" + String(obj[LOG_LINE_LIMIT_ID]);
|
|
50
|
+
}
|
|
51
|
+
return logType;
|
|
52
|
+
|
|
53
|
+
}
|
|
38
54
|
export const LOG_LIMIT_FLAG = String.fromCharCode(44533) + "LOGS_LIMITED_FLAG-9277640b-d709-4591-ab08-2bb29bbb94f4";
|
|
55
|
+
export const LOG_LINE_LIMIT_FLAG = String.fromCharCode(44534) + "LOGS_LINE_LIMIT_FLAG-dd50ab1f-3021-45e3-82fc-d2702c7a64c8";
|
|
56
|
+
|
|
57
|
+
/** If this key exists in the logged object, as in a key in one of the objects logged, then we will use the value of it as the limit ID. This is useful as it allows us to either override a limit or limit something independently from other logs in the file. */
|
|
58
|
+
export const LOG_LINE_LIMIT_ID = "LIMIT_LINE_ID";
|
|
39
59
|
|
|
40
60
|
export const getLoggers = lazy(function () {
|
|
41
61
|
const { FastArchiveAppendable } = require("./FastArchiveAppendable") as typeof import("./FastArchiveAppendable");
|
|
@@ -52,6 +72,10 @@ export const getLoggers = lazy(function () {
|
|
|
52
72
|
errorLogs: new FastArchiveAppendable<LogDatum>("logs-error/"),
|
|
53
73
|
};
|
|
54
74
|
});
|
|
75
|
+
setImmediate(() => {
|
|
76
|
+
// If we don't import it at all, then it doesn't work client-side.
|
|
77
|
+
require("./FastArchiveAppendable") as typeof import("./FastArchiveAppendable");
|
|
78
|
+
});
|
|
55
79
|
const getNotifyErrors = lazy(function () {
|
|
56
80
|
const { notifyWatchersOfError: notifyErrors } = require("./errorNotifications/ErrorNotificationController") as typeof import("./errorNotifications/ErrorNotificationController");
|
|
57
81
|
if (typeof notifyErrors !== "function") {
|
|
@@ -88,6 +112,8 @@ let logLimitLookup: {
|
|
|
88
112
|
|
|
89
113
|
const LIMIT_PERIOD = timeInMinute * 15;
|
|
90
114
|
const LIMIT_THRESHOLD = 1000;
|
|
115
|
+
const WARN_LIMIT = 100;
|
|
116
|
+
const ERROR_LIMIT = 100;
|
|
91
117
|
|
|
92
118
|
const logDiskDontShim = logDisk;
|
|
93
119
|
/** NOTE: Calling this directly means we lose __FILE__ tracking. But... that's probably fine... */
|
|
@@ -104,6 +130,11 @@ export function logDisk(type: "log" | "warn" | "info" | "error", ...args: unknow
|
|
|
104
130
|
if (logObj.__FILE__) {
|
|
105
131
|
logType = String(logObj.__FILE__);
|
|
106
132
|
}
|
|
133
|
+
let hasLineLimit = false;
|
|
134
|
+
if (logObj[LOG_LINE_LIMIT_ID]) {
|
|
135
|
+
logType += "::" + String(logObj[LOG_LINE_LIMIT_ID]);
|
|
136
|
+
hasLineLimit = true;
|
|
137
|
+
}
|
|
107
138
|
|
|
108
139
|
if (logLimitLookup) {
|
|
109
140
|
if (logObj.time > logLimitLookup.resetTime) {
|
|
@@ -120,32 +151,47 @@ export function logDisk(type: "log" | "warn" | "info" | "error", ...args: unknow
|
|
|
120
151
|
let count = logLimitLookup.counts.get(logType) || 0;
|
|
121
152
|
count++;
|
|
122
153
|
logLimitLookup.counts.set(logType, count);
|
|
123
|
-
|
|
154
|
+
let limit = LIMIT_THRESHOLD;
|
|
155
|
+
if (type === "warn") {
|
|
156
|
+
limit = WARN_LIMIT;
|
|
157
|
+
} else if (type === "error") {
|
|
158
|
+
limit = ERROR_LIMIT;
|
|
159
|
+
}
|
|
160
|
+
if (count > limit) {
|
|
124
161
|
let timeUntilReset = logLimitLookup.resetTime - logObj.time;
|
|
125
|
-
|
|
162
|
+
if (hasLineLimit) {
|
|
163
|
+
process.stdout.write(`Log type hit limit, not writing log type to disk for ~${formatTime(timeUntilReset)}: ${logType}\n`);
|
|
164
|
+
}
|
|
126
165
|
return;
|
|
127
166
|
}
|
|
128
|
-
if (count
|
|
129
|
-
|
|
167
|
+
if (count >= limit) {
|
|
168
|
+
if (hasLineLimit) {
|
|
169
|
+
logObj[LOG_LINE_LIMIT_FLAG] = true;
|
|
170
|
+
} else {
|
|
171
|
+
logObj[LOG_LIMIT_FLAG] = true;
|
|
172
|
+
}
|
|
130
173
|
}
|
|
131
174
|
|
|
132
|
-
|
|
133
|
-
if (
|
|
134
|
-
getLoggers
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
logLogs
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
175
|
+
// We don't want developer errors clogging up the error logs. However, they can still notify errors, Because this will only notify nodes that are able to access us (It uses a reverse connection scheme, so instead of talking to nodes that we can access, we only talk to nodes that can access us), Which will mean it will only notify for local services, so the developer still gets error notifications, But our errors won't be spread to all developers. BUT, we will still watch global errors, because we can contact the global server, so developers will still get errors about production issues, even while developing!
|
|
176
|
+
if (isPublic()) {
|
|
177
|
+
let loggers = startupDone ? getLoggers() : undefined;
|
|
178
|
+
if (!loggers) {
|
|
179
|
+
getLoggers.reset();
|
|
180
|
+
setImmediate(() => {
|
|
181
|
+
logDiskDontShim(type, ...args);
|
|
182
|
+
});
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
const { logLogs, warnLogs, infoLogs, errorLogs } = loggers;
|
|
186
|
+
if (type === "log") {
|
|
187
|
+
logLogs.append(logObj);
|
|
188
|
+
} else if (type === "warn") {
|
|
189
|
+
warnLogs.append(logObj);
|
|
190
|
+
} else if (type === "info") {
|
|
191
|
+
infoLogs.append(logObj);
|
|
192
|
+
} else {
|
|
193
|
+
errorLogs.append(logObj);
|
|
194
|
+
}
|
|
149
195
|
}
|
|
150
196
|
|
|
151
197
|
if (type === "warn" || type === "error") {
|
|
@@ -162,12 +208,12 @@ let lastLogTime = 0;
|
|
|
162
208
|
|
|
163
209
|
function packageLogObj(type: string, args: unknown[]): LogDatum {
|
|
164
210
|
let now = Date.now();
|
|
165
|
-
if (now
|
|
211
|
+
if (now <= lastLogTime) {
|
|
166
212
|
now = addEpsilons(lastLogTime, 1);
|
|
167
213
|
}
|
|
168
214
|
lastLogTime = now;
|
|
169
215
|
let logObj: LogDatum = {
|
|
170
|
-
time:
|
|
216
|
+
time: 0,
|
|
171
217
|
__LOG_TYPE: type,
|
|
172
218
|
};
|
|
173
219
|
for (let part of globalContextParts) {
|
|
@@ -184,5 +230,6 @@ function packageLogObj(type: string, args: unknown[]): LogDatum {
|
|
|
184
230
|
stringCount++;
|
|
185
231
|
}
|
|
186
232
|
}
|
|
233
|
+
logObj.time = now;
|
|
187
234
|
return logObj;
|
|
188
235
|
}
|
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
module.noserverhotreload = false;
|
|
2
|
+
module.hotreload = true;
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
import { SocketFunction } from "socket-function/SocketFunction";
|
|
6
|
+
import { qreact } from "../../../4-dom/qreact";
|
|
7
|
+
import { getSyncedController } from "../../../library-components/SyncedController";
|
|
8
|
+
import { ErrorDigestController, errorDigestHistory, ErrorDigestInfo } from "./errorDigests";
|
|
9
|
+
import { isManagementUser } from "../../../-0-hooks/hooks";
|
|
10
|
+
import { assertIsManagementUser } from "../../managementPages";
|
|
11
|
+
import { URLParam } from "../../../library-components/URLParam";
|
|
12
|
+
import { tabURL } from "../../../library-components/urlResetGroups";
|
|
13
|
+
import { Anchor, ATag } from "../../../library-components/ATag";
|
|
14
|
+
import { css } from "../../../4-dom/css";
|
|
15
|
+
import { list, sort } from "socket-function/src/misc";
|
|
16
|
+
import { formatDateTime, formatVeryNiceDateTime, formatNumber } from "socket-function/src/formatting/format";
|
|
17
|
+
import { Table } from "../../../5-diagnostics/Table";
|
|
18
|
+
import { Histogram } from "../../../library-components/Histogram";
|
|
19
|
+
import { TabbedUI } from "../../../library-components/TabbedUI";
|
|
20
|
+
import { LogDatum } from "../diskLogger";
|
|
21
|
+
import { t } from "../../../2-proxy/schema2";
|
|
22
|
+
import { getErrorLogsLink } from "./ErrorWarning";
|
|
23
|
+
|
|
24
|
+
export const digestKeyURL = new URLParam("digestKey", "");
|
|
25
|
+
export const fileDetailTabURL = new URLParam("fileDetailTab", "errors");
|
|
26
|
+
|
|
27
|
+
export class ErrorDigestPage extends qreact.Component {
|
|
28
|
+
state = t.state({
|
|
29
|
+
selectedFile: t.string("")
|
|
30
|
+
});
|
|
31
|
+
render() {
|
|
32
|
+
// Get the current tab mode
|
|
33
|
+
let currentTab = tabURL.value;
|
|
34
|
+
let selectedDigestKey = digestKeyURL.value;
|
|
35
|
+
|
|
36
|
+
if (currentTab === "detail" && selectedDigestKey) {
|
|
37
|
+
return this.renderDigestDetail(selectedDigestKey);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return this.renderDigestList();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
renderDigestList() {
|
|
44
|
+
let controller = ErrorDigestController(SocketFunction.browserNodeId());
|
|
45
|
+
|
|
46
|
+
let keys = controller.getDigestKeys();
|
|
47
|
+
|
|
48
|
+
if (!keys || keys.length === 0) {
|
|
49
|
+
return (
|
|
50
|
+
<div className={css.vbox(16).pad2(24)}>
|
|
51
|
+
<div className={css.fontSize(24).fontWeight(600)}>
|
|
52
|
+
Error Digest History
|
|
53
|
+
</div>
|
|
54
|
+
<div>No digest data available</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Sort lexicographically (highest = newest) and take latest 100
|
|
60
|
+
let sortedKeys = sort(keys as string[], key => key).reverse().slice(0, 100);
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={css.vbox(16).paddingTop(24).paddingLeft(24).fillHeight.fillWidth}>
|
|
64
|
+
<div className={css.fontSize(24).fontWeight(600)}>
|
|
65
|
+
Error Digest History
|
|
66
|
+
</div>
|
|
67
|
+
<div className={css.fontSize(14).color("gray")}>
|
|
68
|
+
Showing latest {Math.min(sortedKeys.length, 100)} digests (sorted by creation time, newest first)
|
|
69
|
+
</div>
|
|
70
|
+
|
|
71
|
+
<div className={css.vbox(8).overflowAuto.fillWidth}>
|
|
72
|
+
{sortedKeys.map(key => (
|
|
73
|
+
<ATag
|
|
74
|
+
key={key}
|
|
75
|
+
values={[
|
|
76
|
+
tabURL.getOverride("detail"),
|
|
77
|
+
digestKeyURL.getOverride(key)
|
|
78
|
+
]}
|
|
79
|
+
className={css.pad2(12).hsla(0, 0, 0, 0.1).pointer.hslahover(0, 0, 0, 0.15)}
|
|
80
|
+
>
|
|
81
|
+
{formatVeryNiceDateTime(+key.split(".")[0])}
|
|
82
|
+
</ATag>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
renderDigestDetail(digestKey: string) {
|
|
90
|
+
let controller = ErrorDigestController(SocketFunction.browserNodeId());
|
|
91
|
+
|
|
92
|
+
let digestData = controller.getDigest(digestKey);
|
|
93
|
+
|
|
94
|
+
if (!digestData) {
|
|
95
|
+
return (
|
|
96
|
+
<div className={css.vbox(16).pad2(24)}>
|
|
97
|
+
<div className={css.hbox(16)}>
|
|
98
|
+
<ATag
|
|
99
|
+
values={[
|
|
100
|
+
tabURL.getOverride(""),
|
|
101
|
+
digestKeyURL.getOverride("")
|
|
102
|
+
]}
|
|
103
|
+
>
|
|
104
|
+
← Back to Digest List
|
|
105
|
+
</ATag>
|
|
106
|
+
</div>
|
|
107
|
+
<div>Loading digest data...</div>
|
|
108
|
+
</div>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// If a file is selected, show file details
|
|
113
|
+
if (this.state.selectedFile) {
|
|
114
|
+
return this.renderFileDetail(digestData, digestKey);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return (
|
|
118
|
+
<div className={css.vbox(24).pad2(24).fillHeight.overflowAuto}>
|
|
119
|
+
{/* Header */}
|
|
120
|
+
<div className={css.hbox(16).alignItems("center")}>
|
|
121
|
+
<ATag
|
|
122
|
+
values={[
|
|
123
|
+
tabURL.getOverride(""),
|
|
124
|
+
digestKeyURL.getOverride("")
|
|
125
|
+
]}
|
|
126
|
+
>
|
|
127
|
+
← Back to Digest List
|
|
128
|
+
</ATag>
|
|
129
|
+
<div className={css.fontSize(24).fontWeight(600)}>
|
|
130
|
+
Digest: {formatVeryNiceDateTime(+digestKey.split(".")[0])}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
|
|
134
|
+
<Anchor
|
|
135
|
+
className={css.fontSize(24)}
|
|
136
|
+
values={getErrorLogsLink({
|
|
137
|
+
startTime: digestData.startTime,
|
|
138
|
+
endTime: digestData.endTime,
|
|
139
|
+
})}
|
|
140
|
+
>
|
|
141
|
+
View live logs
|
|
142
|
+
</Anchor>
|
|
143
|
+
|
|
144
|
+
{/* Metadata Section */}
|
|
145
|
+
{this.renderMetadata(digestData)}
|
|
146
|
+
|
|
147
|
+
{/* Charts Section */}
|
|
148
|
+
{this.renderCharts(digestData)}
|
|
149
|
+
|
|
150
|
+
{/* Files Table */}
|
|
151
|
+
{this.renderFilesTable(digestData)}
|
|
152
|
+
</div>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
renderMetadata(digestData: ErrorDigestInfo) {
|
|
157
|
+
return (
|
|
158
|
+
<div className={css.vbox(16)}>
|
|
159
|
+
<div className={css.fontSize(20).fontWeight(600)}>Metadata</div>
|
|
160
|
+
<div className={css.hbox(32).wrap}>
|
|
161
|
+
<div className={css.vbox(4)}>
|
|
162
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Compressed Bytes</div>
|
|
163
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
164
|
+
{formatNumber(digestData.totalCompressedBytes)}
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div className={css.vbox(4)}>
|
|
168
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Uncompressed Bytes</div>
|
|
169
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
170
|
+
{formatNumber(digestData.totalUncompressedBytes)}
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
<div className={css.vbox(4)}>
|
|
174
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Total Files</div>
|
|
175
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
176
|
+
{formatNumber(digestData.totalFiles)}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
<div className={css.vbox(4)}>
|
|
180
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Scan Duration</div>
|
|
181
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
182
|
+
{formatNumber(digestData.scanDuration)}ms
|
|
183
|
+
</div>
|
|
184
|
+
</div>
|
|
185
|
+
<div className={css.vbox(4)}>
|
|
186
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Start Time</div>
|
|
187
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
188
|
+
{formatVeryNiceDateTime(digestData.startTime)}
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
<div className={css.vbox(4)}>
|
|
192
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>End Time</div>
|
|
193
|
+
<div className={css.fontSize(16).fontWeight(500)}>
|
|
194
|
+
{formatVeryNiceDateTime(digestData.endTime)}
|
|
195
|
+
</div>
|
|
196
|
+
</div>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
renderCharts(digestData: ErrorDigestInfo) {
|
|
203
|
+
// Convert histogram data to chart data
|
|
204
|
+
let unsuppressedErrorsData: { x: number; y: number }[] = [];
|
|
205
|
+
let unsuppressedWarningsData: { x: number; y: number }[] = [];
|
|
206
|
+
let corruptData: { x: number; y: number }[] = [];
|
|
207
|
+
let suppressedData: { x: number; y: number }[] = [];
|
|
208
|
+
|
|
209
|
+
for (let [time, data] of digestData.histogram.entries()) {
|
|
210
|
+
unsuppressedErrorsData.push({ x: time, y: data.unsuppressedErrors });
|
|
211
|
+
unsuppressedWarningsData.push({ x: time, y: data.unsuppressedWarnings });
|
|
212
|
+
corruptData.push({ x: time, y: data.corruptErrors + data.corruptWarnings });
|
|
213
|
+
suppressedData.push({ x: time, y: data.suppressedErrors + data.suppressedWarnings });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return (
|
|
217
|
+
<div className={css.vbox(16)}>
|
|
218
|
+
<div className={css.fontSize(20).fontWeight(600)}>Error Distribution Over Time</div>
|
|
219
|
+
<div className={css.vbox(16)}>
|
|
220
|
+
<div className={css.hbox(16).wrap}>
|
|
221
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
222
|
+
<Histogram
|
|
223
|
+
title="Unsuppressed Errors"
|
|
224
|
+
values={unsuppressedErrorsData}
|
|
225
|
+
xColumn={{
|
|
226
|
+
title: "Time",
|
|
227
|
+
format: (value) => formatDateTime(value)
|
|
228
|
+
}}
|
|
229
|
+
yColumn={{ title: "Count" }}
|
|
230
|
+
/>
|
|
231
|
+
</div>
|
|
232
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
233
|
+
<Histogram
|
|
234
|
+
title="Unsuppressed Warnings"
|
|
235
|
+
values={unsuppressedWarningsData}
|
|
236
|
+
xColumn={{
|
|
237
|
+
title: "Time",
|
|
238
|
+
format: (value) => formatDateTime(value)
|
|
239
|
+
}}
|
|
240
|
+
yColumn={{ title: "Count" }}
|
|
241
|
+
/>
|
|
242
|
+
</div>
|
|
243
|
+
</div>
|
|
244
|
+
<div className={css.hbox(16).wrap}>
|
|
245
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
246
|
+
<Histogram
|
|
247
|
+
title="Corrupt Errors + Warnings"
|
|
248
|
+
values={corruptData}
|
|
249
|
+
xColumn={{
|
|
250
|
+
title: "Time",
|
|
251
|
+
format: (value) => formatDateTime(value)
|
|
252
|
+
}}
|
|
253
|
+
yColumn={{ title: "Count" }}
|
|
254
|
+
/>
|
|
255
|
+
</div>
|
|
256
|
+
<div className={css.fillWidth.minWidth(400).height(300)}>
|
|
257
|
+
<Histogram
|
|
258
|
+
title="Suppressed Errors + Warnings"
|
|
259
|
+
values={suppressedData}
|
|
260
|
+
xColumn={{
|
|
261
|
+
title: "Time",
|
|
262
|
+
format: (value) => formatDateTime(value)
|
|
263
|
+
}}
|
|
264
|
+
yColumn={{ title: "Count" }}
|
|
265
|
+
/>
|
|
266
|
+
</div>
|
|
267
|
+
</div>
|
|
268
|
+
</div>
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
renderFilesTable(digestData: ErrorDigestInfo) {
|
|
274
|
+
// Convert byFile map to table rows
|
|
275
|
+
let fileRows = Array.from(digestData.byFile.entries()).map(([fileName, fileData]) => {
|
|
276
|
+
let latestError = fileData.latestErrors[fileData.latestErrors.length - 1];
|
|
277
|
+
let latestWarning = fileData.latestWarnings[fileData.latestWarnings.length - 1];
|
|
278
|
+
let latestLog = latestError ?? latestWarning;
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
fileName,
|
|
282
|
+
errorCount: fileData.errors,
|
|
283
|
+
warningCount: fileData.warnings,
|
|
284
|
+
latestMessage: latestLog?.param0 ?? "No messages",
|
|
285
|
+
latestTime: latestLog?.time ?? 0
|
|
286
|
+
};
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Sort by error count descending
|
|
290
|
+
fileRows = sort(fileRows, row => row.errorCount).reverse();
|
|
291
|
+
|
|
292
|
+
return (
|
|
293
|
+
<div className={css.vbox(16)}>
|
|
294
|
+
<div className={css.fontSize(20).fontWeight(600)}>Files ({fileRows.length} files)</div>
|
|
295
|
+
<Table
|
|
296
|
+
columns={{
|
|
297
|
+
fileName: { title: "File Name" },
|
|
298
|
+
errorCount: { title: "Errors" },
|
|
299
|
+
warningCount: { title: "Warnings" },
|
|
300
|
+
latestMessage: {
|
|
301
|
+
title: "Latest Message",
|
|
302
|
+
formatter: (value: any) => String(value).slice(0, 100) + (String(value).length > 100 ? "..." : "")
|
|
303
|
+
}
|
|
304
|
+
}}
|
|
305
|
+
rows={fileRows}
|
|
306
|
+
getRowAttributes={(row: any) => ({
|
|
307
|
+
className: css.cursor("pointer").hslhover(210, 20, 95),
|
|
308
|
+
onClick: () => {
|
|
309
|
+
this.state.selectedFile = row.fileName;
|
|
310
|
+
}
|
|
311
|
+
})}
|
|
312
|
+
/>
|
|
313
|
+
</div>
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
renderFileDetail(digestData: ErrorDigestInfo, digestKey: string) {
|
|
318
|
+
let selectedFileName = this.state.selectedFile;
|
|
319
|
+
let fileData = digestData.byFile.get(selectedFileName);
|
|
320
|
+
|
|
321
|
+
if (!fileData) {
|
|
322
|
+
return (
|
|
323
|
+
<div className={css.vbox(16).pad2(24)}>
|
|
324
|
+
<div>File not found</div>
|
|
325
|
+
</div>
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return (
|
|
330
|
+
<div className={css.vbox(24).pad2(24).fillHeight}>
|
|
331
|
+
{/* Header with back button */}
|
|
332
|
+
<div className={css.hbox(16).alignItems("center")}>
|
|
333
|
+
<ATag
|
|
334
|
+
clickOverride={() => {
|
|
335
|
+
this.state.selectedFile = "";
|
|
336
|
+
}}
|
|
337
|
+
className={css.cursor("pointer")}
|
|
338
|
+
>
|
|
339
|
+
← Back to Files Table
|
|
340
|
+
</ATag>
|
|
341
|
+
<div className={css.fontSize(20).fontWeight(600)}>
|
|
342
|
+
{selectedFileName}
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
|
|
346
|
+
{/* File stats */}
|
|
347
|
+
<div className={css.hbox(32)}>
|
|
348
|
+
<div className={css.vbox(4)}>
|
|
349
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Errors</div>
|
|
350
|
+
<div className={css.fontSize(16).fontWeight(500)}>{fileData.errors}</div>
|
|
351
|
+
</div>
|
|
352
|
+
<div className={css.vbox(4)}>
|
|
353
|
+
<div className={css.fontSize(14).colorhsl(0, 0, 60)}>Warnings</div>
|
|
354
|
+
<div className={css.fontSize(16).fontWeight(500)}>{fileData.warnings}</div>
|
|
355
|
+
</div>
|
|
356
|
+
</div>
|
|
357
|
+
|
|
358
|
+
{/* Tabbed interface for errors/warnings */}
|
|
359
|
+
<div className={css.fillHeight.overflowHidden}>
|
|
360
|
+
<TabbedUI
|
|
361
|
+
tab={fileDetailTabURL}
|
|
362
|
+
tabs={[
|
|
363
|
+
{
|
|
364
|
+
value: "errors",
|
|
365
|
+
title: `Errors (${fileData.errors})`,
|
|
366
|
+
contents: this.renderLogEntries(fileData.latestErrors, "errors")
|
|
367
|
+
},
|
|
368
|
+
{
|
|
369
|
+
value: "warnings",
|
|
370
|
+
title: `Warnings (${fileData.warnings})`,
|
|
371
|
+
contents: this.renderLogEntries(fileData.latestWarnings, "warnings")
|
|
372
|
+
}
|
|
373
|
+
]}
|
|
374
|
+
/>
|
|
375
|
+
</div>
|
|
376
|
+
</div>
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
renderLogEntries(logs: LogDatum[], type: "errors" | "warnings") {
|
|
381
|
+
if (logs.length === 0) {
|
|
382
|
+
return <div className={css.colorhsl(0, 0, 60)}>No {type} found</div>;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
let tableRows = logs.map((log, index) => ({
|
|
386
|
+
index: logs.length - index,
|
|
387
|
+
time: formatVeryNiceDateTime(log.time),
|
|
388
|
+
message: log.param0 ?? "No message",
|
|
389
|
+
threadId: log.__threadId ?? "Unknown",
|
|
390
|
+
name: log.__NAME__ ?? "Unknown"
|
|
391
|
+
}));
|
|
392
|
+
|
|
393
|
+
return (
|
|
394
|
+
<div className={css.fillHeight.overflowAuto}>
|
|
395
|
+
<Table
|
|
396
|
+
columns={{
|
|
397
|
+
index: { title: "#" },
|
|
398
|
+
time: { title: "Time" },
|
|
399
|
+
name: { title: "Source" },
|
|
400
|
+
threadId: { title: "Thread" },
|
|
401
|
+
message: { title: "Message" }
|
|
402
|
+
}}
|
|
403
|
+
rows={tableRows}
|
|
404
|
+
initialLimit={50}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
}
|