webspresso 0.0.101 → 0.0.103
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/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +11 -0
- package/.agents/CONVENTIONS.md +4 -0
- package/.agents/ORM.md +13 -4
- package/.agents/PLUGINS.md +32 -6
- package/.agents/ROUTING.md +99 -2
- package/.agents/skills/webspresso-framework/SKILL.md +32 -0
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/.agents/skills/webspresso-routing-and-api/SKILL.md +4 -4
- package/AGENTS.md +6 -0
- package/README.md +20 -7
- package/bin/commands/add-tailwind.js +10 -0
- package/bin/commands/build.js +34 -0
- package/bin/commands/routes.js +8 -0
- package/core/applySchema.js +18 -1
- package/core/auth/jwt.js +10 -17
- package/core/auth/user-agent.js +61 -42
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/events/dispatcher.js +56 -33
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/json-fields.js +7 -1
- package/core/orm/model.js +99 -8
- package/core/orm/query-builder.js +71 -25
- package/core/orm/repository.js +42 -17
- package/core/orm/schema-helpers.js +49 -0
- package/core/orm/scopes.js +40 -9
- package/core/orm/types.js +12 -1
- package/core/queue/manager.js +5 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +178 -6
- package/index.js +71 -16
- package/package.json +6 -5
- package/plugins/admin-panel/index.js +12 -0
- package/plugins/audit-log/middleware.js +39 -4
- package/plugins/csrf/index.js +1 -1
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/index.js +24 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/index.js +2 -0
- package/plugins/lazy.js +258 -0
- package/plugins/site-analytics/bot-patterns.js +3 -0
- package/plugins/site-analytics/client-error-handler.js +3 -1
- package/plugins/site-analytics/index.js +10 -1
- package/plugins/site-analytics/tracking.js +245 -46
- package/src/api/api-loader.js +18 -2
- package/src/api/validation-i18n.js +231 -0
- package/src/discovery/route-manifest.js +197 -0
- package/src/discovery/scan-routes.js +84 -8
- package/src/file-router.js +215 -97
- package/src/helpers.js +139 -26
- package/src/njk-frontmatter.js +17 -1
- package/src/plugin-manager.js +46 -13
- package/src/routing/route-table.js +15 -5
- package/src/server.js +86 -15
- package/src/services/builtins/system.js +4 -6
- package/src/services/registry.js +12 -2
- package/src/services/validator.js +57 -32
- package/src/template/precompile.js +247 -0
|
@@ -44,8 +44,21 @@ function fileManagerPlugin(options = {}) {
|
|
|
44
44
|
permissions = null,
|
|
45
45
|
security: securityOpts = { validateMagicBytes: true, sanitizeSvg: true },
|
|
46
46
|
includePage = true,
|
|
47
|
+
lazy: lazyOption,
|
|
47
48
|
} = options;
|
|
48
49
|
|
|
50
|
+
if (lazyOption) {
|
|
51
|
+
const { lazyPlugin } = require('../lazy');
|
|
52
|
+
const lazyOpts = typeof lazyOption === 'object' ? lazyOption : {};
|
|
53
|
+
return lazyPlugin(() => fileManagerPlugin({ ...options, lazy: false }), {
|
|
54
|
+
name: 'file-manager',
|
|
55
|
+
prefix: apiPrefix,
|
|
56
|
+
services: ['file-manager.list', 'file-manager.upload', 'file-manager.delete', 'file-manager.create-folder'],
|
|
57
|
+
dependencies: { 'admin-panel': '*' },
|
|
58
|
+
...lazyOpts,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
const resolvedBaseDir = path.resolve(baseDir);
|
|
50
63
|
|
|
51
64
|
const handlers = createFileManagerApiHandlers({
|
|
@@ -101,15 +114,17 @@ function fileManagerPlugin(options = {}) {
|
|
|
101
114
|
// so window.__openFileManagerPicker is available across all admin pages (models, records, rich-text)
|
|
102
115
|
if (typeof adminApi.getRegistry === 'function') {
|
|
103
116
|
const registry = adminApi.getRegistry();
|
|
104
|
-
registry
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
if (registry) {
|
|
118
|
+
registry.configure({
|
|
119
|
+
fileManager: {
|
|
120
|
+
enabled: true,
|
|
121
|
+
apiPrefix,
|
|
122
|
+
publicBasePath,
|
|
123
|
+
pageTitle,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
registry.registerClientComponent('files', componentCode);
|
|
127
|
+
}
|
|
113
128
|
}
|
|
114
129
|
|
|
115
130
|
adminApi.registerModule({
|
|
@@ -24,6 +24,16 @@ const DANGEROUS_EXTENSIONS = new Set([
|
|
|
24
24
|
'htaccess', 'htpasswd', 'ini', 'env', 'config', 'conf', 'passwd', 'shadow',
|
|
25
25
|
]);
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Common safe extensions allowed when passed as raw extensions (without filename dot)
|
|
29
|
+
*/
|
|
30
|
+
const COMMON_SAFE_EXTENSIONS = new Set([
|
|
31
|
+
'jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'avif', 'bmp', 'ico', 'tiff',
|
|
32
|
+
'pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'odt', 'ods', 'odp', 'rtf',
|
|
33
|
+
'txt', 'csv', 'md', 'json', 'xml', 'html', 'css', 'js', 'ts', 'jsx', 'tsx',
|
|
34
|
+
'sql', 'yaml', 'yml', 'zip', 'tar', 'gz', 'mp3', 'wav', 'mp4', 'mov', 'webm',
|
|
35
|
+
]);
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* Known magic numbers table for high-speed signature verification.
|
|
29
39
|
* Only validates when extension matches known binary format.
|
|
@@ -118,9 +128,8 @@ function validateSafeExtension(filename) {
|
|
|
118
128
|
throw new ValidationError('Filename must have a valid extension');
|
|
119
129
|
}
|
|
120
130
|
|
|
121
|
-
// Check all
|
|
122
|
-
for (
|
|
123
|
-
const segment = parts[i];
|
|
131
|
+
// Check all segments against dangerous extensions to block double extensions and dangerous dotfiles (e.g. .env, payload.php.jpg)
|
|
132
|
+
for (const segment of parts) {
|
|
124
133
|
if (DANGEROUS_EXTENSIONS.has(segment)) {
|
|
125
134
|
throw new SecurityError(
|
|
126
135
|
`Dangerous extension or double-extension detected: .${segment}`,
|
|
@@ -129,11 +138,14 @@ function validateSafeExtension(filename) {
|
|
|
129
138
|
}
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
141
|
+
if (parts.length < 2 && !cleanName.startsWith('.')) {
|
|
142
|
+
if (COMMON_SAFE_EXTENSIONS.has(parts[0])) {
|
|
143
|
+
return parts[0];
|
|
144
|
+
}
|
|
145
|
+
throw new ValidationError('Filename must have a valid extension');
|
|
135
146
|
}
|
|
136
147
|
|
|
148
|
+
const finalExt = parts[parts.length - 1];
|
|
137
149
|
return finalExt;
|
|
138
150
|
}
|
|
139
151
|
|
package/plugins/index.js
CHANGED
|
@@ -61,6 +61,8 @@ module.exports = {
|
|
|
61
61
|
get createApiKeyMiddleware() { return require('./api-key').createApiKeyMiddleware; },
|
|
62
62
|
get webhooksPlugin() { return require('./webhooks').webhooksPlugin; },
|
|
63
63
|
get WebhookDispatcher() { return require('./webhooks').WebhookDispatcher; },
|
|
64
|
+
get lazyPlugin() { return require('./lazy').lazyPlugin; },
|
|
65
|
+
get defineLazyPlugin() { return require('./lazy').defineLazyPlugin; },
|
|
64
66
|
};
|
|
65
67
|
|
|
66
68
|
|
package/plugins/lazy.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Webspresso On-Demand (Lazy) Plugin Wrapper
|
|
3
|
+
*
|
|
4
|
+
* Defers execution of heavy plugin initialization, filesystem scans, route mounting,
|
|
5
|
+
* and dependency loading until the plugin is actually accessed via:
|
|
6
|
+
* 1. HTTP request matching its configured URL prefix (e.g. /_admin)
|
|
7
|
+
* 2. Service execution (e.g. ctx.service('email.send'))
|
|
8
|
+
* 3. Direct inter-plugin API access (e.g. ctx.usePlugin('admin-panel'))
|
|
9
|
+
* 4. Template helper execution (e.g. fsy.adminHelper())
|
|
10
|
+
* 5. Explicit programmatic hydration (lazyPlugin.hydrate())
|
|
11
|
+
*
|
|
12
|
+
* @module plugins/lazy
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const express = require('express');
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Wraps a plugin definition or factory function into an on-demand lazy plugin.
|
|
19
|
+
*
|
|
20
|
+
* @param {Function|Object} pluginFactory - Factory `() => plugin` or plugin definition object
|
|
21
|
+
* @param {Object} [lazyOptions={}]
|
|
22
|
+
* @param {string} [lazyOptions.name] - Plugin name (inferred if not provided)
|
|
23
|
+
* @param {string|string[]} [lazyOptions.prefix] - URL path prefix(es) that trigger on-demand hydration (e.g. '/_admin')
|
|
24
|
+
* @param {string[]} [lazyOptions.services=[]] - Service names that trigger on-demand hydration
|
|
25
|
+
* @param {string[]} [lazyOptions.helpers=[]] - Template helpers that trigger on-demand hydration
|
|
26
|
+
* @param {string[]} [lazyOptions.events=[]] - Event names that trigger on-demand hydration
|
|
27
|
+
* @param {Object} [lazyOptions.dependencies] - Plugin dependencies
|
|
28
|
+
* @param {Object} [lazyOptions.csp] - Content Security Policy requirements
|
|
29
|
+
* @param {Function} [lazyOptions.onHydrate] - Callback invoked when hydration finishes
|
|
30
|
+
* @returns {Object} Webspresso Lazy Plugin definition
|
|
31
|
+
*/
|
|
32
|
+
function lazyPlugin(pluginFactory, lazyOptions = {}) {
|
|
33
|
+
let realPlugin = null;
|
|
34
|
+
let isHydrated = false;
|
|
35
|
+
let isHydrating = false;
|
|
36
|
+
|
|
37
|
+
let regCtx = null;
|
|
38
|
+
let routesCtx = null;
|
|
39
|
+
const router = express.Router();
|
|
40
|
+
|
|
41
|
+
const name =
|
|
42
|
+
lazyOptions.name ||
|
|
43
|
+
(typeof pluginFactory === 'object' && pluginFactory ? pluginFactory.name : null) ||
|
|
44
|
+
'anonymous-lazy-plugin';
|
|
45
|
+
|
|
46
|
+
const rawPrefixes = lazyOptions.prefix
|
|
47
|
+
? (Array.isArray(lazyOptions.prefix) ? lazyOptions.prefix : [lazyOptions.prefix])
|
|
48
|
+
: [];
|
|
49
|
+
// Normalize prefixes: ensure leading slash, remove trailing slash (except root '/')
|
|
50
|
+
const prefixes = rawPrefixes.map(p => {
|
|
51
|
+
let norm = String(p).trim();
|
|
52
|
+
if (!norm.startsWith('/')) norm = '/' + norm;
|
|
53
|
+
if (norm.length > 1 && norm.endsWith('/')) norm = norm.slice(0, -1);
|
|
54
|
+
return norm;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const lazyServices = Array.isArray(lazyOptions.services) ? lazyOptions.services : [];
|
|
58
|
+
const lazyHelpers = Array.isArray(lazyOptions.helpers) ? lazyOptions.helpers : [];
|
|
59
|
+
const lazyEvents = Array.isArray(lazyOptions.events) ? lazyOptions.events : [];
|
|
60
|
+
|
|
61
|
+
function hydrate() {
|
|
62
|
+
if (isHydrated) return realPlugin;
|
|
63
|
+
if (isHydrating) return realPlugin;
|
|
64
|
+
isHydrating = true;
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
realPlugin = typeof pluginFactory === 'function' ? pluginFactory() : pluginFactory;
|
|
68
|
+
if (!realPlugin) {
|
|
69
|
+
throw new Error(`[lazyPlugin] Factory for plugin "${name}" did not return a valid plugin object`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 1. Call setup hook if present
|
|
73
|
+
if (typeof realPlugin.setup === 'function' && regCtx?.app) {
|
|
74
|
+
const disposer = realPlugin.setup(regCtx.app);
|
|
75
|
+
if (typeof disposer === 'function' && regCtx.onDispose) {
|
|
76
|
+
regCtx.onDispose(disposer);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 2. Call register hook
|
|
81
|
+
if (typeof realPlugin.register === 'function' && regCtx) {
|
|
82
|
+
const disposer = realPlugin.register(regCtx);
|
|
83
|
+
if (typeof disposer === 'function' && regCtx.onDispose) {
|
|
84
|
+
regCtx.onDispose(disposer);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 3. Register real routes onto the lazy router
|
|
89
|
+
if (typeof realPlugin.onRoutesReady === 'function' && routesCtx) {
|
|
90
|
+
const customRoutesCtx = {
|
|
91
|
+
...routesCtx,
|
|
92
|
+
addRoute(method, routePath, ...handlers) {
|
|
93
|
+
const m = String(method).toLowerCase();
|
|
94
|
+
let matchedPrefix = null;
|
|
95
|
+
for (const p of prefixes) {
|
|
96
|
+
if (routePath === p || routePath.startsWith(p + '/')) {
|
|
97
|
+
matchedPrefix = p;
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (matchedPrefix) {
|
|
103
|
+
// Convert absolute route path to router-relative subpath
|
|
104
|
+
let subPath = routePath.slice(matchedPrefix.length);
|
|
105
|
+
if (!subPath || !subPath.startsWith('/')) {
|
|
106
|
+
subPath = '/' + (subPath || '');
|
|
107
|
+
}
|
|
108
|
+
if (router[m]) {
|
|
109
|
+
router[m](subPath, ...handlers);
|
|
110
|
+
} else if (router.all) {
|
|
111
|
+
router.all(subPath, ...handlers);
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
// Route is outside configured prefix: mount directly on app
|
|
115
|
+
if (routesCtx.addRoute) {
|
|
116
|
+
routesCtx.addRoute(method, routePath, ...handlers);
|
|
117
|
+
} else if (routesCtx.app && routesCtx.app[m]) {
|
|
118
|
+
routesCtx.app[m](routePath, ...handlers);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const disposer = realPlugin.onRoutesReady(customRoutesCtx);
|
|
125
|
+
if (typeof disposer === 'function' && routesCtx.onDispose) {
|
|
126
|
+
routesCtx.onDispose(disposer);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 4. Update exposed API
|
|
131
|
+
if (realPlugin.api && lazyWrapper.api) {
|
|
132
|
+
Object.assign(lazyWrapper.api, realPlugin.api);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 5. Fire optional callback
|
|
136
|
+
if (typeof lazyOptions.onHydrate === 'function') {
|
|
137
|
+
try {
|
|
138
|
+
lazyOptions.onHydrate(realPlugin);
|
|
139
|
+
} catch (_) {}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
isHydrated = true;
|
|
143
|
+
return realPlugin;
|
|
144
|
+
} finally {
|
|
145
|
+
isHydrating = false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Create API Proxy that auto-hydrates on property access
|
|
150
|
+
const apiProxy = new Proxy({}, {
|
|
151
|
+
get(target, prop, receiver) {
|
|
152
|
+
if (prop === '_isLazyProxy') return true;
|
|
153
|
+
if (prop === 'hydrate') return hydrate;
|
|
154
|
+
if (prop === 'isHydrated') return isHydrated;
|
|
155
|
+
hydrate();
|
|
156
|
+
if (realPlugin && realPlugin.api) {
|
|
157
|
+
const val = Reflect.get(realPlugin.api, prop, receiver);
|
|
158
|
+
return typeof val === 'function' ? val.bind(realPlugin.api) : val;
|
|
159
|
+
}
|
|
160
|
+
return Reflect.get(target, prop, receiver);
|
|
161
|
+
},
|
|
162
|
+
has(target, prop) {
|
|
163
|
+
hydrate();
|
|
164
|
+
return realPlugin && realPlugin.api ? Reflect.has(realPlugin.api, prop) : Reflect.has(target, prop);
|
|
165
|
+
},
|
|
166
|
+
set(target, prop, value, receiver) {
|
|
167
|
+
hydrate();
|
|
168
|
+
if (realPlugin && realPlugin.api) {
|
|
169
|
+
return Reflect.set(realPlugin.api, prop, value, receiver);
|
|
170
|
+
}
|
|
171
|
+
return Reflect.set(target, prop, value, receiver);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const lazyWrapper = {
|
|
176
|
+
name,
|
|
177
|
+
version: lazyOptions.version || (typeof pluginFactory === 'object' && pluginFactory?.version) || '1.0.0',
|
|
178
|
+
dependencies: lazyOptions.dependencies || (typeof pluginFactory === 'object' && pluginFactory?.dependencies) || {},
|
|
179
|
+
csp: lazyOptions.csp || (typeof pluginFactory === 'object' && pluginFactory?.csp) || null,
|
|
180
|
+
isLazy: true,
|
|
181
|
+
get isHydrated() { return isHydrated; },
|
|
182
|
+
hydrate,
|
|
183
|
+
api: apiProxy,
|
|
184
|
+
|
|
185
|
+
register(ctx) {
|
|
186
|
+
regCtx = ctx;
|
|
187
|
+
|
|
188
|
+
// Register proxy services in app service registry
|
|
189
|
+
if (ctx.app && ctx.app.serviceRegistry && lazyServices.length > 0) {
|
|
190
|
+
for (const svcName of lazyServices) {
|
|
191
|
+
if (!ctx.app.serviceRegistry.has(svcName)) {
|
|
192
|
+
const proxyHandler = async (input, serviceCtx) => {
|
|
193
|
+
hydrate();
|
|
194
|
+
const realDef = ctx.app.serviceRegistry.get(svcName);
|
|
195
|
+
if (realDef && typeof realDef.handler === 'function' && realDef.handler !== proxyHandler) {
|
|
196
|
+
return typeof realDef.rawHandler === 'function'
|
|
197
|
+
? realDef.rawHandler(input, serviceCtx)
|
|
198
|
+
: realDef.handler(input, serviceCtx);
|
|
199
|
+
}
|
|
200
|
+
return null;
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
ctx.app.serviceRegistry.register(
|
|
204
|
+
svcName,
|
|
205
|
+
{ handler: proxyHandler },
|
|
206
|
+
{ overwrite: true }
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Register proxy helpers in template engine
|
|
213
|
+
if (typeof ctx.addHelper === 'function' && lazyHelpers.length > 0) {
|
|
214
|
+
for (const helperName of lazyHelpers) {
|
|
215
|
+
ctx.addHelper(helperName, (...args) => {
|
|
216
|
+
hydrate();
|
|
217
|
+
const helpers = regCtx?.pluginManager?.getHelpers ? regCtx.pluginManager.getHelpers() : {};
|
|
218
|
+
if (typeof helpers[helperName] === 'function') {
|
|
219
|
+
return helpers[helperName](...args);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Wire event triggers if eventDispatcher is available
|
|
226
|
+
if (ctx.app?.events && lazyEvents.length > 0) {
|
|
227
|
+
for (const eventName of lazyEvents) {
|
|
228
|
+
ctx.app.events.on(eventName, () => {
|
|
229
|
+
hydrate();
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
|
|
235
|
+
onRoutesReady(ctx) {
|
|
236
|
+
routesCtx = ctx;
|
|
237
|
+
|
|
238
|
+
if (!ctx.app) return;
|
|
239
|
+
|
|
240
|
+
// Mount lazy trigger router for each prefix
|
|
241
|
+
for (const prefix of prefixes) {
|
|
242
|
+
ctx.app.use(prefix, (req, res, next) => {
|
|
243
|
+
if (!isHydrated) {
|
|
244
|
+
hydrate();
|
|
245
|
+
}
|
|
246
|
+
router(req, res, next);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
return lazyWrapper;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
module.exports = {
|
|
256
|
+
lazyPlugin,
|
|
257
|
+
defineLazyPlugin: lazyPlugin,
|
|
258
|
+
};
|
|
@@ -27,9 +27,12 @@ const BOT_PATTERNS = [
|
|
|
27
27
|
{ pattern: /petalbot/i, name: 'PetalBot' },
|
|
28
28
|
{ pattern: /gptbot/i, name: 'GPTBot' },
|
|
29
29
|
{ pattern: /claudebot/i, name: 'ClaudeBot' },
|
|
30
|
+
{ pattern: /amazonbot/i, name: 'Amazon' },
|
|
30
31
|
{ pattern: /anthropic/i, name: 'Anthropic' },
|
|
31
32
|
{ pattern: /ccbot/i, name: 'CCBot' },
|
|
32
33
|
{ pattern: /bytespider/i, name: 'ByteSpider' },
|
|
34
|
+
{ pattern: /dataforseo/i, name: 'DataForSEO' },
|
|
35
|
+
{ pattern: /yisouspider/i, name: 'Yisou' },
|
|
33
36
|
{ pattern: /open-research-bot/i, name: 'open-research-bot' },
|
|
34
37
|
{ pattern: /ptb12/i, name: 'PTB12' },
|
|
35
38
|
{ pattern: /curl\//i, name: 'curl' },
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @module plugins/site-analytics/client-error-handler
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { formatDbError } = require('./tracking');
|
|
8
|
+
|
|
7
9
|
const DEFAULT_TABLE = 'analytics_client_errors';
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -71,7 +73,7 @@ function createErrorReportHandler(options) {
|
|
|
71
73
|
|
|
72
74
|
res.status(202).json({ ok: true });
|
|
73
75
|
} catch (e) {
|
|
74
|
-
console.error('[site-analytics] Client error report failed:', e
|
|
76
|
+
console.error('[site-analytics] Client error report failed:', formatDbError(e));
|
|
75
77
|
res.status(500).json({ error: 'Report failed' });
|
|
76
78
|
}
|
|
77
79
|
};
|
|
@@ -140,6 +140,8 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
let trackingMiddleware = null;
|
|
144
|
+
|
|
143
145
|
return {
|
|
144
146
|
name: 'site-analytics',
|
|
145
147
|
version: '1.0.0',
|
|
@@ -177,13 +179,17 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
177
179
|
register(ctx) {
|
|
178
180
|
const knex = db.knex || db;
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
trackingMiddleware = createTrackingMiddleware({
|
|
181
183
|
knex,
|
|
182
184
|
excludePaths,
|
|
183
185
|
trackBots,
|
|
184
186
|
tableName,
|
|
185
187
|
batchSize: options.batchSize ?? 20,
|
|
186
188
|
flushIntervalMs: options.flushIntervalMs ?? 3000,
|
|
189
|
+
maxRetries: options.maxRetries ?? 3,
|
|
190
|
+
maxBackoffMs: options.maxBackoffMs ?? 60000,
|
|
191
|
+
maxQueueSize: options.maxQueueSize ?? 5000,
|
|
192
|
+
errorLogIntervalMs: options.errorLogIntervalMs ?? 60000,
|
|
187
193
|
});
|
|
188
194
|
|
|
189
195
|
ctx.app.use(trackingMiddleware);
|
|
@@ -199,15 +205,18 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
199
205
|
|
|
200
206
|
return () => {
|
|
201
207
|
stopCleanup();
|
|
208
|
+
if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
|
|
202
209
|
};
|
|
203
210
|
},
|
|
204
211
|
|
|
205
212
|
dispose() {
|
|
206
213
|
stopCleanup();
|
|
214
|
+
if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
|
|
207
215
|
},
|
|
208
216
|
|
|
209
217
|
shutdown() {
|
|
210
218
|
stopCleanup();
|
|
219
|
+
if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
|
|
211
220
|
},
|
|
212
221
|
|
|
213
222
|
onRoutesReady(ctx) {
|