From 78d85aa823fcf285535522c9f257ddc4c9694037 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 06:04:02 +0800 Subject: [PATCH 01/10] [CDX-708] Add the 9 Product Insights Agent tracking events Add trackProductInsightsAgent{Focus,OutOfView,View,Views,QuestionClick, QuestionSubmit,AnswerFeedback,ResultClick,AnswerView} with a public request struct per event, plus the CIOProductInsightsAgentQuestion, CIOProductInsightsAgentViewTimespan and CIOProductInsightsAgentFeedbackLabel models. - section is sent as a query param, resolved from the request, the config default, then "Products" - analyticsTags are merged with config.defaultAnalyticsTags - view timespans are sent as ISO-8601 UTC date-times - items and followUpQuestions are capped at 100; items never send quantity - request-builder, worker and live integration tests for every event Co-Authored-By: Claude Opus 5.5 --- AutocompleteClient.xcodeproj/project.pbxproj | 156 ++++++++++++++ AutocompleteClient/Constants/Constants.swift | 36 ++++ ...CIOProductInsightsAgentFeedbackLabel.swift | 25 +++ .../CIOProductInsightsAgentQuestion.swift | 45 ++++ .../CIOProductInsightsAgentViewTimespan.swift | 53 +++++ ...oductInsightsAgentAnswerFeedbackData.swift | 75 +++++++ ...ctInsightsAgentAnswerFeedbackRequest.swift | 79 +++++++ ...ckProductInsightsAgentAnswerViewData.swift | 113 ++++++++++ ...roductInsightsAgentAnswerViewRequest.swift | 107 +++++++++ ...IOTrackProductInsightsAgentFocusData.swift | 67 ++++++ ...rackProductInsightsAgentFocusRequest.swift | 65 ++++++ ...ackProductInsightsAgentOutOfViewData.swift | 67 ++++++ ...ProductInsightsAgentOutOfViewRequest.swift | 65 ++++++ ...roductInsightsAgentQuestionClickData.swift | 75 +++++++ ...uctInsightsAgentQuestionClickRequest.swift | 79 +++++++ ...oductInsightsAgentQuestionSubmitData.swift | 75 +++++++ ...ctInsightsAgentQuestionSubmitRequest.swift | 79 +++++++ ...kProductInsightsAgentResultClickData.swift | 93 ++++++++ ...oductInsightsAgentResultClickRequest.swift | 107 +++++++++ ...CIOTrackProductInsightsAgentViewData.swift | 70 ++++++ ...TrackProductInsightsAgentViewRequest.swift | 72 +++++++ ...IOTrackProductInsightsAgentViewsData.swift | 73 +++++++ ...rackProductInsightsAgentViewsRequest.swift | 79 +++++++ .../FW/Logic/Worker/ConstructorIO.swift | 180 ++++++++++++++++ ...entAnswerFeedbackRequestBuilderTests.swift | 80 +++++++ ...tsAgentAnswerViewRequestBuilderTests.swift | 139 ++++++++++++ ...nsightsAgentFocusRequestBuilderTests.swift | 66 ++++++ ...htsAgentOutOfViewRequestBuilderTests.swift | 66 ++++++ ...gentQuestionClickRequestBuilderTests.swift | 72 +++++++ ...entQuestionSubmitRequestBuilderTests.swift | 72 +++++++ ...sAgentResultClickRequestBuilderTests.swift | 92 ++++++++ ...InsightsAgentViewRequestBuilderTests.swift | 69 ++++++ ...nsightsAgentViewsRequestBuilderTests.swift | 95 ++++++++ .../ConstructorIOIntegrationTests.swift | 99 +++++++++ ...ductInsightsAgentAnswerFeedbackTests.swift | 157 ++++++++++++++ ...kProductInsightsAgentAnswerViewTests.swift | 204 ++++++++++++++++++ ...OTrackProductInsightsAgentFocusTests.swift | 151 +++++++++++++ ...ckProductInsightsAgentOutOfViewTests.swift | 151 +++++++++++++ ...oductInsightsAgentQuestionClickTests.swift | 157 ++++++++++++++ ...ductInsightsAgentQuestionSubmitTests.swift | 157 ++++++++++++++ ...ProductInsightsAgentResultClickTests.swift | 184 ++++++++++++++++ ...IOTrackProductInsightsAgentViewTests.swift | 154 +++++++++++++ ...OTrackProductInsightsAgentViewsTests.swift | 157 ++++++++++++++ 43 files changed, 4257 insertions(+) create mode 100644 AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentFeedbackLabel.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentQuestion.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewRequest.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsRequest.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift create mode 100644 AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift diff --git a/AutocompleteClient.xcodeproj/project.pbxproj b/AutocompleteClient.xcodeproj/project.pbxproj index 4ed4cf0c..ce9d011c 100644 --- a/AutocompleteClient.xcodeproj/project.pbxproj +++ b/AutocompleteClient.xcodeproj/project.pbxproj @@ -58,14 +58,25 @@ 08C1E6072193C29C00A2E24E /* TrackPurchaseRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08C1E6062193C29C00A2E24E /* TrackPurchaseRequestBuilderTests.swift */; }; 08FFB76C215EBBF8008CAA7D /* CIOTrackAutocompleteSelectData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FFB76B215EBBF8008CAA7D /* CIOTrackAutocompleteSelectData.swift */; }; 08FFB76E215EC1E2008CAA7D /* CIOTrackSearchSubmitData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08FFB76D215EC1DF008CAA7D /* CIOTrackSearchSubmitData.swift */; }; + 0AAC5669EDB5FED3712BDD03 /* ConstructorIOTrackProductInsightsAgentViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8A8AE47CB7C51A27D92005E /* ConstructorIOTrackProductInsightsAgentViewTests.swift */; }; 0D6F0DA342504F8D1DB657B5 /* TrackAgentResultViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD016CFF1317FE8714751737 /* TrackAgentResultViewRequestBuilderTests.swift */; }; 120264B1D888913DB32E9BDD /* ConstructorIOTrackAgentResultLoadStartedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2B2AB45915473F18E0232C00 /* ConstructorIOTrackAgentResultLoadStartedTests.swift */; }; + 12856114DA0E06D22F228AF4 /* CIOTrackProductInsightsAgentQuestionSubmitRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DADC00EC0CF7961F94F3DF15 /* CIOTrackProductInsightsAgentQuestionSubmitRequest.swift */; }; 12E9B333053CDAF000D91898 /* ConstructorIOTrackAgentSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9285ABCDB8F27B257DC85C7 /* ConstructorIOTrackAgentSubmitTests.swift */; }; 13D84626E02649E5690DEDBE /* ConstructorIOTrackAgentResultViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 14F639EF2F5B572DE847C09B /* ConstructorIOTrackAgentResultViewTests.swift */; }; + 151BD639718ABAB1CB984AA7 /* CIOProductInsightsAgentQuestion.swift in Sources */ = {isa = PBXBuildFile; fileRef = A9C0EF1F8EE9EC8B9A8B5F73 /* CIOProductInsightsAgentQuestion.swift */; }; + 287F2A489FC50C1FD4A33FAF /* TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BE11F4EB73A03F056BC1153E /* TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift */; }; 2D161DBA81321B1026F1BA16 /* TrackAgentSubmitRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 342A61B41033F3637499EC14 /* TrackAgentSubmitRequestBuilderTests.swift */; }; + 2F178AD4206D3F6F6824A75C /* CIOTrackProductInsightsAgentFocusData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09BADEEBF39C071120BBF365 /* CIOTrackProductInsightsAgentFocusData.swift */; }; 30DC8B3897E361EB4ADC91D5 /* ConstructorIOTrackAgentResultClickTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6B647AA1B70A24E3B043AB6A /* ConstructorIOTrackAgentResultClickTests.swift */; }; + 436758DF0758AE28441118AF /* CIOTrackProductInsightsAgentAnswerFeedbackData.swift in Sources */ = {isa = PBXBuildFile; fileRef = F9CBD5E534369A2055DA24AA /* CIOTrackProductInsightsAgentAnswerFeedbackData.swift */; }; 449B02753FBBE5501BD3269B /* ConstructorIOTrackAgentSearchSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92E18BD16D9DFC799A5BC5D5 /* ConstructorIOTrackAgentSearchSubmitTests.swift */; }; + 4F2D9EF7C91B101E074D7E8D /* TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84E5F118001792BF54984EB /* TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift */; }; 4F30F4822C4C805F450EB488 /* CIOTrackAgentSearchSubmitData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 54889113E31C87DDB613F090 /* CIOTrackAgentSearchSubmitData.swift */; }; + 5188B65D1B10B35BABCCFE32 /* TrackProductInsightsAgentViewsRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 885ABDD759DA765CC2F840E2 /* TrackProductInsightsAgentViewsRequestBuilderTests.swift */; }; + 55A08346890C4997406C3842 /* ConstructorIOTrackProductInsightsAgentResultClickTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D0BB2A5C31AC46886069B7A /* ConstructorIOTrackProductInsightsAgentResultClickTests.swift */; }; + 5BA5481625CA59D9C488B5A6 /* ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92147D6EF3B93DB58A00F37D /* ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift */; }; + 5EF1CD01D67398A03CF98731 /* TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A049DC718BFF97C061C7A116 /* TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift */; }; 5F12F5692FDAE58B002945A3 /* CIOTrackResultsImpressionViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F12F5682FDAE58B002945A3 /* CIOTrackResultsImpressionViewData.swift */; }; 5F12F56B2FDAF53A002945A3 /* TrackResultsImpressionViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F12F56A2FDAF53A002945A3 /* TrackResultsImpressionViewRequestBuilderTests.swift */; }; 5F12F56D2FDAF555002945A3 /* ConstructorIOTrackResultsImpressionViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F12F56C2FDAF555002945A3 /* ConstructorIOTrackResultsImpressionViewTests.swift */; }; @@ -74,8 +85,13 @@ 5FEE54822F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE547E2F61E25E00A74E64 /* TrackMediaImpressionViewRequestBuilderTests.swift */; }; 5FEE54842F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */; }; 651F5B9E3E0C5CC7A2090EC8 /* Pods_AutocompleteClientTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C8134D150C9D4CC91DD5715F /* Pods_AutocompleteClientTests.framework */; }; + 6BC60CF71388EA7445B00159 /* ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 97B0EE92FCAC2A60F7607A5E /* ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift */; }; + 702AD679D59CB6074872D4AD /* CIOTrackProductInsightsAgentOutOfViewRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5D17AD8DAF395F6E7F5D9EA6 /* CIOTrackProductInsightsAgentOutOfViewRequest.swift */; }; 7348FD5306045E1BE067F8E4 /* CIOTrackAgentResultLoadFinishedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D00CDA834603F215E1B1457 /* CIOTrackAgentResultLoadFinishedData.swift */; }; + 75A15E0E959A2263749A24D0 /* CIOTrackProductInsightsAgentQuestionSubmitData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 061E4BC68F1EDC002BE25C75 /* CIOTrackProductInsightsAgentQuestionSubmitData.swift */; }; 77A50DEFD10E34705F1925E0 /* TrackAgentResultLoadFinishedRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1DD9138622D7CACE62DE89F /* TrackAgentResultLoadFinishedRequestBuilderTests.swift */; }; + 7992ADFD7873CA7C61FA6F9F /* CIOTrackProductInsightsAgentAnswerViewRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92323150DDB4DCC6ABB61318 /* CIOTrackProductInsightsAgentAnswerViewRequest.swift */; }; + 7AA8B287A23F3EFA49B57119 /* CIOTrackProductInsightsAgentResultClickRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B189C70622D60B6107475B9D /* CIOTrackProductInsightsAgentResultClickRequest.swift */; }; 7D06CCA128E5122B00AB6A8C /* response_quiz_results.json in Resources */ = {isa = PBXBuildFile; fileRef = 7D54FF8528E3C04C00A584E3 /* response_quiz_results.json */; }; 7D06CCA328E516BC00AB6A8C /* CIOQuizResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7D06CCA228E516BC00AB6A8C /* CIOQuizResult.swift */; }; 7D0F5EAB01E5BD5582866F96 /* CIOTrackAgentResultClickData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A32D86CF3E9CB3706BD4996 /* CIOTrackAgentResultClickData.swift */; }; @@ -97,6 +113,10 @@ 7DDA7D9328DCF513004C8AC8 /* AbstractQuizQuestionResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDA7D9228DCF513004C8AC8 /* AbstractQuizQuestionResponseParser.swift */; }; 7DDA7D9528DCF5B7004C8AC8 /* QuizQuestionResponseParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDA7D9428DCF5B7004C8AC8 /* QuizQuestionResponseParser.swift */; }; 7DDA7D9728DCFDC4004C8AC8 /* QuizzesQueryRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7DDA7D9628DCFDC4004C8AC8 /* QuizzesQueryRequestBuilderTests.swift */; }; + 7E88F825FDEB47B44CCE9C13 /* CIOTrackProductInsightsAgentViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76CE3D18DD769396D19AE44F /* CIOTrackProductInsightsAgentViewData.swift */; }; + 7F4EFC5A6328D2A6211DFB92 /* CIOTrackProductInsightsAgentViewsData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6FC0FD8D7D1299CFE25B0E84 /* CIOTrackProductInsightsAgentViewsData.swift */; }; + 8432A57AEDC0EDF4BD2A52A5 /* ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4D0B2FA4EDB0AF90CCD5CE25 /* ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift */; }; + 89763179C7ADBED811F268DD /* CIOTrackProductInsightsAgentFocusRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2E5687AB79AC0D2B97A4F26 /* CIOTrackProductInsightsAgentFocusRequest.swift */; }; 8A5572812A1429930041DB65 /* CIOBrowseGroupsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A5572802A1429930041DB65 /* CIOBrowseGroupsQuery.swift */; }; 8A5572832A1433350041DB65 /* CIOBrowseGroupsQueryBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A5572822A1433350041DB65 /* CIOBrowseGroupsQueryBuilder.swift */; }; 8A5572852A143E840041DB65 /* ConstructorIOBrowseGroupsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A5572842A143E840041DB65 /* ConstructorIOBrowseGroupsTests.swift */; }; @@ -141,8 +161,15 @@ 8DCA83EB2A1C067F00CD2110 /* BrowseFacetsTaskResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DCA83EA2A1C067F00CD2110 /* BrowseFacetsTaskResponse.swift */; }; 8DF2172D2888AE5E006384A5 /* CIOResultSources.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2172C2888AE5E006384A5 /* CIOResultSources.swift */; }; 8DF2172F2888B56D006384A5 /* CIOResultSourceData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8DF2172E2888B56D006384A5 /* CIOResultSourceData.swift */; }; + 914E0500EDC9AAD8ABBE4F68 /* ConstructorIOTrackProductInsightsAgentFocusTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 95B42E8D2567188BA1F71B26 /* ConstructorIOTrackProductInsightsAgentFocusTests.swift */; }; + 9217BA423BAEC4DE24CE7323 /* CIOProductInsightsAgentFeedbackLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3E15EB900F309DFC8E1CDD61 /* CIOProductInsightsAgentFeedbackLabel.swift */; }; + 977D116BA3000898EE0F1C6F /* CIOTrackProductInsightsAgentQuestionClickRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 19B352A04190627348E9FEA3 /* CIOTrackProductInsightsAgentQuestionClickRequest.swift */; }; 99D6877853DE657CAF7E876C /* Pods_UserApplication.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D938EAC4478309F3008F3B0D /* Pods_UserApplication.framework */; }; + 9F60E3A4CAB308554487D645 /* ConstructorIOTrackProductInsightsAgentViewsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DF54EC50588B3DA1C99AE3B /* ConstructorIOTrackProductInsightsAgentViewsTests.swift */; }; + A5D7039D9A00FCDCF7CE2CEE /* CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F794A697D6C9A78EA3F2C3EA /* CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift */; }; AE94DB499481274AB9C2EC8B /* CIOTrackAgentResultViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B64FEEB5E72E52585E8F498 /* CIOTrackAgentResultViewData.swift */; }; + BABFD40203A95945AD2F46C3 /* CIOTrackProductInsightsAgentQuestionClickData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 75C1A954AFF8D2C3A6CF5EC9 /* CIOTrackProductInsightsAgentQuestionClickData.swift */; }; + BBB2E127B5674B3A1F0D5CD8 /* TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866BFFAB70ADF6DF794B2DF /* TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift */; }; BF02E29F28B5739C0062B44F /* CIOItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF02E29E28B5739C0062B44F /* CIOItem.swift */; }; BF12196825D1F06200496189 /* CIORecommendationsQuery.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF12196725D1F06200496189 /* CIORecommendationsQuery.swift */; }; BF12196A25D1FD5700496189 /* CIORecommendationsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = BF12196925D1FD5700496189 /* CIORecommendationsResponse.swift */; }; @@ -172,10 +199,19 @@ BFE1C72D2602CF0E0017B4A0 /* ConstructorIOUserSegmentsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE1C72C2602CF0E0017B4A0 /* ConstructorIOUserSegmentsTests.swift */; }; BFE918F528DB9721005D92D8 /* CIORefinedContent.swift in Sources */ = {isa = PBXBuildFile; fileRef = BFE918F428DB9721005D92D8 /* CIORefinedContent.swift */; }; BFE918F928DBB727005D92D8 /* response_search_json_refined_content.json in Resources */ = {isa = PBXBuildFile; fileRef = BFE918F828DBB4AC005D92D8 /* response_search_json_refined_content.json */; }; + C3BEA3561A5FD6C91942A9EC /* TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9878B034B60AED6EBF4B610A /* TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift */; }; + C48F21785156E7D3D4A94DF8 /* TrackProductInsightsAgentFocusRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D57364981A16D18B682C8F7 /* TrackProductInsightsAgentFocusRequestBuilderTests.swift */; }; + CDA43CFAE25B8C354ADDCE39 /* CIOTrackProductInsightsAgentOutOfViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 25D9704C68C3A419912C856D /* CIOTrackProductInsightsAgentOutOfViewData.swift */; }; + CF83F2B8A45DE57007BA43A3 /* CIOTrackProductInsightsAgentViewsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 78F25B5E1835A7C2C32A3D8D /* CIOTrackProductInsightsAgentViewsRequest.swift */; }; D4C8CAA080A9F0747D32A2C6 /* CIOTrackAgentSubmitData.swift in Sources */ = {isa = PBXBuildFile; fileRef = C2B16E34123AB13AE715C823 /* CIOTrackAgentSubmitData.swift */; }; + D4DA80E74933A0242D69111C /* CIOTrackProductInsightsAgentViewRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 91FA523A8C110147035C3D17 /* CIOTrackProductInsightsAgentViewRequest.swift */; }; D70C13C42EDD31D56EFECF6B /* CIOTrackAgentResultLoadStartedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = CD68EBF1F36134FE6C500E50 /* CIOTrackAgentResultLoadStartedData.swift */; }; + DF5314F3EC1D042F38FB2736 /* CIOTrackProductInsightsAgentResultClickData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 36C901176A992CFD86A40827 /* CIOTrackProductInsightsAgentResultClickData.swift */; }; E749F804FA6619483A107E70 /* TrackAgentResultClickRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FABF1A9FFAFEDA72E638A2FA /* TrackAgentResultClickRequestBuilderTests.swift */; }; EABEC0CDDCFC4A7FF049E1FB /* TrackAgentResultLoadStartedRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E779F24A02A46FA5300DD8D /* TrackAgentResultLoadStartedRequestBuilderTests.swift */; }; + ED7C45224157AD7A5E39C16B /* ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7EE0186DB9833CE0382C0F41 /* ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift */; }; + F14E8EAC391CD86273FBF66C /* CIOTrackProductInsightsAgentAnswerViewData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DC4701329FBCAC12F65D2F2 /* CIOTrackProductInsightsAgentAnswerViewData.swift */; }; + F15620AF69178D8BA6397A43 /* ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 08A0B99FF8B0533C02E42727 /* ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift */; }; F174C1EB1F5F4D07009E354B /* String+CharacterSetIteratorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174C1EA1F5F4D07009E354B /* String+CharacterSetIteratorTests.swift */; }; F174C1ED1F5F5AC1009E354B /* NSString+RangeOfCharactersTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F174C1EC1F5F5AC1009E354B /* NSString+RangeOfCharactersTest.swift */; }; F192DDEE1F5F123400114C8C /* RequestBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = F192DDED1F5F123400114C8C /* RequestBuilder.swift */; }; @@ -189,6 +225,7 @@ F1F68DE51F58BBA400B42602 /* String+CharacterSetIterator.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F68DE41F58BBA400B42602 /* String+CharacterSetIterator.swift */; }; F1F68DE71F58C78C00B42602 /* ConstructorIO.swift in Sources */ = {isa = PBXBuildFile; fileRef = F1F68DE61F58C78C00B42602 /* ConstructorIO.swift */; }; F348DBB2C440A7E34E0159DF /* ConstructorIOTrackAgentResultLoadFinishedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E515E2A713406EB70485D4BF /* ConstructorIOTrackAgentResultLoadFinishedTests.swift */; }; + F424450F56E9454EDD49EC9B /* CIOProductInsightsAgentViewTimespan.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76B153A7ECC09624463C16D7 /* CIOProductInsightsAgentViewTimespan.swift */; }; F60BEA1A1F571956008F0053 /* CIOAutocompleteDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60BEA191F571956008F0053 /* CIOAutocompleteDelegate.swift */; }; F60BEA1D1F571C5C008F0053 /* SearchResultCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60BEA1C1F571C5C008F0053 /* SearchResultCell.swift */; }; F60BEA201F5726CB008F0053 /* EmptyScreenView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F60BEA1F1F5726CB008F0053 /* EmptyScreenView.swift */; }; @@ -367,6 +404,8 @@ F6F1D707234B693D0092341D /* response_search_json_variations.json in Resources */ = {isa = PBXBuildFile; fileRef = F6F1D706234B693D0092341D /* response_search_json_variations.json */; }; F6F1D709234B6BFA0092341D /* response_search_json_invalid_variations.json in Resources */ = {isa = PBXBuildFile; fileRef = F6F1D708234B6BFA0092341D /* response_search_json_invalid_variations.json */; }; F6F1D70B234B6FD50092341D /* response_search_json_variation_custom_data.json in Resources */ = {isa = PBXBuildFile; fileRef = F6F1D70A234B6FD50092341D /* response_search_json_variation_custom_data.json */; }; + FC5DEABC0479238454C51141 /* TrackProductInsightsAgentViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD854D16E22B01230B750A2 /* TrackProductInsightsAgentViewRequestBuilderTests.swift */; }; + FFBFF2D9B43441E34588BFC7 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D4419F69611F6AE5E1AF63 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -401,6 +440,7 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 061E4BC68F1EDC002BE25C75 /* CIOTrackProductInsightsAgentQuestionSubmitData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentQuestionSubmitData.swift; sourceTree = ""; }; 08289538256C5A84009A00BC /* CIOSortOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOSortOption.swift; sourceTree = ""; }; 08289539256C5A85009A00BC /* CIOFilterFacetOption.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOFilterFacetOption.swift; sourceTree = ""; }; 0828953A256C5A85009A00BC /* CIOFilterFacet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOFilterFacet.swift; sourceTree = ""; }; @@ -438,6 +478,7 @@ 089399DE2161B92600BFE3D9 /* TrackInputFocusRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackInputFocusRequestBuilderTests.swift; sourceTree = ""; }; 089399E02161C51800BFE3D9 /* TrackSearchSubmitRequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackSearchSubmitRequestBuilder.swift; sourceTree = ""; }; 089399E22161C67D00BFE3D9 /* TrackSearchResultsLoadedRequestBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackSearchResultsLoadedRequestBuilder.swift; sourceTree = ""; }; + 08A0B99FF8B0533C02E42727 /* ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift; sourceTree = ""; }; 08A7E40C2575E37A000FA02F /* CIOTrackBrowseResultClickData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOTrackBrowseResultClickData.swift; sourceTree = ""; }; 08A7E40D2575E37A000FA02F /* CIOTrackBrowseResultsLoadedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOTrackBrowseResultsLoadedData.swift; sourceTree = ""; }; 08A7E4102575FA15000FA02F /* ConstructorIOTrackInputFocusTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackInputFocusTests.swift; sourceTree = ""; }; @@ -451,13 +492,25 @@ 08C1E6062193C29C00A2E24E /* TrackPurchaseRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackPurchaseRequestBuilderTests.swift; sourceTree = ""; }; 08FFB76B215EBBF8008CAA7D /* CIOTrackAutocompleteSelectData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOTrackAutocompleteSelectData.swift; sourceTree = ""; }; 08FFB76D215EC1DF008CAA7D /* CIOTrackSearchSubmitData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CIOTrackSearchSubmitData.swift; sourceTree = ""; }; + 09BADEEBF39C071120BBF365 /* CIOTrackProductInsightsAgentFocusData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentFocusData.swift; sourceTree = ""; }; + 0D0BB2A5C31AC46886069B7A /* ConstructorIOTrackProductInsightsAgentResultClickTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentResultClickTests.swift; sourceTree = ""; }; + 0DC4701329FBCAC12F65D2F2 /* CIOTrackProductInsightsAgentAnswerViewData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentAnswerViewData.swift; sourceTree = ""; }; + 0DF54EC50588B3DA1C99AE3B /* ConstructorIOTrackProductInsightsAgentViewsTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentViewsTests.swift; sourceTree = ""; }; 14F639EF2F5B572DE847C09B /* ConstructorIOTrackAgentResultViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentResultViewTests.swift; sourceTree = ""; }; + 19B352A04190627348E9FEA3 /* CIOTrackProductInsightsAgentQuestionClickRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentQuestionClickRequest.swift; sourceTree = ""; }; + 25D9704C68C3A419912C856D /* CIOTrackProductInsightsAgentOutOfViewData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentOutOfViewData.swift; sourceTree = ""; }; 2B2AB45915473F18E0232C00 /* ConstructorIOTrackAgentResultLoadStartedTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentResultLoadStartedTests.swift; sourceTree = ""; }; + 31D4419F69611F6AE5E1AF63 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentResultClickRequestBuilderTests.swift; sourceTree = ""; }; 342A61B41033F3637499EC14 /* TrackAgentSubmitRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentSubmitRequestBuilderTests.swift; sourceTree = ""; }; + 36C901176A992CFD86A40827 /* CIOTrackProductInsightsAgentResultClickData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentResultClickData.swift; sourceTree = ""; }; + 3D57364981A16D18B682C8F7 /* TrackProductInsightsAgentFocusRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentFocusRequestBuilderTests.swift; sourceTree = ""; }; + 3E15EB900F309DFC8E1CDD61 /* CIOProductInsightsAgentFeedbackLabel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOProductInsightsAgentFeedbackLabel.swift; sourceTree = ""; }; 453FBD0FBF9C8817B960B758 /* Pods-UserApplication.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-UserApplication.release.xcconfig"; path = "Pods/Target Support Files/Pods-UserApplication/Pods-UserApplication.release.xcconfig"; sourceTree = ""; }; 4D00CDA834603F215E1B1457 /* CIOTrackAgentResultLoadFinishedData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackAgentResultLoadFinishedData.swift; sourceTree = ""; }; + 4D0B2FA4EDB0AF90CCD5CE25 /* ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift; sourceTree = ""; }; 5362DE84EA35B4B9FE26EE1F /* Pods_AutocompleteClient.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AutocompleteClient.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 54889113E31C87DDB613F090 /* CIOTrackAgentSearchSubmitData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackAgentSearchSubmitData.swift; sourceTree = ""; }; + 5D17AD8DAF395F6E7F5D9EA6 /* CIOTrackProductInsightsAgentOutOfViewRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentOutOfViewRequest.swift; sourceTree = ""; }; 5F12F5682FDAE58B002945A3 /* CIOTrackResultsImpressionViewData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOTrackResultsImpressionViewData.swift; sourceTree = ""; }; 5F12F56A2FDAF53A002945A3 /* TrackResultsImpressionViewRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrackResultsImpressionViewRequestBuilderTests.swift; sourceTree = ""; }; 5F12F56C2FDAF555002945A3 /* ConstructorIOTrackResultsImpressionViewTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackResultsImpressionViewTests.swift; sourceTree = ""; }; @@ -467,6 +520,11 @@ 5FEE54832F61E28500A74E64 /* ConstructorIOTrackMediaImpressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackMediaImpressionTests.swift; sourceTree = ""; }; 6B647AA1B70A24E3B043AB6A /* ConstructorIOTrackAgentResultClickTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentResultClickTests.swift; sourceTree = ""; }; 6E779F24A02A46FA5300DD8D /* TrackAgentResultLoadStartedRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentResultLoadStartedRequestBuilderTests.swift; sourceTree = ""; }; + 6FC0FD8D7D1299CFE25B0E84 /* CIOTrackProductInsightsAgentViewsData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentViewsData.swift; sourceTree = ""; }; + 75C1A954AFF8D2C3A6CF5EC9 /* CIOTrackProductInsightsAgentQuestionClickData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentQuestionClickData.swift; sourceTree = ""; }; + 76B153A7ECC09624463C16D7 /* CIOProductInsightsAgentViewTimespan.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOProductInsightsAgentViewTimespan.swift; sourceTree = ""; }; + 76CE3D18DD769396D19AE44F /* CIOTrackProductInsightsAgentViewData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentViewData.swift; sourceTree = ""; }; + 78F25B5E1835A7C2C32A3D8D /* CIOTrackProductInsightsAgentViewsRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentViewsRequest.swift; sourceTree = ""; }; 7D06CCA228E516BC00AB6A8C /* CIOQuizResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizResult.swift; sourceTree = ""; }; 7D45383928DC085600490BFE /* CIOQuizQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuery.swift; sourceTree = ""; }; 7D45383D28DC14AE00490BFE /* CIOQuizQuestionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOQuizQuestionResponse.swift; sourceTree = ""; }; @@ -487,6 +545,8 @@ 7DDA7D9228DCF513004C8AC8 /* AbstractQuizQuestionResponseParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AbstractQuizQuestionResponseParser.swift; sourceTree = ""; }; 7DDA7D9428DCF5B7004C8AC8 /* QuizQuestionResponseParser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizQuestionResponseParser.swift; sourceTree = ""; }; 7DDA7D9628DCFDC4004C8AC8 /* QuizzesQueryRequestBuilderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuizzesQueryRequestBuilderTests.swift; sourceTree = ""; }; + 7EE0186DB9833CE0382C0F41 /* ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift; sourceTree = ""; }; + 885ABDD759DA765CC2F840E2 /* TrackProductInsightsAgentViewsRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentViewsRequestBuilderTests.swift; sourceTree = ""; }; 8A5572802A1429930041DB65 /* CIOBrowseGroupsQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOBrowseGroupsQuery.swift; sourceTree = ""; }; 8A5572822A1433350041DB65 /* CIOBrowseGroupsQueryBuilder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOBrowseGroupsQueryBuilder.swift; sourceTree = ""; }; 8A5572842A143E840041DB65 /* ConstructorIOBrowseGroupsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConstructorIOBrowseGroupsTests.swift; sourceTree = ""; }; @@ -532,9 +592,20 @@ 8DCA83EA2A1C067F00CD2110 /* BrowseFacetsTaskResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BrowseFacetsTaskResponse.swift; sourceTree = ""; }; 8DF2172C2888AE5E006384A5 /* CIOResultSources.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOResultSources.swift; sourceTree = ""; }; 8DF2172E2888B56D006384A5 /* CIOResultSourceData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOResultSourceData.swift; sourceTree = ""; }; + 91FA523A8C110147035C3D17 /* CIOTrackProductInsightsAgentViewRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentViewRequest.swift; sourceTree = ""; }; + 92147D6EF3B93DB58A00F37D /* ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift; sourceTree = ""; }; + 92323150DDB4DCC6ABB61318 /* CIOTrackProductInsightsAgentAnswerViewRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentAnswerViewRequest.swift; sourceTree = ""; }; 92E18BD16D9DFC799A5BC5D5 /* ConstructorIOTrackAgentSearchSubmitTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentSearchSubmitTests.swift; sourceTree = ""; }; + 95B42E8D2567188BA1F71B26 /* ConstructorIOTrackProductInsightsAgentFocusTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentFocusTests.swift; sourceTree = ""; }; + 97B0EE92FCAC2A60F7607A5E /* ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift; sourceTree = ""; }; + 9878B034B60AED6EBF4B610A /* TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift; sourceTree = ""; }; 9A32D86CF3E9CB3706BD4996 /* CIOTrackAgentResultClickData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackAgentResultClickData.swift; sourceTree = ""; }; + A049DC718BFF97C061C7A116 /* TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift; sourceTree = ""; }; A466AF5E4660C198B64CE5CE /* Pods-AutocompleteClientTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutocompleteClientTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests.release.xcconfig"; sourceTree = ""; }; + A9C0EF1F8EE9EC8B9A8B5F73 /* CIOProductInsightsAgentQuestion.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOProductInsightsAgentQuestion.swift; sourceTree = ""; }; + B189C70622D60B6107475B9D /* CIOTrackProductInsightsAgentResultClickRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentResultClickRequest.swift; sourceTree = ""; }; + B84E5F118001792BF54984EB /* TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift; sourceTree = ""; }; + BE11F4EB73A03F056BC1153E /* TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift; sourceTree = ""; }; BF02E29E28B5739C0062B44F /* CIOItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIOItem.swift; sourceTree = ""; }; BF12196725D1F06200496189 /* CIORecommendationsQuery.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIORecommendationsQuery.swift; sourceTree = ""; }; BF12196925D1FD5700496189 /* CIORecommendationsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CIORecommendationsResponse.swift; sourceTree = ""; }; @@ -569,7 +640,9 @@ CD68EBF1F36134FE6C500E50 /* CIOTrackAgentResultLoadStartedData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackAgentResultLoadStartedData.swift; sourceTree = ""; }; D8824AB4E0C4BF784703D6BA /* Pods-AutocompleteClientTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutocompleteClientTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests.debug.xcconfig"; sourceTree = ""; }; D938EAC4478309F3008F3B0D /* Pods_UserApplication.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UserApplication.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + DADC00EC0CF7961F94F3DF15 /* CIOTrackProductInsightsAgentQuestionSubmitRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentQuestionSubmitRequest.swift; sourceTree = ""; }; E1DD9138622D7CACE62DE89F /* TrackAgentResultLoadFinishedRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentResultLoadFinishedRequestBuilderTests.swift; sourceTree = ""; }; + E2E5687AB79AC0D2B97A4F26 /* CIOTrackProductInsightsAgentFocusRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentFocusRequest.swift; sourceTree = ""; }; E515E2A713406EB70485D4BF /* ConstructorIOTrackAgentResultLoadFinishedTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentResultLoadFinishedTests.swift; sourceTree = ""; }; E9285ABCDB8F27B257DC85C7 /* ConstructorIOTrackAgentSubmitTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentSubmitTests.swift; sourceTree = ""; }; F174C1EA1F5F4D07009E354B /* String+CharacterSetIteratorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+CharacterSetIteratorTests.swift"; sourceTree = ""; }; @@ -764,7 +837,12 @@ F6F1D706234B693D0092341D /* response_search_json_variations.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = response_search_json_variations.json; sourceTree = ""; }; F6F1D708234B6BFA0092341D /* response_search_json_invalid_variations.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = response_search_json_invalid_variations.json; sourceTree = ""; }; F6F1D70A234B6FD50092341D /* response_search_json_variation_custom_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = response_search_json_variation_custom_data.json; sourceTree = ""; }; + F794A697D6C9A78EA3F2C3EA /* CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift; sourceTree = ""; }; + F866BFFAB70ADF6DF794B2DF /* TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift; sourceTree = ""; }; + F8A8AE47CB7C51A27D92005E /* ConstructorIOTrackProductInsightsAgentViewTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackProductInsightsAgentViewTests.swift; sourceTree = ""; }; + F9CBD5E534369A2055DA24AA /* CIOTrackProductInsightsAgentAnswerFeedbackData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentAnswerFeedbackData.swift; sourceTree = ""; }; FABF1A9FFAFEDA72E638A2FA /* TrackAgentResultClickRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentResultClickRequestBuilderTests.swift; sourceTree = ""; }; + FBD854D16E22B01230B750A2 /* TrackProductInsightsAgentViewRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackProductInsightsAgentViewRequestBuilderTests.swift; sourceTree = ""; }; FD016CFF1317FE8714751737 /* TrackAgentResultViewRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentResultViewRequestBuilderTests.swift; sourceTree = ""; }; FFD18C54E183B1297B1D1C15 /* TrackAgentSearchSubmitRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentSearchSubmitRequestBuilderTests.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -970,6 +1048,27 @@ 9A32D86CF3E9CB3706BD4996 /* CIOTrackAgentResultClickData.swift */, 8B64FEEB5E72E52585E8F498 /* CIOTrackAgentResultViewData.swift */, 54889113E31C87DDB613F090 /* CIOTrackAgentSearchSubmitData.swift */, + 3E15EB900F309DFC8E1CDD61 /* CIOProductInsightsAgentFeedbackLabel.swift */, + A9C0EF1F8EE9EC8B9A8B5F73 /* CIOProductInsightsAgentQuestion.swift */, + 76B153A7ECC09624463C16D7 /* CIOProductInsightsAgentViewTimespan.swift */, + F9CBD5E534369A2055DA24AA /* CIOTrackProductInsightsAgentAnswerFeedbackData.swift */, + F794A697D6C9A78EA3F2C3EA /* CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift */, + 0DC4701329FBCAC12F65D2F2 /* CIOTrackProductInsightsAgentAnswerViewData.swift */, + 92323150DDB4DCC6ABB61318 /* CIOTrackProductInsightsAgentAnswerViewRequest.swift */, + 09BADEEBF39C071120BBF365 /* CIOTrackProductInsightsAgentFocusData.swift */, + E2E5687AB79AC0D2B97A4F26 /* CIOTrackProductInsightsAgentFocusRequest.swift */, + 25D9704C68C3A419912C856D /* CIOTrackProductInsightsAgentOutOfViewData.swift */, + 5D17AD8DAF395F6E7F5D9EA6 /* CIOTrackProductInsightsAgentOutOfViewRequest.swift */, + 75C1A954AFF8D2C3A6CF5EC9 /* CIOTrackProductInsightsAgentQuestionClickData.swift */, + 19B352A04190627348E9FEA3 /* CIOTrackProductInsightsAgentQuestionClickRequest.swift */, + 061E4BC68F1EDC002BE25C75 /* CIOTrackProductInsightsAgentQuestionSubmitData.swift */, + DADC00EC0CF7961F94F3DF15 /* CIOTrackProductInsightsAgentQuestionSubmitRequest.swift */, + 36C901176A992CFD86A40827 /* CIOTrackProductInsightsAgentResultClickData.swift */, + B189C70622D60B6107475B9D /* CIOTrackProductInsightsAgentResultClickRequest.swift */, + 76CE3D18DD769396D19AE44F /* CIOTrackProductInsightsAgentViewData.swift */, + 91FA523A8C110147035C3D17 /* CIOTrackProductInsightsAgentViewRequest.swift */, + 6FC0FD8D7D1299CFE25B0E84 /* CIOTrackProductInsightsAgentViewsData.swift */, + 78F25B5E1835A7C2C32A3D8D /* CIOTrackProductInsightsAgentViewsRequest.swift */, ); path = Request; sourceTree = ""; @@ -1101,6 +1200,15 @@ 6B647AA1B70A24E3B043AB6A /* ConstructorIOTrackAgentResultClickTests.swift */, 14F639EF2F5B572DE847C09B /* ConstructorIOTrackAgentResultViewTests.swift */, 92E18BD16D9DFC799A5BC5D5 /* ConstructorIOTrackAgentSearchSubmitTests.swift */, + 08A0B99FF8B0533C02E42727 /* ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift */, + 7EE0186DB9833CE0382C0F41 /* ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift */, + 95B42E8D2567188BA1F71B26 /* ConstructorIOTrackProductInsightsAgentFocusTests.swift */, + 4D0B2FA4EDB0AF90CCD5CE25 /* ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift */, + 97B0EE92FCAC2A60F7607A5E /* ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift */, + 92147D6EF3B93DB58A00F37D /* ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift */, + 0D0BB2A5C31AC46886069B7A /* ConstructorIOTrackProductInsightsAgentResultClickTests.swift */, + F8A8AE47CB7C51A27D92005E /* ConstructorIOTrackProductInsightsAgentViewTests.swift */, + 0DF54EC50588B3DA1C99AE3B /* ConstructorIOTrackProductInsightsAgentViewsTests.swift */, ); path = Worker; sourceTree = ""; @@ -1158,6 +1266,15 @@ FABF1A9FFAFEDA72E638A2FA /* TrackAgentResultClickRequestBuilderTests.swift */, FD016CFF1317FE8714751737 /* TrackAgentResultViewRequestBuilderTests.swift */, FFD18C54E183B1297B1D1C15 /* TrackAgentSearchSubmitRequestBuilderTests.swift */, + A049DC718BFF97C061C7A116 /* TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift */, + B84E5F118001792BF54984EB /* TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift */, + 3D57364981A16D18B682C8F7 /* TrackProductInsightsAgentFocusRequestBuilderTests.swift */, + 9878B034B60AED6EBF4B610A /* TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift */, + F866BFFAB70ADF6DF794B2DF /* TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift */, + BE11F4EB73A03F056BC1153E /* TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift */, + 31D4419F69611F6AE5E1AF63 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift */, + FBD854D16E22B01230B750A2 /* TrackProductInsightsAgentViewRequestBuilderTests.swift */, + 885ABDD759DA765CC2F840E2 /* TrackProductInsightsAgentViewsRequestBuilderTests.swift */, ); path = Request; sourceTree = ""; @@ -2692,6 +2809,27 @@ 7D0F5EAB01E5BD5582866F96 /* CIOTrackAgentResultClickData.swift in Sources */, AE94DB499481274AB9C2EC8B /* CIOTrackAgentResultViewData.swift in Sources */, 4F30F4822C4C805F450EB488 /* CIOTrackAgentSearchSubmitData.swift in Sources */, + 9217BA423BAEC4DE24CE7323 /* CIOProductInsightsAgentFeedbackLabel.swift in Sources */, + 151BD639718ABAB1CB984AA7 /* CIOProductInsightsAgentQuestion.swift in Sources */, + F424450F56E9454EDD49EC9B /* CIOProductInsightsAgentViewTimespan.swift in Sources */, + 436758DF0758AE28441118AF /* CIOTrackProductInsightsAgentAnswerFeedbackData.swift in Sources */, + A5D7039D9A00FCDCF7CE2CEE /* CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift in Sources */, + F14E8EAC391CD86273FBF66C /* CIOTrackProductInsightsAgentAnswerViewData.swift in Sources */, + 7992ADFD7873CA7C61FA6F9F /* CIOTrackProductInsightsAgentAnswerViewRequest.swift in Sources */, + 2F178AD4206D3F6F6824A75C /* CIOTrackProductInsightsAgentFocusData.swift in Sources */, + 89763179C7ADBED811F268DD /* CIOTrackProductInsightsAgentFocusRequest.swift in Sources */, + CDA43CFAE25B8C354ADDCE39 /* CIOTrackProductInsightsAgentOutOfViewData.swift in Sources */, + 702AD679D59CB6074872D4AD /* CIOTrackProductInsightsAgentOutOfViewRequest.swift in Sources */, + BABFD40203A95945AD2F46C3 /* CIOTrackProductInsightsAgentQuestionClickData.swift in Sources */, + 977D116BA3000898EE0F1C6F /* CIOTrackProductInsightsAgentQuestionClickRequest.swift in Sources */, + 75A15E0E959A2263749A24D0 /* CIOTrackProductInsightsAgentQuestionSubmitData.swift in Sources */, + 12856114DA0E06D22F228AF4 /* CIOTrackProductInsightsAgentQuestionSubmitRequest.swift in Sources */, + DF5314F3EC1D042F38FB2736 /* CIOTrackProductInsightsAgentResultClickData.swift in Sources */, + 7AA8B287A23F3EFA49B57119 /* CIOTrackProductInsightsAgentResultClickRequest.swift in Sources */, + 7E88F825FDEB47B44CCE9C13 /* CIOTrackProductInsightsAgentViewData.swift in Sources */, + D4DA80E74933A0242D69111C /* CIOTrackProductInsightsAgentViewRequest.swift in Sources */, + 7F4EFC5A6328D2A6211DFB92 /* CIOTrackProductInsightsAgentViewsData.swift in Sources */, + CF83F2B8A45DE57007BA43A3 /* CIOTrackProductInsightsAgentViewsRequest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2805,6 +2943,24 @@ 30DC8B3897E361EB4ADC91D5 /* ConstructorIOTrackAgentResultClickTests.swift in Sources */, 13D84626E02649E5690DEDBE /* ConstructorIOTrackAgentResultViewTests.swift in Sources */, 449B02753FBBE5501BD3269B /* ConstructorIOTrackAgentSearchSubmitTests.swift in Sources */, + 5EF1CD01D67398A03CF98731 /* TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift in Sources */, + 4F2D9EF7C91B101E074D7E8D /* TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift in Sources */, + C48F21785156E7D3D4A94DF8 /* TrackProductInsightsAgentFocusRequestBuilderTests.swift in Sources */, + C3BEA3561A5FD6C91942A9EC /* TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift in Sources */, + BBB2E127B5674B3A1F0D5CD8 /* TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift in Sources */, + 287F2A489FC50C1FD4A33FAF /* TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift in Sources */, + FFBFF2D9B43441E34588BFC7 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift in Sources */, + FC5DEABC0479238454C51141 /* TrackProductInsightsAgentViewRequestBuilderTests.swift in Sources */, + 5188B65D1B10B35BABCCFE32 /* TrackProductInsightsAgentViewsRequestBuilderTests.swift in Sources */, + F15620AF69178D8BA6397A43 /* ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift in Sources */, + ED7C45224157AD7A5E39C16B /* ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift in Sources */, + 914E0500EDC9AAD8ABBE4F68 /* ConstructorIOTrackProductInsightsAgentFocusTests.swift in Sources */, + 8432A57AEDC0EDF4BD2A52A5 /* ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift in Sources */, + 6BC60CF71388EA7445B00159 /* ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift in Sources */, + 5BA5481625CA59D9C488B5A6 /* ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift in Sources */, + 55A08346890C4997406C3842 /* ConstructorIOTrackProductInsightsAgentResultClickTests.swift in Sources */, + 0AAC5669EDB5FED3712BDD03 /* ConstructorIOTrackProductInsightsAgentViewTests.swift in Sources */, + 9F60E3A4CAB308554487D645 /* ConstructorIOTrackProductInsightsAgentViewsTests.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AutocompleteClient/Constants/Constants.swift b/AutocompleteClient/Constants/Constants.swift index 20b98e95..67141bf0 100644 --- a/AutocompleteClient/Constants/Constants.swift +++ b/AutocompleteClient/Constants/Constants.swift @@ -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" } diff --git a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentFeedbackLabel.swift b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentFeedbackLabel.swift new file mode 100644 index 00000000..ce17a1b9 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentFeedbackLabel.swift @@ -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" +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentQuestion.swift b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentQuestion.swift new file mode 100644 index 00000000..8decabdf --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentQuestion.swift @@ -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 + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift new file mode 100644 index 00000000..b95a68cd --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift @@ -0,0 +1,53 @@ +// +// 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 + } + + // ISO8601DateFormatter is thread-safe, so a single shared instance is used for all timespans + static let dateFormatter: ISO8601DateFormatter = { + let formatter = ISO8601DateFormatter() + formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + formatter.timeZone = TimeZone(secondsFromGMT: 0) + 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) + ] + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift new file mode 100644 index 00000000..61049172 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift @@ -0,0 +1,75 @@ +// +// 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: CIORequestData { + + 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 decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "feedback_label": self.feedbackLabel.rawValue + ] as [String: Any] + + if let qnaResultID = self.qnaResultID { + dict["qna_result_id"] = qnaResultID + } + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift new file mode 100644 index 00000000..6469e887 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackRequest.swift @@ -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 + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift new file mode 100644 index 00000000..1cfed110 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift @@ -0,0 +1,113 @@ +// +// CIOTrackProductInsightsAgentAnswerViewData.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 view + */ +struct CIOTrackProductInsightsAgentAnswerViewData: CIORequestData { + + let itemID: String + let question: String + let answerText: String + let questionTopic: String? + let qnaResultID: String? + let items: [CIOItem]? + let followUpQuestions: [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.TrackProductInsightsAgentAnswerView.format, baseURL) + } + + init(itemID: String, question: String, answerText: String, questionTopic: String? = nil, qnaResultID: String? = nil, items: [CIOItem]? = nil, followUpQuestions: [String]? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.answerText = answerText + self.questionTopic = questionTopic + self.qnaResultID = qnaResultID + self.items = items + self.followUpQuestions = followUpQuestions + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "question": self.question, + "answer_text": self.answerText + ] as [String: Any] + + if let questionTopic = self.questionTopic { + dict["question_topic"] = questionTopic + } + if let qnaResultID = self.qnaResultID { + dict["qna_result_id"] = qnaResultID + } + if let items = self.items { + dict["items"] = items.prefix(100).map { $0.productInsightsAgentDictionary() } + } + if let followUpQuestions = self.followUpQuestions { + dict["follow_up_questions"] = followUpQuestions.prefix(100).map { ["value": $0] } + } + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} + +private extension CIOItem { + // Only the keys accepted by the API are sent. `quantity` is rejected with a 400, so it is never included + func productInsightsAgentDictionary() -> [String: String] { + 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 + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift new file mode 100644 index 00000000..3809a8fb --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift @@ -0,0 +1,107 @@ +// +// CIOTrackProductInsightsAgentAnswerViewRequest.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.trackProductInsightsAgentAnswerView() function. + */ +public struct CIOTrackProductInsightsAgentAnswerViewRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: String + + /** + The question the answer was provided for + */ + public let question: String + + /** + The text of the answer shown to the user (an empty string is allowed) + */ + public let answerText: String + + /** + The topic category of the question + */ + public let questionTopic: String? + + /** + The ID of the answer result returned by the Product Insights Agent + */ + public let qnaResultID: String? + + /** + The recommended items shown with the answer (capped at 100). `quantity` is not sent + */ + public let items: [CIOItem]? + + /** + The follow-up questions shown with the answer (capped at 100) + */ + public let followUpQuestions: [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 view request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - question: The question the answer was provided for + - answerText: The text of the answer shown to the user (an empty string is allowed) + - questionTopic: The topic category of the question + - qnaResultID: The ID of the answer result returned by the Product Insights Agent + - items: The recommended items shown with the answer (capped at 100). `quantity` is not sent + - followUpQuestions: The follow-up questions shown with the answer (capped at 100) + - 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, question: String, answerText: String, questionTopic: String? = nil, qnaResultID: String? = nil, items: [CIOItem]? = nil, followUpQuestions: [String]? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.answerText = answerText + self.questionTopic = questionTopic + self.qnaResultID = qnaResultID + self.items = items + self.followUpQuestions = followUpQuestions + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift new file mode 100644 index 00000000..70942ad1 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift @@ -0,0 +1,67 @@ +// +// CIOTrackProductInsightsAgentFocusData.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 focus + */ +struct CIOTrackProductInsightsAgentFocusData: CIORequestData { + + let itemID: 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.TrackProductInsightsAgentFocus.format, baseURL) + } + + init(itemID: String, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID + ] as [String: Any] + + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusRequest.swift new file mode 100644 index 00000000..d54d8dc5 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusRequest.swift @@ -0,0 +1,65 @@ +// +// CIOTrackProductInsightsAgentFocusRequest.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.trackProductInsightsAgentFocus() function. + */ +public struct CIOTrackProductInsightsAgentFocusRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: 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 focus request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - 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, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift new file mode 100644 index 00000000..97f056fd --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift @@ -0,0 +1,67 @@ +// +// CIOTrackProductInsightsAgentOutOfViewData.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 out of view + */ +struct CIOTrackProductInsightsAgentOutOfViewData: CIORequestData { + + let itemID: 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.TrackProductInsightsAgentOutOfView.format, baseURL) + } + + init(itemID: String, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID + ] as [String: Any] + + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewRequest.swift new file mode 100644 index 00000000..a866f025 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewRequest.swift @@ -0,0 +1,65 @@ +// +// CIOTrackProductInsightsAgentOutOfViewRequest.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.trackProductInsightsAgentOutOfView() function. + */ +public struct CIOTrackProductInsightsAgentOutOfViewRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: 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 out of view request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - 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, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift new file mode 100644 index 00000000..399e5797 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift @@ -0,0 +1,75 @@ +// +// CIOTrackProductInsightsAgentQuestionClickData.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 question click + */ +struct CIOTrackProductInsightsAgentQuestionClickData: CIORequestData { + + let itemID: String + let question: String + let questionTopic: 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.TrackProductInsightsAgentQuestionClick.format, baseURL) + } + + init(itemID: String, question: String, questionTopic: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.questionTopic = questionTopic + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "question": self.question + ] as [String: Any] + + if let questionTopic = self.questionTopic { + dict["question_topic"] = questionTopic + } + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift new file mode 100644 index 00000000..e661bf24 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift @@ -0,0 +1,79 @@ +// +// CIOTrackProductInsightsAgentQuestionClickRequest.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.trackProductInsightsAgentQuestionClick() function. + */ +public struct CIOTrackProductInsightsAgentQuestionClickRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: String + + /** + The question the user clicked on + */ + public let question: String + + /** + The topic category of the question + */ + public let questionTopic: 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 question click request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - question: The question the user clicked on + - questionTopic: The topic category of the question + - 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, question: String, questionTopic: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.questionTopic = questionTopic + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift new file mode 100644 index 00000000..4c8e2108 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift @@ -0,0 +1,75 @@ +// +// CIOTrackProductInsightsAgentQuestionSubmitData.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 question submit + */ +struct CIOTrackProductInsightsAgentQuestionSubmitData: CIORequestData { + + let itemID: String + let question: String + let questionTopic: 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.TrackProductInsightsAgentQuestionSubmit.format, baseURL) + } + + init(itemID: String, question: String, questionTopic: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.questionTopic = questionTopic + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "question": self.question + ] as [String: Any] + + if let questionTopic = self.questionTopic { + dict["question_topic"] = questionTopic + } + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift new file mode 100644 index 00000000..5e4804bf --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift @@ -0,0 +1,79 @@ +// +// CIOTrackProductInsightsAgentQuestionSubmitRequest.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.trackProductInsightsAgentQuestionSubmit() function. + */ +public struct CIOTrackProductInsightsAgentQuestionSubmitRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: String + + /** + The question the user submitted + */ + public let question: String + + /** + The topic category of the question + */ + public let questionTopic: 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 question submit request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - question: The question the user submitted + - questionTopic: The topic category of the question + - 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, question: String, questionTopic: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.questionTopic = questionTopic + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift new file mode 100644 index 00000000..c316a1f8 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift @@ -0,0 +1,93 @@ +// +// CIOTrackProductInsightsAgentResultClickData.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 result click + */ +struct CIOTrackProductInsightsAgentResultClickData: CIORequestData { + + let itemID: String + let question: String + let seedItemID: String + let position: Int? + let seedItemName: String? + let seedVariationID: String? + 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.TrackProductInsightsAgentResultClick.format, baseURL) + } + + init(itemID: String, question: String, seedItemID: String, position: Int? = nil, seedItemName: String? = nil, seedVariationID: String? = nil, qnaResultID: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.seedItemID = seedItemID + self.position = position + self.seedItemName = seedItemName + self.seedVariationID = seedVariationID + self.qnaResultID = qnaResultID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "question": self.question, + "seed_item_id": self.seedItemID + ] as [String: Any] + + if let position = self.position { + dict["position"] = position + } + if let seedItemName = self.seedItemName { + dict["seed_item_name"] = seedItemName + } + if let seedVariationID = self.seedVariationID { + dict["seed_variation_id"] = seedVariationID + } + if let qnaResultID = self.qnaResultID { + dict["qna_result_id"] = qnaResultID + } + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift new file mode 100644 index 00000000..6164f887 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift @@ -0,0 +1,107 @@ +// +// CIOTrackProductInsightsAgentResultClickRequest.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.trackProductInsightsAgentResultClick() function. + */ +public struct CIOTrackProductInsightsAgentResultClickRequest { + + /** + The ID of the clicked recommended product + */ + public let itemID: String + + /** + The question that produced the answer containing the clicked recommendation + */ + public let question: String + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let seedItemID: String + + /** + The position of the clicked item in the list of recommendations + */ + public let position: Int? + + /** + The name of the product whose page the Product Insights Agent is on + */ + public let seedItemName: String? + + /** + The variation ID of the product whose page the Product Insights Agent is on + */ + public let seedVariationID: String? + + /** + The ID of the answer result returned by the Product Insights Agent + */ + public let qnaResultID: String? + + /** + The name of the clicked recommended product + */ + public let itemName: String? + + /** + The variation ID of the clicked recommended product + */ + 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 result click request + + - Parameters: + - itemID: The ID of the clicked recommended product + - question: The question that produced the answer containing the clicked recommendation + - seedItemID: The ID of the product whose page the Product Insights Agent is on + - position: The position of the clicked item in the list of recommendations + - seedItemName: The name of the product whose page the Product Insights Agent is on + - seedVariationID: The variation ID of the product whose page the Product Insights Agent is on + - qnaResultID: The ID of the answer result returned by the Product Insights Agent + - itemName: The name of the clicked recommended product + - variationID: The variation ID of the clicked recommended product + - 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, question: String, seedItemID: String, position: Int? = nil, seedItemName: String? = nil, seedVariationID: String? = nil, qnaResultID: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.question = question + self.seedItemID = seedItemID + self.position = position + self.seedItemName = seedItemName + self.seedVariationID = seedVariationID + self.qnaResultID = qnaResultID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift new file mode 100644 index 00000000..78017a7d --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift @@ -0,0 +1,70 @@ +// +// CIOTrackProductInsightsAgentViewData.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 view + */ +struct CIOTrackProductInsightsAgentViewData: CIORequestData { + + let itemID: String + let questions: [CIOProductInsightsAgentQuestion] + 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.TrackProductInsightsAgentView.format, baseURL) + } + + init(itemID: String, questions: [CIOProductInsightsAgentQuestion], itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.questions = questions + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "questions": self.questions.map { $0.toDictionary() } + ] as [String: Any] + + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewRequest.swift new file mode 100644 index 00000000..eaa8ae05 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewRequest.swift @@ -0,0 +1,72 @@ +// +// CIOTrackProductInsightsAgentViewRequest.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.trackProductInsightsAgentView() function. + */ +public struct CIOTrackProductInsightsAgentViewRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: String + + /** + The list of pre-defined questions shown to the user + */ + public let questions: [CIOProductInsightsAgentQuestion] + + /** + 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 view request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - questions: The list of pre-defined questions shown to the user + - 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, questions: [CIOProductInsightsAgentQuestion], itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.questions = questions + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift new file mode 100644 index 00000000..ba2f4e1b --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift @@ -0,0 +1,73 @@ +// +// CIOTrackProductInsightsAgentViewsData.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 views + */ +struct CIOTrackProductInsightsAgentViewsData: CIORequestData { + + let itemID: String + let questions: [CIOProductInsightsAgentQuestion] + let viewTimespans: [CIOProductInsightsAgentViewTimespan] + 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.TrackProductInsightsAgentViews.format, baseURL) + } + + init(itemID: String, questions: [CIOProductInsightsAgentQuestion], viewTimespans: [CIOProductInsightsAgentViewTimespan], itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.questions = questions + self.viewTimespans = viewTimespans + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = [ + "item_id": self.itemID, + "questions": self.questions.map { $0.toDictionary() }, + "view_timespans": self.viewTimespans.map { $0.toDictionary() } + ] as [String: Any] + + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsRequest.swift new file mode 100644 index 00000000..a3501036 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsRequest.swift @@ -0,0 +1,79 @@ +// +// CIOTrackProductInsightsAgentViewsRequest.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.trackProductInsightsAgentViews() function. + */ +public struct CIOTrackProductInsightsAgentViewsRequest { + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let itemID: String + + /** + The list of pre-defined questions shown to the user + */ + public let questions: [CIOProductInsightsAgentQuestion] + + /** + The periods of time during which the Product Insights Agent was visible. Sent as ISO-8601 date-times in UTC + */ + public let viewTimespans: [CIOProductInsightsAgentViewTimespan] + + /** + 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 views request + + - Parameters: + - itemID: The ID of the product whose page the Product Insights Agent is on + - questions: The list of pre-defined questions shown to the user + - viewTimespans: The periods of time during which the Product Insights Agent was visible. Sent as ISO-8601 date-times in UTC + - 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, questions: [CIOProductInsightsAgentQuestion], viewTimespans: [CIOProductInsightsAgentViewTimespan], itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.questions = questions + self.viewTimespans = viewTimespans + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } +} diff --git a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift index 2b5f6951..16c1fc98 100644 --- a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift +++ b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift @@ -974,6 +974,186 @@ public class ConstructorIO: CIOSessionManagerDelegate { executeTracking(request, completionHandler: completionHandler) } + /** + Track when a user focuses on the Product Insights Agent input element + + - Parameters: + - request: The focus request. See `CIOTrackProductInsightsAgentFocusRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentFocus(_ request: CIOTrackProductInsightsAgentFocusRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentFocusData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when the Product Insights Agent element disappears from the visible part of the page + + - Parameters: + - request: The out of view request. See `CIOTrackProductInsightsAgentOutOfViewRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentOutOfView(_ request: CIOTrackProductInsightsAgentOutOfViewRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentOutOfViewData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when the Product Insights Agent element appears in the visible part of the page + + - Parameters: + - request: The view request. See `CIOTrackProductInsightsAgentViewRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentView(_ request: CIOTrackProductInsightsAgentViewRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentViewData(itemID: request.itemID, questions: request.questions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track the periods of time during which the Product Insights Agent element was visible on the page + + - Parameters: + - request: The views request. See `CIOTrackProductInsightsAgentViewsRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792550405.123), end: Date(timeIntervalSince1970: 1792550410.456))], itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentViews(_ request: CIOTrackProductInsightsAgentViewsRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentViewsData(itemID: request.itemID, questions: request.questions, viewTimespans: request.viewTimespans, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when a user clicks on a question within the Product Insights Agent + + - Parameters: + - request: The question click request. See `CIOTrackProductInsightsAgentQuestionClickRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: "10001", question: "Why choose this?", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentQuestionClick(_ request: CIOTrackProductInsightsAgentQuestionClickRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentQuestionClickData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when a user submits a question to the Product Insights Agent + + - Parameters: + - request: The question submit request. See `CIOTrackProductInsightsAgentQuestionSubmitRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "10001", question: "Why choose this?", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentQuestionSubmit(_ request: CIOTrackProductInsightsAgentQuestionSubmitRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentQuestionSubmitData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when a user gives feedback on the usefulness of a Product Insights Agent answer + + - Parameters: + - request: The answer feedback request. See `CIOTrackProductInsightsAgentAnswerFeedbackRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: "10001", feedbackLabel: .thumbsUp, qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentAnswerFeedback(_ request: CIOTrackProductInsightsAgentAnswerFeedbackRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: request.itemID, feedbackLabel: request.feedbackLabel, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when a user clicks on a recommended product within the Product Insights Agent + + - Parameters: + - request: The result click request. See `CIOTrackProductInsightsAgentResultClickRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentResultClick(_ request: CIOTrackProductInsightsAgentResultClickRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentResultClickData(itemID: request.itemID, question: request.question, seedItemID: request.seedItemID, position: request.position, seedItemName: request.seedItemName, seedVariationID: request.seedVariationID, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + + /** + Track when a user views an answer provided by the Product Insights Agent + + - Parameters: + - request: The answer view request. See `CIOTrackProductInsightsAgentAnswerViewRequest` for the available fields + - completionHandler: The callback to execute on completion. + + ### Usage Example: ### + ``` + constructorIO.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "10001", question: "Why choose this?", answerText: "This product is made from recycled materials.", qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentAnswerView(_ request: CIOTrackProductInsightsAgentAnswerViewRequest, completionHandler: TrackingCompletionHandler? = nil) { + let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) + let data = CIOTrackProductInsightsAgentAnswerViewData(itemID: request.itemID, question: request.question, answerText: request.answerText, questionTopic: request.questionTopic, qnaResultID: request.qnaResultID, items: request.items, followUpQuestions: request.followUpQuestions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) + } + /** Set a custom clientID diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift new file mode 100644 index 00000000..7e0794b2 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift @@ -0,0 +1,80 @@ +// +// TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let feedbackLabel: CIOProductInsightsAgentFeedbackLabel = .thumbsUp + fileprivate let qnaResultID: String = "qna-result-123" + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentAnswerFeedbackBuilder() { + let tracker = CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: itemID, feedbackLabel: feedbackLabel) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["feedback_label"] as? String, "thumbs_up") + XCTAssertNil(payload?["qna_result_id"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentAnswerFeedbackBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: itemID, feedbackLabel: feedbackLabel, qnaResultID: qnaResultID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["feedback_label"] as? String, "thumbs_up") + XCTAssertEqual(payload?["qna_result_id"] as? String, qnaResultID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentAnswerFeedbackBuilder_WithThumbsDown() { + let tracker = CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: itemID, feedbackLabel: .thumbsDown) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["feedback_label"] as? String, "thumbs_down") + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift new file mode 100644 index 00000000..27cbb5bc --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift @@ -0,0 +1,139 @@ +// +// TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentAnswerViewRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let question: String = "Why choose this?" + fileprivate let answerText: String = "This product is made from recycled materials." + fileprivate let questionTopic: String = "highlights" + fileprivate let qnaResultID: String = "qna-result-123" + fileprivate let items: [CIOItem] = [CIOItem(customerID: "10002", itemName: "Item2", variationID: "10002-A", quantity: 3, slCampaignID: "camp1", slCampaignOwner: "owner1"), CIOItem(customerID: "10003")] + fileprivate let followUpQuestions: [String] = ["What sizes are available?", "Is it machine washable?"] + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentAnswerViewBuilder() { + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["answer_text"] as? String, answerText) + XCTAssertNil(payload?["question_topic"]) + XCTAssertNil(payload?["qna_result_id"]) + XCTAssertNil(payload?["items"]) + XCTAssertNil(payload?["follow_up_questions"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentAnswerViewBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText, questionTopic: questionTopic, qnaResultID: qnaResultID, items: items, followUpQuestions: followUpQuestions, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["answer_text"] as? String, answerText) + XCTAssertEqual(payload?["question_topic"] as? String, questionTopic) + XCTAssertEqual(payload?["qna_result_id"] as? String, qnaResultID) + XCTAssertEqual(payload?["items"] as? [[String: String]], [["item_id": "10002", "item_name": "Item2", "variation_id": "10002-A", "sl_campaign_id": "camp1", "sl_campaign_owner": "owner1"], ["item_id": "10003"]]) + XCTAssertEqual(payload?["follow_up_questions"] as? [[String: String]], [["value": "What sizes are available?"], ["value": "Is it machine washable?"]]) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentAnswerViewBuilder_SendsEmptyAnswerText() { + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: "") + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["answer_text"] as? String, "", "an empty answer_text should be sent") + } + + func testTrackProductInsightsAgentAnswerViewBuilder_NeverSendsItemQuantity() { + let items = [CIOItem(customerID: "10002", quantity: 3)] + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText, items: items) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + let payloadItems = payload?["items"] as? [[String: Any]] ?? [] + XCTAssertEqual(payloadItems.count, 1) + XCTAssertEqual(payloadItems[0]["item_id"] as? String, "10002") + XCTAssertNil(payloadItems[0]["quantity"], "quantity should never be sent") + } + + func testTrackProductInsightsAgentAnswerViewBuilder_CapsItemsAt100() { + let items = (1...150).map { CIOItem(customerID: "item\($0)") } + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText, items: items) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + let payloadItems = payload?["items"] as? [[String: String]] ?? [] + XCTAssertEqual(payloadItems.count, 100, "items should be capped at 100") + XCTAssertEqual(payloadItems[0]["item_id"], "item1") + XCTAssertEqual(payloadItems[99]["item_id"], "item100") + } + + func testTrackProductInsightsAgentAnswerViewBuilder_WithExactly100Items() { + let items = (1...100).map { CIOItem(customerID: "item\($0)") } + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText, items: items) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + let payloadItems = payload?["items"] as? [[String: String]] ?? [] + XCTAssertEqual(payloadItems.count, 100, "exactly 100 items should pass through unchanged") + XCTAssertEqual(payloadItems[99]["item_id"], "item100") + } + + func testTrackProductInsightsAgentAnswerViewBuilder_CapsFollowUpQuestionsAt100() { + let followUpQuestions = (1...150).map { "Question \($0)?" } + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText, followUpQuestions: followUpQuestions) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + let payloadFollowUpQuestions = payload?["follow_up_questions"] as? [[String: String]] ?? [] + XCTAssertEqual(payloadFollowUpQuestions.count, 100, "follow_up_questions should be capped at 100") + XCTAssertEqual(payloadFollowUpQuestions[0]["value"], "Question 1?") + XCTAssertEqual(payloadFollowUpQuestions[99]["value"], "Question 100?") + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift new file mode 100644 index 00000000..850ef574 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift @@ -0,0 +1,66 @@ +// +// TrackProductInsightsAgentFocusRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentFocusRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentFocusBuilder() { + let tracker = CIOTrackProductInsightsAgentFocusData(itemID: itemID) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentFocusBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentFocusData(itemID: itemID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift new file mode 100644 index 00000000..2cfe2fc3 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift @@ -0,0 +1,66 @@ +// +// TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentOutOfViewRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentOutOfViewBuilder() { + let tracker = CIOTrackProductInsightsAgentOutOfViewData(itemID: itemID) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentOutOfViewBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentOutOfViewData(itemID: itemID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift new file mode 100644 index 00000000..88cf4de0 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift @@ -0,0 +1,72 @@ +// +// TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentQuestionClickRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let question: String = "Why choose this?" + fileprivate let questionTopic: String = "highlights" + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentQuestionClickBuilder() { + let tracker = CIOTrackProductInsightsAgentQuestionClickData(itemID: itemID, question: question) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertNil(payload?["question_topic"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentQuestionClickBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentQuestionClickData(itemID: itemID, question: question, questionTopic: questionTopic, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["question_topic"] as? String, questionTopic) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift new file mode 100644 index 00000000..060e9ebb --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift @@ -0,0 +1,72 @@ +// +// TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentQuestionSubmitRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let question: String = "Why choose this?" + fileprivate let questionTopic: String = "highlights" + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentQuestionSubmitBuilder() { + let tracker = CIOTrackProductInsightsAgentQuestionSubmitData(itemID: itemID, question: question) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertNil(payload?["question_topic"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentQuestionSubmitBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentQuestionSubmitData(itemID: itemID, question: question, questionTopic: questionTopic, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["question_topic"] as? String, questionTopic) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift new file mode 100644 index 00000000..36ab8015 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift @@ -0,0 +1,92 @@ +// +// TrackProductInsightsAgentResultClickRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentResultClickRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10002" + fileprivate let question: String = "Why choose this?" + fileprivate let seedItemID: String = "10001" + fileprivate let position: Int = 3 + fileprivate let seedItemName: String = "Item1" + fileprivate let seedVariationID: String = "10001-A" + fileprivate let qnaResultID: String = "qna-result-123" + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentResultClickBuilder() { + let tracker = CIOTrackProductInsightsAgentResultClickData(itemID: itemID, question: question, seedItemID: seedItemID) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["seed_item_id"] as? String, seedItemID) + XCTAssertNil(payload?["position"]) + XCTAssertNil(payload?["seed_item_name"]) + XCTAssertNil(payload?["seed_variation_id"]) + XCTAssertNil(payload?["qna_result_id"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentResultClickBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentResultClickData(itemID: itemID, question: question, seedItemID: seedItemID, position: position, seedItemName: seedItemName, seedVariationID: seedVariationID, qnaResultID: qnaResultID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["seed_item_id"] as? String, seedItemID) + XCTAssertEqual(payload?["position"] as? Int, position) + XCTAssertEqual(payload?["seed_item_name"] as? String, seedItemName) + XCTAssertEqual(payload?["seed_variation_id"] as? String, seedVariationID) + XCTAssertEqual(payload?["qna_result_id"] as? String, qnaResultID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentResultClickBuilder_SendsPositionZero() { + let tracker = CIOTrackProductInsightsAgentResultClickData(itemID: itemID, question: question, seedItemID: seedItemID, position: 0) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["position"] as? Int, 0, "position 0 should be sent") + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift new file mode 100644 index 00000000..c1af31c3 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift @@ -0,0 +1,69 @@ +// +// TrackProductInsightsAgentViewRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentViewRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let questions: [CIOProductInsightsAgentQuestion] = [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")] + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentViewBuilder() { + let tracker = CIOTrackProductInsightsAgentViewData(itemID: itemID, questions: questions) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentViewBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentViewData(itemID: itemID, questions: questions, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift new file mode 100644 index 00000000..d1f17e36 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift @@ -0,0 +1,95 @@ +// +// TrackProductInsightsAgentViewsRequestBuilderTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +@testable import ConstructorAutocomplete +import XCTest + +class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { + + fileprivate let testACKey = "asdf1213123" + fileprivate let itemID: String = "10001" + fileprivate let questions: [CIOProductInsightsAgentQuestion] = [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")] + fileprivate let viewTimespans: [CIOProductInsightsAgentViewTimespan] = [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.456))] + fileprivate let itemName: String = "Item1" + fileprivate let variationID: String = "10001-A" + fileprivate let threadID: String = "thread-123" + fileprivate let sectionName: String = "Search Suggestions" + fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] + + fileprivate var builder: RequestBuilder! + + override func setUp() { + super.setUp() + self.builder = RequestBuilder(apiKey: testACKey, baseURL: Constants.Query.baseURLString) + } + + fileprivate func payload(of request: URLRequest) -> [String: Any]? { + return try? JSONSerialization.jsonObject(with: request.httpBody!, options: []) as? [String: Any] + } + + func testTrackProductInsightsAgentViewsBuilder() { + let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: viewTimespans) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.hasPrefix("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?")) + XCTAssertFalse(request.url!.absoluteString.contains("section=")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertNil(payload?["section"]) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentViewsBuilder_WithAllFields() { + let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: viewTimespans, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags) + builder.build(trackData: tracker) + let request = builder.getRequest() + let payload = self.payload(of: request) + + XCTAssertEqual(request.httpMethod, "POST") + XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentViewsBuilder_FormatsTimespansAsISO8601InUTC() { + let viewTimespans = [ + CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 0), end: Date(timeIntervalSince1970: 1.5)), + CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805), end: Date(timeIntervalSince1970: 1792636810)) + ] + let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: viewTimespans) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [ + ["start": "1970-01-01T00:00:00.000Z", "end": "1970-01-01T00:00:01.500Z"], + ["start": "2026-10-22T02:40:05.000Z", "end": "2026-10-22T02:40:10.000Z"] + ]) + } + + func testTrackProductInsightsAgentViewsBuilder_WithEmptyTimespans() { + let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: []) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual((payload?["view_timespans"] as? [Any])?.count, 0) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift index 9d281943..13cc9b46 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift @@ -63,6 +63,105 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentFocus() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentFocusRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentFocus(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentOutOfView() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentOutOfViewRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentOutOfView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentView() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentViewRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentViews() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792550405.123), end: Date(timeIntervalSince1970: 1792550410.456))], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentViews(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentQuestionClick() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentQuestionClickRequest(itemID: "10001", question: "Why choose this?", questionTopic: "highlights", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentQuestionClick(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentQuestionSubmit() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "10001", question: "Why choose this?", questionTopic: "highlights", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentQuestionSubmit(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentAnswerFeedback() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: "10001", feedbackLabel: .thumbsUp, qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentAnswerFeedback(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentResultClick() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", seedVariationID: "10001-A", qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentResultClick(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentAnswerView() { + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "10001", question: "Why choose this?", answerText: "This product is made from recycled materials.", questionTopic: "highlights", qnaResultID: "qna-result-123", items: [CIOItem(customerID: "10002", itemName: "Item2", variationID: "10002-A", quantity: 3, slCampaignID: "camp1", slCampaignOwner: "owner1"), CIOItem(customerID: "10003")], followUpQuestions: ["What sizes are available?", "Is it machine washable?"], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + self.constructor.trackProductInsightsAgentAnswerView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackAutocompleteSelect() { let expectation = XCTestExpectation(description: "Tracking 204") self.constructor.trackAutocompleteSelect(searchTerm: searchTerm, originalQuery: originalQuery, sectionName: sectionName, group: group, resultID: resultID, itemID: customerID, completionHandler: { response in diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift new file mode 100644 index 00000000..8f04991e --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift @@ -0,0 +1,157 @@ +// +// ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let feedbackLabel: CIOProductInsightsAgentFeedbackLabel = .thumbsUp + let qnaResultID: String = "qna-result-123" + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentAnswerFeedback() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["feedback_label"] as? String, "thumbs_up") + XCTAssertNil(payload?["qna_result_id"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentAnswerFeedback_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel, qnaResultID: qnaResultID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["feedback_label"] as? String, "thumbs_up") + XCTAssertEqual(payload?["qna_result_id"] as? String, qnaResultID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentAnswerFeedback_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentAnswerFeedback_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentAnswerFeedback_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentAnswerFeedback_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentAnswerFeedback with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentAnswerFeedback_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentAnswerFeedback with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentAnswerFeedback_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentAnswerFeedback with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift new file mode 100644 index 00000000..195a19cf --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift @@ -0,0 +1,204 @@ +// +// ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentAnswerViewTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let question: String = "Why choose this?" + let answerText: String = "This product is made from recycled materials." + let questionTopic: String = "highlights" + let qnaResultID: String = "qna-result-123" + let items: [CIOItem] = [CIOItem(customerID: "10002", itemName: "Item2", variationID: "10002-A", quantity: 3, slCampaignID: "camp1", slCampaignOwner: "owner1"), CIOItem(customerID: "10003")] + let followUpQuestions: [String] = ["What sizes are available?", "Is it machine washable?"] + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentAnswerView() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["answer_text"] as? String, answerText) + XCTAssertNil(payload?["question_topic"]) + XCTAssertNil(payload?["qna_result_id"]) + XCTAssertNil(payload?["items"]) + XCTAssertNil(payload?["follow_up_questions"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentAnswerView_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText, questionTopic: questionTopic, qnaResultID: qnaResultID, items: items, followUpQuestions: followUpQuestions, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["answer_text"] as? String, answerText) + XCTAssertEqual(payload?["question_topic"] as? String, questionTopic) + XCTAssertEqual(payload?["qna_result_id"] as? String, qnaResultID) + XCTAssertEqual(payload?["items"] as? [[String: String]], [["item_id": "10002", "item_name": "Item2", "variation_id": "10002-A", "sl_campaign_id": "camp1", "sl_campaign_owner": "owner1"], ["item_id": "10003"]]) + XCTAssertEqual(payload?["follow_up_questions"] as? [[String: String]], [["value": "What sizes are available?"], ["value": "Is it machine washable?"]]) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentAnswerView_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentAnswerView_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentAnswerView_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentAnswerView_SendsEmptyAnswerText() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView with an empty answerText should send it.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: "")) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["answer_text"] as? String, "", "an empty answer_text should be sent") + } + + func testTrackProductInsightsAgentAnswerView_CapsItemsAndFollowUpQuestionsAt100() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView should cap items and followUpQuestions at 100 and never send quantity.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let items = (1...150).map { CIOItem(customerID: "item\($0)", quantity: 2) } + let followUpQuestions = (1...150).map { "Question \($0)?" } + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText, items: items, followUpQuestions: followUpQuestions)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + let payloadItems = payload?["items"] as? [[String: Any]] ?? [] + XCTAssertEqual(payloadItems.count, 100, "items should be capped at 100") + XCTAssertTrue(payloadItems.allSatisfy { $0["quantity"] == nil }, "quantity should never be sent") + XCTAssertEqual((payload?["follow_up_questions"] as? [[String: String]])?.count, 100, "follow_up_questions should be capped at 100") + } + + func testTrackProductInsightsAgentAnswerView_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentAnswerView with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentAnswerView_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentAnswerView with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentAnswerView_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentAnswerView with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift new file mode 100644 index 00000000..fbec2024 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift @@ -0,0 +1,151 @@ +// +// ConstructorIOTrackProductInsightsAgentFocusTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentFocusTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentFocus() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentFocus_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentFocus_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentFocus_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentFocus_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentFocus_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentFocus with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentFocus_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentFocus with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentFocus_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentFocus with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift new file mode 100644 index 00000000..74dbbe14 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift @@ -0,0 +1,151 @@ +// +// ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentOutOfViewTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentOutOfView() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentOutOfView_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentOutOfView_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentOutOfView_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentOutOfView_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentOutOfView_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentOutOfView with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentOutOfView_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentOutOfView with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentOutOfView_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentOutOfView with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift new file mode 100644 index 00000000..cd827dbc --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift @@ -0,0 +1,157 @@ +// +// ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentQuestionClickTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let question: String = "Why choose this?" + let questionTopic: String = "highlights" + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentQuestionClick() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertNil(payload?["question_topic"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentQuestionClick_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question, questionTopic: questionTopic, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["question_topic"] as? String, questionTopic) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentQuestionClick_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentQuestionClick_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentQuestionClick_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentQuestionClick_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentQuestionClick with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentQuestionClick_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentQuestionClick with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentQuestionClick_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentQuestionClick with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift new file mode 100644 index 00000000..05d92934 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift @@ -0,0 +1,157 @@ +// +// ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentQuestionSubmitTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let question: String = "Why choose this?" + let questionTopic: String = "highlights" + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentQuestionSubmit() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertNil(payload?["question_topic"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentQuestionSubmit_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question, questionTopic: questionTopic, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["question_topic"] as? String, questionTopic) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentQuestionSubmit_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentQuestionSubmit_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentQuestionSubmit_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentQuestionSubmit_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentQuestionSubmit with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentQuestionSubmit_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentQuestionSubmit with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentQuestionSubmit_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentQuestionSubmit with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift new file mode 100644 index 00000000..73eb511f --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift @@ -0,0 +1,184 @@ +// +// ConstructorIOTrackProductInsightsAgentResultClickTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentResultClickTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10002" + let question: String = "Why choose this?" + let seedItemID: String = "10001" + let position: Int = 3 + let seedItemName: String = "Item1" + let seedVariationID: String = "10001-A" + let qnaResultID: String = "qna-result-123" + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentResultClick() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["seed_item_id"] as? String, seedItemID) + XCTAssertNil(payload?["position"]) + XCTAssertNil(payload?["seed_item_name"]) + XCTAssertNil(payload?["seed_variation_id"]) + XCTAssertNil(payload?["qna_result_id"]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentResultClick_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID, position: position, seedItemName: seedItemName, seedVariationID: seedVariationID, qnaResultID: qnaResultID, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["question"] as? String, question) + XCTAssertEqual(payload?["seed_item_id"] as? String, seedItemID) + XCTAssertEqual(payload?["position"] as? Int, position) + XCTAssertEqual(payload?["seed_item_name"] as? String, seedItemName) + XCTAssertEqual(payload?["seed_variation_id"] as? String, seedVariationID) + XCTAssertEqual(payload?["qna_result_id"] as? String, qnaResultID) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentResultClick_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentResultClick_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentResultClick_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentResultClick_SendsPositionZero() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick with position 0 should send it.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID, position: 0)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["position"] as? Int, 0, "position 0 should be sent") + } + + func testTrackProductInsightsAgentResultClick_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentResultClick with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentResultClick_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentResultClick with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentResultClick_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentResultClick with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift new file mode 100644 index 00000000..dee8f3b9 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift @@ -0,0 +1,154 @@ +// +// ConstructorIOTrackProductInsightsAgentViewTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentViewTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let questions: [CIOProductInsightsAgentQuestion] = [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")] + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentView() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentView_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentView_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentView_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentView_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentView_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentView with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentView_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentView with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentView_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentView with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift new file mode 100644 index 00000000..4ac1c38c --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift @@ -0,0 +1,157 @@ +// +// ConstructorIOTrackProductInsightsAgentViewsTests.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import ConstructorAutocomplete +import OHHTTPStubs +import XCTest + +class ConstructorIOTrackProductInsightsAgentViewsTests: XCTestCase { + + var constructor: ConstructorIO! + let itemID: String = "10001" + let questions: [CIOProductInsightsAgentQuestion] = [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")] + let viewTimespans: [CIOProductInsightsAgentViewTimespan] = [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.456))] + let itemName: String = "Item1" + let variationID: String = "10001-A" + let threadID: String = "thread-123" + let sectionName: String = "Search Suggestions" + let analyticsTags: [String: String] = ["tag1": "value1"] + + override func setUp() { + super.setUp() + self.constructor = TestConstants.testConstructor() + } + + override func tearDown() { + super.tearDown() + OHHTTPStubs.removeAllStubs() + } + + func testTrackProductInsightsAgentViews() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews should send a valid request with the default section name.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertNil(payload?["item_name"]) + XCTAssertNil(payload?["variation_id"]) + XCTAssertNil(payload?["thread_id"]) + XCTAssertNil(payload?["analytics_tags"]) + XCTAssertEqual(payload?["section"] as? String, "Products") + XCTAssertEqual(payload?["beacon"] as? Bool, true) + } + + func testTrackProductInsightsAgentViews_WithAllFields() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews with all fields should send them all.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + self.constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans, itemName: itemName, variationID: variationID, threadID: threadID, sectionName: sectionName, analyticsTags: analyticsTags)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["item_id"] as? String, itemID) + XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertEqual(payload?["item_name"] as? String, itemName) + XCTAssertEqual(payload?["variation_id"] as? String, variationID) + XCTAssertEqual(payload?["thread_id"] as? String, threadID) + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) + } + + func testTrackProductInsightsAgentViews_WithSectionFromConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews should send a valid request with a section name from config.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=section321&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans)) + self.wait(for: builder.expectation) + } + + func testTrackProductInsightsAgentViews_MergesDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value", "tag1": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans, analyticsTags: ["tag1": "value1"])) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value", "tag1": "value1"]) + } + + func testTrackProductInsightsAgentViews_WithOnlyDefaultAnalyticsTags() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews with only defaultAnalyticsTags should send the default tags.", builder: http(200)) + var capturedRequest: URLRequest? + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)")) { request in + capturedRequest = request + return builder.create()(request) + } + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultAnalyticsTags: ["default_tag": "default_value"]) + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans)) + self.wait(for: builder.expectation) + + let payload = capturedRequest?.ohhttpStubs_httpBody.flatMap { try? JSONSerialization.jsonObject(with: $0, options: []) as? [String: Any] } + XCTAssertNotNil(payload, "Request body should contain valid JSON") + XCTAssertEqual(payload?["analytics_tags"] as? [String: String], ["default_tag": "default_value"]) + } + + func testTrackProductInsightsAgentViews_With400() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentViews with 400 should return badRequest CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(400)) + self.constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .badRequest, "If tracking call returns status code 400, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentViews_With500() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentViews with 500 should return internalServerError CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), http(500)) + self.constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .internalServerError, "If tracking call returns status code 500, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } + + func testTrackProductInsightsAgentViews_WithNoConnectivity() { + let expectation = self.expectation(description: "Calling trackProductInsightsAgentViews with no connectivity should return noConnectivity CIOError.") + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Products&\(TestConstants.defaultSegments)"), noConnectivity()) + self.constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans), completionHandler: { response in + if let cioError = response.error as? CIOError { + XCTAssertEqual(cioError.errorType, .noConnection, "If tracking call returns no connectivity, the error should be delegated to the completion handler") + expectation.fulfill() + } + }) + self.wait(for: expectation) + } +} From a6592f1779bd3b8e2d7deeac0eda3552231c4455 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 06:04:02 +0800 Subject: [PATCH 02/10] [CDX-708] Document Product Insights Agent events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a Product Insights Agent Events subsection to README §12 and include the new request and model structs in the jazzy docs. Co-Authored-By: Claude Opus 5.5 --- .jazzy.yaml | 16 ++++++++++++++++ README.md | 40 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/.jazzy.yaml b/.jazzy.yaml index 60c3a689..6e9193ef 100644 --- a/.jazzy.yaml +++ b/.jazzy.yaml @@ -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" @@ -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 diff --git a/README.md b/README.md index fba869b9..05e1a098 100644 --- a/README.md +++ b/README.md @@ -249,6 +249,7 @@ Three types of these events exist: 6. **Quiz Events** measure user interaction with quiz results 7. **Conversion Events** measure user events like `add to cart` or `purchase` 8. **AI Shopping Agent Events** measure user interaction with AI Shopping Agent results +9. **Product Insights Agent Events** measure user interaction with the Product Insights Agent on product detail pages ### Autocomplete Events @@ -361,6 +362,45 @@ constructorIO.trackAgentResultClick(intent: "show me running shoes", searchResul constructorIO.trackAgentSearchSubmit(intent: "show me running shoes", searchTerm: "trail running shoes", searchResultID: "179b8a0e-3799-4a31-be87-127b06871de2", intentResultID: "2b5f8c1e-6a0b-4c2d-9f3e-1a2b3c4d5e6f") ``` +### Product Insights Agent Events + +Each event takes a request object. `itemID` is the ID of the product whose page the Product Insights Agent is on (for `trackProductInsightsAgentResultClick`, it is the clicked recommendation and `seedItemID` is the page's product). `qnaResultID` is the answer result ID from the Product Insights Agent response. Every request also accepts `itemName`, `variationID`, `threadID`, `sectionName` and `analyticsTags`. + +```swift +let questions = [ + CIOProductInsightsAgentQuestion(question: "Why choose this?"), + CIOProductInsightsAgentQuestion(question: "How is this product made?", questionTopic: "materials") +] + +// Track when the Product Insights Agent appears in the visible part of the page +constructorIO.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: "1234567-AB", questions: questions, itemName: "Trail Runner")) + +// Track the periods of time during which the Product Insights Agent was visible +let viewTimespans = [CIOProductInsightsAgentViewTimespan(start: viewStartDate, end: viewEndDate)] +constructorIO.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: "1234567-AB", questions: questions, viewTimespans: viewTimespans)) + +// Track when the Product Insights Agent disappears from the visible part of the page +constructorIO.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: "1234567-AB")) + +// Track when the user focuses on the Product Insights Agent input +constructorIO.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: "1234567-AB")) + +// Track when the user clicks on a suggested question +constructorIO.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: "1234567-AB", question: "Why choose this?")) + +// Track when the user submits a question +constructorIO.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "1234567-AB", question: "Is this waterproof?", questionTopic: "materials")) + +// Track when the user views an answer (items and followUpQuestions are capped at 100) +constructorIO.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "1234567-AB", question: "Is this waterproof?", answerText: "Yes, it has a waterproof membrane.", qnaResultID: "8c1e2b5f-0b6a-2d4c-3e9f-6f5e4d3c2b1a", items: [CIOItem(customerID: "1234765-CD", itemName: "Road Runner")], followUpQuestions: ["What sizes are available?"])) + +// Track when the user gives feedback on an answer +constructorIO.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: "1234567-AB", feedbackLabel: .thumbsUp, qnaResultID: "8c1e2b5f-0b6a-2d4c-3e9f-6f5e4d3c2b1a")) + +// Track when the user clicks a product recommended in an answer +constructorIO.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: "1234765-CD", question: "Is this waterproof?", seedItemID: "1234567-AB", position: 0, qnaResultID: "8c1e2b5f-0b6a-2d4c-3e9f-6f5e4d3c2b1a", itemName: "Road Runner")) +``` + ### Media Impression Events ```swift From 24b6dfd146cd3e74b55b3ce4ca1b452fbc547c41 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 06:20:15 +0800 Subject: [PATCH 03/10] [CDX-708] Address review: distinct result click fixtures, ms rounding, live A/B tests - Use distinct clicked-item and seed values in result click tests and docs - Round view timespans to the nearest millisecond before formatting - Document position as zero-based - Add a live test per event with a userID and test cells set Co-Authored-By: Claude Opus 5.5 --- .../CIOProductInsightsAgentViewTimespan.swift | 10 +- ...oductInsightsAgentResultClickRequest.swift | 4 +- .../FW/Logic/Worker/ConstructorIO.swift | 4 +- ...sAgentResultClickRequestBuilderTests.swift | 4 +- ...nsightsAgentViewsRequestBuilderTests.swift | 19 ++- .../ConstructorIOIntegrationTests.swift | 121 +++++++++++++++++- ...ProductInsightsAgentResultClickTests.swift | 4 +- ...OTrackProductInsightsAgentViewsTests.swift | 6 +- 8 files changed, 154 insertions(+), 18 deletions(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift index b95a68cd..8efed9ac 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift @@ -46,8 +46,14 @@ public struct CIOProductInsightsAgentViewTimespan { // 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) + "start": CIOProductInsightsAgentViewTimespan.format(self.start), + "end": CIOProductInsightsAgentViewTimespan.format(self.end) ] } + + // The formatter truncates to milliseconds, so round first to avoid floating point error dropping a millisecond + static func format(_ date: Date) -> String { + let roundedInterval = (date.timeIntervalSinceReferenceDate * 1000).rounded() / 1000 + return dateFormatter.string(from: Date(timeIntervalSinceReferenceDate: roundedInterval)) + } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift index 6164f887..3483ed18 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift @@ -29,7 +29,7 @@ public struct CIOTrackProductInsightsAgentResultClickRequest { public let seedItemID: String /** - The position of the clicked item in the list of recommendations + The zero-based position of the clicked item in the list of recommendations */ public let position: Int? @@ -80,7 +80,7 @@ public struct CIOTrackProductInsightsAgentResultClickRequest { - itemID: The ID of the clicked recommended product - question: The question that produced the answer containing the clicked recommendation - seedItemID: The ID of the product whose page the Product Insights Agent is on - - position: The position of the clicked item in the list of recommendations + - position: The zero-based position of the clicked item in the list of recommendations - seedItemName: The name of the product whose page the Product Insights Agent is on - seedVariationID: The variation ID of the product whose page the Product Insights Agent is on - qnaResultID: The ID of the answer result returned by the Product Insights Agent diff --git a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift index 16c1fc98..4ba23a71 100644 --- a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift +++ b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift @@ -1043,7 +1043,7 @@ public class ConstructorIO: CIOSessionManagerDelegate { ### Usage Example: ### ``` - constructorIO.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792550405.123), end: Date(timeIntervalSince1970: 1792550410.456))], itemName: "Item1", variationID: "10001-A")) + constructorIO.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.125), end: Date(timeIntervalSince1970: 1792636810.5))], itemName: "Item1", variationID: "10001-A")) ``` */ public func trackProductInsightsAgentViews(_ request: CIOTrackProductInsightsAgentViewsRequest, completionHandler: TrackingCompletionHandler? = nil) { @@ -1123,7 +1123,7 @@ public class ConstructorIO: CIOSessionManagerDelegate { ### Usage Example: ### ``` - constructorIO.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A")) + constructorIO.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", seedVariationID: "10001-A", qnaResultID: "qna-result-123", itemName: "Item2", variationID: "10002-A")) ``` */ public func trackProductInsightsAgentResultClick(_ request: CIOTrackProductInsightsAgentResultClickRequest, completionHandler: TrackingCompletionHandler? = nil) { diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift index 36ab8015..e332a759 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift @@ -19,8 +19,8 @@ class TrackProductInsightsAgentResultClickRequestBuilderTests: XCTestCase { fileprivate let seedItemName: String = "Item1" fileprivate let seedVariationID: String = "10001-A" fileprivate let qnaResultID: String = "qna-result-123" - fileprivate let itemName: String = "Item1" - fileprivate let variationID: String = "10001-A" + fileprivate let itemName: String = "Item2" + fileprivate let variationID: String = "10002-A" fileprivate let threadID: String = "thread-123" fileprivate let sectionName: String = "Search Suggestions" fileprivate let analyticsTags: [String: String] = ["tag1": "value1"] diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift index d1f17e36..9076fb1c 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift @@ -14,7 +14,7 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { fileprivate let testACKey = "asdf1213123" fileprivate let itemID: String = "10001" fileprivate let questions: [CIOProductInsightsAgentQuestion] = [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")] - fileprivate let viewTimespans: [CIOProductInsightsAgentViewTimespan] = [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.456))] + fileprivate let viewTimespans: [CIOProductInsightsAgentViewTimespan] = [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.125), end: Date(timeIntervalSince1970: 1792636810.5))] fileprivate let itemName: String = "Item1" fileprivate let variationID: String = "10001-A" fileprivate let threadID: String = "thread-123" @@ -43,7 +43,7 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { XCTAssertFalse(request.url!.absoluteString.contains("section=")) XCTAssertEqual(payload?["item_id"] as? String, itemID) XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) - XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.125Z", "end": "2026-10-22T02:40:10.500Z"]]) XCTAssertNil(payload?["item_name"]) XCTAssertNil(payload?["variation_id"]) XCTAssertNil(payload?["thread_id"]) @@ -62,7 +62,7 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { XCTAssertTrue(request.url!.absoluteString.contains("section=Search%20Suggestions")) XCTAssertEqual(payload?["item_id"] as? String, itemID) XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) - XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.125Z", "end": "2026-10-22T02:40:10.500Z"]]) XCTAssertEqual(payload?["item_name"] as? String, itemName) XCTAssertEqual(payload?["variation_id"] as? String, variationID) XCTAssertEqual(payload?["thread_id"] as? String, threadID) @@ -85,6 +85,19 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { ]) } + func testTrackProductInsightsAgentViewsBuilder_RoundsTimespansToTheNearestMillisecond() { + let viewTimespans = [ + CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.4567)) + ] + let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: viewTimespans) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [ + ["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.457Z"] + ]) + } + func testTrackProductInsightsAgentViewsBuilder_WithEmptyTimespans() { let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: []) builder.build(trackData: tracker) diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift index 13cc9b46..543f9444 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift @@ -74,6 +74,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentFocus_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentFocusRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentFocus(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentOutOfView() { let expectation = XCTestExpectation(description: "Tracking 204") let request = CIOTrackProductInsightsAgentOutOfViewRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) @@ -85,6 +98,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentOutOfView_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentOutOfViewRequest(itemID: "10001", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentOutOfView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentView() { let expectation = XCTestExpectation(description: "Tracking 204") let request = CIOTrackProductInsightsAgentViewRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) @@ -96,9 +122,22 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentView_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentViewRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentViews() { let expectation = XCTestExpectation(description: "Tracking 204") - let request = CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792550405.123), end: Date(timeIntervalSince1970: 1792550410.456))], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + let request = CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.125), end: Date(timeIntervalSince1970: 1792636810.5))], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) self.constructor.trackProductInsightsAgentViews(request, completionHandler: { response in let cioError = response.error as? CIOError XCTAssertNil(cioError) @@ -107,6 +146,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentViews_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentViewsRequest(itemID: "10001", questions: [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")], viewTimespans: [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.125), end: Date(timeIntervalSince1970: 1792636810.5))], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentViews(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentQuestionClick() { let expectation = XCTestExpectation(description: "Tracking 204") let request = CIOTrackProductInsightsAgentQuestionClickRequest(itemID: "10001", question: "Why choose this?", questionTopic: "highlights", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) @@ -118,6 +170,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentQuestionClick_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentQuestionClickRequest(itemID: "10001", question: "Why choose this?", questionTopic: "highlights", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentQuestionClick(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentQuestionSubmit() { let expectation = XCTestExpectation(description: "Tracking 204") let request = CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "10001", question: "Why choose this?", questionTopic: "highlights", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) @@ -129,6 +194,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentQuestionSubmit_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "10001", question: "Why choose this?", questionTopic: "highlights", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentQuestionSubmit(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentAnswerFeedback() { let expectation = XCTestExpectation(description: "Tracking 204") let request = CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: "10001", feedbackLabel: .thumbsUp, qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) @@ -140,9 +218,22 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentAnswerFeedback_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: "10001", feedbackLabel: .thumbsUp, qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentAnswerFeedback(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentResultClick() { let expectation = XCTestExpectation(description: "Tracking 204") - let request = CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", seedVariationID: "10001-A", qnaResultID: "qna-result-123", itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + let request = CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", seedVariationID: "10001-A", qnaResultID: "qna-result-123", itemName: "Item2", variationID: "10002-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) self.constructor.trackProductInsightsAgentResultClick(request, completionHandler: { response in let cioError = response.error as? CIOError XCTAssertNil(cioError) @@ -151,6 +242,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentResultClick_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentResultClickRequest(itemID: "10002", question: "Why choose this?", seedItemID: "10001", position: 3, seedItemName: "Item1", seedVariationID: "10001-A", qnaResultID: "qna-result-123", itemName: "Item2", variationID: "10002-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentResultClick(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackProductInsightsAgentAnswerView() { let expectation = XCTestExpectation(description: "Tracking 204") let request = CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "10001", question: "Why choose this?", answerText: "This product is made from recycled materials.", questionTopic: "highlights", qnaResultID: "qna-result-123", items: [CIOItem(customerID: "10002", itemName: "Item2", variationID: "10002-A", quantity: 3, slCampaignID: "camp1", slCampaignOwner: "owner1"), CIOItem(customerID: "10003")], followUpQuestions: ["What sizes are available?", "Is it machine washable?"], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) @@ -162,6 +266,19 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentAnswerView_WithUserIDAndTestCells() { + let expectation = XCTestExpectation(description: "Tracking 204") + let constructor = ConstructorIO(config: ConstructorIOConfig(apiKey: testACKey, testCells: [CIOABTestCell(key: "search", value: "constructor")], segments: ["123", "234"], defaultAnalyticsTags: ["appVersion": "123", "appPlatform": "ios"])) + constructor.userID = "user-123" + let request = CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "10001", question: "Why choose this?", answerText: "This product is made from recycled materials.", questionTopic: "highlights", qnaResultID: "qna-result-123", items: [CIOItem(customerID: "10002", itemName: "Item2", variationID: "10002-A", quantity: 3, slCampaignID: "camp1", slCampaignOwner: "owner1"), CIOItem(customerID: "10003")], followUpQuestions: ["What sizes are available?", "Is it machine washable?"], itemName: "Item1", variationID: "10001-A", threadID: "thread-123", sectionName: sectionName, analyticsTags: ["tag1": "value1"]) + constructor.trackProductInsightsAgentAnswerView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackAutocompleteSelect() { let expectation = XCTestExpectation(description: "Tracking 204") self.constructor.trackAutocompleteSelect(searchTerm: searchTerm, originalQuery: originalQuery, sectionName: sectionName, group: group, resultID: resultID, itemID: customerID, completionHandler: { response in diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift index 73eb511f..943e8f5e 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift @@ -20,8 +20,8 @@ class ConstructorIOTrackProductInsightsAgentResultClickTests: XCTestCase { let seedItemName: String = "Item1" let seedVariationID: String = "10001-A" let qnaResultID: String = "qna-result-123" - let itemName: String = "Item1" - let variationID: String = "10001-A" + let itemName: String = "Item2" + let variationID: String = "10002-A" let threadID: String = "thread-123" let sectionName: String = "Search Suggestions" let analyticsTags: [String: String] = ["tag1": "value1"] diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift index 4ac1c38c..517d9f31 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift @@ -15,7 +15,7 @@ class ConstructorIOTrackProductInsightsAgentViewsTests: XCTestCase { var constructor: ConstructorIO! let itemID: String = "10001" let questions: [CIOProductInsightsAgentQuestion] = [CIOProductInsightsAgentQuestion(question: "Why choose this?", questionTopic: "highlights"), CIOProductInsightsAgentQuestion(question: "How is this product made?")] - let viewTimespans: [CIOProductInsightsAgentViewTimespan] = [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.456))] + let viewTimespans: [CIOProductInsightsAgentViewTimespan] = [CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.125), end: Date(timeIntervalSince1970: 1792636810.5))] let itemName: String = "Item1" let variationID: String = "10001-A" let threadID: String = "thread-123" @@ -46,7 +46,7 @@ class ConstructorIOTrackProductInsightsAgentViewsTests: XCTestCase { XCTAssertNotNil(payload, "Request body should contain valid JSON") XCTAssertEqual(payload?["item_id"] as? String, itemID) XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) - XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.125Z", "end": "2026-10-22T02:40:10.500Z"]]) XCTAssertNil(payload?["item_name"]) XCTAssertNil(payload?["variation_id"]) XCTAssertNil(payload?["thread_id"]) @@ -69,7 +69,7 @@ class ConstructorIOTrackProductInsightsAgentViewsTests: XCTestCase { XCTAssertNotNil(payload, "Request body should contain valid JSON") XCTAssertEqual(payload?["item_id"] as? String, itemID) XCTAssertEqual(payload?["questions"] as? [[String: String]], [["question": "Why choose this?", "question_topic": "highlights"], ["question": "How is this product made?"]]) - XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.456Z"]]) + XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [["start": "2026-10-22T02:40:05.125Z", "end": "2026-10-22T02:40:10.500Z"]]) XCTAssertEqual(payload?["item_name"] as? String, itemName) XCTAssertEqual(payload?["variation_id"] as? String, variationID) XCTAssertEqual(payload?["thread_id"] as? String, threadID) From 2d416a3c007e31f4a9dd7af9e1ba2554d630a8a8 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 06:33:47 +0800 Subject: [PATCH 04/10] [CDX-708] Drop redundant timespan rounding ISO8601DateFormatter already rounds to the nearest millisecond, so format dates directly and cover more millisecond values in the test. Co-Authored-By: Claude Opus 5.5 --- .../Request/CIOProductInsightsAgentViewTimespan.swift | 10 ++-------- ...kProductInsightsAgentViewsRequestBuilderTests.swift | 8 +++++--- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift index 8efed9ac..b95a68cd 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift @@ -46,14 +46,8 @@ public struct CIOProductInsightsAgentViewTimespan { // 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.format(self.start), - "end": CIOProductInsightsAgentViewTimespan.format(self.end) + "start": CIOProductInsightsAgentViewTimespan.dateFormatter.string(from: self.start), + "end": CIOProductInsightsAgentViewTimespan.dateFormatter.string(from: self.end) ] } - - // The formatter truncates to milliseconds, so round first to avoid floating point error dropping a millisecond - static func format(_ date: Date) -> String { - let roundedInterval = (date.timeIntervalSinceReferenceDate * 1000).rounded() / 1000 - return dateFormatter.string(from: Date(timeIntervalSinceReferenceDate: roundedInterval)) - } } diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift index 9076fb1c..88855d22 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift @@ -85,16 +85,18 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { ]) } - func testTrackProductInsightsAgentViewsBuilder_RoundsTimespansToTheNearestMillisecond() { + func testTrackProductInsightsAgentViewsBuilder_FormatsTimespansToTheNearestMillisecond() { let viewTimespans = [ - CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.4567)) + CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.123), end: Date(timeIntervalSince1970: 1792636810.4567)), + CIOProductInsightsAgentViewTimespan(start: Date(timeIntervalSince1970: 1792636805.009), end: Date(timeIntervalSince1970: 1792636805.015)) ] let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: viewTimespans) builder.build(trackData: tracker) let payload = self.payload(of: builder.getRequest()) XCTAssertEqual(payload?["view_timespans"] as? [[String: String]], [ - ["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.457Z"] + ["start": "2026-10-22T02:40:05.123Z", "end": "2026-10-22T02:40:10.457Z"], + ["start": "2026-10-22T02:40:05.009Z", "end": "2026-10-22T02:40:05.015Z"] ]) } From 0ea69cd12cf905b21502f79f7c962ec550fbac41 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 22:23:09 +0800 Subject: [PATCH 05/10] [CDX-708] Share PIA common body fields and CIOItem mapping - Add an internal CIOTrackProductInsightsAgentData protocol whose addCommonFields(to:) writes item_name, variation_id, thread_id and analytics_tags for all 9 events - Move the CIOItem body mapping to CIOItem.toDictionary() and reuse it in trackResultsImpressionView, which already sent the same shape Co-Authored-By: Claude Opus 5.5 --- AutocompleteClient.xcodeproj/project.pbxproj | 4 ++ .../FW/Logic/Request/CIOItem.swift | 18 +++++++++ ...oductInsightsAgentAnswerFeedbackData.swift | 15 +------ ...ckProductInsightsAgentAnswerViewData.swift | 37 ++---------------- .../CIOTrackProductInsightsAgentData.swift | 39 +++++++++++++++++++ ...IOTrackProductInsightsAgentFocusData.swift | 15 +------ ...ackProductInsightsAgentOutOfViewData.swift | 15 +------ ...roductInsightsAgentQuestionClickData.swift | 15 +------ ...oductInsightsAgentQuestionSubmitData.swift | 15 +------ ...kProductInsightsAgentResultClickData.swift | 15 +------ ...CIOTrackProductInsightsAgentViewData.swift | 15 +------ ...IOTrackProductInsightsAgentViewsData.swift | 15 +------ .../CIOTrackResultsImpressionViewData.swift | 11 +----- 13 files changed, 81 insertions(+), 148 deletions(-) create mode 100644 AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift diff --git a/AutocompleteClient.xcodeproj/project.pbxproj b/AutocompleteClient.xcodeproj/project.pbxproj index ce9d011c..3c05b2b7 100644 --- a/AutocompleteClient.xcodeproj/project.pbxproj +++ b/AutocompleteClient.xcodeproj/project.pbxproj @@ -404,6 +404,7 @@ F6F1D707234B693D0092341D /* response_search_json_variations.json in Resources */ = {isa = PBXBuildFile; fileRef = F6F1D706234B693D0092341D /* response_search_json_variations.json */; }; F6F1D709234B6BFA0092341D /* response_search_json_invalid_variations.json in Resources */ = {isa = PBXBuildFile; fileRef = F6F1D708234B6BFA0092341D /* response_search_json_invalid_variations.json */; }; F6F1D70B234B6FD50092341D /* response_search_json_variation_custom_data.json in Resources */ = {isa = PBXBuildFile; fileRef = F6F1D70A234B6FD50092341D /* response_search_json_variation_custom_data.json */; }; + F7F8653BE4EC229CBCF39EF9 /* CIOTrackProductInsightsAgentData.swift in Sources */ = {isa = PBXBuildFile; fileRef = E0FE6A4B1E7D5F0C882461A5 /* CIOTrackProductInsightsAgentData.swift */; }; FC5DEABC0479238454C51141 /* TrackProductInsightsAgentViewRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FBD854D16E22B01230B750A2 /* TrackProductInsightsAgentViewRequestBuilderTests.swift */; }; FFBFF2D9B43441E34588BFC7 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 31D4419F69611F6AE5E1AF63 /* TrackProductInsightsAgentResultClickRequestBuilderTests.swift */; }; /* End PBXBuildFile section */ @@ -641,6 +642,7 @@ D8824AB4E0C4BF784703D6BA /* Pods-AutocompleteClientTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AutocompleteClientTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AutocompleteClientTests/Pods-AutocompleteClientTests.debug.xcconfig"; sourceTree = ""; }; D938EAC4478309F3008F3B0D /* Pods_UserApplication.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_UserApplication.framework; sourceTree = BUILT_PRODUCTS_DIR; }; DADC00EC0CF7961F94F3DF15 /* CIOTrackProductInsightsAgentQuestionSubmitRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentQuestionSubmitRequest.swift; sourceTree = ""; }; + E0FE6A4B1E7D5F0C882461A5 /* CIOTrackProductInsightsAgentData.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentData.swift; sourceTree = ""; }; E1DD9138622D7CACE62DE89F /* TrackAgentResultLoadFinishedRequestBuilderTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = TrackAgentResultLoadFinishedRequestBuilderTests.swift; sourceTree = ""; }; E2E5687AB79AC0D2B97A4F26 /* CIOTrackProductInsightsAgentFocusRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CIOTrackProductInsightsAgentFocusRequest.swift; sourceTree = ""; }; E515E2A713406EB70485D4BF /* ConstructorIOTrackAgentResultLoadFinishedTests.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ConstructorIOTrackAgentResultLoadFinishedTests.swift; sourceTree = ""; }; @@ -1069,6 +1071,7 @@ 91FA523A8C110147035C3D17 /* CIOTrackProductInsightsAgentViewRequest.swift */, 6FC0FD8D7D1299CFE25B0E84 /* CIOTrackProductInsightsAgentViewsData.swift */, 78F25B5E1835A7C2C32A3D8D /* CIOTrackProductInsightsAgentViewsRequest.swift */, + E0FE6A4B1E7D5F0C882461A5 /* CIOTrackProductInsightsAgentData.swift */, ); path = Request; sourceTree = ""; @@ -2830,6 +2833,7 @@ D4DA80E74933A0242D69111C /* CIOTrackProductInsightsAgentViewRequest.swift in Sources */, 7F4EFC5A6328D2A6211DFB92 /* CIOTrackProductInsightsAgentViewsData.swift in Sources */, CF83F2B8A45DE57007BA43A3 /* CIOTrackProductInsightsAgentViewsRequest.swift in Sources */, + F7F8653BE4EC229CBCF39EF9 /* CIOTrackProductInsightsAgentData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/AutocompleteClient/FW/Logic/Request/CIOItem.swift b/AutocompleteClient/FW/Logic/Request/CIOItem.swift index c5ae5c86..62b44f5a 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOItem.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOItem.swift @@ -24,4 +24,22 @@ public struct CIOItem { self.slCampaignID = slCampaignID self.slCampaignOwner = slCampaignOwner } + + // Builds the item object sent in tracking request bodies. `quantity` is never included + func toDictionary() -> [String: String] { + 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 + } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift index 61049172..5189b849 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent answer feedback */ -struct CIOTrackProductInsightsAgentAnswerFeedbackData: CIORequestData { +struct CIOTrackProductInsightsAgentAnswerFeedbackData: CIOTrackProductInsightsAgentData { let itemID: String let feedbackLabel: CIOProductInsightsAgentFeedbackLabel @@ -54,18 +54,7 @@ struct CIOTrackProductInsightsAgentAnswerFeedbackData: CIORequestData { if let qnaResultID = self.qnaResultID { dict["qna_result_id"] = qnaResultID } - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift index 1cfed110..4ba335a3 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent answer view */ -struct CIOTrackProductInsightsAgentAnswerViewData: CIORequestData { +struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentData { let itemID: String let question: String @@ -67,23 +67,12 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIORequestData { dict["qna_result_id"] = qnaResultID } if let items = self.items { - dict["items"] = items.prefix(100).map { $0.productInsightsAgentDictionary() } + dict["items"] = items.prefix(100).map { $0.toDictionary() } } if let followUpQuestions = self.followUpQuestions { dict["follow_up_questions"] = followUpQuestions.prefix(100).map { ["value": $0] } } - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } @@ -91,23 +80,3 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIORequestData { return try? JSONSerialization.data(withJSONObject: dict) } } - -private extension CIOItem { - // Only the keys accepted by the API are sent. `quantity` is rejected with a 400, so it is never included - func productInsightsAgentDictionary() -> [String: String] { - 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 - } -} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift new file mode 100644 index 00000000..ce44553d --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift @@ -0,0 +1,39 @@ +// +// CIOTrackProductInsightsAgentData.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import Foundation + +/** + Protocol for the fields shared by every Product Insights Agent tracking event + */ +protocol CIOTrackProductInsightsAgentData: CIORequestData { + + var itemName: String? { get } + var variationID: String? { get } + var threadID: String? { get } + var analyticsTags: [String: String]? { get } +} + +extension CIOTrackProductInsightsAgentData { + + // Adds the optional fields shared by every Product Insights Agent event to the request body + func addCommonFields(to dict: inout [String: Any]) { + if let itemName = self.itemName { + dict["item_name"] = itemName + } + if let variationID = self.variationID { + dict["variation_id"] = variationID + } + if let threadID = self.threadID { + dict["thread_id"] = threadID + } + if let analyticsTags = self.analyticsTags { + dict["analytics_tags"] = analyticsTags + } + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift index 70942ad1..a80509c8 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent focus */ -struct CIOTrackProductInsightsAgentFocusData: CIORequestData { +struct CIOTrackProductInsightsAgentFocusData: CIOTrackProductInsightsAgentData { let itemID: String let itemName: String? @@ -46,18 +46,7 @@ struct CIOTrackProductInsightsAgentFocusData: CIORequestData { "item_id": self.itemID ] as [String: Any] - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift index 97f056fd..83c8674a 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent out of view */ -struct CIOTrackProductInsightsAgentOutOfViewData: CIORequestData { +struct CIOTrackProductInsightsAgentOutOfViewData: CIOTrackProductInsightsAgentData { let itemID: String let itemName: String? @@ -46,18 +46,7 @@ struct CIOTrackProductInsightsAgentOutOfViewData: CIORequestData { "item_id": self.itemID ] as [String: Any] - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift index 399e5797..5bbc11ae 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent question click */ -struct CIOTrackProductInsightsAgentQuestionClickData: CIORequestData { +struct CIOTrackProductInsightsAgentQuestionClickData: CIOTrackProductInsightsAgentData { let itemID: String let question: String @@ -54,18 +54,7 @@ struct CIOTrackProductInsightsAgentQuestionClickData: CIORequestData { if let questionTopic = self.questionTopic { dict["question_topic"] = questionTopic } - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift index 4c8e2108..f5e8bf7a 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent question submit */ -struct CIOTrackProductInsightsAgentQuestionSubmitData: CIORequestData { +struct CIOTrackProductInsightsAgentQuestionSubmitData: CIOTrackProductInsightsAgentData { let itemID: String let question: String @@ -54,18 +54,7 @@ struct CIOTrackProductInsightsAgentQuestionSubmitData: CIORequestData { if let questionTopic = self.questionTopic { dict["question_topic"] = questionTopic } - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift index c316a1f8..014d6adc 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent result click */ -struct CIOTrackProductInsightsAgentResultClickData: CIORequestData { +struct CIOTrackProductInsightsAgentResultClickData: CIOTrackProductInsightsAgentData { let itemID: String let question: String @@ -72,18 +72,7 @@ struct CIOTrackProductInsightsAgentResultClickData: CIORequestData { if let qnaResultID = self.qnaResultID { dict["qna_result_id"] = qnaResultID } - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift index 78017a7d..10e1ca00 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent view */ -struct CIOTrackProductInsightsAgentViewData: CIORequestData { +struct CIOTrackProductInsightsAgentViewData: CIOTrackProductInsightsAgentData { let itemID: String let questions: [CIOProductInsightsAgentQuestion] @@ -49,18 +49,7 @@ struct CIOTrackProductInsightsAgentViewData: CIORequestData { "questions": self.questions.map { $0.toDictionary() } ] as [String: Any] - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift index ba2f4e1b..15581330 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift @@ -11,7 +11,7 @@ import Foundation /** Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent views */ -struct CIOTrackProductInsightsAgentViewsData: CIORequestData { +struct CIOTrackProductInsightsAgentViewsData: CIOTrackProductInsightsAgentData { let itemID: String let questions: [CIOProductInsightsAgentQuestion] @@ -52,18 +52,7 @@ struct CIOTrackProductInsightsAgentViewsData: CIORequestData { "view_timespans": self.viewTimespans.map { $0.toDictionary() } ] as [String: Any] - if let itemName = self.itemName { - dict["item_name"] = itemName - } - if let variationID = self.variationID { - dict["variation_id"] = variationID - } - if let threadID = self.threadID { - dict["thread_id"] = threadID - } - if let analyticsTags = self.analyticsTags { - dict["analytics_tags"] = analyticsTags - } + self.addCommonFields(to: &dict) dict["beacon"] = true dict.merge(baseParams) { current, _ in current } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift index 85d7a1ba..cdfce843 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift @@ -37,17 +37,8 @@ struct CIOTrackResultsImpressionViewData: CIORequestData { } func httpBody(baseParams: [String: Any]) -> Data? { - let itemDicts: [[String: Any]] = items.map { item in - var dict: [String: Any] = ["item_id": item.customerID] - if let name = item.itemName { dict["item_name"] = name } - if let vid = item.variationID { dict["variation_id"] = vid } - if let cid = item.slCampaignID { dict["sl_campaign_id"] = cid } - if let owner = item.slCampaignOwner { dict["sl_campaign_owner"] = owner } - return dict - } - var dict: [String: Any] = [ - "items": itemDicts, + "items": items.map { $0.toDictionary() }, "beacon": true ] From 90c26102e42fea9ab028797a62b6f80ec258eda2 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 23:02:57 +0800 Subject: [PATCH 06/10] [CDX-708] Address review: shared PIA request data, section precedence tests - Move decorateRequest, httpMethod and httpBody into the CIOTrackProductInsightsAgentData protocol extension; each event now only provides eventFields() - Add a test per event that the request section overrides the config default - Rename CIOItem.toDictionary() to toImpressionDictionary() and document which events use that shape - Document the 1-1000 character limit on question fields - Define the timespan dates in the README example Co-Authored-By: Claude Opus 5.5 --- .../FW/Logic/Request/CIOItem.swift | 5 ++-- ...oductInsightsAgentAnswerFeedbackData.swift | 17 ++---------- ...ckProductInsightsAgentAnswerViewData.swift | 19 +++---------- ...roductInsightsAgentAnswerViewRequest.swift | 4 +-- .../CIOTrackProductInsightsAgentData.swift | 27 ++++++++++++++++--- ...IOTrackProductInsightsAgentFocusData.swift | 21 ++------------- ...ackProductInsightsAgentOutOfViewData.swift | 21 ++------------- ...roductInsightsAgentQuestionClickData.swift | 17 ++---------- ...uctInsightsAgentQuestionClickRequest.swift | 4 +-- ...oductInsightsAgentQuestionSubmitData.swift | 17 ++---------- ...ctInsightsAgentQuestionSubmitRequest.swift | 4 +-- ...kProductInsightsAgentResultClickData.swift | 17 ++---------- ...oductInsightsAgentResultClickRequest.swift | 4 +-- ...CIOTrackProductInsightsAgentViewData.swift | 22 +++------------ ...IOTrackProductInsightsAgentViewsData.swift | 22 +++------------ .../CIOTrackResultsImpressionViewData.swift | 2 +- ...ductInsightsAgentAnswerFeedbackTests.swift | 9 +++++++ ...kProductInsightsAgentAnswerViewTests.swift | 9 +++++++ ...OTrackProductInsightsAgentFocusTests.swift | 9 +++++++ ...ckProductInsightsAgentOutOfViewTests.swift | 9 +++++++ ...oductInsightsAgentQuestionClickTests.swift | 9 +++++++ ...ductInsightsAgentQuestionSubmitTests.swift | 9 +++++++ ...ProductInsightsAgentResultClickTests.swift | 9 +++++++ ...IOTrackProductInsightsAgentViewTests.swift | 9 +++++++ ...OTrackProductInsightsAgentViewsTests.swift | 9 +++++++ README.md | 2 ++ 26 files changed, 140 insertions(+), 166 deletions(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOItem.swift b/AutocompleteClient/FW/Logic/Request/CIOItem.swift index 62b44f5a..2d0ae697 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOItem.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOItem.swift @@ -25,8 +25,9 @@ public struct CIOItem { self.slCampaignOwner = slCampaignOwner } - // Builds the item object sent in tracking request bodies. `quantity` is never included - func toDictionary() -> [String: String] { + // Builds the item object sent by trackResultsImpressionView and trackProductInsightsAgentAnswerView. + // Other events send different item shapes (e.g. purchase sends quantity, and ASA/search/browse/recommendations omit item_name) + func toImpressionDictionary() -> [String: String] { var dict = ["item_id": self.customerID] if let itemName = self.itemName { dict["item_name"] = itemName diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift index 5189b849..7223abba 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift @@ -37,28 +37,15 @@ struct CIOTrackProductInsightsAgentAnswerFeedbackData: CIOTrackProductInsightsAg self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { + func eventFields() -> [String: Any] { var dict = [ - "item_id": self.itemID, "feedback_label": self.feedbackLabel.rawValue ] as [String: Any] if let qnaResultID = self.qnaResultID { dict["qna_result_id"] = qnaResultID } - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - return try? JSONSerialization.data(withJSONObject: dict) + return dict } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift index 4ba335a3..d6b88bfc 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift @@ -45,17 +45,8 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentD self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { + func eventFields() -> [String: Any] { var dict = [ - "item_id": self.itemID, "question": self.question, "answer_text": self.answerText ] as [String: Any] @@ -67,16 +58,12 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentD dict["qna_result_id"] = qnaResultID } if let items = self.items { - dict["items"] = items.prefix(100).map { $0.toDictionary() } + dict["items"] = items.prefix(100).map { $0.toImpressionDictionary() } } if let followUpQuestions = self.followUpQuestions { dict["follow_up_questions"] = followUpQuestions.prefix(100).map { ["value": $0] } } - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - return try? JSONSerialization.data(withJSONObject: dict) + return dict } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift index 3809a8fb..d9d54278 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift @@ -19,7 +19,7 @@ public struct CIOTrackProductInsightsAgentAnswerViewRequest { public let itemID: String /** - The question the answer was provided for + The question the answer was provided for (1-1000 characters) */ public let question: String @@ -78,7 +78,7 @@ public struct CIOTrackProductInsightsAgentAnswerViewRequest { - Parameters: - itemID: The ID of the product whose page the Product Insights Agent is on - - question: The question the answer was provided for + - question: The question the answer was provided for (1-1000 characters) - answerText: The text of the answer shown to the user (an empty string is allowed) - questionTopic: The topic category of the question - qnaResultID: The ID of the answer result returned by the Product Insights Agent diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift index ce44553d..21eea1aa 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift @@ -9,20 +9,36 @@ import Foundation /** - Protocol for the fields shared by every Product Insights Agent tracking event + Protocol for the request data shared by every Product Insights Agent tracking event. + Conforming types only provide their URL and event-specific body fields. */ protocol CIOTrackProductInsightsAgentData: CIORequestData { + var itemID: String { get } var itemName: String? { get } var variationID: String? { get } var threadID: String? { get } + var sectionName: String? { get } var analyticsTags: [String: String]? { get } + + // The body fields specific to this event, excluding the fields shared by every event + func eventFields() -> [String: Any] } extension CIOTrackProductInsightsAgentData { - // Adds the optional fields shared by every Product Insights Agent event to the request body - func addCommonFields(to dict: inout [String: Any]) { + func decorateRequest(requestBuilder: RequestBuilder) { + requestBuilder.set(autocompleteSection: self.sectionName) + } + + func httpMethod() -> String { + return "POST" + } + + func httpBody(baseParams: [String: Any]) -> Data? { + var dict = self.eventFields() + dict["item_id"] = self.itemID + if let itemName = self.itemName { dict["item_name"] = itemName } @@ -35,5 +51,10 @@ extension CIOTrackProductInsightsAgentData { if let analyticsTags = self.analyticsTags { dict["analytics_tags"] = analyticsTags } + + dict["beacon"] = true + dict.merge(baseParams) { current, _ in current } + + return try? JSONSerialization.data(withJSONObject: dict) } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift index a80509c8..842be2d0 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift @@ -33,24 +33,7 @@ struct CIOTrackProductInsightsAgentFocusData: CIOTrackProductInsightsAgentData { self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { - var dict = [ - "item_id": self.itemID - ] as [String: Any] - - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - - return try? JSONSerialization.data(withJSONObject: dict) + func eventFields() -> [String: Any] { + return [:] } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift index 83c8674a..839a493c 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift @@ -33,24 +33,7 @@ struct CIOTrackProductInsightsAgentOutOfViewData: CIOTrackProductInsightsAgentDa self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { - var dict = [ - "item_id": self.itemID - ] as [String: Any] - - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - - return try? JSONSerialization.data(withJSONObject: dict) + func eventFields() -> [String: Any] { + return [:] } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift index 5bbc11ae..abc0be62 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift @@ -37,28 +37,15 @@ struct CIOTrackProductInsightsAgentQuestionClickData: CIOTrackProductInsightsAge self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { + func eventFields() -> [String: Any] { var dict = [ - "item_id": self.itemID, "question": self.question ] as [String: Any] if let questionTopic = self.questionTopic { dict["question_topic"] = questionTopic } - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - return try? JSONSerialization.data(withJSONObject: dict) + return dict } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift index e661bf24..6c5e961e 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift @@ -19,7 +19,7 @@ public struct CIOTrackProductInsightsAgentQuestionClickRequest { public let itemID: String /** - The question the user clicked on + The question the user clicked on (1-1000 characters) */ public let question: String @@ -58,7 +58,7 @@ public struct CIOTrackProductInsightsAgentQuestionClickRequest { - Parameters: - itemID: The ID of the product whose page the Product Insights Agent is on - - question: The question the user clicked on + - question: The question the user clicked on (1-1000 characters) - questionTopic: The topic category of the question - 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 diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift index f5e8bf7a..20744b2c 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift @@ -37,28 +37,15 @@ struct CIOTrackProductInsightsAgentQuestionSubmitData: CIOTrackProductInsightsAg self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { + func eventFields() -> [String: Any] { var dict = [ - "item_id": self.itemID, "question": self.question ] as [String: Any] if let questionTopic = self.questionTopic { dict["question_topic"] = questionTopic } - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - return try? JSONSerialization.data(withJSONObject: dict) + return dict } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift index 5e4804bf..8f2ea567 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift @@ -19,7 +19,7 @@ public struct CIOTrackProductInsightsAgentQuestionSubmitRequest { public let itemID: String /** - The question the user submitted + The question the user submitted (1-1000 characters) */ public let question: String @@ -58,7 +58,7 @@ public struct CIOTrackProductInsightsAgentQuestionSubmitRequest { - Parameters: - itemID: The ID of the product whose page the Product Insights Agent is on - - question: The question the user submitted + - question: The question the user submitted (1-1000 characters) - questionTopic: The topic category of the question - 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 diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift index 014d6adc..cfcaf837 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift @@ -45,17 +45,8 @@ struct CIOTrackProductInsightsAgentResultClickData: CIOTrackProductInsightsAgent self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { + func eventFields() -> [String: Any] { var dict = [ - "item_id": self.itemID, "question": self.question, "seed_item_id": self.seedItemID ] as [String: Any] @@ -72,11 +63,7 @@ struct CIOTrackProductInsightsAgentResultClickData: CIOTrackProductInsightsAgent if let qnaResultID = self.qnaResultID { dict["qna_result_id"] = qnaResultID } - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - return try? JSONSerialization.data(withJSONObject: dict) + return dict } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift index 3483ed18..8022e718 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift @@ -19,7 +19,7 @@ public struct CIOTrackProductInsightsAgentResultClickRequest { public let itemID: String /** - The question that produced the answer containing the clicked recommendation + The question that produced the answer containing the clicked recommendation (1-1000 characters) */ public let question: String @@ -78,7 +78,7 @@ public struct CIOTrackProductInsightsAgentResultClickRequest { - Parameters: - itemID: The ID of the clicked recommended product - - question: The question that produced the answer containing the clicked recommendation + - question: The question that produced the answer containing the clicked recommendation (1-1000 characters) - seedItemID: The ID of the product whose page the Product Insights Agent is on - position: The zero-based position of the clicked item in the list of recommendations - seedItemName: The name of the product whose page the Product Insights Agent is on diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift index 10e1ca00..bb6ed8b6 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift @@ -35,25 +35,9 @@ struct CIOTrackProductInsightsAgentViewData: CIOTrackProductInsightsAgentData { self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { - var dict = [ - "item_id": self.itemID, + func eventFields() -> [String: Any] { + return [ "questions": self.questions.map { $0.toDictionary() } - ] as [String: Any] - - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - - return try? JSONSerialization.data(withJSONObject: dict) + ] } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift index 15581330..ac647adc 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift @@ -37,26 +37,10 @@ struct CIOTrackProductInsightsAgentViewsData: CIOTrackProductInsightsAgentData { self.analyticsTags = analyticsTags } - func decorateRequest(requestBuilder: RequestBuilder) { - requestBuilder.set(autocompleteSection: self.sectionName) - } - - func httpMethod() -> String { - return "POST" - } - - func httpBody(baseParams: [String: Any]) -> Data? { - var dict = [ - "item_id": self.itemID, + func eventFields() -> [String: Any] { + return [ "questions": self.questions.map { $0.toDictionary() }, "view_timespans": self.viewTimespans.map { $0.toDictionary() } - ] as [String: Any] - - self.addCommonFields(to: &dict) - - dict["beacon"] = true - dict.merge(baseParams) { current, _ in current } - - return try? JSONSerialization.data(withJSONObject: dict) + ] } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift index cdfce843..2d029590 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift @@ -38,7 +38,7 @@ struct CIOTrackResultsImpressionViewData: CIORequestData { func httpBody(baseParams: [String: Any]) -> Data? { var dict: [String: Any] = [ - "items": items.map { $0.toDictionary() }, + "items": items.map { $0.toImpressionDictionary() }, "beacon": true ] diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift index 8f04991e..3413a53d 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift @@ -85,6 +85,15 @@ class ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentAnswerFeedback_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_feedback?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerFeedback(CIOTrackProductInsightsAgentAnswerFeedbackRequest(itemID: itemID, feedbackLabel: feedbackLabel, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentAnswerFeedback_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerFeedback should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift index 195a19cf..230c6be1 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift @@ -97,6 +97,15 @@ class ConstructorIOTrackProductInsightsAgentAnswerViewTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentAnswerView_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_answer_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: itemID, question: question, answerText: answerText, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentAnswerView_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentAnswerView should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift index fbec2024..04855b0d 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift @@ -79,6 +79,15 @@ class ConstructorIOTrackProductInsightsAgentFocusTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentFocus_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_focus?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentFocus(CIOTrackProductInsightsAgentFocusRequest(itemID: itemID, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentFocus_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentFocus should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift index 74dbbe14..aac99e58 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift @@ -79,6 +79,15 @@ class ConstructorIOTrackProductInsightsAgentOutOfViewTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentOutOfView_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_out_of_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentOutOfView(CIOTrackProductInsightsAgentOutOfViewRequest(itemID: itemID, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentOutOfView_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentOutOfView should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift index cd827dbc..9c1ffde8 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift @@ -85,6 +85,15 @@ class ConstructorIOTrackProductInsightsAgentQuestionClickTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentQuestionClick_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgentQuestionClickRequest(itemID: itemID, question: question, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentQuestionClick_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionClick should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift index 05d92934..f3252d0c 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift @@ -85,6 +85,15 @@ class ConstructorIOTrackProductInsightsAgentQuestionSubmitTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentQuestionSubmit_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_question_submit?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: itemID, question: question, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentQuestionSubmit_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentQuestionSubmit should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift index 943e8f5e..1e260c4f 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift @@ -97,6 +97,15 @@ class ConstructorIOTrackProductInsightsAgentResultClickTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentResultClick_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_result_click?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentResultClick(CIOTrackProductInsightsAgentResultClickRequest(itemID: itemID, question: question, seedItemID: seedItemID, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentResultClick_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentResultClick should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift index dee8f3b9..a10ab066 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift @@ -82,6 +82,15 @@ class ConstructorIOTrackProductInsightsAgentViewTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentView_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_view?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: itemID, questions: questions, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentView_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentView should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift index 517d9f31..1c981d57 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift @@ -85,6 +85,15 @@ class ConstructorIOTrackProductInsightsAgentViewsTests: XCTestCase { self.wait(for: builder.expectation) } + func testTrackProductInsightsAgentViews_RequestSectionOverridesConfig() { + let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews should prefer the request section name over the config default.", builder: http(200)) + stub(regex("https://ac.cnstrc.com/v2/behavioral_action/product_insights_agent_views?_dt=\(kRegexTimestamp)&c=\(kRegexVersion)&i=\(kRegexClientID)&key=\(kRegexAutocompleteKey)&s=\(kRegexSession)§ion=Search%20Suggestions&\(TestConstants.defaultSegments)"), builder.create()) + let config = ConstructorIOConfig(apiKey: TestConstants.testApiKey, defaultItemSectionName: "section321") + let constructor = TestConstants.testConstructor(config) + constructor.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: itemID, questions: questions, viewTimespans: viewTimespans, sectionName: sectionName)) + self.wait(for: builder.expectation) + } + func testTrackProductInsightsAgentViews_MergesDefaultAnalyticsTags() { let builder = CIOBuilder(expectation: "Calling trackProductInsightsAgentViews should merge defaultAnalyticsTags and analyticsTags.", builder: http(200)) var capturedRequest: URLRequest? diff --git a/README.md b/README.md index 05e1a098..17cd5400 100644 --- a/README.md +++ b/README.md @@ -376,6 +376,8 @@ let questions = [ constructorIO.trackProductInsightsAgentView(CIOTrackProductInsightsAgentViewRequest(itemID: "1234567-AB", questions: questions, itemName: "Trail Runner")) // Track the periods of time during which the Product Insights Agent was visible +let viewStartDate = Date() +let viewEndDate = viewStartDate.addingTimeInterval(5) let viewTimespans = [CIOProductInsightsAgentViewTimespan(start: viewStartDate, end: viewEndDate)] constructorIO.trackProductInsightsAgentViews(CIOTrackProductInsightsAgentViewsRequest(itemID: "1234567-AB", questions: questions, viewTimespans: viewTimespans)) From e7178d54350377bb5a27d73b70f4b24e0dfaca25 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 23:27:31 +0800 Subject: [PATCH 07/10] [CDX-708] Address review: iOS 11 safe timespan formatting, body key-set tests - Format view timespans with a fixed-format DateFormatter, since ISO8601DateFormatter's .withFractionalSeconds throws on iOS 11.0/11.1 - Resolve section and analytics tags for all 9 events in one private helper - Assert each event's body contains only keys accepted by the API - Document the 2000 character limit on answerText Co-Authored-By: Claude Opus 5.5 --- .../CIOProductInsightsAgentViewTimespan.swift | 12 ++- ...roductInsightsAgentAnswerViewRequest.swift | 4 +- .../FW/Logic/Worker/ConstructorIO.swift | 88 +++++++++---------- ...entAnswerFeedbackRequestBuilderTests.swift | 1 + ...tsAgentAnswerViewRequestBuilderTests.swift | 1 + ...nsightsAgentFocusRequestBuilderTests.swift | 1 + ...htsAgentOutOfViewRequestBuilderTests.swift | 1 + ...gentQuestionClickRequestBuilderTests.swift | 1 + ...entQuestionSubmitRequestBuilderTests.swift | 1 + ...sAgentResultClickRequestBuilderTests.swift | 1 + ...InsightsAgentViewRequestBuilderTests.swift | 1 + ...nsightsAgentViewsRequestBuilderTests.swift | 1 + README.md | 2 +- 13 files changed, 63 insertions(+), 52 deletions(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift index b95a68cd..feb2c40e 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift @@ -35,11 +35,15 @@ public struct CIOProductInsightsAgentViewTimespan { self.end = end } - // ISO8601DateFormatter is thread-safe, so a single shared instance is used for all timespans - static let dateFormatter: ISO8601DateFormatter = { - let formatter = ISO8601DateFormatter() - formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + // 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 = { + 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 }() diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift index d9d54278..75391efc 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift @@ -24,7 +24,7 @@ public struct CIOTrackProductInsightsAgentAnswerViewRequest { public let question: String /** - The text of the answer shown to the user (an empty string is allowed) + The text of the answer shown to the user (0-2000 characters; an empty string is allowed) */ public let answerText: String @@ -79,7 +79,7 @@ public struct CIOTrackProductInsightsAgentAnswerViewRequest { - Parameters: - itemID: The ID of the product whose page the Product Insights Agent is on - question: The question the answer was provided for (1-1000 characters) - - answerText: The text of the answer shown to the user (an empty string is allowed) + - answerText: The text of the answer shown to the user (0-2000 characters; an empty string is allowed) - questionTopic: The topic category of the question - qnaResultID: The ID of the answer result returned by the Product Insights Agent - items: The recommended items shown with the answer (capped at 100). `quantity` is not sent diff --git a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift index 4ba23a71..87e48e44 100644 --- a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift +++ b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift @@ -987,11 +987,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentFocus(_ request: CIOTrackProductInsightsAgentFocusRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentFocusData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentFocusData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1007,11 +1005,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentOutOfView(_ request: CIOTrackProductInsightsAgentOutOfViewRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentOutOfViewData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentOutOfViewData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1027,11 +1023,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentView(_ request: CIOTrackProductInsightsAgentViewRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentViewData(itemID: request.itemID, questions: request.questions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentViewData(itemID: request.itemID, questions: request.questions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1047,11 +1041,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentViews(_ request: CIOTrackProductInsightsAgentViewsRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentViewsData(itemID: request.itemID, questions: request.questions, viewTimespans: request.viewTimespans, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentViewsData(itemID: request.itemID, questions: request.questions, viewTimespans: request.viewTimespans, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1067,11 +1059,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentQuestionClick(_ request: CIOTrackProductInsightsAgentQuestionClickRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentQuestionClickData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentQuestionClickData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1087,11 +1077,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentQuestionSubmit(_ request: CIOTrackProductInsightsAgentQuestionSubmitRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentQuestionSubmitData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentQuestionSubmitData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1107,11 +1095,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentAnswerFeedback(_ request: CIOTrackProductInsightsAgentAnswerFeedbackRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: request.itemID, feedbackLabel: request.feedbackLabel, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: request.itemID, feedbackLabel: request.feedbackLabel, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1127,11 +1113,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentResultClick(_ request: CIOTrackProductInsightsAgentResultClickRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentResultClickData(itemID: request.itemID, question: request.question, seedItemID: request.seedItemID, position: request.position, seedItemName: request.seedItemName, seedVariationID: request.seedVariationID, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentResultClickData(itemID: request.itemID, question: request.question, seedItemID: request.seedItemID, position: request.position, seedItemName: request.seedItemName, seedVariationID: request.seedVariationID, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1147,11 +1131,9 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentAnswerView(_ request: CIOTrackProductInsightsAgentAnswerViewRequest, completionHandler: TrackingCompletionHandler? = nil) { - let sectionName = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags) - let data = CIOTrackProductInsightsAgentAnswerViewData(itemID: request.itemID, question: request.question, answerText: request.answerText, questionTopic: request.questionTopic, qnaResultID: request.qnaResultID, items: request.items, followUpQuestions: request.followUpQuestions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: mergedTags) - let urlRequest = self.buildRequest(data: data) - executeTracking(urlRequest, completionHandler: completionHandler) + trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in + CIOTrackProductInsightsAgentAnswerViewData(itemID: request.itemID, question: request.question, answerText: request.answerText, questionTopic: request.questionTopic, qnaResultID: request.qnaResultID, items: request.items, followUpQuestions: request.followUpQuestions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) + } } /** @@ -1206,6 +1188,22 @@ public class ConstructorIO: CIOSessionManagerDelegate { } + /** + Resolve the section and analytics tags shared by every Product Insights Agent event, then build and send the request + + - Parameters: + - sectionName: The section passed on the request, falling back to the configured default section, then "Products" + - analyticsTags: The analytics tags passed on the request, merged with the configured default analytics tags + - completionHandler: The callback to execute on completion. + - makeData: Builds the event's request data from the resolved section and analytics tags + */ + private func trackProductInsightsAgent(sectionName: String?, analyticsTags: [String: String]?, completionHandler: TrackingCompletionHandler?, makeData: (String, [String: String]?) -> CIOTrackProductInsightsAgentData) { + let resolvedSectionName = sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: analyticsTags) + let urlRequest = self.buildRequest(data: makeData(resolvedSectionName, mergedTags)) + executeTracking(urlRequest, completionHandler: completionHandler) + } + private func buildRequest(data: CIORequestData) -> URLRequest { let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseURL: self.config.baseURL ?? Constants.Query.baseURLString) self.attachClientID(requestBuilder: requestBuilder) diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift index 7e0794b2..32069d80 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift @@ -68,6 +68,7 @@ class TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "feedback_label", "item_id", "item_name", "key", "qna_result_id", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } func testTrackProductInsightsAgentAnswerFeedbackBuilder_WithThumbsDown() { diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift index 27cbb5bc..32dcff8f 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift @@ -80,6 +80,7 @@ class TrackProductInsightsAgentAnswerViewRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "answer_text", "beacon", "c", "follow_up_questions", "item_id", "item_name", "items", "key", "qna_result_id", "question", "question_topic", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } func testTrackProductInsightsAgentAnswerViewBuilder_SendsEmptyAnswerText() { diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift index 850ef574..70748b72 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift @@ -62,5 +62,6 @@ class TrackProductInsightsAgentFocusRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } } diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift index 2cfe2fc3..1078e2be 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift @@ -62,5 +62,6 @@ class TrackProductInsightsAgentOutOfViewRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } } diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift index 88cf4de0..f17278d3 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift @@ -68,5 +68,6 @@ class TrackProductInsightsAgentQuestionClickRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "question", "question_topic", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } } diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift index 060e9ebb..b5466ccb 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift @@ -68,5 +68,6 @@ class TrackProductInsightsAgentQuestionSubmitRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "question", "question_topic", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } } diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift index e332a759..88a9c858 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift @@ -80,6 +80,7 @@ class TrackProductInsightsAgentResultClickRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "position", "qna_result_id", "question", "section", "seed_item_id", "seed_item_name", "seed_variation_id", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } func testTrackProductInsightsAgentResultClickBuilder_SendsPositionZero() { diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift index c1af31c3..6684b733 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift @@ -65,5 +65,6 @@ class TrackProductInsightsAgentViewRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "questions", "section", "thread_id", "variation_id"], "the body should only contain keys accepted by the API") } } diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift index 88855d22..4574298e 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift @@ -68,6 +68,7 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["thread_id"] as? String, threadID) XCTAssertEqual(payload?["analytics_tags"] as? [String: String], analyticsTags) XCTAssertEqual(payload?["beacon"] as? Bool, true) + XCTAssertEqual(payload.map { Set($0.keys) }, ["_dt", "analytics_tags", "beacon", "c", "item_id", "item_name", "key", "questions", "section", "thread_id", "variation_id", "view_timespans"], "the body should only contain keys accepted by the API") } func testTrackProductInsightsAgentViewsBuilder_FormatsTimespansAsISO8601InUTC() { diff --git a/README.md b/README.md index 17cd5400..ae1f3eda 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ constructorIO.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgen // Track when the user submits a question constructorIO.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "1234567-AB", question: "Is this waterproof?", questionTopic: "materials")) -// Track when the user views an answer (items and followUpQuestions are capped at 100) +// Track when the user views an answer (answerText is limited to 2000 characters; items and followUpQuestions are capped at 100) constructorIO.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "1234567-AB", question: "Is this waterproof?", answerText: "Yes, it has a waterproof membrane.", qnaResultID: "8c1e2b5f-0b6a-2d4c-3e9f-6f5e4d3c2b1a", items: [CIOItem(customerID: "1234765-CD", itemName: "Road Runner")], followUpQuestions: ["What sizes are available?"])) // Track when the user gives feedback on an answer From 6acebdfcd0962abb63ca6709c22ac276fac6ab04 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Thu, 22 Oct 2026 23:52:10 +0800 Subject: [PATCH 08/10] [CDX-708] Address review: pin timespan formatter config, fix CIOItem comment - Test that the timespan formatter is UTC, en_US_POSIX and Gregorian, and that its output round-trips to the original instant - Correct the CIOItem.toImpressionDictionary() comment about purchase items Co-Authored-By: Claude Opus 5.5 --- .../FW/Logic/Request/CIOItem.swift | 3 ++- ...nsightsAgentViewsRequestBuilderTests.swift | 25 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOItem.swift b/AutocompleteClient/FW/Logic/Request/CIOItem.swift index 2d0ae697..8f881216 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOItem.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOItem.swift @@ -26,7 +26,8 @@ public struct CIOItem { } // Builds the item object sent by trackResultsImpressionView and trackProductInsightsAgentAnswerView. - // Other events send different item shapes (e.g. purchase sends quantity, and ASA/search/browse/recommendations omit item_name) + // 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] { var dict = ["item_id": self.customerID] if let itemName = self.itemName { diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift index 4574298e..5aad6a3a 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift @@ -101,6 +101,31 @@ class TrackProductInsightsAgentViewsRequestBuilderTests: XCTestCase { ]) } + func testTrackProductInsightsAgentViewsBuilder_TimespanFormatterIsPinnedToUTCAndGregorian() { + // The formatted strings above only match on any host if these are set explicitly, so pin them directly + let formatter = CIOProductInsightsAgentViewTimespan.dateFormatter + XCTAssertEqual(formatter.timeZone.secondsFromGMT(), 0) + XCTAssertEqual(formatter.locale.identifier, "en_US_POSIX") + XCTAssertEqual(formatter.calendar.identifier, .gregorian) + XCTAssertEqual(formatter.dateFormat, "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") + } + + func testTrackProductInsightsAgentViewsBuilder_TimespansRoundTripAsISO8601() { + let start = Date(timeIntervalSince1970: 1792636805.125) + let end = Date(timeIntervalSince1970: 1792636810.5) + let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: [CIOProductInsightsAgentViewTimespan(start: start, end: end)]) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + let timespan = (payload?["view_timespans"] as? [[String: String]])?.first + + let parser = ISO8601DateFormatter() + parser.formatOptions = [.withInternetDateTime, .withFractionalSeconds] + let parsedStart = timespan?["start"].flatMap { parser.date(from: $0) } + let parsedEnd = timespan?["end"].flatMap { parser.date(from: $0) } + XCTAssertEqual(parsedStart?.timeIntervalSince1970 ?? 0, start.timeIntervalSince1970, accuracy: 0.001) + XCTAssertEqual(parsedEnd?.timeIntervalSince1970 ?? 0, end.timeIntervalSince1970, accuracy: 0.001) + } + func testTrackProductInsightsAgentViewsBuilder_WithEmptyTimespans() { let tracker = CIOTrackProductInsightsAgentViewsData(itemID: itemID, questions: questions, viewTimespans: []) builder.build(trackData: tracker) From 72fc969fa00319a973aa9fbb7c343bf09d71ae30 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Mon, 26 Oct 2026 23:11:41 +0800 Subject: [PATCH 09/10] [CDX-708] Match sibling patterns: inline item mapping and section/tag resolution - Map answer_view items in CIOTrackProductInsightsAgentAnswerViewData, like every other event that sends [CIOItem]. CIOItem and CIOTrackResultsImpressionViewData are back to master. - Resolve the section and merge analytics tags inline in each of the 9 methods, like the sibling tracking methods, and drop the private makeData helper. No wire output changes. Co-Authored-By: Claude Opus 5.5 --- .../FW/Logic/Request/CIOItem.swift | 20 ----- ...ckProductInsightsAgentAnswerViewData.swift | 19 ++++- .../CIOTrackResultsImpressionViewData.swift | 11 ++- .../FW/Logic/Worker/ConstructorIO.swift | 79 +++++++++---------- 4 files changed, 64 insertions(+), 65 deletions(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOItem.swift b/AutocompleteClient/FW/Logic/Request/CIOItem.swift index 8f881216..c5ae5c86 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOItem.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOItem.swift @@ -24,24 +24,4 @@ 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] { - 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 - } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift index d6b88bfc..be02b0ea 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift @@ -58,7 +58,7 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentD dict["qna_result_id"] = qnaResultID } if let items = self.items { - dict["items"] = items.prefix(100).map { $0.toImpressionDictionary() } + dict["items"] = items.prefix(100).map { self.itemDictionary(from: $0) } } if let followUpQuestions = self.followUpQuestions { dict["follow_up_questions"] = followUpQuestions.prefix(100).map { ["value": $0] } @@ -66,4 +66,21 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentD return dict } + + private func itemDictionary(from item: CIOItem) -> [String: String] { + var dict = ["item_id": item.customerID] + if let itemName = item.itemName { + dict["item_name"] = itemName + } + if let variationID = item.variationID { + dict["variation_id"] = variationID + } + if let slCampaignID = item.slCampaignID { + dict["sl_campaign_id"] = slCampaignID + } + if let slCampaignOwner = item.slCampaignOwner { + dict["sl_campaign_owner"] = slCampaignOwner + } + return dict + } } diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift index 2d029590..85d7a1ba 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackResultsImpressionViewData.swift @@ -37,8 +37,17 @@ struct CIOTrackResultsImpressionViewData: CIORequestData { } func httpBody(baseParams: [String: Any]) -> Data? { + let itemDicts: [[String: Any]] = items.map { item in + var dict: [String: Any] = ["item_id": item.customerID] + if let name = item.itemName { dict["item_name"] = name } + if let vid = item.variationID { dict["variation_id"] = vid } + if let cid = item.slCampaignID { dict["sl_campaign_id"] = cid } + if let owner = item.slCampaignOwner { dict["sl_campaign_owner"] = owner } + return dict + } + var dict: [String: Any] = [ - "items": items.map { $0.toImpressionDictionary() }, + "items": itemDicts, "beacon": true ] diff --git a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift index 87e48e44..3b22abaf 100644 --- a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift +++ b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift @@ -987,9 +987,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentFocus(_ request: CIOTrackProductInsightsAgentFocusRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentFocusData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentFocusData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1005,9 +1006,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentOutOfView(_ request: CIOTrackProductInsightsAgentOutOfViewRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentOutOfViewData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentOutOfViewData(itemID: request.itemID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1023,9 +1025,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentView(_ request: CIOTrackProductInsightsAgentViewRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentViewData(itemID: request.itemID, questions: request.questions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentViewData(itemID: request.itemID, questions: request.questions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1041,9 +1044,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentViews(_ request: CIOTrackProductInsightsAgentViewsRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentViewsData(itemID: request.itemID, questions: request.questions, viewTimespans: request.viewTimespans, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentViewsData(itemID: request.itemID, questions: request.questions, viewTimespans: request.viewTimespans, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1059,9 +1063,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentQuestionClick(_ request: CIOTrackProductInsightsAgentQuestionClickRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentQuestionClickData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentQuestionClickData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1077,9 +1082,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentQuestionSubmit(_ request: CIOTrackProductInsightsAgentQuestionSubmitRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentQuestionSubmitData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentQuestionSubmitData(itemID: request.itemID, question: request.question, questionTopic: request.questionTopic, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1095,9 +1101,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentAnswerFeedback(_ request: CIOTrackProductInsightsAgentAnswerFeedbackRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: request.itemID, feedbackLabel: request.feedbackLabel, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentAnswerFeedbackData(itemID: request.itemID, feedbackLabel: request.feedbackLabel, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1113,9 +1120,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentResultClick(_ request: CIOTrackProductInsightsAgentResultClickRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentResultClickData(itemID: request.itemID, question: request.question, seedItemID: request.seedItemID, position: request.position, seedItemName: request.seedItemName, seedVariationID: request.seedVariationID, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentResultClickData(itemID: request.itemID, question: request.question, seedItemID: request.seedItemID, position: request.position, seedItemName: request.seedItemName, seedVariationID: request.seedVariationID, qnaResultID: request.qnaResultID, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1131,9 +1139,10 @@ public class ConstructorIO: CIOSessionManagerDelegate { ``` */ public func trackProductInsightsAgentAnswerView(_ request: CIOTrackProductInsightsAgentAnswerViewRequest, completionHandler: TrackingCompletionHandler? = nil) { - trackProductInsightsAgent(sectionName: request.sectionName, analyticsTags: request.analyticsTags, completionHandler: completionHandler) { sectionName, analyticsTags in - CIOTrackProductInsightsAgentAnswerViewData(itemID: request.itemID, question: request.question, answerText: request.answerText, questionTopic: request.questionTopic, qnaResultID: request.qnaResultID, items: request.items, followUpQuestions: request.followUpQuestions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: sectionName, analyticsTags: analyticsTags) - } + let section = request.sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName + let data = CIOTrackProductInsightsAgentAnswerViewData(itemID: request.itemID, question: request.question, answerText: request.answerText, questionTopic: request.questionTopic, qnaResultID: request.qnaResultID, items: request.items, followUpQuestions: request.followUpQuestions, itemName: request.itemName, variationID: request.variationID, threadID: request.threadID, sectionName: section, analyticsTags: mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: request.analyticsTags)) + let urlRequest = self.buildRequest(data: data) + executeTracking(urlRequest, completionHandler: completionHandler) } /** @@ -1188,22 +1197,6 @@ public class ConstructorIO: CIOSessionManagerDelegate { } - /** - Resolve the section and analytics tags shared by every Product Insights Agent event, then build and send the request - - - Parameters: - - sectionName: The section passed on the request, falling back to the configured default section, then "Products" - - analyticsTags: The analytics tags passed on the request, merged with the configured default analytics tags - - completionHandler: The callback to execute on completion. - - makeData: Builds the event's request data from the resolved section and analytics tags - */ - private func trackProductInsightsAgent(sectionName: String?, analyticsTags: [String: String]?, completionHandler: TrackingCompletionHandler?, makeData: (String, [String: String]?) -> CIOTrackProductInsightsAgentData) { - let resolvedSectionName = sectionName ?? self.config.defaultItemSectionName ?? Constants.Track.defaultItemSectionName - let mergedTags = mergeAnalyticsTags(defaultAnalyticsTags: self.config.defaultAnalyticsTags, newAnalyticsTags: analyticsTags) - let urlRequest = self.buildRequest(data: makeData(resolvedSectionName, mergedTags)) - executeTracking(urlRequest, completionHandler: completionHandler) - } - private func buildRequest(data: CIORequestData) -> URLRequest { let requestBuilder = RequestBuilder(apiKey: self.config.apiKey, baseURL: self.config.baseURL ?? Constants.Query.baseURLString) self.attachClientID(requestBuilder: requestBuilder) From d3647e5f136f9d381c6a847b898708bc07e78c55 Mon Sep 17 00:00:00 2001 From: mudaafi Date: Tue, 27 Oct 2026 02:16:59 +0800 Subject: [PATCH 10/10] [CDX-708] Address review: truncate answer_text, live test without itemName - Truncate answer_view answer_text to 2000 Unicode code points (the unit the server counts) on character boundaries, so an over-limit answer no longer 400s and drops the event. - Add a live answer_view test with only the required fields (no itemName) and an over-limit answerText, to show both are accepted. Co-Authored-By: Claude Opus 5.5 --- ...ckProductInsightsAgentAnswerViewData.swift | 20 +++++++++- ...roductInsightsAgentAnswerViewRequest.swift | 4 +- ...tsAgentAnswerViewRequestBuilderTests.swift | 38 +++++++++++++++++++ .../ConstructorIOIntegrationTests.swift | 12 ++++++ README.md | 2 +- 5 files changed, 72 insertions(+), 4 deletions(-) diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift index be02b0ea..5751f8f7 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift @@ -48,7 +48,7 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentD func eventFields() -> [String: Any] { var dict = [ "question": self.question, - "answer_text": self.answerText + "answer_text": self.truncatedAnswerText() ] as [String: Any] if let questionTopic = self.questionTopic { @@ -67,6 +67,24 @@ struct CIOTrackProductInsightsAgentAnswerViewData: CIOTrackProductInsightsAgentD return dict } + // The server rejects answer_text over 2000 Unicode code points with a 400, which drops the whole event. + // Truncate on character boundaries so a multi-scalar character (e.g. an accent or emoji sequence) is never split + private func truncatedAnswerText() -> String { + guard self.answerText.unicodeScalars.count > 2000 else { + return self.answerText + } + var scalarCount = 0 + var endIndex = self.answerText.startIndex + for character in self.answerText { + scalarCount += character.unicodeScalars.count + if scalarCount > 2000 { + break + } + endIndex = self.answerText.index(after: endIndex) + } + return String(self.answerText[.. [String: String] { var dict = ["item_id": item.customerID] if let itemName = item.itemName { diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift index 75391efc..e3ce6e66 100644 --- a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift @@ -24,7 +24,7 @@ public struct CIOTrackProductInsightsAgentAnswerViewRequest { public let question: String /** - The text of the answer shown to the user (0-2000 characters; an empty string is allowed) + The text of the answer shown to the user (an empty string is allowed). Text over 2000 Unicode code points is truncated to fit */ public let answerText: String @@ -79,7 +79,7 @@ public struct CIOTrackProductInsightsAgentAnswerViewRequest { - Parameters: - itemID: The ID of the product whose page the Product Insights Agent is on - question: The question the answer was provided for (1-1000 characters) - - answerText: The text of the answer shown to the user (0-2000 characters; an empty string is allowed) + - answerText: The text of the answer shown to the user (an empty string is allowed). Text over 2000 Unicode code points is truncated to fit - questionTopic: The topic category of the question - qnaResultID: The ID of the answer result returned by the Product Insights Agent - items: The recommended items shown with the answer (capped at 100). `quantity` is not sent diff --git a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift index 32dcff8f..8a223189 100644 --- a/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift @@ -91,6 +91,44 @@ class TrackProductInsightsAgentAnswerViewRequestBuilderTests: XCTestCase { XCTAssertEqual(payload?["answer_text"] as? String, "", "an empty answer_text should be sent") } + func testTrackProductInsightsAgentAnswerViewBuilder_SendsAnswerTextAtExactly2000CodePoints() { + let answer = String(repeating: "a", count: 2000) + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answer) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["answer_text"] as? String, answer) + } + + func testTrackProductInsightsAgentAnswerViewBuilder_TruncatesAnswerTextTo2000CodePoints() { + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: String(repeating: "a", count: 2500)) + builder.build(trackData: tracker) + let payload = self.payload(of: builder.getRequest()) + + XCTAssertEqual(payload?["answer_text"] as? String, String(repeating: "a", count: 2000)) + } + + func testTrackProductInsightsAgentAnswerViewBuilder_TruncatesAnswerTextByCodePointsNotCharacters() { + // "e" + combining acute is 1 Character but 2 code points; the server counts code points + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: String(repeating: "e\u{301}", count: 1001)) + builder.build(trackData: tracker) + let answer = self.payload(of: builder.getRequest())?["answer_text"] as? String + + XCTAssertEqual(answer?.unicodeScalars.count, 2000) + XCTAssertEqual(answer, String(repeating: "e\u{301}", count: 1000)) + } + + func testTrackProductInsightsAgentAnswerViewBuilder_TruncationNeverSplitsACharacter() { + // The family emoji is 1 Character made of 7 code points: 285 fit (1995), and the 286th (2002) is dropped whole rather than split + let family = "\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}\u{200D}\u{1F466}" + let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: String(repeating: family, count: 400)) + builder.build(trackData: tracker) + let answer = self.payload(of: builder.getRequest())?["answer_text"] as? String + + XCTAssertEqual(answer, String(repeating: family, count: 285)) + XCTAssertLessThanOrEqual(answer?.unicodeScalars.count ?? Int.max, 2000) + } + func testTrackProductInsightsAgentAnswerViewBuilder_NeverSendsItemQuantity() { let items = [CIOItem(customerID: "10002", quantity: 3)] let tracker = CIOTrackProductInsightsAgentAnswerViewData(itemID: itemID, question: question, answerText: answerText, items: items) diff --git a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift index 543f9444..b99a9070 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift @@ -279,6 +279,18 @@ class ConstructorIOIntegrationTests: XCTestCase { self.wait(for: expectation) } + func testTrackProductInsightsAgentAnswerView_WithRequiredFieldsOnlyAndLongAnswerText() { + // No itemName, and an answerText over the server's 2000 code point limit, which the SDK truncates + let expectation = XCTestExpectation(description: "Tracking 204") + let request = CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "10001", question: "Why choose this?", answerText: String(repeating: "e\u{301}", count: 1001)) + self.constructor.trackProductInsightsAgentAnswerView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + self.wait(for: expectation) + } + func testTrackAutocompleteSelect() { let expectation = XCTestExpectation(description: "Tracking 204") self.constructor.trackAutocompleteSelect(searchTerm: searchTerm, originalQuery: originalQuery, sectionName: sectionName, group: group, resultID: resultID, itemID: customerID, completionHandler: { response in diff --git a/README.md b/README.md index ae1f3eda..3b68257d 100644 --- a/README.md +++ b/README.md @@ -393,7 +393,7 @@ constructorIO.trackProductInsightsAgentQuestionClick(CIOTrackProductInsightsAgen // Track when the user submits a question constructorIO.trackProductInsightsAgentQuestionSubmit(CIOTrackProductInsightsAgentQuestionSubmitRequest(itemID: "1234567-AB", question: "Is this waterproof?", questionTopic: "materials")) -// Track when the user views an answer (answerText is limited to 2000 characters; items and followUpQuestions are capped at 100) +// Track when the user views an answer (answerText over 2000 characters is truncated; items and followUpQuestions are capped at 100) constructorIO.trackProductInsightsAgentAnswerView(CIOTrackProductInsightsAgentAnswerViewRequest(itemID: "1234567-AB", question: "Is this waterproof?", answerText: "Yes, it has a waterproof membrane.", qnaResultID: "8c1e2b5f-0b6a-2d4c-3e9f-6f5e4d3c2b1a", items: [CIOItem(customerID: "1234765-CD", itemName: "Road Runner")], followUpQuestions: ["What sizes are available?"])) // Track when the user gives feedback on an answer