Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
NODE_ENV=

RETRYABLE_MONITORING_SLACK_WEBHOOK_URL=
RETRYABLE_MONITORING_SLACK_TOKEN=
RETRYABLE_MONITORING_SLACK_CHANNEL=
RETRYABLE_MONITORING_PRIVATE_KEY=

BATCH_POSTER_MONITORING_SLACK_WEBHOOK_URL=
BATCH_POSTER_MONITORING_SLACK_TOKEN=
BATCH_POSTER_MONITORING_SLACK_CHANNEL=

ASSERTION_MONITORING_SLACK_WEBHOOK_URL=
ASSERTION_MONITORING_SLACK_TOKEN=
ASSERTION_MONITORING_SLACK_CHANNEL=
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,17 @@ cp config.example.json config.json
cp .env.sample .env
```

2. Set up Slack alerts in `.env` (optional):
2. Set up Slack alerts in `.env` (optional). For each monitor, set either a webhook URL (preferred, scoped to a single channel) or a token + channel; the webhook takes precedence if both are set:

```bash
NODE_ENV=CI
RETRYABLE_MONITORING_SLACK_WEBHOOK_URL=your-slack-webhook-url
RETRYABLE_MONITORING_SLACK_TOKEN=your-slack-token
RETRYABLE_MONITORING_SLACK_CHANNEL=your-slack-channel
BATCH_POSTER_MONITORING_SLACK_WEBHOOK_URL=your-slack-webhook-url
BATCH_POSTER_MONITORING_SLACK_TOKEN=your-slack-token
BATCH_POSTER_MONITORING_SLACK_CHANNEL=your-slack-channel
ASSERTION_MONITORING_SLACK_WEBHOOK_URL=your-slack-webhook-url
ASSERTION_MONITORING_SLACK_TOKEN=your-slack-token
ASSERTION_MONITORING_SLACK_CHANNEL=your-slack-channel
```
Expand Down
1 change: 1 addition & 0 deletions packages/assertion-monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Examples:
yarn assertion-monitor --configPath=custom.json Use custom config file

Environment Variables:
ASSERTION_MONITORING_SLACK_WEBHOOK_URL Slack webhook URL for alerts (preferred over token+channel)
ASSERTION_MONITORING_SLACK_TOKEN Slack API token for alerts
ASSERTION_MONITORING_SLACK_CHANNEL Slack channel for alerts
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSlackPoster } from 'utils'

export const reportAssertionMonitorErrorToSlack = createSlackPoster({
webhookUrlEnvVar: 'ASSERTION_MONITORING_SLACK_WEBHOOK_URL',
tokenEnvVar: 'ASSERTION_MONITORING_SLACK_TOKEN',
channelEnvVar: 'ASSERTION_MONITORING_SLACK_CHANNEL',
})
1 change: 1 addition & 0 deletions packages/batch-poster-monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Examples:
yarn batch-poster-monitor --configPath=custom.json Use custom config file

Environment Variables:
BATCH_POSTER_MONITORING_SLACK_WEBHOOK_URL Slack webhook URL for alerts (preferred over token+channel)
BATCH_POSTER_MONITORING_SLACK_TOKEN Slack API token for alerts
BATCH_POSTER_MONITORING_SLACK_CHANNEL Slack channel for alerts
```
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSlackPoster } from 'utils'

export const reportBatchPosterErrorToSlack = createSlackPoster({
webhookUrlEnvVar: 'BATCH_POSTER_MONITORING_SLACK_WEBHOOK_URL',
tokenEnvVar: 'BATCH_POSTER_MONITORING_SLACK_TOKEN',
channelEnvVar: 'BATCH_POSTER_MONITORING_SLACK_CHANNEL',
})
1 change: 1 addition & 0 deletions packages/retryable-monitor/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Examples:
yarn retryable-monitor --enableAlerting --writeToNotion Enables Slack alerts and syncs retryable data to Notion

Environment Variables:
RETRYABLE_MONITORING_SLACK_WEBHOOK_URL Slack webhook URL for alerts (preferred over token+channel)
RETRYABLE_MONITORING_SLACK_TOKEN Slack API token for alerts
RETRYABLE_MONITORING_SLACK_CHANNEL Slack channel for alerts
RETRYABLE_MONITORING_NOTION_TOKEN Notion integration token
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createSlackPoster } from 'utils'

export const postSlackMessage = createSlackPoster({
webhookUrlEnvVar: 'RETRYABLE_MONITORING_SLACK_WEBHOOK_URL',
tokenEnvVar: 'RETRYABLE_MONITORING_SLACK_TOKEN',
channelEnvVar: 'RETRYABLE_MONITORING_SLACK_CHANNEL',
})
112 changes: 112 additions & 0 deletions packages/utils/__test__/createSlackPoster.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ vi.mock('../postSlackMessage', () => ({
postSlackMessage: vi.fn().mockResolvedValue({ ok: true }),
}))

vi.mock('../postSlackMessageViaWebhook', () => ({
postSlackMessageViaWebhook: vi.fn().mockResolvedValue({ ok: true }),
}))

import { postSlackMessage } from '../postSlackMessage'
import { postSlackMessageViaWebhook } from '../postSlackMessageViaWebhook'

describe('createSlackPoster', () => {
const originalEnv = process.env
Expand All @@ -15,6 +20,7 @@ describe('createSlackPoster', () => {
process.env = { ...originalEnv }
delete process.env.TEST_SLACK_TOKEN
delete process.env.TEST_SLACK_CHANNEL
delete process.env.TEST_SLACK_WEBHOOK_URL
delete process.env.NODE_ENV
})

Expand All @@ -40,6 +46,84 @@ describe('createSlackPoster', () => {
expect(() => poster({ message: 'test' })).toThrow('Slack channel is required.')
})

test('throws with either-or message when webhook is supported but nothing is configured', () => {
const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})
expect(() => poster({ message: 'test' })).toThrow(
'Slack configuration is required: set TEST_SLACK_WEBHOOK_URL (preferred) or both TEST_SLACK_TOKEN and TEST_SLACK_CHANNEL.'
)
})

test('throws with either-or message when webhook is supported and only token is set', () => {
process.env.TEST_SLACK_TOKEN = 'token'
const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})
expect(() => poster({ message: 'test' })).toThrow(
'Slack configuration is required: set TEST_SLACK_WEBHOOK_URL (preferred) or both TEST_SLACK_TOKEN and TEST_SLACK_CHANNEL.'
)
})

test('posts via webhook when only webhook is configured', () => {
process.env.TEST_SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/x'
const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})

poster({ message: 'Hello' })

expect(postSlackMessageViaWebhook).toHaveBeenCalledWith({
webhookUrl: 'https://hooks.slack.com/services/x',
message: 'Hello',
})
expect(postSlackMessage).not.toHaveBeenCalled()
})

test('webhook takes precedence when both webhook and token+channel are configured', () => {
process.env.TEST_SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/x'
process.env.TEST_SLACK_TOKEN = 'token'
process.env.TEST_SLACK_CHANNEL = 'channel'
const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})

poster({ message: 'Hello' })

expect(postSlackMessageViaWebhook).toHaveBeenCalledWith({
webhookUrl: 'https://hooks.slack.com/services/x',
message: 'Hello',
})
expect(postSlackMessage).not.toHaveBeenCalled()
})

test('falls back to token+channel when webhook is supported but not set', () => {
process.env.TEST_SLACK_TOKEN = 'token'
process.env.TEST_SLACK_CHANNEL = 'channel'
const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})

poster({ message: 'Hello' })

expect(postSlackMessage).toHaveBeenCalledWith({
slackToken: 'token',
slackChannel: 'channel',
message: 'Hello',
})
expect(postSlackMessageViaWebhook).not.toHaveBeenCalled()
})

test('skips posting in DEV mode', () => {
process.env.TEST_SLACK_TOKEN = 'token'
process.env.TEST_SLACK_CHANNEL = 'channel'
Expand All @@ -54,6 +138,20 @@ describe('createSlackPoster', () => {
expect(postSlackMessage).not.toHaveBeenCalled()
})

test('skips posting in DEV mode with webhook config', () => {
process.env.TEST_SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/x'
process.env.NODE_ENV = 'DEV'

const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})

expect(poster({ message: 'test' })).toBeUndefined()
expect(postSlackMessageViaWebhook).not.toHaveBeenCalled()
})

test('skips posting in CI mode when message is "success"', () => {
process.env.TEST_SLACK_TOKEN = 'token'
process.env.TEST_SLACK_CHANNEL = 'channel'
Expand All @@ -68,6 +166,20 @@ describe('createSlackPoster', () => {
expect(postSlackMessage).not.toHaveBeenCalled()
})

test('skips posting in CI mode when message is "success" with webhook config', () => {
process.env.TEST_SLACK_WEBHOOK_URL = 'https://hooks.slack.com/services/x'
process.env.NODE_ENV = 'CI'

const poster = createSlackPoster({
tokenEnvVar: 'TEST_SLACK_TOKEN',
channelEnvVar: 'TEST_SLACK_CHANNEL',
webhookUrlEnvVar: 'TEST_SLACK_WEBHOOK_URL',
})

expect(poster({ message: 'success' })).toBeUndefined()
expect(postSlackMessageViaWebhook).not.toHaveBeenCalled()
})

test('posts in CI mode when message is not "success"', () => {
process.env.TEST_SLACK_TOKEN = 'token'
process.env.TEST_SLACK_CHANNEL = 'channel'
Expand Down
28 changes: 24 additions & 4 deletions packages/utils/createSlackPoster.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
import { postSlackMessage } from './postSlackMessage'
import { postSlackMessageViaWebhook } from './postSlackMessageViaWebhook'

export const createSlackPoster = ({
tokenEnvVar,
channelEnvVar,
webhookUrlEnvVar,
}: {
tokenEnvVar: string
channelEnvVar: string
webhookUrlEnvVar?: string
}) => {
return ({ message }: { message: string }) => {
const slackWebhookUrl = webhookUrlEnvVar
? process.env[webhookUrlEnvVar]
: undefined
const slackToken = process.env[tokenEnvVar]
const slackChannel = process.env[channelEnvVar]

if (!slackToken) throw new Error('Slack token is required.')
if (!slackChannel) throw new Error('Slack channel is required.')
if (!slackWebhookUrl && !(slackToken && slackChannel)) {
if (webhookUrlEnvVar) {
throw new Error(
`Slack configuration is required: set ${webhookUrlEnvVar} (preferred) or both ${tokenEnvVar} and ${channelEnvVar}.`
)
}
if (!slackToken) throw new Error('Slack token is required.')
throw new Error('Slack channel is required.')
}

if (process.env.NODE_ENV === 'DEV') return
if (process.env.NODE_ENV === 'CI' && message === 'success') return

if (slackWebhookUrl) {
return postSlackMessageViaWebhook({
webhookUrl: slackWebhookUrl,
message,
})
}

return postSlackMessage({
slackToken,
slackChannel,
slackToken: slackToken as string,
slackChannel: slackChannel as string,
message,
})
}
Expand Down
1 change: 1 addition & 0 deletions packages/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './types'
export * from './config'
export { getExplorerUrlPrefixes } from './getExplorerUrlPrefixes'
export { postSlackMessage } from './postSlackMessage'
export { postSlackMessageViaWebhook } from './postSlackMessageViaWebhook'
export { createSlackPoster } from './createSlackPoster'
export { parseAmount } from './amountUtils'
export { resolveRollupAddress } from './resolveRollupAddress'
Expand Down
35 changes: 35 additions & 0 deletions packages/utils/postSlackMessageViaWebhook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { sanitizeSlackMessage } from './sanitizeSlackMessage'
import { formatGitHubCIInfo } from './githubCIUtils'

export const postSlackMessageViaWebhook = async ({
webhookUrl,
message,
}: {
webhookUrl: string
message: string
}) => {
// Append GitHub CI run information if available
const ciInfo = formatGitHubCIInfo()
const messageWithCIInfo = ciInfo ? `${message}\n\n${ciInfo}` : message

console.log(`>>> Posting message to Slack (webhook) -> ${messageWithCIInfo}`)

const response = await fetch(webhookUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: sanitizeSlackMessage(messageWithCIInfo),
unfurl_links: false,
}),
})

if (!response.ok) {
throw new Error(
`Failed to post Slack message via webhook: ${
response.status
} ${await response.text()}`
)
}

return response
}
Loading