Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Comprehensive debugging toolkit for Linear integrations. Use when setting up logging, tracing API calls, or building debug utilities for Linear. Trigger: "debug linear integration", "linear logging", "trace linear API", "linear debugging tools".
.claude/skills/jeremylongshore-linear-debug-bundle/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 140% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 51% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 106% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 72% | 0% |
Production-ready debugging tools for Linear API integrations: instrumented client with request/response logging, request tracer with performance metrics, health check endpoint, environment validator, and interactive debug console.
@linear/sdk installed and configuredIntercept all API calls with timing, logging, and error capture by wrapping the SDK's underlying fetch.
typescriptimport { LinearClient } from "@linear/sdk"; interface DebugOptions { logRequests?: boolean; logResponses?: boolean; onRequest?: (query: string, variables: any) => void; onResponse?: (query: string, duration: number, data: any) => void; onError?: (query: string, duration: number, error: any) => void; } function createDebugClient(apiKey: string, opts: DebugOptions = {}): LinearClient { const { logRequests = true, logResponses = true } = opts; return new LinearClient({ apiKey, headers: { "X-Debug": "true" }, }); } // Manual instrumentation wrapper async function debugQuery<T>( label: string, fn: () => Promise<T>, opts?: DebugOptions ): Promise<T> { const start = Date.now(); console.log(`[Linear:DEBUG] >>> ${label}`); try { const result = await fn(); const ms = Date.now() - start; console.log(`[Linear:DEBUG] <<< ${label} (${ms}ms) OK`); opts?.onResponse?.(label, ms, result); return result; } catch (error) { const ms = Date.now() - start; console.error(`[Linear:DEBUG] !!! ${label} (${ms}ms) FAILED:`, error); opts?.onError?.(label, ms, error); throw error; } } // Usage const client = new LinearClient({ apiKey: process.env.LINEAR_API_KEY! }); const teams = await debugQuery("teams()", () => client.teams()); const issues = await debugQuery("issues(first:50)", () => client.issues({ first: 50 }));
Track all API calls with timing, success/failure, and aggregate stats.
typescriptinterface TraceEntry { id: string; operation: string; startTime: number; endTime?: number; duration?: number; success: boolean; error?: string; } class LinearTracer { private traces: TraceEntry[] = []; private maxTraces = 200; startTrace(operation: string): string { const id = `trace-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`; this.traces.push({ id, operation, startTime: Date.now(), success: false }); if (this.traces.length > this.maxTraces) this.traces = this.traces.slice(-100); return id; } endTrace(id: string, success: boolean, error?: string): void { const trace = this.traces.find(t => t.id === id); if (trace) { trace.endTime = Date.now(); trace.duration = trace.endTime - trace.startTime; trace.success = success; trace.error = error; } } getSlowTraces(thresholdMs = 2000): TraceEntry[] { return this.traces.filter(t => (t.duration ?? 0) > thresholdMs); } getFailedTraces(): TraceEntry[] { return this.traces.filter(t => !t.success && t.endTime); } getSummary() { const completed = this.traces.filter(t => t.endTime); const durations = completed.map(t => t.duration ?? 0); return { total: this.traces.length, completed: completed.length, failed: this.getFailedTraces().length, avgMs: durations.length ? Math.round(durations.reduce((a, b) => a + b, 0) / durations.length) : 0, maxMs: durations.length ? Math.max(...durations) : 0, p95Ms: durations.length ? durations.sort((a, b) => a - b)[Math.floor(durations.length * 0.95)] : 0, }; } } // Usage const tracer = new LinearTracer(); async function tracedCall<T>(operation: string, fn: () => Promise<T>): Promise<T> { const id = tracer.startTrace(operation); try { const result = await fn(); tracer.endTrace(id, true); return result; } catch (error: any) { tracer.endTrace(id, false, error.message); throw error; } } // After running operations: console.log("Trace summary:", tracer.getSummary()); console.log("Slow traces:", tracer.getSlowTraces(1000));
typescriptinterface HealthResult { status: "healthy" | "degraded" | "unhealthy"; latencyMs: number; user?: string; teamCount?: number; error?: string; } async function checkLinearHealth(client: LinearClient): Promise<HealthResult> { const start = Date.now(); try { const [viewer, teams] = await Promise.all([client.viewer, client.teams()]); const latencyMs = Date.now() - start; return { status: latencyMs > 3000 ? "degraded" : "healthy", latencyMs, user: viewer.name, teamCount: teams.nodes.length, }; } catch (error: any) { return { status: "unhealthy", latencyMs: Date.now() - start, error: error.message, }; } } // Express endpoint app.get("/health/linear", async (req, res) => { const health = await checkLinearHealth(client); res.status(health.status === "unhealthy" ? 503 : 200).json(health); });
typescriptfunction validateLinearEnv(): { valid: boolean; issues: string[] } { const issues: string[] = []; const apiKey = process.env.LINEAR_API_KEY; if (!apiKey) { issues.push("LINEAR_API_KEY is not set"); } else if (!apiKey.startsWith("lin_api_")) { issues.push("LINEAR_API_KEY must start with 'lin_api_'"); } else if (apiKey.length < 30) { issues.push("LINEAR_API_KEY appears truncated"); } if (!process.env.LINEAR_WEBHOOK_SECRET) { issues.push("WARNING: LINEAR_WEBHOOK_SECRET not set (webhooks won't verify)"); } if (process.env.NODE_ENV === "production" && apiKey?.includes("dev")) { issues.push("WARNING: API key appears to be a development key in production"); } const valid = issues.filter(i => !i.startsWith("WARNING")).length === 0; return { valid, issues }; } // Auto-run on import const envCheck = validateLinearEnv(); if (!envCheck.valid) { console.error("[Linear] Environment validation failed:"); envCheck.issues.forEach(i => console.error(` - ${i}`)); }
typescriptimport readline from "readline"; import { LinearClient } from "@linear/sdk"; async function debugConsole(client: LinearClient): Promise<void> { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const prompt = () => rl.question("linear> ", handleCommand); async function handleCommand(cmd: string) { const trimmed = cmd.trim(); try { switch (trimmed) { case "me": { const v = await client.viewer; console.log(`${v.name} (${v.email})`); break; } case "teams": { const t = await client.teams(); t.nodes.forEach(team => console.log(` ${team.key}: ${team.name}`)); break; } case "issues": { const i = await client.issues({ first: 10, orderBy: "updatedAt" }); i.nodes.forEach(issue => console.log(` ${issue.identifier}: ${issue.title}`)); break; } case "health": { const h = await checkLinearHealth(client); console.log(JSON.stringify(h, null, 2)); break; } case "exit": rl.close(); return; default: console.log("Commands: me, teams, issues, health, exit"); } } catch (e: any) { console.error(`Error: ${e.message}`); } prompt(); } console.log("Linear Debug Console — type 'help' for commands"); prompt(); }
| Issue | Cause | Solution | |-------|-------|----------| | Circular JSON in logs | Logging full SDK objects | Use selective fields, not JSON.stringify(issue) | | Memory leak | Unbounded trace storage | Set maxTraces limit, trim oldest | | Missing env vars | Env not loaded | Call validateLinearEnv() on startup | | Health check timeout | Network issue or Linear outage | Add 10s timeout, check status.linear.app |
bash# One-liner to test API connectivity and print auth info curl -s -X POST https://api.linear.app/graphql \ -H "Authorization: $LINEAR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"query": "{ viewer { name email } }"}' | jq .
typescriptasync function benchmark(label: string, fn: () => Promise<any>) { const runs = 5; const times: number[] = []; for (let i = 0; i < runs; i++) { const start = Date.now(); await fn(); times.push(Date.now() - start); } const avg = Math.round(times.reduce((a, b) => a + b) / runs); const max = Math.max(...times); console.log(`${label}: avg=${avg}ms, max=${max}ms (${runs} runs)`); } await benchmark("viewer", () => client.viewer); await benchmark("teams", () => client.teams()); await benchmark("issues(50)", () => client.issues({ first: 50 }));
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 29,931 | 25,030 | -16% | 1 | 1 | 0% | 5,426 | 7,025 | +29% | 0 | 0 | — |
case-02 | fail→fail | 17,198 | 18,031 | +5% | 1 | 1 | 0% | 2,676 | 5,460 | +104% | 0 | 0 | — |
case-03 | fail→fail | 22,074 | 26,728 | +21% | 1 | 1 | 0% | 3,452 | 7,300 | +111% | 0 | 0 | — |
case-04 | fail→fail | 24,106 | 21,929 | -9% | 1 | 1 | 0% | 3,765 | 6,038 | +60% | 0 | 0 | — |
case-05 | fail→fail | 23,356 | 19,993 | -14% | 1 | 1 | 0% | 3,249 | 5,358 | +65% | 0 | 0 | — |
case-06 | fail→fail | 21,486 | 18,956 | -12% | 1 | 1 | 0% | 2,921 | 5,162 | +77% | 0 | 0 | — |
case-07 | pass→pass | 21,713 | 17,140 | -21% | 1 | 1 | 0% | 3,071 | 4,636 | +51% | 0 | 0 | — |
case-08 | fail→fail | 15,459 | 21,184 | +37% | 1 | 1 | 0% | 2,288 | 5,316 | +132% | 0 | 0 | — |
case-09 | fail→fail | 21,860 | 16,664 | -24% | 1 | 1 | 0% | 2,557 | 5,989 | +134% | 0 | 0 | — |
case-10 | fail→pass | 15,896 | 4,765 | -70% | 1 | 1 | 0% | 1,463 | 3,512 | +140% | 0 | 0 | — |
case-11 | fail→pass | 13,550 | 7,002 | -48% | 1 | 1 | 0% | 2,474 | 3,744 | +51% | 0 | 0 | — |
case-12 | fail→pass | 14,838 | 15,719 | +6% | 1 | 1 | 0% | 2,113 | 4,363 | +106% | 0 | 0 | — |
case-13 | pass→pass | 19,024 | 32,365 | +70% | 1 | 1 | 0% | 3,723 | 7,788 | +109% | 0 | 0 | — |
case-14 | fail→pass | 18,288 | 21,255 | +16% | 1 | 1 | 0% | 3,216 | 5,540 | +72% | 0 | 0 | — |
case-15 | pass→pass | 18,143 | 10,271 | -43% | 1 | 1 | 0% | 2,609 | 4,159 | +59% | 0 | 0 | — |
case-16 | pass→pass | 10,441 | 4,382 | -58% | 1 | 1 | 0% | 1,022 | 3,583 | +251% | 0 | 0 | — |
case-17 | fail→pass | 18,577 | 24,592 | +32% | 1 | 1 | 0% | 2,877 | 5,591 | +94% | 0 | 0 | — |
case-18 | fail→pass | 21,985 | 1,990 | -91% | 1 | 1 | 0% | 3,803 | 3,071 | -19% | 0 | 0 | — |
case-19 | fail→pass | 19,713 | 2,301 | -88% | 1 | 1 | 0% | 2,449 | 2,992 | +22% | 0 | 0 | — |
case-20 | fail→pass | 19,335 | 18,128 | -6% | 1 | 1 | 0% | 2,153 | 4,899 | +128% | 0 | 0 | — |
case-21 | pass→pass | 11,721 | 16,113 | +37% | 1 | 1 | 0% | 2,402 | 6,083 | +153% | 0 | 0 | — |
case-22 | pass→pass | 22,494 | 27,929 | +24% | 1 | 1 | 0% | 3,920 | 7,269 | +85% | 0 | 0 | — |
case-23 | pass→pass | 22,353 | 30,937 | +38% | 1 | 1 | 0% | 3,547 | 6,937 | +96% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +39 percentage points is the difference between those two pass rates over the 23 comparable cases. 2 cases got worse with the skill loaded, and they are included in that figure.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.