diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a3715ac5..cd47c09fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,8 @@ ### Fixes +- Fixed crashes when adding breadcrumbs on iOS/macOS. This was caused by invalid date parsing and corrupted string data in the native bridge responsible for scope sync ([#2327](https://github.com/getsentry/sentry-unity/pull/2327)) +- Fixed input handling in samples to work with old and new input system ([#2319](https://github.com/getsentry/sentry-unity/pull/2319)) - The SDK now captures exceptions on WebGL through the logging integration instead of the incompatible log handler, providing better stack trace support . ([#2322](https://github.com/getsentry/sentry-unity/pull/2322)) - Fixed input handling in samples to work with old and new input system. ([#2319](https://github.com/getsentry/sentry-unity/pull/2319)) diff --git a/package-dev/Plugins/iOS/SentryNativeBridge.m b/package-dev/Plugins/iOS/SentryNativeBridge.m index e1e204285..be28d5a6d 100644 --- a/package-dev/Plugins/iOS/SentryNativeBridge.m +++ b/package-dev/Plugins/iOS/SentryNativeBridge.m @@ -6,6 +6,39 @@ NS_ASSUME_NONNULL_BEGIN +static NSDateFormatter *_Nullable sentry_cachedISO8601Formatter(void) { + static NSDateFormatter *formatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSDateFormatter alloc] init]; + formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'"; + formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + }); + return formatter; +} + +static inline NSString *_NSStringOrNil(const char *value) +{ + return value ? [NSString stringWithUTF8String:value] : nil; +} + +static inline NSNumber *_NSNumberOrNil(int32_t value) +{ + return value == 0 ? nil : @(value); +} + +static inline NSNumber *_NSBoolOrNil(int8_t value) +{ + if (value == 0) { + return @NO; + } + if (value == 1) { + return @YES; + } + return nil; +} + // macOS only // On iOS, the SDK is linked statically so we don't need to dlopen() it. int SentryNativeBridgeLoadLibrary() { return 1; } @@ -64,24 +97,26 @@ void SentryNativeBridgeAddBreadcrumb( return; } + NSString *timestampString = _NSStringOrNil(timestamp); + NSString *messageString = _NSStringOrNil(message); + NSString *typeString = _NSStringOrNil(type); + NSString *categoryString = _NSStringOrNil(category) ?: @"default"; // Category cannot be nil + [SentrySDK configureScope:^(SentryScope *scope) { SentryBreadcrumb *breadcrumb = [[SentryBreadcrumb alloc] initWithLevel:level - category:(category ? [NSString stringWithUTF8String:category] : nil)]; + category:categoryString]; - if (timestamp != NULL) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:NSCalendarIdentifierISO8601]; - breadcrumb.timestamp = - [dateFormatter dateFromString:[NSString stringWithUTF8String:timestamp]]; + if (timestampString != nil && timestampString.length > 0) { + breadcrumb.timestamp = [sentry_cachedISO8601Formatter() dateFromString:timestampString]; } - if (message != NULL) { - breadcrumb.message = [NSString stringWithUTF8String:message]; + if (messageString != nil) { + breadcrumb.message = messageString; } - if (type != NULL) { - breadcrumb.type = [NSString stringWithUTF8String:type]; + if (typeString != nil) { + breadcrumb.type = typeString; } [scope addBreadcrumb:breadcrumb]; @@ -94,13 +129,11 @@ void SentryNativeBridgeSetExtra(const char *key, const char *value) return; } + NSString *keyString = [NSString stringWithUTF8String:key]; + NSString *valueString = _NSStringOrNil(value); + [SentrySDK configureScope:^(SentryScope *scope) { - if (value != NULL) { - [scope setExtraValue:[NSString stringWithUTF8String:value] - forKey:[NSString stringWithUTF8String:key]]; - } else { - [scope removeExtraForKey:[NSString stringWithUTF8String:key]]; - } + [scope setExtraValue:valueString forKey:keyString]; }]; } @@ -110,13 +143,11 @@ void SentryNativeBridgeSetTag(const char *key, const char *value) return; } + NSString *keyString = [NSString stringWithUTF8String:key]; + NSString *valueString = _NSStringOrNil(value); + [SentrySDK configureScope:^(SentryScope *scope) { - if (value != NULL) { - [scope setTagValue:[NSString stringWithUTF8String:value] - forKey:[NSString stringWithUTF8String:key]]; - } else { - [scope removeTagForKey:[NSString stringWithUTF8String:key]]; - } + [scope setTagValue:valueString forKey:keyString]; }]; } @@ -126,35 +157,28 @@ void SentryNativeBridgeUnsetTag(const char *key) return; } - [SentrySDK configureScope:^( - SentryScope *scope) { [scope removeTagForKey:[NSString stringWithUTF8String:key]]; }]; + NSString *keyString = [NSString stringWithUTF8String:key]; + + [SentrySDK configureScope:^(SentryScope *scope) { + [scope removeTagForKey:keyString]; + }]; } void SentryNativeBridgeSetUser( const char *email, const char *userId, const char *ipAddress, const char *username) { - if (email == NULL && userId == NULL && ipAddress == NULL && username == NULL) { - return; - } - + NSString *emailString = _NSStringOrNil(email); + NSString *userIdString = _NSStringOrNil(userId); + NSString *ipAddressString = _NSStringOrNil(ipAddress); + NSString *usernameString = _NSStringOrNil(username); + [SentrySDK configureScope:^(SentryScope *scope) { SentryUser *user = [[SentryUser alloc] init]; - if (email != NULL) { - user.email = [NSString stringWithUTF8String:email]; - } - - if (userId != NULL) { - user.userId = [NSString stringWithUTF8String:userId]; - } - - if (ipAddress != NULL) { - user.ipAddress = [NSString stringWithUTF8String:ipAddress]; - } - - if (username != NULL) { - user.username = [NSString stringWithUTF8String:username]; - } + user.email = emailString; + user.userId = userIdString; + user.ipAddress = ipAddressString; + user.username = usernameString; [scope setUser:user]; }]; @@ -184,42 +208,21 @@ void SentryNativeBridgeSetTrace(const char *traceId, const char *spanId) NSString *traceIdStr = [NSString stringWithUTF8String:traceId]; NSString *spanIdStr = [NSString stringWithUTF8String:spanId]; - + // This is a workaround to deal with SentryId living inside the Swift header Class sentryIdClass = NSClassFromString(@"_TtC6Sentry8SentryId"); Class sentrySpanIdClass = NSClassFromString(@"SentrySpanId"); - + if (sentryIdClass && sentrySpanIdClass) { id sentryTraceId = [[sentryIdClass alloc] initWithUUIDString:traceIdStr]; id sentrySpanId = [[sentrySpanIdClass alloc] initWithValue:spanIdStr]; - + if (sentryTraceId && sentrySpanId) { [PrivateSentrySDKOnly setTrace:sentryTraceId spanId:sentrySpanId]; } } } -static inline NSString *_NSStringOrNil(const char *value) -{ - return value ? [NSString stringWithUTF8String:value] : nil; -} - -static inline NSNumber *_NSNumberOrNil(int32_t value) -{ - return value == 0 ? nil : @(value); -} - -static inline NSNumber *_NSBoolOrNil(int8_t value) -{ - if (value == 0) { - return @NO; - } - if (value == 1) { - return @YES; - } - return nil; -} - void SentryNativeBridgeWriteScope( // clang-format off // // const char *AppStartTime, // const char *AppBuildType, @@ -288,4 +291,4 @@ void SentryNativeBridgeWriteScope( // clang-format off }]; } -NS_ASSUME_NONNULL_END \ No newline at end of file +NS_ASSUME_NONNULL_END diff --git a/package-dev/Plugins/macOS/SentryNativeBridge.m b/package-dev/Plugins/macOS/SentryNativeBridge.m index 8156671aa..f8845abef 100644 --- a/package-dev/Plugins/macOS/SentryNativeBridge.m +++ b/package-dev/Plugins/macOS/SentryNativeBridge.m @@ -1,6 +1,39 @@ #import #include +static NSDateFormatter *_Nullable sentry_cachedISO8601Formatter(void) { + static NSDateFormatter *formatter = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + formatter = [[NSDateFormatter alloc] init]; + [formatter setDateFormat:@"yyyy-MM-dd'T'HH:mm:ss'Z'"]; + formatter.timeZone = [NSTimeZone timeZoneWithAbbreviation:@"UTC"]; + formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"]; + }); + return formatter; +} + +static inline NSString *_NSStringOrNil(const char *value) +{ + return value ? [NSString stringWithUTF8String:value] : nil; +} + +static inline NSString *_NSNumberOrNil(int32_t value) +{ + return value == 0 ? nil : @(value); +} + +static inline NSNumber *_NSBoolOrNil(int8_t value) +{ + if (value == 0) { + return @NO; + } + if (value == 1) { + return @YES; + } + return nil; +} + static int loadStatus = -1; // unitialized static Class SentrySDK; @@ -156,27 +189,31 @@ void SentryNativeBridgeAddBreadcrumb( return; } + NSString *timestampString = _NSStringOrNil(timestamp); + NSString *messageString = _NSStringOrNil(message); + NSString *typeString = _NSStringOrNil(type); + NSString *categoryString = _NSStringOrNil(category) ?: @"default"; // Category cannot be nil + SentryConfigureScope(^(id scope) { id breadcrumb = [[SentryBreadcrumb alloc] init]; - if (timestamp != NULL) { - NSDateFormatter *dateFormatter = [[NSDateFormatter alloc] init]; - [dateFormatter setDateFormat:NSCalendarIdentifierISO8601]; - [breadcrumb - setValue:[dateFormatter dateFromString:[NSString stringWithUTF8String:timestamp]] - forKey:@"timestamp"]; + if (timestampString != nil && timestampString.length > 0) { + NSDate *date = [sentry_cachedISO8601Formatter() dateFromString:timestampString]; + if (date != nil) { + [breadcrumb setValue:date forKey:@"timestamp"]; + } } - if (message != NULL) { - [breadcrumb setValue:[NSString stringWithUTF8String:message] forKey:@"message"]; + if (messageString != nil) { + [breadcrumb setValue:messageString forKey:@"message"]; } - if (type != NULL) { - [breadcrumb setValue:[NSString stringWithUTF8String:type] forKey:@"type"]; + if (typeString != nil) { + [breadcrumb setValue:typeString forKey:@"type"]; } - if (category != NULL) { - [breadcrumb setValue:[NSString stringWithUTF8String:category] forKey:@"category"]; + if (categoryString != nil) { + [breadcrumb setValue:categoryString forKey:@"category"]; } [breadcrumb setValue:[NSNumber numberWithInt:level] forKey:@"level"]; @@ -191,15 +228,13 @@ void SentryNativeBridgeSetExtra(const char *key, const char *value) return; } + NSString *keyString = [NSString stringWithUTF8String:key]; + NSString *valueString = _NSStringOrNil(value); + SentryConfigureScope(^(id scope) { - if (value != NULL) { - [scope performSelector:@selector(setExtraValue:forKey:) - withObject:[NSString stringWithUTF8String:value] - withObject:[NSString stringWithUTF8String:key]]; - } else { - [scope performSelector:@selector(removeExtraForKey:) - withObject:[NSString stringWithUTF8String:key]]; - } + [scope performSelector:@selector(setExtraValue:forKey:) + withObject:valueString + withObject:keyString]; }); } @@ -209,15 +244,13 @@ void SentryNativeBridgeSetTag(const char *key, const char *value) return; } + NSString *keyString = [NSString stringWithUTF8String:key]; + NSString *valueString = _NSStringOrNil(value); + SentryConfigureScope(^(id scope) { - if (value != NULL) { - [scope performSelector:@selector(setTagValue:forKey:) - withObject:[NSString stringWithUTF8String:value] - withObject:[NSString stringWithUTF8String:key]]; - } else { - [scope performSelector:@selector(removeTagForKey:) - withObject:[NSString stringWithUTF8String:key]]; - } + [scope performSelector:@selector(setTagValue:forKey:) + withObject:valueString + withObject:keyString]; }); } @@ -227,38 +260,29 @@ void SentryNativeBridgeUnsetTag(const char *key) return; } + NSString *keyString = [NSString stringWithUTF8String:key]; + SentryConfigureScope(^(id scope) { - [scope performSelector:@selector(removeTagForKey:) - withObject:[NSString stringWithUTF8String:key]]; + [scope performSelector:@selector(removeTagForKey:) withObject:keyString]; }); } void SentryNativeBridgeSetUser( const char *email, const char *userId, const char *ipAddress, const char *username) { - if (email == NULL && userId == NULL && ipAddress == NULL && username == NULL) { - return; - } + NSString *emailString = _NSStringOrNil(email); + NSString *userIdString = _NSStringOrNil(userId); + NSString *ipAddressString = _NSStringOrNil(ipAddress); + NSString *usernameString = _NSStringOrNil(username); SentryConfigureScope(^(id scope) { id user = [[SentryUser alloc] init]; - if (email != NULL) { - [user setValue:[NSString stringWithUTF8String:email] forKey:@"email"]; - } - - if (userId != NULL) { - [user setValue:[NSString stringWithUTF8String:userId] forKey:@"userId"]; - } - - if (ipAddress != NULL) { - [user setValue:[NSString stringWithUTF8String:ipAddress] forKey:@"ipAddress"]; - } - - if (username != NULL) { - [user setValue:[NSString stringWithUTF8String:username] forKey:@"username"]; - } - + [user setValue:emailString forKey:@"email"]; + [user setValue:userIdString forKey:@"userId"]; + [user setValue:ipAddressString forKey:@"ipAddress"]; + [user setValue:usernameString forKey:@"username"]; + [scope performSelector:@selector(setUser:) withObject:user]; }); } @@ -301,27 +325,6 @@ void SentryNativeBridgeSetTrace(const char *traceId, const char *spanId) withObject:sentrySpanId]; } -static inline NSString *_NSStringOrNil(const char *value) -{ - return value ? [NSString stringWithUTF8String:value] : nil; -} - -static inline NSString *_NSNumberOrNil(int32_t value) -{ - return value == 0 ? nil : @(value); -} - -static inline NSNumber *_NSBoolOrNil(int8_t value) -{ - if (value == 0) { - return @NO; - } - if (value == 1) { - return @YES; - } - return nil; -} - void SentryNativeBridgeWriteScope( // clang-format off // // const char *AppStartTime, // const char *AppBuildType, diff --git a/test/Scripts.Integration.Test/Scripts/SmokeTester.cs b/test/Scripts.Integration.Test/Scripts/SmokeTester.cs index 0de0ead59..1cbf56009 100644 --- a/test/Scripts.Integration.Test/Scripts/SmokeTester.cs +++ b/test/Scripts.Integration.Test/Scripts/SmokeTester.cs @@ -195,6 +195,26 @@ private IEnumerator SmokeTestCoroutine() t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'"); t.ExpectMessageNot(currentMessage, "'length':0"); + ex = new Exception("Exception & removed context test"); + RemoveContext(); + SentrySdk.CaptureException(ex); + + // Wait for screenshot capture to complete + yield return null; + + currentMessage++; // The exception event + + t.ExpectMessage(currentMessage, "'type':'event'"); + t.ExpectMessageNot(currentMessage, "'extra':{'extra-key':42}"); + t.ExpectMessageNot(currentMessage, "'tag-key':'tag-value'"); + t.ExpectMessageNot(currentMessage, "user-id"); + t.ExpectMessageNot(currentMessage, "'length':0"); + + currentMessage++; // The screenshot envelope + + t.ExpectMessage(currentMessage, "'filename':'screenshot.jpg','attachment_type':'event.attachment'"); + t.ExpectMessageNot(currentMessage, "'length':0"); + Debug.Log("Finished checking messages."); t.Pass(); @@ -257,6 +277,22 @@ private static void AddContext() }); } + private static void RemoveContext() + { + SentrySdk.ConfigureScope((Scope scope) => + { + scope.SetExtra("extra-key", null); + scope.UnsetTag("tag-key"); + scope.User = new SentryUser + { + Username = null, + Email = null, + IpAddress = null, + Id = null, + }; + }); + } + // CppPlugin.cpp [DllImport("__Internal")] private static extern void throw_cpp();