Tracer acts as an in-memory log which can run time-scoped traces to validate UX flows, analytic events, or any other kind of serial event bus.
For example, if you're expecting EventOne
, EventTwo
and EventThree
to be fired during a sign-up flow, Tracer can help your development or QA team validate those analytics don't break at some point.
Anything that can be represented as Equatable
can be logged and validated during a trace. Common examples of this would be strings, arrays or dictionaries (especially with conditional conformance in Swift 4.1) but you can also pass in your own custom structs or classes as long as they are Equatable
.
You can either run the traces manually, using the built-in UI that floats over top of your app, or have important traces run automatically during unit or UI tests.
See example gifs below or see the Examples folder for demonstrations of using it.
Or jump into the API section below if you're ready to implement.
Quickly install using CocoaPods:
pod 'Tracer/UI'
# or optionally don't include the UI component
pod 'Tracer/Core'
Or Carthage:
github "KeepSafe/Tracer-iOS"
After you tell the trace tool about your traces, it will list them all and enable you or your QA team to start one manually.
Here's a simple example of a trace where order matters:
And here's the same trace failing when an event is fired out of order (in this case, it fails all events before it that haven't already been matched since they would also be considered out-of-order).
You can expand or collapse the UI at any time, or even move it around the screen or drag the top of it to resize how much screen real estate it covers when expanded.
Logging uses the debugDescription
of each item to display it on screen, so be sure your more complex data structures implement this for how you'd like to show it.
Traces have multiple options like enforceOrder
, allowDuplicates
or assertOnFailure
that you can configure for specific scenarios. You can even perform programmatic setup steps (like resetting your app to a certain configuration) before each trace is started or list optional setup steps for someone to do so manually.
(passing/failing this trace was shown in the first example section above)
Order doesn't matter for this trace, so passing it would look like:
But, duplicates do matter so if we fired any during the trace it would fail:
Optionally, you can also add your own custom settings to the trace tool (or even show custom emojis for a logged item):
If you're using the UI frontend, you'll be interacting directly through TraceUI.swift. See the TraceUIExample.
let traceUI = TraceUI()
traceUI.show()
You can also toggle this to .hide()
via a debug setting if you'd like.
Note: If you're starting this tool in your
AppDelegate
, you should lazily load it after your app window loads to ensure the tool starts properly. The best thing to do is listen for.UIWindowDidBecomeKey
and only show it after that happens.
Load traces into the UI using:
traceUI.add(traces: [MyTraces.traceOne, MyTraces.traceTwo])
If you're logging an item specifically to be validated against for a trace, use:
traceUI.log(traceItem: Event.three.toTraceItem)
Here we're just creating an Event
enum with the events we're firing within the app and then using the .toTraceItem
property to transform it:
enum Event: String {
case one
case two
case three
var uxFlowHint: String {
switch self {
case .one: return "Press the 'Fire event 1' button"
case .two: return "Press the 'Fire event 2' button"
case .three: return "Press the 'Fire event 3' button"
}
}
var toTraceItem: TraceItem {
return TraceItem(type: "event", itemToMatch: AnyTraceEquatable(self), uxFlowHint: uxFlowHint)
}
}
Otherwise, you can also generically log (without it validating against an ongoing trace) by using:
traceUI.log(genericItem: AnyTraceEquatable("Moooooooooo"), emojiToPrepend: "🐄")
You can see all logging functions and their documentation in TraceUI.swift:
public func log(traceItem: TraceItem, verboseLog: AnyTraceEquatable? = nil, emojiToPrepend: String? = "⚡️") {}
public func logVerbose(traceItem: TraceItem, emojiToPrepend: String? = "⚡️") { }
public func log(genericItem: AnyTraceEquatable, properties: LoggedItemProperties? = nil, emojiToPrepend: String? = "⚡️") { }
If you'd rather not use the built-in UI frontend, you can set up your traces to run manually, such as during unit or UI tests. See the AnalyticsTraceExample.
Feel free to have a look through the unit tests for examples.
// This is an individual item to match and could be in multiple traces
let answerTraceItem = TraceItem(type: "The answer to the universe", itemToMatch: AnyTraceEquatable(42))
// This is a trace with an array of items it needs to match in order to pass
let trace = Trace(name: "Find the answer", itemsToMatch: [answerTraceItem])
// And this is the time-scoped tracer that handles logging and creating pass/fail results
let tracer = Tracer(trace: trace)
// Starting a trace returns a tuple with the current state and two signals to listen to
let (currentState, stateChangedSignal, itemLoggedSignal) = tracer.start()
print("\n\n---> TRACE STARTED: \(analyticsTrace.name)")
print("---> Current trace state: \(currentState)")
// Optionally, listen to changes in this trace (and you can remove the listener at any point)
itemLoggedListener = itemLoggedSignal.listen { traceItem in
print("---> Trace item logged: \(traceItem)")
}
stateChangedListener = stateChangedSignal.listen { traceState in
print("---> Trace state updated to: \(traceState.rawValue)")
print("---> Trace state description: \(traceState)")
}
Logging during a trace is simple:
tracer.log(item: answerTraceItem)
// After an item is logged, your trace will immediately either be passing or failing.
// Optionally, you can set `assertOnFailure` to `true` on your `Trace` instance to stop
// app execution as soon as a trace fails so you can debug.
Or you can even hook it into your Analytics
struct:
Analytics.log(event: .thirdViewSeen)
/// See example app for this setup
struct Analytics {
static func log(event: AnalyticsEvent) {
print("\n\nANALYTICS: \(event.rawValue) logged")
Tracers.analytics.activeTracer?.log(item: event.toTraceItem)
}
}
Stopping a trace returns a TraceReport
with raw or summary versions of the results
// FYI: signal listeners are automatically removed when stopped
let report = tracer.stop()
print(report.summary)
print(report.rawLog)
// Or manually parse the results yourself
let results = report.result
- Clone this repository and drag the
Tracer.xcodeproj
into the Project Navigator of your application's Xcode project.
- It should appear nested underneath your application's blue project icon. Whether it is above or below all the other Xcode groups does not matter.
- Select the
Tracer.xcodeproj
in the Project Navigator and verify the deployment target matches that of your application target. - Select your application project in the Project Navigator (blue project icon) to navigate to the target configuration window and select the application target under the
Targets
heading in the sidebar. - In the tab bar at the top of that window, open the
General
panel. - Click on the
+
button under theEmbedded Binaries
section. - Search for and select the top
Tracer.framework
.
And that's it!
The Tracer.framework
is automagically added as a target dependency, linked framework and embedded framework in a copy files build phase which is all you need to build on the simulator and a device.
Please use the Github issue tracker to let us know about any issues you may be experiencing.
Tracer for iOS is licensed under the Apache Software License, 2.0 ("Apache 2.0")
Tracer for iOS is brought to you by Rob Phillips and the rest of the Keepsafe team. We'd love to have you contribute or join us.