From 17340ac1f58acc8a5299c0f4389517a0dd7246d4 Mon Sep 17 00:00:00 2001 From: Maximilian Kaske <56969857+mxkaske@users.noreply.github.com> Date: Wed, 15 Jul 2026 21:48:33 +0200 Subject: [PATCH] Stop Sentry from capturing tRPC loggerLink noise as errors (#2387) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Stop Sentry from capturing tRPC loggerLink noise as errors captureConsoleIntegration (levels: ["error"]) was scraping the raw console.error call that trpc's loggerLink makes for failed queries, producing spurious Sentry issues whose title/message was just the CSS-styled printf format string (e.g. "%c << query #1 %cstatusPage.getMonitor%c %O") instead of the actual error (OPENSTATUS-FRONTEND-ET, OPENSTATUS-FRONTEND-E5). Replace the default logger with one that reports the real error to Sentry directly via captureException and only logs to the console (never console.error) in development, across dashboard, status-page, and web's tRPC client/server setups. * Extract tRPC Sentry logger into shared helper, log errors in dev console Addresses PR review: the loggerLink callback was duplicated across all 6 tRPC client/server setups. Extract it into a per-app `sentryLoggerLink()` factory in each `shared.ts` (matching the repo's existing per-app tRPC shared-module convention) so the error-reporting logic lives in one place per app. Also fixes a dev-mode gap: failed queries were captured to Sentry and then early-returned before the console.log fallback, hiding them from the local console. They now console.warn the path + error in development before returning. * Don't attach raw tRPC input to Sentry error reports Addresses PR review (P1): capturing opts.input persisted request payloads to Sentry on every failed query — including page passwords, subscriber tokens, and email addresses. Attach only the operation path, which is enough to identify the failing procedure without leaking secrets. --------- Co-authored-by: Claude --- apps/dashboard/src/lib/trpc/client.tsx | 10 +++----- apps/dashboard/src/lib/trpc/server.tsx | 10 +++----- apps/dashboard/src/lib/trpc/shared.ts | 32 +++++++++++++++++++++++- apps/status-page/src/lib/trpc/client.tsx | 10 +++----- apps/status-page/src/lib/trpc/server.tsx | 10 +++----- apps/status-page/src/lib/trpc/shared.ts | 32 +++++++++++++++++++++++- apps/web/src/trpc/client.ts | 10 +++----- apps/web/src/trpc/server.ts | 10 +++----- apps/web/src/trpc/shared.ts | 32 +++++++++++++++++++++++- 9 files changed, 111 insertions(+), 45 deletions(-) diff --git a/apps/dashboard/src/lib/trpc/client.tsx b/apps/dashboard/src/lib/trpc/client.tsx index 296c0c42..6a98c272 100644 --- a/apps/dashboard/src/lib/trpc/client.tsx +++ b/apps/dashboard/src/lib/trpc/client.tsx @@ -2,11 +2,11 @@ import type { AppRouter } from "@openstatus/api"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createTRPCClient, loggerLink } from "@trpc/client"; +import { createTRPCClient } from "@trpc/client"; import { createTRPCContext } from "@trpc/tanstack-react-query"; import { useState } from "react"; -import { endingLink } from "@/lib/trpc/shared"; +import { endingLink, sentryLoggerLink } from "@/lib/trpc/shared"; export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext(); @@ -41,11 +41,7 @@ export function TRPCReactProvider({ children }: { children: React.ReactNode }) { const [trpcClient] = useState(() => createTRPCClient({ links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), + sentryLoggerLink(), endingLink({ headers: { "x-trpc-source": "client", diff --git a/apps/dashboard/src/lib/trpc/server.tsx b/apps/dashboard/src/lib/trpc/server.tsx index 689df696..2356eb07 100644 --- a/apps/dashboard/src/lib/trpc/server.tsx +++ b/apps/dashboard/src/lib/trpc/server.tsx @@ -2,7 +2,7 @@ import "server-only"; import type { AppRouter } from "@openstatus/api"; import { HydrationBoundary } from "@tanstack/react-query"; import { dehydrate } from "@tanstack/react-query"; -import { TRPCClientError, createTRPCClient, loggerLink } from "@trpc/client"; +import { TRPCClientError, createTRPCClient } from "@trpc/client"; import { type ResolverDef, type TRPCQueryOptions, @@ -13,7 +13,7 @@ import { notFound } from "next/navigation"; import { cache } from "react"; import { makeQueryClient } from "./query-client"; -import { endingLink } from "./shared"; +import { endingLink, sentryLoggerLink } from "./shared"; // IMPORTANT: Create a stable getter for the query client that // will return the same client during the same request. @@ -23,11 +23,7 @@ export const trpc = createTRPCOptionsProxy({ queryClient: getQueryClient, client: createTRPCClient({ links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), + sentryLoggerLink(), endingLink({ headers: { "x-trpc-source": "server", diff --git a/apps/dashboard/src/lib/trpc/shared.ts b/apps/dashboard/src/lib/trpc/shared.ts index 41f74d6f..b80281e1 100644 --- a/apps/dashboard/src/lib/trpc/shared.ts +++ b/apps/dashboard/src/lib/trpc/shared.ts @@ -1,8 +1,38 @@ import type { AppRouter } from "@openstatus/api"; +import * as Sentry from "@sentry/nextjs"; import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; -import { httpBatchLink } from "@trpc/client"; +import { httpBatchLink, loggerLink } from "@trpc/client"; import superjson from "superjson"; +/** + * tRPC logger link that reports failed queries to Sentry directly instead of + * letting captureConsoleIntegration scrape tRPC's styled console.error format + * string (which surfaces as noise like "%c << query #1 %c...%c %O"). + * + * Only the operation `path` is attached — never `input`, which can carry + * secrets (page passwords, subscriber tokens, emails). + */ +export const sentryLoggerLink = (): TRPCLink => + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + logger: (opts) => { + if (opts.direction === "down" && opts.result instanceof Error) { + Sentry.captureException(opts.result, { + extra: { path: opts.path }, + }); + if (process.env.NODE_ENV === "development") { + console.warn("[tRPC error]", opts.path, opts.result); + } + return; + } + if (process.env.NODE_ENV === "development") { + console.log(opts); + } + }, + }); + /** * Shared onError handler for tRPC route handlers. */ diff --git a/apps/status-page/src/lib/trpc/client.tsx b/apps/status-page/src/lib/trpc/client.tsx index 51ecd68c..dbb12668 100644 --- a/apps/status-page/src/lib/trpc/client.tsx +++ b/apps/status-page/src/lib/trpc/client.tsx @@ -2,11 +2,11 @@ import type { AppRouter } from "@openstatus/api"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { createTRPCClient, loggerLink } from "@trpc/client"; +import { createTRPCClient } from "@trpc/client"; import { createTRPCContext } from "@trpc/tanstack-react-query"; import { useState } from "react"; -import { endingLink } from "./shared"; +import { endingLink, sentryLoggerLink } from "./shared"; export const { TRPCProvider, useTRPC, useTRPCClient } = createTRPCContext(); @@ -41,11 +41,7 @@ export function TRPCReactProvider({ children }: { children: React.ReactNode }) { const [trpcClient] = useState(() => createTRPCClient({ links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), + sentryLoggerLink(), endingLink({ headers: { "x-trpc-source": "client", diff --git a/apps/status-page/src/lib/trpc/server.tsx b/apps/status-page/src/lib/trpc/server.tsx index dad41dfd..726febc8 100644 --- a/apps/status-page/src/lib/trpc/server.tsx +++ b/apps/status-page/src/lib/trpc/server.tsx @@ -2,7 +2,7 @@ import "server-only"; import type { AppRouter } from "@openstatus/api"; import { HydrationBoundary } from "@tanstack/react-query"; import { dehydrate } from "@tanstack/react-query"; -import { createTRPCClient, loggerLink } from "@trpc/client"; +import { createTRPCClient } from "@trpc/client"; import { type TRPCQueryOptions, createTRPCOptionsProxy, @@ -11,7 +11,7 @@ import { cookies } from "next/headers"; import { cache } from "react"; import { makeQueryClient } from "./query-client"; -import { endingLink } from "./shared"; +import { endingLink, sentryLoggerLink } from "./shared"; // IMPORTANT: Create a stable getter for the query client that // will return the same client during the same request. @@ -21,11 +21,7 @@ export const trpc = createTRPCOptionsProxy({ queryClient: getQueryClient, client: createTRPCClient({ links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), + sentryLoggerLink(), endingLink({ headers: { "x-trpc-source": "server", diff --git a/apps/status-page/src/lib/trpc/shared.ts b/apps/status-page/src/lib/trpc/shared.ts index f1b1516d..f0dc5fa2 100644 --- a/apps/status-page/src/lib/trpc/shared.ts +++ b/apps/status-page/src/lib/trpc/shared.ts @@ -1,9 +1,39 @@ import type { AppRouter } from "@openstatus/api"; +import * as Sentry from "@sentry/nextjs"; import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; -import { httpBatchLink } from "@trpc/client"; +import { httpBatchLink, loggerLink } from "@trpc/client"; import type { TRPCError } from "@trpc/server"; import superjson from "superjson"; +/** + * tRPC logger link that reports failed queries to Sentry directly instead of + * letting captureConsoleIntegration scrape tRPC's styled console.error format + * string (which surfaces as noise like "%c << query #1 %c...%c %O"). + * + * Only the operation `path` is attached — never `input`, which can carry + * secrets (page passwords, subscriber tokens, emails). + */ +export const sentryLoggerLink = (): TRPCLink => + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + logger: (opts) => { + if (opts.direction === "down" && opts.result instanceof Error) { + Sentry.captureException(opts.result, { + extra: { path: opts.path }, + }); + if (process.env.NODE_ENV === "development") { + console.warn("[tRPC error]", opts.path, opts.result); + } + return; + } + if (process.env.NODE_ENV === "development") { + console.log(opts); + } + }, + }); + /** * Shared onError handler for tRPC route handlers. */ diff --git a/apps/web/src/trpc/client.ts b/apps/web/src/trpc/client.ts index 3294a095..9fe37e96 100644 --- a/apps/web/src/trpc/client.ts +++ b/apps/web/src/trpc/client.ts @@ -1,15 +1,11 @@ import type { AppRouter } from "@openstatus/api"; -import { createTRPCClient, loggerLink } from "@trpc/client"; +import { createTRPCClient } from "@trpc/client"; -import { endingLink } from "./shared"; +import { endingLink, sentryLoggerLink } from "./shared"; export const api = createTRPCClient({ links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), + sentryLoggerLink(), endingLink({ headers: { "x-trpc-source": "client", diff --git a/apps/web/src/trpc/server.ts b/apps/web/src/trpc/server.ts index 3d27b1e4..6cd03169 100644 --- a/apps/web/src/trpc/server.ts +++ b/apps/web/src/trpc/server.ts @@ -1,16 +1,12 @@ import type { AppRouter } from "@openstatus/api"; -import { createTRPCClient, loggerLink } from "@trpc/client"; +import { createTRPCClient } from "@trpc/client"; import { headers } from "next/headers"; -import { endingLink } from "./shared"; +import { endingLink, sentryLoggerLink } from "./shared"; export const api = createTRPCClient({ links: [ - loggerLink({ - enabled: (opts) => - process.env.NODE_ENV === "development" || - (opts.direction === "down" && opts.result instanceof Error), - }), + sentryLoggerLink(), endingLink({ headers: async () => { const h = new Map(await headers()); diff --git a/apps/web/src/trpc/shared.ts b/apps/web/src/trpc/shared.ts index 72087789..5696444e 100644 --- a/apps/web/src/trpc/shared.ts +++ b/apps/web/src/trpc/shared.ts @@ -1,8 +1,38 @@ import type { AppRouter } from "@openstatus/api"; +import * as Sentry from "@sentry/nextjs"; import type { HTTPBatchLinkOptions, HTTPHeaders, TRPCLink } from "@trpc/client"; -import { httpBatchLink } from "@trpc/client"; +import { httpBatchLink, loggerLink } from "@trpc/client"; import superjson from "superjson"; +/** + * tRPC logger link that reports failed queries to Sentry directly instead of + * letting captureConsoleIntegration scrape tRPC's styled console.error format + * string (which surfaces as noise like "%c << query #1 %c...%c %O"). + * + * Only the operation `path` is attached — never `input`, which can carry + * secrets (page passwords, subscriber tokens, emails). + */ +export const sentryLoggerLink = (): TRPCLink => + loggerLink({ + enabled: (opts) => + process.env.NODE_ENV === "development" || + (opts.direction === "down" && opts.result instanceof Error), + logger: (opts) => { + if (opts.direction === "down" && opts.result instanceof Error) { + Sentry.captureException(opts.result, { + extra: { path: opts.path }, + }); + if (process.env.NODE_ENV === "development") { + console.warn("[tRPC error]", opts.path, opts.result); + } + return; + } + if (process.env.NODE_ENV === "development") { + console.log(opts); + } + }, + }); + /** * Shared onError handler for tRPC route handlers. */ -- 2.51.2