Skip to content

Commit f41cbcd

Browse files
authored
fix: Multiple attachments support for feedback (#6752)
1 parent 543b791 commit f41cbcd

File tree

7 files changed

+84
-42
lines changed

7 files changed

+84
-42
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
If you need a precompiled XCFramework built with Xcode 15, continue using Sentry SDK 8.x.x.
4141
- Set `SentryException.type` to `nil` when `NSException` has no `reason` (#6653). The backend then can provide a proper message when there is no reason.
4242
- Rename `SentryLog.Level` and `SentryLog.Attribute` for ObjC (#6666)
43+
- Change `SentryFeedback` initializer to support multiple attachments (#6752)
4344

4445
### Fixes
4546

Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -351,10 +351,19 @@ extension SentrySDKWrapper {
351351
alert.addAction(.init(title: "Deal with it 🕶️", style: .default))
352352
UIApplication.shared.delegate?.window??.rootViewController?.present(alert, animated: true)
353353

354-
// if there's a screenshot's Data in this dictionary, JSONSerialization crashes _even though_ there's a `try?`, so we'll write the base64 encoding of it
355354
var infoToWriteToFile = info
356-
if let attachments = info["attachments"] as? [Any], let screenshot = attachments.first as? Data {
357-
infoToWriteToFile["attachments"] = [screenshot.base64EncodedString()]
355+
if let attachments = info["attachments"] as? [[String: Any]] {
356+
// Extract data from each attachment dictionary (JSONSerialization crashes _even though_ there's a `try?`, so we'll write the base64 encoding of it)
357+
let processedAttachments = attachments.compactMap { attachment -> [String: Any]? in
358+
var processed = attachment
359+
if let data = attachment["data"] as? Data {
360+
processed["data"] = data.base64EncodedString()
361+
}
362+
return processed
363+
}
364+
if !processedAttachments.isEmpty {
365+
infoToWriteToFile["attachments"] = processedAttachments
366+
}
358367
}
359368

360369
let jsonData = (try? JSONSerialization.data(withJSONObject: infoToWriteToFile, options: .sortedKeys)) ?? Data()

Samples/iOS-Swift/iOS-Swift/ExtraViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -205,9 +205,9 @@ class ExtraViewController: UIViewController {
205205

206206
@IBAction func captureUserFeedbackV2(_ sender: UIButton) {
207207
highlightButton(sender)
208-
var attachments: [Data]?
208+
var attachments: [Attachment]?
209209
if let url = BundleResourceProvider.screenshotURL, let data = try? Data(contentsOf: url) {
210-
attachments = [data]
210+
attachments = [Attachment(data: data, filename: "screenshot.png", contentType: "image/png")]
211211
}
212212
let errorEventID = SentrySDK.capture(error: NSError(domain: "test-error.user-feedback.iOS-Swift", code: 1))
213213
let feedback = SentryFeedback(message: "It broke again on iOS-Swift. I don't know why, but this happens.", name: "John Me", email: "[email protected]", source: .custom, associatedEventId: errorEventID, attachments: attachments)

Sources/Swift/Integrations/UserFeedback/SentryFeedback.swift

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ public final class SentryFeedback: NSObject {
2020
var message: String
2121
var source: SentryFeedbackSource
2222
@_spi(Private) public let eventId: SentryId
23-
24-
/// Data objects for any attachments. Currently the web UI only supports showing one attached image, like for a screenshot.
25-
private var attachments: [Data]?
26-
23+
24+
/// Attachments for this feedback submission, like a screenshot.
25+
private var attachments: [Attachment]?
26+
2727
/// The event id that this feedback is associated with, like a crash report.
2828
var associatedEventId: SentryId?
29-
29+
3030
/// - parameters:
3131
/// - associatedEventId The ID for an event you'd like associated with the feedback.
32-
/// - attachments Data objects for any attachments. Currently the web UI only supports showing one attached image, like for a screenshot.
33-
@objc public init(message: String, name: String?, email: String?, source: SentryFeedbackSource = .widget, associatedEventId: SentryId? = nil, attachments: [Data]? = nil) {
32+
/// - attachments Attachment objects for any files to include with the feedback.
33+
@objc public init(message: String, name: String?, email: String?, source: SentryFeedbackSource = .widget, associatedEventId: SentryId? = nil, attachments: [Attachment]? = nil) {
3434
self.eventId = SentryId()
3535
self.name = name
3636
self.email = email
@@ -83,19 +83,32 @@ extension SentryFeedback {
8383
dict["email"] = email
8484
}
8585
if let attachments = attachments {
86-
dict["attachments"] = attachments
86+
dict["attachments"] = attachments.map { $0.dataDictionary() }
8787
}
8888
return dict
8989
}
9090

9191
/**
92-
* - note: Currently there is only a single attachment possible, for the screenshot, of which there can be only one.
92+
* Returns all attachments for inclusion in the feedback envelope.
9393
*/
9494
@_spi(Private) public func attachmentsForEnvelope() -> [Attachment] {
95-
var items = [Attachment]()
96-
if let screenshot = attachments?.first {
97-
items.append(Attachment(data: screenshot, filename: "screenshot.png", contentType: "application/png"))
95+
return attachments ?? []
96+
}
97+
}
98+
99+
// MARK: Attachment Serialization
100+
extension Attachment {
101+
func dataDictionary() -> [String: Any] {
102+
var attDict: [String: Any] = ["filename": filename]
103+
if let data = data {
104+
attDict["data"] = data
105+
}
106+
if let path = path {
107+
attDict["path"] = path
108+
}
109+
if let contentType = contentType {
110+
attDict["contentType"] = contentType
98111
}
99-
return items
112+
return attDict
100113
}
101114
}

Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -455,11 +455,11 @@ extension SentryUserFeedbackFormViewModel {
455455
}
456456

457457
func feedbackObject() -> SentryFeedback {
458-
var attachmentDatas: [Data]?
458+
var attachments: [Attachment]?
459459
if let image = screenshotImageView.image, let data = image.pngData() {
460-
attachmentDatas = [data]
460+
attachments = [Attachment(data: data, filename: "screenshot.png", contentType: "image/png")]
461461
}
462-
return SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, attachments: attachmentDatas)
462+
return SentryFeedback(message: messageTextView.text, name: fullNameTextField.text, email: emailTextField.text, attachments: attachments)
463463
}
464464
}
465465

Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift

Lines changed: 34 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,65 +30,84 @@ class SentryFeedbackTests: XCTestCase {
3030
}
3131

3232
func testSerializeWithAllFields() throws {
33-
let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "[email protected]", attachments: [Data()])
34-
33+
let attachment = Attachment(data: Data(), filename: "screenshot.png", contentType: "image/png")
34+
let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "[email protected]", attachments: [attachment])
35+
3536
let serialization = sut.serialize()
3637
XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message")
3738
XCTAssertEqual(try XCTUnwrap(serialization["name"] as? String), "Test feedback provider")
3839
XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "[email protected]")
3940
XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "widget")
40-
41+
4142
let attachments = sut.attachmentsForEnvelope()
4243
XCTAssertEqual(attachments.count, 1)
4344
XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png")
44-
XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png")
45+
XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "image/png")
4546
}
4647

4748
func testSerializeCustomFeedback() throws {
48-
let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "[email protected]", source: .custom, attachments: [Data()])
49-
49+
let attachment = Attachment(data: Data(), filename: "screenshot.png", contentType: "image/png")
50+
let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "[email protected]", source: .custom, attachments: [attachment])
51+
5052
let serialization = sut.serialize()
5153
XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message")
5254
XCTAssertEqual(try XCTUnwrap(serialization["name"] as? String), "Test feedback provider")
5355
XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "[email protected]")
5456
XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "custom")
55-
57+
5658
let attachments = sut.attachmentsForEnvelope()
5759
XCTAssertEqual(attachments.count, 1)
5860
XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png")
59-
XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png")
61+
XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "image/png")
6062
}
6163

6264
func testSerializeWithAssociatedEventID() throws {
6365
let eventID = SentryId()
64-
65-
let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "[email protected]", source: .custom, associatedEventId: eventID, attachments: [Data()])
66-
66+
let attachment = Attachment(data: Data(), filename: "screenshot.png", contentType: "image/png")
67+
let sut = SentryFeedback(message: "Test feedback message", name: "Test feedback provider", email: "[email protected]", source: .custom, associatedEventId: eventID, attachments: [attachment])
68+
6769
let serialization = sut.serialize()
6870
XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message")
6971
XCTAssertEqual(try XCTUnwrap(serialization["name"] as? String), "Test feedback provider")
7072
XCTAssertEqual(try XCTUnwrap(serialization["contact_email"] as? String), "[email protected]")
7173
XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "custom")
7274
XCTAssertEqual(try XCTUnwrap(serialization["associated_event_id"] as? String), eventID.sentryIdString)
73-
75+
7476
let attachments = sut.attachmentsForEnvelope()
7577
XCTAssertEqual(attachments.count, 1)
7678
XCTAssertEqual(try XCTUnwrap(attachments.first).filename, "screenshot.png")
77-
XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "application/png")
79+
XCTAssertEqual(try XCTUnwrap(attachments.first).contentType, "image/png")
7880
}
7981

8082
func testSerializeWithNoOptionalFields() throws {
8183
let sut = SentryFeedback(message: "Test feedback message", name: nil, email: nil)
82-
84+
8385
let serialization = sut.serialize()
8486
XCTAssertEqual(try XCTUnwrap(serialization["message"] as? String), "Test feedback message")
8587
XCTAssertNil(serialization["name"])
8688
XCTAssertNil(serialization["contact_email"])
8789
XCTAssertEqual(try XCTUnwrap(serialization["source"] as? String), "widget")
88-
90+
8991
let attachments = sut.attachmentsForEnvelope()
9092
XCTAssertEqual(attachments.count, 0)
9193
}
94+
95+
func testMultipleAttachments() throws {
96+
let screenshot = Attachment(data: Data("screenshot".utf8), filename: "screenshot.png", contentType: "image/png")
97+
let logFile = Attachment(data: Data("log content".utf8), filename: "app.log", contentType: "text/plain")
98+
let videoFile = Attachment(data: Data("video".utf8), filename: "recording.mp4", contentType: "video/mp4")
99+
100+
let sut = SentryFeedback(message: "Test feedback with multiple attachments", name: "Test User", email: "[email protected]", attachments: [screenshot, logFile, videoFile])
101+
102+
let attachments = sut.attachmentsForEnvelope()
103+
XCTAssertEqual(attachments.count, 3)
104+
XCTAssertEqual(attachments[0].filename, "screenshot.png")
105+
XCTAssertEqual(attachments[0].contentType, "image/png")
106+
XCTAssertEqual(attachments[1].filename, "app.log")
107+
XCTAssertEqual(attachments[1].contentType, "text/plain")
108+
XCTAssertEqual(attachments[2].filename, "recording.mp4")
109+
XCTAssertEqual(attachments[2].contentType, "video/mp4")
110+
}
92111

93112
private let inputCombinations: [FeedbackTestCase] = [
94113
// base case: don't require name or email, don't input a name or email, don't input a message or screenshot

sdk_api.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32783,18 +32783,18 @@
3278332783
{
3278432784
"kind": "TypeNominal",
3278532785
"name": "Optional",
32786-
"printedName": "[Foundation.Data]?",
32786+
"printedName": "[Sentry.Attachment]?",
3278732787
"children": [
3278832788
{
3278932789
"kind": "TypeNominal",
3279032790
"name": "Array",
32791-
"printedName": "[Foundation.Data]",
32791+
"printedName": "[Sentry.Attachment]",
3279232792
"children": [
3279332793
{
3279432794
"kind": "TypeNominal",
32795-
"name": "Data",
32796-
"printedName": "Foundation.Data",
32797-
"usr": "s:10Foundation4DataV"
32795+
"name": "Attachment",
32796+
"printedName": "Sentry.Attachment",
32797+
"usr": "c:objc(cs)SentryAttachment"
3279832798
}
3279932799
],
3280032800
"usr": "s:Sa"
@@ -32806,7 +32806,7 @@
3280632806
],
3280732807
"declKind": "Constructor",
3280832808
"usr": "c:@M@Sentry@objc(cs)SentryFeedback(im)initWithMessage:name:email:source:associatedEventId:attachments:",
32809-
"mangledName": "$s6Sentry0A8FeedbackC7message4name5email6source17associatedEventId11attachmentsACSS_SSSgAjC0aB6SourceOSo0aI0CSgSay10Foundation4DataVGSgtcfc",
32809+
"mangledName": "$s6Sentry0A8FeedbackC7message4name5email6source17associatedEventId11attachmentsACSS_SSSgAjC0aB6SourceOSo0aI0CSgSaySo0A10AttachmentCGSgtcfc",
3281032810
"moduleName": "Sentry",
3281132811
"objc_name": "initWithMessage:name:email:source:associatedEventId:attachments:",
3281232812
"declAttributes": [

0 commit comments

Comments
 (0)