querysub 0.378.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.
@@ -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
+ }
@@ -8,29 +8,54 @@ import { MachineInfo } from "../../../deployManager/machineSchema";
8
8
  import { createMatchesPattern } from "../IndexedLogs/bufferSearchFindMatcher";
9
9
  import { LogDatum, getErrorLogs } from "../diskLogger";
10
10
  import { watchAllValues } from "./logWatcher";
11
- import { batchFunction, runInfinitePollCallAtStart } from "socket-function/src/batching";
12
- import { nextId, timeInMinute } from "socket-function/src/misc";
11
+ import { batchFunction, runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
12
+ import { nextId, sort, throttleFunction, timeInMinute } from "socket-function/src/misc";
13
13
  import { SocketFunction } from "socket-function/SocketFunction";
14
14
  import { assertIsManagementUser } from "../../managementPages";
15
15
  import { getSyncedController } from "../../../library-components/SyncedController";
16
16
  import { getControllerNodeId } from "../../../-g-core-values/NodeCapabilities";
17
+ import { t } from "../../../2-proxy/schema2";
18
+ import { sendDiscordMessage } from "../../../email_ims_notifications/discord";
19
+ import { user_data } from "../../../user-implementation/userData";
20
+ import { callOpenRouterJSON } from "./openRouterHelper";
21
+ import { createLink } from "../../../library-components/ATag";
22
+ import { showingManagementURL, managementPageURL } from "../../managementPages";
23
+ import { atomic } from "../../../2-proxy/PathValueProxyWatcher";
24
+ import { magenta } from "socket-function/src/formatting/logColors";
17
25
 
18
26
 
19
27
  type SuppressionEntry = {
20
28
  id: string;
21
- description: string;
29
+ notes?: string;
22
30
  pattern: string;
23
31
 
32
+ // After this time, this suppression won't suppress data anymore.
33
+ timeout: number;
34
+
24
35
  createdTime: number;
25
36
  lastUpdatedTime: number;
26
37
  };
27
38
  const suppression = archiveJSONT<SuppressionEntry>(() => nestArchives("logs/error-suppression/", getArchivesBackblaze(getDomain())));
28
39
  let suppressionCache: SuppressionEntry[] = [];
40
+
41
+ // In-memory Discord notification throttling
42
+ const timeInHour = 60 * 60 * 1000;
43
+ const NEW_SUPPRESSION_THROTTLE = timeInHour * 6;
44
+ const EXISTING_SUPPRESSION_THROTTLE = timeInHour * 24;
45
+
46
+ let isThrottlingNewSuppressions = false;
47
+ let isThrottlingExistingSuppressions = false;
48
+ let pendingDiscordNotifications = new Map<string, {
49
+ errorCount: number;
50
+ isNew: boolean;
51
+ }>();
29
52
  let ensureWatching = lazy(async () => {
30
53
  await runInfinitePollCallAtStart(timeInMinute * 5, updateNow);
31
54
  });
32
55
  async function updateNow() {
33
56
  suppressionCache = await suppression.values();
57
+ // Can't wait, because that will cause a cyclic loop
58
+ void reapplySuppressions();
34
59
  }
35
60
  async function getSuppressionEntries(): Promise<SuppressionEntry[]> {
36
61
  await ensureWatching();
@@ -43,14 +68,16 @@ const MAX_UNMATCHED = 10000;
43
68
  const MAX_EXAMPLES = 100;
44
69
  let unmatchedIndex = 0;
45
70
  let unmatchedErrors: LogDatum[] = [];
46
- let suppressionMatches = new Map<string, {
71
+ export type SuppressionMatch = {
72
+ id: string;
47
73
  // getHistoryChunk =>
48
74
  history: Map<number, {
49
75
  count: number;
50
76
  }>;
51
77
  exampleIndex: number;
52
78
  examples: LogDatum[];
53
- }>();
79
+ };
80
+ let suppressionMatches = new Map<string, SuppressionMatch>();
54
81
  const chunkUnit = timeInMinute * 5;
55
82
  export function getHistoryChunk(time: number) {
56
83
  return Math.floor(time / chunkUnit) * chunkUnit;
@@ -60,50 +87,265 @@ export function getChunkEndTime(chunk: number) {
60
87
  return chunk + chunkUnit;
61
88
  }
62
89
 
90
+ type SuppressionStatus = "suppressed" | "matched-timeout-passed" | "no-match";
91
+
92
+ function applySuppression(error: LogDatum, suppressionEntry: SuppressionEntry): SuppressionStatus {
93
+ let errorBuffer = Buffer.from(JSON.stringify(error));
94
+ let isMatch = getMatcher(suppressionEntry.pattern);
95
+ if (!isMatch(errorBuffer)) return "no-match";
96
+
97
+ let history = suppressionMatches.get(suppressionEntry.id);
98
+ if (!history) {
99
+ history = {
100
+ id: suppressionEntry.id,
101
+ history: new Map(),
102
+ exampleIndex: 0,
103
+ examples: [],
104
+ };
105
+ suppressionMatches.set(suppressionEntry.id, history);
106
+ }
107
+
108
+ if (history.examples.length > MAX_EXAMPLES) {
109
+ history.examples[history.exampleIndex] = error;
110
+ history.exampleIndex = (history.exampleIndex + 1) % MAX_EXAMPLES;
111
+ } else {
112
+ history.examples.push(error);
113
+ }
114
+
115
+ let historyChunk = getHistoryChunk(error.time);
116
+ let historyEntry = history.history.get(historyChunk);
117
+ if (!historyEntry) {
118
+ historyEntry = { count: 0 };
119
+ history.history.set(historyChunk, historyEntry);
120
+ }
121
+ historyEntry.count++;
122
+
123
+ if (error.time > suppressionEntry.timeout) {
124
+ return "matched-timeout-passed";
125
+ }
126
+
127
+ return "suppressed";
128
+ }
129
+
130
+ async function reapplySuppressions() {
131
+ let suppressionEntries = await getSuppressionEntries();
132
+ let newUnmatchedErrors: LogDatum[] = [];
133
+ let changedSuppressionIds = new Set<string>();
134
+
135
+ for (let error of unmatchedErrors) {
136
+ let suppressed = false;
137
+ for (let suppressionEntry of suppressionEntries) {
138
+ let status = applySuppression(error, suppressionEntry);
139
+ if (status === "suppressed" || status === "matched-timeout-passed") {
140
+ changedSuppressionIds.add(suppressionEntry.id);
141
+ }
142
+ if (status === "suppressed") {
143
+ suppressed = true;
144
+ }
145
+ }
146
+ if (!suppressed) {
147
+ newUnmatchedErrors.push(error);
148
+ }
149
+ }
150
+
151
+ unmatchedErrors = newUnmatchedErrors;
152
+ unmatchedIndex = unmatchedErrors.length % MAX_UNMATCHED;
153
+
154
+ for (let id of changedSuppressionIds) {
155
+ void ErrorNotificationService.triggerSuppressionUpdate(id);
156
+ }
157
+ }
158
+
159
+ async function onSuppressionsChanged() {
160
+ // NOTE: Update Now also reapplies suppression internally. after it finishes the update.
161
+ void updateNow();
162
+ await reapplySuppressions();
163
+ }
164
+
165
+ type PatternResponse = {
166
+ pattern: string;
167
+ };
168
+
169
+ const PATTERN_GENERATION_LIMIT = 100;
170
+ const PATTERN_GENERATION_WINDOW = 15 * 60 * 1000;
171
+ let patternGenerationTimes: number[] = [];
172
+
173
+ async function generatePatternForError(error: LogDatum): Promise<string> {
174
+ const now = Date.now();
175
+ const windowStart = now - PATTERN_GENERATION_WINDOW;
176
+
177
+ patternGenerationTimes = patternGenerationTimes.filter(time => time > windowStart);
178
+
179
+ if (patternGenerationTimes.length >= PATTERN_GENERATION_LIMIT) {
180
+ console.log(magenta(`Rate limit exceeded for pattern generation (${patternGenerationTimes.length}/${PATTERN_GENERATION_LIMIT} in last 15 minutes)`));
181
+ return "";
182
+ }
183
+
184
+ patternGenerationTimes.push(now);
185
+ const openRouterAPIKey = await Querysub.commitAsync(() => atomic(user_data().secure.openRouterAPIKey));
186
+ if (!openRouterAPIKey) {
187
+ throw new Error("OpenRouter API key not set");
188
+ }
189
+
190
+ console.log(magenta(`Createing pattern for error: ${error.param0}`));
191
+
192
+ const param0 = error.param0 && String(error.param0) || "(no message)";
193
+
194
+ const prompt = `You are helping to categorize error messages. We categorize them by finding specific unique text inside of the error message which will be the same even if the variables and the instance changes.
195
+
196
+ Here is an error message:
197
+ ${param0}
198
+
199
+ Please provide a SHORT search string that would match this type of error. The string should:
200
+ - Be specific enough to identify this error type
201
+ - Be generic enough to match similar errors (avoid dynamic values like timestamps, IDs, or specific numbers)
202
+ - Be a simple substring (no special operators)
203
+
204
+ Respond with a JSON object with a "pattern" field containing the search string.`;
205
+
206
+ const response = await callOpenRouterJSON<PatternResponse>(openRouterAPIKey, prompt);
207
+
208
+ const pattern = response.pattern && response.pattern.trim();
209
+
210
+ if (!pattern) {
211
+ throw new Error("AI returned an empty pattern");
212
+ }
213
+
214
+ return pattern;
215
+ }
216
+
217
+ function queueDiscordNotification(suppressionId: string, errorCount: number, isNew: boolean) {
218
+ const existing = pendingDiscordNotifications.get(suppressionId);
219
+ if (existing) {
220
+ existing.errorCount += errorCount;
221
+ } else {
222
+ pendingDiscordNotifications.set(suppressionId, { errorCount, isNew });
223
+ }
224
+ void trySendDiscordNotifications(isNew);
225
+ }
226
+
227
+ function trySendDiscordNotifications(isNew: boolean) {
228
+ if (isNew && isThrottlingNewSuppressions) return;
229
+ if (isThrottlingExistingSuppressions) return;
230
+ if (pendingDiscordNotifications.size === 0) return;
231
+ void trySendDiscordNotificationsBase();
232
+ }
233
+
234
+ const trySendDiscordNotificationsBase = batchFunction({ delay: 1000 * 3 }, async function trySendDiscordNotificationsBase(nothing: void[]) {
235
+ if (pendingDiscordNotifications.size === 0) return;
236
+
237
+ const webhookURL = await Querysub.commitAsync(() => atomic(user_data().secure.notifyDiscordWebhookURL));
238
+ if (!webhookURL) {
239
+ return;
240
+ }
241
+
242
+
243
+ const newSuppressions: Array<{ id: string; count: number; pattern: string }> = [];
244
+ const existingSuppressions: Array<{ id: string; count: number; pattern: string }> = [];
245
+
246
+ for (const [suppressionId, data] of pendingDiscordNotifications.entries()) {
247
+ const suppressionEntry = suppressionCache.find(e => e.id === suppressionId);
248
+ if (!suppressionEntry) continue;
249
+
250
+ if (data.isNew) {
251
+ newSuppressions.push({ id: suppressionId, count: data.errorCount, pattern: suppressionEntry.pattern });
252
+ } else {
253
+ existingSuppressions.push({ id: suppressionId, count: data.errorCount, pattern: suppressionEntry.pattern });
254
+ }
255
+ }
256
+
257
+ let messageLines: string[] = [];
258
+
259
+ for (const sup of newSuppressions) {
260
+ const line = `(${sup.count}) "${sup.pattern.slice(0, 50)}" [NEW]`;
261
+ if (messageLines.join("\n").length + line.length < 400) {
262
+ messageLines.push(line);
263
+ }
264
+ }
265
+
266
+ for (const sup of existingSuppressions) {
267
+ const line = `(${sup.count}) "${sup.pattern.slice(0, 50)}" [EXISTING]`;
268
+ if (messageLines.join("\n").length + line.length < 400) {
269
+ messageLines.push(line);
270
+ }
271
+ }
272
+
273
+ const link = createLink([
274
+ { param: showingManagementURL, value: true },
275
+ { param: managementPageURL, value: "ErrorNotificationPage" },
276
+ ]);
277
+ const messageText = messageLines.join("\n");
278
+ const message = `[${messageText}](${link})`;
279
+
280
+ try {
281
+ await sendDiscordMessage({ webhookURL, message });
282
+
283
+ isThrottlingNewSuppressions = true;
284
+ isThrottlingExistingSuppressions = true;
285
+ setTimeout(() => {
286
+ isThrottlingNewSuppressions = false;
287
+ let anyNew = Array.from(pendingDiscordNotifications.values()).some(data => data.isNew);
288
+ void trySendDiscordNotifications(anyNew);
289
+ }, NEW_SUPPRESSION_THROTTLE);
290
+ setTimeout(() => {
291
+ isThrottlingExistingSuppressions = false;
292
+ void trySendDiscordNotifications(false);
293
+ }, EXISTING_SUPPRESSION_THROTTLE);
294
+
295
+ pendingDiscordNotifications.clear();
296
+ } catch (error) {
297
+ console.error("Failed to send Discord notification:", error);
298
+ }
299
+ });
300
+
63
301
  export async function exposeErrorWatchService() {
64
302
  SocketFunction.expose(ErrorNotificationServiceBase);
65
303
 
66
304
  let errorLogs = await getErrorLogs();
67
305
  for await (let error of watchAllValues(errorLogs)) {
68
- let errorBuffer = Buffer.from(JSON.stringify(error));
69
306
  let suppressionEntries = await getSuppressionEntries();
70
- let anyMatches = false;
71
- for (let suppressionEntry of suppressionEntries) {
72
- let matcher = getMatcher(suppressionEntry.pattern);
73
- if (!matcher(errorBuffer)) continue;
74
- anyMatches = true;
75
- let history = suppressionMatches.get(suppressionEntry.id);
76
- if (!history) {
77
- history = {
78
- history: new Map(),
79
- exampleIndex: 0,
80
- examples: [],
81
- };
82
- suppressionMatches.set(suppressionEntry.id, history);
83
- }
307
+ let suppressed = false;
308
+ let matchedSuppressionId: string | undefined = undefined;
84
309
 
85
- if (history.examples.length > MAX_EXAMPLES) {
86
- history.examples[history.exampleIndex] = error;
87
- history.exampleIndex = (history.exampleIndex + 1) % MAX_EXAMPLES;
88
- } else {
89
- history.examples.push(error);
310
+ for (let suppressionEntry of suppressionEntries) {
311
+ let status = applySuppression(error, suppressionEntry);
312
+ if (status === "suppressed" || status === "matched-timeout-passed") {
313
+ void ErrorNotificationService.triggerSuppressionUpdate(suppressionEntry.id);
90
314
  }
91
-
92
- let historyChunk = getHistoryChunk(error.time);
93
- let historyEntry = history.history.get(historyChunk);
94
- if (!historyEntry) {
95
- historyEntry = { count: 0 };
96
- history.history.set(historyChunk, historyEntry);
315
+ if (status === "suppressed") {
316
+ suppressed = true;
317
+ matchedSuppressionId = suppressionEntry.id;
97
318
  }
98
- historyEntry.count++;
99
319
  }
100
- if (!anyMatches) {
320
+
321
+ if (suppressed && matchedSuppressionId) {
322
+ queueDiscordNotification(matchedSuppressionId, 1, false);
323
+ } else if (!suppressed) {
101
324
  if (unmatchedErrors.length > unmatchedIndex) {
102
325
  unmatchedErrors[unmatchedIndex] = error;
103
326
  unmatchedIndex = (unmatchedIndex + 1) % MAX_UNMATCHED;
104
327
  } else {
105
328
  unmatchedErrors.push(error);
106
329
  }
330
+
331
+ const pattern = await generatePatternForError(error);
332
+ if (pattern) {
333
+ const newEntry: SuppressionEntry = {
334
+ id: nextId(),
335
+ pattern,
336
+ timeout: 0,
337
+ createdTime: Date.now(),
338
+ lastUpdatedTime: Date.now(),
339
+ };
340
+
341
+ suppressionCache.push(newEntry);
342
+ void suppression.set(newEntry.id, newEntry);
343
+ applySuppression(error, newEntry);
344
+ queueDiscordNotification(newEntry.id, 1, true);
345
+
346
+ void ErrorNotificationService.triggerSuppressionUpdate(newEntry.id);
347
+ }
348
+
107
349
  void ErrorNotificationService.triggerUnmatchedError(error);
108
350
  }
109
351
  }
@@ -117,6 +359,14 @@ class ErrorNotificationService {
117
359
  };
118
360
  }
119
361
 
362
+ public async getUnmatchedErrorsLimited() {
363
+ // Oldest first, otherwise, while you're trying to deal with one match, it might keep changing, which is annoying
364
+ sort(unmatchedErrors, x => x.time);
365
+ // We have to sort from oldest to first, otherwise the way we write it will be weird, as we write it in the order from oldest to first already. However, we don't want to clobber the ones we're returning, so we start right after them. So we're going to keep the oldest ones for a little bit longer after we call this function.
366
+ unmatchedIndex = 5;
367
+ return unmatchedErrors.slice(0, 5);
368
+ }
369
+
120
370
  private static watchersSERVICE = new Set<string>();
121
371
  public async watchUnmatchedErrorsSERVICE(): Promise<void> {
122
372
  let caller = SocketFunction.getCaller();
@@ -134,6 +384,67 @@ class ErrorNotificationService {
134
384
  })();
135
385
  }
136
386
  });
387
+
388
+ public static triggerSuppressionUpdate = batchFunction({ delay: 100 }, (ids: string[]) => {
389
+ let uniqueIds = new Set(ids);
390
+ let matches: SuppressionMatch[] = [];
391
+ for (let id of uniqueIds) {
392
+ let match = suppressionMatches.get(id);
393
+ if (match) {
394
+ matches.push(match);
395
+ }
396
+ }
397
+
398
+ for (let nodeId of ErrorNotificationService.watchersSERVICE) {
399
+ void (async () => {
400
+ try {
401
+ await ErrorNotificationDataBase.nodes[nodeId].receiveSuppressionHTTP(matches);
402
+ } catch {
403
+ ErrorNotificationService.watchersSERVICE.delete(nodeId);
404
+ }
405
+ })();
406
+ }
407
+ });
408
+
409
+ public async getSuppressionEntries() {
410
+ return await getSuppressionEntries();
411
+ }
412
+
413
+ public async setSuppressionEntry(entry: SuppressionEntry) {
414
+ let suppressionPromise = suppression.set(entry.id, entry);
415
+ let prevEntry = suppressionCache.findIndex(e => e.id === entry.id);
416
+ if (prevEntry !== -1) {
417
+ suppressionCache[prevEntry] = entry;
418
+ } else {
419
+ suppressionCache.push(entry);
420
+ }
421
+ await onSuppressionsChanged();
422
+ void suppressionPromise.finally(() => {
423
+ void onSuppressionsChanged();
424
+ });
425
+ }
426
+
427
+ public async deleteSuppressionEntry(id: string) {
428
+ let suppressionPromise = suppression.delete(id);
429
+ let prevEntry = suppressionCache.findIndex(e => e.id === id);
430
+ if (prevEntry !== -1) {
431
+ suppressionCache.splice(prevEntry, 1);
432
+ }
433
+ await onSuppressionsChanged();
434
+ void suppressionPromise.finally(() => {
435
+ void onSuppressionsChanged();
436
+ });
437
+ }
438
+
439
+ public async updateSuppressionNotes(id: string, notes: string | undefined) {
440
+ let entry = suppressionCache.find(e => e.id === id);
441
+ if (!entry) {
442
+ throw new Error(`Suppression entry ${id} not found`);
443
+ }
444
+ let updatedEntry = { ...entry, notes, lastUpdatedTime: Date.now() };
445
+ suppressionCache[suppressionCache.findIndex(e => e.id === id)] = updatedEntry;
446
+ void suppression.set(id, updatedEntry);
447
+ }
137
448
  }
138
449
 
139
450
  const ErrorNotificationServiceBase = SocketFunction.register(
@@ -141,7 +452,12 @@ const ErrorNotificationServiceBase = SocketFunction.register(
141
452
  new ErrorNotificationService(),
142
453
  () => ({
143
454
  getData: {},
455
+ getUnmatchedErrorsLimited: {},
144
456
  watchUnmatchedErrorsSERVICE: {},
457
+ getSuppressionEntries: {},
458
+ setSuppressionEntry: {},
459
+ deleteSuppressionEntry: {},
460
+ updateSuppressionNotes: {},
145
461
  }),
146
462
  () => ({
147
463
  hooks: [assertIsManagementUser],
@@ -160,14 +476,26 @@ class ErrorNotificationData {
160
476
  return await ErrorNotificationServiceBase.nodes[controllerNodeId].getData();
161
477
  }
162
478
 
163
- static browserCallbacks = new Set<(datum: LogDatum[]) => void>();
479
+ public async getUnmatchedErrorsLimited() {
480
+ let controllerNodeId = await getControllerNodeId(ErrorNotificationServiceBase);
481
+ if (!controllerNodeId) {
482
+ throw new Error(`Could not find node exposing controller ErrorNotificationServiceBase`);
483
+ }
484
+ return await ErrorNotificationServiceBase.nodes[controllerNodeId].getUnmatchedErrorsLimited();
485
+ }
486
+
487
+ static browserErrorCallbacks = new Set<(datum: LogDatum[]) => void>();
488
+ static browserSuppressionCallbacks = new Set<(matches: SuppressionMatch[]) => void>();
164
489
  public static async watchUnmatchedErrors(config: {
165
- callback: (datum: LogDatum[]) => void;
490
+ errorCallback: (datum: LogDatum[]) => void;
491
+ suppressionCallback: (matches: SuppressionMatch[]) => void;
166
492
  }) {
167
- ErrorNotificationData.browserCallbacks.add(config.callback);
493
+ ErrorNotificationData.browserErrorCallbacks.add(config.errorCallback);
494
+ ErrorNotificationData.browserSuppressionCallbacks.add(config.suppressionCallback);
168
495
  await ErrorNotificationData.ensureWatchingErrorsBrowser();
169
496
  return () => {
170
- ErrorNotificationData.browserCallbacks.delete(config.callback);
497
+ ErrorNotificationData.browserErrorCallbacks.delete(config.errorCallback);
498
+ ErrorNotificationData.browserSuppressionCallbacks.delete(config.suppressionCallback);
171
499
  };
172
500
  }
173
501
 
@@ -176,11 +504,17 @@ class ErrorNotificationData {
176
504
  });
177
505
 
178
506
  public async receiveErrorBrowser(datums: LogDatum[]) {
179
- for (let callback of ErrorNotificationData.browserCallbacks) {
507
+ for (let callback of ErrorNotificationData.browserErrorCallbacks) {
180
508
  callback(datums);
181
509
  }
182
510
  }
183
511
 
512
+ public async receiveSuppressionBrowser(matches: SuppressionMatch[]) {
513
+ for (let callback of ErrorNotificationData.browserSuppressionCallbacks) {
514
+ callback(matches);
515
+ }
516
+ }
517
+
184
518
  private watchersHTTP = new Set<string>();
185
519
  public async watchUnmatchedErrorsHTTP() {
186
520
  let caller = SocketFunction.getCaller();
@@ -212,33 +546,53 @@ class ErrorNotificationData {
212
546
  }
213
547
  }
214
548
 
549
+ public async receiveSuppressionHTTP(matches: SuppressionMatch[]) {
550
+ for (let nodeId of this.watchersHTTP) {
551
+ void (async () => {
552
+ try {
553
+ await ErrorNotificationDataBase.nodes[nodeId].receiveSuppressionBrowser(matches);
554
+ } catch {
555
+ this.watchersHTTP.delete(nodeId);
556
+ }
557
+ })();
558
+ }
559
+ }
560
+
215
561
  public async getSuppressionEntries() {
216
- return await getSuppressionEntries();
562
+ let controllerNodeId = await getControllerNodeId(ErrorNotificationServiceBase);
563
+ if (!controllerNodeId) {
564
+ throw new Error(`Could not find node exposing controller ErrorNotificationServiceBase`);
565
+ }
566
+ return await ErrorNotificationServiceBase.nodes[controllerNodeId].getSuppressionEntries();
217
567
  }
218
568
 
219
569
  public async setSuppressionEntry(entry: SuppressionEntry) {
220
- await suppression.set(entry.id, entry);
221
- let prevEntry = suppressionCache.findIndex(e => e.id === entry.id);
222
- if (prevEntry !== -1) {
223
- suppressionCache[prevEntry] = entry;
224
- } else {
225
- suppressionCache.push(entry);
570
+ let controllerNodeId = await getControllerNodeId(ErrorNotificationServiceBase);
571
+ if (!controllerNodeId) {
572
+ throw new Error(`Could not find node exposing controller ErrorNotificationServiceBase`);
226
573
  }
227
- void updateNow();
574
+ await ErrorNotificationServiceBase.nodes[controllerNodeId].setSuppressionEntry(entry);
228
575
  }
229
576
 
230
577
  public async deleteSuppressionEntry(id: string) {
231
- await suppression.delete(id);
232
- let prevEntry = suppressionCache.findIndex(e => e.id === id);
233
- if (prevEntry !== -1) {
234
- suppressionCache.splice(prevEntry, 1);
578
+ let controllerNodeId = await getControllerNodeId(ErrorNotificationServiceBase);
579
+ if (!controllerNodeId) {
580
+ throw new Error(`Could not find node exposing controller ErrorNotificationServiceBase`);
235
581
  }
236
- void updateNow();
582
+ await ErrorNotificationServiceBase.nodes[controllerNodeId].deleteSuppressionEntry(id);
237
583
  }
238
584
 
239
585
  public async logTestError(errorMessage: string) {
240
586
  console.error(errorMessage);
241
587
  }
588
+
589
+ public async updateSuppressionNotes(id: string, notes: string | undefined) {
590
+ let controllerNodeId = await getControllerNodeId(ErrorNotificationServiceBase);
591
+ if (!controllerNodeId) {
592
+ throw new Error(`Could not find node exposing controller ErrorNotificationServiceBase`);
593
+ }
594
+ await ErrorNotificationServiceBase.nodes[controllerNodeId].updateSuppressionNotes(id, notes);
595
+ }
242
596
  }
243
597
 
244
598
  const ErrorNotificationDataBase = SocketFunction.register(
@@ -246,13 +600,17 @@ const ErrorNotificationDataBase = SocketFunction.register(
246
600
  new ErrorNotificationData(),
247
601
  () => ({
248
602
  getData: {},
603
+ getUnmatchedErrorsLimited: {},
249
604
  receiveErrorBrowser: {},
605
+ receiveSuppressionBrowser: {},
250
606
  receiveErrorHTTP: {},
607
+ receiveSuppressionHTTP: {},
251
608
  watchUnmatchedErrorsHTTP: {},
252
609
  getSuppressionEntries: {},
253
610
  setSuppressionEntry: {},
254
611
  deleteSuppressionEntry: {},
255
612
  logTestError: {},
613
+ updateSuppressionNotes: {},
256
614
  }),
257
615
  () => ({
258
616
  hooks: [assertIsManagementUser],
@@ -261,9 +619,11 @@ const ErrorNotificationDataBase = SocketFunction.register(
261
619
  export const ErrorNotificationsController = getSyncedController(ErrorNotificationDataBase);
262
620
 
263
621
  export function watchUnmatchedErrors(config: {
264
- callback: (datum: LogDatum[]) => void;
622
+ errorCallback: (datum: LogDatum[]) => void;
623
+ suppressionCallback: (matches: SuppressionMatch[]) => void;
265
624
  }) {
266
625
  return ErrorNotificationData.watchUnmatchedErrors({
267
- callback: config.callback,
626
+ errorCallback: config.errorCallback,
627
+ suppressionCallback: config.suppressionCallback,
268
628
  });
269
- }
629
+ }