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
45 changes: 13 additions & 32 deletions extensions/sentry-triage/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,17 +40,11 @@ that drafts a fix pull request.

- **Node.js 22 or newer.**
- The GitHub Copilot app canvas / UI-extensions experiment enabled.
- A Sentry sign-in. This canvas reads issues through the
- **A Sentry sign-in.** This canvas reads issues through the
[Sentry CLI](https://cli.sentry.dev) in library mode — there is no MCP server
to configure. After installing the dependency (see **Install** below), sign in
once with the package-local CLI, run from the extension folder:

```sh
npx sentry auth login
```

This stores an OAuth credential the canvas auto-detects. For non-interactive
environments, export a token instead:
to configure. The setup gate walks you through installing the dependency and
signing in with one-click buttons (see **Install** below); no terminal
required. For non-interactive environments, you can instead export a token:

```sh
export SENTRY_AUTH_TOKEN=<your-token>
Expand All @@ -67,30 +61,17 @@ repository at `.github/extensions/sentry-triage/` for project scope.

This canvas depends on the [`sentry`](https://cli.sentry.dev) npm package at
runtime, which isn't bundled with the extension source. If it's missing, the canvas
still **opens** and shows a setup gate explaining what to do instead of crashing.

### Let Copilot set it up (recommended)

Because the canvas runs inside GitHub Copilot, the agent can install the dependency
for you. Paste this into Copilot:
still **opens** and shows a setup gate — click its **Install dependencies** button
and the extension runs `npm install` in its own directory (wherever it's actually
installed, so there's no path to guess). Once installed, if you aren't signed in
yet the gate shows a **Sign in with Sentry** button that opens your browser to
approve access and returns automatically — no terminal step required. The gate
clears once both finish; no reload or Copilot involvement needed.

> Locate the loaded `sentry-triage` canvas extension folder — the directory that
> contains its `package.json` — run `npm install` there, then reload extensions.

(That folder is `~/.copilot/extensions/sentry-triage/` for user scope,
`.github/extensions/sentry-triage/` for project scope, or, if you installed the
published plugin, `com.github.copilot/extensions/sentry-triage` inside the
installed plugin.)

Then finish the one interactive step yourself — sign in so Copilot never handles a
raw secret. Run this from the same extension folder (`npx` resolves the CLI the
local `npm install` just placed in `node_modules`):

```sh
npx sentry auth login
```
### Or set it up manually

### Or install it manually
If you'd rather not use the buttons (or `npm`/a browser isn't available to the
extension process), you can run the same steps yourself:

```sh
# User scope
Expand Down
247 changes: 233 additions & 14 deletions extensions/sentry-triage/components/page.mjs

Large diffs are not rendered by default.

53 changes: 52 additions & 1 deletion extensions/sentry-triage/extension.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { execSync } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { startServer } from './server.mjs'
import { scanIssues, listOrgs, listProjects, findProject } from './sentry.mjs'
import { checkConnections, checkConnectionsOnce } from './preflight.mjs'
import { checkConnections, checkConnectionsOnce, installDependencies, authenticate } from './preflight.mjs'
import { sanitizeForPrompt } from './escape.mjs'

// A Sentry-derived URL is safe to pass through only if it PARSES as a real
Expand Down Expand Up @@ -1309,6 +1309,55 @@ async function onRecheckConnections(entry) {
return connections
}

// "Install dependencies" button handler on the package-missing setup gate.
// Delegates to preflight's installDependencies (npm install + re-probe, rooted
// at the extension's own directory) and publishes the fresh connection state so
// the gate updates live. On success (Sentry now reachable and an org already
// committed) kicks a scan, mirroring onRecheckConnections's post-recovery path.
async function onInstallDependencies(entry) {
if (entry.closed) return entry.state.getConnections?.() || { sentry: { reachable: false } }
const connections = await installDependencies()
if (entry.closed) return connections
entry.state.setConnections(connections)
entry.notifyClients()
if (connections.sentry.reachable && entry.state.getOrg()) {
triageSentry(entry).catch((err) => {
console.error('[sentry-triage] post-install scan failed:', err instanceof Error ? err.message : err)
})
} else if (connections.sentry.reachable) {
discoverOrgs(entry).catch((err) => {
console.error('[sentry-triage] post-install org discovery failed:', err instanceof Error ? err.message : err)
})
}
return connections
}

// "Sign in with Sentry" button handler on the not-authenticated setup gate
// (shown only once the package is installed — see components/page.mjs). Runs
// the SDK's OAuth device-code login (opens the user's browser directly, no
// terminal) and publishes the fresh connection state so the gate updates
// live, mirroring onInstallDependencies's post-recovery path. Unlike install,
// a failed/cancelled login is allowed to propagate so the server route can
// report the specific reason instead of a generic "still signed out".
async function onAuthenticate(entry) {
if (entry.closed) return entry.state.getConnections?.() || { sentry: { reachable: false } }
const connections = await authenticate()
if (entry.closed) return connections
entry.state.setConnections(connections)
entry.notifyClients()
if (connections.sentry.reachable && entry.state.getOrg()) {
triageSentry(entry).catch((err) => {
console.error('[sentry-triage] post-auth scan failed:', err instanceof Error ? err.message : err)
})
} else if (connections.sentry.reachable) {
discoverOrgs(entry).catch((err) => {
console.error('[sentry-triage] post-auth org discovery failed:', err instanceof Error ? err.message : err)
})
}
return connections
}


async function onWorkSelected(entry, issueKeys, modelByKey, assignCopilot) {
if (entry.closed) return
const uniqueKeys = [...new Set(issueKeys)].filter(Boolean)
Expand Down Expand Up @@ -1982,6 +2031,8 @@ const session = await joinSession({
onRefresh: () => refreshAll(entry),
onWorkSelected: (keys, modelByKey, assignCopilot) => onWorkSelected(entry, keys, modelByKey, assignCopilot),
onRecheck: () => onRecheckConnections(entry),
onInstallDependencies: () => onInstallDependencies(entry),
onAuthenticate: () => onAuthenticate(entry),
onListProjects: (org) => discoverProjects(entry, org, { force: true }),
onResolveProject: (org, slug) => resolveProject(entry, org, slug),
onInvalidateEnrichment: () => {
Expand Down
47 changes: 44 additions & 3 deletions extensions/sentry-triage/preflight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// buildConnections() is pure so it can be unit-tested offline; checkConnections()
// is the thin wrapper that talks to Sentry.

import { whoami, SentryError } from './sentryClient.mjs'
import { whoami, installPackage, login, SentryError } from './sentryClient.mjs'

// Shape the raw signals into the connection status the UI consumes. Pure.
export function buildConnections({
Expand Down Expand Up @@ -60,6 +60,12 @@ const msg = (err) => (err instanceof Error ? err.message : String(err))
// network blip and not an auth failure, so it gets its own gate branch.
const isPackageMissing = (err) => Boolean(err) && err.code === 'SENTRY_PACKAGE_MISSING'

// An active SENTRY_AUTH_TOKEN/SENTRY_TOKEN in the environment takes precedence
// over any OAuth login, so the "Sign in" button can't do anything useful while
// one is set — sentryClient's login() detects this up front and fails fast
// with this code instead of running an OAuth flow that can't take effect.
const isEnvTokenActive = (err) => Boolean(err) && err.code === 'SENTRY_ENV_TOKEN_ACTIVE'

// Text used for classification: the message plus any CLI stderr, since a rejected
// credential (HTTP 401/403) often surfaces its status in stderr rather than the
// Error message.
Expand Down Expand Up @@ -95,11 +101,14 @@ const humanizeSentryError = (err) => {
if (isPackageMissing(err)) {
return 'The Sentry CLI isn’t installed for this canvas yet. Ask Copilot to “install the sentry-triage dependencies and reload extensions,” then run `npx sentry auth login` from the extension folder and re-open this canvas.'
}
if (isEnvTokenActive(err)) {
return t
}
if (isNotAuthenticated(err)) {
return 'Sentry isn’t connected yet. Run `npx sentry auth login` from the extension folder, then re-open this canvas.'
return 'Sentry isn’t connected yet.'
}
if (isAuthFailure(err)) {
return 'Sentry rejected your credential (expired or invalid). Run `npx sentry auth login` from the extension folder, then re-open this canvas.'
return 'Sentry rejected your credential (expired or invalid). Sign in again below.'
}
if (isTransient(err)) {
return 'Couldn’t reach Sentry just now (network). It should recover on the next check.'
Expand Down Expand Up @@ -174,3 +183,35 @@ export async function checkConnections() {
}
return shape(result)
}

// One-click fix for the package-missing gate: run `npm install` in the
// extension's own directory (via sentryClient's installPackage, so the path is
// never guessed by an agent or user) and immediately re-probe. Returns the fresh
// connection state either way so the gate/setup UI can render the outcome —
// success clears the gate, and a failed install surfaces as a normal probe error
// (e.g. still package-missing, or an npm/network failure) rather than throwing.
export async function installDependencies() {
try {
await installPackage()
} catch (err) {
console.error('[sentry-triage] npm install failed:', err instanceof Error ? err.message : err)
}
const { connections } = await checkConnectionsOnce()
return connections
}

// One-click fix for the "not authenticated" setup gate: run the SDK's own
// OAuth device-code login (sentryClient's login(), the in-process equivalent
// of `sentry auth login`) and immediately re-probe. Only ever called for a
// package-present, not-signed-in state — the gate never shows this button
// while the package itself is missing (see components/page.mjs) — so unlike
// installDependencies() a thrown login error (user closed the browser tab,
// denied consent, or the device code expired) is left to propagate: the
// caller (extension.mjs onAuthenticate) surfaces it to the gate rather than
// silently falling back to a generic "still signed out" re-probe, since the
// specific reason (denied vs. expired vs. cancelled) is worth showing.
export async function authenticate() {
await login()
const { connections } = await checkConnectionsOnce()
return connections
}
Loading
Loading