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