Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Set up monitoring, metrics, and alerting for Figma API integrations. Use when implementing observability for Figma operations, tracking API health, or configuring alerts for rate limits and errors. Trigger with phrases like "figma monitoring", "figma metrics", "figma observability", "figma alerts", "figma dashboard".
.claude/skills/jeremylongshore-figma-observability/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-08 | ✗→✓ | ▲ Improved | 55% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 219% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 154% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 118% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 100% | 0% |
Monitor Figma REST API health with custom metrics, structured logging, and alerts. Track request latency, error rates, rate limit headroom, and cache hit rates.
typescript// Wrap every Figma API call with metrics and logging class InstrumentedFigmaClient { private metrics = { requests: 0, errors: 0, rateLimits: 0, totalLatencyMs: 0, }; async request<T>(path: string, token: string): Promise<T> { const start = performance.now(); const endpoint = path.replace(/[a-zA-Z0-9]{15,}/, ':key'); // normalize try { const res = await fetch(`https://api.figma.com${path}`, { headers: { 'X-Figma-Token': token }, }); const latencyMs = performance.now() - start; this.metrics.requests++; this.metrics.totalLatencyMs += latencyMs; // Log every request with structured data console.log(JSON.stringify({ service: 'figma', endpoint, status: res.status, latencyMs: Math.round(latencyMs), rateLimit: { remaining: res.headers.get('X-RateLimit-Remaining'), type: res.headers.get('X-Figma-Rate-Limit-Type'), }, })); if (res.status === 429) { this.metrics.rateLimits++; const retryAfter = parseInt(res.headers.get('Retry-After') || '60'); throw new FigmaRateLimitError(retryAfter); } if (!res.ok) { this.metrics.errors++; throw new FigmaApiError(res.status, await res.text()); } return res.json(); } catch (error) { if (!(error instanceof FigmaApiError)) { this.metrics.errors++; console.error(JSON.stringify({ service: 'figma', endpoint, error: error instanceof Error ? error.message : 'Unknown', latencyMs: Math.round(performance.now() - start), })); } throw error; } } getMetrics() { return { ...this.metrics, avgLatencyMs: this.metrics.requests > 0 ? Math.round(this.metrics.totalLatencyMs / this.metrics.requests) : 0, errorRate: this.metrics.requests > 0 ? (this.metrics.errors / this.metrics.requests * 100).toFixed(1) + '%' : '0%', }; } }
typescriptimport { Registry, Counter, Histogram, Gauge } from 'prom-client'; const registry = new Registry(); const figmaRequests = new Counter({ name: 'figma_api_requests_total', help: 'Total Figma API requests', labelNames: ['endpoint', 'status'], registers: [registry], }); const figmaLatency = new Histogram({ name: 'figma_api_request_duration_seconds', help: 'Figma API request duration in seconds', labelNames: ['endpoint'], buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10], registers: [registry], }); const figmaRateLimitRemaining = new Gauge({ name: 'figma_rate_limit_remaining', help: 'Remaining Figma API rate limit', registers: [registry], }); const figmaCacheHits = new Counter({ name: 'figma_cache_hits_total', help: 'Figma cache hits vs misses', labelNames: ['result'], // 'hit' or 'miss' registers: [registry], }); // Expose /metrics endpoint app.get('/metrics', async (req, res) => { res.set('Content-Type', registry.contentType); res.send(await registry.metrics()); });
yaml# prometheus-alerts.yml groups: - name: figma rules: - alert: FigmaHighErrorRate expr: | rate(figma_api_requests_total{status=~"4..|5.."}[5m]) / rate(figma_api_requests_total[5m]) > 0.05 for: 5m labels: { severity: warning } annotations: summary: "Figma API error rate > 5% for 5 minutes" - alert: FigmaRateLimited expr: figma_rate_limit_remaining < 5 for: 1m labels: { severity: warning } annotations: summary: "Figma rate limit nearly exhausted" - alert: FigmaHighLatency expr: | histogram_quantile(0.95, rate(figma_api_request_duration_seconds_bucket[5m]) ) > 5 for: 5m labels: { severity: warning } annotations: summary: "Figma API P95 latency > 5 seconds" - alert: FigmaAuthFailure expr: figma_api_requests_total{status="403"} > 0 for: 1m labels: { severity: critical } annotations: summary: "Figma auth failures detected (possible expired PAT)"
typescriptasync function figmaHealthCheck(): Promise<{ status: 'healthy' | 'degraded' | 'unhealthy'; details: Record<string, any>; }> { const start = Date.now(); try { const res = await fetch('https://api.figma.com/v1/me', { headers: { 'X-Figma-Token': process.env.FIGMA_PAT! }, signal: AbortSignal.timeout(5000), }); const latencyMs = Date.now() - start; const remaining = res.headers.get('X-RateLimit-Remaining'); return { status: res.ok ? (latencyMs > 3000 ? 'degraded' : 'healthy') : 'degraded', details: { authenticated: res.ok, latencyMs, rateLimitRemaining: remaining ? parseInt(remaining) : null, planTier: res.headers.get('X-Figma-Plan-Tier'), }, }; } catch { return { status: 'unhealthy', details: { authenticated: false, latencyMs: Date.now() - start }, }; } }
| Issue | Cause | Solution | |-------|-------|----------| | High cardinality | Too many label values | Normalize endpoint paths | | Alert storms | Threshold too low | Tune for duration and thresholds | | Missing rate limit headers | Not all endpoints return them | Handle null values gracefully | | Metrics not scraping | Wrong port or path | Verify Prometheus scrape config |
Scrape the instrumented client's metrics (Step 2) and confirm request tracking works:
bashcurl -s localhost:9090/metrics | /usr/bin/grep figma_
textfigma_api_requests_total{endpoint="/v1/files",status="200"} 1042 figma_api_requests_total{endpoint="/v1/files",status="429"} 3 figma_api_request_duration_seconds_bucket{le="0.5"} 981 figma_rate_limit_remaining 118
Fire the health check with dependency detail (Step 4):
bashcurl -s localhost:3000/health | jq '{status, figma: .checks.figma_api}' # {"status": "ok", "figma": {"reachable": true, "latency_ms": 212}}
Alert thresholds (429 rate, p95 latency) are in references/alert-rules.md.
For incident response, see figma-incident-runbook.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 36,568 | 35,935 | -2% | 1 | 1 | 0% | 8,250 | 7,755 | -6% | 0 | 0 | — |
case-02 | fail→fail | 20,594 | 22,844 | +11% | 1 | 1 | 0% | 4,456 | 5,963 | +34% | 0 | 0 | — |
case-03 | fail→fail | 19,539 | 11,379 | -42% | 1 | 1 | 0% | 4,169 | 4,681 | +12% | 0 | 0 | — |
case-04 | pass→pass | 13,239 | 15,426 | +17% | 1 | 1 | 0% | 2,674 | 4,627 | +73% | 0 | 0 | — |
case-05 | fail→fail | 13,301 | 8,807 | -34% | 1 | 1 | 0% | 2,164 | 3,881 | +79% | 0 | 0 | — |
case-06 | pass→pass | 6,641 | 4,994 | -25% | 1 | 1 | 0% | 1,232 | 3,037 | +147% | 0 | 0 | — |
case-07 | pass→pass | 18,192 | 14,374 | -21% | 1 | 1 | 0% | 3,684 | 5,275 | +43% | 0 | 0 | — |
case-08 | fail→pass | 16,330 | 12,788 | -22% | 1 | 1 | 0% | 2,741 | 4,261 | +55% | 0 | 0 | — |
case-09 | pass→fail | 727,383 | 8,171 | -99% | 1 | 1 | 0% | 1,954 | 3,398 | +74% | 0 | 0 | — |
case-10 | pass→pass | 13,226 | 5,626 | -57% | 1 | 1 | 0% | 2,953 | 3,291 | +11% | 0 | 0 | — |
case-11 | fail→pass | 5,009 | 4,535 | -9% | 1 | 1 | 0% | 937 | 2,993 | +219% | 0 | 0 | — |
case-12 | pass→pass | 6,067 | 4,743 | -22% | 1 | 1 | 0% | 1,269 | 3,043 | +140% | 0 | 0 | — |
case-13 | fail→fail | 9,390 | 5,639 | -40% | 1 | 1 | 0% | 1,711 | 3,283 | +92% | 0 | 0 | — |
case-14 | pass→pass | 6,025 | 3,717 | -38% | 1 | 1 | 0% | 1,126 | 2,873 | +155% | 0 | 0 | — |
case-15 | fail→pass | 7,218 | 5,614 | -22% | 1 | 1 | 0% | 1,296 | 3,290 | +154% | 0 | 0 | — |
case-16 | pass→pass | 7,948 | 6,786 | -15% | 1 | 1 | 0% | 1,555 | 3,314 | +113% | 0 | 0 | — |
case-17 | fail→pass | 7,255 | 5,086 | -30% | 1 | 1 | 0% | 1,472 | 3,208 | +118% | 0 | 0 | — |
case-18 | fail→pass | 8,272 | 4,756 | -43% | 1 | 1 | 0% | 1,485 | 2,970 | +100% | 0 | 0 | — |
case-19 | fail→fail | 14,878 | 13,639 | -8% | 1 | 1 | 0% | 2,574 | 4,858 | +89% | 0 | 0 | — |
case-20 | fail→fail | 12,206 | 5,164 | -58% | 1 | 1 | 0% | 1,996 | 3,050 | +53% | 0 | 0 | — |
case-21 | pass→pass | 9,158 | 3,474 | -62% | 1 | 1 | 0% | 1,635 | 2,705 | +65% | 0 | 0 | — |
case-22 | fail→fail | 5,182 | 3,308 | -36% | 1 | 1 | 0% | 871 | 2,666 | +206% | 0 | 0 | — |
case-23 | pass→pass | 17,399 | 9,408 | -46% | 1 | 1 | 0% | 3,077 | 3,940 | +28% | 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 +17 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is 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.