diff --git a/CHANGELOG.md b/CHANGELOG.md index ccf5e9d67d9..9b5d0740f41 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- feat: Send Locale with Events (#1539) + ## 7.6.1 - fix: iOS13-Swift build (#1522) diff --git a/Sources/Sentry/SentryCrashIntegration.m b/Sources/Sentry/SentryCrashIntegration.m index 439a23d6dc1..190609459ea 100644 --- a/Sources/Sentry/SentryCrashIntegration.m +++ b/Sources/Sentry/SentryCrashIntegration.m @@ -23,6 +23,9 @@ static dispatch_once_t installationToken = 0; static SentryCrashInstallationReporter *installation = nil; +static NSString *const DEVICE_KEY = @"device"; +static NSString *const LOCALE_KEY = @"locale"; + @interface SentryCrashIntegration () @@ -159,6 +162,8 @@ - (void)uninstall installationToken = 0; } [self.crashAdapter deactivateAsyncHooks]; + + [NSNotificationCenter.defaultCenter removeObserver:self]; } - (void)configureScope @@ -229,10 +234,13 @@ - (void)configureScope [deviceData setValue:systemInfo[@"bootTime"] forKey:@"boot_time"]; [deviceData setValue:systemInfo[@"timezone"] forKey:@"timezone"]; - [outerScope setContextValue:deviceData forKey:@"device"]; + NSString *locale = + [[NSLocale autoupdatingCurrentLocale] objectForKey:NSLocaleIdentifier]; + [deviceData setValue:locale forKey:LOCALE_KEY]; - // APP + [outerScope setContextValue:deviceData forKey:DEVICE_KEY]; + // APP NSMutableDictionary *appData = [NSMutableDictionary new]; NSDictionary *infoDict = [[NSBundle mainBundle] infoDictionary]; @@ -264,6 +272,29 @@ - (void)configureScope [outerScope addObserver:self.scopeObserver]; }]; } + + [NSNotificationCenter.defaultCenter addObserver:self + selector:@selector(currentLocaleDidChange) + name:NSCurrentLocaleDidChangeNotification + object:nil]; +} + +- (void)currentLocaleDidChange +{ + [SentrySDK.currentHub configureScope:^(SentryScope *_Nonnull scope) { + NSMutableDictionary *device; + if (scope.contextDictionary != nil && scope.contextDictionary[DEVICE_KEY] != nil) { + device = [[NSMutableDictionary alloc] + initWithDictionary:scope.contextDictionary[DEVICE_KEY]]; + } else { + device = [NSMutableDictionary new]; + } + + NSString *locale = [[NSLocale autoupdatingCurrentLocale] objectForKey:NSLocaleIdentifier]; + device[LOCALE_KEY] = locale; + + [scope setContextValue:device forKey:DEVICE_KEY]; + }]; } @end diff --git a/Sources/Sentry/include/SentryScope+Private.h b/Sources/Sentry/include/SentryScope+Private.h index c6e423c528f..6d392ae93a0 100644 --- a/Sources/Sentry/include/SentryScope+Private.h +++ b/Sources/Sentry/include/SentryScope+Private.h @@ -14,6 +14,9 @@ SentryScope (Private) @property (atomic, strong) SentryUser *_Nullable userObject; +@property (atomic, strong) + NSMutableDictionary *> *contextDictionary; + - (void)addObserver:(id)observer; @end diff --git a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift index 66d0e2e0096..e31eaa06fe2 100644 --- a/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift +++ b/Tests/SentryTests/Integrations/SentryCrash/SentryCrashIntegrationTests.swift @@ -143,10 +143,8 @@ class SentryCrashIntegrationTests: XCTestCase { #endif func testEndSessionAsCrashed_NoClientSet() { - let hub = SentryHub(client: nil, andScope: nil) - SentrySDK.setCurrentHub(hub) + let (sut, _) = givenSutWithGlobalHub() - let sut = fixture.getSut() sut.install(with: Options()) let fileManager = fixture.fileManager @@ -168,9 +166,8 @@ class SentryCrashIntegrationTests: XCTestCase { } func testEndSessionAsCrashed_NoCurrentSession() { - SentrySDK.setCurrentHub(fixture.hub) + let (sut, _) = givenSutWithGlobalHub() - let sut = fixture.getSut() sut.install(with: Options()) let fileManager = fixture.fileManager @@ -204,16 +201,53 @@ class SentryCrashIntegrationTests: XCTestCase { XCTAssertTrue(fixture.sentryCrash.deactivateAsyncHooksCalled) } + func testUninstall_DoesNotUpdateLocale_OnLocaleDidChangeNotification() { + let (sut, hub) = givenSutWithGlobalHub() + + sut.install(with: Options()) + + let locale = "garbage" + setLocaleToGlobalScope(locale: locale) + + sut.uninstall() + + TestNotificationCenter.localeDidChange() + + assertLocaleOnHub(locale: locale, hub: hub) + } + func testOSCorrectlySetToScopeContext() { - let hub = fixture.hub - SentrySDK.setCurrentHub(hub) + let (sut, hub) = givenSutWithGlobalHub() - let sut = fixture.getSut() sut.install(with: Options()) - let context = hub.scope.serialize()["context"]as? [String: Any] ?? ["": ""] + assertContext(context: hub.scope.contextDictionary as? [String: Any] ?? ["": ""]) + } + + func testLocaleChanged_NoDeviceContext_SetsCurrentLocale() { + let (sut, hub) = givenSutWithGlobalHub() - assertContext(context: context) + sut.install(with: Options()) + + SentrySDK.configureScope { scope in + scope.removeContext(key: "device") + } + + TestNotificationCenter.localeDidChange() + + assertLocaleOnHub(locale: Locale.autoupdatingCurrent.identifier, hub: hub) + } + + func testLocaleChanged_DifferentLocale_SetsCurrentLocale() { + let (sut, hub) = givenSutWithGlobalHub() + + sut.install(with: Options()) + + setLocaleToGlobalScope(locale: "garbage") + + TestNotificationCenter.localeDidChange() + + assertLocaleOnHub(locale: Locale.autoupdatingCurrent.identifier, hub: hub) } private func givenCurrentSession() -> SentrySession { @@ -238,6 +272,26 @@ class SentryCrashIntegrationTests: XCTestCase { } #endif + private func givenSutWithGlobalHub() -> (SentryCrashIntegration, SentryHub) { + let sut = fixture.getSut() + let hub = fixture.hub + SentrySDK.setCurrentHub(hub) + + return (sut, hub) + } + + private func setLocaleToGlobalScope(locale: String) { + SentrySDK.configureScope { scope in + guard var device = scope.contextDictionary["device"] as? [String: Any] else { + XCTFail("No device found on context.") + return + } + + device["locale"] = locale + scope.setContext(value: device, key: "device") + } + } + private func assertUserInfoField(userInfo: [AnyHashable: Any], key: String, expected: String) { if let actual = userInfo[key] as? String { XCTAssertEqual(expected, actual) @@ -279,6 +333,19 @@ class SentryCrashIntegrationTests: XCTestCase { XCTAssertEqual("tvOS", os["name"] as? String) XCTAssertEqual(UIDevice.current.systemVersion, os["version"] as? String) #endif + + XCTAssertEqual(Locale.autoupdatingCurrent.identifier, device["locale"] as? String) + } + + private func assertLocaleOnHub(locale: String, hub: SentryHub) { + let context = hub.scope.contextDictionary as? [String: Any] ?? ["": ""] + + guard let device = context["device"] as? [String: Any] else { + XCTFail("No device found on context.") + return + } + + XCTAssertEqual(locale, device["locale"] as? String) } private func advanceTime(bySeconds: TimeInterval) { diff --git a/Tests/SentryTests/Integrations/TestNotificationCenter.swift b/Tests/SentryTests/Integrations/TestNotificationCenter.swift index 468c8bfab6e..df7e69941a3 100644 --- a/Tests/SentryTests/Integrations/TestNotificationCenter.swift +++ b/Tests/SentryTests/Integrations/TestNotificationCenter.swift @@ -65,4 +65,8 @@ class TestNotificationCenter { NotificationCenter.default.post(Notification(name: UIWindow.didBecomeVisibleNotification)) #endif } + + static func localeDidChange() { + NotificationCenter.default.post(Notification(name: NSLocale.currentLocaleDidChangeNotification)) + } }