Skip to content

Commit 89e00f7

Browse files
authored
A liberal dose of Rollbar around upload (#67)
1 parent 30db2aa commit 89e00f7

File tree

10 files changed

+78
-85
lines changed

10 files changed

+78
-85
lines changed

README.md

+24-6
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,21 @@
22
App for taking pictures of trees and storing that on a remote server. Mainly used by people who plant trees so they don't have to manually type coordinates with pictures they took and then try to guess the site/species afterwards.
33

44
## Running the app from Xcode with Mock server
5-
1. Make sure you have downloaded Xcode 12.2+
5+
1. Make sure you have downloaded Xcode 13.4+
66
2. Open the project in Xcode (you'll notice the dependencies will start to fetch in the background).
77
(In the meantime, Xcode will need to fetch dependencies for the project... 😴)
8-
3. You'll most likely need to change bundle identifier of the project. Basically because the project is set to auto-sign, each person that wants to run this on the device would need to update the bundle to be a unique id not registered before. E.g. from `com.protect.earth.Tree-Tracker` to `com.mynickname.Tree-Tracker`.
9-
4. Make sure you are running `Tree Tracker (Mock server)` scheme and hit run!
10-
5. When running on a device, you'll also need to trust the certificate in Settings -> General -> Profiles, otherwise you'll see an error after installing the build and before running it.
8+
3. The signing settings for the project are configured for our CICD build pipeline, and will not allow you to build and run the app on your own device. To fix this, simply enable automatic signing in XCode and update the bundle identifier to something unique to you. This will update the .xcodeproj file accordingly. **NOTE** _Changes to signing settings must not be checked in, as these will break the automated builds._
9+
4. Running the `Tree Tracker` scheme will use the main Airtable base you [configure in your secrets file](#config) and will make inserts to your base tables. Running the `Tree Tracker (Mock server` scheme will use hard-coded mock API responses and will not touch Airtable.
10+
5. When running on a device, you'll also need to trust the certificate in _Settings -> General -> Profiles_, otherwise you'll see an error after installing the build and before running it.
1111

1212
## Using your own Airtable/Cloudinary server
13-
Well, this is a bit complicated but still doable.
13+
Well, this is a bit complicated but still doable.
14+
Sign up for a free [Airtable](https://www.airtable.com) account, as you will need to provide the details of *2* Airtable bases - one
15+
to support the execution of integration tests, and one for the app to use when in normal usage.
16+
17+
For development purposes, the 2 bases
18+
can actually be the same. If you are doing this, it is recommended to create two sets of tables in the same base, and use a prefix on
19+
the table name. This can then be specified in the `TEST_AIRTABLE_TABLE_NAME_PREFIX` secret (see [later](#config)).
1420

1521
### Airtable tables
1622
Our current API type expects that you have 4 tables:
@@ -49,7 +55,15 @@ Because Airtable doesn't support uploading images yet, we have to use an externa
4955
2. Now create an [upload preset](https://cloudinary.com/console/settings/upload) (this will give you the Upload Preset name).
5056
3. Keep the keys as you'd need to add them to Secrets.xcconfig later on.
5157

52-
### Additional project config
58+
## Rollbar
59+
We use [Rollbar](https://www.rollbar.com) for centralised logging of errors, to help us troubleshoot issues with the app during real world usage.
60+
If you wish, you can sign up for a free Rollbar account, generate your own API token and provide it through `ROLLBAR_AUTH_TOKEN` to see telemetry
61+
in Rollbar during development. This can be useful if you are specifically adding telemetry features, but otherwise is probably more complex than
62+
just looking at the logs in XCode console.
63+
64+
If you choose not to setup Rollbar, simply add a dummy value for `ROLLBAR_AUTH_TOKEN` and any Rollbar calls will silently fail.
65+
66+
## Additional project config {#config}
5367
Now, to run the project, we'll need to generate Secrets file. This means you need to run first install [`pouch`](https://github.com/sunshinejr/pouch) (the easiest is using `brew install sunshinejr/formulae/pouch`). Now, you need to have these environment variables available. Have this at the end of the file (bash: most likely in `.bash_profile` or `.bashrc`, zsh: most likely `.zshenv` or `.zshrc`):
5468
```
5569
export AIRTABLE_API_KEY=yourKey123
@@ -60,6 +74,10 @@ export AIRTABLE_SUPERVISORS_TABLE_NAME=Supervisors
6074
export AIRTABLE_SITES_TABLE_NAME=Sites
6175
export CLOUDINARY_CLOUD_NAME=qqq2ek4mq
6276
export CLOUDINARY_UPLOAD_PRESET_NAME=iadfadff
77+
export TEST_AIRTABLE_API_KEY=yourTestKey123
78+
export TEST_AIRTABLE_BASE_ID=appNiceTreeTest
79+
export TEST_AIRTABLE_TABLE_NAME_PREFIX=test_
80+
export ROLLBAR_AUTH_TOKEN=yourRollbarToken
6381
```
6482
In the root folder, run `pouch`, which should generate a file at `./TreeTracker/Secrets.swift`.
6583

Tree Tracker.xcodeproj/project.pbxproj

+4
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@
116116
9D5D5E2A284B635900F3AD3E /* AirtableSpeciesService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5D5E29284B635900F3AD3E /* AirtableSpeciesService.swift */; };
117117
9D5D5E2C284B66BB00F3AD3E /* SupervisorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5D5E2B284B66BB00F3AD3E /* SupervisorService.swift */; };
118118
9D5D5E2E284B670400F3AD3E /* AirtableSupervisorService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5D5E2D284B670400F3AD3E /* AirtableSupervisorService.swift */; };
119+
9D5F06332878ADF000C8D4A6 /* DataResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5F06322878ADF000C8D4A6 /* DataResponse.swift */; };
119120
9D79A5A7283AE03100F0F96C /* SiteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D79A5A6283AE03100F0F96C /* SiteService.swift */; };
120121
9D79A5AA283AE27500F0F96C /* DataAccessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D79A5A9283AE27500F0F96C /* DataAccessError.swift */; };
121122
9D79A5AC283AE32C00F0F96C /* AirtableSiteService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D79A5AB283AE32C00F0F96C /* AirtableSiteService.swift */; };
@@ -253,6 +254,7 @@
253254
9D5D5E29284B635900F3AD3E /* AirtableSpeciesService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirtableSpeciesService.swift; sourceTree = "<group>"; };
254255
9D5D5E2B284B66BB00F3AD3E /* SupervisorService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SupervisorService.swift; sourceTree = "<group>"; };
255256
9D5D5E2D284B670400F3AD3E /* AirtableSupervisorService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirtableSupervisorService.swift; sourceTree = "<group>"; };
257+
9D5F06322878ADF000C8D4A6 /* DataResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DataResponse.swift; sourceTree = "<group>"; };
256258
9D79A5A6283AE03100F0F96C /* SiteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SiteService.swift; sourceTree = "<group>"; };
257259
9D79A5A9283AE27500F0F96C /* DataAccessError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DataAccessError.swift; sourceTree = "<group>"; };
258260
9D79A5AB283AE32C00F0F96C /* AirtableSiteService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirtableSiteService.swift; sourceTree = "<group>"; };
@@ -379,6 +381,7 @@
379381
85792A7425B0A35100BFDA96 /* Extensions */ = {
380382
isa = PBXGroup;
381383
children = (
384+
9D5F06322878ADF000C8D4A6 /* DataResponse.swift */,
382385
85B839EB25B8661E0008E167 /* Collection.swift */,
383386
85B83A3725B9D9C40008E167 /* Data.swift */,
384387
851DAC1C262B4B0B0087E1D4 /* Date.swift */,
@@ -804,6 +807,7 @@
804807
9DB29B562821C28400AAC73D /* SettingsController.swift in Sources */,
805808
9D5D5E2E284B670400F3AD3E /* AirtableSupervisorService.swift in Sources */,
806809
853ABD562596144900144B0D /* AppDelegate.swift in Sources */,
810+
9D5F06332878ADF000C8D4A6 /* DataResponse.swift in Sources */,
807811
857BADA825B1FA93005D7D35 /* TreeDetailsViewController.swift in Sources */,
808812
9D5D5E2C284B66BB00F3AD3E /* SupervisorService.swift in Sources */,
809813
85B83A1C25B8AC650008E167 /* EditLocalTreeViewModel.swift in Sources */,
+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import Foundation
2+
import Alamofire
3+
4+
extension DataResponse {
5+
6+
func dataAsUTF8String() -> String {
7+
guard let data = self.data else { return "" }
8+
guard let utf8String = String.init( data: data, encoding: .utf8) else { return "" }
9+
return utf8String
10+
}
11+
12+
}

Tree Tracker/Info.plist

+1-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
<key>CFBundlePackageType</key>
1818
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
1919
<key>CFBundleShortVersionString</key>
20-
<string>0.8.1</string>
20+
<string>0.8.2</string>
2121
<key>CFBundleVersion</key>
2222
<string>$(CURRENT_PROJECT_VERSION)</string>
2323
<key>ITSAppUsesNonExemptEncryption</key>

Tree Tracker/Screens/Upload/UploadViewModel.swift

+12
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Resolver
3+
import RollbarNotifier
34

45
protocol UploadNavigating: AnyObject {
56
func triggerAddTreesFlow(completion: @escaping (Bool) -> Void)
@@ -133,8 +134,10 @@ final class UploadViewModel: CollectionViewModel {
133134
logger.log(.upload, "Uploading images...")
134135
database.fetchLocalTrees { [weak self] trees in
135136
self?.logger.log(.upload, "Trees to upload: \(trees.count)")
137+
Rollbar.infoMessage("Starting upload of trees", data: ["tree_count": trees.count], context: "UploadViewModel.uploadLocalTreesRecursively")
136138

137139
guard let tree = trees.sorted(by: \.createDate, order: .descending).first else {
140+
Rollbar.infoMessage("Trees upload complete")
138141
self?.logger.log(.upload, "No more items to upload - bailing.")
139142
self?.stopUploading()
140143
return
@@ -150,6 +153,8 @@ final class UploadViewModel: CollectionViewModel {
150153
completion: { result in
151154
switch result {
152155
case let .success(airtableTree):
156+
Rollbar.infoMessage("Successfully uploaded tree", data: ["id": airtableTree.id,
157+
"md5": airtableTree.imageMd5 ?? ""])
153158
self?.logger.log(.upload, "Successfully uploaded tree.")
154159
self?.database.save([airtableTree], sentFromThisDevice: true)
155160
self?.database.remove(tree: tree) {
@@ -159,6 +164,13 @@ final class UploadViewModel: CollectionViewModel {
159164
case let .failure(error):
160165
self?.update(uploadProgress: 0.0, for: tree)
161166
self?.presentUploadButton(isUploading: false)
167+
Rollbar.errorError(error,
168+
data: ["supervisor": tree.supervisor,
169+
"site": tree.site,
170+
"coordinates": tree.coordinates ?? "",
171+
"md5": tree.imageMd5 ?? "",
172+
"phImageId": tree.phImageId],
173+
context: "UploadViewModel.uploadLocalTreesRecursively")
162174
self?.logger.log(.upload, "Error when uploading a local tree: \(error)")
163175
}
164176
}

Tree Tracker/Services/AlamofireApi.swift

+22-50
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import Foundation
22
import Alamofire
33
import class UIKit.UIImage
4+
import RollbarNotifier
45

56
fileprivate extension LogCategory {
67
static var api = LogCategory(name: "Api")
@@ -36,52 +37,6 @@ final class AlamofireApi: Api {
3637
maxRetries: Constants.Http.requestRetryLimit))
3738

3839
}
39-
40-
func treesPlanted(offset: String?, completion: @escaping (Result<Paginated<AirtableTree>, AFError>) -> Void) {
41-
let request = session.request(Config.treesUrl, method: .get, parameters: ["offset": offset].compactMapValues { $0 }, encoding: URLEncoding.queryString, headers: Config.headers, interceptor: nil, requestModifier: nil)
42-
43-
request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { (response: DataResponse<Paginated<AirtableTree>, AFError>) in
44-
completion(response.result)
45-
}
46-
}
47-
48-
func species(offset: String?, completion: @escaping (Result<Paginated<AirtableSpecies>, AFError>) -> Void) {
49-
let request = session.request(Config.speciesUrl, method: .get, parameters: ["offset": offset].compactMapValues { $0 }, encoding: URLEncoding.queryString, headers: Config.headers, interceptor: nil, requestModifier: nil)
50-
51-
request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { (response: DataResponse<Paginated<AirtableSpecies>, AFError>) in
52-
completion(response.result)
53-
}
54-
}
55-
56-
func sites(offset: String?, completion: @escaping (Result<Paginated<AirtableSite>, AFError>) -> Void) {
57-
let request = session.request(Config.sitesUrl, method: .get, parameters: ["offset": offset].compactMapValues { $0 }, encoding: URLEncoding.queryString, headers: Config.headers, interceptor: nil, requestModifier: nil)
58-
59-
request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { (response: DataResponse<Paginated<AirtableSite>, AFError>) in
60-
completion(response.result)
61-
}
62-
}
63-
64-
func supervisors(offset: String?, completion: @escaping (Result<Paginated<AirtableSupervisor>, AFError>) -> Void) {
65-
let request = session.request(Config.supervisorsUrl, method: .get, parameters: ["offset": offset].compactMapValues { $0 }, encoding: URLEncoding.queryString, headers: Config.headers, interceptor: nil, requestModifier: nil)
66-
67-
request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { (response: DataResponse<Paginated<AirtableSupervisor>, AFError>) in
68-
completion(response.result)
69-
}
70-
}
71-
72-
func addSite(name: String, completion: @escaping (Result<AirtableSite, AFError>) -> Void) {
73-
// build struct to represent target JSON body
74-
let parameters: [String: [String: String]] = [
75-
"fields": ["Name": name]
76-
]
77-
78-
// TODO: does specifying a nil interceptor here override the retrying interceptor we configure at session level?
79-
let request = session.request(Config.sitesUrl, method: .post, parameters: parameters, encoder: JSONParameterEncoder.default, headers: Config.headers, interceptor: nil, requestModifier: nil)
80-
81-
request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { (response: DataResponse<AirtableSite, AFError>) in
82-
completion(response.result)
83-
}
84-
}
8540

8641
func upload(tree: LocalTree, progress: @escaping (Double) -> Void = { _ in }, completion: @escaping (Result<AirtableTree, AFError>) -> Void) -> Cancellable {
8742
let upload = ImageUpload(tree: tree, logger: logger)
@@ -145,6 +100,13 @@ final class ImageUpload: Cancellable {
145100
newTree.imageMd5 = md5
146101
self?.request = self?.upload(tree: newTree, imageUrl: url, session: session, completion: completion)
147102
case let .failure(error):
103+
Rollbar.errorError(error,
104+
data: ["md5": tree.imageMd5 ?? "",
105+
"phImageId": tree.phImageId,
106+
"coordinates": tree.coordinates ?? "",
107+
"supervisor": tree.supervisor,
108+
"site": tree.site],
109+
context: "Fetching upload image for tree")
148110
completion(.failure(error))
149111
}
150112
}
@@ -155,6 +117,7 @@ final class ImageUpload: Cancellable {
155117
logger.log(.api, "Uploading image to Cloudinary...")
156118
guard let data = image.jpegData(compressionQuality: 0.8) else {
157119
logger.log(.api, "No pngData for the image, bailing")
120+
Rollbar.errorMessage("No pngData for image, upload will be skipped")
158121
completion(.failure(.explicitlyCancelled))
159122
return nil
160123
}
@@ -175,7 +138,10 @@ final class ImageUpload: Cancellable {
175138
return request.validate().responseJSON { [weak self] response in
176139
switch response.result {
177140
case let .failure(error):
178-
self?.logger.log(.api, "Error when uploading image: \(response.data.map { String.init(data: $0, encoding: .utf8) })")
141+
Rollbar.errorError(error,
142+
data: [:],
143+
context: response.dataAsUTF8String())
144+
self?.logger.log(.api, "Error when uploading image: \(response.dataAsUTF8String())")
179145
completion(.failure(error))
180146
case let .success(json as [String: Any]):
181147
let url = json["secure_url"] as? String
@@ -186,15 +152,18 @@ final class ImageUpload: Cancellable {
186152
fallthrough
187153
}
188154
default:
189-
self?.logger.log(.api, "Error when parsing json: \(response.data.map { String.init(data: $0, encoding: .utf8) })")
155+
Rollbar.errorMessage("Error while parsing JSON",
156+
data: [:],
157+
context: response.dataAsUTF8String())
158+
self?.logger.log(.api, "Error when parsing json: \(response.dataAsUTF8String())")
190159
completion(.failure(.explicitlyCancelled))
191160
}
192161
}
193162
}
194163

195164
private func upload(tree: LocalTree, imageUrl: String, session: Session, completion: @escaping (Result<AirtableTree, AFError>) -> Void) -> Request? {
196165
let airtableTree = tree.toAirtableTree(imageUrl: imageUrl)
197-
let request = session.request(AlamofireApi.Config.treesUrl, method: .post, parameters: airtableTree, encoder: JSONParameterEncoder(encoder: ._iso8601ms), headers: AlamofireApi.Config.headers, interceptor: nil, requestModifier: nil)
166+
let request = session.request(AlamofireApi.Config.treesUrl, method: .post, parameters: airtableTree, encoder: JSONParameterEncoder(encoder: ._iso8601ms), headers: AlamofireApi.Config.headers)
198167

199168
return request.validate().responseDecodable(decoder: JSONDecoder._iso8601ms) { [weak self] (response: DataResponse<AirtableTree, AFError>) in
200169
self?.progress?(1.0)
@@ -204,7 +173,10 @@ final class ImageUpload: Cancellable {
204173
self?.logger.log(.api, "Tree uploaded!")
205174
completion(.success(tree))
206175
case let .failure(error):
207-
self?.logger.log(.api, "Error when creating Airtable record: \(response.data.map { String.init(data: $0, encoding: .utf8) })")
176+
Rollbar.errorError(error,
177+
data: [:],
178+
context: response.dataAsUTF8String())
179+
self?.logger.log(.api, "Error when creating Airtable record: \(response.dataAsUTF8String())")
208180
completion(.failure(error))
209181
}
210182
}

Tree Tracker/Services/Api.swift

-5
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,6 @@ import Alamofire
33
import class UIKit.UIImage
44

55
protocol Api {
6-
func treesPlanted(offset: String?, completion: @escaping (Result<Paginated<AirtableTree>, AFError>) -> Void)
7-
func species(offset: String?, completion: @escaping (Result<Paginated<AirtableSpecies>, AFError>) -> Void)
8-
func sites(offset: String?, completion: @escaping (Result<Paginated<AirtableSite>, AFError>) -> Void)
9-
func supervisors(offset: String?, completion: @escaping (Result<Paginated<AirtableSupervisor>, AFError>) -> Void)
106
func upload(tree: LocalTree, progress: @escaping (Double) -> Void, completion: @escaping (Result<AirtableTree, AFError>) -> Void) -> Cancellable
117
func loadImage(url: String, completion: @escaping (UIImage?) -> Void)
12-
func addSite(name: String, completion: @escaping (Result<AirtableSite, AFError>) -> Void)
138
}

0 commit comments

Comments
 (0)