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

Set Scope.User.Id to the InstallationId by default #3425

Merged
merged 9 commits into from
Jun 20, 2024
Merged
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
- Fixed null IServiceProvider in anonymous routes with OpenTelemetry ([#3401](https://github.com/getsentry/sentry-dotnet/pull/3401))
- Fixed Trim warnings in Sentry.DiagnosticSource and WinUIUnhandledException integrations ([#3410](https://github.com/getsentry/sentry-dotnet/pull/3410))
- Fixed memory leak when tracing is enabled ([#3432](https://github.com/getsentry/sentry-dotnet/pull/3432))
- `Scope.User.Id` now correctly defaults to the InstallationId unless it has been set otherwise ([#3425](https://github.com/getsentry/sentry-dotnet/pull/3425))

### Dependencies

Expand Down
123 changes: 1 addition & 122 deletions src/Sentry/GlobalSessionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,12 @@ internal class GlobalSessionManager : ISessionManager
{
private const string PersistedSessionFileName = ".session";

private readonly object _installationIdLock = new();

private readonly ISystemClock _clock;
private readonly Func<string, PersistedSessionUpdate> _persistedSessionProvider;
private readonly SentryOptions _options;

private readonly string? _persistenceDirectoryPath;

private string? _resolvedInstallationId;
private SentrySession? _currentSession;
private DateTimeOffset? _lastPauseTimestamp;

Expand All @@ -42,124 +39,6 @@ internal class GlobalSessionManager : ISessionManager
_persistenceDirectoryPath = options.TryGetDsnSpecificCacheDirectoryPath();
}

private string? TryGetPersistentInstallationId()
{
try
{
var directoryPath =
_persistenceDirectoryPath
?? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"Sentry",
_options.Dsn!.GetHashString());

Directory.CreateDirectory(directoryPath);

_options.LogDebug("Created directory for installation ID file ({0}).", directoryPath);

var filePath = Path.Combine(directoryPath, ".installation");

// Read installation ID stored in a file
try
{
return File.ReadAllText(filePath);
}
catch (FileNotFoundException)
{
_options.LogDebug("File containing installation ID does not exist ({0}).", filePath);
}
catch (DirectoryNotFoundException)
{
// on PS4 we're seeing CreateDirectory work but ReadAllText throw DirectoryNotFoundException
_options.LogDebug("Directory containing installation ID does not exist ({0}).", filePath);
}

// Generate new installation ID and store it in a file
var id = Guid.NewGuid().ToString();
File.WriteAllText(filePath, id);

_options.LogDebug("Saved installation ID '{0}' to file '{1}'.", id, filePath);
return id;
}
// If there's no write permission or the platform doesn't support this, we handle
// and let the next installation id strategy kick in
catch (Exception ex)
{
_options.LogError(ex, "Failed to resolve persistent installation ID.");
return null;
}
}

private string? TryGetHardwareInstallationId()
{
try
{
// Get MAC address of the first network adapter
var installationId = NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic =>
nic.OperationalStatus == OperationalStatus.Up &&
nic.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Select(nic => nic.GetPhysicalAddress().ToString())
.FirstOrDefault();

if (string.IsNullOrWhiteSpace(installationId))
{
_options.LogError("Failed to find an appropriate network interface for installation ID.");
return null;
}

return installationId;
}
catch (Exception ex)
{
_options.LogError(ex, "Failed to resolve hardware installation ID.");
return null;
}
}

// Internal for testing
internal static string GetMachineNameInstallationId() =>
// Never fails
Environment.MachineName.GetHashString();

private string? TryGetInstallationId()
{
// Installation ID could have already been resolved by this point
if (!string.IsNullOrWhiteSpace(_resolvedInstallationId))
{
return _resolvedInstallationId;
}

// Resolve installation ID in a locked manner to guarantee consistency because ID can be non-deterministic.
// Note: in the future, this probably has to be synchronized across multiple processes too.
lock (_installationIdLock)
{
// We may have acquired the lock after another thread has already resolved
// installation ID, so check the cache one more time before proceeding with I/O.
if (!string.IsNullOrWhiteSpace(_resolvedInstallationId))
{
return _resolvedInstallationId;
}

var id =
TryGetPersistentInstallationId() ??
TryGetHardwareInstallationId() ??
GetMachineNameInstallationId();

if (!string.IsNullOrWhiteSpace(id))
{
_options.LogDebug("Resolved installation ID '{0}'.", id);
}
else
{
_options.LogDebug("Failed to resolve installation ID.");
}

return _resolvedInstallationId = id;
}
}

// Take pause timestamp directly instead of referencing _lastPauseTimestamp to avoid
// potential race conditions.
private void PersistSession(SessionUpdate update, DateTimeOffset? pauseTimestamp = null)
Expand Down Expand Up @@ -305,7 +184,7 @@ private void DeletePersistedSession()

// Extract other parameters
var environment = _options.SettingLocator.GetEnvironment();
var distinctId = TryGetInstallationId();
var distinctId = _options.InstallationId;

// Create new session
var session = new SentrySession(distinctId, release, environment);
Expand Down
125 changes: 125 additions & 0 deletions src/Sentry/InstallationIdHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
using Sentry.Extensibility;
using Sentry.Internal.Extensions;

namespace Sentry;

internal class InstallationIdHelper
{
private readonly object _installationIdLock = new();
private string? _installationId;
private readonly SentryOptions _options;
private readonly string? _persistenceDirectoryPath;

public InstallationIdHelper(SentryOptions options)
{
_options = options;
_persistenceDirectoryPath = options.CacheDirectoryPath ?? options.TryGetDsnSpecificCacheDirectoryPath();
}

public string? TryGetInstallationId()
{
// Installation ID could have already been resolved by this point
if (!string.IsNullOrWhiteSpace(_installationId))
{
return _installationId;
}

// Resolve installation ID in a locked manner to guarantee consistency because ID can be non-deterministic.
// Note: in the future, this probably has to be synchronized across multiple processes too.
lock (_installationIdLock)
{
// We may have acquired the lock after another thread has already resolved
// installation ID, so check the cache one more time before proceeding with I/O.
if (!string.IsNullOrWhiteSpace(_installationId))
{
return _installationId;
}

var id =
TryGetPersistentInstallationId() ??
TryGetHardwareInstallationId() ??
GetMachineNameInstallationId();

if (!string.IsNullOrWhiteSpace(id))
{
_options.LogDebug("Resolved installation ID '{0}'.", id);
}
else
{
_options.LogDebug("Failed to resolve installation ID.");
}

return _installationId = id;
}
}

private string? TryGetPersistentInstallationId()
{
try
{
var rootPath = _persistenceDirectoryPath ??
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var directoryPath = Path.Combine(rootPath, "Sentry", _options.Dsn!.GetHashString());

Directory.CreateDirectory(directoryPath);

_options.LogDebug("Created directory for installation ID file ({0}).", directoryPath);

var filePath = Path.Combine(directoryPath, ".installation");

// Read installation ID stored in a file
if (File.Exists(filePath))
{
return File.ReadAllText(filePath);
}
_options.LogDebug("File containing installation ID does not exist ({0}).", filePath);

// Generate new installation ID and store it in a file
var id = Guid.NewGuid().ToString();
File.WriteAllText(filePath, id);

_options.LogDebug("Saved installation ID '{0}' to file '{1}'.", id, filePath);
return id;
}
// If there's no write permission or the platform doesn't support this, we handle
// and let the next installation id strategy kick in
catch (Exception ex)
{
_options.LogError(ex, "Failed to resolve persistent installation ID.");
return null;
}
}

private string? TryGetHardwareInstallationId()
{
try
{
// Get MAC address of the first network adapter
var installationId = NetworkInterface
.GetAllNetworkInterfaces()
.Where(nic =>
nic.OperationalStatus == OperationalStatus.Up &&
nic.NetworkInterfaceType != NetworkInterfaceType.Loopback)
.Select(nic => nic.GetPhysicalAddress().ToString())
.FirstOrDefault();

if (string.IsNullOrWhiteSpace(installationId))
{
_options.LogError("Failed to find an appropriate network interface for installation ID.");
return null;
}

return installationId;
}
catch (Exception ex)
{
_options.LogError(ex, "Failed to resolve hardware installation ID.");
return null;
}
}

// Internal for testing
internal static string GetMachineNameInstallationId() =>
// Never fails
Environment.MachineName.GetHashString();
}
1 change: 1 addition & 0 deletions src/Sentry/Internal/Enricher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ public void Apply(IEventLike eventLike)
{
eventLike.User.Username = Environment.UserName;
}
eventLike.User.Id ??= _options.InstallationId;
eventLike.User.IpAddress ??= DefaultIpAddress;

//Apply App startup and Boot time
Expand Down
4 changes: 4 additions & 0 deletions src/Sentry/SentryOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ public class SentryOptions
/// </remarks>
internal IScopeStackContainer? ScopeStackContainer { get; set; }

private Lazy<string?> LazyInstallationId => new(()
=> new InstallationIdHelper(this).TryGetInstallationId());
internal string? InstallationId => LazyInstallationId.Value;

#if __MOBILE__
private bool _isGlobalModeEnabled = true;
/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
}
},
User: {
Id: Guid_1,
IpAddress: {{auto}}
},
Environment: production,
Expand Down Expand Up @@ -76,7 +77,7 @@
http.response.status_code: 200
},
Tags: {
ActionId: Guid_1,
ActionId: Guid_2,
ActionName: Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests),
route.action: Method,
route.controller: Version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
}
},
User: {
Id: Guid_1,
IpAddress: {{auto}}
},
Environment: production,
Expand Down Expand Up @@ -76,7 +77,7 @@
http.response.status_code: 200
},
Tags: {
ActionId: Guid_1,
ActionId: Guid_2,
ActionName: Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests),
route.action: Method,
route.controller: Version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
}
},
User: {
Id: Guid_1,
IpAddress: {{auto}}
},
Environment: production,
Expand Down Expand Up @@ -76,7 +77,7 @@
http.response.status_code: 200
},
Tags: {
ActionId: Guid_1,
ActionId: Guid_2,
ActionName: Sentry.AspNetCore.Tests.WebIntegrationTests+VersionController.Method (Sentry.AspNetCore.Tests),
route.action: Method,
route.controller: Version,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
}
},
User: {
Id: Guid_1,
IpAddress: {{auto}}
},
Breadcrumbs: [
Expand Down Expand Up @@ -69,6 +70,7 @@ WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();,
}
},
User: {
Id: Guid_1,
IpAddress: {{auto}}
},
Spans: [
Expand All @@ -80,9 +82,9 @@ WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();,
IsSampled: true,
Extra: {
bytes_sent : 510,
db.connection_id: Guid_1,
db.connection_id: Guid_2,
db.name: SqlListenerTests.verify_LoggingAsync,
db.operation_id: Guid_2,
db.operation_id: Guid_3,
db.server: (LocalDb)\SqlListenerTests,
db.system: sql,
rows_sent: 0
Expand All @@ -103,9 +105,9 @@ WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
Status: InternalError,
IsSampled: true,
Extra: {
db.connection_id: Guid_1,
db.connection_id: Guid_2,
db.name: SqlListenerTests.verify_LoggingAsync,
db.operation_id: Guid_3,
db.operation_id: Guid_4,
db.system: sql
}
}
Expand Down
Loading
Loading