Skip to content

Commit f934757

Browse files
author
Alexander Schueren
authored
chore(maintenance): add powertools to user-agent in SDK clients (#1567)
* add custom user agent middleware * remove singleton for ddb client, add useragent in constructor * remove conditional, because SDK will always resolve user-agent header * simplify test * remove retry, no longer needed * remove ua from idempotency, will be done in separate PR * review changes * revert client lazy loading * Revert "remove ua from idempotency, will be done in separate PR" This reverts commit bdda143. * revert the revert, misunderstanding * add explicit ts-ignore instead of the entire file * parameterized tests for useragent * in case of failure, warn and don't block user code * add test to check if middleware absorbs an error and continue * add more client to useragent test * fix imports
1 parent 78721c2 commit f934757

File tree

9 files changed

+188
-40
lines changed

9 files changed

+188
-40
lines changed

package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/commons/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"test:unit": "jest --group=unit --detectOpenHandles --coverage --verbose",
1515
"test:e2e": "echo 'Not Applicable'",
1616
"watch": "jest --watch",
17+
"generateVersionFile": "echo \"// this file is auto generated, do not modify\nexport const PT_VERSION = '$(jq -r '.version' package.json)';\" > src/version.ts",
1718
"build": "tsc",
1819
"lint": "eslint --ext .ts,.js --no-error-on-unmatched-pattern .",
1920
"lint-fix": "eslint --fix --ext .ts,.js --no-error-on-unmatched-pattern .",
@@ -47,6 +48,10 @@
4748
],
4849
"devDependencies": {
4950
"@aws-sdk/client-lambda": "^3.360.0",
51+
"@aws-sdk/client-dynamodb": "^3.360.0",
52+
"@aws-sdk/client-appconfigdata": "^3.360.0",
53+
"@aws-sdk/client-secrets-manager": "^3.360.0",
54+
"@aws-sdk/client-ssm": "^3.360.0",
5055
"@aws-sdk/util-utf8-node": "^3.259.0"
5156
}
5257
}

packages/commons/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@ export * as ContextExamples from './samples/resources/contexts';
55
export * as Events from './samples/resources/events';
66
export * from './types/middy';
77
export * from './types/utils';
8+
export * from './userAgentMiddleware';
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import { PT_VERSION } from './version';
2+
3+
/**
4+
* @internal
5+
*/
6+
const EXEC_ENV = process.env.AWS_EXECUTION_ENV || 'NA';
7+
const middlewareOptions = {
8+
relation: 'after',
9+
toMiddleware: 'getUserAgentMiddleware',
10+
name: 'addPowertoolsToUserAgent',
11+
tags: ['POWERTOOLS', 'USER_AGENT'],
12+
};
13+
14+
/**
15+
* @internal
16+
* returns a middleware function for the MiddlewareStack, that can be used for the SDK clients
17+
* @param feature
18+
*/
19+
const customUserAgentMiddleware = (feature: string) => {
20+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
21+
// @ts-ignore
22+
return (next, _context) => async (args) => {
23+
const powertoolsUserAgent = `PT/${feature}/${PT_VERSION} PTEnv/${EXEC_ENV}`;
24+
args.request.headers[
25+
'user-agent'
26+
] = `${args.request.headers['user-agent']} ${powertoolsUserAgent}`;
27+
28+
return await next(args);
29+
};
30+
};
31+
32+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
33+
// @ts-ignore
34+
const addUserAgentMiddleware = (client, feature: string): void => {
35+
try {
36+
client.middlewareStack.addRelativeTo(
37+
customUserAgentMiddleware(feature),
38+
middlewareOptions
39+
);
40+
} catch (e) {
41+
console.warn('Failed to add user agent middleware', e);
42+
}
43+
};
44+
45+
export { addUserAgentMiddleware };

packages/commons/src/version.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
// this file is auto generated, do not modify
2+
export const PT_VERSION = '1.11.0';
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { addUserAgentMiddleware } from '../../src/userAgentMiddleware';
2+
import { InvokeCommand, LambdaClient } from '@aws-sdk/client-lambda';
3+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
4+
import { ScanCommand } from '@aws-sdk/lib-dynamodb';
5+
import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
6+
import { version as PT_VERSION } from '../../package.json';
7+
import { AppConfigDataClient } from '@aws-sdk/client-appconfigdata';
8+
import {
9+
GetSecretValueCommand,
10+
SecretsManagerClient,
11+
} from '@aws-sdk/client-secrets-manager';
12+
13+
const options = {
14+
region: 'us-east-1',
15+
endpoint: 'http://localhost:9001',
16+
credentials: {
17+
accessKeyId: 'test',
18+
secretAccessKey: 'test',
19+
sessionToken: 'test',
20+
},
21+
};
22+
23+
describe('Given a client of instance: ', () => {
24+
it.each([
25+
{
26+
name: 'LambdaClient',
27+
client: new LambdaClient(options),
28+
command: new InvokeCommand({ FunctionName: 'test', Payload: '' }),
29+
},
30+
{
31+
name: 'DynamoDBClient',
32+
client: new DynamoDBClient(options),
33+
command: new ScanCommand({ TableName: 'test' }),
34+
},
35+
{
36+
name: 'SSMClient',
37+
client: new SSMClient(options),
38+
command: new GetParameterCommand({ Name: 'test' }),
39+
},
40+
{
41+
name: 'AppConfigDataClient',
42+
client: new AppConfigDataClient(options),
43+
command: new GetParameterCommand({ Name: 'test' }),
44+
},
45+
{
46+
name: 'SecretsManagerClient',
47+
client: new SecretsManagerClient(options),
48+
command: new GetSecretValueCommand({ SecretId: 'test' }),
49+
},
50+
])(
51+
`using $name, add powertools user agent to request header at the end`,
52+
async ({ client, command }) => {
53+
addUserAgentMiddleware(client, 'my-feature');
54+
55+
expect(client.middlewareStack.identify()).toContain(
56+
'addPowertoolsToUserAgent: POWERTOOLS,USER_AGENT'
57+
);
58+
59+
client.middlewareStack.addRelativeTo(
60+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
61+
// @ts-ignore
62+
(next) => (args) => {
63+
const userAgent = args?.request?.headers['user-agent'];
64+
expect(userAgent).toContain(`PT/my-feature/${PT_VERSION} PTEnv/NA`);
65+
// make sure it's at the end of the user agent
66+
expect(
67+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
68+
// @ts-ignore
69+
userAgent
70+
?.split(' ')
71+
.slice(userAgent?.split(' ').length - 2) // take the last to entries of the user-agent header
72+
.join(' ')
73+
).toEqual(`PT/my-feature/${PT_VERSION} PTEnv/NA`);
74+
75+
return next(args);
76+
},
77+
{
78+
relation: 'after',
79+
toMiddleware: 'addPowertoolsToUserAgent',
80+
name: 'testUserAgentHeader',
81+
tags: ['TEST'],
82+
}
83+
);
84+
85+
try {
86+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
87+
// @ts-ignore
88+
await client.send(command);
89+
} catch (e) {
90+
if (e instanceof Error && e.name === 'JestAssertionError') {
91+
throw e;
92+
}
93+
}
94+
}
95+
);
96+
97+
it('should not throw erro, when client fails to add middleware', () => {
98+
// create mock client that throws error when adding middleware
99+
const client = {
100+
middlewareStack: {
101+
addRelativeTo: () => {
102+
throw new Error('test');
103+
},
104+
},
105+
};
106+
107+
expect(() => addUserAgentMiddleware(client, 'my-feature')).not.toThrow();
108+
});
109+
});

packages/idempotency/src/middleware/makeHandlerIdempotent.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,11 @@ import {
88
} from '../errors';
99
import { IdempotencyRecord } from '../persistence';
1010
import { MAX_RETRIES } from '../constants';
11-
import type {
11+
import type { IdempotencyLambdaHandlerOptions } from '../types';
12+
import {
1213
MiddlewareLikeObj,
1314
MiddyLikeRequest,
1415
} from '@aws-lambda-powertools/commons';
15-
import type { IdempotencyLambdaHandlerOptions } from '../types';
1616

1717
/**
1818
* A middy middleware to make your Lambda Handler idempotent.

packages/idempotency/src/persistence/DynamoDBPersistenceLayer.ts

Lines changed: 12 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@ import {
55
import { IdempotencyRecordStatus } from '../types';
66
import type { DynamoDBPersistenceOptions } from '../types';
77
import {
8+
AttributeValue,
9+
DeleteItemCommand,
810
DynamoDBClient,
911
DynamoDBClientConfig,
1012
DynamoDBServiceException,
11-
DeleteItemCommand,
1213
GetItemCommand,
1314
PutItemCommand,
1415
UpdateItemCommand,
15-
AttributeValue,
1616
} from '@aws-sdk/client-dynamodb';
1717
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
1818
import { IdempotencyRecord } from './IdempotencyRecord';
1919
import { BasePersistenceLayer } from './BasePersistenceLayer';
20+
import { addUserAgentMiddleware } from '@aws-lambda-powertools/commons';
2021

2122
/**
2223
* DynamoDB persistence layer for idempotency records. This class will use the AWS SDK V3 to write and read idempotency records from DynamoDB.
@@ -28,7 +29,7 @@ import { BasePersistenceLayer } from './BasePersistenceLayer';
2829
* @implements {BasePersistenceLayer}
2930
*/
3031
class DynamoDBPersistenceLayer extends BasePersistenceLayer {
31-
private client?: DynamoDBClient;
32+
private client: DynamoDBClient;
3233
private clientConfig: DynamoDBClientConfig = {};
3334
private dataAttr: string;
3435
private expiryAttr: string;
@@ -64,18 +65,18 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
6465
if (config?.awsSdkV3Client instanceof DynamoDBClient) {
6566
this.client = config.awsSdkV3Client;
6667
} else {
67-
console.warn(
68-
'Invalid AWS SDK V3 client passed to DynamoDBPersistenceLayer. Using default client.'
69-
);
68+
throw Error('Not valid DynamoDBClient provided.');
7069
}
7170
} else {
7271
this.clientConfig = config?.clientConfig ?? {};
72+
this.client = new DynamoDBClient(this.clientConfig);
7373
}
74+
75+
addUserAgentMiddleware(this.client, 'idempotency');
7476
}
7577

7678
protected async _deleteRecord(record: IdempotencyRecord): Promise<void> {
77-
const client = this.getClient();
78-
await client.send(
79+
await this.client.send(
7980
new DeleteItemCommand({
8081
TableName: this.tableName,
8182
Key: this.getKey(record.idempotencyKey),
@@ -86,8 +87,7 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
8687
protected async _getRecord(
8788
idempotencyKey: string
8889
): Promise<IdempotencyRecord> {
89-
const client = this.getClient();
90-
const result = await client.send(
90+
const result = await this.client.send(
9191
new GetItemCommand({
9292
TableName: this.tableName,
9393
Key: this.getKey(idempotencyKey),
@@ -111,8 +111,6 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
111111
}
112112

113113
protected async _putRecord(record: IdempotencyRecord): Promise<void> {
114-
const client = this.getClient();
115-
116114
const item = {
117115
...this.getKey(record.idempotencyKey),
118116
...marshall({
@@ -163,7 +161,7 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
163161
].join(' OR ');
164162

165163
const now = Date.now();
166-
await client.send(
164+
await this.client.send(
167165
new PutItemCommand({
168166
TableName: this.tableName,
169167
Item: item,
@@ -195,8 +193,6 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
195193
}
196194

197195
protected async _updateRecord(record: IdempotencyRecord): Promise<void> {
198-
const client = this.getClient();
199-
200196
const updateExpressionFields: string[] = [
201197
'#response_data = :response_data',
202198
'#expiry = :expiry',
@@ -219,7 +215,7 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
219215
expressionAttributeValues[':validation_key'] = record.payloadHash;
220216
}
221217

222-
await client.send(
218+
await this.client.send(
223219
new UpdateItemCommand({
224220
TableName: this.tableName,
225221
Key: this.getKey(record.idempotencyKey),
@@ -230,14 +226,6 @@ class DynamoDBPersistenceLayer extends BasePersistenceLayer {
230226
);
231227
}
232228

233-
private getClient(): DynamoDBClient {
234-
if (!this.client) {
235-
this.client = new DynamoDBClient(this.clientConfig);
236-
}
237-
238-
return this.client;
239-
}
240-
241229
/**
242230
* Build primary key attribute simple or composite based on params.
243231
*

packages/idempotency/tests/unit/persistence/DynamoDbPersistenceLayer.test.ts

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -163,20 +163,14 @@ describe('Class: DynamoDBPersistenceLayer', () => {
163163
);
164164
});
165165

166-
test('when passed an invalid AWS SDK client it logs a warning', () => {
167-
// Prepare
168-
const consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
169-
170-
// Act
171-
new TestDynamoDBPersistenceLayer({
172-
tableName: dummyTableName,
173-
awsSdkV3Client: {} as DynamoDBClient,
174-
});
175-
176-
// Assess
177-
expect(consoleWarnSpy).toHaveBeenCalledWith(
178-
'Invalid AWS SDK V3 client passed to DynamoDBPersistenceLayer. Using default client.'
179-
);
166+
test('when passed an invalid AWS SDK client, it throws an error', () => {
167+
// Act & Assess
168+
expect(() => {
169+
new TestDynamoDBPersistenceLayer({
170+
tableName: dummyTableName,
171+
awsSdkV3Client: {} as DynamoDBClient,
172+
});
173+
}).toThrow();
180174
});
181175

182176
test('when passed a client config it stores it for later use', () => {

0 commit comments

Comments
 (0)