Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 5 additions & 2 deletions src/instrument.server.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import * as Sentry from '@sentry/tanstackstart-react'
import { redactByokRequestHeaders } from './utils/sentry-redaction'
import {
filterSentryEvent,
redactByokRequestHeaders,
} from './utils/sentry-redaction'

if (!Sentry.getClient())
Sentry.init({
Expand All @@ -25,6 +28,6 @@ if (!Sentry.getClient())
return null
return breadcrumb
},
beforeSend: redactByokRequestHeaders,
beforeSend: filterSentryEvent,
beforeSendTransaction: redactByokRequestHeaders,
})
6 changes: 5 additions & 1 deletion src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ import {
installStaleAppReloadHandlers,
consumeStaleAppRecovery,
} from './utils/stale-app-reload'
import { redactByokRequestHeaders } from './utils/sentry-redaction'
import {
isRouterControlFlow,
redactByokRequestHeaders,
} from './utils/sentry-redaction'
import type { DocsNavTabId } from './utils/docsNavTabs'
import type { CustomToolTabId } from './libraries/custom-tool-tabs'

Expand All @@ -25,6 +28,7 @@ if (typeof document !== 'undefined') {
// Set 'tracePropagationTargets' to control for which URLs distributed tracing should be enabled
tracePropagationTargets: ['localhost', /^https:\/\/tanstack\.com\//],
beforeSend(event, hint) {
if (isRouterControlFlow(hint.originalException)) return null
redactByokRequestHeaders(event)
// Only suppress the exact error that triggered a reload. Errors during
// the reload cooldown still need reporting.
Expand Down
22 changes: 22 additions & 0 deletions src/utils/sentry-redaction.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { isNotFound, isRedirect } from '@tanstack/react-router'

type EventWithRequestHeaders = {
request?: {
headers?: Record<string, string>
Expand All @@ -14,6 +16,10 @@ type EventWithRequestHeaders = {
exception?: { values?: { value?: string }[] }
}

type EventHintWithOriginalException = {
originalException?: unknown
}

export function redactByokRequestHeaders<
TEvent extends EventWithRequestHeaders,
>(event: TEvent) {
Expand Down Expand Up @@ -59,3 +65,19 @@ export function redactByokRequestHeaders<

return event
}

/**
* TanStack Router/Start throws `notFound()` and `redirect()` objects as
* control flow. They are not real errors and should never be reported.
*/
export function isRouterControlFlow(value: unknown): boolean {
return isNotFound(value) || isRedirect(value)
}

export function filterSentryEvent<TEvent extends EventWithRequestHeaders>(
event: TEvent,
hint?: EventHintWithOriginalException,
): TEvent | null {
if (isRouterControlFlow(hint?.originalException)) return null
return redactByokRequestHeaders(event)
}
26 changes: 25 additions & 1 deletion tests/sentry-redaction.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import assert from 'node:assert/strict'
import test from 'node:test'
import { redactByokRequestHeaders } from '../src/utils/sentry-redaction'
import { notFound, redirect } from '@tanstack/react-router'
import {
filterSentryEvent,
redactByokRequestHeaders,
} from '../src/utils/sentry-redaction'

test('Sentry events never include BYOK request headers', () => {
const event = {
Expand All @@ -17,3 +21,23 @@ test('Sentry events never include BYOK request headers', () => {
assert.equal(redactByokRequestHeaders(event), event)
assert.deepEqual(event.request.headers, { Accept: 'application/json' })
})

test('Sentry filters actual Router control flow and keeps real failures redacted', () => {
for (const originalException of [notFound(), redirect({ href: '/' })]) {
assert.equal(filterSentryEvent({}, { originalException }), null)
}
for (const originalException of [
new Error('failure'),
new Response(null, { status: 500 }),
{ isRedirect: true },
undefined,
]) {
const event = {
request: {
headers: { 'x-byok-openai': 'secret', Accept: 'application/json' },
},
}
assert.equal(filterSentryEvent(event, { originalException }), event)
assert.deepEqual(event.request.headers, { Accept: 'application/json' })
}
})
Loading