mirror of
https://github.com/System-End/identity-vault.git
synced 2026-04-19 15:18:23 +00:00
switch error handling to sentry
This commit is contained in:
parent
69c2507793
commit
3c70ca8c28
23 changed files with 532 additions and 416 deletions
4
Gemfile
4
Gemfile
|
|
@ -47,7 +47,8 @@ gem "vite_rails"
|
|||
|
||||
gem "pundit", "~> 2.5"
|
||||
|
||||
gem "honeybadger", "~> 5.28"
|
||||
gem "sentry-ruby", "~> 5.22"
|
||||
gem "sentry-rails", "~> 5.22"
|
||||
|
||||
gem "http", "~> 5.2"
|
||||
|
||||
|
|
@ -152,4 +153,3 @@ end
|
|||
gem "premailer-rails", "~> 1.12"
|
||||
|
||||
gem "openssl", "~> 3.3"
|
||||
|
||||
|
|
|
|||
12
Gemfile.lock
12
Gemfile.lock
|
|
@ -254,9 +254,6 @@ GEM
|
|||
hashids (~> 1.0)
|
||||
hashids (1.0.6)
|
||||
hashie (5.0.0)
|
||||
honeybadger (5.28.0)
|
||||
logger
|
||||
ostruct
|
||||
htmlentities (4.4.2)
|
||||
http (5.2.0)
|
||||
addressable (~> 2.8)
|
||||
|
|
@ -537,6 +534,12 @@ GEM
|
|||
securerandom (0.4.1)
|
||||
semantic_logger (4.16.1)
|
||||
concurrent-ruby (~> 1.0)
|
||||
sentry-rails (5.28.1)
|
||||
railties (>= 5.0)
|
||||
sentry-ruby (~> 5.28.1)
|
||||
sentry-ruby (5.28.1)
|
||||
bigdecimal
|
||||
concurrent-ruby (~> 1.0, >= 1.0.2)
|
||||
serve_byte_range (1.0.0)
|
||||
rack (>= 1.0)
|
||||
slack-ruby-client (2.6.0)
|
||||
|
|
@ -636,7 +639,6 @@ DEPENDENCIES
|
|||
geocoder (~> 1.8)
|
||||
good_job (~> 4.10)
|
||||
hashid-rails (~> 1.4)
|
||||
honeybadger (~> 5.28)
|
||||
http (~> 5.2)
|
||||
image_processing (~> 1.2)
|
||||
jb (~> 0.8.2)
|
||||
|
|
@ -667,6 +669,8 @@ DEPENDENCIES
|
|||
rubocop-rails-omakase
|
||||
ruby-vips (~> 2.2)
|
||||
saml2 (~> 3.2)
|
||||
sentry-rails (~> 5.22)
|
||||
sentry-ruby (~> 5.22)
|
||||
slack-ruby-client (~> 2.6)
|
||||
slocks (~> 0.1.0)
|
||||
superform (~> 0.5.1)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,38 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Components::Banner < Components::Base
|
||||
def initialize(kind:)
|
||||
def initialize(kind:, event_id: nil)
|
||||
@kind = kind
|
||||
@event_id = event_id
|
||||
end
|
||||
|
||||
def view_template(&block)
|
||||
div(class: "banner flex #{banner_class}") do
|
||||
render_icon
|
||||
yield
|
||||
div(class: "flex-1") do
|
||||
yield
|
||||
render_event_id if @event_id.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def render_event_id
|
||||
div(class: "error-id-container mt-1") do
|
||||
small(class: "error-id-text text-sm opacity-75") do
|
||||
plain "Error ID: "
|
||||
code(
|
||||
class: "error-id-code cursor-pointer hover:opacity-80",
|
||||
data_error_id: @event_id,
|
||||
onclick: "copyErrorId(this)",
|
||||
title: "Click to copy"
|
||||
) { @event_id }
|
||||
span(class: "copy-feedback ml-1 hidden") { " ✓ Copied!" }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def banner_class
|
||||
case @kind.to_sym
|
||||
when :success then "success"
|
||||
|
|
|
|||
|
|
@ -47,9 +47,18 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
def set_honeybadger_context
|
||||
Honeybadger.context({
|
||||
identity_id: current_identity&.id
|
||||
})
|
||||
return unless current_identity
|
||||
|
||||
Sentry.set_user(
|
||||
id: current_identity.public_id, # Use public_id (ident!xyz) not database ID
|
||||
email: current_identity.primary_email
|
||||
)
|
||||
|
||||
Sentry.set_context(:identity, {
|
||||
identity_public_id: current_identity.public_id,
|
||||
identity_email: current_identity.primary_email,
|
||||
slack_id: current_identity.slack_id
|
||||
}.compact)
|
||||
end
|
||||
|
||||
# Best-effort country detection from request IP; returns ISO3166 alpha-2.
|
||||
|
|
@ -86,8 +95,19 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |e|
|
||||
event_id = Sentry.capture_exception(e)
|
||||
flash[:error] = "sorry, couldn't find that object... (404)"
|
||||
redirect_to root_path
|
||||
flash[:sentry_event_id] = event_id if event_id
|
||||
redirect_to root_path unless request.path == "/"
|
||||
end
|
||||
|
||||
rescue_from StandardError do |e|
|
||||
event_id = Sentry.capture_exception(e)
|
||||
flash[:error] = "Something went wrong. Please try again."
|
||||
flash[:sentry_event_id] = event_id if event_id
|
||||
|
||||
raise e if Rails.env.development?
|
||||
redirect_to root_path unless request.path == "/"
|
||||
end
|
||||
|
||||
private
|
||||
|
|
|
|||
|
|
@ -51,22 +51,54 @@ module Backend
|
|||
end
|
||||
|
||||
def set_honeybadger_context
|
||||
Honeybadger.context({
|
||||
user_id: current_user&.id,
|
||||
return unless current_identity
|
||||
|
||||
# Set user context with public_id
|
||||
Sentry.set_user(
|
||||
id: current_identity.public_id, # Use identity public_id (ident!xyz)
|
||||
username: current_user&.username,
|
||||
email: current_identity.primary_email
|
||||
)
|
||||
|
||||
# Set backend user context
|
||||
Sentry.set_context(:user, {
|
||||
user_username: current_user&.username,
|
||||
identity_id: current_identity&.id,
|
||||
identity_email: current_identity&.primary_email
|
||||
})
|
||||
user_identity_public_id: current_user&.identity&.public_id,
|
||||
user_slack_id: current_user&.slack_id,
|
||||
is_super_admin: current_user&.super_admin?,
|
||||
is_program_manager: current_user&.program_manager?,
|
||||
can_break_glass: current_user&.can_break_glass?
|
||||
}.compact)
|
||||
|
||||
# Set identity context (the identity being acted upon)
|
||||
Sentry.set_context(:identity, {
|
||||
identity_public_id: current_identity.public_id,
|
||||
identity_email: current_identity.primary_email,
|
||||
slack_id: current_identity.slack_id
|
||||
}.compact)
|
||||
|
||||
# Set impersonation context if applicable
|
||||
if current_impersonator
|
||||
Sentry.set_context(:impersonation, {
|
||||
impersonator_username: current_impersonator.username,
|
||||
impersonator_identity_public_id: current_impersonator.identity&.public_id,
|
||||
is_impersonating: true
|
||||
}.compact)
|
||||
end
|
||||
end
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError do |e|
|
||||
event_id = Sentry.capture_exception(e)
|
||||
flash[:error] = "you don't seem to be authorized to do that?"
|
||||
redirect_to backend_root_path
|
||||
flash[:sentry_event_id] = event_id if event_id
|
||||
redirect_to backend_root_path unless request.path == "/backend" || request.path == "/backend/"
|
||||
end
|
||||
|
||||
rescue_from ActiveRecord::RecordNotFound do |e|
|
||||
event_id = Sentry.capture_exception(e)
|
||||
flash[:error] = "sorry, couldn't find that object... (404)"
|
||||
redirect_to backend_root_path
|
||||
flash[:sentry_event_id] = event_id if event_id
|
||||
redirect_to backend_root_path unless request.path == "/backend" || request.path == "/backend/"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
25
app/controllers/errors_controller.rb
Normal file
25
app/controllers/errors_controller.rb
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
class ErrorsController < ActionController::Base
|
||||
layout "errors"
|
||||
|
||||
def not_found
|
||||
@event_id = request.env["sentry.error_event_id"]
|
||||
render status: :not_found
|
||||
rescue => e
|
||||
# Last resort: render plain text to avoid infinite loops
|
||||
render plain: "404 - Page Not Found", status: :not_found
|
||||
end
|
||||
|
||||
def unprocessable_entity
|
||||
@event_id = request.env["sentry.error_event_id"]
|
||||
render status: :unprocessable_entity
|
||||
rescue => e
|
||||
render plain: "422 - Unprocessable Entity", status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def internal_server_error
|
||||
@event_id = request.env["sentry.error_event_id"]
|
||||
render status: :internal_server_error
|
||||
rescue => e
|
||||
render plain: "500 - Internal Server Error", status: :internal_server_error
|
||||
end
|
||||
end
|
||||
|
|
@ -294,12 +294,13 @@ class LoginsController < ApplicationController
|
|||
slack_result
|
||||
else
|
||||
Rails.logger.error "Slack provisioning failed for #{@identity.id}: #{slack_result[:error]}"
|
||||
Honeybadger.notify(
|
||||
Sentry.capture_message(
|
||||
"Slack provisioning failed on first login",
|
||||
context: {
|
||||
identity_id: @identity.id,
|
||||
email: @identity.primary_email,
|
||||
error: slack_result[:error]
|
||||
level: :error,
|
||||
extra: {
|
||||
identity_public_id: @identity.public_id,
|
||||
identity_email: @identity.primary_email,
|
||||
slack_error: slack_result[:error]
|
||||
}
|
||||
)
|
||||
if scenario.should_create_slack? || @attempt.next_action == "slack"
|
||||
|
|
|
|||
|
|
@ -113,12 +113,13 @@ class SAMLController < ApplicationController
|
|||
Rails.logger.info "Slack provisioning successful via SCIM for #{current_identity.id}: #{slack_result[:message]}"
|
||||
else
|
||||
Rails.logger.error "Slack provisioning failed via SCIM for #{current_identity.id}: #{slack_result[:error]}"
|
||||
Honeybadger.notify(
|
||||
Sentry.capture_message(
|
||||
"Slack provisioning failed via SCIM",
|
||||
context: {
|
||||
identity_id: current_identity.id,
|
||||
email: current_identity.primary_email,
|
||||
error: slack_result[:error]
|
||||
level: :error,
|
||||
extra: {
|
||||
identity_public_id: current_identity.public_id,
|
||||
identity_email: current_identity.primary_email,
|
||||
slack_error: slack_result[:error]
|
||||
}
|
||||
)
|
||||
flash[:error] = "We couldn't create your Slack account. Please contact support."
|
||||
|
|
|
|||
|
|
@ -12,4 +12,26 @@ document.addEventListener('htmx:configRequest', (event) => {
|
|||
if (csrfToken) {
|
||||
event.detail.headers['X-CSRF-Token'] = csrfToken;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Copy error ID to clipboard
|
||||
window.copyErrorId = function(element) {
|
||||
const errorId = element.dataset.errorId;
|
||||
const feedback = element.nextElementSibling || element.parentElement.querySelector('.copy-feedback');
|
||||
|
||||
navigator.clipboard.writeText(errorId).then(() => {
|
||||
// Show feedback
|
||||
if (feedback) {
|
||||
feedback.classList.add('show');
|
||||
feedback.classList.remove('hidden');
|
||||
|
||||
// Hide after 2 seconds
|
||||
setTimeout(() => {
|
||||
feedback.classList.remove('show');
|
||||
feedback.classList.add('hidden');
|
||||
}, 2000);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Failed to copy:', err);
|
||||
});
|
||||
};
|
||||
|
|
@ -101,7 +101,10 @@ module RalseiEngine
|
|||
Rails.logger.info "RalseiEngine sent message to #{identity.slack_id} via #{channel_id} (template: #{template_name})"
|
||||
rescue => e
|
||||
Rails.logger.error "RalseiEngine failed to send message: #{e.message}"
|
||||
Honeybadger.notify(e, context: { identity_id: identity.id, template: template_name })
|
||||
Sentry.capture_exception(e, extra: {
|
||||
identity_public_id: identity.public_id,
|
||||
ralsei_template: template_name
|
||||
})
|
||||
raise
|
||||
end
|
||||
|
||||
|
|
@ -128,7 +131,9 @@ module RalseiEngine
|
|||
dm_channel_id
|
||||
rescue => e
|
||||
Rails.logger.error "RalseiEngine failed to open DM channel: #{e.message}"
|
||||
Honeybadger.notify(e, context: { identity_id: identity.id })
|
||||
Sentry.capture_exception(e, extra: {
|
||||
identity_public_id: identity.public_id
|
||||
})
|
||||
nil
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -154,7 +154,10 @@ module SCIMService
|
|||
}
|
||||
rescue => e
|
||||
Rails.logger.error "Error creating Slack user: #{e.message}"
|
||||
Honeybadger.notify(e, context: { identity_id: identity.id, email: identity.primary_email })
|
||||
Sentry.capture_exception(e, extra: {
|
||||
identity_public_id: identity.public_id,
|
||||
identity_email: identity.primary_email
|
||||
})
|
||||
|
||||
{
|
||||
success: false,
|
||||
|
|
|
|||
102
app/views/errors/internal_server_error.html.erb
Normal file
102
app/views/errors/internal_server_error.html.erb
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.error-page-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1.5rem;
|
||||
color: var(--pico-primary);
|
||||
}
|
||||
.error-code {
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
color: var(--pico-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.error-description {
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.error-id-box {
|
||||
background: var(--pico-card-background-color);
|
||||
border: 2px solid var(--pico-primary);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.error-id-label {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.error-id-code {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--pico-background-color);
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error-id-code:hover {
|
||||
border-color: var(--pico-primary);
|
||||
background: var(--pico-primary-focus);
|
||||
}
|
||||
.copy-feedback {
|
||||
display: none;
|
||||
margin-top: 0.5rem;
|
||||
color: #2ecc71;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.copy-feedback.show {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="error-page">
|
||||
<div class="error-page-content">
|
||||
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"></path>
|
||||
</svg>
|
||||
|
||||
<div class="error-code">500</div>
|
||||
<h1 class="error-title">Internal Server Error</h1>
|
||||
<p class="error-description">We're sorry, but something went wrong on our end.</p>
|
||||
|
||||
<% if @event_id.present? %>
|
||||
<div class="error-id-box">
|
||||
<p class="error-id-label">
|
||||
If you need help, please provide this error ID:
|
||||
</p>
|
||||
<code
|
||||
class="error-id-code"
|
||||
data-error-id="<%= @event_id %>"
|
||||
onclick="copyErrorId(this)"
|
||||
title="Click to copy">
|
||||
<%= @event_id %>
|
||||
</code>
|
||||
<div class="copy-feedback">✓ Copied to clipboard!</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<a href="/" role="button">Go back home</a>
|
||||
</div>
|
||||
</div>
|
||||
100
app/views/errors/not_found.html.erb
Normal file
100
app/views/errors/not_found.html.erb
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.error-page-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1.5rem;
|
||||
color: var(--pico-primary);
|
||||
}
|
||||
.error-code {
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
color: var(--pico-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.error-description {
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.error-id-box {
|
||||
background: var(--pico-card-background-color);
|
||||
border: 2px solid var(--pico-primary);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.error-id-label {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.error-id-code {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--pico-background-color);
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error-id-code:hover {
|
||||
border-color: var(--pico-primary);
|
||||
background: var(--pico-primary-focus);
|
||||
}
|
||||
.copy-feedback {
|
||||
display: none;
|
||||
margin-top: 0.5rem;
|
||||
color: #2ecc71;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.copy-feedback.show {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="error-page">
|
||||
<div class="error-page-content">
|
||||
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
|
||||
<div class="error-code">404</div>
|
||||
<h1 class="error-title">Page Not Found</h1>
|
||||
<p class="error-description">The page you were looking for doesn't exist.</p>
|
||||
|
||||
<% if @event_id.present? %>
|
||||
<div class="error-id-box">
|
||||
<p class="error-id-label">Error ID:</p>
|
||||
<code
|
||||
class="error-id-code"
|
||||
data-error-id="<%= @event_id %>"
|
||||
onclick="copyErrorId(this)"
|
||||
title="Click to copy">
|
||||
<%= @event_id %>
|
||||
</code>
|
||||
<div class="copy-feedback">✓ Copied to clipboard!</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<a href="/" role="button">Go back home</a>
|
||||
</div>
|
||||
</div>
|
||||
100
app/views/errors/unprocessable_entity.html.erb
Normal file
100
app/views/errors/unprocessable_entity.html.erb
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<style>
|
||||
.error-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
.error-page-content {
|
||||
max-width: 600px;
|
||||
}
|
||||
.error-icon {
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
margin: 0 auto 1.5rem;
|
||||
color: var(--pico-primary);
|
||||
}
|
||||
.error-code {
|
||||
font-size: 4rem;
|
||||
font-weight: 700;
|
||||
color: var(--pico-primary);
|
||||
margin-bottom: 0.5rem;
|
||||
line-height: 1;
|
||||
}
|
||||
.error-title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.error-description {
|
||||
margin-bottom: 2rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
.error-id-box {
|
||||
background: var(--pico-card-background-color);
|
||||
border: 2px solid var(--pico-primary);
|
||||
border-radius: var(--pico-border-radius);
|
||||
padding: 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.error-id-label {
|
||||
font-size: 0.875rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.error-id-code {
|
||||
display: block;
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
padding: 0.75rem 1rem;
|
||||
background: var(--pico-background-color);
|
||||
border: 1px solid var(--pico-muted-border-color);
|
||||
border-radius: var(--pico-border-radius);
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
font-weight: 600;
|
||||
}
|
||||
.error-id-code:hover {
|
||||
border-color: var(--pico-primary);
|
||||
background: var(--pico-primary-focus);
|
||||
}
|
||||
.copy-feedback {
|
||||
display: none;
|
||||
margin-top: 0.5rem;
|
||||
color: #2ecc71;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
.copy-feedback.show {
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="error-page">
|
||||
<div class="error-page-content">
|
||||
<svg class="error-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636"></path>
|
||||
</svg>
|
||||
|
||||
<div class="error-code">422</div>
|
||||
<h1 class="error-title">Unprocessable Entity</h1>
|
||||
<p class="error-description">The change you wanted was rejected.</p>
|
||||
|
||||
<% if @event_id.present? %>
|
||||
<div class="error-id-box">
|
||||
<p class="error-id-label">Error ID:</p>
|
||||
<code
|
||||
class="error-id-code"
|
||||
data-error-id="<%= @event_id %>"
|
||||
onclick="copyErrorId(this)"
|
||||
title="Click to copy">
|
||||
<%= @event_id %>
|
||||
</code>
|
||||
<div class="copy-feedback">✓ Copied to clipboard!</div>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<a href="/" role="button">Go back home</a>
|
||||
</div>
|
||||
</div>
|
||||
36
app/views/layouts/errors.html.erb
Normal file
36
app/views/layouts/errors.html.erb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!DOCTYPE html>
|
||||
<html data-theme="light">
|
||||
<head>
|
||||
<title><%= content_for(:title) || "Error - Hack Club Auth" %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<%= csrf_meta_tags %>
|
||||
<%= csp_meta_tag %>
|
||||
<script>
|
||||
(function() {
|
||||
try {
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const defaultTheme = systemPrefersDark ? "dark" : "light";
|
||||
const savedTheme = localStorage.getItem("theme") || defaultTheme;
|
||||
document.documentElement.dataset.theme = savedTheme;
|
||||
} catch(e) {
|
||||
// Silently fail if localStorage is unavailable
|
||||
document.documentElement.dataset.theme = "light";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
<link rel="icon" href="/icon.png" type="image/png">
|
||||
<link rel="icon" href="/icon.svg" type="image/svg+xml">
|
||||
<link rel="apple-touch-icon" href="/icon.png">
|
||||
<%= vite_client_tag %>
|
||||
<%= vite_stylesheet_tag "application.css" %>
|
||||
<%= vite_javascript_tag 'application' %>
|
||||
</head>
|
||||
<body>
|
||||
<%= yield %>
|
||||
<% unless Rails.env.production? %>
|
||||
<div class="environment <%= Rails.env %>" />
|
||||
<% end %>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
<% flash.each do |type, message| %>
|
||||
<% next if type == "sentry_event_id" %> <!-- Skip event_id from being rendered as message -->
|
||||
<% if message.present? %>
|
||||
<%= render Components::Banner.new(kind: type) do %>
|
||||
<%= render Components::Banner.new(kind: type, event_id: %w[error alert danger].include?(type) ? flash[:sentry_event_id] : nil) do %>
|
||||
<%= message %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
|
|
|
|||
|
|
@ -49,9 +49,12 @@ module IdentityVault
|
|||
unless Rails.env.development?
|
||||
config.rails_semantic_logger.add_file_appender = false
|
||||
config.semantic_logger.add_appender(io: $stdout, formatter: :json)
|
||||
config.semantic_logger.add_appender(appender: :honeybadger_insights)
|
||||
config.semantic_logger.add_appender(appender: :sentry_ruby)
|
||||
end
|
||||
|
||||
# Use dynamic error pages to display Sentry event IDs
|
||||
config.exceptions_app = self.routes
|
||||
|
||||
config.to_prepare do
|
||||
Doorkeeper::ApplicationController.layout "logged_out"
|
||||
Doorkeeper::ApplicationController.skip_before_action :authenticate_identity!
|
||||
|
|
|
|||
|
|
@ -1,39 +0,0 @@
|
|||
---
|
||||
# For more options, see https://docs.honeybadger.io/lib/ruby/gem-reference/configuration
|
||||
|
||||
api_key: '<%= ENV["HONEYBADGER_API_KEY"] %>'
|
||||
|
||||
# The environment your app is running in.
|
||||
env: "<%= Rails.env %>"
|
||||
|
||||
# The absolute path to your project folder.
|
||||
root: "<%= Rails.root.to_s %>"
|
||||
|
||||
# Honeybadger won't report errors in these environments.
|
||||
development_environments:
|
||||
- test
|
||||
- development
|
||||
- cucumber
|
||||
|
||||
# By default, Honeybadger won't report errors in the development_environments.
|
||||
# You can override this by explicitly setting report_data to true or false.
|
||||
# report_data: true
|
||||
|
||||
# The current Git revision of your project. Defaults to the last commit hash.
|
||||
# revision: null
|
||||
|
||||
# Enable verbose debug logging (useful for troubleshooting).
|
||||
debug: false
|
||||
|
||||
# Enable Honeybadger Insights
|
||||
insights:
|
||||
enabled: true
|
||||
rails:
|
||||
insights:
|
||||
metrics: true
|
||||
net_http:
|
||||
insights:
|
||||
metrics: true
|
||||
puma:
|
||||
insights:
|
||||
metrics: true
|
||||
18
config/initializers/sentry.rb
Normal file
18
config/initializers/sentry.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
git_hash = ENV["SOURCE_COMMIT"] || `git rev-parse HEAD` rescue "unknown"
|
||||
short_hash = git_hash[0..7]
|
||||
is_dirty = `git status --porcelain`.strip.length > 0 rescue false
|
||||
git_version = is_dirty ? "#{short_hash}-dirty" : short_hash
|
||||
|
||||
Sentry.init do |config|
|
||||
config.dsn = ENV["SENTRY_DSN"]
|
||||
config.environment = Rails.env
|
||||
config.release = git_version
|
||||
config.enabled_environments = %w[production staging uat]
|
||||
config.breadcrumbs_logger = [ :active_support_logger, :http_logger ]
|
||||
config.send_default_pii = true
|
||||
config.traces_sample_rate = Rails.env.production? ? 0.1 : 1.0
|
||||
config.enabled_patches = [ :http, :redis, :puma ]
|
||||
|
||||
# Capture error event IDs in Rack env for error pages
|
||||
config.rails.report_rescued_exceptions = true
|
||||
end
|
||||
|
|
@ -367,6 +367,11 @@ Rails.application.routes.draw do
|
|||
get "/auth", to: "saml#sp_initiated_get"
|
||||
end
|
||||
|
||||
# Error pages
|
||||
match "/404", to: "errors#not_found", via: :all
|
||||
match "/422", to: "errors#unprocessable_entity", via: :all
|
||||
match "/500", to: "errors#internal_server_error", via: :all
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
# Can be used by load balancers and uptime monitors to verify that the app is live.
|
||||
get "up" => "rails/health#show", as: :rails_health_check
|
||||
|
|
|
|||
114
public/404.html
114
public/404.html
|
|
@ -1,114 +0,0 @@
|
|||
<!doctype html>
|
||||
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
|
||||
<title>The page you were looking for doesn’t exist (404 Not found)</title>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="initial-scale=1, width=device-width">
|
||||
<meta name="robots" content="noindex, nofollow">
|
||||
|
||||
<style>
|
||||
|
||||
*, *::before, *::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
body {
|
||||
background: #FFF;
|
||||
color: #261B23;
|
||||
display: grid;
|
||||
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, Aptos, Roboto, "Segoe UI", "Helvetica Neue", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
|
||||
font-size: clamp(1rem, 2.5vw, 2rem);
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
letter-spacing: -0.0025em;
|
||||
line-height: 1.4;
|
||||
min-height: 100vh;
|
||||
place-items: center;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
font-weight: 700;
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 0.0925em;
|
||||
}
|
||||
|
||||
b, strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
i, em {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
main {
|
||||
display: grid;
|
||||
gap: 1em;
|
||||
padding: 2em;
|
||||
place-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
main header {
|
||||
width: min(100%, 12em);
|
||||
}
|
||||
|
||||
main header svg {
|
||||
height: auto;
|
||||
max-width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
main article {
|
||||
width: min(100%, 30em);
|
||||
}
|
||||
|
||||
main article p {
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
main article br {
|
||||
|
||||
display: none;
|
||||
|
||||
@media(min-width: 48em) {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
</head>
|
||||
|
||||
<body>
|
||||
|
||||
<!-- This file lives in public/404.html -->
|
||||
|
||||
<main>
|
||||
<header>
|
||||
<svg height="172" viewBox="0 0 480 172" width="480" xmlns="http://www.w3.org/2000/svg"><path d="m124.48 3.00509-45.6889 100.02991h26.2239v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.1833v-31.901l50.2851-103.27391zm115.583 168.69891c-40.822 0-64.884-35.146-64.884-85.7015 0-50.5554 24.062-85.700907 64.884-85.700907 40.823 0 64.884 35.145507 64.884 85.700907 0 50.5555-24.061 85.7015-64.884 85.7015zm0-133.2831c-17.572 0-22.709 21.8984-22.709 47.5816 0 25.6835 5.137 47.5815 22.709 47.5815 17.303 0 22.71-21.898 22.71-47.5815 0-25.6832-5.407-47.5816-22.71-47.5816zm165.328-35.41581-45.689 100.02991h26.224v-28.1168h38.119v28.1168h21.628v35.145h-21.628v30.82h-37.308v-30.82h-72.184v-31.901l50.285-103.27391z" fill="#f0eff0"/><path d="m157.758 68.9967v34.0033h-7.199l-14.233-19.8814v19.8814h-8.584v-34.0033h8.307l13.125 18.7184v-18.7184zm28.454 21.5428c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.528 0c0-3.4336-1.496-5.8703-4.209-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.209-2.3813 4.209-5.8149zm13.184 3.8766v-9.5807h-3.655v-6.7564h3.655v-6.8671h8.584v6.8671h5.205v6.7564h-5.205v8.307c0 1.9383.941 2.769 2.658 2.769.941 0 1.994-.2216 2.769-.5538v7.3654c-.997.443-2.88.775-4.818.775-5.87 0-9.193-2.769-9.193-9.0819zm37.027 8.5839h-8.806v-34.0033h23.924v7.6978h-15.118v6.7564h13.9v7.5316h-13.9zm41.876-12.4605c0 7.6978-5.15 13.0145-12.737 13.0145-7.532 0-12.738-5.3167-12.738-13.0145s5.206-13.0143 12.738-13.0143c7.587 0 12.737 5.3165 12.737 13.0143zm-8.529 0c0-3.4336-1.495-5.8703-4.208-5.8703-2.659 0-4.154 2.4367-4.154 5.8703s1.495 5.8149 4.154 5.8149c2.713 0 4.208-2.3813 4.208-5.8149zm35.337-12.4605v24.921h-8.695v-2.16c-1.329 1.551-3.821 2.714-6.646 2.714-5.482 0-8.75-3.5999-8.75-9.1379v-16.3371h8.64v14.288c0 2.1045.997 3.5997 3.212 3.5997 1.606 0 3.101-1.0522 3.544-2.769v-15.1187zm4.076 24.921v-24.921h8.694v2.1598c1.385-1.5506 3.822-2.7136 6.701-2.7136 5.538 0 8.806 3.5997 8.806 9.1377v16.3371h-8.639v-14.2327c0-2.049-1.053-3.5443-3.268-3.5443-1.717 0-3.156.9969-3.6 2.7136v15.0634zm44.113 0v-1.994c-1.163 1.329-3.6 2.548-6.147 2.548-7.2 0-11.132-5.8151-11.132-13.0145s3.932-13.0143 11.132-13.0143c2.547 0 4.984 1.2184 6.147 2.5475v-13.0697h8.695v35.997zm0-9.1931v-6.5902c-.665-1.3291-2.16-2.326-3.821-2.326-2.991 0-4.763 2.4368-4.763 5.6488s1.772 5.5934 4.763 5.5934c1.717 0 3.156-.9415 3.821-2.326z" fill="#d30001"/></svg>
|
||||
</header>
|
||||
<article>
|
||||
<p><strong>The page you were looking for doesn’t exist.</strong> You may have mistyped the address or the page may have moved. If you’re the application owner check the logs for more information.</p>
|
||||
</article>
|
||||
</main>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
114
public/422.html
114
public/422.html
File diff suppressed because one or more lines are too long
114
public/500.html
114
public/500.html
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue