Skip to content

Commit

Permalink
Merge pull request #13 from orchidfire/main
Browse files Browse the repository at this point in the history
Improve comments with cursor.so
  • Loading branch information
eonist authored Sep 20, 2023
2 parents 9748946 + 9041a5d commit 30cb162
Show file tree
Hide file tree
Showing 17 changed files with 535 additions and 206 deletions.
57 changes: 38 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,30 +7,35 @@

# 🔬 Telemetry

> Telemetry is an open SDK for google analytics. Because closed source sdks are terrible for security
> Telemetry is an open-source SDK for Google Analytics. We believe in transparency and security, hence the open-source approach.
### Reasoning
- 🤖 GA is a great way to gather real engagement data and measure traction. In order to improve it.
- 🐛 GA is great for error and crash reporting. Get ahead of UX issues by getting notified if there are bugs
- 🌍 GA is great for measuring which markets that are using your app.

## Installation
You can install Telemetry via Swift Package Manager (SPM) using the following URL: `https://github.com/sentryco/Telemetry`.

### Why Use Telemetry?
- 🤖 Google Analytics (GA) provides valuable insights into user engagement and traction. Telemetry helps you leverage these insights to improve your application.
- 🐛 GA is an excellent tool for error and crash reporting. Stay ahead of UX issues by receiving notifications about bugs.
- 🌍 GA helps you understand the geographical distribution of your app's users.

> **Warning**
> Currenly using GA3 API which is sunset in July. This lib will migrate to GA4 before that
> We currently use the GA3 API, which will be deprecated in July. We plan to migrate to GA4 before then.
### Events
With the Telemetry's event() method, you can monitor any event you want.
### Event Tracking
With Telemetry's `event()` method, you can monitor any event of interest.
```swift
Telemetry.event("Authorize", action: "Access granted")
```

### Screenviews
You should frequently keep track of the "screens" the user navigates to. Your ViewController's viewDidAppear is a logical place to do that. The Telemetry's screenView() method can be used; it performs the same function as event().
It's important to track the "screens" that users navigate to. A logical place to do this is in your ViewController's `viewDidAppear` method. Use Telemetry's `screenView()` method for this purpose.
```swift
Telemetry.screenView("Cheers")
```

### Sessions
By calling session(start: true) when the user opens the application and session(start: false) when they close it, you can keep track of each user's individual sessions. You can do this in your UIApplicationDelegate application by following the example given here:.
By calling `session(start: true)` when the application opens and `session(start: false)` when it closes, you can track individual user sessions. Here's an example of how to do this in your `UIApplicationDelegate` application:


```swift
Telemetry.trackerID = "UA-XXXXX-XX")
Expand All @@ -39,30 +44,44 @@ Telemetry.session(start: false) // applicationDidEnterBackground
```

### Exception
Use exception to report warnings and errors
Use the `exception` method to report warnings and errors.
```swift
Telemetry.exception("Error - database not available", isFatal: false)
```

### Timing
This example tracks the time to fetch user data from a database. The 'category' parameter denotes the operation type ("Database"). The 'variable' parameter specifies the operation ("Fetch"). The 'time' parameter records elapsed time in milliseconds. The optional 'label' parameter can provide additional details.

```swift
// Add example for this later
// Start the timer
let startTime = Date()

// Perform some operation
// ...

// Calculate the elapsed time
let elapsedTime = Date().timeIntervalSince(startTime)

// Log the timing event
Telemetry.timing(category: "Database", variable: "Fetch", time: elapsedTime, label: "User data fetch")
```

### Gotchas:
- Telemetry will automatically request that Google Analytics anonymize user IPs in order to comply with GDPR.
The token can be obtained from the admin page of the tracked Google Analytics entity.
- Firebase crashlytics is the way to go now days. Its also free to use etc. But can be over the top complex. You have to use their SDK etc. Sometimes simple is better etc.
- When setting up google analytics account. Make sure to use legacy `Universal Analytics property` and not GA4. This legacy option is under advance menu when you setup the account
- Why are closed source sdks bad? From apples app-review guidelines: `Ensure that all software frameworks and dependencies also adhere to the App Store Review Guidelines`
- Telemetry automatically requests Google Analytics to anonymize user IPs to comply with GDPR.
- Obtain the token from the admin page of the tracked Google Analytics entity.
- While Firebase Crashlytics is a popular choice, it can be complex due to the need to use their SDK. Sometimes, simplicity is key.
- When setting up your Google Analytics account, ensure to use the legacy `Universal Analytics property` and not GA4. This legacy option is under the advanced menu during account setup.
- Why are closed-source SDKs a concern? According to Apple's app-review guidelines: `Ensure that all software frameworks and dependencies also adhere to the App Store Review Guidelines`.


### Resources:
- Anonymous GA: https://stackoverflow.com/questions/50392242/how-anonymize-google-analytics-for-ios-for-gdpr-rgpd-purpose
- Guide on fingerprinting in iOS: https://nshipster.com/device-identifiers/
- Guide on identifiers: https://gist.github.com/hujunfeng/6265995
- Nice tracker project: https://github.com/kafejo/Tracker-Aggregator
- Another nice tracker project: https://github.com/devxoul/Umbrella
- Noteworthy tracker project: https://github.com/kafejo/Tracker-Aggregator
- Another noteworthy tracker project: https://github.com/devxoul/Umbrella
- Using Google Analytics for Tracking SaaS: https://reflectivedata.com/using-google-analytics-for-tracking-saas/


### Todo:
- Add documentation to this readme on how to setup Google analytics for your google account etc 🚧
63 changes: 47 additions & 16 deletions Sources/Telemetry/Telemetry+Action.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,24 @@ import Foundation
extension Telemetry {
/**
* Action call that can take type
* - Remark: The class support tracking of sessions, screen/page views, events and timings with optional custom dimension parameters.
* - Remark: For a full list of all the supported parameters please refer to the [Google Analytics parameter reference](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters)
* - Fixme: ⚠️️ Add example
* - Remark: This class supports tracking of sessions, screen/page views, events, and timings with optional custom dimension parameters.
* - Remark: For a comprehensive list of all the supported parameters, please refer to the [Google Analytics parameter reference](https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters)
* - Fixme: ⚠️️ Add an example here
* - Parameters:
* - action: - Fixme: ⚠️️ Add doc
* - complete: Only use complete for the GA type
* - action: The action to be performed - Fixme: ⚠️️ Add more documentation here
* - complete: Use complete only for the GA type
*/
public static func action(_ action: ActionKind, complete: Complete? = nil) {
// Check if the telemetry type is Google Analytics
if case TMType.ga = tmType {
// Send the action to Google Analytics
send(type: action.key, parameters: action.output, complete: complete)
} else if case TMType.agg(let agg) = tmType {
do { try agg.append(action: action) }
// If the telemetry type is not Google Analytics, append the action to the aggregator
do {
try agg.append(action: action)
}
// Catch and print any errors that occur when appending the action
catch { Swift.print("Error: \(error.localizedDescription)") }
}
}
Expand All @@ -23,32 +29,49 @@ extension Telemetry {
* Helper
*/
extension Telemetry {
/**
* - Remark: If you get: swift a server with the specified hostname could not be found. Make sure to enable outgoing network in sandbox: https://stackoverflow.com/a/57292829/5389500
/**
* - Remark: If you encounter: swift a server with the specified hostname could not be found. Ensure to enable outgoing network in sandbox: https://stackoverflow.com/a/57292829/5389500
* - Parameters:
* - parameters: custom params for the type
* - type: timing, exception, pageview, session, exception etc
* - complete: Call back with success or not. Useful for Unit-testing when dealing with async code etc
* - parameters: Custom parameters for the type
* - type: The type of telemetry data (timing, exception, pageview, session, exception etc)
* - complete: Callback with success or failure. Useful for Unit-testing when dealing with asynchronous code etc
*/
internal static func send(type: String?, parameters: [String: String], complete: Complete? = nil) {
var queryArgs = Self.queryArgs // Meta data
// Initialize query arguments with metadata
var queryArgs = Self.queryArgs

// If type is not nil and not empty, add it to the query arguments
if let type: String = type, !type.isEmpty {
queryArgs.updateValue(type, forKey: "t")
}

// If custom dimensions are not nil, merge them into the query arguments
if let customDim: [String: String] = self.customDimArgs {
queryArgs.merge(customDim) { _, new in new }
}

// Update the "aip" key in the query arguments based on the anonymizeIP flag
queryArgs["aip"] = anonymizeIP ? "1" : nil

// Combine the query arguments with the parameters
let arguments: [String: String] = queryArgs.combinedWith(parameters)

// Generate a URL with the arguments, return if URL generation fails
guard let url: URL = Self.getURL(with: arguments) else { return }

// Create a data task with the URL
let task = session.dataTask(with: url) { _, _, error in
// If there is an error, print it and call the completion handler with false
if let errorResponse = error?.localizedDescription {
// - Fixme: ⚠️️ shorten error respons to mac 20-30 chars?
Swift.print("⚠️️ Failed to deliver GA Request. ", errorResponse)
complete?(false)
}

// If there is no error, call the completion handler with true
complete?(true)
}

// Start the data task
task.resume()
}
}
Expand All @@ -58,6 +81,7 @@ extension Telemetry {
extension Telemetry {
/**
* URL query (Meta data)
* - Remark: This dictionary contains the metadata for the URL query
*/
fileprivate static var queryArgs: [String: String] {
[
Expand All @@ -74,22 +98,29 @@ extension Telemetry {
}
/**
* URL
* - Parameter parameters: parameters to turn into a url request
* - Parameter parameters: Parameters to convert into a URL request
* - Remark: This function generates a URL from the given parameters
*/
fileprivate static func getURL(with parameters: [String: String]) -> URL? {
// Define the character set for URL path
let characterSet = CharacterSet.urlPathAllowed
// Join the parameters into a string, encoding the values for URL
let joined: String = parameters.reduce("collect?") { path, query in
// Encode the value of each parameter
let value = query.value.addingPercentEncoding(withAllowedCharacters: characterSet)
// Return the path with the key-value pair appended
return .init(format: "%@%@=%@&", path, query.key, value ?? "")
}
// Trim the trailing &
// Trim the trailing '&' from the joined string
let path: String = .init(joined[..<joined.index(before: joined.endIndex)])
// Make sure we generated a valid URL
// Check if the base URL is valid
guard let baseURL: URL = baseURL else { Swift.print("baseURL error"); return nil }
// Generate the final URL relative to the base URL
guard let url: URL = .init(string: path, relativeTo: baseURL) else {
Swift.print("Failed to generate a valid GA url for path ", path, " relative to ", baseURL.absoluteString)
return nil
}
// Return the final URL
return url
}
}
43 changes: 27 additions & 16 deletions Sources/Telemetry/Telemetry+Const.swift
Original file line number Diff line number Diff line change
@@ -1,47 +1,58 @@
import Foundation

/**
* const
* Constants for Telemetry
*/
extension Telemetry {
// Typealias for completion handler
public typealias Complete = (_ success: Bool) -> Void

// Base URL for Google Analytics
internal static let baseURL: URL? = .init(string: "https://www.google-analytics.com/")
}

/**
* variables
* Variables for Telemetry
*/
extension Telemetry {
/**
* Users IP should be anonymized
* - Description: In order to be GDPR compliant, Telemetry will ask Google Analytics to anonymize users IP's by default. If you wish to opt-out of this you will neeed to set anonymizeIP to false.
* Flag to anonymize user's IP
* - Description: To ensure GDPR compliance, Telemetry requests Google Analytics to anonymize user IPs by default. Set this to false to opt-out.
*/
public static var anonymizeIP = true

/**
* Google Analytics Identifier (Tracker ID)
* - Remark: The token can be obtained from the admin page of the tracked Google Analytics entity.
* - Remark: A valid Google Analytics tracker ID of form UA-XXXXX-XX must be set before reporting any events.
* - Remark: This token can be obtained from the Google Analytics entity's admin page.
* - Remark: A valid Google Analytics tracker ID (format: UA-XXXXX-XX) must be set before reporting any events.
*/
public static var trackerId: String = "UA-XXXXX-XX"

/**
* Custom dimension arguments
* - Description: Dictionary of custom key value pairs to add to every query.
* - Remark: Use it for custom dimensions (cd1, cd2...).
* - Note: More information on Custom Dimensions https://support.google.com/analytics/answer/2709828?hl=en
* - Description: A dictionary of custom key-value pairs to be added to every query.
* - Remark: Useful for custom dimensions (cd1, cd2...).
* - Note: For more information on Custom Dimensions, visit https://support.google.com/analytics/answer/2709828?hl=en
*/
public static var customDimArgs: [String: String]?

/**
* Options are: .vendor, .userdef, .keychain
* - Remark: Type of persistence
* - Fixme: ⚠️️ Rename to currentIdentifierType? or curIdType?
* Identifier type
* - Remark: Defines the type of persistence. Options are: .vendor, .userdef, .keychain
* - Fixme: Consider renaming to currentIdentifierType or curIdType
*/
public static var idType: IDType = .userdefault

/**
* Network, rename to urlSession
* Network session
* - Remark: Consider renaming to urlSession
*/
public static let session = URLSession.shared

/**
* Telemetry type
* - Description: A way to switch from ga-endpoint to aggregator-endpoint
* - Fixme: ⚠️️ Rename to endPointType? EPType ? Maybe
* - Description: Allows switching between ga-endpoint and aggregator-endpoint
* - Fixme: Consider renaming to endPointType or EPType
*/
public static var tmType: TMType = .ga // .agg()
}
}
13 changes: 8 additions & 5 deletions Sources/Telemetry/Telemetry.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import Foundation

/**
* With the help of Telemetry, Google Analytics is able to track events and screen views. The Google Analytics Measurement protocol, which is, is used in the class. (https://developers.google.com/analytics/devguides/collection/protocol/v1/reference).
* - Remark: Since Google has officially discontinued the ability to track mobile analytics through Google Analytics, new apps are urged to use Firebase.
* - Remark: This library transforms screen views into pageviews, and you must configure new tracking properties as websites in the Google Analytics admin console.
* - Remark: The app bundle identifier, which can be set to any custom value for privacy purposes, will be used as a dummy hostname for tracking pageviews and screen views.
* The Telemetry class facilitates event and screen view tracking using the Google Analytics Measurement protocol.
* More details can be found here: https://developers.google.com/analytics/devguides/collection/protocol/v1/reference
*
* - Note: Google has officially discontinued mobile analytics tracking through Google Analytics. It is recommended for new apps to use Firebase instead.
* - Note: This library converts screen views into pageviews. Therefore, new tracking properties must be configured as websites in the Google Analytics admin console.
* - Note: The app bundle identifier, which can be customized for privacy reasons, is used as a dummy hostname for tracking pageviews and screen views.
*/
public class Telemetry {}
public class Telemetry {}
6 changes: 3 additions & 3 deletions Sources/Telemetry/ext/Dict+Ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import Foundation

extension Dictionary {
/**
* - Fixme: ⚠️️ Add doc
* - Fixme: ⚠️️ We could also use reduce like shown here: https://stackoverflow.com/a/75548503/5389500
* - Fixme: ⚠️️ We can even add it to a + operator like: `func + <Self>(a: Self, b: Self) -> Self {...}`
* - Parameter other: - Fixme: ⚠️️ Add doc
* - Returns: - Fixme: ⚠️️ Add doc
* Combines the current dictionary with another dictionary.
* - Parameter other: The dictionary to be combined with.
* - Returns: A new dictionary that contains the combined key-value pairs.
*/
func combinedWith(_ other: [Key: Value]) -> [Key: Value] {
var dict = self
Expand Down
Loading

0 comments on commit 30cb162

Please sign in to comment.