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/AutocompleteClient.xcodeproj/project.pbxproj b/AutocompleteClient.xcodeproj/project.pbxproj index 4ed4cf0c..3c05b2b7 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,9 @@ 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 */ /* Begin PBXContainerItemProxy section */ @@ -401,6 +441,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 +479,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 +493,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 +521,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 +546,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 +593,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 +641,10 @@ 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 = ""; }; + 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 = ""; }; 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 +839,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 +1050,28 @@ 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 */, + E0FE6A4B1E7D5F0C882461A5 /* CIOTrackProductInsightsAgentData.swift */, ); path = Request; sourceTree = ""; @@ -1101,6 +1203,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 +1269,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 +2812,28 @@ 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 */, + F7F8653BE4EC229CBCF39EF9 /* CIOTrackProductInsightsAgentData.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2805,6 +2947,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..feb2c40e --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOProductInsightsAgentViewTimespan.swift @@ -0,0 +1,57 @@ +// +// CIOProductInsightsAgentViewTimespan.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import Foundation + +/** + Struct representing a period of time during which the Product Insights Agent was visible to the user + */ +public struct CIOProductInsightsAgentViewTimespan { + + /** + The time the Product Insights Agent became visible + */ + public let start: Date + + /** + The time the Product Insights Agent stopped being visible + */ + public let end: Date + + /** + Create a Product Insights Agent view timespan + + - Parameters: + - start: The time the Product Insights Agent became visible + - end: The time the Product Insights Agent stopped being visible + */ + public init(start: Date, end: Date) { + self.start = start + self.end = end + } + + // A fixed-format DateFormatter is used instead of ISO8601DateFormatter because setting + // `.withFractionalSeconds` throws an exception on iOS 11.0 and 11.1. DateFormatter is thread-safe, + // so a single shared instance is used for all timespans + static let dateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.locale = Locale(identifier: "en_US_POSIX") + formatter.calendar = Calendar(identifier: .gregorian) + formatter.timeZone = TimeZone(secondsFromGMT: 0) + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + return formatter + }() + + // Dates are formatted as ISO-8601 strings in UTC (e.g. "2026-10-22T02:40:05.123Z") so the body stays JSON-serializable + func toDictionary() -> [String: String] { + return [ + "start": CIOProductInsightsAgentViewTimespan.dateFormatter.string(from: self.start), + "end": CIOProductInsightsAgentViewTimespan.dateFormatter.string(from: self.end) + ] + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift new file mode 100644 index 00000000..7223abba --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerFeedbackData.swift @@ -0,0 +1,51 @@ +// +// CIOTrackProductInsightsAgentAnswerFeedbackData.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import Foundation + +/** + Struct encapsulating the parameters that must/can be set in order to track a Product Insights Agent answer feedback + */ +struct CIOTrackProductInsightsAgentAnswerFeedbackData: CIOTrackProductInsightsAgentData { + + let itemID: String + let feedbackLabel: CIOProductInsightsAgentFeedbackLabel + let qnaResultID: String? + let itemName: String? + let variationID: String? + let threadID: String? + let sectionName: String? + let analyticsTags: [String: String]? + + func url(with baseURL: String) -> String { + return String(format: Constants.TrackProductInsightsAgentAnswerFeedback.format, baseURL) + } + + init(itemID: String, feedbackLabel: CIOProductInsightsAgentFeedbackLabel, qnaResultID: String? = nil, itemName: String? = nil, variationID: String? = nil, threadID: String? = nil, sectionName: String? = nil, analyticsTags: [String: String]? = nil) { + self.itemID = itemID + self.feedbackLabel = feedbackLabel + self.qnaResultID = qnaResultID + self.itemName = itemName + self.variationID = variationID + self.threadID = threadID + self.sectionName = sectionName + self.analyticsTags = analyticsTags + } + + func eventFields() -> [String: Any] { + var dict = [ + "feedback_label": self.feedbackLabel.rawValue + ] as [String: Any] + + if let qnaResultID = self.qnaResultID { + dict["qna_result_id"] = qnaResultID + } + + return dict + } +} 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..5751f8f7 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewData.swift @@ -0,0 +1,104 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + var dict = [ + "question": self.question, + "answer_text": self.truncatedAnswerText() + ] 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 { self.itemDictionary(from: $0) } + } + if let followUpQuestions = self.followUpQuestions { + dict["follow_up_questions"] = followUpQuestions.prefix(100).map { ["value": $0] } + } + + 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 { + 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/CIOTrackProductInsightsAgentAnswerViewRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentAnswerViewRequest.swift new file mode 100644 index 00000000..e3ce6e66 --- /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 (1-1000 characters) + */ + public let question: String + + /** + 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 + + /** + 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 (1-1000 characters) + - 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 + - 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/CIOTrackProductInsightsAgentData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift new file mode 100644 index 00000000..21eea1aa --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentData.swift @@ -0,0 +1,60 @@ +// +// CIOTrackProductInsightsAgentData.swift +// Constructor.io +// +// Copyright (c) Constructor.io Corporation. All rights reserved. +// http://constructor.io/ +// + +import Foundation + +/** + 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 { + + 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 + } + 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/CIOTrackProductInsightsAgentFocusData.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift new file mode 100644 index 00000000..842be2d0 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentFocusData.swift @@ -0,0 +1,39 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + return [:] + } +} 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..839a493c --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentOutOfViewData.swift @@ -0,0 +1,39 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + return [:] + } +} 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..abc0be62 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickData.swift @@ -0,0 +1,51 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + var dict = [ + "question": self.question + ] as [String: Any] + + if let questionTopic = self.questionTopic { + dict["question_topic"] = questionTopic + } + + return dict + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionClickRequest.swift new file mode 100644 index 00000000..6c5e961e --- /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 (1-1000 characters) + */ + 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 (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 + - 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..20744b2c --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitData.swift @@ -0,0 +1,51 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + var dict = [ + "question": self.question + ] as [String: Any] + + if let questionTopic = self.questionTopic { + dict["question_topic"] = questionTopic + } + + return dict + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentQuestionSubmitRequest.swift new file mode 100644 index 00000000..8f2ea567 --- /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 (1-1000 characters) + */ + 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 (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 + - 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..cfcaf837 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickData.swift @@ -0,0 +1,69 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + var dict = [ + "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 + } + + return dict + } +} diff --git a/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentResultClickRequest.swift new file mode 100644 index 00000000..8022e718 --- /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 (1-1000 characters) + */ + public let question: String + + /** + The ID of the product whose page the Product Insights Agent is on + */ + public let seedItemID: String + + /** + The zero-based 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 (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 + - 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..bb6ed8b6 --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewData.swift @@ -0,0 +1,43 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + return [ + "questions": self.questions.map { $0.toDictionary() } + ] + } +} 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..ac647adc --- /dev/null +++ b/AutocompleteClient/FW/Logic/Request/CIOTrackProductInsightsAgentViewsData.swift @@ -0,0 +1,46 @@ +// +// 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: CIOTrackProductInsightsAgentData { + + 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 eventFields() -> [String: Any] { + return [ + "questions": self.questions.map { $0.toDictionary() }, + "view_timespans": self.viewTimespans.map { $0.toDictionary() } + ] + } +} 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..3b22abaf 100644 --- a/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift +++ b/AutocompleteClient/FW/Logic/Worker/ConstructorIO.swift @@ -974,6 +974,177 @@ 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 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) + } + + /** + 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 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) + } + + /** + 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 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) + } + + /** + 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: 1792636805.125), end: Date(timeIntervalSince1970: 1792636810.5))], itemName: "Item1", variationID: "10001-A")) + ``` + */ + public func trackProductInsightsAgentViews(_ request: CIOTrackProductInsightsAgentViewsRequest, completionHandler: TrackingCompletionHandler? = nil) { + 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) + } + + /** + 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 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) + } + + /** + 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 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) + } + + /** + 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 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) + } + + /** + 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", seedVariationID: "10001-A", qnaResultID: "qna-result-123", itemName: "Item2", variationID: "10002-A")) + ``` + */ + public func trackProductInsightsAgentResultClick(_ request: CIOTrackProductInsightsAgentResultClickRequest, completionHandler: TrackingCompletionHandler? = nil) { + 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) + } + + /** + 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 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) + } + /** 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..32069d80 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerFeedbackRequestBuilderTests.swift @@ -0,0 +1,81 @@ +// +// 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) + 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() { + 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..8a223189 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentAnswerViewRequestBuilderTests.swift @@ -0,0 +1,178 @@ +// +// 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) + 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() { + 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_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) + 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..70748b72 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentFocusRequestBuilderTests.swift @@ -0,0 +1,67 @@ +// +// 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) + 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 new file mode 100644 index 00000000..1078e2be --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentOutOfViewRequestBuilderTests.swift @@ -0,0 +1,67 @@ +// +// 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) + 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 new file mode 100644 index 00000000..f17278d3 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionClickRequestBuilderTests.swift @@ -0,0 +1,73 @@ +// +// 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) + 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 new file mode 100644 index 00000000..b5466ccb --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentQuestionSubmitRequestBuilderTests.swift @@ -0,0 +1,73 @@ +// +// 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) + 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 new file mode 100644 index 00000000..88a9c858 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentResultClickRequestBuilderTests.swift @@ -0,0 +1,93 @@ +// +// 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 = "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"] + + 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) + 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() { + 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..6684b733 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewRequestBuilderTests.swift @@ -0,0 +1,70 @@ +// +// 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) + 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 new file mode 100644 index 00000000..5aad6a3a --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Request/TrackProductInsightsAgentViewsRequestBuilderTests.swift @@ -0,0 +1,136 @@ +// +// 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.125), end: Date(timeIntervalSince1970: 1792636810.5))] + 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.125Z", "end": "2026-10-22T02:40:10.500Z"]]) + 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.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) + 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() { + 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_FormatsTimespansToTheNearestMillisecond() { + let viewTimespans = [ + 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.009Z", "end": "2026-10-22T02:40:05.015Z"] + ]) + } + + 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) + 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..b99a9070 100644 --- a/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOIntegrationTests.swift @@ -63,6 +63,234 @@ 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 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"]) + self.constructor.trackProductInsightsAgentOutOfView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + 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"]) + self.constructor.trackProductInsightsAgentView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + 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: 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) + expectation.fulfill() + }) + 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"]) + self.constructor.trackProductInsightsAgentQuestionClick(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + 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"]) + self.constructor.trackProductInsightsAgentQuestionSubmit(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + 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"]) + self.constructor.trackProductInsightsAgentAnswerFeedback(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + 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: "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) + expectation.fulfill() + }) + 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"]) + self.constructor.trackProductInsightsAgentAnswerView(request, completionHandler: { response in + let cioError = response.error as? CIOError + XCTAssertNil(cioError) + expectation.fulfill() + }) + 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 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/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift new file mode 100644 index 00000000..3413a53d --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerFeedbackTests.swift @@ -0,0 +1,166 @@ +// +// 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_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? + 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..230c6be1 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentAnswerViewTests.swift @@ -0,0 +1,213 @@ +// +// 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_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? + 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..04855b0d --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentFocusTests.swift @@ -0,0 +1,160 @@ +// +// 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_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? + 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..aac99e58 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentOutOfViewTests.swift @@ -0,0 +1,160 @@ +// +// 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_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? + 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..9c1ffde8 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionClickTests.swift @@ -0,0 +1,166 @@ +// +// 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_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? + 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..f3252d0c --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentQuestionSubmitTests.swift @@ -0,0 +1,166 @@ +// +// 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_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? + 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..1e260c4f --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentResultClickTests.swift @@ -0,0 +1,193 @@ +// +// 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 = "Item2" + let variationID: String = "10002-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_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? + 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..a10ab066 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewTests.swift @@ -0,0 +1,163 @@ +// +// 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_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? + 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..1c981d57 --- /dev/null +++ b/AutocompleteClientTests/FW/Logic/Worker/ConstructorIOTrackProductInsightsAgentViewsTests.swift @@ -0,0 +1,166 @@ +// +// 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.125), end: Date(timeIntervalSince1970: 1792636810.5))] + 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.125Z", "end": "2026-10-22T02:40:10.500Z"]]) + 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.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) + 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_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? + 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) + } +} diff --git a/README.md b/README.md index fba869b9..3b68257d 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,47 @@ 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 viewStartDate = Date() +let viewEndDate = viewStartDate.addingTimeInterval(5) +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 (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 +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