Skip to content

Sentry

Server-side Sentry instrumentation for unhandled exception capture and optional performance monitoring. Requires pip install vgi-rpc[sentry].

vgi-rpc does not manage Sentry's DSN or SDK lifecycle — you must call sentry_sdk.init() yourself. Setting SENTRY_DSN alone is not enough: the Sentry SDK does not auto-initialise on import. sentry_sdk.init() will fall back to reading SENTRY_DSN from the environment only if you don't pass an explicit dsn=.

Automatic instrumentation

If sentry_sdk is initialised in the worker process, vgi-rpc attaches default-config Sentry instrumentation to every RpcServer automatically. No flag, no extra env var — sentry_sdk.is_initialized() is the signal of intent.

import sentry_sdk
sentry_sdk.init()                         # reads SENTRY_DSN if no dsn= passed

from vgi_rpc import RpcServer
server = RpcServer(MyService, MyServiceImpl())   # auto-attached

The check is gated on sentry_sdk already being importable in the process, so workers that have not opted into Sentry pay nothing.

Order matters. sentry_sdk.init() must run before RpcServer(...). If the SDK is not initialised when the constructor runs, auto-attach is a no-op (logged at DEBUG on vgi_rpc.sentry); attach explicitly later if needed.

Customising the configuration

Call instrument_server_sentry() explicitly with a SentryConfig to override the auto-attached default. The explicit call replaces any prior Sentry hook in the dispatch chain — explicit config wins regardless of order:

from vgi_rpc.sentry import SentryConfig, instrument_server_sentry

server = RpcServer(MyService, MyServiceImpl())   # auto-attached default
instrument_server_sentry(
    server,
    SentryConfig(custom_tags={"env": "prod"}, enable_performance=True),
)  # replaces the default-config hook

For HTTP, pass sentry_config= directly to make_wsgi_app()/serve_http() — same replace semantics:

from vgi_rpc.http import make_wsgi_app

app = make_wsgi_app(server, sentry_config=SentryConfig(custom_tags={"env": "prod"}))

Captured Context

When record_request_context=True (the default), each Sentry event carries:

Sentry field Source
contexts.rpc.method RPC method name
contexts.rpc.method_type "unary" or "stream"
contexts.rpc.service Protocol class name
contexts.rpc.server_id RpcServer.server_id
user.username auth.principal (when authenticated)
tags["rpc.method"] RPC method name (always — for Issues filtering)
tags["rpc.method_type"] "unary" or "stream" (always)
tags["auth.domain"] auth.domain (when set)
tags["auth.authenticated"] "true" or "false"
tags[<claim_tag>] Mapped via SentryConfig.claim_tags
tags[<custom_tag>] From SentryConfig.custom_tags
span data rpc.system Always "vgi_rpc"
span data rpc.service Protocol class name
span data rpc.method RPC method name
span data rpc.method_type "unary" / "stream"
span data rpc.stream_id uuid shared across all HTTP turns of one stream call (streams only)

Unhandled exceptions are sent via sentry_sdk.capture_exception() unless their type appears in SentryConfig.ignored_exceptions (e.g. ignore PermissionError for noisy auth-failure events).

Transaction naming

By default vgi-rpc replaces Sentry's WSGI-derived transaction name (a literal route template like /{method} from Falcon) with rpc {method}, so transactions group by RPC method in Sentry's Performance dashboard. Disable with SentryConfig(set_transaction_name=False) if you have alerts pinned to the route-template names.

RPC parameters and Trace Explorer

Opt in to per-call argument recording with SentryConfig(record_params=True). Kwargs become rpc.param.<k> span attributes on the transaction's root span — searchable in Sentry Trace Explorer / Insights.

config = SentryConfig(
    record_params=True,
    tag_params=("table", "format"),     # also duplicate as scope tags for Issues
)

After enabling, queries like the following work in Trace Explorer:

span.op:rpc.server rpc.method:executeScan rpc.param.table:orders
-> chart p99(span.duration) GROUP BY rpc.param.predicate

This is the same pattern Sentry's own SQLAlchemy integration uses (db.system, db.name, etc.).

Tags vs. span data. rpc.param.<k> lands in two places:

  • Span data (always when record_params=True): high-cardinality, indexed for Trace Explorer and Insights, supports aggregations (p99 GROUP BY).
  • Scope tags (only for keys listed in tag_params): low-cardinality, used for filtering error events in the Issues view. Operator-curated whitelist — never auto-promoted.

Caveats:

  • Stream /exchange turns don't carry kwargs. Params live on the /init transaction only. Filter Trace Explorer by rpc.stream_id to find sibling turns of one logical call.
  • Sentry's default scrubber matches kwarg key names only. Free-text values such as predicates pass through. Supply a param_redactor callable, or use Sentry Advanced Data Scrubbing, if your kwargs may contain PII. send_default_pii=False does not save you here.
  • Long string values are truncated at max_param_value_bytes (default 1024 — Relay's practical per-attribute cap). A debug log line records the truncation.
  • Span attributes accept primitives only: str, bool, int, float, and homogeneous lists of one of those types. Dicts, bytes, dataclasses, and mixed-type lists are silently dropped to avoid Sentry ingestion errors.
  • tag_params values are clipped to 200 chars (Sentry's tag value cap). Keep this whitelist for short identifiers.

Performance Monitoring

Performance is opt-in to avoid duplicate tracing when used alongside OpenTelemetry and to conserve Sentry quota:

config = SentryConfig(enable_performance=True, op_name="rpc.server")
instrument_server_sentry(server, config)

Each dispatch then starts a Sentry transaction named vgi_rpc/<method> with operation op_name (default "rpc.server"), finished with status ok or internal_error based on dispatch outcome.

Coexistence with OpenTelemetry

Sentry and OTel hooks compose into a _CompositeDispatchHook and run independently — register either order:

from vgi_rpc.otel import OtelConfig, instrument_server
from vgi_rpc.sentry import SentryConfig, instrument_server_sentry

instrument_server(server, OtelConfig(...))
instrument_server_sentry(server, SentryConfig())

Hook failures are isolated — one hook raising does not stop the others.

API Reference

SentryConfig

SentryConfig dataclass

SentryConfig(
    enable_error_capture: bool = True,
    enable_performance: bool = False,
    record_request_context: bool = True,
    custom_tags: Mapping[str, str] = dict(),
    ignored_exceptions: tuple[
        type[BaseException], ...
    ] = (),
    op_name: str = "rpc.server",
    claim_tags: Mapping[str, str] = dict(),
    user_claim_map: Mapping[str, str] = (
        lambda: {
            "username": "preferred_username",
            "email": "email",
            "name": "name",
        }
    )(),
    set_transaction_name: bool = True,
    record_params: bool = False,
    tag_params: Sequence[str] = (),
    param_redactor: (
        Callable[[Mapping[str, Any]], Mapping[str, Any]]
        | None
    ) = None,
    max_param_value_bytes: int = 1024,
)

Configuration for Sentry error reporting and optional performance monitoring.

ATTRIBUTE DESCRIPTION
enable_error_capture

Capture exceptions via sentry_sdk.capture_exception (default True).

TYPE: bool

enable_performance

Start a Sentry transaction per dispatch. Opt-in to avoid duplicate tracing when used alongside OpenTelemetry and to conserve Sentry quota.

TYPE: bool

record_request_context

Set Sentry scope context with method name, server ID, and authentication info (default True).

TYPE: bool

custom_tags

Extra tags applied to every Sentry event.

TYPE: Mapping[str, str]

ignored_exceptions

Exception types to skip when reporting (e.g. PermissionError).

TYPE: tuple[type[BaseException], ...]

op_name

Sentry transaction operation name (default "rpc.server").

TYPE: str

claim_tags

Maps claim keys to Sentry tag names, e.g. {"tenant_id": "auth.tenant_id"}.

TYPE: Mapping[str, str]

user_claim_map

Maps Sentry user-object fields to JWT claim names, used to populate user.username / user.email / user.name from the decoded JWT. Defaults to standard OIDC claims (preferred_usernameusername, emailemail, namename). user.id is always set from auth.principal (typically the sub claim) and is not configurable here. Override per-key for non-standard IdPs (e.g. Auth0 namespaced claims). Values absent from the JWT are skipped.

TYPE: Mapping[str, str]

set_transaction_name

Override Sentry's WSGI-derived transaction name with rpc {method}. Default True. Disable to keep the route-template grouping (rarely useful — the default /{method} placeholder loses the actual method name).

TYPE: bool

record_params

Attach RPC kwargs as rpc.param.<k> span attributes for Trace Explorer / Insights. Default False: kwargs may contain user data, and Sentry's default scrubber matches key names only — free-text values pass through. Operators must opt in explicitly.

TYPE: bool

tag_params

Short whitelist of param names to also duplicate as scope tags so they are filterable in Issues view (errors). Keep this list for low-cardinality identifiers (table names, format enums) — never predicates, ids, or row counts. Values are stringified and clipped to 200 chars.

TYPE: Sequence[str]

param_redactor

Optional sanitizer applied to kwargs before recording. Default redacts keys matching password|token|secret|key|authorization (case-insensitive). Pass an explicit callable to replace the default, or :func:noop_redactor to disable filtering.

TYPE: Callable[[Mapping[str, Any]], Mapping[str, Any]] | None

max_param_value_bytes

Truncate string param values longer than this (Relay's practical per-attribute cap). Default 1024.

TYPE: int

instrument_server_sentry

instrument_server_sentry

instrument_server_sentry(
    server: RpcServer, config: SentryConfig | None = None
) -> RpcServer

Attach Sentry error reporting to a server, replacing any prior Sentry hook.

Must be called before serve() — not thread-safe during dispatch. If a _SentryDispatchHook is already registered on server (e.g. an auto-attached default), it is removed and replaced with one configured from config. This guarantees explicit calls always win regardless of whether they happen before or after auto-attach.

PARAMETER DESCRIPTION
server

The RpcServer to instrument.

TYPE: RpcServer

config

Optional configuration; uses defaults when None.

TYPE: SentryConfig | None DEFAULT: None

RETURNS DESCRIPTION
RpcServer

The same server instance (for chaining).

Source code in vgi_rpc/sentry.py
def instrument_server_sentry(server: RpcServer, config: SentryConfig | None = None) -> RpcServer:
    """Attach Sentry error reporting to a server, replacing any prior Sentry hook.

    Must be called before ``serve()`` — not thread-safe during dispatch.
    If a ``_SentryDispatchHook`` is already registered on *server* (e.g. an
    auto-attached default), it is removed and replaced with one configured
    from *config*.  This guarantees explicit calls always win regardless of
    whether they happen before or after auto-attach.

    Args:
        server: The ``RpcServer`` to instrument.
        config: Optional configuration; uses defaults when ``None``.

    Returns:
        The same *server* instance (for chaining).

    """
    if config is None:
        config = SentryConfig()
    server._dispatch_hook = _strip_sentry_hook(server._dispatch_hook)
    hook = _SentryDispatchHook(config, server.protocol_name, server.server_id)
    server._dispatch_hook = _register_dispatch_hook(server._dispatch_hook, hook)
    return server