How to wrap NewRelic SDK on iOS?

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).

Thought I’d provide a bit of an update as after giving this a bit of a thought I managed to come up with a solution. It’s not ideal but it’s the best I can manage with the SDK as it is currently designed.

Firstly I created a sub-class of NewRelic and stubbed out all of the SDK methods that we are using, e.g.:

class StubbedNewRelic: NewRelic {
  override class func recordError(_ error: Error) {}

I then used a compiler directive to create a conditional type alias:

    typealias EventRecorder = StubbedNewRelic
    typealias EventRecorder = NewRelic

We can now use EventRecorder throughout our code and it will be an alias to NewRelic in release builds and should work as if we were using it directly, however in our DEBUG builds it will use the stubbed version.

We were able to take this one step further and use a launch flag in DEBUG builds to conditionally call the super class from our sub-class, which allows to conditionally run NewRelic in DEBUG builds (though generally we don’t need to).

This feels about as good a solution as I can come up with. Its still not ideal, EventRecorder isn’t a real type and we cannot inject mocks or other test doubles in our tests (there were some places where it was useful for us to test that we were recording errors - I have had to remove these tests).

1 Like

@luke.redpath - Welcome to the community! This is an EPIC first topic, and I’m grateful that you have shared all that you learned with the entire community.