DEV Community

Bugfender
Bugfender

Posted on • Originally published at bugfender.com on

Using Alamofire and integrating it with Bugfender

Its ability to simplify a variety of tasks such as making HTTP requests, handling responses, and managing network activities, has made Alamofire one of the most popular and powerful networking libraries in Swift. Today we’ll be looking at how Alamofire can be integrated with Bugfender to cut through the complexities of URLSession to streamline networking operations in our apps.

As this is a comprehensive guide, we’ll be starting at the very beginning by going step-by-step through the process of installation and configuration of both Alamofire and Bugfender. If you’ve already done this you might want to jump straight to integration.

Alright then, let’s go.

Installing Alamofire

We’ll start with installing Alamofire by following these steps:

  • Go to File
  • Then Add Package Dependencies
  • Add the Alamofire URL to the search-field (https://github.com/Alamofire/Alamofire.git)

Now we can load Alamofire as a package, as shown below:

Alamofire installation

Once loaded, we just need to press Add Package when prompted, like this:

Alamofire installation

Alamofire should now appear as a package dependency on the left side navigation, as follows:

Alamofire installation

It’s as simple as that, Alamofire is now ready to be used.

Installing and configuring Bugfender

The installation process for Bugfender is essentially the same as Alamofire, however there are a small number of configuration steps we’ll need to complete so we can view all our analytics on the console.

Installing Bugfender

Like Alamofire, we’ll follow these steps to install Bugfender:

Bugfender installation

Once it loads (just like with Alamofire), we can add it as a package, as below:

Bugfender Installation

Now we’ll see Bugfender alongside Alamofire as a package dependency on the left side navigation, as shown here:

Alamofire & Bugfender

Configuring Bugfender

To configure Bugfender we’ll need an API Key, and to get this we need to register a Bugfender account, as well as an app, so we can start viewing our logs.

Account and app registration

First we need a Bugfender account, to create one simply open www.bugfender.com – the important links for us at this point are:

  • Get Started for FREE
  • DASHBOARD

If you don’t yet have an account, tap Get Started for FREE and go through the account creation process. If you already have an account we can head straight for the DASHBOARD.

Great, we should now see the empty Dashboard:

Proceed by clicking ADD NEW APP and following the simple process, like this:

And that’s it!

Next we’re presented with a screen for the installation of Bugfender but, since we already installed it with Swift package manager and will be going through the configuration steps next, we can simply click on the Bugfender icon in the top left to go back to our Dashboard:

We should now see the app we added, like below:

Finally, we just need to grab the API Key and we’ll find this by hovering over the information icon next to our app and copying it to our clipboard using the button, like this:

Great! We now have everything we need to configure Bugfender.

Client-side configuration

We’ll start the configuration process by creating an IssueTracker class to track all our logs and issues. For this we’ll need a new Swift file, which we’ve called IssueTracker.swift, but it can be named anything as this has no impact on anything else.

Initially our IssueTracker singleton will be configured to be called only when our app starts, as below:

import Foundation
import BugfenderSDK

class IssueTracker {
    static let instance = IssueTracker()

    init() { }

    func configure() {
        Bugfender.activateLogger("myAPIKey") //Your own key here
        Bugfender.enableCrashReporting()

                //Optional: If you want to log all NSLogs automatically
        //Bugfender.enableNSLogLogging()

        //OPtional: If you want to log all UIEvents automatically
                //Bugfender.enableUIEventLogging()
    }
}

Enter fullscreen mode Exit fullscreen mode

We’ve configured our example to log only crashes automatically, although we can opt-in to any of the other automatic logging options as needed.

Now our client is ready, we can configure it on launch by explicitly adding an init to our app, and configuring the client there, as follows:

import SwiftUI

@main
struct AlamofireAndBugfenderApp: App {
    init() {
        IssueTracker.instance.configure()
    }

    var body: some Scene {
        WindowGroup {
            ContentView()
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Awesome. Now we need to test if the connection was successful, which we’ll do by adding a temporary method to our IssueTracker, like this:

class IssueTracker {
    static let instance = IssueTracker()

    init() { }

    func configure() {
        Bugfender.activateLogger("myAPIKey") //Your own key here
        Bugfender.enableCrashReporting()
        Bugfender.enableNSLogLogging()
        Bugfender.enableUIEventLogging()
    }

    func testLog() {
        bfprint("Test error")
    }
}

Enter fullscreen mode Exit fullscreen mode

We’ll also add a button on our default View, like this:

struct ContentView: View {
    var body: some View {
        VStack {
            Button {
                IssueTracker.instance.testLog()
            } label: {
                Text("Test Log to Bugfender")
            }
        }
        .padding()
    }
}

Enter fullscreen mode Exit fullscreen mode

Now if we run the app and tap the button a few times, logs will be sent to our Bugfender project. It might take a few minutes but the message we just added should now show in our log, as below:

_ Note: If you have any installation issues, please refer to the documentation here._

Great work! We’ve now successfully installed and configured Alamofire and Bugfender, and we can start developing our APIClient and integrating both. Exciting!

Integrating logging with Bugfender and Alamofire

Now, we could manually integrate Bugfender with our Alamofire requests but there’s a more advanced way that will save us a lot of time and effort, that’s EventMonitor.

EventMonitor is a protocol provided by Alamofire that allows us to add customization to any event from Alamofire, as well as providing tools to monitor any requests and react to them.

First we need to update our IssueTracker so it can log messages with Bugfender, which we do as follows:

class IssueTracker {
    static let instance = IssueTracker()

    init() { }

    func configure() {
        Bugfender.activateLogger("myAPIKey")
        Bugfender.enableCrashReporting()
        Bugfender.enableNSLogLogging()
        Bugfender.enableUIEventLogging()
    }

    func log(string: String) {
        bfprint(string)
    }
}

Enter fullscreen mode Exit fullscreen mode

Next we’ll create a BugfenderEventMonitor.swift file, and add our EventMonitor to log events using our IssueTracker, like this:

import Alamofire
import BugfenderSDK

class BugfenderEventMonitor: EventMonitor {
    func requestDidFinish(_ request: Request) {
        let message = "Request: \(request)"
        IssueTracker.instance.log(string: message)
    }

    func request<Value>(_ request: DataRequest, didParseResponse response: DataResponse<Value, AFError>) {
        let message = "Response: \(response)"
        IssueTracker.instance.log(string: message)
    }
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve opted for just two methods to log some basic request info, but there are a range of options for event monitoring depending on requirements and a list of them can be found here.

Here’s what we added to create simple logs of all requests on our Bugfender console:

Of course, we still need to add the EventMonitor to our Alamofire session, which we’ll do next when we create our APIClient.

Our APIClient

Our demo project will be an API Client in Swift for the Rick and Morty API, which will have three main resources:

  • https://rickandmortyapi.com/api/character: From which we can get lists of characters, paged or search, for based on their ID
  • https://rickandmortyapi.com/api/location: As above but for locations
  • https://rickandmortyapi.com/api/episode: Again, as above but for episodes

Our client will allow us to generate a list of any of these three things, while automatically creating activity and error logs in our Bugfender console. Now let’s configure our APIClient.

Creating and configuring our APIClient

We’ll start by adding a RickAndMortyClient.swift file to our project – our initial configuration will be as shown below:

import Foundation
import BugfenderSDK
import Alamofire

class RickAndMortyClient {
    private let basePath: String
    private var session: Session

    init(with basePath: String = "<https://rickandmortyapi.com/api/>") {
        self.basePath = basePath
        session = Session(eventMonitors: [BugfenderEventMonitor()])
    }
}

Enter fullscreen mode Exit fullscreen mode

Here we’ve added a basepath for our requests, as well as the BugfenderEventMonitor configuration to our Alamofire Session. Now all requests will be logged to Bugfender, as we specified earlier. Now for our models.

Adding our models

Before we add methods that consume Rick and Morty’s REST API to our APIClient, we’ll need to add the models where the API data will be stored.

For this we’ll have a ‘Models’ folder, with four files:

  • RMCharacter: Character models
  • RMEpisode: Episode models
  • RMResponses: Response models
  • RMLocation: Location models

You’ll notice we’ve added ‘RM’ (for Rick and Morty) to all of them so that character, location, responses, and episode names are available to any of our types, and our related structs are all identified as well, as shown for each below:

RMCharacter:

import Foundation

struct RMCharacter: Identifiable, Codable {
    let id: Int
    let name: String
    let status: String
    let species: String
    let type: String
    let gender: String
    let origin: RMCharacterLocationDetails
    let location: RMCharacterLocationDetails
    let image: String
    let episode: [String]
    let url: String
    let created: String
}

struct RMCharacterLocationDetails: Codable {
    let name: String
    let url: String
}

Enter fullscreen mode Exit fullscreen mode

RMEpisode:

import Foundation

struct RMEpisode: Codable, Identifiable {
    let id: Int
    let name: String
    let airDate: String
    let episode: String
    let characters: [String]
    let url: String
    let created: String

    enum CodingKeys: String, CodingKey {
        case id, name, episode, characters, url, created
        case airDate = "air_date"
    }
}

Enter fullscreen mode Exit fullscreen mode

RMResponses:

import Foundation

struct RMResponseInfo: Codable {
    let count: Int
    let pages: Int
    let next: String?
    let previous: String?
}

struct RMLocationResponse: Codable {
    let results: [RMLocation]
    let info: RMResponseInfo
}

struct RMEpisodeResponse: Codable {
    let results: [RMEpisode]
    let info: RMResponseInfo
}

struct RMCharacterResponse: Codable {
    let results: [RMCharacter]
    let info: RMResponseInfo
}

Enter fullscreen mode Exit fullscreen mode

RMLocation:

import Foundation

struct RMLocation: Codable, Identifiable {
    let id: Int
    let name: String
    let type: String
    let dimension: String
    let residents: [String]
    let url: String
    let created: String
}

Enter fullscreen mode Exit fullscreen mode

Now we’re ready to add our methods.

Adding methods to fetch data

Now we have our Codable models, we can write the methods to fetch any data as required. We’re demonstrating with a simple version of the client and, as we won’t need any fancy filtering options, our methods will be quite straightforward.

We’ll query for all elements of a certain type on a given page – our initial APIClient with methods will be as follows:

import Foundation
import BugfenderSDK
import Alamofire

class RickAndMortyClient {
    private let basePath: String
    private var session: Session

    init(with basePath: String = "<https://rickandmortyapi.com/api/>") {
        self.basePath = basePath
        session = Session(eventMonitors: [BugfenderEventMonitor()])
    }

    func getCharacters(page p: Int = 0,
                       completion: @escaping (Result<[RMCharacter], Error>)->()) {

        get(urlString: basePath + "character/?page=\(p)",
            type: RMCharacterResponse.self) {
            result in
            switch result {
            case .success(let characterResponse):
                completion(.success(characterResponse.results))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

    func getEpisodes(page p: Int = 0,
                       completion: @escaping (Result<[RMEpisode], Error>)->()) {

        get(urlString: basePath + "episode/?page=\(p)",
            type: RMEpisodeResponse.self) {
            result in
            switch result {
            case .success(let episodeResponse):
                completion(.success(episodeResponse.results))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

    func getLocations(page p: Int = 0,
                       completion: @escaping (Result<[RMLocation], Error>)->()) {

        get(urlString: basePath + "location/?page=\(p)",
            type: RMLocationResponse.self) {
            result in
            switch result {
            case .success(let locationResponse):
                completion(.success(locationResponse.results))
            case .failure(let error):
                completion(.failure(error))
            }
        }
    }

    private func decode<T: Codable>(type: T.Type, from data: Data?) -> T? {
        guard let data = data else {
            return nil
        }

        do {
            return try JSONDecoder().decode(type, from: data)
        } catch let error {
            print(error)
            return nil
        }
    }

    private func get<T: Codable>(urlString: String, 
                                 type: T.Type,
                                 completion: @escaping (Result<T, Error>)-> ()) {
        guard let url = URL(string: urlString) else {
            return
        }

        session.request(url).response() { [weak self]
            dataResponse in
            switch dataResponse.result {
            case .success(let data):
                guard let decodedData = self?.decode(type: type, from: data) else {
                    return
                }
                completion(.success(decodedData))

            case .failure(let afError):
                completion(.failure(afError))
            }
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

Using generics, we have all the networking request logic in the get method, and all the json decoding login in the decode method. That said, if you look closely you’ll notice we aren’t yet handling any errors that might occur – we’ll look at that next.

Integrating our APIClient with Bugfender

We want to send all our errors to Bugfender so they appear on the console together with the logs for all requests that we configured previously.

To do this, we first need to go back to our IssueTracker and update it to log errors into Bugfender, like this:

class IssueTracker {
    static let instance = IssueTracker()

    init() { }

    func configure() {
        Bugfender.activateLogger("myAPIKey")
        Bugfender.enableCrashReporting()
        Bugfender.enableNSLogLogging()
        Bugfender.enableUIEventLogging()
    }

    func logError(error: Error) {
        Bugfender.error(error.localisedDescription)
    }

    func log(string: String) {
        bfprint(string)
    }
}

Enter fullscreen mode Exit fullscreen mode

With that done, we’ll create an Error enum that will be used for all our APIClient errors, as below:

enum RMError: Error {
    case invalidURL(url: String)
    case networkError(code: Int)
    case invalidData
    case decodingError(error: Error)
}

Enter fullscreen mode Exit fullscreen mode

Next we’ll update our get method to handle errors, as follows:

    private func get<T: Codable>(urlString: String, 
                                 type: T.Type,
                                 completion: @escaping (Result<T, Error>)-> ()) {
        guard let url = URL(string: urlString) else {
            IssueTracker.instance.logError(error: RMError.invalidURL(url: urlString))
            return
        }

        session.request(url).response() { [weak self]
            dataResponse in
            switch dataResponse.result {
            case .success(let data):
                guard let decodedData = self?.decode(type: type, from: data) else {
                    return
                }
                completion(.success(decodedData))

            case .failure(let afError):
                IssueTracker.instance.logError(error: RMError.networkError(code: afError.responseCode))
                completion(.failure(afError))
            }
        }
    }

Enter fullscreen mode Exit fullscreen mode

Finally we’ll update our decode method to also handle errors, like this:

    private func decode<T: Codable>(type: T.Type, from data: Data?) -> T? {
        guard let data = data else {
            IssueTracker.instance.logError(error: RMError.invalidData)
            return nil
        }

        do {
            return try JSONDecoder().decode(type, from: data)
        } catch let error {
            IssueTracker.instance.logError(error: RMError.decodingError(error: error))
            return nil
        }
    }

Enter fullscreen mode Exit fullscreen mode

That’s it. Now all the logs from our Alamofire APIClient, as well as all our errors, are being logged into Bugfender.

To sum up

In this article we’ve demonstrated how to successfully integrate Bugfender and Alamofire, including:

  1. Installing both packages – we used Swift package manager but you could use Cocoapods or just download the whole packages
  2. Creating a Bugfender project and configuring a Swift project to connect to it
  3. Adding an EventMonitor to take care of the vast majority of logs automatically
  4. Implementing specific error scenario logging to make sure all errors are captured

Hopefully you now feel confident to try this yourself.

Top comments (0)