diff --git a/.github/scripts/notify-api-e2e-failure.sh b/.github/scripts/notify-api-e2e-failure.sh new file mode 100755 index 0000000000..2cc7ce84bc --- /dev/null +++ b/.github/scripts/notify-api-e2e-failure.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +set -euo pipefail + +: "${SLACK_WEBHOOK_URL:?}" +: "${RUN_URL:?}" +: "${EVENT_NAME:?}" +: "${ALERT_TITLE:?}" + +failed_step() { + local pairs=( + "${OUTCOME_RESOLVE_TAG}:Resolve deploy image tag" + "${OUTCOME_DEPLOY}:Deploy api-e2e" + "${OUTCOME_HEALTH}:Wait for api-e2e service to be ready" + "${OUTCOME_E2E}:Run e2e tests" + ) + local pair outcome label + for pair in "${pairs[@]}"; do + outcome="${pair%%:*}" + label="${pair#*:}" + if [[ "$outcome" == "failure" ]]; then + printf '%s\n' "$label" + return + fi + done + printf 'unknown\n' +} + +read_summary() { + local path="${E2E_SUMMARY_PATH:-e2e.summary}" + results="" + failures="" + + [[ -f "$path" ]] || return 0 + + local passed failed + passed="$(awk -F= '/^passed=/{print $2; exit}' "$path")" + failed="$(awk -F= '/^failed=/{print $2; exit}' "$path")" + if [[ -n "${passed}" || -n "${failed}" ]]; then + results="Passed: ${passed:-?} / Failed: ${failed:-?}" + fi + failures="$(awk '/^FAIL /{print; if (++n == 10) exit}' "$path")" +} + +sha_short="${DEPLOY_TAG:-unknown}" +sha_short="${sha_short:0:12}" + +read_summary + +lines=( + ":rotating_light: *${ALERT_TITLE}*" + "*Event:* \`${EVENT_NAME}\`" + "*Failed step:* \`$(failed_step)\`" + "*Deploy SHA:* \`${sha_short}\`" + "*Run:* <${RUN_URL}|View run>" +) + +[[ -n "${results}" ]] && lines+=("*Results:* ${results}") + +if [[ -n "${failures}" ]]; then + lines+=("*Failed tests:*" $'```\n'"${failures}"$'\n```') +fi + +text="$(printf '%s\n' "${lines[@]}")" +payload="$(jq -n --arg text "$text" '{text: $text}')" + +curl -fsS -X POST "$SLACK_WEBHOOK_URL" \ + -H 'Content-Type: application/json' \ + -d "$payload" diff --git a/.github/scripts/public-api-e2e-tests.sh b/.github/scripts/public-api-e2e-tests.sh index 5c2a61baf3..2a9caad2e0 100755 --- a/.github/scripts/public-api-e2e-tests.sh +++ b/.github/scripts/public-api-e2e-tests.sh @@ -47,6 +47,7 @@ SOURCE="lfxOne-api-e2e" PASS=0 FAIL=0 +FAILURE_LINES=() TOKEN="" HTTP_CODE="" BODY="" @@ -81,11 +82,17 @@ log_fail_body() { printf ' body: %s\n' "$snippet" } +record_failure() { + local line=$1 + FAILURE_LINES+=("$line") + printf ' %s\n' "$line" +} + check() { local name=$1 expected=$2 shift 2 if [[ $HTTP_CODE != "$expected" ]]; then - printf ' FAIL %s — expected HTTP %s, got %s\n' "$name" "$expected" "$HTTP_CODE" + record_failure "FAIL ${name} — expected HTTP ${expected}, got ${HTTP_CODE}" log_fail_body FAIL=$((FAIL + 1)) return @@ -93,7 +100,7 @@ check() { local expr for expr in "$@"; do if ! jq -e "$expr" >/dev/null 2>&1 <<<"$BODY"; then - printf ' FAIL %s (%s)\n' "$name" "$expr" + record_failure "FAIL ${name} (${expr})" log_fail_body FAIL=$((FAIL + 1)) return @@ -103,6 +110,16 @@ check() { PASS=$((PASS + 1)) } +write_summary() { + local path="${E2E_SUMMARY_PATH:-}" + [[ -n "$path" ]] || return 0 + { + printf 'passed=%s\n' "$PASS" + printf 'failed=%s\n' "$FAIL" + printf '%s\n' "${FAILURE_LINES[@]+"${FAILURE_LINES[@]}"}" + } >"$path" +} + require() { local expected=$1 name=$2 [[ $HTTP_CODE == "$expected" ]] || die "$name — expected HTTP $expected, got $HTTP_CODE" @@ -502,6 +519,7 @@ main() { echo "=== Results ===" printf 'Passed: %s\n' "$PASS" printf 'Failed: %s\n' "$FAIL" + write_summary [[ $FAIL -eq 0 ]] } diff --git a/.github/workflows/api-e2e-tests.yml b/.github/workflows/api-e2e-tests.yml index 5ebf21b6a5..10af417ecc 100644 --- a/.github/workflows/api-e2e-tests.yml +++ b/.github/workflows/api-e2e-tests.yml @@ -25,7 +25,7 @@ jobs: ref: main - name: Resolve deploy image tag - id: deploy + id: resolve_tag run: echo "tag=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" - name: Install OCI @@ -43,6 +43,7 @@ jobs: run: echo "${{ env.OCI_CLI_DIR }}" >> "$GITHUB_PATH" - name: Deploy api-e2e + id: deploy uses: ./.github/actions/node/builder env: CLOUD_ENV: lf-oracle-staging @@ -56,9 +57,10 @@ jobs: ORACLE_CLUSTER: ${{ secrets.ORACLE_STAGING_CLUSTER }} with: services: api-e2e - tag: ${{ steps.deploy.outputs.tag }} + tag: ${{ steps.resolve_tag.outputs.tag }} - name: Wait for api-e2e service to be ready + id: health run: | set -euo pipefail # Builder only runs kubectl set image; wait for the new revision before health. @@ -72,43 +74,26 @@ jobs: exit 1 - name: Run Public API e2e tests + id: e2e env: AUTH0_STAGING_AUDIENCE: ${{ vars.AUTH0_STAGING_AUDIENCE }} AUTH0_STAGING_ISSUER: ${{ vars.AUTH0_STAGING_ISSUER }} AUTH0_STAGING_API_E2E_CLIENT_ID: ${{ secrets.AUTH0_STAGING_API_E2E_CLIENT_ID }} AUTH0_STAGING_API_E2E_CLIENT_SECRET: ${{ secrets.AUTH0_STAGING_API_E2E_CLIENT_SECRET }} + E2E_SUMMARY_PATH: e2e.summary run: bash .github/scripts/public-api-e2e-tests.sh - name: Notify Slack on failure - if: failure() + if: failure() && !cancelled() env: - CDP_ALERTS_SLACK_WEBHOOK_URL: ${{ secrets.CDP_ALERTS_SLACK_WEBHOOK_URL }} + SLACK_WEBHOOK_URL: ${{ secrets.CDP_ALERTS_SLACK_WEBHOOK_URL }} + ALERT_TITLE: Public API e2e tests failed RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} EVENT_NAME: ${{ github.event_name }} - run: | - set -euo pipefail - payload="$(jq -n \ - --arg run_url "$RUN_URL" \ - --arg event "$EVENT_NAME" \ - '{ - blocks: [ - { - type: "header", - text: { - type: "plain_text", - text: ":rotating_light: Public API e2e tests failed", - emoji: true - } - }, - { - type: "section", - text: { - type: "mrkdwn", - text: ("*Workflow:* `API e2e tests`\n*Event:* `" + $event + "`\n*Run:* <" + $run_url + "|View run>") - } - } - ] - }')" - curl -fsS -X POST "$CDP_ALERTS_SLACK_WEBHOOK_URL" \ - -H 'Content-Type: application/json' \ - -d "$payload" + DEPLOY_TAG: ${{ steps.resolve_tag.outputs.tag }} + E2E_SUMMARY_PATH: e2e.summary + OUTCOME_RESOLVE_TAG: ${{ steps.resolve_tag.outcome }} + OUTCOME_DEPLOY: ${{ steps.deploy.outcome }} + OUTCOME_HEALTH: ${{ steps.health.outcome }} + OUTCOME_E2E: ${{ steps.e2e.outcome }} + run: bash .github/scripts/notify-api-e2e-failure.sh diff --git a/backend/src/api/public/alerts/alertOnce.ts b/backend/src/api/public/alerts/alertOnce.ts new file mode 100644 index 0000000000..166a95b243 --- /dev/null +++ b/backend/src/api/public/alerts/alertOnce.ts @@ -0,0 +1,111 @@ +import { createHash } from 'crypto' +import type { Request } from 'express' + +import { generateUUIDv4 } from '@crowd/common' +import { RedisCache } from '@crowd/redis' +import { + SlackChannel, + type SlackMessageSection, + SlackPersona, + sendSlackNotification, +} from '@crowd/slack' + +const PATH_UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi + +export async function alertOnce( + req: Request, + { + status, + code, + message, + name, + context, + stack, + }: { + status: number + code: string + message: string + name?: string + context?: Record + stack?: string + }, +): Promise { + if (status !== 409 && status < 500) return + + const path = (req.originalUrl || req.url || '').split('?')[0] + + // akrites alerts are handled separately, so skip them here. + if (path.startsWith('/v1/akrites') || path.startsWith('/v1/akrites-external')) { + return + } + + const route = resolveRoute(req) + + const dedupeKey = createHash('sha256') + .update( + [status, req.method, route, code, message, serializeContext(context)] + .filter((part) => part !== '') + .join(':'), + ) + .digest('hex') + + const cache = new RedisCache('public-api-alerts', req.redis, req.log) + const lease = generateUUIDv4() + + try { + const held = await cache.setIfNotExistsOrGet(dedupeKey, lease, 60 * 60) + if (held !== lease) { + req.log.info({ dedupeKey }, 'Skipping duplicate public API alert') + return + } + } catch (err) { + req.log.warn({ err, dedupeKey }, 'Alert dedupe failed; sending anyway') + } + + const sections: SlackMessageSection[] = [ + { + title: 'Request', + text: `*Method:* \`${req.method}\`\n*URL:* \`${req.originalUrl || req.url}\``, + }, + { + title: 'Error', + text: `*Code:* \`${code}\`\n*Name:* \`${name || code}\`\n*Message:* ${message}`, + }, + ] + + if (context && Object.keys(context).length > 0) { + sections.push({ + title: 'Context', + text: `\`\`\`${JSON.stringify(context, null, 2)}\`\`\``, + }) + } + + if (stack) { + sections.push({ + title: 'Stack Trace', + text: `\`\`\`${stack.substring(0, 2700)}\`\`\``, + }) + } + + sendSlackNotification( + SlackChannel.CDP_PUBLIC_API_ALERTS, + status >= 500 ? SlackPersona.ERROR_REPORTER : SlackPersona.WARNING_PROPAGATOR, + status >= 500 ? `500 Error: ${name || message}` : `${status} Conflict: ${message}`, + sections, + ) +} + +function resolveRoute(req: Request): string { + return (req.originalUrl || req.url || '').split('?')[0].replace(PATH_UUID, ':id') +} + +function serializeContext(context?: Record): string { + if (!context) return '' + + const normalized: Record = {} + for (const key of Object.keys(context).sort()) { + const value = context[key] + normalized[key] = Array.isArray(value) ? [...value].map(String).sort() : value + } + return JSON.stringify(normalized) +} diff --git a/backend/src/api/public/alerts/identityConflict.ts b/backend/src/api/public/alerts/identityConflict.ts deleted file mode 100644 index d35b8fc99e..0000000000 --- a/backend/src/api/public/alerts/identityConflict.ts +++ /dev/null @@ -1,61 +0,0 @@ -import type { Request } from 'express' - -import { ConflictError } from '@crowd/common' -import type { IMemberIdentity } from '@crowd/types' - -import { notifyOnce } from '@/api/public/alerts/notifyOnce' -import { rethrowDbConflict } from '@/utils/err' - -type IdentityConflictSubject = Pick - -function notifyIdentityConflict( - req: Request, - identity: IdentityConflictSubject, - message: string, -): void { - const dedupeKey = [ - 'member-identity-conflict', - identity.platform, - identity.type, - identity.value, - identity.memberId, - ] - .filter(Boolean) - .join(':') - - const context = { - platform: identity.platform, - value: identity.value, - type: identity.type, - ...(identity.memberId ? { memberId: identity.memberId } : {}), - } - - void notifyOnce(req, dedupeKey, 'Public API Identity Conflict 409', [ - { - title: 'Request', - text: `*Method:* \`${req.method}\`\n*URL:* \`${req.originalUrl}\``, - }, - { title: 'Conflict', text: `*Message:* ${message}` }, - { title: 'Context', text: `\`\`\`${JSON.stringify(context, null, 2)}\`\`\`` }, - ]) -} - -/** Maps identity unique violations to ConflictError and alerts once. */ -export function rethrowIdentityConflict( - req: Request, - error: unknown, - identity: IdentityConflictSubject, -): never { - try { - rethrowDbConflict(error, { - platform: identity.platform, - value: identity.value, - type: identity.type, - }) - } catch (e) { - if (e instanceof ConflictError) { - notifyIdentityConflict(req, identity, e.message) - } - throw e - } -} diff --git a/backend/src/api/public/alerts/memberResolveConflict.ts b/backend/src/api/public/alerts/memberResolveConflict.ts deleted file mode 100644 index 752eca9aea..0000000000 --- a/backend/src/api/public/alerts/memberResolveConflict.ts +++ /dev/null @@ -1,28 +0,0 @@ -import type { Request } from 'express' - -import { ConflictError } from '@crowd/common' - -import { notifyOnce } from '@/api/public/alerts/notifyOnce' - -function notifyMemberResolveConflict(req: Request, memberIds: string[], message: string): void { - const dedupeKey = `member-resolve:${[...memberIds].sort().join(':')}` - - void notifyOnce(req, dedupeKey, 'Public API Member Resolve Conflict 409', [ - { - title: 'Request', - text: `*Method:* \`${req.method}\`\n*URL:* \`${req.originalUrl}\``, - }, - { title: 'Conflict', text: `*Message:* ${message}` }, - { - title: 'Context', - text: `\`\`\`${JSON.stringify({ memberIds }, null, 2)}\`\`\``, - }, - ]) -} - -/** Throws ConflictError for ambiguous resolve and alerts once. */ -export function throwMemberResolveConflict(req: Request, memberIds: string[]): never { - const message = 'Multiple member profiles matched' - notifyMemberResolveConflict(req, memberIds, message) - throw new ConflictError(message, { memberIds }) -} diff --git a/backend/src/api/public/alerts/notifyOnce.ts b/backend/src/api/public/alerts/notifyOnce.ts deleted file mode 100644 index b35a89acf5..0000000000 --- a/backend/src/api/public/alerts/notifyOnce.ts +++ /dev/null @@ -1,39 +0,0 @@ -import type { Request } from 'express' - -import { generateUUIDv4 } from '@crowd/common' -import { RedisCache } from '@crowd/redis' -import { - SlackChannel, - type SlackMessageSection, - SlackPersona, - sendSlackNotification, -} from '@crowd/slack' - -/** Sends a Slack alert once per dedupe key. */ -export async function notifyOnce( - req: Request, - key: string, - title: string, - sections: SlackMessageSection[], -): Promise { - const cache = new RedisCache('public-api-alerts', req.redis, req.log) - const token = generateUUIDv4() - - try { - const result = await cache.setIfNotExistsOrGet(key, token, 60 * 60) - - if (result !== token) { - req.log.info({ key }, 'Skipping duplicate public API alert') - return - } - } catch (err) { - req.log.warn({ err, key }, 'Failed to deduplicate, sending alert') - } - - sendSlackNotification( - SlackChannel.CDP_LFX_SELF_SERVE_ALERTS, - SlackPersona.WARNING_PROPAGATOR, - title, - sections, - ) -} diff --git a/backend/src/api/public/middlewares/errorHandler.ts b/backend/src/api/public/middlewares/errorHandler.ts index 4da3552876..4a5ce182f3 100644 --- a/backend/src/api/public/middlewares/errorHandler.ts +++ b/backend/src/api/public/middlewares/errorHandler.ts @@ -4,8 +4,15 @@ import { UnauthorizedError as Auth0UnauthorizedError, } from 'express-oauth2-jwt-bearer' -import { HttpError, InsufficientScopeError, InternalError, UnauthorizedError } from '@crowd/common' -import { SlackChannel, SlackPersona, sendSlackNotification } from '@crowd/slack' +import { + ConflictError, + HttpError, + InsufficientScopeError, + InternalError, + UnauthorizedError, +} from '@crowd/common' + +import { alertOnce } from '@/api/public/alerts/alertOnce' /** * Converts errors to structured JSON: `{ error: { code, message } }`. @@ -18,6 +25,14 @@ export const errorHandler: ErrorRequestHandler = ( _next: NextFunction, ) => { if (error instanceof HttpError) { + void alertOnce(req, { + status: error.status, + code: error.code, + message: error.message, + name: error.name, + context: error instanceof ConflictError ? error.context : undefined, + stack: error.status >= 500 ? error.stack : undefined, + }) res.status(error.status).json(error.toJSON()) return } @@ -45,29 +60,13 @@ export const errorHandler: ErrorRequestHandler = ( 'Unhandled error in public API', ) - sendSlackNotification( - SlackChannel.CDP_ALERTS, - SlackPersona.ERROR_REPORTER, - `Public API Error 500: ${req.method} ${req.url}`, - [ - { - title: 'Request', - text: `*Method:* \`${req.method}\`\n*URL:* \`${req.url}\``, - }, - { - title: 'Error', - text: `*Name:* \`${error?.name || 'Unknown'}\`\n*Message:* ${error?.message || 'No message'}`, - }, - ...(error?.stack - ? [ - { - title: 'Stack Trace', - text: `\`\`\`${error.stack.substring(0, 2700)}\`\`\``, - }, - ] - : []), - ], - ) + void alertOnce(req, { + status: 500, + code: 'INTERNAL_ERROR', + message: error?.message || 'No message', + name: error?.name || 'Unknown', + stack: error?.stack, + }) const unknownError = new InternalError() res.status(unknownError.status).json(unknownError.toJSON()) diff --git a/backend/src/api/public/v1/members/createMember.ts b/backend/src/api/public/v1/members/createMember.ts index cdcd7504b6..d11d4671ba 100644 --- a/backend/src/api/public/v1/members/createMember.ts +++ b/backend/src/api/public/v1/members/createMember.ts @@ -6,7 +6,6 @@ import { getProperDisplayName } from '@crowd/common' import { createMember as insertMember, insertMemberIdentities } from '@crowd/data-access-layer' import { MemberIdentityType } from '@crowd/types' -import { rethrowIdentityConflict } from '@/api/public/alerts/identityConflict' import { optionsQx } from '@/database/sequelizeQueryExecutor' import { created } from '@/utils/api' import { rethrowDbConflict } from '@/utils/err' @@ -65,12 +64,9 @@ export async function createMember(req: Request, res: Response): Promise { // Only notify for a single identity because we can't tell which one conflicted in a batch. if (identities.length === 1) { const identity = identities[0] - rethrowIdentityConflict(req, error, { + return rethrowDbConflict(error, { platform: identity.platform, - value: - identity.type === MemberIdentityType.EMAIL - ? identity.value.trim().toLowerCase() - : identity.value.trim(), + value: identity.value, type: identity.type, }) } diff --git a/backend/src/api/public/v1/members/identities/createMemberIdentity.ts b/backend/src/api/public/v1/members/identities/createMemberIdentity.ts index 6f1877ebb3..029605ee86 100644 --- a/backend/src/api/public/v1/members/identities/createMemberIdentity.ts +++ b/backend/src/api/public/v1/members/identities/createMemberIdentity.ts @@ -13,9 +13,9 @@ import { } from '@crowd/data-access-layer' import { IMemberIdentity, MemberIdentityType } from '@crowd/types' -import { rethrowIdentityConflict } from '@/api/public/alerts/identityConflict' import { optionsQx } from '@/database/sequelizeQueryExecutor' import { created, ok } from '@/utils/api' +import { rethrowDbConflict } from '@/utils/err' import { validateOrThrow } from '@/utils/validation' const paramsSchema = z.object({ @@ -101,7 +101,7 @@ export async function createMemberIdentity(req: Request, res: Response): Promise } } } catch (error) { - rethrowIdentityConflict(req, error, { + rethrowDbConflict(error, { memberId, platform: data.platform, value: data.value, diff --git a/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts b/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts index 8539e1894f..f62ceea289 100644 --- a/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts +++ b/backend/src/api/public/v1/members/identities/verifyMemberIdentity.ts @@ -21,7 +21,6 @@ import { queryActivityRelations, updateMemberIdentity, } from '@crowd/data-access-layer' -import { SlackChannel, SlackPersona, sendSlackNotification } from '@crowd/slack' import { IMemberIdentity, IMemberUnmergePreviewResult, @@ -29,9 +28,9 @@ import { MemberUnmergeResult, } from '@crowd/types' -import { rethrowIdentityConflict } from '@/api/public/alerts/identityConflict' import { optionsQx } from '@/database/sequelizeQueryExecutor' import { noContent, ok } from '@/utils/api' +import { rethrowDbConflict } from '@/utils/err' import { validateOrThrow } from '@/utils/validation' const paramsSchema = z.object({ @@ -94,7 +93,7 @@ export async function verifyMemberIdentity(req: Request, res: Response): Promise }) } catch (error) { if (verified) { - rethrowIdentityConflict(req, error, { + rethrowDbConflict(error, { memberId, platform: identity.platform, value: identity.value, @@ -141,26 +140,16 @@ export async function verifyMemberIdentity(req: Request, res: Response): Promise if (unmerge) { const { preview, result } = unmerge - try { - await captureApiChange( - req, - memberUnmergeAction(memberId, async (captureOldState, captureNewState) => { - captureOldState({ primary: preview.primary }) - captureNewState({ - primary: result.primary, - secondary: result.secondary, - }) - }), - ) - } catch (error) { - req.log.warn({ error }, 'Audit log capture failed after identity unmerge') - sendSlackNotification( - SlackChannel.CDP_ALERTS, - SlackPersona.ERROR_REPORTER, - `Audit log capture failed after identity unmerge: member ${memberId}`, - [{ title: 'Error', text: `\`${error?.message || error}\`` }], - ) - } + await captureApiChange( + req, + memberUnmergeAction(memberId, async (captureOldState, captureNewState) => { + captureOldState({ primary: preview.primary }) + captureNewState({ + primary: result.primary, + secondary: result.secondary, + }) + }), + ) try { await invalidateMemberQueryCache(req.redis, [result.primary.id, result.secondary.id], true) @@ -178,19 +167,8 @@ export async function verifyMemberIdentity(req: Request, res: Response): Promise actorId: req.actor.id, }) } catch (error) { - req.log.warn({ error }, 'Failed to start unmerge workflow after identity unmerge') - sendSlackNotification( - SlackChannel.CDP_ALERTS, - SlackPersona.ERROR_REPORTER, - `Failed to start unmerge workflow after identity unmerge: member ${memberId}`, - [ - { - title: 'Context', - text: `*Primary:* \`${result.primary.id}\`\n*Secondary:* \`${result.secondary.id}\``, - }, - { title: 'Error', text: `\`${error?.message || error}\`` }, - ], - ) + req.log.error({ error }, 'Failed to start unmerge workflow') + throw error } } diff --git a/backend/src/api/public/v1/members/resolveMember.ts b/backend/src/api/public/v1/members/resolveMember.ts index 3d22b5d309..0db66b33ae 100644 --- a/backend/src/api/public/v1/members/resolveMember.ts +++ b/backend/src/api/public/v1/members/resolveMember.ts @@ -1,11 +1,10 @@ import type { Request, Response } from 'express' import { z } from 'zod' -import { NotFoundError } from '@crowd/common' +import { ConflictError, NotFoundError } from '@crowd/common' import { findMemberIdsByIdentities } from '@crowd/data-access-layer' import { IMemberIdentity, MemberIdentityType, PlatformType } from '@crowd/types' -import { throwMemberResolveConflict } from '@/api/public/alerts/memberResolveConflict' import { optionsQx } from '@/database/sequelizeQueryExecutor' import { ok } from '@/utils/api' import { validateOrThrow } from '@/utils/validation' @@ -39,7 +38,7 @@ export async function resolveMemberByIdentities(req: Request, res: Response): Pr if (memberIds.length === 0) { throw new NotFoundError('Member not found') } else if (memberIds.length > 1) { - throwMemberResolveConflict(req, memberIds) + throw new ConflictError('Multiple member profiles matched', { memberIds }) } const memberId = memberIds[0] diff --git a/services/libs/slack/src/channels.ts b/services/libs/slack/src/channels.ts index ddb2612e12..e1210275b7 100644 --- a/services/libs/slack/src/channels.ts +++ b/services/libs/slack/src/channels.ts @@ -12,7 +12,7 @@ const CHANNEL_WEBHOOK_URLS: Record = { [SlackChannel.CDP_PROJECTS_ALERTS]: process.env.CDP_PROJECTS_ALERTS_SLACK_WEBHOOK_URL, [SlackChannel.INSIGHTS_ALERTS]: process.env.INSIGHTS_ALERTS_SLACK_WEBHOOK_URL, [SlackChannel.INSIGHTS_CRITICAL_ALERTS]: process.env.INSIGHTS_CRITICAL_ALERTS_SLACK_WEBHOOK_URL, - [SlackChannel.CDP_LFX_SELF_SERVE_ALERTS]: process.env.CDP_LFX_SELF_SERVE_ALERTS_SLACK_WEBHOOK_URL, + [SlackChannel.CDP_PUBLIC_API_ALERTS]: process.env.CDP_PUBLIC_API_ALERTS_SLACK_WEBHOOK_URL, [SlackChannel.CDP_AKRITES_ALERTS]: process.env.CDP_AKRITES_ALERTS_SLACK_WEBHOOK_URL, } diff --git a/services/libs/slack/src/types.ts b/services/libs/slack/src/types.ts index eb10e30c40..27a4899b2d 100644 --- a/services/libs/slack/src/types.ts +++ b/services/libs/slack/src/types.ts @@ -6,7 +6,7 @@ export enum SlackChannel { CDP_PROJECTS_ALERTS = 'CDP_PROJECTS_ALERTS', INSIGHTS_ALERTS = 'INSIGHTS_ALERTS', INSIGHTS_CRITICAL_ALERTS = 'INSIGHTS_CRITICAL_ALERTS', - CDP_LFX_SELF_SERVE_ALERTS = 'CDP_LFX_SELF_SERVE_ALERTS', + CDP_PUBLIC_API_ALERTS = 'CDP_PUBLIC_API_ALERTS', CDP_AKRITES_ALERTS = 'CDP_AKRITES_ALERTS', }