Application Monitoring (AppSignal)
Overview
Heatwave uses AppSignal for comprehensive application monitoring:
| Feature | Description |
|---|---|
| Error Tracking | Exception capture, alerting, and error grouping |
| APM | Request performance monitoring and slow query detection |
| Host Metrics | CPU, memory, disk usage monitoring |
| Anomaly Detection | Automatic alert triggers for unusual patterns |
| Deploy Markers | Correlate deploys with errors/performance changes |
| Frontend Errors | JavaScript error capture with sourcemaps |
Dashboard: https://appsignal.com/warmlyyours
File Locations
Backend (Ruby)
| File | Purpose |
|---|---|
config/appsignal.rb |
Main AppSignal configuration |
app/services/error_reporting.rb |
Unified error reporting abstraction |
app/concerns/models/validation_error_reporting.rb |
Model validation error reporting |
app/concerns/workers/error_reporting_concern.rb |
Worker/job error reporting helpers |
lib/sidekiq/appsignal_context_middleware.rb |
Sidekiq context enrichment |
config/initializers/sidekiq.rb |
Sidekiq middleware registration |
config/application.rb |
Sets config.x.revision from APP_REVISION (drives deploy markers) |
Frontend (JavaScript)
| File | Purpose |
|---|---|
client/js/common/error_handling.js |
AppSignal JS initialization and ErrorReporter |
client/js/www/www.bundler.js |
WWW bundle entry (calls errorHandlingInit) |
client/js/www/core.js |
Core JS entry (calls errorHandlingInit) |
client/js/crm/crm.index.js |
CRM bundle entry (calls errorHandlingInit) |
client/js/common/page_config.js |
Exports appsignalFrontendKey from the #page-config JSON |
app/views/globals/_page_config.html.erb |
Renders the per-env frontend key into #page-config (runtime) |
Deployment
| File | Purpose |
|---|---|
bin/deploy |
Kamal deploy wrapper; daemonizes the AppSignal sourcemap upload post-deploy |
config/appsignal.rb |
Sets config.revision (the gem emits the deploy marker on revision change) |
script/upload_sourcemaps_to_appsignal.rb |
Uploads frontend sourcemaps to AppSignal's private API |
Configuration Details
Backend Configuration (config/appsignal.rb)
Key settings:
- Development disabled - Prevents noise during local dev
- Minimal ignore list - Most errors tracked for visibility
- Rich context - All request headers, params, and session data captured
- Full instrumentation - ViewComponent, Redis, HTTP, GVL, host metrics
Enabled Integrations
| Integration | Config | Description |
|---|---|---|
| ViewComponent | config.view_component.instrumentation_enabled |
Component render performance |
| Redis | instrument_redis = true |
Cache and Sidekiq visibility |
| Net::HTTP | instrument_net_http = true |
Outgoing HTTP requests |
| HTTP.rb | instrument_http_rb = true |
HTTP gem requests |
| Host Metrics | enable_host_metrics = true |
CPU, memory, disk, network |
| Minutely Probes | enable_minutely_probes = true |
Metrics collection |
| Allocation Tracking | enable_allocation_tracking = true |
Memory allocation insights |
| GVL Timer | enable_gvl_global_timer = true |
Ruby threading insights |
| GVL Threads | enable_gvl_waiting_threads = true |
Thread waiting analysis |
| Rails Error Reporter | enable_rails_error_reporter = true |
Rails 7+ integration |
| Sidekiq Errors | sidekiq_report_errors = :all |
All errors including retries |
| ActiveJob Errors | activejob_report_errors = :all |
All errors including retries |
ViewComponent setup (config/application.rb):
config.view_component.instrumentation_enabled = true
config.view_component.use_deprecated_instrumentation_name = false
See: AppSignal ViewComponent Integration
Captured Request Headers
All headers captured for debugging context:
| Category | Headers |
|---|---|
| Standard | Accept, Accept-Language, Accept-Encoding, Host, Referer, User-Agent, Origin |
| Cloudflare | CF-Connecting-IP, CF-IPCountry, CF-Ray, CF-Visitor, True-Client-IP |
| Forwarding | X-Forwarded-For, X-Forwarded-Host, X-Forwarded-Proto, X-Real-IP, X-Request-ID |
| Client Hints | Sec-CH-UA, Sec-CH-UA-Mobile, Sec-CH-UA-Platform |
| Fetch Metadata | Sec-Fetch-Dest, Sec-Fetch-Mode, Sec-Fetch-Site |
| Request | Content-Type, Content-Length, Path-Info, Query-String, Request-Method |
Filtered Data
| Type | Filtered Items |
|---|---|
| Parameters | Rails default filter list (password, token, etc.) |
| Session | _csrf_token, session_id, warden.user.account.key |
Frontend Configuration (client/js/common/error_handling.js)
The frontend uses @appsignal/javascript with these features:
| Feature | Implementation |
|---|---|
| Null Adapter | Development mode uses no-op adapter to prevent errors |
| Runtime Key | Per-env frontend key from the #page-config JSON (config.x → page_config.js) |
| Namespace Detection | Uses window.subdomain to set www/crm/api namespace |
| User Context | Automatically includes current_user data when available |
| Window Error Handling | Wraps global error handlers to report errors |
| Test Function | window.testAppSignal() for manual testing |
Frontend Plugins
We use AppSignal's official plugins for enhanced error context:
| Plugin | Description |
|---|---|
plugin-breadcrumbs-console |
Captures console.log/warn/error calls as breadcrumbs |
plugin-breadcrumbs-network |
Captures fetch and XMLHttpRequest calls as breadcrumbs |
plugin-window-events |
Catches unhandled errors and promise rejections |
Breadcrumbs show what happened before an error occurred, making debugging much easier:
- Console logs leading up to the error
- API calls made (URL, status, timing)
- User interactions that triggered the error
Frontend key injection flow (runtime, per-env):
config/application.rbfetches the per-envfront_end_keyfrom credentials once at boot intoconfig.x.appsignal_frontend_key.app/views/globals/_page_config.html.erbrenders it into the<script id="page-config">JSON.client/js/common/page_config.jsexports it asappsignalFrontendKey;error_handling.jsreads it and inits AppSignal (null adapter when absent).
Injected at runtime, not build time, so the shared prod/staging Docker image serves each
env its own key and rotating it needs no rebuild.nullin dev → null adapter. (Fixed in
PR #1168 — the key was previously baked at webpack build time but never set, so frontend
tracking was silently dark in prod/staging.)
Credentials
Stored in Rails credentials (config/credentials.yml.enc):
appsignal:
production:
push_api_key: 'backend-api-key'
front_end_key: 'frontend-public-key'
staging:
push_api_key: 'backend-api-key'
front_end_key: 'frontend-public-key'
Access via:
Heatwave::Configuration.fetch(:appsignal, :push_api_key)
Heatwave::Configuration.fetch(:appsignal, :front_end_key)
Error Reporting API
Namespace Structure
We use 9 clean namespaces combining source and severity.
Configure notification settings per namespace in AppSignal dashboard:
| Source | Severity | Namespace | Notification |
|---|---|---|---|
| Rails (www/crm/api) | Error | web |
First in Deploy |
| Rails (www/crm/api) | Warning | web_warning |
First Occurrence |
| Rails (www/crm/api) | Info | web_info |
Never Notify |
| JavaScript | Error | frontend |
First in Deploy |
| JavaScript | Warning | frontend_warning |
First Occurrence |
| JavaScript | Info | frontend_info |
Never Notify |
| Sidekiq/ActiveJob | Error | background |
First in Deploy |
| Sidekiq/ActiveJob | Warning | background_warning |
First Occurrence |
| Sidekiq/ActiveJob | Info | background_info |
Never Notify |
Setup: In AppSignal dashboard, go to App Settings → Notification Defaults and configure each namespace.
See: AppSignal Namespaces Guide
Backend (Ruby) - ErrorReporting Service
The ErrorReporting class provides a unified, provider-agnostic interface:
# From a controller
ErrorReporting.from_controller(self).error(exception) # → web
ErrorReporting.from_controller(self).warning(exception) # → web_warning
ErrorReporting.from_controller(self).informational(ex) # → web_info
# Direct error reporting
ErrorReporting.error(exception) # → web
ErrorReporting.error(exception, { user_id: 123 }) # → web with context
# Warning
ErrorReporting.warning("Unusual condition", { value: x }) # → web_warning
# Critical (goes to web with severity: critical tag)
ErrorReporting.critical(exception, { urgent: true })
# Informational (goes to web_info - configure "Never Notify")
# Use for: bot blocks, 404s, CSRF failures, record not found
ErrorReporting.informational("Bot blocked", { reason: 'bot_detection' })
# Info/debug (logs only, NOT sent to AppSignal)
ErrorReporting.info("Process completed", { count: 100 })
# From a worker (uses background namespace family)
ErrorReporting.error(exception, source: :background) # → background
ErrorReporting.warning(exception, source: :background) # → background_warning
ErrorReporting.informational(msg, source: :background) # → background_info
# Scoped context (thread-safe, applies to all errors in block)
ErrorReporting.scoped({ order_id: order.id }) do
process_order(order) # Any errors include the context
end
Context automatically extracted by from_controller:
- Request URL, method, path, format
- User agent, remote IP, referer
- Filtered params
- Current user/account ID and email
- Subdomain (www, crm, api) stored as tag for filtering
Frontend (JavaScript) - window.ErrorReporter
// Report an error (goes to 'frontend' namespace)
window.ErrorReporter.error(error, { extra: "data" });
// Report warning (goes to 'frontend_warning' namespace)
window.ErrorReporter.warning(error, { context: "info" });
// Report critical (goes to 'frontend' namespace with severity: critical)
window.ErrorReporter.critical(error);
// Report informational (goes to 'frontend_info' namespace)
window.ErrorReporter.informational("Expected event", { reason: "x" });
// Info/debug (console only, NOT sent to AppSignal)
window.ErrorReporter.info("Info message");
window.ErrorReporter.debug("Debug message");
// Test function (also works via URL: ?test_appsignal=1)
window.testAppSignal();
Sidekiq Integration
Automatic Context Middleware
lib/sidekiq/appsignal_context_middleware.rb automatically enriches all Sidekiq jobs with:
| Tag | Description |
|---|---|
job_class |
Worker class name |
job_id |
Sidekiq JID |
queue |
Queue name |
retry_count |
Number of retries |
batch_id |
Sidekiq Pro batch ID (if applicable) |
job_args |
Sanitized job arguments |
Registration (config/initializers/sidekiq.rb):
config.server_middleware do |chain|
chain.add Sidekiq::AppsignalContextMiddleware
end
Manual Error Reporting in Workers
For batch processing where you want to continue after errors:
class MyWorker
include Sidekiq::Job
include Workers::ErrorReportingConcern
def perform(ids)
ids.each do |id|
process_item(id)
rescue StandardError => e
report_worker_error(e, item_id: id)
# Continue with next item
end
end
end
Model Validation Errors
For tracking validation failures:
class Order < ApplicationRecord
include Models::ValidationErrorReporting
after_validation :report_validation_errors, if: :should_report?
private
def should_report?
errors.present? && some_condition?
end
end
Deploy Markers
Deploys go through Kamal (bin/deploy), not Capistrano. Markers are
emitted by the AppSignal Ruby gem itself from config.revision, which is set
in config/appsignal.rb to the app's revision:
# config/appsignal.rb
config.revision = Heatwave::Application.config.x.revision
config.x.revision is the full git SHA, baked into the production image at
build time as the APP_REVISION env layer (Dockerfile ARG GIT_REVISION ←
config/deploy.yml builder args) and read back in config/application.rb. When
a new container boots reporting a changed revision, the gem records a deploy
marker showing:
- Git revision (SHA)
- Environment
- Deploy time
Use markers to correlate:
- Error spikes after deploys
- Performance changes
- New errors introduced
Sourcemaps
Frontend sourcemaps are uploaded privately to AppSignal (not served publicly):
- Webpack generates hidden sourcemaps (
devtool: 'hidden-source-map')- Creates
.mapfiles but no//# sourceMappingURLcomment in JS files - This keeps your source code private from end users
- Creates
- Deploy script uploads sourcemaps to AppSignal's private API
- Uses
script/upload_sourcemaps_to_appsignal.rb - Tied to git revision for matching errors to correct source versions
- Uses
- Sourcemaps are deleted from build directory after upload
- They never reach the production servers
- AppSignal uses private maps to resolve stack traces
Security Benefits
- Source code is not exposed to end users
- Attackers cannot reverse-engineer minified code
- Stack traces still show original source locations
Manual Upload (if needed)
If automatic upload fails, you can manually upload:
APPSIGNAL_PUSH_API_KEY=xxx \
ENVIRONMENT=production \
REVISION=$(git rev-parse HEAD) \
BUILD_DIR=public/javascripts/webpack-prod \
bundle exec ruby script/upload_sourcemaps_to_appsignal.rb
Set DRY_RUN=true to preview without uploading.
Environments
| Environment | AppSignal Active | Frontend Key |
|---|---|---|
| production | ✅ | production key |
| staging | ✅ | staging key |
| development | ❌ (null adapter) | N/A |
| test | ❌ | N/A |
AI Assistant Access
The standalone AppSignal MCP server has been retired. For AI-assistant access
to AppSignal — listing exception/anomaly incidents, fetching samples, querying
logs and metrics, checking deploy markers, adding notes, closing incidents —
use the appsignal skill, which
calls AppSignal's MCP HTTP API directly via curl.
Error Handling in Controllers
ApplicationController Integration
# Rescue handlers (non-local requests only)
rescue_from StandardError, with: :render_500
rescue_from ActiveRecord::RecordNotFound, with: :render_404
rescue_from ActionController::InvalidAuthenticityToken, with: :render_invalid_authenticity_token
def render_500(exception = nil)
# Reports as ERROR with full request context
ErrorReporting.from_controller(self).error(exception) if exception
# ... render error page
end
def render_404(exception = nil)
# Reports as INFORMATIONAL - expected for old links/bots/typos
if exception.is_a?(ActiveRecord::RecordNotFound) || exception.is_a?(ActionController::RoutingError)
ErrorReporting.from_controller(self).informational(exception, reason: 'not_found')
end
# ... render 404 page
end
def render_invalid_authenticity_token(exception = nil)
# Reports as INFORMATIONAL - expected for expired sessions/bots
ErrorReporting.from_controller(self).informational(exception, reason: 'csrf_token_invalid')
reset_session
# ... redirect to login
end
def prevent_bots
return unless bot_request?
# Reports as INFORMATIONAL - expected behavior, not an error
ErrorReporting.from_controller(self).informational("Bot blocked: #{request.user_agent}", reason: 'bot_detection')
head :forbidden
end
Namespace Classification Guide
| Error Type | Namespace | Notification | Reason |
|---|---|---|---|
InvalidCrossOriginRequest |
Ignored | N/A | CORS preflight, browser behavior |
RecordNotFound |
web_info |
Never Notify | 404s - expected for old links/bots |
RoutingError |
web_info |
Never Notify | 404s - expected for old links/bots |
InvalidAuthenticityToken |
web_warning |
First Occurrence | Expected for expired sessions/bots |
| Bot detection blocks | web_info |
Never Notify | Expected security behavior |
| Turnstile validation failures | web_info |
Never Notify | Expected for bots/JS disabled |
| All other errors | web/frontend/background |
First in Deploy | Full tracking |
Alerts & Notifications
Configure in AppSignal dashboard:
- Error rate thresholds
- Performance anomalies
- Host metric alerts
Notification channels:
- Slack
- PagerDuty
- Webhooks
Troubleshooting
Errors not appearing in AppSignal
- Check environment is
productionorstaging - Verify API key in credentials:
Heatwave::Configuration.fetch(:appsignal, :push_api_key) - Check
Appsignal.active?returnstrue - Review
log/production.logfor AppSignal errors
Frontend errors not tracking
Frontend AppSignal is on in production/staging (per-env key injected at runtime via
#page-config— see the Frontend key injection flow above). In dev the null adapter is
expected. If errors aren't tracking in prod/staging, work through:
- Verify
window.envis not'development' - Check browser console for AppSignal init messages
- Run
window.testAppSignal()to test - Check Network tab for requests to
appsignal-endpoint.net(204 = success) - Verify the per-env key is present in the
#page-configJSON (View Source →<script id="page-config">→appsignalFrontendKey)
Development mode
Development uses a null adapter - this is intentional:
[AppSignal] Development mode - using null adapter
Errors log to console but don't send to AppSignal.
Checking adapter status
// In browser console:
console.log('env:', window.env);
console.log('_isNullAdapter:', window.appsignal?._isNullAdapter);
console.log('ErrorReporter.enabled:', window.ErrorReporter?.enabled);
window.testAppSignal(); // Should show "✅ Test error sent!"
Deployment errors
The app runs as Puma in Docker behind Thruster, deployed via Kamal. If a
deploy fails to boot the new container:
- Check
kamal app logs(andlog/production.log) for the boot error bin/deployaborts before swapping traffic, so the previous release keeps serving- Fix the error and re-run
bin/deploy
Related Documentation
- Vector Log Shipping — Ship Sidekiq, Rails, and PostgreSQL logs to AppSignal
- AppSignal Ruby Documentation
- AppSignal JavaScript Documentation
appsignalskill —curl-based AppSignal access for AI assistants (replaces the retired MCP server)- Changelog: AppSignal Migration