Files
keyboard/Shared/KBMaiPointReporter.m

400 lines
17 KiB
Mathematica
Raw Normal View History

2025-12-25 17:20:24 +08:00
//
// KBMaiPointReporter.m
// keyBoard
//
#import "KBMaiPointReporter.h"
2025-12-26 15:51:27 +08:00
#import "KBLog.h"
2026-01-06 19:25:34 +08:00
#import "KBAuthManager.h"
#if __has_include(<UIKit/UIKit.h>)
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#endif
2025-12-25 17:20:24 +08:00
NSString * const KBMaiPointErrorDomain = @"KBMaiPointErrorDomain";
2026-01-06 19:25:34 +08:00
NSString * const KBMaiPointEventTypePageExposure = @"page_exposure";
NSString * const KBMaiPointEventTypeClick = @"click";
2025-12-25 17:20:24 +08:00
2025-12-26 15:51:27 +08:00
#if DEBUG
static void KBMaiPoint_DebugLogURL(NSURLRequest *request) {
NSString *url = request.URL.absoluteString ?: @"";
KBLOG(@"🍃[KBMaiPointReporter] url=%@", url);
}
static void KBMaiPoint_DebugLogError(NSURLResponse *response, NSError *error) {
if (error) {
NSString *msg = error.localizedDescription ?: @"(no description)";
KBLOG(@"🍃[KBMaiPointReporter] error=%@ domain=%@ code=%ld", msg, error.domain ?: @"", (long)error.code);
return;
}
if ([response isKindOfClass:NSHTTPURLResponse.class]) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
if (statusCode >= 200 && statusCode < 300) {
KBLOG(@"🍃[KBMaiPointReporter] status=HTTP_%ld", (long)statusCode);
} else {
KBLOG(@"🍃[KBMaiPointReporter] error=HTTP_%ld", (long)statusCode);
}
}
}
#endif
2025-12-25 17:20:24 +08:00
@implementation KBMaiPointReporter
+ (instancetype)sharedReporter {
static KBMaiPointReporter *reporter = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
reporter = [[KBMaiPointReporter alloc] init];
});
return reporter;
}
- (NSString *)kb_trimmedStringOrEmpty:(NSString * _Nullable)string {
NSString *value = [string isKindOfClass:[NSString class]] ? string : @"";
return [value stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] ?: @"";
}
2026-01-06 19:25:34 +08:00
- (NSString *)kb_currentTokenOrEmpty {
NSString *t = [KBAuthManager shared].current.accessToken;
return [self kb_trimmedStringOrEmpty:t];
}
- (void)reportEventType:(NSString *)eventType
eventName:(NSString *)eventName
value:(NSDictionary * _Nullable)value
completion:(KBMaiPointReportCompletion _Nullable)completion {
NSString *trimmedType = [self kb_trimmedStringOrEmpty:eventType];
NSString *trimmedName = [self kb_trimmedStringOrEmpty:eventName];
if (trimmedType.length == 0 || trimmedName.length == 0) {
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, error);
});
}
return;
}
NSMutableDictionary *val = [NSMutableDictionary dictionary];
if ([value isKindOfClass:[NSDictionary class]] && value.count > 0) {
[val addEntriesFromDictionary:value];
}
if (![val[@"token"] isKindOfClass:NSString.class]) {
val[@"token"] = [self kb_currentTokenOrEmpty];
} else {
// tokennil -> @"" / trim
val[@"token"] = [self kb_trimmedStringOrEmpty:val[@"token"]];
}
NSDictionary *params = @{
// eventId eventName
@"eventType": trimmedType,
@"eventName": trimmedName,
@"eventId": trimmedName,
@"value": val.copy
};
[self postPath:KB_MAI_POINT_PATH_GENERIC_DATA parameters:params completion:completion];
}
- (void)reportPageExposureWithEventName:(NSString *)eventName
pageId:(NSString *)pageId
extra:(NSDictionary * _Nullable)extra
completion:(KBMaiPointReportCompletion _Nullable)completion {
NSString *pid = [self kb_trimmedStringOrEmpty:pageId];
NSMutableDictionary *val = [NSMutableDictionary dictionary];
if (pid.length > 0) {
val[@"page_id"] = pid;
}
if ([extra isKindOfClass:[NSDictionary class]] && extra.count > 0) {
[val addEntriesFromDictionary:extra];
}
[self reportEventType:KBMaiPointEventTypePageExposure eventName:eventName value:val completion:completion];
}
- (void)reportClickWithEventName:(NSString *)eventName
pageId:(NSString *)pageId
elementId:(NSString *)elementId
extra:(NSDictionary * _Nullable)extra
completion:(KBMaiPointReportCompletion _Nullable)completion {
NSString *pid = [self kb_trimmedStringOrEmpty:pageId];
NSString *eid = [self kb_trimmedStringOrEmpty:elementId];
NSMutableDictionary *val = [NSMutableDictionary dictionary];
if (pid.length > 0) {
val[@"page_id"] = pid;
}
if (eid.length > 0) {
val[@"element_id"] = eid;
}
if ([extra isKindOfClass:[NSDictionary class]] && extra.count > 0) {
[val addEntriesFromDictionary:extra];
}
[self reportEventType:KBMaiPointEventTypeClick eventName:eventName value:val completion:completion];
}
2025-12-25 17:20:24 +08:00
- (void)reportNewAccountWithType:(NSString *)type
account:(NSString * _Nullable)account
completion:(KBMaiPointReportCompletion _Nullable)completion {
NSString *trimmedType = [self kb_trimmedStringOrEmpty:type];
NSString *trimmedAccount = [self kb_trimmedStringOrEmpty:account];
2026-01-06 19:25:34 +08:00
if (trimmedType.length == 0) {
2025-12-25 17:20:24 +08:00
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, error);
});
}
return;
}
NSDictionary *params = @{
@"type": trimmedType,
2026-01-06 19:25:34 +08:00
@"account": trimmedAccount ?: @"",
@"token": [self kb_currentTokenOrEmpty]
2025-12-25 17:20:24 +08:00
};
[self postPath:KB_MAI_POINT_PATH_NEW_ACCOUNT parameters:params completion:completion];
}
//- (void)reportGenericDataWithEvent:(NSString *)event
// account:(NSString * _Nullable)account
// completion:(KBMaiPointReportCompletion _Nullable)completion {
// [self reportGenericDataWithType:KBMaiPointGenericReportTypeUnknown
// event:event
// account:account
// completion:completion];
//}
- (void)reportGenericDataWithEventType:(KBMaiPointGenericReportType)eventType
account:(nullable NSString *)account
completion:(KBMaiPointReportCompletion _Nullable)completion{
2026-01-06 19:25:34 +08:00
// eventName
NSString *typeStr = @"unknown";
switch (eventType) {
case KBMaiPointGenericReportTypeClick: typeStr = KBMaiPointEventTypeClick; break;
case KBMaiPointGenericReportTypeExposure: typeStr = @"exposure"; break;
case KBMaiPointGenericReportTypePage: typeStr = KBMaiPointEventTypePageExposure; break;
default: break;
}
NSMutableDictionary *val = [NSMutableDictionary dictionary];
NSString *trimmedAccount = [self kb_trimmedStringOrEmpty:account];
2026-01-06 19:25:34 +08:00
if (trimmedAccount.length > 0) {
val[@"account"] = trimmedAccount;
}
2026-01-06 19:25:34 +08:00
[self reportEventType:typeStr eventName:@"generic_event" value:val completion:completion];
}
2025-12-25 17:20:24 +08:00
- (void)postPath:(NSString *)path
parameters:(NSDictionary *)parameters
completion:(KBMaiPointReportCompletion _Nullable)completion {
2025-12-25 17:20:24 +08:00
if (path.length == 0 || ![parameters isKindOfClass:[NSDictionary class]]) {
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
code:-1
userInfo:@{NSLocalizedDescriptionKey: @"Invalid parameter"}];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, error);
});
}
return;
}
NSString *safePath = [path hasPrefix:@"/"] ? path : [@"/" stringByAppendingString:path];
NSString *urlString = [NSString stringWithFormat:@"%@%@", KB_MAI_POINT_BASE_URL, safePath];
NSURL *url = [NSURL URLWithString:urlString];
if (!url) {
NSError *error = [NSError errorWithDomain:KBMaiPointErrorDomain
code:-2
userInfo:@{NSLocalizedDescriptionKey: @"Invalid URL"}];
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, error);
});
}
return;
}
NSError *jsonError = nil;
NSData *body = [NSJSONSerialization dataWithJSONObject:parameters options:0 error:&jsonError];
if (jsonError) {
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(NO, jsonError);
});
}
return;
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
request.HTTPMethod = @"POST";
request.timeoutInterval = 10.0;
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
request.HTTPBody = body;
2025-12-26 15:51:27 +08:00
#if DEBUG
KBMaiPoint_DebugLogURL(request);
#endif
2025-12-25 17:20:24 +08:00
NSURLSessionConfiguration *config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
NSURLSession *session = [NSURLSession sessionWithConfiguration:config];
NSURLSessionDataTask *task = [session dataTaskWithRequest:request
completionHandler:^(NSData *data,
NSURLResponse *response,
NSError *error) {
BOOL success = NO;
NSError *finalError = error;
if (!finalError) {
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSInteger statusCode = ((NSHTTPURLResponse *)response).statusCode;
success = (statusCode >= 200 && statusCode < 300);
if (!success) {
finalError = [NSError errorWithDomain:KBMaiPointErrorDomain
code:statusCode
userInfo:@{NSLocalizedDescriptionKey: @"Invalid response"}];
}
} else {
finalError = [NSError errorWithDomain:KBMaiPointErrorDomain
code:-3
userInfo:@{NSLocalizedDescriptionKey: @"Invalid response"}];
}
}
2025-12-26 15:51:27 +08:00
#if DEBUG
KBMaiPoint_DebugLogError(response, finalError);
#endif
2025-12-25 17:20:24 +08:00
if (completion) {
dispatch_async(dispatch_get_main_queue(), ^{
completion(success, finalError);
});
}
}];
[task resume];
}
@end
2026-01-06 19:25:34 +08:00
#if __has_include(<UIKit/UIKit.h>)
// ============================
// viewDidAppear
// VC VC
// ============================
static NSDictionary<NSString *, NSDictionary *> *KBMaiPoint_PageExposureMap(void) {
static NSDictionary<NSString *, NSDictionary *> *m;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
m = @{
//
@"HomeMainVC": @{@"event_name": @"enter_home_main", @"page_id": @"home_main"},
@"HomeVC": @{@"event_name": @"enter_home", @"page_id": @"home"},
@"HomeHotVC": @{@"event_name": @"enter_home_hot", @"page_id": @"home_hot"},
@"HomeRankVC": @{@"event_name": @"enter_home_rank", @"page_id": @"home_rank"},
@"HomeRankContentVC": @{@"event_name": @"enter_home_rank_content", @"page_id": @"home_rank_content"},
@"HomeSheetVC": @{@"event_name": @"enter_home_sheet", @"page_id": @"home_sheet"},
@"KBCommunityVC": @{@"event_name": @"enter_community", @"page_id": @"community"},
@"KBSearchVC": @{@"event_name": @"enter_search", @"page_id": @"search"},
@"KBSearchResultVC": @{@"event_name": @"enter_search_result", @"page_id": @"search_result"},
@"KBShopVC": @{@"event_name": @"enter_shop", @"page_id": @"shop"},
@"KBShopItemVC": @{@"event_name": @"enter_shop_item_list", @"page_id": @"shop_item_list"},
@"KBSkinDetailVC": @{@"event_name": @"enter_skin_detail", @"page_id": @"skin_detail"},
@"MyVC": @{@"event_name": @"enter_my", @"page_id": @"my"},
@"MySkinVC": @{@"event_name": @"enter_my_skin", @"page_id": @"my_skin"},
@"KBMyKeyBoardVC": @{@"event_name": @"enter_my_keyboard", @"page_id": @"my_keyboard"},
@"KBPersonInfoVC": @{@"event_name": @"enter_person_info", @"page_id": @"person_info"},
@"KBFeedBackVC": @{@"event_name": @"enter_feedback", @"page_id": @"feedback"},
@"KBNoticeVC": @{@"event_name": @"enter_notice", @"page_id": @"notice"},
@"KBConsumptionRecordVC": @{@"event_name": @"enter_consumption_record", @"page_id": @"consumption_record"},
@"KBVipPay": @{@"event_name": @"enter_vip_pay", @"page_id": @"vip_pay"},
@"KBJfPay": @{@"event_name": @"enter_points_recharge", @"page_id": @"points_recharge"},
@"KBLoginVC": @{@"event_name": @"enter_login", @"page_id": @"login"},
@"KBEmailLoginVC": @{@"event_name": @"enter_login_email", @"page_id": @"login_email"},
@"KBEmailRegistVC": @{@"event_name": @"enter_register_email", @"page_id": @"register_email"},
@"KBRegistVerEmailVC": @{@"event_name": @"enter_register_verify_email", @"page_id": @"register_verify_email"},
@"KBForgetPwdVC": @{@"event_name": @"enter_forgot_password_email", @"page_id": @"forgot_password_email"},
@"KBForgetVerPwdVC": @{@"event_name": @"enter_forgot_password_verify", @"page_id": @"forgot_password_verify"},
@"KBForgetPwdNewPwdVC": @{@"event_name": @"enter_forgot_password_newpwd", @"page_id": @"forgot_password_newpwd"},
@"KBPermissionViewController": @{@"event_name": @"enter_keyboard_permission_guide", @"page_id": @"keyboard_permission_guide"},
@"KBGuideVC": @{@"event_name": @"enter_guide", @"page_id": @"guide"},
@"KBSexSelVC": @{@"event_name": @"enter_sex_select", @"page_id": @"sex_select"},
@"KBWebViewViewController": @{@"event_name": @"enter_webview", @"page_id": @"webview"},
//
@"KeyboardViewController": @{@"event_name": @"enter_keyboard", @"page_id": @"keyboard"},
};
});
return m;
}
static inline void KBMaiPoint_SwizzleInstanceMethod(Class cls, SEL originalSel, SEL swizzledSel) {
Method original = class_getInstanceMethod(cls, originalSel);
Method swizzled = class_getInstanceMethod(cls, swizzledSel);
if (!original || !swizzled) return;
BOOL added = class_addMethod(cls,
originalSel,
method_getImplementation(swizzled),
method_getTypeEncoding(swizzled));
if (added) {
class_replaceMethod(cls,
swizzledSel,
method_getImplementation(original),
method_getTypeEncoding(original));
} else {
method_exchangeImplementations(original, swizzled);
}
}
@interface UIViewController (KBMaiPointAutoReport)
@end
@implementation UIViewController (KBMaiPointAutoReport)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
KBMaiPoint_SwizzleInstanceMethod(self, @selector(viewDidAppear:), @selector(kb_maipoint_viewDidAppear:));
});
}
- (void)kb_maipoint_viewDidAppear:(BOOL)animated {
[self kb_maipoint_viewDidAppear:animated];
NSString *clsName = NSStringFromClass(self.class);
NSDictionary *cfg = KBMaiPoint_PageExposureMap()[clsName];
if (![cfg isKindOfClass:NSDictionary.class]) { return; }
NSString *eventName = cfg[@"event_name"];
NSString *pageId = cfg[@"page_id"];
if (![eventName isKindOfClass:NSString.class] || ![pageId isKindOfClass:NSString.class]) { return; }
//
NSMutableDictionary *extra = [NSMutableDictionary dictionary];
if ([clsName isEqualToString:@"KBSkinDetailVC"]) {
id themeId = nil;
@try { themeId = [self valueForKey:@"themeId"]; } @catch (__unused NSException *e) { themeId = nil; }
if ([themeId isKindOfClass:NSString.class] && ((NSString *)themeId).length > 0) {
extra[@"theme_id"] = themeId;
}
} else if ([clsName isEqualToString:@"KBWebViewViewController"]) {
id url = nil;
@try { url = [self valueForKey:@"url"]; } @catch (__unused NSException *e) { url = nil; }
if ([url isKindOfClass:NSString.class] && ((NSString *)url).length > 0) {
extra[@"url"] = url;
}
}
[[KBMaiPointReporter sharedReporter] reportPageExposureWithEventName:eventName
pageId:pageId
extra:(extra.count > 0 ? extra.copy : nil)
completion:nil];
}
@end
#endif