From 8e833c8314be4b98503b22ba5f07de59d59573bc Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Mon, 13 Jan 2025 14:08:22 +0700
Subject: [PATCH 1/9] chore(mac): reworked sentry testing

can now force crashes or send messages by typing a
single character when sentry testing is enabled
in the UserDefaults
---
 .../Keyman4MacIM/KMInputController.m          |  4 +-
 .../Keyman4MacIM/KMInputMethodAppDelegate.h   |  1 +
 .../Keyman4MacIM/KMInputMethodAppDelegate.m   | 40 ++++++++++-
 .../Keyman4MacIM/KMInputMethodEventHandler.m  | 67 ++++++++-----------
 .../Keyman4MacIM/KMInputMethodLifecycle.h     |  3 +-
 .../Keyman4MacIM/KMInputMethodLifecycle.m     |  5 +-
 6 files changed, 74 insertions(+), 46 deletions(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
index e97be3e389c..acefcc84170 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
@@ -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];
 
 }
 
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
index fe06fa603a8..d41bf258999 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)isSentryTestingEnabled;
 @end
 
 #endif /* KMInputMethodAppDelegate_h */
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index b938279197b..9a5a98de473 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -48,6 +48,7 @@ @interface KMInputMethodAppDelegate ()
 @property (nonatomic, strong) KMPackageReader *packageReader;
 @property BOOL receivedKeyDownFromOsk;
 @property NSEventModifierFlags oskEventModifiers;
+@property (nonatomic, assign) BOOL sentryTestingEnabled;
 @end
 
 @implementation KMInputMethodAppDelegate
@@ -143,11 +144,18 @@ - (void)inputMethodActivated:(NSNotification *)notification {
     os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap...");
     CGEventTapEnable(self.lowLevelEventTap, YES);
   }
-
+  
   if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) {
     os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
     [self showOSK];
   }
+
+  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+  crumb.level = kSentryLevelInfo;
+  crumb.category = @"startup";
+  //crumb.message = [NSString stringWithFormat:@"Authenticated user %@", user.email];
+  crumb.message = [NSString stringWithFormat:@"inputMethodActivated"];
+  [SentrySDK addBreadcrumb:crumb];
 }
 
 - (KeymanVersionInfo)versionInfo {
@@ -181,6 +189,7 @@ -(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
   [self setDefaultKeymanMenuItems];
   [self updateKeyboardMenuItems];
   [self setPostLaunchKeymanSentryTags];
+  self.sentryTestingEnabled = [KMSettingsRepository.shared readForceSentryError];
   // [SentrySDK captureMessage:@"Starting Keyman [test message]"];
 }
 
@@ -196,6 +205,29 @@ - (void)startSentry {
   }];
 }
 
+/**
+ * Determine whether sentry testing is enabled.
+ * When it is enabled, then typing certain characters will cause Sentry events or crashes to occur.
+ *
+ * To enable, set the flag is UserDefaults
+ * `defaults write keyman.inputmethod.Keyman KMForceSentryError 1`
+ * Also, use the SIL Euro Latin keyboard and type in the Stickies app
+ */
+- (BOOL)isSentryTestingEnabled {
+  NSString* const testKeyboardName = @"sil_euro_latin.kmx";
+  NSString* const testClientApplicationId = @"com.apple.Stickies";
+  BOOL testingEnabled = NO;
+
+  if (self.sentryTestingEnabled) {
+    NSString * kmxName = [[self.kmx filePath] lastPathComponent];
+    NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId];
+    testingEnabled = [kmxName isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId];
+    os_log_info([KMLogs testLog], "isSentryTestingEnabled, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxName, applicationId, testingEnabled?@"YES":@"NO");
+  }
+    
+  return testingEnabled;
+}
+
 - (void)setPostLaunchKeymanSentryTags {
   NSString *keyboardFileName = [self.kmx.filePath lastPathComponent];
    os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName);
@@ -825,6 +857,12 @@ - (void) setDefaultSelectedKeyboard {
   [self setSelectedKeyboard:keyboardName inMenuItem:menuItem];
   [self setSelectedKeyboard:keyboardName];
   [self setContextBuffer:nil];
+  
+  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+  crumb.level = kSentryLevelInfo;
+  crumb.category = @"startup";
+  crumb.message = [NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName];
+  [SentrySDK addBreadcrumb:crumb];
 }
 
 - (void) addKeyboardPlaceholderMenuItem {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
index 2e38dd4bc3a..91ebe13f33f 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
@@ -22,6 +22,7 @@ @interface KMInputMethodEventHandler ()
 @property (nonatomic, retain) NSString* clientApplicationId;
 @property NSString *queuedText;
 @property BOOL contextChanged;
+@property BOOL sentryTestingEnabled;
 @end
 
 @implementation KMInputMethodEventHandler
@@ -33,9 +34,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,20 +42,7 @@ - (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"];
   }];
@@ -110,9 +95,15 @@ - (KMEngine *)kme {
 - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender {
   CoreKeyOutput *output = nil;
   
+  if ([self.appDelegate isSentryTestingEnabled]) {
+    if ([self forceSentryEvent:event]) {
+      return NO;
+    }
+  }
+  
   output = [self processEventWithKeymanEngine:event in:sender];
+
   if (output == nil) {
-    [self checkEventForSentryEasterEgg:event];
     return NO;
   }
   
@@ -139,28 +130,26 @@ - (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");
+    [SentrySDK crash];
+  } else if ([sentryCommand isEqualToString:@"["]) {
+    os_log_debug([KMLogs testLog], "forceSentryEvent: forcing message now");
+    [SentrySDK captureMessage:@"Forced test message"];
+  } else {
+    os_log_debug([KMLogs testLog], "forceSentryEvent: unrecognized command");
+    forcedEvent = NO;
   }
+  
+  return forcedEvent;
 }
 
 /**
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..0b82c830061 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
@@ -61,7 +61,6 @@ @interface KMInputMethodLifecycle()
 
 @property LifecycleState lifecycleState;
 @property NSString *inputSourceId;
-@property NSString *clientApplicationId;
 @end
 
 @implementation KMInputMethodLifecycle
@@ -106,7 +105,7 @@ + (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];
   return clientAppId;
@@ -156,7 +155,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];

From 76d0d69e6b5e42a3e27a4ff6ed305667ee83cf95 Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Thu, 16 Jan 2025 15:26:03 +0700
Subject: [PATCH 2/9] chore(mac): add KMSentryHelper class, and new tags and
 breadcrumbs

---
 .../Keyman4MacIM.xcodeproj/project.pbxproj    | 18 ++++-
 .../KMConfigurationWindowController.m         | 33 ++++++--
 .../KMDownloadKBWindowController.m            |  1 +
 .../Keyman4MacIM/KMInputController.m          |  6 +-
 .../Keyman4MacIM/KMInputMethodAppDelegate.h   |  2 +-
 .../Keyman4MacIM/KMInputMethodAppDelegate.m   | 74 ++++++++++--------
 .../Keyman4MacIM/KMInputMethodEventHandler.m  |  7 +-
 .../Keyman4MacIM/KMInputMethodLifecycle.m     |  8 +-
 .../Keyman4MacIM/{ => Logging}/KMLogs.h       |  3 +-
 .../Keyman4MacIM/{ => Logging}/KMLogs.m       |  2 +-
 .../Keyman4MacIM/Logging/KMSentryHelper.h     | 25 ++++++
 .../Keyman4MacIM/Logging/KMSentryHelper.m     | 77 +++++++++++++++++++
 .../OnScreenKeyboard/OSKWindowController.m    |  2 +
 .../Keyman4MacIM/TextApiCompliance.m          | 13 ++++
 14 files changed, 217 insertions(+), 54 deletions(-)
 rename mac/Keyman4MacIM/Keyman4MacIM/{ => Logging}/KMLogs.h (84%)
 rename mac/Keyman4MacIM/Keyman4MacIM/{ => Logging}/KMLogs.m (99%)
 create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
 create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m

diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
index 483998a16e0..3436ed498a3 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
+++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
@@ -35,6 +35,7 @@
 		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 */; };
 		37A245C12565DFA6000BBF92 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 37A245C02565DFA6000BBF92 /* Assets.xcassets */; };
 		37AE5C9D239A7B770086CC7C /* qrcode.min.js in Resources */ = {isa = PBXBuildFile; fileRef = 37AE5C9C239A7B770086CC7C /* qrcode.min.js */; };
@@ -275,6 +276,8 @@
 		29D470972C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/KMKeyboardHelpWindowController.strings; sourceTree = "<group>"; };
 		29D470982C648D5200224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/MainMenu.strings; sourceTree = "<group>"; };
 		29D470992C648D7100224B4F /* el */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = el; path = el.lproj/Localizable.strings; sourceTree = "<group>"; };
+		29DBBF652D35053E00D8E33C /* KMSentryHelper.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMSentryHelper.h; sourceTree = "<group>"; };
+		29DBBF662D35053E00D8E33C /* KMSentryHelper.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMSentryHelper.m; sourceTree = "<group>"; };
 		29DD5F432CFEF88000683388 /* SILAndikaV1RGB.png */ = {isa = PBXFileReference; lastKnownFileType = image.png; path = SILAndikaV1RGB.png; sourceTree = "<group>"; };
 		29DD8400276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
 		29DD8401276C49E20066A16E /* am */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = am; path = am.lproj/preferences.strings; sourceTree = "<group>"; };
@@ -433,6 +436,17 @@
 			path = Privacy;
 			sourceTree = "<group>";
 		};
+		29DBBF642D34FFE000D8E33C /* Logging */ = {
+			isa = PBXGroup;
+			children = (
+				29B4A0D42BF7675A00682049 /* KMLogs.h */,
+				29B4A0D32BF7675A00682049 /* KMLogs.m */,
+				29DBBF652D35053E00D8E33C /* KMSentryHelper.h */,
+				29DBBF662D35053E00D8E33C /* KMSentryHelper.m */,
+			);
+			path = Logging;
+			sourceTree = "<group>";
+		};
 		2CDC487CDFFBB26D621D73BD /* Pods */ = {
 			isa = PBXGroup;
 			children = (
@@ -564,8 +578,6 @@
 		989C9C0A1A7876DE00A20425 /* Keyman4MacIM */ = {
 			isa = PBXGroup;
 			children = (
-				29B4A0D42BF7675A00682049 /* KMLogs.h */,
-				29B4A0D32BF7675A00682049 /* KMLogs.m */,
 				299ABD6F29ECE75B00AA5948 /* KeySender.m */,
 				299ABD7029ECE75B00AA5948 /* KeySender.h */,
 				D861B03D2C5747F70003675E /* KMSettingsRepository.h */,
@@ -575,6 +587,7 @@
 				297A501128DF4D360074EB1B /* Privacy */,
 				98FE105B1B4DE86300525F54 /* Categories */,
 				98D6DA791A799EE700B09822 /* Frameworks */,
+				29DBBF642D34FFE000D8E33C /* Logging */,
 				989C9C121A7876DE00A20425 /* Images.xcassets */,
 				98BDD3681BC3511200FAC7C4 /* Keyman4MacIM.entitlements */,
 				98BF92401BF01CBE0002126A /* KMAboutWindow */,
@@ -1000,6 +1013,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 */,
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
index 202be65ef5e..142606152fa 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;
@@ -363,18 +364,23 @@ - (void)checkBoxAction:(id)sender {
   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}@", kmxFilePath);
+    NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", kmxFilePath];
+    [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}@", kmxFilePath);
+    NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", kmxFilePath];
+    [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 +395,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,16 +413,25 @@ - (void)removeAction:(id)sender {
   NSButton *deleteButton = (NSButton *)sender;
   NSDictionary *info = [self.tableContents objectAtIndex:deleteButton.tag];
   NSString *deleteKeyboardMessage = NSLocalizedString(@"message-confirm-delete-keyboard", nil);
+  NSString *keyboardName = nil;
+
+  if ([info objectForKey:@"HeaderTitle"] != nil) {
+    keyboardName = [info objectForKey:@"HeaderTitle"];
+    [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
+  } else {
+    keyboardName = [info objectForKey:kKMKeyboardNameKey];
+    [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
+  }
   
-  if ([info objectForKey:@"HeaderTitle"] != nil)
-    [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:@"HeaderTitle"]]];
-  else
-    [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, [info objectForKey:kKMKeyboardNameKey]]];
-  
+  os_log_debug([KMLogs testLog], "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 testLog], "removeAction for keyboardName: %{public}@", keyboardName);
+      [KMSentryHelper addBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]];
     }
     self.deleteAlertView = nil;
   }];
@@ -453,7 +469,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 addBreadCrumb:@"configure" message:@"install package file"];
+
   BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile];
   
   if (!didUnzip) {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m
index 4b5ce44dea8..2fec9f8b320 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m
@@ -9,6 +9,7 @@
 #import "KMDownloadKBWindowController.h"
 #import "KMInputMethodAppDelegate.h"
 #import "KMLogs.h"
+#import "KMSentryHelper.h"
 
 @interface KMDownloadKBWindowController ()
 @property (nonatomic, weak) IBOutlet WebView *webView;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
index acefcc84170..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
 
@@ -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 d41bf258999..eade3d6d2f4 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h
@@ -124,7 +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)isSentryTestingEnabled;
+- (BOOL)canForceSentryEvents;
 @end
 
 #endif /* KMInputMethodAppDelegate_h */
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index 9a5a98de473..ad0249643cb 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -17,6 +17,7 @@
 #import "KMPackageInfo.h"
 #import "PrivacyConsent.h"
 #import "KMLogs.h"
+#import "KMSentryHelper.h"
 @import Sentry;
 
 NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification";
@@ -125,10 +126,12 @@ - (void)initCompletion {
 - (void)inputMethodDeactivated:(NSNotification *)notification {
   if ([self.oskWindow.window isVisible]) {
     os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK");
+    [KMSentryHelper addBreadCrumb:@"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]?@"true":@"false"];
 
   if (self.lowLevelEventTap) {
     os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap");
@@ -144,18 +147,14 @@ - (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 addBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
     [self showOSK];
   }
-
-  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
-  crumb.level = kSentryLevelInfo;
-  crumb.category = @"startup";
-  //crumb.message = [NSString stringWithFormat:@"Authenticated user %@", user.email];
-  crumb.message = [NSString stringWithFormat:@"inputMethodActivated"];
-  [SentrySDK addBreadcrumb:crumb];
 }
 
 - (KeymanVersionInfo)versionInfo {
@@ -206,39 +205,45 @@ - (void)startSentry {
 }
 
 /**
- * Determine whether sentry testing is enabled.
- * When it is enabled, then typing certain characters will cause Sentry events or crashes to occur.
+ * 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 is UserDefaults
+ * 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)isSentryTestingEnabled {
+- (BOOL)canForceSentryEvents {
   NSString* const testKeyboardName = @"sil_euro_latin.kmx";
   NSString* const testClientApplicationId = @"com.apple.Stickies";
-  BOOL testingEnabled = NO;
+  BOOL canForce = NO;
 
   if (self.sentryTestingEnabled) {
     NSString * kmxName = [[self.kmx filePath] lastPathComponent];
     NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId];
-    testingEnabled = [kmxName isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId];
-    os_log_info([KMLogs testLog], "isSentryTestingEnabled, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxName, applicationId, testingEnabled?@"YES":@"NO");
+    canForce = [[self getKmxFileName] isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId];
+    os_log_info([KMLogs testLog], "canForceSentryEvents, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxName, applicationId, canForce?@"YES":@"NO");
   }
     
-  return testingEnabled;
+  return canForce;
+}
+
+- (NSString*) getKmxFileName {
+  return [[self.kmx filePath] lastPathComponent];
 }
 
 - (void)setPostLaunchKeymanSentryTags {
   NSString *keyboardFileName = [self.kmx.filePath lastPathComponent];
    os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName);
 
-  NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"Yes":@"No";
+  NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"true":@"false";
 
-   // assign custom keyboard tag in Sentry to initial keyboard
-   [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
-      [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"];
-      [scope setTagValue:keyboardFileName forKey:@"keyboard"];
-   }];
+  // assign custom keyboard tag in Sentry for accessibility and initial keyboard
+  [KMSentryHelper addKeyboardTag:keyboardFileName];
+  [KMSentryHelper addHasAccessibilityTag:hasAccessibility];
+//   [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
+//      [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"];
+//      [scope setTagValue:keyboardFileName forKey:@"keyboard"];
+//   }];
 }
 
 #ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE
@@ -438,22 +443,28 @@ - (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 addBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [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"];
-   }];
+  [KMSentryHelper addKeyboardTag:keyboardInfo.keyboardId];
 }
 
 - (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 {
@@ -858,11 +869,7 @@ - (void) setDefaultSelectedKeyboard {
   [self setSelectedKeyboard:keyboardName];
   [self setContextBuffer:nil];
   
-  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
-  crumb.level = kSentryLevelInfo;
-  crumb.category = @"startup";
-  crumb.message = [NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName];
-  [SentrySDK addBreadcrumb:crumb];
+  [KMSentryHelper addBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]];
 }
 
 - (void) addKeyboardPlaceholderMenuItem {
@@ -986,6 +993,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]?@"true":@"false"];
 }
 
 - (void)showAboutWindow {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
index 91ebe13f33f..340a06a04eb 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 ()
@@ -43,9 +44,7 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender {
   _lowLevelBackspaceCount = 0;
   _queuedText = nil;
 
-  [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
-      [scope setTagValue:clientAppId forKey:@"clientAppId"];
-  }];
+  [KMSentryHelper addClientAppIdTag:clientAppId];
 
   return self;
 }
@@ -95,7 +94,7 @@ - (KMEngine *)kme {
 - (BOOL) handleEventWithKeymanEngine:(NSEvent *)event in:(id) sender {
   CoreKeyOutput *output = nil;
   
-  if ([self.appDelegate isSentryTestingEnabled]) {
+  if ([self.appDelegate canForceSentryEvents]) {
     if ([self forceSentryEvent:event]) {
       return NO;
     }
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
index 0b82c830061..543d7fe4d8d 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 <AppKit/AppKit.h>
 #import "KMSettingsRepository.h"
 #import <Carbon/Carbon.h>
+#import "KMSentryHelper.h"
 
 NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification";
 NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification";
@@ -165,7 +166,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 addBreadCrumb:@"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.
@@ -176,10 +178,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 addBreadCrumb:@"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 addBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"change clients for input method '%@' to application ID '%@'", currentInputSource, currentClientAppId]];
       [self changeClient];
       break;
   }
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 <Foundation/Foundation.h>
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..e9d6936c0e6
--- /dev/null
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
@@ -0,0 +1,25 @@
+/*
+ * Keyman is copyright (C) SIL International. MIT License.
+ *
+ * Created by Shawn Schantz on 2025-01-13.
+ *
+ */
+
+#import <Foundation/Foundation.h>
+
+NS_ASSUME_NONNULL_BEGIN
+
+@interface KMSentryHelper : NSObject
+
++ (void)addApiComplianceTag: (NSString *)value;
++ (void)addOskVisibleTag:(NSString *)value;
++ (void)addClientAppIdTag:(NSString *)value;
++ (void)addKeyboardTag:(NSString *)value;
++ (void)addHasAccessibilityTag:(NSString *)value;
++ (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText;
++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText;
++ (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map;
+
+@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..5278e36037d
--- /dev/null
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
@@ -0,0 +1,77 @@
+/*
+ * Keyman is copyright (C) SIL International. MIT License.
+ *
+ * Created by Shawn Schantz on 2025-01-13.
+ *
+ * Description
+ */
+
+#import "KMSentryHelper.h"
+@import Sentry;
+
+@implementation KMSentryHelper
+
+NSString * const kApiComplianceTagName = @"apiCompliance";
+NSString * const kOskVisibleTagName = @"oskVisible";
+NSString * const kClientAppIdTagName = @"clientAppId";
+NSString * const kKeyboardTagName = @"keyboard";
+NSString * const kHasAccessibilityTagName = @"accessibilityEnabled";
+
++ (void)addApiComplianceTag: (NSString *)value {
+  [self addCustomTag:kApiComplianceTagName withValue:value];
+}
+
++ (void)addOskVisibleTag:(NSString *)value {
+  [self addCustomTag:kOskVisibleTagName withValue:value];
+}
+
++ (void)addClientAppIdTag:(NSString *)value {
+  [self addCustomTag:kClientAppIdTagName withValue:value];
+}
+
++ (void)addKeyboardTag:(NSString *)value {
+  [self addCustomTag:kKeyboardTagName withValue:value];
+}
+
++ (void)addHasAccessibilityTag:(NSString *)value {
+  [self addCustomTag:kHasAccessibilityTagName withValue:value];
+}
+
+/**
+ *assign custom keyboard tag in Sentry to initial keyboard
+ */
++ (void)addCustomTag:(NSString *)tagName withValue:(NSString *)value {
+  [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
+    [scope setTagValue:value forKey:tagName];
+  }];
+}
+
++ (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText {
+  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+  crumb.level = kSentryLevelInfo;
+  crumb.category = category;
+  crumb.message = messageText;
+  //crumb.message = [NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName];
+  [SentrySDK addBreadcrumb:crumb];
+}
+
++ (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText {
+  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+  crumb.type = @"user";
+  crumb.level = kSentryLevelInfo;
+  crumb.category = category;
+  crumb.message = messageText;
+  //crumb.message = [NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName];
+  [SentrySDK addBreadcrumb:crumb];
+}
+
++ (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map {
+  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+  crumb.level = kSentryLevelInfo;
+  crumb.category = category;
+  crumb.message = messageText;
+  crumb.data = map;
+  [SentrySDK addBreadcrumb:crumb];
+}
+
+@end
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
index b3824fa8976..df0e723534d 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:@"false"];
 }
 
 - (void)helpAction:(id)sender {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
index e83439794b6..4d77bada0f1 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
@@ -36,6 +36,7 @@
 #import <InputMethodKit/InputMethodKit.h>
 #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";
@@ -76,6 +77,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 +102,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 +147,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  {

From 162dc40f2cea367c0ed298044c4292cc58318094 Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Mon, 20 Jan 2025 14:17:15 +0700
Subject: [PATCH 3/9] chore(mac): added architecture tag, more breadcrumbs

---
 .../KMConfigurationWindowController.m         | 17 +++++----
 .../Keyman4MacIM/KMInputMethodAppDelegate.m   | 38 +++++++++++--------
 .../Keyman4MacIM/KMInputMethodEventHandler.m  |  8 +++-
 .../Keyman4MacIM/Logging/KMSentryHelper.h     |  6 ++-
 .../Keyman4MacIM/Logging/KMSentryHelper.m     | 18 +++++++--
 .../OnScreenKeyboard/OSKWindowController.m    |  2 +-
 6 files changed, 58 insertions(+), 31 deletions(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
index 142606152fa..6f63bcfccf3 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
@@ -361,18 +361,19 @@ - (BOOL)tableView:(NSTableView *)tableView acceptDrop:(id<NSDraggingInfo>)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], "enabling active keyboard: %{public}@", kmxFilePath);
-    NSString *message = [NSString stringWithFormat:@"enabling active keyboard: %@", 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);
-    NSString *message = [NSString stringWithFormat:@"disabling active keyboard: %@", 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];
@@ -423,21 +424,23 @@ - (void)removeAction:(id)sender {
     [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
   }
   
-  os_log_debug([KMLogs testLog], "entered removeAction for keyboardName: %{public}@", 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 testLog], "removeAction for keyboardName: %{public}@", keyboardName);
-      [KMSentryHelper addBreadCrumb:@"configure" message:[NSString stringWithFormat:@"remove keyboard: %@", keyboardName]];
+      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];
   
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index ad0249643cb..66842926b3f 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -18,8 +18,17 @@
 #import "PrivacyConsent.h"
 #import "KMLogs.h"
 #import "KMSentryHelper.h"
+//#include "TargetConditionals.h"
 @import Sentry;
 
+#if TARGET_CPU_ARM64
+NSString *processorType = @"ARM";
+#elif TARGET_CPU_X86_64
+NSString *processorType = @"Intel";
+#elif
+NSString *processorType = @"Unknown";
+#endif
+
 NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification";
 
 @implementation NSString (VersionNumbers)
@@ -131,7 +140,7 @@ - (void)inputMethodDeactivated:(NSNotification *)notification {
   } else {
     os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden");
   }
-  [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]?@"true":@"false"];
+  [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
 
   if (self.lowLevelEventTap) {
     os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap");
@@ -218,10 +227,10 @@ - (BOOL)canForceSentryEvents {
   BOOL canForce = NO;
 
   if (self.sentryTestingEnabled) {
-    NSString * kmxName = [[self.kmx filePath] lastPathComponent];
     NSString * applicationId = [KMInputMethodLifecycle.shared clientApplicationId];
-    canForce = [[self getKmxFileName] isEqualToString:testKeyboardName] && [applicationId isEqualToString:testClientApplicationId];
-    os_log_info([KMLogs testLog], "canForceSentryEvents, self.sentryTestingEnabled: %{public}@ kmxName: %{public}@ applicationId: %{public}@ testingEnabled: %{public}@", self.sentryTestingEnabled?@"YES":@"NO", kmxName, applicationId, canForce?@"YES":@"NO");
+    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;
@@ -235,15 +244,12 @@ - (void)setPostLaunchKeymanSentryTags {
   NSString *keyboardFileName = [self.kmx.filePath lastPathComponent];
    os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName);
 
-  NSString *hasAccessibility = [PrivacyConsent.shared checkAccessibility]?@"true":@"false";
-
   // assign custom keyboard tag in Sentry for accessibility and initial keyboard
   [KMSentryHelper addKeyboardTag:keyboardFileName];
-  [KMSentryHelper addHasAccessibilityTag:hasAccessibility];
-//   [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
-//      [scope setTagValue:hasAccessibility forKey:@"accessibilityEnabled"];
-//      [scope setTagValue:keyboardFileName forKey:@"keyboard"];
-//   }];
+  [KMSentryHelper addHasAccessibilityTag:[PrivacyConsent.shared checkAccessibility]];
+  [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
+  [KMSentryHelper addArchitectureTag:processorType];
+  
 }
 
 #ifdef USE_ALERT_SHOW_HELP_TO_FORCE_EASTER_EGG_CRASH_FROM_ENGINE
@@ -354,6 +360,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
           os_log_debug([KMLogs eventsLog], "*** kKeymanEventKeyCode = 0xFF");
         } else {
           if ([OSKView isOskKeyDownEvent:event]) {
+            [KMSentryHelper addBreadCrumb:@"event" message:@"processing OSK-generated keydown event"];
             NSEventModifierFlags oskEventModifiers = [OSKView extractModifierFlagsFromOskEvent:event];
             appDelegate.receivedKeyDownFromOsk = YES;
             appDelegate.oskEventModifiers = oskEventModifiers;
@@ -445,13 +452,11 @@ - (void)loadKeyboardFromKmxFile:(KMXFile *)kmx {
 
   os_log_info([KMLogs keyboardLog], "loadKeyboardFromKmxFile, keyboard info: %{public}@", keyboardInfo);
   [KMSentryHelper addBreadCrumb:@"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
-  [KMSentryHelper addKeyboardTag:keyboardInfo.keyboardId];
 }
 
 - (void)setKvk:(KVKFile *)kvk {
@@ -659,6 +664,8 @@ - (NSMutableArray *)activeKeyboards {
   if (!_activeKeyboards) {
     os_log_debug([KMLogs dataLog], "initializing activeKeyboards");
     _activeKeyboards = [[KMSettingsRepository.shared readActiveKeyboards] mutableCopy];
+    
+    [KMSentryHelper addActiveKeyboardCountTag:_activeKeyboards.count];
   }
 
   return _activeKeyboards;
@@ -669,6 +676,7 @@ - (void)saveActiveKeyboards {
   [KMSettingsRepository.shared writeActiveKeyboards:_activeKeyboards];
   [self resetActiveKeyboards];
   [self updateKeyboardMenuItems];
+  [KMSentryHelper addActiveKeyboardCountTag:self.activeKeyboards.count];
 }
 
 - (void)clearActiveKeyboards {
@@ -993,7 +1001,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]?@"true":@"false"];
+  [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
 }
 
 - (void)showAboutWindow {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
index 340a06a04eb..d900cf073ab 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
@@ -45,7 +45,6 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender {
   _queuedText = nil;
 
   [KMSentryHelper addClientAppIdTag:clientAppId];
-
   return self;
 }
 
@@ -139,9 +138,11 @@ - (BOOL)forceSentryEvent:(NSEvent *)event {
   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");
@@ -162,7 +163,8 @@ - (BOOL)forceSentryEvent:(NSEvent *)event {
  */
 - (void)handleBackspace:(NSEvent *)event {
   os_log_debug([KMLogs eventsLog], "KMInputMethodEventHandler handleBackspace, event = %{public}@", event);
-  
+  [KMSentryHelper addBreadCrumb:@"user" message:@"handle backspace for non-compliant app"];
+
   if (self.generatedBackspaceCount > 0) {
     self.generatedBackspaceCount--;
     self.lowLevelBackspaceCount++;
@@ -218,6 +220,7 @@ - (BOOL)handleEvent:(NSEvent *)event client:(id)sender {
   [self handleContextChangedByLowLevelEvent];
   
   if (event.type == NSEventTypeKeyDown) {
+    [KMSentryHelper addBreadCrumb:@"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) {
@@ -410,6 +413,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 addBreadCrumb:@"event" message:@"persist options"];
       [[KMSettingsRepository shared] writeOptionForSelectedKeyboard:key withValue:value];
     }
     else {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
index e9d6936c0e6..5699cef1d2b 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
@@ -12,10 +12,12 @@ NS_ASSUME_NONNULL_BEGIN
 @interface KMSentryHelper : NSObject
 
 + (void)addApiComplianceTag: (NSString *)value;
-+ (void)addOskVisibleTag:(NSString *)value;
++ (void)addArchitectureTag: (NSString *)value;
++ (void)addOskVisibleTag:(BOOL)value;
 + (void)addClientAppIdTag:(NSString *)value;
 + (void)addKeyboardTag:(NSString *)value;
-+ (void)addHasAccessibilityTag:(NSString *)value;
++ (void)addHasAccessibilityTag:(BOOL)value;
++ (void)addActiveKeyboardCountTag:(NSUInteger)value;
 + (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
index 5278e36037d..66656b24d5b 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
@@ -12,17 +12,23 @@
 @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";
 
 + (void)addApiComplianceTag: (NSString *)value {
   [self addCustomTag:kApiComplianceTagName withValue:value];
 }
 
-+ (void)addOskVisibleTag:(NSString *)value {
-  [self addCustomTag:kOskVisibleTagName 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 {
@@ -33,8 +39,12 @@ + (void)addKeyboardTag:(NSString *)value {
   [self addCustomTag:kKeyboardTagName withValue:value];
 }
 
-+ (void)addHasAccessibilityTag:(NSString *)value {
-  [self addCustomTag:kHasAccessibilityTagName 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]];
 }
 
 /**
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
index df0e723534d..d4f457c7544 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m
@@ -77,7 +77,7 @@ - (void)windowWillClose:(NSNotification *)notification {
   
   // whenever the OSK is closing clear all of its modifier keys
   [self.oskView clearOskModifiers];
-  [KMSentryHelper addOskVisibleTag:@"false"];
+  [KMSentryHelper addOskVisibleTag:NO];
 }
 
 - (void)helpAction:(id)sender {

From 2b390274c87545b1221a263a9a38bd3772356184 Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Mon, 20 Jan 2025 16:17:36 +0700
Subject: [PATCH 4/9] chore(mac): add KMSentryHelper to test project

resolves link error when running unit tests
---
 mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
index 3436ed498a3..87b005309cb 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
+++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj
@@ -37,6 +37,7 @@
 		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 */; };
@@ -1052,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 */,

From d6ce0af77e5bd07ccae568f3823cf1a473968573 Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Wed, 22 Jan 2025 11:28:50 +0700
Subject: [PATCH 5/9] chore(mac): added tag for lifecycleState

Fixes: #11623
---
 .../Keyman4MacIM/KMInputMethodEventHandler.m  | 40 ++++++++++++++++---
 .../Keyman4MacIM/KMInputMethodLifecycle.m     | 40 +++++++++++++++++--
 .../Keyman4MacIM/Logging/KMSentryHelper.h     |  2 +
 .../Keyman4MacIM/Logging/KMSentryHelper.m     | 15 ++++++-
 .../Keyman4MacIM/TextApiCompliance.m          |  4 ++
 5 files changed, 89 insertions(+), 12 deletions(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
index d900cf073ab..fcb381013e7 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
@@ -44,6 +44,10 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender {
   _lowLevelBackspaceCount = 0;
   _queuedText = nil;
 
+  _apiCompliance = [[TextApiCompliance alloc]initWithClient:sender applicationId:clientAppId];
+  os_log_info([KMLogs lifecycleLog], "KMInputMethodEventHandler initWithClient, clientAppId: %{public}@", clientAppId);
+  [KMSentryHelper addBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"KMInputMethodEventHandler initWithClient, clientAppId '%@'", clientAppId]];
+
   [KMSentryHelper addClientAppIdTag:clientAppId];
   return self;
 }
@@ -184,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) {
@@ -199,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) {
@@ -220,7 +248,7 @@ - (BOOL)handleEvent:(NSEvent *)event client:(id)sender {
   [self handleContextChangedByLowLevelEvent];
   
   if (event.type == NSEventTypeKeyDown) {
-    [KMSentryHelper addBreadCrumb:@"event" message:@"handling keydown event"];
+    [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) {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
index 543d7fe4d8d..6c98c17b3c3 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
@@ -66,6 +66,8 @@ @interface KMInputMethodLifecycle()
 
 @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;
 }
 
 /**
@@ -109,6 +140,7 @@ + (NSString*)getCurrentInputSourceId {
 + (NSString*)getRunningApplicationId {
   NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
   NSString *clientAppId = [currentApp bundleIdentifier];
+  os_log_debug([KMLogs lifecycleLog], "getRunningApplicationId, frontmost: %{public}@", clientAppId);
   return clientAppId;
 }
 
@@ -217,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];
 }
 
@@ -226,7 +258,7 @@ - (void)activateInputMethod {
  */
 - (void)deactivateInputMethod {
   os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod");
-  _lifecycleState = Inactive;
+  self.lifecycleState = Inactive;
   [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self];
 }
 
@@ -234,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/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
index 5699cef1d2b..adbce67a538 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
@@ -11,6 +11,7 @@ NS_ASSUME_NONNULL_BEGIN
 
 @interface KMSentryHelper : NSObject
 
++ (void)addLifecycleStateTag: (NSString *)value;
 + (void)addApiComplianceTag: (NSString *)value;
 + (void)addArchitectureTag: (NSString *)value;
 + (void)addOskVisibleTag:(BOOL)value;
@@ -19,6 +20,7 @@ NS_ASSUME_NONNULL_BEGIN
 + (void)addHasAccessibilityTag:(BOOL)value;
 + (void)addActiveKeyboardCountTag:(NSUInteger)value;
 + (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText;
++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map;
 
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
index 66656b24d5b..b28e0b5b76d 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
@@ -18,6 +18,11 @@ @implementation KMSentryHelper
 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];
@@ -61,7 +66,14 @@ + (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText {
   crumb.level = kSentryLevelInfo;
   crumb.category = category;
   crumb.message = messageText;
-  //crumb.message = [NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName];
+  [SentrySDK addBreadcrumb:crumb];
+}
+
++ (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText {
+  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
+  crumb.level = kSentryLevelDebug;
+  crumb.category = category;
+  crumb.message = messageText;
   [SentrySDK addBreadcrumb:crumb];
 }
 
@@ -71,7 +83,6 @@ + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText {
   crumb.level = kSentryLevelInfo;
   crumb.category = category;
   crumb.message = messageText;
-  //crumb.message = [NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName];
   [SentrySDK addBreadcrumb:crumb];
 }
 
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
index 4d77bada0f1..81f50c5edda 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/TextApiCompliance.m
@@ -63,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]) {

From 0ec2664126941726d6ddd07c58f8aa427150d4d6 Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Wed, 22 Jan 2025 12:52:44 +0700
Subject: [PATCH 6/9] chore(mac): clean up comments

---
 .../Keyman4MacIM/KMInputMethodAppDelegate.m       | 15 +--------------
 1 file changed, 1 insertion(+), 14 deletions(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index 66842926b3f..3c2fd7f7109 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -18,7 +18,6 @@
 #import "PrivacyConsent.h"
 #import "KMLogs.h"
 #import "KMSentryHelper.h"
-//#include "TargetConditionals.h"
 @import Sentry;
 
 #if TARGET_CPU_ARM64
@@ -198,7 +197,6 @@ -(void)applicationDidFinishLaunching:(NSNotification *)aNotification {
   [self updateKeyboardMenuItems];
   [self setPostLaunchKeymanSentryTags];
   self.sentryTestingEnabled = [KMSettingsRepository.shared readForceSentryError];
-  // [SentrySDK captureMessage:@"Starting Keyman [test message]"];
 }
 
 - (void)startSentry {
@@ -209,7 +207,6 @@ - (void)startSentry {
     options.dsn = @"https://960f8b8e574c46e3be385d60ce8e1fea@o1005580.ingest.sentry.io/5983522";
     options.releaseName = releaseName;
     options.environment = keymanVersionInfo.sentryEnvironment;
-    //options.debug = YES;
   }];
 }
 
@@ -244,23 +241,13 @@ - (void)setPostLaunchKeymanSentryTags {
   NSString *keyboardFileName = [self.kmx.filePath lastPathComponent];
    os_log_info([KMLogs keyboardLog], "initial kmx set to %{public}@", keyboardFileName);
 
-  // assign custom keyboard tag in Sentry for accessibility and initial keyboard
+  // assign custom keyboard tags in Sentry
   [KMSentryHelper addKeyboardTag:keyboardFileName];
   [KMSentryHelper addHasAccessibilityTag:[PrivacyConsent.shared checkAccessibility]];
   [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]];
   [KMSentryHelper addArchitectureTag:processorType];
-  
 }
 
-#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;
-}
-#endif
-
 - (void)handleURLEvent:(NSAppleEventDescriptor*)event withReplyEvent:(NSAppleEventDescriptor*)replyEvent {
   
   [self processURL:[[event paramDescriptorForKeyword:keyDirectObject] stringValue]];

From c9c47e8438a99793b8bb1595ebfeed1edad482be Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Thu, 23 Jan 2025 11:42:47 +0700
Subject: [PATCH 7/9] chore(mac): fixed else statement for architecture tag

---
 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index 3c2fd7f7109..b3020e1a55d 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -24,7 +24,7 @@
 NSString *processorType = @"ARM";
 #elif TARGET_CPU_X86_64
 NSString *processorType = @"Intel";
-#elif
+#else
 NSString *processorType = @"Unknown";
 #endif
 

From 077229e89c7de60d56385e3ebf47a8f8c2340abc Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Mon, 27 Jan 2025 09:21:26 +0700
Subject: [PATCH 8/9] chore(mac): added comments and renamed function

---
 .../KMConfigurationWindowController.m         |  2 +-
 .../Keyman4MacIM/KMInputMethodAppDelegate.m   | 10 ++++----
 .../Keyman4MacIM/KMInputMethodEventHandler.m  |  6 ++---
 .../Keyman4MacIM/KMInputMethodLifecycle.m     |  6 ++---
 .../Keyman4MacIM/Logging/KMSentryHelper.h     |  4 ++--
 .../Keyman4MacIM/Logging/KMSentryHelper.m     | 24 +++++++++----------
 6 files changed, 26 insertions(+), 26 deletions(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
index 6f63bcfccf3..8a35afd31c9 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
@@ -472,7 +472,7 @@ - (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 addBreadCrumb:@"configure" message:@"install package file"];
+  [KMSentryHelper addInfoBreadCrumb:@"configure" message:@"install package file"];
 
   BOOL didUnzip = [self.AppDelegate unzipFile:kmpFile];
   
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
index b3020e1a55d..cfff0d80cd6 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
@@ -134,7 +134,7 @@ - (void)initCompletion {
 - (void)inputMethodDeactivated:(NSNotification *)notification {
   if ([self.oskWindow.window isVisible]) {
     os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK");
-    [KMSentryHelper addBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"];
+    [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");
@@ -160,7 +160,7 @@ - (void)inputMethodActivated:(NSNotification *)notification {
 
   if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) {
     os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
-    [KMSentryHelper addBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
+    [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"];
     [self showOSK];
   }
 }
@@ -347,7 +347,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
           os_log_debug([KMLogs eventsLog], "*** kKeymanEventKeyCode = 0xFF");
         } else {
           if ([OSKView isOskKeyDownEvent:event]) {
-            [KMSentryHelper addBreadCrumb:@"event" message:@"processing OSK-generated keydown event"];
+            [KMSentryHelper addInfoBreadCrumb:@"event" message:@"processing OSK-generated keydown event"];
             NSEventModifierFlags oskEventModifiers = [OSKView extractModifierFlagsFromOskEvent:event];
             appDelegate.receivedKeyDownFromOsk = YES;
             appDelegate.oskEventModifiers = oskEventModifiers;
@@ -438,7 +438,7 @@ - (void)loadKeyboardFromKmxFile:(KMXFile *)kmx {
   CoreKeyboardInfo *keyboardInfo = [self.kme loadKeyboardFromKmxFile:kmx];
 
   os_log_info([KMLogs keyboardLog], "loadKeyboardFromKmxFile, keyboard info: %{public}@", keyboardInfo);
-  [KMSentryHelper addBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [self getKmxFileName]]];
+  [KMSentryHelper addInfoBreadCrumb:@"configure" message:[NSString stringWithFormat:@"loadKeyboardFromKmxFile: %@", [self getKmxFileName]]];
   [KMSentryHelper addKeyboardTag:[self getKmxFileName]];
 
   _modifierMapping = [[KMModifierMapping alloc] init:keyboardInfo];
@@ -864,7 +864,7 @@ - (void) setDefaultSelectedKeyboard {
   [self setSelectedKeyboard:keyboardName];
   [self setContextBuffer:nil];
   
-  [KMSentryHelper addBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]];
+  [KMSentryHelper addInfoBreadCrumb:@"startup" message:[NSString stringWithFormat:@"setDefaultSelectedKeyboard: %@", keyboardName]];
 }
 
 - (void) addKeyboardPlaceholderMenuItem {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
index fcb381013e7..d4ab67d10fe 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodEventHandler.m
@@ -46,7 +46,7 @@ - (instancetype)initWithClient:(NSString *)clientAppId client:(id) sender {
 
   _apiCompliance = [[TextApiCompliance alloc]initWithClient:sender applicationId:clientAppId];
   os_log_info([KMLogs lifecycleLog], "KMInputMethodEventHandler initWithClient, clientAppId: %{public}@", clientAppId);
-  [KMSentryHelper addBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"KMInputMethodEventHandler initWithClient, clientAppId '%@'", clientAppId]];
+  [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"KMInputMethodEventHandler initWithClient, clientAppId '%@'", clientAppId]];
 
   [KMSentryHelper addClientAppIdTag:clientAppId];
   return self;
@@ -167,7 +167,7 @@ - (BOOL)forceSentryEvent:(NSEvent *)event {
  */
 - (void)handleBackspace:(NSEvent *)event {
   os_log_debug([KMLogs eventsLog], "KMInputMethodEventHandler handleBackspace, event = %{public}@", event);
-  [KMSentryHelper addBreadCrumb:@"user" message:@"handle backspace for non-compliant app"];
+  [KMSentryHelper addInfoBreadCrumb:@"user" message:@"handle backspace for non-compliant app"];
 
   if (self.generatedBackspaceCount > 0) {
     self.generatedBackspaceCount--;
@@ -441,7 +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 addBreadCrumb:@"event" message:@"persist options"];
+      [KMSentryHelper addInfoBreadCrumb:@"event" message:@"persist options"];
       [[KMSettingsRepository shared] writeOptionForSelectedKeyboard:key withValue:value];
     }
     else {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
index 6c98c17b3c3..ba53d9ffa65 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
@@ -199,7 +199,7 @@ - (void)performTransition:(id)client {
      break;
     case Activate:
       os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new application ID: %{public}@", currentClientAppId);
-      [KMSentryHelper addBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"activated input method '%@' for application ID '%@'", currentInputSource, 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.
@@ -210,12 +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 addBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"deactivated input method '%@' for application ID '%@'", 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 addBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"change clients for input method '%@' to application ID '%@'", currentInputSource, currentClientAppId]];
+      [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:[NSString stringWithFormat:@"change clients for input method '%@' to application ID '%@'", currentInputSource, currentClientAppId]];
       [self changeClient];
       break;
   }
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
index adbce67a538..8a7d458967c 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
@@ -1,5 +1,5 @@
 /*
- * Keyman is copyright (C) SIL International. MIT License.
+ * Keyman is copyright (C) SIL Global. MIT License.
  *
  * Created by Shawn Schantz on 2025-01-13.
  *
@@ -19,7 +19,7 @@ NS_ASSUME_NONNULL_BEGIN
 + (void)addKeyboardTag:(NSString *)value;
 + (void)addHasAccessibilityTag:(BOOL)value;
 + (void)addActiveKeyboardCountTag:(NSUInteger)value;
-+ (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText;
++ (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
index b28e0b5b76d..7c37be9ad99 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.m
@@ -1,5 +1,5 @@
 /*
- * Keyman is copyright (C) SIL International. MIT License.
+ * Keyman is copyright (C) SIL Global. MIT License.
  *
  * Created by Shawn Schantz on 2025-01-13.
  *
@@ -53,7 +53,7 @@ + (void)addActiveKeyboardCountTag:(NSUInteger)value {
 }
 
 /**
- *assign custom keyboard tag in Sentry to initial keyboard
+ *assign custom keyboard tag in Sentry for the given name and value
  */
 + (void)addCustomTag:(NSString *)tagName withValue:(NSString *)value {
   [SentrySDK configureScope:^(SentryScope * _Nonnull scope) {
@@ -61,7 +61,10 @@ + (void)addCustomTag:(NSString *)tagName withValue:(NSString *)value {
   }];
 }
 
-+ (void)addBreadCrumb:(NSString *)category message:(NSString *)messageText {
+/**
+ *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;
@@ -69,6 +72,9 @@ + (void)addBreadCrumb:(NSString *)category message:(NSString *)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;
@@ -77,6 +83,9 @@ + (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)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";
@@ -86,13 +95,4 @@ + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText {
   [SentrySDK addBreadcrumb:crumb];
 }
 
-+ (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map {
-  SentryBreadcrumb *crumb = [[SentryBreadcrumb alloc] init];
-  crumb.level = kSentryLevelInfo;
-  crumb.category = category;
-  crumb.message = messageText;
-  crumb.data = map;
-  [SentrySDK addBreadcrumb:crumb];
-}
-
 @end

From e5820f532237812b63c32e7a103a57020dd3d920 Mon Sep 17 00:00:00 2001
From: sgschantz <shawn_schantz@sil.org>
Date: Mon, 27 Jan 2025 12:52:41 +0700
Subject: [PATCH 9/9] chore(mac): remove extraneous code

---
 .../KMConfiguration/KMConfigurationWindowController.m         | 4 +---
 .../KMDownloadKeyboard/KMDownloadKBWindowController.m         | 1 -
 mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h        | 1 -
 3 files changed, 1 insertion(+), 5 deletions(-)

diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
index 8a35afd31c9..2b06db4cade 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m
@@ -418,12 +418,10 @@ - (void)removeAction:(id)sender {
 
   if ([info objectForKey:@"HeaderTitle"] != nil) {
     keyboardName = [info objectForKey:@"HeaderTitle"];
-    [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
   } else {
     keyboardName = [info objectForKey:kKMKeyboardNameKey];
-    [self.deleteAlertView setMessageText:[NSString localizedStringWithFormat:deleteKeyboardMessage, keyboardName]];
   }
-  
+  [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)  {
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m
index 2fec9f8b320..4b5ce44dea8 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m
+++ b/mac/Keyman4MacIM/Keyman4MacIM/KMDownloadKeyboard/KMDownloadKBWindowController.m
@@ -9,7 +9,6 @@
 #import "KMDownloadKBWindowController.h"
 #import "KMInputMethodAppDelegate.h"
 #import "KMLogs.h"
-#import "KMSentryHelper.h"
 
 @interface KMDownloadKBWindowController ()
 @property (nonatomic, weak) IBOutlet WebView *webView;
diff --git a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
index 8a7d458967c..75a5c272825 100644
--- a/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
+++ b/mac/Keyman4MacIM/Keyman4MacIM/Logging/KMSentryHelper.h
@@ -22,7 +22,6 @@ NS_ASSUME_NONNULL_BEGIN
 + (void)addInfoBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addDebugBreadCrumb:(NSString *)category message:(NSString *)messageText;
 + (void)addUserBreadCrumb:(NSString *)category message:(NSString *)messageText;
-+ (void)addDetailedBreadCrumb:(NSString *)category message:(NSString *)messageText data:( NSDictionary *) map;
 
 @end