Skip to content

Commit 1a733fe

Browse files
authored
telemetry(lambda): Send meaningful telemetry error codes for SAM CLI invocation failures (#6042)
## Problem With only Unknown errors, It's hard to differentiate errors or make possible improvements ## Solution - Parse SAM CLI output - Add telemetry code based on the output error --- <!--- REMINDER: Ensure that your PR meets the guidelines in CONTRIBUTING.md --> License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 5f012c8 commit 1a733fe

File tree

6 files changed

+94
-14
lines changed

6 files changed

+94
-14
lines changed

packages/core/src/shared/sam/build.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import globals from '../extensionGlobals'
1919
import { TreeNode } from '../treeview/resourceTreeDataProvider'
2020
import { telemetry } from '../telemetry/telemetry'
2121
import { getSpawnEnv } from '../env/resolveEnv'
22-
import { getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime } from './utils'
22+
import { getErrorCode, getProjectRoot, getSamCliPathAndVersion, isDotnetRuntime } from './utils'
2323
import { getConfigFileUri, validateSamBuildConfig } from './config'
2424
import { runInTerminal } from './processTerminal'
2525

@@ -236,6 +236,7 @@ export async function runBuild(arg?: TreeNode): Promise<SamBuildResult> {
236236
} catch (error) {
237237
throw ToolkitError.chain(error, 'Failed to build SAM template', {
238238
details: { ...resolveBuildArgConflict(buildFlags) },
239+
code: getErrorCode(error),
239240
})
240241
}
241242
}

packages/core/src/shared/sam/deploy.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { Wizard } from '../wizards/wizard'
2424
import { addTelemetryEnvVar } from './cli/samCliInvokerUtils'
2525
import { validateSamDeployConfig, SamConfig, writeSamconfigGlobal } from './config'
2626
import { TemplateItem, createStackPrompter, createBucketPrompter, createTemplatePrompter } from './sync'
27-
import { getProjectRoot, getSamCliPathAndVersion, getSource } from './utils'
27+
import { getErrorCode, getProjectRoot, getSamCliPathAndVersion, getSource } from './utils'
2828
import { runInTerminal } from './processTerminal'
2929

3030
export interface DeployParams {
@@ -392,7 +392,10 @@ export async function runDeploy(arg: any, wizardParams?: DeployParams): Promise<
392392
throw error
393393
}
394394
} catch (error) {
395-
throw ToolkitError.chain(error, 'Failed to deploy SAM template', { details: { ...deployFlags } })
395+
throw ToolkitError.chain(error, 'Failed to deploy SAM template', {
396+
details: { ...deployFlags },
397+
code: getErrorCode(error),
398+
})
396399
}
397400
return {
398401
isSuccess: true,

packages/core/src/shared/sam/processTerminal.ts

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,17 @@ import { CancellationError } from '../utilities/timeoutUtils'
1212
import { getLogger } from '../logger'
1313
import { removeAnsi } from '../utilities/textUtilities'
1414
import { isAutomation } from '../vscode/env'
15+
import { throwIfErrorMatches } from './utils'
1516

1617
let oldTerminal: ProcessTerminal | undefined
1718
export async function runInTerminal(proc: ChildProcess, cmd: string) {
1819
const handleResult = (result?: ChildProcessResult) => {
1920
if (result && result.exitCode !== 0) {
20-
const message = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}`
21-
if (result.stderr.includes('is up to date')) {
22-
throw ToolkitError.chain(result.error, message, {
23-
code: 'NoUpdateExitCode',
24-
})
25-
}
26-
throw ToolkitError.chain(result.error, message, {
21+
throwIfErrorMatches(result)
22+
23+
// If no specific error matched, throw the default non-zero exit code error.
24+
const defaultMessage = `sam ${cmd} exited with a non-zero exit code: ${result.exitCode}`
25+
throw ToolkitError.chain(result.error, defaultMessage, {
2726
code: 'NonZeroExitCode',
2827
})
2928
}

packages/core/src/shared/sam/sync.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import { IamConnection } from '../../auth/connection'
4141
import { CloudFormationTemplateRegistry } from '../fs/templateRegistry'
4242
import { TreeNode } from '../treeview/resourceTreeDataProvider'
4343
import { getSpawnEnv } from '../env/resolveEnv'
44-
import { getProjectRoot, getProjectRootUri, getSamCliPathAndVersion, getSource } from './utils'
44+
import { getErrorCode, getProjectRoot, getProjectRootUri, getSamCliPathAndVersion, getSource } from './utils'
4545
import { runInTerminal } from './processTerminal'
4646

4747
const localize = nls.loadMessageBundle()
@@ -661,7 +661,10 @@ export async function runSync(
661661
isSuccess: true,
662662
}
663663
} catch (err) {
664-
throw ToolkitError.chain(err, 'Failed to sync SAM application', { details: { ...params } })
664+
throw ToolkitError.chain(err, 'Failed to sync SAM application', {
665+
details: { ...params },
666+
code: getErrorCode(err),
667+
})
665668
}
666669
})
667670
}

packages/core/src/shared/sam/utils.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { ToolkitError } from '../errors'
1515
import { SamCliSettings } from './cli/samCliSettings'
1616
import { SamCliInfoInvocation } from './cli/samCliInfo'
1717
import { parse } from 'semver'
18+
import { ChildProcessResult } from '../utilities/processUtils'
1819

1920
/**
2021
* @description determines the root directory of the project given Template Item
@@ -83,3 +84,36 @@ export async function getSamCliPathAndVersion() {
8384

8485
return { path: samCliPath, parsedVersion }
8586
}
87+
88+
export function getSamCliErrorMessage(stderr: string): string {
89+
// Split the stderr string by newline, filter out empty lines, and get the last line
90+
const lines = stderr
91+
.trim()
92+
.split('\n')
93+
.filter((line) => line.trim() !== '')
94+
return lines[lines.length - 1]
95+
}
96+
97+
export function getErrorCode(error: unknown): string | undefined {
98+
return error instanceof ToolkitError ? error.code : undefined
99+
}
100+
101+
export function throwIfErrorMatches(result: ChildProcessResult) {
102+
const errorMessage = getSamCliErrorMessage(result.stderr)
103+
for (const errorType in SamCliErrorTypes) {
104+
if (errorMessage.includes(SamCliErrorTypes[errorType as keyof typeof SamCliErrorTypes])) {
105+
throw ToolkitError.chain(result.error, errorMessage, {
106+
code: errorType,
107+
})
108+
}
109+
}
110+
}
111+
112+
export enum SamCliErrorTypes {
113+
DockerUnreachable = 'Docker is unreachable.',
114+
ResolveS3AndS3Set = 'Cannot use both --resolve-s3 and --s3-bucket parameters in non-guided deployments.',
115+
DeployStackStatusMissing = 'Was not able to find a stack with the name:',
116+
DeployStackOutPutFailed = 'Failed to get outputs from stack',
117+
DeployBucketRequired = 'Templates with a size greater than 51,200 bytes must be deployed via an S3 Bucket.',
118+
ChangeSetEmpty = 'is up to date',
119+
}

packages/core/src/test/shared/sam/utils.test.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,20 @@
66
import * as vscode from 'vscode'
77
import assert from 'assert'
88
import sinon from 'sinon'
9-
import { getProjectRootUri, getProjectRoot, getSource, isDotnetRuntime } from '../../../shared/sam/utils'
9+
import {
10+
getProjectRootUri,
11+
getProjectRoot,
12+
getSource,
13+
isDotnetRuntime,
14+
getSamCliErrorMessage,
15+
throwIfErrorMatches,
16+
} from '../../../shared/sam/utils'
1017
import { TemplateItem } from '../../../shared/sam/sync'
1118
import { RegionNode } from '../../../awsexplorer/regionNode'
1219
import { Region } from '../../../shared/regions/endpoints'
13-
import { RegionProvider } from '../../../shared'
20+
import { RegionProvider, ToolkitError } from '../../../shared'
1421
import { DeployedResource, DeployedResourceNode } from '../../../awsService/appBuilder/explorer/nodes/deployedNode'
22+
import { ChildProcessResult } from '../../../shared/utilities/processUtils'
1523

1624
describe('SAM utils', async function () {
1725
it('returns the projectRoot', async function () {
@@ -148,4 +156,36 @@ describe('SAM utils', async function () {
148156
})
149157
})
150158
})
159+
160+
describe('gets the SAM CLI error from stderr', async function () {
161+
it('returns the error message', async function () {
162+
const stderr =
163+
'Starting Build use cache\nStarting Build inside a container\nCache is invalid, running build and copying resources for following functions (ResizerFunction)\nBuilding codeuri: /Users/mbfreder/TestApp/JavaSamApp/serverless-patterns/s3lambda-resizing-python/src runtime: python3.12 metadata: {} architecture: x86_64 functions: ResizerFunction\nError: Docker is unreachable. Docker needs to be running to build inside a container.'
164+
const response = getSamCliErrorMessage(stderr)
165+
assert.deepStrictEqual(
166+
response,
167+
'Error: Docker is unreachable. Docker needs to be running to build inside a container.'
168+
)
169+
})
170+
})
171+
172+
describe('throwIfErrorMatches', async function () {
173+
it('should throw a ToolkitError with the correct code when an error message matches', () => {
174+
const mockError = new Error('Mock Error')
175+
const mockResult: ChildProcessResult = {
176+
exitCode: 1,
177+
error: mockError,
178+
stdout: '',
179+
stderr: 'Docker is unreachable.',
180+
}
181+
assert.throws(
182+
() => throwIfErrorMatches(mockResult),
183+
(e: any) => {
184+
assert.strictEqual(e instanceof ToolkitError, true)
185+
assert.strictEqual(e.code, 'DockerUnreachable')
186+
return true
187+
}
188+
)
189+
})
190+
})
151191
})

0 commit comments

Comments
 (0)