Skip to content

Rework Initialization #2227

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 37 commits into
base: chore/cleanup-init
Choose a base branch
from
Open

Conversation

bitsandfoxes
Copy link
Contributor

@bitsandfoxes bitsandfoxes commented Jul 3, 2025

Fixes #2200
Required getsentry/sentry-dotnet#4322

Goal

As stated in the issue: The SDK needs to initializeable in two ways: self, and manual. Both these paths need to provide the same functionality.

Approach

  1. Hide the .NET SDK static API by making it internal
  2. Mirror the now hidden API into Sentry.Unity.SentrySdk - This way the Unity SDK can omit certain methods like the .NET SDKs Init
  3. Hide the SentryInitialization.cs that is getting compiled with the game
  4. Have all actual initialization - that includes platform specific configuration like scope sync - happen within the pre-compiled SDK.
  5. The code that gets compiled with the game is strictly for setting up and calling Init. This allows users to call the same method.

Implementation Details

Hide the .NET SDK

This is done by conditionally making the .NET SDKs SentrySdks accessor internal getsentry/sentry-dotnet#4322
The .NET SDK now considers Unity as a platform and our very own .props allows us to conditionally set a constant.

Rename Rename SentryUnity to SentrySdk

Ideally, we'd only have one static API that allows for discoverability of what's available. This could either be SentrySdk or SentryUnity. I'm going with SentrySdk here as:

  • it's a smoother migration to fix usings. This change will require users to change them from using Sentry; to using Sentry.Unity. Which is still easier than having to go through every instance of calling into the SDK and change these from SentrySdk to SentryUnity.
  • it resides in the Sentry.Unity namespace and avoids conflicts. Attempting to access Sentry.SentrySdk.Capture will create an error Cannot access internal class 'SentrySdk' here.
    • We're helping out here by adding a summary to the internal SentrySdk guiding users towards the right namespace

Make Sentry.Unity.SentrySdk a partial class

The Unity SDK version of SentrySdk is now split in two partial classes.

SentrySdk.cs is where the Unity SDK specific code resides and basically contains "overwritten" methods like Init, GetLastRunState.

SentrySdk.Dotnet.cs - TODO: This should be source generated and end up as SentrySdk.Dotnet.g.cs
The .NET SDK SentrySdk is now hidden and inaccessible. To give users access to the rest of the API we're putting all the rest of the static API in there hand have it forward the calls into the .NET SDK.

Hide the .cs scripts SentryInitialization and SentryIntegrations

Our package ships with io.sentry.asmdef which creates our own assembly for us within the game's project. This allows us to make these classes internal, but keep their name. They are now undiscoverable for users - and should have been so in the first place.

Changes to SentryInitialization

The SentryInitialization still receives the "first thing that happens when the game starts callback via the RuntimeLoad attribute. This allows us to set up the SDK all the way to be ready to be initialized. Instead of doing the actual initialization inside the script that gets compiled with the game, this now injects the platform specific configure calls into the SDK.

Add a static SentryPlatformServices

This is now the place that holds the injected platform specific configure callbacks and the IUnityInfo that we use to access version and platform specific code like IL2CPP runtime checks and backend calls for line number support.

We're setting these as the very first thing and can so guarantee that they are available, regardless whether the SDK auto-initializes or has the user call Init manually.

Move all initialization code into the SDK

Having the platform specific configure callbacks available via the SentryPlatformServices class allows us to call conditionally compiled code from within the pre-compiled SDK. All initialization paths funnel back together in that one Init call.

Migration Path

Unfortunately, this is a breaking change. As such, we need to provide a migration path. When updating the SDK users will face the following error on the console

Screenshot 2025-07-07 at 13 25 24

Since we can modify the now internal SDK's <summary> we can already put some help there

Screenshot 2025-07-07 at 13 27 41

IDEs like Rider will allow to automatically import the missing references

using Sentry;
using Sentry.Unity;

Things to follow up on

using Sentry.Unity vs. using Sentry

The entirety of the API is now discoverable within SentrySdk. but the namespace is split

  • Sentry - Contains the .NET SDK's types, i.e. SentryId
  • Sentry.Unity - Contains the static API

So users end up in situations where they need to always bring using Sentry.Unity and sometimes additionally need using Sentry. That was an issue before already, it's just something to be aware of.

Consider turning IsEnabled in the config window into isSelfInitEnabled

During build the SDK was opinionated regarding whether to bring a native SDK and tied setup and symbol upload to the isEnabled check. When we merged all option-configuration callbacks into one in #1888 the line already blurred.
And now, the SDK cannot reasonably be certain whether when it will be initialized at runtime. With this I think we have to consider

  1. IsEnabled cannot be considered anymore
  2. Is the symbol upload enabled -> upload symbols
  3. Is the native support enabled -> add native SDK
  4. On mobile: If IsEnabled and InitializationType.BuildTime -> setup native project

Source Generator for Sentry.Dotnet

Right now the Sentry.Dotnet.cs is created by hand. It forwards all calls to the now internal static API while omitting Init. This allows us to keep the Unity specific methods in the SentrySdk.cs file but it also requires us to manually update if and when the .NET SDK's public API changes.

Alternative (attempted) approaches

  • I gave extension members a try by bumping to C#14. But this cannot be consumed by the user's code.
  • Making only SentrySdk.Init methods internal. This somewhat works but gets us stuck in a place. We'd have the API split between SentryUnity.Init vs SentrySdk.Capture.
  • Going with SentryUnity as name for the type for the static API. But - and that's an assumption on my part - that would break the actual calls from SentrySdk.CaptureException() SentryUnity.CaptureException() instead of having the IDE fix the usings and be done with it.
  • Adding the Unity SDK init to the Sentry namespace. This did not work for conflicting reasons.

@bitsandfoxes bitsandfoxes changed the title initial working implementation Rework Initialization Jul 3, 2025
@@ -86,7 +86,7 @@ public void SendFeedback()
else
{
// Since there is no screenshot added we can capture the feedback right away
SentryUnity.CaptureFeedback(_description.text, _email.text, _name.text, addScreenshot: false);
SentrySdk.CaptureFeedback(_description.text, _email.text, _name.text, addScreenshot: false);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the desired effect. All things reside within SentrySdk.

@@ -39,7 +41,7 @@

namespace Sentry.Unity
{
public static class SentryInitialization
internal static class SentryInitialization
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can safely make this internal. The SDK ships with it's own .asmdef that allows us to hide internal types from the user.

@@ -0,0 +1,15 @@
using System;

namespace Sentry.Unity.NativeUtils;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting the services into a very distinctive namespace to "discourage" usage.

@@ -120,12 +121,12 @@ public static string GetConfigPath(string? notDefaultConfigName = null)
/// <remarks>
/// Used for loading the SentryUnityOptions from the ScriptableSentryUnityOptions during runtime.
/// </remarks>
public static SentryUnityOptions? LoadSentryUnityOptions(ISentryUnityInfo unityInfo)
public static SentryUnityOptions? LoadSentryUnityOptions()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We no longer need to provide the IUnityInfo since these are getting set up during startup as part of the new platform services that are statically available.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should get source generated in a follow up PR.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a better approach than the test I recommended 👍

@@ -49,11 +49,11 @@ private SentryUnitySdk(SentryUnityOptions options)
}
}

unitySdk._dotnetSdk = SentrySdk.Init(options);
unitySdk._dotnetSdk = Sentry.SentrySdk.Init(options);
Copy link
Contributor Author

@bitsandfoxes bitsandfoxes Jul 7, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A drawback is that we now need to fully qualify calls through the static API from within the Unity SDK. But our users won't have that problem.

@@ -79,7 +79,7 @@ public void Arguments()

// act
MainThreadData.SentrySystemInfo = sysInfo;
SentryUnity.Init(options);
SentrySdk.Init(options);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No more separation between SentryUnity and SentrySdk.

Copy link
Member

@bruno-garcia bruno-garcia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

quick pass


Logger.LogDebug("Creating '{0}' span.", InitSpanOperation);
InitSpan = runtimeStartTransaction.StartChild(InitSpanOperation, "runtime initialization");
Logger.LogDebug("Creating '{0}' span.", SubSystemSpanOperation);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could these two be combined in 1 log message? Debug log can be overwhelming at times

/// We do not intend to remove it.
/// </remarks>
[Obsolete("WARNING: This method deliberately causes a crash, and should not be used in a real application.")]
public static void CauseCrash(CrashType crashType) => Sentry.SentrySdk.CauseCrash(crashType);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since this now will require manually keeping in sync. You could have a test that does reflection on SentrySdk and gets all public members, does the same on the .NET and Unity SDKs and compares the two to make sure there's no new APIs on the .NET side

@bitsandfoxes bitsandfoxes changed the base branch from main to chore/cleanup-init July 10, 2025 14:02
@bitsandfoxes bitsandfoxes marked this pull request as ready for review July 10, 2025 14:34
Copy link
Member

@bruno-garcia bruno-garcia left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I didn't notice this before but while in general I think this is a great approach, we now expect the static fields to be set before our public methods are called but we don't verify anywhere that's the case and we have a lot of null propagation patterns which hinder the ability to debug what was going on. In particular when customers change the init code, possibly bypassing our auto init/static property set in order to do things "by hand".


- **Breaking Change**: The Unity SDK's static API has been simplified moved from `Sentry.Unity.SentryUnity` and `Sentry.SentrySdk`
to `Sentry.Unity.SentrySdk`.
This change enables manual SDK initialization with full functionality, previously only available through auto-initialization.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
This change enables manual SDK initialization with full functionality, previously only available through auto-initialization.
This change enables [manual/programatic SDK initialization](https://docs.sentry.io/platforms/unity/configuration/options/programmatic-configuration/) with full functionality, previously only available through auto-initialization.

{
Logger = options.DiagnosticLogger;

Logger?.LogInfo("Attempting to configure native support via the Native SDK");

if (!sentryUnityInfo.IsNativeSupportEnabled(options, ApplicationAdapter.Instance.Platform))
if (!sentryUnityInfo?.IsNativeSupportEnabled(options, ApplicationAdapter.Instance.Platform) ?? false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sentryUnityInfo being null is a bigger problem, no?

Shoujld we split this code so we can log specifically sentryUnityInfo being null as an error. And the native support being off can stay as debug, as-is?

{
_isLinux = sentryUnityInfo.IsLinux();
_isLinux = sentryUnityInfo?.IsLinux() ?? false;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

since this is all internal code, should we expect null ISentryUnityInfo?
Seems to me the caller should either have the instance or avoid getting an insance of this altogether?

If we have that null here, we'd still go ahead initializing everything below. Is that creating a weird scenario where instead we should have avoided Init being successful?

/// <param name="sentryUnityInfo">Infos about the current Unity environment</param>
public static void Configure(SentryUnityOptions options, ISentryUnityInfo sentryUnityInfo) =>
Configure(options, sentryUnityInfo, ApplicationAdapter.Instance.Platform);
public static void Configure(SentryUnityOptions options) =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a public method, and we only need to pass options.
But if the static SentryPlatformServices.UnityInfo wasn't set properly before hand, we'll just pass that through below and it'll be hard to trace back why that was.

I think we could validate here (if we expect SentryPlatformServices.UnityInfo not to be null here) that it is indeed not null. And deal with it (log and continue, throw?)

{
options.DiagnosticLogger?.LogInfo("Attempting to configure native support via the Cocoa SDK");

if (!sentryUnityInfo.IsNativeSupportEnabled(options, platform))
if (!sentryUnityInfo?.IsNativeSupportEnabled(options, platform) ?? false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

same situation where it's not clear if it's a mix up with setting up this static thing before calling this, or IsNativeSupportEnabled returning false, and making things hard to debug

@@ -67,7 +67,7 @@ internal static void Configure(SentryUnityOptions options, ISentryUnityInfo sent
};

options.NativeSupportCloseCallback += () => Close(options, sentryUnityInfo, platform);
if (sentryUnityInfo.IL2CPP)
if (sentryUnityInfo?.IL2CPP ?? false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

also, lets avoid this pattern throughout.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a better approach than the test I recommended 👍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Improve SDK Initialization
3 participants