Skip to content

Conversation

@philprime
Copy link
Member

@philprime philprime commented Dec 22, 2025

📜 Description

This PR introduces a protocol-based system for typed attribute values to improve type safety when working with structured logging attributes. The changes add:

  • A type-safe enum SentryAttributeValue that represents attribute values (string, boolean, integer, double, and their array variants)
  • A protocol SentryAttributeValuable that allows types to convert themselves to SentryAttributeValue
  • Extensions on String, Bool, Int, Double, and Float to conform to SentryAttributeValuable

This change refactors the Objective-C compatible SentryAttribute to use the Swift enum SentryAttributeValue as its backing type. It also updates BatcherScope to use SentryAttributeValue directly instead of SentryAttribute objects.

This PR also introduces support for Array attribute types as defined in the documentation.

Changes in Public API:

The currently proposed changes contain new public API endpoints for the SentryLogger to accept [String: SentryAttributeValuable] instead of [String: Any] for improved type safety. I acknowledge this is a breaking change, therefore I am open to remove it until the next major version.

If we keep it it could cause ambiguity issues because we have to method signatures both with the same parameters, only having different Dictionary types as the attributes.

The main use case are the new methods introduced by Metrics in #6957

💡 Motivation and Context

Previously, attributes were stored as [String: Any] dictionaries, which provided no compile-time type safety. This made it easy to accidentally pass unsupported types or incorrect values, which are converted to String as a fallback, sending unpredictable data. By introducing a protocol-based system with SentryAttributeValuable, we can:

  1. Improve the type-safety as the compiler now enforces that only supported types (String, Bool, Int, Double, Float) can be used as attribute values
  2. Simplify API usage as developers can directly pass native Swift types without wrapping them in SentryAttribute objects (using th ExpressibleBy... protocol extensions)
  3. Maintain backward compatibility the SentryAttribute class still exists and can convert from SentryAttributeValue for Objective-C interop.

I also need this change to implement #6957.

💚 How did you test it?

  • Updated existing unit tests in BatcherScopeTests, BatcherTests, and SentryLoggerTests to use the new typed attribute system
  • All tests pass with the new protocol-based approach
  • Verified that the changes maintain the same runtime behavior while improving compile-time safety

📝 Checklist

You have to check all boxes before merging:

  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

@philprime philprime self-assigned this Dec 22, 2025
@codecov
Copy link

codecov bot commented Dec 22, 2025

Codecov Report

❌ Patch coverage is 90.20173% with 34 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@d069d70). Learn more about missing BASE report.
⚠️ Report is 4 commits behind head on main.
✅ All tests successful. No failed tests found.

Files with missing lines Patch % Lines
Sources/Swift/Tools/SentryLogger.swift 55.882% 15 Missing ⚠️
Sources/Swift/Protocol/SentryAttribute.swift 86.597% 12 Missing and 1 partial ⚠️
...urces/Swift/Protocol/SentryAttributeValuable.swift 94.117% 5 Missing ⚠️
Sources/Swift/Protocol/SentryAttributeValue.swift 98.913% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff            @@
##             main     #7077   +/-   ##
========================================
  Coverage        ?   84.846%           
========================================
  Files           ?       461           
  Lines           ?     27816           
  Branches        ?     12334           
========================================
  Hits            ?     23601           
  Misses          ?      4171           
  Partials        ?        44           
Files with missing lines Coverage Δ
Sources/Swift/Tools/Batcher/BatcherScope.swift 100.000% <100.000%> (ø)
Sources/Swift/Tools/SentryLogBatcher.swift 95.918% <100.000%> (ø)
Sources/Swift/Protocol/SentryAttributeValue.swift 98.913% <98.913%> (ø)
...urces/Swift/Protocol/SentryAttributeValuable.swift 94.117% <94.117%> (ø)
Sources/Swift/Protocol/SentryAttribute.swift 90.277% <86.597%> (ø)
Sources/Swift/Tools/SentryLogger.swift 74.766% <55.882%> (ø)

Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update d069d70...ee2b24a. Read the comment docs.

@philprime
Copy link
Member Author

@sentry review
@cursor review

@@ -1,5 +1,5 @@
protocol BatcherItem: Encodable {
var attributes: [String: SentryAttribute] { get set }
var attributesMap: [String: SentryAttributeValue] { get set }
Copy link
Member Author

Choose a reason for hiding this comment

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

This was renamed because it would otherwise collide with the type of SentryAttribute.attributes

@philprime philprime marked this pull request as ready for review December 22, 2025 09:26
@philprime philprime added the ready-to-merge Use this label to trigger all PR workflows label Dec 22, 2025
}


/// Logs a trace-level message with structured string interpolation and optional attributes.
Copy link
Member Author

Choose a reason for hiding this comment

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

These newly introduced logger methods are not strictly necessary but can be beneficial for SDK users due to type hints. We still need the type-erased method to keep supporting Objective-C but eventually we should deprecate the Any-typed version for Swift and have an Objective-C wrapper

@github-actions
Copy link
Contributor

github-actions bot commented Dec 22, 2025

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 1220.62 ms 1250.33 ms 29.70 ms
Size 24.14 KiB 1.04 MiB 1.01 MiB

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
854ca12 1219.94 ms 1251.32 ms 31.38 ms
2675d3c 1218.94 ms 1239.36 ms 20.42 ms
6ad363f 1196.04 ms 1223.27 ms 27.23 ms
87fb58a 1233.12 ms 1257.17 ms 24.04 ms
449d185 1216.31 ms 1251.94 ms 35.62 ms
d3f650a 1225.45 ms 1241.86 ms 16.41 ms
99104c9 1224.84 ms 1247.08 ms 22.24 ms
0f410ad 1193.34 ms 1255.49 ms 62.15 ms
f8029e2 1245.16 ms 1261.32 ms 16.16 ms
e3ebff3 1223.47 ms 1249.27 ms 25.80 ms

App size

Revision Plain With Sentry Diff
854ca12 23.74 KiB 996.96 KiB 973.22 KiB
2675d3c 23.75 KiB 928.16 KiB 904.41 KiB
6ad363f 23.75 KiB 1022.66 KiB 998.91 KiB
87fb58a 23.75 KiB 919.91 KiB 896.16 KiB
449d185 23.75 KiB 980.81 KiB 957.06 KiB
d3f650a 23.75 KiB 902.48 KiB 878.73 KiB
99104c9 23.75 KiB 894.83 KiB 871.09 KiB
0f410ad 24.14 KiB 1.01 MiB 1014.82 KiB
f8029e2 23.75 KiB 893.72 KiB 869.97 KiB
e3ebff3 23.75 KiB 878.48 KiB 854.73 KiB

Previous results on branch: philprime/attributable

Startup times

Revision Plain With Sentry Diff
4ef1623 1213.43 ms 1240.73 ms 27.30 ms

App size

Revision Plain With Sentry Diff
4ef1623 24.14 KiB 1.04 MiB 1.01 MiB

@philprime philprime marked this pull request as draft December 22, 2025 12:22
@philprime
Copy link
Member Author

Array handling is incomplete, moved back to draft

@philprime
Copy link
Member Author

@sentry review
@cursor review

@philprime philprime marked this pull request as ready for review December 22, 2025 13:07
func getContextForKey(_ key: String) -> [String: Any]?

/// List of attributes with erased value type for compatibility with public ``Scope``.
var attributes: [String: Any] { get }
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add @deprecated?

Copy link
Member Author

Choose a reason for hiding this comment

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

Not sure why we would want to do that. It's not deprecated, the attributesMap is just because for the Metrics API it won't be the type [String: Any]

Copy link
Member

@philipphofmann philipphofmann left a comment

Choose a reason for hiding this comment

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

I really like that we're adding type safety to the attributes with this PR. I think we need some slight tweaking for the public logging API to better communicate which types our users can use, such as:

public func warn(_ message: SentryLogMessage, attributes: [String: SentryAttributeValuable] = [:])


func testEncodeStringArrayAttribute() throws {
// -- Arrange --
let attribute = SentryLog.Attribute(stringArray: ["hello", "world", "test"])
Copy link
Member

Choose a reason for hiding this comment

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

l: As we're not only going to use this for logs but also for metrics, I would rather use this naming in the tests

Suggested change
let attribute = SentryLog.Attribute(stringArray: ["hello", "world", "test"])
let attribute = SentryAttribute(stringArray: ["hello", "world", "test"])

Comment on lines +43 to +47
public init(stringArray values: [String]) {
self.type = "string[]"
self.value = values
super.init()
}
Copy link
Member

Choose a reason for hiding this comment

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

h: I would highly appreciate proper code docs for all these new init methods, as these are public API.

}

public init(booleanArray values: [Bool]) {
self.type = "boolean[]"
Copy link
Member

Choose a reason for hiding this comment

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

m: What about defining an internal string-based enum for all the allowed types? So something like

private enum AttributeType: String {
    case stringType = "string",
         booleanType = "boolean",
         integerType = "integer",
         integerArrayType = "integer[]",
         doubleType = "double",
         doubleArrayType = "double[]"
}

SentryAttributeValue could also use it for the types.

@testable import Sentry
import XCTest

final class SentryAttributeValuableTests: XCTestCase {
Copy link
Member

Choose a reason for hiding this comment

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

Thanks for the clean test coverage.

@@ -0,0 +1,145 @@
public protocol SentryAttributeValuable {
var asAttributeValue: SentryAttributeValue { get }
Copy link
Member

Choose a reason for hiding this comment

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

m: We have sentry in the naming everywhere. I think this would make it a bit clearer that this returns a SentryAttributeValue.

Suggested change
var asAttributeValue: SentryAttributeValue { get }
var asSentryAttributeValue: SentryAttributeValue { get }

})
}

private func castArrayToAttributeValue(values: [SentryAttributeValuable]) -> SentryAttributeValue {
Copy link
Member

Choose a reason for hiding this comment

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

m: Honestly, I find that a bit confusing. I can do this, and the compiler doesn't complain. When looking at this code, I could assume that we support arrays with different types for the attributes, but instead, we silently convert them to a string array. Instead, it would be great if the following code wouldn't even compile.

let attribute : [SentryAttributeValuable] = [1,2,3, 3.3, "test"]
let attributeValue = attribute.asAttributeValue

Copy link
Member Author

@philprime philprime Dec 23, 2025

Choose a reason for hiding this comment

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

I tried to do that but it's not possible in Swift to have multiple conformances like this:

extension Array: SentryAttributeValuable where Element == Bool {
    public var asAttributeValue: SentryAttributeValue {
      return .booleanArray(self)
    }
}

extension Array: SentryAttributeValuable where Element == Int {
    public var asAttributeValue: SentryAttributeValue {
      return .integerArray(self)
    }
}

extension Array: SentryAttributeValuable where Element == String {
    public var asAttributeValue: SentryAttributeValue {
      return .stringArray(self)
    }
}

extension Array: SentryAttributeValuable where Element == Double {
    public var asAttributeValue: SentryAttributeValue {
      return .doubleArray(self)
    }
}

...because Swift does not allow multiple conditional conformances of the same generic type to the same protocol — even if the constraints are mutually exclusive. This is a hard compiler rule, not a limitation of my code.

If you, @itaybre or @noahsmartin know how we can make only these types of arrays can actually be casted to SentryAttributeValuable, I happily integrate it. I've already tried it before and couldn't find a way.

Copy link
Member

@philipphofmann philipphofmann Dec 23, 2025

Choose a reason for hiding this comment

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

OK, maybe we need a completely different approach if it doesn't work with the current one. I can try to come up with something, but that will take some time, and that will have to wait after the holidays.

Comment on lines +56 to +58
public func trace(_ message: SentryLogMessage, attributes: [String: SentryAttributeValuable] = [:]) {
captureLog(level: .trace, logMessage: message, attributes: attributes)
}
Copy link
Member

Choose a reason for hiding this comment

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

h: I'm all in favor of adding type safety. This is the API our users interact with most, and currently, this approach could do a better job of communicating to our users which types they can use. When I declare the attributes, I see SentryAttributeValuable, which I find a bit confusing. What is SentryAttributeValuable? Which types can I use?

OK, so this works:

let attributes: [String: SentryAttributeValuable] = ["retry_policy": "exponential"])]

This leads to two compiler errors, which is excellent:

let attributes: [String: SentryAttributeValuable] = ["string": "hello", "double": 0.0, "int": 1, "bool": true, "array": [1,2,3, 3.3], "dict": ["hello": "you"], "null": NSNull()]
Image

But if I do this, the code compiles. It should also fail to compile here. It doesn't because it falls back to using [String: Any] for the attributes.

SentrySDK.logger.warn(logString, attributes: ["string": "hello", "double": 0.0, "int": 1, "bool": true, "array": [1,2,3, 3.3], "dict": ["hello": "you"], "null": NSNull()])

Copy link
Member Author

Choose a reason for hiding this comment

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

That's what I am afraid off, we can't really offer this API for Logs, because it's so ambiguous with Any. I actually fee like we shouldn't introduce this for Logs right now and instead when we have a Objective-C wrapper framework.

Copy link
Member

@philipphofmann philipphofmann Dec 23, 2025

Choose a reason for hiding this comment

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

We could make it work by using a different name for attributes such as typeSafeAttributes, or typedAttributes and even consider using _ typeSafeAttributes.

public func warn(_ message: SentryLogMessage, _ typeSafeAttributes: [String: SentryAttributeValuable] = [:]) {

And I think we could rename SentryAttributeValuable to SentryTypedAttribute or SentryTypeSafeAttribute.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ready-to-merge Use this label to trigger all PR workflows

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants