mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-27 19:15:15 +00:00
Fix sentry-triage canvas crash when the sentry package isn't bundled (#2810)
Published awesome-copilot plugins ship extension source only, so the optional `sentry` npm package the canvas depends on at runtime may be absent. Previously that made the canvas crash on open instead of guiding the user through setup. - Load the optional `sentry` package lazily and translate only the top-level ERR_MODULE_NOT_FOUND for `sentry` into a package-missing setup state; any other import failure (missing transitive dep, entrypoint throwing) is rethrown so a real defect isn't masked behind a misleading "reinstall" message. - Add a dedicated package-missing branch to the connection preflight and a matching setup gate, kept distinct from the auth and transient-network gates so the user never sees contradictory guidance. The canvas now opens and explains what to do rather than crashing. - Clear `configured` for the package-missing state so the status is no longer the contradictory `configured:true` + `setup:'package-missing'`. - Tell users to sign in with the package-local CLI via `npx sentry auth login` run from the extension folder — the only form that resolves after a local `npm install`, since a package-local binary isn't on the shell PATH. - Update the README so the sign-in step and install guidance cover the published-plugin layout (`com.github.copilot/extensions/sentry-triage`), not just the standalone user/project extension paths. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -42,10 +42,11 @@ that drafts a fix pull request.
|
||||
- The GitHub Copilot app canvas / UI-extensions experiment enabled.
|
||||
- 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. Sign in once from your terminal:
|
||||
to configure. After installing the dependency (see **Install** below), sign in
|
||||
once with the package-local CLI, run from the extension folder:
|
||||
|
||||
```sh
|
||||
sentry auth login
|
||||
npx sentry auth login
|
||||
```
|
||||
|
||||
This stores an OAuth credential the canvas auto-detects. For non-interactive
|
||||
@@ -61,10 +62,35 @@ that drafts a fix pull request.
|
||||
|
||||
## Install
|
||||
|
||||
Drop this folder at `~/.copilot/extensions/sentry-triage/` for user scope, or in
|
||||
a repository at `.github/extensions/sentry-triage/` for project scope. Then
|
||||
install dependencies from inside the copied folder (this canvas depends on the
|
||||
`sentry` npm package at runtime):
|
||||
Drop this folder at `~/.copilot/extensions/sentry-triage/` for user scope, or in a
|
||||
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:
|
||||
|
||||
> 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 install it manually
|
||||
|
||||
```sh
|
||||
# User scope
|
||||
@@ -73,11 +99,15 @@ cd ~/.copilot/extensions/sentry-triage
|
||||
# Or project scope, from the repository root
|
||||
cd .github/extensions/sentry-triage
|
||||
|
||||
# Or, if you installed the published plugin (`copilot plugin install`), the source
|
||||
# lives inside the installed plugin — cd into its extension folder:
|
||||
cd <installed-plugin-path>/com.github.copilot/extensions/sentry-triage
|
||||
|
||||
npm install
|
||||
npx sentry auth login
|
||||
```
|
||||
|
||||
Reload extensions in the GitHub Copilot app, then open the `sentry-triage`
|
||||
canvas.
|
||||
Reload extensions in the GitHub Copilot app, then open the `sentry-triage` canvas.
|
||||
|
||||
## Open the canvas in the correct repository scope
|
||||
|
||||
|
||||
@@ -66,6 +66,10 @@ export function Page({
|
||||
// required, …). Shown in the gate so users see the real reason instead of a
|
||||
// generic "connect" message for problems reconnecting won't fix.
|
||||
const gateError = (conn.sentry && conn.sentry.error) || ''
|
||||
// A dedicated setup reason (currently only the optional `sentry` package being
|
||||
// absent) that takes precedence over the auth/connectivity gate variants, so the
|
||||
// install prompt is never shown beneath contradictory "sign in" guidance.
|
||||
const packageMissing = (conn.sentry && conn.sentry.setup) === 'package-missing'
|
||||
// Distinguish "not signed in" (needs `sentry auth login`) from a transient
|
||||
// connectivity failure where a credential likely exists. probeSentry() reports
|
||||
// configured:false only for auth failures, so a gated-but-configured state means
|
||||
@@ -184,20 +188,24 @@ export function Page({
|
||||
<section class="sentry-gate" role="alert">
|
||||
<div class="gate-card">
|
||||
<div class="gate-icon">⚠️</div>
|
||||
<h2 id="gate-title">${signedOut ? 'Connect Sentry to start triaging' : 'Can’t reach Sentry right now'}</h2>
|
||||
<div class="gate-body gate-body-auth" id="gate-body-auth"${signedOut ? '' : ' style="display:none;"'}>
|
||||
<p class="gate-lead">This canvas reads your live Sentry issues through the Sentry CLI, but it isn't signed in yet.</p>
|
||||
<p class="gate-steps">Run <code>sentry auth login</code> in your terminal to sign in, then re-open this canvas.</p>
|
||||
<h2 id="gate-title">${packageMissing ? 'Set up the Sentry CLI' : (signedOut ? 'Connect Sentry to start triaging' : 'Can’t reach Sentry right now')}</h2>
|
||||
<div class="gate-body gate-body-setup" id="gate-body-setup"${packageMissing ? '' : ' style="display:none;"'}>
|
||||
<p class="gate-lead">This canvas reads your live Sentry issues through the Sentry CLI, but its <code>sentry</code> package isn’t installed for this extension yet.</p>
|
||||
<p class="gate-steps">Ask Copilot to “install the sentry-triage dependencies and reload extensions,” then run <code>npx sentry auth login</code> from the extension folder to sign in and re-open this canvas.</p>
|
||||
</div>
|
||||
<div class="gate-body gate-body-conn" id="gate-body-conn"${signedOut || !transientConn ? ' style="display:none;"' : ''}>
|
||||
<div class="gate-body gate-body-auth" id="gate-body-auth"${signedOut && !packageMissing ? '' : ' style="display:none;"'}>
|
||||
<p class="gate-lead">This canvas reads your live Sentry issues through the Sentry CLI, but it isn't signed in yet.</p>
|
||||
<p class="gate-steps">Run <code>npx sentry auth login</code> from the extension folder to sign in, then re-open this canvas.</p>
|
||||
</div>
|
||||
<div class="gate-body gate-body-conn" id="gate-body-conn"${signedOut || !transientConn || packageMissing ? ' style="display:none;"' : ''}>
|
||||
<p class="gate-lead">You’re signed in, but this canvas couldn’t reach Sentry — usually a temporary network blip.</p>
|
||||
<p class="gate-steps">It should recover on the next check. If it persists, check your network or VPN, then re-open this canvas.</p>
|
||||
</div>
|
||||
<div class="gate-body gate-body-unknown" id="gate-body-unknown"${signedOut || transientConn ? ' style="display:none;"' : ''}>
|
||||
<div class="gate-body gate-body-unknown" id="gate-body-unknown"${signedOut || transientConn || packageMissing ? ' style="display:none;"' : ''}>
|
||||
<p class="gate-lead">You’re signed in, but this canvas couldn’t reach Sentry.</p>
|
||||
<p class="gate-steps">See the details below, then re-open this canvas to try again.</p>
|
||||
</div>
|
||||
<p class="gate-error" id="gate-error" ${sentryReady || !gateError ? 'style="display:none;"' : ''}>${escapeHtml(gateError || '')}</p>
|
||||
<p class="gate-error" id="gate-error" ${sentryReady || !gateError || packageMissing ? 'style="display:none;"' : ''}>${escapeHtml(gateError || '')}</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -1108,25 +1116,30 @@ export function Page({
|
||||
// sentry auth login command, nor promise recovery for what won't self-heal.
|
||||
const signedOut = !(c.sentry && c.sentry.configured);
|
||||
const transientConn = Boolean(c.sentry && c.sentry.transient);
|
||||
const packageMissing = (c.sentry && c.sentry.setup) === "package-missing";
|
||||
const gateTitleEl = document.getElementById("gate-title");
|
||||
if (gateTitleEl) {
|
||||
gateTitleEl.textContent = signedOut
|
||||
gateTitleEl.textContent = packageMissing
|
||||
? "Set up the Sentry CLI"
|
||||
: signedOut
|
||||
? "Connect Sentry to start triaging"
|
||||
: "Can’t reach Sentry right now";
|
||||
}
|
||||
const setupBody = document.getElementById("gate-body-setup");
|
||||
const authBody = document.getElementById("gate-body-auth");
|
||||
const connBody = document.getElementById("gate-body-conn");
|
||||
const unknownBody = document.getElementById("gate-body-unknown");
|
||||
if (authBody) authBody.style.display = signedOut ? "" : "none";
|
||||
if (connBody) connBody.style.display = !signedOut && transientConn ? "" : "none";
|
||||
if (unknownBody) unknownBody.style.display = !signedOut && !transientConn ? "" : "none";
|
||||
if (setupBody) setupBody.style.display = packageMissing ? "" : "none";
|
||||
if (authBody) authBody.style.display = signedOut && !packageMissing ? "" : "none";
|
||||
if (connBody) connBody.style.display = !signedOut && !packageMissing && transientConn ? "" : "none";
|
||||
if (unknownBody) unknownBody.style.display = !signedOut && !packageMissing && !transientConn ? "" : "none";
|
||||
|
||||
// Surface the specific preflight failure in the gate (or hide it when the
|
||||
// error clears / there's nothing actionable beyond the sign-in steps).
|
||||
const gateErrEl = document.getElementById("gate-error");
|
||||
if (gateErrEl) {
|
||||
const err = (c.sentry && c.sentry.error) || "";
|
||||
if (gatedNow && err) {
|
||||
if (gatedNow && err && !packageMissing) {
|
||||
gateErrEl.textContent = err;
|
||||
gateErrEl.style.display = "";
|
||||
} else {
|
||||
|
||||
@@ -23,6 +23,7 @@ export function buildConnections({
|
||||
sentryReachable = false,
|
||||
sentryError = '',
|
||||
sentryTransient = false,
|
||||
sentrySetup = '',
|
||||
} = {}) {
|
||||
return {
|
||||
checked: true,
|
||||
@@ -34,6 +35,10 @@ export function buildConnections({
|
||||
// recovery for blips but show neutral guidance for unknown failures that
|
||||
// won't self-heal, instead of routing every failure to a "network" message.
|
||||
transient: sentryReachable ? false : Boolean(sentryTransient),
|
||||
// Explicit setup reason (currently only 'package-missing') for gate variants
|
||||
// that must NOT be inferred from the auth boolean. Keeps a missing-dependency
|
||||
// install prompt from rendering beneath contradictory "sign in" guidance.
|
||||
setup: sentryReachable ? '' : String(sentrySetup || ''),
|
||||
error: sentryReachable ? '' : String(sentryError || ''),
|
||||
},
|
||||
}
|
||||
@@ -44,12 +49,17 @@ export function buildConnections({
|
||||
export function unknownConnections() {
|
||||
return {
|
||||
checked: false,
|
||||
sentry: { configured: false, reachable: false, transient: false, error: '' },
|
||||
sentry: { configured: false, reachable: false, transient: false, setup: '', error: '' },
|
||||
}
|
||||
}
|
||||
|
||||
const msg = (err) => (err instanceof Error ? err.message : String(err))
|
||||
|
||||
// The optional `sentry` package isn't installed for this extension (a published
|
||||
// awesome-copilot plugin ships source only). This is a one-time setup step, not a
|
||||
// network blip and not an auth failure, so it gets its own gate branch.
|
||||
const isPackageMissing = (err) => Boolean(err) && err.code === 'SENTRY_PACKAGE_MISSING'
|
||||
|
||||
// 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.
|
||||
@@ -82,11 +92,14 @@ const isTransient = (err) =>
|
||||
// Turn the raw error into something a human can act on.
|
||||
const humanizeSentryError = (err) => {
|
||||
const t = msg(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 (isNotAuthenticated(err)) {
|
||||
return 'Sentry isn’t connected yet. Run `sentry auth login` in your terminal, then re-open this canvas.'
|
||||
return 'Sentry isn’t connected yet. Run `npx sentry auth login` from the extension folder, then re-open this canvas.'
|
||||
}
|
||||
if (isAuthFailure(err)) {
|
||||
return 'Sentry rejected your credential (expired or invalid). Run `sentry auth login` in your terminal, then re-open this canvas.'
|
||||
return 'Sentry rejected your credential (expired or invalid). Run `npx sentry auth login` from the extension folder, then re-open this canvas.'
|
||||
}
|
||||
if (isTransient(err)) {
|
||||
return 'Couldn’t reach Sentry just now (network). It should recover on the next check.'
|
||||
@@ -97,14 +110,16 @@ const humanizeSentryError = (err) => {
|
||||
// Classify a failed Sentry probe into the two decisions the caller cares about,
|
||||
// plus a human message. Pure and exported so the gate/retry branching can be
|
||||
// unit-tested without mocking the SDK:
|
||||
// - configured: does a usable credential exist? Auth failures (no login OR a
|
||||
// rejected/expired credential) clear it so the gate shows sign-in guidance;
|
||||
// - configured: is the canvas actually set up to reach Sentry? Auth failures
|
||||
// (no login OR a rejected/expired credential) clear it so the gate shows
|
||||
// sign-in guidance, and a missing `sentry` package clears it too so the gate
|
||||
// shows install guidance instead of a contradictory configured:true state;
|
||||
// everything else keeps it so the gate shows connectivity guidance.
|
||||
// - transient: worth retrying? Only network blips — never auth failures, even
|
||||
// when their text happens to mention a network keyword.
|
||||
export function classifySentryError(err) {
|
||||
return {
|
||||
configured: !isAuthFailure(err),
|
||||
configured: !isAuthFailure(err) && !isPackageMissing(err),
|
||||
transient: isTransient(err),
|
||||
message: humanizeSentryError(err),
|
||||
}
|
||||
@@ -135,6 +150,7 @@ const shape = (result) =>
|
||||
sentryConfigured: result.configured,
|
||||
sentryReachable: result.reachable,
|
||||
sentryTransient: result.transient,
|
||||
sentrySetup: !result.reachable && isPackageMissing(result.error) ? 'package-missing' : '',
|
||||
sentryError: result.reachable ? '' : humanizeSentryError(result.error),
|
||||
})
|
||||
|
||||
|
||||
@@ -13,16 +13,71 @@
|
||||
// canvas's internal issue model lives in sentry.mjs so this file stays a thin,
|
||||
// swappable transport.
|
||||
|
||||
import createSentrySDK, { SentryError } from 'sentry'
|
||||
// The heavyweight `sentry` CLI package is an OPTIONAL, lazily-loaded dependency.
|
||||
// awesome-copilot ships extension *source* only (no node_modules), so an installed
|
||||
// plugin may not have it. A top-level `import ... from 'sentry'` would throw
|
||||
// `Cannot find package 'sentry'` at module load and take the whole canvas down
|
||||
// before any UI renders. Instead we import it dynamically on first use and, when
|
||||
// it's absent, throw a clear SentryError (tagged SENTRY_PACKAGE_MISSING) that the
|
||||
// setup gate turns into actionable guidance — Copilot can install it for you.
|
||||
|
||||
// A stand-in SentryError so callers can `import { SentryError }` at load time and
|
||||
// `instanceof`-check even when the package never loads. When the real package IS
|
||||
// present we replace this binding (a live ESM export) with the SDK's own
|
||||
// SentryError class, so existing `instanceof` + `.exitCode` checks keep matching
|
||||
// the errors the SDK actually throws.
|
||||
let SentryError = class SentryError extends Error {
|
||||
constructor(message, opts = {}) {
|
||||
super(message)
|
||||
this.name = 'SentryError'
|
||||
if (opts.code) this.code = opts.code
|
||||
}
|
||||
}
|
||||
|
||||
export { SentryError }
|
||||
|
||||
// Fallback message when the optional `sentry` package can't be resolved. The
|
||||
// setup gate (preflight.mjs) rewrites this into fuller guidance; this is what
|
||||
// surfaces anywhere the raw error is shown.
|
||||
const PACKAGE_MISSING_MESSAGE =
|
||||
'The Sentry CLI (the `sentry` npm package) is not installed for this extension. ' +
|
||||
'Ask Copilot to set it up, or run `npm install` in the extension folder.'
|
||||
|
||||
let sdk = null
|
||||
let sdkFactory = null
|
||||
|
||||
// Import the optional `sentry` package exactly once. Throws a SentryError tagged
|
||||
// SENTRY_PACKAGE_MISSING when it can't be resolved, and swaps in the SDK's real
|
||||
// SentryError class (live ESM binding) when it can.
|
||||
async function loadFactory() {
|
||||
if (sdkFactory) return sdkFactory
|
||||
let mod
|
||||
try {
|
||||
mod = await import('sentry')
|
||||
} catch (err) {
|
||||
// Only translate "the `sentry` package itself isn't installed" into the setup
|
||||
// gate. Node reports that as ERR_MODULE_NOT_FOUND naming the `sentry` package.
|
||||
// Any OTHER failure — a missing TRANSITIVE dependency, or the package's own
|
||||
// entrypoint throwing at import — is a real defect we must surface, not mask
|
||||
// behind a misleading "reinstall sentry" message. Rethrow those unchanged.
|
||||
const message = String((err && err.message) || '')
|
||||
const sentryPackageMissing =
|
||||
err && err.code === 'ERR_MODULE_NOT_FOUND' && /Cannot find (?:package|module) 'sentry'/.test(message)
|
||||
if (!sentryPackageMissing) throw err
|
||||
throw new SentryError(PACKAGE_MISSING_MESSAGE, { code: 'SENTRY_PACKAGE_MISSING' })
|
||||
}
|
||||
if (mod.SentryError) SentryError = mod.SentryError
|
||||
sdkFactory = mod.default
|
||||
return sdkFactory
|
||||
}
|
||||
|
||||
// Lazily construct the SDK once. cwd affects the CLI's project-root / DSN
|
||||
// detection; we anchor it to the extension's cwd for determinism.
|
||||
function getSdk() {
|
||||
if (!sdk) sdk = createSentrySDK({ cwd: process.cwd() })
|
||||
async function getSdk() {
|
||||
if (!sdk) {
|
||||
const create = await loadFactory()
|
||||
sdk = create({ cwd: process.cwd() })
|
||||
}
|
||||
return sdk
|
||||
}
|
||||
|
||||
@@ -77,13 +132,13 @@ function asArray(res) {
|
||||
// credential is present and valid; throws SentryError ("Not authenticated…")
|
||||
// otherwise. Used by preflight to gate the board.
|
||||
export async function whoami() {
|
||||
return runSerial(() => getSdk().auth.whoami())
|
||||
return runSerial(async () => (await getSdk()).auth.whoami())
|
||||
}
|
||||
|
||||
// All organizations the stored credential can see. Raw org objects (each has a
|
||||
// `slug`).
|
||||
export async function orgList(limit = 100) {
|
||||
return runSerial(async () => asArray(await getSdk().org.list({ limit })))
|
||||
return runSerial(async () => asArray(await (await getSdk()).org.list({ limit })))
|
||||
}
|
||||
|
||||
// Single-page project fetch within an org. Deliberately NOT wrapped in
|
||||
@@ -96,13 +151,13 @@ export async function orgList(limit = 100) {
|
||||
// (see listProjects in sentry.mjs).
|
||||
export async function projectListRaw(org, limit = 100, cursor) {
|
||||
const orgProject = `${String(org || '').replace(/\/+$/, '')}/`
|
||||
return asArray(await getSdk().project.list({ orgProject, limit, ...(cursor ? { cursor } : {}) }))
|
||||
return asArray(await (await getSdk()).project.list({ orgProject, limit, ...(cursor ? { cursor } : {}) }))
|
||||
}
|
||||
|
||||
// Verify a specific project exists / is accessible. Returns the raw project
|
||||
// object on success; throws SentryError when the slug is unknown or forbidden.
|
||||
export async function projectView(org, slug) {
|
||||
return runSerial(() => getSdk().project.view({ orgProject: `${org}/${slug}` }))
|
||||
return runSerial(async () => (await getSdk()).project.view({ orgProject: `${org}/${slug}` }))
|
||||
}
|
||||
|
||||
// Search issues. `orgProject` is "org/project" (or the trailing-slash "org/" form
|
||||
@@ -113,7 +168,7 @@ export async function projectView(org, slug) {
|
||||
export async function issueList({ orgProject, query, sort = 'date', limit = 100, period } = {}) {
|
||||
return runSerial(async () =>
|
||||
asArray(
|
||||
await getSdk().issue.list({
|
||||
await (await getSdk()).issue.list({
|
||||
...(orgProject ? { orgProject } : {}),
|
||||
...(query ? { query } : {}),
|
||||
...(period ? { period } : {}),
|
||||
|
||||
Reference in New Issue
Block a user