clickup-observability

'Monitor ClickUp API integrations with metrics, tracing, structured logging,

3 Tools
clickup-pack Plugin
saas packs Category

Allowed Tools

ReadWriteEdit

Provided by Plugin

clickup-pack

Claude Code skill pack for ClickUp (24 skills)

saas packs v1.0.0
View Plugin

Installation

This skill is included in the clickup-pack plugin:

/plugin install clickup-pack@claude-code-plugins-plus

Click to copy

Instructions

ClickUp Observability

Overview

Monitor ClickUp API v2 integrations with metrics (request rate, latency, errors, rate limit usage), distributed tracing, and alerting.

Key Metrics

Metric Type Labels Description
clickuprequeststotal Counter method, endpoint, status Total API requests
clickuprequestduration_seconds Histogram method, endpoint Request latency
clickuperrorstotal Counter statuscode, errortype Errors by type
clickupratelimit_remaining Gauge token_hash Rate limit headroom
clickupratelimitresetstotal Counter Times we hit 429

Prometheus Instrumentation


import { Registry, Counter, Histogram, Gauge } from 'prom-client';

const registry = new Registry();

const requestCounter = new Counter({
  name: 'clickup_requests_total',
  help: 'Total ClickUp API v2 requests',
  labelNames: ['method', 'endpoint', 'status'] as const,
  registers: [registry],
});

const requestDuration = new Histogram({
  name: 'clickup_request_duration_seconds',
  help: 'ClickUp API request duration in seconds',
  labelNames: ['method', 'endpoint'] as const,
  buckets: [0.05, 0.1, 0.25, 0.5, 1, 2, 5],
  registers: [registry],
});

const rateLimitGauge = new Gauge({
  name: 'clickup_rate_limit_remaining',
  help: 'ClickUp rate limit remaining requests',
  registers: [registry],
});

const errorCounter = new Counter({
  name: 'clickup_errors_total',
  help: 'ClickUp API errors by status code',
  labelNames: ['status_code', 'error_type'] as const,
  registers: [registry],
});

Instrumented Client


async function instrumentedClickUpRequest<T>(
  path: string,
  options: RequestInit = {}
): Promise<T> {
  const method = options.method ?? 'GET';
  // Normalize endpoint for cardinality control (replace UUIDs)
  const endpoint = path.replace(/\/[a-zA-Z0-9]{6,}(?=\/|$|\?)/g, '/:id');
  const timer = requestDuration.startTimer({ method, endpoint });

  try {
    const response = await fetch(`https://api.clickup.com/api/v2${path}`, {
      ...options,
      headers: {
        'Authorization': process.env.CLICKUP_API_TOKEN!,
        'Content-Type': 'application/json',
        ...options.headers,
      },
    });

    // Update rate limit gauge
    const remaining = response.headers.get('X-RateLimit-Remaining');
    if (remaining) rateLimitGauge.set(parseInt(remaining));

    const status = response.ok ? 'success' : `${response.status}`;
    requestCounter.inc({ method, endpoint, status });

    if (!response.ok) {
      const body = await response.json().catch(() => ({}));
      errorCounter.inc({
        status_code: String(response.status),
        error_type: body.ECODE ?? 'unknown',
      });
      throw new Error(`ClickUp ${response.status}: ${body.err}`);
    }

    return response.json();
  } catch (error) {
    if (!(error instanceof Error && error.message.startsWith('ClickUp'))) {
      errorCounter.inc({ status_code: 'network', error_type: 'fetch_error' });
    }
    throw error;
  } finally {
    timer();
  }
}

OpenTelemetry Tracing


import { trace, SpanStatusCode } from '@opentelemetry/api';

const tracer = trace.getTracer('clickup-integration', '1.0.0');

async function tracedClickUpCall<T>(
  operationName: string,
  path: string,
  fn: () => Promise<T>
): Promise<T> {
  return tracer.startActiveSpan(`clickup.${operationName}`, async (span) => {
    span.setAttribute('clickup.path', path);
    span.setAttribute('clickup.method', 'GET');

    try {
      const result = await fn();
      span.setStatus({ code: SpanStatusCode.OK });
      return result;
    } catch (error: any) {
      span.setStatus({ code: SpanStatusCode.ERROR, message: error.message });
      span.recordException(error);
      throw error;
    } finally {
      span.end();
    }
  });
}

Structured Logging


import pino from 'pino';

const logger = pino({ name: 'clickup', level: process.env.LOG_LEVEL ?? 'info' });

function logClickUpCall(data: {
  method: string;
  path: string;
  status: number;
  durationMs: number;
  rateLimitRemaining?: number;
  error?: string;
}): void {
  const level = data.status >= 500 ? 'error' : data.status >= 400 ? 'warn' : 'info';
  logger[level]({
    service: 'clickup',
    ...data,
  }, `ClickUp ${data.method} ${data.path} -> ${data.status} (${data.durationMs}ms)`);
}

Alert Rules


# prometheus/clickup_alerts.yaml
groups:
  - name: clickup
    rules:
      - alert: ClickUpHighErrorRate
        expr: rate(clickup_errors_total[5m]) / rate(clickup_requests_total[5m]) > 0.05
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "ClickUp API error rate > 5%"

      - alert: ClickUpHighLatency
        expr: histogram_quantile(0.95, rate(clickup_request_duration_seconds_bucket[5m])) > 3
        for: 5m
        labels: { severity: warning }
        annotations:
          summary: "ClickUp P95 latency > 3s"

      - alert: ClickUpRateLimitLow
        expr: clickup_rate_limit_remaining < 10
        for: 1m
        labels: { severity: critical }
        annotations:
          summary: "ClickUp rate limit nearly exhausted"

      - alert: ClickUpAuthFailures
        expr: increase(clickup_errors_total{status_code="401"}[5m]) > 0
        labels: { severity: critical }
        annotations:
          summary: "ClickUp authentication failures detected"

Metrics Endpoint


app.get('/metrics', async (req, res) => {
  res.set('Content-Type', registry.contentType);
  res.send(await registry.metrics());
});

Error Handling

Issue Cause Solution
High cardinality Dynamic IDs in labels Normalize paths (replace IDs with :id)
Missing metrics Uninstrumented code path Wrap all API calls through instrumented client
Alert storm Threshold too sensitive Tune for duration and threshold
Trace gaps Missing context propagation Ensure span context is passed

Resources

Next Steps

For incident response, see clickup-incident-runbook.

Ready to use clickup-pack?