Skip to content

Commit 44c9e25

Browse files
committed
Update Preset Articles to put more emphasis on convenience methods
1 parent c4f5846 commit 44c9e25

File tree

2 files changed

+112
-117
lines changed

2 files changed

+112
-117
lines changed

articles/preset-errors.md

Lines changed: 58 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -18,36 +18,17 @@ A smooth experience without bugs & issues is one of the key factors contributing
1818
Error Handling is any logic that detects that something unexpected happened and reacts to that information in some useful way. It's commonly interpreted as 'showing an error message to a user', but that's just the most basic form of Error Handling. Other common things you can do to improve your app are Empty States (Why is this empty?), Call-to-Actions (What can I do?), and Auto-Repair (resiliency against common unexpected user input). But always make sure to give some form of feedback instead of failing silently, which makes your app feel broken.
1919
{% endnoteinfo %}
2020

21-
## Sending the Signal
21+
## Using the TelemetryDeck Swift SDK
2222

23-
To report unexpected events to TelemetryDeck, send the event name `TelemetryDeck.Error.occurred` with the parameter `TelemetryDeck.Error.id` set to something that can be used to group errors of the same kind. For example, using the Swift SDK you could simply pass `error.localizedDescription` whenever an exception is thrown that you don't expect to happen:
24-
25-
```swift
26-
do {
27-
let object = try JSONDecoder().decode(Object.self, from: data)
28-
} catch {
29-
// your error handling code
30-
31-
TelemetryDeck.signal(
32-
"TelemetryDeck.Error.occurred",
33-
parameters: ["TelemetryDeck.Error.id": error.localizedDescription]
34-
)
35-
}
36-
```
37-
38-
The Swift SDK ships with a convenience method for so you don't have to remember the event & parameter keys:
23+
If you're using the TelemetryDeck Swift SDK, tracking errors is simple. Use the convenience method we've prepared:
3924

4025
```swift
4126
TelemetryDeck.errorOccurred(id: error.localizedDescription)
4227
```
4328

44-
The `errorOccurred` function also accepts the same arguments as the `signal` function (namely `parameters`, `floatValue`, `customUserID`) in case you want to provide additional context info.
29+
### Better Error Identification
4530

46-
## Separation of ID & Message
47-
48-
While the above code is a good starting point, the localized nature of the `localizedDescription` message attached to all thrown exceptions in Swift isn't optimal. The same issue will be reported with different messages simply because the text will differ based on the users language settings. And you might have even created your own error types that provide dynamic content such as the file path in the error message, which makes things even worse. To see which errors affect most users, it's best to give the same kind of error the same ID.
49-
50-
So, whenever possible, it's recommended that you pass a made-up value to the `TelemetryDeck.Error.id` parameter that rather represents the context of the error. The full message can be provided with the optional parameter `TelemetryDeck.Error.message`. The Swift SDK provides several convenient ways to do this:
31+
While `error.localizedDescription` works, it's not optimal because the same error will be reported differently based on user language settings. For better error grouping, provide a consistent ID:
5132

5233
```swift
5334
do {
@@ -63,100 +44,88 @@ do {
6344
id: "ImportObject.jsonDecode",
6445
message: error.localizedDescription
6546
)
66-
67-
// Option 3: Using the full signal syntax
68-
TelemetryDeck.signal(
69-
"TelemetryDeck.Error.occurred",
70-
parameters: [
71-
"TelemetryDeck.Error.id": "ImportObject.jsonDecode",
72-
"TelemetryDeck.Error.message": error.localizedDescription
73-
]
74-
)
7547
}
7648
```
7749

78-
For your own `Error` types, you could introduce an `IdentifiableError` protocol and conform to that to make this process easier (the Swift SDK has this protocol built-in):
50+
### Custom Error Types
7951

80-
```swift
81-
protocol IdentifiableError: Error {
82-
var id: String { get }
83-
}
52+
For your own error types, conform to the `IdentifiableError` protocol (built into the Swift SDK):
8453

54+
```swift
8555
enum MyError: String, IdentifiableError {
8656
case fileMissing
8757
case invalidFormat
8858

8959
var id: String { self.rawValue }
9060
}
61+
62+
// Then report directly:
63+
TelemetryDeck.errorOccurred(identifiableError: myError)
9164
```
9265

93-
Now you can pass custom errors directly to the SDK:
66+
### Error Categories
67+
68+
The Swift SDK provides three built-in error categories for better organization:
9469

9570
```swift
96-
do {
97-
let object = try JSONDecoder().decode(Object.self, from: data)
98-
} catch {
99-
// For custom errors that conform to IdentifiableError
100-
if let myError = error as? MyError {
101-
TelemetryDeck.errorOccurred(identifiableError: myError)
102-
} else {
103-
// For system errors or other errors, use with(id:)
104-
TelemetryDeck.errorOccurred(
105-
identifiableError: error.with(id: "ImportObject.jsonDecode")
106-
)
107-
}
108-
}
71+
// Thrown exceptions (parsing errors, I/O errors, permission errors)
72+
TelemetryDeck.errorOccurred(
73+
id: "FileNotFound",
74+
category: .thrownException,
75+
message: error.localizedDescription
76+
)
77+
78+
// User input errors (invalid format, invalid range)
79+
TelemetryDeck.errorOccurred(
80+
id: "ProjectForm.hourlyRateConversionFailed",
81+
category: .userInput,
82+
message: "Text '\(self.textFieldInput)' could not be converted to type 'Int'."
83+
)
84+
85+
// App state errors (inconsistent navigation, invalid combinations)
86+
TelemetryDeck.errorOccurred(
87+
id: "NavigationState.invalidTransition",
88+
category: .appState,
89+
message: "Cannot navigate from login to dashboard without authentication"
90+
)
10991
```
11092

111-
Note that `error.localizedDescription` will be sent as the `message` by default, but you can override it.
93+
{% noteinfo "Default Category" %}
94+
When using `TelemetryDeck.errorOccurred(identifiableError:)`, the category defaults to `.thrownException`, but you can override it if needed.
95+
{% endnoteinfo %}
11296

113-
## Built-In Error Categories
97+
The `errorOccurred` function accepts the same optional arguments as the `signal` function (namely `parameters`, `floatValue`, `customUserID`) in case you want to provide additional context info.
11498

115-
Reporting exceptions that you didn't expect to happen isn't enough to cover all "unexpected behaviors" that you will encounter in your app. We found that unexpected behavior generally falls into one of the following 3 categories:
99+
## Manual Signal Construction for Other Platforms
116100

117-
1. Unexpected **Thrown Exceptions** (e.g. parsing errors, I/O errors, permission errors)
118-
2. Unexpected **User Input** (e.g. invalid text format, invalid number format, invalid date range)
119-
3. Unexpected **App State** (e.g. inconsistent navigation request, invalid combination of form options)
101+
If you're not using the TelemetryDeck Swift SDK (for example, on Android, Web, or other platforms), you'll need to manually build the error signal.
120102

121-
Each of these has a dedicated chart in the "Errors" tab, you just need to report one of `thrown-exception`, `user-input`, or `app-state` to the parameter `TelemetryDeck.Error.category`.
103+
### Signal Structure
122104

123-
Here's some guidance on when to use which category in Swift:
105+
**Event name**: `TelemetryDeck.Error.occurred`
124106

125-
- A clear sign to report a `thrown-exception` error is a `do-catch` clause or uses of `try?` in Swift where you can send the error signal when it returns `nil`.
126-
- Whenever you make use of the nil-coalescing operator `??` or unwrap an Optional with `if-let` or `guard-let`, potentially some kind of conversion of user input into another type might happen with a fallback behavior – this is a typical `user-input` error.
127-
- Search for any uses of [`assert`](<https://developer.apple.com/documentation/swift/assert(_:_:file:line:)>) or [`assertionFailure`](<https://developer.apple.com/documentation/swift/assertionfailure(_:file:line:)>) in your code and additionally report these detected unexpected states of your app during runtime as `app-state` errors. If you weren't aware, the `assert`/`assertionFailure` functions are similar to `fatalError`/`precondition`/`preconditionFailure` with the difference that they only stop program execution during DEBUG builds, not in production builds.
107+
**Required parameter**:
128108

129-
A full signal that reports a `user-input` category error could end up looking something like this in Swift:
109+
- `TelemetryDeck.Error.id`: A consistent identifier for grouping similar errors
130110

131-
```swift
132-
var hourlyRate: Int {
133-
if let hourlyRate = Int(self.textFieldInput) {
134-
return hourlyRate
135-
} else {
136-
TelemetryDeck.signal(
137-
"TelemetryDeck.Error.occurred",
138-
parameters: [
139-
"TelemetryDeck.Error.id": "ProjectForm.hourlyRateConversionFailed",
140-
"TelemetryDeck.Error.category": "user-input",
141-
"TelemetryDeck.Error.message": "Text '\(self.textFieldInput)' could not be converted to type 'Int'."
142-
]
143-
)
144-
return 0 // fallback value
145-
}
146-
}
147-
```
111+
**Optional parameters**:
148112

149-
In the Swift SDK, you can instead call this shorter function with a built-in `ErrorCategory` enum:
113+
- `TelemetryDeck.Error.message`: The full error message or description
114+
- `TelemetryDeck.Error.category`: One of `thrown-exception`, `user-input`, or `app-state`
150115

151-
```swift
152-
TelemetryDeck.errorOccurred(
153-
id: "ProjectForm.hourlyRateConversionFailed",
154-
category: .userInput,
155-
message: "Text '\(self.textFieldInput)' could not be converted to type 'Int'."
156-
)
157-
```
116+
## Built-In Error Categories
117+
118+
Unexpected behavior generally falls into one of three categories, each with a dedicated chart in the "Errors" tab:
119+
120+
1. **Thrown Exceptions** (`thrown-exception`): Parsing errors, I/O errors, permission errors
121+
2. **User Input** (`user-input`): Invalid text format, invalid number format, invalid date range
122+
3. **App State** (`app-state`): Inconsistent navigation request, invalid combination of form options
123+
124+
### When to Use Each Category
158125

159-
Please note that when calling the `TelemetryDeck.errorOccurred(identifiableError:)` function, the category is implicitly set to `.thrownException`, but you can override it if needed.
126+
- **thrown-exception**: Use in `try-catch` blocks or when handling `null`/`nil` returns from operations that might fail
127+
- **user-input**: Use when converting or validating user input with fallback behavior
128+
- **app-state**: Use for assertion failures or unexpected application states that shouldn't occur in normal operation
160129

161130
## Effect on Privacy & App Tracking Transparency
162131

articles/preset-purchases.md

Lines changed: 54 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,87 @@ If you are offering In-App Purchases in your app, you might have noticed some de
1414

1515
That's why you might want to set up a signal in your application to track purchases in your app through TelemetryDeck with just a couple of seconds delay, providing you with the live data you want.
1616

17+
You can use these methods to include your purchase data in TelemetryDeck:
18+
19+
- Use the TelemetryDeck Swift SDK directly
20+
- If you're already using RevenueCat, you can use the RevenueCat Integration
21+
- If you're using FreemiumKit, you can connect that to TelemetryDeck
22+
23+
See the sections below for a detailed description.
24+
1725
{% notewarning "Live Data vs. Correct Data" %}
1826
We do not offer any intelligence to correct once reported purchases, such as when users make refunds, or to detect subscription renewals. Therefore, our insights focus on more recent data. For longer-term or 100% correct data, refer to official sources.
1927
{% endnotewarning %}
2028

21-
## Sending the Signal
29+
## Using the TelemetryDeck Swift SDK
2230

23-
To report purchases to TelemetryDeck, send the event name `TelemetryDeck.Purchase.completed` with the `floatValue` parameter set to the USD amount the user purchased. For example, using the Swift SDK you can get the price from `StoreKit.Transaction` like so:
31+
If you're using the TelemetryDeck Swift SDK, tracking purchases is incredibly simple. Just call the convenience method when you receive a StoreKit transaction:
2432

2533
```swift
26-
let priceValue = NSDecimalNumber(decimal: transaction.price ?? Decimal()).doubleValue
27-
28-
TelemetryDeck.signal("TelemetryDeck.Purchase.completed", floatValue: priceValue)
34+
TelemetryDeck.purchaseCompleted(transaction: transaction)
2935
```
3036

31-
{% notewarning "Converting Currencies" %}
32-
Make sure to convert any currencies to USD before sending them as signals. You can use [an API like this](https://www.exchangerate-api.com/docs/standard-requests) which offers 1,500 requests per month free of charge to get current exchange rates if needed. You could also fetch & hard-code them to your app for a rough estimate if you expect more than 1,500 purchases per month.
37+
That's it! This method automatically:
38+
39+
- Extracts the price from the transaction
40+
- Converts the currency to USD (using hard-coded exchange rates)
41+
- Determines if it's a subscription or one-time purchase
42+
- Includes the storefront country and currency codes
43+
- Sends the properly formatted signal to TelemetryDeck
44+
45+
{% noteinfo "Requirements" %}
46+
The `purchaseCompleted` convenience function is only available on iOS 15 or higher. It accepts the same optional arguments as the `signal` function (namely `parameters` and `customUserID`) in case you want to provide additional context info.
47+
{% endnoteinfo %}
48+
49+
## Using TelemetryDeck with RevenueCat
50+
51+
If you use [RevenueCat](https://revenuecat.com) and followed their [setup guide](https://www.revenuecat.com/docs/getting-started/making-purchases), you will have a `Purchases.shared.purchase(package:)` call somewhere in your code. The closure of this function gets a RevenueCat-specific `transaction` [wrapper](https://github.com/RevenueCat/purchases-ios/blob/11f3962192271cdbbb70096ff5a693b8a0e48f49/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift) as its first parameter. You can get the native `StoreKit.Transaction` type by calling `transaction.sk2Transaction` and then pass it to `TelemetryDeck.purchaseCompleted()`. If you use RevenueCats built-in paywalls, they currently don't provide access to `transaction`, which we reported in [this issue](https://github.com/RevenueCat/purchases-ios/issues/4007).
52+
53+
## Using FreemiumKit
54+
55+
If you use [FreemiumKit](https://freemiumkit.app), just add their SDKs `.onPurchaseCompleted` view modifier to your main view. It passes the `transaction` parameter to the closure, which you can directly pass to `TelemetryDeck.purchaseCompleted(transaction: transaction)`. Read the related section in their [setup guide](https://freemiumkit.app/documentation/freemiumkit/setupguide#Direct-Access-to-StoreKit-Transactions) to learn more.
56+
57+
## Manual Signal Structure for Other Platforms
58+
59+
{% notewarning "Only Needed for Non-Swift Platforms" %}
60+
The following section describes the manual signal structure only necessary if you are NOT using the TelemetryDeck Swift SDK. Swift developers should use the `purchaseCompleted` convenience method described above.
3361
{% endnotewarning %}
3462

35-
The Swift SDK ships with a more convenient API that handles reading the price from the StoreKit transaction and converting to USD (with hard-coded non-live currency conversion) for you like so:
63+
If you're reporting purchases from other platforms (Android, Web, etc.), you'll need to manually construct and send the purchase signal with the following structure:
3664

37-
```swift
38-
TelemetryDeck.purchaseCompleted(transaction: transaction)
39-
```
65+
### Required Fields
4066

41-
{% noteinfo "Built-In Automatics" %}
42-
The `purchaseCompleted` convenience function in the Swift SDK also automates the extraction of the values explained in the next section. Optionally, it accepts the same arguments as the `signal` function (namely `parameters` and `customUserID`) in case you want to provide additional context info. The function is only available on iOS 15 or higher.
43-
{% endnoteinfo %}
67+
- **Event name**: Must be `TelemetryDeck.Purchase.completed`
68+
- **`floatValue`**: The purchase amount in USD
4469

45-
{% noteinfo "RevenueCat" %}
46-
If you use [RevenueCat](https://revenuecat.com) and followed their [setup guide](https://www.revenuecat.com/docs/getting-started/making-purchases), you will have a `Purchases.shared.purchase(package:)` call somewhere in your code. The closure of this function gets a RevenueCat-specific `transaction` [wrapper](https://github.com/RevenueCat/purchases-ios/blob/11f3962192271cdbbb70096ff5a693b8a0e48f49/Sources/Purchasing/StoreKitAbstractions/StoreTransaction.swift) as its first parameter. You can either access fields directly from that or get the native `StoreKit.Transaction` type by calling `transaction.sk2Transaction`. If you use RevenueCats built-in paywalls, they currently don't provide access to `transaction`, which we reported in [this issue](https://github.com/RevenueCat/purchases-ios/issues/4007).
47-
{% endnoteinfo %}
70+
{% notewarning "Manual Currency Conversion Required" %}
71+
When sending purchase signals manually, you MUST convert the transaction value to USD yourself before sending. You can use [an API like this](https://www.exchangerate-api.com/docs/standard-requests) which offers 1,500 requests per month free of charge to get current exchange rates. Alternatively, you could fetch & hard-code exchange rates in your app for a rough estimate if you expect more than 1,500 purchases per month.
72+
{% endnotewarning %}
4873

49-
{% noteinfo "FreemiumKit" %}
50-
If you use [FreemiumKit](https://freemiumkit.app), just add their SDKs `.onPurchaseCompleted` view modifier to your main view. It passes the `transaction` parameter to the closure, which is exactly what we need so you can just copy & paste the above code as-is. Read the related section in their [setup guide](https://freemiumkit.app/documentation/freemiumkit/setupguide#Direct-Access-to-StoreKit-Transactions) to learn more.
51-
{% endnoteinfo %}
74+
### Optional but Recommended Payload Keys
5275

53-
## Attaching the Payload
76+
To get more detailed insights, include these additional parameters:
5477

55-
Optionally (but recommended), there are two additional payload keys that will give you additional insights:
78+
- `TelemetryDeck.Purchase.type`: Either `subscription` or `one-time-purchase`
79+
- `TelemetryDeck.Purchase.countryCode`: The country code of the storefront
80+
- `TelemetryDeck.Purchase.currencyCode`: The currency code of the storefront
5681

57-
- `TelemetryDeck.Purchase.type`: Pass either `subscription` or `one-time-purchase` to see the type distribution.
58-
- `TelemetryDeck.Purchase.countryCode`: Pass the country code of the storefront to see the country distribution.
59-
- `TelemetryDeck.Purchase.currencyCode`: Pass the currency code of the storefront to see currency distribution.
82+
### Example Manual Implementation (Swift)
6083

61-
In Swift getting all values and sending them looks like this:
84+
Here's what the manual implementation looks like if you need to customize it or understand what the convenience method does internally:
6285

6386
```swift
87+
// Convert price to USD first (you need to handle currency conversion)
88+
let priceInUSD = convertToUSD(transaction.price, from: transaction.currencyCode)
89+
6490
TelemetryDeck.signal(
6591
"TelemetryDeck.Purchase.completed",
6692
parameters: [
6793
"TelemetryDeck.Purchase.type": transaction.subscriptionGroupID != nil ? "subscription" : "one-time-purchase",
6894
"TelemetryDeck.Purchase.countryCode": transaction.storefrontCountryCode,
6995
"TelemetryDeck.Purchase.currencyCode": transaction.currencyCode ?? "???"
7096
],
71-
floatValue: NSDecimalNumber(decimal: transaction.price ?? Decimal()).doubleValue
97+
floatValue: priceInUSD
7298
)
7399
```
74100

@@ -82,4 +108,4 @@ You can answer all subsequent questions with "No" because we neither link collec
82108

83109
When all is configured your "Purchases" entry in your App Privacy page should end up looking like this:
84110

85-
![Purchases entry with only 'Used for Analytics' in the box](/docs/images/purchases-privacy-box.png)
111+
![Purchases entry with only 'Used for Analytics' in the box](/docs/images/purchases-privacy-box.png)

0 commit comments

Comments
 (0)