querysub 0.689.0 → 0.690.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "querysub",
3
- "version": "0.689.0",
3
+ "version": "0.690.0",
4
4
  "main": "index.js",
5
5
  "license": "MIT",
6
6
  "note1": "note on node-forge fork, see https://github.com/digitalbazaar/forge/issues/744 for details",
@@ -109,6 +109,12 @@ function getSimplerStructure(structure: MatchStructure): Buffer[][] {
109
109
  }
110
110
  export const createMatchesPatternCached = cacheLimited(1000, (pattern: string) => createMatchesPattern(Buffer.from(pattern), false));
111
111
 
112
+ // Suppression patterns are usually copied out of a UI that renders multi-line errors on a single line, so the copied text has plain spaces where the real error has line breaks and indentation — those patterns can never match unless whitespace is collapsed on both sides. The matched subject is JSON-stringified, where whitespace appears as escape sequences rather than raw bytes, so both forms collapse to one space. This must NOT be applied to log-index searches: the on-disk index units are built from the raw bytes, so a normalized query would miss them.
113
+ const WHITESPACE_OR_JSON_ESCAPED_WHITESPACE_RUN = /(?:\s|\\n|\\r|\\t)+/g;
114
+ export function normalizeMatchWhitespace(text: string): string {
115
+ return text.replace(WHITESPACE_OR_JSON_ESCAPED_WHITESPACE_RUN, " ");
116
+ }
117
+
112
118
  // Each WILD_CARD_BYTE in pattern acts as a multi-byte wildcard: the segments on either
113
119
  // side must appear in order somewhere within buffer.
114
120
  // Returns a function that matches buffers against the pre-processed pattern.
@@ -10,7 +10,7 @@ import { isPublic } from "../../../config";
10
10
  import { MachineThreadInfo } from "../../MachineThreadInfo";
11
11
  import { ErrorWarning } from "./ErrorWarning";
12
12
  import { getErrorNotificationsManager } from "./errorWatcher";
13
- import { createMatchesPattern } from "../IndexedLogs/bufferSearchFindMatcher";
13
+ import { createMatchesPattern, normalizeMatchWhitespace } from "../IndexedLogs/bufferSearchFindMatcher";
14
14
  import { cacheLimited } from "socket-function/src/caching";
15
15
  import { managementPageURL, showingManagementURL } from "../../managementPages";
16
16
  import { searchTextURL, readProductionLogsURL } from "../IndexedLogs/LogViewerParams";
@@ -58,9 +58,13 @@ function TicketButton(props: {
58
58
  qreact.inline(TicketButton);
59
59
 
60
60
  export let getMatcher = cacheLimited(100, (pattern: string) =>
61
- createMatchesPattern(Buffer.from(pattern), false)
61
+ createMatchesPattern(Buffer.from(normalizeMatchWhitespace(pattern)), false)
62
62
  );
63
63
 
64
+ export function getMatchBuffer(datum: LogDatum): Buffer {
65
+ return Buffer.from(normalizeMatchWhitespace(JSON.stringify(datum)));
66
+ }
67
+
64
68
  export class LogDatumRenderer extends qreact.Component<{
65
69
  datum: LogDatum;
66
70
  inlineMode?: boolean;
@@ -81,7 +85,7 @@ export class LogDatumRenderer extends qreact.Component<{
81
85
  if (!this.manager.state.isLoading && !this.props.hideSuppression) {
82
86
  for (let suppression of this.manager.state.suppressionEntries) {
83
87
  let matcher = getMatcher(suppression.pattern);
84
- let errorBuffer = Buffer.from(JSON.stringify(datum));
88
+ let errorBuffer = getMatchBuffer(datum);
85
89
  if (matcher(errorBuffer)) {
86
90
  matchingSuppressions.push(suppression);
87
91
  }
@@ -461,7 +465,7 @@ class SuppressionItem extends qreact.Component<{
461
465
  if (example) return example;
462
466
  // No example recorded for this suppression — find any live error matching the pattern, so the ticket stores a real, full log entry instead of a synthetic stub.
463
467
  let matcher = getMatcher(suppression.pattern);
464
- let fallback = this.manager.state.unmatchedErrors.find(e => matcher(Buffer.from(JSON.stringify(e))));
468
+ let fallback = this.manager.state.unmatchedErrors.find(e => matcher(getMatchBuffer(e)));
465
469
  return fallback || {
466
470
  time: Date.now(),
467
471
  __LOG_TYPE: "error",
@@ -4,7 +4,7 @@ import { css } from "typesafecss";
4
4
  import { SocketFunction } from "socket-function/SocketFunction";
5
5
  import { ATag } from "../../../library-components/ATag";
6
6
  import { managementPageURL, showingManagementURL } from "../../managementPages";
7
- import { getMatcher, LogDatumRenderer } from "./ErrorNotificationPage";
7
+ import { getMatcher, getMatchBuffer, LogDatumRenderer } from "./ErrorNotificationPage";
8
8
  import { getErrorNotificationsManager } from "./errorWatcher";
9
9
  import { LogDatum } from "../diskLogger";
10
10
  import { TicketsController } from "../errorTickets/tickets";
@@ -58,7 +58,7 @@ export class ErrorWarning extends qreact.Component {
58
58
  openTicketPatterns.push(ticket.suppressionPattern);
59
59
  }
60
60
  const isCoveredByOpenTicket = (datum: LogDatum) => {
61
- let errorBuffer = Buffer.from(JSON.stringify(datum));
61
+ let errorBuffer = getMatchBuffer(datum);
62
62
  return openTicketPatterns.some(pattern => getMatcher(pattern)(errorBuffer));
63
63
  };
64
64
 
@@ -3,7 +3,7 @@ import { archiveJSONT } from "../../../-a-archives/archivesJSONT";
3
3
  import { Querysub } from "../../../4-querysub/Querysub";
4
4
  import { getDomain, isPublic } from "../../../config";
5
5
  import { MachineInfo } from "../../../deployManager/machineSchema";
6
- import { createMatchesPattern } from "../IndexedLogs/bufferSearchFindMatcher";
6
+ import { createMatchesPattern, normalizeMatchWhitespace } from "../IndexedLogs/bufferSearchFindMatcher";
7
7
  import { LogDatum, getErrorLogs } from "../diskLogger";
8
8
  import { watchAllValues } from "./logWatcher";
9
9
  import { batchFunction, runInSerial, runInfinitePollCallAtStart } from "socket-function/src/batching";
@@ -55,7 +55,7 @@ export async function getSuppressionEntries(): Promise<SuppressionEntry[]> {
55
55
  return await suppression.values();
56
56
  }
57
57
 
58
- let getMatcher = cacheLimited(10000, (text: string) => createMatchesPattern(Buffer.from(text), false));
58
+ let getMatcher = cacheLimited(10000, (text: string) => createMatchesPattern(Buffer.from(normalizeMatchWhitespace(text)), false));
59
59
 
60
60
  // The watch loop checks every incoming error against the suppression list. Reading the suppression archive once per error produced thousands of storage listings per minute, so the hot path is served from a cached copy: the first pass tolerates 15 minute staleness, and only errors that pass unsuppressed force a fresher (30 second) re-read.
61
61
  const SUPPRESSION_CACHE_TTL = timeInMinute * 15;
@@ -89,7 +89,7 @@ function invalidateSuppressionEntriesCaches() {
89
89
 
90
90
  // Side-effect-free version of applySuppression's "suppressed" verdict, for deciding which entry set the watch loop should apply against.
91
91
  function isSuppressedByAny(error: LogDatum, suppressionEntries: SuppressionEntry[]): boolean {
92
- let errorBuffer = Buffer.from(JSON.stringify(error));
92
+ let errorBuffer = Buffer.from(normalizeMatchWhitespace(JSON.stringify(error)));
93
93
  for (let suppressionEntry of suppressionEntries) {
94
94
  let isMatch = getMatcher(suppressionEntry.pattern);
95
95
  if (isMatch(errorBuffer) && error.time <= suppressionEntry.timeout) {
@@ -125,7 +125,7 @@ export function getChunkEndTime(chunk: number) {
125
125
  type SuppressionStatus = "suppressed" | "matched-timeout-passed" | "no-match";
126
126
 
127
127
  function applySuppression(error: LogDatum, suppressionEntry: SuppressionEntry): SuppressionStatus {
128
- let errorBuffer = Buffer.from(JSON.stringify(error));
128
+ let errorBuffer = Buffer.from(normalizeMatchWhitespace(JSON.stringify(error)));
129
129
  let isMatch = getMatcher(suppressionEntry.pattern);
130
130
  if (!isMatch(errorBuffer)) return "no-match";
131
131