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
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ SESSION_SECRET="super-duper-s3cret"
HONEYPOT_SECRET="super-duper-s3cret"
RESEND_API_KEY="re_blAh_blaHBlaHblahBLAhBlAh"
SENTRY_DSN="your-dsn"
SENTRY_ENVIRONMENT="development"

# this is set to a random value in the Dockerfile
INTERNAL_COMMAND_TOKEN="some-made-up-token"
Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@ jobs:
if: ${{ github.ref == 'refs/heads/dev' }}
run: |
flyctl deploy \
--env SENTRY_ENVIRONMENT=staging \
--image "registry.fly.io/${{ steps.app_name.outputs.value }}-staging:${{ github.sha }}" \
--app ${{ steps.app_name.outputs.value }}-staging
env:
Expand All @@ -224,6 +225,18 @@ jobs:
if: ${{ github.ref == 'refs/heads/main' }}
run: |
flyctl deploy \
--env SENTRY_ENVIRONMENT=production \
--image "registry.fly.io/${{ steps.app_name.outputs.value }}:${{ github.sha }}"
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

- name: Create Sentry release
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
# SENTRY_ORG: 'your sentry org id or slug'
# SENTRY_PROJECT: 'your sentry project id or slug'
if: ${{ env.SENTRY_AUTH_TOKEN }}
uses: getsentry/action-release@v3
with:
environment:
${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
40 changes: 37 additions & 3 deletions app/entry.client.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
import * as Sentry from '@sentry/react-router'
import { startTransition } from 'react'
import { hydrateRoot } from 'react-dom/client'
import { HydratedRouter } from 'react-router/dom'

if (ENV.MODE === 'production' && ENV.SENTRY_DSN) {
void import('./utils/monitoring.client.tsx').then(({ init }) => init())
}
Sentry.init({
// Sentry will only send requests if SENTRY_DSN is defined
dsn: ENV.MODE === 'production' ? ENV.SENTRY_DSN : undefined,
// See https://spotlightjs.com/ for how to install the Spotlight Desktop app for local development
spotlight: ENV.MODE === 'development',
environment: ENV.MODE,
beforeSend(event) {
if (event.request?.url) {
const url = new URL(event.request.url)
if (
url.protocol === 'chrome-extension:' ||
url.protocol === 'moz-extension:'
) {
// This error is from a browser extension, ignore it
return null
}
}
return event
},
integrations: [
Sentry.reactRouterTracingIntegration(),
Sentry.replayIntegration(),
],

// Set tracesSampleRate to 1.0 to capture 100%
// of transactions for performance monitoring.
// We recommend adjusting this value in production
tracesSampleRate: 1.0,

// Capture Replay for 10% of all sessions,
// plus for 100% of sessions with an error
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,

enableLogs: true,
})

startTransition(() => {
hydrateRoot(document, <HydratedRouter />)
Expand Down
36 changes: 10 additions & 26 deletions app/entry.server.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,11 @@
import crypto from 'node:crypto'
import { PassThrough } from 'node:stream'
import { styleText } from 'node:util'
import { contentSecurity } from '@nichtsam/helmet/content'
import { createReadableStreamFromReadable } from '@react-router/node'
import * as Sentry from '@sentry/react-router'
import { isbot } from 'isbot'
import { renderToPipeableStream } from 'react-dom/server'
import {
ServerRouter,
type LoaderFunctionArgs,
type ActionFunctionArgs,
type HandleDocumentRequestFunction,
} from 'react-router'
import { type HandleDocumentRequestFunction, ServerRouter } from 'react-router'
import { getEnv, init } from './utils/env.server.ts'
import { getInstanceInfo } from './utils/litefs.server.ts'
import { NonceProvider } from './utils/nonce-provider.ts'
Expand All @@ -26,7 +20,7 @@ const MODE = process.env.NODE_ENV ?? 'development'

type DocRequestArgs = Parameters<HandleDocumentRequestFunction>

export default async function handleRequest(...args: DocRequestArgs) {
async function handleRequest(...args: DocRequestArgs) {
const [request, responseStatusCode, responseHeaders, reactRouterContext] =
args
const { currentInstance, primaryInstance } = await getInstanceInfo()
Expand Down Expand Up @@ -74,6 +68,10 @@ export default async function handleRequest(...args: DocRequestArgs) {
'connect-src': [
MODE === 'development' ? 'ws:' : undefined,
process.env.SENTRY_DSN ? '*.sentry.io' : undefined,
// Spotlight (SSE to the sidecar)
MODE === 'development'
? 'http://localhost:8969'
: undefined,
"'self'",
],
'font-src': ["'self'"],
Expand All @@ -96,7 +94,8 @@ export default async function handleRequest(...args: DocRequestArgs) {
status: didError ? 500 : responseStatusCode,
}),
)
pipe(body)
// this enables distributed tracing between client and server!
pipe(Sentry.getMetaTagTransformer(body))
},
onShellError: (err: unknown) => {
reject(err)
Expand All @@ -122,21 +121,6 @@ export async function handleDataRequest(response: Response) {
return response
}

export function handleError(
error: unknown,
{ request }: LoaderFunctionArgs | ActionFunctionArgs,
): void {
// Skip capturing if the request is aborted as Remix docs suggest
// Ref: https://remix.run/docs/en/main/file-conventions/entry.server#handleerror
if (request.signal.aborted) {
return
}

if (error instanceof Error) {
console.error(styleText('red', String(error.stack)))
} else {
console.error(error)
}
export const handleError = Sentry.createSentryHandleError({ logErrors: true })

Sentry.captureException(error)
}
export default Sentry.wrapSentryHandleRequest(handleRequest)
29 changes: 29 additions & 0 deletions app/routes/sentry/api.sentry-example-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as Sentry from '@sentry/react-router'
import { requireUserWithRole } from '../../utils/permissions.server.ts'
import { type Route } from './+types/api.sentry-example-api.ts'

class SentryExampleBackendError extends Error {
constructor(message: string | undefined) {
super(message)
this.name = 'SentryExampleBackendError'
}
}

export async function loader({ request }: Route.LoaderArgs) {
await requireUserWithRole(request, 'admin')

await Sentry.startSpan(
{
name: 'Example Backend Span',
op: 'test',
},
async () => {
// Simulate some backend work
await new Promise((resolve) => setTimeout(resolve, 100))
},
)

throw new SentryExampleBackendError(
'This error is raised on the backend API route.',
)
}
Loading