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

Commit 5d769f0

Browse files
Merge pull request #58 from nodes-vapor/feature/add-reportable-error-protocol
Add ReportableErrorProtocol
2 parents 17949ac + 28f8d2f commit 5d769f0

File tree

7 files changed

+233
-116
lines changed

7 files changed

+233
-116
lines changed

README.md

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public func configure(
5252
```
5353

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

5757
##### Examples
5858
```swift
@@ -72,6 +72,15 @@ reporter.report(
7272
)
7373
```
7474

75+
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:
76+
77+
| Name | Type | Function | Default |
78+
|---|---|---|---|
79+
| `shouldReport` | `Bool` | Opt out of error reporting by returning `false` | `true` |
80+
| `severity` | `Severity` | Indicate error severity (`.info`\|`.warning`\|`.error`) | `.error` |
81+
| `userId` | `CustomStringConvertible?` | An optional user id associated with the error | `nil` |
82+
| `metadata` | `[String: CustomDebugStringConvertible]` | Additional metadata to include in the report | `[:]` |
83+
7584
#### Users
7685
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`.
7786

Sources/Bugsnag/Bugsnag.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,11 +87,10 @@ struct BugsnagException: Encodable {
8787
let type: String
8888

8989
init(error: Error, stacktrace: BugsnagStacktrace) {
90-
let abort = error as? AbortError
9190
self.errorClass = error.localizedDescription
92-
self.message = abort?.reason ?? "Something went wrong"
91+
self.message = (error as? Debuggable)?.reason ?? "Something went wrong"
9392
self.stacktrace = [stacktrace]
94-
self.type = (abort?.status ?? .internalServerError).reasonPhrase
93+
self.type = ((error as? AbortError)?.status ?? .internalServerError).reasonPhrase
9594
}
9695
}
9796

Sources/Bugsnag/BugsnagMiddleware.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,30 @@ public struct BugsnagMiddleware {
77

88
extension BugsnagMiddleware: Middleware {
99
public func respond(to req: Request, chainingTo next: Responder) throws -> Future<Response> {
10-
return Future.flatMap(on: req) {
10+
return Future
11+
.flatMap(on: req) {
1112
try next.respond(to: req)
12-
}.catchFlatMap { error in
13-
self.reporter
14-
.report(error, on: req)
15-
.map { throw error }
1613
}
14+
.catchFlatMap { error in
15+
self.handleError(error, on: req).map { throw error }
16+
}
17+
}
18+
19+
private func handleError(_ error: Error, on container: Container) -> Future<Void> {
20+
if let reportableError = error as? ReportableError {
21+
guard reportableError.shouldReport else {
22+
return container.future()
23+
}
24+
return self.reporter.report(
25+
reportableError,
26+
severity: reportableError.severity,
27+
userId: reportableError.userId,
28+
metadata: reportableError.metadata,
29+
on: container
30+
)
31+
} else {
32+
return self.reporter.report(error, on: container)
33+
}
1734
}
1835
}
1936

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/// Errors conforming to this protocol have more control about how (or if) they will be reported.
2+
public protocol ReportableError: Error {
3+
4+
/// Whether to report this error (defaults to `true`)
5+
var shouldReport: Bool { get }
6+
7+
/// Error severity (defaults to `.error`)
8+
var severity: Severity { get }
9+
10+
/// The associated user id (if any) for the error (defaults to `nil`)
11+
var userId: CustomStringConvertible? { get }
12+
13+
/// Any additional metadata (defaults to `[:]`)
14+
var metadata: [String: CustomDebugStringConvertible] { get }
15+
}
16+
17+
public extension ReportableError {
18+
var shouldReport: Bool { return true }
19+
var severity: Severity { return .error }
20+
var userId: CustomStringConvertible? { return nil }
21+
var metadata: [String: CustomDebugStringConvertible] { return [:] }
22+
}

Tests/BugsnagTests/BugsnagTests.swift

Lines changed: 3 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,108 +1,8 @@
1-
import Vapor
1+
import Bugsnag
22
import XCTest
3-
@testable import Bugsnag
4-
5-
extension Application {
6-
public static func test() throws -> Application {
7-
var services = Services()
8-
try services.register(BugsnagProvider(config: BugsnagConfig(
9-
apiKey: "e9792272fae71a3b869a1152008f7f0f",
10-
releaseStage: "development"
11-
)))
12-
13-
var middlewaresConfig = MiddlewareConfig()
14-
middlewaresConfig.use(BugsnagMiddleware.self)
15-
services.register(middlewaresConfig)
16-
17-
let sharedThreadPool = BlockingIOThreadPool(numberOfThreads: 2)
18-
sharedThreadPool.start()
19-
services.register(sharedThreadPool)
20-
21-
return try Application(config: Config(), environment: .testing, services: services)
22-
}
23-
}
24-
25-
private class TestErrorReporter: ErrorReporter {
26-
27-
var capturedReportParameters: (
28-
error: Error,
29-
severity: Severity,
30-
userId: CustomStringConvertible?,
31-
metadata: [String: CustomDebugStringConvertible],
32-
file: String,
33-
function: String,
34-
line: Int,
35-
column: Int,
36-
container: Container
37-
)?
38-
func report(
39-
_ error: Error,
40-
severity: Severity,
41-
userId: CustomStringConvertible?,
42-
metadata: [String: CustomDebugStringConvertible],
43-
file: String,
44-
function: String,
45-
line: Int,
46-
column: Int,
47-
on container: Container
48-
) -> Future<Void> {
49-
capturedReportParameters = (
50-
error,
51-
severity,
52-
userId,
53-
metadata,
54-
file,
55-
function,
56-
line,
57-
column,
58-
container
59-
)
60-
return container.future()
61-
}
62-
}
63-
64-
private class TestResponder: Responder {
65-
var mockErrorToThrow: Error?
66-
var mockErrorToReturnInFuture: Error?
67-
func respond(to req: Request) throws -> Future<Response> {
68-
if let error = mockErrorToThrow {
69-
throw error
70-
} else if let error = mockErrorToReturnInFuture {
71-
return req.future(error: error)
72-
} else {
73-
return req.future(Response(using: req))
74-
}
75-
}
76-
}
3+
import Vapor
774

785
final class BugsnagTests: XCTestCase {
79-
func testMiddleware() throws {
80-
let application = try Application.test()
81-
let request = Request(using: application)
82-
let errorReporter = TestErrorReporter()
83-
let middleware = BugsnagMiddleware(reporter: errorReporter)
84-
let responder = TestResponder()
85-
_ = try middleware.respond(to: request, chainingTo: responder).wait()
86-
87-
// expect no error reported when response is successful
88-
XCTAssertNil(errorReporter.capturedReportParameters)
89-
90-
responder.mockErrorToThrow = NotFound()
91-
92-
_ = try? middleware.respond(to: request, chainingTo: responder).wait()
93-
94-
// expect an error to be reported when responder throws
95-
XCTAssertNotNil(errorReporter.capturedReportParameters)
96-
97-
errorReporter.capturedReportParameters = nil
98-
responder.mockErrorToReturnInFuture = NotFound()
99-
100-
_ = try? middleware.respond(to: request, chainingTo: responder).wait()
101-
102-
// expect an error to be reported when responder returns an errored future
103-
XCTAssertNotNil(errorReporter.capturedReportParameters)
104-
}
105-
1066
func testSendReport() throws {
1077
var capturedSendReportParameters: (
1088
host: String,
@@ -116,7 +16,7 @@ final class BugsnagTests: XCTestCase {
11616
sendReport: { host, headers, data, container in
11717
capturedSendReportParameters = (host, headers, data, container)
11818
return container.future(Response(http: HTTPResponse(status: .ok), using: container))
119-
})
19+
})
12020
let application = try Application.test()
12121
let request = Request(using: application)
12222
request.breadcrumb(name: "a", type: .log)
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import Vapor
2+
import XCTest
3+
@testable import Bugsnag
4+
5+
extension Application {
6+
public static func test() throws -> Application {
7+
var services = Services()
8+
try services.register(BugsnagProvider(config: BugsnagConfig(
9+
apiKey: "e9792272fae71a3b869a1152008f7f0f",
10+
releaseStage: "development"
11+
)))
12+
13+
var middlewaresConfig = MiddlewareConfig()
14+
middlewaresConfig.use(BugsnagMiddleware.self)
15+
services.register(middlewaresConfig)
16+
17+
let sharedThreadPool = BlockingIOThreadPool(numberOfThreads: 2)
18+
sharedThreadPool.start()
19+
services.register(sharedThreadPool)
20+
21+
return try Application(config: Config(), environment: .testing, services: services)
22+
}
23+
}
24+
25+
final class TestErrorReporter: ErrorReporter {
26+
27+
var capturedReportParameters: (
28+
error: Error,
29+
severity: Severity,
30+
userId: CustomStringConvertible?,
31+
metadata: [String: CustomDebugStringConvertible],
32+
file: String,
33+
function: String,
34+
line: Int,
35+
column: Int,
36+
container: Container
37+
)?
38+
func report(
39+
_ error: Error,
40+
severity: Severity,
41+
userId: CustomStringConvertible?,
42+
metadata: [String: CustomDebugStringConvertible],
43+
file: String,
44+
function: String,
45+
line: Int,
46+
column: Int,
47+
on container: Container
48+
) -> Future<Void> {
49+
capturedReportParameters = (
50+
error,
51+
severity,
52+
userId,
53+
metadata,
54+
file,
55+
function,
56+
line,
57+
column,
58+
container
59+
)
60+
return container.future()
61+
}
62+
}
63+
64+
final class TestResponder: Responder {
65+
var mockErrorToThrow: Error?
66+
var mockErrorToReturnInFuture: Error?
67+
func respond(to req: Request) throws -> Future<Response> {
68+
if let error = mockErrorToThrow {
69+
throw error
70+
} else if let error = mockErrorToReturnInFuture {
71+
return req.future(error: error)
72+
} else {
73+
return req.future(Response(using: req))
74+
}
75+
}
76+
}
77+
78+
final class MiddlewareTests: XCTestCase {
79+
var application: Application!
80+
var request: Request!
81+
var middleware: BugsnagMiddleware!
82+
83+
let errorReporter = TestErrorReporter()
84+
let responder = TestResponder()
85+
86+
override func setUp() {
87+
application = try! Application.test()
88+
request = Request(using: application)
89+
middleware = BugsnagMiddleware(reporter: errorReporter)
90+
}
91+
92+
func testNoErrorReportedByDefault() throws {
93+
_ = try middleware.respond(to: request, chainingTo: responder).wait()
94+
95+
// expect no error reported when response is successful
96+
XCTAssertNil(errorReporter.capturedReportParameters)
97+
}
98+
99+
func testRespondErrorsAreCaptured() throws {
100+
responder.mockErrorToThrow = NotFound()
101+
102+
_ = try? middleware.respond(to: request, chainingTo: responder).wait()
103+
104+
// expect an error to be reported when responder throws
105+
XCTAssertNotNil(errorReporter.capturedReportParameters)
106+
}
107+
108+
func testErrorsInFutureAreCaptured() throws {
109+
errorReporter.capturedReportParameters = nil
110+
responder.mockErrorToReturnInFuture = NotFound()
111+
112+
_ = try? middleware.respond(to: request, chainingTo: responder).wait()
113+
114+
// expect an error to be reported when responder returns an errored future
115+
XCTAssertNotNil(errorReporter.capturedReportParameters)
116+
}
117+
118+
func testReportableErrorPropertiesAreRespected() throws {
119+
struct MyError: ReportableError {
120+
let severity = Severity.info
121+
let userId: CustomStringConvertible? = 123
122+
let metadata: [String: CustomDebugStringConvertible] = ["meta": "data"]
123+
}
124+
125+
let error = MyError()
126+
responder.mockErrorToThrow = error
127+
128+
_ = try? middleware.respond(to: request, chainingTo: responder).wait()
129+
130+
guard
131+
let params = errorReporter.capturedReportParameters
132+
else {
133+
XCTFail("No error was thrown")
134+
return
135+
}
136+
137+
XCTAssertNotNil(params.error as? MyError)
138+
XCTAssertEqual(params.metadata as? [String: String], ["meta": "data"])
139+
XCTAssertEqual(params.severity.value, "info")
140+
XCTAssertEqual(params.userId as? Int, 123)
141+
}
142+
143+
func testOptOutOfErrorReporting() throws {
144+
struct MyError: ReportableError {
145+
let shouldReport = false
146+
}
147+
148+
responder.mockErrorToThrow = MyError()
149+
150+
_ = try? middleware.respond(to: request, chainingTo: responder).wait()
151+
152+
XCTAssertNil(errorReporter.capturedReportParameters)
153+
}
154+
}

0 commit comments

Comments
 (0)