web-agent-bridge 3.9.1 → 3.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,93 +1,93 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "web-agent-bridge",
|
|
3
|
-
"version": "3.9.
|
|
4
|
-
"description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
|
|
5
|
-
"author": "Web Agent Bridge <dev@webagentbridge.com>",
|
|
6
|
-
"main": "server/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"web-agent-bridge": "./bin/cli.js",
|
|
9
|
-
"wab": "./bin/cli.js",
|
|
10
|
-
"wab-agent": "./bin/cli.js",
|
|
11
|
-
"wab-init": "./bin/wab-init.js"
|
|
12
|
-
},
|
|
13
|
-
"scripts": {
|
|
14
|
-
"start": "node server/index.js",
|
|
15
|
-
"dev": "node server/index.js",
|
|
16
|
-
"test": "jest --forceExit --detectOpenHandles",
|
|
17
|
-
"build:script": "node scripts/build.js",
|
|
18
|
-
"prepublishOnly": "npm test"
|
|
19
|
-
},
|
|
20
|
-
"keywords": [
|
|
21
|
-
"ai",
|
|
22
|
-
"agent",
|
|
23
|
-
"bridge",
|
|
24
|
-
"protocol",
|
|
25
|
-
"platform",
|
|
26
|
-
"automation",
|
|
27
|
-
"web",
|
|
28
|
-
"ai-agent",
|
|
29
|
-
"agent-mesh",
|
|
30
|
-
"sovereign-browser",
|
|
31
|
-
"phone-shield",
|
|
32
|
-
"dns-discovery",
|
|
33
|
-
"api-gateway",
|
|
34
|
-
"browser-automation",
|
|
35
|
-
"webdriver-bidi"
|
|
36
|
-
],
|
|
37
|
-
"repository": {
|
|
38
|
-
"type": "git",
|
|
39
|
-
"url": "git+https://github.com/abokenan444/web-agent-bridge.git"
|
|
40
|
-
},
|
|
41
|
-
"homepage": "https://github.com/abokenan444/web-agent-bridge#readme",
|
|
42
|
-
"bugs": {
|
|
43
|
-
"url": "https://github.com/abokenan444/web-agent-bridge/issues"
|
|
44
|
-
},
|
|
45
|
-
"files": [
|
|
46
|
-
"bin/",
|
|
47
|
-
"server/",
|
|
48
|
-
"public/*.html",
|
|
49
|
-
"public/*.txt",
|
|
50
|
-
"public/*.xml",
|
|
51
|
-
"public/*.json",
|
|
52
|
-
"public/css/",
|
|
53
|
-
"public/js/",
|
|
54
|
-
"public/script/",
|
|
55
|
-
"public/assets/",
|
|
56
|
-
"public/.well-known/",
|
|
57
|
-
"script/",
|
|
58
|
-
"sdk/",
|
|
59
|
-
"templates/",
|
|
60
|
-
"examples/",
|
|
61
|
-
"README.md",
|
|
62
|
-
"README.ar.md",
|
|
63
|
-
"LICENSE"
|
|
64
|
-
],
|
|
65
|
-
"engines": {
|
|
66
|
-
"node": ">=18.0.0"
|
|
67
|
-
},
|
|
68
|
-
"license": "MIT",
|
|
69
|
-
"dependencies": {
|
|
70
|
-
"bcryptjs": "^3.0.3",
|
|
71
|
-
"better-sqlite3": "^11.6.0",
|
|
72
|
-
"cors": "^2.8.5",
|
|
73
|
-
"dotenv": "^16.4.5",
|
|
74
|
-
"express": "^4.21.0",
|
|
75
|
-
"express-rate-limit": "^7.4.1",
|
|
76
|
-
"helmet": "^8.0.0",
|
|
77
|
-
"jsonwebtoken": "^9.0.2",
|
|
78
|
-
"nodemailer": "^8.0.7",
|
|
79
|
-
"stripe": "^20.4.1",
|
|
80
|
-
"ws": "^8.20.0"
|
|
81
|
-
},
|
|
82
|
-
"devDependencies": {
|
|
83
|
-
"all-contributors-cli": "^6.26.1",
|
|
84
|
-
"jest": "^30.3.0",
|
|
85
|
-
"supertest": "^7.2.2"
|
|
86
|
-
},
|
|
87
|
-
"jest": {
|
|
88
|
-
"testPathIgnorePatterns": [
|
|
89
|
-
"/node_modules/",
|
|
90
|
-
"/packages/"
|
|
91
|
-
]
|
|
92
|
-
}
|
|
93
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "web-agent-bridge",
|
|
3
|
+
"version": "3.9.2",
|
|
4
|
+
"description": "Agent Transaction Bridge — the trust + transaction layer for agentic commerce. Signed intent contracts, idempotent transactions, Ed25519-verifiable receipts, explicit compensation. Plus the original WAB stack: sovereign browser, ShieldQR, SSL health, DNS discovery, agent mesh, and unified gateway for safe AI–website interaction.",
|
|
5
|
+
"author": "Web Agent Bridge <dev@webagentbridge.com>",
|
|
6
|
+
"main": "server/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"web-agent-bridge": "./bin/cli.js",
|
|
9
|
+
"wab": "./bin/cli.js",
|
|
10
|
+
"wab-agent": "./bin/cli.js",
|
|
11
|
+
"wab-init": "./bin/wab-init.js"
|
|
12
|
+
},
|
|
13
|
+
"scripts": {
|
|
14
|
+
"start": "node server/index.js",
|
|
15
|
+
"dev": "node server/index.js",
|
|
16
|
+
"test": "jest --forceExit --detectOpenHandles",
|
|
17
|
+
"build:script": "node scripts/build.js",
|
|
18
|
+
"prepublishOnly": "npm test"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"ai",
|
|
22
|
+
"agent",
|
|
23
|
+
"bridge",
|
|
24
|
+
"protocol",
|
|
25
|
+
"platform",
|
|
26
|
+
"automation",
|
|
27
|
+
"web",
|
|
28
|
+
"ai-agent",
|
|
29
|
+
"agent-mesh",
|
|
30
|
+
"sovereign-browser",
|
|
31
|
+
"phone-shield",
|
|
32
|
+
"dns-discovery",
|
|
33
|
+
"api-gateway",
|
|
34
|
+
"browser-automation",
|
|
35
|
+
"webdriver-bidi"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/abokenan444/web-agent-bridge.git"
|
|
40
|
+
},
|
|
41
|
+
"homepage": "https://github.com/abokenan444/web-agent-bridge#readme",
|
|
42
|
+
"bugs": {
|
|
43
|
+
"url": "https://github.com/abokenan444/web-agent-bridge/issues"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"bin/",
|
|
47
|
+
"server/",
|
|
48
|
+
"public/*.html",
|
|
49
|
+
"public/*.txt",
|
|
50
|
+
"public/*.xml",
|
|
51
|
+
"public/*.json",
|
|
52
|
+
"public/css/",
|
|
53
|
+
"public/js/",
|
|
54
|
+
"public/script/",
|
|
55
|
+
"public/assets/",
|
|
56
|
+
"public/.well-known/",
|
|
57
|
+
"script/",
|
|
58
|
+
"sdk/",
|
|
59
|
+
"templates/",
|
|
60
|
+
"examples/",
|
|
61
|
+
"README.md",
|
|
62
|
+
"README.ar.md",
|
|
63
|
+
"LICENSE"
|
|
64
|
+
],
|
|
65
|
+
"engines": {
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
},
|
|
68
|
+
"license": "MIT",
|
|
69
|
+
"dependencies": {
|
|
70
|
+
"bcryptjs": "^3.0.3",
|
|
71
|
+
"better-sqlite3": "^11.6.0",
|
|
72
|
+
"cors": "^2.8.5",
|
|
73
|
+
"dotenv": "^16.4.5",
|
|
74
|
+
"express": "^4.21.0",
|
|
75
|
+
"express-rate-limit": "^7.4.1",
|
|
76
|
+
"helmet": "^8.0.0",
|
|
77
|
+
"jsonwebtoken": "^9.0.2",
|
|
78
|
+
"nodemailer": "^8.0.7",
|
|
79
|
+
"stripe": "^20.4.1",
|
|
80
|
+
"ws": "^8.20.0"
|
|
81
|
+
},
|
|
82
|
+
"devDependencies": {
|
|
83
|
+
"all-contributors-cli": "^6.26.1",
|
|
84
|
+
"jest": "^30.3.0",
|
|
85
|
+
"supertest": "^7.2.2"
|
|
86
|
+
},
|
|
87
|
+
"jest": {
|
|
88
|
+
"testPathIgnorePatterns": [
|
|
89
|
+
"/node_modules/",
|
|
90
|
+
"/packages/"
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
}
|
package/server/index.js
CHANGED
|
@@ -232,6 +232,15 @@ const licenseLimiter = rateLimit({
|
|
|
232
232
|
}
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
// Visitor analytics — record every public page hit (HTML routes only) before
|
|
236
|
+
// they're served by express.static. Skips assets, /api, /admin and other noise.
|
|
237
|
+
try {
|
|
238
|
+
const visitorTracker = require('./services/visitor-tracker');
|
|
239
|
+
app.use(visitorTracker.middleware());
|
|
240
|
+
} catch (e) {
|
|
241
|
+
console.warn('[wab] visitor-tracker disabled:', e.message);
|
|
242
|
+
}
|
|
243
|
+
|
|
235
244
|
// Whitepaper guard — must run BEFORE express.static so we can apply strict headers
|
|
236
245
|
// and intercept both /whitepaper and /whitepaper.html with the same protections.
|
|
237
246
|
const whitepaperHandler = (req, res) => {
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
2
|
+
-- Migration 021 — Visitor analytics (page_visits)
|
|
3
|
+
--
|
|
4
|
+
-- Captures every public page request (registered or anonymous) so the admin
|
|
5
|
+
-- panel can show real traffic data. IPs are hashed for privacy.
|
|
6
|
+
-- ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS page_visits (
|
|
9
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
10
|
+
path TEXT NOT NULL,
|
|
11
|
+
query_string TEXT,
|
|
12
|
+
referrer TEXT,
|
|
13
|
+
host TEXT,
|
|
14
|
+
user_agent TEXT,
|
|
15
|
+
ip_hash TEXT, -- sha256(ip + salt), first 32 chars
|
|
16
|
+
country TEXT, -- best-effort from Cloudflare/CF-IPCountry; nullable
|
|
17
|
+
device TEXT, -- desktop | mobile | tablet | bot
|
|
18
|
+
is_bot INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
session_id TEXT, -- random per-visitor cookie or derived from ip_hash+UA
|
|
20
|
+
user_id TEXT, -- nullable; populated if request carried an auth cookie/token
|
|
21
|
+
status_code INTEGER, -- HTTP status that was returned
|
|
22
|
+
duration_ms INTEGER, -- server-side handler time
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_pv_created ON page_visits(created_at);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_pv_path ON page_visits(path);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_pv_ip ON page_visits(ip_hash);
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_pv_session ON page_visits(session_id);
|
|
30
|
+
CREATE INDEX IF NOT EXISTS idx_pv_is_bot ON page_visits(is_bot);
|
|
31
|
+
CREATE INDEX IF NOT EXISTS idx_pv_user ON page_visits(user_id);
|
package/server/routes/admin.js
CHANGED
|
@@ -23,6 +23,7 @@ const {
|
|
|
23
23
|
} = require('../models/db');
|
|
24
24
|
const { sendEmail } = require('../services/email');
|
|
25
25
|
const { createCheckoutSession, createPortalSession, isStripeConfigured, getStripePrices } = require('../services/stripe');
|
|
26
|
+
const visitorTracker = require('../services/visitor-tracker');
|
|
26
27
|
|
|
27
28
|
// ─── Auth ──────────────────────────────────────────────────────────────
|
|
28
29
|
|
|
@@ -60,6 +61,35 @@ router.get('/me', authenticateAdmin, (req, res) => {
|
|
|
60
61
|
router.get('/stats', authenticateAdmin, (req, res) => {
|
|
61
62
|
const stats = getAdminStats();
|
|
62
63
|
stats.stripeConfigured = isStripeConfigured();
|
|
64
|
+
|
|
65
|
+
// Expose normalized keys that the overview dashboard expects, while keeping
|
|
66
|
+
// the legacy keys for any older clients still reading them.
|
|
67
|
+
stats.users = stats.totalUsers;
|
|
68
|
+
stats.sites = stats.totalSites;
|
|
69
|
+
stats.analytics_total = stats.totalAnalytics;
|
|
70
|
+
stats.analytics_today = stats.todayAnalytics;
|
|
71
|
+
stats.revenue30d = (() => {
|
|
72
|
+
try {
|
|
73
|
+
const r = db.prepare(
|
|
74
|
+
`SELECT COALESCE(SUM(amount),0) AS t FROM payments WHERE status='succeeded' AND created_at >= datetime('now','-30 days')`
|
|
75
|
+
).get();
|
|
76
|
+
return r ? r.t : 0;
|
|
77
|
+
} catch { return 0; }
|
|
78
|
+
})();
|
|
79
|
+
stats.active_subscriptions = (() => {
|
|
80
|
+
try {
|
|
81
|
+
const r = db.prepare(
|
|
82
|
+
`SELECT COUNT(*) AS c FROM stripe_subscriptions WHERE status='active'`
|
|
83
|
+
).get();
|
|
84
|
+
return r ? r.c : 0;
|
|
85
|
+
} catch { return 0; }
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
// Visitor counts (registered + anonymous).
|
|
89
|
+
try {
|
|
90
|
+
Object.assign(stats, visitorTracker.getQuickCounts());
|
|
91
|
+
} catch { /* table may not exist on first boot before migration */ }
|
|
92
|
+
|
|
63
93
|
res.json(stats);
|
|
64
94
|
});
|
|
65
95
|
|
|
@@ -69,6 +99,26 @@ router.get('/analytics', authenticateAdmin, (req, res) => {
|
|
|
69
99
|
res.json(data);
|
|
70
100
|
});
|
|
71
101
|
|
|
102
|
+
// ─── Visitor analytics (page hits, registered + anonymous) ────────────
|
|
103
|
+
|
|
104
|
+
router.get('/analytics/visits', authenticateAdmin, (req, res) => {
|
|
105
|
+
try {
|
|
106
|
+
const data = visitorTracker.getVisitorAnalytics(req.query.days);
|
|
107
|
+
res.json({ ok: true, ...data });
|
|
108
|
+
} catch (e) {
|
|
109
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
router.get('/analytics/visits/recent', authenticateAdmin, (req, res) => {
|
|
114
|
+
try {
|
|
115
|
+
const visits = visitorTracker.getRecentVisits(req.query.limit);
|
|
116
|
+
res.json({ ok: true, visits });
|
|
117
|
+
} catch (e) {
|
|
118
|
+
res.status(500).json({ ok: false, error: e.message });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
72
122
|
// ─── Users Management ─────────────────────────────────────────────────
|
|
73
123
|
|
|
74
124
|
router.get('/users', authenticateAdmin, (req, res) => {
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Visitor tracking — records every public HTML page hit into `page_visits`.
|
|
3
|
+
* Anonymous-friendly: IPs are hashed (sha256 + IP_HASH_SALT) so we never store raw IPs.
|
|
4
|
+
*
|
|
5
|
+
* Exposes:
|
|
6
|
+
* - middleware() — Express middleware to mount before express.static
|
|
7
|
+
* - getVisitorAnalytics(days) — totals + timeline + breakdowns for /api/admin/analytics/visits
|
|
8
|
+
* - getRecentVisits(limit) — latest individual page hits for the admin live feed
|
|
9
|
+
* - getQuickCounts() — visits_24h / visitors_24h / visits_30d for /api/admin/stats
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
const { db } = require('../models/db');
|
|
14
|
+
|
|
15
|
+
const IP_SALT = process.env.IP_HASH_SALT || process.env.JWT_SECRET || 'wab-visitor-salt-v1';
|
|
16
|
+
|
|
17
|
+
// ── Bot detection ────────────────────────────────────────────────────
|
|
18
|
+
const BOT_RE = /(bot|crawler|spider|crawl|slurp|bingpreview|facebookexternalhit|preview|monitor|uptime|curl|wget|axios|node-fetch|python-requests|java\/|ahrefs|semrush|petalbot|yandex|baiduspider|duckduckbot|googlebot|applebot|gpt|claude|anthropic|openai|perplexity)/i;
|
|
19
|
+
|
|
20
|
+
// ── Path filter ──────────────────────────────────────────────────────
|
|
21
|
+
// We track real page requests, not asset/API noise.
|
|
22
|
+
const SKIP_PREFIX = ['/api/', '/css/', '/js/', '/assets/', '/script/', '/v3/', '/v2/', '/v1/', '/latest/', '/.well-known/', '/admin/', '/socket.io', '/favicon', '/sitemap', '/robots.txt', '/feed.xml', '/downloads/'];
|
|
23
|
+
const SKIP_EXT = /\.(?:js|mjs|css|map|png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|eot|mp4|webm|mp3|pdf|xml|txt|wasm)$/i;
|
|
24
|
+
|
|
25
|
+
function shouldTrack(req) {
|
|
26
|
+
if (req.method !== 'GET' && req.method !== 'HEAD') return false;
|
|
27
|
+
const p = req.path || '/';
|
|
28
|
+
if (SKIP_EXT.test(p)) return false;
|
|
29
|
+
for (const pre of SKIP_PREFIX) if (p.startsWith(pre)) return false;
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function hashIp(ip) {
|
|
34
|
+
if (!ip) return null;
|
|
35
|
+
return crypto.createHash('sha256').update(IP_SALT + ':' + ip).digest('hex').slice(0, 32);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function detectDevice(ua) {
|
|
39
|
+
if (!ua) return 'unknown';
|
|
40
|
+
if (BOT_RE.test(ua)) return 'bot';
|
|
41
|
+
if (/iPad|Tablet|PlayBook|Silk/i.test(ua)) return 'tablet';
|
|
42
|
+
if (/Mobi|Android|iPhone|iPod|Opera Mini|IEMobile/i.test(ua)) return 'mobile';
|
|
43
|
+
return 'desktop';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function extractUserId(req) {
|
|
47
|
+
// Best-effort: req.user (set by upstream auth middleware) wins; otherwise null.
|
|
48
|
+
if (req.user && req.user.id) return String(req.user.id);
|
|
49
|
+
if (req.session && req.session.userId) return String(req.session.userId);
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Lazily prepared so the require() of this module can run before migrations
|
|
54
|
+
// have created the page_visits table (e.g. in tests or during cold boot).
|
|
55
|
+
let _insertVisit = null;
|
|
56
|
+
function getInsertStmt() {
|
|
57
|
+
if (_insertVisit) return _insertVisit;
|
|
58
|
+
_insertVisit = db.prepare(`
|
|
59
|
+
INSERT INTO page_visits
|
|
60
|
+
(path, query_string, referrer, host, user_agent, ip_hash, country, device, is_bot, session_id, user_id, status_code, duration_ms)
|
|
61
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
62
|
+
`);
|
|
63
|
+
return _insertVisit;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function middleware() {
|
|
67
|
+
return function visitTracker(req, res, next) {
|
|
68
|
+
if (!shouldTrack(req)) return next();
|
|
69
|
+
const start = Date.now();
|
|
70
|
+
res.on('finish', () => {
|
|
71
|
+
try {
|
|
72
|
+
const ua = req.get('user-agent') || null;
|
|
73
|
+
const ref = req.get('referer') || req.get('referrer') || null;
|
|
74
|
+
const ipHash = hashIp(req.ip);
|
|
75
|
+
const device = detectDevice(ua);
|
|
76
|
+
const isBot = device === 'bot' ? 1 : 0;
|
|
77
|
+
const country = req.get('cf-ipcountry') || req.get('x-vercel-ip-country') || null;
|
|
78
|
+
const session = ipHash && ua
|
|
79
|
+
? crypto.createHash('sha256').update(ipHash + ':' + ua).digest('hex').slice(0, 24)
|
|
80
|
+
: null;
|
|
81
|
+
const qs = req.url.includes('?') ? req.url.slice(req.url.indexOf('?') + 1, req.url.indexOf('?') + 257) : null;
|
|
82
|
+
getInsertStmt().run(
|
|
83
|
+
req.path.slice(0, 512),
|
|
84
|
+
qs,
|
|
85
|
+
ref ? ref.slice(0, 512) : null,
|
|
86
|
+
(req.get('host') || '').slice(0, 255),
|
|
87
|
+
ua ? ua.slice(0, 512) : null,
|
|
88
|
+
ipHash,
|
|
89
|
+
country ? country.slice(0, 4) : null,
|
|
90
|
+
device,
|
|
91
|
+
isBot,
|
|
92
|
+
session,
|
|
93
|
+
extractUserId(req),
|
|
94
|
+
res.statusCode,
|
|
95
|
+
Date.now() - start
|
|
96
|
+
);
|
|
97
|
+
} catch (e) {
|
|
98
|
+
if (process.env.NODE_ENV !== 'production') {
|
|
99
|
+
console.warn('[visit-tracker] insert failed:', e.message);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
next();
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Read queries ────────────────────────────────────────────────────
|
|
108
|
+
function getVisitorAnalytics(days) {
|
|
109
|
+
const n = Math.max(1, Math.min(365, parseInt(days, 10) || 30));
|
|
110
|
+
const since = new Date(Date.now() - n * 86400000).toISOString();
|
|
111
|
+
|
|
112
|
+
const totalsRow = db.prepare(`
|
|
113
|
+
SELECT
|
|
114
|
+
COUNT(*) AS pageviews,
|
|
115
|
+
COUNT(DISTINCT session_id) AS visitors,
|
|
116
|
+
COALESCE(SUM(CASE WHEN is_bot=1 THEN 1 ELSE 0 END), 0) AS bot_hits,
|
|
117
|
+
COALESCE(SUM(CASE WHEN user_id IS NOT NULL THEN 1 ELSE 0 END), 0) AS authenticated_hits,
|
|
118
|
+
COUNT(DISTINCT user_id) AS authenticated_users
|
|
119
|
+
FROM page_visits WHERE created_at >= ?
|
|
120
|
+
`).get(since);
|
|
121
|
+
|
|
122
|
+
const last24Row = db.prepare(`
|
|
123
|
+
SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
|
|
124
|
+
FROM page_visits WHERE created_at >= datetime('now','-1 day')
|
|
125
|
+
`).get();
|
|
126
|
+
|
|
127
|
+
const todayRow = db.prepare(`
|
|
128
|
+
SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
|
|
129
|
+
FROM page_visits WHERE date(created_at) = date('now')
|
|
130
|
+
`).get();
|
|
131
|
+
|
|
132
|
+
const timeline = db.prepare(`
|
|
133
|
+
SELECT date(created_at) AS day,
|
|
134
|
+
COUNT(*) AS pageviews,
|
|
135
|
+
COUNT(DISTINCT session_id) AS visitors,
|
|
136
|
+
SUM(CASE WHEN is_bot=1 THEN 1 ELSE 0 END) AS bots
|
|
137
|
+
FROM page_visits WHERE created_at >= ?
|
|
138
|
+
GROUP BY day ORDER BY day
|
|
139
|
+
`).all(since);
|
|
140
|
+
|
|
141
|
+
const topPaths = db.prepare(`
|
|
142
|
+
SELECT path, COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
|
|
143
|
+
FROM page_visits WHERE created_at >= ? AND is_bot = 0
|
|
144
|
+
GROUP BY path ORDER BY pageviews DESC LIMIT 25
|
|
145
|
+
`).all(since);
|
|
146
|
+
|
|
147
|
+
const topReferrers = db.prepare(`
|
|
148
|
+
SELECT
|
|
149
|
+
COALESCE(NULLIF(substr(referrer, 1, instr(substr(referrer, 9), '/') + 7), ''), 'Direct') AS source,
|
|
150
|
+
COUNT(*) AS hits
|
|
151
|
+
FROM page_visits WHERE created_at >= ? AND is_bot = 0
|
|
152
|
+
GROUP BY source ORDER BY hits DESC LIMIT 15
|
|
153
|
+
`).all(since);
|
|
154
|
+
|
|
155
|
+
const devices = db.prepare(`
|
|
156
|
+
SELECT device, COUNT(*) AS hits
|
|
157
|
+
FROM page_visits WHERE created_at >= ?
|
|
158
|
+
GROUP BY device ORDER BY hits DESC
|
|
159
|
+
`).all(since);
|
|
160
|
+
|
|
161
|
+
const countries = db.prepare(`
|
|
162
|
+
SELECT COALESCE(country, 'Unknown') AS country, COUNT(*) AS hits
|
|
163
|
+
FROM page_visits WHERE created_at >= ? AND is_bot = 0
|
|
164
|
+
GROUP BY country ORDER BY hits DESC LIMIT 20
|
|
165
|
+
`).all(since);
|
|
166
|
+
|
|
167
|
+
const topBots = db.prepare(`
|
|
168
|
+
SELECT
|
|
169
|
+
CASE
|
|
170
|
+
WHEN user_agent LIKE '%Googlebot%' THEN 'Googlebot'
|
|
171
|
+
WHEN user_agent LIKE '%bingbot%' OR user_agent LIKE '%Bingbot%' THEN 'Bingbot'
|
|
172
|
+
WHEN user_agent LIKE '%DuckDuckBot%' THEN 'DuckDuckBot'
|
|
173
|
+
WHEN user_agent LIKE '%AhrefsBot%' THEN 'AhrefsBot'
|
|
174
|
+
WHEN user_agent LIKE '%SemrushBot%' THEN 'SemrushBot'
|
|
175
|
+
WHEN user_agent LIKE '%YandexBot%' THEN 'YandexBot'
|
|
176
|
+
WHEN user_agent LIKE '%Baiduspider%' THEN 'Baiduspider'
|
|
177
|
+
WHEN user_agent LIKE '%facebookexternalhit%' THEN 'Facebook'
|
|
178
|
+
WHEN user_agent LIKE '%GPTBot%' OR user_agent LIKE '%ChatGPT%' THEN 'GPTBot'
|
|
179
|
+
WHEN user_agent LIKE '%anthropic%' OR user_agent LIKE '%Claude%' THEN 'Claude / Anthropic'
|
|
180
|
+
WHEN user_agent LIKE '%PerplexityBot%' THEN 'Perplexity'
|
|
181
|
+
WHEN user_agent LIKE '%Applebot%' THEN 'Applebot'
|
|
182
|
+
ELSE 'Other bot'
|
|
183
|
+
END AS bot,
|
|
184
|
+
COUNT(*) AS hits
|
|
185
|
+
FROM page_visits WHERE created_at >= ? AND is_bot = 1
|
|
186
|
+
GROUP BY bot ORDER BY hits DESC LIMIT 15
|
|
187
|
+
`).all(since);
|
|
188
|
+
|
|
189
|
+
const signups = db.prepare(`
|
|
190
|
+
SELECT date(created_at) AS day, COUNT(*) AS count
|
|
191
|
+
FROM users WHERE created_at >= ? GROUP BY day ORDER BY day
|
|
192
|
+
`).all(since);
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
period_days: n,
|
|
196
|
+
totals: {
|
|
197
|
+
pageviews: totalsRow.pageviews || 0,
|
|
198
|
+
visitors: totalsRow.visitors || 0,
|
|
199
|
+
bot_hits: totalsRow.bot_hits || 0,
|
|
200
|
+
human_hits: (totalsRow.pageviews || 0) - (totalsRow.bot_hits || 0),
|
|
201
|
+
authenticated_hits: totalsRow.authenticated_hits || 0,
|
|
202
|
+
authenticated_users: totalsRow.authenticated_users || 0,
|
|
203
|
+
pageviews_24h: last24Row.pageviews || 0,
|
|
204
|
+
visitors_24h: last24Row.visitors || 0,
|
|
205
|
+
pageviews_today: todayRow.pageviews || 0,
|
|
206
|
+
visitors_today: todayRow.visitors || 0,
|
|
207
|
+
},
|
|
208
|
+
timeline,
|
|
209
|
+
topPaths,
|
|
210
|
+
topReferrers,
|
|
211
|
+
devices,
|
|
212
|
+
countries,
|
|
213
|
+
topBots,
|
|
214
|
+
signups,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function getRecentVisits(limit) {
|
|
219
|
+
const n = Math.max(1, Math.min(500, parseInt(limit, 10) || 50));
|
|
220
|
+
return db.prepare(`
|
|
221
|
+
SELECT id, path, referrer, host, user_agent, country, device, is_bot, session_id, user_id, status_code, duration_ms, created_at
|
|
222
|
+
FROM page_visits ORDER BY id DESC LIMIT ?
|
|
223
|
+
`).all(n);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function getQuickCounts() {
|
|
227
|
+
const row24 = db.prepare(`
|
|
228
|
+
SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
|
|
229
|
+
FROM page_visits WHERE created_at >= datetime('now','-1 day')
|
|
230
|
+
`).get();
|
|
231
|
+
const row30 = db.prepare(`
|
|
232
|
+
SELECT COUNT(*) AS pageviews, COUNT(DISTINCT session_id) AS visitors
|
|
233
|
+
FROM page_visits WHERE created_at >= datetime('now','-30 days')
|
|
234
|
+
`).get();
|
|
235
|
+
const total = db.prepare(`SELECT COUNT(*) AS c FROM page_visits`).get();
|
|
236
|
+
return {
|
|
237
|
+
pageviews_24h: row24.pageviews || 0,
|
|
238
|
+
visitors_24h: row24.visitors || 0,
|
|
239
|
+
pageviews_30d: row30.pageviews || 0,
|
|
240
|
+
visitors_30d: row30.visitors || 0,
|
|
241
|
+
pageviews_total: total.c || 0,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
module.exports = {
|
|
246
|
+
middleware,
|
|
247
|
+
getVisitorAnalytics,
|
|
248
|
+
getRecentVisits,
|
|
249
|
+
getQuickCounts,
|
|
250
|
+
};
|