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. */