Sentinel – A Library Loved by Developers and Testers Alike

Our open-source library packs a ton of useful features that make testing, debugging, and life in general much easier.

Have you ever been stuck in a loop where you have to rebuild your app constantly just so you can change a feature flag? What if you wanted to change your location or the base URL while using the app? You’re in for some more rebuilding. 

And what about untraceable UI bugs or elusive network issues in your app? Don’t count on “It works on my machine!” That ship has sailed a long time ago. 

Imagine a tool that would help you easily tackle the issues above and more. One that would provide you with network logs and useful information that allows you to quickly pinpoint what went wrong.

Meet Sentinel – an open-source library that will help you integrate these features with ease, allowing you and your QA colleague to find the root cause of the issues you’re experiencing.

Note: This article focuses on using Sentinel in iOS, but there is also an Android version available on GitHub.

Multiple QA debugging tools at your disposal

Let’s start with some basic info. As mentioned, Sentinel is an open-source library that we sponsor and maintain. I’ll reference a part of the readme file from the GitHub repository here:

“Sentinel is a simple library that gives developers the possibility to configure one entry point for every debug tool. The idea of Sentinel is to give the developers the ability to configure a screen with multiple debug tools, which are available via some event (e.g., shake, notification).”

It’s important to note here that the library states a single entry point for every debug tool. This indicates that it must never end up in a production environment.

Sentinel aims to give developers an easy way to integrate any number of tools that can help them adjust the application data without the need to rebuild it. That is precisely the reason why it shouldn’t be exposed in production environments.

Simple to implement, powerful when used 

There are a number of benefits to using the Sentinel library. First of all, it is so easy to implement and use that the learning curve is not even a curve – it’s practically just the X-axis.

Sentinel can change the data in your application without Xcode and without needing to rebuild it. It also provides device and application information and shows you how the app is performing in its performance tab.

Another major benefit is that Sentinel can contain any number of debug tools, which means that you can implement your own tools, tailored to your needs.

For example, you have a logging screen that opens by shaking the device, and you have a report-a-bug feature that is triggered by using a screenshot on debug builds. That’s already two different inputs for opening a debugging tool. What if you wanted to add a database export debugging feature? How would you do that without overwhelming the tester with a new button combination? Sentinel comes in very handy here.

Finally, mastering Sentinel has a learning curve that isn’t much steeper than the one for learning to implement it. Once you’ve mastered it, you can think of new ideas to help yourself and your testing team make your app more bulletproof and easy to test out.

A lifesaver for handling multiple Bluetooth connections and network requests

To use an example from my own experience, there’s an IoT project I work on where we use Sentinel to a great extent. The project involves a lot (and I mean a lot!) of Bluetooth communication and network requests. 

Utilizing Sentinel’s ability to log every Bluetooth command and every network request, we can actually see what we sent and if the request was successful or not. Besides helping us developers, this also saves our QA engineer’s time. Once we release a build, they check the feature we implemented by examining the logs to see if we’re sending the right commands. That way, the QA engineer can also indicate if there’s an issue, and whether it’s a firmware or an app issue.

Another example is getting a bug report with all the logs attached. Most of the time, we can pinpoint where the issue is happening just by using the elaborate logging integrated within the app.

Implementing Sentinel in 3 minutes or less

You can add Sentinel to your project via the Swift Package Manager (SPM) or CocoaPods. This example will demonstrate how to add it via SPM.

Open your project in Xcode → Files → Add package dependencies… → Search for Sentinel or use this link → Add your project → Add package.

This will prompt you to add Loggie and Collar to your project too. Both of these are optional, but if you want a simple logging tool, you can use Loggie, and if you want an analytics tool that gives you a visual representation of your events, you can add Collar. After picking the right tools for the job, this is how you set up Sentinel in your app:

	class AppDelegate: NSObject, UIApplicationDelegate {
    var shouldUseAnalytics = false
    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey : Any]? = nil
    ) -> Bool {
        setupSentinel()
        return true
    }
}

private extension AppDelegate {
    func setupSentinel() {
        let configuration = Sentinel.Configuration(
            trigger: Triggers.shake,
            tools: [
                UserDefaultsTool(),
                CustomLocationTool()
            ],
            preferences: [
                .init(
                    name: "Analytics",
                    setter: { [unowned self] in  shouldUseAnalytics = $0 },
                    getter: { [unowned self] in shouldUseAnalytics },
                    userDefaultsKey: nil
                )
            ]
        )
        Sentinel.shared.setup(with: configuration)
    }
}

With these 18 lines of code in the setupSentinel() function, we have successfully set up Sentinel, and the only thing left to do is to call it in the didFinishLaunchingWithOptions function.

We had to create the Configuration object, which is exposed by the library. The configuration takes in a trigger that will tell your application how you want to open Sentinel. Currently, there are three options – screenshot, shake, and notification.

The tools parameter takes in an array of Tool objects, which represent all the tools that are available once you launch Sentinel. The preferences array lists all the switch options for your feature flags or other boolean values you want the user to be able to change without needing to rebuild the application. These can also be saved in UserDefaults, hence the userDefaultsKey parameter. The last and most important thing you have to do is set up Sentinel with your configuration.

Using this code, we have set up Sentinel in two minutes and exposed two custom tools – one for changing your location and the other one to read all the UserDefault values your app is storing.

Go above and beyond with custom tools

As shown above, setting up Sentinel takes no more than a couple of minutes. You might expect there’s some sort of a catch with custom tools, but in this case – it’s not the case. Designing and customizing tools is just as simple.

How tools are designed

Debugging tools in Sentinel should be designed in a KISS manner. One debugging tool should only be used for one purpose. If we create a tool that inspects a database, then it shouldn’t be able to also inspect UserDefaults. 

Before you start creating your own custom tools, you should check out the ones provided by the Sentinel library:

UserDefaultsTool

Gives you an overview of the values stored in the UserDefaults.

TextEditingTool

Gives you an interface where you can write and store a new value to a predefined variable.

CustomLocationTool

Gives you the ability to change the current user location. Keep in mind that you have to restart the app to apply the location.

CustomInfoTool

Gives you an interface where you can specify some items with a name and a value.

Collar and Loggie tools

If you are using these, you can also use their respective tools.

How to create your own custom tool

Let’s say we love the UserDefaultsTool, which is provided by Sentinel, but it’s missing something we need. In our case, the ability to change the value. We can easily create a new custom tool and import it to our application. In this example, we’ll create a simple custom tool that can change a UserDefaults key for a system image and update the image afterward by pressing a button. To achieve this, we first need to create a Constants enum where we’ll save all the values we want to store in the UserDefaults.

	enum UserDefaultsConstants: String {
    case imageName
}

In this case, we only have one value, but your app might require more. It’s always a good idea to extract constants like these into enums so you can namespace them easily. You potentially might want to add a UserDefaultsResetTool. That way, you can easily conform to CaseIterable and reset every single UserDefault value you added.

Let’s get back to our example.

Since I created this example project with Xcode 15, I got a SwiftUI view, and we’ll have to update it a bit.

	import SwiftUI

struct ContentView: View {

    @AppStorage(UserDefaultsConstants.imageName.rawValue) var imageSystemName: String = "globe"

    var body: some View {
        VStack {
            Image(systemName: imageSystemName.lowercased())
                .imageScale(.large)
                .foregroundStyle(.tint)
            Text("Hello, world!")
        }
        .padding()
    }
}

In this case, we added an AppStorage variable which will fetch the value from the UserDefaults, and update the view if it gets changed.

	import Sentinel
import UIKit

final class SystemImageNameChangerTool: Tool {

    let name: String
    private let userDefaults: UserDefaults
    private var systemImageName: String = "globe"

    init(name: String = "System Image Name Changer", userDefaults: UserDefaults = .standard) {
        self.name = name
        self.userDefaults = userDefaults
    }

    func presentPreview(from viewController: UIViewController) {
        TextEditingTool(
            name: "System Image Name",
            setter: { [unowned self] in systemImageName = $0 },
            getter: { [unowned self] in systemImageName },
            userDefaults: .standard,
            userDefaultsKey: UserDefaultsConstants.imageName.rawValue
        )
        .presentPreview(from: viewController)
    }
}

It allows you to inject your UserDefaults, in which you’ll want to store the image name. In this simple example, I used TextEditingTool, which is provided by Sentinel, but if you need more complexity in your own custom tools, feel free to create your own solutions.

When creating your own custom Tool to integrate into the Sentinel configuration, you’ll need to conform to the Tool protocol. The protocol states that your custom tool has to implement a name, which is displayed in the list of your tools, and that it has to have a function presentPreview to handle the navigation. It’s as simple as that. Last but not least, we have to add our tool to the Sentinel configuration. So let’s update our setupSentinel function:

	    func setupSentinel() {
        let configuration = Sentinel.Configuration(
            trigger: Triggers.shake,
            tools: [
                UserDefaultsTool(),
                CustomLocationTool(),
                SystemImageNameChangerTool(userDefaults: .standard)
            ],
            preferences: [
                .init(
                    name: "Analytics",
                    setter: { [unowned self] in  shouldUseAnalytics = $0 },
                    getter: { [unowned self] in shouldUseAnalytics },
                    userDefaultsKey: nil
                )
            ]
        )
        Sentinel.shared.setup(with: configuration)
    }

If we run the application now, we can shake the device and see our tool in action.

Why is Sentinel an efficient testing and debugging tool?

Besides being useful for developers, Sentinel is also an efficient debugging tool. It allows both developers and testers to view some information not so easily accessible otherwise. If you’re a bit more adventurous, you can even add a functionality that updates the information from the tool – only your creativity is the limit. You can create mocking tools to help you easily recreate a situation without having to rebuild the app or even use Xcode and its breakpoints.

All of this can significantly improve collaboration and efficiency on a project, not to mention make everyone’s lives easier.

Your tester colleagues will highly appreciate every tool you create. They can even propose new ones since they probably have their own pain points they would love to see resolved. 

As I’ve been preaching, it helps out everyone included in the testing process in various scenarios, and the QA engineer will always be able to report bugs with a better understanding of why something happened. That way, you’ll also be able to fix those bugs with ease.

Always keep your guard up

From its simple implementation to many possibilities offered through custom tool integration, Sentinel is a versatile open-source library that can make both developers’ and testers’ lives easier. 

If you liked the pack of features presented in this article, be sure to head over to our GitHub page and get that guard to your own front door.