@@ -3,7 +3,7 @@ import { AWSError } from 'aws-sdk'
33import { distance } from 'fastest-levenshtein'
44import { Suggestion } from './codeWhispererService'
55import { CodewhispererCompletionType } from './telemetry/types'
6- import { BUILDER_ID_START_URL , MISSING_BEARER_TOKEN_ERROR } from './constants'
6+ import { BUILDER_ID_START_URL , crashMonitoringDirName , driveLetterRegex , MISSING_BEARER_TOKEN_ERROR } from './constants'
77export type SsoConnectionType = 'builderId' | 'identityCenter' | 'none'
88
99export function isAwsError ( error : unknown ) : error is AWSError {
@@ -14,6 +14,168 @@ export function isAwsError(error: unknown): error is AWSError {
1414 return error instanceof Error && hasCode ( error ) && hasTime ( error )
1515}
1616
17+ /**
18+ * Returns the identifier the given error.
19+ * Depending on the implementation, the identifier may exist on a
20+ * different property.
21+ */
22+ export function getErrorId ( error : Error ) : string {
23+ // prioritize code over the name
24+ return hasCode ( error ) ? error . code : error . name
25+ }
26+
27+ /**
28+ * Derives an error message from the given error object.
29+ * Depending on the Error, the property used to derive the message can vary.
30+ *
31+ * @param withCause Append the message(s) from the cause chain, recursively.
32+ * The message(s) are delimited by ' | '. Eg: msg1 | causeMsg1 | causeMsg2
33+ */
34+ export function getErrorMsg ( err : Error | undefined , withCause : boolean = false ) : string | undefined {
35+ if ( err === undefined ) {
36+ return undefined
37+ }
38+
39+ // Non-standard SDK fields added by the OIDC service, to conform to the OAuth spec
40+ // (https://datatracker.ietf.org/doc/html/rfc6749#section-4.1.2.1) :
41+ // - error: code per the OAuth spec
42+ // - error_description: improved error message provided by OIDC service. Prefer this to
43+ // `message` if present.
44+ // https://github.com/aws/aws-toolkit-jetbrains/commit/cc9ed87fa9391dd39ac05cbf99b4437112fa3d10
45+ // - error_uri: not provided by OIDC currently?
46+ //
47+ // Example:
48+ //
49+ // [error] API response (oidc.us-east-1.amazonaws.com /token): {
50+ // name: 'InvalidGrantException',
51+ // '$fault': 'client',
52+ // '$metadata': {
53+ // httpStatusCode: 400,
54+ // requestId: '7f5af448-5af7-45f2-8e47-5808deaea4ab',
55+ // extendedRequestId: undefined,
56+ // cfId: undefined
57+ // },
58+ // error: 'invalid_grant',
59+ // error_description: 'Invalid refresh token provided',
60+ // message: 'UnknownError'
61+ // }
62+ const anyDesc = ( err as any ) . error_description
63+ const errDesc = typeof anyDesc === 'string' ? anyDesc . trim ( ) : ''
64+ let msg = errDesc !== '' ? errDesc : err . message ?. trim ( )
65+
66+ if ( typeof msg !== 'string' ) {
67+ return undefined
68+ }
69+
70+ // append the cause's message
71+ if ( withCause ) {
72+ const errorId = getErrorId ( err )
73+ // - prepend id to message
74+ // - If a generic error does not have the `name` field explicitly set, it returns a generic 'Error' name. So skip since it is useless.
75+ if ( errorId && errorId !== 'Error' ) {
76+ msg = `${ errorId } : ${ msg } `
77+ }
78+
79+ const cause = ( err as any ) . cause
80+ return `${ msg } ${ cause ? ' | ' + getErrorMsg ( cause , withCause ) : '' } `
81+ }
82+
83+ return msg
84+ }
85+
86+ /**
87+ * Removes potential PII from a string, for logging/telemetry.
88+ *
89+ * Examples:
90+ * - "Failed to save c:/fooß/bar/baz.txt" => "Failed to save c:/xß/x/x.txt"
91+ * - "EPERM for dir c:/Users/user1/.aws/sso/cache/abc123.json" => "EPERM for dir c:/Users/x/.aws/sso/cache/x.json"
92+ */
93+ export function scrubNames ( s : string , username ?: string ) {
94+ let r = ''
95+ const fileExtRe = / \. [ ^ . \/ ] + $ /
96+ const slashdot = / ^ [ ~ . ] * [ \/ \\ ] * /
97+
98+ /** Allowlisted filepath segments. */
99+ const keep = new Set < string > ( [
100+ '~' ,
101+ '.' ,
102+ '..' ,
103+ '.aws' ,
104+ 'aws' ,
105+ 'sso' ,
106+ 'cache' ,
107+ 'credentials' ,
108+ 'config' ,
109+ 'Users' ,
110+ 'users' ,
111+ 'home' ,
112+ 'tmp' ,
113+ 'aws-toolkit-vscode' ,
114+ 'globalStorage' , // from vscode globalStorageUri
115+ crashMonitoringDirName ,
116+ ] )
117+
118+ if ( username && username . length > 2 ) {
119+ s = s . replaceAll ( username , 'x' )
120+ }
121+
122+ // Replace contiguous whitespace with 1 space.
123+ s = s . replace ( / \s + / g, ' ' )
124+
125+ // 1. split on whitespace.
126+ // 2. scrub words that match username or look like filepaths.
127+ const words = s . split ( / \s + / )
128+ for ( const word of words ) {
129+ const pathSegments = word . split ( / [ \/ \\ ] / )
130+ if ( pathSegments . length < 2 ) {
131+ // Not a filepath.
132+ r += ' ' + word
133+ continue
134+ }
135+
136+ // Replace all (non-allowlisted) ASCII filepath segments with "x".
137+ // "/foo/bar/aws/sso/" => "/x/x/aws/sso/"
138+ let scrubbed = ''
139+ // Get the frontmatter ("/", "../", "~/", or "./").
140+ const start = word . trimStart ( ) . match ( slashdot ) ?. [ 0 ] ?? ''
141+ pathSegments [ 0 ] = pathSegments [ 0 ] . trimStart ( ) . replace ( slashdot , '' )
142+ for ( const seg of pathSegments ) {
143+ if ( driveLetterRegex . test ( seg ) ) {
144+ scrubbed += seg
145+ } else if ( keep . has ( seg ) ) {
146+ scrubbed += '/' + seg
147+ } else {
148+ // Save the first non-ASCII (unicode) char, if any.
149+ const nonAscii = seg . match ( / [ ^ \p{ ASCII} ] / u) ?. [ 0 ] ?? ''
150+ // Replace all chars (except [^…]) with "x" .
151+ const ascii = seg . replace ( / [ ^ $ [ \] ( ) { } : ; ' " ] + / g, 'x' )
152+ scrubbed += `/${ ascii } ${ nonAscii } `
153+ }
154+ }
155+
156+ // includes leading '.', eg: '.json'
157+ const fileExt = pathSegments [ pathSegments . length - 1 ] . match ( fileExtRe ) ?? ''
158+ r += ` ${ start . replace ( / \\ / g, '/' ) } ${ scrubbed . replace ( / ^ [ \/ \\ ] + / , '' ) } ${ fileExt } `
159+ }
160+
161+ return r . trim ( )
162+ }
163+
164+ // Port of implementation in AWS Toolkit for VSCode
165+ // https://github.com/aws/aws-toolkit-vscode/blob/c22efa03e73b241564c8051c35761eb8620edb83/packages/core/src/shared/errors.ts#L455
166+ /**
167+ * Gets the (partial) error message detail for the `reasonDesc` field.
168+ *
169+ * @param err Error object, or message text
170+ */
171+ export function getTelemetryReasonDesc ( err : unknown | undefined ) : string | undefined {
172+ const m = typeof err === 'string' ? err : ( getErrorMsg ( err as Error , true ) ?? '' )
173+ const msg = scrubNames ( m )
174+
175+ // Truncate message as these strings can be very long.
176+ return msg && msg . length > 0 ? msg . substring ( 0 , 350 ) : undefined
177+ }
178+
17179function hasCode < T > ( error : T ) : error is T & { code : string } {
18180 return typeof ( error as { code ?: unknown } ) . code === 'string'
19181}
0 commit comments