Skip to content

Commit e5e08fa

Browse files
authored
Add options to customize UserFeedback error messages (#6790)
* Add new options to customize error messages * Use new user messages in User Feedback form * Add custom messages for our test samples * Update broken test * Update changelog * Update sdk_api.json * Update changelog * Apply suggestions from code review * Update changelog to reflect the new feature instead of improvement * Update changelog * Keep `InputError` compliant to `CustomStringConvertible`, add formConfig as an associated value to the error for the description generation * Use LocalizedError * Replace FormConfiguration with plain localized string * Update UI tests * Call `try XCTUnwrap` outside of XCTassertFalse autoclosure
1 parent 3540969 commit e5e08fa

File tree

8 files changed

+238
-14
lines changed

8 files changed

+238
-14
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Feature
6+
7+
- Add options to customize UserFeedback error messages (#6790)
8+
59
### Fixes
610

711
- Fix UIWindow being possibly accessed from a background thread in SentryCrashWrapper (#6905)

Samples/SentrySampleShared/SentrySampleShared/SentrySDKWrapper.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,10 @@ extension SentrySDKWrapper {
288288
config.messageLabel = "Thy complaint"
289289
config.emailLabel = "Thine email"
290290
config.nameLabel = "Thy name"
291+
config.unexpectedErrorText = "Santry doesn't know how to process this error"
292+
config.validationErrorMessage = { multipleErrors in
293+
return "You got \(multipleErrors ? "many" : "an" ) error\(multipleErrors ? "s" : "") in this form:"
294+
}
291295
}
292296

293297
func configureFeedbackTheme(config: SentryUserFeedbackThemeConfiguration) {

Samples/iOS-Swift/iOS-Swift-UITests/UserFeedbackUITests.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -414,7 +414,7 @@ extension UserFeedbackUITests {
414414
submit(expectingError: true)
415415

416416
XCTAssert(app.staticTexts["Error"].exists)
417-
XCTAssert(app.staticTexts["You must provide all required information before submitting. Please check the following fields: thine email and thy complaint."].exists)
417+
XCTAssert(app.staticTexts["You got many errors in this form: thine email and thy complaint."].exists)
418418

419419
app.buttons["OK"].tap()
420420

@@ -441,7 +441,7 @@ extension UserFeedbackUITests {
441441
submit(expectingError: true)
442442

443443
XCTAssert(app.staticTexts["Error"].exists)
444-
XCTAssert(app.staticTexts.element(matching: NSPredicate(format: "label LIKE 'You must provide all required information before submitting. Please check the following fields: thy name, thine email and thy complaint.'")).exists)
444+
XCTAssert(app.staticTexts.element(matching: NSPredicate(format: "label LIKE 'You got many errors in this form: thy name, thine email and thy complaint.'")).exists)
445445

446446
app.buttons["OK"].tap()
447447

Sources/Swift/Integrations/UserFeedback/Configuration/SentryUserFeedbackFormConfiguration.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,20 @@ public final class SentryUserFeedbackFormConfiguration: NSObject {
155155
func fullLabelText(labelText: String, required: Bool) -> String {
156156
required ? labelText + " " + isRequiredLabel : labelText
157157
}
158+
159+
/**
160+
* Message shown to the user when an unexpected error happens while submitting feedback.
161+
* - note: Default: `"Unexpected client error."`
162+
*/
163+
public var unexpectedErrorText: String = "Unexpected client error."
164+
165+
/**
166+
* Message shown to the user when the form fails the validation.
167+
* - note: Default: `"You must provide all required information before submitting. Please check the following field(s)"`
168+
*/
169+
public var validationErrorMessage: (Bool) -> String = { multipleErrors in
170+
return "You must provide all required information before submitting. Please check the following field\(multipleErrors ? "s" : ""):"
171+
}
158172
}
159173

160174
#endif // os(iOS) && !SENTRY_NO_UIKIT

Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormController.swift

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,14 @@ extension SentryUserFeedbackFormController: SentryUserFeedbackFormViewModelDeleg
8686
}
8787
}
8888

89-
guard case let SentryUserFeedbackFormViewModel.InputError.validationError(missing) = error else {
89+
guard case let SentryUserFeedbackFormViewModel.InputError.validationError(missing, _) = error,
90+
let errorDescription = error.errorDescription else {
9091
SentrySDKLog.warning("Unexpected error type.")
91-
presentAlert(message: "Unexpected client error.", errorCode: 2, info: [NSLocalizedDescriptionKey: "Client error: ."])
92+
presentAlert(message: config.formConfig.unexpectedErrorText, errorCode: 2, info: [NSLocalizedDescriptionKey: "Client error: ."])
9293
return
9394
}
9495

95-
presentAlert(message: error.description, errorCode: 1, info: ["missing_fields": missing, NSLocalizedDescriptionKey: "The user did not complete the feedback form."])
96+
presentAlert(message: errorDescription, errorCode: 1, info: ["missing_fields": missing, NSLocalizedDescriptionKey: "The user did not complete the feedback form."])
9697
}
9798
}
9899

Sources/Swift/Integrations/UserFeedback/SentryUserFeedbackFormViewModel.swift

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@ extension SentryUserFeedbackFormViewModel {
325325
func updateSubmitButtonAccessibilityHint() {
326326
switch validate() {
327327
case .success(let hint): submitButton.accessibilityHint = hint
328-
case .failure(let error): submitButton.accessibilityHint = error.description
328+
case .failure(let error): submitButton.accessibilityHint = error.errorDescription
329329
}
330330
}
331331

@@ -433,23 +433,32 @@ extension SentryUserFeedbackFormViewModel {
433433
}
434434

435435
guard missing.isEmpty else {
436-
let result = SentryUserFeedbackFormValidation.failure(InputError.validationError(missingFields: missing))
436+
let localizedError = InputError.buildDescriptionFor(missingFields: missing, validationErrorMessage: config.formConfig.validationErrorMessage)
437+
let result = SentryUserFeedbackFormValidation.failure(InputError.validationError(missingFields: missing, localizedError: localizedError))
437438
return result
438439
}
439440

440441
return SentryUserFeedbackFormValidation.success(hint.joined(separator: " ").appending("."))
441442
}
442443

443-
enum InputError: Error {
444-
case validationError(missingFields: [String])
444+
enum InputError: LocalizedError {
445+
case validationError(missingFields: [String], localizedError: String)
445446

446447
var description: String {
447448
switch self {
448-
case .validationError(let missingFields):
449-
let list = missingFields.count == 1 ? missingFields[0] : missingFields[0 ..< missingFields.count - 1].joined(separator: ", ") + " and " + missingFields[missingFields.count - 1]
450-
return "You must provide all required information before submitting. Please check the following field\(missingFields.count > 1 ? "s" : ""): \(list)."
449+
case .validationError(_, let localizedError):
450+
return localizedError
451451
}
452452
}
453+
454+
var errorDescription: String? {
455+
return description
456+
}
457+
458+
static func buildDescriptionFor(missingFields: [String], validationErrorMessage: (Bool) -> String) -> String {
459+
let list = missingFields.count == 1 ? missingFields[0] : missingFields[0 ..< missingFields.count - 1].joined(separator: ", ") + " and " + missingFields[missingFields.count - 1]
460+
return "\(validationErrorMessage(missingFields.count > 1 )) \(list)."
461+
}
453462
}
454463

455464
func feedbackObject() -> SentryFeedback {

Tests/SentryTests/Integrations/Feedback/SentryFeedbackTests.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ class SentryFeedbackTests: XCTestCase {
183183
(config: (requiresName: true, requiresEmail: true, nameInput: "tester", emailInput: "[email protected]", messageInput: "Test message", includeScreenshot: true), shouldValidate: true, expectedSubmitButtonAccessibilityHint: "Will submit feedback for tester at [email protected] including attached screenshot with message: Test message.")
184184
]
185185

186-
func testSubmitButtonAccessibilityHint() {
186+
func testSubmitButtonAccessibilityHint() throws {
187187
for input in inputCombinations {
188188
let config = SentryUserFeedbackConfiguration()
189189
config.configureForm = {
@@ -204,7 +204,8 @@ class SentryFeedbackTests: XCTestCase {
204204
XCTAssert(input.shouldValidate)
205205
XCTAssertEqual(hint, input.expectedSubmitButtonAccessibilityHint, testCaseDescription())
206206
case .failure(let error):
207-
XCTAssertFalse(input.shouldValidate, error.description + "; " + testCaseDescription())
207+
let errorDescription = try XCTUnwrap(error.errorDescription)
208+
XCTAssertFalse(input.shouldValidate, errorDescription + "; " + testCaseDescription())
208209
}
209210

210211
}

sdk_api.json

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56154,6 +56154,197 @@
5615456154
}
5615556155
]
5615656156
},
56157+
{
56158+
"kind": "Var",
56159+
"name": "unexpectedErrorText",
56160+
"printedName": "unexpectedErrorText",
56161+
"children": [
56162+
{
56163+
"kind": "TypeNominal",
56164+
"name": "String",
56165+
"printedName": "Swift.String",
56166+
"usr": "s:SS"
56167+
}
56168+
],
56169+
"declKind": "Var",
56170+
"usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(py)unexpectedErrorText",
56171+
"mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC19unexpectedErrorTextSSvp",
56172+
"moduleName": "Sentry",
56173+
"declAttributes": [
56174+
"Final",
56175+
"ObjC",
56176+
"HasStorage"
56177+
],
56178+
"hasStorage": true,
56179+
"accessors": [
56180+
{
56181+
"kind": "Accessor",
56182+
"name": "Get",
56183+
"printedName": "Get()",
56184+
"children": [
56185+
{
56186+
"kind": "TypeNominal",
56187+
"name": "String",
56188+
"printedName": "Swift.String",
56189+
"usr": "s:SS"
56190+
}
56191+
],
56192+
"declKind": "Accessor",
56193+
"usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)unexpectedErrorText",
56194+
"mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC19unexpectedErrorTextSSvg",
56195+
"moduleName": "Sentry",
56196+
"implicit": true,
56197+
"declAttributes": [
56198+
"Final",
56199+
"ObjC"
56200+
],
56201+
"accessorKind": "get"
56202+
},
56203+
{
56204+
"kind": "Accessor",
56205+
"name": "Set",
56206+
"printedName": "Set()",
56207+
"children": [
56208+
{
56209+
"kind": "TypeNominal",
56210+
"name": "Void",
56211+
"printedName": "()"
56212+
},
56213+
{
56214+
"kind": "TypeNominal",
56215+
"name": "String",
56216+
"printedName": "Swift.String",
56217+
"usr": "s:SS"
56218+
}
56219+
],
56220+
"declKind": "Accessor",
56221+
"usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)setUnexpectedErrorText:",
56222+
"mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC19unexpectedErrorTextSSvs",
56223+
"moduleName": "Sentry",
56224+
"implicit": true,
56225+
"declAttributes": [
56226+
"Final",
56227+
"ObjC"
56228+
],
56229+
"accessorKind": "set"
56230+
}
56231+
]
56232+
},
56233+
{
56234+
"kind": "Var",
56235+
"name": "validationErrorMessage",
56236+
"printedName": "validationErrorMessage",
56237+
"children": [
56238+
{
56239+
"kind": "TypeFunc",
56240+
"name": "Function",
56241+
"printedName": "(Swift.Bool) -> Swift.String",
56242+
"children": [
56243+
{
56244+
"kind": "TypeNominal",
56245+
"name": "String",
56246+
"printedName": "Swift.String",
56247+
"usr": "s:SS"
56248+
},
56249+
{
56250+
"kind": "TypeNominal",
56251+
"name": "Bool",
56252+
"printedName": "Swift.Bool",
56253+
"usr": "s:Sb"
56254+
}
56255+
]
56256+
}
56257+
],
56258+
"declKind": "Var",
56259+
"usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(py)validationErrorMessage",
56260+
"mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC22validationErrorMessageySSSbcvp",
56261+
"moduleName": "Sentry",
56262+
"declAttributes": [
56263+
"Final",
56264+
"ObjC",
56265+
"HasStorage"
56266+
],
56267+
"hasStorage": true,
56268+
"accessors": [
56269+
{
56270+
"kind": "Accessor",
56271+
"name": "Get",
56272+
"printedName": "Get()",
56273+
"children": [
56274+
{
56275+
"kind": "TypeFunc",
56276+
"name": "Function",
56277+
"printedName": "(Swift.Bool) -> Swift.String",
56278+
"children": [
56279+
{
56280+
"kind": "TypeNominal",
56281+
"name": "String",
56282+
"printedName": "Swift.String",
56283+
"usr": "s:SS"
56284+
},
56285+
{
56286+
"kind": "TypeNominal",
56287+
"name": "Bool",
56288+
"printedName": "Swift.Bool",
56289+
"usr": "s:Sb"
56290+
}
56291+
]
56292+
}
56293+
],
56294+
"declKind": "Accessor",
56295+
"usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)validationErrorMessage",
56296+
"mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC22validationErrorMessageySSSbcvg",
56297+
"moduleName": "Sentry",
56298+
"implicit": true,
56299+
"declAttributes": [
56300+
"Final",
56301+
"ObjC"
56302+
],
56303+
"accessorKind": "get"
56304+
},
56305+
{
56306+
"kind": "Accessor",
56307+
"name": "Set",
56308+
"printedName": "Set()",
56309+
"children": [
56310+
{
56311+
"kind": "TypeNominal",
56312+
"name": "Void",
56313+
"printedName": "()"
56314+
},
56315+
{
56316+
"kind": "TypeFunc",
56317+
"name": "Function",
56318+
"printedName": "(Swift.Bool) -> Swift.String",
56319+
"children": [
56320+
{
56321+
"kind": "TypeNominal",
56322+
"name": "String",
56323+
"printedName": "Swift.String",
56324+
"usr": "s:SS"
56325+
},
56326+
{
56327+
"kind": "TypeNominal",
56328+
"name": "Bool",
56329+
"printedName": "Swift.Bool",
56330+
"usr": "s:Sb"
56331+
}
56332+
]
56333+
}
56334+
],
56335+
"declKind": "Accessor",
56336+
"usr": "c:@M@Sentry@objc(cs)SentryUserFeedbackFormConfiguration(im)setValidationErrorMessage:",
56337+
"mangledName": "$s6Sentry0A29UserFeedbackFormConfigurationC22validationErrorMessageySSSbcvs",
56338+
"moduleName": "Sentry",
56339+
"implicit": true,
56340+
"declAttributes": [
56341+
"Final",
56342+
"ObjC"
56343+
],
56344+
"accessorKind": "set"
56345+
}
56346+
]
56347+
},
5615756348
{
5615856349
"kind": "Constructor",
5615956350
"name": "init",

0 commit comments

Comments
 (0)