|
8 | 8 | const express = require('express'); |
9 | 9 | const router = express.Router(); |
10 | 10 | const db = require('../services/db'); |
| 11 | +const dns = require('dns'); |
11 | 12 | const fs = require('fs'); |
12 | 13 | const http = require('http'); |
13 | 14 | const path = require('path'); |
@@ -117,4 +118,117 @@ router.get('/outbound', (_req, res) => { |
117 | 118 | }).on('error', () => res.json({ ok: true, note: 'httpbin unreachable' })); |
118 | 119 | }); |
119 | 120 |
|
| 121 | +// ── Query hints: missing-where-delete ───────────────────────────────────────── |
| 122 | + |
| 123 | +// Triggers: missing-where-delete (critical — deletes every row without WHERE) |
| 124 | +router.post('/delete-all', async (_req, res) => { |
| 125 | + try { |
| 126 | + await db.query('DELETE FROM quote'); |
| 127 | + res.json({ deleted: 'all rows' }); |
| 128 | + } catch (err) { |
| 129 | + res.status(500).json({ message: err.message }); |
| 130 | + } |
| 131 | +}); |
| 132 | + |
| 133 | +// ── Slow query ──────────────────────────────────────────────────────────────── |
| 134 | + |
| 135 | +// Triggers: slow-query event (pg_sleep(0.6) exceeds the 500ms pg default threshold) |
| 136 | +router.get('/slow-query', async (_req, res) => { |
| 137 | + try { |
| 138 | + await db.query('SELECT pg_sleep(0.6)'); |
| 139 | + res.json({ ok: true }); |
| 140 | + } catch (err) { |
| 141 | + res.status(500).json({ message: err.message }); |
| 142 | + } |
| 143 | +}); |
| 144 | + |
| 145 | +// ── Transaction monitoring ──────────────────────────────────────────────────── |
| 146 | + |
| 147 | +// Triggers: transaction event with aborted=false (COMMIT) |
| 148 | +// Note: pool.query() may use different connections per call; the DB-level transaction |
| 149 | +// may be a no-op, but TransactionMonitor correlates by SQL pattern, not connection. |
| 150 | +router.get('/transaction', async (_req, res) => { |
| 151 | + try { |
| 152 | + await db.query('BEGIN'); |
| 153 | + await db.query('SELECT id FROM quote LIMIT 1'); |
| 154 | + await db.query('COMMIT'); |
| 155 | + res.json({ ok: true, outcome: 'committed' }); |
| 156 | + } catch (err) { |
| 157 | + await db.query('ROLLBACK').catch(() => {}); |
| 158 | + res.status(500).json({ message: err.message }); |
| 159 | + } |
| 160 | +}); |
| 161 | + |
| 162 | +// Triggers: transaction event with aborted=true (ROLLBACK) |
| 163 | +router.get('/rollback', async (_req, res) => { |
| 164 | + try { |
| 165 | + await db.query('BEGIN'); |
| 166 | + await db.query('SELECT id FROM quote LIMIT 1'); |
| 167 | + await db.query('ROLLBACK'); |
| 168 | + res.json({ ok: true, outcome: 'rolled back' }); |
| 169 | + } catch (err) { |
| 170 | + res.status(500).json({ message: err.message }); |
| 171 | + } |
| 172 | +}); |
| 173 | + |
| 174 | +// ── Crash detection ─────────────────────────────────────────────────────────── |
| 175 | + |
| 176 | +// Triggers: crash event via unhandledRejection. |
| 177 | +// CrashGuard intercepts the rejection and emits 'crash' without exiting the process |
| 178 | +// (unhandledRejection is recoverable since Node 15+ when a listener is registered). |
| 179 | +router.get('/crash', (_req, res) => { |
| 180 | + Promise.reject(new Error('[demo] Simulated unhandled rejection — .catch() was intentionally omitted')); |
| 181 | + res.json({ ok: true, note: 'unhandledRejection fired — watch for CRASH event' }); |
| 182 | +}); |
| 183 | + |
| 184 | +// ── DNS monitoring ──────────────────────────────────────────────────────────── |
| 185 | + |
| 186 | +// Triggers: dns event (+ slow-dns if resolution exceeds the 100ms threshold) |
| 187 | +router.get('/dns-lookup', (_req, res) => { |
| 188 | + dns.lookup('example.com', (err, address) => { |
| 189 | + if (err) return res.json({ ok: false, error: err.message }); |
| 190 | + res.json({ ok: true, address }); |
| 191 | + }); |
| 192 | +}); |
| 193 | + |
| 194 | +// ── HTTP hint sinks ──────────────────────────────────────────────────────────── |
| 195 | +// These are internal targets used by the outbound routes below. |
| 196 | +// They are NOT meant to be called directly from traffic.js. |
| 197 | + |
| 198 | +router.get('/sink-500', (_req, res) => res.status(500).json({ error: 'demo server error' })); |
| 199 | +router.get('/sink-429', (_req, res) => res.status(429).json({ error: 'demo rate limit' })); |
| 200 | +router.get('/sink-slow', (_req, res) => { setTimeout(() => res.json({ ok: true }), 2500); }); |
| 201 | + |
| 202 | +// ── HTTP hints via outbound calls ───────────────────────────────────────────── |
| 203 | + |
| 204 | +function selfGet(urlPath) { |
| 205 | + const port = parseInt(process.env.TARGET_PORT || '3000', 10); |
| 206 | + return new Promise((resolve) => { |
| 207 | + const req = http.request({ host: 'localhost', port, path: urlPath, method: 'GET' }, (r) => { |
| 208 | + r.resume(); |
| 209 | + r.on('end', resolve); |
| 210 | + }); |
| 211 | + req.on('error', resolve); |
| 212 | + req.end(); |
| 213 | + }); |
| 214 | +} |
| 215 | + |
| 216 | +// Triggers: http-server-error hint (outbound call receives 500) |
| 217 | +router.get('/error-500', async (_req, res) => { |
| 218 | + await selfGet('/debug/sink-500'); |
| 219 | + res.json({ ok: true }); |
| 220 | +}); |
| 221 | + |
| 222 | +// Triggers: http-rate-limited hint (outbound call receives 429) |
| 223 | +router.get('/rate-limited', async (_req, res) => { |
| 224 | + await selfGet('/debug/sink-429'); |
| 225 | + res.json({ ok: true }); |
| 226 | +}); |
| 227 | + |
| 228 | +// Triggers: slow-http-request hint (outbound call takes > 2000ms) |
| 229 | +router.get('/slow-outbound', async (_req, res) => { |
| 230 | + await selfGet('/debug/sink-slow'); |
| 231 | + res.json({ ok: true }); |
| 232 | +}); |
| 233 | + |
120 | 234 | module.exports = router; |
0 commit comments