Skip to content
New issue

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

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

Already on GitHub? # to your account

feat: options.AddProfilingIntegration() #3660

Merged
merged 9 commits into from
Nov 22, 2024
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Features

- Added `SentryOptions` extension for profiling: `options.AddProfilingIntegration()` ([#3660](https://github.com/getsentry/sentry-dotnet/pull/3660))

### Fixes

- Fixed ArgumentNullException in FormRequestPayloadExtractor when handling invalid form data on ASP.NET ([#3734](https://github.com/getsentry/sentry-dotnet/pull/3734))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
builder.WebHost.UseSentry(o =>
{
o.Dsn = "https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537";
o.AddIntegration(new ProfilingIntegration());
o.AddProfilingIntegration();
o.ProfilesSampleRate = 0.1;
o.TracesSampleRate = 1.0;
});
Expand Down
2 changes: 1 addition & 1 deletion samples/Sentry.Samples.Console.Profiling/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ private static void Main()
// Debugging
options.ShutdownTimeout = TimeSpan.FromMinutes(5);

options.AddIntegration(new ProfilingIntegration(TimeSpan.FromMilliseconds(500)));
options.AddProfilingIntegration(TimeSpan.FromMilliseconds(500));
}))
{
var tx = SentrySdk.StartTransaction("app", "run");
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry.Profiling/ProfilingIntegration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,9 @@ public void Register(IHub hub, SentryOptions options)
options.LogError(e, "Failed to initialize the profiler");
}
}
else
{
options.LogInfo("Profiling Integration is disabled because profiling is disabled by configuration.");
}
}
}
44 changes: 44 additions & 0 deletions src/Sentry.Profiling/SentryOptionsProfilingExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
using Sentry.Extensibility;
using Sentry.Profiling;

namespace Sentry;

/// <summary>
/// The additional Sentry Options extensions from Sentry Profiling.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
public static class SentryOptionsProfilingExtensions
{
/// <summary>
/// Adds ProfilingIntegration to Sentry.
/// </summary>
/// <param name="options">The Sentry options.</param>
/// <param name="startupTimeout">
/// If not given or TimeSpan.Zero, then the profiler initialization is asynchronous.
/// This is useful for applications that need to start quickly. The profiler will start in the background
/// and will be ready to capture transactions that have started after the profiler has started.
///
/// If given a non-zero timeout, profiling startup blocks up to the given amount of time. If the timeout is reached
/// and the profiler session hasn't started yet, the execution is unblocked and behaves as the async startup,
/// i.e. transactions will be profiled only after the session is eventually started.
/// </param>
public static void AddProfilingIntegration(this SentryOptions options, TimeSpan startupTimeout = default)
Copy link
Member

@bruno-garcia bruno-garcia Nov 16, 2024

Choose a reason for hiding this comment

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

We could though #if IOS here and have a no-op version?
"on iOS we use a native profiler"

Then it wouldn't matter during Set Up if we say AddProfilerIntegration on all platforms. It would just no-op on iOS?

#if IOS
// debug log?
return;
#endif

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

how about 5abd66d

Copy link
Member

Choose a reason for hiding this comment

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

Nice! that looks good

{
if (options.HasIntegration<ProfilingIntegration>())
{
options.LogWarning($"{nameof(ProfilingIntegration)} has already been added. The second call to {nameof(AddProfilingIntegration)} will be ignored.");
return;
}

options.AddIntegration(new ProfilingIntegration(startupTimeout));
}

/// <summary>
/// Disables the Profiling integration.
/// </summary>
/// <param name="options">The SentryOptions to remove the integration from.</param>
public static void DisableProfilingIntegration(this SentryOptions options)
{
options.RemoveIntegration<ProfilingIntegration>();
}
}
31 changes: 31 additions & 0 deletions src/Sentry/Platforms/Cocoa/ProfilingIntegration.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using Sentry.Extensibility;
using Sentry.Integrations;

namespace Sentry.Cocoa;

/// <summary>
/// Enables transaction performance profiling.
/// </summary>
public class ProfilingIntegration : ISdkIntegration
{
/// <inheritdoc/>
public void Register(IHub hub, SentryOptions options)
{
if (options.IsProfilingEnabled)
{
try
{
options.LogDebug("Profiling is enabled, attaching native SDK profiler factory");
options.TransactionProfilerFactory ??= new CocoaProfilerFactory(options);
}
catch (Exception e)
{
options.LogError(e, "Failed to initialize the profiler");
}
}
else
{
options.LogInfo("Profiling Integration is disabled because profiling is disabled by configuration.");
}
}
}
36 changes: 36 additions & 0 deletions src/Sentry/Platforms/Cocoa/SentryOptions.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using ObjCRuntime;
using Sentry.Cocoa;
using Sentry.Extensibility;

// ReSharper disable once CheckNamespace
namespace Sentry;
Expand Down Expand Up @@ -215,4 +217,38 @@ public void AddInAppInclude(string prefix)
InAppIncludes.Add(prefix);
}
}

// We actually add the profiling integration automatically in InitSentryCocoaSdk().
// However, if user calls AddProfilingIntegration() multiple times, we print a warning, as usual.
private bool _profilingIntegrationAddedByUser = false;

/// <summary>
/// Adds ProfilingIntegration to Sentry.
/// </summary>
/// <param name="startupTimeout">
/// Unused, only here so that the signature is the same as AddProfilingIntegration() from package Sentry.Profiling.
/// </param>
public void AddProfilingIntegration(TimeSpan startupTimeout = default)
{
if (HasIntegration<ProfilingIntegration>())
{
if (_profilingIntegrationAddedByUser)
{
DiagnosticLogger?.LogWarning($"{nameof(ProfilingIntegration)} has already been added. The second call to {nameof(AddProfilingIntegration)} will be ignored.");
Copy link
Member

Choose a reason for hiding this comment

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

Wondering if a warning is needed. Because if I have a shared Init logic for Sentry by used Apps, I just want profiling to be there. the fact iOS has it automatically but not the other platforms is irrelevant.

Suggested change
DiagnosticLogger?.LogWarning($"{nameof(ProfilingIntegration)} has already been added. The second call to {nameof(AddProfilingIntegration)} will be ignored.");
DiagnosticLogger?.LogDebug($"{nameof(ProfilingIntegration)} on iOS is added automatically. The second call to {nameof(AddProfilingIntegration)} will be ignored.");

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've added the warning (actually the exact line of code) from the other profiling integration. Arguably, it may be unnecessary to warn when you add the same thing multiple times, no clue why integrations have warnings about that.

Because if I have a shared Init logic for Sentry by used Apps, I just want profiling to be there. the fact iOS has it automatically but not the other platforms is irrelevant.

Doesn't play a role here. The warning only triggers when a user added the integration manually multiple times. Not when they add it once after we've added it automatically during init.

I'll go ahead and merge this as is so it's exactly the same as the other profiling integration (which also warns only when you add it manually multiple times). We can change it later in all the places with he same LogWarning if you think we should

}
return;
}

_profilingIntegrationAddedByUser = true;
AddIntegration(new ProfilingIntegration());
}

/// <summary>
/// Disables the Profiling integration.
/// </summary>
public void DisableProfilingIntegration()
{
_profilingIntegrationAddedByUser = false;
RemoveIntegration<ProfilingIntegration>();
}
}
8 changes: 2 additions & 6 deletions src/Sentry/Platforms/Cocoa/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,12 +192,8 @@ private static void InitSentryCocoaSdk(SentryOptions options)
options.EnableScopeSync = true;
options.ScopeObserver = new CocoaScopeObserver(options);

if (options.IsProfilingEnabled)
{
options.LogDebug("Profiling is enabled, attaching native SDK profiler factory");
options.TransactionProfilerFactory ??= new CocoaProfilerFactory(options);
}

// Note: don't use AddProfilingIntegration as it would print a warning if user used it too.
options.AddIntegration(new ProfilingIntegration());
// TODO: Pause/Resume
}

Expand Down
2 changes: 1 addition & 1 deletion src/Sentry/SentrySdk.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ internal static IHub InitHub(SentryOptions options)
#endif
{
LogWarningIfProfilingMisconfigured(options, ", because ProfilingIntegration from package Sentry.Profiling" +
" hasn't been registered. You can do that by calling 'options.AddIntegration(new ProfilingIntegration())'");
" hasn't been registered. You can do that by calling 'options.AddProfilingIntegration()'");
}
#endif

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
namespace Sentry.Profiling.Tests;

#nullable enable

[UsesVerify]

public class ProfilingSentryOptionsExtensionsTests
{
private readonly InMemoryDiagnosticLogger _logger = new();
private readonly SentryOptions _options = new()
{
Dsn = ValidDsn,
AutoSessionTracking = false,
IsGlobalModeEnabled = true,
BackgroundWorker = Substitute.For<IBackgroundWorker>(),
Debug = true,

// Set explicitly for this test in case the defaults change in the future.
TracesSampleRate = 0.0,
TracesSampler = null
};

public ProfilingSentryOptionsExtensionsTests()
{
_options.DiagnosticLogger = _logger;
_options.AddProfilingIntegration();
}

private Hub GetSut() => new(_options, Substitute.For<ISentryClient>());

private static IEnumerable<ISdkIntegration> GetIntegrations(ISentryClient hub) =>
hub.GetSentryOptions()?.Integrations ?? Enumerable.Empty<ISdkIntegration>();

[Fact]
public void Integration_DisabledWithDefaultOptions()
{
using var hub = GetSut();
var integrations = GetIntegrations(hub);
Assert.Contains(_logger.Entries, x => x.Message == "Profiling Integration is disabled because profiling is disabled by configuration."
&& x.Level == SentryLevel.Info);
}

[Fact]
public void Integration_EnabledBySampleRate()
{
_options.TracesSampleRate = 1.0;
_options.ProfilesSampleRate = 1.0;

using var hub = GetSut();
var integrations = GetIntegrations(hub);
Assert.Contains(integrations, i => i is ProfilingIntegration);
}

[Fact]
public void DisableProfilingIntegration_RemovesProfilingIntegration()
{
_options.TracesSampleRate = 1.0;
_options.ProfilesSampleRate = 1.0;
_options.DisableProfilingIntegration();

using var hub = GetSut();
var integrations = GetIntegrations(hub);
Assert.DoesNotContain(integrations, i => i is ProfilingIntegration);
}

[Fact]
public void AddProfilingIntegration_DoesntDuplicate()
{
var options = new SentryOptions();

options.AddProfilingIntegration();
options.AddProfilingIntegration();

Assert.Single(options.Integrations, x => x is ProfilingIntegration);
}
}
2 changes: 1 addition & 1 deletion test/Sentry.Profiling.Tests/Resources/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public static int Main(string[] args)
o.Dsn = DefaultDsn;
o.Debug = true;
o.TracesSampleRate = 1.0;
o.AddIntegration(new ProfilingIntegration(Path.GetTempPath()));
o.AddProfilingIntegration();
o.DiagnosticLogger = new FileAppenderDiagnosticLogger("C:/dev/Aura.UI/test.log", SentryLevel.Debug);
}))
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ async Task VerifyAsync(HttpRequestMessage message)
// Disable process exit flush to resolve "There is no currently active test." errors.
options.DisableAppDomainProcessExitFlush();

options.AddIntegration(new ProfilingIntegration(TimeSpan.FromSeconds(10)));
options.AddProfilingIntegration(TimeSpan.FromSeconds(10));

try
{
Expand Down Expand Up @@ -314,7 +314,7 @@ async Task VerifyAsync(HttpRequestMessage message)
ProfilesSampleRate = 1.0,
};

options.AddIntegration(new ProfilingIntegration(TimeSpan.FromSeconds(10)));
options.AddProfilingIntegration(TimeSpan.FromSeconds(10));

try
{
Expand Down Expand Up @@ -361,7 +361,7 @@ public void ProfilerIntegration_WithProfilingDisabled_LeavesFactoryNull()
TracesSampleRate = 1.0,
ProfilesSampleRate = 0,
};
options.AddIntegration(new ProfilingIntegration());
options.AddProfilingIntegration();
using var hub = new Hub(options);
Assert.Null(hub.Options.TransactionProfilerFactory);
}
Expand All @@ -375,7 +375,7 @@ public void ProfilerIntegration_WithTracingDisabled_LeavesFactoryNull()
TracesSampleRate = 0,
ProfilesSampleRate = 1.0,
};
options.AddIntegration(new ProfilingIntegration());
options.AddProfilingIntegration();
using var hub = new Hub(options);
Assert.Null(hub.Options.TransactionProfilerFactory);
}
Expand All @@ -389,7 +389,7 @@ public void ProfilerIntegration_WithProfilingEnabled_SetsFactory()
TracesSampleRate = 1.0,
ProfilesSampleRate = 1.0,
};
options.AddIntegration(new ProfilingIntegration());
options.AddProfilingIntegration();
using var hub = new Hub(options);
Assert.NotNull(hub.Options.TransactionProfilerFactory);
}
Expand Down
Loading