Skip to content
16 changes: 16 additions & 0 deletions .jazzy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ skip_undocumented: true
include:
- "/**/Worker/ConstructorIO.swift"
- "/**/Request/*Query.swift"
- "/**/Request/CIOTrackProductInsightsAgent*Request.swift"
- "/**/Request/CIOProductInsightsAgent*.swift"
- "/**/Request/Builder/*QueryBuilder.swift"
- "/**/Result/Responses/*.swift"
- "/**/Result/*.swift"
Expand All @@ -24,6 +26,20 @@ custom_categories:
- CIORecommendationsQuery
- CIOSearchQuery
- CIOQuizQuery
- name: Tracking Request Structures
children:
- CIOTrackProductInsightsAgentFocusRequest
- CIOTrackProductInsightsAgentOutOfViewRequest
- CIOTrackProductInsightsAgentViewRequest
- CIOTrackProductInsightsAgentViewsRequest
- CIOTrackProductInsightsAgentQuestionClickRequest
- CIOTrackProductInsightsAgentQuestionSubmitRequest
- CIOTrackProductInsightsAgentAnswerFeedbackRequest
- CIOTrackProductInsightsAgentResultClickRequest
- CIOTrackProductInsightsAgentAnswerViewRequest
- CIOProductInsightsAgentQuestion
- CIOProductInsightsAgentViewTimespan
- CIOProductInsightsAgentFeedbackLabel
- name: Result Request Builders
children:
- CIOAutocompleteQueryBuilder
Expand Down
160 changes: 160 additions & 0 deletions AutocompleteClient.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

36 changes: 36 additions & 0 deletions AutocompleteClient/Constants/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,42 @@ struct Constants {
static let format = "%@/v2/behavioral_action/assistant_search_submit"
}

struct TrackProductInsightsAgentFocus {
static let format = "%@/v2/behavioral_action/product_insights_agent_focus"
}

struct TrackProductInsightsAgentOutOfView {
static let format = "%@/v2/behavioral_action/product_insights_agent_out_of_view"
}

struct TrackProductInsightsAgentView {
static let format = "%@/v2/behavioral_action/product_insights_agent_view"
}

struct TrackProductInsightsAgentViews {
static let format = "%@/v2/behavioral_action/product_insights_agent_views"
}

struct TrackProductInsightsAgentQuestionClick {
static let format = "%@/v2/behavioral_action/product_insights_agent_question_click"
}

struct TrackProductInsightsAgentQuestionSubmit {
static let format = "%@/v2/behavioral_action/product_insights_agent_question_submit"
}

struct TrackProductInsightsAgentAnswerFeedback {
static let format = "%@/v2/behavioral_action/product_insights_agent_answer_feedback"
}

struct TrackProductInsightsAgentResultClick {
static let format = "%@/v2/behavioral_action/product_insights_agent_result_click"
}

struct TrackProductInsightsAgentAnswerView {
static let format = "%@/v2/behavioral_action/product_insights_agent_answer_view"
}

struct TrackConversion {
static let format = "%@/v2/behavioral_action/conversion"
}
Expand Down
20 changes: 20 additions & 0 deletions AutocompleteClient/FW/Logic/Request/CIOItem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,24 @@ public struct CIOItem {
self.slCampaignID = slCampaignID
self.slCampaignOwner = slCampaignOwner
}

// Builds the item object sent by trackResultsImpressionView and trackProductInsightsAgentAnswerView.
// Other events send different item shapes (purchase repeats each item `quantity` times and sends only item_id/variation_id,
// and ASA/search/browse/recommendations omit item_name)
func toImpressionDictionary() -> [String: String] {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: toImpressionDictionary() is internal (no access modifier) but it's used by two callers: the existing CIOTrackResultsImpressionViewData and the new CIOTrackProductInsightsAgentAnswerViewData. The method's comment accurately describes the shared shape. Consider whether internal is the right default here versus making it fileprivate — but given cross-file usage is intentional, internal is correct.

One minor naming nit: the method is on CIOItem but the shape it produces is also used by PIA's answer_view event. The existing comment (// Builds the item object sent by trackResultsImpressionView and trackProductInsightsAgentAnswerView) has been updated to reflect the two callers, which is good. No change required.

var dict = ["item_id": self.customerID]
if let itemName = self.itemName {
dict["item_name"] = itemName
}
if let variationID = self.variationID {
dict["variation_id"] = variationID
}
if let campaignID = self.slCampaignID {
dict["sl_campaign_id"] = campaignID
}
if let campaignOwner = self.slCampaignOwner {
dict["sl_campaign_owner"] = campaignOwner
}
return dict
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//
// CIOProductInsightsAgentFeedbackLabel.swift
// Constructor.io
//
// Copyright (c) Constructor.io Corporation. All rights reserved.
// http://constructor.io/
//

import Foundation

/**
Enum representing the feedback a user can give on a Product Insights Agent answer
*/
public enum CIOProductInsightsAgentFeedbackLabel: String {

/**
The user found the answer useful
*/
case thumbsUp = "thumbs_up"

/**
The user did not find the answer useful
*/
case thumbsDown = "thumbs_down"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
//
// CIOProductInsightsAgentQuestion.swift
// Constructor.io
//
// Copyright (c) Constructor.io Corporation. All rights reserved.
// http://constructor.io/
//

import Foundation

/**
Struct representing a question shown to the user by the Product Insights Agent
*/
public struct CIOProductInsightsAgentQuestion {

/**
The question text (1-1000 characters)
*/
public let question: String

/**
The topic category of the question
*/
public let questionTopic: String?

/**
Create a Product Insights Agent question

- Parameters:
- question: The question text (1-1000 characters)
- questionTopic: The topic category of the question
*/
public init(question: String, questionTopic: String? = nil) {
self.question = question
self.questionTopic = questionTopic
}

func toDictionary() -> [String: String] {
var dict = ["question": self.question]
if let questionTopic = self.questionTopic {
dict["question_topic"] = questionTopic
}
return dict
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
//
// CIOProductInsightsAgentViewTimespan.swift
// Constructor.io
//
// Copyright (c) Constructor.io Corporation. All rights reserved.
// http://constructor.io/
//

import Foundation

/**
Struct representing a period of time during which the Product Insights Agent was visible to the user
*/
public struct CIOProductInsightsAgentViewTimespan {

/**
The time the Product Insights Agent became visible
*/
public let start: Date

/**
The time the Product Insights Agent stopped being visible
*/
public let end: Date

/**
Create a Product Insights Agent view timespan

- Parameters:
- start: The time the Product Insights Agent became visible
- end: The time the Product Insights Agent stopped being visible
*/
public init(start: Date, end: Date) {
self.start = start
self.end = end
}

// A fixed-format DateFormatter is used instead of ISO8601DateFormatter because setting
// `.withFractionalSeconds` throws an exception on iOS 11.0 and 11.1. DateFormatter is thread-safe,
// so a single shared instance is used for all timespans
static let dateFormatter: DateFormatter = {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggestion: DateFormatter is documented as thread-safe for string(from:) calls, and the comment in the code references this. That said, the comment currently says "DateFormatter is thread-safe, so a single shared instance is used for all timespans". Consider clarifying the specific thread-safety scope: DateFormatter is thread-safe for reading (formatting) after initial creation, but not for writes (changing dateFormat, locale, etc.). Since this formatter is static let with a fixed configuration and never mutated after creation, it is safe. A brief comment noting that the formatter must not be mutated after creation would guard against someone inadvertently adding formatter.dateFormat = ... elsewhere.

This is a documentation nit only — the current code is correct.

let formatter = DateFormatter()
formatter.locale = Locale(identifier: "en_US_POSIX")
formatter.calendar = Calendar(identifier: .gregorian)
formatter.timeZone = TimeZone(secondsFromGMT: 0)
formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
return formatter
}()

// Dates are formatted as ISO-8601 strings in UTC (e.g. "2026-10-22T02:40:05.123Z") so the body stays JSON-serializable
func toDictionary() -> [String: String] {
return [
"start": CIOProductInsightsAgentViewTimespan.dateFormatter.string(from: self.start),
"end": CIOProductInsightsAgentViewTimespan.dateFormatter.string(from: self.end)
]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
//
// CIOTrackProductInsightsAgentAnswerFeedbackData.swift
// Constructor.io
//
// Copyright (c) Constructor.io Corporation. All rights reserved.
// http://constructor.io/
//

import Foundation

/**
Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent answer feedback
*/
struct CIOTrackProductInsightsAgentAnswerFeedbackData: CIOTrackProductInsightsAgentData {

let itemID: String
let feedbackLabel: CIOProductInsightsAgentFeedbackLabel
let qnaResultID: String?
let itemName: String?
let variationID: String?
let threadID: String?
let sectionName: String?
let analyticsTags: [String: String]?

func url(with baseURL: String) -> String {
return String(format: Constants.TrackProductInsightsAgentAnswerFeedback.format, baseURL)
}

init(itemID: String, feedbackLabel: CIOProductInsightsAgentFeedbackLabel, qnaResultID: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) {
self.itemID = itemID
self.feedbackLabel = feedbackLabel
self.qnaResultID = qnaResultID
self.itemName = itemName
self.variationID = variationID
self.threadID = threadID
self.sectionName = sectionName
self.analyticsTags = analyticsTags
}

func eventFields() -> [String: Any] {
var dict = [
"feedback_label": self.feedbackLabel.rawValue
] as [String: Any]

if let qnaResultID = self.qnaResultID {
dict["qna_result_id"] = qnaResultID
}

return dict
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//
// CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift
// Constructor.io
//
// Copyright (c) Constructor.io Corporation. All rights reserved.
// http://constructor.io/
//

import Foundation

/**
Struct encapsulating the parameters that can be passed to constructorIO.trackProductInsightsAgentAnswerFeedback() function.
*/
public struct CIOTrackProductInsightsAgentAnswerFeedbackRequest {

/**
The ID of the product whose page the Product Insights Agent is on
*/
public let itemID: String

/**
The feedback the user gave on the answer
*/
public let feedbackLabel: CIOProductInsightsAgentFeedbackLabel

/**
The ID of the answer result returned by the Product Insights Agent
*/
public let qnaResultID: String?

/**
The name of the product whose page the Product Insights Agent is on
*/
public let itemName: String?

/**
The variation ID of the product whose page the Product Insights Agent is on
*/
public let variationID: String?

/**
The ID used to group events within a single conversation thread
*/
public let threadID: String?

/**
The name of the section the item belongs to (defaults to the configured default section, or "Products")
*/
public let sectionName: String?

/**
Additional custom analytics tags to be sent with the event. Merged with the default analytics tags
*/
public let analyticsTags: [String: String]?

/**
Create a Product Insights Agent answer feedback request

- Parameters:
- itemID: The ID of the product whose page the Product Insights Agent is on
- feedbackLabel: The feedback the user gave on the answer
- qnaResultID: The ID of the answer result returned by the Product Insights Agent
- itemName: The name of the product whose page the Product Insights Agent is on
- variationID: The variation ID of the product whose page the Product Insights Agent is on
- threadID: The ID used to group events within a single conversation thread
- sectionName: The name of the section the item belongs to (defaults to the configured default section, or "Products")
- analyticsTags: Additional custom analytics tags to be sent with the event. Merged with the default analytics tags
*/
public init(itemID: String, feedbackLabel: CIOProductInsightsAgentFeedbackLabel, qnaResultID: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) {
self.itemID = itemID
self.feedbackLabel = feedbackLabel
self.qnaResultID = qnaResultID
self.itemName = itemName
self.variationID = variationID
self.threadID = threadID
self.sectionName = sectionName
self.analyticsTags = analyticsTags
}
}
Loading
Loading