agentsclimarketplace

Canva observability

Skill jeremylongshore/claude-code-plugins-plus-skills/plugins/saas-packs/canva-pack/skills/canva-observability

425 plugins, 2,810 skills, 200 agents for Claude Code. Open-source marketplace at tonsofskills.com with the ccpi CLI package manager.

Install
npx -y skills add jeremylongshore/claude-code-plugins-plus-skills --skill canva-observability

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

What its author says it does

Copied from the file, not written here

'Set up observability for Canva Connect API integrations with metrics, traces, and alerts.

The file declares its own license as MIT. That is the author’s claim about this one file, and it is not the same thing as the license GitHub reports for the repository, which is listed with the other numbers below.

SKILL.md

6.9 KB, as published. Nobody here has run it

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.

Key Metrics

MetricTypeLabelsDescription
canva_api_requests_totalCountermethod, endpoint, statusTotal API calls
canva_api_duration_secondsHistogrammethod, endpointRequest latency
canva_api_errors_totalCounterendpoint, error_codeError count
canva_export_duration_secondsHistogramformatExport completion time
canva_token_refresh_totalCounterstatusToken refresh attempts
canva_rate_limit_remainingGaugeendpointRate limit headroom

Prometheus Instrumentation

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

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

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

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

# 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]))

Error Handling

IssueCauseSolution
Missing metricsNo instrumentationWrap all API calls
High cardinalityToo many label valuesUse endpoint patterns, not full paths
Alert stormsThresholds too sensitiveTune for-duration and threshold
Token in logsMissing redactionNever log Authorization headers

Resources

Next Steps

For incident response, see canva-incident-runbook.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.