Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
ce8f34b
fix(observability): group db outage errors and fix blank Failed query…
kojiwakayama Aug 14, 2026
f6edf3a
Prevent Sentry title truncation from exposing token fragments
kojiwakayama Aug 14, 2026
b4cf8f4
Group client-side postgres connection closures
kojiwakayama Aug 14, 2026
cdcdcad
Keep database incidents precise without exposing query inputs
kojiwakayama Aug 14, 2026
cd43d81
Prevent SQL literals from becoming Sentry issue titles
kojiwakayama Aug 14, 2026
2d0a556
Prevent SQL comments from shaping Sentry titles
kojiwakayama Aug 14, 2026
ac0339b
Keep ordinary database errors and radix values out of outage titles
kojiwakayama Aug 14, 2026
6f2844c
Keep separated PostgreSQL numbers out of Sentry titles
kojiwakayama Aug 14, 2026
f320822
Redact Unicode dollar-quoted SQL literals
kojiwakayama Aug 14, 2026
f557ab5
Cover Unicode identifier planes in Sentry redaction
kojiwakayama Aug 14, 2026
99719b2
Redact PostgreSQL dollar-quote literals beyond ECMAScript tags
kojiwakayama Aug 14, 2026
62b01e9
Redact radix literals with prefix separators
kojiwakayama Aug 14, 2026
4a46e77
Keep Sentry failed-query titles redacted after normalization
kojiwakayama Aug 15, 2026
541375d
fix(observability): normalize single-line failed-query values and wid…
kwakayama Aug 15, 2026
9f4e00c
fix(observability): drop the dollar-tag length cap and respect identi…
kwakayama Aug 15, 2026
4cfabad
fix(observability): redact compatibility string escapes
kojiwakayama Aug 15, 2026
97417ef
fix(observability): fail safe when a backslash makes a string literal…
kwakayama Aug 15, 2026
bae1b95
fix(observability): redact high-byte dollar tags
kojiwakayama Aug 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 116 additions & 0 deletions extensions/ext-observability-sentry/src/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,122 @@ it("policy redacts application error attribute keys and credential-shaped values
);
});

it("policy groups pgbouncer connection PostgresErrors by stable db-error fingerprint", () => {
const event = prepareSentryEvent(
{
exception: {
values: [{
type: "PostgresError",
value: "pgbouncer cannot connect to server (server_login_retry)",
}],
},
fingerprint: ["POST /api/graphql"],
},
"veryfront-api",
);

assertEquals(event.fingerprint, ["veryfront-db-error", "server_login_retry"]);
});

it("policy groups each pgbouncer connection code into its own db-error issue", () => {
for (
const code of ["server_login_retry", "query_wait_timeout", "CONNECTION_CLOSED"]
) {
const event = prepareSentryEvent(
{
exception: {
values: [{ type: "PostgresError", value: `write ${code} db.example.test:6432` }],
},
},
"veryfront-api",
);

assertEquals(event.fingerprint, ["veryfront-db-error", code]);
}
});

it("policy keeps the default fingerprint for non-connection PostgresErrors and other errors", () => {
const postgresEvent = prepareSentryEvent(
{
exception: {
values: [{
type: "PostgresError",
value: "duplicate key value violates unique constraint",
}],
},
},
"veryfront-api",
);
assertEquals(postgresEvent.fingerprint, ["veryfront-api", "{{ default }}"]);

const unrelatedEvent = prepareSentryEvent(
{
exception: {
values: [{ type: "TypeError", value: "server_login_retry is not a function" }],
},
},
"veryfront-api",
);
assertEquals(unrelatedEvent.fingerprint, ["veryfront-api", "{{ default }}"]);
});

it("policy collapses leading sql whitespace in Failed query titles", () => {
const event = prepareSentryEvent(
{
exception: {
values: [{
type: "DrizzleQueryError",
value: 'Failed query: \n select "id", "email"\n from "users"',
}],
},
},
"veryfront-studio",
);

assertEquals(
event.exception?.values?.[0]?.value,
'Failed query: select "id", "email" from "users"',
);
});

it("policy caps collapsed Failed query values and leaves other values untouched", () => {
const longSql = `\n select ${"c, ".repeat(200)}from t`;
const event = prepareSentryEvent(
{
exception: {
values: [
{ type: "DrizzleQueryError", value: `Failed query: ${longSql}` },
{ type: "Error", value: " leading spaces stay untouched" },
],
},
},
"veryfront-studio",
);

const collapsed = event.exception?.values?.[0]?.value ?? "";
assertEquals(collapsed.startsWith("Failed query: select c, c,"), true);
assertEquals(collapsed.length <= "Failed query: ".length + 201, true);
assertEquals(event.exception?.values?.[1]?.value, " leading spaces stay untouched");
});

it("policy redacts full Failed query values before truncating the Sentry title", () => {
const jwt = `${"a".repeat(16)}.${"b".repeat(16)}.${"c".repeat(180)}`;
const queryHead = `\n select '${"x".repeat(175)}${jwt}' from tokens`;
const event = prepareSentryEvent(
{
exception: {
values: [{ type: "DrizzleQueryError", value: `Failed query: ${queryHead}` }],
},
},
"veryfront-studio",
);

const collapsed = event.exception?.values?.[0]?.value ?? "";
assertStringIncludes(collapsed, "[REDACTED]");
assertEquals(collapsed.includes("a".repeat(12)), false);
assertEquals(collapsed.includes("c".repeat(20)), false);
});

it("policy redacts credentials from custom-host DSNs", () => {
const event = prepareSentryEvent(
{
Expand Down
41 changes: 39 additions & 2 deletions extensions/ext-observability-sentry/src/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ import type {

export const DEFAULT_FINGERPRINT = "{{ default }}";

const DB_ERROR_FINGERPRINT = "veryfront-db-error";
const DB_CONNECTION_ERROR_CODES = [
"server_login_retry",
"query_wait_timeout",
"CONNECTION_CLOSED",
] as const;
const FAILED_QUERY_PREFIX = "Failed query: ";
const FAILED_QUERY_HEAD_MAX_LENGTH = 200;

const SENTRY_TOKEN_PATTERN = /\bsntrys_[A-Za-z0-9_+/=-]+\b/g;
const BEARER_TOKEN_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+\b/gi;
const JWT_PATTERN = /\b[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g;
Expand All @@ -29,6 +38,7 @@ export type SentryPolicyEvent = {
breadcrumbs?: unknown;
exception?: {
values?: Array<{
type?: string;
value?: string;
stacktrace?: {
frames?: Array<{
Expand Down Expand Up @@ -112,7 +122,10 @@ export function prepareSentryEvent<TEvent extends SentryPolicyEvent>(
...event.tags,
"service.name": serviceName,
};
event.fingerprint = [serviceName, ...(event.fingerprint ?? [DEFAULT_FINGERPRINT])];
const dbConnectionErrorCode = detectDbConnectionErrorCode(event);
event.fingerprint = dbConnectionErrorCode
? [DB_ERROR_FINGERPRINT, dbConnectionErrorCode]
: [serviceName, ...(event.fingerprint ?? [DEFAULT_FINGERPRINT])];

delete event.breadcrumbs;
delete event.request;
Expand All @@ -123,7 +136,9 @@ export function prepareSentryEvent<TEvent extends SentryPolicyEvent>(
event.logentry.message = redactSensitiveText(event.logentry.message);
}
for (const value of event.exception?.values ?? []) {
if (value.value) value.value = redactSensitiveText(value.value);
if (value.value) {
value.value = normalizeFailedQueryValue(redactSensitiveText(value.value));
}
for (const frame of value.stacktrace?.frames ?? []) {
if (frame.filename) frame.filename = sanitizeStackFramePath(frame.filename);
if (frame.abs_path) frame.abs_path = sanitizeStackFramePath(frame.abs_path);
Expand All @@ -133,6 +148,28 @@ export function prepareSentryEvent<TEvent extends SentryPolicyEvent>(
return event;
}

function detectDbConnectionErrorCode(event: SentryPolicyEvent): string | undefined {
for (const exceptionValue of event.exception?.values ?? []) {
if (exceptionValue.type !== "PostgresError") continue;
Comment thread
kojiwakayama marked this conversation as resolved.
Outdated
const message = exceptionValue.value ?? "";
const code = DB_CONNECTION_ERROR_CODES.find((candidate) => message.includes(candidate));
if (code) return code;
}
return undefined;
}

export function normalizeFailedQueryValue(value: string): string {
if (!value.startsWith(FAILED_QUERY_PREFIX.trimEnd())) return value;
const remainder = value.slice(FAILED_QUERY_PREFIX.trimEnd().length);
if (!/^\s*\n/.test(remainder)) return value;
const head = remainder
.slice(0, FAILED_QUERY_HEAD_MAX_LENGTH)
.replace(/\s+/g, " ")
.trim();
Comment thread
kojiwakayama marked this conversation as resolved.
Outdated
const truncated = remainder.trimEnd().length > FAILED_QUERY_HEAD_MAX_LENGTH;
return `${FAILED_QUERY_PREFIX}${head}${truncated ? "…" : ""}`;
}

export function redactSensitiveText(value: string): string {
return value
.replace(SENTRY_TOKEN_PATTERN, "[REDACTED]")
Expand Down