diff --git a/Demo/ParselyDemo.xcodeproj/project.pbxproj b/Demo/ParselyDemo.xcodeproj/project.pbxproj index 96b50e5..7fea298 100644 --- a/Demo/ParselyDemo.xcodeproj/project.pbxproj +++ b/Demo/ParselyDemo.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 2A1F02DDE91E502E3B49AFAB /* StorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2A1F03819E4FAF1F979D5926 /* StorageTests.swift */; }; + 3F0B192F2A8DD03B0012C731 /* ParselyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3F0B192D2A8DCFBC0012C731 /* ParselyTests.swift */; }; 3F147F8F29EF8CA100752DFB /* Nimble in Frameworks */ = {isa = PBXBuildFile; productRef = 3F147F8E29EF8CA100752DFB /* Nimble */; }; 3F147F9529EF965500752DFB /* ParselyAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3F147F9429EF965500752DFB /* ParselyAnalytics */; }; 3F147F9729EF96EE00752DFB /* ParselyAnalytics in Frameworks */ = {isa = PBXBuildFile; productRef = 3F147F9629EF96EE00752DFB /* ParselyAnalytics */; }; @@ -57,6 +58,7 @@ /* Begin PBXFileReference section */ 2A1F03819E4FAF1F979D5926 /* StorageTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StorageTests.swift; sourceTree = ""; }; + 3F0B192D2A8DCFBC0012C731 /* ParselyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParselyTests.swift; sourceTree = ""; }; 3F147F9129EF962200752DFB /* AnalyticsSDK-iOS */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "AnalyticsSDK-iOS"; path = ..; sourceTree = ""; }; 3FCAFC8B29E9775A00BC9360 /* UnitTests.xctestplan */ = {isa = PBXFileReference; lastKnownFileType = text; path = UnitTests.xctestplan; sourceTree = ""; }; AA73AAAE2242C1F10089BF1D /* ParselyTestCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParselyTestCase.swift; sourceTree = ""; }; @@ -176,6 +178,7 @@ B205889E220CB72A00476E27 /* RequestBuilderTests.swift */, B2FC40BF221CC43200C70806 /* MetadataTests.swift */, AA73AAAE2242C1F10089BF1D /* ParselyTestCase.swift */, + 3F0B192D2A8DCFBC0012C731 /* ParselyTests.swift */, 3FCAFC8B29E9775A00BC9360 /* UnitTests.xctestplan */, ); name = Tests; @@ -314,6 +317,7 @@ files = ( B2FC40C0221CC43200C70806 /* MetadataTests.swift in Sources */, F410D63121061D7800DB3EBE /* PixelTests.swift in Sources */, + 3F0B192F2A8DD03B0012C731 /* ParselyTests.swift in Sources */, F4BF866E2190DB4A00BD3867 /* VideoTests.swift in Sources */, AA73AAAF2242C1F10089BF1D /* ParselyTestCase.swift in Sources */, F441A55C20F3B8BF009B556E /* EventQueueTests.swift in Sources */, diff --git a/Sources/EngagedTime.swift b/Sources/EngagedTime.swift index c186027..fea01a2 100644 --- a/Sources/EngagedTime.swift +++ b/Sources/EngagedTime.swift @@ -17,17 +17,17 @@ class EngagedTime: Sampler { } let roundedSecs: Int = Int(data.accumulatedTime) let totalMs: Int = Int(data.totalTime.milliseconds()) - let eventArgs = data.eventArgs! + let eventArgs = data.eventArgs let event = Heartbeat( "heartbeat", - url: eventArgs["url"] as! String, + url: eventArgs["url"] as? String ?? "URL_MISSING", urlref: eventArgs["urlref"] as? String, inc: roundedSecs, tt: totalMs, metadata: eventArgs["metadata"] as? ParselyMetadata, extra_data: eventArgs["extra_data"] as? Dictionary, - idsite: (eventArgs["idsite"] as! String) + idsite: eventArgs["idsite"] as? String ) parselyTracker.track.event(event: event) diff --git a/Sources/Event.swift b/Sources/Event.swift index be980de..4534bca 100644 --- a/Sources/Event.swift +++ b/Sources/Event.swift @@ -105,10 +105,28 @@ class Heartbeat: Event { var tt: Int var inc: Int - init(_ action: String, url: String, urlref: String?, inc: Int, tt: Int, metadata: ParselyMetadata?, extra_data: Dictionary?, idsite: String = "") { + init( + _ action: String, + url: String, + urlref: String?, + inc: Int, + tt: Int, + metadata: ParselyMetadata?, + extra_data: Dictionary?, + idsite: String? + ) { self.tt = tt self.inc = inc - super.init(action, url: url, urlref: urlref, metadata: metadata, extra_data: extra_data, idsite: idsite) + + super.init( + action, + url: url, + urlref: urlref, + metadata: metadata, + extra_data: extra_data, + // empty string seems a weird default, but is what we had in the code before this comment was written + idsite: idsite ?? "" + ) } override func toDict() -> Dictionary { diff --git a/Sources/Metadata.swift b/Sources/Metadata.swift index 2fcaa76..bc906ba 100644 --- a/Sources/Metadata.swift +++ b/Sources/Metadata.swift @@ -44,29 +44,29 @@ public class ParselyMetadata { func toDict() -> Dictionary { var metas: Dictionary = [:] - if canonical_url != nil { - metas["link"] = canonical_url! + if let canonical_url { + metas["link"] = canonical_url } - if pub_date != nil { - metas["pub_date"] = String(format:"%i", pub_date!.millisecondsSince1970) + if let pub_date { + metas["pub_date"] = String(format:"%i", pub_date.millisecondsSince1970) } - if title != nil { - metas["title"] = title! + if let title { + metas["title"] = title } - if authors != nil { - metas["authors"] = authors! + if let authors { + metas["authors"] = authors } - if image_url != nil { - metas["image_url"] = image_url! + if let image_url { + metas["image_url"] = image_url } - if section != nil { - metas["section"] = section! + if let section { + metas["section"] = section } - if tags != nil { - metas["tags"] = tags! + if let tags { + metas["tags"] = tags } - if duration != nil { - metas["duration"] = duration! + if let duration { + metas["duration"] = duration } return metas diff --git a/Sources/ParselyTracker.swift b/Sources/ParselyTracker.swift index 9be5ea8..6d84dfb 100644 --- a/Sources/ParselyTracker.swift +++ b/Sources/ParselyTracker.swift @@ -8,10 +8,7 @@ public class Parsely { public var apikey = "" public var secondsBetweenHeartbeats: TimeInterval? { get { - if let secondsBtwnHeartbeats = config["secondsBetweenHeartbeats"] as! TimeInterval? { - return secondsBtwnHeartbeats - } - return nil + config["secondsBetweenHeartbeats"] as? TimeInterval } } public static let sharedInstance = Parsely() @@ -232,7 +229,7 @@ public class Parsely { let events = eventQueue.get() os_log("Got %s events", log: OSLog.tracker, type:.debug, String(describing: events.count)) let request = RequestBuilder.buildRequest(events: events) - HttpClient.sendRequest(request: request!, queue: eventProcessor) { error in + HttpClient.sendRequest(request: request, queue: eventProcessor) { error in if let error = error as? URLError, error.code == .notConnectedToInternet { // When offline, return the events to the queue for the next flush(). self.eventQueue.push(contentsOf: events) diff --git a/Sources/RequestBuilder.swift b/Sources/RequestBuilder.swift index 45c30a7..d77efdd 100644 --- a/Sources/RequestBuilder.swift +++ b/Sources/RequestBuilder.swift @@ -24,25 +24,26 @@ class RequestBuilder { return platform } - internal static func getUserAgent() -> String { - if userAgent == nil { + static func getUserAgent() -> String { + guard let userAgent else { var appDescriptor: String = "" - if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String { - if let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { - appDescriptor = String(format: "%@/%@", appName, appVersion) - } + if let appName = Bundle.main.infoDictionary?["CFBundleName"] as? String, + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String { + appDescriptor = String(format: "%@/%@", appName, appVersion) } let osDescriptor = String(format: "iOS/%@", UIDevice.current.systemVersion) let hardwareString = getHardwareString() let userAgentString = String(format: "%@ %@ (%@)", appDescriptor, osDescriptor, hardwareString) // encode the user agent into latin1 in case there are utf8 characters let userAgentData = Data(userAgentString.utf8) - userAgent = String(data: userAgentData, encoding: .isoLatin1) + + return String(data: userAgentData, encoding: .isoLatin1) ?? "invalid user agent" } - return userAgent! + + return userAgent } - internal static func buildRequest(events: Array) -> ParselyRequest? { + static func buildRequest(events: Array) -> ParselyRequest { let request = ParselyRequest.init( url: buildPixelEndpoint(), headers: buildHeadersDict(events: events), @@ -52,9 +53,10 @@ class RequestBuilder { return request } - internal static func buildPixelEndpoint() -> String { - self._baseURL = "https://p1.parsely.com/mobileproxy" - return self._baseURL! + static func buildPixelEndpoint() -> String { + let endpoint = "https://p1.parsely.com/mobileproxy" + _baseURL = endpoint + return endpoint } internal static func buildHeadersDict(events: Array) -> Dictionary { diff --git a/Sources/Sampler.swift b/Sources/Sampler.swift index 02bcf7f..2eae593 100644 --- a/Sources/Sampler.swift +++ b/Sources/Sampler.swift @@ -17,7 +17,7 @@ struct Accumulator { var heartbeatTimeout: TimeInterval? var contentDuration: TimeInterval? var isEngaged: Bool - var eventArgs: Dictionary? + var eventArgs: Dictionary } extension TimeInterval { @@ -58,7 +58,7 @@ class Sampler { func trackKey( key: String, contentDuration: TimeInterval?, - eventArgs: Dictionary?, + eventArgs: Dictionary = [:], resetOnExisting: Bool = false ) -> Void { os_log("Sampler tracked key: %s in class %@", log: OSLog.tracker, type: .debug, key, String(describing: self)) diff --git a/Sources/Session.swift b/Sources/Session.swift index aac5745..9d160bc 100644 --- a/Sources/Session.swift +++ b/Sources/Session.swift @@ -22,7 +22,10 @@ class SessionManager { if session.isEmpty { var visitorInfo = visitorManager.getVisitorInfo() - visitorInfo["session_count"] = visitorInfo["session_count"] as! Int + 1 + + if let previousCount = visitorInfo["session_count"] as? Int { + visitorInfo["session_count"] = previousCount + 1 + } session = [:] session["session_id"] = visitorInfo["session_count"] diff --git a/Sources/Video.swift b/Sources/Video.swift index c36f242..bde1035 100644 --- a/Sources/Video.swift +++ b/Sources/Video.swift @@ -16,10 +16,7 @@ class VideoManager: Sampler { var trackedVideos: Dictionary = [:] override func sampleFn(key: String) -> Bool { - if trackedVideos[key] == nil { - return false - } - return (trackedVideos[key]?.isPlaying)! + trackedVideos[key]?.isPlaying ?? false } override func heartbeatFn(data: Accumulator, enableHeartbeats: Bool) -> Void { @@ -42,7 +39,7 @@ class VideoManager: Sampler { tt: totalMs, metadata: curVideo.eventArgs["metadata"] as? ParselyMetadata, extra_data: curVideo.eventArgs["extra_data"] as? Dictionary, - idsite: curVideo.eventArgs["idsite"] as! String + idsite: curVideo.eventArgs["idsite"] as? String ) parselyTracker.track.event(event: event) os_log("Sent vheartbeat for video %s", log: OSLog.tracker, type:.debug, data.key) diff --git a/Tests/ParselyTests.swift b/Tests/ParselyTests.swift new file mode 100644 index 0000000..30aad1c --- /dev/null +++ b/Tests/ParselyTests.swift @@ -0,0 +1,37 @@ +import Nimble +@testable import ParselyAnalytics +import XCTest + +class ParselyTests: ParselyTestCase { + + func testSecondsBetweenHeartbeatsNilByDefault() { + XCTAssertNil(makePareslyTracker().secondsBetweenHeartbeats) + } + + func testSecondsBetweenHeartbeatsNilWhenNotInConfigDict() { + let parsely = makePareslyTracker() + parsely.config = ["key": "value"] + + XCTAssertNil(parsely.secondsBetweenHeartbeats) + } + + func testSecondsBetweenHeartbeatsNilWhenInConfigDictButNotTimeInterval() { + let parsely = makePareslyTracker() + parsely.config = ["secondsBetweenHeartbeats": "not seconds"] + + XCTAssertNil(parsely.secondsBetweenHeartbeats) + + // Notice that Int doesn't cast to TimeInterval + let parsely2 = makePareslyTracker() + parsely2.config = ["secondsBetweenHeartbeats": 123] + + XCTAssertNil(parsely2.secondsBetweenHeartbeats) + } + + func testSecondsBetweenHeartbeatsParsesValueFromConfigDict() { + let parsely = makePareslyTracker() + parsely.config = ["secondsBetweenHeartbeats": 123.0] + + XCTAssertEqual(parsely.secondsBetweenHeartbeats, TimeInterval(123)) + } +} diff --git a/Tests/RequestBuilderTests.swift b/Tests/RequestBuilderTests.swift index 3cfdf97..4c0301f 100644 --- a/Tests/RequestBuilderTests.swift +++ b/Tests/RequestBuilderTests.swift @@ -24,18 +24,8 @@ class RequestBuilderTests: XCTestCase { )] } - func testEndpoint() { - let endpoint = RequestBuilder.buildPixelEndpoint() - XCTAssert(endpoint != "", "buildPixelEndpoint should return a non-empty string") - } - func testBuildPixelEndpoint() { - var expected: String = "https://p1.parsely.com/mobileproxy" - var actual = RequestBuilder.buildPixelEndpoint() - XCTAssert(actual == expected, "buildPixelEndpoint should return the correct URL for the given date") - expected = "https://p1.parsely.com/mobileproxy" - actual = RequestBuilder.buildPixelEndpoint() - XCTAssert(actual == expected, "buildPixelEndpoint should return the correct URL for the given date") + XCTAssertEqual(RequestBuilder.buildPixelEndpoint(), "https://p1.parsely.com/mobileproxy") } func testHeaders() { @@ -48,16 +38,15 @@ class RequestBuilderTests: XCTestCase { func testBuildRequest() { let events = makeEvents() let request = RequestBuilder.buildRequest(events: events) - XCTAssertNotNil(request, "buildRequest should return a non-nil value") - XCTAssert(request!.url.contains("https://p1"), + XCTAssert(request.url.contains("https://p1"), "RequestBuilder.buildRequest should return a request with a valid-looking url attribute") - XCTAssertNotNil(request!.headers, + XCTAssertNotNil(request.headers, "RequestBuilder.buildRequest should return a request with a non-nil headers attribute") - XCTAssertNotNil(request!.headers["User-Agent"], + XCTAssertNotNil(request.headers["User-Agent"], "RequestBuilder.buildRequest should return a request with a non-nil User-Agent header") - XCTAssertNotNil(request!.params, + XCTAssertNotNil(request.params, "RequestBuilder.buildRequest should return a request with a non-nil params attribute") - let actualEvents: Array> = request!.params["events"] as! Array> + let actualEvents: Array> = request.params["events"] as! Array> XCTAssertEqual(actualEvents.count, events.count, "RequestBuilder.buildRequest should return a request with an events array containing all " + "relevant revents") @@ -68,7 +57,7 @@ class RequestBuilderTests: XCTestCase { let request = RequestBuilder.buildRequest(events: events) var jsonData: Data? = nil do { - jsonData = try JSONSerialization.data(withJSONObject: request!.params) + jsonData = try JSONSerialization.data(withJSONObject: request.params) } catch { } XCTAssertNotNil(jsonData, "Request params should serialize to JSON") }