-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
18 changed files
with
711 additions
and
7 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,7 @@ Pod::Spec.new do |spec| | |
spec.authors = { 'Automattic' => '[email protected]' } | ||
spec.summary = 'Simple way to track events in an iOS app with Automattic Tracks internal service' | ||
spec.source = { :git => 'https://github.com/Automattic/Automattic-Tracks-iOS.git', :tag => spec.version.to_s } | ||
spec.swift_version = '4.2' | ||
spec.swift_version = '5.0' | ||
|
||
spec.ios.source_files = 'Automattic-Tracks-iOS/**/*.{h,m,swift}' | ||
spec.ios.exclude_files = 'Automattic-Tracks-OSX/Automattic_Tracks_OSX.h' | ||
|
Large diffs are not rendered by default.
Oops, something went wrong.
19 changes: 19 additions & 0 deletions
19
Automattic-Tracks-iOS/Event Logging/EventLoggingDataSource.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import Foundation | ||
|
||
public protocol EventLoggingDataSource { | ||
/// A base-64 encoded representation of the encryption key. | ||
var loggingEncryptionKey: String { get } | ||
|
||
/// The URL to the upload endpoint for encrypted logs. | ||
var logUploadURL: URL { get } | ||
|
||
/// The path to the log file for the most recent session. | ||
var previousSessionLogPath: URL? { get } | ||
} | ||
|
||
public extension EventLoggingDataSource { | ||
// The default implementation points to the WP.com private encrypted logging API | ||
var logUploadURL: URL { | ||
return URL(string: "https://public-api.wordpress.com/rest/v1.1/encrypted-logging")! | ||
} | ||
} |
35 changes: 35 additions & 0 deletions
35
Automattic-Tracks-iOS/Event Logging/EventLoggingDelegate.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import Foundation | ||
|
||
public protocol EventLoggingDelegate { | ||
/// The event logging system will call this delegate property prior to attempting to upload, giving the application a chance to determine | ||
/// whether or not the upload should proceed. If this is not overridden, the default is `false`. | ||
var shouldUploadLogFiles: Bool { get } | ||
|
||
/// The event logging system will call this delegate method each time a log file starts uploading. | ||
func didStartUploadingLog(_ log: LogFile) | ||
|
||
/// The event logging system will call this delegate method if a log file upload is cancelled by the delegate. | ||
func uploadCancelledByDelegate(_ log: LogFile) | ||
|
||
/// The event logging system will call this delegate method if a log file fails to upload. | ||
/// It may be called prior to upload starting if the file is missing, and is called prior to the `upload` callback. | ||
func uploadFailed(withError: Error, forLog: LogFile) | ||
|
||
/// The event logging system will call this delegate method each time a log file finishes uploading. | ||
/// It is called prior to the `upload` callback. | ||
func didFinishUploadingLog(_ log: LogFile) | ||
} | ||
|
||
/// Default implementations for EventLoggingDelegate | ||
public extension EventLoggingDelegate { | ||
|
||
var shouldUploadLogFiles: Bool { | ||
return false // Use a privacy-preserving default | ||
} | ||
|
||
// Empty default implementations allow the developer to only implement these if they need them | ||
func didStartUploadingLog(_ log: LogFile) {} | ||
func uploadCancelledByDelegate(_ log: LogFile) {} | ||
func uploadFailed(withError error: Error, forLog log: LogFile) {} | ||
func didFinishUploadingLog(_ log: LogFile) {} | ||
} |
59 changes: 59 additions & 0 deletions
59
Automattic-Tracks-iOS/Event Logging/EventLoggingUploadManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import Foundation | ||
import CocoaLumberjack | ||
import Sodium | ||
|
||
class EventLoggingUploadManager { | ||
|
||
private enum Constants { | ||
static let uuidHeaderKey = "log-uuid" | ||
static let uploadHttpMethod = "POST" | ||
} | ||
|
||
private let dataSource: EventLoggingDataSource | ||
private let delegate: EventLoggingDelegate | ||
private let networkService: EventLoggingNetworkService | ||
private let fileManager: FileManager | ||
|
||
typealias LogUploadCallback = (Result<Void, Error>) -> Void | ||
|
||
init(dataSource: EventLoggingDataSource, | ||
delegate: EventLoggingDelegate, | ||
networkService: EventLoggingNetworkService = EventLoggingNetworkService(), | ||
fileManager: FileManager = FileManager.default | ||
) { | ||
self.dataSource = dataSource | ||
self.delegate = delegate | ||
self.networkService = networkService | ||
self.fileManager = fileManager | ||
} | ||
|
||
func upload(_ log: LogFile, then callback: @escaping LogUploadCallback) { | ||
guard delegate.shouldUploadLogFiles else { | ||
delegate.uploadCancelledByDelegate(log) | ||
return | ||
} | ||
|
||
guard let fileContents = fileManager.contents(atUrl: log.url) else { | ||
delegate.uploadFailed(withError: EventLoggingFileUploadError.fileMissing, forLog: log) | ||
return | ||
} | ||
|
||
var request = URLRequest(url: dataSource.logUploadURL) | ||
request.addValue(log.uuid, forHTTPHeaderField: Constants.uuidHeaderKey) | ||
request.httpMethod = Constants.uploadHttpMethod | ||
request.httpBody = fileContents | ||
|
||
delegate.didStartUploadingLog(log) | ||
|
||
networkService.uploadFile(request: request, fileURL: log.url) { result in | ||
switch result { | ||
case .success: | ||
self.delegate.didFinishUploadingLog(log) | ||
callback(.success(())) | ||
case .failure(let error): | ||
self.delegate.uploadFailed(withError: error, forLog: log) | ||
callback(.failure(error)) | ||
} | ||
} | ||
} | ||
} |
27 changes: 27 additions & 0 deletions
27
Automattic-Tracks-iOS/Extensions/FileManager+Extensions.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import Foundation | ||
|
||
extension FileManager { | ||
|
||
var documentsDirectory: URL { | ||
let documentsDirectory = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first! | ||
return URL(fileURLWithPath: documentsDirectory, isDirectory: true) | ||
} | ||
|
||
func contentsOfDirectory(at url: URL) throws -> [URL] { | ||
return try self.contentsOfDirectory(atPath: url.path).map { url.appendingPathComponent($0) } | ||
} | ||
|
||
func contents(atUrl url: URL) -> Data? { | ||
return self.contents(atPath: url.path) | ||
} | ||
|
||
func fileExistsAtURL(_ url: URL) -> Bool { | ||
return self.fileExists(atPath: url.path) | ||
} | ||
|
||
func directoryExistsAtURL(_ url: URL) -> Bool { | ||
var isDir: ObjCBool = false | ||
let exists = self.fileExists(atPath: url.path, isDirectory: &isDir) | ||
return exists && isDir.boolValue | ||
} | ||
} |
15 changes: 15 additions & 0 deletions
15
Automattic-Tracks-iOS/Model/EventLoggingFileUploadError.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import Foundation | ||
|
||
public enum EventLoggingFileUploadError: Error, LocalizedError { | ||
case httpError(String) | ||
case fileMissing | ||
|
||
public var errorDescription: String? { | ||
switch self { | ||
case .httpError(let message): return message | ||
case .fileMissing: return NSLocalizedString( | ||
"File not found", comment: "A message indicating that a file queued for upload could not be found" | ||
) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
35 changes: 35 additions & 0 deletions
35
Automattic-Tracks-iOS/Services/EventLoggingNetworkService.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
import Foundation | ||
|
||
class EventLoggingNetworkService { | ||
|
||
typealias ResultCallback = (Result<Data?, Error>) -> Void | ||
|
||
private let urlSession: URLSession | ||
|
||
init(urlSession: URLSession = URLSession.shared) { | ||
self.urlSession = urlSession | ||
} | ||
|
||
func uploadFile(request: URLRequest, fileURL: URL, completion: @escaping ResultCallback) { | ||
urlSession.uploadTask(with: request, fromFile: fileURL, completionHandler: { data, response, error in | ||
|
||
if let error = error { | ||
completion(.failure(error)) | ||
return | ||
} | ||
|
||
/// The `response` should *always* be an HTTPURLResponse. | ||
/// Fail fast by force-unwrapping – this will cause a crash and will bring the issue to our attention if something has changed. | ||
let statusCode = (response as! HTTPURLResponse).statusCode | ||
|
||
/// Generate a reasonable error message based on the HTTP status | ||
if !(200 ... 299).contains(statusCode) { | ||
let errorMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode) | ||
completion(.failure(EventLoggingFileUploadError.httpError(errorMessage))) | ||
return | ||
} | ||
|
||
completion(.success(data)) | ||
}).resume() | ||
} | ||
} |
40 changes: 40 additions & 0 deletions
40
Automattic-Tracks-iOS/Services/EventLoggingUploadQueue.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,40 @@ | ||
import Foundation | ||
|
||
class EventLoggingUploadQueue { | ||
|
||
private let fileManager: FileManager | ||
var storageDirectory: URL | ||
|
||
init(storageDirectory: URL? = nil, fileManager: FileManager = FileManager.default) { | ||
let defaultStorageDirectory = fileManager.documentsDirectory.appendingPathComponent("log-upload-queue") | ||
self.storageDirectory = storageDirectory ?? defaultStorageDirectory | ||
self.fileManager = fileManager | ||
} | ||
|
||
/// The log file on top of the queue | ||
var first: LogFile? { | ||
guard let url = try? fileManager.contentsOfDirectory(at: storageDirectory).first else { | ||
return nil | ||
} | ||
|
||
return LogFile.fromExistingFile(at: url) | ||
} | ||
|
||
func add(_ log: LogFile) throws { | ||
try createStorageDirectoryIfNeeded() | ||
try fileManager.copyItem(at: log.url, to: storageDirectory.appendingPathComponent(log.fileName)) | ||
} | ||
|
||
func remove(_ log: LogFile) throws { | ||
let url = storageDirectory.appendingPathComponent(log.fileName) | ||
if fileManager.fileExistsAtURL(url) { | ||
try fileManager.removeItem(at: url) | ||
} | ||
} | ||
|
||
func createStorageDirectoryIfNeeded() throws { | ||
if !fileManager.directoryExistsAtURL(storageDirectory) { | ||
try fileManager.createDirectory(at: storageDirectory, withIntermediateDirectories: true, attributes: nil) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
import Foundation | ||
import Sodium | ||
@testable import AutomatticTracks | ||
|
||
typealias LogFileCallback = (LogFile) -> () | ||
typealias ErrorWithLogFileCallback = (Error, LogFile) -> () | ||
|
||
enum MockError: Error { | ||
case generic | ||
} | ||
|
||
class MockEventLoggingDataSource: EventLoggingDataSource { | ||
|
||
var loggingEncryptionKey: String = "foo" | ||
var previousSessionLogPath: URL? = nil | ||
|
||
/// Overrides for logUploadURL | ||
var _logUploadURL: URL = URL(string: "example.com")! | ||
var logUploadURL: URL { | ||
return _logUploadURL | ||
} | ||
|
||
func setLogUploadUrl(_ url: URL) { | ||
self._logUploadURL = url | ||
} | ||
|
||
func withEncryptionKeys() -> Self { | ||
let keyPair = Sodium().box.keyPair()! | ||
loggingEncryptionKey = Data(keyPair.publicKey).base64EncodedString() | ||
return self | ||
} | ||
} | ||
|
||
class MockEventLoggingDelegate: EventLoggingDelegate { | ||
|
||
var didStartUploadingTriggered = false | ||
var didStartUploadingCallback: LogFileCallback? | ||
|
||
func didStartUploadingLog(_ log: LogFile) { | ||
didStartUploadingTriggered = true | ||
didStartUploadingCallback?(log) | ||
} | ||
|
||
var didFinishUploadingTriggered = false | ||
var didFinishUploadingCallback: LogFileCallback? | ||
|
||
func didFinishUploadingLog(_ log: LogFile) { | ||
didFinishUploadingTriggered = true | ||
didFinishUploadingCallback?(log) | ||
} | ||
|
||
var uploadCancelledByDelegateTriggered = false | ||
var uploadCancelledByDelegateCallback: LogFileCallback? | ||
|
||
func uploadCancelledByDelegate(_ log: LogFile) { | ||
uploadCancelledByDelegateTriggered = true | ||
uploadCancelledByDelegateCallback?(log) | ||
} | ||
|
||
var uploadFailedTriggered = false | ||
var uploadFailedCallback: ErrorWithLogFileCallback? | ||
|
||
func uploadFailed(withError error: Error, forLog log: LogFile) { | ||
uploadFailedTriggered = true | ||
uploadFailedCallback?(error, log) | ||
} | ||
|
||
func setShouldUploadLogFiles(_ newValue: Bool) { | ||
_shouldUploadLogFiles = newValue | ||
} | ||
|
||
private var _shouldUploadLogFiles: Bool = true | ||
var shouldUploadLogFiles: Bool { | ||
return _shouldUploadLogFiles | ||
} | ||
} | ||
|
||
class MockEventLoggingNetworkService: EventLoggingNetworkService { | ||
var shouldSucceed = true | ||
|
||
override func uploadFile(request: URLRequest, fileURL: URL, completion: @escaping EventLoggingNetworkService.ResultCallback) { | ||
shouldSucceed ? completion(.success(Data())) : completion(.failure(MockError.generic)) | ||
} | ||
} | ||
|
||
class MockEventLoggingUploadQueue: EventLoggingUploadQueue { | ||
|
||
typealias LogFileCallback = (LogFile) -> () | ||
|
||
var queue = [LogFile]() | ||
|
||
var addCallback: LogFileCallback? | ||
override func add(_ log: LogFile) { | ||
self.addCallback?(log) | ||
self.queue.append(log) | ||
} | ||
|
||
var removeCallback: LogFileCallback? | ||
override func remove(_ log: LogFile) { | ||
self.removeCallback?(log) | ||
self.queue.removeAll(where: { $0.uuid == log.uuid }) | ||
} | ||
|
||
override var first: LogFile? { | ||
return self.queue.first | ||
} | ||
} |
Oops, something went wrong.