From 626b9e70c9ac70049b8923aa651fca5cbd941c02 Mon Sep 17 00:00:00 2001 From: Anthony Oliveri Date: Tue, 6 Dec 2016 15:26:18 -0600 Subject: [PATCH] Added new type for recording network request metadata Moved this type from BMSCore to BMSAnalyticsAPI since it is heavily oriented towards analytics --- BMSAnalyticsAPI.podspec | 2 +- BMSAnalyticsAPI.xcodeproj/project.pbxproj | 10 ++ Source/RequestMetadata.swift | 194 +++++++++++++++++++++ Tests/RequestMetadataTests.swift | 196 ++++++++++++++++++++++ 4 files changed, 401 insertions(+), 1 deletion(-) create mode 100644 Source/RequestMetadata.swift create mode 100644 Tests/RequestMetadataTests.swift diff --git a/BMSAnalyticsAPI.podspec b/BMSAnalyticsAPI.podspec index 36e9e63..586c6c1 100644 --- a/BMSAnalyticsAPI.podspec +++ b/BMSAnalyticsAPI.podspec @@ -1,7 +1,7 @@ Pod::Spec.new do |s| s.name = 'BMSAnalyticsAPI' - s.version = '2.1.2' + s.version = '2.2.0' s.summary = 'The analytics and logger APIs of the Swift client SDK for IBM Bluemix Mobile Services' s.homepage = 'https://github.com/ibm-bluemix-mobile-services/bms-clientsdk-swift-analytics-api' s.license = 'Apache License, Version 2.0' diff --git a/BMSAnalyticsAPI.xcodeproj/project.pbxproj b/BMSAnalyticsAPI.xcodeproj/project.pbxproj index e7ed577..8c3315f 100644 --- a/BMSAnalyticsAPI.xcodeproj/project.pbxproj +++ b/BMSAnalyticsAPI.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 920D57BE1DF75F4700923C21 /* RequestMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920D57BD1DF75F4700923C21 /* RequestMetadata.swift */; }; + 920D57BF1DF75F4700923C21 /* RequestMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920D57BD1DF75F4700923C21 /* RequestMetadata.swift */; }; + 920D57C31DF75F7F00923C21 /* RequestMetadataTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 920D57C01DF75F5D00923C21 /* RequestMetadataTests.swift */; }; 922CD67B1CA08E00002E29C0 /* BMSAnalyticsAPI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 922CD6701CA08E00002E29C0 /* BMSAnalyticsAPI.framework */; }; 922CD6BF1CA09837002E29C0 /* Logger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922CD6BE1CA09837002E29C0 /* Logger.swift */; }; 922CD6C51CA0AA54002E29C0 /* Analytics.swift in Sources */ = {isa = PBXBuildFile; fileRef = 922CD6C41CA0AA54002E29C0 /* Analytics.swift */; }; @@ -28,6 +31,8 @@ /* End PBXContainerItemProxy section */ /* Begin PBXFileReference section */ + 920D57BD1DF75F4700923C21 /* RequestMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestMetadata.swift; sourceTree = ""; }; + 920D57C01DF75F5D00923C21 /* RequestMetadataTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RequestMetadataTests.swift; sourceTree = ""; }; 922CD6701CA08E00002E29C0 /* BMSAnalyticsAPI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = BMSAnalyticsAPI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 922CD67A1CA08E00002E29C0 /* BMSAnalyticsAPI Tests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "BMSAnalyticsAPI Tests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 922CD6971CA08F03002E29C0 /* BMSAnalyticsAPI.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = BMSAnalyticsAPI.h; sourceTree = ""; }; @@ -90,6 +95,7 @@ children = ( 922CD6BE1CA09837002E29C0 /* Logger.swift */, 922CD6C41CA0AA54002E29C0 /* Analytics.swift */, + 920D57BD1DF75F4700923C21 /* RequestMetadata.swift */, 922CD6961CA08F03002E29C0 /* Resources */, ); path = Source; @@ -109,6 +115,7 @@ isa = PBXGroup; children = ( 92BE7D671CD7E22500DC9FA4 /* LoggerTests.swift */, + 920D57C01DF75F5D00923C21 /* RequestMetadataTests.swift */, 92BE7D681CD7E22500DC9FA4 /* Resources */, ); path = Tests; @@ -270,6 +277,7 @@ files = ( 922CD6BF1CA09837002E29C0 /* Logger.swift in Sources */, 922CD6C51CA0AA54002E29C0 /* Analytics.swift in Sources */, + 920D57BE1DF75F4700923C21 /* RequestMetadata.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -277,6 +285,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 920D57C31DF75F7F00923C21 /* RequestMetadataTests.swift in Sources */, 92BE7D6C1CD7E22B00DC9FA4 /* LoggerTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -287,6 +296,7 @@ files = ( 922CD6C61CA0AA54002E29C0 /* Analytics.swift in Sources */, 922CD6C71CA0AA57002E29C0 /* Logger.swift in Sources */, + 920D57BF1DF75F4700923C21 /* RequestMetadata.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Source/RequestMetadata.swift b/Source/RequestMetadata.swift new file mode 100644 index 0000000..5d16a21 --- /dev/null +++ b/Source/RequestMetadata.swift @@ -0,0 +1,194 @@ +/* +*     Copyright 2016 IBM Corp. +*     Licensed under the Apache License, Version 2.0 (the "License"); +*     you may not use this file except in compliance with the License. +*     You may obtain a copy of the License at +*     http://www.apache.org/licenses/LICENSE-2.0 +*     Unless required by applicable law or agreed to in writing, software +*     distributed under the License is distributed on an "AS IS" BASIS, +*     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*     See the License for the specific language governing permissions and +*     limitations under the License. +*/ + + + +// MARK: - Swift 3 + +#if swift(>=3.0) + + + +/* + Contains all of the metadata for one network request made via the `Request` or `BMSURLSession` APIs in BMSCore. + Once the response is received and all of the metadata has been gathered, the metadata can be logged with Analytics. + + Note: This is not part of the API documentation because it is only meant to be used by BMSCore. +*/ +public struct RequestMetadata { + + + // The URL of the resource that the request is being sent to. + public var url: URL? + + // The time at which the request is considered to have started. + public let startTime: Int64 + + // Allows Analytics to track each network request and its associated metadata. + public let trackingId: String + + // The response received. + public var response: URLResponse? = nil + + // The time at which the request is considered complete. + public var endTime: Int64 = 0 + + // Amount of data sent. + public var bytesSent: Int64 = 0 + + // Amount of data received in the response. + public var bytesReceived: Int64 = 0 + + // Combines all of the metadata into a single JSON object + public var combinedMetadata: [String: Any] { + + var roundTripTime = 0 + // If this is not true, that means some BMSCore developer forgot to set the endTime somewhere + if endTime > startTime { + roundTripTime = endTime - startTime + } + + // Data for analytics logging + // NSNumber is used because, for some reason, JSONSerialization fails to convert Int64 to JSON + var responseMetadata: [String: Any] = [:] + responseMetadata["$category"] = "network" + responseMetadata["$trackingid"] = trackingId + responseMetadata["$outboundTimestamp"] = NSNumber(value: startTime) + responseMetadata["$inboundTimestamp"] = NSNumber(value: endTime) + responseMetadata["$roundTripTime"] = NSNumber(value: roundTripTime) + responseMetadata["$bytesSent"] = NSNumber(value: bytesSent) + responseMetadata["$bytesReceived"] = NSNumber(value: bytesReceived) + + if let urlString = url?.absoluteString { + responseMetadata["$path"] = urlString + } + + if let httpResponse = response as? HTTPURLResponse { + responseMetadata["$responseCode"] = httpResponse.statusCode + } + + return responseMetadata + } + + + + public init(url: URL?, startTime: Int64, trackingId: String) { + self.url = url + self.startTime = startTime + self.trackingId = trackingId + } + + + // Use analytics to record the request metadata + public func recordMetadata() { + + Analytics.log(metadata: combinedMetadata) + } +} + + + + + +/**************************************************************************************************/ + + + + + +// MARK: - Swift 2 + +#else + + + +/* + Contains all of the metadata for one network request made via the `Request` or `BMSURLSession` APIs in BMSCore. + Once the response is received and all of the metadata has been gathered, the metadata can be logged with Analytics. + + Note: This is not part of the API documentation because it is only meant to be used by BMSCore. +*/ +public struct RequestMetadata { + + + // The URL of the resource that the request is being sent to. + public var url: NSURL? + + // The time at which the request is considered to have started. + public let startTime: Int64 + + // Allows Analytics to track each network request and its associated metadata. + public let trackingId: String + + // The response received. + public var response: NSURLResponse? = nil + + // The time at which the request is considered complete. + public var endTime: Int64 = 0 + + // Amount of data sent. + public var bytesSent: Int64 = 0 + + // Amount of data received in the response. + public var bytesReceived: Int64 = 0 + + // Combines all of the metadata into a single JSON object + public var combinedMetadata: [String: AnyObject] { + + var roundTripTime = 0 + // If this is not true, that means some BMSCore developer forgot to set the endTime somewhere + if endTime > startTime { + roundTripTime = endTime - startTime + } + + // Data for analytics logging + // NSNumber is used because, for some reason, JSONSerialization fails to convert Int64 to JSON + var responseMetadata: [String: AnyObject] = [:] + responseMetadata["$category"] = "network" + responseMetadata["$trackingid"] = trackingId + responseMetadata["$outboundTimestamp"] = NSNumber(longLong: startTime) + responseMetadata["$inboundTimestamp"] = NSNumber(longLong: endTime) + responseMetadata["$roundTripTime"] = NSNumber(integer: roundTripTime) + responseMetadata["$bytesSent"] = NSNumber(longLong: bytesSent) + responseMetadata["$bytesReceived"] = NSNumber(longLong: bytesReceived) + + if let urlString = url?.absoluteString { + responseMetadata["$path"] = urlString + } + + if let httpResponse = response as? NSHTTPURLResponse { + responseMetadata["$responseCode"] = httpResponse.statusCode + } + + return responseMetadata + } + + + + public init(url: NSURL?, startTime: Int64, trackingId: String) { + self.url = url + self.startTime = startTime + self.trackingId = trackingId + } + + + // Use analytics to record the request metadata + public func recordMetadata() { + + Analytics.log(metadata: combinedMetadata) + } +} + + + +#endif diff --git a/Tests/RequestMetadataTests.swift b/Tests/RequestMetadataTests.swift new file mode 100644 index 0000000..89c9827 --- /dev/null +++ b/Tests/RequestMetadataTests.swift @@ -0,0 +1,196 @@ +/* +*     Copyright 2016 IBM Corp. +*     Licensed under the Apache License, Version 2.0 (the "License"); +*     you may not use this file except in compliance with the License. +*     You may obtain a copy of the License at +*     http://www.apache.org/licenses/LICENSE-2.0 +*     Unless required by applicable law or agreed to in writing, software +*     distributed under the License is distributed on an "AS IS" BASIS, +*     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +*     See the License for the specific language governing permissions and +*     limitations under the License. +*/ + +import XCTest +@testable import BMSAnalyticsAPI + + + +// MARK: - Swift 3 + +#if swift(>=3.0) + + + +class RequestMetadataTests: XCTestCase { + + + func testCombinedMetadata() { + + let url = URL(string:"http://example.com")! + let startTime = Int64(Date.timeIntervalSinceReferenceDate * 1000) + let trackingId = "1234" + let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: nil) + let bytesSent = Int64(555) + let bytesReceived = Int64(666) + + var requestMetadata = RequestMetadata(url: url, startTime: startTime, trackingId: trackingId) + requestMetadata.response = response + requestMetadata.bytesSent = bytesSent + requestMetadata.bytesReceived = bytesReceived + + let expectation = self.expectation(description: "Should receive request metadata.") + + // Need to wait so that the endTime and roundtripTime are larger than startTime + DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 5000000) , execute: { + + requestMetadata.endTime = Int64(Date.timeIntervalSinceReferenceDate * 1000) + + let combinedMetadata: [String: Any] = requestMetadata.combinedMetadata + let endTime = combinedMetadata["$inboundTimestamp"] as! Int + + XCTAssertEqual(combinedMetadata["$category"] as! String, "network") + XCTAssertEqual(combinedMetadata["$trackingid"] as! String, trackingId) + XCTAssertEqual(combinedMetadata["$outboundTimestamp"] as! Int, Int(startTime)) + XCTAssertGreaterThan(endTime, Int(startTime)) + XCTAssertEqual(combinedMetadata["$roundTripTime"] as! Int, endTime - Int(startTime)) + XCTAssertEqual(combinedMetadata["$bytesSent"] as! Int, Int(bytesSent)) + XCTAssertEqual(combinedMetadata["$bytesReceived"] as! Int, Int(bytesReceived)) + XCTAssertEqual(combinedMetadata["$path"] as! String, url.absoluteString) + XCTAssertEqual((combinedMetadata["$responseCode"] as! Int), response?.statusCode) + + expectation.fulfill() + }) + + self.waitForExpectations(timeout: 0.1, handler: nil) + } + + + func testCombinedMetadataWithNilValues() { + + let startTime = Int64(Date.timeIntervalSinceReferenceDate * 1000) + let trackingId = "1234" + + let requestMetadata = RequestMetadata(url: nil, startTime: startTime, trackingId: trackingId) + + let expectation = self.expectation(description: "Should receive request metadata.") + + // Need to wait so that the endTime and roundtripTime are larger than startTime + DispatchQueue.main.asyncAfter(deadline: DispatchTime(uptimeNanoseconds: 5000000) , execute: { + + let combinedMetadata: [String: Any] = requestMetadata.combinedMetadata + + XCTAssertEqual(combinedMetadata["$category"] as! String, "network") + XCTAssertEqual(combinedMetadata["$trackingid"] as! String, trackingId) + XCTAssertEqual(combinedMetadata["$outboundTimestamp"] as! Int, Int(startTime)) + XCTAssertEqual(combinedMetadata["$inboundTimestamp"] as! Int, 0) + XCTAssertEqual(combinedMetadata["$roundTripTime"] as! Int, 0) + XCTAssertEqual(combinedMetadata["$bytesSent"] as! Int, 0) + XCTAssertEqual(combinedMetadata["$bytesReceived"] as! Int, 0) + XCTAssertNil(combinedMetadata["$path"]) + XCTAssertNil(combinedMetadata["$responseCode"]) + + expectation.fulfill() + }) + + self.waitForExpectations(timeout: 0.1, handler: nil) + } + +} + + + + + +/**************************************************************************************************/ + + + + + +// MARK: - Swift 2 + +#else + + + +class RequestMetadataTests: XCTestCase { + + + func testCombinedMetadata() { + + let url = NSURL(string:"http://example.com")! + let startTime = Int64(NSDate.timeIntervalSinceReferenceDate() * 1000) + let trackingId = "1234" + let response = NSHTTPURLResponse(URL: url, statusCode: 200, HTTPVersion: nil, headerFields: nil) + let bytesSent = Int64(555) + let bytesReceived = Int64(666) + + var requestMetadata = RequestMetadata(url: url, startTime: startTime, trackingId: trackingId) + requestMetadata.response = response + requestMetadata.bytesSent = bytesSent + requestMetadata.bytesReceived = bytesReceived + + let expectation = self.expectationWithDescription("Should receive request metadata.") + + // Need to wait so that the endTime and roundtripTime are larger than startTime + let timeDelay = dispatch_time(DISPATCH_TIME_NOW, 5000000) // 5 milliseconds + dispatch_after(timeDelay, dispatch_get_main_queue()) { () -> Void in + + requestMetadata.endTime = Int64(NSDate.timeIntervalSinceReferenceDate() * 1000) + + let combinedMetadata: [String: AnyObject] = requestMetadata.combinedMetadata + let endTime = combinedMetadata["$inboundTimestamp"] as! Int + + XCTAssertEqual(combinedMetadata["$category"] as? String, "network") + XCTAssertEqual(combinedMetadata["$trackingid"] as? String, trackingId) + XCTAssertEqual(combinedMetadata["$outboundTimestamp"] as? Int, Int(startTime)) + XCTAssertGreaterThan(endTime, Int(startTime)) + XCTAssertEqual(combinedMetadata["$roundTripTime"] as? Int, endTime - Int(startTime)) + XCTAssertEqual(combinedMetadata["$bytesSent"] as? Int, Int(bytesSent)) + XCTAssertEqual(combinedMetadata["$bytesReceived"] as? Int, Int(bytesReceived)) + XCTAssertEqual(combinedMetadata["$path"] as? String, url.absoluteString) + XCTAssertEqual((combinedMetadata["$responseCode"] as! Int), response?.statusCode) + + expectation.fulfill() + } + + self.waitForExpectationsWithTimeout(0.1, handler: nil) + } + + + func testCombinedMetadataWithNilValues() { + + let startTime = Int64(NSDate.timeIntervalSinceReferenceDate() * 1000) + let trackingId = "1234" + + let requestMetadata = RequestMetadata(url: nil, startTime: startTime, trackingId: trackingId) + + let expectation = self.expectationWithDescription("Should receive request metadata.") + + // Need to wait so that the endTime and roundtripTime are larger than startTime + let timeDelay = dispatch_time(DISPATCH_TIME_NOW, 5000000) // 5 milliseconds + dispatch_after(timeDelay, dispatch_get_main_queue()) { () -> Void in + + let combinedMetadata: [String: AnyObject] = requestMetadata.combinedMetadata + + XCTAssertEqual(combinedMetadata["$category"] as? String, "network") + XCTAssertEqual(combinedMetadata["$trackingid"] as? String, trackingId) + XCTAssertEqual(combinedMetadata["$outboundTimestamp"] as? Int, Int(startTime)) + XCTAssertEqual(combinedMetadata["$inboundTimestamp"] as? Int, 0) + XCTAssertEqual(combinedMetadata["$roundTripTime"] as? Int, 0) + XCTAssertEqual(combinedMetadata["$bytesSent"] as? Int, 0) + XCTAssertEqual(combinedMetadata["$bytesReceived"] as? Int, 0) + XCTAssertNil(combinedMetadata["$path"]) + XCTAssertNil(combinedMetadata["$responseCode"]) + + expectation.fulfill() + } + + self.waitForExpectationsWithTimeout(0.1, handler: nil) + } +} + + + +#endif