From 857eed666ec25afbcdff4731db27bff458f771d9 Mon Sep 17 00:00:00 2001 From: tikidunpon Date: Wed, 12 Jan 2022 14:42:41 +0900 Subject: [PATCH] add dynamic vt field (#240) --- .spm-version | 2 +- CHANGELOG.md | 9 +- Karte.xcodeproj/project.pbxproj | 16 +- ...efinitionsRequestForDynamicFieldSpec.swift | 146 +++++++++++ .../UnitTests/ActionSpec.swift | 48 +++- .../UnitTests/InspectorSpec.swift | 49 ++++ KarteTests/Stub/StubResource.swift | 1 + ...ss_vt_definitions_with_dynamic_fields.json | 248 ++++++++++++++++++ KarteVisualTracking.podspec | 2 +- KarteVisualTracking/Inspector.swift | 17 ++ .../UIApplication+VisualTracking.swift | 12 +- .../UIGestureRecognizer+VisualTracking.swift | 7 +- ...INavigationController+VisualTracking.swift | 15 +- .../Tracking/Definition/Definition.swift | 6 +- .../Tracking/Definition/DynamicField.swift | 31 +++ .../Tracking/Definition/Trigger.swift | 36 +++ KarteVisualTracking/UIKitAction.swift | 39 ++- 17 files changed, 640 insertions(+), 44 deletions(-) create mode 100644 KarteTests/KarteVisualTrackingTests/IntegrationTests/DefinitionsRequestForDynamicFieldSpec.swift create mode 100644 KarteTests/Stub/success_vt_definitions_with_dynamic_fields.json create mode 100644 KarteVisualTracking/Tracking/Definition/DynamicField.swift diff --git a/.spm-version b/.spm-version index 359a5b95..50aea0e7 100644 --- a/.spm-version +++ b/.spm-version @@ -1 +1 @@ -2.0.0 \ No newline at end of file +2.1.0 \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7fec8078..4cf084de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,17 @@ | KarteInAppMessaging | アプリ内メッセージ機能を提供します。 | 2.10.1 | | KarteRemoteNotification | プッシュ通知の受信および効果測定機能を提供します。 | 2.6.0 | | KarteVariables | 設定値配信機能を提供します。 | 2.3.0 | -| KarteVisualTracking | ビジュアルトラッキング機能を提供します。 | 2.6.0 | +| KarteVisualTracking | ビジュアルトラッキング機能を提供します。 | 2.7.0 | | KarteCrashReporting | クラッシュイベントのトラッキング機能を提供します。 | 2.4.0 | | KarteUtilities | KarteCore モジュール等が利用するUtility機能を提供します。通常直接参照する必要はありません。 | 3.6.0 | +# Releases - 2022.1.12 + +### VisualTracking 2.7.0 +** 🎉 FEATURE** +- 動的なフィールドの付与に対応しました。 + - 動的フィールドについては[こちら](https://support.karte.io/post/7JbUVotDwZMvl6h3HL9Zt7#6-0)を参考ください。 + # Releases - 2021.11.25 ### Core 2.19.0 ** 🎉 FEATURE** diff --git a/Karte.xcodeproj/project.pbxproj b/Karte.xcodeproj/project.pbxproj index c3e5e130..aed17764 100644 --- a/Karte.xcodeproj/project.pbxproj +++ b/Karte.xcodeproj/project.pbxproj @@ -380,7 +380,10 @@ 683762F9252F201D00E690CE /* SelectorDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 683762F8252F201D00E690CE /* SelectorDetector.swift */; }; 6837631B252FDAE200E690CE /* SelectorDetectorMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6837631A252FDAE200E690CE /* SelectorDetectorMock.swift */; }; 684F02062417640900AF4AC0 /* SafeTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 684F02052417640900AF4AC0 /* SafeTimer.swift */; }; + 6857834427478B4D00A1AE58 /* DefinitionsRequestForDynamicFieldSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6857834327478B4D00A1AE58 /* DefinitionsRequestForDynamicFieldSpec.swift */; }; + 6857834627478B7000A1AE58 /* success_vt_definitions_with_dynamic_fields.json in Resources */ = {isa = PBXBuildFile; fileRef = 6857834527478B7000A1AE58 /* success_vt_definitions_with_dynamic_fields.json */; }; 68751C272583692D00C6306A /* CrashReporter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = 68751C0E258368FF00C6306A /* CrashReporter.xcframework */; }; + 688AB96D274DF02C001C01B5 /* DynamicField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 688AB96C274DF02C001C01B5 /* DynamicField.swift */; }; 6898FEC725D3CCA500D0839A /* EventFilterError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6898FEC625D3CCA500D0839A /* EventFilterError.swift */; }; 68B40CBA25D2F2870081B1AE /* UnretryableEventFilterRule.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68B40CB925D2F2870081B1AE /* UnretryableEventFilterRule.swift */; }; 68BBBCD524BB5156009A1CE0 /* IAMProcessSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 68BBBCD424BB5156009A1CE0 /* IAMProcessSpec.swift */; }; @@ -905,8 +908,11 @@ 683762F8252F201D00E690CE /* SelectorDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorDetector.swift; sourceTree = ""; }; 6837631A252FDAE200E690CE /* SelectorDetectorMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SelectorDetectorMock.swift; sourceTree = ""; }; 684F02052417640900AF4AC0 /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = ""; }; + 6857834327478B4D00A1AE58 /* DefinitionsRequestForDynamicFieldSpec.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DefinitionsRequestForDynamicFieldSpec.swift; sourceTree = ""; }; + 6857834527478B7000A1AE58 /* success_vt_definitions_with_dynamic_fields.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = success_vt_definitions_with_dynamic_fields.json; sourceTree = ""; }; 68597A44239A324F00607D73 /* InspectorSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorSpec.swift; sourceTree = ""; }; 68751C0E258368FF00C6306A /* CrashReporter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = CrashReporter.xcframework; path = KarteCrashReporting/PLCrashReporter/CrashReporter.xcframework; sourceTree = ""; }; + 688AB96C274DF02C001C01B5 /* DynamicField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicField.swift; sourceTree = ""; }; 6898FEC625D3CCA500D0839A /* EventFilterError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventFilterError.swift; sourceTree = ""; }; 68B40CB925D2F2870081B1AE /* UnretryableEventFilterRule.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UnretryableEventFilterRule.swift; sourceTree = ""; }; 68BBBCD424BB5156009A1CE0 /* IAMProcessSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IAMProcessSpec.swift; sourceTree = ""; }; @@ -1817,6 +1823,7 @@ 0C8976EB237FECFF00098CD8 /* LogicalOperator.swift */, 0C8976EF237FFD5200098CD8 /* ComparisonOperator.swift */, 369C2FDF256B9E6D00ED5948 /* DefinitionsRequest.swift */, + 688AB96C274DF02C001C01B5 /* DynamicField.swift */, ); path = Definition; sourceTree = ""; @@ -1862,6 +1869,7 @@ 0CC648C4238D5559009EB5DF /* success_vt_1.json */, 0CC648C5238D5559009EB5DF /* success_vt_2.json */, 363F260D2582AE60007E6E4B /* success_vt_definitions.json */, + 6857834527478B7000A1AE58 /* success_vt_definitions_with_dynamic_fields.json */, ); path = Stub; sourceTree = ""; @@ -1883,6 +1891,7 @@ 0CC648BF238CF97C009EB5DF /* IntegrationTests */ = { isa = PBXGroup; children = ( + 6857834327478B4D00A1AE58 /* DefinitionsRequestForDynamicFieldSpec.swift */, 0CA6D437238E14D800583E5C /* DefinitionLoadSpec.swift */, 0CA6D435238E13D300583E5C /* DefinitionMatchSpec.swift */, 0CA6D441238EC5AD00583E5C /* TracerTests.swift */, @@ -2582,6 +2591,7 @@ 0CC648C7238D5559009EB5DF /* success_vt_2.json in Resources */, 0CF44AFB242E451000CA7F3A /* success_variables_3.json in Resources */, 363F26172582B028007E6E4B /* success_vt_definitions.json in Resources */, + 6857834627478B7000A1AE58 /* success_vt_definitions_with_dynamic_fields.json in Resources */, 36EFDED5271BF26700C60666 /* failure_invalid_request.json in Resources */, 36EFDEDF271BF26B00C60666 /* failure_server_error.json in Resources */, 0CC648BB238CD4AA009EB5DF /* success_empty.json in Resources */, @@ -2938,6 +2948,7 @@ 0C8976C5237EDD4D00098CD8 /* Account.swift in Sources */, 0C8976F823801DC200098CD8 /* PairingClient.swift in Sources */, 0C8976CB237EE78D00098CD8 /* UIViewController+VisualTracking.swift in Sources */, + 688AB96D274DF02C001C01B5 /* DynamicField.swift in Sources */, 369C2FE0256B9E6D00ED5948 /* DefinitionsRequest.swift in Sources */, 0C8976CE237EE78D00098CD8 /* UINavigationController+VisualTracking.swift in Sources */, 0C8976EC237FECFF00098CD8 /* LogicalOperator.swift in Sources */, @@ -3052,6 +3063,7 @@ 0C83931D240838320014C2BF /* TracerTests.swift in Sources */, 0C8392F42407CCE60014C2BF /* ConfigurationTests.swift in Sources */, 0C839310240831630014C2BF /* SetupSpec.swift in Sources */, + 6857834427478B4D00A1AE58 /* DefinitionsRequestForDynamicFieldSpec.swift in Sources */, 36EFDEC0271ABDE200C60666 /* TodaySupplierMock.swift in Sources */, 0C839305240822A40014C2BF /* EventSpec.swift in Sources */, 0CFD001524ECCA5800598C8C /* CommandBundlerProxySpec.swift in Sources */, @@ -3622,7 +3634,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.6.0; + MARKETING_VERSION = 2.7.0; PRODUCT_BUNDLE_IDENTIFIER = io.karte.KarteVisualTracking; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; @@ -3653,7 +3665,7 @@ "@executable_path/Frameworks", "@loader_path/Frameworks", ); - MARKETING_VERSION = 2.6.0; + MARKETING_VERSION = 2.7.0; PRODUCT_BUNDLE_IDENTIFIER = io.karte.KarteVisualTracking; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; diff --git a/KarteTests/KarteVisualTrackingTests/IntegrationTests/DefinitionsRequestForDynamicFieldSpec.swift b/KarteTests/KarteVisualTrackingTests/IntegrationTests/DefinitionsRequestForDynamicFieldSpec.swift new file mode 100644 index 00000000..79bfcaaa --- /dev/null +++ b/KarteTests/KarteVisualTrackingTests/IntegrationTests/DefinitionsRequestForDynamicFieldSpec.swift @@ -0,0 +1,146 @@ +// +// Copyright 2020 PLAID, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Quick +import Nimble +import Mockingjay +import KarteUtilities +@testable import KarteCore +@testable import KarteVisualTracking + +class DefinitionsRequestForDynamicFieldSpec: QuickSpec { + + override func spec() { + var configuration: KarteCore.Configuration! + var builder: Builder! + + beforeSuite { + configuration = Configuration { (configuration) in + configuration.isSendInitializationEventEnabled = false + } + builder = StubBuilder(spec: self, resource: .vt_definitions_with_dynamic_fields).build() + } + + describe("a definition get") { + var request: URLRequest! + beforeEach { + let exp = self.expectation(description: "Wait for get definitions.") + let stub = self.stub(uri("/v0/native/auto-track/definitions"), {(r) -> (Response) in + request = r + return builder(request) + }) + + KarteApp.setup(appKey: APP_KEY, configuration: configuration) + VisualTrackingManager.shared.tracker?.refreshDefinitions{ + exp.fulfill() + } + + self.wait(for: [exp], timeout: 10) + self.removeStub(stub) + } + + describe("its request") { + it("has `X-KARTE-Auto-Track-OS` header") { + expect(request.allHTTPHeaderFields?.keys.contains("X-KARTE-Auto-Track-OS")).to(beTrue()) + } + + it("`X-KARTE-Auto-Track-OS` header value is `iOS`") { + expect(request.allHTTPHeaderFields?["X-KARTE-Auto-Track-OS"]).to(equal("iOS")) + } + + it("has `X-KARTE-Auto-Track-If-Modified-Since` header") { + expect(request.allHTTPHeaderFields?.keys.contains("X-KARTE-Auto-Track-If-Modified-Since")).to(beTrue()) + } + + it("`has X-KARTE-Auto-Track-If-Modified-Since` header that value is `0`") { + expect(request.allHTTPHeaderFields?["X-KARTE-Auto-Track-If-Modified-Since"]).to(equal("0")) + } + } + + describe("its definitions") { + var definitions: AutoTrackDefinition? + beforeEach { + definitions = VisualTrackingManager.shared.tracker?.definitions + } + + it("is not nil") { + expect(definitions).toNot(beNil()) + } + + it("only has valid trigger") { + expect(definitions?.definitions?.first?.triggers.count).to(equal(4)) + } + + it("only has valid conditions") { + if case let .and(c) = definitions?.definitions?.first?.triggers.first?.condition { + expect(c.count).to(equal(2)) + } else { + fail() + } + } + } + describe("its definitions with dynamic fields") { + var definitions: AutoTrackDefinition? + var window: UIWindow! + var view1: UIView! + var view2: UIView! + var view3: UIView! + var label: UILabel! + + beforeEach { + window = UIWindow() + view1 = UIView() + view2 = UIView() + view3 = UIView() + label = UILabel() + label.text = "test" + view1.addSubview(view2) + view1.addSubview(view3) + view1.addSubview(label) + window.addSubview(view1) + } + beforeEach { + definitions = VisualTrackingManager.shared.tracker?.definitions + } + it("returns valid dynamic fields") { + let dynamicFieldsCount = definitions?.definitions?.first?.triggers.first?.dynamicFields?.count + expect(dynamicFieldsCount).to(equal(4)) + } + + it("returns valid dynamic values") { + let dynamicValues = definitions?.definitions?.first?.triggers.first?.dynamicValues(window: window) + expect(dynamicValues?.count).to(equal(4)) + expect(dynamicValues! as? [String: String]).to(equal(["foo":"test","bar":"test","baz":"test","has_unknown_key":"test"])) + } + + it("returns invalid dynamic values") { + let dynamicValues = definitions?.definitions?.first?.triggers[1].dynamicValues(window: window) + expect(dynamicValues).to(beNil()) + } + + it("returns invalid dynamic values") { + let dynamicValues = definitions?.definitions?.first?.triggers[2].dynamicValues(window: window) + expect(dynamicValues).to(beNil()) + } + + it("returns invalid dynamic values") { + let dynamicValues = definitions?.definitions?.first?.triggers[3].dynamicValues(window: window) + expect(dynamicValues).to(beNil()) + } + } + } + } +} diff --git a/KarteTests/KarteVisualTrackingTests/UnitTests/ActionSpec.swift b/KarteTests/KarteVisualTrackingTests/UnitTests/ActionSpec.swift index d1282b2c..a81fcb09 100644 --- a/KarteTests/KarteVisualTrackingTests/UnitTests/ActionSpec.swift +++ b/KarteTests/KarteVisualTrackingTests/UnitTests/ActionSpec.swift @@ -37,7 +37,8 @@ class ActionSpec: QuickSpec { "dummy_action", view: button, viewController: viewController, - targetText: "dummy_target_text" + targetText: "dummy_target_text", + actionId: UIKitAction.actionId(view: button) ) } @@ -84,7 +85,8 @@ class ActionSpec: QuickSpec { "dummy_action", view: button0, viewController: viewController, - targetText: "dummy_target_text" + targetText: "dummy_target_text", + actionId: UIKitAction.actionId(view: button0) ) expect(action!.actionId!).to(equal("UIButton0UIView0UIView")) } @@ -93,7 +95,8 @@ class ActionSpec: QuickSpec { "dummy_action", view: button1, viewController: viewController, - targetText: "dummy_target_text" + targetText: "dummy_target_text", + actionId: UIKitAction.actionId(view: button1) ) expect(action!.actionId!).to(equal("UIButton1UIView0UIView")) } @@ -158,6 +161,45 @@ class ActionSpec: QuickSpec { } } } + describe("its viewPathIndices") { + context("when passing nil") { + it("returns empty array") { + let actual = UIKitAction.viewPathIndices(actionId: nil) + expect(actual).to(equal([])) + } + } + context("when passing empty string") { + it("returns empty array") { + let actual = UIKitAction.viewPathIndices(actionId: "") + expect(actual).to(equal([])) + } + } + context("when passing UIView") { + it("returns empty array") { + let actual = UIKitAction.viewPathIndices(actionId: "UIView") + expect(actual).to(equal([])) + } + } + context("when passing UIView0") { + it("returns array with 0") { + let actual = UIKitAction.viewPathIndices(actionId: "UIView0") + expect(actual).to(equal([0])) + } + } + context("when passing complexView") { + it("returns array with 0,0,0,0,0,0,0,11") { + let actual = UIKitAction.viewPathIndices(actionId: "UIView11UITableView0UIView0UIViewControllerWrapperView0UINavigationTransitionView0UILayoutContainerView0UIDropShadowView0UITransitionView0SimpleUIWindow") + expect(actual).toNot(beNil()) + expect(actual).to(equal([0,0,0,0,0,0,0,11])) + } + } + context("when passing UIView999UIView2000UIView3000") { + it("returns array with 3000,2000,999") { + let actual = UIKitAction.viewPathIndices(actionId: "UIView999UIView2000UIView3000") + expect(actual).to(equal([3000,2000,999])) + } + } + } } } } diff --git a/KarteTests/KarteVisualTrackingTests/UnitTests/InspectorSpec.swift b/KarteTests/KarteVisualTrackingTests/UnitTests/InspectorSpec.swift index 26c71b3b..438231bd 100644 --- a/KarteTests/KarteVisualTrackingTests/UnitTests/InspectorSpec.swift +++ b/KarteTests/KarteVisualTrackingTests/UnitTests/InspectorSpec.swift @@ -22,6 +22,55 @@ class InspectorSpec: QuickSpec { override func spec() { describe("a inspector") { + describe("its inspectView") { + var window: UIWindow! + var view1: UIView! + var view2: UIView! + var view3: UIView! + var view4: UIView! + + beforeEach { + window = UIWindow() + view1 = UIView() + view2 = UIView() + view3 = UIView() + view4 = UIView() + view1.addSubview(view2) + view2.addSubview(view3) + view2.addSubview(view4) + window.addSubview(view1) + } + + context("when passing inWindow nil") { + it("returns nil") { + let actual = Inspector.inspectView(with: [0], inWindow: nil) + expect(actual).to(beNil()) + } + } + context("when passing empty array") { + it("returns nil") { + let actual = Inspector.inspectView(with: [], inWindow: UIWindow()) + expect(actual).to(beNil()) + } + } + context("when passing out-of-bounds indices") { + it("returns nil") { + let viewPathIndices = UIKitAction.viewPathIndices(actionId: "Olympic0View11UIView0") + let actual = Inspector.inspectView(with: viewPathIndices, inWindow: window) + expect(actual).to(beNil()) + } + } + context("when passing UIView1UIView0UIView0UIWindow") { + it("returns not nil") { + let actionId = UIKitAction.actionId(view: view4) + let viewPathIndices = UIKitAction.viewPathIndices(actionId: actionId) + let actual = Inspector.inspectView(with: viewPathIndices, inWindow: window) + expect(actual).toNot(beNil()) + expect(actionId).to(equal("UIView1UIView0UIView0UIWindow")) + } + } + } + describe("its inspectText") { context("when passing nil") { it("returns nil") { diff --git a/KarteTests/Stub/StubResource.swift b/KarteTests/Stub/StubResource.swift index f2a21fc7..df0c947c 100644 --- a/KarteTests/Stub/StubResource.swift +++ b/KarteTests/Stub/StubResource.swift @@ -46,4 +46,5 @@ extension StubResource { static var vt1 = StubResource("success_vt_1.json") static var vt2 = StubResource("success_vt_2.json") static var vt_definitions = StubResource("success_vt_definitions.json") + static var vt_definitions_with_dynamic_fields = StubResource("success_vt_definitions_with_dynamic_fields.json") } diff --git a/KarteTests/Stub/success_vt_definitions_with_dynamic_fields.json b/KarteTests/Stub/success_vt_definitions_with_dynamic_fields.json new file mode 100644 index 00000000..15a18814 --- /dev/null +++ b/KarteTests/Stub/success_vt_definitions_with_dynamic_fields.json @@ -0,0 +1,248 @@ +{ + "success": 1, + "status": 200, + "response": { + "status": "modified", + "last_modified": 1555039737865, + "definitions": [ + { + "event_name": "view", + "triggers": [ + { + "fields": { + "foo": "bar" + }, + "dynamic_fields": [ + { + "type": "target_text", + "name": "foo", + "action_id": "UILabel2UIView0UIWindow" + }, + { + "type": "unknown_type", + "name": "bar", + "action_id": "UILabel2UIView0UIWindow" + }, + { + "name": "baz", + "action_id": "UILabel2UIView0UIWindow" + }, + { + "name": "has_unknown_key", + "action_id": "UILabel2UIView0UIWindow", + "unknown_key": "foo" + } + ], + "condition": { + "$and": [ + { + "unknown_key": { + "$eq": "購入" + } + }, + { + "view_controller": { + "unknown_key": "購入" + } + }, + { + "target_text": { + "$eq": "購入" + } + }, + { + "view": { + "$eq": "UIButton" + } + } + ] + } + }, + { + "fields": {}, + "dynamic_fields": [ + { + "name": "foo" + }, + { + "type": "foo" + }, + { + "type": "foo", + "action_id": "UIButton" + } + ], + "condition": { + "$and": [ + { + "target_text": { + "$ne": "-" + } + }, + { + "view": { + "$eq": "UITableView" + } + }, + { + "view_controller": { + "$eq": "Tracker.BasicViewController" + } + }, + { + "action": { + "$eq": "viewDidAppear" + } + } + ] + } + }, + { + "fields": {}, + "dynamic_fields": [], + "condition": { + "$and": [ + { + "target_text": { + "$ne": "-" + } + }, + { + "view": { + "$eq": "UITableView" + } + }, + { + "view_controller": { + "$eq": "Tracker.BasicViewController" + } + }, + { + "action": { + "$eq": "viewDidAppear" + } + } + ] + } + }, + { + "fields": {}, + "dynamic_fields": [{}], + "condition": { + "$and": [ + { + "target_text": { + "$ne": "-" + } + }, + { + "view": { + "$eq": "UITableView" + } + }, + { + "view_controller": { + "$eq": "Tracker.BasicViewController" + } + }, + { + "action": { + "$eq": "viewDidAppear" + } + } + ] + } + } + ] + }, + { + "event_name": "vt_test1", + "triggers": [ + { + "fields": {}, + "condition": { + "$and": [ + { + "target_text": { + "$eq": "購入" + } + }, + { + "view": { + "$eq": "UIButton" + } + }, + { + "view_controller": { + "$eq": "ItemDetailViewController" + } + }, + { + "action": { + "$eq": "_buttonDown" + } + }, + { + "app_info.version_name": { + "$ne": "1.0.0" + } + }, + { + "app_info.version_code": { + "$ne": "0" + } + }, + { + "app_info.karte_sdk_version": { + "$ne": "1.0.0" + } + }, + { + "app_info.system_info.os": { + "$startsWith": "iO" + } + }, + { + "app_info.system_info.os_version": { + "$ne": "1" + } + }, + { + "app_info.system_info.device": { + "$startsWith": "i" + } + }, + { + "app_info.system_info.model": { + "$endsWith": "64" + } + }, + { + "app_info.system_info.bundle_id": { + "$contains": "karte" + } + }, + { + "app_info.system_info.idfv": { + "$contains": "1" + } + }, + { + "app_info.system_info.idfa": { + "$contains": "1" + } + }, + { + "app_info.system_info.language": { + "$contains": "en" + } + } + ] + } + } + ] + } + ] + } + } + diff --git a/KarteVisualTracking.podspec b/KarteVisualTracking.podspec index 68b72ed7..f06d7c1c 100644 --- a/KarteVisualTracking.podspec +++ b/KarteVisualTracking.podspec @@ -8,7 +8,7 @@ Pod::Spec.new do |s| s.name = 'KarteVisualTracking' - s.version = '2.6.0' + s.version = '2.7.0' s.summary = 'KARTE Visual tracking SDK' s.homepage = 'https://karte.io' s.author = { 'PLAID' => 'dev.share@plaid.co.jp' } diff --git a/KarteVisualTracking/Inspector.swift b/KarteVisualTracking/Inspector.swift index ba21ab2d..7393de08 100644 --- a/KarteVisualTracking/Inspector.swift +++ b/KarteVisualTracking/Inspector.swift @@ -39,6 +39,23 @@ internal enum Inspector { return nil } + /// View階層のパス情報でwindowのsubViewを探索して見つかったViewを返す + static func inspectView(with viewPathIndices: [Int], inWindow window: UIWindow?) -> UIView? { + guard var target: UIView = window, viewPathIndices.count > 0 else { + return nil + } + + for index in viewPathIndices { + guard target.subviews.indices.contains(index) else { + return nil + } + let next = target.subviews[index] + target = next + } + + return target + } + static func takeSnapshot(with view: UIView?) -> UIImage? { guard let view = view else { return nil diff --git a/KarteVisualTracking/Swizzlers/UIApplication+VisualTracking.swift b/KarteVisualTracking/Swizzlers/UIApplication+VisualTracking.swift index 54cb8bfa..2059d62d 100644 --- a/KarteVisualTracking/Swizzlers/UIApplication+VisualTracking.swift +++ b/KarteVisualTracking/Swizzlers/UIApplication+VisualTracking.swift @@ -35,11 +35,13 @@ extension UIApplication { private func krt_vt_sendAction(_ action: Selector, to target: Any?, from sender: Any?, for event: UIEvent?) -> Bool { if let view = target as? UIView, event != nil { let viewController = UIResponder.krt_vt_retrieveViewController(for: view) + let appropriateView = UIKitAction.AppropriateViewDetector(view: view)?.detect() let action = UIKitAction( NSStringFromSelector(action), view: view, viewController: viewController, - targetText: Inspector.inspectText(with: sender) + targetText: Inspector.inspectText(with: sender), + actionId: UIKitAction.actionId(view: appropriateView) ) VisualTrackingManager.shared.dispatch(action: action) } @@ -52,14 +54,18 @@ extension UIApplication { for touch in touches where touch.phase == .ended { let view = touch.view let viewController = UIResponder.krt_vt_retrieveViewController(for: view) + let appropriateView = UIKitAction.AppropriateViewDetector(view: view)?.detect() + let actionId = UIKitAction.actionId(view: appropriateView) let action = UIKitAction( "touch", - view: view, + view: appropriateView, viewController: viewController, - targetText: Inspector.inspectText(with: view) + targetText: Inspector.inspectText(with: view), + actionId: actionId ) VisualTrackingManager.shared.dispatch(action: action) } + krt_vt_sendEvent(event) } } diff --git a/KarteVisualTracking/Swizzlers/UIGestureRecognizer+VisualTracking.swift b/KarteVisualTracking/Swizzlers/UIGestureRecognizer+VisualTracking.swift index b3a15ab6..c5648d8a 100644 --- a/KarteVisualTracking/Swizzlers/UIGestureRecognizer+VisualTracking.swift +++ b/KarteVisualTracking/Swizzlers/UIGestureRecognizer+VisualTracking.swift @@ -45,11 +45,14 @@ extension UIGestureRecognizer { let viewController = UIResponder.krt_vt_retrieveViewController(for: view) if let match = RE.shared.firstMatch(in: description) { let range = match.range(at: 1) + let appropriateView = UIKitAction.AppropriateViewDetector(view: view)?.detect() + let actionId = UIKitAction.actionId(view: appropriateView) let action = UIKitAction( (description as NSString).substring(with: range), - view: view, + view: appropriateView, viewController: viewController, - targetText: Inspector.inspectText(with: view) + targetText: Inspector.inspectText(with: appropriateView), + actionId: actionId ) VisualTrackingManager.shared.dispatch(action: action) } diff --git a/KarteVisualTracking/Swizzlers/UINavigationController+VisualTracking.swift b/KarteVisualTracking/Swizzlers/UINavigationController+VisualTracking.swift index 5f2f395e..74cfe118 100644 --- a/KarteVisualTracking/Swizzlers/UINavigationController+VisualTracking.swift +++ b/KarteVisualTracking/Swizzlers/UINavigationController+VisualTracking.swift @@ -51,8 +51,7 @@ extension UINavigationController { let action = UIKitAction( "setViewControllers", view: nil, - viewController: viewControllers.last, - targetText: nil + viewController: viewControllers.last ) VisualTrackingManager.shared.dispatch(action: action) @@ -64,8 +63,7 @@ extension UINavigationController { let action = UIKitAction( "pushViewController", view: nil, - viewController: visibleViewController, - targetText: nil + viewController: visibleViewController ) VisualTrackingManager.shared.dispatch(action: action) @@ -77,8 +75,7 @@ extension UINavigationController { let action = UIKitAction( "popViewController", view: nil, - viewController: visibleViewController, - targetText: nil + viewController: visibleViewController ) VisualTrackingManager.shared.dispatch(action: action) @@ -91,8 +88,7 @@ extension UINavigationController { let action = UIKitAction( "popToViewController", view: nil, - viewController: viewController, - targetText: nil + viewController: viewController ) VisualTrackingManager.shared.dispatch(action: action) @@ -105,8 +101,7 @@ extension UINavigationController { let action = UIKitAction( "popToRootViewController", view: nil, - viewController: viewControllers.first, - targetText: nil + viewController: viewControllers.first ) VisualTrackingManager.shared.dispatch(action: action) diff --git a/KarteVisualTracking/Tracking/Definition/Definition.swift b/KarteVisualTracking/Tracking/Definition/Definition.swift index 766f65a9..3209301c 100644 --- a/KarteVisualTracking/Tracking/Definition/Definition.swift +++ b/KarteVisualTracking/Tracking/Definition/Definition.swift @@ -43,7 +43,11 @@ internal struct Definition: Codable { triggers.filter { trigger -> Bool in trigger.match(data: data) }.map { trigger -> Event in - let values: [String: JSONConvertible] = trigger.fields + var values: [String: JSONConvertible] = trigger.fields + let window = WindowDetector.retrieveRelatedWindows().first + if let dynamicValues = trigger.dynamicValues(window: window) { + values.merge(dynamicValues) { (values, _) in values } + } let other: [String: JSONConvertible] = [ "_system": [ "auto_track": 1 diff --git a/KarteVisualTracking/Tracking/Definition/DynamicField.swift b/KarteVisualTracking/Tracking/Definition/DynamicField.swift new file mode 100644 index 00000000..e7f7ab02 --- /dev/null +++ b/KarteVisualTracking/Tracking/Definition/DynamicField.swift @@ -0,0 +1,31 @@ +// +// Copyright 2020 PLAID, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation + +internal struct DynamicField: Codable { + var type: String? + var name: String? + var actionId: String? +} + +extension DynamicField { + enum CodingKeys: String, CodingKey { + case type + case name + case actionId = "action_id" + } +} diff --git a/KarteVisualTracking/Tracking/Definition/Trigger.swift b/KarteVisualTracking/Tracking/Definition/Trigger.swift index 9d3c25ae..54abadd5 100644 --- a/KarteVisualTracking/Tracking/Definition/Trigger.swift +++ b/KarteVisualTracking/Tracking/Definition/Trigger.swift @@ -16,12 +16,48 @@ import Foundation import KarteCore +import UIKit internal struct Trigger: Codable { var condition: LogicalOperator var fields: [String: String] + var dynamicFields: [DynamicField]? + + func dynamicValues(window: UIWindow?) -> [String: JSONConvertible]? { + guard let dynamicFields = self.dynamicFields, + let window = window, + !dynamicFields.isEmpty else { + return nil + } + var result: [String: JSONConvertible] = [:] + for dynamicField in dynamicFields { + guard let dynamicFieldActionId = dynamicField.actionId, + let dynamicFieldName = dynamicField.name + else { + return nil + } + + let viewPath = UIKitAction.viewPathIndices(actionId: dynamicFieldActionId) + let view = Inspector.inspectView(with: viewPath, inWindow: window) + let actionId = UIKitAction.actionId(view: view) + if dynamicFieldActionId == actionId, + let targetText = Inspector.inspectText(with: view) { + result[dynamicFieldName] = targetText + } + } + + return result + } func match(data: [String: JSONValue]) -> Bool { condition.match(data: data) } } + +extension Trigger { + enum CodingKeys: String, CodingKey { + case condition + case fields + case dynamicFields = "dynamic_fields" + } +} diff --git a/KarteVisualTracking/UIKitAction.swift b/KarteVisualTracking/UIKitAction.swift index c95c1440..3a40afd0 100644 --- a/KarteVisualTracking/UIKitAction.swift +++ b/KarteVisualTracking/UIKitAction.swift @@ -43,11 +43,11 @@ public enum ActionFactory { ) -> ActionProtocol? { UIKitAction( actionName, - view: view, + view: UIKitAction.AppropriateViewDetector(view: view)?.detect(), viewController: viewController, targetText: targetText, actionId: actionId, - imageProvider: imageProvider ?? UIKitAction.defaultImageProvider(view: view, viewController: viewController) + imageProvider: imageProvider ) } } @@ -80,8 +80,8 @@ extension UIKitActionProtocol { } internal struct UIKitAction: UIKitActionProtocol { - static let ignoreActions = ["handlePan:", "handlePanGesture:", "handlePinchGesture:", "handleTouchMonitor:", "handleTiltGesture:"] - + static let ignoreActions = ["handlePan:", "handlePanGesture:", "handlePinchGesture:", + "handleTouchMonitor:", "handleTiltGesture:", "observerGestureHandler:"] var action: String var view: UIView? var viewController: UIViewController? @@ -103,26 +103,13 @@ internal struct UIKitAction: UIKitActionProtocol { return String(describing: type(of: viewController)) } - init?(_ action: String, view: UIView?, viewController: UIViewController?, targetText: String?, imageProvider: ImageProvider? = nil) { - guard UIKitAction.validate(actoionName: action, view: view, viewController: viewController, targetText: targetText) else { - return nil - } - - self.action = action - self.view = AppropriateViewDetector(view: view)?.detect() - self.viewController = viewController - self.targetText = targetText - self.actionId = UIKitAction.actionId(view: self.view) - self.imageProvider = imageProvider ?? Self.defaultImageProvider(view: view, viewController: viewController) - } - - init?(_ action: String, view: UIView?, viewController: UIViewController?, targetText: String?, actionId: String?, imageProvider: ImageProvider? = nil) { + init?(_ action: String, view: UIView?, viewController: UIViewController?, targetText: String? = nil, actionId: String? = nil, imageProvider: ImageProvider? = nil) { guard UIKitAction.validate(actoionName: action, view: view, viewController: viewController, targetText: targetText) else { return nil } self.action = action - self.view = AppropriateViewDetector(view: view)?.detect() + self.view = view self.viewController = viewController self.targetText = targetText self.actionId = actionId @@ -149,7 +136,8 @@ internal struct UIKitAction: UIKitActionProtocol { } extension UIKitAction { - private static func actionId(view: UIView?) -> String? { + /// Viewの階層情報を連結してactionIdとして返す。(例: UIButton0UIView0UIView) + static func actionId(view: UIView?) -> String? { guard view != nil else { return nil } @@ -170,6 +158,15 @@ extension UIKitAction { } return actionId } + + /// actionIdからView階層のパスを示すindex配列を返す。(例: UIView1UIView0UIViewからは、[0,1]が返される) + static func viewPathIndices(actionId: String?) -> [Int] { + guard let actionId = actionId else { + return [] + } + let indices = actionId.components(separatedBy: CharacterSet.decimalDigits.inverted).compactMap { Int($0) } + return indices.reversed() + } } extension UIKitAction { @@ -183,6 +180,8 @@ extension UIKitAction { self.view = view } + /// View階層を探索してビジュアルトラッキングの発火条件として扱いやすい親Viewがあれば返す。(UITableViewCellなど) + /// 発火条件として扱いやすい親Viewがなければ、保持しているviewを返す。 func detect() -> UIView? { if isAppropriateView { return view