Skip to content

Commit

Permalink
Set in_foreground app context (#2983)
Browse files Browse the repository at this point in the history
* Set in_foreground app context

* Update Changelog

* Check OS

* Update verify tests

* Update remaining verify tests

* Remove redundant code.

* Update app context tests

* Catch IsOSPlatform failure

* Update CHANGELOG.md

Co-authored-by: Bruno Garcia <bruno@brunogarcia.com>

* Refactor active window check.

* Add a comment

---------

Co-authored-by: James Crosswell <jamescrosswell@users.noreply.github.com>
Co-authored-by: Bruno Garcia <bruno@brunogarcia.com>
  • Loading branch information
3 people committed Dec 22, 2023
1 parent fbc468c commit 790e40d
Show file tree
Hide file tree
Showing 11 changed files with 99 additions and 6 deletions.
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

- MAUI: App context has `in_foreground` indicating whether app was on the background or foreground. ([#2983](https://github.com/getsentry/sentry-dotnet/pull/2983))

### Significant change in behavior

- The User.IpAddress is now set to {{auto}} by default, even when sendDefaultPII is disabled ([#2981](https://github.com/getsentry/sentry-dotnet/pull/2981))
Expand Down
3 changes: 3 additions & 0 deletions src/Sentry.Maui/Internal/SentryMauiEventProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ namespace Sentry.Maui.Internal;

internal class SentryMauiEventProcessor : ISentryEventProcessor
{
public static bool? InForeground { get; set; }

private readonly SentryMauiOptions _options;

public SentryMauiEventProcessor(SentryMauiOptions options)
Expand All @@ -17,6 +19,7 @@ public SentryEvent Process(SentryEvent @event)
@event.Sdk.Version = Constants.SdkVersion;
@event.Contexts.Device.ApplyMauiDeviceData(_options.DiagnosticLogger);
@event.Contexts.OperatingSystem.ApplyMauiOsData(_options.DiagnosticLogger);
@event.Contexts.App.InForeground = InForeground;

return @event;
}
Expand Down
15 changes: 15 additions & 0 deletions src/Sentry.Maui/SentryMauiAppBuilderExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,28 @@ private static void RegisterMauiEventsBinder(this MauiAppBuilder builder)
var platformApplication = application.Delegate as IPlatformApplication;
platformApplication?.HandleMauiEvents(bind: false);
//According to https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623111-applicationwillterminate#discussion
//WillTerminate is called: in situations where the app is running in the background (not suspended) and the system needs to terminate it for some reason.
SentryMauiEventProcessor.InForeground = false;
});
lifecycle.OnActivated(application => SentryMauiEventProcessor.InForeground = true);
lifecycle.DidEnterBackground(application => SentryMauiEventProcessor.InForeground = false);
lifecycle.OnResignActivation(application => SentryMauiEventProcessor.InForeground = false);
});
#elif ANDROID
events.AddAndroid(lifecycle =>
{
lifecycle.OnApplicationCreating(application => (application as IPlatformApplication)?.HandleMauiEvents());
lifecycle.OnDestroy(application => (application as IPlatformApplication)?.HandleMauiEvents(bind: false));
lifecycle.OnResume(activity => SentryMauiEventProcessor.InForeground = true);
lifecycle.OnStart(activity => SentryMauiEventProcessor.InForeground = true);
lifecycle.OnStop(activity => SentryMauiEventProcessor.InForeground = false);
lifecycle.OnPause(activity => SentryMauiEventProcessor.InForeground = false);
});
#elif WINDOWS
events.AddWindows(lifecycle =>
Expand Down
2 changes: 2 additions & 0 deletions src/Sentry/Internal/Enricher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ public void Apply(IEventLike eventLike)
//Apply App startup and Boot time
eventLike.Contexts.App.StartTime ??= ProcessInfo.Instance?.StartupTime;
eventLike.Contexts.Device.BootTime ??= ProcessInfo.Instance?.BootTime;

eventLike.Contexts.App.InForeground = ProcessInfo.Instance?.ApplicationIsActivated(_options);

// Default tags
_options.ApplyDefaultTags(eventLike);
Expand Down
55 changes: 53 additions & 2 deletions src/Sentry/Internal/ProcessInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ internal class ProcessInfo
internal DateTimeOffset? BootTime { get; }

private volatile Task _preciseAppStartupTask = Task.CompletedTask;
private int? _id;

// For testability
internal Task PreciseAppStartupTask
Expand All @@ -25,6 +26,25 @@ internal Task PreciseAppStartupTask
private set => _preciseAppStartupTask = value;
}

public int? GetId(SentryOptions options) => _id ??= GetCurrentProcessId(options);

private int? GetCurrentProcessId(SentryOptions options)
{
#if NET6_0_OR_GREATER
return Environment.ProcessId;
#else
try
{
return Process.GetCurrentProcess().Id;
}
catch (Exception ex)
{
options.LogError(ex, "Error getting current process Id");
return null;
}
#endif
}

internal ProcessInfo(
SentryOptions options,
Func<DateTimeOffset>? findPreciseStartupTime = null)
Expand Down Expand Up @@ -68,13 +88,13 @@ internal Task PreciseAppStartupTask
if (options.DetectStartupTime == StartupTimeDetectionMode.Best)
{
#if __MOBILE__
options.LogWarning("StartupTimeDetectionMode.Best is not available on this platform. Using 'Fast' mode.");
options.LogWarning("StartupTimeDetectionMode.Best is not available on this platform. Using 'Fast' mode.");
#else
// StartupTime is set to UtcNow in this constructor.
// That's computationally cheap but not very precise.
// This method will give a better precision to the StartupTime at a cost
// of calling Process.GetCurrentProcess, on a thread pool thread.
var preciseStartupTimeFunc = findPreciseStartupTime ?? GetStartupTime;
var preciseStartupTimeFunc = findPreciseStartupTime ?? GetStartupTime;
PreciseAppStartupTask = Task.Run(() =>
{
try
Expand All @@ -100,4 +120,35 @@ private static DateTimeOffset GetStartupTime()
return proc.StartTime.ToUniversalTime();
}
#endif

public bool? ApplicationIsActivated(SentryOptions options)
{
try
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var activatedHandle = GetForegroundWindow();
if (activatedHandle == IntPtr.Zero)
{
return false; // No window is currently activated
}

var currentProcessId = ProcessInfo.Instance?.GetId(options);
GetWindowThreadProcessId(activatedHandle, out var activeProcessId);

return activeProcessId == currentProcessId;
}
}
catch (Exception e)
{
options.LogError(e, "Error getting foreground window state.");
}
return null;
}

[DllImport("user32.dll", CharSet = CharSet.Auto, ExactSpelling = true)]
private static extern IntPtr GetForegroundWindow();

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern int GetWindowThreadProcessId(IntPtr handle, out int processId);
}
14 changes: 12 additions & 2 deletions src/Sentry/Protocol/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ public sealed class App : IJsonSerializable, ICloneable<App>, IUpdatable<App>
/// </summary>
public string? Build { get; set; }

/// <summary>
/// A flag indicating whether the app is in foreground or not. An app is in foreground when it's visible to the user.
/// </summary>
public bool? InForeground { get; set; }

/// <summary>
/// Clones this instance.
/// </summary>
Expand All @@ -68,7 +73,8 @@ App ICloneable<App>.Clone()
BuildType = BuildType,
Name = Name,
Version = Version,
Build = Build
Build = Build,
InForeground = InForeground
};

/// <summary>
Expand All @@ -94,6 +100,7 @@ void IUpdatable<App>.UpdateFrom(App source)
Name ??= source.Name;
Version ??= source.Version;
Build ??= source.Build;
InForeground ??= source.InForeground;
}

/// <inheritdoc />
Expand All @@ -109,6 +116,7 @@ public void WriteTo(Utf8JsonWriter writer, IDiagnosticLogger? _)
writer.WriteStringIfNotWhiteSpace("app_name", Name);
writer.WriteStringIfNotWhiteSpace("app_version", Version);
writer.WriteStringIfNotWhiteSpace("app_build", Build);
writer.WriteBooleanIfNotNull("in_foreground", InForeground);

writer.WriteEndObject();
}
Expand All @@ -125,6 +133,7 @@ public static App FromJson(JsonElement json)
var name = json.GetPropertyOrNull("app_name")?.GetString();
var version = json.GetPropertyOrNull("app_version")?.GetString();
var build = json.GetPropertyOrNull("app_build")?.GetString();
var inForeground = json.GetPropertyOrNull("in_foreground")?.GetBoolean();

return new App
{
Expand All @@ -134,7 +143,8 @@ public static App FromJson(JsonElement json)
BuildType = buildType,
Name = name,
Version = version,
Build = build
Build = build,
InForeground = inForeground
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,7 @@ namespace Sentry.Protocol
public string? BuildType { get; set; }
public string? Hash { get; set; }
public string? Identifier { get; set; }
public bool? InForeground { get; set; }
public string? Name { get; set; }
public System.DateTimeOffset? StartTime { get; set; }
public string? Version { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,7 @@ namespace Sentry.Protocol
public string? BuildType { get; set; }
public string? Hash { get; set; }
public string? Identifier { get; set; }
public bool? InForeground { get; set; }
public string? Name { get; set; }
public System.DateTimeOffset? StartTime { get; set; }
public string? Version { get; set; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1446,6 +1446,7 @@ namespace Sentry.Protocol
public string? BuildType { get; set; }
public string? Hash { get; set; }
public string? Identifier { get; set; }
public bool? InForeground { get; set; }
public string? Name { get; set; }
public System.DateTimeOffset? StartTime { get; set; }
public string? Version { get; set; }
Expand Down
1 change: 1 addition & 0 deletions test/Sentry.Tests/ApiApprovalTests.Run.Net4_8.verified.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1443,6 +1443,7 @@ namespace Sentry.Protocol
public string? BuildType { get; set; }
public string? Hash { get; set; }
public string? Identifier { get; set; }
public bool? InForeground { get; set; }
public string? Name { get; set; }
public System.DateTimeOffset? StartTime { get; set; }
public string? Version { get; set; }
Expand Down
8 changes: 6 additions & 2 deletions test/Sentry.Tests/Protocol/Context/AppTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public void SerializeObject_AllPropertiesSetToNonDefault_SerializesValidObject()
BuildType = "nightly",
Hash = "93fd0e9a",
Name = "Sentry.Test.App",
StartTime = DateTimeOffset.MaxValue
StartTime = DateTimeOffset.MaxValue,
InForeground = true
};

var actualString = sut.ToJsonString(_testOutputLogger);
Expand All @@ -39,7 +40,8 @@ public void Clone_CopyValues()
Identifier = "identifier",
Name = "name",
StartTime = DateTimeOffset.UtcNow,
Version = "version"
Version = "version",
InForeground = false
};

var clone = sut.Clone();
Expand All @@ -51,6 +53,7 @@ public void Clone_CopyValues()
Assert.Equal(sut.Name, clone.Name);
Assert.Equal(sut.StartTime, clone.StartTime);
Assert.Equal(sut.Version, clone.Version);
Assert.Equal(sut.InForeground, clone.InForeground);
}

[Theory]
Expand All @@ -72,5 +75,6 @@ public static IEnumerable<object[]> TestCases()
yield return new object[] { (new App { StartTime = DateTimeOffset.MaxValue }, """{"type":"app","app_start_time":"9999-12-31T23:59:59.9999999+00:00"}""") };
yield return new object[] { (new App { Version = "some version" }, """{"type":"app","app_version":"some version"}""") };
yield return new object[] { (new App { Identifier = "some identifier" }, """{"type":"app","app_identifier":"some identifier"}""") };
yield return new object[] { (new App { InForeground = true}, """{"type":"app","in_foreground":true}""") };
}
}

0 comments on commit 790e40d

Please sign in to comment.