Skip to content
Merged
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
44 changes: 43 additions & 1 deletion .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ name: Docker image
# staging, which runs `main`. A release, the `v1.2.3` tag Release Please starts this workflow for,
# publishes `latest`, `1.2.3` and `1.2` and deploys production, which runs `latest`. Every image also
# gets a `sha-<commit>` tag to roll back to. With the Sentry secret and variables set, the build
# uploads source maps. See DEPLOY.md.
# uploads source maps and each deploy is recorded on its release in Sentry. See DEPLOY.md.
on:
push:
branches: [main]
Expand All @@ -25,6 +25,8 @@ jobs:
image:
name: build, scan and push
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.value }}
permissions:
contents: read
packages: write
Expand Down Expand Up @@ -146,8 +148,13 @@ jobs:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: staging
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
steps:
- name: Call the staging deploy webhook
id: deploy
env:
WEBHOOK_URL: ${{ secrets.DOKPLOY_STAGING_WEBHOOK_URL }}
run: |
Expand All @@ -158,15 +165,35 @@ jobs:
curl -fsS --retry 3 --retry-all-errors -X POST "$WEBHOOK_URL"
echo
echo "Dokploy is deploying the main image to staging."
echo "called=true" >> "$GITHUB_OUTPUT"

# Records the deploy on the release the image reports, `sha-<commit>` or the version, so
# Sentry's Releases page shows when each one reached staging. Skipped without the token.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.deploy.outputs.called == 'true' && env.SENTRY_AUTH_TOKEN != ''
- name: Record the deploy in Sentry
if: steps.deploy.outputs.called == 'true' && env.SENTRY_AUTH_TOKEN != ''
uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3.7.0
with:
release: ${{ needs.image.outputs.version }}
environment: staging
# Linking commits needs Sentry's GitHub integration. Switch to `auto` once it is installed.
set_commits: skip
disable_telemetry: true

deploy-production:
name: deploy production
needs: image
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
environment: production
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ vars.SENTRY_ORG }}
SENTRY_PROJECT: ${{ vars.SENTRY_PROJECT }}
steps:
- name: Call the production deploy webhook
id: deploy
env:
WEBHOOK_URL: ${{ secrets.DOKPLOY_WEBHOOK_URL }}
run: |
Expand All @@ -177,3 +204,18 @@ jobs:
curl -fsS --retry 3 --retry-all-errors -X POST "$WEBHOOK_URL"
echo
echo "Dokploy is deploying the release to production."
echo "called=true" >> "$GITHUB_OUTPUT"

# Records the deploy on the release the image reports, `sha-<commit>` or the version, so
# Sentry's Releases page shows when each one reached production. Skipped without the token.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.deploy.outputs.called == 'true' && env.SENTRY_AUTH_TOKEN != ''
- name: Record the deploy in Sentry
if: steps.deploy.outputs.called == 'true' && env.SENTRY_AUTH_TOKEN != ''
uses: getsentry/action-release@ff07929a6537bac57790c3451cf4d364aca38528 # v3.7.0
with:
release: ${{ needs.image.outputs.version }}
environment: production
# Linking commits needs Sentry's GitHub integration. Switch to `auto` once it is installed.
set_commits: skip
disable_telemetry: true
16 changes: 15 additions & 1 deletion DEPLOY.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,13 +159,27 @@ Without them the image builds the same and Sentry shows minified stack traces.
With `VITE_SENTRY_DSN` set, Sentry receives:

- **Errors** from the browser, server requests and server functions, with file names removed.
- **Logs**: warnings and errors the app writes to the console, with file names removed.
- **Logs**: everything the server writes to the console, the same lines Dokploy shows, and the
warnings and errors the browser writes, with file names removed.
- **Traces** of a fifth of page loads, navigations and server requests, under **Explore → Traces**
and **Insights**.

Browser reports go to a same-origin route the build generates, which forwards them to Sentry, so
content blockers do not drop them. Session replay stays off.

What it leaves out:

- **PostHog's own console messages**, such as its toolbar failing to load for a signed-in admin.
- **Visitor IP addresses.** Server spans have the address the proxy forwards removed, and header
values that name an address are filtered. Also turn on **Settings → Projects → hexlode →
Security & Privacy → Prevent Storing of IP Addresses**, so Sentry keeps none either.
- **Aborts from visitors leaving.** A browser that closes a tab while it sends a report makes the
server's read fail with `AbortError`; the server drops those instead of reporting a 500.

With the Sentry token and variables set, every deploy is recorded on its release: `sha-<commit>`
for staging and the version for production. **Releases** then shows when each one went out and
where. Profiling is left off because Sentry's free plan does not include it.

Set these up in Sentry itself:

- **Uptime monitor** (**Insights → Uptime**): check `https://your-domain/api/health` so Sentry
Expand Down
1 change: 1 addition & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"!src/routeTree.gen.ts",
"!src/styles.css",
"!pnpm-lock.yaml",
"!.release-please-manifest.json",
"!src/features/theme/hexlode.js",
"!src/features/theme/hexlode.d.ts",
"!src/features/theme/hexlode.variants.d.ts",
Expand Down
28 changes: 28 additions & 0 deletions instrument.server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@ import * as Sentry from '@sentry/tanstackstart-react'

const sentryDsn = import.meta.env?.VITE_SENTRY_DSN ?? process.env.VITE_SENTRY_DSN

/**
* The server starts no aborts of its own, so an AbortError means the browser went away before its
* request was read, such as a tab closing while it sent an error report through the tunnel.
*/
function isClientDisconnect(error) {
// A few causes deep at most, in case a chain of causes loops.
for (let current = error, depth = 0; current && depth < 5; current = current.cause, depth++) {
if (current.name === 'AbortError') return true
}
return false
}

/**
* Span attributes that hold the visitor's IP address. Sentry copies the first X-Forwarded-For
* address, which the proxy in front of the server sets, into every request span even without
* personal data, and analytics must not keep IP addresses (ADR 0005).
*/
const IP_ATTRIBUTES = ['http.client_ip', 'client.address']

function withoutIp(span) {
for (const key of IP_ATTRIBUTES) delete span.data?.[key]
return span
}

if (sentryDsn) {
Sentry.init({
dsn: sentryDsn,
Expand All @@ -18,7 +42,11 @@ if (sentryDsn) {
httpBodies: [],
},
enableLogs: true,
// Everything the server writes to the console, which is what Dokploy's log view shows.
integrations: [Sentry.consoleLoggingIntegration({ levels: ['log', 'info', 'warn', 'error'] })],
// Matches TRACES_SAMPLE_RATE in src/features/usage/constants.ts.
tracesSampleRate: 0.2,
beforeSendSpan: withoutIp,
beforeSend: (event, hint) => (isClientDisconnect(hint.originalException) ? null : event),
})
}
Loading
Loading