Skip to content
This repository has been archived by the owner on Apr 20, 2024. It is now read-only.

Commit

Permalink
Merge pull request #58 from nodes-vapor/feature/add-reportable-error-…
Browse files Browse the repository at this point in the history
…protocol

Add ReportableErrorProtocol
  • Loading branch information
siemensikkema authored Jul 31, 2019
2 parents 17949ac + 28f8d2f commit 5d769f0
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 116 deletions.
11 changes: 10 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ public func configure(
```

### Reporting
Bugsnag offers three different types of reports: info, warning and error. To make a report just instantiate a `ErrorReporter` and use the respective functions.
Bugsnag offers three different types of reports: info, warning and error. To make a report just instantiate an `ErrorReporter` and use the respective functions.

##### Examples
```swift
Expand All @@ -72,6 +72,15 @@ reporter.report(
)
```

By conforming to the `ReportableError` protocol you can have full control over how (and if) the BugsnagMiddleware reports your errors. It has the following properties:

| Name | Type | Function | Default |
|---|---|---|---|
| `shouldReport` | `Bool` | Opt out of error reporting by returning `false` | `true` |
| `severity` | `Severity` | Indicate error severity (`.info`\|`.warning`\|`.error`) | `.error` |
| `userId` | `CustomStringConvertible?` | An optional user id associated with the error | `nil` |
| `metadata` | `[String: CustomDebugStringConvertible]` | Additional metadata to include in the report | `[:]` |

#### Users
Conforming your `Authenticatable` model to `BugsnagReportableUser` allows you to easily pair the data to a report. The protocol requires your model to have an `id` field that is `CustomStringConvertible`.

Expand Down
5 changes: 2 additions & 3 deletions Sources/Bugsnag/Bugsnag.swift
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,10 @@ struct BugsnagException: Encodable {
let type: String

init(error: Error, stacktrace: BugsnagStacktrace) {
let abort = error as? AbortError
self.errorClass = error.localizedDescription
self.message = abort?.reason ?? "Something went wrong"
self.message = (error as? Debuggable)?.reason ?? "Something went wrong"
self.stacktrace = [stacktrace]
self.type = (abort?.status ?? .internalServerError).reasonPhrase
self.type = ((error as? AbortError)?.status ?? .internalServerError).reasonPhrase
}
}

Expand Down
27 changes: 22 additions & 5 deletions Sources/Bugsnag/BugsnagMiddleware.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,30 @@ public struct BugsnagMiddleware {

extension BugsnagMiddleware: Middleware {
public func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> {
return Future.flatMap(on: req) {
return Future
.flatMap(on: req) {
try next.respond(to: req)
}.catchFlatMap { error in
self.reporter
.report(error, on: req)
.map { throw error }
}
.catchFlatMap { error in
self.handleError(error, on: req).map { throw error }
}
}

private func handleError(_ error: Error, on container: Container) -> Future<Void> {
if let reportableError = error as? ReportableError {
guard reportableError.shouldReport else {
return container.future()
}
return self.reporter.report(
reportableError,
severity: reportableError.severity,
userId: reportableError.userId,
metadata: reportableError.metadata,
on: container
)
} else {
return self.reporter.report(error, on: container)
}
}
}

Expand Down
22 changes: 22 additions & 0 deletions Sources/Bugsnag/ReportableError.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// Errors conforming to this protocol have more control about how (or if) they will be reported.
public protocol ReportableError: Error {

/// Whether to report this error (defaults to `true`)
var shouldReport: Bool { get }

/// Error severity (defaults to `.error`)
var severity: Severity { get }

/// The associated user id (if any) for the error (defaults to `nil`)
var userId: CustomStringConvertible? { get }

/// Any additional metadata (defaults to `[:]`)
var metadata: [String: CustomDebugStringConvertible] { get }
}

public extension ReportableError {
var shouldReport: Bool { return true }
var severity: Severity { return .error }
var userId: CustomStringConvertible? { return nil }
var metadata: [String: CustomDebugStringConvertible] { return [:] }
}
106 changes: 3 additions & 103 deletions Tests/BugsnagTests/BugsnagTests.swift
Original file line number Diff line number Diff line change
@@ -1,108 +1,8 @@
import Vapor
import Bugsnag
import XCTest
@testable import Bugsnag

extension Application {
public static func test() throws -> Application {
var services = Services()
try services.register(BugsnagProvider(config: BugsnagConfig(
apiKey: "e9792272fae71a3b869a1152008f7f0f",
releaseStage: "development"
)))

var middlewaresConfig = MiddlewareConfig()
middlewaresConfig.use(BugsnagMiddleware.self)
services.register(middlewaresConfig)

let sharedThreadPool = BlockingIOThreadPool(numberOfThreads: 2)
sharedThreadPool.start()
services.register(sharedThreadPool)

return try Application(config: Config(), environment: .testing, services: services)
}
}

private class TestErrorReporter: ErrorReporter {

var capturedReportParameters: (
error: Error,
severity: Severity,
userId: CustomStringConvertible?,
metadata: [String: CustomDebugStringConvertible],
file: String,
function: String,
line: Int,
column: Int,
container: Container
)?
func report(
_ error: Error,
severity: Severity,
userId: CustomStringConvertible?,
metadata: [String: CustomDebugStringConvertible],
file: String,
function: String,
line: Int,
column: Int,
on container: Container
) -> Future<Void> {
capturedReportParameters = (
error,
severity,
userId,
metadata,
file,
function,
line,
column,
container
)
return container.future()
}
}

private class TestResponder: Responder {
var mockErrorToThrow: Error?
var mockErrorToReturnInFuture: Error?
func respond(to req: Request) throws -> Future<Response> {
if let error = mockErrorToThrow {
throw error
} else if let error = mockErrorToReturnInFuture {
return req.future(error: error)
} else {
return req.future(Response(using: req))
}
}
}
import Vapor

final class BugsnagTests: XCTestCase {
func testMiddleware() throws {
let application = try Application.test()
let request = Request(using: application)
let errorReporter = TestErrorReporter()
let middleware = BugsnagMiddleware(reporter: errorReporter)
let responder = TestResponder()
_ = try middleware.respond(to: request, chainingTo: responder).wait()

// expect no error reported when response is successful
XCTAssertNil(errorReporter.capturedReportParameters)

responder.mockErrorToThrow = NotFound()

_ = try? middleware.respond(to: request, chainingTo: responder).wait()

// expect an error to be reported when responder throws
XCTAssertNotNil(errorReporter.capturedReportParameters)

errorReporter.capturedReportParameters = nil
responder.mockErrorToReturnInFuture = NotFound()

_ = try? middleware.respond(to: request, chainingTo: responder).wait()

// expect an error to be reported when responder returns an errored future
XCTAssertNotNil(errorReporter.capturedReportParameters)
}

func testSendReport() throws {
var capturedSendReportParameters: (
host: String,
Expand All @@ -116,7 +16,7 @@ final class BugsnagTests: XCTestCase {
sendReport: { host, headers, data, container in
capturedSendReportParameters = (host, headers, data, container)
return container.future(Response(http: HTTPResponse(status: .ok), using: container))
})
})
let application = try Application.test()
let request = Request(using: application)
request.breadcrumb(name: "a", type: .log)
Expand Down
154 changes: 154 additions & 0 deletions Tests/BugsnagTests/MiddlewareTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import Vapor
import XCTest
@testable import Bugsnag

extension Application {
public static func test() throws -> Application {
var services = Services()
try services.register(BugsnagProvider(config: BugsnagConfig(
apiKey: "e9792272fae71a3b869a1152008f7f0f",
releaseStage: "development"
)))

var middlewaresConfig = MiddlewareConfig()
middlewaresConfig.use(BugsnagMiddleware.self)
services.register(middlewaresConfig)

let sharedThreadPool = BlockingIOThreadPool(numberOfThreads: 2)
sharedThreadPool.start()
services.register(sharedThreadPool)

return try Application(config: Config(), environment: .testing, services: services)
}
}

final class TestErrorReporter: ErrorReporter {

var capturedReportParameters: (
error: Error,
severity: Severity,
userId: CustomStringConvertible?,
metadata: [String: CustomDebugStringConvertible],
file: String,
function: String,
line: Int,
column: Int,
container: Container
)?
func report(
_ error: Error,
severity: Severity,
userId: CustomStringConvertible?,
metadata: [String: CustomDebugStringConvertible],
file: String,
function: String,
line: Int,
column: Int,
on container: Container
) -> Future<Void> {
capturedReportParameters = (
error,
severity,
userId,
metadata,
file,
function,
line,
column,
container
)
return container.future()
}
}

final class TestResponder: Responder {
var mockErrorToThrow: Error?
var mockErrorToReturnInFuture: Error?
func respond(to req: Request) throws -> Future<Response> {
if let error = mockErrorToThrow {
throw error
} else if let error = mockErrorToReturnInFuture {
return req.future(error: error)
} else {
return req.future(Response(using: req))
}
}
}

final class MiddlewareTests: XCTestCase {
var application: Application!
var request: Request!
var middleware: BugsnagMiddleware!

let errorReporter = TestErrorReporter()
let responder = TestResponder()

override func setUp() {
application = try! Application.test()
request = Request(using: application)
middleware = BugsnagMiddleware(reporter: errorReporter)
}

func testNoErrorReportedByDefault() throws {
_ = try middleware.respond(to: request, chainingTo: responder).wait()

// expect no error reported when response is successful
XCTAssertNil(errorReporter.capturedReportParameters)
}

func testRespondErrorsAreCaptured() throws {
responder.mockErrorToThrow = NotFound()

_ = try? middleware.respond(to: request, chainingTo: responder).wait()

// expect an error to be reported when responder throws
XCTAssertNotNil(errorReporter.capturedReportParameters)
}

func testErrorsInFutureAreCaptured() throws {
errorReporter.capturedReportParameters = nil
responder.mockErrorToReturnInFuture = NotFound()

_ = try? middleware.respond(to: request, chainingTo: responder).wait()

// expect an error to be reported when responder returns an errored future
XCTAssertNotNil(errorReporter.capturedReportParameters)
}

func testReportableErrorPropertiesAreRespected() throws {
struct MyError: ReportableError {
let severity = Severity.info
let userId: CustomStringConvertible? = 123
let metadata: [String: CustomDebugStringConvertible] = ["meta": "data"]
}

let error = MyError()
responder.mockErrorToThrow = error

_ = try? middleware.respond(to: request, chainingTo: responder).wait()

guard
let params = errorReporter.capturedReportParameters
else {
XCTFail("No error was thrown")
return
}

XCTAssertNotNil(params.error as? MyError)
XCTAssertEqual(params.metadata as? [String: String], ["meta": "data"])
XCTAssertEqual(params.severity.value, "info")
XCTAssertEqual(params.userId as? Int, 123)
}

func testOptOutOfErrorReporting() throws {
struct MyError: ReportableError {
let shouldReport = false
}

responder.mockErrorToThrow = MyError()

_ = try? middleware.respond(to: request, chainingTo: responder).wait()

XCTAssertNil(errorReporter.capturedReportParameters)
}
}
Loading

0 comments on commit 5d769f0

Please sign in to comment.