Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Sentry backend conventions for logging, tracing/spans, 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, or reads registered options with options.get(). Trigger on "add logging", "log an error", "add a metric", "add a span", "instrument tracing", "read an option", "LOG005", "LOG011", or metrics tag cardinality questions.
.claude/skills/getsentry-backend-conventions/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 29% | 0% |
| case-03 | ✗→✓ | ▲ Improved | 104% | 0% |
| case-04 | ✗→✓ | ▲ Improved | 5% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 10% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 35% | 0% |
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.
pythonimport 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", ) )
Use the wrappers in sentry.utils.tracing instead of calling the SDK directly. This is required while we dogfood the streaming trace lifecycle (Span First rollout).
| Instead of | Use | | -------------------------------- | ------------------------------------------------ | | sentry_sdk.start_span() | start_span(name=..., op=...) | | sentry_sdk.start_transaction() | start_span(name=..., op=..., transaction=True) | | span.set_tag(key, value) | set_span_tag(span, key, value) | | span.set_data(key, value) | set_span_data(span, key, value) |
pythonfrom sentry.utils.tracing import start_span, set_span_tag, set_span_data # Child span — no need to capture the span when you don't set tags/data with start_span(name="event_manager.save", op="save"): do_work() # Child span with tags/data — capture via `as span` with start_span(name="event_manager.save", op="save") as span: set_span_tag(span, "platform", platform) set_span_data(span, "rows_count", len(rows)) # Transaction root (replaces sentry_sdk.start_transaction) with start_span(name="monitors.consumer", op="process", transaction=True): process_batch()
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.
pythonfrom 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.
Every distinct tag-value combination is a separate time series, so keep tags low-cardinality, meaningful, and minimal:
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.
pythonmetrics.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.
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→pass | 18,805 | 13,445 | -29% | 1 | 1 | 0% | 2,435 | 3,145 | +29% | 0 | 0 | — |
case-02 | fail→fail | 13,268 | 15,109 | +14% | 1 | 1 | 0% | 2,490 | 3,410 | +37% | 0 | 0 | — |
case-03 | fail→pass | 11,192 | 7,997 | -29% | 1 | 1 | 0% | 1,108 | 2,255 | +104% | 0 | 0 | — |
case-04 | fail→pass | 16,457 | 7,786 | -53% | 1 | 1 | 0% | 2,000 | 2,095 | +5% | 0 | 0 | — |
case-05 | pass→pass | 14,115 | 9,261 | -34% | 1 | 1 | 0% | 1,692 | 2,250 | +33% | 0 | 0 | — |
case-06 | fail→pass | 14,597 | 7,835 | -46% | 1 | 1 | 0% | 1,907 | 2,105 | +10% | 0 | 0 | — |
case-16 | pass→pass | 15,496 | 12,276 | -21% | 1 | 1 | 0% | 1,920 | 2,661 | +39% | 0 | 0 | — |
case-07 | fail→pass | 13,568 | 8,760 | -35% | 1 | 1 | 0% | 1,726 | 2,326 | +35% | 0 | 0 | — |
case-08 | fail→pass | 14,081 | 7,112 | -49% | 1 | 1 | 0% | 1,814 | 1,995 | +10% | 0 | 0 | — |
case-09 | fail→pass | 16,849 | 3,429 | -80% | 1 | 1 | 0% | 1,947 | 2,270 | +17% | 0 | 0 | — |
case-10 | fail→pass | 17,735 | 9,680 | -45% | 1 | 1 | 0% | 2,198 | 2,529 | +15% | 0 | 0 | — |
case-11 | pass→pass | 11,308 | 3,885 | -66% | 1 | 1 | 0% | 1,236 | 2,224 | +80% | 0 | 0 | — |
case-12 | pass→pass | 14,411 | 7,089 | -51% | 1 | 1 | 0% | 1,694 | 1,847 | +9% | 0 | 0 | — |
case-13 | pass→pass | 10,985 | 7,712 | -30% | 1 | 1 | 0% | 1,075 | 2,133 | +98% | 0 | 0 | — |
case-14 | pass→pass | 7,896 | 7,422 | -6% | 1 | 1 | 0% | 594 | 2,016 | +239% | 0 | 0 | — |
case-15 | fail→pass | 22,577 | 8,341 | -63% | 1 | 1 | 0% | 1,828 | 2,173 | +19% | 0 | 0 | — |
case-17 | fail→pass | 24,746 | 7,177 | -71% | 1 | 1 | 0% | 3,577 | 1,946 | -46% | 0 | 0 | — |
case-18 | fail→pass | 11,078 | 7,178 | -35% | 1 | 1 | 0% | 1,020 | 2,002 | +96% | 0 | 0 | — |
case-19 | fail→pass | 19,057 | 9,711 | -49% | 1 | 1 | 0% | 2,574 | 2,336 | -9% | 0 | 0 | — |
case-20 | pass→pass | 6,402 | 13,378 | +109% | 1 | 1 | 0% | 1,109 | 2,986 | +169% | 0 | 0 | — |
case-21 | pass→pass | 14,035 | 15,140 | +8% | 1 | 1 | 0% | 2,480 | 3,463 | +40% | 0 | 0 | — |
case-22 | pass→pass | 17,644 | 17,055 | -3% | 1 | 1 | 0% | 2,320 | 3,943 | +70% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +55 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.6-flash | verified | 8/28/2026 | +48% |
Other measured skills in the registry, with their headline benchmark lift.