'Set up observability for Canva Connect API integrations with metrics,
Scanned 9/2/2026
Install to Claude Code
npx -y skills add jeremylongshore/tons-of-skills-marketplace --skill canva-observability --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Canva Observability?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/jeremylongshore-canva-observability-tons-of-skills-marketplace)More formats (shields.io, HTML) on the badges page.
---
name: canva-observability
description: 'Set up observability for Canva Connect API integrations with metrics,
traces, and alerts.
Use when implementing monitoring for Canva API operations, setting up dashboards,
or configuring alerting for Canva integration health.
Trigger with phrases like "canva monitoring", "canva metrics",
"canva observability", "monitor canva", "canva alerts", "canva tracing".
'
allowed-tools: Read, Write, Edit
version: 1.5.0
license: MIT
author: Jeremy Longshore <jeremy@intentsolutions.io>
tags:
- saas
- design
- canva
compatibility: Designed for Claude Code
---
# Canva Observability
## Overview
Instrument Canva Connect API calls with metrics, traces, and structured logging. Track latency, error rates, rate limit headroom, and export job completion times.
## Prerequisites
- A protected telemetry destination, retention policy, and alert owner.
- A documented metric allowlist that excludes OAuth values, design contents, signed URLs, and personal identifiers.
## Instructions
1. Emit aggregate endpoint/status/latency metrics and opaque request identifiers only.
2. Redact errors before logs/traces, bound log retention, and restrict dashboards to authorized operators.
3. Alert on sustained authorization, rate, export, or webhook failures and link to the incident runbook.
4. Do not automate retries or scope changes from a metric alone; reconcile through the protected operation ledger.
## Key Metrics
| Metric | Type | Labels | Description |
|--------|------|--------|-------------|
| `canva_api_requests_total` | Counter | `method`, `endpoint`, `status` | Total API calls |
| `canva_api_duration_seconds` | Histogram | `method`, `endpoint` | Request latency |
| `canva_api_errors_total` | Counter | `endpoint`, `error_code` | Error count |
| `canva_export_duration_seconds` | Histogram | `format` | Export completion time |
| `canva_token_refresh_total` | Counter | `status` | Token refresh attempts |
| `canva_rate_limit_remaining` | Gauge | `endpoint` | Rate limit headroom |
## Prometheus Instrumentation
```typescript
import { Registry, Counter, Histogram, Gauge } from 'prom-client';
const registry = new Registry();
const requestCounter = new Counter({
name: 'canva_api_requests_total',
help: 'Total Canva Connect API requests',
labelNames: ['method', 'endpoint', 'status'],
registers: [registry],
});
const requestDuration = new Histogram({
name: 'canva_api_duration_seconds',
help: 'Canva API request duration',
labelNames: ['method', 'endpoint'],
buckets: [0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10],
registers: [registry],
});
const rateLimitGauge = new Gauge({
name: 'canva_rate_limit_remaining',
help: 'Remaining rate limit for endpoint',
labelNames: ['endpoint'],
registers: [registry],
});
const exportDuration = new Histogram({
name: 'canva_export_duration_seconds',
help: 'Time from export request to completion',
labelNames: ['format'],
buckets: [1, 2, 5, 10, 20, 30, 60],
registers: [registry],
});
```
## Instrumented Client Wrapper
```typescript
async function instrumentedCanvaRequest<T>(
method: string,
endpoint: string,
fn: () => Promise<Response>
): Promise<T> {
const timer = requestDuration.startTimer({ method, endpoint });
try {
const res = await fn();
// Track rate limit headroom
const remaining = res.headers.get('X-RateLimit-Remaining');
if (remaining) {
rateLimitGauge.set({ endpoint }, parseInt(remaining));
}
const status = res.ok ? 'success' : `error_${res.status}`;
requestCounter.inc({ method, endpoint, status });
if (!res.ok) {
const body = await res.text();
throw new CanvaAPIError(res.status, body, endpoint);
}
return res.json();
} catch (error) {
requestCounter.inc({ method, endpoint, status: 'exception' });
throw error;
} finally {
timer();
}
}
```
## OpenTelemetry Tracing
```typescript
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('canva-connect-api');
async function tracedCanvaCall<T>(
operationName: string,
fn: () => Promise<T>
): Promise<T> {
return tracer.startActiveSpan(`canva.${operationName}`, async (span) => {
span.setAttribute('canva.base_url', 'api.canva.com/rest/v1');
try {
const result = await fn();
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error: any) {
span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
span.setAttribute('canva.error_code', error.status || 'unknown');
span.recordException(error);
throw error;
} finally {
span.end();
}
});
}
```
## Structured Logging
```typescript
import pino from 'pino';
const logger = pino({ name: 'canva', level: process.env.LOG_LEVEL || 'info' });
function logCanvaRequest(data: {
method: string;
endpoint: string;
status: number;
durationMs: number;
rateLimitRemaining?: number;
}) {
// NEVER log access tokens or refresh tokens
logger.info({
service: 'canva-connect-api',
...data,
});
}
```
## Alert Rules
```yaml
# prometheus/canva-alerts.yml
groups:
- name: canva_connect_api
rules:
- alert: CanvaHighErrorRate
expr: |
rate(canva_api_errors_total[5m]) /
rate(canva_api_requests_total[5m]) > 0.05
for: 5m
labels:
severity: warning
annotations:
summary: "Canva API error rate > 5%"
- alert: CanvaHighLatency
expr: |
histogram_quantile(0.95,
rate(canva_api_duration_seconds_bucket[5m])
) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Canva API P95 latency > 3s"
- alert: CanvaRateLimitLow
expr: canva_rate_limit_remaining < 5
for: 1m
labels:
severity: warning
annotations:
summary: "Canva rate limit nearly exhausted"
- alert: CanvaTokenRefreshFailing
expr: increase(canva_token_refresh_total{status="error"}[15m]) > 0
labels:
severity: critical
annotations:
summary: "Canva token refresh failing — users may lose access"
- alert: CanvaExportSlow
expr: |
histogram_quantile(0.95,
rate(canva_export_duration_seconds_bucket[15m])
) > 30
for: 10m
labels:
severity: warning
annotations:
summary: "Canva exports taking > 30s at P95"
```
## Grafana Dashboard Queries
```
# Request rate by endpoint
rate(canva_api_requests_total[5m])
# P95 latency
histogram_quantile(0.95, rate(canva_api_duration_seconds_bucket[5m]))
# Error rate percentage
100 * rate(canva_api_requests_total{status=~"error.*"}[5m]) / rate(canva_api_requests_total[5m])
# Rate limit headroom
canva_rate_limit_remaining
# Export completion time P50/P95
histogram_quantile(0.5, rate(canva_export_duration_seconds_bucket[5m]))
histogram_quantile(0.95, rate(canva_export_duration_seconds_bucket[5m]))
```
## Output
Observability produces redacted health, aggregate latency/error/rate indicators, alert state, and trace references. It contains no tokens, design text, asset URLs, raw payloads, or user profile data.
## Examples
When export failures cross the configured threshold, issue one deduplicated alert containing the aggregate count and trace reference, pause dependent exports, and invoke the incident runbook. Do not attach raw Canva responses or a design identifier to a broadly visible alert.
## Error Handling
| Issue | Cause | Solution |
|-------|-------|----------|
| Missing metrics | No instrumentation | Wrap all API calls |
| High cardinality | Too many label values | Use endpoint patterns, not full paths |
| Alert storms | Thresholds too sensitive | Tune for-duration and threshold |
| Token in logs | Missing redaction | Never log Authorization headers |
## Resources
- Canva API Reference
- [Prometheus](https://prometheus.io/docs/practices/naming/)
- [OpenTelemetry](https://opentelemetry.io/docs/)
## Next Steps
For incident response, see `canva-incident-runbook`.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!