diff --git a/Directory.Build.targets b/Directory.Build.targets
index e389b647e0..9b68e5aacb 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -23,6 +23,11 @@
+
+
+
+
+
@@ -65,9 +70,7 @@
-
+
+#include
+
+static int loadStatus = -1; // unitialized
+
+static Class SentrySDK;
+static Class SentryScope;
+static Class SentryBreadcrumb;
+static Class SentryUser;
+static Class SentryOptions;
+static Class PrivateSentrySDKOnly;
+
+#define LOAD_CLASS_OR_BREAK(name) \
+ name = (__bridge Class)dlsym(dylib, "OBJC_CLASS_$_" #name); \
+ if (!name) { \
+ NSLog(@"Sentry (bridge): Couldn't load class '" #name "' from the dynamic library"); \
+ break; \
+ }
+
+// Returns (bool): 0 on failure, 1 on success
+// WARNING: you may only call other Sentry* functions AFTER calling this AND only if it returned "1"
+int SentryNativeBridgeLoadLibrary()
+{
+ if (loadStatus == -1) {
+ loadStatus = 0; // init to "error"
+ do {
+ void *dylib = dlopen("libSentry.dylib", RTLD_LAZY);
+ if (!dylib) {
+ NSLog(@"Sentry (bridge): Couldn't load Sentry.dylib - dlopen() failed");
+ break;
+ }
+
+ LOAD_CLASS_OR_BREAK(SentrySDK)
+ LOAD_CLASS_OR_BREAK(SentryScope)
+ LOAD_CLASS_OR_BREAK(SentryBreadcrumb)
+ LOAD_CLASS_OR_BREAK(SentryUser)
+ LOAD_CLASS_OR_BREAK(SentryOptions)
+ LOAD_CLASS_OR_BREAK(PrivateSentrySDKOnly)
+
+ // everything above passed - mark as successfully loaded
+ loadStatus = 1;
+ } while (false);
+ }
+ return loadStatus;
+}
+
+const void *SentryNativeBridgeOptionsNew()
+{
+ NSMutableDictionary *dictOptions = [[NSMutableDictionary alloc] init];
+ dictOptions[@"sdk"] = @ { @"name" : @"sentry.cocoa.unity" };
+ dictOptions[@"enableAutoSessionTracking"] = @NO;
+ dictOptions[@"enableAppHangTracking"] = @NO;
+ return CFBridgingRetain(dictOptions);
+}
+
+void SentryNativeBridgeOptionsSetString(const void *options, const char *name, const char *value)
+{
+ NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options;
+ dictOptions[[NSString stringWithUTF8String:name]] = [NSString stringWithUTF8String:value];
+}
+
+void SentryNativeBridgeOptionsSetInt(const void *options, const char *name, int32_t value)
+{
+ NSMutableDictionary *dictOptions = (__bridge NSMutableDictionary *)options;
+ dictOptions[[NSString stringWithUTF8String:name]] = [NSNumber numberWithInt:value];
+}
+
+void SentryNativeBridgeStartWithOptions(const void *options)
+{
+ NSMutableDictionary *dictOptions = (__bridge_transfer NSMutableDictionary *)options;
+ id sentryOptions = [[SentryOptions alloc]
+ performSelector:@selector(initWithDict:didFailWithError:)
+ withObject:dictOptions withObject:nil];
+
+ [SentrySDK performSelector:@selector(startWithOptions:) withObject:sentryOptions];
+}
+
+void SentryConfigureScope(void (^callback)(id))
+{
+ // setValue:forKey: may throw if the property is not found; same for performSelector.
+ // Even though this shouldn't happen, better not take the chance of letting an unhandled
+ // exception while setting error info - it would just crash the app immediately.
+ @try {
+ [SentrySDK performSelector:@selector(configureScope:) withObject:callback];
+ } @catch (NSException *exception) {
+ NSLog(@"Sentry (bridge): failed to configure scope: %@", exception.reason);
+ }
+}
+
+/*******************************************************************************/
+/* The remaining code is a copy of iOS/SentryNativeBridge.m with changes to */
+/* make it work with dynamically loaded classes. Mainly: */
+/* - call: [class performSelector:@selector(arg1:arg2:) */
+/* withObject:arg1Value withObject:arg2Value]; */
+/* or xCode warns of class/instance method not found */
+/* - use `id` as variable types */
+/* - use [obj setValue:value forKey:@"prop"] instead of `obj.prop = value` */
+/*******************************************************************************/
+
+int SentryNativeBridgeCrashedLastRun()
+{
+ @try {
+ return [SentrySDK performSelector:@selector(crashedLastRun)] ? 1 : 0;
+ } @catch (NSException *exception) {
+ NSLog(@"Sentry (bridge): failed to get crashedLastRun: %@", exception.reason);
+ }
+ return -1;
+}
+
+void SentryNativeBridgeClose()
+{
+ @try {
+ [SentrySDK performSelector:@selector(close)];
+ } @catch (NSException *exception) {
+ NSLog(@"Sentry (bridge): failed to close: %@", exception.reason);
+ }
+}
+
+void SentryNativeBridgeAddBreadcrumb(
+ const char *timestamp, const char *message, const char *type, const char *category, int level)
+{
+ if (timestamp == NULL && message == NULL && type == NULL && category == NULL) {
+ return;
+ }
+
+ 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 (message != NULL) {
+ [breadcrumb setValue:[NSString stringWithUTF8String:message] forKey:@"message"];
+ }
+
+ if (type != NULL) {
+ [breadcrumb setValue:[NSString stringWithUTF8String:type] forKey:@"type"];
+ }
+
+ if (category != NULL) {
+ [breadcrumb setValue:[NSString stringWithUTF8String:category] forKey:@"category"];
+ }
+
+ [breadcrumb setValue:[NSNumber numberWithInt:level] forKey:@"level"];
+
+ [scope performSelector:@selector(addBreadcrumb:) withObject:breadcrumb];
+ });
+}
+
+void SentryNativeBridgeSetExtra(const char *key, const char *value)
+{
+ if (key == NULL) {
+ return;
+ }
+
+ 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]];
+ }
+ });
+}
+
+void SentryNativeBridgeSetTag(const char *key, const char *value)
+{
+ if (key == NULL) {
+ return;
+ }
+
+ 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]];
+ }
+ });
+}
+
+void SentryNativeBridgeUnsetTag(const char *key)
+{
+ if (key == NULL) {
+ return;
+ }
+
+ SentryConfigureScope(^(id scope) {
+ [scope performSelector:@selector(removeTagForKey:)
+ withObject:[NSString stringWithUTF8String:key]];
+ });
+}
+
+void SentryNativeBridgeSetUser(
+ const char *email, const char *userId, const char *ipAddress, const char *username)
+{
+ if (email == NULL && userId == NULL && ipAddress == NULL && username == NULL) {
+ return;
+ }
+
+ 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"];
+ }
+
+ [scope performSelector:@selector(setUser:) withObject:user];
+ });
+}
+
+void SentryNativeBridgeUnsetUser()
+{
+ SentryConfigureScope(
+ ^(id scope) { [scope performSelector:@selector(setUser:) withObject:nil]; });
+}
+
+char *SentryNativeBridgeGetInstallationId()
+{
+ // Create a null terminated C string on the heap as expected by marshalling.
+ // See Tips for iOS in https://docs.unity3d.com/Manual/PluginsForIOS.html
+ const char *nsStringUtf8 =
+ [[PrivateSentrySDKOnly performSelector:@selector(installationID)] UTF8String];
+ size_t len = strlen(nsStringUtf8) + 1;
+ char *cString = (char *)malloc(len);
+ memcpy(cString, nsStringUtf8, len);
+ return cString;
+}
+
+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,
+ // // const char *OperatingSystemRawDescription,
+ // int DeviceProcessorCount,
+ // const char *DeviceCpuDescription,
+ // const char *DeviceTimezone,
+ // int8_t DeviceSupportsVibration,
+ // const char *DeviceName,
+ // int8_t DeviceSimulator,
+ // const char *DeviceDeviceUniqueIdentifier,
+ // const char *DeviceDeviceType,
+ // // const char *DeviceModel,
+ // // long DeviceMemorySize,
+ int32_t GpuId,
+ const char *GpuName,
+ const char *GpuVendorName,
+ int32_t GpuMemorySize,
+ const char *GpuNpotSupport,
+ const char *GpuVersion,
+ const char *GpuApiType,
+ int32_t GpuMaxTextureSize,
+ int8_t GpuSupportsDrawCallInstancing,
+ int8_t GpuSupportsRayTracing,
+ int8_t GpuSupportsComputeShaders,
+ int8_t GpuSupportsGeometryShaders,
+ const char *GpuVendorId,
+ int8_t GpuMultiThreadedRendering,
+ const char *GpuGraphicsShaderLevel,
+ const char *EditorVersion,
+ const char *UnityInstallMode,
+ const char *UnityTargetFrameRate,
+ const char *UnityCopyTextureSupport,
+ const char *UnityRenderingThreadingMode
+) // clang-format on
+{
+ // Note: we're using a NSMutableDictionary because it will skip fields with nil values.
+ SentryConfigureScope(^(id scope) {
+ NSMutableDictionary *gpu = [[NSMutableDictionary alloc] init];
+ gpu[@"id"] = _NSNumberOrNil(GpuId);
+ gpu[@"name"] = _NSStringOrNil(GpuName);
+ gpu[@"vendor_name"] = _NSStringOrNil(GpuVendorName);
+ gpu[@"memory_size"] = _NSNumberOrNil(GpuMemorySize);
+ gpu[@"npot_support"] = _NSStringOrNil(GpuNpotSupport);
+ gpu[@"version"] = _NSStringOrNil(GpuVersion);
+ gpu[@"api_type"] = _NSStringOrNil(GpuApiType);
+ gpu[@"max_texture_size"] = _NSNumberOrNil(GpuMaxTextureSize);
+ gpu[@"supports_draw_call_instancing"] = _NSBoolOrNil(GpuSupportsDrawCallInstancing);
+ gpu[@"supports_ray_tracing"] = _NSBoolOrNil(GpuSupportsRayTracing);
+ gpu[@"supports_compute_shaders"] = _NSBoolOrNil(GpuSupportsComputeShaders);
+ gpu[@"supports_geometry_shaders"] = _NSBoolOrNil(GpuSupportsGeometryShaders);
+ gpu[@"vendor_id"] = _NSStringOrNil(GpuVendorId);
+ gpu[@"multi_threaded_rendering"] = _NSBoolOrNil(GpuMultiThreadedRendering);
+ gpu[@"graphics_shader_level"] = _NSStringOrNil(GpuGraphicsShaderLevel);
+ [scope performSelector:@selector(setContextValue:forKey:) withObject:gpu withObject:@"gpu"];
+
+ NSMutableDictionary *unity = [[NSMutableDictionary alloc] init];
+ unity[@"editor_version"] = _NSStringOrNil(EditorVersion);
+ unity[@"install_mode"] = _NSStringOrNil(UnityInstallMode);
+ unity[@"target_frame_rate"] = _NSStringOrNil(UnityTargetFrameRate);
+ unity[@"copy_texture_support"] = _NSStringOrNil(UnityCopyTextureSupport);
+ unity[@"rendering_threading_mode"] = _NSStringOrNil(UnityRenderingThreadingMode);
+ [scope performSelector:@selector(setContextValue:forKey:)
+ withObject:unity
+ withObject:@"unity"];
+ });
+}
+
+void crashBridge()
+{
+ int *p = 0;
+ *p = 0;
+}
diff --git a/lib/sentry-native/macos/libBridge.dylib b/lib/sentry-native/macos/libBridge.dylib
new file mode 100755
index 0000000000..fd818afc89
Binary files /dev/null and b/lib/sentry-native/macos/libBridge.dylib differ
diff --git a/src/Sentry/CrashType.cs b/src/Sentry/CrashType.cs
index 0fb43468e1..7957eb4075 100644
--- a/src/Sentry/CrashType.cs
+++ b/src/Sentry/CrashType.cs
@@ -29,7 +29,7 @@ public enum CrashType
JavaBackgroundThread,
#endif
-#if __MOBILE__
+#if __MOBILE__ || MACOS
///
/// A native operation that will crash the application will be performed by a C library.
///
diff --git a/src/Sentry/Internal/ScopeObserver.cs b/src/Sentry/Internal/ScopeObserver.cs
new file mode 100644
index 0000000000..5fb6efa488
--- /dev/null
+++ b/src/Sentry/Internal/ScopeObserver.cs
@@ -0,0 +1,87 @@
+namespace Sentry;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity/ScopeObserver.cs
+
+///
+/// Sentry Unity Scope Observer wrapper for the common behaviour accross platforms.
+///
+internal abstract class ScopeObserver : Sentry.IScopeObserver
+{
+ private readonly SentryOptions _options;
+ private readonly string _name;
+
+ public ScopeObserver(
+ string name, SentryOptions options)
+ {
+ _name = name;
+ _options = options;
+ }
+
+ public void AddBreadcrumb(Breadcrumb breadcrumb)
+ {
+ _options.DiagnosticLogger?.Log(SentryLevel.Debug,
+ "{0} Scope Sync - Adding breadcrumb m:\"{1}\" l:\"{2}\"", null, _name,
+ breadcrumb.Message, breadcrumb.Level);
+ AddBreadcrumbImpl(breadcrumb);
+ }
+
+ public abstract void AddBreadcrumbImpl(Breadcrumb breadcrumb);
+
+ public void SetExtra(string key, object? value)
+ {
+ // var serialized = value is null ? null : SafeSerializer.SerializeSafely(value);
+ // if (value is not null && serialized is null)
+ // {
+ // _options.DiagnosticLogger?.Log(SentryLevel.Warning,
+ // "{0} Scope Sync - SetExtra k:\"{1}\" v:\"{2}\" - value was serialized as null",
+ // null, _name, key, value);
+ // }
+ // else
+ // {
+ // _options.DiagnosticLogger?.Log(SentryLevel.Debug,
+ // "{0} Scope Sync - Setting Extra k:\"{1}\" v:\"{2}\"", null, _name, key, value);
+ // }
+ // SetExtraImpl(key, serialized);
+ }
+
+ public abstract void SetExtraImpl(string key, string? value);
+
+ public void SetTag(string key, string value)
+ {
+ _options.DiagnosticLogger?.Log(SentryLevel.Debug,
+ "{0} Scope Sync - Setting Tag k:\"{1}\" v:\"{2}\"", null, _name, key, value);
+ SetTagImpl(key, value);
+ }
+
+ public abstract void SetTagImpl(string key, string value);
+
+ public void UnsetTag(string key)
+ {
+ _options.DiagnosticLogger?.Log(
+ SentryLevel.Debug, "{0} Scope Sync - Unsetting Tag k:\"{1}\"", null, _name, key);
+ UnsetTagImpl(key);
+ }
+
+ public abstract void UnsetTagImpl(string key);
+
+ public void SetUser(User? user)
+ {
+ if (user is null)
+ {
+ _options.DiagnosticLogger?.Log(
+ SentryLevel.Debug, "{0} Scope Sync - Unsetting User", null, _name);
+ UnsetUserImpl();
+ }
+ else
+ {
+ _options.DiagnosticLogger?.Log(SentryLevel.Debug,
+ "{0} Scope Sync - Setting User i:\"{1}\" n:\"{2}\"", null, _name, user.Id,
+ user.Username);
+ SetUserImpl(user);
+ }
+ }
+
+ public abstract void SetUserImpl(User user);
+
+ public abstract void UnsetUserImpl();
+}
diff --git a/src/Sentry/Platforms/Native/BaseContextWriter.cs b/src/Sentry/Platforms/Native/BaseContextWriter.cs
new file mode 100644
index 0000000000..9ff5f58742
--- /dev/null
+++ b/src/Sentry/Platforms/Native/BaseContextWriter.cs
@@ -0,0 +1,155 @@
+namespace Sentry.Native;
+
+///
+/// Allows synchronizing Context from .NET to native layers.
+/// We're providing a single method that the implementations should override.
+/// They can choose to either have the single method directly in native using p/invoke,
+/// or use a more fine-grained interface, whatever is best for the platform.
+///
+///
+/// WriteScope() is called in a new Task (background thread from a pool).
+///
+internal abstract class BaseContextWriter
+{
+ internal static void WriteApp(string? AppStartTime, string? AppBuildType)
+ {
+ var obj = C.sentry_value_new_object();
+ C.SetValueIfNotNull(obj, "app_start_time", AppStartTime);
+ C.SetValueIfNotNull(obj, "build_type", AppBuildType);
+ C.sentry_set_context(Sentry.Protocol.App.Type, obj);
+ }
+
+ internal static void WriteOS(string? OperatingSystemRawDescription)
+ {
+ var obj = C.sentry_value_new_object();
+ C.SetValueIfNotNull(obj, "raw_description", OperatingSystemRawDescription);
+ C.sentry_set_context(Sentry.Protocol.OperatingSystem.Type, obj);
+ }
+
+ internal static void WriteDevice(
+ int? DeviceProcessorCount,
+ string? DeviceCpuDescription,
+ string? DeviceTimezone,
+ bool? DeviceSupportsVibration,
+ string? DeviceName,
+ bool? DeviceSimulator,
+ string? DeviceDeviceUniqueIdentifier,
+ string? DeviceDeviceType,
+ string? DeviceModel,
+ long? DeviceMemorySize)
+ {
+ var obj = C.sentry_value_new_object();
+ C.SetValueIfNotNull(obj, "processor_count", DeviceProcessorCount);
+ C.SetValueIfNotNull(obj, "cpu_description", DeviceCpuDescription);
+ C.SetValueIfNotNull(obj, "timezone", DeviceTimezone);
+ C.SetValueIfNotNull(obj, "supports_vibration", DeviceSupportsVibration);
+ C.SetValueIfNotNull(obj, "name", DeviceName);
+ C.SetValueIfNotNull(obj, "simulator", DeviceSimulator);
+ C.SetValueIfNotNull(obj, "device_unique_identifier", DeviceDeviceUniqueIdentifier);
+ C.SetValueIfNotNull(obj, "device_type", DeviceDeviceType);
+ C.SetValueIfNotNull(obj, "model", DeviceModel);
+ C.SetValueIfNotNull(obj, "memory_size", DeviceMemorySize);
+ C.sentry_set_context(Sentry.Protocol.Device.Type, obj);
+ }
+
+ internal static void WriteGpu(
+ int? GpuId,
+ string? GpuName,
+ string? GpuVendorName,
+ int? GpuMemorySize,
+ string? GpuNpotSupport,
+ string? GpuVersion,
+ string? GpuApiType,
+ int? GpuMaxTextureSize,
+ bool? GpuSupportsDrawCallInstancing,
+ bool? GpuSupportsRayTracing,
+ bool? GpuSupportsComputeShaders,
+ bool? GpuSupportsGeometryShaders,
+ string? GpuVendorId,
+ bool? GpuMultiThreadedRendering,
+ string? GpuGraphicsShaderLevel)
+ {
+ var obj = C.sentry_value_new_object();
+ C.SetValueIfNotNull(obj, "id", GpuId);
+ C.SetValueIfNotNull(obj, "name", GpuName);
+ C.SetValueIfNotNull(obj, "vendor_name", GpuVendorName);
+ C.SetValueIfNotNull(obj, "memory_size", GpuMemorySize);
+ C.SetValueIfNotNull(obj, "npot_support", GpuNpotSupport);
+ C.SetValueIfNotNull(obj, "version", GpuVersion);
+ C.SetValueIfNotNull(obj, "api_type", GpuApiType);
+ C.SetValueIfNotNull(obj, "max_texture_size", GpuMaxTextureSize);
+ C.SetValueIfNotNull(obj, "supports_draw_call_instancing", GpuSupportsDrawCallInstancing);
+ C.SetValueIfNotNull(obj, "supports_ray_tracing", GpuSupportsRayTracing);
+ C.SetValueIfNotNull(obj, "supports_compute_shaders", GpuSupportsComputeShaders);
+ C.SetValueIfNotNull(obj, "supports_geometry_shaders", GpuSupportsGeometryShaders);
+ C.SetValueIfNotNull(obj, "vendor_id", GpuVendorId);
+ C.SetValueIfNotNull(obj, "multi_threaded_rendering", GpuMultiThreadedRendering);
+ C.SetValueIfNotNull(obj, "graphics_shader_level", GpuGraphicsShaderLevel);
+ C.sentry_set_context(Sentry.Protocol.Gpu.Type, obj);
+ }
+
+ public void Write(Scope scope)
+ {
+ WriteScope(
+ scope.Contexts.App.StartTime?.ToString("o"),
+ scope.Contexts.App.BuildType,
+ scope.Contexts.OperatingSystem.RawDescription,
+ scope.Contexts.Device.ProcessorCount,
+ scope.Contexts.Device.CpuDescription,
+ scope.Contexts.Device.Timezone?.Id,
+ scope.Contexts.Device.SupportsVibration,
+ scope.Contexts.Device.Name,
+ scope.Contexts.Device.Simulator,
+ scope.Contexts.Device.DeviceUniqueIdentifier,
+ scope.Contexts.Device.DeviceType,
+ scope.Contexts.Device.Model,
+ scope.Contexts.Device.MemorySize,
+ scope.Contexts.Gpu.Id,
+ scope.Contexts.Gpu.Name,
+ scope.Contexts.Gpu.VendorName,
+ scope.Contexts.Gpu.MemorySize,
+ scope.Contexts.Gpu.NpotSupport,
+ scope.Contexts.Gpu.Version,
+ scope.Contexts.Gpu.ApiType,
+ scope.Contexts.Gpu.MaxTextureSize,
+ scope.Contexts.Gpu.SupportsDrawCallInstancing,
+ scope.Contexts.Gpu.SupportsRayTracing,
+ scope.Contexts.Gpu.SupportsComputeShaders,
+ scope.Contexts.Gpu.SupportsGeometryShaders,
+ scope.Contexts.Gpu.VendorId,
+ scope.Contexts.Gpu.MultiThreadedRendering,
+ scope.Contexts.Gpu.GraphicsShaderLevel
+ );
+ }
+
+ protected abstract void WriteScope(
+ string? AppStartTime,
+ string? AppBuildType,
+ string? OperatingSystemRawDescription,
+ int? DeviceProcessorCount,
+ string? DeviceCpuDescription,
+ string? DeviceTimezone,
+ bool? DeviceSupportsVibration,
+ string? DeviceName,
+ bool? DeviceSimulator,
+ string? DeviceDeviceUniqueIdentifier,
+ string? DeviceDeviceType,
+ string? DeviceModel,
+ long? DeviceMemorySize,
+ int? GpuId,
+ string? GpuName,
+ string? GpuVendorName,
+ int? GpuMemorySize,
+ string? GpuNpotSupport,
+ string? GpuVersion,
+ string? GpuApiType,
+ int? GpuMaxTextureSize,
+ bool? GpuSupportsDrawCallInstancing,
+ bool? GpuSupportsRayTracing,
+ bool? GpuSupportsComputeShaders,
+ bool? GpuSupportsGeometryShaders,
+ string? GpuVendorId,
+ bool? GpuMultiThreadedRendering,
+ string? GpuGraphicsShaderLevel
+ );
+}
diff --git a/src/Sentry/Platforms/Native/NativeContextWriter.cs b/src/Sentry/Platforms/Native/NativeContextWriter.cs
new file mode 100644
index 0000000000..88503f7ee9
--- /dev/null
+++ b/src/Sentry/Platforms/Native/NativeContextWriter.cs
@@ -0,0 +1,73 @@
+namespace Sentry.Native;
+using CWUtil = BaseContextWriter;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/NativeContextWriter.cs
+
+internal class NativeContextWriter : BaseContextWriter
+{
+ protected override void WriteScope(
+ string? AppStartTime,
+ string? AppBuildType,
+ string? OperatingSystemRawDescription,
+ int? DeviceProcessorCount,
+ string? DeviceCpuDescription,
+ string? DeviceTimezone,
+ bool? DeviceSupportsVibration,
+ string? DeviceName,
+ bool? DeviceSimulator,
+ string? DeviceDeviceUniqueIdentifier,
+ string? DeviceDeviceType,
+ string? DeviceModel,
+ long? DeviceMemorySize,
+ int? GpuId,
+ string? GpuName,
+ string? GpuVendorName,
+ int? GpuMemorySize,
+ string? GpuNpotSupport,
+ string? GpuVersion,
+ string? GpuApiType,
+ int? GpuMaxTextureSize,
+ bool? GpuSupportsDrawCallInstancing,
+ bool? GpuSupportsRayTracing,
+ bool? GpuSupportsComputeShaders,
+ bool? GpuSupportsGeometryShaders,
+ string? GpuVendorId,
+ bool? GpuMultiThreadedRendering,
+ string? GpuGraphicsShaderLevel
+ )
+ {
+ CWUtil.WriteApp(AppStartTime, AppBuildType);
+
+ CWUtil.WriteOS(OperatingSystemRawDescription);
+
+ CWUtil.WriteDevice(
+ DeviceProcessorCount,
+ DeviceCpuDescription,
+ DeviceTimezone,
+ DeviceSupportsVibration,
+ DeviceName,
+ DeviceSimulator,
+ DeviceDeviceUniqueIdentifier,
+ DeviceDeviceType,
+ DeviceModel,
+ DeviceMemorySize
+ );
+
+ CWUtil.WriteGpu(
+ GpuId,
+ GpuName,
+ GpuVendorName,
+ GpuMemorySize,
+ GpuNpotSupport,
+ GpuVersion,
+ GpuApiType,
+ GpuMaxTextureSize,
+ GpuSupportsDrawCallInstancing,
+ GpuSupportsRayTracing,
+ GpuSupportsComputeShaders,
+ GpuSupportsGeometryShaders,
+ GpuVendorId,
+ GpuMultiThreadedRendering,
+ GpuGraphicsShaderLevel);
+ }
+}
diff --git a/src/Sentry/Platforms/Native/NativeScopeObserver.cs b/src/Sentry/Platforms/Native/NativeScopeObserver.cs
new file mode 100644
index 0000000000..669752d341
--- /dev/null
+++ b/src/Sentry/Platforms/Native/NativeScopeObserver.cs
@@ -0,0 +1,46 @@
+namespace Sentry.Native;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/NativeScopeObserver.cs
+///
+/// Scope Observer for Native through P/Invoke.
+///
+///
+internal class NativeScopeObserver : ScopeObserver
+{
+ public NativeScopeObserver(SentryOptions options) : base("Native", options) { }
+
+ public override void AddBreadcrumbImpl(Breadcrumb breadcrumb)
+ {
+ // see https://develop.sentry.dev/sdk/event-payloads/breadcrumbs/
+ var crumb = C.sentry_value_new_breadcrumb(breadcrumb.Type, breadcrumb.Message);
+ C.sentry_value_set_by_key(crumb, "level", C.sentry_value_new_string(breadcrumb.Level.ToString().ToLower()));
+ C.sentry_value_set_by_key(crumb, "timestamp", C.sentry_value_new_string(GetTimestamp(breadcrumb.Timestamp)));
+ C.SetValueIfNotNull(crumb, "category", breadcrumb.Category);
+ C.sentry_add_breadcrumb(crumb);
+ }
+
+ public override void SetExtraImpl(string key, string? value) =>
+ C.sentry_set_extra(key, value is null ? C.sentry_value_new_null() : C.sentry_value_new_string(value));
+
+ public override void SetTagImpl(string key, string value) => C.sentry_set_tag(key, value);
+
+ public override void UnsetTagImpl(string key) => C.sentry_remove_tag(key);
+
+ public override void SetUserImpl(User user)
+ {
+ // see https://develop.sentry.dev/sdk/event-payloads/user/
+ var cUser = C.sentry_value_new_object();
+ C.SetValueIfNotNull(cUser, "id", user.Id);
+ C.SetValueIfNotNull(cUser, "username", user.Username);
+ C.SetValueIfNotNull(cUser, "email", user.Email);
+ C.SetValueIfNotNull(cUser, "ip_address", user.IpAddress);
+ C.sentry_set_user(cUser);
+ }
+
+ public override void UnsetUserImpl() => C.sentry_remove_user();
+
+ private static string GetTimestamp(DateTimeOffset timestamp) =>
+ // "o": Using ISO 8601 to make sure the timestamp makes it to the bridge correctly.
+ // https://docs.microsoft.com/en-gb/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip
+ timestamp.ToString("o");
+}
diff --git a/src/Sentry/Platforms/Native/SentryNative.cs b/src/Sentry/Platforms/Native/SentryNative.cs
new file mode 100644
index 0000000000..ca40eefebf
--- /dev/null
+++ b/src/Sentry/Platforms/Native/SentryNative.cs
@@ -0,0 +1,65 @@
+using Sentry.Extensibility;
+
+namespace Sentry.Native;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/SentryNative.cs
+
+///
+/// Access to the Sentry native support of the sentry-native SDK.
+///
+internal static class SentryNative
+{
+ private static readonly Dictionary PerDirectoryCrashInfo = new();
+
+ ///
+ /// Configures the native SDK.
+ ///
+ /// The Sentry Unity options to use.
+ public static void Configure(SentryOptions options)
+ {
+ if (!SentryNativeBridge.Init(options))
+ {
+ options.DiagnosticLogger?
+ .LogWarning("Sentry native initialization failed - native crashes are not captured.");
+ return;
+ }
+
+ // ApplicationAdapter.Instance.Quitting += () =>
+ // {
+ // options.DiagnosticLogger?.LogDebug("Closing the sentry-native SDK");
+ // SentryNativeBridge.Close();
+ // };
+ options.ScopeObserver = new NativeScopeObserver(options);
+ options.EnableScopeSync = true;
+
+ // options.NativeContextWriter = new NativeContextWriter();
+
+ // // Use AnalyticsSessionInfo.userId as the default UserID in native & dotnet
+ // options.DefaultUserId = AnalyticsSessionInfo.userId;
+ // if (options.DefaultUserId is not null)
+ // {
+ // options.ScopeObserver.SetUser(new User { Id = options.DefaultUserId });
+ // }
+
+ // Note: we must actually call the function now and on every other call use the value we get here.
+ // Additionally, we cannot call this multiple times for the same directory, because the result changes
+ // on subsequent runs. Therefore, we cache the value during the whole runtime of the application.
+ var cacheDirectory = SentryNativeBridge.GetCacheDirectory(options);
+ var crashedLastRun = false;
+ // In the event the SDK is re-initialized with a different path on disk, we need to track which ones were already read
+ // Similarly we need to cache the value of each call since a subsequent call would return a different value
+ // as the file used on disk to mark it as crashed is deleted after we read it.
+ lock (PerDirectoryCrashInfo)
+ {
+ if (!PerDirectoryCrashInfo.TryGetValue(cacheDirectory, out crashedLastRun))
+ {
+ crashedLastRun = SentryNativeBridge.HandleCrashedLastRun(options);
+ PerDirectoryCrashInfo.Add(cacheDirectory, crashedLastRun);
+
+ options.DiagnosticLogger?
+ .LogDebug("Native SDK reported: 'crashedLastRun': '{0}'", crashedLastRun);
+ }
+ }
+ options.CrashedLastRun = () => crashedLastRun;
+ }
+}
diff --git a/src/Sentry/Platforms/Native/SentryNativeBridge.cs b/src/Sentry/Platforms/Native/SentryNativeBridge.cs
new file mode 100644
index 0000000000..f03a22b2d4
--- /dev/null
+++ b/src/Sentry/Platforms/Native/SentryNativeBridge.cs
@@ -0,0 +1,276 @@
+using Sentry.Extensibility;
+
+namespace Sentry.Native;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.Native/SentryNativeBridge.cs
+
+///
+/// P/Invoke to `sentry-native` functions.
+///
+///
+internal static class SentryNativeBridge
+{
+ public static bool Init(SentryOptions options)
+ {
+ _isLinux = RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+
+ var cOptions = sentry_options_new();
+
+ // Note: DSN is not null because options.IsValid() must have returned true for this to be called.
+ sentry_options_set_dsn(cOptions, options.Dsn!);
+
+ if (options.Release is not null)
+ {
+ options.DiagnosticLogger?.LogDebug("Setting Release: {0}", options.Release);
+ sentry_options_set_release(cOptions, options.Release);
+ }
+
+ if (options.Environment is not null)
+ {
+ options.DiagnosticLogger?.LogDebug("Setting Environment: {0}", options.Environment);
+ sentry_options_set_environment(cOptions, options.Environment);
+ }
+
+ options.DiagnosticLogger?.LogDebug("Setting Debug: {0}", options.Debug);
+ sentry_options_set_debug(cOptions, options.Debug ? 1 : 0);
+
+ if (options.SampleRate.HasValue)
+ {
+ options.DiagnosticLogger?.LogDebug("Setting Sample Rate: {0}", options.SampleRate.Value);
+ sentry_options_set_sample_rate(cOptions, options.SampleRate.Value);
+ }
+
+ // Disabling the native in favor of the C# layer for now
+ options.DiagnosticLogger?.LogDebug("Disabling native auto session tracking");
+ sentry_options_set_auto_session_tracking(cOptions, 0);
+
+ var dir = GetCacheDirectory(options);
+ // Note: don't use RuntimeInformation.IsOSPlatform - it will report windows on WSL.
+ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+ {
+ options.DiagnosticLogger?.LogDebug("Setting CacheDirectoryPath on Windows: {0}", dir);
+ sentry_options_set_database_pathw(cOptions, dir);
+ }
+ else
+ {
+ options.DiagnosticLogger?.LogDebug("Setting CacheDirectoryPath: {0}", dir);
+ sentry_options_set_database_path(cOptions, dir);
+ }
+
+ if (options.DiagnosticLogger is null)
+ {
+ _logger?.LogDebug("Unsetting the current native logger");
+ _logger = null;
+ }
+ else
+ {
+ options.DiagnosticLogger.LogDebug($"{(_logger is null ? "Setting a" : "Replacing the")} native logger");
+ _logger = options.DiagnosticLogger;
+ sentry_options_set_logger(cOptions, new sentry_logger_function_t(nativeLog), IntPtr.Zero);
+ }
+
+ options.DiagnosticLogger?.LogDebug("Initializing sentry native");
+ return 0 == sentry_init(cOptions);
+ }
+
+ ///
+ /// Closes the native SDK.
+ ///
+ public static void Close() => sentry_close();
+
+ // Call after native init() to check if the application has crashed in the previous run and clear the status.
+ // Because the file is removed, the result will change on subsequent calls so it must be cached for the current runtime.
+ internal static bool HandleCrashedLastRun(SentryOptions options)
+ {
+ var result = sentry_get_crashed_last_run() == 1;
+ sentry_clear_crashed_last_run();
+ return result;
+ }
+
+ internal static string GetCacheDirectory(SentryOptions options)
+ {
+ if (options.CacheDirectoryPath is null)
+ {
+ // same as the default of sentry-native
+ return Path.Combine(Directory.GetCurrentDirectory(), ".sentry-native");
+ }
+ else
+ {
+ return Path.Combine(options.CacheDirectoryPath, "SentryNative");
+ }
+ }
+
+ // libsentry.so
+ [DllImport("sentry")]
+ private static extern IntPtr sentry_options_new();
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_dsn(IntPtr options, string dsn);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_release(IntPtr options, string release);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_debug(IntPtr options, int debug);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_environment(IntPtr options, string environment);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_sample_rate(IntPtr options, double rate);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_database_path(IntPtr options, string path);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_database_pathw(IntPtr options, [MarshalAs(UnmanagedType.LPWStr)] string path);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_auto_session_tracking(IntPtr options, int debug);
+
+ [UnmanagedFunctionPointer(CallingConvention.Cdecl, SetLastError = true)]
+ private delegate void sentry_logger_function_t(int level, IntPtr message, IntPtr argsAddress, IntPtr userData);
+
+ [DllImport("sentry")]
+ private static extern void sentry_options_set_logger(IntPtr options, [MarshalAs(UnmanagedType.FunctionPtr)]sentry_logger_function_t logger, IntPtr userData);
+
+ // The logger we should forward native messages to. This is referenced by nativeLog() which in turn for.
+ private static IDiagnosticLogger? _logger;
+ private static bool _isLinux = false;
+
+ // This method is called from the C library and forwards incoming messages to the currently set _logger.
+ // TODO: Tried replacing this with [MarshalAs(UnmanagedType.FunctionPtr)] on the target argument (above)
+ // [MonoPInvokeCallback(typeof(sentry_logger_function_t))]
+ private static void nativeLog(int cLevel, IntPtr format, IntPtr args, IntPtr userData)
+ {
+ try
+ {
+ nativeLogImpl(cLevel, format, args, userData);
+ }
+ catch
+ {
+ // never allow an exception back to native code - it would crash the app
+ }
+ }
+
+ private static void nativeLogImpl(int cLevel, IntPtr format, IntPtr args, IntPtr userData)
+ {
+ var logger = _logger;
+ if (logger is null || format == IntPtr.Zero || args == IntPtr.Zero)
+ {
+ return;
+ }
+
+ // see sentry.h: sentry_level_e
+ var level = cLevel switch
+ {
+ -1 => SentryLevel.Debug,
+ 0 => SentryLevel.Info,
+ 1 => SentryLevel.Warning,
+ 2 => SentryLevel.Error,
+ 3 => SentryLevel.Fatal,
+ _ => SentryLevel.Info,
+ };
+
+ if (!logger.IsEnabled(level))
+ {
+ return;
+ }
+
+ string? message = null;
+ try
+ {
+ // We cannot access C var-arg (va_list) in c# thus we pass it back to vsnprintf to do the formatting.
+ // For Linux, we must make a copy of the VaList to be able to pass it back...
+ if (_isLinux)
+ {
+ var argsStruct = Marshal.PtrToStructure(args);
+ var formattedLength = 0;
+ WithMarshalledStruct(argsStruct, argsPtr =>
+ {
+ formattedLength = 1 + vsnprintf_linux(IntPtr.Zero, UIntPtr.Zero, format, argsPtr);
+ });
+
+ WithAllocatedPtr(formattedLength, buffer =>
+ WithMarshalledStruct(argsStruct, argsPtr =>
+ {
+ vsnprintf_linux(buffer, (UIntPtr)formattedLength, format, argsPtr);
+ message = Marshal.PtrToStringAnsi(buffer);
+ }));
+ }
+ else
+ {
+ var formattedLength = 1 + vsnprintf_windows(IntPtr.Zero, UIntPtr.Zero, format, args);
+ WithAllocatedPtr(formattedLength, buffer =>
+ {
+ vsnprintf_windows(buffer, (UIntPtr)formattedLength, format, args);
+ message = Marshal.PtrToStringAnsi(buffer);
+ });
+ }
+ }
+ catch (Exception err)
+ {
+ logger.LogError("Exception in native log forwarder.", err);
+ }
+
+ if (message == null)
+ {
+ // try to at least print the unreplaced message pattern
+ message = Marshal.PtrToStringAnsi(format);
+ }
+
+ if (message != null)
+ {
+ logger.Log(level, $"Native: {message}");
+ }
+ }
+
+ [DllImport("msvcrt", EntryPoint = "vsnprintf")]
+ private static extern int vsnprintf_windows(IntPtr buffer, UIntPtr bufferSize, IntPtr format, IntPtr args);
+
+ [DllImport("libc", EntryPoint = "vsnprintf")]
+ private static extern int vsnprintf_linux(IntPtr buffer, UIntPtr bufferSize, IntPtr format, IntPtr args);
+
+ // https://stackoverflow.com/a/4958507/2386130
+ [StructLayout(LayoutKind.Sequential, Pack = 4)]
+ private struct VaListLinux64
+ {
+ uint gp_offset;
+ uint fp_offset;
+ IntPtr overflow_arg_area;
+ IntPtr reg_save_area;
+ }
+
+ private static void WithAllocatedPtr(int size, Action action)
+ {
+ var ptr = IntPtr.Zero;
+ try
+ {
+ ptr = Marshal.AllocHGlobal(size);
+ action(ptr);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(ptr);
+ }
+ }
+
+ private static void WithMarshalledStruct(T structure, Action action) where T : notnull =>
+ WithAllocatedPtr(Marshal.SizeOf(structure), ptr =>
+ {
+ Marshal.StructureToPtr(structure, ptr, false);
+ action(ptr);
+ });
+
+ [DllImport("sentry")]
+ private static extern int sentry_init(IntPtr options);
+
+ [DllImport("sentry")]
+ private static extern int sentry_close();
+
+ [DllImport("sentry")]
+ private static extern int sentry_get_crashed_last_run();
+
+ [DllImport("sentry")]
+ private static extern int sentry_clear_crashed_last_run();
+}
diff --git a/src/Sentry/Platforms/macOS/CocoaContextWriter.cs b/src/Sentry/Platforms/macOS/CocoaContextWriter.cs
new file mode 100644
index 0000000000..69efb2ae75
--- /dev/null
+++ b/src/Sentry/Platforms/macOS/CocoaContextWriter.cs
@@ -0,0 +1,102 @@
+using Sentry.Internal;
+
+namespace Sentry.macOS;
+
+internal class CocoaContextWriter
+{
+ protected void WriteScope(
+ string? AppStartTime,
+ string? AppBuildType,
+ string? OperatingSystemRawDescription,
+ int? DeviceProcessorCount,
+ string? DeviceCpuDescription,
+ string? DeviceTimezone,
+ bool? DeviceSupportsVibration,
+ string? DeviceName,
+ bool? DeviceSimulator,
+ string? DeviceDeviceUniqueIdentifier,
+ string? DeviceDeviceType,
+ string? DeviceModel,
+ long? DeviceMemorySize,
+ int? GpuId,
+ string? GpuName,
+ string? GpuVendorName,
+ int? GpuMemorySize,
+ string? GpuNpotSupport,
+ string? GpuVersion,
+ string? GpuApiType,
+ int? GpuMaxTextureSize,
+ bool? GpuSupportsDrawCallInstancing,
+ bool? GpuSupportsRayTracing,
+ bool? GpuSupportsComputeShaders,
+ bool? GpuSupportsGeometryShaders,
+ string? GpuVendorId,
+ bool? GpuMultiThreadedRendering,
+ string? GpuGraphicsShaderLevel
+ ) => SentryNativeBridgeWriteScope(
+ // // AppStartTime,
+ // AppBuildType,
+ // // OperatingSystemRawDescription,
+ // DeviceProcessorCount ?? 0,
+ // DeviceCpuDescription,
+ // DeviceTimezone,
+ // marshallNullableBool(DeviceSupportsVibration),
+ // DeviceName,
+ // marshallNullableBool(DeviceSimulator),
+ // DeviceDeviceUniqueIdentifier,
+ // DeviceDeviceType,
+ // // DeviceModel,
+ // // DeviceMemorySize,
+ GpuId ?? 0,
+ GpuName,
+ GpuVendorName,
+ GpuMemorySize ?? 0,
+ GpuNpotSupport,
+ GpuVersion,
+ GpuApiType,
+ GpuMaxTextureSize ?? 0,
+ marshallNullableBool(GpuSupportsDrawCallInstancing),
+ marshallNullableBool(GpuSupportsRayTracing),
+ marshallNullableBool(GpuSupportsComputeShaders),
+ marshallNullableBool(GpuSupportsGeometryShaders),
+ GpuVendorId,
+ marshallNullableBool(GpuMultiThreadedRendering),
+ GpuGraphicsShaderLevel
+ );
+
+ private static sbyte marshallNullableBool(bool? value) => (sbyte)(value.HasValue ? (value.Value ? 1 : 0) : -1);
+
+ // Note: we only forward information that's missing or significantly different in cocoa SDK events.
+ // Additionally, there's currently no way to update existing contexts, so no more Device info for now...
+ [DllImport("libBridge")]
+ private static extern void SentryNativeBridgeWriteScope(
+ // // string? AppStartTime,
+ // string? AppBuildType,
+ // // string? OperatingSystemRawDescription,
+ // int DeviceProcessorCount,
+ // string? DeviceCpuDescription,
+ // string? DeviceTimezone,
+ // sbyte DeviceSupportsVibration,
+ // string? DeviceName,
+ // sbyte DeviceSimulator,
+ // string? DeviceDeviceUniqueIdentifier,
+ // string? DeviceDeviceType,
+ // // string? DeviceModel,
+ // // long? DeviceMemorySize,
+ int GpuId,
+ string? GpuName,
+ string? GpuVendorName,
+ int GpuMemorySize,
+ string? GpuNpotSupport,
+ string? GpuVersion,
+ string? GpuApiType,
+ int GpuMaxTextureSize,
+ sbyte GpuSupportsDrawCallInstancing,
+ sbyte GpuSupportsRayTracing,
+ sbyte GpuSupportsComputeShaders,
+ sbyte GpuSupportsGeometryShaders,
+ string? GpuVendorId,
+ sbyte GpuMultiThreadedRendering,
+ string? GpuGraphicsShaderLevel
+ );
+}
diff --git a/src/Sentry/Platforms/macOS/CocoaScopeObserver.cs b/src/Sentry/Platforms/macOS/CocoaScopeObserver.cs
new file mode 100644
index 0000000000..20fadba1ba
--- /dev/null
+++ b/src/Sentry/Platforms/macOS/CocoaScopeObserver.cs
@@ -0,0 +1,43 @@
+namespace Sentry.macOS;
+
+internal class CocoaScopeObserver : ScopeObserver
+{
+ public CocoaScopeObserver(string name, SentryOptions options) : base(name, options) { }
+
+ public override void AddBreadcrumbImpl(Breadcrumb breadcrumb)
+ {
+ var level = GetBreadcrumbLevel(breadcrumb.Level);
+ var timestamp = GetTimestamp(breadcrumb.Timestamp);
+
+ SentryCocoaBridgeProxy.SentryNativeBridgeAddBreadcrumb(timestamp, breadcrumb.Message, breadcrumb.Type, breadcrumb.Category, level);
+ }
+
+ public override void SetExtraImpl(string key, string? value) =>
+ SentryCocoaBridgeProxy.SentryNativeBridgeSetExtra(key, value);
+
+ public override void SetTagImpl(string key, string value) => SentryCocoaBridgeProxy.SentryNativeBridgeSetTag(key, value);
+
+ public override void UnsetTagImpl(string key) => SentryCocoaBridgeProxy.SentryNativeBridgeUnsetTag(key);
+
+ public override void SetUserImpl(User user) =>
+ SentryCocoaBridgeProxy.SentryNativeBridgeSetUser(user.Email, user.Id, user.IpAddress, user.Username);
+
+ public override void UnsetUserImpl() => SentryCocoaBridgeProxy.SentryNativeBridgeUnsetUser();
+
+ internal static string GetTimestamp(DateTimeOffset timestamp) =>
+ // "o": Using ISO 8601 to make sure the timestamp makes it to the bridge correctly.
+ // https://docs.microsoft.com/en-gb/dotnet/standard/base-types/standard-date-and-time-format-strings#Roundtrip
+ timestamp.ToString("o");
+
+ internal static int GetBreadcrumbLevel(BreadcrumbLevel breadcrumbLevel) =>
+ // https://github.com/getsentry/sentry-cocoa/blob/50f955aeb214601dd62b5dae7abdaddc8a1f24d9/Sources/Sentry/Public/SentryDefines.h#L99-L105
+ breadcrumbLevel switch
+ {
+ BreadcrumbLevel.Debug => 1,
+ BreadcrumbLevel.Info => 2,
+ BreadcrumbLevel.Warning => 3,
+ BreadcrumbLevel.Error => 4,
+ BreadcrumbLevel.Critical => 5,
+ _ => 0
+ };
+}
diff --git a/src/Sentry/Platforms/macOS/Sentry.macOS.props b/src/Sentry/Platforms/macOS/Sentry.macOS.props
new file mode 100644
index 0000000000..2aa2ee23fb
--- /dev/null
+++ b/src/Sentry/Platforms/macOS/Sentry.macOS.props
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+ PreserveNewest
+ true
+ %(RecursiveDir)\%(Filename)%(Extension)
+
+ .
+
+
+
+
+
+
diff --git a/src/Sentry/Platforms/macOS/SentryCocoaBridge.cs b/src/Sentry/Platforms/macOS/SentryCocoaBridge.cs
new file mode 100644
index 0000000000..4bd0b864cc
--- /dev/null
+++ b/src/Sentry/Platforms/macOS/SentryCocoaBridge.cs
@@ -0,0 +1,53 @@
+using Sentry.Extensibility;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.iOS/SentryNativeCocoa.cs
+
+namespace Sentry.macOS;
+
+ ///
+ /// Access to the Sentry native support on iOS/macOS.
+ ///
+ internal static class SentryCocoaBridge
+ {
+ internal static void Configure(SentryOptions options)
+ // , ISentryUnityInfo sentryUnityInfo, RuntimePlatform platform)
+ {
+ if (!options.EnableNativeCrashReporting)
+ {
+ options.DiagnosticLogger?.LogDebug("Not initializing Sentry Cocoa. EnableNativeCrashReporting is false.");
+ return;
+ }
+ if (!SentryCocoaBridgeProxy.Init(options))
+ {
+ options.DiagnosticLogger?.LogError("Failed to initialize the native SDK. This doesn't affect .NET monitoring.");
+ return;
+ }
+
+ // options.NativeContextWriter = new CocoaContextWriter();
+ options.ScopeObserver = new CocoaScopeObserver("macOS", options);
+ options.EnableScopeSync = true;
+ options.CrashedLastRun = () =>
+ {
+ var crashedLastRun = SentryCocoaBridgeProxy.CrashedLastRun() == 1;
+ options.DiagnosticLogger?
+ .LogDebug("Native SDK reported: 'crashedLastRun': '{0}'", crashedLastRun);
+
+ return crashedLastRun;
+ };
+
+ // options.NativeSupportCloseCallback += () => Close(options.DiagnosticLogger);
+ // if (sentryUnityInfo.IL2CPP)
+ // {
+ // options.DefaultUserId = SentryCocoaBridgeProxy.GetInstallationId();
+ // }
+ }
+
+ ///
+ /// Closes the native Cocoa support.
+ ///
+ public static void Close(IDiagnosticLogger? logger = null)
+ {
+ logger?.LogDebug("Closing the sentry-cocoa SDK");
+ SentryCocoaBridgeProxy.Close();
+ }
+ }
diff --git a/src/Sentry/Platforms/macOS/SentryCocoaBridgeProxy.cs b/src/Sentry/Platforms/macOS/SentryCocoaBridgeProxy.cs
new file mode 100644
index 0000000000..7d3c3bb10c
--- /dev/null
+++ b/src/Sentry/Platforms/macOS/SentryCocoaBridgeProxy.cs
@@ -0,0 +1,110 @@
+using Sentry.Extensibility;
+
+namespace Sentry.macOS;
+
+// https://github.com/getsentry/sentry-unity/blob/3eb6eca6ed270c5ec023bf75ee53c1ca00bb7c82/src/Sentry.Unity.iOS/SentryCocoaBridgeProxy.cs
+
+///
+/// P/Invoke to SentryNativeBridge.m which communicates with the `sentry-cocoa` SDK.
+///
+///
+/// Functions are declared in `SentryNativeBridge.m`
+///
+///
+internal static class SentryCocoaBridgeProxy
+{
+ // Note: used on macOS only
+ public static bool Init(SentryOptions options)
+ {
+ if (LoadLibrary() != 1)
+ {
+ return false;
+ }
+
+ var cOptions = OptionsNew();
+
+ // Note: DSN is not null because options.IsValid() must have returned true for this to be called.
+ OptionsSetString(cOptions, "dsn", options.Dsn!);
+
+ if (options.Release is not null)
+ {
+ options.DiagnosticLogger?.LogDebug("Setting Release: {0}", options.Release);
+ OptionsSetString(cOptions, "release", options.Release);
+ }
+
+ if (options.Environment is not null)
+ {
+ options.DiagnosticLogger?.LogDebug("Setting Environment: {0}", options.Environment);
+ OptionsSetString(cOptions, "environment", options.Environment);
+ }
+
+ options.DiagnosticLogger?.LogDebug("Setting Debug: {0}", options.Debug);
+ OptionsSetInt(cOptions, "debug", options.Debug ? 1 : 0);
+
+ var diagnosticLevel = options.DiagnosticLevel.ToString().ToLowerInvariant();
+ options.DiagnosticLogger?.LogDebug("Setting DiagnosticLevel: {0}", diagnosticLevel);
+ OptionsSetString(cOptions, "diagnosticLevel", diagnosticLevel);
+
+ options.DiagnosticLogger?.LogDebug("Setting SendDefaultPii: {0}", options.SendDefaultPii);
+ OptionsSetInt(cOptions, "sendDefaultPii", options.SendDefaultPii ? 1 : 0);
+
+ // macOS screenshots currently don't work, because there's no UIKit. Cocoa logs: "Sentry - info:: NO UIKit"
+ // options.DiagnosticLogger?.LogDebug("Setting AttachScreenshot: {0}", options.AttachScreenshot);
+ // OptionsSetInt(cOptions, "attachScreenshot", options.AttachScreenshot ? 1 : 0);
+ OptionsSetInt(cOptions, "attachScreenshot", 0);
+
+ options.DiagnosticLogger?.LogDebug("Setting MaxBreadcrumbs: {0}", options.MaxBreadcrumbs);
+ OptionsSetInt(cOptions, "maxBreadcrumbs", options.MaxBreadcrumbs);
+
+ options.DiagnosticLogger?.LogDebug("Setting MaxCacheItems: {0}", options.MaxCacheItems);
+ OptionsSetInt(cOptions, "maxCacheItems", options.MaxCacheItems);
+
+ StartWithOptions(cOptions);
+ return true;
+ }
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeLoadLibrary")]
+ private static extern int LoadLibrary();
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeOptionsNew")]
+ private static extern IntPtr OptionsNew();
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeOptionsSetString")]
+ private static extern void OptionsSetString(IntPtr options, string name, string value);
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeOptionsSetInt")]
+ private static extern void OptionsSetInt(IntPtr options, string name, int value);
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeStartWithOptions")]
+ private static extern void StartWithOptions(IntPtr options);
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeCrashedLastRun")]
+ public static extern int CrashedLastRun();
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeClose")]
+ public static extern void Close();
+
+ [DllImport("libBridge")]
+ public static extern void SentryNativeBridgeAddBreadcrumb(string timestamp, string? message, string? type, string? category, int level);
+
+ [DllImport("libBridge")]
+ public static extern void SentryNativeBridgeSetExtra(string key, string? value);
+
+ [DllImport("libBridge")]
+ public static extern void SentryNativeBridgeSetTag(string key, string value);
+
+ [DllImport("libBridge")]
+ public static extern void SentryNativeBridgeUnsetTag(string key);
+
+ [DllImport("libBridge")]
+ public static extern void SentryNativeBridgeSetUser(string? email, string? userId, string? ipAddress, string? username);
+
+ [DllImport("libBridge")]
+ public static extern void SentryNativeBridgeUnsetUser();
+
+ [DllImport("libBridge", EntryPoint = "SentryNativeBridgeGetInstallationId")]
+ public static extern string GetInstallationId();
+
+ [DllImport("libBridge", EntryPoint = "crashBridge")]
+ public static extern void Crash();
+}
diff --git a/src/Sentry/Platforms/macOS/SentryOptions.cs b/src/Sentry/Platforms/macOS/SentryOptions.cs
new file mode 100644
index 0000000000..8a0dda6514
--- /dev/null
+++ b/src/Sentry/Platforms/macOS/SentryOptions.cs
@@ -0,0 +1,9 @@
+namespace Sentry;
+
+public partial class SentryOptions
+{
+ ///
+ /// Enables native macOS crash reporting.
+ ///
+ public bool EnableNativeCrashReporting { get; set; } = true;
+}
diff --git a/src/Sentry/Sentry.csproj b/src/Sentry/Sentry.csproj
index aae6ad3a30..f109d955be 100644
--- a/src/Sentry/Sentry.csproj
+++ b/src/Sentry/Sentry.csproj
@@ -4,6 +4,8 @@
Official SDK for Sentry - Open-source error tracking that helps developers monitor and fix crashes in real time.
$(NoWarn);RS0017
true
+
+ true
true
@@ -12,6 +14,7 @@
$(TargetFrameworks);net7.0-android
$(TargetFrameworks);net7.0-ios
$(TargetFrameworks);net7.0-maccatalyst
+ $(TargetFrameworks);net7.0-macos
@@ -22,6 +25,7 @@
+