feat(notifications): Slack alerts for new pilot requests

- Add src/notifications/slack.ts — Slack webhook integration with rich blocks
- Add src/notifications/index.ts — dispatcher with test-submission filtering
- Wire notifyNewPilotRequest() into POST /api/pilot-request (fire-and-forget)
- Filter out test submissions (@example.com, E2E, Smoke Test, QA Test, Browser Test)
- Skip alert gracefully when SLACK_PILOT_WEBHOOK_URL is not set
- Update .gitignore to exclude .playwright-mcp/ artifacts
This commit is contained in:
Garfield
2026-05-14 18:22:52 -04:00
parent 1ddc1aab19
commit 1742a2f599
4 changed files with 160 additions and 0 deletions

1
.gitignore vendored
View File

@@ -22,3 +22,4 @@ videos/remotion-demo/out
videos/remotion-demo/public/*.mov
product/*.mov
.gstack/
.playwright-mcp/

View File

@@ -34,6 +34,7 @@ import { recordUsage, getMonthlyUsage, getUsageBreakdown, checkLimit } from './b
import { getCustomerInvoices, getInvoiceByNumber, markInvoiceSent, markInvoicePaid, generateMonthlyInvoice } from './billing/invoices.js';
import { getAllPlatformHealth } from './multitenancy/platform-health.js';
import { deliverWebhook, isValidWebhookUrl } from './webhooks/delivery.js';
import { notifyNewPilotRequest } from './notifications/index.js';
import redis from './redis.js';
const app = express();
@@ -2041,6 +2042,10 @@ app.post('/api/pilot-request', async (req, res) => {
try {
await appendPilotRequestToVault(requestId, body, req);
// Fire-and-forget notification — don't block response
notifyNewPilotRequest(body, requestId, req).catch((err) =>
console.error(`[notification] requestId=${requestId} error:`, err)
);
res.status(201).json({ ok: true, request_id: requestId });
} catch (error) {
console.error(`[squaremcp] pilot_request ERROR requestId=${requestId}:`, error);

View File

@@ -0,0 +1,59 @@
import { sendSlackAlert, type PilotAlertPayload } from './slack.js';
const TEST_PATTERNS = {
email: [/@example\.com$/i],
name: [/e2e/i, /smoke test/i, /qa test/i, /browser test/i],
company: [/qa check/i, /qa corp/i, /^squaremcp\s*(e2e|test|browser)/i],
};
function isTestSubmission(body: {
name: string;
email: string;
company: string;
}): boolean {
if (TEST_PATTERNS.email.some((p) => p.test(body.email))) return true;
if (TEST_PATTERNS.name.some((p) => p.test(body.name))) return true;
if (TEST_PATTERNS.company.some((p) => p.test(body.company))) return true;
return false;
}
interface PilotRequestBody {
name: string;
email: string;
company: string;
role: string;
use_case: string;
timeline: string;
systems: string;
requirements: string;
}
export async function notifyNewPilotRequest(
body: PilotRequestBody,
requestId: string,
req: { get: (header: string) => string | undefined; ip?: string; socket?: { remoteAddress?: string } }
): Promise<void> {
if (isTestSubmission(body)) {
console.log(`[notification] skipped test submission: ${body.email}`);
return;
}
const payload: PilotAlertPayload = {
requestId,
name: body.name,
email: body.email,
company: body.company,
role: body.role,
use_case: body.use_case,
timeline: body.timeline,
systems: body.systems,
requirements: body.requirements,
source: req.get('origin') || req.get('host') || 'unknown',
ipAddress: req.ip || req.socket?.remoteAddress || 'unknown',
};
// Fire-and-forget — don't block the HTTP response
sendSlackAlert(payload).catch((err) =>
console.error('[notification] slack error:', err)
);
}

View File

@@ -0,0 +1,95 @@
const SLACK_WEBHOOK_URL = process.env['SLACK_PILOT_WEBHOOK_URL'] ?? '';
export interface PilotAlertPayload {
requestId: string;
name: string;
email: string;
company: string;
role: string;
use_case: string;
timeline: string;
systems: string;
requirements: string;
source: string;
ipAddress: string;
}
export async function sendSlackAlert(payload: PilotAlertPayload): Promise<boolean> {
if (!SLACK_WEBHOOK_URL) {
console.warn('[notification:slack] SLACK_PILOT_WEBHOOK_URL not set, skipping');
return false;
}
const blocks = [
{
type: 'header',
text: {
type: 'plain_text',
text: '🚨 New SquareMCP Pilot Request',
emoji: true,
},
},
{
type: 'section',
fields: [
{ type: 'mrkdwn', text: `*Name:*\n${payload.name}` },
{ type: 'mrkdwn', text: `*Company:*\n${payload.company}` },
{ type: 'mrkdwn', text: `*Email:*\n${payload.email}` },
{ type: 'mrkdwn', text: `*Role:*\n${payload.role}` },
{ type: 'mrkdwn', text: `*Timeline:*\n${payload.timeline}` },
{ type: 'mrkdwn', text: `*Source:*\n${payload.source}` },
],
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Use Case:*\n${payload.use_case}`,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Systems:*\n${payload.systems || '_None specified_'}`,
},
},
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Requirements:*\n${payload.requirements || '_None specified_'}`,
},
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `Request ID: \`${payload.requestId}\` | IP: ${payload.ipAddress}`,
},
],
},
];
try {
const res = await fetch(SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ blocks }),
signal: AbortSignal.timeout(10_000),
});
if (!res.ok) {
const text = await res.text();
console.error(`[notification:slack] HTTP ${res.status}: ${text}`);
return false;
}
console.log(`[notification:slack] alert sent for request ${payload.requestId}`);
return true;
} catch (err) {
console.error('[notification:slack] fetch error:', (err as Error).message);
return false;
}
}