Skip to content
Back to skills

Backend Conventions

ASecurity

Sentry backend conventions for logging, tracing/spans, span/tag attribute naming, metrics tags, and the options system. Use when adding or editing Python in src/ that logs (logger.info/exception), records metrics (metrics.incr/timing with tags), instruments spans/transactions, calls sentry_sdk.set_tag/set_attribute or set_span_tag/set_span_data, or reads registered options with options.get(). Trigger on "add logging", "log an error", "add a metric", "add a span", "instrument tracing", "set an...

  • 44,888 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 1, 2026
devopspythonbackend

Security analysis

A100/100

Scanned September 24, 2026

npx -y skills add getsentry/sentry --skill backend-conventions --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Backend Conventions?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Backend Conventions
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/getsentry-backend-conventions/badge)](https://www.skillsdirectory.com/skills/getsentry-backend-conventions)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: backend-conventions
description: Sentry backend conventions for logging, tracing/spans, span/tag attribute naming, metrics tags, and the options system. Use when adding or editing Python in src/ that logs (logger.info/exception), records metrics (metrics.incr/timing with tags), instruments spans/transactions, calls sentry_sdk.set_tag/set_attribute or set_span_tag/set_span_data, or reads registered options with options.get(). Trigger on "add logging", "log an error", "add a metric", "add a span", "instrument tracing", "set an attribute", "add a tag", "read an option", "LOG005", "LOG011", or metrics tag cardinality questions.
---

# Backend Conventions: Logging, Tracing, Metrics, Options

## Options System

Sentry uses a centralized options system where all options are registered in `src/sentry/options/defaults.py` with required default values.

```python
# CORRECT: options.get() without default - registered default is used
from sentry import options

batch_size = options.get("deletions.group-hash-metadata.batch-size")

# WRONG: Redundant default value
batch_size = options.get("deletions.group-hash-metadata.batch-size", 1000)
```

**Important**: Never add a default value to `options.get()` calls. All options are registered via `register()` in `defaults.py` which requires a default value. The options system always returns the registered default if no value is set, making a second default parameter redundant and potentially inconsistent.

## Logging Pattern

```python
import logging
from sentry import analytics
from sentry.analytics.events.feature_used import FeatureUsedEvent  # does not exist, only for demonstration purposes

logger = logging.getLogger(__name__)

# Structured logging
logger.info(
    "user.action.complete",
    extra={
        "user_id": user.id,
        "action": "login",
        "ip_address": request.META.get("REMOTE_ADDR"),
    }
)

# IMPORTANT: LOG005 use exception() within an exception handler
# WRONG: Calling logger.error() when capturing exception
try:
    risky_operation()
except ValidationError as e:
    logger.error("error.invalid_payload")

# RIGHT: Use logger.exception() with a message when capturing an exception
try:
    risky_operation()
except ValidationError:
    logger.exception("error.invalid_payload")

# IMPORTANT: Avoid LOG011 - Never pre-format log messages with f-strings or .format()
# WRONG: Pre-formatting evaluates before logger call, even if logging is disabled
logger.info(f"User {user.id} completed {action}")
logger.info("User {} completed {}".format(user.id, action))

# RIGHT: Use logger's %-formatting for lazy evaluation
logger.info("%s.user.action.complete", PREFIX)

# ALSO RIGHT: Use structured logging with extra parameters only
logger.info(
    "user.action.complete", extra={"user_id": user.id}
)

# Analytics event
analytics.record(
    FeatureUsedEvent(
        user_id=user.id,
        organization_id=org.id,
        feature="new-dashboard",
    )
)
```

## Span / Tag Attribute Names

Before inventing a key for `sentry_sdk.set_tag`/`set_attribute`, `set_span_tag`, or `set_span_data`, check whether OTel or Sentry already has a standard name for it in `sentry_conventions.attributes.ATTRIBUTE_NAMES`. Reusing a convention name keeps the attribute queryable and consistent with what other producers (SDKs, Relay) already emit for the same concept — a bespoke name fragments the same data across two keys.

```python
from sentry_conventions.attributes import ATTRIBUTE_NAMES

# WRONG: inventing a name for a concept the conventions already cover
sentry_sdk.set_attribute("request_user_agent", user_agent)

# RIGHT: use the existing convention name
sentry_sdk.set_attribute(ATTRIBUTE_NAMES.USER_AGENT_ORIGINAL, user_agent)
```

`ATTRIBUTE_NAMES` is generated from the OTel semantic conventions plus Sentry's own model (`.venv/lib/python*/site-packages/sentry_conventions/attributes.py`); grep it for candidate keywords before adding a new one. Only fall back to a custom key when the concept genuinely isn't covered, and prefer a namespaced, descriptive name over a generic one. A key kept behind a `_test`/POC suffix while a feature is unreleased is a separate, deliberate case — that's about hiding the field, not about picking its name.

## Metrics Tags

Every distinct tag-value combination is a separate time series, so keep tags **low-cardinality, meaningful, and minimal**:

- Add a tag only if you'll actually filter or group by it. Fewer is better.
- Tag values must be bounded/enumerable (e.g. `status`, `platform`, `reason`) — never unbounded identifiers (IDs, emails, URLs, free text).

The middleware (`src/sentry/metrics/middleware.py`) enforces this by denylisting tag keys that **end in `_id`** or that are exactly **`event`/`project`/`group`**. Such tags **will not work**: they're silently stripped by default, and raise `BadMetricTags` when `SENTRY_METRICS_DISALLOW_BAD_TAGS` is on (e.g. CI) — so a metric that looks fine locally can fail elsewhere.

```python
metrics.incr("my.metric", tags={"project_id": project.id})   # WRONG: stripped / raises
metrics.incr("my.metric", tags={"platform": project.platform})  # RIGHT: bounded values
```

A few keys are allowlisted despite the rule (see `_NOT_BAD_TAGS`); don't expand it to work around the constraint — pick a low-cardinality tag instead.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…