Skip to content

Commit b93430c

Browse files
PearappsKen Ackerson
andauthored
[Identity] Adding in new error logs and improving many existing ones (#6243)
This adds in a variety of new error logs so that we can gather more information on the error states in the identity flow. Additionally, this adds in a bunch more details to existing and new logs to help us figure out more when we go to read the logs. Went through a handful of scenarios to make sure I saw the analytics log logged to console (which we have enabled in debug) --------- Co-authored-by: Ken Ackerson <ken@lickability.net>
1 parent 20e9fa3 commit b93430c

19 files changed

Lines changed: 956 additions & 126 deletions

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ MINOR
33

44
## X.Y.Z - changes pending release
55

6+
### Identity
7+
* [Improved] Improved StripeIdentity analytics with richer error details and screen/camera context to help debug verification flows.
8+
69
### All
710
* [Removed] Removed UPI support across the SDK.
811

StripeIdentity/StripeIdentity/Source/Analytics/IdentityAnalyticsClient.swift

Lines changed: 182 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,11 @@ enum IdentityAnalyticsClientError: AnalyticLoggableErrorV2 {
3636
}
3737
}
3838

39+
private enum CameraPermissionError: String, AnalyticLoggableStringErrorV2 {
40+
case denied = "cameraPermissionDenied"
41+
case unknown = "cameraPermissionUnknown"
42+
}
43+
3944
/// Wrapper for AnalyticsClient that formats Identity-specific analytics
4045
final class IdentityAnalyticsClient {
4146

@@ -85,6 +90,15 @@ final class IdentityAnalyticsClient {
8590
case selfie
8691
}
8792

93+
enum CameraSource: String {
94+
case cameraSession = "camera_session"
95+
case imagePicker = "image_picker"
96+
}
97+
enum CameraEventKind: String {
98+
case permission = "permission"
99+
case runtimeError = "runtime_error"
100+
}
101+
88102
static let sharedAnalyticsClient = AnalyticsClientV2(
89103
clientId: "mobile-identity-sdk",
90104
origin: "stripe-identity-ios"
@@ -143,6 +157,36 @@ final class IdentityAnalyticsClient {
143157
}
144158
}
145159

160+
private func cameraMetadata(
161+
screenName: ScreenName,
162+
cameraSource: CameraSource,
163+
cameraEventKind: CameraEventKind
164+
) -> [String: Any] {
165+
return [
166+
"screen_name": screenName.rawValue,
167+
"camera_source": cameraSource.rawValue,
168+
"camera_event_kind": cameraEventKind.rawValue,
169+
]
170+
}
171+
172+
private func cameraAccessState(isGranted: Bool?) -> String {
173+
guard let isGranted = isGranted else {
174+
return "unknown"
175+
}
176+
return isGranted ? "granted" : "denied"
177+
}
178+
179+
private func cameraPermissionError(isGranted: Bool?) -> CameraPermissionError {
180+
guard let isGranted = isGranted else {
181+
return .unknown
182+
}
183+
if isGranted {
184+
assertionFailure("cameraPermissionError should not be created for granted camera access")
185+
return .unknown
186+
}
187+
return .denied
188+
}
189+
146190
private func logAnalytic(
147191
_ eventName: EventName,
148192
metadata: [String: Any],
@@ -219,8 +263,8 @@ final class IdentityAnalyticsClient {
219263
}
220264
}
221265

222-
/// Helper to create metadata common to both failed, canceled, and succeed analytic events
223-
private func failedCanceledSucceededCommonMetadataPayload(
266+
/// Helper to create metadata common to flow outcome analytic events
267+
private func flowOutcomeCommonMetadataPayload(
224268
sheetController: VerificationSheetControllerProtocol
225269
) -> [String: Any] {
226270
var metadata: [String: Any] = [:]
@@ -239,13 +283,28 @@ final class IdentityAnalyticsClient {
239283
return metadata
240284
}
241285

286+
private func addLastScreenNameIfAvailable(
287+
to metadata: inout [String: Any],
288+
sheetController: VerificationSheetControllerProtocol
289+
) {
290+
if let lastScreenName = sheetController.flowController.analyticsLastScreen?.analyticsScreenName.rawValue {
291+
metadata["last_screen_name"] = lastScreenName
292+
}
293+
}
294+
242295
/// Logs an event when the verification sheet is closed
243296
private func logSheetClosed(sessionResult: String, sheetController: VerificationSheetControllerProtocol) {
297+
var metadata = flowOutcomeCommonMetadataPayload(
298+
sheetController: sheetController
299+
)
300+
metadata["session_result"] = sessionResult
301+
addLastScreenNameIfAvailable(
302+
to: &metadata,
303+
sheetController: sheetController
304+
)
244305
logAnalytic(
245306
.sheetClosed,
246-
metadata: [
247-
"session_result": sessionResult
248-
],
307+
metadata: metadata,
249308
verificationPage: try? sheetController.verificationPageResponse?.get()
250309
)
251310
}
@@ -257,7 +316,11 @@ final class IdentityAnalyticsClient {
257316
filePath: StaticString,
258317
line: UInt
259318
) {
260-
var metadata = failedCanceledSucceededCommonMetadataPayload(
319+
var metadata = flowOutcomeCommonMetadataPayload(
320+
sheetController: sheetController
321+
)
322+
addLastScreenNameIfAvailable(
323+
to: &metadata,
261324
sheetController: sheetController
262325
)
263326
metadata["error"] = AnalyticsClientV2.serialize(
@@ -273,12 +336,13 @@ final class IdentityAnalyticsClient {
273336
private func logVerificationCanceled(
274337
sheetController: VerificationSheetControllerProtocol
275338
) {
276-
var metadata = failedCanceledSucceededCommonMetadataPayload(
339+
var metadata = flowOutcomeCommonMetadataPayload(
340+
sheetController: sheetController
341+
)
342+
addLastScreenNameIfAvailable(
343+
to: &metadata,
277344
sheetController: sheetController
278345
)
279-
if let lastScreen = sheetController.flowController.analyticsLastScreen {
280-
metadata["last_screen_name"] = lastScreen.analyticsScreenName.rawValue
281-
}
282346

283347
logAnalytic(.verificationCanceled, metadata: metadata, verificationPage: try? sheetController.verificationPageResponse?.get())
284348
}
@@ -287,7 +351,7 @@ final class IdentityAnalyticsClient {
287351
func logVerificationSucceeded(
288352
sheetController: VerificationSheetControllerProtocol
289353
) {
290-
var metadata = failedCanceledSucceededCommonMetadataPayload(
354+
var metadata = flowOutcomeCommonMetadataPayload(
291355
sheetController: sheetController
292356
)
293357

@@ -317,11 +381,15 @@ final class IdentityAnalyticsClient {
317381
/// Logs an event when a screen is presented
318382
func logScreenAppeared(
319383
screenName: ScreenName,
384+
previousScreenName: ScreenName? = nil,
320385
sheetController: VerificationSheetControllerProtocol
321386
) {
322-
let metadata: [String: Any] = [
387+
var metadata: [String: Any] = [
323388
"screen_name": screenName.rawValue,
324389
]
390+
if let previousScreenName = previousScreenName {
391+
metadata["previous_screen_name"] = previousScreenName.rawValue
392+
}
325393

326394
logAnalytic(.screenAppeared, metadata: metadata, verificationPage: try? sheetController.verificationPageResponse?.get())
327395
}
@@ -330,10 +398,16 @@ final class IdentityAnalyticsClient {
330398
func logCameraError(
331399
sheetController: VerificationSheetControllerProtocol,
332400
error: Error,
401+
screenName: ScreenName,
402+
cameraSource: CameraSource,
333403
filePath: StaticString = #filePath,
334404
line: UInt = #line
335405
) {
336-
var metadata: [String: Any] = [:]
406+
var metadata = cameraMetadata(
407+
screenName: screenName,
408+
cameraSource: cameraSource,
409+
cameraEventKind: .runtimeError
410+
)
337411
metadata["error"] = AnalyticsClientV2.serialize(
338412
error: error,
339413
filePath: filePath,
@@ -342,15 +416,74 @@ final class IdentityAnalyticsClient {
342416
logAnalytic(.cameraError, metadata: metadata, verificationPage: try? sheetController.verificationPageResponse?.get())
343417
}
344418

345-
/// Logs either a permission denied or granted event when the camera permissions are checked prior to starting a camera session
419+
/// Logs a permission analytic when camera access is checked prior to starting a camera session
346420
func logCameraPermissionsChecked(
347421
sheetController: VerificationSheetControllerProtocol,
348-
isGranted: Bool?
422+
isGranted: Bool?,
423+
screenName: ScreenName,
424+
cameraSource: CameraSource,
425+
filePath: StaticString = #filePath,
426+
line: UInt = #line
349427
) {
350-
let eventName: EventName =
351-
(isGranted == true) ? .cameraPermissionGranted : .cameraPermissionDenied
428+
guard isGranted != true else {
429+
var metadata = cameraMetadata(
430+
screenName: screenName,
431+
cameraSource: cameraSource,
432+
cameraEventKind: .permission
433+
)
434+
metadata["camera_access_state"] = cameraAccessState(isGranted: isGranted)
435+
logAnalytic(
436+
.cameraPermissionGranted,
437+
metadata: metadata,
438+
verificationPage: try? sheetController.verificationPageResponse?.get()
439+
)
440+
return
441+
}
442+
logCameraPermissionDeniedOrUnknown(
443+
sheetController: sheetController,
444+
isGranted: isGranted,
445+
screenName: screenName,
446+
cameraSource: cameraSource,
447+
filePath: filePath,
448+
line: line
449+
)
450+
}
352451

353-
logAnalytic(eventName, metadata: [:], verificationPage: try? sheetController.verificationPageResponse?.get())
452+
/// Logs a permission analytic only when camera access is denied or unknown
453+
func logCameraPermissionDeniedOrUnknown(
454+
sheetController: VerificationSheetControllerProtocol,
455+
isGranted: Bool?,
456+
screenName: ScreenName,
457+
cameraSource: CameraSource,
458+
filePath: StaticString = #filePath,
459+
line: UInt = #line
460+
) {
461+
guard isGranted != true else {
462+
return
463+
}
464+
var metadata = cameraMetadata(
465+
screenName: screenName,
466+
cameraSource: cameraSource,
467+
cameraEventKind: .permission
468+
)
469+
metadata["camera_access_state"] = cameraAccessState(isGranted: isGranted)
470+
471+
logAnalytic(
472+
.cameraPermissionDenied,
473+
metadata: metadata,
474+
verificationPage: try? sheetController.verificationPageResponse?.get()
475+
)
476+
477+
metadata["error"] = AnalyticsClientV2.serialize(
478+
error: cameraPermissionError(isGranted: isGranted),
479+
filePath: filePath,
480+
line: line
481+
)
482+
logAnalytic(
483+
.cameraError,
484+
metadata: metadata,
485+
verificationPage: try? sheetController.verificationPageResponse?.get()
486+
)
354487
}
355488

356489
/// Logs an event when document capture times out
@@ -515,20 +648,44 @@ final class IdentityAnalyticsClient {
515648
/// Logs when an error occurs.
516649
func logGenericError(
517650
error: Error,
651+
additionalMetadata: [String: Any] = [:],
518652
filePath: StaticString = #filePath,
519653
line: UInt = #line,
520654
sheetController: VerificationSheetControllerProtocol
521655
) {
656+
var metadata = additionalMetadata
657+
metadata["error_details"] = AnalyticsClientV2.serialize(
658+
error: error,
659+
filePath: filePath,
660+
line: line
661+
)
522662
logAnalytic(
523663
.genericError,
524-
metadata: [
525-
"error_details": AnalyticsClientV2.serialize(
526-
error: error,
527-
filePath: filePath,
528-
line: line
529-
),
530-
],
664+
metadata: metadata,
531665
verificationPage: try? sheetController.verificationPageResponse?.get()
532666
)
533667
}
668+
669+
static func logUnscopedGenericError(
670+
_ error: Error,
671+
context: String,
672+
additionalMetadata: [String: Any] = [:],
673+
filePath: StaticString = #filePath,
674+
line: UInt = #line
675+
) {
676+
var eventMetadata = additionalMetadata
677+
eventMetadata["error_context"] = context
678+
eventMetadata["error_details"] = AnalyticsClientV2.serialize(
679+
error: error,
680+
filePath: filePath,
681+
line: line
682+
)
683+
684+
sharedAnalyticsClient.log(
685+
eventName: EventName.genericError.rawValue,
686+
parameters: [
687+
"event_metadata": eventMetadata,
688+
]
689+
)
690+
}
534691
}

StripeIdentity/StripeIdentity/Source/NativeComponents/Coordinators/ImageScanner/ImageScanningConcurrencyManager.swift

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,13 +61,19 @@ final class ImageScanningConcurrencyManager: ImageScanningConcurrencyManagerProt
6161
private var futureQueue: DispatchQueue = DispatchQueue(label: "com.stripe.identity.concurrent-image-scanner.futures")
6262

6363
private let analyticsClient: IdentityAnalyticsClient
64+
private let scannerName: IdentityAnalyticsClient.ScannerName
65+
private let screenName: IdentityAnalyticsClient.ScreenName
6466
private let sheetController: VerificationSheetControllerProtocol
6567

6668
init(
6769
sheetController: VerificationSheetControllerProtocol,
70+
scannerName: IdentityAnalyticsClient.ScannerName,
71+
screenName: IdentityAnalyticsClient.ScreenName,
6872
maxConcurrentScans: Int = kConcurrentImageScannerDefaultMaxConcurrentScans
6973
) {
7074
self.analyticsClient = sheetController.analyticsClient
75+
self.scannerName = scannerName
76+
self.screenName = screenName
7177
self.sheetController = sheetController
7278
self.semaphore = DispatchSemaphore(value: maxConcurrentScans)
7379
}
@@ -142,7 +148,15 @@ final class ImageScanningConcurrencyManager: ImageScanningConcurrencyManagerProt
142148
case .success(let scannerOutput):
143149
wrappedCompletion(scannerOutput)
144150
case .failure(let error):
145-
self.analyticsClient.logGenericError(error: error, sheetController: self.sheetController)
151+
self.analyticsClient.logGenericError(
152+
error: error,
153+
additionalMetadata: [
154+
"error_context": "image_scan",
155+
"scanner_name": self.scannerName.rawValue,
156+
"screen_name": self.screenName.rawValue,
157+
],
158+
sheetController: self.sheetController
159+
)
146160
}
147161

148162
// Track when the scan ended

0 commit comments

Comments
 (0)