Skip to content
Merged
50 changes: 45 additions & 5 deletions packages/metrics/src/Metrics.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { Console } from 'node:console';
import {
isIntegerNumber,
isNullOrUndefined,
isNumber,
isRecord,
isString,
isStringUndefinedNullEmpty,
Utility,
Expand Down Expand Up @@ -240,8 +242,10 @@ class Metrics extends Utility implements MetricsInterface {
super();

this.dimensions = {};
this.setOptions(options);
this.setEnvConfig();
this.setConsole();
this.#logger = options.logger || this.console;
this.setOptions(options);
}

/**
Expand Down Expand Up @@ -824,13 +828,51 @@ class Metrics extends Utility implements MetricsInterface {
* @param dimensions - The dimensions to be added to the default dimensions object
*/
public setDefaultDimensions(dimensions: Dimensions | undefined): void {
if (isNullOrUndefined(dimensions)) {
this.#logger.warn(
'No dimensions were supplied to setDefaultDimensions. Skipping update.'
);
return;
}

if (!isRecord(dimensions)) {
this.#logger.warn(
'Invalid dimensions type provided to setDefaultDimensions. Expected an object.'
);
return;
}

const cleanedDimensions: Dimensions = {};

for (const [key, value] of Object.entries(dimensions)) {
if (
isStringUndefinedNullEmpty(key) ||
isStringUndefinedNullEmpty(value)
) {
this.#logger.warn(
`The dimension ${key} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
);
continue;
}

if (Object.hasOwn(this.defaultDimensions, key)) {
this.#logger.warn(
`Dimension "${key}" has already been added. The previous value will be overwritten.`
);
}

cleanedDimensions[key] = value;
}

const targetDimensions = {
...this.defaultDimensions,
...dimensions,
...cleanedDimensions,
};
if (MAX_DIMENSION_COUNT <= Object.keys(targetDimensions).length) {

if (Object.keys(targetDimensions).length >= MAX_DIMENSION_COUNT) {
throw new Error('Max dimension count hit');
}

this.defaultDimensions = targetDimensions;
}

Expand Down Expand Up @@ -1058,8 +1100,6 @@ class Metrics extends Utility implements MetricsInterface {
functionName,
} = options;

this.setEnvConfig();
this.setConsole();
this.setCustomConfigService(customConfigService);
this.setDisabled();
this.setNamespace(namespace);
Expand Down
3 changes: 3 additions & 0 deletions packages/metrics/tests/unit/creatingMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ describe('Creating metrics', () => {
const metrics = new Metrics({
singleMetric: false,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
enviroment: 'test',
},
});

// Act
Expand Down
20 changes: 18 additions & 2 deletions packages/metrics/tests/unit/customTimestamp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ describe('Setting custom timestamp', () => {

it('logs a warning when the provided timestamp is too far in the past', () => {
// Prepare
const metrics = new Metrics({ singleMetric: true });
const metrics = new Metrics({
singleMetric: true,
defaultDimensions: {
environment: 'test',
},
});

// Act
metrics.setTimestamp(Date.now() - EMF_MAX_TIMESTAMP_PAST_AGE - 1000);
Expand All @@ -63,7 +68,12 @@ describe('Setting custom timestamp', () => {

it('logs a warning when the provided timestamp is too far in the future', () => {
// Prepare
const metrics = new Metrics({ singleMetric: true });
const metrics = new Metrics({
singleMetric: true,
defaultDimensions: {
environment: 'test',
},
});

// Act
metrics.setTimestamp(Date.now() + EMF_MAX_TIMESTAMP_FUTURE_AGE + 1000);
Expand All @@ -81,6 +91,9 @@ describe('Setting custom timestamp', () => {
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
environment: 'test',
},
});

// Act
Expand Down Expand Up @@ -108,6 +121,9 @@ describe('Setting custom timestamp', () => {
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
environment: 'test',
},
});

// Act
Expand Down
116 changes: 116 additions & 0 deletions packages/metrics/tests/unit/dimensions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,9 @@ describe('Working with dimensions', () => {
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
enviroment: 'test',
},
});

// Act & Assess
Expand Down Expand Up @@ -498,6 +501,9 @@ describe('Working with dimensions', () => {
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
enviroment: 'test',
},
});

// Act & Assess
Expand Down Expand Up @@ -552,4 +558,114 @@ describe('Working with dimensions', () => {
})
);
});

it('warns when setDefaultDimensions overwrites existing dimensions', () => {
// Prepare
const metrics = new Metrics({
namespace: DEFAULT_NAMESPACE,
defaultDimensions: { environment: 'prod' },
});

// Act
metrics.setDefaultDimensions({ region: 'us-east-1' });
metrics.setDefaultDimensions({
environment: 'staging', // overwrites default dimension
});

// Assess
expect(console.warn).toHaveBeenCalledOnce();
expect(console.warn).toHaveBeenCalledWith(
'Dimension "environment" has already been added. The previous value will be overwritten.'
);
});

it('logs a warning and returns immediately if dimensions is undefined', () => {
// Prepare
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
});

// Act
metrics.addMetric('myMetric', MetricUnit.Count, 1);

// Assert
expect(console.warn).toHaveBeenCalledWith(
'No dimensions were supplied to setDefaultDimensions. Skipping update.'
);

expect(console.log).toHaveEmittedEMFWith(
expect.objectContaining({
service: 'hello-world',
})
);
});

it.each([
{ value: undefined, name: 'valid-name' },
{ value: null, name: 'valid-name' },
{ value: '', name: 'valid-name' },
{ value: 'valid-value', name: '' },
])(
'skips invalid default dimension values in setDefaultDimensions ($name)',
({ value, name }) => {
// Arrange
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
enviroment: 'test',
},
});

// Act
metrics.setDefaultDimensions({
validDimension: 'valid',
[name as string]: value as string,
});

metrics.addMetric('test', MetricUnit.Count, 1);
metrics.publishStoredMetrics();

// Assert
expect(console.warn).toHaveBeenCalledWith(
`The dimension ${name} doesn't meet the requirements and won't be added. Ensure the dimension name and value are non empty strings`
);

expect(console.log).toHaveEmittedEMFWith(
expect.objectContaining({ validDimension: 'valid' })
);

expect(console.log).toHaveEmittedEMFWith(
expect.not.objectContaining({ [name]: value })
);
}
);
it('logs a warning and returns immediately if dimensions is not a plain object (fails isRecord)', () => {
// Prepare
const metrics = new Metrics({
singleMetric: true,
namespace: DEFAULT_NAMESPACE,
defaultDimensions: {
enviroment: 'test',
},
});

// Act
// @ts-expect-error – intentionally passing an invalid value to simulate bad input at runtime
metrics.setDefaultDimensions('invalid-dimensions');

// Assert
expect(console.warn).toHaveBeenCalledWith(
'Invalid dimensions type provided to setDefaultDimensions. Expected an object.'
);

metrics.addMetric('someMetric', MetricUnit.Count, 1);

expect(console.log).toHaveEmittedEMFWith(
expect.objectContaining({
service: 'hello-world',
})
);
});
});
7 changes: 6 additions & 1 deletion packages/metrics/tests/unit/initializeMetrics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ describe('Initialize Metrics', () => {

it('uses the default namespace when none is provided', () => {
// Prepare
const metrics = new Metrics({ singleMetric: true });
const metrics = new Metrics({
singleMetric: true,
defaultDimensions: {
environment: 'test',
},
});

// Act
metrics.addMetric('test', MetricUnit.Count, 1);
Expand Down