From 2e85103ccfe5f00423736cec74f14ecd35f3da50 Mon Sep 17 00:00:00 2001 From: AimeurAmin Date: Wed, 29 Oct 2025 19:53:57 +0000 Subject: [PATCH 1/4] adds slack webhook package --- .pnp.cjs | 1 + apps/gp2-server/package.json | 1 + yarn.lock | 1 + 3 files changed, 3 insertions(+) diff --git a/.pnp.cjs b/.pnp.cjs index b003539d654..5fa08be90c2 100755 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -13038,6 +13038,7 @@ function $$SETUP_STATE(hydrateRuntimeState, basePath) { ["@hapi/boom", "npm:9.1.4"],\ ["@sentry/serverless", "npm:6.19.7"],\ ["@serverless/typescript", "npm:3.38.0"],\ + ["@slack/webhook", "npm:6.1.0"],\ ["@types/apicache", "npm:1.6.8"],\ ["@types/apr-intercept", "workspace:@types/apr-intercept"],\ ["@types/aws-lambda", "npm:8.10.157"],\ diff --git a/apps/gp2-server/package.json b/apps/gp2-server/package.json index 027e3ab52a8..4e6a58b627f 100644 --- a/apps/gp2-server/package.json +++ b/apps/gp2-server/package.json @@ -38,6 +38,7 @@ "@graphql-typed-document-node/core": "3.2.0", "@hapi/boom": "9.1.4", "@sentry/serverless": "6.19.7", + "@slack/webhook": "^6.1.0", "@types/yargs": "17.0.5", "ajv": "8.11.0", "algoliasearch": "4.22.1", diff --git a/yarn.lock b/yarn.lock index 8d9b4a31e11..dcdea5a398a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1520,6 +1520,7 @@ __metadata: "@hapi/boom": 9.1.4 "@sentry/serverless": 6.19.7 "@serverless/typescript": 3.38.0 + "@slack/webhook": ^6.1.0 "@types/apicache": ^1 "@types/apr-intercept": "workspace:*" "@types/aws-lambda": 8.10.157 From 5b340afe9759bfde05b0044f4c3ff7d94a2ec470 Mon Sep 17 00:00:00 2001 From: AimeurAmin Date: Wed, 29 Oct 2025 19:54:04 +0000 Subject: [PATCH 2/4] links slack webhook to alert 5xx erros on GP2 API --- apps/gp2-server/serverless.ts | 52 +++++++++++++++++++ .../src/handlers/send-slack-alert.ts | 22 ++++++++ 2 files changed, 74 insertions(+) create mode 100644 apps/gp2-server/src/handlers/send-slack-alert.ts diff --git a/apps/gp2-server/serverless.ts b/apps/gp2-server/serverless.ts index 5cc50080248..b21b28d722b 100644 --- a/apps/gp2-server/serverless.ts +++ b/apps/gp2-server/serverless.ts @@ -48,6 +48,7 @@ const contentfulSpaceId = process.env.CONTENTFUL_SPACE_ID!; const contentfulWebhookAuthenticationToken = process.env.CONTENTFUL_WEBHOOK_AUTHENTICATION_TOKEN!; const openaiApiKey = process.env.OPENAI_API_KEY!; +const slackWebhook = process.env.SLACK_WEBHOOK!; if (stage === 'dev' || stage === 'production') { ['SENTRY_DSN_API', 'SENTRY_DSN_PUBLIC_API', 'SENTRY_DSN_HANDLERS'].forEach( @@ -253,6 +254,8 @@ const serverlessConfig: AWS = { bundle: true, concurrency: 8, }, + apiGateway5xxTopic: + '${self:service}-${self:provider.stage}-topic-api-gateway-5xx', s3Sync: [ { bucketName: '${self:service}-${self:provider.stage}-gp2-frontend', @@ -288,6 +291,20 @@ const serverlessConfig: AWS = { SENTRY_DSN: sentryDsnPublicApi, }, }, + sendSlackAlert: { + handler: './src/handlers/send-slack-alert.handler', + events: [ + { + sns: { + arn: { Ref: 'TopicCloudwatchAlarm' }, + topicName: '${self:custom.apiGateway5xxTopic}', + }, + }, + ], + environment: { + SLACK_WEBHOOK: slackWebhook, + }, + }, apiHandler: { handler: 'src/handlers/api-handler.apiHandler', events: [ @@ -1624,6 +1641,41 @@ const serverlessConfig: AWS = { MessageRetentionPeriod: 1209600, // 14 days }, }, + ApiGatewayAlarm5xx: { + Type: 'AWS::CloudWatch::Alarm', + Properties: { + AlarmDescription: '5xx errors detected at API Gateway', + Namespace: 'AWS/ApiGateway', + MetricName: '5xx', + Statistic: 'Sum', + Threshold: 0, + ComparisonOperator: 'GreaterThanThreshold', + EvaluationPeriods: 1, + Period: 60, + AlarmActions: [{ Ref: 'TopicCloudwatchAlarm' }], + TreatMissingData: 'notBreaching', + Dimensions: [ + { + Name: 'ApiId', + Value: { + Ref: 'HttpApi', + }, + }, + { + Name: 'Stage', + Value: { + Ref: 'HttpApiStage', + }, + }, + ], + }, + }, + TopicCloudwatchAlarm: { + Type: 'AWS::SNS::Topic', + Properties: { + TopicName: '${self:custom.apiGateway5xxTopic}', + }, + }, }, extensions: { SubscribeCalendarLambdaFunction: { diff --git a/apps/gp2-server/src/handlers/send-slack-alert.ts b/apps/gp2-server/src/handlers/send-slack-alert.ts new file mode 100644 index 00000000000..29ef9bf62c6 --- /dev/null +++ b/apps/gp2-server/src/handlers/send-slack-alert.ts @@ -0,0 +1,22 @@ +import { IncomingWebhook } from '@slack/webhook'; +// eslint-disable-next-line @typescript-eslint/no-non-null-assertion +const url = process.env.SLACK_WEBHOOK!; +const appEnv = process.env.ENVIRONMENT; +const region = process.env.AWS_REGION; + +export async function handler() { + if (!url) { + return; + } + + const webhook = new IncomingWebhook(url); + + await webhook.send({ + text: + `🚨 *GP2-${appEnv}*: API Gateway 5xx errors detected\n` + + `• Environment: ${appEnv}\n` + + `• Time: ${new Date().toISOString()}\n` + + `• [View CloudWatch Alarm](https://console.aws.amazon.com/cloudwatch/home?region=${region}#alarmsV2:alarm/API%20Gateway%205xx%20errors%20detected%20at%20API%20Gateway)\n` + + `• [View API Gateway](https://console.aws.amazon.com/apigateway/main/apis)`, + }); +} From 06ea52ac3fdca7b1c2a78ba9ef3bc2251684d099 Mon Sep 17 00:00:00 2001 From: AimeurAmin Date: Wed, 29 Oct 2025 20:15:38 +0000 Subject: [PATCH 3/4] adds slack alert test --- .../test/handlers/send-slack-alert.test.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 apps/gp2-server/test/handlers/send-slack-alert.test.ts diff --git a/apps/gp2-server/test/handlers/send-slack-alert.test.ts b/apps/gp2-server/test/handlers/send-slack-alert.test.ts new file mode 100644 index 00000000000..350316bf009 --- /dev/null +++ b/apps/gp2-server/test/handlers/send-slack-alert.test.ts @@ -0,0 +1,48 @@ +export {}; + +const mockSend = jest.fn(); + +jest.mock('@slack/webhook', () => { + return { + IncomingWebhook: jest.fn().mockImplementation((url) => { + return { + send: async (...args: any) => mockSend(...args), + }; + }), + }; +}); + +describe('sendSlackAlert (gp2)', () => { + const OLD_ENV = process.env; + + beforeEach(() => { + jest.resetModules(); // This clears the module cache + jest.clearAllMocks(); // Clear mock call history + process.env = { ...OLD_ENV }; + }); + + afterAll(() => { + process.env = OLD_ENV; + }); + + it('sends a message to slack with environment and links', async () => { + process.env.SLACK_WEBHOOK = 'https://example.com'; + process.env.ENVIRONMENT = 'test'; + process.env.AWS_REGION = 'us-east-1'; + + // Import AFTER setting up env vars and AFTER jest.resetModules() + const { handler } = await import('../../src/handlers/send-slack-alert'); + await handler(); + + expect(mockSend).toHaveBeenCalledTimes(1); + }); + + it('does not send message to slack, when webhook url is missing', async () => { + // Don't set SLACK_WEBHOOK env var + process.env.SLACK_WEBHOOK = ''; + const { handler } = await import('../../src/handlers/send-slack-alert'); + await handler(); + + expect(mockSend).not.toHaveBeenCalled(); + }); +}); From ed64b3e5d452938d5b6c3f2bd1d51b1bd7d5626d Mon Sep 17 00:00:00 2001 From: AimeurAmin Date: Thu, 6 Nov 2025 11:34:45 +0000 Subject: [PATCH 4/4] better logs with test fix --- apps/gp2-server/serverless.ts | 3 +- .../src/handlers/send-slack-alert.ts | 33 +++++---- .../test/handlers/send-slack-alert.test.ts | 68 +++++++++++++++---- 3 files changed, 76 insertions(+), 28 deletions(-) diff --git a/apps/gp2-server/serverless.ts b/apps/gp2-server/serverless.ts index b21b28d722b..0b72db70eab 100644 --- a/apps/gp2-server/serverless.ts +++ b/apps/gp2-server/serverless.ts @@ -48,7 +48,7 @@ const contentfulSpaceId = process.env.CONTENTFUL_SPACE_ID!; const contentfulWebhookAuthenticationToken = process.env.CONTENTFUL_WEBHOOK_AUTHENTICATION_TOKEN!; const openaiApiKey = process.env.OPENAI_API_KEY!; -const slackWebhook = process.env.SLACK_WEBHOOK!; +const slackWebhook = process.env.SLACK_WEBHOOK || ''; if (stage === 'dev' || stage === 'production') { ['SENTRY_DSN_API', 'SENTRY_DSN_PUBLIC_API', 'SENTRY_DSN_HANDLERS'].forEach( @@ -303,6 +303,7 @@ const serverlessConfig: AWS = { ], environment: { SLACK_WEBHOOK: slackWebhook, + ENVIRONMENT: '${env:SLS_STAGE}', }, }, apiHandler: { diff --git a/apps/gp2-server/src/handlers/send-slack-alert.ts b/apps/gp2-server/src/handlers/send-slack-alert.ts index 29ef9bf62c6..a3c3272f2e2 100644 --- a/apps/gp2-server/src/handlers/send-slack-alert.ts +++ b/apps/gp2-server/src/handlers/send-slack-alert.ts @@ -1,22 +1,31 @@ import { IncomingWebhook } from '@slack/webhook'; -// eslint-disable-next-line @typescript-eslint/no-non-null-assertion -const url = process.env.SLACK_WEBHOOK!; -const appEnv = process.env.ENVIRONMENT; -const region = process.env.AWS_REGION; +import logger from '../utils/logger'; export async function handler() { + const url = process.env.SLACK_WEBHOOK; + const appEnv = process.env.ENVIRONMENT; + const region = process.env.AWS_REGION; + if (!url) { + logger.warn('SLACK_WEBHOOK not configured, skipping alert'); return; } + // Validate that the webhook URL is a proper URL, not localhost + if (!url.startsWith('https://hooks.slack.com/')) { + throw new Error( + `Invalid SLACK_WEBHOOK configuration: URL must start with 'https://hooks.slack.com/' - current value: ${url}`, + ); + } + const webhook = new IncomingWebhook(url); - await webhook.send({ - text: - `🚨 *GP2-${appEnv}*: API Gateway 5xx errors detected\n` + - `• Environment: ${appEnv}\n` + - `• Time: ${new Date().toISOString()}\n` + - `• [View CloudWatch Alarm](https://console.aws.amazon.com/cloudwatch/home?region=${region}#alarmsV2:alarm/API%20Gateway%205xx%20errors%20detected%20at%20API%20Gateway)\n` + - `• [View API Gateway](https://console.aws.amazon.com/apigateway/main/apis)`, - }); + try { + await webhook.send({ + text: `🚨 *GP2-${appEnv}*: API Gateway 5xx errors detected\n• Environment: ${appEnv}\n• Time: ${new Date().toISOString()}\n• [View CloudWatch Alarm](https://console.aws.amazon.com/cloudwatch/home?region=${region}#alarmsV2:alarm/API%20Gateway%205xx%20errors%20detected%20at%20API%20Gateway)\n• [View API Gateway](https://console.aws.amazon.com/apigateway/main/apis)`, + }); + } catch (error) { + logger.warn('Failed to send Slack alert:', error); + throw error; + } } diff --git a/apps/gp2-server/test/handlers/send-slack-alert.test.ts b/apps/gp2-server/test/handlers/send-slack-alert.test.ts index 350316bf009..72220e1165c 100644 --- a/apps/gp2-server/test/handlers/send-slack-alert.test.ts +++ b/apps/gp2-server/test/handlers/send-slack-alert.test.ts @@ -1,6 +1,9 @@ export {}; const mockSend = jest.fn(); +const mockLogger = { + warn: jest.fn(), +}; jest.mock('@slack/webhook', () => { return { @@ -12,37 +15,72 @@ jest.mock('@slack/webhook', () => { }; }); -describe('sendSlackAlert (gp2)', () => { +jest.mock('../../src/utils/logger', () => ({ + __esModule: true, + default: mockLogger, +})); + +describe('sendSlackAlert', () => { const OLD_ENV = process.env; beforeEach(() => { - jest.resetModules(); // This clears the module cache - jest.clearAllMocks(); // Clear mock call history + jest.resetModules(); + jest.resetAllMocks(); process.env = { ...OLD_ENV }; }); - afterAll(() => { - process.env = OLD_ENV; - }); - - it('sends a message to slack with environment and links', async () => { - process.env.SLACK_WEBHOOK = 'https://example.com'; + it('sends message to slack', async () => { + process.env.SLACK_WEBHOOK = 'https://hooks.slack.com/services/TEST/WEBHOOK'; process.env.ENVIRONMENT = 'test'; process.env.AWS_REGION = 'us-east-1'; - // Import AFTER setting up env vars and AFTER jest.resetModules() const { handler } = await import('../../src/handlers/send-slack-alert'); + await handler(); - expect(mockSend).toHaveBeenCalledTimes(1); + expect(mockSend).toBeCalledWith({ + text: expect.stringContaining( + '🚨 *GP2-test*: API Gateway 5xx errors detected', + ), + }); }); - it('does not send message to slack, when webhook url is missing', async () => { - // Don't set SLACK_WEBHOOK env var - process.env.SLACK_WEBHOOK = ''; + it('does not sends message to slack, when webhook url is missing', async () => { const { handler } = await import('../../src/handlers/send-slack-alert'); + await handler(); - expect(mockSend).not.toHaveBeenCalled(); + expect(mockSend).not.toBeCalled(); + }); + + it('throws error when webhook URL is not a valid Slack URL', async () => { + process.env.SLACK_WEBHOOK = 'https://example.com/webhook'; + process.env.ENVIRONMENT = 'test'; + process.env.AWS_REGION = 'us-east-1'; + + const { handler } = await import('../../src/handlers/send-slack-alert'); + + await expect(handler()).rejects.toThrow( + "Invalid SLACK_WEBHOOK configuration: URL must start with 'https://hooks.slack.com/'", + ); + + expect(mockSend).not.toBeCalled(); + }); + + it('logs and re-throws error when sending to Slack fails', async () => { + process.env.SLACK_WEBHOOK = 'https://hooks.slack.com/services/TEST/WEBHOOK'; + process.env.ENVIRONMENT = 'test'; + process.env.AWS_REGION = 'us-east-1'; + + const sendError = new Error('Network error'); + mockSend.mockRejectedValueOnce(sendError); + + const { handler } = await import('../../src/handlers/send-slack-alert'); + + await expect(handler()).rejects.toThrow('Network error'); + expect(mockLogger.warn).toHaveBeenCalledWith( + 'Failed to send Slack alert:', + sendError, + ); }); });