Skip to content

Add device and OS attributes to logs #4493

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

Merged
merged 17 commits into from
Jun 17, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
SentryUserFeedbackDialog.Builder(context).create().show()
```
- Add `user.id`, `user.name` and `user.email` to log attributes ([#4486](https://github.com/getsentry/sentry-java/pull/4486))
- User `name` attribute has been deprecated, please use `username` instead ([#4486](https://github.com/getsentry/sentry-java/pull/4486))
- Add device (`device.brand`, `device.model` and `device.family`) and OS (`os.name` and `os.version`) attributes to logs ([#4493](https://github.com/getsentry/sentry-java/pull/4493))
- Serialize `preContext` and `postContext` in `SentryStackFrame` ([#4482](https://github.com/getsentry/sentry-java/pull/4482))

### Internal
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@
import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Build;
import io.sentry.DateUtils;
import io.sentry.EventProcessor;
import io.sentry.Hint;
import io.sentry.IpAddressUtils;
import io.sentry.NoOpLogger;
import io.sentry.SentryAttributeType;
import io.sentry.SentryBaseEvent;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.SentryLogEvent;
import io.sentry.SentryLogEventAttributeValue;
import io.sentry.SentryReplayEvent;
import io.sentry.android.core.internal.util.AndroidThreadChecker;
import io.sentry.android.core.performance.AppStartMetrics;
Expand All @@ -23,6 +28,7 @@
import io.sentry.protocol.SentryTransaction;
import io.sentry.protocol.User;
import io.sentry.util.HintUtils;
import io.sentry.util.LazyEvaluator;
import io.sentry.util.Objects;
import java.util.Collections;
import java.util.List;
Expand All @@ -42,6 +48,8 @@ final class DefaultAndroidEventProcessor implements EventProcessor {
private final @NotNull BuildInfoProvider buildInfoProvider;
private final @NotNull SentryAndroidOptions options;
private final @NotNull Future<DeviceInfoUtil> deviceInfoUtil;
private final @NotNull LazyEvaluator<String> deviceFamily =
new LazyEvaluator<>(() -> ContextUtils.getFamily(NoOpLogger.getInstance()));

public DefaultAndroidEventProcessor(
final @NotNull Context context,
Expand Down Expand Up @@ -81,6 +89,13 @@ public DefaultAndroidEventProcessor(
return event;
}

@Override
public @Nullable SentryLogEvent process(@NotNull SentryLogEvent event) {
setDevice(event);
setOs(event);
return event;
}

/**
* The last exception is usually used for picking the issue title, but the convention is to send
* inner exceptions first, e.g. [inner, outer] This doesn't work very well on Android, as some
Expand Down Expand Up @@ -199,6 +214,34 @@ private void mergeOS(final @NotNull SentryBaseEvent event) {
}
}

Copy link
Member

Choose a reason for hiding this comment

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

so I'm thinking querying DeviceInfoUtil is probably too much work for this, shall we maybe just directly use the const values for brand, model and family from android.os.Build? Same for the OS name and version, those are also const values. Just trying to minimize impact since logs can happen frequent and originate from the main thread.

Copy link
Member Author

Choose a reason for hiding this comment

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

Updated, introduced a LazyEvaluator so we don't run a regex for the family on each log emitted.

private void setDevice(final @NotNull SentryLogEvent event) {
try {
event.setAttribute(
"device.brand",
new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.BRAND));
event.setAttribute(
"device.model",
new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.MODEL));
event.setAttribute(
"device.family",
new SentryLogEventAttributeValue(SentryAttributeType.STRING, deviceFamily.getValue()));
} catch (Throwable e) {
options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve device info", e);
}
}

private void setOs(final @NotNull SentryLogEvent event) {
try {
event.setAttribute(
"os.name", new SentryLogEventAttributeValue(SentryAttributeType.STRING, "Android"));
event.setAttribute(
"os.version",
new SentryLogEventAttributeValue(SentryAttributeType.STRING, Build.VERSION.RELEASE));
} catch (Throwable e) {
options.getLogger().log(SentryLevel.ERROR, "Failed to retrieve os system", e);
}
}

// Data to be applied to events that was created in the running process
private void processNonCachedEvent(
final @NotNull SentryBaseEvent event, final @NotNull Hint hint) {
Expand Down
3 changes: 3 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,7 @@ public final class io/sentry/EnvelopeSender : io/sentry/IEnvelopeSender {
public abstract interface class io/sentry/EventProcessor {
public fun getOrder ()Ljava/lang/Long;
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
public fun process (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent;
public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent;
public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
}
Expand Down Expand Up @@ -1284,6 +1285,7 @@ public final class io/sentry/MainEventProcessor : io/sentry/EventProcessor, java
public fun close ()V
public fun getOrder ()Ljava/lang/Long;
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
public fun process (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent;
public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent;
public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
}
Expand Down Expand Up @@ -3143,6 +3145,7 @@ public final class io/sentry/SentryLogEvent : io/sentry/JsonSerializable, io/sen
public fun getTimestamp ()Ljava/lang/Double;
public fun getUnknown ()Ljava/util/Map;
public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V
public fun setAttribute (Ljava/lang/String;Lio/sentry/SentryLogEventAttributeValue;)V
public fun setAttributes (Ljava/util/Map;)V
public fun setBody (Ljava/lang/String;)V
public fun setLevel (Lio/sentry/SentryLogLevel;)V
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/EventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,17 @@ default SentryReplayEvent process(@NotNull SentryReplayEvent event, @NotNull Hin
return event;
}

/**
Copy link
Member Author

Choose a reason for hiding this comment

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

An alternative to going through EventProcessor would be something very similar to it, like a LogProcessor so I opted for simply extending EventProcessor instead.

We could also offer hooks but I didn't want to hack that in, so I went this route and we can transform it into a better hook API in the future.

* May mutate or drop a SentryLogEvent
*
* @param event the SentryLogEvent
* @return the event itself, a mutated SentryLogEvent or null
*/
@Nullable
default SentryLogEvent process(@NotNull SentryLogEvent event) {
Copy link
Member Author

Choose a reason for hiding this comment

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

Decided not to add Hint here since we decided to not have it for logs.beforeSend either. LMK if you think it's needed.

Copy link
Member

Choose a reason for hiding this comment

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

Can we also add an empty override to MainEventProcessor? I recall there used to be problems in dotnet/unity due to absence of proper desugaring in some versions, so the default interface methods have caused crashes.

return event;
}

/**
* Controls when this EventProcessor is invoked.
*
Expand Down
5 changes: 5 additions & 0 deletions sentry/src/main/java/io/sentry/MainEventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ private void processNonCachedEvent(final @NotNull SentryBaseEvent event) {
return event;
}

@Override
public @Nullable SentryLogEvent process(@NotNull SentryLogEvent event) {
return event;
}

private void setCommons(final @NotNull SentryBaseEvent event) {
setPlatform(event);
}
Expand Down
45 changes: 45 additions & 0 deletions sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,38 @@ private SentryEvent processEvent(
return event;
}

@Nullable
private SentryLogEvent processLogEvent(
@NotNull SentryLogEvent event, final @NotNull List<EventProcessor> eventProcessors) {
for (final EventProcessor processor : eventProcessors) {
try {
event = processor.process(event);
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
e,
"An exception occurred while processing log event by processor: %s",
processor.getClass().getName());
}

if (event == null) {
options
.getLogger()
.log(
SentryLevel.DEBUG,
"Log event was dropped by a processor: %s",
processor.getClass().getName());
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.LogItem);
break;
}
}
return event;
}

private @Nullable SentryTransaction processTransaction(
@NotNull SentryTransaction transaction,
final @NotNull Hint hint,
Expand Down Expand Up @@ -1138,6 +1170,19 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint
@ApiStatus.Experimental
@Override
public void captureLog(@Nullable SentryLogEvent logEvent, @Nullable IScope scope) {
if (logEvent != null && scope != null) {
logEvent = processLogEvent(logEvent, scope.getEventProcessors());
if (logEvent == null) {
return;
}
}

if (logEvent != null) {
logEvent = processLogEvent(logEvent, options.getEventProcessors());
if (logEvent == null) {
return;
}
}

if (logEvent != null) {
logEvent = executeBeforeSendLog(logEvent);
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/SentryLogEvent.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,17 @@ public void setAttributes(final @Nullable Map<String, SentryLogEventAttributeVal
this.attributes = attributes;
}

public void setAttribute(
final @Nullable String key, final @Nullable SentryLogEventAttributeValue value) {
if (key == null) {
return;
}
if (this.attributes == null) {
this.attributes = new HashMap<>();
}
this.attributes.put(key, value);
}

public @Nullable Integer getSeverityNumber() {
return severityNumber;
}
Expand Down
Loading