Skip to content
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
b491448
Add beforeSendLog with mutable log class
denrase Jul 22, 2025
4799ae8
add pr to changelog
denrase Jul 22, 2025
d0afa5a
remove xcode created folder recovered references
denrase Jul 22, 2025
0c6509a
Merge branch 'main' into feat/structured-logs-before-send
denrase Jul 22, 2025
c440e7f
make public api
denrase Jul 22, 2025
181cfb8
Merge branch 'main' into feat/structured-logs-before-send
denrase Jul 28, 2025
780341e
add docs
denrase Jul 28, 2025
f904584
Merge branch 'main' into feat/structured-logs-before-send
denrase Jul 29, 2025
58fe518
Merge branch 'main' into feat/structured-logs-before-send
denrase Jul 29, 2025
399fa55
Change `SentryLog` to NSObject, make public and remove the wrapper cl…
denrase Jul 29, 2025
1661c2f
make public api
denrase Jul 29, 2025
c9c5c71
don’t force unwrap value
denrase Jul 29, 2025
a5a9d98
Merge branch 'main' into feat/structured-logs-before-send
denrase Jul 30, 2025
08beca0
fix cl
denrase Jul 30, 2025
579aa99
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 4, 2025
43001c0
update changelog
denrase Aug 4, 2025
b034d88
fix tests, generate public api
denrase Aug 4, 2025
777d8bb
Fix SMP issue with dynamic fallback for SPM builds
denrase Aug 4, 2025
f6e9001
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 4, 2025
2844982
remove open
denrase Aug 4, 2025
4e22c52
make typealias public
denrase Aug 4, 2025
4aa1fa9
add docs for logs
denrase Aug 4, 2025
736ff34
recreate api
denrase Aug 4, 2025
0c82fca
male log final and codabple package private
denrase Aug 5, 2025
3beece2
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 5, 2025
ae88b04
bump diffMax
denrase Aug 6, 2025
c1db59e
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 6, 2025
354250e
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 7, 2025
6321376
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 7, 2025
e05bbc5
revert diffmax
denrase Aug 7, 2025
0a6a7b7
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 11, 2025
b3944d8
fix cl
denrase Aug 11, 2025
038ab02
fix swift dynamic call extension
denrase Aug 11, 2025
5a0f736
Merge branch 'main' into feat/structured-logs-before-send
denrase Aug 11, 2025
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
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

- Add `beforeSendLog` callback to `SentryOptions` (#5678)

### Fixes

- Add support for PDFKit views in session replay (#5750)
Expand Down
4 changes: 0 additions & 4 deletions Sentry.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -795,7 +795,6 @@
9264E1EB2E2E385E00B077CF /* SentryLogMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */; };
9264E1ED2E2E397C00B077CF /* SentryLogMessageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */; };
92672BB629C9A2A9006B021C /* SentryBreadcrumb+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */; settings = {ATTRIBUTES = (Private, ); }; };
926C89732E26A30C006F3154 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 926C89722E26A30C006F3154 /* SentryScope+PrivateSwift.h */; };
927A5CC42DD7626B00B82404 /* SentryEnvelopeItemHeaderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */; };
928207C42E251B8F009285A4 /* SentryScope+PrivateSwift.h in Headers */ = {isa = PBXBuildFile; fileRef = 928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */; };
9286059529A5096600F96038 /* SentryGeo.h in Headers */ = {isa = PBXBuildFile; fileRef = 9286059429A5096600F96038 /* SentryGeo.h */; settings = {ATTRIBUTES = (Public, ); }; };
Expand Down Expand Up @@ -2115,7 +2114,6 @@
9264E1EA2E2E385B00B077CF /* SentryLogMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessage.swift; sourceTree = "<group>"; };
9264E1EC2E2E397400B077CF /* SentryLogMessageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryLogMessageTests.swift; sourceTree = "<group>"; };
92672BB529C9A2A9006B021C /* SentryBreadcrumb+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "SentryBreadcrumb+Private.h"; path = "include/HybridPublic/SentryBreadcrumb+Private.h"; sourceTree = "<group>"; };
926C89722E26A30C006F3154 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = "<group>"; };
927A5CC32DD7626400B82404 /* SentryEnvelopeItemHeaderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SentryEnvelopeItemHeaderTests.swift; sourceTree = "<group>"; };
928207C32E251B8F009285A4 /* SentryScope+PrivateSwift.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "SentryScope+PrivateSwift.h"; path = "include/SentryScope+PrivateSwift.h"; sourceTree = "<group>"; };
9286059429A5096600F96038 /* SentryGeo.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = SentryGeo.h; path = Public/SentryGeo.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -4726,7 +4724,6 @@
F474CB872E2EC5040001DF41 /* Recovered References */ = {
isa = PBXGroup;
children = (
926C89722E26A30C006F3154 /* SentryScope+PrivateSwift.h */,
);
name = "Recovered References";
sourceTree = "<group>";
Expand Down Expand Up @@ -4933,7 +4930,6 @@
63FE713F20DA4C1100CDBAE8 /* SentryCrashStackCursor_SelfThread.h in Headers */,
639FCFA41EBC809A00778193 /* SentryStacktrace.h in Headers */,
620379DB2AFE1415005AC0C1 /* SentryBuildAppStartSpans.h in Headers */,
926C89732E26A30C006F3154 /* SentryScope+PrivateSwift.h in Headers */,
63FE716320DA4C1100CDBAE8 /* SentryCrashDynamicLinker.h in Headers */,
D4AF00232D2E931000F5F3D7 /* SentryNSFileManagerSwizzling.h in Headers */,
639FCF981EBC7B9700778193 /* SentryEvent.h in Headers */,
Expand Down
9 changes: 9 additions & 0 deletions Sources/Sentry/Public/SentryDefines.h
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
@class SentryEvent;
@class SentrySamplingContext;
@class SentryUserFeedbackConfiguration;
@class SentryLog;
@protocol SentrySpan;

/**
Expand Down Expand Up @@ -107,6 +108,14 @@ typedef SentryEvent *_Nullable (^SentryBeforeSendEventCallback)(SentryEvent *_No
*/
typedef id<SentrySpan> _Nullable (^SentryBeforeSendSpanCallback)(id<SentrySpan> _Nonnull span);

#if !SWIFT_PACKAGE
/**
* Use this block to drop or modify a log before the SDK sends it to Sentry. Return @c nil to drop
* the log.
*/
typedef SentryLog *_Nullable (^SentryBeforeSendLogCallback)(SentryLog *_Nonnull log);
#endif // !SWIFT_PACKAGE

/**
* Block can be used to decide if the SDK should capture a screenshot or not. Return @c true if the
* SDK should capture a screenshot, return @c false if not. This callback doesn't work for crashes.
Expand Down
8 changes: 8 additions & 0 deletions Sources/Sentry/Public/SentryOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,14 @@ NS_SWIFT_NAME(Options)
*/
@property (nullable, nonatomic, copy) SentryBeforeSendSpanCallback beforeSendSpan NS_SWIFT_SENDABLE;

#if !SWIFT_PACKAGE
/**
* Use this callback to drop or modify a log before the SDK sends it to Sentry. Return @c nil to
* drop the log.
*/
@property (nullable, nonatomic, copy) SentryBeforeSendLogCallback beforeSendLog NS_SWIFT_SENDABLE;
#endif // !SWIFT_PACKAGE

/**
* This block can be used to modify the event before it will be serialized and sent.
*/
Expand Down
17 changes: 17 additions & 0 deletions Sources/Sentry/SentryOptions.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,25 @@ @implementation SentryOptions {
#if !SDK_V9
BOOL _enableTracingManual;
#endif // !SDK_V9
#if SWIFT_PACKAGE || SENTRY_TEST
id _beforeSendLogDynamic;
#endif // SWIFT_PACKAGE || SENTRY_TEST
}

#if SWIFT_PACKAGE || SENTRY_TEST
// Provide explicit implementation for SPM builds where the property is excluded from header
// Use id to avoid typedef dependency, Swift extension provides type safety
- (id)beforeSendLogDynamic
{
return _beforeSendLogDynamic;
}

- (void)setBeforeSendLogDynamic:(id)beforeSendLogDynamic
{
_beforeSendLogDynamic = beforeSendLogDynamic;
}
#endif // SWIFT_PACKAGE || SENTRY_TEST

+ (NSArray<NSString *> *)defaultIntegrations
{
NSArray<Class> *defaultIntegrationClasses = [SentryOptionsInternal defaultIntegrationClasses];
Expand Down
41 changes: 23 additions & 18 deletions Sources/Swift/Protocol/SentryLog.swift
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
struct SentryLog: Codable {
let timestamp: Date
let traceId: SentryId
let level: SentryLog.Level
let body: String
let attributes: [String: SentryLog.Attribute]
let severityNumber: Int?
@objc
@objcMembers
public class SentryLog: NSObject, Codable {
public var timestamp: Date
public var traceId: SentryId
public var level: Level
public var body: String
public var attributes: [String: Attribute]
public var severityNumber: NSNumber?

private enum CodingKeys: String, CodingKey {
case timestamp
Expand All @@ -17,42 +19,45 @@ struct SentryLog: Codable {

/// The traceId is initially an empty default value and is populated during processing;
/// by the time processing completes, it is guaranteed to be a valid non-empty trace id.
init(
public init(
timestamp: Date,
traceId: SentryId,
level: SentryLog.Level,
level: Level,
body: String,
attributes: [String: SentryLog.Attribute],
severityNumber: Int? = nil
attributes: [String: Attribute],
severityNumber: NSNumber? = nil
) {
self.timestamp = timestamp
self.traceId = traceId
self.level = level
self.body = body
self.attributes = attributes
self.severityNumber = severityNumber ?? level.toSeverityNumber()
self.severityNumber = severityNumber ?? NSNumber(value: level.toSeverityNumber())
super.init()
}

init(from decoder: any Decoder) throws {
required public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

timestamp = try container.decode(Date.self, forKey: .timestamp)
let traceIdString = try container.decode(String.self, forKey: .traceId)
traceId = SentryId(uuidString: traceIdString)
level = try container.decode(SentryLog.Level.self, forKey: .level)
level = try container.decode(Level.self, forKey: .level)
body = try container.decode(String.self, forKey: .body)
attributes = try container.decode([String: SentryLog.Attribute].self, forKey: .attributes)
severityNumber = try container.decodeIfPresent(Int.self, forKey: .severityNumber)
attributes = try container.decode([String: Attribute].self, forKey: .attributes)
severityNumber = try container.decodeIfPresent(Int.self, forKey: .severityNumber).map { NSNumber(value: $0) }

super.init()
}

func encode(to encoder: any Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(timestamp, forKey: .timestamp)
try container.encode(traceId.sentryIdString, forKey: .traceId)
try container.encode(level, forKey: .level)
try container.encode(body, forKey: .body)
try container.encode(attributes, forKey: .attributes)
try container.encodeIfPresent(severityNumber, forKey: .severityNumber)
try container.encodeIfPresent(severityNumber?.intValue, forKey: .severityNumber)
}
}
126 changes: 78 additions & 48 deletions Sources/Swift/Protocol/SentryLogAttribute.swift
Original file line number Diff line number Diff line change
@@ -1,91 +1,121 @@
extension SentryLog {
enum Attribute: Codable {
case string(String)
case boolean(Bool)
case integer(Int)
case double(Double)
@objc(SentryStructuredLogAttribute)
@objcMembers
public class Attribute: NSObject, Codable {
public let type: String
public let value: Any

var type: String {
switch self {
case .string: return "string"
case .boolean: return "boolean"
case .integer: return "integer"
case .double: return "double"
}
public init(string value: String) {
self.type = "string"
self.value = value
super.init()
}

var value: Any {
switch self {
case .string(let value): return value
case .boolean(let value): return value
case .integer(let value): return value
case .double(let value): return value
}
public init(boolean value: Bool) {
self.type = "boolean"
self.value = value
super.init()
}

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

// MARK: - Initializers
public init(double value: Double) {
self.type = "double"
self.value = value
super.init()
}

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

/// Initializes a SentryLog.Attribute from any value, converting it to the appropriate type.
///
/// Supported types: String, Bool, Int, Double, and Float (converted to Double).
/// Other types (including Date, NSNumber, CGFloat, etc.) are converted to string representation.
///
/// See: https://develop.sentry.dev/sdk/telemetry/logs/#appendix-b-otel_log-envelope-item-payload
init(value: Any) {
internal init(value: Any) {
switch value {
case let stringValue as String:
self = .string(stringValue)
self.type = "string"
self.value = stringValue
case let boolValue as Bool:
self = .boolean(boolValue)
self.type = "boolean"
self.value = boolValue
case let intValue as Int:
self = .integer(intValue)
self.type = "integer"
self.value = intValue
case let doubleValue as Double:
self = .double(doubleValue)
self.type = "double"
self.value = doubleValue
case let floatValue as Float:
self = .double(Double(floatValue))
self.type = "double"
self.value = Double(floatValue)
default:
// For any other type, convert to string representation
self = .string(String(describing: value))
self.type = "string"
self.value = String(describing: value)
}
super.init()
}

private enum CodingKeys: String, CodingKey {
case value
case type
}

init(from decoder: any Decoder) throws {
required public init(from decoder: any Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)

let type = try container.decode(String.self, forKey: .type)
self.type = type

switch type {
case "string":
self = .string(try container.decode(String.self, forKey: .value))
self.value = try container.decode(String.self, forKey: .value)
case "boolean":
self = .boolean(try container.decode(Bool.self, forKey: .value))
self.value = try container.decode(Bool.self, forKey: .value)
case "integer":
self = .integer(try container.decode(Int.self, forKey: .value))
self.value = try container.decode(Int.self, forKey: .value)
case "double":
self = .double(try container.decode(Double.self, forKey: .value))
self.value = try container.decode(Double.self, forKey: .value)
default:
throw DecodingError.dataCorruptedError(forKey: .type, in: container, debugDescription: "Unknown type: \(type)")
}

super.init()
}

func encode(to encoder: any Encoder) throws {
public func encode(to encoder: any Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)

try container.encode(type, forKey: .type)

switch self {
case .string(let value):
try container.encode(value, forKey: .value)
case .boolean(let value):
try container.encode(value, forKey: .value)
case .integer(let value):
try container.encode(value, forKey: .value)
case .double(let value):
try container.encode(value, forKey: .value)
switch 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))"))
}
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))"))
}
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))"))
}
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))"))
}
try container.encode(doubleValue, forKey: .value)
default:
try container.encode(String(describing: value), forKey: .value)
}
}
}
Expand Down
Loading
Loading