Skip to content
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

Throw ArgumentNullException if DSN is null #2655

Merged
merged 9 commits into from
Sep 28, 2023

Conversation

jamescrosswell
Copy link
Collaborator

@jamescrosswell jamescrosswell commented Sep 26, 2023

Completes #2494

Migration notes

In Sentry.NET SDK 3.x.x and below, it was possible to initialise Sentry with a DisabledHub by setting the DSN to either String.Empty or null. However this sometimes tripped up people who were new to Sentry and/or had forgotten to configure the DSN in their Sentry options.

As such, starting with version 4.x.x of the SDK, if you want to configure a DisabledHub then you have to set the DSN to String.Empty. If you ommit the DSN altogether or forget to supply this, Sentry will throw an exception during initialization to let you know you've made a mistake.

@jamescrosswell jamescrosswell changed the base branch from main to feat/4.0.0 September 26, 2023 03:42
@github-actions
Copy link
Contributor

github-actions bot commented Sep 26, 2023

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against 4b39688

@jamescrosswell jamescrosswell added this to the 4.0.0 milestone Sep 26, 2023
src/Sentry/Internal/SettingLocator.cs Outdated Show resolved Hide resolved
src/Sentry/SentrySdk.cs Outdated Show resolved Hide resolved
if (dsn != null)
_options.Dsn ??= GetEnvironmentVariable(Constants.DsnEnvironmentVariable)
?? AssemblyForAttributes?.GetCustomAttribute<DsnAttribute>()?.Dsn;
if (_options.Dsn is null)
Copy link
Member

Choose a reason for hiding this comment

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

Do we always call the locator after any framework bootstrapping phase? Like ASP.NET Core Configuration system? I imagine integration tests would break otherwise but worth testing a sample that loads the DSN from appsettings.json

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah I think it's all covered by the integration tests.

Some manual smoke tests on my part:

ASP.NET Core

Configure Disabled DSN as Assembly Attribute and removing from Options Callback

Setting this in the Directory.Build.props in the Samples directory and removing any other DSN configuration in C# resulted in a DisabledHub (as expected):

  <ItemGroup>
    <SentryAttributes Include="Sentry.DsnAttribute">
      <_Parameter1></_Parameter1>
    </SentryAttributes>
  </ItemGroup>

Removing DSN as Assembly Attribute and from Options Callback

Removing the Assembly Attribute from the Directory.Build.props in the Samples directory and removing any other DSN configuration in C# code resulted in an ArgumentNullException, as expected:

/usr/local/share/dotnet/dotnet /Users/jamescrosswell/code/sentry-dotnet/samples/Sentry.Samples.AspNetCore.Basic/bin/Debug/net6.0/Sentry.Samples.AspNetCore.Basic.dll
  Debug: Logging enabled with ConsoleDiagnosticLogger and min level: Debug
Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'You must supply a DSN to use Sentry.To disable Sentry, pass an empty string: "".See https://docs.sentry.io/platforms/dotnet/configuration/options/#dsn')
   at Sentry.Internal.SettingLocator.GetDsn() in /Users/jamescrosswell/code/sentry-dotnet/src/Sentry/Internal/SettingLocator.cs:line 42

CHANGELOG.md Outdated Show resolved Hide resolved
{
return dsn;
throw new ArgumentNullException(
Copy link
Member

Choose a reason for hiding this comment

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

Also, kind of unrelated, if we can use the new ArgumentNullException.ThrowIfNull(); and friends. We'll help JIT emit faster code.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we can use ArgumentNullException.ThrowIfNull() as it doesn't allow you to pass an exception message (only the name of the parameter that was null).

@bruno-garcia bruno-garcia changed the title Feat/4.0.0 throw if no dsn 2494 Feat/4.0.0 throw if dsn is null Sep 26, 2023
@jamescrosswell jamescrosswell changed the title Feat/4.0.0 throw if dsn is null Throw ArgumentNullException if DSN is null Sep 26, 2023
@slonopotamus
Copy link

slonopotamus commented Feb 8, 2024

So how are we now supposed to write ASP.NET apps that want to conditionally enable Sentry?

Before this PR, I unconditionally called SentryWebHostBuilderExtensions.UseSentry. And then controlled whether Sentry will actually be used by passing (or not passing) DSN via SENTRY_DSN environment variable.

But now I can't do that.

If I call SentryWebHostBuilderExtensions.UseSentry with DSN=string.Empty, then DSN cannot be overriden with SENTRY_DSN env variable: https://github.com/getsentry/sentry-dotnet/blob/4.0.3/src/Sentry/Internal/SettingLocator.cs#L38
And if I call SentryWebHostBuilderExtensions.UseSentry without passing DSN (so it is null), it throws ArgumentNullException exception introduced in current PR.

@bitsandfoxes bitsandfoxes restored the feat/4.0.0-throw-if-no-dsn-2494 branch February 8, 2024 16:50
@bitsandfoxes
Copy link
Contributor

Hey @slonopotamus. Looks like we made a mistake there with our assumptions. Based on your input I think we should not have changed it in the first place. We'll fix this asap.

@bitsandfoxes bitsandfoxes deleted the feat/4.0.0-throw-if-no-dsn-2494 branch February 8, 2024 20:47
@jamescrosswell
Copy link
Collaborator Author

Hey @slonopotamus. Looks like we made a mistake there with our assumptions. Based on your input I think we should not have changed it in the first place. We'll fix this asap.

Hm, I'm not so sure. If @slonopotamus is providing a DSN via an environment variable then it shouldn't throw. I think this is an issue with the config bindings that needs to be worked through.

@slonopotamus are you able to provide some minimal sample code illustrarting how you're initialising the SDK?

I just want to double check there isn't a way to achieve what you want with the current set of initialization APIs before we throw the baby out with the bathwater. It is really useful to throw an exception when people try to initialize the SDK without supplying a DSN (it saves us a lot of support requests).

@jamescrosswell
Copy link
Collaborator Author

So how are we now supposed to write ASP.NET apps that want to conditionally enable Sentry?

@slonopotamus I think I follow what you're trying to do now. I believe you should be able to achieve what you want with this:

builder.WebHost.UseSentry((SentryAspNetCoreOptions options) =>
{
    options.Dsn ??= SentryConstants.DisableSdkDsnValue;
});

That code will explicitly disable Sentry if the Dsn has not been provided via configuration binding...

@slonopotamus
Copy link

Buy that won't fix SENTRY_DSN env value. I'll make a minimal testcase soon.

@slonopotamus
Copy link

slonopotamus commented Feb 9, 2024

Minimal repro:

using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;

var webHost = WebHost
    .CreateDefaultBuilder()
    .UseSentry()
    .UseKestrel()
    .Configure(
        applicationBuilder => applicationBuilder
            .UseRouting())
    .Build();

await webHost.RunAsync();

As I said, with Sentry 3.x, this works the following way:

  • By default, Sentry is disabled
  • If I pass SENTRY_DSN via environment, Sentry is enabled and sends data

But with Sentry 4.0.x, this fails:

Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'You must supply a DSN to use Sentry.To disable Sentry, pass an empty string: "".See https://docs.sentry.io/platforms/dotnet/configuration/options/#dsn')
   at Sentry.Internal.SettingLocator.GetDsn()
   at Sentry.SentrySdk.InitHub(SentryOptions options)
   at Sentry.Extensions.Logging.Extensions.DependencyInjection.ServiceCollectionExtensions.<>c__0`1.<AddSentry>b__0_3(IServiceProvider c)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Sentry.Extensions.Logging.Extensions.DependencyInjection.ServiceCollectionExtensions.<>c__0`1.<AddSentry>b__0_2(IServiceProvider c)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitDisposeCache(ServiceCallSite transientCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitIEnumerable(IEnumerableCallSite enumerableCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitConstructor(ConstructorCallSite constructorCallSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSiteMain(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.VisitRootCache(ServiceCallSite callSite, RuntimeResolverContext context)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteVisitor`2.VisitCallSite(ServiceCallSite callSite, TArgument argument)
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.CallSiteRuntimeResolver.Resolve(ServiceCallSite callSite, ServiceProviderEngineScope scope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.CreateServiceAccessor(ServiceIdentifier serviceIdentifier)
   at System.Collections.Concurrent.ConcurrentDictionary`2.GetOrAdd(TKey key, Func`2 valueFactory)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(ServiceIdentifier serviceIdentifier, ServiceProviderEngineScope serviceProviderEngineScope)
   at Microsoft.Extensions.DependencyInjection.ServiceProvider.GetService(Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService(IServiceProvider provider, Type serviceType)
   at Microsoft.Extensions.DependencyInjection.ServiceProviderServiceExtensions.GetRequiredService[T](IServiceProvider provider)
   at Microsoft.AspNetCore.Hosting.WebHostBuilder.Build()
   at Program.<Main>$(String[] args) in Program.cs:line 5
   at Program.<Main>(String[] args)

If I use UseSentry(string.Empty) as you suggest, SENTRY_DSN env variable will be ignored because of how code is written in SettingLocator.

@jamescrosswell
Copy link
Collaborator Author

As I said, with Sentry 3.x, this works the following way:

  • By default, Sentry is disabled
  • If I pass SENTRY_DSN via environment, Sentry is enabled and sends data

But with Sentry 4.0.x, this fails:

OK understood. This is the same behaviour another customer wanted as well - see comment here for a solution.

@bitsandfoxes maybe we add that to the migration guide for people who want to force Sentry 4.x to behave like Sentry 3.x in this respect?

@slonopotamus
Copy link

slonopotamus commented Feb 9, 2024

Just in case - I would agree if you said that what I do is now wrong and suggested a different approach, but I don't see how that is possible with current code, because env variable has lower priority.

Whoops, I missed a link in your comment

@jamescrosswell
Copy link
Collaborator Author

Whoops, I missed a link in your comment

All good. Now I think I understand your problem better, we should be able to improve our docs:

@slonopotamus
Copy link

After thinking about it more, I think I will go the different route. I am going to UseSentry(string.Empty) plus AddEnvironment() and then provide DSN address via a different env variable directly into .NET configuration. Afair, it would be ASPNET_blabla or maybe DOTNET_blabla.

@jamescrosswell
Copy link
Collaborator Author

That's probably more explicit and easier for other dev's on the team to understand yeah 👍

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.

Sentry should raise an exception if the DSN has not been configured
4 participants