Skip to content

Commit f673c07

Browse files
committed
Added runtime validations for the metrics and dimensions utility functions
1 parent 204e2d7 commit f673c07

File tree

7 files changed

+222
-37
lines changed

7 files changed

+222
-37
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: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,28 @@ 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
*
@@ -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: 32 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,24 @@ 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)) throw new Error(`${value} is not a valid number`);
1090+
if (!Object.values(MetricUnits).includes(unit))
1091+
throw new Error(
1092+
`Invalid metric unit '${unit}', expected either option: ${Object.values(MetricUnits).join(',')}`
1093+
);
1094+
if (!Object.values(MetricResolutions).includes(resolution))
1095+
throw new Error(
1096+
`Invalid metric resolution '${resolution}', expected either option: ${Object.values(MetricResolutions).join(',')}`
1097+
);
1098+
10701099
if (Object.keys(this.storedMetrics).length >= MAX_METRICS_SIZE) {
10711100
this.publishStoredMetrics();
10721101
}

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: 76 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,78 @@ 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('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+
`Invalid metric unit 'invalid-unit', expected either option: ${Object.values(MetricUnit).join(',')}`
331+
);
332+
});
333+
334+
it('throws when an invalid resolution is passed', () => {
335+
// Prepare
336+
const metrics = new Metrics();
337+
338+
// Act & Assess
339+
expect(() =>
340+
// @ts-expect-error - Testing runtime behavior with invalid metric unit
341+
metrics.addMetric('test', MetricUnit.Count, 1, 'invalid-resolution')
342+
).toThrowError(
343+
`Invalid metric resolution 'invalid-resolution', expected either option: ${Object.values(MetricResolution).join(',')}`
344+
);
345+
});
270346
});

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

Lines changed: 47 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -445,26 +445,32 @@ describe('Working with dimensions', () => {
445445
});
446446

447447
it.each([
448-
{ value: undefined, name: 'undefined' },
449-
{ value: null, name: 'null' },
448+
{ value: undefined, name: 'valid-name' },
449+
{ value: null, name: 'valid-name' },
450450
{
451451
value: '',
452-
name: 'empty string',
452+
name: 'valid-name',
453453
},
454-
])('skips invalid dimension values ($name)', ({ value }) => {
454+
{ value: 'valid-value', name: undefined },
455+
{ value: 'valid-value', name: null },
456+
{
457+
value: 'valid-value',
458+
name: '',
459+
},
460+
])('skips invalid dimension values ($name)', ({ value, name }) => {
455461
// Prepare
456462
const metrics = new Metrics({
457463
singleMetric: true,
458464
namespace: DEFAULT_NAMESPACE,
459465
});
460466

461467
// Act & Assess
462-
metrics.addDimension('test', value as string);
468+
metrics.addDimension(name as string, value as string);
463469
metrics.addMetric('test', MetricUnit.Count, 1);
464470

465471
expect(console.warn).toHaveBeenCalledTimes(1);
466472
expect(console.warn).toHaveBeenCalledWith(
467-
`The dimension test doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
473+
`The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
468474
);
469475
expect(console.log).toHaveEmittedEMFWith(
470476
expect.not.objectContaining({ test: value })
@@ -475,37 +481,44 @@ describe('Working with dimensions', () => {
475481
});
476482

477483
it.each([
478-
{ value: undefined, name: 'undefined' },
479-
{ value: null, name: 'null' },
484+
{ value: undefined, name: 'valid-name' },
485+
{ value: null, name: 'valid-name' },
480486
{
481487
value: '',
482-
name: 'empty string',
488+
name: 'valid-name',
483489
},
484-
])('skips invalid dimension values in addDimensions ($name)', ({ value }) => {
485-
// Prepare
486-
const metrics = new Metrics({
487-
singleMetric: true,
488-
namespace: DEFAULT_NAMESPACE,
489-
});
490-
491-
// Act & Assess
492-
metrics.addDimensions({
493-
validDimension: 'valid',
494-
invalidDimension: value as string,
495-
});
496-
metrics.addMetric('test', MetricUnit.Count, 1);
497-
metrics.publishStoredMetrics();
498-
499-
expect(console.warn).toHaveBeenCalledWith(
500-
`The dimension invalidDimension doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
501-
);
502-
expect(console.log).toHaveEmittedEMFWith(
503-
expect.objectContaining({ validDimension: 'valid' })
504-
);
505-
expect(console.log).toHaveEmittedEMFWith(
506-
expect.not.objectContaining({ invalidDimension: value })
507-
);
508-
});
490+
{
491+
value: 'valid-value',
492+
name: '',
493+
},
494+
])(
495+
'skips invalid dimension values in addDimensions ($name)',
496+
({ value, name }) => {
497+
// Prepare
498+
const metrics = new Metrics({
499+
singleMetric: true,
500+
namespace: DEFAULT_NAMESPACE,
501+
});
502+
503+
// Act & Assess
504+
metrics.addDimensions({
505+
validDimension: 'valid',
506+
[name as string]: value as string,
507+
});
508+
metrics.addMetric('test', MetricUnit.Count, 1);
509+
metrics.publishStoredMetrics();
510+
511+
expect(console.warn).toHaveBeenCalledWith(
512+
`The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
513+
);
514+
expect(console.log).toHaveEmittedEMFWith(
515+
expect.objectContaining({ validDimension: 'valid' })
516+
);
517+
expect(console.log).toHaveEmittedEMFWith(
518+
expect.not.objectContaining({ invalidDimension: value })
519+
);
520+
}
521+
);
509522

510523
it('warns when addDimensions overwrites existing dimensions', () => {
511524
// Prepare

0 commit comments

Comments
 (0)