@@ -3,7 +3,7 @@ import { AWSError } from 'aws-sdk'
3
3
import { distance } from 'fastest-levenshtein'
4
4
import { Suggestion } from './codeWhispererService'
5
5
import { 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'
7
7
export type SsoConnectionType = 'builderId' | 'identityCenter' | 'none'
8
8
9
9
export function isAwsError ( error : unknown ) : error is AWSError {
@@ -14,6 +14,175 @@ export function isAwsError(error: unknown): error is AWSError {
14
14
return error instanceof Error && hasCode ( error ) && hasTime ( error )
15
15
}
16
16
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
+
17
186
function hasCode < T > ( error : T ) : error is T & { code : string } {
18
187
return typeof ( error as { code ?: unknown } ) . code === 'string'
19
188
}
0 commit comments