diff --git a/SensorsAnalyticsSDK.podspec b/SensorsAnalyticsSDK.podspec index 2677a775..ae7c2ec9 100644 --- a/SensorsAnalyticsSDK.podspec +++ b/SensorsAnalyticsSDK.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SensorsAnalyticsSDK" - s.version = "3.1.7" + s.version = "3.1.8" s.summary = "The official iOS SDK of Sensors Analytics." s.homepage = "http://www.sensorsdata.cn" s.source = { :git => 'https://github.com/sensorsdata/sa-sdk-ios.git', :tag => "v#{s.version}" } diff --git a/SensorsAnalyticsSDK.xcodeproj/project.pbxproj b/SensorsAnalyticsSDK.xcodeproj/project.pbxproj index 28e8010b..cf6e1fad 100644 --- a/SensorsAnalyticsSDK.xcodeproj/project.pbxproj +++ b/SensorsAnalyticsSDK.xcodeproj/project.pbxproj @@ -88,7 +88,7 @@ 4DD1282025F87225008C0B1E /* UIView+SAElementPath.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1281D25F87225008C0B1E /* UIView+SAElementPath.h */; }; 4DD1282125F87225008C0B1E /* SAVisualizedViewPathProperty.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1281E25F87225008C0B1E /* SAVisualizedViewPathProperty.h */; }; 4DD1282225F87225008C0B1E /* UIView+SAElementPath.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1281F25F87225008C0B1E /* UIView+SAElementPath.m */; }; - 4DD1284B25F872A4008C0B1E /* SAJSTouchEventView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1282625F872A2008C0B1E /* SAJSTouchEventView.h */; }; + 4DD1284B25F872A4008C0B1E /* SAWebElementView.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1282625F872A2008C0B1E /* SAWebElementView.h */; }; 4DD1284C25F872A4008C0B1E /* NSInvocation+SAHelpers.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1282725F872A2008C0B1E /* NSInvocation+SAHelpers.m */; }; 4DD1284D25F872A4008C0B1E /* SAVisualizedConnection.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1282825F872A2008C0B1E /* SAVisualizedConnection.m */; }; 4DD1284E25F872A4008C0B1E /* SATypeDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1282925F872A2008C0B1E /* SATypeDescription.h */; }; @@ -118,7 +118,7 @@ 4DD1286725F872A4008C0B1E /* NSInvocation+SAHelpers.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1284225F872A4008C0B1E /* NSInvocation+SAHelpers.h */; }; 4DD1286825F872A4008C0B1E /* SAObjectSerializerContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1284325F872A4008C0B1E /* SAObjectSerializerContext.h */; }; 4DD1286925F872A4008C0B1E /* SATypeDescription.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1284425F872A4008C0B1E /* SATypeDescription.m */; }; - 4DD1286A25F872A4008C0B1E /* SAJSTouchEventView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1284525F872A4008C0B1E /* SAJSTouchEventView.m */; }; + 4DD1286A25F872A4008C0B1E /* SAWebElementView.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1284525F872A4008C0B1E /* SAWebElementView.m */; }; 4DD1286C25F872A4008C0B1E /* SAClassDescription.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1284725F872A4008C0B1E /* SAClassDescription.h */; }; 4DD1286D25F872A4008C0B1E /* SAVisualizedManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 4DD1284825F872A4008C0B1E /* SAVisualizedManager.m */; }; 4DD1286E25F872A4008C0B1E /* SAValueTransformers.h in Headers */ = {isa = PBXBuildFile; fileRef = 4DD1284925F872A4008C0B1E /* SAValueTransformers.h */; }; @@ -177,8 +177,6 @@ 883BAAB02669CD18008105D2 /* SAExceptionManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 883BAAAE2669CD18008105D2 /* SAExceptionManager.h */; }; 883BAAB12669CD18008105D2 /* SAExceptionManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 883BAAAF2669CD18008105D2 /* SAExceptionManager.m */; }; 884B423F25540DEE00416DB6 /* SensorsAnalyticsSDK+Public.h in Headers */ = {isa = PBXBuildFile; fileRef = 884B423B25540D7C00416DB6 /* SensorsAnalyticsSDK+Public.h */; settings = {ATTRIBUTES = (Public, ); }; }; - 887602D42685E0AA00C5EA51 /* SAModuleManager+Visualized.h in Headers */ = {isa = PBXBuildFile; fileRef = 887602D22685E0AA00C5EA51 /* SAModuleManager+Visualized.h */; }; - 887602D52685E0AA00C5EA51 /* SAModuleManager+Visualized.m in Sources */ = {isa = PBXBuildFile; fileRef = 887602D32685E0AA00C5EA51 /* SAModuleManager+Visualized.m */; }; A806642A26905F6C00FFDEBA /* SensorsAnalyticsSDK+SAChannelMatch.m in Sources */ = {isa = PBXBuildFile; fileRef = A806642626905F6C00FFDEBA /* SensorsAnalyticsSDK+SAChannelMatch.m */; }; A806642B26905F6C00FFDEBA /* SAChannelMatchManager.h in Headers */ = {isa = PBXBuildFile; fileRef = A806642726905F6C00FFDEBA /* SAChannelMatchManager.h */; }; A806642C26905F6C00FFDEBA /* SensorsAnalyticsSDK+SAChannelMatch.h in Headers */ = {isa = PBXBuildFile; fileRef = A806642826905F6C00FFDEBA /* SensorsAnalyticsSDK+SAChannelMatch.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -410,7 +408,7 @@ 4DD1281D25F87225008C0B1E /* UIView+SAElementPath.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UIView+SAElementPath.h"; sourceTree = ""; }; 4DD1281E25F87225008C0B1E /* SAVisualizedViewPathProperty.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAVisualizedViewPathProperty.h; sourceTree = ""; }; 4DD1281F25F87225008C0B1E /* UIView+SAElementPath.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIView+SAElementPath.m"; sourceTree = ""; }; - 4DD1282625F872A2008C0B1E /* SAJSTouchEventView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAJSTouchEventView.h; sourceTree = ""; }; + 4DD1282625F872A2008C0B1E /* SAWebElementView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAWebElementView.h; sourceTree = ""; }; 4DD1282725F872A2008C0B1E /* NSInvocation+SAHelpers.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSInvocation+SAHelpers.m"; sourceTree = ""; }; 4DD1282825F872A2008C0B1E /* SAVisualizedConnection.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAVisualizedConnection.m; sourceTree = ""; }; 4DD1282925F872A2008C0B1E /* SATypeDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SATypeDescription.h; sourceTree = ""; }; @@ -440,7 +438,7 @@ 4DD1284225F872A4008C0B1E /* NSInvocation+SAHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSInvocation+SAHelpers.h"; sourceTree = ""; }; 4DD1284325F872A4008C0B1E /* SAObjectSerializerContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAObjectSerializerContext.h; sourceTree = ""; }; 4DD1284425F872A4008C0B1E /* SATypeDescription.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SATypeDescription.m; sourceTree = ""; }; - 4DD1284525F872A4008C0B1E /* SAJSTouchEventView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAJSTouchEventView.m; sourceTree = ""; }; + 4DD1284525F872A4008C0B1E /* SAWebElementView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAWebElementView.m; sourceTree = ""; }; 4DD1284725F872A4008C0B1E /* SAClassDescription.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAClassDescription.h; sourceTree = ""; }; 4DD1284825F872A4008C0B1E /* SAVisualizedManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SAVisualizedManager.m; sourceTree = ""; }; 4DD1284925F872A4008C0B1E /* SAValueTransformers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAValueTransformers.h; sourceTree = ""; }; @@ -502,8 +500,6 @@ 883BAAAE2669CD18008105D2 /* SAExceptionManager.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SAExceptionManager.h; sourceTree = ""; }; 883BAAAF2669CD18008105D2 /* SAExceptionManager.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SAExceptionManager.m; sourceTree = ""; }; 884B423B25540D7C00416DB6 /* SensorsAnalyticsSDK+Public.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SensorsAnalyticsSDK+Public.h"; sourceTree = ""; }; - 887602D22685E0AA00C5EA51 /* SAModuleManager+Visualized.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SAModuleManager+Visualized.h"; sourceTree = ""; }; - 887602D32685E0AA00C5EA51 /* SAModuleManager+Visualized.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SAModuleManager+Visualized.m"; sourceTree = ""; }; A806642626905F6C00FFDEBA /* SensorsAnalyticsSDK+SAChannelMatch.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "SensorsAnalyticsSDK+SAChannelMatch.m"; sourceTree = ""; }; A806642726905F6C00FFDEBA /* SAChannelMatchManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SAChannelMatchManager.h; sourceTree = ""; }; A806642826905F6C00FFDEBA /* SensorsAnalyticsSDK+SAChannelMatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "SensorsAnalyticsSDK+SAChannelMatch.h"; sourceTree = ""; }; @@ -688,6 +684,7 @@ 4D0571A525A2FC46007F7B72 /* Visualized */ = { isa = PBXGroup; children = ( + 4D754E2626CB58A900300835 /* WebElementInfo */, 4DD128B125F8A003008C0B1E /* Config */, 4D6AE7F626086CED00A9CB08 /* EventCheck */, 4D0571AA25A2FC46007F7B72 /* ElementSelector */, @@ -703,8 +700,6 @@ 4DD1282B25F872A2008C0B1E /* SAClassDescription.m */, 4DD1283425F872A3008C0B1E /* SAEnumDescription.h */, 4DD1282F25F872A3008C0B1E /* SAEnumDescription.m */, - 4DD1282625F872A2008C0B1E /* SAJSTouchEventView.h */, - 4DD1284525F872A4008C0B1E /* SAJSTouchEventView.m */, 4DD1283025F872A3008C0B1E /* SAObjectIdentityProvider.h */, 4DD1283125F872A3008C0B1E /* SAObjectIdentityProvider.m */, 4DD1284025F872A4008C0B1E /* SAObjectSerializerConfig.h */, @@ -789,6 +784,15 @@ path = EventCheck; sourceTree = ""; }; + 4D754E2626CB58A900300835 /* WebElementInfo */ = { + isa = PBXGroup; + children = ( + 4DD1282625F872A2008C0B1E /* SAWebElementView.h */, + 4DD1284525F872A4008C0B1E /* SAWebElementView.m */, + ); + path = WebElementInfo; + sourceTree = ""; + }; 4D8CE4C522E872B400829B29 /* Frameworks */ = { isa = PBXGroup; children = ( @@ -1050,8 +1054,6 @@ children = ( A8356D8F2656459A00FD64AA /* SensorsAnalyticsSDK+SAAutoTrack.h */, A8356DB12656459A00FD64AA /* SensorsAnalyticsSDK+SAAutoTrack.m */, - 887602D22685E0AA00C5EA51 /* SAModuleManager+Visualized.h */, - 887602D32685E0AA00C5EA51 /* SAModuleManager+Visualized.m */, A8356D902656459A00FD64AA /* SAAutoTrackProperty.h */, A8356D982656459A00FD64AA /* SAAutoTrackManager.h */, A8356D882656459A00FD64AA /* SAAutoTrackManager.m */, @@ -1400,7 +1402,7 @@ 4DD1285125F872A4008C0B1E /* SAApplicationStateSerializer.h in Headers */, A8356DC12656459A00FD64AA /* SAAppEndTracker.h in Headers */, A82E8957267D918100475757 /* SASecretKeyFactory.h in Headers */, - 4DD1284B25F872A4008C0B1E /* SAJSTouchEventView.h in Headers */, + 4DD1284B25F872A4008C0B1E /* SAWebElementView.h in Headers */, F277F5C925CF9A43009B5CE6 /* SANotificationUtil.h in Headers */, A8CC223E2685E50C00E96A03 /* SARemoteConfigModel.h in Headers */, 881A41A0253D7B4F00854F69 /* SensorsAnalyticsSDK+Private.h in Headers */, @@ -1466,7 +1468,6 @@ 45A56560263C174300C9C41B /* SAIDFAHelper.h in Headers */, 4DD1281925F8721A008C0B1E /* UIView+SAElementSelector.h in Headers */, A82E8953267D918100475757 /* SAECCEncryptor.h in Headers */, - 887602D42685E0AA00C5EA51 /* SAModuleManager+Visualized.h in Headers */, A8356DDB2656459A00FD64AA /* UIViewController+AutoTrack.h in Headers */, 4D6AE7FC26086E9300A9CB08 /* SAVisualizedEventCheck.h in Headers */, 45A56563263C174300C9C41B /* SAPropertyValidator.h in Headers */, @@ -1740,7 +1741,7 @@ 4DD1285E25F872A4008C0B1E /* SAVisualizedObjectSerializerManager.m in Sources */, 45A5655E263C174300C9C41B /* SASuperProperty.m in Sources */, 4DD1286D25F872A4008C0B1E /* SAVisualizedManager.m in Sources */, - 4DD1286A25F872A4008C0B1E /* SAJSTouchEventView.m in Sources */, + 4DD1286A25F872A4008C0B1E /* SAWebElementView.m in Sources */, F277F5BF25CF9A43009B5CE6 /* SAApplicationDelegateProxy.m in Sources */, A82E894F267D918100475757 /* SAEncryptManager.m in Sources */, 881A41A1253D7B4F00854F69 /* SAAppExtensionDataManager.m in Sources */, @@ -1758,7 +1759,6 @@ A8CC223A2685E50C00E96A03 /* SARemoteConfigOperator.m in Sources */, A8356DCC2656459A00FD64AA /* SAGestureTarget.m in Sources */, 4D6AE7FD26086E9300A9CB08 /* SAVisualizedEventCheck.m in Sources */, - 887602D52685E0AA00C5EA51 /* SAModuleManager+Visualized.m in Sources */, F244AE3D26B170F000D8C60B /* UIViewController+SAPageView.m in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SensorsAnalyticsSDK/AutoTrack/AppClick/SAAppClickTracker.m b/SensorsAnalyticsSDK/AutoTrack/AppClick/SAAppClickTracker.m index fd1d85b1..f80d9e05 100644 --- a/SensorsAnalyticsSDK/AutoTrack/AppClick/SAAppClickTracker.m +++ b/SensorsAnalyticsSDK/AutoTrack/AppClick/SAAppClickTracker.m @@ -31,8 +31,8 @@ #import "SAAutoTrackUtils.h" #import "UIView+AutoTrack.h" #import "UIViewController+AutoTrack.h" +#import "SAModuleManager.h" #import "SALog.h" -#import "SAModuleManager+Visualized.h" @interface SAAppClickTracker () diff --git a/SensorsAnalyticsSDK/AutoTrack/SAAutoTrackUtils.m b/SensorsAnalyticsSDK/AutoTrack/SAAutoTrackUtils.m index 8886379e..418b7ab1 100644 --- a/SensorsAnalyticsSDK/AutoTrack/SAAutoTrackUtils.m +++ b/SensorsAnalyticsSDK/AutoTrack/SAAutoTrackUtils.m @@ -29,8 +29,8 @@ #import "UIView+AutoTrack.h" #import "SALog.h" #import "SAAlertController.h" +#import "SAModuleManager.h" #import "SAValidator.h" -#import "SAModuleManager+Visualized.h" /// 一个元素 $AppClick 全埋点最小时间间隔,100 毫秒 static NSTimeInterval SATrackAppClickMinTimeInterval = 0.1; diff --git a/SensorsAnalyticsSDK/AutoTrack/SAModuleManager+Visualized.h b/SensorsAnalyticsSDK/AutoTrack/SAModuleManager+Visualized.h deleted file mode 100644 index dd39e5b5..00000000 --- a/SensorsAnalyticsSDK/AutoTrack/SAModuleManager+Visualized.h +++ /dev/null @@ -1,45 +0,0 @@ -// -// SAModuleManager+Visualized.h -// SensorsAnalyticsSDK -// -// Created by 张敏超🍎 on 2021/6/25. -// Copyright © 2021 Sensors Data Co., Ltd. All rights reserved. -// -// 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 -// -// http://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 "SAModuleManager.h" -#import - -NS_ASSUME_NONNULL_BEGIN - -@protocol SAVisualizedModuleProtocol - -/// 元素相关属性 -/// @param view 需要采集的 view -- (nullable NSDictionary *)propertiesWithView:(UIView *)view; - -#pragma mark visualProperties - -/// 采集元素自定义属性 -/// @param view 触发事件的元素 -/// @param completionHandler 采集完成回调 -- (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDictionary *_Nullable visualProperties))completionHandler; - -@end - -@interface SAModuleManager (Visualized) - -@end - -NS_ASSUME_NONNULL_END diff --git a/SensorsAnalyticsSDK/AutoTrack/SAModuleManager+Visualized.m b/SensorsAnalyticsSDK/AutoTrack/SAModuleManager+Visualized.m deleted file mode 100644 index 5eb9d74e..00000000 --- a/SensorsAnalyticsSDK/AutoTrack/SAModuleManager+Visualized.m +++ /dev/null @@ -1,45 +0,0 @@ -// -// SAModuleManager+Visualized.m -// SensorsAnalyticsSDK -// -// Created by 张敏超🍎 on 2021/6/25. -// Copyright © 2021 Sensors Data Co., Ltd. All rights reserved. -// -// 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 -// -// http://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. -// - -#if ! __has_feature(objc_arc) -#error This file must be compiled with ARC. Either turn on ARC for the project or use -fobjc-arc flag on this file. -#endif - -#import "SAModuleManager+Visualized.h" - -@implementation SAModuleManager (Visualized) - -#pragma mark properties -// 采集元素属性 -- (nullable NSDictionary *)propertiesWithView:(UIView *)view { - id manager = (id)[SAModuleManager.sharedInstance managerForModuleType:SAModuleTypeVisualized]; - return [manager propertiesWithView:view]; -} - -// 采集元素自定义属性 -- (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDictionary *_Nullable))completionHandler { - id manager = (id)[SAModuleManager.sharedInstance managerForModuleType:SAModuleTypeVisualized]; - if (!manager) { - return completionHandler(nil); - } - [manager visualPropertiesWithView:view completionHandler:completionHandler]; -} - -@end diff --git a/SensorsAnalyticsSDK/Core/Builder/EventObject/SABaseEventObject.m b/SensorsAnalyticsSDK/Core/Builder/EventObject/SABaseEventObject.m index 0cc72831..c78ecf4a 100644 --- a/SensorsAnalyticsSDK/Core/Builder/EventObject/SABaseEventObject.m +++ b/SensorsAnalyticsSDK/Core/Builder/EventObject/SABaseEventObject.m @@ -144,6 +144,7 @@ - (id)sensorsdata_validKey:(NSString *)key value:(id)value error:(NSError *__aut return nil; } + // key 校验 [(id )key sensorsdata_isValidPropertyKeyWithError:error]; if (*error) { return nil; diff --git a/SensorsAnalyticsSDK/Core/Builder/EventObject/SAPropertyValidator.m b/SensorsAnalyticsSDK/Core/Builder/EventObject/SAPropertyValidator.m index 19b3a695..e4bf7137 100644 --- a/SensorsAnalyticsSDK/Core/Builder/EventObject/SAPropertyValidator.m +++ b/SensorsAnalyticsSDK/Core/Builder/EventObject/SAPropertyValidator.m @@ -81,7 +81,9 @@ - (id)sensorsdata_propertyValueWithKey:(NSString *)key error:(NSError *__autorel return nil; } id sensorsValue = [(id )element sensorsdata_propertyValueWithKey:key error:error]; - [result addObject:sensorsValue]; + if (sensorsValue) { + [result addObject:sensorsValue]; + } } return [result copy]; } @@ -98,7 +100,9 @@ - (id)sensorsdata_propertyValueWithKey:(NSString *)key error:(NSError *__autorel return nil; } id sensorsValue = [(id )element sensorsdata_propertyValueWithKey:key error:error]; - [result addObject:sensorsValue]; + if (sensorsValue) { + [result addObject:sensorsValue]; + } } return [result copy]; } diff --git a/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.h b/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.h index d4764a20..4f946fed 100644 --- a/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.h +++ b/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.h @@ -36,4 +36,57 @@ NS_ASSUME_NONNULL_BEGIN @end + +/** + * @abstract + * App 调用 JS 方法的类型。 + * + * @discussion + * 调用 JS 方法类型枚举 + */ +typedef NS_ENUM(NSInteger, SAJavaScriptCallJSType) { + /// 进入可视化扫码模式通知 JS + SAJavaScriptCallJSTypeVisualized, + /// 检测是否集成 JS SDK + SAJavaScriptCallJSTypeCheckJSSDK, + /// 更新自定义属性配置 + SAJavaScriptCallJSTypeUpdateVisualConfig, + /// 获取 App 内嵌 H5 采集的自定义属性 + SAJavaScriptCallJSTypeWebVisualProperties +}; + +/// 打通写入 serverURL +extern NSString * const kSAJSBridgeServerURL; + +/// 可视化通知已进入扫码模式 +extern NSString * const kSAJSBridgeVisualizedMode; + +/// js 方法调用 +extern NSString * const kSAJSBridgeCallMethod; + +/// 构建 js 相关 bridge 和变量 +@interface SAJavaScriptBridgeBuilder : NSObject + +#pragma mark 注入 js +/// 注入打通bridge,并设置 serverURL +/// @param serverURL 数据接收地址 ++ (nullable NSString *)buildJSBridgeWithServerURL:(NSString *)serverURL; + +/// 注入可视化 bridge,并设置扫码模式 +/// @param isVisualizedMode 是否为可视化扫码模式 ++ (nullable NSString *)buildVisualBridgeWithVisualizedMode:(BOOL)isVisualizedMode; + +/// 注入自定义属性 bridge,配置信息 +/// @param originalConfig 配置信息原始 json ++ (nullable NSString *)buildVisualPropertyBridgeWithVisualConfig:(NSDictionary *)originalConfig; + +#pragma mark JS 调用 + +/// js 方法调用 +/// @param type 调用类型 +/// @param object 传参 ++ (nullable NSString *)buildCallJSMethodStringWithType:(SAJavaScriptCallJSType)type jsonObject:(nullable id)object; + +@end + NS_ASSUME_NONNULL_END diff --git a/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.m b/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.m index f1317064..ab353842 100644 --- a/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.m +++ b/SensorsAnalyticsSDK/Core/JSBridge/SAJavaScriptBridgeManager.m @@ -59,13 +59,11 @@ - (NSString *)javaScriptSource { return nil; } if (self.configOptions.serverURL) { - NSString *initSource = @"window.SensorsData_iOS_JS_Bridge = {};"; - NSString *setServerURLSource = [NSString stringWithFormat:@"window.SensorsData_iOS_JS_Bridge.sensorsdata_app_server_url = '%@';", self.configOptions.serverURL]; - return [NSString stringWithFormat:@"%@%@", initSource, setServerURLSource]; - } else { - SALogError(@"%@ get network serverURL is failed!", self); - return nil; + return [SAJavaScriptBridgeBuilder buildJSBridgeWithServerURL:self.configOptions.serverURL]; } + + SALogError(@"%@ get network serverURL is failed!", self); + return nil; } #pragma mark - Private @@ -123,7 +121,7 @@ - (void)addScriptMessageHandlerWithWebView:(WKWebView *)webView { NSArray *userScripts = contentController.userScripts; __block BOOL isContainJavaScriptBridge = NO; [userScripts enumerateObjectsUsingBlock:^(WKUserScript *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - if ([obj.source containsString:@"sensorsdata_app_server_url"] || [obj.source containsString:@"sensorsdata_visualized_mode"]) { + if ([obj.source containsString:kSAJSBridgeServerURL] || [obj.source containsString:kSAJSBridgeVisualizedMode]) { isContainJavaScriptBridge = YES; *stop = YES; } @@ -135,7 +133,7 @@ - (void)addScriptMessageHandlerWithWebView:(WKWebView *)webView { [contentController addUserScript:userScript]; // 通知其他模块,开启打通 H5 - if ([javaScriptSource containsString:@"sensorsdata_app_server_url"]) { + if ([javaScriptSource containsString:kSAJSBridgeServerURL]) { [[NSNotificationCenter defaultCenter] postNotificationName:SA_H5_BRIDGE_NOTIFICATION object:webView]; } } @@ -151,12 +149,12 @@ - (void)userContentController:(WKUserContentController *)userContentController d if (![message.name isEqualToString:SA_SCRIPT_MESSAGE_HANDLER_NAME]) { return; } - + if (![message.body isKindOfClass:[NSString class]]) { SALogError(@"Message body is not kind of 'NSString' from JS SDK"); return; } - + @try { NSString *body = message.body; NSData *messageData = [body dataUsingEncoding:NSUTF8StringEncoding]; @@ -164,13 +162,13 @@ - (void)userContentController:(WKUserContentController *)userContentController d SALogError(@"Message body is invalid from JS SDK"); return; } - + NSDictionary *messageDic = [SAJSONUtil JSONObjectWithData:messageData]; if (![messageDic isKindOfClass:[NSDictionary class]]) { SALogError(@"Message body is formatted failure from JS SDK"); return; } - + NSString *callType = messageDic[@"callType"]; if ([callType isEqualToString:@"app_h5_track"]) { // H5 发送事件 @@ -179,14 +177,14 @@ - (void)userContentController:(WKUserContentController *)userContentController d SALogError(@"Data of message body is not kind of 'NSDictionary' from JS SDK"); return; } - + NSString *trackMessageString = [SAJSONUtil stringWithJSONObject:trackMessageDic]; [[SensorsAnalyticsSDK sharedInstance] trackFromH5WithEvent:trackMessageString]; } else if ([callType isEqualToString:@"visualized_track"] || [callType isEqualToString:@"app_alert"] || [callType isEqualToString:@"page_info"]) { /* 缓存 H5 页面信息 visualized_track:H5 可点击元素数据,数组; app_alert:H5 弹框信息,提示配置错误信息; - page_info:H5 页面信息,包括 url 和 title + page_info:H5 页面信息,包括 url、title 和 lib_version */ [[NSNotificationCenter defaultCenter] postNotificationName:SA_VISUALIZED_H5_MESSAGE_NOTIFICATION object:message]; } else if ([callType isEqualToString:@"abtest"]) { @@ -199,3 +197,99 @@ - (void)userContentController:(WKUserContentController *)userContentController d } @end + +/// 打通 Bridge +NSString * const kSAJSBridgeObject = @"window.SensorsData_iOS_JS_Bridge = {};"; + +/// 打通设置 serverURL +NSString * const kSAJSBridgeServerURL = @"window.SensorsData_iOS_JS_Bridge.sensorsdata_app_server_url"; + +/// 可视化 Bridge +NSString * const kSAVisualBridgeObject = @"window.SensorsData_App_Visual_Bridge = {};"; + +/// 标识扫码进入可视化模式 +NSString * const kSAJSBridgeVisualizedMode = @"window.SensorsData_App_Visual_Bridge.sensorsdata_visualized_mode"; + +/// 自定义属性 Bridge +NSString * const kSAVisualPropertyBridge = @"window.SensorsData_APP_New_H5_Bridge = {};"; + +/// 写入自定义属性配置 +NSString * const kSAJSBridgeVisualConfig = @"window.SensorsData_APP_New_H5_Bridge.sensorsdata_get_app_visual_config"; + +/// js 方法调用 +NSString * const kSAJSBridgeCallMethod = @"window.sensorsdata_app_call_js"; + +@implementation SAJavaScriptBridgeBuilder + +#pragma mark 注入 js + +/// 注入打通bridge,并设置 serverURL +/// @param serverURL 数据接收地址 ++ (nullable NSString *)buildJSBridgeWithServerURL:(NSString *)serverURL { + if (serverURL.length == 0) { + return nil; + } + return [NSString stringWithFormat:@"%@%@ = '%@';", kSAJSBridgeObject, kSAJSBridgeServerURL, serverURL]; +} + +/// 注入可视化 bridge,并设置扫码模式 +/// @param isVisualizedMode 是否为可视化扫码模式 ++ (nullable NSString *)buildVisualBridgeWithVisualizedMode:(BOOL)isVisualizedMode { + return [NSString stringWithFormat:@"%@%@ = %@;", kSAVisualBridgeObject, kSAJSBridgeVisualizedMode, isVisualizedMode ? @"true" : @"false"]; +} + +/// 注入自定义属性 bridge,配置信息 +/// @param originalConfig 配置信息原始 json ++ (nullable NSString *)buildVisualPropertyBridgeWithVisualConfig:(NSDictionary *)originalConfig { + if (originalConfig.count == 0) { + return nil; + } + NSMutableString *javaScriptSource = [NSMutableString stringWithString:kSAVisualPropertyBridge]; + [javaScriptSource appendString:kSAJSBridgeVisualConfig]; + + // 注入完整配置信息 + NSData *callJSData = [SAJSONUtil dataWithJSONObject:originalConfig]; + // base64 编码,避免转义字符丢失的问题 + NSString *callJSJsonString = [callJSData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]; + [javaScriptSource appendFormat:@" = '%@';", callJSJsonString]; + return [javaScriptSource copy]; +} + +#pragma mark JS 方法调用 ++ (nullable NSString *)buildCallJSMethodStringWithType:(SAJavaScriptCallJSType)type jsonObject:(nullable id)object { + NSString *typeString = [self callJSTypeStringWithType:type]; + if (!typeString) { + return nil; + } + NSMutableString *javaScriptSource = [NSMutableString stringWithString:kSAJSBridgeCallMethod]; + if (!object) { + [javaScriptSource appendFormat:@"('%@')", typeString]; + return [javaScriptSource copy]; + } + + NSData *callJSData = [SAJSONUtil dataWithJSONObject:object]; + if (!callJSData) { + return nil; + } + // base64 编码,避免转义字符丢失的问题 + NSString *callJSJsonString = [callJSData base64EncodedStringWithOptions:NSDataBase64EncodingEndLineWithCarriageReturn]; + [javaScriptSource appendFormat:@"('%@', '%@')", typeString, callJSJsonString]; + return [javaScriptSource copy]; +} + ++ (nullable NSString *)callJSTypeStringWithType:(SAJavaScriptCallJSType)type { + switch (type) { + case SAJavaScriptCallJSTypeVisualized: + return @"visualized"; + case SAJavaScriptCallJSTypeCheckJSSDK: + return @"sensorsdata-check-jssdk"; + case SAJavaScriptCallJSTypeUpdateVisualConfig: + return @"updateH5VisualConfig"; + case SAJavaScriptCallJSTypeWebVisualProperties: + return @"getJSVisualProperties"; + default: + return nil; + } +} + +@end diff --git a/SensorsAnalyticsSDK/Core/SAConstants+Private.h b/SensorsAnalyticsSDK/Core/SAConstants+Private.h index 4f3e4ec2..b36ec016 100644 --- a/SensorsAnalyticsSDK/Core/SAConstants+Private.h +++ b/SensorsAnalyticsSDK/Core/SAConstants+Private.h @@ -54,6 +54,8 @@ extern NSString * const kSAEventNameAppEnd; extern NSString * const kSAEventNameAppViewScreen; // App 元素点击 extern NSString * const kSAEventNameAppClick; +/// Web 元素点击 +extern NSString * const kSAEventNameWebClick; // 自动追踪相关事件及属性 extern NSString * const kSAEventNameAppStartPassively; diff --git a/SensorsAnalyticsSDK/Core/SAConstants.m b/SensorsAnalyticsSDK/Core/SAConstants.m index 87851a04..1d2bbcb0 100644 --- a/SensorsAnalyticsSDK/Core/SAConstants.m +++ b/SensorsAnalyticsSDK/Core/SAConstants.m @@ -57,6 +57,9 @@ NSString * const kSAEventNameAppViewScreen = @"$AppViewScreen"; // App 元素点击 NSString * const kSAEventNameAppClick = @"$AppClick"; +// web 元素点击 +NSString * const kSAEventNameWebClick = @"$WebClick"; + // 自动追踪相关事件及属性 NSString * const kSAEventNameAppStartPassively = @"$AppStartPassively"; diff --git a/SensorsAnalyticsSDK/Core/SAModuleManager.h b/SensorsAnalyticsSDK/Core/SAModuleManager.h index b35e3332..1882dacb 100644 --- a/SensorsAnalyticsSDK/Core/SAModuleManager.h +++ b/SensorsAnalyticsSDK/Core/SAModuleManager.h @@ -106,6 +106,12 @@ typedef NS_ENUM(NSUInteger, SAModuleType) { #pragma mark - +@interface SAModuleManager (Visualized) + +@end + +#pragma mark - + @interface SAModuleManager (JavaScriptBridge) @end diff --git a/SensorsAnalyticsSDK/Core/SAModuleManager.m b/SensorsAnalyticsSDK/Core/SAModuleManager.m index 57aad356..fde27f6a 100644 --- a/SensorsAnalyticsSDK/Core/SAModuleManager.m +++ b/SensorsAnalyticsSDK/Core/SAModuleManager.m @@ -413,6 +413,38 @@ - (void)trackPageLeaveWhenCrashed { #pragma mark - +@implementation SAModuleManager (Visualized) + +#pragma mark properties +// 采集元素属性 +- (nullable NSDictionary *)propertiesWithView:(id)view { + id manager = (id)[SAModuleManager.sharedInstance managerForModuleType:SAModuleTypeVisualized]; + return [manager propertiesWithView:view]; +} + +#pragma mark visualProperties +// 采集元素自定义属性 +- (void)visualPropertiesWithView:(id)view completionHandler:(void (^)(NSDictionary *_Nullable))completionHandler { + id manager = (id)[SAModuleManager.sharedInstance managerForModuleType:SAModuleTypeVisualized]; + if (!manager) { + return completionHandler(nil); + } + [manager visualPropertiesWithView:view completionHandler:completionHandler]; +} + +// 根据属性配置,采集 App 属性值 +- (void)queryVisualPropertiesWithConfigs:(NSArray *)propertyConfigs completionHandler:(void (^)(NSDictionary *_Nullable properties))completionHandler { + id manager = (id)[SAModuleManager.sharedInstance managerForModuleType:SAModuleTypeVisualized]; + if (!manager) { + return completionHandler(nil); + } + [manager queryVisualPropertiesWithConfigs:propertyConfigs completionHandler:completionHandler]; +} + +@end + +#pragma mark - + @implementation SAModuleManager (JavaScriptBridge) - (NSString *)javaScriptSource { diff --git a/SensorsAnalyticsSDK/Core/SAModuleProtocol.h b/SensorsAnalyticsSDK/Core/SAModuleProtocol.h index f3216a4d..576cb553 100644 --- a/SensorsAnalyticsSDK/Core/SAModuleProtocol.h +++ b/SensorsAnalyticsSDK/Core/SAModuleProtocol.h @@ -164,12 +164,31 @@ NS_ASSUME_NONNULL_BEGIN @end +@protocol SAVisualizedModuleProtocol + +/// 元素相关属性 +/// @param view 需要采集的 view +- (nullable NSDictionary *)propertiesWithView:(id)view; + +#pragma mark visualProperties + +/// 采集元素自定义属性 +/// @param view 触发事件的元素 +/// @param completionHandler 采集完成回调 +- (void)visualPropertiesWithView:(id)view completionHandler:(void (^)(NSDictionary *_Nullable visualProperties))completionHandler; + +/// 根据配置,采集属性 +/// @param propertyConfigs 自定义属性配置 +/// @param completionHandler 采集完成回调 +- (void)queryVisualPropertiesWithConfigs:(NSArray *)propertyConfigs completionHandler:(void (^)(NSDictionary *_Nullable properties))completionHandler; + +@end + #pragma mark - @protocol SAJavaScriptBridgeModuleProtocol - (nullable NSString *)javaScriptSource; - @end @protocol SARemoteConfigModuleProtocol diff --git a/SensorsAnalyticsSDK/Core/SensorsAnalyticsSDK.m b/SensorsAnalyticsSDK/Core/SensorsAnalyticsSDK.m index 62fdf8d8..2910c481 100755 --- a/SensorsAnalyticsSDK/Core/SensorsAnalyticsSDK.m +++ b/SensorsAnalyticsSDK/Core/SensorsAnalyticsSDK.m @@ -41,7 +41,7 @@ #import "SAProfileEventObject.h" #import "SAJSONUtil.h" -#define VERSION @"3.1.7" +#define VERSION @"3.1.8" void *SensorsAnalyticsQueueTag = &SensorsAnalyticsQueueTag; @@ -1100,153 +1100,188 @@ - (void)trackFromH5WithEvent:(NSString *)eventInfo { } - (void)trackFromH5WithEvent:(NSString *)eventInfo enableVerify:(BOOL)enableVerify { - __block NSNumber *timeStamp = @([[self class] getCurrentTime]); - + if (!eventInfo) { + return; + } + NSMutableDictionary *eventDict = [SAJSONUtil JSONObjectWithString:eventInfo options:NSJSONReadingMutableContainers]; + if (!eventDict) { + return; + } + + if (enableVerify) { + NSString *serverUrl = eventDict[@"server_url"]; + if (![self.network isSameProjectWithURLString:serverUrl]) { + SALogError(@"Server_url verified faild, Web event lost! Web server_url = '%@'", serverUrl); + return; + } + } + dispatch_async(self.serialQueue, ^{ - @try { - if (!eventInfo) { - return; - } - - NSMutableDictionary *eventDict = [SAJSONUtil JSONObjectWithString:eventInfo options:NSJSONReadingMutableContainers]; - if(!eventDict) { - return; - } + NSString *type = eventDict[kSAEventType]; + NSMutableDictionary *propertiesDict = eventDict[kSAEventProperties]; - if (enableVerify) { - NSString *serverUrl = eventDict[@"server_url"]; - if (![self.network isSameProjectWithURLString:serverUrl]) { - SALogError(@"Server_url verified faild, Web event lost! Web server_url = '%@'",serverUrl); - return; - } - } + if ([type isEqualToString:kSAEventTypeSignup]) { + eventDict[@"original_id"] = self.anonymousId; + } else { + eventDict[kSAEventDistinctId] = self.distinctId; + } + eventDict[kSAEventTrackId] = @(arc4random()); + + NSMutableDictionary *libMDic = eventDict[kSAEventLib]; + //update lib $app_version from super properties + NSDictionary *superProperties = [self.superProperty currentSuperProperties]; + id appVersion = superProperties[kSAEventPresetPropertyAppVersion] ? : self.presetProperty.appVersion; + if (appVersion) { + libMDic[kSAEventPresetPropertyAppVersion] = appVersion; + } - NSString *type = eventDict[kSAEventType]; - NSString *bestId = self.distinctId; + NSMutableDictionary *automaticPropertiesCopy = [NSMutableDictionary dictionaryWithDictionary:self.presetProperty.automaticProperties]; + [automaticPropertiesCopy removeObjectForKey:kSAEventPresetPropertyLib]; + [automaticPropertiesCopy removeObjectForKey:kSAEventPresetPropertyLibVersion]; - if([type isEqualToString:kSAEventTypeSignup]) { - eventDict[@"original_id"] = self.anonymousId; - } else { - eventDict[kSAEventDistinctId] = bestId; - } - eventDict[kSAEventTrackId] = @(arc4random()); - - NSMutableDictionary *libMDic = eventDict[kSAEventLib]; - //update lib $app_version from super properties - NSDictionary *superProperties = [self.superProperty currentSuperProperties]; - id appVersion = superProperties[kSAEventPresetPropertyAppVersion] ?: self.presetProperty.appVersion; - if (appVersion) { - libMDic[kSAEventPresetPropertyAppVersion] = appVersion; - } + if ([type isEqualToString:kSAEventTypeTrack] || [type isEqualToString:kSAEventTypeSignup]) { + // track / track_signup 类型的请求,还是要加上各种公共property + // 这里注意下顺序,按照优先级从低到高,依次是automaticProperties, superProperties,dynamicSuperPropertiesDict,propertieDict + [propertiesDict addEntriesFromDictionary:automaticPropertiesCopy]; - NSMutableDictionary *automaticPropertiesCopy = [NSMutableDictionary dictionaryWithDictionary:self.presetProperty.automaticProperties]; - [automaticPropertiesCopy removeObjectForKey:kSAEventPresetPropertyLib]; - [automaticPropertiesCopy removeObjectForKey:kSAEventPresetPropertyLibVersion]; - - // 校验 properties - NSError *validError = nil; - NSMutableDictionary *propertiesDict = [SAPropertyValidator validProperties:eventDict[kSAEventProperties] error:&validError]; - if (validError) { - SALogError(@"%@", validError.localizedDescription); - SALogError(@"%@ failed to track event from H5.", self); - [SAModuleManager.sharedInstance showDebugModeWarning:validError.localizedDescription]; - return; - } + NSDictionary *dynamicSuperPropertiesDict = [self.superProperty acquireDynamicSuperProperties]; + [propertiesDict addEntriesFromDictionary:self.superProperty.currentSuperProperties]; + [propertiesDict addEntriesFromDictionary:dynamicSuperPropertiesDict]; - if([type isEqualToString:kSAEventTypeTrack] || [type isEqualToString:kSAEventTypeSignup]) { + // 每次 track 时手机网络状态 + [propertiesDict addEntriesFromDictionary:[self.presetProperty currentNetworkProperties]]; + } - // track / track_signup 类型的请求,还是要加上各种公共property - // 这里注意下顺序,按照优先级从低到高,依次是automaticProperties, superProperties,dynamicSuperPropertiesDict,propertieDict - [propertiesDict addEntriesFromDictionary:automaticPropertiesCopy]; + NSString *visualProperties = eventDict[kSAEventProperties][@"sensorsdata_app_visual_properties"]; + // 是否包含自定义属性配置 + if (!visualProperties || ![eventDict[kSAEventName] isEqualToString:kSAEventNameWebClick]) { + eventDict[kSAEventProperties] = propertiesDict; + [self trackFromH5WithEventDict:eventDict]; + return; + } - NSDictionary *dynamicSuperPropertiesDict = [self.superProperty acquireDynamicSuperProperties]; - [propertiesDict addEntriesFromDictionary:self.superProperty.currentSuperProperties]; - [propertiesDict addEntriesFromDictionary:dynamicSuperPropertiesDict]; + NSData *data = [[NSData alloc] initWithBase64EncodedString:visualProperties options:NSDataBase64DecodingIgnoreUnknownCharacters]; + NSArray *visualPropertyConfigs = [SAJSONUtil JSONObjectWithData:data]; - // 每次 track 时手机网络状态 - [propertiesDict addEntriesFromDictionary:[self.presetProperty currentNetworkProperties]]; + // 查询 App 自定义属性值 + NSDate *currentTime = [NSDate date]; + [SAModuleManager.sharedInstance queryVisualPropertiesWithConfigs:visualPropertyConfigs completionHandler:^(NSDictionary *_Nullable properties) { - // 是否首日访问 - if([type isEqualToString:kSAEventTypeTrack]) { - propertiesDict[kSAEventPresetPropertyIsFirstDay] = @([self.presetProperty isFirstDay]); + // 切换到 serialQueue 执行 + dispatch_async(self.serialQueue, ^{ + if (properties.count > 0) { + [propertiesDict addEntriesFromDictionary:properties]; } - [propertiesDict removeObjectForKey:@"_nocache"]; - // 添加 DeepLink 来源渠道参数。优先级最高,覆盖 H5 传过来的同名字段 - [propertiesDict addEntriesFromDictionary:SAModuleManager.sharedInstance.latestUtmProperties]; + // 设置 $time,自定义时间,防止事件序列错误 + if (!propertiesDict[kSAEventCommonOptionalPropertyTime]) { + propertiesDict[kSAEventCommonOptionalPropertyTime] = currentTime; + } + propertiesDict[@"sensorsdata_app_visual_properties"] = nil; + eventDict[kSAEventProperties] = propertiesDict; + [self trackFromH5WithEventDict:eventDict]; + }); + }]; + }); +} +- (void)trackFromH5WithEventDict:(NSMutableDictionary *)eventDict { + NSNumber *timeStamp = @([[self class] getCurrentTime]); + NSString *type = eventDict[kSAEventType]; + @try { + // 校验 properties + NSError *validError = nil; + NSMutableDictionary *propertiesDict = [SAPropertyValidator validProperties:eventDict[kSAEventProperties] error:&validError]; + if (validError) { + SALogError(@"%@", validError.localizedDescription); + SALogError(@"%@ failed to track event from H5.", self); + [SAModuleManager.sharedInstance showDebugModeWarning:validError.localizedDescription]; + return; + } + + [eventDict removeObjectForKey:@"_nocache"]; + [eventDict removeObjectForKey:@"server_url"]; + + if (([type isEqualToString:kSAEventTypeTrack] || [type isEqualToString:kSAEventTypeSignup])) { + // 是否首日访问 + if ([type isEqualToString:kSAEventTypeTrack]) { + propertiesDict[kSAEventPresetPropertyIsFirstDay] = @([self.presetProperty isFirstDay]); } + [propertiesDict removeObjectForKey:@"_nocache"]; - [eventDict removeObjectForKey:@"_nocache"]; - [eventDict removeObjectForKey:@"server_url"]; + // 添加 DeepLink 来源渠道参数。优先级最高,覆盖 H5 传过来的同名字段 + [propertiesDict addEntriesFromDictionary:SAModuleManager.sharedInstance.latestUtmProperties]; + } - // $project & $token - NSString *project = propertiesDict[kSAEventCommonOptionalPropertyProject]; - NSString *token = propertiesDict[kSAEventCommonOptionalPropertyToken]; - id timeNumber = propertiesDict[kSAEventCommonOptionalPropertyTime]; + // $project & $token + NSString *project = propertiesDict[kSAEventCommonOptionalPropertyProject]; + NSString *token = propertiesDict[kSAEventCommonOptionalPropertyToken]; + id timeNumber = propertiesDict[kSAEventCommonOptionalPropertyTime]; - if (project) { - [propertiesDict removeObjectForKey:kSAEventCommonOptionalPropertyProject]; - eventDict[kSAEventProject] = project; - } - if (token) { - [propertiesDict removeObjectForKey:kSAEventCommonOptionalPropertyToken]; - eventDict[kSAEventToken] = token; + if (project) { + [propertiesDict removeObjectForKey:kSAEventCommonOptionalPropertyProject]; + eventDict[kSAEventProject] = project; + } + if (token) { + [propertiesDict removeObjectForKey:kSAEventCommonOptionalPropertyToken]; + eventDict[kSAEventToken] = token; + } + if (timeNumber) { //包含 $time + NSNumber *customTime = nil; + if ([timeNumber isKindOfClass:[NSDate class]]) { + customTime = @([(NSDate *)timeNumber timeIntervalSince1970] * 1000); + } else if ([timeNumber isKindOfClass:[NSNumber class]]) { + customTime = timeNumber; } - if (timeNumber) { //包含 $time - NSNumber *customTime = nil; - if ([timeNumber isKindOfClass:[NSDate class]]) { - customTime = @([(NSDate *)timeNumber timeIntervalSince1970] * 1000); - } else if ([timeNumber isKindOfClass:[NSNumber class]]) { - customTime = timeNumber; - } - if (!customTime) { - SALogError(@"H5 $time '%@' invalid,Please check the value", timeNumber); - } else if ([customTime compare:@(kSAEventCommonOptionalPropertyTimeInt)] == NSOrderedAscending) { - SALogError(@"H5 $time error %@,Please check the value", timeNumber); - } else { - timeStamp = @([customTime unsignedLongLongValue]); - } - [propertiesDict removeObjectForKey:kSAEventCommonOptionalPropertyTime]; + if (!customTime) { + SALogError(@"H5 $time '%@' invalid,Please check the value", timeNumber); + } else if ([customTime compare:@(kSAEventCommonOptionalPropertyTimeInt)] == NSOrderedAscending) { + SALogError(@"H5 $time error %@,Please check the value", timeNumber); + } else { + timeStamp = @([customTime unsignedLongLongValue]); } + [propertiesDict removeObjectForKey:kSAEventCommonOptionalPropertyTime]; + } - eventDict[kSAEventProperties] = propertiesDict; - eventDict[kSAEventTime] = timeStamp; + eventDict[kSAEventProperties] = propertiesDict; + eventDict[kSAEventTime] = timeStamp; - //JS SDK Data add _hybrid_h5 flag - eventDict[kSAEventHybridH5] = @(YES); + //JS SDK Data add _hybrid_h5 flag + eventDict[kSAEventHybridH5] = @(YES); - NSMutableDictionary *enqueueEvent = [[self willEnqueueWithType:type andEvent:eventDict] mutableCopy]; + NSMutableDictionary *enqueueEvent = [[self willEnqueueWithType:type andEvent:eventDict] mutableCopy]; - if (!enqueueEvent) { - return; - } - // 只有当本地 loginId 不为空时才覆盖 H5 数据 - if (self.loginId) { - enqueueEvent[kSAEventLoginId] = self.loginId; - } - enqueueEvent[kSAEventAnonymousId] = self.anonymousId; - - if([type isEqualToString:kSAEventTypeSignup]) { - NSString *newLoginId = eventDict[kSAEventDistinctId]; - if ([self.identifier isValidLoginId:newLoginId]) { - [self.identifier login:newLoginId]; - enqueueEvent[kSAEventLoginId] = newLoginId; - [[NSNotificationCenter defaultCenter] postNotificationName:SA_TRACK_EVENT_H5_NOTIFICATION object:nil userInfo:[enqueueEvent copy]]; - [self.eventTracker trackEvent:enqueueEvent isSignUp:YES]; - SALogDebug(@"\n【track event from H5】:\n%@", enqueueEvent); - [[NSNotificationCenter defaultCenter] postNotificationName:SA_TRACK_LOGIN_NOTIFICATION object:nil]; - } - } else { + if (!enqueueEvent) { + return; + } + // 只有当本地 loginId 不为空时才覆盖 H5 数据 + if (self.loginId) { + enqueueEvent[kSAEventLoginId] = self.loginId; + } + enqueueEvent[kSAEventAnonymousId] = self.anonymousId; + + if ([type isEqualToString:kSAEventTypeSignup]) { + NSString *newLoginId = eventDict[kSAEventDistinctId]; + if ([self.identifier isValidLoginId:newLoginId]) { + [self.identifier login:newLoginId]; + enqueueEvent[kSAEventLoginId] = newLoginId; [[NSNotificationCenter defaultCenter] postNotificationName:SA_TRACK_EVENT_H5_NOTIFICATION object:nil userInfo:[enqueueEvent copy]]; - [self.eventTracker trackEvent:enqueueEvent]; + [self.eventTracker trackEvent:enqueueEvent isSignUp:YES]; SALogDebug(@"\n【track event from H5】:\n%@", enqueueEvent); + [[NSNotificationCenter defaultCenter] postNotificationName:SA_TRACK_LOGIN_NOTIFICATION object:nil]; } - } @catch (NSException *exception) { - SALogError(@"%@: %@", self, exception); + } else { + [[NSNotificationCenter defaultCenter] postNotificationName:SA_TRACK_EVENT_H5_NOTIFICATION object:nil userInfo:[enqueueEvent copy]]; + + eventDict[kSAEventProperties][@"sensorsdata_web_visual_eventName"] = nil; + [self.eventTracker trackEvent:enqueueEvent]; + SALogDebug(@"\n【track event from H5】:\n%@", enqueueEvent); } - }); + } @catch (NSException *exception) { + SALogError(@"%@: %@", self, exception); + } } + @end diff --git a/SensorsAnalyticsSDK/SensorsAnalyticsSDK.bundle/sa_visualized_path.json b/SensorsAnalyticsSDK/SensorsAnalyticsSDK.bundle/sa_visualized_path.json index b374072d..ef127f19 100644 --- a/SensorsAnalyticsSDK/SensorsAnalyticsSDK.bundle/sa_visualized_path.json +++ b/SensorsAnalyticsSDK/SensorsAnalyticsSDK.bundle/sa_visualized_path.json @@ -112,7 +112,7 @@ ] }, { - "name": "SAJSTouchEventView", + "name": "SAWebElementView", "superclass": "UIView", "properties": [ { @@ -125,10 +125,14 @@ "type": "NSString" }, { - "name": "sensorsdata_elementSelector", - "key": "element_selector", - "type": "NSString", - "use_kvc": false + "name": "tagName", + "key": "tag_name", + "type": "NSString" + }, + { + "name": "listSelector", + "key": "list_selector", + "type": "NSString" } ] }, diff --git a/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.h b/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.h index 490d1e6b..71f91065 100644 --- a/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.h +++ b/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.h @@ -77,6 +77,9 @@ NS_ASSUME_NONNULL_BEGIN /// 是否限制元素内容 @property (nonatomic, assign, getter=isLimitContent) BOOL limitContent; +/// 是否为 H5 事件 +@property (nonatomic, assign, getter=isH5) BOOL h5; + /// 当前事件配置,是否命中元素 - (BOOL)isMatchVisualEventWithViewIdentify:(SAViewIdentifier *)viewIdentify; @end @@ -95,6 +98,12 @@ NS_ASSUME_NONNULL_BEGIN /// 是否限制元素位置 @property (nonatomic, assign, getter=isLimitPosition) BOOL limitPosition; +/// 是否为 H5 属性 +@property (nonatomic, assign, getter=isH5) BOOL h5; + +/// webview 的元素路径,App 内嵌 H5 属性配置才包含 +@property (nonatomic, copy) NSString *webViewElementPath; + /* 本地扩展,用于元素匹配 */ /// 点击事件所在元素位置,点击元素传值 @property (nonatomic, copy) NSString *clickElementPosition; @@ -120,6 +129,8 @@ NS_ASSUME_NONNULL_BEGIN /// 属性配置 @property (nonatomic, strong) NSArray *properties; +/// web 属性配置,原始配置 json +@property (nonatomic, strong) NSArray *webProperties; @end diff --git a/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.m b/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.m index f89bdf5e..65a6b3fa 100644 --- a/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.m +++ b/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfig.m @@ -101,6 +101,7 @@ - (instancetype)initWithDictionary:(NSDictionary *)eventDic { if (self) { _limitPosition = [dictionaryValueForKey(eventDic, @"limit_element_position") boolValue]; _limitContent = [dictionaryValueForKey(eventDic, @"limit_element_content") boolValue]; + _h5 = [dictionaryValueForKey(eventDic, @"h5") boolValue]; } return self; } @@ -126,6 +127,7 @@ - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; [coder encodeBool:self.limitPosition forKey:@"limitPosition"]; [coder encodeBool:self.limitContent forKey:@"limitContent"]; + [coder encodeBool:self.isH5 forKey:@"h5"]; } - (instancetype)initWithCoder:(NSCoder *)coder { @@ -133,6 +135,7 @@ - (instancetype)initWithCoder:(NSCoder *)coder { if (self) { self.limitPosition = [coder decodeBoolForKey:@"limitPosition"]; self.limitContent = [coder decodeBoolForKey:@"limitContent"]; + self.h5 = [coder decodeBoolForKey:@"h5"]; } return self; } @@ -153,13 +156,24 @@ - (instancetype)initWithDictionary:(NSDictionary *)propertiesDic { } else { _type = SAVisualPropertyTypeString; } + + _h5 = [dictionaryValueForKey(propertiesDic, @"h5") boolValue]; + // h5 属性对应的 webview 路径 + _webViewElementPath = dictionaryValueForKey(propertiesDic, @"webview_element_path"); } return self; } /// 当前属性配置,是否命中元素 - (BOOL)isMatchVisualPropertiesWithViewIdentify:(SAViewIdentifier *)viewIdentify { - if (![self isEqualToViewIdentify:viewIdentify]) { + BOOL isEqualToIdentify = NO; + // H5 属性配置,只用于查询 weView,单独判断 + if (self.isH5) { + isEqualToIdentify = [self isEqualToWebViewIdentify:viewIdentify]; + } else { + isEqualToIdentify = [self isEqualToViewIdentify:viewIdentify]; + } + if (!isEqualToIdentify) { return NO; } @@ -168,6 +182,10 @@ - (BOOL)isMatchVisualPropertiesWithViewIdentify:(SAViewIdentifier *)viewIdentify if (enableMatchPageIndex && self.pageIndex != viewIdentify.pageIndex) { return NO; } + // H5 配置,只用于查询 weView,不能比较 App 元素位置 + if (self.isH5) { + return YES; + } /* 属性元素,位置匹配场景 1. 属性元素为列表,事件元素为列表 @@ -192,6 +210,17 @@ - (BOOL)isMatchVisualPropertiesWithViewIdentify:(SAViewIdentifier *)viewIdentify return [self.elementPosition isEqualToString:viewIdentify.elementPosition]; } +/// h5 属性配置,匹配元素 +- (BOOL)isEqualToWebViewIdentify:(SAViewIdentifier *)object { + if (!self.isH5) { + return NO; + } + + BOOL sameElementPath = [self.webViewElementPath isEqualToString:object.elementPath]; + BOOL sameScreenName = [self.screenName isEqualToString:object.screenName]; + return sameElementPath && sameScreenName; +} + #pragma mark NSCoding - (void)encodeWithCoder:(NSCoder *)coder { [super encodeWithCoder:coder]; @@ -199,6 +228,8 @@ - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:self.regular forKey:@"regular"]; [coder encodeBool:self.limitPosition forKey:@"limitPosition"]; [coder encodeInteger:self.type forKey:@"type"]; + [coder encodeBool:self.isH5 forKey:@"h5"]; + [coder encodeObject:self.webViewElementPath forKey:@"webViewElementPath"]; } - (instancetype)initWithCoder:(NSCoder *)coder { @@ -208,6 +239,8 @@ - (instancetype)initWithCoder:(NSCoder *)coder { self.regular = [coder decodeObjectForKey:@"regular"]; self.limitPosition = [coder decodeBoolForKey:@"limitPosition"]; self.type = [coder decodeIntegerForKey:@"type"]; + self.h5 = [coder decodeBoolForKey:@"h5"]; + self.webViewElementPath = [coder decodeObjectForKey:@"webViewElementPath"]; } return self; } @@ -232,15 +265,22 @@ - (instancetype)initWithDictionary:(NSDictionary *)eventsDic { NSArray *propertiesArray = dictionaryValueForKey(eventsDic, @"properties"); if (propertiesArray) { + NSMutableArray *properties = [NSMutableArray array]; + NSMutableArray *webProperties = [NSMutableArray array]; for (NSDictionary *dic in propertiesArray) { SAVisualPropertiesPropertyConfig *config = [[SAVisualPropertiesPropertyConfig alloc] initWithDictionary:dic]; - - // 保存是否限定位置 - config.limitPosition = _event.limitPosition; - [properties addObject:config]; + // h5 配置不必解析,单独保存原始 json,直接发送给 js 即可 + if (config.isH5) { + [webProperties addObject:dic]; + } else { + // 保存是否限定位置 + config.limitPosition = _event.limitPosition; + [properties addObject:config]; + } } - _properties = [properties copy]; + _properties = properties.count > 0 ? [properties copy]: nil;; + _webProperties = webProperties.count > 0 ? [webProperties copy]: nil; } } return self; @@ -252,6 +292,7 @@ - (void)encodeWithCoder:(NSCoder *)coder { [coder encodeObject:self.event forKey:@"event"]; [coder encodeObject:self.properties forKey:@"properties"]; [coder encodeObject:self.eventName forKey:@"eventName"]; + [coder encodeObject:self.webProperties forKey:@"webProperties"]; } - (instancetype)initWithCoder:(NSCoder *)coder { @@ -261,6 +302,7 @@ - (instancetype)initWithCoder:(NSCoder *)coder { self.event = [coder decodeObjectForKey:@"event"]; self.properties = [coder decodeObjectForKey:@"properties"]; self.eventName = [coder decodeObjectForKey:@"eventName"]; + self.webProperties = [coder decodeObjectForKey:@"webProperties"]; } return self; } @@ -283,7 +325,11 @@ - (instancetype)initWithDictionary:(NSDictionary *)responseDic { NSMutableArray *eventsArray = [NSMutableArray array]; for (NSDictionary *eventDic in events) { SAVisualPropertiesConfig *event = [[SAVisualPropertiesConfig alloc] initWithDictionary:eventDic]; - [eventsArray addObject:event]; + + // H5 事件配置,不必解析 + if (!event.event.isH5) { + [eventsArray addObject:event]; + } } _events = [eventsArray copy]; } diff --git a/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfigSources.m b/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfigSources.m index 2734e5de..ab6351b6 100644 --- a/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfigSources.m +++ b/SensorsAnalyticsSDK/Visualized/Config/SAVisualPropertiesConfigSources.m @@ -95,7 +95,7 @@ - (void)updateConfigStatus { } - (BOOL)isValid { - return self.configResponse.events.count > 0; + return self.configResponse.originalResponse.count > 0; } - (NSString *)configVersion { @@ -288,7 +288,7 @@ - (void)cleanConfig { // 查询元素点击事件配置 for (SAVisualPropertiesConfig *config in configSources) { // 普通可视化全埋点事件,不包含自定义属性,直接跳过 - if (config.properties.count == 0 || !config.event) { + if ((config.properties.count == 0 && config.webProperties.count == 0) || !config.event) { continue; } // 命中配置信息 @@ -303,7 +303,10 @@ - (void)cleanConfig { - (nullable NSArray *)propertiesConfigsWithEventIdentifier:(SAEventIdentifier *)eventIdentifier { NSArray *configSources = self.configResponse.events; - if (configSources.count == 0 || !eventIdentifier || eventIdentifier.eventType != SensorsAnalyticsEventTypeAppClick) { + if (configSources.count == 0 || !eventIdentifier) { + return nil; + } + if (![eventIdentifier.eventName isEqualToString:kSAEventNameAppClick]) { return nil; } diff --git a/SensorsAnalyticsSDK/Visualized/ElementPath/SAVisualizedViewPathProperty.h b/SensorsAnalyticsSDK/Visualized/ElementPath/SAVisualizedViewPathProperty.h index e0302ab9..417b8a02 100644 --- a/SensorsAnalyticsSDK/Visualized/ElementPath/SAVisualizedViewPathProperty.h +++ b/SensorsAnalyticsSDK/Visualized/ElementPath/SAVisualizedViewPathProperty.h @@ -70,8 +70,8 @@ /// 是否为列表(本身支持限定位置,比如 Cell) @property (nonatomic, assign) BOOL sensorsdata_isListView; -@end +@end #pragma mark - Extension @protocol SAVisualizedExtensionProperty diff --git a/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.h b/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.h index 74839538..87d9208c 100644 --- a/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.h +++ b/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.h @@ -21,7 +21,7 @@ #import #import #import -#import "SAJSTouchEventView.h" +#import "SAWebElementView.h" #import "SAAutoTrackProperty.h" #import "SAVisualizedViewPathProperty.h" @@ -48,7 +48,7 @@ NS_ASSUME_NONNULL_BEGIN @interface UIWindow (SAElementPath) @end -@interface SAJSTouchEventView (SAElementPath) +@interface SAWebElementView (SAElementPath) @end #pragma mark - UIControl diff --git a/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.m b/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.m index e4afdfa3..97e020fe 100644 --- a/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.m +++ b/SensorsAnalyticsSDK/Visualized/ElementPath/UIView+SAElementPath.m @@ -143,10 +143,6 @@ - (BOOL)sensorsdata_isAutoTrackAppClick { return YES; } - // Native 全埋点被忽略元素,不可圈选,RN 全埋点事件由插件触发,不经过此判断 - if (self.sensorsdata_isIgnored) { - return NO; - } if ([self isKindOfClass:UIControl.class]) { // UISegmentedControl 高亮渲染内部嵌套的 UISegment if ([self isKindOfClass:UISegmentedControl.class]) { @@ -462,7 +458,12 @@ - (NSArray *)sensorsdata_subElements { @end -@implementation SAJSTouchEventView (SAElementPath) +@implementation SAWebElementView (SAElementPath) + +#pragma mark SAVisualizedViewPathProperty +- (NSString *)sensorsdata_title { + return self.title; +} - (NSString *)sensorsdata_elementSelector { return self.elementSelector; @@ -477,7 +478,7 @@ - (CGRect)sensorsdata_frame { } - (BOOL)sensorsdata_enableAppClick { - return YES; + return self.enableAppClick; } - (NSArray *)sensorsdata_subElements { @@ -491,6 +492,18 @@ - (BOOL)sensorsdata_isFromWeb { return YES; } +- (BOOL)sensorsdata_isListView { + return self.isListView; +} + +- (NSString *)sensorsdata_elementPath { + return self.elementPath; +} + +- (NSString *)sensorsdata_elementPosition { + return self.elementPosition; +} + @end #pragma mark - UIControl diff --git a/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.h b/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.h index 2f5e936d..42ba962f 100644 --- a/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.h +++ b/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.h @@ -26,8 +26,9 @@ NS_ASSUME_NONNULL_BEGIN /// 事件标识 @interface SAEventIdentifier : SAViewIdentifier -/// 事件类型,目前只支持 AppClick 事件 -@property (nonatomic, assign) SensorsAnalyticsAutoTrackEventType eventType; +/// 事件名 +@property (nonatomic, copy) NSString *eventName; +@property (nonatomic, strong) NSMutableDictionary *properties; - (instancetype)initWithEventInfo:(NSDictionary *)eventInfo; diff --git a/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.m b/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.m index d2966e87..b5237e38 100644 --- a/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.m +++ b/SensorsAnalyticsSDK/Visualized/EventCheck/SAEventIdentifier.m @@ -33,22 +33,18 @@ - (instancetype)initWithEventInfo:(NSDictionary *)eventInfo { NSDictionary *dic = [SAEventIdentifier eventIdentifierDicWithEventInfo:eventInfo]; self = [super initWithDictionary:dic]; if (self) { - NSString *eventName = eventInfo[@"event"]; - if ([eventName isEqualToString:kSAEventNameAppClick]) { - _eventType = SensorsAnalyticsEventTypeAppClick; - } else { - _eventType = SensorsAnalyticsEventTypeNone; - } + _eventName = eventInfo[@"event"]; + _properties = [eventInfo[kSAEventProperties] mutableCopy]; } return self; } + (NSDictionary *)eventIdentifierDicWithEventInfo:(NSDictionary *)eventInfo { NSMutableDictionary *eventInfoDic = [NSMutableDictionary dictionary]; - eventInfoDic[@"element_path"] = eventInfo[@"properties"][kSAEventPropertyElementPath]; - eventInfoDic[@"element_position"] = eventInfo[@"properties"][kSAEventPropertyElementPosition]; - eventInfoDic[@"element_content"] = eventInfo[@"properties"][kSAEventPropertyElementContent]; - eventInfoDic[@"screen_name"] = eventInfo[@"properties"][kSAEventPropertyScreenName]; + eventInfoDic[@"element_path"] = eventInfo[kSAEventProperties][kSAEventPropertyElementPath]; + eventInfoDic[@"element_position"] = eventInfo[kSAEventProperties][kSAEventPropertyElementPosition]; + eventInfoDic[@"element_content"] = eventInfo[kSAEventProperties][kSAEventPropertyElementContent]; + eventInfoDic[@"screen_name"] = eventInfo[kSAEventProperties][kSAEventPropertyScreenName]; return eventInfoDic; } @end diff --git a/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.h b/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.h index c0c58563..100740e9 100644 --- a/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.h +++ b/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.h @@ -23,6 +23,9 @@ NS_ASSUME_NONNULL_BEGIN +/// H5 可视化全埋点事件标记 +extern NSString * const kSAWebVisualEventName; + /// 可视化全埋点埋点校验 @interface SAVisualizedEventCheck : NSObject - (instancetype)initWithConfigSources:(SAVisualPropertiesConfigSources *)configSources; diff --git a/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.m b/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.m index de2bb336..da923d0e 100644 --- a/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.m +++ b/SensorsAnalyticsSDK/Visualized/EventCheck/SAVisualizedEventCheck.m @@ -27,6 +27,8 @@ #import "SAEventIdentifier.h" #import "SALog.h" +NSString * const kSAWebVisualEventName = @"sensorsdata_web_visual_eventName"; + @interface SAVisualizedEventCheck() @property (nonatomic, strong) SAVisualPropertiesConfigSources *configSources; @@ -51,18 +53,19 @@ - (instancetype)initWithConfigSources:(SAVisualPropertiesConfigSources *)configS - (void)setupListeners { NSNotificationCenter *notificationCenter = [NSNotificationCenter defaultCenter]; [notificationCenter addObserver:self selector:@selector(trackEvent:) name:SA_TRACK_EVENT_NOTIFICATION object:nil]; + [notificationCenter addObserver:self selector:@selector(trackEventFromH5:) name:SA_TRACK_EVENT_H5_NOTIFICATION object:nil]; } - (void)trackEvent:(NSNotification *)notification { - NSMutableDictionary *trackEventInfo = [notification.userInfo mutableCopy]; - if (trackEventInfo.count == 0) { + if (![notification.userInfo isKindOfClass:NSDictionary.class]) { return; } + NSDictionary *trackEventInfo = [notification.userInfo copy]; // 构造事件标识 SAEventIdentifier *eventIdentifier = [[SAEventIdentifier alloc] initWithEventInfo:trackEventInfo]; - // 埋点校验,暂时只支持 $AppClick 事件 - if (eventIdentifier.eventType != SensorsAnalyticsEventTypeAppClick) { + // App 埋点校验,只支持 $AppClick 可视化全埋点事件 + if (![eventIdentifier.eventName isEqualToString:kSAEventNameAppClick]) { return; } @@ -77,17 +80,52 @@ - (void)trackEvent:(NSNotification *)notification { continue; } SALogDebug(@"调试模式,匹配到可视化全埋点事件 %@", config.eventName); + [self cacheVisualEvent:config.eventName eventInfo:trackEventInfo]; + } +} - // 保存当前事件 - NSMutableArray *eventInfos = self.eventCheckCache[config.eventName]; - if (!eventInfos) { - eventInfos = [NSMutableArray array]; - self.eventCheckCache[config.eventName] = eventInfos; - } +- (void)trackEventFromH5:(NSNotification *)notification { + if (![notification.userInfo isKindOfClass:NSDictionary.class]) { + return; + } + + NSDictionary *trackEventInfo = notification.userInfo; + // 构造事件标识 + SAEventIdentifier *eventIdentifier = [[SAEventIdentifier alloc] initWithEventInfo:trackEventInfo]; + //App 内嵌 H5 埋点校验,只支持 $WebClick 可视化全埋点事件 + if (![eventIdentifier.eventName isEqualToString:kSAEventNameWebClick]) { + return; + } + + // 针对 $WebClick 可视化全埋点事件,Web JS SDK 已做标记 + NSArray *webVisualEventNames = trackEventInfo[kSAEventProperties][kSAWebVisualEventName]; + if (!webVisualEventNames) { + return; + } + // 移除标记 + eventIdentifier.properties[kSAWebVisualEventName] = nil; + + // 缓存 H5 可视化全埋点事件 + for (NSString *eventName in webVisualEventNames) { + [self cacheVisualEvent:eventName eventInfo:trackEventInfo]; + } +} - trackEventInfo[@"event_name"] = config.eventName; - [eventInfos addObject:trackEventInfo]; +/// 缓存可视化全埋点事件 +- (void)cacheVisualEvent:(NSString *)eventName eventInfo:(NSDictionary *)eventInfo { + if (!eventName) { + return; } + // 保存当前事件 + NSMutableArray *eventInfos = self.eventCheckCache[eventName]; + if (!eventInfos) { + eventInfos = [NSMutableArray array]; + self.eventCheckCache[eventName] = eventInfos; + } + + NSMutableDictionary *visualEventInfo = [eventInfo mutableCopy]; + visualEventInfo[@"event_name"] = eventName; + [eventInfos addObject:visualEventInfo]; } - (NSArray *)eventCheckResult { diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedAbstractMessage.m b/SensorsAnalyticsSDK/Visualized/SAVisualizedAbstractMessage.m index 00ddb837..7aced2d1 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedAbstractMessage.m +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedAbstractMessage.m @@ -131,6 +131,17 @@ - (NSData *)JSONDataWithFeatureCode:(NSString *)featureCode { // 增加 appId jsonObject[@"app_id"] = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleIdentifier"]; + // 上传全埋点配置开启状态 + NSMutableArray* autotrackOptions = [NSMutableArray array]; + SensorsAnalyticsAutoTrackEventType eventType = SensorsAnalyticsSDK.sharedInstance.configOptions.autoTrackEventType; + if (eventType & SensorsAnalyticsEventTypeAppClick) { + [autotrackOptions addObject:kSAEventNameAppClick]; + } + if (eventType & SensorsAnalyticsEventTypeAppViewScreen) { + [autotrackOptions addObject:kSAEventNameAppViewScreen]; + } + jsonObject[@"app_autotrack"] = autotrackOptions; + // 添加前端弹框信息 if (serializerManager.alertInfos.count > 0) { jsonObject[@"app_alert_infos"] = [serializerManager.alertInfos copy]; @@ -141,6 +152,7 @@ - (NSData *)JSONDataWithFeatureCode:(NSString *)featureCode { SAVisualizedWebPageInfo *webPageInfo = serializerManager.webPageInfo; jsonObject[@"h5_url"] = webPageInfo.url; jsonObject[@"h5_title"] = webPageInfo.title; + jsonObject[@"web_lib_version"] = webPageInfo.webLibVersion; } // SDK 版本号 diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedAutoTrackObjectSerializer.m b/SensorsAnalyticsSDK/Visualized/SAVisualizedAutoTrackObjectSerializer.m index 66f9949b..fcfdedcf 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedAutoTrackObjectSerializer.m +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedAutoTrackObjectSerializer.m @@ -33,8 +33,9 @@ #import "SAObjectSerializerConfig.h" #import "SAObjectSerializerContext.h" #import "SAPropertyDescription.h" -#import "SAJSTouchEventView.h" +#import "SAWebElementView.h" #import "SAVisualizedObjectSerializerManager.h" +#import "SAJavaScriptBridgeManager.h" #import "SAVisualizedManager.h" @interface SAVisualizedAutoTrackObjectSerializer () @@ -115,8 +116,8 @@ - (void)visitObject:(NSObject *)object withContext:(SAObjectSerializerContext *) } NSArray *classNames = [self classHierarchyArrayForObject:object]; - if ([object isKindOfClass:SAJSTouchEventView.class]) { - SAJSTouchEventView *touchView = (SAJSTouchEventView *)object; + if ([object isKindOfClass:SAWebElementView.class]) { + SAWebElementView *touchView = (SAWebElementView *)object; classNames = @[touchView.tagName]; } @@ -287,7 +288,7 @@ - (void)checkWKWebViewInfoWithWebView:(WKWebView *)webView { // 防止重复注入标记(js 发送数据,是异步的,防止 sensorsdata_visualized_mode 已经注入完成,但是尚未接收到 js 数据) __block BOOL isContainVisualized = NO; [userScripts enumerateObjectsUsingBlock:^(WKUserScript *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { - if ([obj.source containsString:@"sensorsdata_visualized_mode"]) { + if ([obj.source containsString:kSAJSBridgeVisualizedMode]) { isContainVisualized = YES; *stop = YES; } @@ -298,7 +299,8 @@ - (void)checkWKWebViewInfoWithWebView:(WKWebView *)webView { [self checkJSSDKIntegrationWithWebView:webView]; } else { // 注入 bridge 属性值,标记当前处于可视化全埋点扫码状态 - [javaScriptSource appendString:@"window.SensorsData_App_Visual_Bridge.sensorsdata_visualized_mode = true;"]; + NSString *visualizedMode = [SAJavaScriptBridgeBuilder buildVisualBridgeWithVisualizedMode:YES]; + [javaScriptSource appendString:visualizedMode]; } } @@ -306,14 +308,15 @@ - (void)checkWKWebViewInfoWithWebView:(WKWebView *)webView { 1. 存在部分场景,H5 页面内元素滚动,JS SDK 无法检测,如果 App 截图变化,直接通知 JS SDK 遍历最新页面元素数据发送 2. 可能先进入 H5,再扫码开启可视化全埋点,此时未成功注入标记,通过调用 JS 方法,手动通知 JS SDK 发送数据 */ - [javaScriptSource appendString:@"window.sensorsdata_app_call_js('visualized')"]; + NSString *jsMethodString = [SAJavaScriptBridgeBuilder buildCallJSMethodStringWithType:SAJavaScriptCallJSTypeVisualized jsonObject:nil]; + [javaScriptSource appendString:jsMethodString]; [webView evaluateJavaScript:javaScriptSource completionHandler:^(id _Nullable response, NSError *_Nullable error) { if (error) { /* 如果 JS SDK 尚未加载完成,可能方法不存在; 等到 JS SDK加载完成检测到 sensorsdata_visualized_mode 会尝试发送数据页面数据 */ - SALogError(@"window.sensorsdata_app_call_js error:%@", error); + SALogDebug(@"window.sensorsdata_app_call_js error:%@", error); } }]; } @@ -328,7 +331,7 @@ - (void)checkJSSDKIntegrationWithWebView:(WKWebView *)webView { return; } // 注入了 bridge 但是未接收到数据 - NSString *javaScript = @"window.sensorsdata_app_call_js('sensorsdata-check-jssdk')"; + NSString *javaScript = [SAJavaScriptBridgeBuilder buildCallJSMethodStringWithType:SAJavaScriptCallJSTypeCheckJSSDK jsonObject:nil]; [webView evaluateJavaScript:javaScript completionHandler:^(id _Nullable response, NSError *_Nullable error) { if (!error) { return; @@ -345,7 +348,7 @@ - (void)checkJSSDKIntegrationWithWebView:(WKWebView *)webView { if ([SAVisualizedManager sharedInstance].visualizedType == SensorsAnalyticsVisualizedTypeHeatMap) { alertInfo[@"title"] = @"当前页面无法进行点击分析"; } - NSDictionary *alertInfoMessage = @{ @"callType": @"app_alert", @"data": @[alertInfo] }; + NSMutableDictionary *alertInfoMessage = [@{ @"callType": @"app_alert", @"data": @[alertInfo] } mutableCopy]; [[SAVisualizedObjectSerializerManager sharedInstance] saveVisualizedWebPageInfoWithWebView:webView webPageInfo:alertInfoMessage]; } }]; diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedConnection.m b/SensorsAnalyticsSDK/Visualized/SAVisualizedConnection.m index 9d8555d4..b3227fb9 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedConnection.m +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedConnection.m @@ -92,14 +92,14 @@ - (void)receiveVisualizedMessageFromH5:(NSNotification *)notification { SALogError(@"Message webview is invalid from JS SDK"); return; } - - NSDictionary *messageDic = [SAJSONUtil JSONObjectWithString:message.body]; + + NSMutableDictionary *messageDic = [SAJSONUtil JSONObjectWithString:message.body options:NSJSONReadingMutableContainers]; if (![messageDic isKindOfClass:[NSDictionary class]]) { SALogError(@"Message body is formatted failure from JS SDK"); return; } - - [[SAVisualizedObjectSerializerManager sharedInstance] saveVisualizedWebPageInfoWithWebView:webView webPageInfo: messageDic]; + + [[SAVisualizedObjectSerializerManager sharedInstance] saveVisualizedWebPageInfoWithWebView:webView webPageInfo:messageDic]; } /// 开始计时 diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.h b/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.h index 94d1b6ca..ed358f14 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.h +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.h @@ -21,7 +21,6 @@ #import #import -#import "SAModuleManager+Visualized.h" #import "SAVisualPropertiesTracker.h" #import "SAVisualizedEventCheck.h" diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.m b/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.m index 339766cb..a470023d 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.m +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedManager.m @@ -30,9 +30,11 @@ #import "UIView+AutoTrack.h" #import "SAVisualizedUtils.h" #import "SAModuleManager.h" +#import "SAJavaScriptBridgeManager.h" #import "SAReachability.h" #import "SAValidator.h" #import "SAURLUtils.h" +#import "SAJSONUtil.h" #import "SASwizzle.h" #import "SALog.h" @@ -84,10 +86,13 @@ - (void)configChangedWithValid:(BOOL)valid { self.visualPropertiesTracker = [[SAVisualPropertiesTracker alloc] initWithConfigSources:self.configSources]; } - // 配置动态变化,开启埋点校验 + // 可能扫码阶段,可能尚未请求到配置,此处再次尝试开启埋点校验 if (!self.eventCheck && self.visualizedType == SensorsAnalyticsVisualizedTypeAutoTrack) { self.eventCheck = [[SAVisualizedEventCheck alloc] initWithConfigSources:self.configSources]; } + + // 配置更新,发送到 WKWebView 的内嵌 H5 + [self.visualPropertiesTracker.viewNodeTree updateConfig:self.configSources.originalResponse]; } else { self.visualPropertiesTracker = nil; self.eventCheck = nil; @@ -135,10 +140,21 @@ - (NSString *)javaScriptSource { } // App 内嵌 H5 数据交互 NSMutableString *javaScriptSource = [NSMutableString string]; - [javaScriptSource appendString:@"window.SensorsData_App_Visual_Bridge = {};"]; if (self.visualizedConnection.isVisualizedConnecting) { - [javaScriptSource appendFormat:@"window.SensorsData_App_Visual_Bridge.sensorsdata_visualized_mode = true;"]; + NSString *jsVisualizedMode = [SAJavaScriptBridgeBuilder buildVisualBridgeWithVisualizedMode:YES]; + [javaScriptSource appendString:jsVisualizedMode]; + } + + if (!self.configSources.isValid || self.configSources.originalResponse.count == 0) { + return javaScriptSource; + } + + // 注入完整配置信息 + NSString *webVisualConfig = [SAJavaScriptBridgeBuilder buildVisualPropertyBridgeWithVisualConfig:self.configSources.originalResponse]; + if (!webVisualConfig) { + return javaScriptSource; } + [javaScriptSource appendString:webVisualConfig]; return javaScriptSource; } @@ -278,6 +294,9 @@ - (BOOL)isVisualizeWithViewController:(UIViewController *)viewController { #pragma mark - Property - (nullable NSDictionary *)propertiesWithView:(UIView *)view { + if (![view isKindOfClass:UIView.class]) { + return nil; + } UIViewController *viewController = view.sensorsdata_viewController; if (!viewController) { return nil; @@ -301,7 +320,7 @@ - (nullable NSDictionary *)propertiesWithView:(UIView *)view { } - (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDictionary * _Nullable))completionHandler { - if (!self.visualPropertiesTracker) { + if (![view isKindOfClass:UIView.class] || !self.visualPropertiesTracker) { return completionHandler(nil); } @@ -313,6 +332,19 @@ - (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDi } } +- (void)queryVisualPropertiesWithConfigs:(NSArray *)propertyConfigs completionHandler:(void (^)(NSDictionary * _Nullable))completionHandler { + if (!self.visualPropertiesTracker) { + return completionHandler(nil); + } + + @try { + [self.visualPropertiesTracker queryVisualPropertiesWithConfigs:propertyConfigs completionHandler:completionHandler]; + } @catch (NSException *exception) { + SALogError(@"visualPropertiesWithView error: %@", exception); + completionHandler(nil); + } +} + #pragma mark - eventCheck /// 是否开启埋点校验 - (void)enableEventCheck:(BOOL)enable { diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.h b/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.h index 737a8674..736d6bed 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.h +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.h @@ -32,11 +32,14 @@ /// H5 标题 @property (nonatomic, copy) NSString *title; -/// H5 可点击元素信息 -@property (nonatomic, copy) NSArray *elementSources; +/// H5 元素信息(包括可点击元素和普通元素) +@property (nonatomic, copy) NSArray *webElementSources; /// 弹框信息 @property (nonatomic, copy) NSArray * alertSources; + +/// Web JS SDK 版本号 +@property (nonatomic, copy) NSString *webLibVersion; @end @@ -76,7 +79,7 @@ - (void)cleanVisualizedWebPageInfoCache; /// 缓存可视化全埋点相关 web 信息 -- (void)saveVisualizedWebPageInfoWithWebView:(WKWebView *)webview webPageInfo:(NSDictionary *)pageInfo; +- (void)saveVisualizedWebPageInfoWithWebView:(WKWebView *)webview webPageInfo:(NSMutableDictionary *)pageInfo; /// 读取当前 webView 页面信息 - (SAVisualizedWebPageInfo *)readWebPageInfoWithWebView:(WKWebView *)webView; diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.m b/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.m index a3ae5cd1..b321a299 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.m +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedObjectSerializerManager.m @@ -110,90 +110,136 @@ - (void)cleanVisualizedWebPageInfoCache { } /// 缓存可视化全埋点相关 web 信息 -- (void)saveVisualizedWebPageInfoWithWebView:(WKWebView *)webview webPageInfo:(NSDictionary *)pageInfo { +- (void)saveVisualizedWebPageInfoWithWebView:(WKWebView *)webview webPageInfo:(NSMutableDictionary *)pageInfo { NSString *callType = pageInfo[@"callType"]; - if (([callType isEqualToString:@"visualized_track"])) { - // H5 页面可点击元素数据 - NSArray *pageDatas = pageInfo[@"data"]; - if ([pageDatas isKindOfClass:NSArray.class]) { - NSDictionary *elementInfo = [pageDatas firstObject]; - NSString *url = elementInfo[@"$url"]; - if (url) { - SAVisualizedWebPageInfo *webPageInfo = [[SAVisualizedWebPageInfo alloc] init]; - // 是否包含当前 url 的页面信息 - if ([self.webPageInfoCache objectForKey:url]) { - webPageInfo = self.webPageInfoCache[url]; - - // 更新 H5 元素信息,则可视化全埋点可用,此时清空弹框信息 - webPageInfo.alertSources = nil; - } - webPageInfo.elementSources = pageDatas; - self.webPageInfoCache[url] = webPageInfo; - - // 刷新数据 - [self refreshPayloadHashWithData:pageDatas]; - } - } + if ([callType isEqualToString:@"visualized_track"]) { // 页面元素信息 + + [self saveWebElementInfoWithData:pageInfo webView:webview]; } else if ([callType isEqualToString:@"app_alert"]) { // 弹框提示信息 - /* - [{ - "title": "弹框标题", - "message": "App SDK 与 Web SDK 没有进行打通,请联系贵方技术人员修正 Web SDK 的配置,详细信息请查看文档。", - "link_text": "配置文档" - "link_url": "https://manual.sensorsdata.cn/sa/latest/app-h5-1573913.html" - }] - */ - NSArray *alertDatas = pageInfo[@"data"]; - NSString *url = webview.URL.absoluteString; - if ([alertDatas isKindOfClass:NSArray.class] && url) { - SAVisualizedWebPageInfo *webPageInfo = [[SAVisualizedWebPageInfo alloc] init]; - // 是否包含当前 url 的页面信息 - if ([self.webPageInfoCache objectForKey:url]) { - webPageInfo = self.webPageInfoCache[url]; - - // 如果 js 发送弹框信息,即 js 环境变化,可视化全埋点不可用,则清空页面信息 - webPageInfo.elementSources = nil; - webPageInfo.url = nil; - webPageInfo.title = nil; - } - // 区分点击分析和可视化全埋点,针对 JS 发送的弹框信息,截取标题替换处理 - if ([SAVisualizedManager sharedInstance].visualizedType == SensorsAnalyticsVisualizedTypeHeatMap) { - NSMutableArray * alertNewDatas = [NSMutableArray array]; - for (NSDictionary *alertDic in alertDatas) { - NSMutableDictionary * alertNewDic = [NSMutableDictionary dictionaryWithDictionary:alertDic]; - alertNewDic[@"title"] = [alertDic[@"title"] stringByReplacingOccurrencesOfString:@"可视化全埋点" withString:@"点击分析"]; - [alertNewDatas addObject:alertNewDic]; - }; - alertDatas = [alertNewDatas copy]; - } - webPageInfo.alertSources = alertDatas; - self.webPageInfoCache[url] = webPageInfo; - // 刷新数据 - [self refreshPayloadHashWithData:alertDatas]; - } - } else if (([callType isEqualToString:@"page_info"])) { // h5 页面信息 - NSDictionary *webInfo = pageInfo[@"data"]; - NSString *url = webInfo[@"$url"]; - if ([webInfo isKindOfClass:NSDictionary.class] && url) { - SAVisualizedWebPageInfo *webPageInfo = [[SAVisualizedWebPageInfo alloc] init]; - // 是否包含当前 url 的页面信息 - if ([self.webPageInfoCache objectForKey:url]) { - webPageInfo = self.webPageInfoCache[url]; - - // 更新 H5 页面信息,则可视化全埋点可用,此时清空弹框信息 - webPageInfo.alertSources = nil; - } - webPageInfo.url = url; - webPageInfo.title = webInfo[@"$title"]; - self.webPageInfoCache[url] = webPageInfo; - // 刷新数据 - [self refreshPayloadHashWithData:webInfo]; - } + + [self saveWebAlertInfoWithData:pageInfo webView:webview]; + + } else if ([callType isEqualToString:@"page_info"]) { // h5 页面信息 + [self saveWebPageInfoWithData:pageInfo webView:webview]; + } + + // 刷新数据 + [self refreshPayloadHashWithData:pageInfo]; +} + +/// 保存 H5 元素信息,并设置状态 +- (void)saveWebElementInfoWithData:(NSMutableDictionary *)pageInfo webView:(WKWebView *)webview { + // H5 页面可点击元素数据 + NSArray *pageDatas = pageInfo[@"data"]; + // 老版本 Web JS SDK 兼容,老版不包含 enable_click 字段,可点击元素需要设置标识 + for (NSMutableDictionary *elementInfoDic in pageDatas) { + elementInfoDic[@"enable_click"] = @YES; + } + + // H5 页面可见非点击元素 + NSArray *extraElements = pageInfo[@"extra_elements"]; + + if (pageDatas.count == 0 && extraElements.count == 0) { + return; + } + NSMutableArray *webElementSources = [NSMutableArray array]; + if (pageDatas.count > 0) { + [webElementSources addObjectsFromArray:pageDatas]; + } + if (extraElements.count > 0) { + [webElementSources addObjectsFromArray:extraElements]; + } + + NSDictionary *elementInfo = [webElementSources firstObject]; + NSString *url = elementInfo[@"$url"]; + if (!url) { + return; + } + + SAVisualizedWebPageInfo *webPageInfo = nil; + // 是否包含当前 url 的页面信息 + if ([self.webPageInfoCache objectForKey:url]) { + webPageInfo = self.webPageInfoCache[url]; + + // 更新 H5 元素信息,则可视化全埋点可用,此时清空弹框信息 + webPageInfo.alertSources = nil; + } else { + webPageInfo = [[SAVisualizedWebPageInfo alloc] init]; + self.webPageInfoCache[url] = webPageInfo; } + webPageInfo.webElementSources = [webElementSources copy]; +} + +/// 保存 H5 页面弹框信息 +- (void)saveWebAlertInfoWithData:(NSDictionary *)pageInfo webView:(WKWebView *)webview { + /* + [{ + "title": "弹框标题", + "message": "App SDK 与 Web SDK 没有进行打通,请联系贵方技术人员修正 Web SDK 的配置,详细信息请查看文档。", + "link_text": "配置文档" + "link_url": "https://manual.sensorsdata.cn/sa/latest/app-h5-1573913.html" + }] + */ + NSArray *alertDatas = pageInfo[@"data"]; + NSString *url = webview.URL.absoluteString; + if (![alertDatas isKindOfClass:NSArray.class] || !url) { + return; + } + + SAVisualizedWebPageInfo *webPageInfo = nil; + // 是否包含当前 url 的页面信息 + if ([self.webPageInfoCache objectForKey:url]) { + webPageInfo = self.webPageInfoCache[url]; + + // 如果 js 发送弹框信息,即 js 环境变化,可视化全埋点不可用,则清空页面信息 + webPageInfo.webElementSources = nil; + webPageInfo.url = nil; + webPageInfo.title = nil; + } else { + webPageInfo = [[SAVisualizedWebPageInfo alloc] init]; + self.webPageInfoCache[url] = webPageInfo; + } + + // 区分点击分析和可视化全埋点,针对 JS 发送的弹框信息,截取标题替换处理 + if ([SAVisualizedManager sharedInstance].visualizedType == SensorsAnalyticsVisualizedTypeHeatMap) { + NSMutableArray * alertNewDatas = [NSMutableArray array]; + for (NSDictionary *alertDic in alertDatas) { + NSMutableDictionary * alertNewDic = [NSMutableDictionary dictionaryWithDictionary:alertDic]; + alertNewDic[@"title"] = [alertDic[@"title"] stringByReplacingOccurrencesOfString:@"可视化全埋点" withString:@"点击分析"]; + [alertNewDatas addObject:alertNewDic]; + }; + alertDatas = [alertNewDatas copy]; + } + webPageInfo.alertSources = alertDatas; +} + +/// 保存 H5 页面信息 +- (void)saveWebPageInfoWithData:(NSDictionary *)pageInfo webView:(WKWebView *)webview { + NSDictionary *webInfo = pageInfo[@"data"]; + NSString *url = webInfo[@"$url"]; + NSString *libVersion = webInfo[@"lib_version"]; + + if (![webInfo isKindOfClass:NSDictionary.class] || !url) { + return; + } + SAVisualizedWebPageInfo *webPageInfo = nil; + // 是否包含当前 url 的页面信息 + if ([self.webPageInfoCache objectForKey:url]) { + webPageInfo = self.webPageInfoCache[url]; + // 更新 H5 页面信息,则可视化全埋点可用,此时清空弹框信息 + webPageInfo.alertSources = nil; + } else { + webPageInfo = [[SAVisualizedWebPageInfo alloc] init]; + self.webPageInfoCache[url] = webPageInfo; + } + + webPageInfo.url = url; + webPageInfo.title = webInfo[@"$title"]; + webPageInfo.webLibVersion = libVersion; } -/// 读取当前 webView 页面信息 +/// 读取当前 webView 页面相关信息 - (SAVisualizedWebPageInfo *)readWebPageInfoWithWebView:(WKWebView *)webView { NSString *url = webView.URL.absoluteString; SAVisualizedWebPageInfo *webPageInfo = [self.webPageInfoCache objectForKey:url]; diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.h b/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.h index 94e52169..cb860074 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.h +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.h @@ -53,10 +53,13 @@ NS_ASSUME_NONNULL_BEGIN /// @param view 当前视图 + (BOOL)isIgnoreSubviewsWithView:(UIView *)view; - /// view 截图 /// @param view 需要截图的 view + (UIImage *)screenshotWithView:(UIView *)view; + +/// 是否支持打通,包含新老打通 +/// @param webview 需要判断的 webview ++ (BOOL)isSupportCallJSWithWebView:(WKWebView *)webview; @end #pragma mark - diff --git a/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.m b/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.m index a2abcbb6..8da343f8 100644 --- a/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.m +++ b/SensorsAnalyticsSDK/Visualized/SAVisualizedUtils.m @@ -23,7 +23,7 @@ #endif #import "SAVisualizedUtils.h" -#import "SAJSTouchEventView.h" +#import "SAWebElementView.h" #import "UIView+SAElementPath.h" #import "UIView+SAElementSelector.h" #import "SAVisualizedViewPathProperty.h" @@ -32,6 +32,8 @@ #import "SAVisualizedManager.h" #import "SAAutoTrackUtils.h" #import "UIView+AutoTrack.h" +#import "SACommonUtility.h" +#import "SAJavaScriptBridgeManager.h" #import "SALog.h" /// 遍历查找页面最大层数,用于判断元素是否被覆盖 @@ -147,15 +149,16 @@ + (BOOL)isKindOfRCTView:(UIView *)view { #pragma mark WebElement + (NSArray *)analysisWebElementWithWebView:(WKWebView *)webView { SAVisualizedWebPageInfo *webPageInfo = [[SAVisualizedObjectSerializerManager sharedInstance] readWebPageInfoWithWebView:webView]; - NSArray *webPageDatas = webPageInfo.elementSources; - if (webPageDatas.count == 0) { + NSArray *webElementSources = webPageInfo.webElementSources; + if (webElementSources.count == 0) { return nil; } - - // 元素去重,去除 id 相同的重复元素 - NSMutableArray *allNoRepeatElementIds = [NSMutableArray array]; - NSMutableArray *touchViewArray = [NSMutableArray array]; - for (NSDictionary *pageData in webPageDatas) { + + // 元素去重,去除 id 相同的重复元素,并构建 model + NSMutableArray *allNoRepeatElementIds = [NSMutableArray array]; + NSMutableArray *webElementArray = [NSMutableArray array]; + + for (NSDictionary *pageData in webElementSources) { NSString *elementId = pageData[@"id"]; if (elementId) { if ([allNoRepeatElementIds containsObject:elementId]) { @@ -163,31 +166,41 @@ + (NSArray *)analysisWebElementWithWebView:(WKWebView obj2.level) { + return NSOrderedDescending; + } else { + return NSOrderedAscending; + } + }]; + // 构建子元素数组 - for (SAJSTouchEventView *touchView1 in [touchViewArray copy]) { - //当前元素嵌套子元素 - if (touchView1.jsSubElementIds.count > 0) { - NSMutableArray *jsSubElement = [NSMutableArray arrayWithCapacity:touchView1.jsSubElementIds.count]; - // 根据子元素 id 查找对应子元素 - for (NSString *elementId in touchView1.jsSubElementIds) { - for (SAJSTouchEventView *touchView2 in [touchViewArray copy]) { - if ([elementId isEqualToString:touchView2.jsElementId]) { - [jsSubElement addObject:touchView2]; - [touchViewArray removeObject:touchView2]; - } - } + for (SAWebElementView *webElement1 in [webElementArray copy]) { + //当前元素是否嵌套子元素 + if (webElement1.jsSubElementIds.count == 0) { + continue; + } + + NSMutableArray *jsSubElements = [NSMutableArray arrayWithCapacity:webElement1.jsSubElementIds.count]; + // 根据子元素 id 查找对应子元素 + for (SAWebElementView *webElement2 in [webElementArray copy]) { + // 如果 element2 是 element1 的子元素,则添加到 jsSubviews + if ([webElement1.jsSubElementIds containsObject:webElement2.jsElementId]) { + [jsSubElements addObject:webElement2]; + [webElementArray removeObject:webElement2]; } - touchView1.jsSubviews = [jsSubElement copy]; } + webElement1.jsSubviews = [jsSubElements copy]; } - return [touchViewArray copy]; + return [webElementArray copy]; } #pragma mark RNUtils @@ -285,10 +298,6 @@ + (BOOL)isAutoTrackAppClickWithControl:(UIControl *)control { if ([control isKindOfClass:UIDatePicker.class]) { return NO; } - // 被忽略元素 - if (control.sensorsdata_isIgnored) { - return NO; - } BOOL userInteractionEnabled = control.userInteractionEnabled; BOOL enabled = control.enabled; @@ -364,6 +373,29 @@ + (UIImage *)screenshotWithView:(UIView *)view { } return screenshotImage; } + ++ (BOOL)isSupportCallJSWithWebView:(WKWebView *)webview { + WKUserContentController *contentController = webview.configuration.userContentController; + NSArray *userScripts = contentController.userScripts; + + // 判断基于 UA 的老版打通 + NSString *currentUserAgent = [SACommonUtility currentUserAgent]; + if ([currentUserAgent containsString:@"sa-sdk-ios"]) { + return YES; + } + + // 判断新版打通 + __block BOOL isContainJavaScriptBridge = NO; + [userScripts enumerateObjectsUsingBlock:^(WKUserScript *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + if ([obj.source containsString:kSAJSBridgeServerURL]) { + isContainJavaScriptBridge = YES; + *stop = YES; + } + }]; + + return isContainJavaScriptBridge; +} + @end diff --git a/SensorsAnalyticsSDK/Visualized/SensorsAnalyticsSDK+Visualized.m b/SensorsAnalyticsSDK/Visualized/SensorsAnalyticsSDK+Visualized.m index 396c2637..302f37b8 100644 --- a/SensorsAnalyticsSDK/Visualized/SensorsAnalyticsSDK+Visualized.m +++ b/SensorsAnalyticsSDK/Visualized/SensorsAnalyticsSDK+Visualized.m @@ -24,8 +24,8 @@ #import "SensorsAnalyticsSDK+Visualized.h" #import "SensorsAnalyticsSDK+Private.h" -#import "SAModuleManager+Visualized.h" #import "SAVisualizedManager.h" +#import "SAModuleManager.h" @implementation SensorsAnalyticsSDK (Visualized) diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.h b/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.h index 71541c9b..f1efc8cc 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.h +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.h @@ -11,7 +11,7 @@ // // http://www.apache.org/licenses/LICENSE-2.0 // -// Unless required by applicable law or agreed to in writing, software +// Unless required by applicable law orviewNodeTree 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 @@ -21,6 +21,7 @@ #import #import #import "SAVisualPropertiesConfigSources.h" +#import "SAViewNodeTree.h" NS_ASSUME_NONNULL_BEGIN @@ -31,6 +32,9 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, strong, readonly) dispatch_queue_t serialQueue; +@property (atomic, strong, readonly) SAViewNodeTree *viewNodeTree; + +#pragma mark view changed /// 视图添加或移除 - (void)didMoveToSuperviewWithView:(UIView *)view; @@ -52,12 +56,19 @@ NS_ASSUME_NONNULL_BEGIN /// @param completionHandler 采集完成回调 - (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDictionary *_Nullable visualProperties))completionHandler; + +/// 根据配置,采集属性 +/// @param propertyConfigs 自定义属性配置 +/// @param completionHandler 采集完成回调 +- (void)queryVisualPropertiesWithConfigs:(NSArray *)propertyConfigs completionHandler:(void (^)(NSDictionary *_Nullable properties))completionHandler; + #pragma mark debugInfo /// 设置采集诊断日志 - (void)enableCollectDebugLog:(BOOL)enable; @property (nonatomic, copy, readonly) NSArray *logInfos; + @end diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.m b/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.m index 5ce88bd5..d312feb3 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.m +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/SAVisualPropertiesTracker.m @@ -28,18 +28,19 @@ #import "SAVisualizedUtils.h" #import "UIView+AutoTrack.h" #import "UIView+SAElementPath.h" -#import "SAViewNodeTree.h" #import "SACommonUtility.h" #import "SAVisualizedDebugLogTracker.h" #import "SAVisualizedLogger.h" +#import "SAJavaScriptBridgeManager.h" #import "SAAlertController.h" #import "SAAutoTrackUtils.h" #import "UIView+SAVisualProperties.h" +#import "SAJSONUtil.h" #import "SALog.h" @interface SAVisualPropertiesTracker() -@property (atomic, strong) SAViewNodeTree *viewNodeTree; +@property (atomic, strong, readwrite) SAViewNodeTree *viewNodeTree; @property (nonatomic, strong) dispatch_queue_t serialQueue; @property (nonatomic, strong) SAVisualPropertiesConfigSources *configSources; @property (nonatomic, strong) SAVisualizedDebugLogTracker *debugLogTracker; @@ -97,21 +98,17 @@ - (void)enterRNViewController:(UIViewController *)viewController { [self.viewNodeTree refreshRNViewScreenNameWithViewController:viewController]; } -#pragma mark visualProperties +#pragma mark - visualProperties +#pragma mark App visualProperties // 采集元素自定义属性 - (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDictionary *_Nullable visualProperties))completionHandler { - - /* 子线程执行 - 1. 根据当前 view 查询事件配置 - 2. 如果命中事件配置,根据当前事件配置,遍历包含的属性配置 - 3. 根据属性配置和对应 path 等信息,查找对应的属性元素 - 4. 从元素中,根据解析规则,解析对应的属性,拼接属性即可 - */ + // 如果列表定义事件不限定元素位置,则只能在当前列表内元素(点击元素所在位置)添加属性。所以此时的属性元素位置,和点击元素位置必须相同 NSString *clickPosition = [view sensorsdata_elementPosition]; NSInteger pageIndex = [SAVisualizedUtils pageIndexWithView:view]; + // 单独队列执行耗时查询 dispatch_async(self.serialQueue, ^{ /* 添加日志信息 在队列执行,防止快速点击导致的顺序错乱 @@ -125,19 +122,39 @@ - (void)visualPropertiesWithView:(UIView *)view completionHandler:(void (^)(NSDi */ SAViewNode *viewNode = view.sensorsdata_viewNode; NSArray *allEventConfigs = [self.configSources propertiesConfigsWithViewNode:viewNode]; + NSMutableDictionary *allEventProperties = [NSMutableDictionary dictionary]; - + NSMutableArray *webPropertiesConfigs = [NSMutableArray array]; for (SAVisualPropertiesConfig *config in allEventConfigs) { - // 查询属性 + if (config.webProperties.count > 0) { + [webPropertiesConfigs addObjectsFromArray:config.webProperties]; + } + + // 查询 native 属性 NSDictionary *properties = [self queryAllPropertiesWithPropertiesConfig:config clickPosition:clickPosition pageIndex:pageIndex]; if (properties.count > 0) { [allEventProperties addEntriesFromDictionary:properties]; } } - - dispatch_async(dispatch_get_main_queue(), ^{ - completionHandler(allEventProperties.count > 0 ? allEventProperties : nil); - }); + + // 不包含 H5 属性配置 + if (webPropertiesConfigs.count == 0) { + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(allEventProperties.count > 0 ? allEventProperties : nil); + }); + return; + } + + // 查询多个 WebView 内所有自定义属性 + [self queryMultiWebViewPropertiesWithConfigs:webPropertiesConfigs viewNode:viewNode completionHandler:^(NSDictionary * _Nullable properties) { + if (properties.count > 0) { + [allEventProperties addEntriesFromDictionary:properties]; + } + + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(allEventProperties.count > 0 ? allEventProperties : nil); + }); + }]; }); } @@ -188,37 +205,12 @@ - (nullable NSDictionary *)queryAllPropertiesWithPropertiesConfig:(SAVisualPrope // 页面序号,仅匹配当前页面元素 propertyConfig.pageIndex = pageIndex; - // 1. 获取属性元素 - UIView *view = [self.viewNodeTree viewWithPropertyConfig:propertyConfig]; - if (!view) { - NSString *logMessage = [SAVisualizedLogger buildLoggerMessageWithTitle:@"获取属性元素" message:@"属性 %@ 未找到对应属性元素", propertyConfig.name]; - SALogDebug(@"%@", logMessage); + // 根据修改后的配置,查询属性值 + NSDictionary *property = [self queryPropertiesWithPropertyConfig:propertyConfig]; + if (!property) { continue; } - - // 2. 根据属性元素,解析属性值 - NSString *propertyValue = [self analysisPropertyWithView:view propertyConfig:propertyConfig]; - if (!propertyValue) { - continue; - } - - // 3. 属性类型转换 - // 字符型属性 - if (propertyConfig.type == SAVisualPropertyTypeString) { - properties[propertyConfig.name] = propertyValue; - continue; - } - - // 数值型属性 - NSDecimalNumber *propertyNumber = [NSDecimalNumber decimalNumberWithString:propertyValue]; - // 判断转换后是否为 NAN - if ([propertyNumber isEqualToNumber:NSDecimalNumber.notANumber]) { - NSString *logMessage = [SAVisualizedLogger buildLoggerMessageWithTitle:@"解析属性" message:@"属性 %@ 正则解析后为:%@,数值型转换失败", propertyConfig.name, propertyValue]; - SALogWarn(@"%@", logMessage); - continue; - } - - properties[propertyConfig.name] = propertyNumber; + [properties addEntriesFromDictionary:property]; } return properties; } @@ -257,6 +249,158 @@ - (NSString *)analysisPropertyWithView:(UIView *)view propertyConfig:(SAVisualPr return value; } +/// 根据属性配置查询属性值 +- (nullable NSDictionary *)queryPropertiesWithPropertyConfig:(SAVisualPropertiesPropertyConfig *)propertyConfig { + // 1. 获取属性元素 + UIView *view = [self.viewNodeTree viewWithPropertyConfig:propertyConfig]; + if (!view) { + NSString *logMessage = [SAVisualizedLogger buildLoggerMessageWithTitle:@"获取属性元素" message:@"属性 %@ 未找到对应属性元素", propertyConfig.name]; + SALogDebug(@"%@", logMessage); + return nil; + } + + // 2. 根据属性元素,解析属性值 + NSString *propertyValue = [self analysisPropertyWithView:view propertyConfig:propertyConfig]; + if (!propertyValue) { + return nil; + } + + NSMutableDictionary *properties = [NSMutableDictionary dictionary]; + // 3. 属性类型转换 + // 字符型属性 + if (propertyConfig.type == SAVisualPropertyTypeString) { + properties[propertyConfig.name] = propertyValue; + return [properties copy]; + } + + // 数值型属性 + NSDecimalNumber *propertyNumber = [NSDecimalNumber decimalNumberWithString:propertyValue]; + // 判断转换后是否为 NAN + if ([propertyNumber isEqualToNumber:NSDecimalNumber.notANumber]) { + NSString *logMessage = [SAVisualizedLogger buildLoggerMessageWithTitle:@"解析属性" message:@"属性 %@ 正则解析后为:%@,数值型转换失败", propertyConfig.name, propertyValue]; + SALogWarn(@"%@", logMessage); + return nil; + } + properties[propertyConfig.name] = propertyNumber; + return [properties copy]; +} + +/// 根据配置,查询 Native 属性 +- (void)queryVisualPropertiesWithConfigs:(NSArray *)propertyConfigs completionHandler:(void (^)(NSDictionary *_Nullable properties))completionHandler { + + dispatch_async(self.serialQueue, ^{ + NSMutableDictionary *allEventProperties = [NSMutableDictionary dictionary]; + for (NSDictionary *propertyConfigDic in propertyConfigs) { + SAVisualPropertiesPropertyConfig *propertyConfig = [[SAVisualPropertiesPropertyConfig alloc] initWithDictionary:propertyConfigDic]; + + /* 查询 native 属性 + 如果存在多个 page 页面,这里可能查询错误 + */ + NSDictionary *property = [self queryPropertiesWithPropertyConfig:propertyConfig]; + if (property.count > 0) { + [allEventProperties addEntriesFromDictionary:property]; + } + } + dispatch_async(dispatch_get_main_queue(), ^{ + completionHandler(allEventProperties); + }); + }); +} + + +#pragma mark webView visualProperties +/// 查询多个 webView 内自定义属性 +- (void)queryMultiWebViewPropertiesWithConfigs:(NSArray *)propertyConfigs viewNode:(SAViewNode *)viewNode completionHandler:(void (^)(NSDictionary *_Nullable properties))completionHandler { + if (propertyConfigs.count == 0) { + completionHandler(nil); + return; + } + + // 事件元素为 App,属性元素可能存在于多个 WebView + NSDictionary * groupPropertyConfigs = [self groupMultiWebViewWithConfigs:propertyConfigs]; + + NSMutableDictionary *webProperties = [NSMutableDictionary dictionary]; + dispatch_group_t group = dispatch_group_create(); + for (NSArray *configArray in groupPropertyConfigs.allValues) { + + dispatch_group_enter(group); + [self queryCurrentWebViewPropertiesWithConfigs:configArray viewNode:viewNode completionHandler:^(NSDictionary * _Nullable properties) { + if (properties.count > 0) { + [webProperties addEntriesFromDictionary:properties]; + } + dispatch_group_leave(group); + }]; + } + + // 多个 webview 属性查询完成,返回结果 + dispatch_group_notify(group, self.serialQueue, ^{ + completionHandler([webProperties copy]); + }); +} + +/// 查询当前 webView 内自定义属性 +- (void)queryCurrentWebViewPropertiesWithConfigs:(NSArray *)propertyConfigs viewNode:(SAViewNode *)viewNode completionHandler:(void (^)(NSDictionary *_Nullable properties))completionHandler { + + NSDictionary *config = [propertyConfigs firstObject]; + SAVisualPropertiesPropertyConfig *propertyConfig = [[SAVisualPropertiesPropertyConfig alloc] initWithDictionary:config]; + // 设置页面信息,准确查找 webView + propertyConfig.screenName = viewNode.screenName; + propertyConfig.pageIndex = viewNode.pageIndex; + + UIView *view = [self.viewNodeTree viewWithPropertyConfig:propertyConfig]; + if (![view isKindOfClass:WKWebView.class]) { + NSString *logMessage = [SAVisualizedLogger buildLoggerMessageWithTitle:@"获取属性元素" message:@"App 内嵌 H5 属性 %@ 未找到对应 WKWebView 元素", propertyConfig.name]; + SALogDebug(@"%@", logMessage); + completionHandler(nil); + return; + } + + WKWebView *webView = (WKWebView *)view; + NSMutableDictionary *webMessageInfo = [NSMutableDictionary dictionary]; + webMessageInfo[@"platform"] = @"ios"; + webMessageInfo[@"sensorsdata_js_visual_properties"] = propertyConfigs; + + // 注入待查询的属性配置信息 + NSString *javaScriptSource = [SAJavaScriptBridgeBuilder buildCallJSMethodStringWithType:SAJavaScriptCallJSTypeWebVisualProperties jsonObject:webMessageInfo]; + if (!javaScriptSource) { + completionHandler(nil); + return; + } + // 使用 webview 调用 JS 方法,获取属性,主线程执行 + dispatch_async(dispatch_get_main_queue(), ^{ + [webView evaluateJavaScript:javaScriptSource completionHandler:^(id _Nullable results, NSError *_Nullable error) { + // 类型判断 + if ([results isKindOfClass:NSDictionary.class]) { + completionHandler(results); + } else { + NSString *logMessage = [SAVisualizedLogger buildLoggerMessageWithTitle:@"解析属性" message:@" 调用 JS 方法 %@,解析 App 内嵌 H5 属性失败", javaScriptSource]; + SALogDebug(@"%@", logMessage); + completionHandler(nil); + } + }]; + }); +} + +/// 对属性配置按照 webview 进行分组处理 +- (NSDictionary *)groupMultiWebViewWithConfigs:(NSArray *)propertyConfigs { + NSMutableDictionary *groupPropertyConfigs = [NSMutableDictionary dictionary]; + for (NSDictionary * propertyConfigDic in propertyConfigs) { + NSString *webViewElementPath = propertyConfigDic[@"webview_element_path"]; + if (!webViewElementPath) { + continue; + } + + // 当前 webview 的属性配置 + NSMutableArray * configs = groupPropertyConfigs[webViewElementPath]; + if (!configs) { + configs = [NSMutableArray array]; + groupPropertyConfigs[webViewElementPath] = configs; + } + [configs addObject:propertyConfigDic]; + } + return [groupPropertyConfigs copy]; +} + #pragma mark - logInfos /// 开始采集调试日志 - (void)enableCollectDebugLog:(BOOL)enable { diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.h b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.h index 81f7b3b9..02d30e54 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.h +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.h @@ -97,6 +97,14 @@ NS_ASSUME_NONNULL_BEGIN @interface SARNViewNode : SAViewNode @end +/// WKWebView 构建的元素节点 +@interface SAWKWebViewNode : SAViewNode + +/// 调用 JS 方法,发送自定义属性配置 +/// @param configResponse 配置原始 json 数据 +- (void)callJSSendVisualConfig:(NSDictionary *)configResponse; +@end + /// 需要忽略相对路径 @interface SAIgnorePathNode : SAViewNode diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.m b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.m index 4047a460..8f4dbc5c 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.m +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNode.m @@ -31,6 +31,10 @@ #import "SAConstants+Private.h" #import "SAVisualizedUtils.h" #import "SAViewElementInfoFactory.h" +#import "SAJavaScriptBridgeManager.h" +#import "SAVisualizedManager.h" +#import "SAJSONUtil.h" +#import "SALog.h" @interface SAViewNode() @@ -545,6 +549,39 @@ + (void)bindScreenNameWithClickableView:(UIView *)rnView { @end + +@implementation SAWKWebViewNode + +- (void)callJSSendVisualConfig:(NSDictionary *)configResponse { + if (configResponse.count == 0) { + return; + } + if (![self.view isKindOfClass:WKWebView.class]) { + return; + } + + WKWebView *webView = (WKWebView *)self.view; + // 判断打通才注入配置 + if (![SAVisualizedUtils isSupportCallJSWithWebView:webView]) { + return; + } + // 调用 JS 函数,发送配置信息 + NSString *javaScriptSource = [SAJavaScriptBridgeBuilder buildCallJSMethodStringWithType:SAJavaScriptCallJSTypeUpdateVisualConfig jsonObject:configResponse]; + if (!javaScriptSource) { + return; + } + [webView evaluateJavaScript:javaScriptSource completionHandler:^(id _Nullable resuts, NSError * _Nullable error) { + if (error) { + SALogDebug(@"%@ updateH5VisualConfig error: %@", kSAJSBridgeCallMethod, error); + } else { + SALogDebug(@"%@ updateH5VisualConfig finish", kSAJSBridgeCallMethod); + } + }]; +} + +@end + + @implementation SAIgnorePathNode - (NSString *)itemPath { diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeFactory.m b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeFactory.m index d4dc5d7c..c648b34a 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeFactory.m +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeFactory.m @@ -43,6 +43,8 @@ + (nullable SAViewNode *)viewNodeWithView:(UIView *)view { return [[SATabBarButtonNode alloc] initWithView:view]; } else if ([SAAutoTrackUtils isKindOfRNView:view]) { return [[SARNViewNode alloc] initWithView:view]; + } else if ([view isKindOfClass:WKWebView.class]) { + return [[SAWKWebViewNode alloc] initWithView:view]; } else if ([SAVisualizedUtils isIgnoredItemPathWithView:view]) { /* 忽略路径 1. UITableViewWrapperView 为 iOS11 以下 UITableView 与 cell 之间的 view diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.h b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.h index b9d38d68..27407275 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.h +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.h @@ -52,6 +52,10 @@ NS_ASSUME_NONNULL_BEGIN /// 根据节点配置信息,获取 view - (UIView *)viewWithPropertyConfig:(SAVisualPropertiesPropertyConfig *)config; +/// 自定义属性配置更新 +/// @param configResponse 配置原始 json 数据 +- (void)updateConfig:(NSDictionary *)configResponse; + @end NS_ASSUME_NONNULL_END diff --git a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.m b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.m index 8d1fcdea..c5c34c55 100644 --- a/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.m +++ b/SensorsAnalyticsSDK/Visualized/VisualProperties/ViewNode/SAViewNodeTree.m @@ -392,4 +392,36 @@ - (UIView *)viewWithPropertyConfig:(SAVisualPropertiesPropertyConfig *)config vi return resultView; } +#pragma mark config +/// 自定义属性配置更新 +/// @param configResponse 配置原始 json 数据 +- (void)updateConfig:(NSDictionary *)configResponse { + if (configResponse.count == 0) { + return; + } + + // 递归遍历,发送自定义属性配置 + [self sendWebViewConfig:configResponse viewNode:self.rootNode]; +} + +- (void)sendWebViewConfig:(NSDictionary *)configResponse viewNode:(SAViewNode *)node { + if ([node isKindOfClass:SAWKWebViewNode.class]) { + SAWKWebViewNode *webViewNode = (SAWKWebViewNode *)node; + + // getWindow 需要在主线程执行 + dispatch_async(dispatch_get_main_queue(), ^{ + // 判断 WebView 是否显示 + if (!webViewNode.view.window || ![SAVisualizedUtils isVisibleForView:webViewNode.view]) { + return; + } + [webViewNode callJSSendVisualConfig:configResponse]; + }); + return; + } + + [node.subNodes enumerateObjectsUsingBlock:^(SAViewNode *_Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) { + [self sendWebViewConfig:configResponse viewNode:obj]; + }]; +} + @end diff --git a/SensorsAnalyticsSDK/Visualized/SAJSTouchEventView.h b/SensorsAnalyticsSDK/Visualized/WebElementInfo/SAWebElementView.h similarity index 65% rename from SensorsAnalyticsSDK/Visualized/SAJSTouchEventView.h rename to SensorsAnalyticsSDK/Visualized/WebElementInfo/SAWebElementView.h index 00af4a8f..cca09ba6 100644 --- a/SensorsAnalyticsSDK/Visualized/SAJSTouchEventView.h +++ b/SensorsAnalyticsSDK/Visualized/WebElementInfo/SAWebElementView.h @@ -1,5 +1,5 @@ // -// SAJSTouchEventView.h +// SAWebElementView.h // SensorsAnalyticsSDK // // Created by 储强盛 on 2020/2/20. @@ -25,7 +25,7 @@ NS_ASSUME_NONNULL_BEGIN /// H5 页面元素构造 -@interface SAJSTouchEventView : UIView +@interface SAWebElementView : UIView /// 根据 web 页面元素信息构造对象 - (instancetype)initWithWebView:(WKWebView *)webView webElementInfo:(NSDictionary *)elementInfo; @@ -35,7 +35,7 @@ NS_ASSUME_NONNULL_BEGIN /// 是否为 H5 元素 @property (nonatomic, assign) BOOL isFromH5; -/// 元素选择器 +/// 元素选择器,老版使用 @property (nonatomic, copy) NSString *elementSelector; /// 元素内容 @@ -47,17 +47,37 @@ NS_ASSUME_NONNULL_BEGIN /// 元素所在页面 url @property (nonatomic, copy) NSString *url; -/// html 页面标题 +/// H5 页面标题 @property (nonatomic, copy) NSString *title; -/// js 生成的 html 元素 id +/// js 生成的 H5 元素 id @property (nonatomic, copy) NSString *jsElementId; -/// js 解析的 html 子元素 id +/// js 解析的 H5 子元素 id @property (nonatomic, copy) NSArray *jsSubElementIds; -/// js 解析的 html 子元素 -@property (nonatomic, copy) NSArray *jsSubviews; +/// js 解析的 H5 子元素 +@property (nonatomic, copy) NSArray *jsSubviews; + +/// 是否可点击 +@property (nonatomic, assign) BOOL enableAppClick; + +/// 是否为列表 +@property (nonatomic, assign) BOOL isListView; + +/// 元素路径,新版使用 +@property (nonatomic, copy) NSString *elementPath; + +/// 元素位置 +@property (nonatomic, copy) NSString *elementPosition; + +/// 元素在列表内的相对位置,列表元素才会有 +@property (nonatomic, copy) NSString *listSelector; + +/// Web JS SDK 版本号 +@property (nonatomic, copy) NSString *webLibVersion; + +@property (nonatomic, assign) NSInteger level; @end NS_ASSUME_NONNULL_END diff --git a/SensorsAnalyticsSDK/Visualized/SAJSTouchEventView.m b/SensorsAnalyticsSDK/Visualized/WebElementInfo/SAWebElementView.m similarity index 65% rename from SensorsAnalyticsSDK/Visualized/SAJSTouchEventView.m rename to SensorsAnalyticsSDK/Visualized/WebElementInfo/SAWebElementView.m index 38bb9df5..2e39dccd 100644 --- a/SensorsAnalyticsSDK/Visualized/SAJSTouchEventView.m +++ b/SensorsAnalyticsSDK/Visualized/WebElementInfo/SAWebElementView.m @@ -1,5 +1,5 @@ // -// SAJSTouchEventView.m +// SAWebElementView.m // SensorsAnalyticsSDK // // Created by 储强盛 on 2020/2/20. @@ -22,12 +22,12 @@ #error This file must be compiled with ARC. Either turn on ARC for the project or use -fobjc-arc flag on this file. #endif -#import "SAJSTouchEventView.h" +#import "SAWebElementView.h" -@interface SAJSTouchEventView() +@interface SAWebElementView() @end -@implementation SAJSTouchEventView +@implementation SAWebElementView - (instancetype)initWithWebView:(WKWebView *)webView webElementInfo:(NSDictionary *)elementInfo { self = [super init]; @@ -46,8 +46,6 @@ - (instancetype)initWithWebView:(WKWebView *)webView webElementInfo:(NSDictionar CGFloat scrollX = [elementInfo[@"scrollX"] floatValue] * zoomScale; CGFloat scrollY = [elementInfo[@"scrollY"] floatValue] * zoomScale; BOOL visibility = [elementInfo[@"visibility"] boolValue]; - NSArray *subelements = elementInfo[@"subelements"]; - if (height <= 0 || !visibility) { return nil; } @@ -66,6 +64,9 @@ - (instancetype)initWithWebView:(WKWebView *)webView webElementInfo:(NSDictionar [self setFrame:validFrame]; self.userInteractionEnabled = YES; + + NSArray *subelements = elementInfo[@"subelements"]; + _jsSubElementIds = subelements; _elementContent = elementInfo[@"$element_content"]; _elementSelector = elementInfo[@"$element_selector"]; _visibility = visibility; @@ -74,9 +75,48 @@ - (instancetype)initWithWebView:(WKWebView *)webView webElementInfo:(NSDictionar _title = elementInfo[@"$title"]; _isFromH5 = YES; _jsElementId = elementInfo[@"id"]; - _jsSubElementIds = subelements; + _enableAppClick = [elementInfo[@"enable_click"] boolValue]; + _isListView = [elementInfo[@"is_list_view"] boolValue]; + _elementPath = elementInfo[@"$element_path"]; + + NSNumber *position = elementInfo[@"$element_position"]; + if ([position isKindOfClass:NSNumber.class]) { + _elementPosition = [position stringValue]; + } else { + _elementPosition = nil; + } + + _level = [elementInfo[@"level"] integerValue]; + _listSelector = elementInfo[@"list_selector"]; + _webLibVersion = elementInfo[@"lib_version"]; } return self; } +- (NSString *)description { + NSMutableString *description = [NSMutableString stringWithString:NSStringFromClass(self.class)]; + if (self.elementContent) { + [description appendFormat:@", elementContent:%@", self.elementContent]; + } + if (self.level > 0) { + [description appendFormat:@", level:%ld", (long)self.level]; + } + if (self.elementPath) { + [description appendFormat:@", elementPath:%@", self.elementPath]; + } + if (self.elementPosition) { + [description appendFormat:@", elementPosition:%@", self.elementPosition]; + } + if (self.listSelector) { + [description appendFormat:@", listSelector:%@", self.listSelector]; + } + if (self.url) { + [description appendFormat:@", url:%@", self.url]; + } + if (self.jsSubviews) { + [description appendFormat:@", jsSubviews:%@", self.jsSubviews]; + } + + return [description copy]; +} @end