1 of 105

SurroundSound, Bugsnag and SumoLogic�Tools for Fixing Errors

{

"Brad Urani" : "Staff Engineer",

"Company" : " ",

}

2 of 105

If we can't tell errors from noise, we can't fix errors

3 of 105

Caution!

Don't

Over-Engineer

@BradUrani

4 of 105

@BradUrani

5 of 105

raise 'Oh no!'

@BradUrani

6 of 105

raise RuntimeError.new('Oh no!')

@BradUrani

7 of 105

raise RuntimeError.new('Oh no!')

=

raise 'Oh no!'

=

raise RuntimeError, 'Oh no!'

@BradUrani

8 of 105

Error vs. Exception

Exception

NoMemoryError

SignalException

StandardError

ArgumentError

IOError

EOFError

RuntimeError

ZeroDivisionError

SystemStackError

@BradUrani

9 of 105

@BradUrani

10 of 105

def StandardError < Exception

end

def ZeroDivisionError < StandardError

end

@BradUrani

11 of 105

begin� �rescue => e� �end

@BradUrani

12 of 105

begin� �rescue StandardError => e� �end

@BradUrani

13 of 105

Default for raise

RuntimeError

Default for rescue

StandardError

class RuntimeError < StandardError

end

@BradUrani

14 of 105

begin� �rescue Exception => e� �end

@BradUrani

15 of 105

SystemStackError

NoMemoryError

SignalException::Interrupt

ScriptError::SyntaxError

@BradUrani

16 of 105

class OutOfNachosError < StandardError

end

raise OutOfNachosError.new('PANIC NOW!')

@BradUrani

17 of 105

Active Record

StandardError

ActiveRecord::ActiveRecordError

ActiveRecord::RecordInvalidError

ActiveRecord::ConnectionNotEstablished

ActiveRecord::RecordNotFound

ActiveRecord::StatementInvalid

ActiveRecord::WrappedDatabaseException

ActiveRecord::RecordNotUnique

ActiveRecord::InvalidForeignKey

@BradUrani

18 of 105

Rails

@BradUrani

19 of 105

20 of 105

def create

@user = User.new(params[:user])

if @user.save

redirect_to @client

else

render "new"

end

end

end

@BradUrani

21 of 105

def create

@user = User.new(params[:user])

if @user.save

redirect_to @user

else

render "new"

end

rescue

flash[:error] = "Please customer contact support"

render "new"

end

@BradUrani

22 of 105

Don't rescue just because you can

(It's ok to crash in rare or unknown circumstances)

@BradUrani

23 of 105

Computers

Users

Developers

@BradUrani

24 of 105

Computers

Users

user_message

redirects�

Developers

message

metadata

reports

logs

Class hierarchy

HTTP Status Codes

@BradUrani

25 of 105

  • Control User Facing Error Message
  • Control Developer Facing Error Message
  • Control HTTP Status Code
  • Adding Contextual Data
  • Flexible Notifications
    • Email, SMS, Slack
  • Logging
  • Response Format (HTML vs JSON)

Goal

@BradUrani

26 of 105

def create

@user = User.new(params[:user])

if @user.save

redirect_to @user

else

render "new"

end

rescue

flash[:error] =" Please customer contact support"

render "new"

end

@BradUrani

27 of 105

DO NOT SWALLOW ERRORS!!

28 of 105

DO NOT SWALLOW ERRORS!!

29 of 105

DO NOT SWALLOW ERRORS!!

30 of 105

31 of 105

Reporting

@BradUrani

32 of 105

@BradUrani

33 of 105

@BradUrani

34 of 105

@BradUrani

35 of 105

  • Searchable
  • Configurable Alerts (email, SMS, Slack)
  • Environment Aware
  • No config
  • Free Tier

Standard Features

@BradUrani

36 of 105

  • Severity Level
  • Custom Metadata

Power User Features

@BradUrani

37 of 105

def create

@user = User.new(params[:user])

if @user.save

redirect_to @user

else

render "new"

end

rescue

flash[:error] =" Please contact customer support"

render "new"

end

@BradUrani

38 of 105

def create

...

rescue => e

flash[:error] << " Please contact customer support"

Procore::ErrorHandler.handle(e)

render "new"

end

@BradUrani

39 of 105

class Procore::ErrorHandler

def self.handle(error)

Bugsnag.notify(error)

end

end

@BradUrani

40 of 105

class Procore::ErrorHandler

def self.handle(error, severity: :error)

Bugsnag.notify(error, severity: severity)

end

end

@BradUrani

41 of 105

def create

...

rescue => e

flash[:error] << " Please contact customer support"

Procore::ErrorHandler.handle(e, severity: :warn)

render "new"

end

@BradUrani

42 of 105

class Procore::ErrorHandler

def self.handle(error, severity: :error)

Bugsnag.notify(error, severity: severity)

end

end

@BradUrani

43 of 105

class Procore::ErrorHandler

def self.handle(error, severity: :error)

Bugsnag.notify(error, severity: severity)

log_error(error, severity)

End

def self.log_error(error, severity)

Rails.logger.send(severity, build_message(error))

end

end

@BradUrani

44 of 105

def create

...

rescue => e

flash[:error] << " Please contact customer support"

Procore::ErrorHandler.handle(e, severity: :warn, metadata: {

tool: :onboarding

})

render "new"

end

@BradUrani

45 of 105

@BradUrani

46 of 105

  • Adding Contextual Data
  • Flexible Notifications
    • Email, SMS, Slack

@BradUrani

47 of 105

Custom

Error

Classes

48 of 105

Deep Call Stacks

PurchaseController#create

Purchase.start

remove_items_from_inventory

item.remove_from_inventory

warehouse.make_unpurchaseable

warehouse.mark_as_purchased

warehouse_item.subtract_qty

warehouse.check_available!

49 of 105

PurchaseController#create

Purchase.start

remove_items_from_inventory

item.remove_from_inventory

warehouse.make_unpurchaseable

warehouse.mark_as_purchased

warehouse_item.subtract_qty

warehouse.check_available!

We know here

What message to display here

50 of 105

result, err := SomeFunction()�if err != nil {� // handle the error�}

@BradUrani

51 of 105

case File.read "hello" do

{:ok, body} -> IO.puts "Success: #{body}"

{:error, reason} -> IO.puts "Error: #{reason}"

end

52 of 105

PurchaseController#create

Purchase.start

remove_items_from_inventory

item.remove_from_inventory

warehouse.make_unpurchaseable

warehouse.mark_as_purchased

warehouse_item.subtract_qty

warehouse.check_available!

raise here

rescue here

53 of 105

A Custom Error Hierarchy

Store::Error

Store::PurchaseError

Store::InventoryError

Store::CouponError

Store::OutOfSeasonError

Store::InadequateInventory

54 of 105

def create

rescue PurchaseError => pe

Store::ErrorHandler.handle(e, { user_id: @user.id }, :warn)

flash[:notify] << pe.user_message

end

@BradUrani

55 of 105

def create

rescue InventoryError => ie

Store::Handler.handle(e, { user_id: @user.id }, :warn)

redirect_to shopping_cart_path(...)

rescue CouponError => ce

Store::Handler.handle(e, { user_id: @user.id }, :info)

redirect_to apply_coupon_path(...)

end

@BradUrani

56 of 105

def create

rescue PurchaseError => pe

Procore::ErrorHandler.handle(e, { user_id: @user.id }, :warn)

flash[:notify] << pe.user_message

end

@BradUrani

57 of 105

Don't expose Exception#message to the user

@BradUrani

58 of 105

Store::Error

Store::SecurityError

Store::PermissionError

Store::UnauthorizedError

Store::ReadOnlyError

Store::InsufficientPermissionError

@BradUrani

59 of 105

class PurchaseController < ApplicationController

rescue_from Store::UnauthorizedError do |error|

Store::Handler.handle(e)

respond_to do |format|

format.html { redirect_to unauthorized_path, status: :unauthorized }

format.json { render json: { error.user_message }, status: :unauthorized }

end

end

end

@BradUrani

60 of 105

def show

Nacho.find(1_000_000_000)

end

# raises ActiveRecord::RecordInvalid

@BradUrani

61 of 105

62 of 105

def create

Nacho.create!({ cheesiness: 1_000_000 })

end

# raises ActiveRecord::RecordInvalid

@BradUrani

63 of 105

@BradUrani

64 of 105

{

"status": 404,

"error": "Not Found"

}

@BradUrani

65 of 105

module ActionDispatchclass ExceptionWrapper� cattr_accessor :rescue_responses@@rescue_responses = Hash.new(:internal_server_error)� @@rescue_responses.merge!(� 'ActionController::RoutingError' => :not_found,� 'AbstractController::ActionNotFound' => :not_found,� 'ActionController::MethodNotAllowed' => :method_not_allowed,� 'ActionController::UnknownHttpMethod' => :method_not_allowed,� 'ActionController::NotImplemented' => :not_implemented,� 'ActionController::UnknownFormat' => :not_acceptable,� 'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,� 'ActionController::InvalidCrossOriginRequest' => :unprocessable_entity,� 'ActionDispatch::ParamsParser::ParseError' => :bad_request,� 'ActionController::BadRequest' => :bad_request,� 'ActionController::ParameterMissing' => :bad_request,� 'Rack::Utils::ParameterTypeError' => :bad_request,� 'Rack::Utils::InvalidParameterError' => :bad_request� )� ...� endend

66 of 105

A Custom Error Hierarchy

Store::Error

Store::PurchaseError

Store::InventoryError

Store::CouponError

Store::OutOfSeasonError

Store::InadequateInventory

67 of 105

class Store::Error < StandardError

attr_accessor :metadata

attr_accessor :severity

def initialize(message, metadata = {}, severity = :error)

super(message)

@metadata = metadata

@severity = severity

end

def user_message

I18n.t('errors.unknown')

end

end

68 of 105

class Store::PurchaseError < Store::Error

end

@BradUrani

69 of 105

class Store::InventoryError < Store::PurchaseError

end

@BradUrani

70 of 105

class Store::InadequateInventoryError < Store::InventoryError

def initialize(product_name, requested_qty, available_qty)

super('Inadequate Inventory', {

product_name: product_name,

requested_qty: requested_qty,

available_qty: available_qty

})

end

def user_message

I18n.t('errors.inadequate_inventory_error', product_name)

end

end

71 of 105

def create

rescue PurchaseError => pe

Procore::ErrorHandler.handle(e, { user_id: @user.id }, :warn)

flash[:notify] << pe.user_message

end

@BradUrani

72 of 105

def create

rescue InventoryError => ie

Store::Handler.handle(e, { user_id: @user.id }, :warn)

redirect_to shopping_cart_path(...)

rescue CouponError => ce

Store::Handler.handle(e, { user_id: @user.id }, :info)

redirect_to apply_coupon_path(...)

end

@BradUrani

73 of 105

PurchaseController#create

Purchase.start

remove_items_from_inventory

item.remove_from_inventory

warehouse.make_unpurchaseable

warehouse.mark_as_purchased

warehouse_item.subtract_qty

warehouse.check_available!

raise

rescue

product_name

requested_qty

available_qty

user_id

74 of 105

PurchaseController#create

Purchase.start

remove_items_from_inventory

item.remove_from_inventory

warehouse.make_unpurchaseable

warehouse.mark_as_purchased

warehouse_item.subtract_qty

warehouse.check_available!

raise

rescue

product_name

requested_qty

available_qty

user_id

coupon_code

???

75 of 105

def remove_items_from_inventory

...

rescue StoreError => se

se.add_metadata(coupon_code: code)

raise

end

@BradUrani

76 of 105

Why Rescue Errors and Re-Raise?

  • Add metadata

@BradUrani

77 of 105

PurchaseController#create

Purchase.start

remove_items_from_inventory

apply_coupon_code

clear_cart

process_payment

@BradUrani

78 of 105

ActiveRecord::Base.transaction do

remove_items_from_inventory

apply_coupon_code

clear_cart

process_payment

end

end

@BradUrani

79 of 105

Rescue and Continue

def get_shipping_price

get_quote_from_ups

rescue Store::ShippingPriceUnavailable => rce

Store::Handler.handle(rce)

6.99

end

end

@BradUrani

80 of 105

Raise and forget

      • Unknown Errors
      • System errors
      • Connectivity Errors

@BradUrani

81 of 105

Raise and rescue in controller

      • Change Status Code
      • Customize Error Message
      • "Soft" Error Pages

@BradUrani

82 of 105

Raise, rescue, continue

  • Anything that is a problem but not enough to stop us from continuing

@BradUrani

83 of 105

Wrapping

Errors

84 of 105

85 of 105

PurchaseController#create

Purchase.start

remove_items_from_inventory

item.remove_from_inventory

warehouse.make_unpurchaseable

warehouse.mark_as_purchased

warehouse_item.subtract_qty

warehouse.check_available!

86 of 105

def check_availability!

result = RestClient.get(...)

...

rescue RestClient::RequestTimeout => rte

raise Store::InventoryUnknownError.new(product_name, request_qty)

end

@BradUrani

87 of 105

def check_availability!

result = RestClient.get(...)

...

rescue RestClient::RequestTimeout => rte

raise Store::InventoryUnknownError.new(product_name, request_qty)

end

begin

check_availability!

rescue => e

puts e.cause.class

end

> RestClient::RequestTimeout < RestClient::RequestFailed

88 of 105

89 of 105

Why Wrap

  • Add metadata
  • Change HTTP Status Code
  • Control Flow

@BradUrani

90 of 105

91 of 105

@BradUrani

92 of 105

Extras

93 of 105

def check_availability!

result = RestClient.get(...)

...

rescue RestClient::RequestTimeout => rte

raise Store::InventoryUnknownError.new(id: 'A3DHJ56')

end

Unique IDs at raise

@BradUrani

94 of 105

def check_availability!

result = RestClient.get(...)

...

rescue RestClient::RequestTimeout => rte

raise Store::InventoryUnknownError.new({

url: 'http://docs.procore.com/unknown_inventory_error'

})

end

Docs

@BradUrani

95 of 105

For every error raised

  • User-Facing Message
  • Programmer-facing message
  • HTTP Status Code
  • Contextual metadata
  • Notifications
    • Slack, Bugsnag, Pager Duty
  • Logging
  • Control Flow
    • Continue
    • Fail
    • Show message

96 of 105

Rescued an Error

Security Error?

Show 404 Page

404

Page not Found

404

User not

Authorized?

Redirect to Auth Request Page

302

Unauthorized

401

Unknown Error?

Error Page (reraise)

500

Unknown Error

500

Availability

Error?

Redirect to Status Page

503

Status Error

503

Everything Else

Validation Error?

Data Error?

Reload Form

(handle in controller)

400

Error Map

Response

400

JSON

HTML

HTML

JSON

HTML

JSON

HTML

JSON

HTML

JSON

notify devops

notify responsible team

on Bugsnag

record to availability log

97 of 105

Further Reading

@BradUrani

98 of 105

Who am I?

@BradUrani

I tweet at:

Connect with me:

I work in Santa Barbara at:

@bradurani@somewhy.com

99 of 105

100 of 105

Tomorrow

1:50 pm

​RAILS APIS: THE NEXT GENERATION

Derek Carter

Room: 162

101 of 105

102 of 105

103 of 105

104 of 105

105 of 105