Skip to content

Commit 91b7682

Browse files
committed
Added runtime validations for the metrics and dimensions utility functions
1 parent abc3813 commit 91b7682

File tree

7 files changed

+241
-51
lines changed

7 files changed

+241
-51
lines changed

packages/commons/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export {
2222
isRecord,
2323
isStrictEqual,
2424
isString,
25+
isStringUndefinedNullEmpty,
2526
isTruthy,
2627
} from './typeUtils.js';
2728
export { Utility } from './Utility.js';

packages/commons/src/typeUtils.ts

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
* }
1212
* ```
1313
*
14-
* @param value The value to check
14+
* @param value - The value to check
1515
*/
1616
const isRecord = (
1717
value: unknown
@@ -35,7 +35,7 @@ const isRecord = (
3535
* }
3636
* ```
3737
*
38-
* @param value The value to check
38+
* @param value - The value to check
3939
*/
4040
const isString = (value: unknown): value is string => {
4141
return typeof value === 'string';
@@ -54,7 +54,7 @@ const isString = (value: unknown): value is string => {
5454
* }
5555
* ```
5656
*
57-
* @param value The value to check
57+
* @param value - The value to check
5858
*/
5959
const isNumber = (value: unknown): value is number => {
6060
return typeof value === 'number';
@@ -73,7 +73,7 @@ const isNumber = (value: unknown): value is number => {
7373
* }
7474
* ```
7575
*
76-
* @param value The value to check
76+
* @param value - The value to check
7777
*/
7878
const isIntegerNumber = (value: unknown): value is number => {
7979
return isNumber(value) && Number.isInteger(value);
@@ -94,7 +94,7 @@ const isIntegerNumber = (value: unknown): value is number => {
9494
*
9595
* @see https://github.com/getify/You-Dont-Know-JS/blob/2nd-ed/types-grammar/ch4.md#toboolean
9696
*
97-
* @param value The value to check
97+
* @param value - The value to check
9898
*/
9999
const isTruthy = (value: unknown): boolean => {
100100
if (isString(value)) {
@@ -129,7 +129,7 @@ const isTruthy = (value: unknown): boolean => {
129129
* }
130130
* ```
131131
*
132-
* @param value The value to check
132+
* @param value - The value to check
133133
*/
134134
const isNull = (value: unknown): value is null => {
135135
return Object.is(value, null);
@@ -148,12 +148,34 @@ const isNull = (value: unknown): value is null => {
148148
* }
149149
* ```
150150
*
151-
* @param value The value to check
151+
* @param value - The value to check
152152
*/
153153
const isNullOrUndefined = (value: unknown): value is null | undefined => {
154154
return isNull(value) || Object.is(value, undefined);
155155
};
156156

157+
/**
158+
* Check if string is undefined, null, empty.
159+
*
160+
* @example
161+
* ```typescript
162+
* import { isStringUndefinedNullEmpty } from '@aws-lambda-powertools/commons/typeUtils';
163+
*
164+
* const value = 'foo';
165+
* if (isStringUndefinedNullEmpty(value)) {
166+
* // value is either undefined, null, or an empty string
167+
* }
168+
* ```
169+
*
170+
* @param value - The value to check
171+
*/
172+
const isStringUndefinedNullEmpty = (value: unknown) => {
173+
if (isNullOrUndefined(value)) return true;
174+
if (!isString(value)) return true;
175+
if (value.trim().length === 0) return true;
176+
return false;
177+
};
178+
157179
/**
158180
* Get the type of a value as a string.
159181
*
@@ -167,7 +189,7 @@ const isNullOrUndefined = (value: unknown): value is null | undefined => {
167189
* const unknownType = getType(Symbol('foo')); // 'unknown'
168190
* ```
169191
*
170-
* @param value The value to check
192+
* @param value - The value to check
171193
*/
172194
const getType = (value: unknown): string => {
173195
if (Array.isArray(value)) {
@@ -210,8 +232,8 @@ const getType = (value: unknown): string => {
210232
* const otherEqual = areArraysEqual(otherLeft, otherRight); // false
211233
* ```
212234
*
213-
* @param left The left array to compare
214-
* @param right The right array to compare
235+
* @param left - The left array to compare
236+
* @param right - The right array to compare
215237
*/
216238
const areArraysEqual = (left: unknown[], right: unknown[]): boolean => {
217239
if (left.length !== right.length) {
@@ -237,8 +259,8 @@ const areArraysEqual = (left: unknown[], right: unknown[]): boolean => {
237259
* const otherEqual = areRecordsEqual(otherLeft, otherRight); // false
238260
* ```
239261
*
240-
* @param left The left record to compare
241-
* @param right The right record to compare
262+
* @param left - The left record to compare
263+
* @param right - The right record to compare
242264
*/
243265
const areRecordsEqual = (
244266
left: Record<string, unknown>,
@@ -283,8 +305,8 @@ const areRecordsEqual = (
283305
* const yetAnotherEqual = isStrictEqual(yetAnotherLeft, yetAnotherRight); // true
284306
* ```
285307
*
286-
* @param left Left side of strict equality comparison
287-
* @param right Right side of strict equality comparison
308+
* @param left - Left side of strict equality comparison
309+
* @param right - Right side of strict equality comparison
288310
*/
289311
const isStrictEqual = (left: unknown, right: unknown): boolean => {
290312
if (left === right) {
@@ -314,6 +336,7 @@ export {
314336
isTruthy,
315337
isNull,
316338
isNullOrUndefined,
339+
isStringUndefinedNullEmpty,
317340
getType,
318341
isStrictEqual,
319342
};

packages/commons/tests/unit/typeUtils.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isRecord,
99
isStrictEqual,
1010
isString,
11+
isStringUndefinedNullEmpty,
1112
isTruthy,
1213
} from '../../src/index.js';
1314

@@ -119,6 +120,38 @@ describe('Functions: typeUtils', () => {
119120
});
120121
});
121122

123+
describe('Function: isStringUndefinedNullEmpty', () => {
124+
it('returns true if input is undefined', () => {
125+
// Act & Assess
126+
expect(isStringUndefinedNullEmpty(undefined)).toBe(true);
127+
});
128+
129+
it('returns true if input is null', () => {
130+
// Act & Assess
131+
expect(isStringUndefinedNullEmpty(null)).toBe(true);
132+
});
133+
134+
it('returns true if input is an empty string', () => {
135+
// Act & Assess
136+
expect(isStringUndefinedNullEmpty('')).toBe(true);
137+
});
138+
139+
it('returns true if input is a whitespace', () => {
140+
// Act & Assess
141+
expect(isStringUndefinedNullEmpty(' ')).toBe(true);
142+
});
143+
144+
it('returns true if input is not a string', () => {
145+
// Act & Assess
146+
expect(isStringUndefinedNullEmpty(1)).toBe(true);
147+
});
148+
149+
it('returns false if input is not undefined, null, or an empty string', () => {
150+
// Act & Assess
151+
expect(isStringUndefinedNullEmpty('test')).toBe(false);
152+
});
153+
});
154+
122155
describe('Function: isNumber', () => {
123156
it('returns true when the passed value is a number', () => {
124157
// Prepare

packages/metrics/src/Metrics.ts

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
import { Console } from 'node:console';
2-
import { isIntegerNumber, Utility } from '@aws-lambda-powertools/commons';
2+
import {
3+
isIntegerNumber,
4+
isNumber,
5+
isString,
6+
isStringUndefinedNullEmpty,
7+
Utility,
8+
} from '@aws-lambda-powertools/commons';
39
import type {
410
GenericLogger,
511
HandlerMethodDecorator,
@@ -12,10 +18,12 @@ import {
1218
EMF_MAX_TIMESTAMP_FUTURE_AGE,
1319
EMF_MAX_TIMESTAMP_PAST_AGE,
1420
MAX_DIMENSION_COUNT,
21+
MAX_METRIC_NAME_LENGTH,
1522
MAX_METRIC_VALUES_SIZE,
1623
MAX_METRICS_SIZE,
1724
MetricResolution as MetricResolutions,
1825
MetricUnit as MetricUnits,
26+
MIN_METRIC_NAME_LENGTH,
1927
} from './constants.js';
2028
import type {
2129
ConfigServiceInterface,
@@ -238,7 +246,7 @@ class Metrics extends Utility implements MetricsInterface {
238246
* @param value - The value of the dimension
239247
*/
240248
public addDimension(name: string, value: string): void {
241-
if (!value) {
249+
if (isStringUndefinedNullEmpty(name) || isStringUndefinedNullEmpty(value)) {
242250
this.#logger.warn(
243251
`The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
244252
);
@@ -275,7 +283,10 @@ class Metrics extends Utility implements MetricsInterface {
275283
public addDimensions(dimensions: Dimensions): void {
276284
const newDimensionSet: Dimensions = {};
277285
for (const [key, value] of Object.entries(dimensions)) {
278-
if (!value) {
286+
if (
287+
isStringUndefinedNullEmpty(key) ||
288+
isStringUndefinedNullEmpty(value)
289+
) {
279290
this.#logger.warn(
280291
`The dimension ${key} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
281292
);
@@ -1067,6 +1078,25 @@ class Metrics extends Utility implements MetricsInterface {
10671078
value: number,
10681079
resolution: MetricResolution
10691080
): void {
1081+
if (!isString(name)) throw new Error(`${name} is not a valid string`);
1082+
if (
1083+
name.length < MIN_METRIC_NAME_LENGTH ||
1084+
name.length > MAX_METRIC_NAME_LENGTH
1085+
)
1086+
throw new RangeError(
1087+
`The metric name should be between ${MIN_METRIC_NAME_LENGTH} and ${MAX_METRIC_NAME_LENGTH} characters`
1088+
);
1089+
if (!isNumber(value))
1090+
throw new RangeError(`${value} is not a valid number`);
1091+
if (!Object.values(MetricUnits).includes(unit))
1092+
throw new RangeError(
1093+
`Invalid metric unit '${unit}', expected either option: ${Object.values(MetricUnits).join(',')}`
1094+
);
1095+
if (!Object.values(MetricResolutions).includes(resolution))
1096+
throw new RangeError(
1097+
`Invalid metric resolution '${resolution}', expected either option: ${Object.values(MetricResolutions).join(',')}`
1098+
);
1099+
10701100
if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) {
10711101
this.publishStoredMetrics();
10721102
}

packages/metrics/src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ const COLD_START_METRIC = 'ColdStart';
66
* The default namespace for metrics.
77
*/
88
const DEFAULT_NAMESPACE = 'default_namespace';
9+
/**
10+
* The minimum length constraint of the metric name
11+
*/
12+
const MIN_METRIC_NAME_LENGTH = 1;
13+
/**
14+
* The maximum length constraint of the metric name
15+
*/
16+
const MAX_METRIC_NAME_LENGTH = 255;
917
/**
1018
* The maximum number of metrics that can be emitted in a single EMF blob.
1119
*/
@@ -78,6 +86,8 @@ const MetricResolution = {
7886
export {
7987
COLD_START_METRIC,
8088
DEFAULT_NAMESPACE,
89+
MIN_METRIC_NAME_LENGTH,
90+
MAX_METRIC_NAME_LENGTH,
8191
MAX_METRICS_SIZE,
8292
MAX_METRIC_VALUES_SIZE,
8393
MAX_DIMENSION_COUNT,

packages/metrics/tests/unit/creatingMetrics.test.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest';
22
import {
33
DEFAULT_NAMESPACE,
4+
MAX_METRIC_NAME_LENGTH,
45
MAX_METRICS_SIZE,
56
MetricResolution,
7+
MIN_METRIC_NAME_LENGTH,
68
} from '../../src/constants.js';
79
import { Metrics, MetricUnit } from '../../src/index.js';
810

@@ -267,4 +269,82 @@ describe('Creating metrics', () => {
267269
})
268270
);
269271
});
272+
273+
it('throws when an invalid metric name is passed', () => {
274+
// Prepare
275+
const metrics = new Metrics();
276+
277+
// Act & Assess
278+
// @ts-expect-error - Testing runtime behavior with non-numeric metric value
279+
expect(() => metrics.addMetric(1, MetricUnit.Count, 1)).toThrowError(
280+
'1 is not a valid string'
281+
);
282+
});
283+
284+
it('throws when an empty string is passed in the metric name', () => {
285+
// Prepare
286+
const metrics = new Metrics();
287+
288+
// Act & Assess
289+
expect(() => metrics.addMetric('', MetricUnit.Count, 1)).toThrowError(
290+
`The metric name should be between ${MIN_METRIC_NAME_LENGTH} and ${MAX_METRIC_NAME_LENGTH} characters`
291+
);
292+
});
293+
294+
it(`throws when a string of more than ${MAX_METRIC_NAME_LENGTH} characters is passed in the metric name`, () => {
295+
// Prepare
296+
const metrics = new Metrics();
297+
298+
// Act & Assess
299+
expect(() =>
300+
metrics.addMetric(
301+
'Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa. Cum sociis natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Donec quam felis, ultricies nec, pellentesque eu, pretium quis,.',
302+
MetricUnit.Count,
303+
1
304+
)
305+
).toThrowError(
306+
new RangeError(
307+
`The metric name should be between ${MIN_METRIC_NAME_LENGTH} and ${MAX_METRIC_NAME_LENGTH} characters`
308+
)
309+
);
310+
});
311+
312+
it('throws when a non-numeric metric value is passed', () => {
313+
// Prepare
314+
const metrics = new Metrics();
315+
316+
// Act & Assess
317+
expect(() =>
318+
// @ts-expect-error - Testing runtime behavior with non-numeric metric value
319+
metrics.addMetric('test', MetricUnit.Count, 'one')
320+
).toThrowError(new RangeError('one is not a valid number'));
321+
});
322+
323+
it('throws when an invalid unit is passed', () => {
324+
// Prepare
325+
const metrics = new Metrics();
326+
327+
// Act & Assess
328+
// @ts-expect-error - Testing runtime behavior with invalid metric unit
329+
expect(() => metrics.addMetric('test', 'invalid-unit', 1)).toThrowError(
330+
new RangeError(
331+
`Invalid metric unit 'invalid-unit', expected either option: ${Object.values(MetricUnit).join(',')}`
332+
)
333+
);
334+
});
335+
336+
it('throws when an invalid resolution is passed', () => {
337+
// Prepare
338+
const metrics = new Metrics();
339+
340+
// Act & Assess
341+
expect(() =>
342+
// @ts-expect-error - Testing runtime behavior with invalid metric unit
343+
metrics.addMetric('test', MetricUnit.Count, 1, 'invalid-resolution')
344+
).toThrowError(
345+
new RangeError(
346+
`Invalid metric resolution 'invalid-resolution', expected either option: ${Object.values(MetricResolution).join(',')}`
347+
)
348+
);
349+
});
270350
});

0 commit comments

Comments
 (0)