Skip to content

Instantly share code, notes, and snippets.

@glowcap
Last active February 24, 2025 13:04
Show Gist options
  • Save glowcap/a7f192ae044e0f2150e8bdd19dc20923 to your computer and use it in GitHub Desktop.
Save glowcap/a7f192ae044e0f2150e8bdd19dc20923 to your computer and use it in GitHub Desktop.
AIChat app analytic events setup
// Basing Modules/Logs, Analytics, Crashlytics/Adding Events (1/4)
/*
The idea here is to eliminate a majority of the duplication while keeping the same event output.
The final result being what is shown below
*/
// MARK: Analytic Events
enum Event: LoggableEvent, EventEnum {
case existingAuthStart
case existingAuthFail(error: Error)
case anonAuthStart
case anonAuthSuccess
case anonAuthFail(error: Error)
// note: I changed LoggableEvent.eventName -> LoggableEvent.name
var name: String {
// example output: AppView_ExistingAuth_Start
eventName(viewName: AppView.viewName())
}
var parameters: [String: Any]? {
switch self {
case .existingAuthFail(let error),
.anonAuthFail(let error):
error.eventParameters
default: nil
}
}
var type: LogType {
switch self {
case .existingAuthFail, .anonAuthFail:
.severe
default:
.analytic
}
}
}
// To get the view name, we'll create an extension of View
extension View {
// A static function to get the name of the view
nonisolated
static func viewName() -> String {
String(describing: Self.self)
}
}
// Next we'll want to split the enum name (anonAuthStart -> ["anon", "Auth", "Start"]
// this can be done with RegexBuilder and a String extension
import RexgexBuilder
extension String {
func splitByCapitals() -> [String] {
let regex = Regex {
NegativeLookahead { // Equivalent to (?<!^)
Anchor.startOfLine
}
Capture {
("A"..."Z")
}
}
// Replace matches with a space followed by the matched capital letter
let modifiedString = self.replacing(regex) { match in
// Insert a space before each match to separate by in the next step
" \(match.1)"
}
// Separate the modified string by spaces
return modifiedString.components(separatedBy: " ")
}
}
// Now that we have an array, we'll want to join it back with an underscore
// before the result ["anon", "Auth", "Start"] -> "anonAuth_Start"
// It's important to note here that the result could be one or more words,
// so we'll need to account for that. We can do this in an extension of Array.
extension Array where Element == String {
/// Joins elements with underscore before the last number of elements provided
/// - Parameter last: Number of elements after the underscore. Defaults to `1`
/// - Returns: Joined string with underscore before the last number of elements
/// - Note: An empty array returns an empty string. If last count is greater than or
/// equal to the count, returns all elements joined prefixed with an underscore.
func underscore(last: Int = 1) -> String {
/// return an empty string if array is empty
guard !isEmpty else { return "" }
/// return array joined if only one element or last value is greater than count
guard count != 1, count > last
else { return "_\(self.joined())" }
/// get index for underscore
let lastIndex = self.count - last
/// join prefix elements
let prefixStr = lastIndex - 1 == 0
? self[0]
: self[0...lastIndex - last].joined()
/// join suffix elements
let suffixStr = last > 1
? self[lastIndex...self.count - 1].joined()
: self[lastIndex]
return prefixStr + "_" + suffixStr
}
}
// underscoreLast(_:) gives us the flexibility needed to
// do "dataFetch_Success" or "dataFetch_RetrySuccess"
// The last step is to build out the EventEnum protocol; starting
// with the public function definition
protocol EventEnum {
func eventName(viewName: String, underscoreLast last: Int) -> String
}
// Next, we'll add an extension with the function implentation that
// joins the viewName with the eventName that keeps the length below
// 40 characters (FB Analytics requirement). Though we're still missing the
// `eventName(underscoreLast:)` and `trimmedEventName(viewName:, eventName:)`
// methods.
extension EventEnum {
func eventName(viewName: String, underscoreLast last: Int = 1) -> String {
let eventName = eventName(underscoreLast: last)
let trimmedEvent = trimmedEventName(viewName: viewName, eventName: eventName)
return trimmedEvent
}
}
// Finally, we will the missing private functions to the EventEnum extension.
extension EventEnum {
// This first one gets the enum case name, applies the split, capitalizes the first
// word of the enum, and joins it with an underscore in where we
// determined it to be.
/// Creates event name with capitalized values and an underscore between the event and result.
/// - Parameter last: number of words after the underscore. Defaults to `1`
/// - Returns: Returns self's name with each word capitalized and an underscore before the last count.
private func eventName(underscoreLast last: Int = 1) -> String {
let mirror = Mirror(reflecting: self)
/// This should always get the enum case name. Fallback will include associated value
let name = mirror.children.first?.label ?? String(describing: self)
/// capitalizing all, but only really needs to be applied to the first element
return name.splitByCapitals().map({ $0.capitalized }).underscore(last: last)
}
// This last one puts the name togther and checks the length. If the length is
// greater than 40, it trims the view name (ex: MyLongNameView_Fetch_Success -> MyLongNam_Fetch_Success).
// There are probably better ways to do this like using RegEx to drop vowels in the
// screen name, but I'll leave that up to you
/// Trims an event name to 40 characters by clipping characters from the view name
/// - Parameters:
/// - viewName: Name of the event owner view
/// - eventName: Name of the event
/// - Returns: Event name limited to 40 characters
private func trimmedEventName(viewName: String, eventName: String) -> String {
let maxLength = 40
let nameSuffix = "_" + eventName
var viewAndEventName = viewName + nameSuffix
if viewAndEventName.count > maxLength {
let charsToTrim = viewAndEventName.count - maxLength
let viewNameTrimmed = viewName.clipped(max: viewName.count - charsToTrim) // clipped(max:) is custom. See below
viewAndEventName = viewNameTrimmed + nameSuffix
}
return viewAndEventName
}
}
// Now every enum that conforms to EventEnum can leverage eventName(viewName:, underscoreLast:)
// The function `clipped(max:)` is a custom method from the course. It returns a prefix of a given string
@glowcap
Copy link
Author

glowcap commented Feb 24, 2025

Enhancement

Updated eventName(viewName:, underscoreLast last:) to trim to 40 characters by clipping the screen name

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment