Skip to content

[REM-3041] Display ads tracking#265

Open
ZSnake wants to merge 10 commits intomasterfrom
REM-3041/display-ads-tracking
Open

[REM-3041] Display ads tracking#265
ZSnake wants to merge 10 commits intomasterfrom
REM-3041/display-ads-tracking

Conversation

@ZSnake
Copy link

@ZSnake ZSnake commented Mar 12, 2026

Add functions to emit tracking events for display ads media impressions view and click

Copilot AI review requested due to automatic review settings March 12, 2026 15:05
@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds tracking support for display ad media impressions (view and click events) to the Swift SDK. It introduces two new data structs, a new baseMediaURL configuration option, a new getMediaRequest() method on RequestBuilder, and two new public methods on ConstructorIO. The implementation is well-structured and follows the existing patterns in the codebase. However, I have identified a few issues worth addressing before merging.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift L13] and [File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift L13] — Internal visibility vs. public docs

Both new data structs are declared as struct (internal by default). This is consistent with other CIORequestData conformers in the codebase. However, .jazzy.yaml adds them under a "Tracking Data Structures" public docs category. Since they are internal types not exposed to consumers, it may be worth either making them public intentionally, or removing them from the Jazzy documentation configuration to avoid confusion.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift L23-25] and [File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift L23-25] — Unused urlWithFormat override

Both structs implement urlWithFormat(baseURL:format:) from the CIORequestData protocol, but this method is only exercised by the quiz flow (getQuizRequest). The getMediaRequest() call path uses only url(with:). The CIORequestData extension already provides a default no-op implementation (returning an empty string), so this override adds no value and should be removed.


[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift L858] — Redundant nil-coalescing in buildMediaRequest

config.baseMediaURL is already nil-coalesced inside RequestBuilder.init using the same constant (Constants.Query.baseMediaURLString). The extra ?? Constants.Query.baseMediaURLString in buildMediaRequest is therefore redundant. It can be simplified to just pass self.config.baseMediaURL directly, consistent with how self.config.baseURL is passed in buildRequest.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift L17-53] — Integration tests make live network calls in setUp

The existing worker-level tracking tests (e.g., ConstructorIOTrackQuizResultClick.swift) use OHHTTPStubs to intercept requests so tests are hermetic and fast. The new ConstructorIOTrackMediaImpressionTests makes a real network call to https://display.media-cnstrc.com in setUp to fetch a valid banner_ad_id. This causes two problems:

  1. Tests will fail in CI environments without external network access.
  2. The XCTFail path in setUp leaves self.bannerAdId as nil (it is an implicitly-unwrapped optional String!), causing a forced-unwrap crash in the test methods instead of a clean test failure.

Consider using a hardcoded stub banner_ad_id and intercepting the tracking request with OHHTTPStubs — following the established pattern — or at minimum guarding against nil in each test body with XCTSkip or an explicit unwrap check.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift L59-77] — Missing negative-case tests

Other worker-level tracking test files include tests for 400, 500, and no-connectivity cases that verify the correct CIOError type is delivered to the completion handler. The new test file only covers the happy path. Please add analogous error-case tests to ensure error handling works correctly for the new tracking endpoints.


[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift L119-148] — getMediaRequest() duplicates getRequest() without explanation

getMediaRequest() is nearly a copy of getRequest(), with two key differences: (1) it uses baseMediaURL, and (2) it unconditionally adds the _dt timestamp (no ignoreDtPaths check). A brief comment clarifying that the ignoreDtPaths logic is intentionally absent would help future maintainers avoid accidentally re-introducing it.


[File: AutocompleteClient.xcodeproj/project.pbxproj] — iOS Deployment Target bump from 11.0 to 15.6

The minimum deployment target is being raised from iOS 11.0 to iOS 15.6 across all targets. This is a potentially breaking change for existing SDK consumers who support iOS 11.x–15.5. Is this intentional? If so, it should be clearly documented in the changelog and PR description. If it is an incidental side effect of Xcode regenerating the project file, it should be reverted.


Conclusion

The feature implementation is clean and consistent with the existing codebase patterns. The key items to address before merging:

  1. (Important) Clarify the iOS deployment target bump to 15.6 — revert if unintentional.
  2. (Important) Fix the integration test fragility: live network dependency in setUp, implicit-unwrap crash on failure, and missing negative-case (400/500/no-connectivity) tests.
  3. (Minor) Remove the redundant urlWithFormat override in both new data structs.
  4. (Minor) Remove the redundant ?? Constants.Query.baseMediaURLString nil-coalesce in buildMediaRequest.
  5. (Minor) Decide whether the internal data structs should appear in the public Jazzy documentation.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds support to emit tracking events for display ads (media impressions) view and click within the Constructor.io Swift client, along with generated documentation updates.

Changes:

  • Add trackMediaImpressionView / trackMediaImpressionClick APIs and supporting request-data structures/constants.
  • Extend request building/configuration to support a separate baseMediaURL for media tracking requests.
  • Regenerate Jazzy docs and update README with media impression examples.

Reviewed changes

Copilot reviewed 129 out of 158 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Adds public media impression tracking methods and a dedicated media request builder path.
AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift Introduces request payload/url builder for “display_ad_view” tracking.
AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift Introduces request payload/url builder for “display_ad_click” tracking.
AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Adds baseMediaURL support and getMediaRequest() to construct media tracking requests.
AutocompleteClient/FW/Config/ConstructorIOConfig.swift Adds baseMediaURL to configuration and initializer.
AutocompleteClient/Constants/Constants.swift Adds default media base URL and tracking endpoint formats.
AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Adds tests for new media tracking APIs (currently implemented with live network dependency).
AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionViewRequestBuilderTests.swift Adds unit tests for media impression view request building (URL/body/custom base URL).
AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift Adds unit tests for media impression click request building (URL/body/custom base URL).
README.md Documents media impression tracking usage examples.
.jazzy.yaml Includes media tracking request structures and adds a “Tracking Data Structures” doc category.
docs/index.html Regenerated docs; adds “Media Impression Events” section and updates footer metadata.
docs/js/jazzy.js Fixes a typo in a comment (“targetted” → “targeted”) in generated docs assets.
docs/undocumented.json Regenerated docs artifact; updates source paths/source_directory (currently absolute).
docs/Client.html Regenerated docs footer metadata.
docs/Classes/ConstructorIO.html Regenerated docs; includes new media tracking methods and updated footer metadata.
docs/Classes/CIOSearchQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOResultSources.html Regenerated docs footer metadata.
docs/Classes/CIOResultSourceData.html Regenerated docs footer metadata.
docs/Classes/CIOResult.html Regenerated docs footer metadata.
docs/Classes/CIORecommendationsQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOGroup.html Regenerated docs footer metadata.
docs/Classes/CIOFilterGroup.html Regenerated docs footer metadata.
docs/Classes/CIOCollectionData.html Regenerated docs footer metadata.
docs/Classes/CIOBrowseQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOBrowseItemsQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOBrowseGroupsQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOBrowseFacetsQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOBrowseFacetOptionsQueryBuilder.html Regenerated docs footer metadata.
docs/Classes/CIOAutocompleteResult.html Regenerated docs footer metadata.
docs/Classes/CIOAutocompleteQueryBuilder.html Regenerated docs footer metadata.
docs/Structs/ConstructorIOConfig.html Regenerated docs; documents baseMediaURL and updated initializer signature.
docs/Structs/CIOSortOption.html Regenerated docs footer metadata.
docs/Structs/CIOSearchResponse.html Regenerated docs footer metadata.
docs/Structs/CIOSearchRedirectInfo.html Regenerated docs footer metadata.
docs/Structs/CIOSearchQuery.html Regenerated docs footer metadata.
docs/Structs/CIOResultFacet.html Regenerated docs footer metadata.
docs/Structs/CIOResultData.html Regenerated docs footer metadata.
docs/Structs/CIORefinedContent.html Regenerated docs footer metadata.
docs/Structs/CIORecommendationsStrategy.html Regenerated docs footer metadata.
docs/Structs/CIORecommendationsResponse.html Regenerated docs footer metadata.
docs/Structs/CIORecommendationsQuery.html Regenerated docs footer metadata.
docs/Structs/CIORecommendationsPod.html Regenerated docs footer metadata.
docs/Structs/CIOQuizResultsResponse.html Regenerated docs footer metadata.
docs/Structs/CIOQuizResult.html Regenerated docs footer metadata.
docs/Structs/CIOQuizQuestionResponse.html Regenerated docs footer metadata.
docs/Structs/CIOQuizQuestion.html Regenerated docs footer metadata.
docs/Structs/CIOQuizQuery.html Regenerated docs footer metadata.
docs/Structs/CIOQuizOptionAttribute.html Regenerated docs footer metadata.
docs/Structs/CIOQuizOption.html Regenerated docs footer metadata.
docs/Structs/CIOQuizImages.html Regenerated docs footer metadata.
docs/Structs/CIOFilterFacetOption.html Regenerated docs footer metadata.
docs/Structs/CIOFilterFacet.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseResponse.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseQuery.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseItemsQuery.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseGroupsQuery.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseFacetsResponse.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseFacetsQuery.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseFacetOptionsResponse.html Regenerated docs footer metadata.
docs/Structs/CIOBrowseFacetOptionsQuery.html Regenerated docs footer metadata.
docs/Structs/CIOAutocompleteResponse.html Regenerated docs footer metadata.
docs/Structs/CIOAutocompleteQuery.html Regenerated docs footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/index.html Regenerated docset index; adds “Media Impression Events” section and updates footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/js/jazzy.js Regenerated docset JS; typo fix in comment.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/undocumented.json Regenerated docset artifact; updates source paths/source_directory (currently absolute).
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Client.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/ConstructorIO.html Regenerated docset docs; includes new media tracking methods and updated footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOSearchQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOResultSources.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOResultSourceData.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOResult.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIORecommendationsQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOGroup.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOFilterGroup.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOCollectionData.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOBrowseQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOBrowseItemsQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOBrowseGroupsQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOBrowseFacetsQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOBrowseFacetOptionsQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOAutocompleteResult.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Classes/CIOAutocompleteQueryBuilder.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/ConstructorIOConfig.html Regenerated docset docs; documents baseMediaURL and updated initializer signature.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOSortOption.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOSearchResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOSearchRedirectInfo.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOSearchQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOResultFacet.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOResultData.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIORefinedContent.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIORecommendationsStrategy.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIORecommendationsResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIORecommendationsQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIORecommendationsPod.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizResultsResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizResult.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizQuestionResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizQuestion.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizOptionAttribute.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizOption.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOQuizImages.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOFilterFacetOption.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOFilterFacet.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseItemsQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseGroupsQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseFacetsResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseFacetsQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseFacetOptionsResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOBrowseFacetOptionsQuery.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOAutocompleteResponse.html Regenerated docset footer metadata.
docs/docsets/ConstructorAutocomplete.docset/Contents/Resources/Documents/Structs/CIOAutocompleteQuery.html Regenerated docset footer metadata.
Podfile.lock Updates lockfile metadata (specs repo URL casing, CocoaPods version, checksum).
Pods/Manifest.lock Updates manifest lockfile metadata (specs repo URL casing, CocoaPods version, checksum).
AutocompleteClient.xcodeproj/project.pbxproj Adds new sources/tests to the Xcode project; also updates CocoaPods integration metadata and raises deployment targets.
Files not reviewed (22)
  • Pods/Target Support Files/Kingfisher/Kingfisher-Info.plist: Language not supported
  • Pods/Target Support Files/Kingfisher/Kingfisher.debug.xcconfig: Language not supported
  • Pods/Target Support Files/Kingfisher/Kingfisher.release.xcconfig: Language not supported
  • Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs-Info.plist: Language not supported
  • Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.debug.xcconfig: Language not supported
  • Pods/Target Support Files/OHHTTPStubs/OHHTTPStubs.release.xcconfig: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-Info.plist: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-input-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Debug-output-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-input-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks-Release-output-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests-frameworks.sh: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests.debug.xcconfig: Language not supported
  • Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests.release.xcconfig: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-Info.plist: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-input-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Debug-output-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-input-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks-Release-output-files.xcfilelist: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication-frameworks.sh: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication.debug.xcconfig: Language not supported
  • Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication.release.xcconfig: Language not supported

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +357 to +377
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)
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.
Comment on lines +20 to +52
let config = ConstructorIOConfig(apiKey: TestConstants.testApiKeyWithAdPlacements)
self.constructor = ConstructorIO(config: config)

// Fetch a valid banner_ad_id from the display ads endpoint
let fetchExpectation = XCTestExpectation(description: "Fetch display ads to get banner_ad_id")
let urlString = "https://display.media-cnstrc.com/display-ads?key=\(TestConstants.testApiKeyWithAdPlacements)&placement_ids=\(TestConstants.testPlacementId)"
let url = URL(string: urlString)!

let task = URLSession.shared.dataTask(with: url) { data, _, error in
guard let data = data, error == nil else {
XCTFail("Failed to fetch display ads: \(error?.localizedDescription ?? "unknown error")")
fetchExpectation.fulfill()
return
}

do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any],
let displayAds = json["display_ads"] as? [String: Any],
let ad = displayAds[TestConstants.testPlacementId] as? [String: Any],
let bannerAdId = ad["banner_ad_id"] as? String {
self.bannerAdId = bannerAdId
} else {
XCTFail("Failed to parse banner_ad_id from display ads response")
}
} catch {
XCTFail("Failed to parse display ads JSON: \(error.localizedDescription)")
}

fetchExpectation.fulfill()
}
task.resume()

wait(for: [fetchExpectation], timeout: 10.0)
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.

This test performs a real network call in setUp() to fetch banner_ad_id, which makes the unit test suite flaky (depends on network availability and external service state) and slows CI. The rest of the tracking tests in this repo use OHHTTPStubs to deterministically stub requests/responses; this test should follow that pattern by stubbing the display-ads fetch (or avoiding it entirely) and stubbing the media tracking endpoints instead of calling live URLs.

Copilot uses AI. Check for mistakes.
Comment on lines +59 to +76
func testTrackMediaImpressionView() {
let expectation = XCTestExpectation(description: "Tracking media impression view")
self.constructor.trackMediaImpressionView(bannerAdId: self.bannerAdId, placementId: TestConstants.testPlacementId, completionHandler: { response in
let cioError = response.error as? CIOError
XCTAssertNil(cioError)
expectation.fulfill()
})
self.wait(for: [expectation], timeout: 10.0)
}

func testTrackMediaImpressionClick() {
let expectation = XCTestExpectation(description: "Tracking media impression click")
self.constructor.trackMediaImpressionClick(bannerAdId: self.bannerAdId, placementId: TestConstants.testPlacementId, completionHandler: { response in
let cioError = response.error as? CIOError
XCTAssertNil(cioError)
expectation.fulfill()
})
self.wait(for: [expectation], timeout: 10.0)
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.

bannerAdId is an implicitly-unwrapped optional that may remain nil if the fetch/parsing fails; the subsequent calls to trackMediaImpressionView/Click will then crash when passing it into a non-optional parameter. If you keep the fetch, fail the setup early (or skip the tests) when bannerAdId cannot be obtained, or make the tests fully stubbed so bannerAdId is always defined.

Copilot uses AI. Check for mistakes.
Comment on lines 3 to 6
{
"file": "/Users/stanleypeng/Documents/Work/constructorio-client-swift/AutocompleteClient/FW/Logic/Result/CIORecommendationsPod.swift",
"file": "/Users/viktor.zavala/projects/constructorio-client-swift/AutocompleteClient/FW/Logic/Result/CIORecommendationsPod.swift",
"line": 15,
"symbol": "CIORecommendationsPod.displayName",
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.

This file contains absolute paths to a developer machine (e.g., /Users/.../projects/...) which are environment-specific and can leak local usernames/paths into the repo. Consider excluding this generated file from version control, or configuring the docs generation step to emit paths relative to source_directory so the output is reproducible across machines.

Copilot uses AI. Check for mistakes.
Comment on lines 3 to 12
{
"file": "/Users/evan.yan/Constructor/constructorio-client-swift/AutocompleteClient/FW/Logic/Result/CIORecommendationsPod.swift",
"file": "/Users/viktor.zavala/projects/constructorio-client-swift/AutocompleteClient/FW/Logic/Result/CIORecommendationsPod.swift",
"line": 15,
"symbol": "CIORecommendationsPod.displayName",
"symbol_kind": "source.lang.swift.decl.var.instance",
"warning": "undocumented"
},
{
"file": "/Users/evan.yan/Constructor/constructorio-client-swift/AutocompleteClient/FW/Logic/Result/CIORecommendationsPod.swift",
"file": "/Users/viktor.zavala/projects/constructorio-client-swift/AutocompleteClient/FW/Logic/Result/CIORecommendationsPod.swift",
"line": 16,
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.

This generated undocumented.json includes absolute local filesystem paths (and in this diff even mixes multiple different /Users/... roots). This makes the docs artifacts non-reproducible and may leak developer machine details; prefer generating relative paths or excluding this file from commits.

Copilot uses AI. Check for mistakes.
@ZSnake ZSnake force-pushed the REM-3041/display-ads-tracking branch from 25444a5 to d6e73c0 Compare March 12, 2026 16:21
@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds display ad tracking functionality (media impression view and click events) to the Constructor.io Swift client. The implementation follows the existing patterns in the codebase (quiz tracking, etc.) well overall, and is accompanied by unit and integration tests. I have a few issues to flag — one significant correctness concern, a few minor issues, and some observations.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift Line: L14]Significant: Data type is internal but referenced from a public API

CIOTrackMediaImpressionViewData and CIOTrackMediaImpressionClickData are declared as internal (struct, no access modifier), but the public ConstructorIO.trackMediaImpressionView/Click functions construct them directly. This is consistent with how other CIOTrack*Data types are handled in this codebase (e.g. CIOTrackQuizConversionData is also internal), so this follows the existing pattern. However, note that the .jazzy.yaml file adds these types to the public documentation (include and custom_categories), which is inconsistent — Jazzy will not be able to document internal types unless they are public. If documentation generation is expected to work, these structs need to be made public, or the Jazzy config entries should be removed.

[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Line: L858]Minor: Redundant nil-coalescing in buildMediaRequest

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString)

RequestBuilder.init already defaults baseMediaURL to Constants.Query.baseMediaURLString when nil is passed:

self.baseMediaURL = baseMediaURL ?? Constants.Query.baseMediaURLString

So the ?? Constants.Query.baseMediaURLString at the call site is redundant. Compare with buildQuizRequest — it has the same redundancy for baseQuizURL. This is a pre-existing pattern issue, but it's worth cleaning up here to avoid confusion about where the default is applied. Simply passing self.config.baseMediaURL (which may be nil) would be sufficient.

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Lines: L119–L148]Minor: getMediaRequest is near-duplicate of getRequest

getMediaRequest() is almost identical to getRequest(), the only meaningful difference being:

  1. It uses self.baseMediaURL instead of self.baseURL.
  2. It does not have the ignoreDtPaths logic.

This duplication could be a source of future drift bugs. A simple refactor — extracting a shared private helper that takes the base URL string and a flag for the ignoreDtPaths check — would reduce the duplication and keep both paths in sync. This is a low-priority suggestion but worth noting for maintainability.

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift & CIOTrackMediaImpressionClickData.swift]Minor: Near-identical structs could be unified

CIOTrackMediaImpressionViewData and CIOTrackMediaImpressionClickData share the same fields (bannerAdId, placementId), the same httpBody implementation, and differ only in the URL path constant they reference. The current approach is consistent with how this codebase handles similar pairs (e.g. recommendation result click vs. view), so this is acceptable. Just noting it as a potential area for future consolidation if the structs diverge with additional fields.

[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Lines: L17–L53]Notable: Integration test makes a live network call in setUp

The setUp method performs a live HTTP request to https://display.media-cnstrc.com/display-ads to fetch a real banner_ad_id before each test. This means:

  • The integration tests are fully dependent on network availability and external service state.
  • A 10-second wait in setUp adds significant overhead, and a service outage will cause both tests to fail with XCTFail rather than a skip.

This is noted as a design choice — integration tests often require live data — but it's worth making sure CI is configured to handle/skip these gracefully when network is unavailable, and ideally to add a XCTSkip guard instead of XCTFail if the network call fails, so flaky network conditions don't fail the suite unexpectedly.

[File: AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift & TrackMediaImpressionViewRequestBuilderTests.swift]Minor: Missing test for _dt (timestamp) query parameter

The other request builder tests in this codebase (e.g. TrackBrowseResultClickRequestBuilderTests) typically assert that the timestamp (_dt) query parameter is present in the URL. The new tests check for c=cioios- and key=, but do not verify _dt is included. Consider adding:

XCTAssertTrue(url.contains("_dt="), "URL should contain the timestamp parameter.")

[File: AutocompleteClient.xcodeproj/project.pbxproj]Observation: iOS deployment target bumped from 11.0 to 15.6

The minimum deployment target across all targets was raised from 11.0 to 15.6. This is a significant breaking change for any users still on older iOS versions. Confirm this is intentional and aligns with the project's support policy. If this was inadvertently changed as a side effect of environment updates, it should be reverted or documented as a deliberate minimum-version bump.

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift Line: L23]Low-priority: urlWithFormat method appears unnecessary

Both new data structs implement urlWithFormat(baseURL:format:) and return String(format: format, baseURL), but neither struct is actually used in a context that calls urlWithFormat (that method is used for quiz requests). Since CIORequestData already provides a default implementation (returning ""), this override isn't needed and is dead code. Removing it would reduce noise.


Conclusion

The implementation is well-structured and follows established patterns in the codebase. The main issue to resolve before merging is the internal visibility of the data structs combined with their inclusion in the public Jazzy documentation config — either make the structs public (if they are intended to be part of the public API / docs) or remove them from .jazzy.yaml. The iOS deployment target bump to 15.6 also needs confirmation that it's intentional. The remaining points are minor quality suggestions.

@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds two new public tracking methods — trackMediaImpressionView and trackMediaImpressionClick — for display ad events, along with the necessary data structs, a dedicated baseMediaURL config option, a new getMediaRequest() builder method, and tests. The overall approach follows existing patterns in the codebase well. There are a few issues worth addressing before merge, ranging from a potential crash to dead code and integration-test concerns.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Lines: 119–148] Duplicate code — getMediaRequest() is nearly identical to getRequest()

The new getMediaRequest() method is a near-copy of getRequest(), with only two differences: it uses self.baseMediaURL instead of self.baseURL, and it always adds the date query item (no ignoreDtPaths check). This is a maintenance burden — any future fix to request building (e.g. the %2B encoding, headers, error handling) will need to be applied in multiple places.

Consider refactoring to a shared private helper, for example:

private func buildRequest(baseURL: String, alwaysIncludeDate: Bool = true) -> URLRequest {
    let urlString = self.trackData!.url(with: baseURL)
    var urlComponents = URLComponents(string: urlString)!
    var allQueryItems = self.queryItems
    allQueryItems.add(URLQueryItem(name: "c", value: Constants.versionString()))
    if alwaysIncludeDate {
        self.addDateQueryItem(queryItems: &allQueryItems)
    }
    // ... rest of shared logic
}

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Line: 120] Force-unwrap crash risk on trackData

self.trackData! is force-unwrapped in getMediaRequest(). If getMediaRequest() is ever called before build(trackData:), the app will crash. There is already a // TODO: Do not force unwrap trackData here; comment on the same pattern in getRequest() at line 73. The same concern applies here and should be tracked (or better, fixed) similarly.


[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Line: 858] Redundant fallback — baseMediaURL default is applied twice

In buildMediaRequest, the fallback default is applied manually:

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString)

However, RequestBuilder.init already applies this same fallback internally (line 28 of RequestBuilder.swift):

self.baseMediaURL = baseMediaURL ?? Constants.Query.baseMediaURLString

This mirrors the same redundancy present in buildQuizRequest. Passing self.config.baseMediaURL (without the nil coalescing) would be sufficient and more consistent, letting RequestBuilder own the default. This is a minor consistency issue, but worth fixing to reduce duplication.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift Lines: 23–25] Dead code — urlWithFormat is never called for media requests

Both CIOTrackMediaImpressionClickData and CIOTrackMediaImpressionViewData implement urlWithFormat(baseURL:format:). However:

  • getMediaRequest() only calls trackData.url(with: self.baseMediaURL).
  • urlWithFormat is only used by getQuizRequest().
  • The CIORequestData protocol already provides a default no-op implementation of urlWithFormat.

These explicit overrides are dead code and should be removed to keep the structs clean.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Lines: 12–53] Integration tests make live network calls in setUp — fragile and slow

The setUp method fires a real HTTP request to https://display.media-cnstrc.com/display-ads?... to fetch a banner_ad_id. This makes these integration tests dependent on external network availability and a specific test API key having ad placements configured. A few concerns:

  1. Hardcoded live URL in test code: The URL https://display.media-cnstrc.com/display-ads?... is constructed inline. It would be cleaner to define it as a constant, similar to how TestConstants.testApiKeyWithAdPlacements is defined.

  2. Silent failure path: If the setUp fetch fails, self.bannerAdId remains nil (an implicitly-unwrapped optional). The actual test methods will then crash with a force-unwrap rather than producing a clear test failure message. Consider either explicitly setting a fallback value and asserting in each test, or using XCTSkip if the fetch fails.

  3. Test isolation: Each test class instantiation hits the live network in setUp, even for the request-builder unit tests in the same suite. Consider whether these integration tests should be in a separate target/scheme that can be run independently.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift / CIOTrackMediaImpressionViewData.swift] Structs are internal but referenced in public API — consider making them public

Both CIOTrackMediaImpressionClickData and CIOTrackMediaImpressionViewData are declared internal (no access modifier). They are used by the public trackMediaImpressionView and trackMediaImpressionClick methods. In the existing codebase, other data structs (e.g. CIOTrackRecommendationResultClickData) are also internal, which is consistent. This is fine if the data structs are not meant to be consumed externally, but it is worth a quick intentional check — the .jazzy.yaml exposes these types under a "Tracking Data Structures" doc category, suggesting they may be intended to be public.


[File: AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift / TrackMediaImpressionViewRequestBuilderTests.swift] Minor: no test for _i (date/timestamp) query param

Other request builder tests in the suite (e.g. for autocomplete select) assert that the _dt timestamp query parameter is present in the URL. The new builder tests verify c=cioios- and key=, but don't verify the date/time parameter. Consider adding this assertion for completeness.


Conclusion

The feature is well-structured and consistent with the existing codebase patterns. The most important items to address before merging are:

  1. The duplicate getMediaRequest() / getRequest() logic (maintenance burden).
  2. The setUp crash on bannerAdId nil in the integration test.
  3. The dead urlWithFormat overrides in both new data structs.

The redundant default in buildMediaRequest and the missing timestamp assertion in tests are minor polish items.

.jazzy.yaml Outdated
- "/**/Result/Responses/*.swift"
- "/**/Result/*.swift"
- "/**/Config/ConstructorIOConfig.swift"
- "/**/Request/CIOTrackMediaImpressionViewData.swift"
Copy link
Contributor

Choose a reason for hiding this comment

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

I just wanted to double-check something following the automated CC review: it seems like these CIOTrackMediaImpressionViewData / CIOTrackMediaImpressionClickData are internal, since they don't have the public keyword. Do you think it makes sense to include them here?

Comment on lines +59 to +61
func testTrackMediaImpressionView() {
let expectation = XCTestExpectation(description: "Tracking media impression view")
self.constructor.trackMediaImpressionView(bannerAdId: self.bannerAdId, placementId: TestConstants.testPlacementId, completionHandler: { response in
Copy link
Contributor

Choose a reason for hiding this comment

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

I know we're not using it in this project, but since we're making a real HTTP call in setUp() to fetch a banner_ad_id, does it makes sense to add some guard with a message?

Suggested change
func testTrackMediaImpressionView() {
let expectation = XCTestExpectation(description: "Tracking media impression view")
self.constructor.trackMediaImpressionView(bannerAdId: self.bannerAdId, placementId: TestConstants.testPlacementId, completionHandler: { response in
func testTrackMediaImpressionView() throws {
let bannerAdId = try XCTUnwrap(self.bannerAdId, "banner_ad_id was not fetched during setUp")
let expectation = XCTestExpectation(description: "Tracking media impression view")
self.constructor.trackMediaImpressionView(bannerAdId: bannerAdId, placementId: TestConstants.testPlacementId, completionHandler: { response in

@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds display ads tracking support by introducing two new public API methods — trackMediaImpressionView and trackMediaImpressionClick — along with corresponding data structs, a new baseMediaURL configuration option, and a dedicated getMediaRequest() builder path. The implementation follows existing patterns very closely and the test coverage is solid. A few issues worth addressing are noted below.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift & CIOTrackMediaImpressionClickData.swift]

1. Structs should be public, not internal

Both CIOTrackMediaImpressionViewData and CIOTrackMediaImpressionClickData are declared without an access modifier (defaulting to internal), but they are referenced in the Jazzy documentation config (.jazzy.yaml) as public API. More importantly, the two public methods on ConstructorIO expose these types implicitly through their parameters — if consumers ever need to construct or inspect these types, they'll be blocked. For consistency with the rest of the CIOTrack*Data types in the project, these should be declared public.

// Before
struct CIOTrackMediaImpressionViewData: CIORequestData {

// After
public struct CIOTrackMediaImpressionViewData: CIORequestData {

2. Duplicate urlWithFormat(baseURL:format:) implementation — not needed

Both new structs override urlWithFormat(baseURL:format:) with return String(format: format, baseURL). This is identical to the default extension implementation already provided in CIORequestData.swift. Since getMediaRequest() only calls url(with:) (never urlWithFormat), and the default extension handles the method anyway, these overrides are dead code and should be removed to keep the structs lean.

3. httpBody — missing optional analytics tags support

All other POST tracking structs (e.g., CIOTrackRecommendationResultClickData, CIOTrackSearchResultClickData) accept an optional analyticsTags: [String: String]? parameter and include it in the body. Display ads tracking will likely need the same capability for attribution and debugging. Consider adding it now for consistency:

let analyticsTags: [String: String]?

// In httpBody:
if let analyticsTags = self.analyticsTags {
    dict["analytics_tags"] = analyticsTags
}

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift]

4. Force-unwrap (!) in getMediaRequest() is inconsistent with stated safety practices

getMediaRequest() uses self.trackData!.url(...), self.trackData!.queryItems(...), and self.trackData!.httpBody(...) — three force-unwraps of an implicitly-unwrapped optional. This is the same pattern used in getRequest() and getQuizRequest(), so it's not a regression, but it's worth noting that if getMediaRequest() is ever called before build(trackData:), it will crash. At minimum, add a precondition or a comment mirroring whatever convention the team uses elsewhere:

// getMediaRequest() must only be called after build(trackData:)

[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swiftbuildMediaRequest]

5. Redundant fallback in buildMediaRequest — double-defaulting baseMediaURL

private func buildMediaRequest(data: CIORequestData) -> URLRequest {
    let requestBuilder = RequestBuilder(apiKey: self.config.apiKey,
        baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString)

RequestBuilder.init already applies ?? Constants.Query.baseMediaURLString internally when baseMediaURL is nil. Passing the same fallback here means the constant is evaluated twice on the happy path, and if the default URL ever changes, there are now two places to update. Compare with buildRequest, which correctly passes self.config.baseURL directly without the manual fallback. This should be:

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey,
    baseMediaURL: self.config.baseMediaURL)

The same pattern inconsistency exists in buildQuizRequest (which uses self.config.baseQuizURL ?? Constants.Query.baseQuizURLString), but since that's pre-existing code, fixing it here for the new method is still beneficial.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift]

6. Integration test makes a live network call in setUp — fragile and slow

The setUp method fetches a live banner_ad_id from https://display.media-cnstrc.com/display-ads?... with a hardcoded API key (key_x6UnCVRZaJgIHFQD) and a 10-second timeout. This means:

  • Tests will fail in offline/CI environments without network access.
  • A live API key is committed to the test source, which can be a security/operational concern if the key is ever rotated or revoked.
  • All tests in this class are blocked even if only one of them needs the bannerAdId.

Look at the other integration tests in the project — they use OHHTTPStubs to stub the network layer. Consider either:

  • Stubbing the display-ads fetch with OHHTTPStubs and using a fixed bannerAdId, or
  • Using a hardcoded valid-format bannerAdId string in setUp directly (since the tracking endpoints won't validate that it maps to a real ad).
// Simpler, deterministic approach:
override func setUp() {
    super.setUp()
    let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey)
    self.constructor = ConstructorIO(config: config)
    self.bannerAdId = "banner-ad-test-123"  // fixed test value
}

7. self.bannerAdId is implicitly-unwrapped optional — crash risk if setUp fails

var bannerAdId: String! combined with the live-fetch setup above means that if the network call fails (or setUp errors), testTrackMediaImpressionView and testTrackMediaImpressionClick will crash with a nil dereference rather than produce a clean test failure. If the live-fetch approach is kept, a guard or XCTSkipIf should be used:

guard self.bannerAdId != nil else {
    XCTSkip("Could not fetch bannerAdId from live API, skipping test")
    return
}

[File: AutocompleteClientTests/Test Utils/Constants/TestConstants.swift]

8. Hardcoded API key in test constants

testApiKeyWithAdPlacements = "key_x6UnCVRZaJgIHFQD" is committed in plain text. If this is a production/staging key, consider documenting that it is intentionally a test key with limited scope, or moving it to a CI secrets mechanism (environment variable) rather than source code.


[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swiftgetMediaRequest()]

9. Minor: var allQueryItems = self.queryItems shadows the property name

The local variable allQueryItems is assigned from self.queryItems (a QueryItemCollection). This is the same naming style used in getRequest() and getQuizRequest(), so it's consistent — just worth noting that the property self.queryItems and the local allQueryItems are distinct after assignment.


Conclusion

The overall approach is clean, idiomatic Swift, and well-aligned with existing patterns in the repository. The most important issues to address before merging are:

  1. The live network call in setUp (item 6) — this is a test reliability/CI concern.
  2. The redundant double-defaulting of baseMediaURL (item 5) — small but creates a maintenance trap.
  3. Missing public visibility on the data structs (item 1) — needed for consistent public API surface.

The optional analyticsTags gap (item 3) and removing the dead urlWithFormat overrides (item 2) are lower priority but would keep the code consistent with the rest of the codebase.

@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds two new public tracking methods — trackMediaImpressionView and trackMediaImpressionClick — to support display ad behavioral event tracking. The implementation follows the established patterns in the codebase (new CIORequestData structs, a dedicated buildMediaRequest helper, a new baseMediaURL config property, and corresponding request builder support). Overall the approach is clean and consistent. I have a few observations below ranging from potential bugs to minor style issues.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Line: L858] — Redundant default URL fallback

In buildMediaRequest, the ?? fallback to Constants.Query.baseMediaURLString is unnecessary because RequestBuilder.init already applies the same default:

// ConstructorIO.swift (current)
let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString)

// RequestBuilder.swift (existing)
self.baseMediaURL = baseMediaURL ?? Constants.Query.baseMediaURLString

This is inconsistent with how buildQuizRequest is handled:

// buildQuizRequest — passes nil and lets RequestBuilder apply the default
let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseQuizURL: self.config.baseQuizURL ?? Constants.Query.baseQuizURLString)

Wait — actually buildQuizRequest does the same thing; both patterns double-apply the default. However it means config.baseMediaURL being nil vs being set to the default string are treated identically in the builder, which is fine. But for consistency, prefer passing self.config.baseMediaURL (which may be nil) and letting RequestBuilder apply the default, matching how the other URL cases work conceptually:

private func buildMediaRequest(data: CIORequestData) -> URLRequest {
    let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL)
    ...
}

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift & CIOTrackMediaImpressionClickData.swift Line: L23] — urlWithFormat implementation is dead code

Both new data structs override urlWithFormat(baseURL:format:), but this method is only called by getQuizRequest and getMediaRequest does not use it. Since CIORequestData provides a default implementation (returning ""), and this override is never invoked for media requests, it is dead code that adds noise.

// This function is never called for media impression data — safe to remove
func urlWithFormat(baseURL: String, format: String) -> String {
    return String(format: format, baseURL)
}

If the intent was to future-proof the implementation, a comment would help. Otherwise removing it keeps the struct lean.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift & CIOTrackMediaImpressionClickData.swift] — Structs are internal but described as public API in .jazzy.yaml

Both data structs are declared without an explicit access modifier (struct CIOTrackMediaImpressionViewData), making them internal. However they are included in the Jazzy documentation config under "Tracking Data Structures", which is a public-facing doc category.

If these structs are intended to be part of the public API (so consumers can construct them directly), they should be declared public. If they are intentionally internal (implementation details), they should be removed from .jazzy.yaml.

Compare with CIOTrackRecommendationResultClickData, which is also internal, suggesting the pattern here is consistent — but it is worth confirming if public documentation of internal types is intentional.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Line: L17] — Integration test makes a live network call in setUp

The integration test fetches a live banner_ad_id from https://display.media-cnstrc.com in setUp. This means:

  1. Both tests fail entirely if the network is unavailable or the API key/placement ID is invalid, with no graceful skip.
  2. self.bannerAdId is implicitly unwrapped (var bannerAdId: String!), so if the fetch fails but XCTFail doesn't stop execution, the downstream test calls will crash with a nil dereference rather than reporting a clean assertion failure.
  3. The fetch adds ~10 seconds to every test run even if only one of the two tests is executed.

Consider using a hardcoded bannerAdId known to be stable, or at least guard against the nil:

func testTrackMediaImpressionView() {
    guard let bannerAdId = self.bannerAdId else {
        XCTFail("bannerAdId was not set in setUp — skipping test")
        return
    }
    ...
}

[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift] — Integration tests use OHHTTPStubs pattern inconsistently

All other worker-level integration tests in this suite (e.g. ConstructorIOTrackRecommendationResultClickTests) use OHHTTPStubs to stub the HTTP layer and assert request parameters rather than making live calls. The new test file makes real outbound requests. This is an inconsistency that can make CI flaky and slow. It also bypasses the request parameter assertions that the stub-based tests provide. Consider adding stub-based assertions for the URL pattern and request body, similar to the existing tests.


[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Lines: L119–L148] — getMediaRequest duplicates getRequest logic

getMediaRequest is nearly identical to getRequest, differing only in which base URL it uses and the omission of the ignoreDtPaths check for the _dt parameter. This duplication violates DRY and means any future bug fix or enhancement to request-building logic must be applied in multiple places.

Consider extracting the shared logic into a private helper:

private func buildURLRequest(urlString: String, addDate: Bool = true) -> URLRequest {
    var urlComponents = URLComponents(string: urlString)!
    var allQueryItems = self.queryItems
    allQueryItems.add(URLQueryItem(name: "c", value: Constants.versionString()))
    if addDate { self.addDateQueryItem(queryItems: &allQueryItems) }
    if let existingItem = urlComponents.queryItems?.first {
        allQueryItems.add(existingItem)
    }
    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
}

[File: AutocompleteClientTests/Test Utils/Constants/TestConstants.swift Line: L24] — API key committed in test constants

testApiKeyWithAdPlacements = "key_x6UnCVRZaJgIHFQD" is a new API key hardcoded in test constants. This matches the existing pattern (testApiKey = "key_OucJxxrfiTVUQx0C"), so it's consistent — but worth confirming this is a non-sensitive/test-only key and that the team is comfortable with it being in version history.


Conclusion

The core implementation is solid and follows the established conventions well. The main items worth addressing before merging are:

  1. Integration test robustness — the live-network setUp fetch and the implicit-unwrap crash risk in the worker tests.
  2. Code duplication in RequestBuilder between getRequest and getMediaRequest.
  3. Dead code (urlWithFormat in the new data structs).
  4. Visibility/documentation mismatch — confirm whether the data structs should be public or removed from Jazzy docs.

The Pods-related file changes appear to be a CocoaPods lock file regeneration and are not a concern.

@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds display ad tracking support (media impression view and click) to the iOS client. The implementation follows existing patterns closely (quiz tracking via buildQuizRequest), introduces two new data structs, a new RequestBuilder method, and integration/unit tests. Overall the approach is sound and well-structured. A few issues are worth addressing before merging.


Detailed Feedback

[File: CIOTrackMediaImpressionViewData.swift L23-L25 and CIOTrackMediaImpressionClickData.swift L23-L25] Unnecessary urlWithFormat override

Both new structs explicitly implement urlWithFormat(baseURL:format:) even though CIORequestData already provides a default implementation returning an empty string. Since getMediaRequest() in RequestBuilder only ever calls url(with:), these overrides are dead code. They should be removed to keep the structs lean and consistent with other non-quiz data structs such as CIOTrackRecommendationResultClickData which do not override this method.


[File: RequestBuilder.swift L119-L148] getMediaRequest() is nearly identical to getRequest() — DRY concern

The two methods share roughly 90% of their body. The only meaningful differences are: (1) getMediaRequest uses self.baseMediaURL instead of self.baseURL, and (2) getMediaRequest always adds the dt query item whereas getRequest has a special-case exclusion for certain browse paths. Consider a shared private helper parameterised on the base URL to eliminate this duplication. At minimum, add a comment explaining why a separate method was needed rather than reusing getRequest.


[File: ConstructorIO.swift L858] Redundant nil-coalescing in buildMediaRequest

RequestBuilder.init already applies the identical fallback internally (baseMediaURL ?? Constants.Query.baseMediaURLString), so passing it explicitly in buildMediaRequest is redundant. The same pattern exists in buildQuizRequest. For consistency, either remove the fallback in both places and let RequestBuilder handle the default, or keep it everywhere — just make the choice intentional.


[File: ConstructorIOTrackMediaImpressionTests.swift L17-L52] Live network call in setUp is fragile and can crash on failure

The setUp method makes a real HTTP request to display.media-cnstrc.com to obtain a banner_ad_id. If the external service is unavailable or the key has no ad placements, the fetch silently fails and self.bannerAdId remains nil. Since bannerAdId is declared as String! (implicitly unwrapped), the subsequent tracking call will crash the test runner rather than produce a meaningful XCTFail.

Suggestions:

  1. Use a hardcoded stub banner_ad_id as done in the request builder tests, and reserve live-network tests for a dedicated integration test target.
  2. If the live fetch is intentional, guard against nil at the start of each test method: guard let bannerAdId = self.bannerAdId else { XCTFail("bannerAdId was not set in setUp"); return }

[Files: CIOTrackMediaImpressionViewData.swift and CIOTrackMediaImpressionClickData.swift] Structs are internal but listed in .jazzy.yaml as public API

Both structs are declared without public, making them internal. Yet .jazzy.yaml adds them under a new "Tracking Data Structures" category, implying they are part of the public API surface. If consumers are expected to see or construct these types, they should be public struct. If they are only internal implementation details, they should be removed from .jazzy.yaml.


[File: TestConstants.swift L24] Second API key committed in plaintext

static let testApiKeyWithAdPlacements = "key_x6UnCVRZaJgIHFQD" follows the existing pattern of testApiKey, but please confirm this key has appropriately restricted permissions and can be safely rotated, since it is now permanently in git history.


[File: RequestBuilder.swift L120-L145] Force-unwraps carried over from getRequest()

getMediaRequest() retains the same ! force-unwraps already present in getRequest() (which has an open TODO comment about this). Since this is new code, it is a good opportunity to address the debt here even if not in the older method. A guard let with preconditionFailure would communicate the programming contract more clearly and avoid a hard crash in production if build(trackData:) is accidentally omitted.


Conclusion

The feature is well-scoped and closely follows existing conventions. The two most important issues to resolve before merging are:

  1. The fragile setUp network call in the integration tests — a network failure silently leaves bannerAdId as nil, which will crash the test runner rather than fail gracefully.
  2. Public API visibility — the new data structs appear in the Jazzy docs but are declared internal; this mismatch should be intentional or corrected.

The remaining points are lower-severity consistency and robustness improvements. Good work overall — the pattern-matching with quiz tracking is clean and sensible.

…ession tests, remove Media impression data files from jazzy
@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds display ads tracking support by introducing two new public tracking methods — trackMediaImpressionView and trackMediaImpressionClick — on ConstructorIO, a dedicated baseMediaURL routing path through RequestBuilder, and a suite of unit tests. The overall approach follows the existing patterns in the codebase well. There are a handful of issues worth addressing before merging, ranging from a potential runtime crash to minor API surface concerns.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Line: getMediaRequest()] — Force-unwraps can crash at runtime

The new getMediaRequest() method (and the existing getRequest()) both use force-unwraps (!) on trackData, URLComponents, and urlComponents.url. The existing code has this issue too, but it's worth flagging here as new code is being added with the same fragility:

let urlString = self.trackData!.url(with: self.baseMediaURL)  // crash if trackData is nil
var urlComponents = URLComponents(string: urlString)!          // crash if URL string is malformed
let url = urlComponents.url!                                   // crash if percent-encoding fails

While build(trackData:) is always called before getMediaRequest(), this contract is implicit and not enforced by the type system. At minimum, the new method could use guard statements or safe optional chaining to fail gracefully.


[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Line: buildMediaRequest] — Redundant fallback: double defaulting of baseMediaURL

private func buildMediaRequest(data: CIORequestData) -> URLRequest {
    let requestBuilder = RequestBuilder(
        apiKey: self.config.apiKey,
        baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString  // ← manual fallback
    )
    ...
}

RequestBuilder.init already handles this fallback internally:

self.baseMediaURL = baseMediaURL ?? Constants.Query.baseMediaURLString

Compare with buildQuizRequest:

let requestBuilder = RequestBuilder(
    apiKey: self.config.apiKey,
    baseQuizURL: self.config.baseQuizURL ?? Constants.Query.baseQuizURLString  // same duplication
)

Both buildMediaRequest and buildQuizRequest are doing the defaulting themselves and then passing a non-nil value to RequestBuilder, making RequestBuilder's own nil-coalescing dead code. The cleaner pattern is to pass the optional directly: baseMediaURL: self.config.baseMediaURL and let RequestBuilder own the default. This removes an inconsistency and a source of divergence if the default URL ever changes.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift and CIOTrackMediaImpressionViewData.swift] — urlWithFormat override is unnecessary

Both new data structs override urlWithFormat(baseURL:format:):

func urlWithFormat(baseURL: String, format: String) -> String {
    return String(format: format, baseURL)
}

getMediaRequest() calls url(with:), not urlWithFormat. The CIORequestData protocol already provides a default implementation for urlWithFormat (returning ""), which is what other non-quiz trackers use. This override adds dead code. It should be removed from both structs.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift and CIOTrackMediaImpressionViewData.swift] — Structs are internal but their documentation suggests public visibility

Both data structs are declared without an access modifier (defaulting to internal):

struct CIOTrackMediaImpressionClickData: CIORequestData { ... }
struct CIOTrackMediaImpressionViewData: CIORequestData { ... }

This matches the pattern for other tracking data structs in the codebase (e.g., CIOTrackBrowseResultClickData), so this is intentional. However, the .jazzy.yaml file adds them to the public documentation under "Tracking Data Structures". If these are internal implementation details, they shouldn't be in the public API docs. If the intent is to expose them publicly (e.g., for consumers to create these objects directly), they should be marked public. Please align the access modifier with the documentation intent.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift] — Integration test performs real network calls and fails the whole test suite if the network is unavailable

The setUp method makes a real HTTP request to https://display.media-cnstrc.com/display-ads and calls XCTFail if it fails:

guard let data = data, error == nil else {
    XCTFail("Failed to fetch display ads: ...")
    fetchExpectation.fulfill()
    return
}

If bannerAdId is nil after setup (e.g., due to a network failure or API key expiry), the test body guards with:

let bannerAdId = try XCTUnwrap(self.bannerAdId, "banner_ad_id was not fetched during setUp")

This is a reasonable pattern for integration tests, but:

  1. Other integration tests in the repo (e.g., ConstructorIOTrackBrowseResultClickTests) use OHHTTPStubs to mock network calls — they don't require a real connection. The new tests are a departure from this pattern and may be flaky in CI.
  2. There is no OHHTTPStubs.removeAllStubs() in tearDown. While the new tests don't register stubs, the pattern used elsewhere in the test suite always has this cleanup to prevent stub leakage.

Consider either: (a) mocking the display ads fetch with OHHTTPStubs and hardcoding a valid bannerAdId in the test, or (b) marking this test class clearly as an integration test that requires network access and ensuring CI is configured to skip it if needed.


[File: AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift and TrackMediaImpressionViewRequestBuilderTests.swift] — Missing _dt timestamp assertion

The builder tests verify c= (version string) and key= are present in the URL, but don't assert that the _dt timestamp is included. getMediaRequest() always calls self.addDateQueryItem(queryItems: &allQueryItems), so this param should always be present. The comparable test for other trackers (e.g., TrackBrowseResultClickRequestBuilderTests) includes a regex check for this:

XCTAssertTrue(url.contains("_dt="), "URL should contain the timestamp.")

Adding this assertion would improve test completeness.


[File: AutocompleteClient/FW/Config/ConstructorIOConfig.swift] — Minor: doc comment typo

In the init parameter docs:

- segments: List of segments to associate with requets  // typo: requets should be requests

This is pre-existing, but since the doc block is being edited in this PR it's a good opportunity to fix.


Conclusion

The implementation is well-structured and follows the existing conventions. The main items to address are: (1) the redundant baseMediaURL defaulting in buildMediaRequest, (2) the dead urlWithFormat overrides in the two data structs, (3) clarifying whether the data structs should be public given they appear in the Jazzy docs, and (4) the integration test's reliance on real network calls which diverges from the established mocking pattern. The force-unwraps are a pre-existing issue, but worth being aware of. Overall this is a solid PR — the above are actionable improvements to clean it up before merge.

@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds display ads tracking support by introducing two new public API methods — trackMediaImpressionView and trackMediaImpressionClick — along with the necessary data models, a new RequestBuilder method, a dedicated media base URL, and tests. The overall structure is consistent with existing patterns in the codebase (e.g., how quiz tracking was added). The implementation is mostly clean, but there are several issues worth addressing before merging.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift Line: L23-L25 / CIOTrackMediaImpressionViewData.swift Line: L23-L25] — Unnecessary urlWithFormat implementation

Both new data structs implement urlWithFormat(baseURL:format:) but this method is only ever called by getQuizRequest in RequestBuilder, which is not used for media requests. Since CIORequestData provides a default implementation for this method (as seen in CIORequestData.swift), these explicit implementations are dead code and add confusion about intent. They should be removed.

// Remove these from both CIOTrackMediaImpressionClickData and CIOTrackMediaImpressionViewData:
func urlWithFormat(baseURL: String, format: String) -> String {
    return String(format: format, baseURL)
}

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Lines: L119-L148] — getMediaRequest() duplicates getRequest() with minor differences

getMediaRequest() is nearly identical to getRequest() (same body, same headers, same method assignment). The only meaningful differences are: (1) it uses self.baseMediaURL instead of self.baseURL, and (2) it unconditionally adds the dt query item (no ignore-path logic for browse endpoints). This is a maintainability concern — any future fix to getRequest() would also need to be manually applied to getMediaRequest().

Consider extracting the shared logic into a private helper, or passing the base URL string as a parameter:

private func buildRequest(using urlString: String) -> URLRequest {
    var urlComponents = URLComponents(string: urlString)!
    var allQueryItems = self.queryItems
    allQueryItems.add(URLQueryItem(name: "c", value: Constants.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
}

[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Line: L858] — buildMediaRequest redundantly resolves the default URL

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString)

RequestBuilder.init already defaults to Constants.Query.baseMediaURLString when baseMediaURL is nil:

self.baseMediaURL = baseMediaURL ?? Constants.Query.baseMediaURLString

So passing self.config.baseMediaURL ?? Constants.Query.baseMediaURLString is redundant. Compare this to how buildQuizRequest passes self.config.baseQuizURL which has the same redundancy — this may be an existing pattern, but it's worth noting that you can simplify to:

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL)

This also matches how the buildRequest method passes self.config.baseURL.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Lines: L17-L53] — Live network call in setUp is fragile

The setUp method makes a real HTTP request to https://display.media-cnstrc.com to obtain a banner_ad_id. This approach has several issues:

  • Tests are dependent on external network availability and the state of a live service, making them unreliable in CI environments without network access.
  • If the setUp fetch fails (e.g., network error, unexpected response shape), self.bannerAdId remains nil, and both test methods will fail with an unwrap error — but the root cause (setUp failure) is not clearly surfaced.
  • The XCTFail calls inside the async closure don't immediately stop setUp, so the test will continue past a failed fetch and attempt to run with a nil bannerAdId.

Consider using a hardcoded or fixture-based bannerAdId for unit/integration tests, consistent with how other tests in the suite use CIOBuilder and fixed test constants. If live-network integration tests are intentional and run separately (e.g., via a separate test scheme), that should be documented.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Lines: L55-L57] — Empty tearDown is unnecessary

override func tearDown() {
    super.tearDown()
}

This override does nothing and can be removed to keep the file clean.


[File: AutocompleteClientTests/FW/Logic/Request/TrackMediaImpressionClickRequestBuilderTests.swift Line: L26] — Minor: CIORequestData type annotation is unnecessary

let tracker: CIORequestData = CIOTrackMediaImpressionClickData(bannerAdId: bannerAdId, placementId: placementId)

The explicit : CIORequestData protocol annotation is not consistent with TrackMediaImpressionViewRequestBuilderTests.swift, which uses direct type inference. Use the inferred type for consistency (or add it to the view test for consistency in the other direction):

let tracker = CIOTrackMediaImpressionClickData(bannerAdId: bannerAdId, placementId: placementId)

[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionViewData.swift / CIOTrackMediaImpressionClickData.swift] — High code duplication between View and Click data types

Both CIOTrackMediaImpressionViewData and CIOTrackMediaImpressionClickData are nearly byte-for-byte identical, differing only in which URL constant they reference (TrackMediaImpressionView.format vs TrackMediaImpressionClick.format). This duplication will cause maintenance burden if the payload structure changes.

A possible improvement would be to use a shared base type or a factory approach (passing the endpoint constant), though this may be acceptable given the rest of the codebase follows similar patterns (e.g., CIOTrackRecommendationResultClickData vs CIOTrackRecommendationResultsViewData). At minimum, this is worth acknowledging as a tradeoff.


[File: AutocompleteClientTests/Test Utils/Constants/TestConstants.swift Lines: L24-L25] — API key committed in test constants

static let testApiKeyWithAdPlacements = "key_x6UnCVRZaJgIHFQD"
static let testPlacementId = "home"

A real-looking API key is being committed to source control. While test API keys are generally lower risk, confirm this key is intended to be public and has appropriate scope restrictions. If it is a key for a test/sandbox account, it should be clearly commented as such.


Conclusion

The feature is well-structured and follows the patterns established by the existing codebase. The main concerns are:

  1. getMediaRequest() code duplication with getRequest() — this is the most impactful issue for long-term maintainability.
  2. Live network dependency in setUp of the integration test — should be reconsidered for reliability in CI.
  3. Small cleanups: remove dead urlWithFormat implementations, remove the empty tearDown, simplify the redundant URL fallback in buildMediaRequest.

Once these are addressed, the PR will be in great shape.

@constructor-claude-bedrock
Copy link

Code Review Summary

This PR adds two new public tracking methods to ConstructorIOtrackMediaImpressionView and trackMediaImpressionClick — for recording display ad events. The implementation follows the existing patterns for quiz tracking (dedicated base URL, separate buildMediaRequest helper, new CIORequestData structs). The overall approach is sound and the test coverage is good. I have a few observations ranging from a potential correctness issue to minor code quality notes.


Detailed Feedback

[File: AutocompleteClient/FW/Logic/Request/Builder/RequestBuilder.swift Lines: L119–L148] Code duplication between getRequest() and getMediaRequest()

getMediaRequest() is nearly identical to getRequest(). The only material differences are:

  1. It calls self.trackData!.url(with: self.baseMediaURL) instead of self.baseURL.
  2. It always appends the _dt query item (no ignoreDtPaths logic).

This duplication will become a maintenance burden. Consider refactoring to a shared private helper:

private func buildURLRequest(urlString: String) -> URLRequest {
    var urlComponents = URLComponents(string: urlString)!
    var allQueryItems = self.queryItems
    allQueryItems.add(URLQueryItem(name: "c", value: Constants.versionString()))
    self.addDateQueryItem(queryItems: &allQueryItems)
    // ...attach action from base URL if present, percent-encode, etc.
    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
}

Then both getRequest() and getMediaRequest() simply compute the correct urlString and call the helper.


[File: AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift Line: L858] Redundant nil-coalescing fallback

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL ?? Constants.Query.baseMediaURLString)

RequestBuilder.init already falls back to Constants.Query.baseMediaURLString when baseMediaURL is nil (line 28 of RequestBuilder.swift). The ?? Constants.Query.baseMediaURLString here is redundant. Compare with how buildQuizRequest is implemented — it has the same redundancy. Passing self.config.baseMediaURL (potentially nil) directly would be cleaner and keep the default in one place:

let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseMediaURL: self.config.baseMediaURL)

This also matches how buildRequest passes self.config.baseURL without a fallback. (Same note applies to the existing buildQuizRequest pattern, but that's pre-existing.)


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift and CIOTrackMediaImpressionViewData.swift] urlWithFormat override is unused / unnecessary

Both new structs override urlWithFormat(baseURL:format:):

func urlWithFormat(baseURL: String, format: String) -> String {
    return String(format: format, baseURL)
}

This is the same as the default implementation provided by the CIORequestData protocol extension. Since these structs don't use getQuizRequest (which is the only caller of urlWithFormat), this override adds noise without benefit. It can simply be removed, letting the default (which returns "") or the protocol extension handle it.


[File: AutocompleteClient/FW/Logic/Request/CIOTrackMediaImpressionClickData.swift and CIOTrackMediaImpressionViewData.swift] Near-identical structs — consider consolidation

Both CIOTrackMediaImpressionClickData and CIOTrackMediaImpressionViewData are byte-for-byte identical except for the constant they reference in url(with:). This is a strong signal that they could be collapsed into a single struct parameterised by event type, similar to how some other data types in the codebase work:

enum MediaImpressionEventType {
    case view
    case click
}

struct CIOTrackMediaImpressionData: CIORequestData {
    let bannerAdId: String
    let placementId: String
    let eventType: MediaImpressionEventType

    func url(with baseURL: String) -> String {
        switch eventType {
        case .view:  return String(format: Constants.TrackMediaImpressionView.format, baseURL)
        case .click: return String(format: Constants.TrackMediaImpressionClick.format, baseURL)
        }
    }
    // ...rest unchanged
}

This reduces duplication and makes future additions (e.g. a third event type) a one-line change.


[File: AutocompleteClientTests/Test Utils/Constants/TestConstants.swift] Unused test constant

static let testApiKeyWithAdPlacements = "key_x6UnCVRZaJgIHFQD"

This constant is declared but never referenced anywhere in the test files added in this PR. If it's not needed, it should be removed to keep the test utilities clean.


[File: AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackMediaImpressionTests.swift Lines: L32, L39, etc.`] Test stub regex does not validate the request body

The integration tests stub by URL regex and verify that a request is sent, but they don't assert that the HTTP body contains the correct banner_ad_id and placement_id values. The unit tests in TrackMediaImpressionClickRequestBuilderTests / TrackMediaImpressionViewRequestBuilderTests do validate the body, so this is a minor gap but worth noting for completeness in the integration tests.


[General] Missing test for payload sent with custom baseMediaURL in ConstructorIO

The request builder tests cover the custom base URL case. However, there's no integration test in ConstructorIOTrackMediaImpressionTests that verifies the custom baseMediaURL configured on ConstructorIOConfig is actually used when calling trackMediaImpressionView / trackMediaImpressionClick through the main ConstructorIO class (analogous to how other integration test files test custom baseURL). This would catch regressions if the buildMediaRequest plumbing changes.


Conclusion

The feature is well-scoped and follows the existing codebase patterns. The main actionable items are: eliminating the redundant ?? Constants.Query.baseMediaURLString fallback in buildMediaRequest, removing the unnecessary urlWithFormat overrides, and considering consolidating the two near-identical data structs into one. The test coverage is a good foundation; the unused testApiKeyWithAdPlacements constant should also be removed or used.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants