Skip to content

Commit 2f3d2a5

Browse files
authored
feat(sam): improve run/debug error handling and telemetry (#2865)
## Problem Our SAM run/debug implementation has several problems with error handling: * Inconsistent error message UX (some are placed in the logs, some are shown directly to the user) * Telemetry only covers a portion of the relevant code paths and does not emit reasons for failure * Some errors are swallowed while others are bubbled up to the VSC API ## Solution * Bubble up all errors but use a single error handler to prevent the VSC error modal from showing * Added a button to open the launch config to preserve current UX * Use telemetry spans to cover almost the entire code path * Remove sam_attachDebugger metric as it is redundant. Failures to attach counts as a failure for the whole execution. * Add some basic error codes to better determine why a run/debug execution failed
1 parent f4522d8 commit 2f3d2a5

File tree

9 files changed

+329
-424
lines changed

9 files changed

+329
-424
lines changed

src/shared/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { Result } from './telemetry/telemetry'
88
import { CancellationError } from './utilities/timeoutUtils'
99
import { isNonNullable } from './utilities/tsUtils'
1010

11-
interface ErrorInformation {
11+
export interface ErrorInformation {
1212
/**
1313
* Error names are optional, but if provided they should be generic yet self-explanatory.
1414
*

src/shared/sam/cli/samCliLocalInvoke.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,7 @@ export class SamCliLocalInvokeInvocation {
214214
this.args.skipPullImage = !!this.args.skipPullImage
215215
}
216216

217-
public async execute(timeout?: Timeout): Promise<void> {
217+
public async execute(timeout?: Timeout): Promise<ChildProcess> {
218218
await this.validate()
219219

220220
const sam = await this.config.getOrDetectSamCli()
@@ -251,7 +251,7 @@ export class SamCliLocalInvokeInvocation {
251251
)
252252
invokeArgs.push(...(this.args.extraArgs ?? []))
253253

254-
await this.args.invoker.invoke({
254+
return await this.args.invoker.invoke({
255255
options: {
256256
env: {
257257
...process.env,

src/shared/sam/debugger/awsSamDebugger.ts

Lines changed: 137 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import * as semver from 'semver'
77
import * as vscode from 'vscode'
88
import * as fs from 'fs-extra'
99
import * as nls from 'vscode-nls'
10-
import { Runtime } from 'aws-sdk/clients/lambda'
1110
import {
1211
getCodeRoot,
1312
getHandlerName,
@@ -17,6 +16,7 @@ import {
1716
GoDebugConfiguration,
1817
getTemplate,
1918
getArchitecture,
19+
isImageLambdaConfig,
2020
} from '../../../lambda/local/debugConfiguration'
2121
import {
2222
Architecture,
@@ -53,10 +53,9 @@ import { makeInputTemplate, makeJsonFiles } from '../localLambdaRunner'
5353
import { SamLocalInvokeCommand } from '../cli/samCliLocalInvoke'
5454
import { getCredentialsFromStore } from '../../../credentials/credentialsStore'
5555
import { fromString } from '../../../credentials/providers/credentials'
56-
import { notifyUserInvalidCredentials } from '../../../credentials/credentialsUtilities'
5756
import { Credentials } from '@aws-sdk/types'
5857
import { CloudFormation } from '../../cloudformation/cloudformation'
59-
import { getSamCliVersion } from '../cli/samCliContext'
58+
import { getSamCliContext, getSamCliVersion } from '../cli/samCliContext'
6059
import {
6160
MINIMUM_SAM_CLI_VERSION_INCLUSIVE_FOR_IMAGE_SUPPORT,
6261
MINIMUM_SAM_CLI_VERSION_INCLUSIVE_FOR_GO_SUPPORT,
@@ -65,9 +64,53 @@ import { getIdeProperties, isCloud9 } from '../../extensionUtilities'
6564
import { resolve } from 'path'
6665
import globals from '../../extensionGlobals'
6766
import { LoginManager } from '../../../credentials/loginManager'
67+
import { Runtime, telemetry } from '../../telemetry/telemetry'
68+
import { ErrorInformation, isUserCancelledError, ToolkitError } from '../../errors'
69+
import { openLaunchJsonFile } from './commands/addSamDebugConfiguration'
70+
import { Logging } from '../../logger/commands'
71+
import { credentialHelpUrl } from '../../constants'
6872

6973
const localize = nls.loadMessageBundle()
7074

75+
interface NotificationButton<T = unknown> {
76+
readonly label: string
77+
readonly onClick: () => Promise<T> | T
78+
}
79+
80+
class SamLaunchRequestError extends ToolkitError.named('SamLaunchRequestError') {
81+
private readonly buttons: NotificationButton[]
82+
83+
public constructor(message: string, info?: ErrorInformation & { readonly extraButtons?: NotificationButton[] }) {
84+
super(message, info)
85+
this.buttons = info?.extraButtons ?? [
86+
{
87+
label: localize('AWS.generic.message.openConfig', 'Open Launch Config'),
88+
onClick: openLaunchJsonFile,
89+
},
90+
]
91+
}
92+
93+
public async showNotification(): Promise<void> {
94+
if (isUserCancelledError(this)) {
95+
getLogger().verbose(`SAM run/debug: user cancelled`)
96+
return
97+
}
98+
99+
const logId = getLogger().error(this.trace)
100+
101+
const viewLogsButton = {
102+
label: localize('AWS.generic.message.viewLogs', 'View Logs...'),
103+
onClick: () => Logging.declared.viewLogsAtMessage.execute(logId),
104+
}
105+
106+
const buttonsWithLogs = [viewLogsButton, ...this.buttons]
107+
108+
await vscode.window.showErrorMessage(this.message, ...buttonsWithLogs.map(b => b.label)).then(resp => {
109+
return buttonsWithLogs.find(({ label }) => label === resp)?.onClick()
110+
})
111+
}
112+
}
113+
71114
/**
72115
* SAM-specific launch attributes (which are not part of the DAP).
73116
*
@@ -310,12 +353,31 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
310353
folder: vscode.WorkspaceFolder | undefined,
311354
config: AwsSamDebuggerConfiguration,
312355
token?: vscode.CancellationToken
313-
): Promise<undefined> {
314-
const resolvedConfig = await this.makeConfig(folder, config, token)
315-
if (!resolvedConfig) {
316-
return undefined
356+
): Promise<void> {
357+
try {
358+
if (config.invokeTarget.target === 'api') {
359+
await telemetry.apigateway_invokeLocal.run(async span => {
360+
const resolved = await this.makeConfig(folder, config, token)
361+
span.record({ httpMethod: resolved.api?.httpMethod })
362+
363+
return this.invokeConfig(resolved)
364+
})
365+
} else {
366+
await telemetry.lambda_invokeLocal.run(async () => {
367+
const resolved = await this.makeConfig(folder, config, token)
368+
369+
return this.invokeConfig(resolved)
370+
})
371+
}
372+
} catch (err) {
373+
if (err instanceof SamLaunchRequestError) {
374+
err.showNotification()
375+
} else if (err instanceof ToolkitError) {
376+
new SamLaunchRequestError(err.message, { ...err }).showNotification()
377+
} else {
378+
SamLaunchRequestError.chain(err, 'Failed to run launch configuration').showNotification()
379+
}
317380
}
318-
await this.invokeConfig(resolvedConfig)
319381
}
320382

321383
/**
@@ -332,22 +394,21 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
332394
folder: vscode.WorkspaceFolder | undefined,
333395
config: AwsSamDebuggerConfiguration,
334396
token?: vscode.CancellationToken
335-
): Promise<SamLaunchRequestArgs | undefined> {
397+
): Promise<SamLaunchRequestArgs> {
336398
if (token?.isCancellationRequested) {
337-
return undefined
399+
throw new ToolkitError('Cancellation requested', { cancelled: true })
338400
}
401+
339402
folder =
340403
folder ?? (vscode.workspace.workspaceFolders?.length ? vscode.workspace.workspaceFolders[0] : undefined)
341404
if (!folder) {
342-
getLogger().error(`SAM debug: no workspace folder`)
343-
vscode.window.showErrorMessage(
344-
localize(
345-
'AWS.sam.debugger.noWorkspace',
346-
'{0} SAM debug: choose a workspace, then try again',
347-
getIdeProperties().company
348-
)
405+
const message = localize(
406+
'AWS.sam.debugger.noWorkspace',
407+
'Choose a workspace, then try again',
408+
getIdeProperties().company
349409
)
350-
return undefined
410+
411+
throw new SamLaunchRequestError(message, { code: 'NoWorkspaceFolder', extraButtons: [] })
351412
}
352413

353414
// If "request" field is missing this means launch.json does not exist.
@@ -356,28 +417,24 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
356417
const configValidator: AwsSamDebugConfigurationValidator = new DefaultAwsSamDebugConfigurationValidator(folder)
357418

358419
if (!hasLaunchJson) {
359-
vscode.window
360-
.showErrorMessage(
361-
localize(
362-
'AWS.sam.debugger.noLaunchJson',
363-
'{0} SAM: To debug a Lambda locally, create a launch.json from the Run panel, then select a configuration.',
364-
getIdeProperties().company
365-
),
366-
localize('AWS.gotoRunPanel', 'Run panel')
367-
)
368-
.then(async result => {
369-
if (!result) {
370-
return
371-
}
372-
await vscode.commands.executeCommand('workbench.view.debug')
373-
})
374-
return undefined
420+
const message = localize(
421+
'AWS.sam.debugger.noLaunchJson',
422+
'To debug a Lambda locally, create a launch.json from the Run panel, then select a configuration.'
423+
)
424+
425+
throw new SamLaunchRequestError(message, {
426+
code: 'NoLaunchConfig',
427+
extraButtons: [
428+
{
429+
label: localize('AWS.gotoRunPanel', 'Run panel'),
430+
onClick: () => vscode.commands.executeCommand('workbench.view.debug'),
431+
},
432+
],
433+
})
375434
} else {
376435
const rv = configValidator.validate(config)
377436
if (!rv.isValid) {
378-
getLogger().error(`SAM debug: invalid config: ${rv.message!}`)
379-
vscode.window.showErrorMessage(rv.message!)
380-
return undefined
437+
throw new ToolkitError(`Invalid launch configuration: ${rv.message}`, { code: 'BadLaunchConfig' })
381438
} else if (rv.message) {
382439
vscode.window.showInformationMessage(rv.message)
383440
}
@@ -433,27 +490,23 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
433490
if (!isZip) {
434491
const samCliVersion = await getSamCliVersion(this.ctx.samCliContext())
435492
if (semver.lt(samCliVersion, MINIMUM_SAM_CLI_VERSION_INCLUSIVE_FOR_IMAGE_SUPPORT)) {
436-
getLogger().error(`SAM debug: version (${samCliVersion}) too low for Image lambdas: ${config})`)
437-
vscode.window.showErrorMessage(
438-
localize(
439-
'AWS.output.sam.no.image.support',
440-
'Support for Image-based Lambdas requires a minimum SAM CLI version of 1.13.0.'
441-
)
493+
const message = localize(
494+
'AWS.output.sam.no.image.support',
495+
'Support for Image-based Lambdas requires a minimum SAM CLI version of 1.13.0.'
442496
)
443-
return undefined
497+
498+
throw new SamLaunchRequestError(message, { code: 'UnsupportedSamVersion', details: { samCliVersion } })
444499
}
445500
}
446501

447502
if (!runtime) {
448-
getLogger().error(`SAM debug: failed to launch config (missing runtime): ${config})`)
449-
vscode.window.showErrorMessage(
450-
localize(
451-
'AWS.sam.debugger.failedLaunch.missingRuntime',
452-
'Toolkit could not infer a runtime for config: {0}. Add a "lambda.runtime" field to your launch configuration.',
453-
config.name
454-
)
503+
const message = localize(
504+
'AWS.sam.debugger.failedLaunch.missingRuntime',
505+
'Toolkit could not infer a runtime for config: {0}. Add a "lambda.runtime" field to your launch configuration.',
506+
config.name
455507
)
456-
return undefined
508+
509+
throw new SamLaunchRequestError(message, { code: 'MissingRuntime' })
457510
}
458511

459512
// SAM CLI versions before 1.18.1 do not work correctly for Go debugging.
@@ -494,11 +547,22 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
494547
if (fromStore) {
495548
awsCredentials = fromStore
496549
} else {
497-
getLogger().error(
498-
`SAM debug: invalid "aws.credentials" value in launch config: ${config.aws.credentials}`
499-
)
500-
notifyUserInvalidCredentials(config.aws.credentials)
501-
return undefined
550+
const credentialsId = config.aws.credentials
551+
const getHelp = localize('AWS.generic.message.getHelp', 'Get Help...')
552+
// TODO: getHelp page for Cloud9.
553+
const extraButtons = isCloud9()
554+
? []
555+
: [
556+
{
557+
label: getHelp,
558+
onClick: () => vscode.env.openExternal(vscode.Uri.parse(credentialHelpUrl)),
559+
},
560+
]
561+
562+
throw new SamLaunchRequestError(`Invalid credentials found in launch configuration: ${credentialsId}`, {
563+
code: 'InvalidCredentials',
564+
extraButtons,
565+
})
502566
}
503567
}
504568

@@ -527,7 +591,7 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
527591
request: 'attach',
528592
codeRoot: codeRoot ?? '',
529593
workspaceFolder: folder,
530-
runtime: runtime,
594+
runtime: runtime as Runtime,
531595
runtimeFamily: runtimeFamily,
532596
handlerName: handlerName,
533597
documentUri: documentUri,
@@ -581,16 +645,13 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
581645
break
582646
}
583647
default: {
584-
getLogger().error(`SAM debug: unknown runtime: ${runtime})`)
585-
vscode.window.showErrorMessage(
586-
localize(
587-
'AWS.sam.debugger.invalidRuntime',
588-
'{0} SAM debug: unknown runtime: {1}',
589-
getIdeProperties().company,
590-
runtime
591-
)
648+
const message = localize(
649+
'AWS.sam.debugger.invalidRuntime',
650+
'Unknown or unsupported runtime: {0}',
651+
runtime
592652
)
593-
return undefined
653+
654+
throw new ToolkitError(message, { code: 'UnsupportedRuntime' })
594655
}
595656
}
596657
await makeJsonFiles(launchConfig)
@@ -618,6 +679,14 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
618679
* Performs the EXECUTE phase of SAM run/debug.
619680
*/
620681
public async invokeConfig(config: SamLaunchRequestArgs): Promise<SamLaunchRequestArgs> {
682+
telemetry.record({
683+
debug: !config.noDebug,
684+
runtime: config.runtime as Runtime,
685+
lambdaArchitecture: config.architecture,
686+
lambdaPackageType: isImageLambdaConfig(config) ? 'Image' : 'Zip',
687+
version: await getSamCliVersion(getSamCliContext()),
688+
})
689+
621690
await LoginManager.tryAutoConnect()
622691
switch (config.runtimeFamily) {
623692
case RuntimeFamily.NodeJS: {
@@ -642,7 +711,7 @@ export class SamDebugConfigProvider implements vscode.DebugConfigurationProvider
642711
return await javaDebug.invokeJavaLambda(this.ctx, config)
643712
}
644713
default: {
645-
throw Error(`unknown runtimeFamily: ${config.runtimeFamily}`)
714+
throw new Error(`unknown runtimeFamily: ${config.runtimeFamily}`)
646715
}
647716
}
648717
}

src/shared/sam/debugger/goSamDebug.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import { Timeout } from '../../utilities/timeoutUtils'
2121
import { SystemUtilities } from '../../../shared/systemUtilities'
2222
import { execFileSync, SpawnOptions } from 'child_process'
2323
import * as nls from 'vscode-nls'
24-
import { showViewLogsMessage } from '../../../shared/utilities/messages'
2524
import { sleep } from '../../utilities/timeoutUtils'
2625
import globals from '../../extensionGlobals'
26+
import { ToolkitError } from '../../errors'
2727
const localize = nls.loadMessageBundle()
2828

2929
/**
@@ -56,12 +56,12 @@ export async function invokeGoLambda(ctx: ExtContext, config: GoDebugConfigurati
5656
config.onWillAttachDebugger = waitForDelve
5757

5858
if (!config.noDebug && !(await installDebugger(config.debuggerPath!))) {
59-
showViewLogsMessage(
60-
localize('AWS.sam.debugger.godelve.failed', 'Failed to install Delve for the lambda container.')
59+
throw new ToolkitError(
60+
localize('AWS.sam.debugger.godelve.failed', 'Failed to install Delve for the lambda container.'),
61+
{
62+
code: 'NoDelveInstallation',
63+
}
6164
)
62-
63-
// Terminates the debug session by sending up a dummy config
64-
return {} as GoDebugConfiguration
6565
}
6666

6767
const c = (await runLambdaFunction(ctx, config, async () => {})) as GoDebugConfiguration

0 commit comments

Comments
 (0)