Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e84bf1c
log EMF object for letter-status-update failure
Vlasis-Perdikidis Feb 10, 2026
3a38fc3
Merge branch 'main' into fix/CCM-14508-metricsNotTracked
masl2 Feb 10, 2026
460aa00
log separate entries for batch post letters
Vlasis-Perdikidis Feb 11, 2026
f3f93c9
merge main
Vlasis-Perdikidis Feb 11, 2026
56b8727
fix lint error
Vlasis-Perdikidis Feb 11, 2026
a69a638
log process.env for lambda runtime environment
Vlasis-Perdikidis Feb 12, 2026
aa23c01
stringify the process.env object
Vlasis-Perdikidis Feb 12, 2026
8f3d6e3
correct metrics for the rest of the lambdas
Vlasis-Perdikidis Feb 12, 2026
fa49df9
add another dimension to postLetters success
Vlasis-Perdikidis Feb 16, 2026
b0611b4
correct dimensions creation
Vlasis-Perdikidis Feb 16, 2026
0a5c4f9
clarify post-mi metrics
Vlasis-Perdikidis Feb 17, 2026
e08cdb3
record the quantity of the lineItems
Vlasis-Perdikidis Feb 17, 2026
c996b08
correct dimensions in transformers
Vlasis-Perdikidis Feb 17, 2026
29295a2
add metric name for mi-updates-transformer
Vlasis-Perdikidis Feb 17, 2026
58305e5
Merge branch 'main' into fix/CCM-14508-metricsNotTracked
masl2 Feb 17, 2026
a50cc00
add metrics to amendment-event-transformer
Vlasis-Perdikidis Feb 17, 2026
386431c
Merge branch 'main' into fix/CCM-14508-metricsNotTracked
Vlasis-Perdikidis Feb 17, 2026
9fdf330
resolve npm vulnerabilities
Vlasis-Perdikidis Feb 18, 2026
34b4f74
move metrics to internal folder
Vlasis-Perdikidis Feb 23, 2026
c487d5d
use metrics from internal helpers folder
Vlasis-Perdikidis Feb 23, 2026
1fa3bc9
use metrics from internal helpers folder for amendment-event-transformer
Vlasis-Perdikidis Feb 24, 2026
231a354
revert minimatch to original version
Vlasis-Perdikidis Feb 24, 2026
0cd4fe2
merge main
Vlasis-Perdikidis Feb 24, 2026
e836a14
npm ci
Vlasis-Perdikidis Feb 24, 2026
c4d892e
resolve unit test and lint failures
Vlasis-Perdikidis Feb 24, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ import { SQSBatchItemFailure, SQSEvent, SQSHandler } from "aws-lambda";
import { PublishCommand } from "@aws-sdk/client-sns";
import { LetterEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-events";
import { mapLetterToCloudEvent } from "@nhsdigital/nhs-notify-event-schemas-supplier-api/src/events/letter-mapper";
import { Unit } from "aws-embedded-metrics";
import pino from "pino";
import {
UpdateLetterCommand,
UpdateLetterCommandSchema,
} from "../contracts/letters";
import { Deps } from "../config/deps";
import { MetricEntry, MetricStatus, buildEMFObject } from "../utils/metrics";

export default function createTransformAmendmentEventHandler(
deps: Deps,
Expand Down Expand Up @@ -39,6 +42,11 @@ export default function createTransformAmendmentEventHandler(
messageId: message.messageId,
correlationId: message.messageAttributes.CorrelationId.stringValue,
});
emitSuccessMetrics(
updateLetterCommand.supplierId,
updateLetterCommand.status,
deps.logger,
);
} catch (error) {
deps.logger.error({
description: "Error processing letter status update",
Expand All @@ -52,7 +60,7 @@ export default function createTransformAmendmentEventHandler(
});

await Promise.all(tasks);

emitFailedItems(batchItemFailures, deps.logger);
return { batchItemFailures };
};
}
Expand All @@ -66,3 +74,43 @@ function buildSnsCommand(
Message: JSON.stringify(letterEvent),
});
}

function emitSuccessMetrics(
supplierId: string,
status: string,
logger: pino.Logger,
) {
const dimensions: Record<string, string> = {
supplier: supplierId,
status,
};
const metric: MetricEntry = {
key: MetricStatus.Success,
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject("amendment-event-transformer", dimensions, metric);
logger.info(emf);
}

function emitFailedItems(
batchFailures: SQSBatchItemFailure[],
logger: pino.Logger,
) {
for (const item of batchFailures) {
const dimensions: Record<string, string> = {
identifier: item.itemIdentifier,
};
const metric: MetricEntry = {
key: MetricStatus.Failure,
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject(
"amendment-event-transformer",
dimensions,
metric,
);
logger.info(emf);
}
}
1 change: 0 additions & 1 deletion lambdas/api-handler/src/handlers/get-letters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { mapToGetLettersResponse } from "../mappers/letter-mapper";
import type { Deps } from "../config/deps";
import { MetricStatus, emitForSingleSupplier } from "../utils/metrics";

// List letters Handlers
// The endpoint should only return pending letters for now
const status = "PENDING";

Expand Down
14 changes: 10 additions & 4 deletions lambdas/api-handler/src/handlers/patch-letter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export default function createPatchLetterHandler(
try {
patchLetterRequest = PatchLetterRequestSchema.parse(JSON.parse(body));
} catch (error) {
emitErrorMetric(metrics, supplierId);
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
Expand All @@ -79,6 +80,7 @@ export default function createPatchLetterHandler(
);

if (updateLetterCommand.id !== letterId) {
emitErrorMetric(metrics, supplierId);
throw new ValidationError(
ApiErrorDetail.InvalidRequestLetterIdsMismatch,
);
Expand All @@ -100,12 +102,16 @@ export default function createPatchLetterHandler(
body: "",
};
} catch (error) {
metrics.putDimensions({
supplier: supplierId,
});
metrics.putMetric(MetricStatus.Success, 1, Unit.Count);
emitErrorMetric(metrics, supplierId);
return processError(error, commonIds.value.correlationId, deps.logger);
}
};
});
}

function emitErrorMetric(metrics: MetricsLogger, supplierId: string) {
metrics.putDimensions({
supplier: supplierId,
});
metrics.putMetric(MetricStatus.Failure, 1, Unit.Count);
}
186 changes: 99 additions & 87 deletions lambdas/api-handler/src/handlers/post-letters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { APIGatewayProxyHandler } from "aws-lambda";
import { MetricsLogger, Unit, metricScope } from "aws-embedded-metrics";
import { Unit } from "aws-embedded-metrics";
import pino from "pino";
import type { Deps } from "../config/deps";
import { ApiErrorDetail } from "../contracts/errors";
import {
Expand All @@ -13,7 +14,7 @@ import { mapToUpdateCommands } from "../mappers/letter-mapper";
import { enqueueLetterUpdateRequests } from "../services/letter-operations";
import { extractCommonIds } from "../utils/common-ids";
import { assertNotEmpty, requireEnvVar } from "../utils/validation";
import { MetricStatus } from "../utils/metrics";
import { MetricEntry, MetricStatus, buildEMFObject } from "../utils/metrics";

function duplicateIdsExist(postLettersRequest: PostLettersRequest) {
const ids = postLettersRequest.data.map((item) => item.id);
Expand All @@ -23,17 +24,23 @@ function duplicateIdsExist(postLettersRequest: PostLettersRequest) {
/**
* emits metrics of successful letter updates, including the supplier and grouped by status
*/
function emitMetics(
metrics: MetricsLogger,
function emitSuccessMetrics(
supplierId: string,
statusesMapping: Map<string, number>,
logger: pino.Logger,
) {
for (const [status, count] of statusesMapping) {
metrics.putDimensions({
const dimensions: Record<string, string> = {
supplier: supplierId,
eventType: status,
});
metrics.putMetric(MetricStatus.Success, count, Unit.Count);
status,
};
const metric: MetricEntry = {
key: MetricStatus.Success,
value: count,
unit: Unit.Count,
};
const emf = buildEMFObject("postLetters", dimensions, metric);
logger.info(emf);
}
}

Expand All @@ -48,92 +55,97 @@ function populateStatusesMap(updateLetterCommands: UpdateLetterCommand[]) {
export default function createPostLettersHandler(
deps: Deps,
): APIGatewayProxyHandler {
return metricScope((metrics: MetricsLogger) => {
return async (event) => {
const commonIds = extractCommonIds(
event.headers,
event.requestContext,
deps,
);
return async (event) => {
const commonIds = extractCommonIds(
event.headers,
event.requestContext,
deps,
);

if (!commonIds.ok) {
return processError(
commonIds.error,
commonIds.correlationId,
deps.logger,
);
}
if (!commonIds.ok) {
return processError(
commonIds.error,
commonIds.correlationId,
deps.logger,
);
}

const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT");
requireEnvVar(deps.env, "QUEUE_URL");
const maxUpdateItems = requireEnvVar(deps.env, "MAX_LIMIT");
requireEnvVar(deps.env, "QUEUE_URL");

const { supplierId } = commonIds.value;
metrics.setNamespace(
process.env.AWS_LAMBDA_FUNCTION_NAME || "postLetters",
const { supplierId } = commonIds.value;
try {
const body = assertNotEmpty(
event.body,
new ValidationError(ApiErrorDetail.InvalidRequestMissingBody),
);

let postLettersRequest: PostLettersRequest;

try {
const body = assertNotEmpty(
event.body,
new ValidationError(ApiErrorDetail.InvalidRequestMissingBody),
);
postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body));
} catch (error) {
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
cause: error,
})
: error;
throw typedError;
}

let postLettersRequest: PostLettersRequest;

try {
postLettersRequest = PostLettersRequestSchema.parse(JSON.parse(body));
} catch (error) {
const typedError =
error instanceof Error
? new ValidationError(ApiErrorDetail.InvalidRequestBody, {
cause: error,
})
: error;
throw typedError;
}

deps.logger.info({
description: "Received post letters request",
supplierId: commonIds.value.supplierId,
letterIds: postLettersRequest.data.map((letter) => letter.id),
correlationId: commonIds.value.correlationId,
});

if (postLettersRequest.data.length > maxUpdateItems) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestLettersToUpdate,
{ args: [maxUpdateItems] },
);
}

if (duplicateIdsExist(postLettersRequest)) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestDuplicateLetterId,
);
}

const updateLetterCommands: UpdateLetterCommand[] = mapToUpdateCommands(
postLettersRequest,
supplierId,
);
const statusesMapping = populateStatusesMap(updateLetterCommands);
await enqueueLetterUpdateRequests(
updateLetterCommands,
commonIds.value.correlationId,
deps,
deps.logger.info({
description: "Received post letters request",
supplierId: commonIds.value.supplierId,
letterIds: postLettersRequest.data.map((letter) => letter.id),
correlationId: commonIds.value.correlationId,
});

if (postLettersRequest.data.length > maxUpdateItems) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestLettersToUpdate,
{ args: [maxUpdateItems] },
);
}

emitMetics(metrics, supplierId, statusesMapping);
return {
statusCode: 202,
body: "",
};
} catch (error) {
metrics.putDimensions({
supplier: supplierId,
});
metrics.putMetric(MetricStatus.Failure, 1, Unit.Count);
return processError(error, commonIds.value.correlationId, deps.logger);
if (duplicateIdsExist(postLettersRequest)) {
throw new ValidationError(
ApiErrorDetail.InvalidRequestDuplicateLetterId,
);
}
};
});

const updateLetterCommands: UpdateLetterCommand[] = mapToUpdateCommands(
postLettersRequest,
supplierId,
);
const statusesMapping = populateStatusesMap(updateLetterCommands);
await enqueueLetterUpdateRequests(
updateLetterCommands,
commonIds.value.correlationId,
deps,
);

emitSuccessMetrics(supplierId, statusesMapping, deps.logger);
return {
statusCode: 202,
body: "",
};
} catch (error) {
// error metrics
emitErrorMetrics(supplierId, deps.logger);

return processError(error, commonIds.value.correlationId, deps.logger);
}
};
}

function emitErrorMetrics(supplierId: string, logger: pino.Logger) {
const dimensions: Record<string, string> = { supplier: supplierId };
const metric: MetricEntry = {
key: MetricStatus.Failure,
value: 1,
unit: Unit.Count,
};
const emf = buildEMFObject("postLetters", dimensions, metric);
logger.info(emf);
}
Loading
Loading