Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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
16 changes: 16 additions & 0 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,10 @@
D48891CC2E98F22A00212823 /* SentryInfoPlistWrapperProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */; };
D48891CE2E98F28E00212823 /* SentryInfoPlistWrapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */; };
D48891D02E98F2E700212823 /* SentryInfoPlistError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */; };
D48954722EF5ABE00086F240 /* SentryAttributeValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = D489546C2EF5ABD90086F240 /* SentryAttributeValue.swift */; };
D48954742EF5B00A0086F240 /* SentryAttributeValuable.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48954732EF5B0060086F240 /* SentryAttributeValuable.swift */; };
D489551C2EF954EA0086F240 /* SentryAttributeValuableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48955162EF954DD0086F240 /* SentryAttributeValuableTests.swift */; };
D489551E2EF955010086F240 /* SentryAttributeValueTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D489551D2EF954FB0086F240 /* SentryAttributeValueTests.swift */; };
D48E8B8B2D3E79610032E35E /* SentryTraceOrigin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */; };
D48E8B9D2D3E82AC0032E35E /* SentrySpanOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */; };
D490648A2DFAE1F600555785 /* SentryScreenshotOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D49064892DFAE1F600555785 /* SentryScreenshotOptions.swift */; };
Expand Down Expand Up @@ -2163,6 +2167,10 @@
D48891C62E98F21D00212823 /* SentryInfoPlistWrapperProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistWrapperProvider.swift; sourceTree = "<group>"; };
D48891CD2E98F28E00212823 /* SentryInfoPlistWrapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistWrapper.swift; sourceTree = "<group>"; };
D48891CF2E98F2E600212823 /* SentryInfoPlistError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryInfoPlistError.swift; sourceTree = "<group>"; };
D489546C2EF5ABD90086F240 /* SentryAttributeValue.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAttributeValue.swift; sourceTree = "<group>"; };
D48954732EF5B0060086F240 /* SentryAttributeValuable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAttributeValuable.swift; sourceTree = "<group>"; };
D48955162EF954DD0086F240 /* SentryAttributeValuableTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAttributeValuableTests.swift; sourceTree = "<group>"; };
D489551D2EF954FB0086F240 /* SentryAttributeValueTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryAttributeValueTests.swift; sourceTree = "<group>"; };
D48E8B8A2D3E79610032E35E /* SentryTraceOrigin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryTraceOrigin.swift; sourceTree = "<group>"; };
D48E8B9C2D3E82AC0032E35E /* SentrySpanOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentrySpanOperation.swift; sourceTree = "<group>"; };
D49064892DFAE1F600555785 /* SentryScreenshotOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryScreenshotOptions.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -3435,6 +3443,8 @@
9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */,
92B6BDAC2E05B9F700D538B3 /* SentryLogTests.swift */,
92ECD7472E05B5760063EC10 /* SentryAttributeTests.swift */,
D48955162EF954DD0086F240 /* SentryAttributeValuableTests.swift */,
D489551D2EF954FB0086F240 /* SentryAttributeValueTests.swift */,
92B6BDA82E05B8F000D538B3 /* SentryLogLevelTests.swift */,
D46712612DCD059500D4074A /* SentryRedactDefaultOptionsTests.swift */,
620078762D3906AD0022CB67 /* Codable */,
Expand Down Expand Up @@ -4906,6 +4916,8 @@
620078752D38F1110022CB67 /* Codable */,
92ECD73B2E05ACDE0063EC10 /* SentryLog.swift */,
92ECD73F2E05AD500063EC10 /* SentryAttribute.swift */,
D48954732EF5B0060086F240 /* SentryAttributeValuable.swift */,
D489546C2EF5ABD90086F240 /* SentryAttributeValue.swift */,
92ECD73D2E05AD2B0063EC10 /* SentryLogLevel.swift */,
9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */,
F458D1122E180BB00028273E /* SentryFileManagerProtocol.swift */,
Expand Down Expand Up @@ -5816,6 +5828,7 @@
D8739D142BEE5049007D2F66 /* SentryRRWebSpanEvent.swift in Sources */,
FAAB2F972E4D345800FE8B7E /* SentryUIDeviceWrapper.swift in Sources */,
7B6438AB26A70F24000D0F65 /* UIViewController+Sentry.m in Sources */,
D48954722EF5ABE00086F240 /* SentryAttributeValue.swift in Sources */,
84302A812B5767A50027A629 /* SentryLaunchProfiling.m in Sources */,
D490648A2DFAE1F600555785 /* SentryScreenshotOptions.swift in Sources */,
FA6FC0AA2E0B6B1100ED2669 /* SentrySdkInfo.swift in Sources */,
Expand Down Expand Up @@ -6008,6 +6021,7 @@
FA67DCFD2DDBD4EA00896B02 /* SentryANRTracker.swift in Sources */,
FA67DCFE2DDBD4EA00896B02 /* SentryANRTrackerV2Delegate.swift in Sources */,
FABE8E152E307A5E0040809A /* SentrySDK.swift in Sources */,
D48954742EF5B00A0086F240 /* SentryAttributeValuable.swift in Sources */,
FA67DCFF2DDBD4EA00896B02 /* SentryMXManager.swift in Sources */,
D41415A72DEEE532003B14D5 /* SentryRedactViewHelper.swift in Sources */,
FA66143A2E4B593900657755 /* SentryApplicationExtensions.swift in Sources */,
Expand Down Expand Up @@ -6322,6 +6336,7 @@
0ADC33F128D9BE940078D980 /* TestSentryUIDeviceWrapper.swift in Sources */,
63FE721420DA66EC00CDBAE8 /* SentryCrashMemory_Tests.m in Sources */,
62885DA729E946B100554F38 /* TestConncurrentModifications.swift in Sources */,
D489551E2EF955010086F240 /* SentryAttributeValueTests.swift in Sources */,
D45E2D772E003EBF0072A6B7 /* TestRedactOptions.swift in Sources */,
FAA4157B2EDA181D00B269CD /* SentrySDKSettings+Equality.swift in Sources */,
63FE720520DA66EC00CDBAE8 /* FileBasedTestCase.m in Sources */,
Expand Down Expand Up @@ -6358,6 +6373,7 @@
7BBD18992449DE9D00427C76 /* TestRateLimits.swift in Sources */,
7B04A9AB24EA5F8D00E710B1 /* SentryUserTests.swift in Sources */,
7BA61CCF247EB59500C130A8 /* SentryCrashUUIDConversionTests.swift in Sources */,
D489551C2EF954EA0086F240 /* SentryAttributeValuableTests.swift in Sources */,
7BBD188D2448453600427C76 /* SentryHttpDateParserTests.swift in Sources */,
D4D7AA782EEAE30F00E28DFB /* BatcherScopeTests.swift in Sources */,
F49D41982DEA27AF00D9244E /* SentryUseNSExceptionCallstackWrapperTests.swift in Sources */,
Expand Down
142 changes: 119 additions & 23 deletions Sources/Swift/Protocol/SentryAttribute.swift
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,67 @@ public final class SentryAttribute: NSObject {
super.init()
}

internal init(value: Any) {
public init(stringArray values: [String]) {
self.type = "string[]"
self.value = values
super.init()
}
Comment on lines +43 to +47
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.

self.value = values
super.init()
}

public init(integerArray values: [Int]) {
self.type = "integer[]"
self.value = values
super.init()
}

public init(doubleArray values: [Double]) {
self.type = "double[]"
self.value = values
super.init()
}

/// Creates a double attribute from a float value
public init(floatArray values: [Float]) {
self.type = "double[]"
self.value = values.map(Double.init)
super.init()
}

internal init(attributableValue: SentryAttributeValue) {
switch attributableValue {
case .boolean(let value):
self.type = "boolean"
self.value = value
case .string(let value):
self.type = "string"
self.value = value
case .integer(let value):
self.type = "integer"
self.value = value
case .double(let value):
self.type = "double"
self.value = value
case .booleanArray(let array):
self.type = "boolean[]"
self.value = array
case .stringArray(let array):
self.type = "string[]"
self.value = array
case .integerArray(let array):
self.type = "integer[]"
self.value = array
case .doubleArray(let array):
self.type = "double[]"
self.value = array
}
}

internal init(value: Any) { // swiftlint:disable:this cyclomatic_complexity
switch value {
case let stringValue as String:
self.type = "string"
Expand All @@ -57,6 +117,31 @@ public final class SentryAttribute: NSObject {
case let floatValue as Float:
self.type = "double"
self.value = Double(floatValue)
case let stringValues as [String]:
self.type = "string[]"
self.value = stringValues
case let boolValues as [Bool]:
self.type = "boolean[]"
self.value = boolValues
case let intValues as [Int]:
self.type = "integer[]"
self.value = intValues
case let doubleValues as [Double]:
self.type = "double[]"
self.value = doubleValues
case let floatValues as [Float]:
self.type = "double[]"
self.value = floatValues.map(Double.init)
case let attributable as SentryAttributeValuable:
let value = attributable.asAttributeValue
self.type = value.type
self.value = value.anyValue
case let attribute as SentryAttributeValue:
self.type = attribute.type
self.value = attribute.anyValue
case let attribute as SentryAttribute:
self.type = attribute.type
self.value = attribute.value
default:
// For any other type, convert to string representation
self.type = "string"
Expand All @@ -67,40 +152,51 @@ public final class SentryAttribute: NSObject {
}

// MARK: - Internal Encodable Support
@_spi(Private) extension SentryAttribute: Encodable {
private enum CodingKeys: String, CodingKey {
case value
case type
}

@_spi(Private) extension SentryAttribute: Encodable {
@_spi(Private) public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(type, forKey: .type)
try self.asAttributeValue.encode(to: encoder)
}
}

switch type {
@_spi(Private) extension SentryAttribute: SentryAttributeValuable {
@_spi(Private) public var asAttributeValue: SentryAttributeValue {
switch self.type {
case "string":
guard let stringValue = value as? String else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Expected String but got \(Swift.type(of: value))"))
if let val = self.value as? String {
return .string(val)
}
try container.encode(stringValue, forKey: .value)
case "boolean":
guard let boolValue = value as? Bool else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Expected Bool but got \(Swift.type(of: value))"))
if let val = self.value as? Bool {
return .boolean(val)
}
try container.encode(boolValue, forKey: .value)
case "integer":
guard let intValue = value as? Int else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Expected Int but got \(Swift.type(of: value))"))
if let val = self.value as? Int {
return .integer(val)
}
try container.encode(intValue, forKey: .value)
case "double":
guard let doubleValue = value as? Double else {
throw EncodingError.invalidValue(value, EncodingError.Context(codingPath: encoder.codingPath, debugDescription: "Expected Double but got \(Swift.type(of: value))"))
if let val = self.value as? Double {
return .double(val)
}
case "string[]":
if let val = self.value as? [String] {
return .stringArray(val)
}
case "boolean[]":
if let val = self.value as? [Bool] {
return .booleanArray(val)
}
case "integer[]":
if let val = self.value as? [Int] {
return .integerArray(val)
}
case "double[]":
if let val = self.value as? [Double] {
return .doubleArray(val)
}
try container.encode(doubleValue, forKey: .value)
default:
try container.encode(String(describing: value), forKey: .value)
break
}
return .string(String(describing: value))
}
}
145 changes: 145 additions & 0 deletions Sources/Swift/Protocol/SentryAttributeValuable.swift
Original file line number Diff line number Diff line change
@@ -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 }

}

extension String: SentryAttributeValuable {
public var asAttributeValue: SentryAttributeValue {
return .string(self)
}
}

extension Bool: SentryAttributeValuable {
public var asAttributeValue: SentryAttributeValue {
return .boolean(self)
}
}

extension Int: SentryAttributeValuable {
public var asAttributeValue: SentryAttributeValue {
return .integer(self)
}
}

extension Double: SentryAttributeValuable {
public var asAttributeValue: SentryAttributeValue {
return .double(self)
}
}

extension Float: SentryAttributeValuable {
public var asAttributeValue: SentryAttributeValue {
return .double(Double(self))
}
}

extension Array: SentryAttributeValuable {
public var asAttributeValue: SentryAttributeValue {
// Arrays can be heterogenous, therefore we must validate if all elements are of the same type.
// We must assert the element type too, because due to Objective-C bridging we noticed invalid conversions
// of empty String Arrays to Bool Arrays.
if Element.self == Bool.self, let values = self as? [Bool] {
return .booleanArray(values)
}
if Element.self == Double.self, let values = self as? [Double] {
return .doubleArray(values)
}
if Element.self == Float.self, let values = self as? [Float] {
return .doubleArray(values.map(Double.init))
}
if Element.self == Int.self, let values = self as? [Int] {
return .integerArray(values)
}
if Element.self == String.self, let values = self as? [String] {
return .stringArray(values)
}
if let values = self as? [SentryAttributeValuable] {
return castArrayToAttributeValue(values: values)
}
return .stringArray(self.map { element in
String(describing: element)
})
}

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.

// Check if the values are homogeneous and can be casted to a specific array type
if let booleanArray = castValuesToBooleanArray(values) {
return booleanArray
}
if let doubleArray = castValuesToDoubleArray(values) {
return doubleArray
}
if let integerArray = castValuesToIntegerArray(values) {
return integerArray
}
if let stringArray = castValuesToStringArray(values) {
return stringArray
}
// If the values are not homogenous we resolve the individual valuables to strings
return .stringArray(values.map { value in
switch value.asAttributeValue {
case .boolean(let value):
return String(describing: value)
case .double(let value):
return String(describing: value)
case .integer(let value):
return String(describing: value)
case .string(let value):
return value
default:
return String(describing: value)
}
})
}

func castValuesToBooleanArray(_ values: [SentryAttributeValuable]) -> SentryAttributeValue? {
let mappedBooleanValues = values.compactMap { element -> Bool? in
guard case .boolean(let value) = element.asAttributeValue else {
return nil
}
return value
}
guard mappedBooleanValues.count == values.count else {
return nil
}
return .booleanArray(mappedBooleanValues)
}

func castValuesToDoubleArray(_ values: [SentryAttributeValuable]) -> SentryAttributeValue? {
let mappedDoubleValues = values.compactMap { element -> Double? in
guard case .double(let value) = element.asAttributeValue else {
return nil
}
return value
}
guard mappedDoubleValues.count == values.count else {
return nil
}
return .doubleArray(mappedDoubleValues)
}

func castValuesToIntegerArray(_ values: [SentryAttributeValuable]) -> SentryAttributeValue? {
let mappedIntegerValues = values.compactMap { element -> Int? in
guard case .integer(let value) = element.asAttributeValue else {
return nil
}
return value
}
guard mappedIntegerValues.count == values.count else {
return nil
}
return .integerArray(mappedIntegerValues)
}

func castValuesToStringArray(_ values: [SentryAttributeValuable]) -> SentryAttributeValue? {
let mappedStringValues = values.compactMap { element -> String? in
guard case .string(let value) = element.asAttributeValue else {
return nil
}
return value
}
guard mappedStringValues.count == values.count else {
return nil
}
return .stringArray(mappedStringValues)
}
}
Loading
Loading