From shopify-pack
Instruments Shopify apps with Prometheus metrics for GraphQL query costs, rate limits, webhook deliveries, API latency, and errors. Requires pino logging and API client interception.
How this skill is triggered — by the user, by Claude, or both
Slash command
/shopify-pack:shopify-observabilityThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
Instrument your Shopify app to track GraphQL query cost, rate limit consumption, webhook delivery success, and API latency. Shopify-specific metrics that generic monitoring misses.
Instrument your Shopify app to track GraphQL query cost, rate limit consumption, webhook delivery success, and API latency. Shopify-specific metrics that generic monitoring misses.
import { Registry, Counter, Histogram, Gauge } from "prom-client";
const registry = new Registry();
// GraphQL query cost tracking
const queryCostHistogram = new Histogram({
name: "shopify_graphql_query_cost",
help: "Shopify GraphQL actual query cost",
labelNames: ["operation", "shop"],
buckets: [1, 10, 50, 100, 250, 500, 1000],
registers: [registry],
});
// Rate limit headroom
const rateLimitGauge = new Gauge({
name: "shopify_rate_limit_available",
help: "Shopify rate limit points currently available",
labelNames: ["shop", "api_type"],
registers: [registry],
});
// REST bucket state
const restBucketGauge = new Gauge({
name: "shopify_rest_bucket_used",
help: "REST API leaky bucket current fill level",
labelNames: ["shop"],
registers: [registry],
});
// API request duration
const apiDuration = new Histogram({
name: "shopify_api_duration_seconds",
help: "Shopify API call duration",
labelNames: ["operation", "status", "api_type"],
buckets: [0.1, 0.25, 0.5, 1, 2, 5, 10],
registers: [registry],
});
// Webhook processing
const webhookCounter = new Counter({
name: "shopify_webhooks_total",
help: "Shopify webhooks received",
labelNames: ["topic", "status"], // status: success, error, invalid_hmac
registers: [registry],
});
// API errors by type
const apiErrors = new Counter({
name: "shopify_api_errors_total",
help: "Shopify API errors by type",
labelNames: ["error_type", "status_code"],
registers: [registry],
});
async function instrumentedGraphqlQuery<T>(
shop: string,
operation: string,
query: string,
variables?: Record<string, unknown>
): Promise<T> {
const timer = apiDuration.startTimer({ operation, api_type: "graphql" });
try {
const client = getGraphqlClient(shop);
const response = await client.request(query, { variables });
// Record cost metrics from Shopify's response
const cost = response.extensions?.cost;
if (cost) {
queryCostHistogram.observe(
{ operation, shop },
cost.actualQueryCost || cost.requestedQueryCost
);
rateLimitGauge.set(
{ shop, api_type: "graphql" },
cost.throttleStatus.currentlyAvailable
);
}
timer({ status: "success" });
return response.data as T;
} catch (error: any) {
const statusCode = error.response?.code || "unknown";
const errorType =
error.body?.errors?.[0]?.extensions?.code === "THROTTLED"
? "throttled"
: statusCode === 401
? "auth"
: "api_error";
apiErrors.inc({ error_type: errorType, status_code: String(statusCode) });
timer({ status: "error" });
throw error;
}
}
function trackRestHeaders(shop: string, headers: Record<string, string>): void {
// Parse X-Shopify-Shop-Api-Call-Limit: "32/40"
const callLimit = headers["x-shopify-shop-api-call-limit"];
if (callLimit) {
const [used, max] = callLimit.split("/").map(Number);
restBucketGauge.set({ shop }, used);
if (used > max * 0.8) {
console.warn(`[shopify] REST bucket at ${used}/${max} for ${shop}`);
}
}
}
app.post("/webhooks", express.raw({ type: "application/json" }), (req, res) => {
const topic = req.headers["x-shopify-topic"] as string;
const shop = req.headers["x-shopify-shop-domain"] as string;
// Track HMAC validation
if (!verifyHmac(req.body, req.headers["x-shopify-hmac-sha256"]!)) {
webhookCounter.inc({ topic, status: "invalid_hmac" });
return res.status(401).send();
}
res.status(200).send("OK");
// Track processing
const start = Date.now();
processWebhook(topic, shop, JSON.parse(req.body.toString()))
.then(() => {
webhookCounter.inc({ topic, status: "success" });
apiDuration.observe(
{ operation: `webhook:${topic}`, status: "success", api_type: "webhook" },
(Date.now() - start) / 1000
);
})
.catch((err) => {
webhookCounter.inc({ topic, status: "error" });
console.error(`Webhook ${topic} failed:`, err.message);
});
});
import pino from "pino";
const logger = pino({
name: "shopify-app",
level: process.env.LOG_LEVEL || "info",
serializers: {
// Redact sensitive fields automatically
shopifyRequest: (req: any) => ({
shop: req.shop,
operation: req.operation,
queryCost: req.cost?.actualQueryCost,
available: req.cost?.throttleStatus?.currentlyAvailable,
// Never log: accessToken, apiSecret, customer PII
}),
},
});
// Log every Shopify API call with structured context
function logShopifyCall(operation: string, shop: string, cost: any, durationMs: number) {
logger.info({
msg: "shopify_api_call",
operation,
shop,
queryCost: cost?.actualQueryCost,
requestedCost: cost?.requestedQueryCost,
availablePoints: cost?.throttleStatus?.currentlyAvailable,
durationMs,
});
}
# prometheus/shopify-alerts.yml
groups:
- name: shopify
rules:
- alert: ShopifyRateLimitLow
expr: shopify_rate_limit_available < 100
for: 2m
labels:
severity: warning
annotations:
summary: "Shopify rate limit below 100 points for {{ $labels.shop }}"
- alert: ShopifyHighQueryCost
expr: histogram_quantile(0.95, rate(shopify_graphql_query_cost_bucket[5m])) > 500
for: 5m
labels:
severity: warning
annotations:
summary: "P95 Shopify query cost > 500 points"
- alert: ShopifyWebhookFailures
expr: rate(shopify_webhooks_total{status="error"}[5m]) > 0.1
for: 5m
labels:
severity: critical
annotations:
summary: "Shopify webhook processing failures > 10%"
- alert: ShopifyAPILatencyHigh
expr: histogram_quantile(0.95, rate(shopify_api_duration_seconds_bucket[5m])) > 3
for: 5m
labels:
severity: warning
annotations:
summary: "Shopify API P95 latency > 3 seconds"
| Issue | Cause | Solution |
|---|---|---|
| Missing cost data | Query error before response | Check error handling wraps correctly |
| High cardinality | Per-shop labels | Aggregate by plan tier instead |
| Alert storms | Aggressive thresholds | Tune based on baseline traffic |
| Webhook metrics missing | Not instrumented | Add counter to webhook handler |
app.get("/metrics", async (req, res) => {
res.set("Content-Type", registry.contentType);
res.send(await registry.metrics());
});
For incident response, see shopify-incident-runbook.
npx claudepluginhub jeremylongshore/claude-code-plugins-plus-skills --plugin shopify-packDebugs complex Shopify API issues with GraphQL cost analysis, request tracing via X-Request-Id, webhook delivery checks, and introspection. For throttling, mystery errors, intermittent failures.
Provides patterns for Shopify app development using Remix/React Router, embedded apps with App Bridge, webhooks, GraphQL Admin API, Polaris components, billing, and extensions.
Sets up Prometheus metrics, OpenTelemetry tracing, pino logging, and Grafana dashboards for Webflow API integrations. Monitors rate limits, errors, latency, and CMS items.