feat: structured DocuSignError for every failure, with an error handling guide - #10
Merged
Merged
Conversation
Failures only said where something broke (login_failed, signing_failed), never why, so an app could show nothing better than a generic message and a developer could not tell an expired token from a lost connection in production. initialize, loginWithAccessToken, presentCaptiveSigning and presentCaptiveSigningWithUrl now reject with a DocuSignError carrying code, reason, native, http and toAttributes(). reason is derived in one place from facts native code reports (error domain, HTTP status, DocuSign's error code), so it never guesses and is covered by Jest on both platforms. The Expo bridge rejects with only a code and a message, so native code now resolves runtime failures with a failure payload and this layer throws. Native rejections remain for caller mistakes and are wrapped into the same class. Messages and details are redacted before they reach app code: JWTs, Bearer credentials, URL query strings and token-like path segments. In development a caller mistake also prints one warning naming the fix. addSigningErrorListener now receives every DocuSignError exactly once, caller mistakes included, instead of wrapping the native onSigningError event.
The SDK raises errors from several domains, and the module forwarded only the bare NSError code, which is ambiguous without its domain. Runtime failures now travel as DocuSignFailure with domain, code, message and NSUnderlyingErrorKey, and settle through one module helper that resolves them with the payload and emits onSigningError once. Login failures keep the userinfo check's HTTP status and DocuSign's error body instead of flattening them into a string, which is what separates an expired token from a valid token the SDK still refuses. SDK errors delivered through the cancel notification now fail like every other failure rather than resolving status "error", matching Android. Two caller mistakes get their own codes (signing_in_progress, invalid_signing_url), and an unreachable initialize failure reports initialize_failed instead of not_initialized. The presenter lookup moves onto the main thread, where UIKit requires it, and a missing presenter settles the promise once instead of completing the slot and throwing.
The module forwarded only exception.message, although DSException exposes getErrorCode() and getErrorMsg() and DSRestException exposes the HTTP status. Runtime failures now travel as DocuSignFailure with the exception class, the SDK error code, the cause and any HTTP details, and settle through one module helper that resolves them with the payload and emits onSigningError once. The recipient-view mint read DocuSign's error body, threw only the status line, and the fetch fallback then discarded even that. The body is parsed into DocuSignHttpException and attached to the failure if the fallback also fails, so a recipient that does not match the envelope is named instead of lost. Login failures keep the userinfo check's status and error body. Two caller mistakes get their own codes (signing_in_progress, invalid_signing_url), and a missing foreground Activity rejects presentation_failed.
docs/ERROR_HANDLING.md covers the error model, translated copy chosen by reason, a retry policy, reporting to Amplitude, New Relic and Sentry, the queries to read those errors in production, and worked examples. Its code lives in examples/error-handling and is type-checked in the lint job against the real SDKs, which are devDependencies only and stay out of the tarball. scripts/check-doc-examples.js fails CI when the guide's copies drift from those files. The test job now runs with coverage so the thresholds are enforced. README and CHANGELOG describe the new contract.
The URL pattern also captured the punctuation that ends a sentence, so a token path segment followed by a period, comma or closing parenthesis failed the anchored token test and reached the message untouched. Trailing punctuation is now peeled off before redacting and put back after. The guide stated that the access token and signing URL never appear in any field. The package never writes them itself, but redacting SDK text is pattern-based, so the guide now says exactly what is removed and points at toAttributes(), which carries no message text, as the safest payload. Android initialize failures rejected with a message alone although they are runtime failures of the SDK. They now resolve with the exception's details like login and signing failures do. Android reported the immediate exception cause as the underlying error. Java wraps transport failures, so the SocketTimeoutException that explains a timeout can sit several levels down and the failure was classified unknown. The underlying error is now the bounded, cycle-safe root cause. iOS keeps the immediate NSUnderlyingErrorKey, whose deeper CFNetwork error would lose the network classification.
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Every failure now rejects with a structured
DocuSignError, so an app can show users a message that fits the failure and a developer can tell from Amplitude, New Relic or Sentry what failed and why. Ships in 2.0.0, which is still unpublished.Why
Errors only said where something broke (
login_failed,signing_failed), never why. An app could show nothing better than "something went wrong", and in production an expired token looked the same as a lost connection. None of that was fixable from an app, because the package threw the information away:NSError.codewithout its domain, and the SDK raises errors from several domains.exception.message, althoughDSExceptionexposesgetErrorCode()andgetErrorMsg()andDSRestExceptionexposes the HTTP status.The contract
reasonis derived in one place, in TypeScript, from facts native code reports: error domain, HTTP status, DocuSign's own error code. Anything unverifiable isunknown. The rules are covered by Jest on both platforms.AsyncFunctionDefinition.swift:reject(error.code, error.description, nil)), so native code rejects only for caller mistakes and resolves runtime failures with a failure payload.src/api.tsturns both into a thrownDocuSignError.addSigningErrorListenerreceives everyDocuSignErrorexactly once, caller mistakes included, so an app can wire logging once at startup.Bearercredentials, URL query strings and token-like path segments. The redaction is pattern-based, and the guide says so.Breaking changes (2.0.0)
DocuSignError. Branch oncodeandreason, not message text.presentCaptiveSigning*never resolvestatus: 'error'. iOS used it for SDK errors reported after the UI was on screen, while Android rejected the same failures.'error'stays inSigningStatusso switch statements compile.signing_failed:signing_in_progressandinvalid_signing_url.addSigningErrorListenerreceives aDocuSignErrorinstead of{ errorCode, errorMessage }.useDocuSignSigningtypeserrorasDocuSignError | null.Full list in
CHANGELOG.md.Also fixed
Documentation
docs/ERROR_HANDLING.mdcovers the error model, translated copy chosen byreason(English and Italian resource files), a retry policy, reporting to Amplitude, New Relic and Sentry, the Amplitude charts and NRQL queries to read the results, and worked examples for every reason.Its code is real files under
examples/error-handling, type-checked in thelintjob against the real SDKs.scripts/check-doc-examples.jsfails CI if the guide's copies drift from those files. The New Relic query targetsMobileJSError, where React Native agent 1.9.0 and later storerecordError.Verification
src/api.tsandsrc/DocuSignError.tsat 100% lines, and thetestjob now runs with coverage so the thresholds are enforced.xcodebuildof theReactNativeDocuSignpod target, and GradlecompileDebugKotlinof the Android module.Not verified yet
CI compiles no Swift or Kotlin and runs nothing on a device. Before tagging 2.0.0, a fault-injection run in a consuming app should confirm, on both platforms:
login_failedwithreason: 'auth'.recipientClientUserIdunder the AndroidsigningUrlstrategy returnsUNKNOWN_ENVELOPE_RECIPIENT. If DocuSign answers with a different code, therecipientmapping gets dropped.reason: 'network', withnativepopulated.DSException.getErrorCode()values, recorded for future mappings.Notes
@amplitude/analytics-react-native,newrelic-react-native-agent,@sentry/react-native,i18nextandreact-i18nextare devDependencies, only for type-checking the examples. They are not in the tarball, and the only runtime dependency is stilladm-zip. They add 3 moderatenpm auditadvisories to the dev tree (27 before, 30 after).