Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

aid alarms-handler testability by making the main logic a more pure function #2635

Merged
merged 4 commits into from
Jan 23, 2025

Conversation

johnduffell
Copy link
Member

@johnduffell johnduffell commented Jan 15, 2025

A while ago I proposed a new feature in alarms handler to send relevant log links to chat as well.

I made a PR but it got very cluttered with pre-refactoring so I will pull out individual changes to make it easier to review.

This is the first PR
Second PR is here: #2636
Third PR is here: #2641

Regarding this PR

  1. the new tests I needed required parameterising the alarm mappings, so I pulled out the main lambda logic into a function and added that parameter
  2. To make it easier to check what messages should be sent, I made the above return the message list rather than having to trap the HTTP fetch callouts. i.e. pure-ish function, and then the effects can sit at the edge.
  3. As part of that, some duplication naturally fell out between the code paths.

Tested in CODE and still sends alarms

@johnduffell johnduffell changed the title aid testability by changing the logic to be an effectful crust around… aid alarms-handler testability by making the main logic a more pure function Jan 15, 2025
Comment on lines +141 to +144
export type AlarmMappings = {
getTeams: (appName?: string) => Team[];
getTeamWebhookUrl: (team: Team) => string;
};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the previous PR I did a class as that gives you the type for free, but I wasn't sure how to do similar as function based (assuming this is even what was meant) - perhaps there's a typeof trick I should be using rather than replicating the function signatures here?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think defining the type to be returned by the function here is good.
Technically this could be an interface though.

Regarding "replicating the function signatures" - I think what you have here is fine. It is possible to reference the type of a field as follows (though I don't think it's necessary here):
const getTeams: AlarmMappings['getTeams'] = (appName) => {...}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks Tom for the link and I will leave it as it is then, I agree on the referencing thing.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the changes in this file are simply to make the mappings parameterised rather than being a singleton.

@@ -62,6 +63,25 @@ describe('Handler', () => {
expect(getAppNameTag).toHaveBeenCalledWith('mock-arn', '111111');
expect(fetch).toHaveBeenCalledWith(mockEnv.SRE_WEBHOOK, expect.any(Object));
});
it('should handle captured CloudWatch alarm message', async () => {
(getAppNameTag as jest.Mock).mockResolvedValueOnce('mock-app');
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we still have one "mocked" function but it's more minor functionality and doesn't lend itself to parameterising as easily as the alarm mappings.

export async function getChatMessages(
record: SQSRecord,
alarmMappings: AlarmMappings,
) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return type?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's Promise<{webhookUrls: string[], text: string} | undefined>

Last year someone in the team told me in typescript you are supposed to avoid specifying return types as it can break your code at run time. Someone sent me some videos about it which I watched, but I forgot the exact reasoning, and can't find them any more. Perhaps some of these? https://marcoheine.com/today-i-learned/dont-use-return-types-in-typescript

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've not heard that before...
In general if the return type isn't obvious then it's helpful to other humans to specify it. In this case it wasn't obvious to me

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great in that case I've added it in!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

aha here was the original note suggesting to avoid them which I clearly took some time to digest! #2317 (comment)

Copy link
Member

@tjmw tjmw left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good! I left a bunch of pretty minor comments/suggestions to take or leave.

};

export const ProdAlarmMappings = buildAlarmMappings(teamToAppMappings);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe just me, but I feel like it's slightly unconventional to have this be camel case with the first letter in caps (which makes me think it's a class name or even a type). I'd probably go for prodAlarmMappings. Obviously a very minor thing so no big deal either way!

Comment on lines +77 to +79
} else {
return undefined;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally subjective, but we don't need this else block I don't think. With no explicit return undefined will be returned implicitly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

got it, so it's like of like mapping an Option in scala

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried that but it complains, not sure if I need to change something else?
image

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh weird sorry about that. Maybe it's the fact that the return value is wrapped in a promise? Best to leave it as is (which I think you've done!)

Copy link
Member

@tjmw tjmw Jan 23, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually I'm just wrong here - even without the promise wrapping I see the same thing in the TS playground:

Screenshot 2025-01-23 at 11 53 32

record: SQSRecord,
alarmMappings: AlarmMappings,
): Promise<{ webhookUrls: string[]; text: string } | undefined> {
console.log('sqsRecord', record);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should these console.logs be removed before merging?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I intentionally left them as I think this is the correct way to log things in cloudwatch in a JS lambda, if that's right?


let message;
if (parsedMessage) {
message = await handleCloudWatchAlarmMessage({ message: parsedMessage });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if handle is the right prefix for this method now? To me it kind of implies actually doing something with the message. What about buildCloudwatchAlarmMessage?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good spot, same for buildSnsPublishMessage


if (message) {
const teams = alarmMappings.getTeams(message.app);
console.log('sending message to teams', teams);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep this log line, what about mapping message to teams since we aren't actually sending it here?

Suggested change
console.log('sending message to teams', teams);
console.log('mapping message to teams', teams);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thanks, went for
console.log(app ${message.app} is assigned to teams ${teams.join(', ')});
in the end

});
}),
);
return { app, text };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally subjective, and very minor, but rather than assigning to a new const I'd probably just map from one name to the other in the return:

Suggested change
return { app, text };
return { app, text: message };

@johnduffell
Copy link
Member Author

This looks good! I left a bunch of pretty minor comments/suggestions to take or leave.

Thanks this is exactly the kind of thing that's useful, I've made all your suggestions apart from the undefined one as that broke the type checker/linter. In retrospect I think it's kind of nice to be explicit, in scala I'd use an Option but I don't think that's usual TS.

@johnduffell johnduffell merged commit 7a662d6 into main Jan 23, 2025
49 checks passed
@johnduffell johnduffell deleted the jd-alarms-handler-testability branch January 23, 2025 10:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants