Skip to content
Open
38 changes: 32 additions & 6 deletions AutocompleteClient.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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 */; };
Expand Down Expand Up @@ -428,6 +433,11 @@
08FFB76D215EC1DF008CAA7D /* CIOTrackSearchSubmitData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOTrackSearchSubmitData.swift; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
5FEE54782F61E12600A74E64 /* CIOTrackMediaImpressionViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOTrackMediaImpressionViewData.swift; sourceTree = "<group>"; };
5FEE547D2F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackMediaImpressionClickRequestBuilderTests.swift; sourceTree = "<group>"; };
5FEE547E2F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackMediaImpressionViewRequestBuilderTests.swift; sourceTree = "<group>"; };
5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackMediaImpressionTests.swift; sourceTree = "<group>"; };
7D06CCA228E516BC00AB6A8C /* CIOQuizResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizResult.swift; sourceTree = "<group>"; };
7D45383928DC085600490BFE /* CIOQuizQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuery.swift; sourceTree = "<group>"; };
7D45383D28DC14AE00490BFE /* CIOQuizQuestionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuestionResponse.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -1004,6 +1016,7 @@
F60FE8FF1F5C5ACD0037A0AB /* Worker */ = {
isa = PBXGroup;
children = (
5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */,
7D4E4A3E2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift */,
08A7E4102575FA15000FA02F /* ConstructorIOTrackInputFocusTests.swift */,
8A93F13F2A70491D000ED6B3 /* ConstructorIOTrackQuizResultClick.swift */,
Expand Down Expand Up @@ -1060,6 +1073,8 @@
F60FE9071F5C5B860037A0AB /* Request */ = {
isa = PBXGroup;
children = (
5FEE547D2F61E25E00A74E64 /* TrackMediaImpressionClickRequestBuilderTests.swift */,
5FEE547E2F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift */,
7D4E4A3C2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift */,
8A93F1452A79B365000ED6B3 /* TrackQuizResultsLoadedRequestBuilder.swift */,
8A93F1492A79C963000ED6B3 /* TrackQuizConversionRequestBuilder.swift */,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 */,
Expand Down Expand Up @@ -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 */,
Expand Down
9 changes: 9 additions & 0 deletions AutocompleteClient/Constants/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)" }
Expand Down
9 changes: 8 additions & 1 deletion AutocompleteClient/FW/Config/ConstructorIOConfig.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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: ###
```
Expand All @@ -79,14 +85,15 @@ 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
self.testCells = testCells
self.segments = segments
self.baseURL = baseURL
self.baseQuizURL = baseQuizURL
self.baseMediaURL = baseMediaURL
self.defaultAnalyticsTags = defaultAnalyticsTags
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
49 changes: 49 additions & 0 deletions AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +357 to +377
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public API naming: new methods use bannerAdId / placementId, but the existing public API consistently uses ...ID (e.g., quizID, resultID, itemID). Consider renaming these parameters to bannerAdID / placementID (and aligning related types/docs/tests) now to keep the API consistent before release.

Copilot uses AI. Check for mistakes.
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

Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading