I’m looking for some advice on how we can wrap our NewRelic SDK calls behind an abstraction to avoid littering our app’s code base with calls to the NewRelic SDK. There’s a number of good reasons to do this:
- Avoids coupling your code to a specific provider
- Makes it easy to mock in tests (especially if you want to test that you are recording errors)
- Makes it easy to stub out in certain build or runtime configurations
At first glance this seemed simple. We added a struct EventRecorder
type which we can pass around as a dependency for code that needs it. This isn’t the full interface, but just to give you an idea:
struct EventRecorder {
var recordError: (Error) -> Void
}
We can now easily create different live and mock implementations. Our live implementation just calls the NewRelic SDK:
extension EventRecorder {
static let live = EventRecorder(
recordError: { NewRelic.recordError($0) }
)
}
Unfortunately this approach has a major drawback - all of our recorded errors are now showing up as a single stack trace location - the recordError
closure that calls the NewRelic SDK. So my first question is - is there any way to use the above design but change the way the stack trace gets reported, or configure NewRelic’s reporting to ignore this location and report the next location in the stack? I can’t seem to find any way of doing this.
Failing that, the alternative approach would be to introduce a protocol that mirrors the NewRelic API (or at least, the functions we are using), conform the NewRelic SDK to that protocol and then create mock implementations that conform to that protocol as needed.
Unfortunately the design of the NewRelic SDK as a bunch of static functions would appear to make this impossible. I can create a protocol with static functions and conform NewRelic
to it, e.g.:
protocol EventRecorder {
static func recordError(_ error: Error)
}
extension NewRelic: EventRecorder {}
But this isn’t useful because there’s no way to pass this around as a dependency due to it being an entirely static implementation. I can’t pass around NewRelic.self
because the meta type does not conform to the protocol.
I also cannot create a protocol with non-static functions as there’s no way to make the NewRelic
metatype conform to that protocol.
So I appear to be stuck - I cannot abstract the SDK away behind a protocol and I cannot wrap calls to the SDK because it messes up stack trace reporting.
I find it incredibly frustrating that the SDK API has been designed in this way. If the SDK was implemented as instance methods and a shared singleton, e.g. NewRelic.sharedInstance.recordError
then I would be able to conform NewRelic
to a protocol with non-static functions and return the NewRelic.sharedInstance
as the conforming type.
Similarly if there was a way of forwarding the actual call-site along in the wrapped abstraction I wouldn’t even need a protocol. If the SDK was open-source I would have been able to potentially dig into this and possibly even open a pull request but for some reason you’ve chosen to make this library closed source which is very developer unfriendly.
Any advice would be appreciated because at the moment its looking like we will need to revert to littering direct calls to the NewRelic SDK throughout our code. Please also consider this a request to redesign your API to something more friendly - you could re-implement your entire SDK as instance methods on a shared instance whilst remaining backwards compatible by deprecating the static functions (and have them delegate to the shared instance in the meantime).