Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
65b3566
Added types for new version of usage data
vigy02 Dec 5, 2024
f8fcec6
renamed the file
vigy02 Dec 5, 2024
d085c26
Added changeset and updated the cause to be an array
vigy02 Dec 5, 2024
099b13e
update: Added event and changes the errordetails
vigy02 Dec 6, 2024
c65d304
Updated aws_region and latency
vigy02 Dec 6, 2024
b515676
Merge branch 'main' into feature/cli-telemetry-v2
rtpascual Jan 30, 2025
0ca1c3f
update types
rtpascual Jan 30, 2025
8dcce09
Merge branch 'feature/cli-telemetry-v2' into feat/telemetry-v2
rtpascual Jan 31, 2025
51c7839
telemetry v2
rtpascual Feb 6, 2025
347bc83
refactor getLocalProjectId and add AccountIdFetcher
rtpascual Feb 6, 2025
0d00cd1
try this
rtpascual Feb 6, 2025
d0f748a
try this
rtpascual Feb 6, 2025
f403651
fix windows tests
rtpascual Feb 6, 2025
ce9c91a
try that
rtpascual Feb 6, 2025
46b83c6
try this
rtpascual Feb 7, 2025
67f4184
expand to sandbox
rtpascual Feb 7, 2025
2294d58
update anonymizePaths
rtpascual Feb 7, 2025
5963b6d
add workaround for abort events
rtpascual Feb 7, 2025
064e113
add workaround for abort events
rtpascual Feb 8, 2025
8ef823d
fix test
rtpascual Feb 8, 2025
68c3a79
try this
rtpascual Feb 8, 2025
c0e29e9
add changeset
rtpascual Feb 11, 2025
b1826fe
add changeset for snapshot
rtpascual Feb 14, 2025
5239b78
fix console logs and file path regex
rtpascual Feb 17, 2025
5f9e93d
remove stacks in errors
rtpascual Feb 17, 2025
d7c758c
try this
rtpascual Feb 17, 2025
79dee7e
try this
rtpascual Feb 17, 2025
2a08ebf
try this
rtpascual Feb 17, 2025
38e3dd3
remove stacks in error stack
rtpascual Feb 18, 2025
e726051
improve anonymization for accountId
rtpascual Feb 19, 2025
6186822
update getLocalProjectId
rtpascual Feb 20, 2025
68b4d1a
Merge branch 'main' into feat/telemetry-v2
rtpascual Mar 3, 2025
da8d3c5
fix some lint errors
rtpascual Mar 3, 2025
49a97e6
add getUrl
rtpascual Mar 3, 2025
8e4c765
remove prodUrl
rtpascual Mar 3, 2025
845961a
uncomment send
rtpascual Mar 4, 2025
5201dc4
fix
rtpascual Mar 4, 2025
7d4f367
Merge branch 'main' into feat/telemetry-v2
rtpascual Mar 17, 2025
c7d847f
use zod schema
rtpascual Mar 18, 2025
b7c6291
Merge branch 'main' into feat/telemetry-v2
rtpascual Mar 21, 2025
4d30d67
Merge branch 'main' into feat/telemetry-v2
rtpascual Mar 26, 2025
b8adb03
remove changeset for snapshot
rtpascual Mar 26, 2025
4e5484d
Merge branch 'main' into feat/telemetry-v2
rtpascual Mar 27, 2025
b2d8c8c
Merge branch 'main' into feat/telemetry-v2
rtpascual Apr 1, 2025
0ee8e2a
add telemetry endpoint
rtpascual Apr 1, 2025
b624e52
lint
rtpascual Apr 1, 2025
4aceea0
Merge branch 'main' into feat/telemetry-v2
rtpascual Apr 2, 2025
34581d6
fix test
rtpascual Apr 7, 2025
851fa96
add seed package and remove api version
rtpascual Apr 8, 2025
db32583
Merge branch 'main' into feat/telemetry-v2
rtpascual Apr 8, 2025
9726840
refactor deployment emissions for cdk toolkit
rtpascual Apr 9, 2025
9dc20f8
update changeset
rtpascual Apr 9, 2025
01943c3
fix test
rtpascual Apr 9, 2025
925fac9
update handling abortion
rtpascual Apr 10, 2025
81b9274
add back counter
rtpascual Apr 10, 2025
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
9 changes: 9 additions & 0 deletions .changeset/smooth-peaches-trade.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@aws-amplify/backend-deployer': minor
'@aws-amplify/platform-core': minor
'@aws-amplify/cli-core': minor
'@aws-amplify/sandbox': minor
'@aws-amplify/backend-cli': patch
---

add new version of telemetry
3 changes: 2 additions & 1 deletion packages/backend-deployer/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AmplifyIOHost } from '@aws-amplify/plugin-types';
import { BackendIdentifier } from '@aws-amplify/plugin-types';
import { PackageManagerController } from '@aws-amplify/plugin-types';
import { SDKProfileResolver } from '@aws-amplify/plugin-types';
import { TelemetryDataEmitter } from '@aws-amplify/platform-core';

// @public
export type BackendDeployer = {
Expand All @@ -17,7 +18,7 @@ export type BackendDeployer = {

// @public
export class BackendDeployerFactory {
constructor(packageManagerController: PackageManagerController, formatter: BackendDeployerOutputFormatter, backendDeployerIOHost: AmplifyIOHost, sdkProfileResolver: SDKProfileResolver);
constructor(packageManagerController: PackageManagerController, formatter: BackendDeployerOutputFormatter, backendDeployerIOHost: AmplifyIOHost, sdkProfileResolver: SDKProfileResolver, telemetryDataEmitter: TelemetryDataEmitter);
Copy link
Contributor

Choose a reason for hiding this comment

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

What's the reason that we're passing telemetry emitter to deployer?
We used to have deployer to return times to sandbox and sandbox emitting the metric.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We used to use

private populateCDKOutputFromStdout = async (
to gather times from stdout to sandbox for emitting the metric (You can see my previous implementation to get deployment and hotswap times).

With this gone after the introduction of CDK toolkit we now have to rely on the events from CDK which are already being captured in AmplifyEventLogger.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Also the event logging captures when a hotswap happens, I can try to work on copying how we used to do this but we lose the ability to differentiate hotswap from deployment times (so in that case maybe we remove hotswap from telemetry schema)

Copy link
Contributor

Choose a reason for hiding this comment

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

Also the event logging captures when a hotswap happens

That can also be achieved if we emit this data from deployer via return type and/or exception. Let's continue this discussion below in #2485 (comment)

getInstance(): BackendDeployer;
}

Expand Down
37 changes: 35 additions & 2 deletions packages/backend-deployer/src/cdk_deployer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
AmplifyError,
AmplifyUserError,
BackendLocator,
TelemetryDataEmitter,
} from '@aws-amplify/platform-core';
import { DeployProps } from './cdk_deployer_singleton_factory.js';
import { CDKDeploymentError, CdkErrorMapper } from './cdk_error_mapper.js';
Expand Down Expand Up @@ -76,12 +77,24 @@ void describe('invokeCDKCommand', () => {
fromAssemblyDirectory: fromAssemblyDirectoryMock,
} as unknown as Toolkit;

const mockTelemetryEmitSuccess = mock.fn();
const mockTelemetryEmitFailure = mock.fn();
const mockTelemetryEmitAbortion = mock.fn();
const telemetryDataEmitter = {
emitSuccess: mockTelemetryEmitSuccess,
emitFailure: mockTelemetryEmitFailure,
emitAbortion: mockTelemetryEmitAbortion,
} as unknown as TelemetryDataEmitter;

const cdkErrorMapper = new CdkErrorMapper(formatterStub);

const invoker = new CDKDeployer(
new CdkErrorMapper(formatterStub),
cdkErrorMapper,
backendLocator,
packageManagerControllerMock as never,
cdkToolkit,
mockIoHost,
telemetryDataEmitter,
);

const tsCompilerMock = mock.method(invoker, 'compileProject', () => {});
Expand All @@ -94,6 +107,7 @@ void describe('invokeCDKCommand', () => {
fromAssemblyBuilderMock.mock.resetCalls();
fromAssemblyDirectoryMock.mock.resetCalls();
tsCompilerMock.mock.resetCalls();
mockTelemetryEmitFailure.mock.resetCalls();
});

void it('handles options for branch deployments', async () => {
Expand Down Expand Up @@ -203,6 +217,9 @@ void describe('invokeCDKCommand', () => {
contextualTsCompilerCommandMock.mock.calls[0].arguments,
['amplify'],
);

// can't test arguments of mockTelemetryEmitFailure because of flakiness around stack trace and latency times
Copy link
Contributor

Choose a reason for hiding this comment

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

if we can't assert values, we could at least assert presence of certain elements in the event.

assert.strictEqual(mockTelemetryEmitFailure.mock.callCount(), 1);
});

void it('throws the original synth error if the synth failed but tsc succeeded', async () => {
Expand Down Expand Up @@ -232,6 +249,9 @@ void describe('invokeCDKCommand', () => {
assert.equal(synthMock.mock.callCount(), 1);

assert.deepStrictEqual(tsCompilerMock.mock.calls[0].arguments, ['amplify']);

// can't test arguments of mockTelemetryEmitFailure because of flakiness around stack trace and latency times
assert.strictEqual(mockTelemetryEmitFailure.mock.callCount(), 1);
});

void it('throws the original synth error if the synth failed to generate function env declaration files', async () => {
Expand Down Expand Up @@ -275,11 +295,15 @@ void describe('invokeCDKCommand', () => {
assert.strictEqual(tsCompilerMock.mock.callCount(), 1);

assert.deepStrictEqual(tsCompilerMock.mock.calls[0].arguments, ['amplify']);

// can't test arguments of mockTelemetryEmitFailure because of flakiness around stack trace and latency times
assert.strictEqual(mockTelemetryEmitFailure.mock.callCount(), 1);
});

void it('returns human readable errors', async () => {
const accessDeniedError = new Error('Access Denied');
synthMock.mock.mockImplementationOnce(() => {
throw new Error('Access Denied');
throw accessDeniedError;
});

await assert.rejects(
Expand All @@ -294,6 +318,9 @@ void describe('invokeCDKCommand', () => {
return true;
},
);

// can't test arguments of mockTelemetryEmitFailure because of flakiness around stack trace and latency times
assert.strictEqual(mockTelemetryEmitFailure.mock.callCount(), 1);
});

void it('displays actionable error when older version of node is used during branch deployments', async () => {
Expand Down Expand Up @@ -321,6 +348,9 @@ void describe('invokeCDKCommand', () => {
},
);

// can't test arguments of mockTelemetryEmitFailure because of flakiness around stack trace and latency times
assert.strictEqual(mockTelemetryEmitFailure.mock.callCount(), 1);

synthMock.mock.mockImplementationOnce(() => {
throw olderNodeVersionError;
});
Expand All @@ -341,5 +371,8 @@ void describe('invokeCDKCommand', () => {
return true;
},
);

// can't test arguments of mockTelemetryEmitFailure because of flakiness around stack trace and latency times
assert.strictEqual(mockTelemetryEmitFailure.mock.callCount(), 2);
});
});
68 changes: 62 additions & 6 deletions packages/backend-deployer/src/cdk_deployer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
AmplifyFault,
BackendLocator,
CDKContextKey,
LatencyDetails,
TelemetryDataEmitter,
} from '@aws-amplify/platform-core';
import path from 'path';
import {
Expand Down Expand Up @@ -47,6 +49,7 @@ export class CDKDeployer implements BackendDeployer {
private readonly packageManagerController: PackageManagerController,
private readonly cdkToolkit: Toolkit,
private readonly ioHost: AmplifyIOHost,
private readonly telemetryDataEmitter: TelemetryDataEmitter,
) {}

/**
Expand Down Expand Up @@ -94,7 +97,9 @@ export class CDKDeployer implements BackendDeployer {
action: 'amplify',
time: new Date(),
level: 'result',
data: undefined,
data: {
duration: synthTimeSeconds * 1000, // convert back to ms for telemetry
},
});

// Typescript compilation. For type related errors, we prefer to show errors from TS to customers rather than synth
Expand All @@ -112,16 +117,36 @@ export class CDKDeployer implements BackendDeployer {
try {
await this.compileProject(path.dirname(this.backendLocator.locate()));
} catch (typeError) {
const synthTime = synthTimeSeconds * 1000; // convert back to ms
const totalTime = synthTime + (Date.now() - typeCheckStartTime);
const latencyDetails: LatencyDetails = {
total: totalTime,
synthesis: synthTime,
};
if (
synthError &&
AmplifyError.isAmplifyError(typeError) &&
typeError.name === 'FunctionEnvVarFileNotGeneratedError'
) {
const error = this.cdkErrorMapper.getAmplifyError(
synthError,
backendId.type,
);
await this.telemetryDataEmitter.emitFailure(error, latencyDetails, {
subCommands: 'SandboxEvent',
});
Comment on lines +135 to +137
Copy link
Contributor

Choose a reason for hiding this comment

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

here and below.

we shouldn't be encoding "sandbox" logic in deployer.
will this be correct if top level command is pipeline-deploy?

I'm guessing that the problem being solved here is to get some latency metrics in error case.
For that I would suggest two alternatives:

  1. Develop DeploymentError (or so) that can carry both the root cause error and metrics back to caller and process that data in sandbox/pipeline deploy.
  2. Perhaps introducing the concept of UsageDataCollector from Emit notices metrics #2698 that I opened in parallel would solve this use case ? I.e. we could inject the collector here to get these metrics but leave emission up to cli and sandbox.

Edit: after reaching end of this PR I notices we haven't touched this piece

nor this
await usageDataEmitter.emitFailure(deployError, {
.

perhaps it would be possible to plumb metrics from deployer up to there and just replace emitter call there ?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You're right, this won't always necessarily be exclusively sandbox but I believe we would still want to capture this even for pipeline-deploy. It may be more suitable to rename this and other places to be DeploymentEvent instead?

after reaching end of this PR I notices we haven't touched this piece

I did touch those places before but they were intentionally removed after merging in changes from pretty sandbox, see my previous comment #2485 (comment)

Copy link
Contributor

Choose a reason for hiding this comment

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

Btw. Some other variation of 2. above would be
process.emit('amplifyMetrics', {...})` in deployer, listening to that in emitter, and emit in sandbox event handlers.

Copy link
Contributor

Choose a reason for hiding this comment

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

Some other thought.
If we emit in deployer then for pipeline deploy - would we double emit ? after deployment and after command exists? If we do double emit then this would be another reason to split metrics collection/recording and emission, so that.

@Amplifiyer plz take a look at this discussion and share your thoughts.

Copy link
Contributor Author

@rtpascual rtpascual Apr 10, 2025

Choose a reason for hiding this comment

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

Yes we would double emit, which I believe is what is happening today for sandbox but not for pipeline deploy: 1 emit for sandbox command and another emit when deployment succeeds/fails (which would be Sandbox in our telemetry)

Edit: And on this note I believe we currently don't have the granularity for time metrics for pipeline deploy (we have total time it took to execute the pipeline-deploy command but I guess we don't have it broken down into synthesis and deployment)

Copy link
Contributor

Choose a reason for hiding this comment

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

For the double emit, I don't see that as a problem, 1 is a deployment event and 1 is a command event. The deployment could be a failure from customer issue, but the command would have been a success.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah I forgot I could add more this.ioHost.notify in deployer for failures... Also is there a benefit over the other for new "logger" vs. new event listener?

As for double emit, since this would also happen for pipeline deploy if we are using some form of event logger (new or existing) then I would probably rename the sub command SandboxEvent -> DeploymentEvent.

Copy link
Contributor

Choose a reason for hiding this comment

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

Also is there a benefit over the other for new "logger" vs. new event listener?

To keep concerns separate for logging versus emitting metrics in case we are adding a bunch of logic for telemetry.

add more this.ioHost.notify in deployer for failures...

For this, why don't we emit from Sandbox when deployer returns with an error. It's sandbox where we swallow errors, so we should be able to emit there?

Copy link
Contributor

Choose a reason for hiding this comment

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

To keep concerns separate for logging versus emitting metrics in case we are adding a bunch of logic for telemetry.

+1 on that one.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For this, why don't we emit from Sandbox when deployer returns with an error. It's sandbox where we swallow errors, so we should be able to emit there?

Problem is we don't know when a hotswap happens instead of full deployment without CDK toolkit code CDK_TOOLKIT_I5403. I can definitely move emits back to sandbox_event_handler_factory.ts but we lose the ability to differentiate hotswap so we would remove hotswap from LatencyDetails in that case.

// synth has failed and we don't have auto generated function environment definition files. This
// resulted in the exception caught here, which is not very useful for the customers.
// We instead throw the synth error for customers to fix what caused the synth to fail.
throw this.cdkErrorMapper.getAmplifyError(synthError, backendId.type);
throw error;
}
const error = this.cdkErrorMapper.getAmplifyError(
typeError as Error,
backendId.type,
);
await this.telemetryDataEmitter.emitFailure(error, latencyDetails, {
subCommands: 'SandboxEvent',
});
throw typeError;
} finally {
const typeCheckTimeSeconds =
Expand All @@ -132,14 +157,29 @@ export class CDKDeployer implements BackendDeployer {
action: 'amplify',
time: new Date(),
level: 'result',
data: undefined,
data: {
duration: typeCheckTimeSeconds * 1000, // convert back to ms for telemetry
},
});
}
}

// If typescript compilation was successful but synth had failed, we throw synth error
if (synthError) {
throw this.cdkErrorMapper.getAmplifyError(synthError, backendId.type);
const synthTime = synthTimeSeconds * 1000; // convert back to ms
const totalTime = synthTime + (Date.now() - typeCheckStartTime);
const latencyDetails: LatencyDetails = {
total: totalTime,
synthesis: synthTime,
};
const error = this.cdkErrorMapper.getAmplifyError(
synthError,
backendId.type,
);
await this.telemetryDataEmitter.emitFailure(error, latencyDetails, {
subCommands: 'SandboxEvent',
});
throw error;
}

// Perform actual deployment. CFN or hotswap
Expand All @@ -157,8 +197,24 @@ export class CDKDeployer implements BackendDeployer {
requireApproval:
backendId.type !== 'sandbox' ? RequireApproval.NEVER : undefined,
});
} catch (error) {
throw this.cdkErrorMapper.getAmplifyError(error as Error, backendId.type);
} catch (deployError) {
const synthTime = synthTimeSeconds * 1000; // convert back to ms
const deploymentTime = Date.now() - deployStartTime;
const totalTime =
synthTime + (Date.now() - typeCheckStartTime) + deploymentTime;
const latencyDetails: LatencyDetails = {
total: totalTime,
synthesis: synthTime,
deployment: deploymentTime,
};
const error = this.cdkErrorMapper.getAmplifyError(
deployError as Error,
backendId.type,
);
await this.telemetryDataEmitter.emitFailure(error, latencyDetails, {
subCommands: 'SandboxEvent',
});
throw error;
}

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ import {
} from '@aws-amplify/plugin-types';
import { CDKDeployer } from './cdk_deployer.js';
import { CdkErrorMapper } from './cdk_error_mapper.js';
import { BackendLocator } from '@aws-amplify/platform-core';
import {
BackendLocator,
TelemetryDataEmitter,
} from '@aws-amplify/platform-core';
import { BackendDeployerOutputFormatter } from './types.js';
import { Toolkit } from '@aws-cdk/toolkit-lib';

Expand Down Expand Up @@ -53,6 +56,7 @@ export class BackendDeployerFactory {
private readonly formatter: BackendDeployerOutputFormatter,
private readonly backendDeployerIOHost: AmplifyIOHost,
private readonly sdkProfileResolver: SDKProfileResolver,
private readonly telemetryDataEmitter: TelemetryDataEmitter,
) {}

/**
Expand All @@ -73,6 +77,7 @@ export class BackendDeployerFactory {
},
}),
this.backendDeployerIOHost,
this.telemetryDataEmitter,
);
}
return BackendDeployerFactory.instance;
Expand Down
2 changes: 1 addition & 1 deletion packages/backend-deployer/src/cdk_error_mapper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -169,7 +169,7 @@ export class CdkErrorMapper {
(errorName) => errorName === error.name,
)
) {
throw new AmplifyUserError(
return new AmplifyUserError(
'SyntaxError',
{
message: 'Unable to build the Amplify backend definition.',
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-core/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,13 @@

import { AmplifyIOHost } from '@aws-amplify/plugin-types';
import { PackageManagerController } from '@aws-amplify/plugin-types';
import { TelemetryDataEmitter } from '@aws-amplify/platform-core';
import { WriteStream } from 'node:tty';
import z from 'zod';

// @public
export class AmplifyIOEventsBridgeSingletonFactory {
constructor(printer?: Printer);
constructor(telemetryDataEmitter: TelemetryDataEmitter, printer?: Printer);
getInstance: () => AmplifyIOHost;
}

Expand Down
Loading
Loading