diff --git a/AutocompleteClient.xcodeproj/project.pbxproj b/AutocompleteClient.xcodeproj/project.pbxproj index f4f6397f..29b2e24b 100644 --- a/AutocompleteClient.xcodeproj/project.pbxproj +++ b/AutocompleteClient.xcodeproj/project.pbxproj @@ -57,6 +57,11 @@ 08C1E6072193C29C00A2E24E /* TrackPurchaseRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C1E6062193C29C00A2E24E /* TrackPurchaseRequestBuilderTests.swift */; }; 08FFB76C215EBBF8008CAA7D /* CIOTrackAutocompleteSelectData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FFB76B215EBBF8008CAA7D /* CIOTrackAutocompleteSelectData.swift */; }; 08FFB76E215EC1E2008CAA7D /* CIOTrackSearchSubmitData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FFB76D215EC1DF008CAA7D /* CIOTrackSearchSubmitData.swift */; }; + 5FEE547B2F61E12600A74E64 /* CIOTrackMediaImpressionClickData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE54772F61E12600A74E64 /* CIOTrackMediaImpressionClickData.swift */; }; + 5FEE547C2F61E12600A74E64 /* CIOTrackMediaImpressionViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE54782F61E12600A74E64 /* CIOTrackMediaImpressionViewData.swift */; }; + 5FEE54812F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE547D2F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift */; }; + 5FEE54822F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE547E2F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift */; }; + 5FEE54842F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */; }; 651F5B9E3E0C5CC7A2090EC8 /* Pods_AutocompleteClientTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8134D150C9D4CC91DD5715F /* Pods_AutocompleteClientTests.framework */; }; 7D06CCA128E5122B00AB6A8C /* response_quiz_results.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D54FF8528E3C04C00A584E3 /* response_quiz_results.json */; }; 7D06CCA328E516BC00AB6A8C /* CIOQuizResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D06CCA228E516BC00AB6A8C /* CIOQuizResult.swift */; }; @@ -428,6 +433,11 @@ 08FFB76D215EC1DF008CAA7D /* CIOTrackSearchSubmitData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOTrackSearchSubmitData.swift; sourceTree = ""; }; 453FBD0FBF9C8817B960B758 /* Pods-UserApplication.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UserApplication.release.xcconfig"; path = "Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication.release.xcconfig"; sourceTree = ""; }; 5362DE84EA35B4B9FE26EE1F /* Pods_AutocompleteClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AutocompleteClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5FEE54772F61E12600A74E64 /* CIOTrackMediaImpressionClickData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOTrackMediaImpressionClickData.swift; sourceTree = ""; }; + 5FEE54782F61E12600A74E64 /* CIOTrackMediaImpressionViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOTrackMediaImpressionViewData.swift; sourceTree = ""; }; + 5FEE547D2F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackMediaImpressionClickRequestBuilderTests.swift; sourceTree = ""; }; + 5FEE547E2F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackMediaImpressionViewRequestBuilderTests.swift; sourceTree = ""; }; + 5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackMediaImpressionTests.swift; sourceTree = ""; }; 7D06CCA228E516BC00AB6A8C /* CIOQuizResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizResult.swift; sourceTree = ""; }; 7D45383928DC085600490BFE /* CIOQuizQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuery.swift; sourceTree = ""; }; 7D45383D28DC14AE00490BFE /* CIOQuizQuestionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuestionResponse.swift; sourceTree = ""; }; @@ -901,6 +911,8 @@ 088F7D1D210FA3C6005B9FB4 /* CIOTrackInputFocusData.swift */, BFC9503825D742.6.2118D4C /* CIOTrackRecommendationResultClickData.swift */, BFC9503A25D74D3F00118D4C /* CIOTrackRecommendationResultsViewData.swift */, + 5FEE54772F61E12600A74E64 /* CIOTrackMediaImpressionClickData.swift */, + 5FEE54782F61E12600A74E64 /* CIOTrackMediaImpressionViewData.swift */, F6DF7B9320849E8D00A7CDAD /* CIOTrackSearchResultClickData.swift */, F6D2E9CB20E21022007F8761 /* CIOTrackSearchResultsLoadedData.swift */, 08FFB76D215EC1DF008CAA7D /* CIOTrackSearchSubmitData.swift */, @@ -1004,6 +1016,7 @@ F60FE8FF1F5C5ACD0037A0AB /* Worker */ = { isa = PBXGroup; children = ( + 5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */, 7D4E4A3E2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift */, 08A7E4102575FA15000FA02F /* ConstructorIOTrackInputFocusTests.swift */, 8A93F13F2A70491D000ED6B3 /* ConstructorIOTrackQuizResultClick.swift */, @@ -1060,6 +1073,8 @@ F60FE9071F5C5B860037A0AB /* Request */ = { isa = PBXGroup; children = ( + 5FEE547D2F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift */, + 5FEE547E2F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift */, 7D4E4A3C2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift */, 8A93F1452A79B365000ED6B3 /* TrackQuizResultsLoadedRequestBuilder.swift */, 8A93F1492A79C963000ED6B3 /* TrackQuizConversionRequestBuilder.swift */, @@ -2326,13 +2341,16 @@ buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework", ); name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2362,13 +2380,16 @@ buildActionMask = 2147483647; files = ( ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks.sh", - "${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework", ); name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); outputPaths = ( - "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2474,6 +2495,8 @@ F68A72FD1F56D31700FA4D1B /* DefaultSearchItemCell.swift in Sources */, 8D689ECD284EAC6F005B6DD6 /* CIOCollectionData.swift in Sources */, BF12196C25D1FDB300496189 /* CIORecommendationsPod.swift in Sources */, + 5FEE547B2F61E12600A74E64 /* CIOTrackMediaImpressionClickData.swift in Sources */, + 5FEE547C2F61E12600A74E64 /* CIOTrackMediaImpressionViewData.swift in Sources */, 0828954C256C5A85009A00BC /* CIOSearchResponse.swift in Sources */, F67BFC9520B845D300986557 /* UISearchBar+TextField.swift in Sources */, 8D0308742A1ED8AF0084AB85 /* AbstractBrowseFacetOptionsResponseParser.swift in Sources */, @@ -2644,12 +2667,15 @@ F63374B01F5AA1C000E481E7 /* ExpectationHandler.swift in Sources */, F64F46C31F59A63B0094C697 /* ClosureAutocompleteViewModelDelegate.swift in Sources */, F64E94E9212DC93300E50EDE /* ConstructorIOAutocompleteTests.swift in Sources */, + 5FEE54842F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift in Sources */, F64E94E6212D87E200E50EDE /* CIOBuilder.swift in Sources */, F62510D120568F250040E3DF /* SessionManagerTests.swift in Sources */, F66B216721DF87EB00AAB030 /* UIColorRGBConversionTests.swift in Sources */, 0879E93F215F290D00018BBA /* TrackAutocompleteSelectRequestBuilderTests.swift in Sources */, F6D86B17216BAD7F00077388 /* SearchResponseParserTests.swift in Sources */, F6EC29AB2175161300DCFA07 /* ClosureSessionManagerDelegate.swift in Sources */, + 5FEE54812F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift in Sources */, + 5FEE54822F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift in Sources */, F63808FF1F5D6C1000C3B322 /* ConstructorIOTests.swift in Sources */, 0874A32E216BCE2700812CDC /* ConstructorIOABTestCellTests.swift in Sources */, 08A7E4182575FDA0000FA02F /* ConstructorIOTrackBrowseResultClickTests.swift in Sources */, diff --git a/AutocompleteClient/Constants/Constants.swift b/AutocompleteClient/Constants/Constants.swift index 77467386..11d177e3 100644 --- a/AutocompleteClient/Constants/Constants.swift +++ b/AutocompleteClient/Constants/Constants.swift @@ -69,6 +69,7 @@ struct Constants { static let apiKey = "key" static let baseURLString = "https://ac.cnstrc.com" static let baseQuizURLString = "https://quizzes.cnstrc.com" + static let baseMediaURLString = "https://behavior.media-cnstrc.com" static let defaultSegments = ["cio-ios", "cio-app"] static let httpMethod = "GET" static let sessionIncrementTimeoutInSeconds: TimeInterval = 1800 // 30 mins @@ -285,6 +286,14 @@ struct Constants { static let format = "%@/v2/behavioral_action/quiz_conversion" } + struct TrackMediaImpressionView { + static let format = "%@/v2/ad_behavioral_action/display_ad_view" + } + + struct TrackMediaImpressionClick { + static let format = "%@/v2/ad_behavioral_action/display_ad_click" + } + struct Logging { private static let prefix = "[ConstructorIO]:" private static let format: (_ message: String) -> String = { message in return "\(Logging.prefix) \(message)" } diff --git a/AutocompleteClient/FW/Config/ConstructorIOConfig.swift b/AutocompleteClient/FW/Config/ConstructorIOConfig.swift index 822929ba..a2db62a6 100644 --- a/AutocompleteClient/FW/Config/ConstructorIOConfig.swift +++ b/AutocompleteClient/FW/Config/ConstructorIOConfig.swift @@ -52,6 +52,11 @@ public struct ConstructorIOConfig { */ public var baseQuizURL: String? + /** + The base URL for media tracking requests + */ + public var baseMediaURL: String? + /** Create a configuration object @@ -63,6 +68,7 @@ public struct ConstructorIOConfig { - segments: List of segments to associate with requets - baseURL: The base URL to make requests to - baseQuizURL: The base Quiz URL to make requests to + - baseMediaURL: The base Media URL to make tracking requests to ### Usage Example: ### ``` @@ -79,7 +85,7 @@ public struct ConstructorIOConfig { ) ``` */ - public init(apiKey: String, resultCount: AutocompleteResultCount? = nil, defaultItemSectionName: String? = nil, testCells: [CIOABTestCell]? = nil, segments: [String]? = nil, baseURL: String? = nil, baseQuizURL: String? = nil, defaultAnalyticsTags: [String: String]? = nil) { + public init(apiKey: String, resultCount: AutocompleteResultCount? = nil, defaultItemSectionName: String? = nil, testCells: [CIOABTestCell]? = nil, segments: [String]? = nil, baseURL: String? = nil, baseQuizURL: String? = nil, baseMediaURL: String? = nil, defaultAnalyticsTags: [String: String]? = nil) { self.apiKey = apiKey self.resultCount = resultCount self.defaultItemSectionName = defaultItemSectionName @@ -87,6 +93,7 @@ public struct ConstructorIOConfig { self.segments = segments self.baseURL = baseURL self.baseQuizURL = baseQuizURL + self.baseMediaURL = baseMediaURL self.defaultAnalyticsTags = defaultAnalyticsTags } diff --git a/AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift b/AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift index b22aea78..745d2a44 100644 --- a/AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift +++ b/AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift @@ -15,15 +15,17 @@ class RequestBuilder { let baseURL: String let baseQuizURL: String + let baseMediaURL: String var trackData: CIORequestData! var searchTerm = "" - init(apiKey: String, dateProvider: DateProvider = CurrentTimeDateProvider(), baseURL: String? = nil, baseQuizURL: String? = nil) { + init(apiKey: String, dateProvider: DateProvider = CurrentTimeDateProvider(), baseURL: String? = nil, baseQuizURL: String? = nil, baseMediaURL: String? = nil) { self.dateProvider = dateProvider self.baseURL = baseURL ?? Constants.Query.baseURLString self.baseQuizURL = baseQuizURL ?? Constants.Query.baseQuizURLString + self.baseMediaURL = baseMediaURL ?? Constants.Query.baseMediaURLString self.set(apiKey: apiKey) } @@ -114,6 +116,37 @@ class RequestBuilder { return request } + final func getMediaRequest() -> URLRequest { + let urlString = self.trackData!.url(with: self.baseMediaURL) + + var urlComponents = URLComponents(string: urlString)! + + var allQueryItems = self.queryItems + + let versionString = Constants.versionString() + allQueryItems.add(URLQueryItem(name: "c", value: versionString)) + + self.addDateQueryItem(queryItems: &allQueryItems) + + urlComponents.queryItems = self.trackData!.queryItems(baseItems: allQueryItems.all()) + + urlComponents.percentEncodedQuery = urlComponents.percentEncodedQuery? + .replacingOccurrences(of: "+", with: "%2B") + + let url = urlComponents.url! + let httpBody = self.trackData!.httpBody(baseParams: allQueryItems.allAsDictionary()) + + var request = URLRequest(url: url) + if httpBody != nil { + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + request.addValue("application/json", forHTTPHeaderField: "Accept") + request.httpBody = httpBody + } + request.httpMethod = self.trackData!.httpMethod() + + return request + } + final func getQuizRequest(finalize: Bool) -> URLRequest { let quizQuestionURLString = self.trackData!.urlWithFormat(baseURL: self.baseQuizURL, format: Constants.Quiz.Question.format) let quizResultsURLString = self.trackData!.urlWithFormat(baseURL: self.baseQuizURL, format: Constants.Quiz.Results.format) diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift new file mode 100644 index 00000000..1fe6d604 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift @@ -0,0 +1,49 @@ +// +// CIOTrackMediaImpressionClickData.swift +// AutocompleteClient +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import Foundation + +/** + Struct encapsulating the parameters that must/can be set in order to track a media impression click + */ +struct CIOTrackMediaImpressionClickData: CIORequestData { + + let bannerAdId: String + let placementId: String + + func url(with baseURL: String) -> String { + return String(format: Constants.TrackMediaImpressionClick.format, baseURL) + } + + func urlWithFormat(baseURL: String, format: String) -> String { + return String(format: format, baseURL) + } + + init(bannerAdId: String, placementId: String) { + self.bannerAdId = bannerAdId + self.placementId = placementId + } + + func decorateRequest(requestBuilder: RequestBuilder) {} + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "banner_ad_id": self.bannerAdId, + "placement_id": self.placementId + ] as [String: Any] + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift new file mode 100644 index 00000000..8c30216c --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift @@ -0,0 +1,49 @@ +// +// CIOTrackMediaImpressionViewData.swift +// AutocompleteClient +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import Foundation + +/** + Struct encapsulating the parameters that must/can be set in order to track a media impression view + */ +struct CIOTrackMediaImpressionViewData: CIORequestData { + + let bannerAdId: String + let placementId: String + + func url(with baseURL: String) -> String { + return String(format: Constants.TrackMediaImpressionView.format, baseURL) + } + + func urlWithFormat(baseURL: String, format: String) -> String { + return String(format: format, baseURL) + } + + init(bannerAdId: String, placementId: String) { + self.bannerAdId = bannerAdId + self.placementId = placementId + } + + func decorateRequest(requestBuilder: RequestBuilder) {} + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "banner_ad_id": self.bannerAdId, + "placement_id": self.placementId + ] as [String: Any] + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift index 9cd330d8..0ffb6d15 100644 --- a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift +++ b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift @@ -341,6 +341,44 @@ public class ConstructorIO: CIOSessionManagerDelegate { executeTracking(request, completionHandler: completionHandler) } + /** + Track when a user views a media impression (display ad) + + - Parameters: + - bannerAdId: The banner ad ID + - placementId: The placement ID + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackMediaImpressionView(bannerAdId: "abc123", placementId: "home") + ``` + */ + public func trackMediaImpressionView(bannerAdId: String, placementId: String, completionHandler: TrackingCompletionHandler? = nil) { + let data = CIOTrackMediaImpressionViewData(bannerAdId: bannerAdId, placementId: placementId) + let request = self.buildMediaRequest(data: data) + executeTracking(request, completionHandler: completionHandler) + } + + /** + Track when a user clicks a media impression (display ad) + + - Parameters: + - bannerAdId: The banner ad ID + - placementId: The placement ID + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackMediaImpressionClick(bannerAdId: "abc123", placementId: "home") + ``` + */ + public func trackMediaImpressionClick(bannerAdId: String, placementId: String, completionHandler: TrackingCompletionHandler? = nil) { + let data = CIOTrackMediaImpressionClickData(bannerAdId: bannerAdId, placementId: placementId) + let request = self.buildMediaRequest(data: data) + executeTracking(request, completionHandler: completionHandler) + } + /** Track when a user selects (clicks, or navigates to via keyboard) a result that appears within autocomplete @@ -816,6 +854,17 @@ public class ConstructorIO: CIOSessionManagerDelegate { return requestBuilder.getRequest() } + private func buildMediaRequest(data: CIORequestData) -> URLRequest { + let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString) + self.attachClientID(requestBuilder: requestBuilder) + self.attachUserID(requestBuilder: requestBuilder) + self.attachSessionIDWithIncrement(requestBuilder: requestBuilder) + self.attachABTestCells(requestBuilder: requestBuilder) + self.attachSegments(requestBuilder: requestBuilder) + requestBuilder.build(trackData: data) + return requestBuilder.getMediaRequest() + } + private func buildQuizRequest(data: CIORequestData, finalize: Bool) -> URLRequest { let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseQuizURL: self.config.baseQuizURL ?? Constants.Query.baseQuizURLString) self.attachClientID(requestBuilder: requestBuilder) diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift new file mode 100644 index 00000000..cf9f21bf --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift @@ -0,0 +1,54 @@ +// +// TrackMediaImpressionClickRequestBuilderTests.swift +// AutocompleteClientTests +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackMediaImpressionClickRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "testKey123213" + fileprivate let bannerAdId = "banner-ad-123" + fileprivate let placementId = "home" + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseMediaURL: Constants.Query.baseMediaURLString) + } + + func testTrackMediaImpressionClickBuilder() { + let tracker: CIORequestData = CIOTrackMediaImpressionClickData(bannerAdId: bannerAdId, placementId: placementId) + builder.build(trackData: tracker) + let request = builder.getMediaRequest() + let url = request.url!.absoluteString + let payload = try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(url.hasPrefix("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_click?")) + XCTAssertTrue(url.contains("c=cioios-"), "URL should contain the version string.") + XCTAssertTrue(url.contains("key=\(testACKey)"), "URL should contain the api key.") + XCTAssertEqual(payload?["banner_ad_id"] as? String, bannerAdId) + XCTAssertEqual(payload?["placement_id"] as? String, placementId) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackMediaImpressionClickBuilder_WithCustomBaseURL() { + let tracker: CIORequestData = CIOTrackMediaImpressionClickData(bannerAdId: bannerAdId, placementId: placementId) + let customBaseURL = "https://custom-media-url.com" + self.builder = RequestBuilder(apiKey: testACKey, baseMediaURL: customBaseURL) + builder.build(trackData: tracker) + let request = builder.getMediaRequest() + let url = request.url!.absoluteString + let payload = try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + + XCTAssertTrue(url.hasPrefix(customBaseURL)) + XCTAssertEqual(payload?["banner_ad_id"] as? String, bannerAdId) + XCTAssertEqual(payload?["placement_id"] as? String, placementId) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionViewRequestBuilderTests.swift new file mode 100644 index 00000000..3415392b --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionViewRequestBuilderTests.swift @@ -0,0 +1,54 @@ +// +// TrackMediaImpressionViewRequestBuilderTests.swift +// AutocompleteClientTests +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackMediaImpressionViewRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "testKey123213" + fileprivate let bannerAdId = "banner-ad-123" + fileprivate let placementId = "home" + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseMediaURL: Constants.Query.baseMediaURLString) + } + + func testTrackMediaImpressionViewBuilder() { + let tracker = CIOTrackMediaImpressionViewData(bannerAdId: bannerAdId, placementId: placementId) + builder.build(trackData: tracker) + let request = builder.getMediaRequest() + let url = request.url!.absoluteString + let payload = try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(url.hasPrefix("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_view?")) + XCTAssertTrue(url.contains("c=cioios-"), "URL should contain the version string.") + XCTAssertTrue(url.contains("key=\(testACKey)"), "URL should contain the api key.") + XCTAssertEqual(payload?["banner_ad_id"] as? String, bannerAdId) + XCTAssertEqual(payload?["placement_id"] as? String, placementId) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackMediaImpressionViewBuilder_WithCustomBaseURL() { + let tracker = CIOTrackMediaImpressionViewData(bannerAdId: bannerAdId, placementId: placementId) + let customBaseURL = "https://custom-media-url.com" + self.builder = RequestBuilder(apiKey: testACKey, baseMediaURL: customBaseURL) + builder.build(trackData: tracker) + let request = builder.getMediaRequest() + let url = request.url!.absoluteString + let payload = try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + + XCTAssertTrue(url.hasPrefix(customBaseURL)) + XCTAssertEqual(payload?["banner_ad_id"] as? String, bannerAdId) + XCTAssertEqual(payload?["placement_id"] as? String, placementId) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift new file mode 100644 index 00000000..96c81adc --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift @@ -0,0 +1,91 @@ +// +// ConstructorIOTrackMediaImpressionTests.swift +// AutocompleteClientTests +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackMediaImpressionTests: XCTestCase { + + private let bannerAdId = "banner-ad-123" + private let placementId = TestConstants.testPlacementId + + var constructor: ConstructorIO! + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackMediaImpressionView() { + let builder = CIOBuilder(expectation: "Calling trackMediaImpressionView should send a valid request.", builder: http(200)) + stub(regex("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), builder.create()) + self.constructor.trackMediaImpressionView(bannerAdId: bannerAdId, placementId: placementId) + self.wait(for: builder.expectation) + } + + func testTrackMediaImpressionView_With400() { + let expectation = self.expectation(description: "Calling trackMediaImpressionView with 400 should return badRequest CIOError.") + stub(regex("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackMediaImpressionView(bannerAdId: bannerAdId, placementId: placementId, completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest) + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackMediaImpressionView_With500() { + let expectation = self.expectation(description: "Calling trackMediaImpressionView with 500 should return internalServerError CIOError.") + stub(regex("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackMediaImpressionView(bannerAdId: bannerAdId, placementId: placementId, completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError) + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackMediaImpressionClick() { + let builder = CIOBuilder(expectation: "Calling trackMediaImpressionClick should send a valid request.", builder: http(200)) + stub(regex("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), builder.create()) + self.constructor.trackMediaImpressionClick(bannerAdId: bannerAdId, placementId: placementId) + self.wait(for: builder.expectation) + } + + func testTrackMediaImpressionClick_With400() { + let expectation = self.expectation(description: "Calling trackMediaImpressionClick with 400 should return badRequest CIOError.") + stub(regex("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackMediaImpressionClick(bannerAdId: bannerAdId, placementId: placementId, completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest) + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackMediaImpressionClick_With500() { + let expectation = self.expectation(description: "Calling trackMediaImpressionClick with 500 should return internalServerError CIOError.") + stub(regex("https://behavior.media-cnstrc.com/v2/ad_behavioral_action/display_ad_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackMediaImpressionClick(bannerAdId: bannerAdId, placementId: placementId, completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError) + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/Test Utils/Constants/TestConstants.swift b/AutocompleteClientTests/Test Utils/Constants/TestConstants.swift index 2bfa0d52..2f997ba6 100644 --- a/AutocompleteClientTests/Test Utils/Constants/TestConstants.swift +++ b/AutocompleteClientTests/Test Utils/Constants/TestConstants.swift @@ -21,6 +21,8 @@ struct TestConstants { static let testApiKey = "key_OucJxxrfiTVUQx0C" static let testConfig = ConstructorIOConfig(apiKey: testApiKey) static let defaultSegments = "us=cio-app&us=cio-ios" + static let testApiKeyWithAdPlacements = "key_x6UnCVRZaJgIHFQD" + static let testPlacementId = "home" static func testConstructor(_ config: ConstructorIOConfig = TestConstants.testConfig) -> ConstructorIO { let constructor = ConstructorIO(config: config) diff --git a/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 00000000..f8c6f9ff --- /dev/null +++ b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks.sh +${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework diff --git a/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 00000000..6dd0f052 --- /dev/null +++ b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework diff --git a/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-input-files.xcfilelist new file mode 100644 index 00000000..f8c6f9ff --- /dev/null +++ b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks.sh +${BUILT_PRODUCTS_DIR}/OHHTTPStubs/OHHTTPStubs.framework diff --git a/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-output-files.xcfilelist new file mode 100644 index 00000000..6dd0f052 --- /dev/null +++ b/Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OHHTTPStubs.framework diff --git a/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-input-files.xcfilelist b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-input-files.xcfilelist new file mode 100644 index 00000000..880b368f --- /dev/null +++ b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks.sh +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework diff --git a/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-output-files.xcfilelist b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-output-files.xcfilelist new file mode 100644 index 00000000..453f4668 --- /dev/null +++ b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework diff --git a/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-input-files.xcfilelist b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-input-files.xcfilelist new file mode 100644 index 00000000..880b368f --- /dev/null +++ b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-input-files.xcfilelist @@ -0,0 +1,2 @@ +${PODS_ROOT}/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks.sh +${BUILT_PRODUCTS_DIR}/Kingfisher/Kingfisher.framework diff --git a/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-output-files.xcfilelist b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-output-files.xcfilelist new file mode 100644 index 00000000..453f4668 --- /dev/null +++ b/Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-output-files.xcfilelist @@ -0,0 +1 @@ +${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/Kingfisher.framework diff --git a/README.md b/README.md index a111d238..126ba1c3 100644 --- a/README.md +++ b/README.md @@ -332,6 +332,16 @@ ConstructorIo.trackQuizConversion(quizID: "coffee-quiz", quizVersionID: "1231244 ``` +### Media Impression Events + +```swift +// Track when a media impression is viewed +ConstructorIo.trackMediaImpressionView(bannerAdId: "banner-ad-id", placementId: "placement-id") + +// Track when a media impression is clicked +ConstructorIo.trackMediaImpressionClick(bannerAdId: "banner-ad-id", placementId: "placement-id") +``` + ### Conversion Events ```swift