Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add usage data metrics for sandbox #642

Merged
merged 28 commits into from
Nov 16, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
b3c410c
feat: Add usage data metrics for sandbox
Amplifiyer Nov 14, 2023
35afe23
Merge branch 'telemetry_2' into telemetry_new
Amplifiyer Nov 14, 2023
09a90da
update api
Amplifiyer Nov 14, 2023
8f086ec
add more tests
Amplifiyer Nov 14, 2023
19be2bc
Merge branch 'main' into telemetry_new
Amplifiyer Nov 14, 2023
47a8c4e
add changeset
Amplifiyer Nov 14, 2023
b86d42d
fix automated tests
Amplifiyer Nov 14, 2023
023297d
Refactor packageJsonReader
Amplifiyer Nov 15, 2023
ffc1b1b
update tsconfig
Amplifiyer Nov 15, 2023
8437715
add comments
Amplifiyer Nov 15, 2023
3f5b1cf
Add usage data emitter factory
Amplifiyer Nov 15, 2023
89b09ad
Update interface of usage metrics to take metrics and dimensions
Amplifiyer Nov 15, 2023
96414ec
is it better?
Amplifiyer Nov 15, 2023
9ce7681
remove extra variable
Amplifiyer Nov 15, 2023
ae1a81f
changing to 'on' data listener
Amplifiyer Nov 15, 2023
905027e
Merge branch 'main' into telemetry_new
Amplifiyer Nov 15, 2023
fd0f1f7
update snapshots
Amplifiyer Nov 15, 2023
3d870ce
minor changes
Amplifiyer Nov 15, 2023
66a7ca8
change to use readline
Amplifiyer Nov 15, 2023
4149515
PR updates
Amplifiyer Nov 15, 2023
c60d63f
Move the uuid types dep
Amplifiyer Nov 15, 2023
8614266
PR updates
Amplifiyer Nov 15, 2023
5ff37f2
Merge branch 'main' into telemetry_new
Amplifiyer Nov 15, 2023
5780a85
update to use __dirname
Amplifiyer Nov 16, 2023
d894d12
update snapshots
Amplifiyer Nov 16, 2023
4288f53
Replace package json reeader in create-amplify
Amplifiyer Nov 16, 2023
95e11a0
small rename
Amplifiyer Nov 16, 2023
442ffc5
remove packageJson.name from installationId
Amplifiyer Nov 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/nervous-emus-deny.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@aws-amplify/backend-output-storage': patch
'@aws-amplify/integration-tests': patch
'@aws-amplify/backend-deployer': patch
'create-amplify': patch
'@aws-amplify/platform-core': patch
'@aws-amplify/sandbox': patch
'@aws-amplify/backend-cli': patch
---

Add usage data metrics
4 changes: 4 additions & 0 deletions .eslint_dictionary.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"deployer",
"disambiguator",
"downlevel",
"durations",
"dynamodb",
"ecma",
"enum",
Expand All @@ -46,6 +47,7 @@
"graphql",
"homedir",
"hotswap",
"hotswapped",
"iamv2",
"identitypool",
"idps",
Expand Down Expand Up @@ -92,6 +94,8 @@
"schema",
"schemas",
"searchable",
"semver",
"serializable",
"shopify",
"shortstat",
"sigint",
Expand Down
2,526 changes: 1,446 additions & 1,080 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 18 additions & 2 deletions packages/backend-deployer/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,27 +9,43 @@ import { DeploymentType } from '@aws-amplify/plugin-types';

// @public
export type BackendDeployer = {
deploy: (backendId?: BackendIdentifier, deployProps?: DeployProps) => Promise<void>;
destroy: (backendId?: BackendIdentifier, destroyProps?: DestroyProps) => Promise<void>;
deploy: (backendId?: BackendIdentifier, deployProps?: DeployProps) => Promise<DeployResult>;
destroy: (backendId?: BackendIdentifier, destroyProps?: DestroyProps) => Promise<DestroyResult>;
};

// @public
export class BackendDeployerFactory {
static getInstance: () => BackendDeployer;
}

// @public (undocumented)
export type DeploymentTimes = {
synthesisTime?: number;
totalTime?: number;
};

// @public (undocumented)
export type DeployProps = {
deploymentType?: DeploymentType;
secretLastUpdated?: Date;
validateAppSources?: boolean;
};

// @public (undocumented)
export type DeployResult = {
deploymentTimes: DeploymentTimes;
};

// @public (undocumented)
export type DestroyProps = {
deploymentType?: DeploymentType;
};

// @public (undocumented)
export type DestroyResult = {
deploymentTimes: DeploymentTimes;
};

// (No @packageDocumentation comment for this package)

```
58 changes: 48 additions & 10 deletions packages/backend-deployer/src/cdk_deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ import stream from 'stream';
import {
BackendDeployer,
DeployProps,
DeployResult,
DestroyProps,
DestroyResult,
} from './cdk_deployer_singleton_factory.js';
import { CdkErrorMapper } from './cdk_error_mapper.js';
import { BackendIdentifier, DeploymentType } from '@aws-amplify/plugin-types';
Expand Down Expand Up @@ -47,7 +49,7 @@ export class CDKDeployer implements BackendDeployer {
}
}

await this.invokeCdk(
return this.invokeCdk(
InvokableCommand.DEPLOY,
backendId,
deployProps?.deploymentType,
Expand All @@ -62,7 +64,7 @@ export class CDKDeployer implements BackendDeployer {
backendId?: BackendIdentifier,
destroyProps?: DestroyProps
) => {
await this.invokeCdk(
return this.invokeCdk(
InvokableCommand.DESTROY,
backendId,
destroyProps?.deploymentType,
Expand Down Expand Up @@ -104,7 +106,7 @@ export class CDKDeployer implements BackendDeployer {
backendId?: BackendIdentifier,
deploymentType?: DeploymentType,
additionalArguments?: string[]
) => {
): Promise<DeployResult | DestroyResult> => {
// Basic args
const cdkCommandArgs = [
'cdk',
Expand Down Expand Up @@ -144,8 +146,15 @@ export class CDKDeployer implements BackendDeployer {
cdkCommandArgs.push(...additionalArguments);
}

const cdkOutput = { deploymentTimes: {} };

try {
await this.executeChildProcess('npx', cdkCommandArgs);
await this.executeChildProcess(
'npx',
cdkCommandArgs,
this.listenStdoutAndPopulateCDKOutput(cdkOutput)
);
return cdkOutput;
} catch (err) {
throw this.cdkErrorMapper.getHumanReadableError(err as Error);
}
Expand All @@ -155,29 +164,58 @@ export class CDKDeployer implements BackendDeployer {
* Wrapper for the child process executor. Helps in unit testing as node:test framework
* doesn't have capabilities to mock exported functions like `execa` as of right now.
*/
executeChildProcess = async (command: string, cdkCommandArgs: string[]) => {
executeChildProcess = async (
command: string,
cdkCommandArgs: string[],
stdoutListener?: (chunk: string) => void
) => {
// We let the stdout and stdin inherit and streamed to parent process but pipe
// the stderr and use it to throw on failure. This is to prevent actual
// actionable errors being hidden among the stdout. Moreover execa errors are
// useless when calling CLIs unless you made execa calling error.
let aggregatedStderr = '';
const aggregatorStream = new stream.Writable();
aggregatorStream._write = function (chunk, encoding, done) {
const aggregatorStderrStream = new stream.Writable();
aggregatorStderrStream._write = function (chunk, encoding, done) {
Comment on lines +167 to +168
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize that aggregating output is existing thing.
Mind logging GH issue to make this part "streaming and parsing errors" ?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will log it, this would be a little harder since we are actually throwing and processing elsewhere.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aggregatedStderr += chunk;
done();
};
const childProcess = execa(command, cdkCommandArgs, {
stdin: 'inherit',
stdout: 'inherit',
stdout: 'pipe',
stderr: 'pipe',
});
childProcess.stderr?.pipe(aggregatorStream);

childProcess.stderr?.pipe(aggregatorStderrStream);
if (stdoutListener) {
childProcess.stdout?.on('data', stdoutListener);
}
childProcess.stdout?.pipe(process.stdout);
Amplifiyer marked this conversation as resolved.
Show resolved Hide resolved
try {
await childProcess;
} catch (error) {
// swallow execa error which is not really helpful, rather throw stderr
throw new Error(aggregatedStderr);
}
};

private listenStdoutAndPopulateCDKOutput = (
output: DeployResult | DestroyResult
) => {
const regexTotalTime = /✨ {2}Total time: (\d*\.*\d*)s.*/;
const regexSynthTime = /✨ {2}Synthesis time: (\d*\.*\d*)s/;
const listener = (chunk: string) => {
const data = Buffer.from(chunk).toString();
if (data.includes('✨')) {
// Good chance that it contains timing information
const totalTime = data.match(regexTotalTime);
Fixed Show fixed Hide fixed
if (totalTime && totalTime.length > 1 && !isNaN(+totalTime[1])) {
output.deploymentTimes.totalTime = +totalTime[1];
}
const synthTime = data.match(regexSynthTime);
Fixed Show fixed Hide fixed
if (synthTime && synthTime.length > 1 && !isNaN(+synthTime[1])) {
output.deploymentTimes.synthesisTime = +synthTime[1];
}
}
};
return listener;
};
}
17 changes: 15 additions & 2 deletions packages/backend-deployer/src/cdk_deployer_singleton_factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,35 @@ export type DeployProps = {
validateAppSources?: boolean;
};

export type DeployResult = {
deploymentTimes: DeploymentTimes;
};

export type DestroyProps = {
deploymentType?: DeploymentType;
};

export type DestroyResult = {
deploymentTimes: DeploymentTimes;
};

export type DeploymentTimes = {
synthesisTime?: number;
totalTime?: number;
};

/**
* Invokes an invokable command
*/
export type BackendDeployer = {
deploy: (
backendId?: BackendIdentifier,
deployProps?: DeployProps
) => Promise<void>;
) => Promise<DeployResult>;
destroy: (
backendId?: BackendIdentifier,
destroyProps?: DestroyProps
) => Promise<void>;
) => Promise<DestroyResult>;
};

/**
Expand Down
61 changes: 38 additions & 23 deletions packages/backend-deployer/src/cdk_error_mapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,61 +5,76 @@ import { CdkErrorMapper } from './cdk_error_mapper.js';
const testErrorMappings = [
{
errorMessage: 'UnknownError',
expectedString: 'UnknownError',
expectedTopLevelErrorMessage: 'UnknownError',
expectedDownstreamErrorMessage: undefined,
},
{
errorMessage: 'ExpiredToken',
expectedString:
expectedTopLevelErrorMessage:
'[ExpiredToken]: The security token included in the request is invalid.',
expectedDownstreamErrorMessage: 'ExpiredToken',
},
{
errorMessage: 'Access Denied',
expectedString:
expectedTopLevelErrorMessage:
'[AccessDenied]: The deployment role does not have sufficient permissions to perform this deployment.',
expectedDownstreamErrorMessage: 'Access Denied',
},
{
errorMessage: 'Has the environment been bootstrapped',
expectedString:
expectedTopLevelErrorMessage:
'[BootstrapFailure]: This AWS account and region has not been bootstrapped. Run `cdk bootstrap aws://{YOUR_ACCOUNT_ID}/{YOUR_REGION}` locally to resolve this.',
expectedDownstreamErrorMessage: 'Has the environment been bootstrapped',
},
{
errorMessage: 'amplify/backend.ts',
expectedString:
expectedTopLevelErrorMessage:
'[SynthError]: Unable to build Amplify backend. Check your backend definition in the `amplify` folder.',
expectedDownstreamErrorMessage: 'amplify/backend.ts',
},
{
errorMessage: 'ROLLBACK_COMPLETE',
expectedString:
'[CloudFormationFailure]: The CloudFormation deployment has failed. Find more information in the CloudFormation AWS Console for this stack.',
},
{
errorMessage: 'ROLLBACK_FAILED',
expectedString:
errorMessage: '❌ Deployment failed: something bad happened\n',
expectedTopLevelErrorMessage:
'[CloudFormationFailure]: The CloudFormation deployment has failed. Find more information in the CloudFormation AWS Console for this stack.',
expectedDownstreamErrorMessage: 'something bad happened',
},
{
errorMessage:
'CFN error happened: Updates are not allowed for property: some property',
expectedString:
expectedTopLevelErrorMessage:
'[UpdateNotSupported]: The changes that you are trying to apply are not supported.',
expectedDownstreamErrorMessage:
'CFN error happened: Updates are not allowed for property: some property',
},
{
errorMessage:
'CFN error happened: Invalid AttributeDataType input, consider using the provided AttributeDataType enum',
expectedString:
expectedTopLevelErrorMessage:
'[UpdateNotSupported]: User pool attributes cannot be changed after a user pool has been created.',
expectedDownstreamErrorMessage:
'CFN error happened: Invalid AttributeDataType input, consider using the provided AttributeDataType enum',
},
];

void describe('invokeCDKCommand', { concurrency: 1 }, () => {
const cdkErrorMapper = new CdkErrorMapper();
testErrorMappings.forEach(({ errorMessage, expectedString }) => {
void it(`handles ${errorMessage} error`, () => {
const humanReadableError = cdkErrorMapper.getHumanReadableError(
new Error(errorMessage)
);
assert.equal(humanReadableError.message, expectedString);
assert.equal((humanReadableError.cause as Error).message, errorMessage);
});
});
testErrorMappings.forEach(
({
errorMessage,
expectedTopLevelErrorMessage,
expectedDownstreamErrorMessage,
}) => {
void it(`handles ${errorMessage} error`, () => {
const humanReadableError = cdkErrorMapper.getHumanReadableError(
new Error(errorMessage)
);
assert.equal(humanReadableError.message, expectedTopLevelErrorMessage);
expectedDownstreamErrorMessage &&
assert.equal(
(humanReadableError.cause as Error).message,
expectedDownstreamErrorMessage
);
});
}
);
});
21 changes: 16 additions & 5 deletions packages/backend-deployer/src/cdk_error_mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,15 @@ export class CdkErrorMapper {
},
{
// the backend entry point file is referenced in the stack indicating a problem in customer code
errorRegex: /amplify\/backend.ts/,
errorRegex: /amplify\/backend/,
humanReadableError:
'[SynthError]: Unable to build Amplify backend. Check your backend definition in the `amplify` folder.',
},
{
errorRegex: /SyntaxError:(.*)\n/,
humanReadableError:
'[SyntaxError]: Unable to build Amplify backend. Check your backend definition in the `amplify` folder.',
},
{
errorRegex: /Updates are not allowed for property/,
humanReadableError:
Expand All @@ -43,7 +48,7 @@ export class CdkErrorMapper {
},
{
// Note that the order matters, this should be the last as it captures generic CFN error
errorRegex: /ROLLBACK_(COMPLETE|FAILED)/,
errorRegex: /❌ Deployment failed: (.*)\n/,
humanReadableError:
'[CloudFormationFailure]: The CloudFormation deployment has failed. Find more information in the CloudFormation AWS Console for this stack.',
},
Expand All @@ -54,8 +59,14 @@ export class CdkErrorMapper {
knownError.errorRegex.test(error.message)
);

return new Error(matchingError?.humanReadableError || error.message, {
cause: error,
});
if (matchingError) {
const underlyingMessage = error.message.match(matchingError.errorRegex);
error.message =
underlyingMessage && underlyingMessage.length == 2
? underlyingMessage[1]
: error.message;
return new Error(matchingError.humanReadableError, { cause: error });
}
return error;
};
}
Loading
Loading