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
5 changes: 5 additions & 0 deletions .changeset/error_page_with_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
default: minor
---

added error page making it easier to report errors when they occur in the field
5 changes: 5 additions & 0 deletions .changeset/feat-sentry-integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'default': minor
---

Add Sentry integration for error tracking and bug reporting
10 changes: 8 additions & 2 deletions .github/actions/prepare-tofu/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,16 @@ runs:
steps:
- name: Setup app and build
uses: ./.github/actions/setup
env:
VITE_IS_RELEASE_TAG: ${{ inputs.is_release_tag }}
with:
build: 'true'
env:
VITE_IS_RELEASE_TAG: ${{ inputs.is_release_tag }}
VITE_SENTRY_DSN: ${{ env.VITE_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: ${{ env.VITE_SENTRY_ENVIRONMENT }}
VITE_APP_VERSION: ${{ env.VITE_APP_VERSION }}
SENTRY_AUTH_TOKEN: ${{ env.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ env.SENTRY_ORG }}
SENTRY_PROJECT: ${{ env.SENTRY_PROJECT }}

- name: Setup OpenTofu
uses: opentofu/setup-opentofu@9d84900f3238fab8cd84ce47d658d25dd008be2f # v1.0.8
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/cloudflare-web-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,13 @@ jobs:
uses: ./.github/actions/prepare-tofu
with:
is_release_tag: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.git_tag != '') }}
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: production
VITE_APP_VERSION: ${{ github.ref_name }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

- name: Comment PR plan
uses: dflook/tofu-plan@3f5dc358343fb58cd60f83b019e810315aa8258f # v2.2.3
Expand All @@ -82,6 +89,13 @@ jobs:
uses: ./.github/actions/prepare-tofu
with:
is_release_tag: ${{ startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && inputs.git_tag != '') }}
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
VITE_SENTRY_ENVIRONMENT: production
VITE_APP_VERSION: ${{ github.ref_name }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}

- name: Plan infrastructure
run: tofu plan -input=false -no-color
Expand Down
16 changes: 16 additions & 0 deletions .github/workflows/cloudflare-web-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ jobs:
echo EOF
} >> "$GITHUB_OUTPUT"
- name: Set Sentry build environment for PR preview
if: github.event_name == 'pull_request'
env:
VITE_SENTRY_DSN: ${{ secrets.VITE_SENTRY_DSN }}
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
shell: bash
run: |
echo "VITE_SENTRY_DSN=$VITE_SENTRY_DSN" >> "$GITHUB_ENV"
echo "VITE_SENTRY_ENVIRONMENT=preview" >> "$GITHUB_ENV"
echo "VITE_SENTRY_PR=${{ github.event.pull_request.number }}" >> "$GITHUB_ENV"
echo "SENTRY_AUTH_TOKEN=$SENTRY_AUTH_TOKEN" >> "$GITHUB_ENV"
echo "SENTRY_ORG=$SENTRY_ORG" >> "$GITHUB_ENV"
echo "SENTRY_PROJECT=$SENTRY_PROJECT" >> "$GITHUB_ENV"
- name: Setup app and build
uses: ./.github/actions/setup
with:
Expand Down
231 changes: 231 additions & 0 deletions .github/workflows/sentry-preview-issues.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
name: Sentry Preview Error Triage

on:
pull_request:
types: [opened, synchronize, reopened]
paths:
- 'src/**'
- 'index.html'
- 'package.json'
- 'vite.config.ts'
- 'tsconfig.json'
workflow_dispatch:
inputs:
pr_number:
description: 'PR number to triage'
required: true
type: number

jobs:
triage:
# Only run for PRs from the same repo (not forks) or manual dispatch
if: >
(github.event_name == 'pull_request' &&
github.event.pull_request.head.repo.full_name == github.repository) ||
github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: write
steps:
- name: Triage Sentry preview errors
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
env:
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
PR_NUMBER: ${{ github.event.pull_request.number || github.event.inputs.pr_number }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const sentryToken = process.env.SENTRY_AUTH_TOKEN;
const sentryOrg = process.env.SENTRY_ORG;
const sentryProject = process.env.SENTRY_PROJECT;
const prNumber = Number(process.env.PR_NUMBER);

if (!prNumber) {
core.info('No PR number available — skipping triage.');
return;
}
if (!sentryToken || !sentryOrg || !sentryProject) {
core.warning('Sentry credentials not configured — skipping triage.');
return;
}

const COMMENT_MARKER = '<!-- sentry-preview-triage -->';
const { owner, repo } = context.repo;

// Create a label if it doesn't already exist
async function ensureLabel(name, description, color) {
try {
await github.rest.issues.getLabel({ owner, repo, name });
} catch {
try {
await github.rest.issues.createLabel({ owner, repo, name, description, color });
} catch (err) {
core.warning(`Could not create label "${name}": ${err.message}`);
}
}
}

// Find an existing GitHub issue that tracks a given Sentry issue ID
async function findExistingGhIssue(sentryIssueId) {
const marker = `sentry-id:${sentryIssueId}`;
const result = await github.rest.search.issuesAndPullRequests({
q: `repo:${owner}/${repo} is:issue label:sentry-preview "${marker}" in:body`,
});
return result.data.total_count > 0 ? result.data.items[0] : null;
}

// Create or update the sticky PR comment with the triage summary table
async function upsertPrComment(rows) {
const now = new Date().toUTCString().replace(':00 GMT', ' UTC');
let body;

if (rows.length === 0) {
body = [
COMMENT_MARKER,
'## Sentry Preview Error Triage',
'',
`No Sentry errors found for this PR's preview deployment as of ${now}.`,
'',
'_This comment updates automatically after each push._',
].join('\n');
} else {
const tableRows = rows.map(
(r) =>
`| [${r.title.slice(0, 70)}](${r.permalink}) | ${r.count} | ${new Date(r.firstSeen).toLocaleDateString()} | #${r.ghIssueNumber} |`
);
body = [
COMMENT_MARKER,
'## Sentry Preview Error Triage',
'',
`**${rows.length} error type(s)** detected in this PR's preview deployment:`,
'',
'| Error | Events | First seen | Issue |',
'| ----- | ------ | ---------- | ----- |',
...tableRows,
'',
`_Last checked: ${now}. Exclude these from your issues view with \`-label:sentry-preview\`._`,
].join('\n');
}

const comments = await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
});
const existing = comments.find(
(c) => c.user.type === 'Bot' && c.body.includes(COMMENT_MARKER)
);

if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body,
});
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body,
});
}
}

// Query Sentry for unresolved issues tagged with this PR number in the preview env
const query = encodeURIComponent(`is:unresolved pr:${prNumber}`);
const sentryUrl =
`https://sentry.io/api/0/projects/${sentryOrg}/${sentryProject}/issues/` +
`?query=${query}&environment=preview&limit=100`;

let sentryIssues;
try {
const resp = await fetch(sentryUrl, {
headers: { Authorization: `Bearer ${sentryToken}` },
});
if (!resp.ok) {
const msg = await resp.text();
core.warning(`Sentry API returned ${resp.status}: ${msg.slice(0, 200)}`);
return;
}
sentryIssues = await resp.json();
} catch (err) {
core.warning(`Sentry API unreachable: ${err.message}`);
return;
}

if (!Array.isArray(sentryIssues) || sentryIssues.length === 0) {
await upsertPrComment([]);
return;
}

// Ensure the shared and PR-specific labels exist
await ensureLabel('sentry-preview', 'Automated Sentry preview error', 'e4e669');
await ensureLabel(`pr-${prNumber}`, `Preview errors from PR #${prNumber}`, 'fbca04');

const rows = [];
for (const issue of sentryIssues) {
const {
id: sentryId,
title,
culprit,
permalink,
count,
userCount,
firstSeen,
lastSeen,
} = issue;
const displayTitle = (title || culprit || 'Unknown error').trim();
const sentryMarker = `sentry-id:${sentryId}`;

const existing = await findExistingGhIssue(sentryId);
let ghIssueNumber;

if (existing) {
ghIssueNumber = existing.number;
// Reopen if it was closed (e.g. after a previous fix that regressed)
if (existing.state === 'closed') {
await github.rest.issues.update({
owner,
repo,
issue_number: ghIssueNumber,
state: 'open',
});
core.info(`Reopened GH issue #${ghIssueNumber} for Sentry issue ${sentryId}`);
}
} else {
const issueBody = [
`<!-- ${sentryMarker} -->`,
`## Sentry Error — PR #${prNumber} Preview`,
'',
`**Error:** [${displayTitle}](${permalink})`,
`**First seen:** ${new Date(firstSeen).toUTCString()}`,
`**Last seen:** ${new Date(lastSeen).toUTCString()}`,
`**Events:** ${count} | **Affected users:** ${userCount}`,
'',
`This issue was automatically created from a Sentry error detected in the preview deployment for PR #${prNumber}.`,
'',
'> [!NOTE]',
'> To exclude automated preview issues from your issues view, filter with: `-label:sentry-preview`',
].join('\n');

const created = await github.rest.issues.create({
owner,
repo,
title: `[Sentry] ${displayTitle.slice(0, 120)}`,
body: issueBody,
labels: ['sentry-preview', `pr-${prNumber}`],
});
ghIssueNumber = created.data.number;
core.info(`Created GH issue #${ghIssueNumber} for Sentry issue ${sentryId}`);
}

rows.push({ title: displayTitle, permalink, count, firstSeen, ghIssueNumber });
}

await upsertPrComment(rows);
core.info(`Triage complete: ${rows.length} Sentry issue(s) processed for PR #${prNumber}.`);
4 changes: 3 additions & 1 deletion Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@
}

try_files {path} /index.html
}

# Required for Sentry browser profiling (JS Self-Profiling API)
header Document-Policy "js-profiling"
3 changes: 3 additions & 0 deletions contrib/nginx/cinny.domain.tld.conf
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ server {
location / {
root /opt/cinny/dist/;

# Required for Sentry browser profiling (JS Self-Profiling API)
add_header Document-Policy "js-profiling" always;

rewrite ^/config.json$ /config.json break;
rewrite ^/manifest.json$ /manifest.json break;

Expand Down
Loading
Loading