Skip to content

Commit 6199605

Browse files
authored
[CI-1832] Add support for Generic Result Click Events (#249)
* Add support for Generic Result Click Events * Fix typo * Add missing project.pbxproj changes
1 parent 2c217bf commit 6199605

File tree

8 files changed

+268
-0
lines changed

8 files changed

+268
-0
lines changed

AutocompleteClient.xcodeproj/project.pbxproj

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@
6565
7D45384028DC16F400490BFE /* CIOQuizQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D45383F28DC16F400490BFE /* CIOQuizQuestion.swift */; };
6666
7D45384228DC181100490BFE /* CIOQuizImages.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D45384128DC181100490BFE /* CIOQuizImages.swift */; };
6767
7D45384428DC19A500490BFE /* CIOQuizOption.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D45384328DC19A500490BFE /* CIOQuizOption.swift */; };
68+
7D4E4A3B2E5B77D900D4EF5A /* CIOTrackGenericResultClick.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4E4A3A2E5B77D900D4EF5A /* CIOTrackGenericResultClick.swift */; };
69+
7D4E4A3D2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4E4A3C2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift */; };
70+
7D4E4A3F2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D4E4A3E2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift */; };
6871
7D54FF8028E3B92000A584E3 /* ConstructorIOQuizTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D54FF7F28E3B92000A584E3 /* ConstructorIOQuizTests.swift */; };
6972
7D54FF8428E3BFA400A584E3 /* response_quiz_next_question.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D54FF8328E3BFA400A584E3 /* response_quiz_next_question.json */; };
7073
7D91F6F7291BE840005F9D8D /* CIOQuizResultsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D91F6F6291BE840005F9D8D /* CIOQuizResultsResponse.swift */; };
@@ -431,6 +434,9 @@
431434
7D45383F28DC16F400490BFE /* CIOQuizQuestion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuestion.swift; sourceTree = "<group>"; };
432435
7D45384128DC181100490BFE /* CIOQuizImages.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizImages.swift; sourceTree = "<group>"; };
433436
7D45384328DC19A500490BFE /* CIOQuizOption.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizOption.swift; sourceTree = "<group>"; };
437+
7D4E4A3A2E5B77D900D4EF5A /* CIOTrackGenericResultClick.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOTrackGenericResultClick.swift; sourceTree = "<group>"; };
438+
7D4E4A3C2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackGenericResultClickRequestBuilderTests.swift; sourceTree = "<group>"; };
439+
7D4E4A3E2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackGenericResultClickTests.swift; sourceTree = "<group>"; };
434440
7D54FF7F28E3B92000A584E3 /* ConstructorIOQuizTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOQuizTests.swift; sourceTree = "<group>"; };
435441
7D54FF8328E3BFA400A584E3 /* response_quiz_next_question.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = response_quiz_next_question.json; sourceTree = "<group>"; };
436442
7D54FF8528E3C04C00A584E3 /* response_quiz_results.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = response_quiz_results.json; sourceTree = "<group>"; };
@@ -905,6 +911,7 @@
905911
8A5572802A1429930041DB65 /* CIOBrowseGroupsQuery.swift */,
906912
8DCA83DC2A1BD82F00CD2110 /* CIOBrowseFacetsQuery.swift */,
907913
8D0308712A1ED6FC0084AB85 /* CIOBrowseFacetOptionsQuery.swift */,
914+
7D4E4A3A2E5B77D900D4EF5A /* CIOTrackGenericResultClick.swift */,
908915
);
909916
path = Request;
910917
sourceTree = "<group>";
@@ -997,6 +1004,7 @@
9971004
F60FE8FF1F5C5ACD0037A0AB /* Worker */ = {
9981005
isa = PBXGroup;
9991006
children = (
1007+
7D4E4A3E2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift */,
10001008
08A7E4102575FA15000FA02F /* ConstructorIOTrackInputFocusTests.swift */,
10011009
8A93F13F2A70491D000ED6B3 /* ConstructorIOTrackQuizResultClick.swift */,
10021010
0855BA4D25B010CC00CC35F9 /* ConstructorIOIntegrationTests.swift */,
@@ -1052,6 +1060,7 @@
10521060
F60FE9071F5C5B860037A0AB /* Request */ = {
10531061
isa = PBXGroup;
10541062
children = (
1063+
7D4E4A3C2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift */,
10551064
8A93F1452A79B365000ED6B3 /* TrackQuizResultsLoadedRequestBuilder.swift */,
10561065
8A93F1492A79C963000ED6B3 /* TrackQuizConversionRequestBuilder.swift */,
10571066
8A93F1472A79C530000ED6B3 /* TrackQuizResultClickRequestBuilder.swift */,
@@ -2497,6 +2506,7 @@
24972506
F6D2E9CC20E21022007F8761 /* CIOTrackSearchResultsLoadedData.swift in Sources */,
24982507
8A93F13E2A6F44BF000ED6B3 /* CIOTrackQuizResultClickData.swift in Sources */,
24992508
08A7E40F2575E37A000FA02F /* CIOTrackBrowseResultsLoadedData.swift in Sources */,
2509+
7D4E4A3B2E5B77D900D4EF5A /* CIOTrackGenericResultClick.swift in Sources */,
25002510
7D06CCA328E516BC00AB6A8C /* CIOQuizResult.swift in Sources */,
25012511
F650154E1F58724B008E50C0 /* UIColor+RGB.swift in Sources */,
25022512
F60BEA1A1F571956008F0053 /* CIOAutocompleteDelegate.swift in Sources */,
@@ -2625,6 +2635,7 @@
26252635
BFC9502625D206C400118D4C /* RecommendationsQueryRequestBuilderTests.swift in Sources */,
26262636
BFC9504325DC76D900118D4C /* ConstructorIOTrackRecommendationResultsViewTests.swift in Sources */,
26272637
F66B217B21E275EE00AAB030 /* QueryItemCollectionTests.swift in Sources */,
2638+
7D4E4A3D2E5B786800D4EF5A /* TrackGenericResultClickRequestBuilderTests.swift in Sources */,
26282639
089399E12161C51800BFE3D9 /* TrackSearchSubmitRequestBuilder.swift in Sources */,
26292640
08A7E4192575FDA0000FA02F /* ConstructorIOTrackBrowseResultsLoadedTests.swift in Sources */,
26302641
F174C1EB1F5F4D07009E354B /* String+CharacterSetIteratorTests.swift in Sources */,
@@ -2690,6 +2701,7 @@
26902701
F6D2E9C420DBDDBB007F8761 /* Data+ToJSON.swift in Sources */,
26912702
BFC9504125DC6B9100118D4C /* ConstructorIOTrackRecommendationResultClickTests.swift in Sources */,
26922703
F62510D3205690670040E3DF /* ClosureDateProvider.swift in Sources */,
2704+
7D4E4A3F2E5B78B200D4EF5A /* ConstructorIOTrackGenericResultClickTests.swift in Sources */,
26932705
F674157B204D489B004F313B /* MockResponseParserDelegate.swift in Sources */,
26942706
F6852556213BCE9E00A27FAA /* MockCommons.swift in Sources */,
26952707
0855BA4E25B010CC00CC35F9 /* ConstructorIOIntegrationTests.swift in Sources */,

AutocompleteClient/Constants/Constants.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,10 @@ struct Constants {
261261
static let format = "%@/v2/behavioral_action/recommendation_result_click"
262262
}
263263

264+
struct TrackGenericResultClick {
265+
static let format = "%@/v2/behavioral_action/result_click"
266+
}
267+
264268
struct TrackConversion {
265269
static let format = "%@/v2/behavioral_action/conversion"
266270
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// CIOTrackGenericResultClick.swift
3+
// Constructor.io
4+
//
5+
// Copyright (c) Constructor.io Corporation. All rights reserved.
6+
// http://constructor.io/
7+
//
8+
9+
import Foundation
10+
11+
/**
12+
Struct encapsulating the parameters that must/can be set in order to track generic result click
13+
*/
14+
struct CIOTrackGenericResultClick: CIORequestData {
15+
16+
let itemID: String
17+
let itemName: String
18+
let variationID: String?
19+
let sectionName: String?
20+
21+
func url(with baseURL: String) -> String {
22+
return String(format: Constants.TrackGenericResultClick.format, baseURL)
23+
}
24+
25+
init(itemID: String, itemName: String, variationID: String? = nil, sectionName: String? = nil) {
26+
self.itemID = itemID
27+
self.itemName = itemName
28+
self.variationID = variationID
29+
self.sectionName = sectionName
30+
}
31+
32+
func decorateRequest(requestBuilder: RequestBuilder) {}
33+
34+
func httpMethod() -> String {
35+
return "POST"
36+
}
37+
38+
func httpBody(baseParams: [String: Any]) -> Data? {
39+
var dict = [
40+
"item_id": self.itemID,
41+
"item_name": self.itemName
42+
] as [String: Any]
43+
44+
if self.variationID != nil {
45+
dict["variation_id"] = self.variationID
46+
}
47+
if self.sectionName != nil {
48+
dict["section"] = self.sectionName
49+
}
50+
51+
dict["beacon"] = true
52+
dict.merge(baseParams) { current, _ in current }
53+
54+
return try? JSONSerialization.data(withJSONObject: dict)
55+
}
56+
}

AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,28 @@ public class ConstructorIO: CIOSessionManagerDelegate {
740740
executeTracking(request, completionHandler: completionHandler)
741741
}
742742

743+
/**
744+
Track when a user clicks on a generic result
745+
746+
- Parameters:
747+
- itemID: The ID of the item that was clicked
748+
- itemName: The name of the item that was clicked
749+
- variationID: Optional; Variation ID of the item
750+
- sectionName: Optional; The name of the section the product is in
751+
752+
### Usage Example: ###
753+
```
754+
755+
constructorIO.trackGenericResultClick(itemID: "7654321-BA", itemName: "Pencil", variationID: "7654321-BA-738", sectionName: "Products")
756+
```
757+
*/
758+
public func trackGenericResultClick(itemID: String, itemName: String, variationID: String? = nil, sectionName: String? = nil, completionHandler: TrackingCompletionHandler? = nil) {
759+
let section = sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName
760+
let data = CIOTrackGenericResultClick(itemID: itemID, itemName: itemName, variationID: variationID, sectionName: section)
761+
let request = self.buildRequest(data: data)
762+
executeTracking(request, completionHandler: completionHandler)
763+
}
764+
743765
/**
744766
Set a custom clientID
745767

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// TrackGenericResultClickRequestBuilder.swift
3+
// Constructor.io
4+
//
5+
// Copyright (c) Constructor.io Corporation. All rights reserved.
6+
// http://constructor.io/
7+
//
8+
9+
@testable import ConstructorAutocomplete
10+
import XCTest
11+
12+
class TrackGenericResultClickRequestBuilderTests: XCTestCase {
13+
14+
fileprivate let testACKey = "asdf1213123"
15+
fileprivate let itemID = "item123"
16+
fileprivate let itemName = "test item"
17+
fileprivate let variationID = "var123"
18+
fileprivate let sectionName = "Products"
19+
20+
fileprivate var builder: RequestBuilder!
21+
22+
override func setUp() {
23+
super.setUp()
24+
self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString)
25+
}
26+
27+
func testTrackGenericResultClickBuilder() {
28+
let tracker = CIOTrackGenericResultClick(itemID: itemID, itemName: itemName)
29+
builder.build(trackData: tracker)
30+
let request = builder.getRequest()
31+
let payload = try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any]
32+
33+
XCTAssertEqual(request.httpMethod, "POST")
34+
XCTAssertEqual(payload?["item_id"] as? String, itemID)
35+
XCTAssertEqual(payload?["item_name"] as? String, itemName)
36+
XCTAssertEqual(payload?["key"] as? String, testACKey)
37+
XCTAssertTrue((payload?["c"] as? String)!.contains("cioios-"))
38+
}
39+
40+
func testTrackGenericResultClickBuilder_WithVariationId() {
41+
let tracker = CIOTrackGenericResultClick(itemID: itemID, itemName: itemName, variationID: variationID, sectionName: sectionName)
42+
builder.build(trackData: tracker)
43+
let request = builder.getRequest()
44+
let payload = try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any]
45+
46+
XCTAssertEqual(request.httpMethod, "POST")
47+
XCTAssertEqual(payload?["item_id"] as? String, itemID)
48+
XCTAssertEqual(payload?["item_name"] as? String, itemName)
49+
XCTAssertEqual(payload?["variation_id"] as? String, variationID)
50+
XCTAssertEqual(payload?["section"] as? String, sectionName)
51+
XCTAssertEqual(payload?["key"] as? String, testACKey)
52+
XCTAssertTrue((payload?["c"] as? String)!.contains("cioios-"))
53+
}
54+
}

AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,36 @@ class ConstructorIOIntegrationTests: XCTestCase {
8383
self.wait(for: expectation)
8484
}
8585

86+
func testTrackGenericResultClick() {
87+
let expectation = XCTestExpectation(description: "Tracking 204")
88+
self.constructor.trackGenericResultClick(itemID: customerID, itemName: itemName, completionHandler: { response in
89+
let cioError = response.error as? CIOError
90+
XCTAssertNil(cioError)
91+
expectation.fulfill()
92+
})
93+
self.wait(for: expectation)
94+
}
95+
96+
func testTrackGenericResultClick_WithVariationID() {
97+
let expectation = XCTestExpectation(description: "Tracking 204")
98+
self.constructor.trackGenericResultClick(itemID: customerID, itemName: itemName, variationID: "var123", completionHandler: { response in
99+
let cioError = response.error as? CIOError
100+
XCTAssertNil(cioError)
101+
expectation.fulfill()
102+
})
103+
self.wait(for: expectation)
104+
}
105+
106+
func testTrackGenericResultClick_WithSection() {
107+
let expectation = XCTestExpectation(description: "Tracking 204")
108+
self.constructor.trackGenericResultClick(itemID: customerID, itemName: itemName, sectionName: sectionName, completionHandler: { response in
109+
let cioError = response.error as? CIOError
110+
XCTAssertNil(cioError)
111+
expectation.fulfill()
112+
})
113+
self.wait(for: expectation)
114+
}
115+
86116
func testTrackSearchResultsLoaded() {
87117
let expectation = XCTestExpectation(description: "Tracking 204")
88118
self.constructor.trackSearchResultsLoaded(searchTerm: searchTerm, resultCount: resultCount, completionHandler: { response in
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//
2+
// ConstructorIOTrackGenericResultClickTests.swift
3+
// Constructor.io
4+
//
5+
// Copyright (c) Constructor.io Corporation. All rights reserved.
6+
// http://constructor.io/
7+
//
8+
9+
import ConstructorAutocomplete
10+
import OHHTTPStubs
11+
import XCTest
12+
13+
class ConstructorIOTrackGenericResultClickTests: XCTestCase {
14+
15+
var constructor: ConstructorIO!
16+
17+
override func setUp() {
18+
super.setUp()
19+
self.constructor = TestConstants.testConstructor()
20+
}
21+
22+
override func tearDown() {
23+
super.tearDown()
24+
OHHTTPStubs.removeAllStubs()
25+
}
26+
27+
func testTrackGenericResultClick() {
28+
let itemName = "potato"
29+
let itemID = "itemID123"
30+
let variationID = "variationID123"
31+
let section = "Products"
32+
let builder = CIOBuilder(expectation: "Calling trackGenericResultClick should send a valid request", builder: http(200))
33+
stub(regex("https://ac.cnstrc.com/v2/behavioral_action/result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), builder.create())
34+
self.constructor.trackGenericResultClick(itemID: itemID, itemName: itemName, variationID: variationID, sectionName: section)
35+
self.wait(for: builder.expectation)
36+
}
37+
38+
func testTrackGenericResultClick_With400() {
39+
let expectation = self.expectation(description: "Calling trackGenericResultClick with 400 should return badRequest CIOError.")
40+
let itemName = "potato"
41+
let itemID = "itemID123"
42+
let variationID = "variationID123"
43+
let section = "Products"
44+
let builder = CIOBuilder(expectation: "Calling trackGenericResultClick should send a valid request", builder: http(400))
45+
stub(regex("https://ac.cnstrc.com/v2/behavioral_action/result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), builder.create())
46+
self.constructor.trackGenericResultClick(itemID: itemID, itemName: itemName, variationID: variationID, sectionName: section, completionHandler: { response in
47+
if let cioError = response.error as? CIOError {
48+
XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler")
49+
expectation.fulfill()
50+
}
51+
})
52+
self.wait(for: expectation)
53+
}
54+
55+
func testTrackGenericResultClick_With500() {
56+
let expectation = self.expectation(description: "Calling trackGenericResultClick with 500 should return internalServerError CIOError.")
57+
let itemName = "potato"
58+
let itemID = "itemID123"
59+
let variationID = "variationID123"
60+
let section = "Products"
61+
let builder = CIOBuilder(expectation: "Calling trackGenericResultClick should send a valid request", builder: http(500))
62+
stub(regex("https://ac.cnstrc.com/v2/behavioral_action/result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), builder.create())
63+
self.constructor.trackGenericResultClick(itemID: itemID, itemName: itemName, variationID: variationID, sectionName: section, completionHandler: { response in
64+
if let cioError = response.error as? CIOError {
65+
XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler")
66+
expectation.fulfill()
67+
}
68+
})
69+
self.wait(for: expectation)
70+
}
71+
72+
func testTrackGenericResultClick_WithNoConnectivity() {
73+
let expectation = self.expectation(description: "Calling trackGenericResultClick with no connectivity should return noConnectivity CIOError.")
74+
let itemName = "potato"
75+
let itemID = "itemID123"
76+
let variationID = "variationID123"
77+
let section = "Products"
78+
stub(regex("https://ac.cnstrc.com/v2/behavioral_action/result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)&\(TestConstants.defaultSegments)"), noConnectivity())
79+
self.constructor.trackGenericResultClick(itemID: itemID, itemName: itemName, variationID: variationID, sectionName: section, completionHandler: { response in
80+
if let cioError = response.error as? CIOError {
81+
XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler")
82+
expectation.fulfill()
83+
}
84+
})
85+
self.wait(for: expectation)
86+
}
87+
}

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,4 +348,7 @@ constructorIO.trackPurchase(items: purchaseItems, revenue: 93.89, orderID: "423-
348348
```swift
349349
// Track when a product detail page is loaded (a.k.a after a user clicks on an item)
350350
constructorIO.trackItemDetailLoad(customerID: "10001", itemName: "item1", variationID: "var1", sectionName: "Products")
351+
352+
// Track when a generic result is clicked
353+
constructorIO.trackGenericResultClick(itemID: "product123", itemName: "Product Name", variationID: "var456", sectionName: "Products")
351354
```

0 commit comments

Comments
 (0)