@veryfront/ext-observability-sentry-node 0.1.1238 → 0.1.1240
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.
|
@@ -14,6 +14,10 @@ export type ApplicationErrorContext = {
|
|
|
14
14
|
spanId?: string;
|
|
15
15
|
/** OpenTelemetry trace correlation identifier. */
|
|
16
16
|
traceId?: string;
|
|
17
|
+
/** Stable failure classification (e.g. "tenant-build") tagged on the event. */
|
|
18
|
+
errorClass?: string;
|
|
19
|
+
/** Severity of the captured event; reporters default to "error" when unset. */
|
|
20
|
+
level?: "error" | "warning";
|
|
17
21
|
/** Sanitized scalar metadata for the failure boundary. */
|
|
18
22
|
attributes?: Record<string, ApplicationErrorAttributeValue>;
|
|
19
23
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"application-error-contract.d.ts","sourceRoot":"","sources":["../src/application-error-contract.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,MAAM,8BAA8B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvE,8EAA8E;AAC9E,MAAM,MAAM,uBAAuB,GAAG;IACpC,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,8BAA8B,CAAC,CAAC;CAC7D,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAAC;IAC9E,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7C,CAAC"}
|
|
1
|
+
{"version":3,"file":"application-error-contract.d.ts","sourceRoot":"","sources":["../src/application-error-contract.ts"],"names":[],"mappings":"AAAA,8DAA8D;AAC9D,MAAM,MAAM,8BAA8B,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;AAEvE,8EAA8E;AAC9E,MAAM,MAAM,uBAAuB,GAAG;IACpC,8DAA8D;IAC9D,QAAQ,EAAE,MAAM,CAAC;IACjB,+CAA+C;IAC/C,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,gEAAgE;IAChE,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sCAAsC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iDAAiD;IACjD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kDAAkD;IAClD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+EAA+E;IAC/E,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+EAA+E;IAC/E,KAAK,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC5B,0DAA0D;IAC1D,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,8BAA8B,CAAC,CAAC;CAC7D,CAAC;AAEF,sEAAsE;AACtE,MAAM,MAAM,wBAAwB,GAAG;IACrC,OAAO,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,uBAAuB,GAAG,MAAM,GAAG,SAAS,CAAC;IAC9E,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC7C,CAAC"}
|
package/esm/policy.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const DEFAULT_FINGERPRINT = "{{ default }}";
|
|
|
3
3
|
export type SentryPolicyScope = {
|
|
4
4
|
setContext(name: string, context: Record<string, unknown>): void;
|
|
5
5
|
setFingerprint(fingerprint: string[]): void;
|
|
6
|
+
setLevel?(level: "error" | "warning"): void;
|
|
6
7
|
setTag(key: string, value: string): void;
|
|
7
8
|
};
|
|
8
9
|
export type SentryPolicySdk = {
|
|
@@ -14,6 +15,7 @@ export type SentryPolicyEvent = {
|
|
|
14
15
|
breadcrumbs?: unknown;
|
|
15
16
|
exception?: {
|
|
16
17
|
values?: Array<{
|
|
18
|
+
type?: string;
|
|
17
19
|
value?: string;
|
|
18
20
|
stacktrace?: {
|
|
19
21
|
frames?: Array<{
|
package/esm/policy.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACxB,MAAM,iCAAiC,CAAC;AAEzC,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"policy.d.ts","sourceRoot":"","sources":["../src/policy.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,uBAAuB,EACxB,MAAM,iCAAiC,CAAC;AAEzC,eAAO,MAAM,mBAAmB,kBAAkB,CAAC;AA2CnD,MAAM,MAAM,iBAAiB,GAAG;IAC9B,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACjE,cAAc,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;IAG5C,QAAQ,CAAC,CAAC,KAAK,EAAE,OAAO,GAAG,SAAS,GAAG,IAAI,CAAC;IAC5C,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CAC1C,CAAC;AAEF,MAAM,MAAM,eAAe,GAAG;IAC5B,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAAC;IACzC,KAAK,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC5C,SAAS,CAAC,QAAQ,EAAE,CAAC,KAAK,EAAE,iBAAiB,KAAK,IAAI,GAAG,IAAI,CAAC;CAC/D,CAAC;AAEF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE;QACV,MAAM,CAAC,EAAE,KAAK,CAAC;YACb,IAAI,CAAC,EAAE,MAAM,CAAC;YACd,KAAK,CAAC,EAAE,MAAM,CAAC;YACf,UAAU,CAAC,EAAE;gBACX,MAAM,CAAC,EAAE,KAAK,CAAC;oBACb,QAAQ,CAAC,EAAE,MAAM,CAAC;oBAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;iBACnB,CAAC,CAAC;aACJ,CAAC;SACH,CAAC,CAAC;KACJ,CAAC;IACF,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,QAAQ,CAAC,EAAE;QACT,OAAO,CAAC,EAAE,MAAM,CAAC;KAClB,CAAC;IACF,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,wBAAgB,uBAAuB,CACrC,GAAG,EAAE,eAAe,EACpB,WAAW,EAAE,MAAM,EACnB,KAAK,EAAE,OAAO,EACd,OAAO,EAAE,uBAAuB,GAC/B,MAAM,GAAG,SAAS,CAWpB;AAED,wBAAsB,qBAAqB,CACzC,GAAG,EAAE,IAAI,CAAC,eAAe,EAAE,OAAO,CAAC,EACnC,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,iBAAiB,EACxB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,uBAAuB,GAC/B,IAAI,CAsBN;AAED,wBAAgB,kBAAkB,CAAC,MAAM,SAAS,iBAAiB,EACjE,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,GAClB,MAAM,GAAG,iBAAiB,CA6B5B;AAwMD,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAOzD;AAED,wBAAgB,kCAAkC,CAChD,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,GACpD,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAU3C;AAOD,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAW5D"}
|
package/esm/policy.js
CHANGED
|
@@ -1,4 +1,33 @@
|
|
|
1
1
|
export const DEFAULT_FINGERPRINT = "{{ default }}";
|
|
2
|
+
const DB_ERROR_FINGERPRINT = "veryfront-db-error";
|
|
3
|
+
// pgbouncer reports these through the wire protocol, so postgres.js surfaces them as a
|
|
4
|
+
// PostgresError whose message ends with "(<code>)".
|
|
5
|
+
const PGBOUNCER_CONNECTION_ERROR_CODES = [
|
|
6
|
+
"server_login_retry",
|
|
7
|
+
"query_wait_timeout",
|
|
8
|
+
];
|
|
9
|
+
// postgres.js `Errors.connection()` builds a plain Error with the message
|
|
10
|
+
// `write <CODE> <host>:<port>` for every client-side connection failure.
|
|
11
|
+
const POSTGRES_JS_CONNECTION_ERROR_PATTERN = /^write (CONNECTION_CLOSED|CONNECTION_DESTROYED|CONNECTION_ENDED|CONNECT_TIMEOUT)(?:\s|$)/;
|
|
12
|
+
const FAILED_QUERY_PREFIX = "Failed query: ";
|
|
13
|
+
const FAILED_QUERY_PARAMS_DELIMITER = "\nparams:";
|
|
14
|
+
const FAILED_QUERY_HEAD_MAX_LENGTH = 200;
|
|
15
|
+
const SQL_DOLLAR_QUOTE_START_PATTERN = /^\$(?:[_\p{ID_Start}][_\p{ID_Continue}]*)?\$/u;
|
|
16
|
+
// PostgreSQL's lexer accepts any high byte in a dollar-quote tag and imposes no length limit, so
|
|
17
|
+
// tags outside ECMAScript `ID_Start` (an emoji tag, for example) are still treated as literals and
|
|
18
|
+
// their contents cannot reach the title. The tag must nonetheless look like a tag: free of
|
|
19
|
+
// PostgreSQL lexer whitespace, quotes and further dollar signs. PostgreSQL lexer whitespace is
|
|
20
|
+
// ASCII-only, so a high byte that ECMAScript classifies as whitespace remains a legal tag byte.
|
|
21
|
+
// Without the shape check, a `$` inside an ordinary identifier such as `col$a` swallows the rest
|
|
22
|
+
// of the query.
|
|
23
|
+
const SQL_UNRECOGNIZED_DOLLAR_QUOTE_START_PATTERN = /^\$[^ \t\n\r\f\v'"$]+\$/u;
|
|
24
|
+
// A dollar quote cannot open straight after an identifier character: PostgreSQL prefers the longer
|
|
25
|
+
// identifier match, so `col$tag$inner$tag$` is one identifier rather than `col` followed by a
|
|
26
|
+
// literal. `$` is deliberately absent from this class so that adjacent literals such as
|
|
27
|
+
// `$$a$$$$b$$` still parse as two dollar-quoted strings.
|
|
28
|
+
const SQL_IDENTIFIER_BEFORE_DOLLAR_PATTERN = /[A-Za-z0-9_]|\P{ASCII}/u;
|
|
29
|
+
const SQL_NUMERIC_LITERAL_PATTERN = /^(?:0[xX]_?[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|0[oO]_?[0-7](?:_?[0-7])*|0[bB]_?[01](?:_?[01])*|(?:\d(?:_?\d)*(?:\.(?:\d(?:_?\d)*)?)?|\.\d(?:_?\d)*)(?:[eE][+-]?\d(?:_?\d)*)?)/;
|
|
30
|
+
const SQL_IDENTIFIER_CHAR_PATTERN = /[A-Za-z0-9_$]/;
|
|
2
31
|
const SENTRY_TOKEN_PATTERN = /\bsntrys_[A-Za-z0-9_+/=-]+\b/g;
|
|
3
32
|
const BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi;
|
|
4
33
|
const JWT_PATTERN = /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g;
|
|
@@ -32,6 +61,10 @@ export function applySentryScopePolicy(scope, serviceName, context) {
|
|
|
32
61
|
if (context.processRole)
|
|
33
62
|
scope.setTag("process_role", context.processRole);
|
|
34
63
|
scope.setTag("veryfront.boundary", context.boundary);
|
|
64
|
+
if (context.errorClass)
|
|
65
|
+
scope.setTag("veryfront.error_class", context.errorClass);
|
|
66
|
+
if (context.level)
|
|
67
|
+
scope.setLevel?.(context.level);
|
|
35
68
|
if (context.method)
|
|
36
69
|
scope.setTag("http.request.method", context.method);
|
|
37
70
|
if (context.requestId)
|
|
@@ -52,7 +85,10 @@ export function prepareSentryEvent(event, serviceName) {
|
|
|
52
85
|
...event.tags,
|
|
53
86
|
"service.name": serviceName,
|
|
54
87
|
};
|
|
55
|
-
|
|
88
|
+
const dbConnectionErrorCode = detectDbConnectionErrorCode(event);
|
|
89
|
+
event.fingerprint = dbConnectionErrorCode
|
|
90
|
+
? [serviceName, DB_ERROR_FINGERPRINT, dbConnectionErrorCode]
|
|
91
|
+
: [serviceName, ...(event.fingerprint ?? [DEFAULT_FINGERPRINT])];
|
|
56
92
|
delete event.breadcrumbs;
|
|
57
93
|
delete event.request;
|
|
58
94
|
delete event.user;
|
|
@@ -62,8 +98,9 @@ export function prepareSentryEvent(event, serviceName) {
|
|
|
62
98
|
event.logentry.message = redactSensitiveText(event.logentry.message);
|
|
63
99
|
}
|
|
64
100
|
for (const value of event.exception?.values ?? []) {
|
|
65
|
-
if (value.value)
|
|
66
|
-
value.value = redactSensitiveText(value.value);
|
|
101
|
+
if (value.value) {
|
|
102
|
+
value.value = normalizeFailedQueryValue(redactSensitiveText(value.value));
|
|
103
|
+
}
|
|
67
104
|
for (const frame of value.stacktrace?.frames ?? []) {
|
|
68
105
|
if (frame.filename)
|
|
69
106
|
frame.filename = sanitizeStackFramePath(frame.filename);
|
|
@@ -73,6 +110,180 @@ export function prepareSentryEvent(event, serviceName) {
|
|
|
73
110
|
}
|
|
74
111
|
return event;
|
|
75
112
|
}
|
|
113
|
+
function detectDbConnectionErrorCode(event) {
|
|
114
|
+
for (const exceptionValue of event.exception?.values ?? []) {
|
|
115
|
+
const message = exceptionValue.value ?? "";
|
|
116
|
+
if (exceptionValue.type === "PostgresError") {
|
|
117
|
+
const trimmedMessage = message.trimEnd();
|
|
118
|
+
const code = PGBOUNCER_CONNECTION_ERROR_CODES.find((candidate) => trimmedMessage.endsWith(`(${candidate})`));
|
|
119
|
+
if (code)
|
|
120
|
+
return code;
|
|
121
|
+
}
|
|
122
|
+
if (exceptionValue.type === "Error") {
|
|
123
|
+
const code = POSTGRES_JS_CONNECTION_ERROR_PATTERN.exec(message)?.[1];
|
|
124
|
+
if (code)
|
|
125
|
+
return code;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return undefined;
|
|
129
|
+
}
|
|
130
|
+
function normalizeFailedQueryValue(value) {
|
|
131
|
+
if (!value.startsWith(FAILED_QUERY_PREFIX.trimEnd()))
|
|
132
|
+
return value;
|
|
133
|
+
const remainder = value.slice(FAILED_QUERY_PREFIX.trimEnd().length);
|
|
134
|
+
const paramsDelimiterIndex = remainder.indexOf(FAILED_QUERY_PARAMS_DELIMITER);
|
|
135
|
+
const query = paramsDelimiterIndex === -1 ? remainder : remainder.slice(0, paramsDelimiterIndex);
|
|
136
|
+
const titleQuery = redactSqlLiteralsForTitle(query)
|
|
137
|
+
.replace(/\s+/g, " ")
|
|
138
|
+
.trim();
|
|
139
|
+
const head = titleQuery
|
|
140
|
+
.slice(0, FAILED_QUERY_HEAD_MAX_LENGTH)
|
|
141
|
+
.trimEnd();
|
|
142
|
+
const truncated = titleQuery.length > FAILED_QUERY_HEAD_MAX_LENGTH;
|
|
143
|
+
return `${FAILED_QUERY_PREFIX}${head}${truncated ? "…" : ""}`;
|
|
144
|
+
}
|
|
145
|
+
function redactSqlLiteralsForTitle(query) {
|
|
146
|
+
let redacted = "";
|
|
147
|
+
let index = 0;
|
|
148
|
+
while (index < query.length) {
|
|
149
|
+
const character = query.charAt(index);
|
|
150
|
+
if ((character === "E" || character === "e") &&
|
|
151
|
+
query[index + 1] === "'" &&
|
|
152
|
+
isSqlStringPrefixBoundary(query, index)) {
|
|
153
|
+
index = findQuotedSqlTokenEnd(query, index + 1, "'", { allowBackslashEscapes: true }).end;
|
|
154
|
+
redacted += "?";
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if ((character === "U" || character === "u") &&
|
|
158
|
+
query[index + 1] === "&" &&
|
|
159
|
+
query[index + 2] === "'" &&
|
|
160
|
+
isSqlStringPrefixBoundary(query, index)) {
|
|
161
|
+
index = findQuotedSqlTokenEnd(query, index + 2, "'").end;
|
|
162
|
+
redacted += "?";
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (character === '"') {
|
|
166
|
+
// Deliberate non-redaction: a double-quoted token is a schema identifier, not data, and
|
|
167
|
+
// keeping it verbatim is what makes titles group by query shape. An unterminated identifier
|
|
168
|
+
// is malformed SQL, so redact it rather than emit the rest of the query untouched.
|
|
169
|
+
const { end, terminated } = findQuotedSqlTokenEnd(query, index, '"');
|
|
170
|
+
redacted += terminated ? query.slice(index, end) : "?";
|
|
171
|
+
index = end;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (character === "'") {
|
|
175
|
+
// The event does not carry the server's `standard_conforming_strings` setting, and neither
|
|
176
|
+
// reading of a backslash is safe on its own: escaping exposes the next literal when the
|
|
177
|
+
// setting is on, not escaping exposes this one when it is off. Bail out on the ambiguous
|
|
178
|
+
// case instead of picking a side.
|
|
179
|
+
index = findQuotedSqlTokenEnd(query, index, "'", { bailOnAmbiguousBackslash: true }).end;
|
|
180
|
+
redacted += "?";
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
if (character === "$" &&
|
|
184
|
+
!SQL_IDENTIFIER_BEFORE_DOLLAR_PATTERN.test(query[index - 1] ?? "")) {
|
|
185
|
+
const dollarQuotedEnd = findDollarQuotedSqlTokenEnd(query, index);
|
|
186
|
+
if (dollarQuotedEnd !== undefined) {
|
|
187
|
+
index = dollarQuotedEnd;
|
|
188
|
+
redacted += "?";
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (character === "-" && query[index + 1] === "-") {
|
|
193
|
+
index += 2;
|
|
194
|
+
while (index < query.length && query[index] !== "\n" && query[index] !== "\r") {
|
|
195
|
+
index += 1;
|
|
196
|
+
}
|
|
197
|
+
redacted += "?";
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (character === "/" && query[index + 1] === "*") {
|
|
201
|
+
let depth = 1;
|
|
202
|
+
index += 2;
|
|
203
|
+
while (index < query.length && depth > 0) {
|
|
204
|
+
if (query[index] === "/" && query[index + 1] === "*") {
|
|
205
|
+
depth += 1;
|
|
206
|
+
index += 2;
|
|
207
|
+
}
|
|
208
|
+
else if (query[index] === "*" && query[index + 1] === "/") {
|
|
209
|
+
depth -= 1;
|
|
210
|
+
index += 2;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
index += 1;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
redacted += "?";
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const canStartNumber = /[0-9]/.test(character) ||
|
|
220
|
+
(character === "." && /[0-9]/.test(query[index + 1] ?? ""));
|
|
221
|
+
const previousCharacter = query[index - 1] ?? "";
|
|
222
|
+
if (canStartNumber && !SQL_IDENTIFIER_CHAR_PATTERN.test(previousCharacter)) {
|
|
223
|
+
const literal = query.slice(index).match(SQL_NUMERIC_LITERAL_PATTERN)?.[0];
|
|
224
|
+
const nextCharacter = literal ? query[index + literal.length] ?? "" : "";
|
|
225
|
+
if (literal && !SQL_IDENTIFIER_CHAR_PATTERN.test(nextCharacter)) {
|
|
226
|
+
redacted += "?";
|
|
227
|
+
index += literal.length;
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
redacted += character;
|
|
232
|
+
index += 1;
|
|
233
|
+
}
|
|
234
|
+
return redacted;
|
|
235
|
+
}
|
|
236
|
+
function findDollarQuotedSqlTokenEnd(query, start) {
|
|
237
|
+
const recognizedDelimiter = query.slice(start).match(SQL_DOLLAR_QUOTE_START_PATTERN)?.[0];
|
|
238
|
+
if (recognizedDelimiter) {
|
|
239
|
+
const contentStart = start + recognizedDelimiter.length;
|
|
240
|
+
const closingDelimiter = query.indexOf(recognizedDelimiter, contentStart);
|
|
241
|
+
return closingDelimiter === -1 ? query.length : closingDelimiter + recognizedDelimiter.length;
|
|
242
|
+
}
|
|
243
|
+
if (/[0-9]/.test(query[start + 1] ?? ""))
|
|
244
|
+
return undefined;
|
|
245
|
+
const delimiter = query.slice(start).match(SQL_UNRECOGNIZED_DOLLAR_QUOTE_START_PATTERN)?.[0];
|
|
246
|
+
if (!delimiter)
|
|
247
|
+
return undefined;
|
|
248
|
+
const contentStart = start + delimiter.length;
|
|
249
|
+
const closingDelimiter = query.indexOf(delimiter, contentStart);
|
|
250
|
+
return closingDelimiter === -1 ? query.length : closingDelimiter + delimiter.length;
|
|
251
|
+
}
|
|
252
|
+
function isSqlStringPrefixBoundary(query, start) {
|
|
253
|
+
return !SQL_IDENTIFIER_CHAR_PATTERN.test(query[start - 1] ?? "");
|
|
254
|
+
}
|
|
255
|
+
function findQuotedSqlTokenEnd(query, start, quote, options = {}) {
|
|
256
|
+
let index = start + 1;
|
|
257
|
+
while (index < query.length) {
|
|
258
|
+
if (options.allowBackslashEscapes && query[index] === "\\" && index + 1 < query.length) {
|
|
259
|
+
index += 2;
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (query[index] !== quote) {
|
|
263
|
+
index += 1;
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (query[index + 1] === quote) {
|
|
267
|
+
index += 2;
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
// An ordinary literal means different things on different servers: under
|
|
271
|
+
// `standard_conforming_strings = off` a backslash escapes the quote that follows it, so an odd
|
|
272
|
+
// run of backslashes here leaves the literal's extent genuinely ambiguous — the same bytes are
|
|
273
|
+
// one literal on one server and two on another. Guessing either way emits the other reading's
|
|
274
|
+
// contents verbatim, so stop and let the caller redact the remainder.
|
|
275
|
+
if (options.bailOnAmbiguousBackslash && hasOddTrailingBackslashRun(query, index))
|
|
276
|
+
break;
|
|
277
|
+
return { end: index + 1, terminated: true };
|
|
278
|
+
}
|
|
279
|
+
return { end: query.length, terminated: false };
|
|
280
|
+
}
|
|
281
|
+
function hasOddTrailingBackslashRun(query, index) {
|
|
282
|
+
let backslashes = 0;
|
|
283
|
+
while (index - backslashes - 1 >= 0 && query[index - backslashes - 1] === "\\")
|
|
284
|
+
backslashes += 1;
|
|
285
|
+
return backslashes % 2 === 1;
|
|
286
|
+
}
|
|
76
287
|
export function redactSensitiveText(value) {
|
|
77
288
|
return value
|
|
78
289
|
.replace(SENTRY_TOKEN_PATTERN, "[REDACTED]")
|