Skip to main content

Sentry Scope

When error occurs, you need contextual information to debug it.

However, errors can come from:

  • application code (e.g. Errors::BaseError)
  • rails framework (e.g. ActiveRecord::RecordNotFound)
  • third party gems (e.g. AWS SDK, Redis client)
  • Ruby standard lib (e.g. NoMethodError, TypeError)

Request Lifecycle

  1. Request enters -> Middleware stack
  2. Routing -> Controller matched
  3. Controller before_action -> Controller#action executes
  4. Controller after_action -> Response rendered
  5. Requests exit <- Middleware stack

If an error occurs at any point, it bubbles up and gets caught by rescue_from handlers

  • e.g. rescue_from (Errors::BaseError, with: :base_error)
note
  • "Request exist" means that the process of serving the request ends.
  • before_action & after_action are Rails Controller Callbacks. As of writing, we do not use after_action.

Sentry Context vs Error Capturing

Sentry Context

  • Happens ONCE per request at the START
    • It's like setting up a "recording studio" before the performance.
  • Available to ALL errors that occur during the request
  • Similar to how CurrentRequest works
    • CurrentRequest is a global object accessible to all application classes.
    • We set it's attributes like identities_user it at the start of the request

Error Capturing

  • Happens when the error occurs
  • Takes a "snapshot" of the current context (previously set at the START of the request)
  • Sends it to Sentry servers

"Sentry Scope" pattern -->

# Early in the request lifecycle
Sentry.configure_scope() do |scope|
scope.set_user(..)
scope.set_context(..)
scope.set_tags(..)
end

# Later, if ANY error happens
Sentry.capture_exception(error)

Flow

Flow Overview

Stage 1: ApplicationController (all requests)
├─ Request metadata (UUID, path, method, IP)
├─ Request parameters
└─ Request headers

Stage 2: IdentitiesController (authenticated requests)
├─ User ID
└─ User email

Stage 3: CareersController/OrgController (profile-determined requests)
├─ Actor class (Careers::UserProfile or Org::UserProfile)
├─ Actor ID
├─ Profile type tag
└─ Company ID (for org users)

Flow Details

┌──────────────┐
│ HTTP Request │
└──────┬───────┘


┌──────────────────────────┐
│ ApplicationController │
│ Set request context │ ◄─ Context set EARLY
└──────┬───────────────────┘


┌──────────────────────────┐
│ IdentitiesController │
│ Set user context │ ◄─ Add user info
└──────┬───────────────────┘


┌──────────────────────────┐
│ CareersController │
│ Set profile context │ ◄─ Add profile info
└──────┬───────────────────┘


│ ANY ERROR HERE...


┌──────────────────────────┐
│ rescue_from │
│ Sentry.capture_exception │ ◄─ Captures with ALL context
└──────┬───────────────────┘


┌──────────────────────────┐
│ Sentry (with context) │
└──────────────────────────┘

✅ AWS Error → Has context
✅ Rails Error → Has context
✅ Ruby Error → Has context
✅ MyError → Has context

Data Flow Diagram

┌───────────────────────────────────────────────────────────────┐
│ CurrentRequest │
│ (Global state, resets after request) │
│ │
│ • identities_user: User │
│ • org_user_profile: Org::UserProfile │
│ • careers_user_profile: Careers::UserProfile │
│ • uuid: String │
│ • remote_ip: String │
│ • user_agent: String │
└─────────────────┬─────────────────────────────────────────────┘

│ Read by

┌───────────────────────────────────────────────────────────────┐
│ SentryEventEnricher │
│ (Stateless module, pure functions) │
│ │
│ • enrich_basic_context(current_request, request) │
│ • enrich_user_context(current_request) │
│ • enrich_profile_context(current_request) │
│ • tag_domain(exception) │
└─────────────────┬─────────────────────────────────────────────┘

│ Calls Sentry SDK

┌───────────────────────────────────────────────────────────────┐
│ Sentry Scope │
│ (Request-scoped, managed by Sentry SDK) │
│ │
│ Sentry.configure_scope do |scope| │
│ scope.set_user(...) │
│ scope.set_context(...) │
│ scope.set_tags(...) │
│ end │
└─────────────────┬─────────────────────────────────────────────┘

│ Used by

┌───────────────────────────────────────────────────────────────┐
│ Sentry.capture_exception(error) │
│ (Called in rescue_from handlers) │
│ │
│ Automatically includes: │
│ • All context from scope.set_context │
│ • User from scope.set_user │
│ • Tags from scope.set_tags │
└─────────────────┬─────────────────────────────────────────────┘

│ Sent to

┌───────────────────────────────────────────────────────────────┐
│ Sentry.io Servers │
└───────────────────────────────────────────────────────────────┘

Timeline Diagram

0ms ----------------- Request enters

├─ ApplicationController#current_request
│ └─ Set CurrentRequest attributes

├─ ApplicationController#enrich_sentry_context
│ └─ Set Sentry basic context

100ms --------------- Authentication

├─ IdentitiesController#authenticate
│ ├─ Decode JWT
│ ├─ Find user
│ ├─ Set CurrentRequest.identities_user
│ └─ Enrich Sentry user context

150ms --------------- Profile determination

├─ CareersController#authenticate_careers_user_profile
│ ├─ Get profile from user
│ ├─ Set CurrentRequest.careers_user_profile
│ └─ Enrich Sentry profile context

200ms --------------- Controller action

├─ CareerUsersController#index
│ ├─ Execute business logic
│ └─ Render response

250ms --------------- Response sent

├─ after_action (if any)

└─ CurrentRequest.reset
└─ Sentry scope cleared

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

What if error at 175ms?

0ms ----------------- Request enters

├─ Set basic context ✓

100ms --------------- Authentication

├─ Set user context ✓

150ms --------------- Profile determination

├─ Set profile context ✓

175ms --------------- 💥 ERROR! (e.g., AWS timeout)

├─ Caught by rescue_from
│ └─ Sentry.capture_exception(error)
│ └─ Includes ALL context set up to this point!

180ms --------------- Error response sent

└─ Request ends

Sentry Event: • request_meta ✓ (from 0ms) • user info ✓ (from 100ms) • profile info ✓ (from 150ms) • AWS error details ✓

State Diagram

┌─────────────┐
│ Request │
│ Starts │
└──────┬──────┘


┌─────────────┐
│ State: None │ ◄─ No context yet
└──────┬──────┘

│ ApplicationController

┌──────────────────────┐
│ State: Basic Context │ ◄─ Has: request_meta
└──────┬───────────────┘

│ IdentitiesController#authenticate

┌──────────────────────┐
│ State: User Context │ ◄─ Has: request_meta + user
└──────┬───────────────┘

│ CareersController#authenticate_careers_user_profile

┌────────────────────────┐
│ State: Profile Context │ ◄─ Has: request_meta + user + profile
└────────┬───────────────┘

│ Controller action

┌─────────┐
│ Success │ → Response sent
└─────────┘

│ OR

┌─────────┐
│ Error │ → Sentry captures with current state context
└─────────┘

Mental Model

Think of Sentry context like a breadcrumb trail:

You (request) are walking through a forest (application):

ApplicationController:
🔵 Drop breadcrumb: "Started at /career_users/careers/user_skills"

IdentitiesController:
🔵 Drop breadcrumb: "User: john@example.com"

CareersController:
🔵 Drop breadcrumb: "Profile: Careers::UserProfile#456"

Controller action:
💥 You fall into a trap! (error)

Rescue team (Sentry):
Follows breadcrumbs backward:
- "Found error at Careers::UserProfile#456"
- "User was john@example.com"
- "They were trying to GET /career_users/careers/user_skills"

Without breadcrumbs:
- "Found someone in the forest" (no context to identify or help)

Key Takeaway

Context is set progressively as we learn more about the request. When an error occurs, it automatically includes ALL context set up to that point.

This is why we set context early (in controllers) rather than late (in error classes) - we want to capture context for ALL errors, not just ones we explicitly raise.