Skip to content
Open
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
16 changes: 0 additions & 16 deletions apps/api/src/routes/v2/alert-destinations.http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import {
HazelAlertDestinationConfig,
HazelOAuthAlertDestinationConfig,
PagerDutyAlertDestinationConfig,
SlackAlertDestinationConfig,
SlackBotAlertDestinationConfig,
WebhookAlertDestinationConfig,
} from "@maple/domain/http"
Expand Down Expand Up @@ -44,14 +43,6 @@ const toV2DestinationMutation = (doc: AlertDestinationDocument): V2AlertDestinat

const toCreateRequest = (params: V2AlertDestinationCreateParams) => {
switch (params.type) {
case "slack":
return new SlackAlertDestinationConfig({
type: "slack",
name: params.name,
webhookUrl: params.webhook_url,
...(params.channel_label !== undefined ? { channelLabel: params.channel_label } : {}),
...(params.enabled !== undefined ? { enabled: params.enabled } : {}),
})
case "slack-bot":
return new SlackBotAlertDestinationConfig({
type: "slack-bot",
Expand Down Expand Up @@ -119,13 +110,6 @@ const toUpdateRequest = (params: V2AlertDestinationUpdateParams): AlertDestinati
...(params.enabled !== undefined ? { enabled: params.enabled } : {}),
}
switch (params.type) {
case "slack":
return {
type: "slack",
...shared,
...(params.webhook_url !== undefined ? { webhookUrl: params.webhook_url } : {}),
...(params.channel_label !== undefined ? { channelLabel: params.channel_label } : {}),
}
case "slack-bot":
return {
type: "slack-bot",
Expand Down
59 changes: 11 additions & 48 deletions apps/api/src/services/AlertDeliveryDispatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,13 +595,22 @@ export const verifyPagerDutyRoutingKey = (
* formatter) or when rendering fails for any reason — templating must never
* block delivery.
*/
/**
* The key a rule's notification-template `overrides` map is keyed by. It mostly
* mirrors `AlertDestinationType`, but `"slack"` is deliberately retained as the
* *dialect* for Slack deliveries even though the legacy `slack` webhook
* destination type is gone: it is the key already stored in customers' template
* overrides, and `slack-bot` renders the same mrkdwn blocks.
*/
type TemplateDialect = AlertDestinationType | "slack"

const renderTitleBody = (
context: TemplateRenderContext,
destinationType: AlertDestinationType,
dialect: TemplateDialect,
linkUrl: string,
chatUrl: string,
): { title: string; body: string } | null => {
const resolved = resolveTemplate(context.template, destinationType)
const resolved = resolveTemplate(context.template, dialect)
if (!hasCustomTemplate(resolved)) return null
try {
const templateCtx = buildTemplateContext(context, linkUrl, chatUrl)
Expand Down Expand Up @@ -684,52 +693,6 @@ export const dispatchDelivery = (
Effect.gen(function* () {
return yield* Match.value(context.secretConfig).pipe(
Match.discriminatorsExhaustive("type")({
slack: (config) =>
Effect.gen(function* () {
const templated = renderTitleBody(context, "slack", linkUrl, chatUrl)
const blocks = templated
? buildSlackBlocksFromTemplate(
templated.title,
templated.body,
context,
linkUrl,
chatUrl,
)
: buildSlackBlocks(context, linkUrl, chatUrl)
const response = yield* runTimedFetch("slack", "Slack", fetchFn, timeoutMs, () =>
safeFetch(config.webhookUrl, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({
// No top-level `text`: alongside attachments (and no top-level
// blocks) Slack renders it as a duplicate line above the color
// bar. `fallback` carries the notification-preview one-liner.
attachments: [
{
color: slackAttachmentColor(context.eventType, context.severity),
fallback: templated?.title ?? buildSlackFallbackText(context),
blocks,
},
],
}),
fetchFn,
}),
)
if (!response.ok) {
const detail = yield* readErrorBody(response)
return yield* Effect.fail(
makeDeliveryError(
`Slack delivery failed with ${response.status}${detail ? `: ${detail}` : ""}`,
"slack",
),
)
}
return {
providerMessage: "Delivered to Slack",
providerReference: null,
responseCode: response.status,
} as DispatchResult
}),
"slack-bot": (config) =>
Effect.gen(function* () {
const botToken = yield* deps.resolveSlackBotToken(context.destination.orgId)
Expand Down
7 changes: 4 additions & 3 deletions apps/api/src/services/AlertDestinationHydration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,13 +112,14 @@ describe("hydrateDestinationRow", () => {
const testDb = createTestDb(createdDbs)
const id = asDestinationId("00000000-0000-4000-8000-000000000002")
const secretConfig: DestinationSecretConfig = {
type: "slack",
webhookUrl: "https://hooks.slack.com/services/T000/B000/xxx",
type: "slack-bot",
channelId: "C0789CHAN",
channelName: "alerts",
}
return Effect.gen(function* () {
yield* seedDestination({
id,
type: "slack",
type: "slack-bot",
publicConfig: {
summary: "#alerts",
channelLabel: "#alerts",
Expand Down
4 changes: 0 additions & 4 deletions apps/api/src/services/AlertDestinationHydration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,6 @@ export const DestinationPublicConfigSchema = Schema.Struct({
})

const DestinationSecretConfigSchema = Schema.Union([
Schema.Struct({
type: Schema.Literal("slack"),
webhookUrl: Schema.String,
}),
Schema.Struct({
type: Schema.Literal("slack-bot"),
// No secret token here — the bot token is resolved from the org's
Expand Down
55 changes: 55 additions & 0 deletions apps/api/src/services/AlertsService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3024,3 +3024,58 @@ describe("AlertsService.previewRule", () => {
}).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub(state), { fetch: okFetch })))
})
})

// Rows written before a destination type was retired (the legacy `slack`
// webhook destination) are still in Postgres. Reads must tolerate them: a
// throwing decode on `alert_destinations.type` would defect, which surfaces as
// a bare 500 on every list for the whole org — not just for that one row.
describe("retired destination types", () => {
it.effect("skips an undecodable stored type instead of failing the destinations list", () => {
const testDb = createTestDb(trackedDbs)
return Effect.gen(function* () {
const alerts = yield* AlertsService
const orgId = asOrgId("org_legacy_type")
const userId = asUserId("user_legacy_type")
const legacy = yield* createWebhookDestination(alerts, orgId, userId)
const supported = yield* alerts.createDestination(orgId, userId, adminRoles, {
type: "discord",
name: "Discord",
enabled: true,
webhookUrl: "https://discord.com/api/webhooks/1/token",
})
const rule = yield* createErrorRateRule(alerts, orgId, userId, legacy.id)
yield* Effect.promise(() =>
executeSql(testDb, "update alert_destinations set type = 'slack' where id = $1", [
legacy.id,
]),
)
yield* Effect.promise(() =>
insertDeliveryEventRow(testDb, {
id: "00000000-0000-4000-8000-00000000dead",
orgId,
incidentId: null,
ruleId: rule.id,
destinationId: legacy.id,
deliveryKey: "legacy-delivery",
eventType: "trigger",
attemptNumber: 1,
status: "success",
scheduledAt: DEFAULT_CLOCK_EPOCH_MS,
payloadJson: JSON.stringify({}),
}),
)

const list = yield* alerts.listDestinations(orgId)
assert.deepStrictEqual(
list.destinations.map((destination) => destination.id),
[supported.id],
)

// Delivery history still renders; the retired type falls back the same
// way a deleted destination does.
const events = yield* alerts.listDeliveryEvents(orgId)
assert.lengthOf(events.events, 1)
assert.strictEqual(events.events[0]?.destinationType, "webhook")
}).pipe(Effect.provide(makeLayer(testDb, makeWarehouseStub({}), { fetch: okFetch })))
})
})
Loading
Loading