diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 483998a16e0..87b005309cb 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -35,7 +35,9 @@ 29BE9D872CA3C21900B67DE7 /* KMModifierMapping.m in Sources */ = {isa = PBXBuildFile; fileRef = 29BE9D862CA3C21900B67DE7 /* KMModifierMapping.m */; }; 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = D861B03E2C5747F70003675E /* KMSettingsRepository.m */; }; 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */ = {isa = PBXBuildFile; fileRef = 29015ABC2C58D86F00CCBB94 /* KMDataRepository.m */; }; + 29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */; }; 29DD5F442CFEF88000683388 /* SILAndikaV1RGB.png in Resources */ = {isa = PBXBuildFile; fileRef = 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */; }; + 29E67D042D3E4A86003CDE8F /* KMSentryHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */; }; 37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; }; 37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; }; 37C2B0CB25FF2C350092E16A /* Help in Resources */ = {isa = PBXBuildFile; fileRef = 37C2B0CA25FF2C340092E16A /* Help */; }; @@ -275,6 +277,8 @@ 29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = ""; }; 29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = ""; }; 29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = ""; }; + 29DBBF652D35053E00D8E33C /* KMSentryHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSentryHelper.h; sourceTree = ""; }; + 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMSentryHelper.m; sourceTree = ""; }; 29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = ""; }; 29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = ""; }; 29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = ""; }; @@ -433,6 +437,17 @@ path = Privacy; sourceTree = ""; }; + 29DBBF642D34FFE000D8E33C /* Logging */ = { + isa = PBXGroup; + children = ( + 29B4A0D42BF7675A00682049 /* KMLogs.h */, + 29B4A0D32BF7675A00682049 /* KMLogs.m */, + 29DBBF652D35053E00D8E33C /* KMSentryHelper.h */, + 29DBBF662D35053E00D8E33C /* KMSentryHelper.m */, + ); + path = Logging; + sourceTree = ""; + }; 2CDC487CDFFBB26D621D73BD /* Pods */ = { isa = PBXGroup; children = ( @@ -564,8 +579,6 @@ 989C9C0A1A7876DE00A20425 /* Keyman4MacIM */ = { isa = PBXGroup; children = ( - 29B4A0D42BF7675A00682049 /* KMLogs.h */, - 29B4A0D32BF7675A00682049 /* KMLogs.m */, 299ABD6F29ECE75B00AA5948 /* KeySender.m */, 299ABD7029ECE75B00AA5948 /* KeySender.h */, D861B03D2C5747F70003675E /* KMSettingsRepository.h */, @@ -575,6 +588,7 @@ 297A501128DF4D360074EB1B /* Privacy */, 98FE105B1B4DE86300525F54 /* Categories */, 98D6DA791A799EE700B09822 /* Frameworks */, + 29DBBF642D34FFE000D8E33C /* Logging */, 989C9C121A7876DE00A20425 /* Images.xcassets */, 98BDD3681BC3511200FAC7C4 /* Keyman4MacIM.entitlements */, 98BF92401BF01CBE0002126A /* KMAboutWindow */, @@ -1000,6 +1014,7 @@ 9A3D6C5D221531B0008785A3 /* KMOSVersion.m in Sources */, 989C9C111A7876DE00A20425 /* main.m in Sources */, 290BC680274B9DB1005CD1C3 /* KMPackageInfo.m in Sources */, + 29DBBF672D35053E00D8E33C /* KMSentryHelper.m in Sources */, 29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */, 98BF924F1BF02DC20002126A /* KMBarView.m in Sources */, E240F599202DED740000067D /* KMPackage.m in Sources */, @@ -1038,6 +1053,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 29E67D042D3E4A86003CDE8F /* KMSentryHelper.m in Sources */, 29C1CDE22C5B2F8B003C23BB /* KMSettingsRepository.m in Sources */, 29C1CDE32C5B2F8B003C23BB /* KMDataRepository.m in Sources */, 2915DC512BFE35DB0051FC52 /* KMLogs.m in Sources */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index 202be65ef5e..2b06db4cade 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -10,6 +10,7 @@ #import "KMDownloadKBWindowController.h" #import "KMDataRepository.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @interface KMConfigurationWindowController () @property (nonatomic, weak) IBOutlet NSTableView *tableView; @@ -360,21 +361,27 @@ - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id)info r - (void)checkBoxAction:(id)sender { NSButton *checkBox = (NSButton *)sender; NSString *kmxFilePath = [self kmxFilePathAtIndex:checkBox.tag]; + NSString * kmxFileName = [kmxFilePath lastPathComponent]; NSString *partialPath = [KMDataRepository.shared trimToPartialPath:kmxFilePath]; os_log_debug([KMLogs uiLog], "checkBoxAction, kmxFilePath = %{public}@ for checkBox.tag %li, partialPath = %{public}@", kmxFilePath, checkBox.tag, partialPath); if (checkBox.state == NSOnState) { - os_log_debug([KMLogs uiLog], "Adding active keyboard: %{public}@", kmxFilePath); + os_log_debug([KMLogs uiLog], "enabling active keyboard: %{public}@", kmxFileName); + NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", kmxFileName]; + [KMSentryHelper addUserBreadCrumb:@"config" message:message]; [self.activeKeyboards addObject:partialPath]; [self saveActiveKeyboards]; } else if (checkBox.state == NSOffState) { - os_log_debug([KMLogs uiLog], "Disabling active keyboard: %{public}@", kmxFilePath); + os_log_debug([KMLogs uiLog], "disabling active keyboard: %{public}@", kmxFileName); + NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", kmxFileName]; + [KMSentryHelper addUserBreadCrumb:@"config" message:message]; [self.activeKeyboards removeObject:partialPath]; [self saveActiveKeyboards]; } } - (void)infoAction:(id)sender { + [KMSentryHelper addUserBreadCrumb:@"user" message:@"getting package information"]; NSButton *infoButton = (NSButton *)sender; NSString *packagePath = [self packagePathAtIndex:infoButton.tag]; if (packagePath != nil) { @@ -389,6 +396,7 @@ - (void)infoAction:(id)sender { } - (void)helpAction:(id)sender { + [KMSentryHelper addUserBreadCrumb:@"user" message:@"displaying help"]; NSButton *helpButton = (NSButton *)sender; NSString *packagePath = [self packagePathAtIndex:helpButton.tag]; if (packagePath != nil) { @@ -406,22 +414,31 @@ - (void)removeAction:(id)sender { NSButton *deleteButton = (NSButton *)sender; NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag]; NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil); - - if ([info objectForKey:@"HeaderTitle"] != nil) - [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:@"HeaderTitle"]]]; - else - [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:kKMKeyboardNameKey]]]; - + NSString *keyboardName = nil; + + if ([info objectForKey:@"HeaderTitle"] != nil) { + keyboardName = [info objectForKey:@"HeaderTitle"]; + } else { + keyboardName = [info objectForKey:kKMKeyboardNameKey]; + } + [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]]; + os_log_debug([KMLogs configLog], "entered removeAction for keyboardName: %{public}@", keyboardName); + [self.deleteAlertView beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) { if (returnCode == NSAlertFirstButtonReturn) { os_log_debug([KMLogs uiLog], "confirm delete keyboard alert dismissed"); [self deleteFileAtIndex:[NSNumber numberWithInteger:deleteButton.tag]]; + + os_log_debug([KMLogs configLog], "removeAction for keyboardName: %{public}@", keyboardName); + [KMSentryHelper addUserBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]]; } self.deleteAlertView = nil; }]; } - (IBAction)downloadAction:(id)sender { + [KMSentryHelper addUserBreadCrumb:@"user" message:@"download keyboard"]; + if (self.AppDelegate.infoWindow_.window != nil) [self.AppDelegate.infoWindow_ close]; @@ -453,7 +470,8 @@ - (void)handleRequestToInstallPackage:(KMPackage *) package { - (void)installPackageFile:(NSString *)kmpFile { // kmpFile could be a temp file (in fact, it always is!), so don't display the name. os_log_debug([KMLogs dataLog], "kmpFile - ready to unzip/install Package File: %{public}@", kmpFile); - + [KMSentryHelper addInfoBreadCrumb:@"configure" message:@"install package file"]; + BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile]; if (!didUnzip) { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index e97be3e389c..dd53b496227 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -13,7 +13,7 @@ #import "KMLogs.h" #import "InputMethodKit/InputMethodKit.h" #import "KMInputMethodLifecycle.h" - +#import "KMSentryHelper.h" @implementation KMInputController @@ -25,7 +25,7 @@ - (KMInputMethodAppDelegate *)appDelegate { - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getClientApplicationId]); + os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]); self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { @@ -84,7 +84,7 @@ - (void)inputMethodChangedClient:(NSNotification *)notification { if (_eventHandler != nil) { [_eventHandler deactivate]; } - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getClientApplicationId] client:self.client]; + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client]; } @@ -111,17 +111,21 @@ - (void)menuAction:(id)sender { NSInteger itag = mItem.tag; os_log_debug([KMLogs uiLog], "Keyman menu clicked - tag: %lu", itag); if (itag == CONFIG_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Configuration..."]; [self showConfigurationWindow:sender]; } else if (itag == OSK_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"On-screen Keyboard"]; [KMSettingsRepository.shared writeShowOskOnActivate:YES]; os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES"); [self.appDelegate showOSK]; } else if (itag == ABOUT_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"About"]; [self.appDelegate showAboutWindow]; } else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { + [KMSentryHelper addUserBreadCrumb:@"menu" message:@"Selected Keyboard"]; [self.appDelegate selectKeyboardFromMenu:itag]; } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index fe06fa603a8..eade3d6d2f4 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -124,6 +124,7 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; - (void)postKeyboardEventWithSource: (CGEventSourceRef)source code:(CGKeyCode) virtualKey postCallback:(PostEventCallback)postEvent; - (KeymanVersionInfo)versionInfo; - (void)registerConfigurationWindow:(NSWindowController *)window; +- (BOOL)canForceSentryEvents; @end #endif /* KMInputMethodAppDelegate_h */ diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index b938279197b..cfff0d80cd6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -17,8 +17,17 @@ #import "KMPackageInfo.h" #import "PrivacyConsent.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @import Sentry; +#if TARGET_CPU_ARM64 +NSString *processorType = @"ARM"; +#elif TARGET_CPU_X86_64 +NSString *processorType = @"Intel"; +#else +NSString *processorType = @"Unknown"; +#endif + NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification"; @implementation NSString (VersionNumbers) @@ -48,6 +57,7 @@ @interface KMInputMethodAppDelegate () @property (nonatomic, strong) KMPackageReader *packageReader; @property BOOL receivedKeyDownFromOsk; @property NSEventModifierFlags oskEventModifiers; +@property (nonatomic, assign) BOOL sentryTestingEnabled; @end @implementation KMInputMethodAppDelegate @@ -124,10 +134,12 @@ - (void)initCompletion { - (void)inputMethodDeactivated:(NSNotification *)notification { if ([self.oskWindow.window isVisible]) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK"); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"]; [self.oskWindow.window setIsVisible:NO]; } else { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden"); } + [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; if (self.lowLevelEventTap) { os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap"); @@ -143,9 +155,12 @@ - (void)inputMethodActivated:(NSNotification *)notification { os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); CGEventTapEnable(self.lowLevelEventTap, YES); } + + os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false"); if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; [self showOSK]; } } @@ -181,7 +196,7 @@ -(void)applicationDidFinishLaunching:(NSNotification *)aNotification { [self setDefaultKeymanMenuItems]; [self updateKeyboardMenuItems]; [self setPostLaunchKeymanSentryTags]; - // [SentrySDK captureMessage:@"Starting Keyman [test message]"]; + self.sentryTestingEnabled = [KMSettingsRepository.shared readForceSentryError]; } - (void)startSentry { @@ -192,31 +207,46 @@ - (void)startSentry { options.dsn = @"https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522"; options.releaseName = releaseName; options.environment = keymanVersionInfo.sentryEnvironment; - //options.debug = YES; }]; } -- (void)setPostLaunchKeymanSentryTags { - NSString *keyboardFileName = [self.kmx.filePath lastPathComponent]; - os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName); +/** + * Determine whether to force sentry events. + * When true, then typing certain characters will cause Sentry events or crashes to occur. + * + * To enable, set the flag in UserDefaults + * `defaults write keyman.inputmethod.Keyman KMForceSentryError 1` + * Also, use the SIL Euro Latin keyboard and type in the Stickies app + */ +- (BOOL)canForceSentryEvents { + NSString* const testKeyboardName = @"sil_euro_latin.kmx"; + NSString* const testClientApplicationId = @"com.apple.Stickies"; + BOOL canForce = NO; - NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"Yes":@"No"; + if (self.sentryTestingEnabled) { + NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId]; + NSString * kmxFileName = [self getKmxFileName]; + canForce = [kmxFileName isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId]; + os_log_info([KMLogs testLog], "canForceSentryEvents, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxFileName, applicationId, canForce?@"YES":@"NO"); + } + + return canForce; +} - // assign custom keyboard tag in Sentry to initial keyboard - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"]; - [scope setTagValue:keyboardFileName forKey:@"keyboard"]; - }]; +- (NSString*) getKmxFileName { + return [[self.kmx filePath] lastPathComponent]; } -#ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE -- (BOOL)alertShowHelp:(NSAlert *)alert { - os_log_error([KMLogs startupLog], "Sentry - KME: Got call to force crash from engine"); - [SentrySDK crash]; - os_log_error([KMLogs startupLog], "Sentry - KME: should not have gotten this far!"); - return NO; +- (void)setPostLaunchKeymanSentryTags { + NSString *keyboardFileName = [self.kmx.filePath lastPathComponent]; + os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName); + + // assign custom keyboard tags in Sentry + [KMSentryHelper addKeyboardTag:keyboardFileName]; + [KMSentryHelper addHasAccessibilityTag:[PrivacyConsent.shared checkAccessibility]]; + [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; + [KMSentryHelper addArchitectureTag:processorType]; } -#endif - (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent { @@ -317,6 +347,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef os_log_debug([KMLogs eventsLog], "*** kKeymanEventKeyCode = 0xFF"); } else { if ([OSKView isOskKeyDownEvent:event]) { + [KMSentryHelper addInfoBreadCrumb:@"event" message:@"processing OSK-generated keydown event"]; NSEventModifierFlags oskEventModifiers = [OSKView extractModifierFlagsFromOskEvent:event]; appDelegate.receivedKeyDownFromOsk = YES; appDelegate.oskEventModifiers = oskEventModifiers; @@ -406,22 +437,26 @@ - (void)loadKeyboardFromKmxFile:(KMXFile *)kmx { _kmx = kmx; CoreKeyboardInfo *keyboardInfo = [self.kme loadKeyboardFromKmxFile:kmx]; - os_log_info([KMLogs keyboardLog], "setKmx loaded keyboard, keyboard info: %{public}@", keyboardInfo); - + os_log_info([KMLogs keyboardLog], "loadKeyboardFromKmxFile, keyboard info: %{public}@", keyboardInfo); + [KMSentryHelper addInfoBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [self getKmxFileName]]]; + [KMSentryHelper addKeyboardTag:[self getKmxFileName]]; + _modifierMapping = [[KMModifierMapping alloc] init:keyboardInfo]; os_log_info([KMLogs keyboardLog], "modifierMapping bothOptionKeysGenerateRightAlt: %d", self.modifierMapping.bothOptionKeysGenerateRightAlt); - - // assign custom keyboard tag in Sentry to default keyboard - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - [scope setTagValue:keyboardInfo.keyboardId forKey:@"keyboard"]; - }]; } - (void)setKvk:(KVKFile *)kvk { + if (kvk != nil) { + os_log_info([KMLogs lifecycleLog], "*** setKvk, kvk.associatedKeyboard: %{public}@", kvk.associatedKeyboard); + } else { + os_log_info([KMLogs lifecycleLog], "*** setKvk, setting to nil"); + } _kvk = kvk; - if (_oskWindow != nil) + + if (_oskWindow != nil) { [_oskWindow resetOSK]; + } } - (void)setKeyboardName:(NSString *)keyboardName { @@ -616,6 +651,8 @@ - (NSMutableArray *)activeKeyboards { if (!_activeKeyboards) { os_log_debug([KMLogs dataLog], "initializing activeKeyboards"); _activeKeyboards = [[KMSettingsRepository.shared readActiveKeyboards] mutableCopy]; + + [KMSentryHelper addActiveKeyboardCountTag:_activeKeyboards.count]; } return _activeKeyboards; @@ -626,6 +663,7 @@ - (void)saveActiveKeyboards { [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards]; [self resetActiveKeyboards]; [self updateKeyboardMenuItems]; + [KMSentryHelper addActiveKeyboardCountTag:self.activeKeyboards.count]; } - (void)clearActiveKeyboards { @@ -825,6 +863,8 @@ - (void) setDefaultSelectedKeyboard { [self setSelectedKeyboard:keyboardName inMenuItem:menuItem]; [self setSelectedKeyboard:keyboardName]; [self setContextBuffer:nil]; + + [KMSentryHelper addInfoBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]]; } - (void) addKeyboardPlaceholderMenuItem { @@ -948,6 +988,7 @@ - (void)showOSK { [[self.oskWindow window] makeKeyAndOrderFront:nil]; [[self.oskWindow window] setLevel:NSStatusWindowLevel]; [[self.oskWindow window] setTitle:self.oskWindowTitle]; + [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; } - (void)showAboutWindow { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m index 2e38dd4bc3a..d4ab67d10fe 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m @@ -12,6 +12,7 @@ #import "TextApiCompliance.h" #import "KMSettingsRepository.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @import Sentry; @interface KMInputMethodEventHandler () @@ -22,6 +23,7 @@ @interface KMInputMethodEventHandler () @property (nonatomic, retain) NSString* clientApplicationId; @property NSString *queuedText; @property BOOL contextChanged; +@property BOOL sentryTestingEnabled; @end @implementation KMInputMethodEventHandler @@ -33,9 +35,6 @@ @implementation KMInputMethodEventHandler CGKeyCode _keyCodeOfOriginalEvent; CGEventSourceRef _sourceFromOriginalEvent = nil; CGEventSourceRef _sourceForGeneratedEvent = nil; -NSMutableString* _easterEggForSentry = nil; -NSString* const kEasterEggText = @"Sentry force now"; -NSString* const kEasterEggKmxName = @"EnglishSpanish.kmx"; // This is the public initializer. - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { @@ -44,24 +43,12 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender { _generatedBackspaceCount = 0; _lowLevelBackspaceCount = 0; _queuedText = nil; - - BOOL forceSentryCrash = [KMSettingsRepository.shared readForceSentryError]; - - // In Xcode, if Keyman is the active IM and the settings include the - // forceSentryCrash flag and "English plus Spanish" is the current keyboard - // and you type "Sentry force now", it will force a simulated crash to - // test reporting to sentry.keyman.com - if (forceSentryCrash && [clientAppId isEqual: @"com.apple.dt.Xcode"]) { - os_log_debug([KMLogs testLog], "initWithClient, preparing to force Sentry crash."); - _easterEggForSentry = [[NSMutableString alloc] init]; - } - else - _easterEggForSentry = nil; - - [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { - [scope setTagValue:clientAppId forKey:@"clientAppId"]; - }]; + _apiCompliance = [[TextApiCompliance alloc]initWithClient:sender applicationId:clientAppId]; + os_log_info([KMLogs lifecycleLog], "KMInputMethodEventHandler initWithClient, clientAppId: %{public}@", clientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"KMInputMethodEventHandler initWithClient, clientAppId '%@'", clientAppId]]; + + [KMSentryHelper addClientAppIdTag:clientAppId]; return self; } @@ -110,9 +97,15 @@ - (KMEngine *)kme { - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender { CoreKeyOutput *output = nil; + if ([self.appDelegate canForceSentryEvents]) { + if ([self forceSentryEvent:event]) { + return NO; + } + } + output = [self processEventWithKeymanEngine:event in:sender]; + if (output == nil) { - [self checkEventForSentryEasterEgg:event]; return NO; } @@ -139,28 +132,28 @@ - (CoreKeyOutput*) processEventWithKeymanEngine:(NSEvent *)event in:(id) sender return coreKeyOutput; } -- (void)checkEventForSentryEasterEgg:(NSEvent *)event { - if (_easterEggForSentry != nil) { - NSString * kmxName = [[self.kme.kmx filePath] lastPathComponent]; - os_log_debug([KMLogs testLog], "Sentry - KMX name: %{public}@", kmxName); - if ([kmxName isEqualToString:kEasterEggKmxName]) { - NSUInteger len = [_easterEggForSentry length]; - os_log_debug([KMLogs testLog], "Sentry - Processing character(s): %{public}@", [event characters]); - if ([[event characters] characterAtIndex:0] == [kEasterEggText characterAtIndex:len]) { - NSString *characterToAdd = [kEasterEggText substringWithRange:NSMakeRange(len, 1)]; - os_log_debug([KMLogs testLog], "Sentry - Adding character to Easter Egg code string: %{public}@", characterToAdd); - [_easterEggForSentry appendString:characterToAdd]; - if ([_easterEggForSentry isEqualToString:kEasterEggText]) { - os_log_debug([KMLogs testLog], "Sentry - Forcing crash now"); - [SentrySDK crash]; - } - } - else if (len > 0) { - os_log_debug([KMLogs testLog], "Sentry - Clearing Easter Egg code string."); - [_easterEggForSentry setString:@""]; - } - } +/** + * When in the mode to test sentry, this method will force certain Sentry APIs to be called, + * such as forcing a crash or capturing a message, depending on the key being typed. + */ +- (BOOL)forceSentryEvent:(NSEvent *)event { + BOOL forcedEvent = YES; + + NSString *sentryCommand = event.characters; + if ([sentryCommand isEqualToString:@"{"]) { + os_log_debug([KMLogs testLog], "forceSentryEvent: forcing crash now"); + NSBeep(); + [SentrySDK crash]; + } else if ([sentryCommand isEqualToString:@"["]) { + os_log_debug([KMLogs testLog], "forceSentryEvent: forcing message now"); + NSBeep(); + [SentrySDK captureMessage:@"Forced test message"]; + } else { + os_log_debug([KMLogs testLog], "forceSentryEvent: unrecognized command"); + forcedEvent = NO; } + + return forcedEvent; } /** @@ -174,7 +167,8 @@ - (void)checkEventForSentryEasterEgg:(NSEvent *)event { */ - (void)handleBackspace:(NSEvent *)event { os_log_debug([KMLogs eventsLog], "KMInputMethodEventHandler handleBackspace, event = %{public}@", event); - + [KMSentryHelper addInfoBreadCrumb:@"user" message:@"handle backspace for non-compliant app"]; + if (self.generatedBackspaceCount > 0) { self.generatedBackspaceCount--; self.lowLevelBackspaceCount++; @@ -194,13 +188,10 @@ - (void)triggerInsertQueuedText:(NSEvent *)event { //MARK: Core-related key processing - (void)checkTextApiCompliance:(id)client { - // If the TextApiCompliance object is nil or the app or the context has changed, - // then create a new object for the current application and context. + // If the TextApiCompliance object is stale, create a new one for the current application and context. // The text api compliance may vary from one text field to another of the same app. - if ((self.apiCompliance == nil) || - (![self.apiCompliance.clientApplicationId isEqualTo:self.clientApplicationId]) || - self.contextChanged) { + if ([self textApiComplianceIsStale]) { self.apiCompliance = [[TextApiCompliance alloc]initWithClient:client applicationId:self.clientApplicationId]; os_log_debug([KMLogs complianceLog], "KMInputMethodHandler initWithClient checkTextApiCompliance: %{public}@", _apiCompliance); } else if (self.apiCompliance.isComplianceUncertain) { @@ -209,6 +200,33 @@ - (void)checkTextApiCompliance:(id)client { } } +- (BOOL)textApiComplianceIsStale { + BOOL stale = false; + NSString *complianceAppId = nil; + TextApiCompliance *currentApiCompliance = self.apiCompliance; + + // test for three scenarios in which the api compliance is stale + if (currentApiCompliance == nil) { // if we have no previous api compliance object + stale = true; + } else { // if we have one but for a different client + complianceAppId = self.apiCompliance.clientApplicationId; + if ([complianceAppId isNotEqualTo:self.clientApplicationId]) { + stale = true; + } else { + stale = false; + } + } + if (self.contextChanged) { // if the context has changed + stale = true; + } + + NSString *message = [NSString stringWithFormat:@"textApiComplianceIsStale = %@ for appId: %@, apiCompliance: %p, complianceAppId: %@", stale?@"true":@"false", self.clientApplicationId, currentApiCompliance, complianceAppId]; + [KMSentryHelper addDebugBreadCrumb:@"compliance" message:message]; + os_log_debug([KMLogs complianceLog], "%{public}@", message); + + return stale; +} + - (void) handleContextChangedByLowLevelEvent { if (self.appDelegate.contextChangedByLowLevelEvent) { if (!self.contextChanged) { @@ -230,6 +248,7 @@ - (BOOL)handleEvent:(NSEvent *)event client:(id)sender { [self handleContextChangedByLowLevelEvent]; if (event.type == NSEventTypeKeyDown) { + [KMSentryHelper addDebugBreadCrumb:@"event" message:@"handling keydown event"]; // indicates that our generated backspace event(s) are consumed // and we can insert text that followed the backspace(s) if (event.keyCode == kKeymanEventKeyCode) { @@ -422,6 +441,7 @@ -(void) persistOptions:(NSDictionary*)options{ NSString *value = [options objectForKey:key]; if(key && value) { os_log_debug([KMLogs keyLog], "persistOptions, key: %{public}@, value: %{public}@", key, value); + [KMSentryHelper addInfoBreadCrumb:@"event" message:@"persist options"]; [[KMSettingsRepository shared] writeOptionForSelectedKeyboard:key withValue:value]; } else { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h index 62a3aac35d1..e467e645bd7 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h @@ -15,7 +15,8 @@ extern NSString *const kInputMethodClientChangeNotification; @interface KMInputMethodLifecycle : NSObject + (KMInputMethodLifecycle *)shared; -+ (NSString*)getClientApplicationId; ++ (NSString*)getRunningApplicationId; +@property NSString *clientApplicationId; - (void)startLifecycle; - (void)activateClient:(id)client; - (void)deactivateClient:(id)client; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index 9b877a530e7..ba53d9ffa65 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -10,7 +10,7 @@ */ /** - * This class is needed because many activateServer and deactivateServer messages sent from macOS + * This class is needed because many activateServer and deactivateServer messages are sent from macOS * to KMInputController, but they are not particularly reliable. Keyman receives some messages when it * is not active and should not become active. It also receives messages when it is active, but there is no * need to change state. For example, when a menu is clicked with Keyman active, macOS will send a @@ -36,6 +36,7 @@ #import #import "KMSettingsRepository.h" #import +#import "KMSentryHelper.h" NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification"; NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; @@ -61,11 +62,12 @@ @interface KMInputMethodLifecycle() @property LifecycleState lifecycleState; @property NSString *inputSourceId; -@property NSString *clientApplicationId; @end @implementation KMInputMethodLifecycle +@synthesize lifecycleState = _lifecycleState; + + (KMInputMethodLifecycle *)shared { static KMInputMethodLifecycle *shared = nil; static dispatch_once_t onceToken; @@ -86,11 +88,40 @@ - (instancetype)init { return self; } +- (void)setLifecycleState:(LifecycleState)state { + _lifecycleState = state; + + // whenever the state is changed, update the Sentry tag + [self addLifecycleStateSentryTag]; +} + +- (void)addLifecycleStateSentryTag { + NSString *stateString = @"Unknown"; + switch (self.lifecycleState) { + case Started: + stateString = @"Started"; + break; + case Active: + stateString = @"Active"; + break; + case Inactive: + stateString = @"Inactive"; + break; + } + [KMSentryHelper addLifecycleStateTag:stateString]; + os_log_info([KMLogs lifecycleLog], "setLifecycleState: %{public}@", stateString); +} + + +- (LifecycleState)lifecycleState { + return _lifecycleState; +} + /** * called from Application Delgate during init */ - (void)startLifecycle { - _lifecycleState = Started; + self.lifecycleState = Started; } /** @@ -106,9 +137,10 @@ + (NSString*)getCurrentInputSourceId { /** * Get the bundle ID of the currently active text input client.. */ -+ (NSString*)getClientApplicationId { ++ (NSString*)getRunningApplicationId { NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; NSString *clientAppId = [currentApp bundleIdentifier]; + os_log_debug([KMLogs lifecycleLog], "getRunningApplicationId, frontmost: %{public}@", clientAppId); return clientAppId; } @@ -156,7 +188,7 @@ - (void)saveNewInputMethodState:(NSString*)newInputSourceId withAppId:(NSString* */ - (void)performTransition:(id)client { NSString *currentInputSource = [KMInputMethodLifecycle getCurrentInputSourceId]; - NSString *currentClientAppId = [KMInputMethodLifecycle getClientApplicationId]; + NSString *currentClientAppId = [KMInputMethodLifecycle getRunningApplicationId]; TransitionType transition = [self determineTransition:currentInputSource withAppId:currentClientAppId]; [self saveNewInputMethodState:currentInputSource withAppId:currentClientAppId]; @@ -166,7 +198,8 @@ - (void)performTransition:(id)client { os_log_info([KMLogs lifecycleLog], "performTransition: None, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); break; case Activate: - os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new application ID: %{public}@", currentClientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"activated input method '%@' for application ID '%@'", currentInputSource, currentClientAppId]]; /** * Perform two actions when activating the input method. * Change the client first which prepares the event handler. @@ -177,10 +210,12 @@ - (void)performTransition:(id)client { break; case Deactivate: os_log_info([KMLogs lifecycleLog], "performTransition: Deactivate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"deactivated input method '%@' for application ID '%@'", currentInputSource, currentClientAppId]]; [self deactivateInputMethod]; break; case ChangeClients: os_log_info([KMLogs lifecycleLog], "performTransition: ChangeClients, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"change clients for input method '%@' to application ID '%@'", currentInputSource, currentClientAppId]]; [self changeClient]; break; } @@ -214,7 +249,7 @@ - (void)performTransitionAfterDelay:(id)client { */ - (void)activateInputMethod { os_log_debug([KMLogs lifecycleLog], "activateInputMethod"); - _lifecycleState = Active; + self.lifecycleState = Active; [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self]; } @@ -223,7 +258,7 @@ - (void)activateInputMethod { */ - (void)deactivateInputMethod { os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod"); - _lifecycleState = Inactive; + self.lifecycleState = Inactive; [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self]; } @@ -231,7 +266,7 @@ - (void)deactivateInputMethod { * Does not change lifecycleState, just fires notification so that InputController knows to change the event handler */ - (void)changeClient { - os_log_debug([KMLogs lifecycleLog], "changeClient"); + os_log_debug([KMLogs lifecycleLog], "changeClient, posting kInputMethodClientChangeNotification"); [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodClientChangeNotification object:self]; } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h similarity index 84% rename from mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h rename to mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h index 002bc5023c1..f7f83702ec5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.h @@ -1,4 +1,4 @@ -/** +/* * Keyman is copyright (C) SIL International. MIT License. * * KMLogs.h @@ -6,7 +6,6 @@ * * Created by Shawn Schantz on 2024-05-16. * - * Contains methods to get singleton logger objects and constants for subsystem and category names. */ #import diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m similarity index 99% rename from mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m rename to mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m index f2d4dc4d534..6a8a5cd9570 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMLogs.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMLogs.m @@ -1,4 +1,4 @@ -/** +/* * Keyman is copyright (C) SIL International. MIT License. * * KMLogs.m diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h new file mode 100644 index 00000000000..75a5c272825 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h @@ -0,0 +1,28 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-01-13. + * + */ + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface KMSentryHelper : NSObject + ++ (void)addLifecycleStateTag: (NSString *)value; ++ (void)addApiComplianceTag: (NSString *)value; ++ (void)addArchitectureTag: (NSString *)value; ++ (void)addOskVisibleTag:(BOOL)value; ++ (void)addClientAppIdTag:(NSString *)value; ++ (void)addKeyboardTag:(NSString *)value; ++ (void)addHasAccessibilityTag:(BOOL)value; ++ (void)addActiveKeyboardCountTag:(NSUInteger)value; ++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText; ++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText; ++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText; + +@end + +NS_ASSUME_NONNULL_END diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m new file mode 100644 index 00000000000..7c37be9ad99 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m @@ -0,0 +1,98 @@ +/* + * Keyman is copyright (C) SIL Global. MIT License. + * + * Created by Shawn Schantz on 2025-01-13. + * + * Description + */ + +#import "KMSentryHelper.h" +@import Sentry; + +@implementation KMSentryHelper + +NSString * const kApiComplianceTagName = @"apiCompliance"; +NSString * const kArchitectureTagName = @"architecture"; +NSString * const kOskVisibleTagName = @"oskVisible"; +NSString * const kClientAppIdTagName = @"clientAppId"; +NSString * const kKeyboardTagName = @"keyboard"; +NSString * const kHasAccessibilityTagName = @"accessibilityEnabled"; +NSString * const kActiveKeyboardCountTagName = @"activeKeyboardCount"; +NSString * const kLifecycleStateTagName = @"lifecycleState"; + ++ (void)addLifecycleStateTag: (NSString *)value { + [self addCustomTag:kLifecycleStateTagName withValue:value]; +} + ++ (void)addApiComplianceTag: (NSString *)value { + [self addCustomTag:kApiComplianceTagName withValue:value]; +} + ++ (void)addArchitectureTag: (NSString *)value { + [self addCustomTag:kArchitectureTagName withValue:value]; +} + ++ (void)addOskVisibleTag:(BOOL)value { + [self addCustomTag:kOskVisibleTagName withValue:value?@"true":@"false"]; +} + ++ (void)addClientAppIdTag:(NSString *)value { + [self addCustomTag:kClientAppIdTagName withValue:value]; +} + ++ (void)addKeyboardTag:(NSString *)value { + [self addCustomTag:kKeyboardTagName withValue:value]; +} + ++ (void)addHasAccessibilityTag:(BOOL)value { + [self addCustomTag:kHasAccessibilityTagName withValue:value?@"true":@"false"]; +} + ++ (void)addActiveKeyboardCountTag:(NSUInteger)value { + [self addCustomTag:kActiveKeyboardCountTagName withValue:[[NSNumber numberWithUnsignedLong:value] stringValue]]; +} + +/** + *assign custom keyboard tag in Sentry for the given name and value + */ ++ (void)addCustomTag:(NSString *)tagName withValue:(NSString *)value { + [SentrySDK configureScope:^(SentryScope * _Nonnull scope) { + [scope setTagValue:value forKey:tagName]; + }]; +} + +/** + *add a Sentry breadcrumb message of level Info with the specified category + */ ++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText { + SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init]; + crumb.level = kSentryLevelInfo; + crumb.category = category; + crumb.message = messageText; + [SentrySDK addBreadcrumb:crumb]; +} + +/** + *add a Sentry breadcrumb message of level debug with the specified category + */ ++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText { + SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init]; + crumb.level = kSentryLevelDebug; + crumb.category = category; + crumb.message = messageText; + [SentrySDK addBreadcrumb:crumb]; +} + +/** + *add a Sentry breadcrumb message of type 'user' and level debug with the specified category + */ ++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText { + SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init]; + crumb.type = @"user"; + crumb.level = kSentryLevelInfo; + crumb.category = category; + crumb.message = messageText; + [SentrySDK addBreadcrumb:crumb]; +} + +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m index b3824fa8976..d4f457c7544 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m @@ -11,6 +11,7 @@ #import "KMInputMethodLifecycle.h" #import "KMSettingsRepository.h" #import "KMLogs.h" +#import "KMSentryHelper.h" @interface OSKWindowController () @property (nonatomic, strong) NSButton *helpButton; @@ -76,6 +77,7 @@ - (void)windowWillClose:(NSNotification *)notification { // whenever the OSK is closing clear all of its modifier keys [self.oskView clearOskModifiers]; + [KMSentryHelper addOskVisibleTag:NO]; } - (void)helpAction:(id)sender { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m index e83439794b6..81f50c5edda 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m @@ -36,6 +36,7 @@ #import #import "KMInputMethodAppDelegate.h" #import "KMLogs.h" +#import "KMSentryHelper.h" // this is the user managed list of non-compliant apps persisted in User Defaults NSString *const kKMLegacyApps = @"KMLegacyApps"; @@ -62,6 +63,10 @@ -(instancetype)initWithClient:(id) client applicationId:(NSString *)appId { _clientApplicationId = appId; _complianceUncertain = YES; _initialSelection = NSMakeRange(NSNotFound, NSNotFound); + + NSString *message = [NSString stringWithFormat:@"TextApiCompliance initWithClient, client: %p applicationId: %@", client, appId]; + [KMSentryHelper addDebugBreadCrumb:@"compliance" message:message]; + os_log_debug([KMLogs complianceLog], "%{public}@", message); // if we do not have hard-coded noncompliance, then test the app if (![self applyNoncompliantAppLists:appId]) { @@ -76,6 +81,16 @@ -(NSString *)description return [NSString stringWithFormat:@"complianceUncertain: %d, hasCompliantSelectionApi: %d, canReadText: %d, canReplaceText: %d, mustBackspaceUsingEvents: %d, clientApplicationId: %@, client: %@", self.complianceUncertain, self.hasCompliantSelectionApi, [self canReadText], [self canReplaceText], [self mustBackspaceUsingEvents], _clientApplicationId, _client]; } +/** + * For Sentry, create a short description as the tag value is limited to 200 characters. + * We don't know how long the clientAppId will be, but this should not be well below 200 characters. + * If the text were to exceed 200 characters, then Sentry would display an error rather than truncating. + */ +-(NSString *)shortSentryTagDescription +{ + return [NSString stringWithFormat:@"uncertain: %@, compliant: %@, clientAppId: %@, ", self.complianceUncertain?@"true":@"false", self.hasCompliantSelectionApi?@"true":@"false", _clientApplicationId]; +} + /** test to see if the API selectedRange functions properly for the text input client */ -(void) checkCompliance:(id) client { // confirm that the API actually exists (this always seems to return true) @@ -91,6 +106,7 @@ -(void) checkCompliance:(id) client { [self checkComplianceUsingInitialSelection]; } os_log_debug([KMLogs complianceLog], "checkCompliance workingSelectionApi for app %{public}@: set to %{public}@", self.clientApplicationId, self.complianceUncertain?@"YES":@"NO"); + [KMSentryHelper addApiComplianceTag:self.shortSentryTagDescription]; } -(void) checkComplianceUsingInitialSelection { @@ -135,6 +151,7 @@ -(void) checkComplianceAfterInsert:(id) client delete:(NSString *)textToDelete i } os_log_info([KMLogs complianceLog], "checkComplianceAfterInsert, self.hasWorkingSelectionApi = %{public}@ for app %{public}@", self.hasCompliantSelectionApi?@"YES":@"NO", self.clientApplicationId); + [KMSentryHelper addApiComplianceTag:self.shortSentryTagDescription]; } - (BOOL)validateNewLocation:(NSUInteger)location delete:(NSString *)textToDelete {