@sonicjs-cms/core 2.15.0 → 2.16.1
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/dist/{app-COElO4Rm.d.cts → app-D9L3mrC-.d.cts} +1 -0
- package/dist/{app-COElO4Rm.d.ts → app-D9L3mrC-.d.ts} +1 -0
- package/dist/{chunk-UFPT5KCQ.cjs → chunk-7HHIZQNE.cjs} +19 -9
- package/dist/chunk-7HHIZQNE.cjs.map +1 -0
- package/dist/{chunk-7MMD5WMK.js → chunk-BAMJVG33.js} +9 -9
- package/dist/{chunk-7MMD5WMK.js.map → chunk-BAMJVG33.js.map} +1 -1
- package/dist/{chunk-RVD7PLMU.cjs → chunk-HU4MN74Q.cjs} +55 -5
- package/dist/chunk-HU4MN74Q.cjs.map +1 -0
- package/dist/{chunk-EWXV2KG2.js → chunk-JF5RQXPN.js} +3 -3
- package/dist/{chunk-EWXV2KG2.js.map → chunk-JF5RQXPN.js.map} +1 -1
- package/dist/{chunk-2BL2A62D.js → chunk-KYAF33AF.js} +15 -5
- package/dist/chunk-KYAF33AF.js.map +1 -0
- package/dist/{chunk-26HYU7MX.cjs → chunk-MZS33LLH.cjs} +114 -114
- package/dist/{chunk-26HYU7MX.cjs.map → chunk-MZS33LLH.cjs.map} +1 -1
- package/dist/{chunk-5SOFMH66.js → chunk-PUZMLXOJ.js} +55 -5
- package/dist/chunk-PUZMLXOJ.js.map +1 -0
- package/dist/{chunk-43AB4EH4.cjs → chunk-U6FOL6EO.cjs} +2 -2
- package/dist/{chunk-43AB4EH4.cjs.map → chunk-U6FOL6EO.cjs.map} +1 -1
- package/dist/{chunk-VUISYUHY.cjs → chunk-V76ERLX6.cjs} +3 -3
- package/dist/{chunk-VUISYUHY.cjs.map → chunk-V76ERLX6.cjs.map} +1 -1
- package/dist/{chunk-G7XSN72O.js → chunk-W33MHOPA.js} +2 -2
- package/dist/{chunk-G7XSN72O.js.map → chunk-W33MHOPA.js.map} +1 -1
- package/dist/index.cjs +829 -135
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +707 -13
- package/dist/index.js.map +1 -1
- package/dist/middleware.cjs +29 -29
- package/dist/middleware.d.cts +1 -1
- package/dist/middleware.d.ts +1 -1
- package/dist/middleware.js +3 -3
- package/dist/migrations-MYQI2KAJ.cjs +13 -0
- package/dist/{migrations-APFGYCB6.cjs.map → migrations-MYQI2KAJ.cjs.map} +1 -1
- package/dist/migrations-WCEBO5QQ.js +4 -0
- package/dist/{migrations-YB52SLW7.js.map → migrations-WCEBO5QQ.js.map} +1 -1
- package/dist/routes.cjs +28 -28
- package/dist/routes.d.cts +1 -1
- package/dist/routes.d.ts +1 -1
- package/dist/routes.js +5 -5
- package/dist/services.cjs +23 -23
- package/dist/services.d.cts +1 -1
- package/dist/services.d.ts +1 -1
- package/dist/services.js +2 -2
- package/dist/utils.cjs +11 -11
- package/dist/utils.js +1 -1
- package/migrations/035_user_profiles_data_column.sql +14 -15
- package/migrations/036_analytics_events.sql +22 -0
- package/package.json +1 -1
- package/dist/chunk-2BL2A62D.js.map +0 -1
- package/dist/chunk-5SOFMH66.js.map +0 -1
- package/dist/chunk-RVD7PLMU.cjs.map +0 -1
- package/dist/chunk-UFPT5KCQ.cjs.map +0 -1
- package/dist/migrations-APFGYCB6.cjs +0 -13
- package/dist/migrations-YB52SLW7.js +0 -4
package/dist/index.js
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-
|
|
2
|
-
export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-
|
|
1
|
+
import { renderConfirmationDialog, getConfirmationDialogScript, api_default, api_media_default, api_system_default, admin_api_default, router, adminCollectionsRoutes, adminFormsRoutes, adminSettingsRoutes, public_forms_default, router2, admin_content_default, adminMediaRoutes, userProfilesPlugin, adminPluginRoutes, adminLogsRoutes, userRoutes, auth_default, test_cleanup_default } from './chunk-BAMJVG33.js';
|
|
2
|
+
export { ROUTES_INFO, admin_api_default as adminApiRoutes, adminCheckboxRoutes, admin_code_examples_default as adminCodeExamplesRoutes, adminCollectionsRoutes, admin_content_default as adminContentRoutes, router as adminDashboardRoutes, adminDesignRoutes, adminLogsRoutes, adminMediaRoutes, adminPluginRoutes, adminSettingsRoutes, admin_testimonials_default as adminTestimonialsRoutes, userRoutes as adminUsersRoutes, api_content_crud_default as apiContentCrudRoutes, api_media_default as apiMediaRoutes, api_default as apiRoutes, api_system_default as apiSystemRoutes, auth_default as authRoutes, createUserProfilesPlugin, defineUserProfile, getUserProfileConfig, userProfilesPlugin } from './chunk-BAMJVG33.js';
|
|
3
3
|
import { SettingsService, setAppInstance, schema_exports } from './chunk-TBJY2FF7.js';
|
|
4
4
|
export { Logger, apiTokens, collections, content, contentVersions, getLogger, initLogger, insertCollectionSchema, insertContentSchema, insertLogConfigSchema, insertMediaSchema, insertPluginActivityLogSchema, insertPluginAssetSchema, insertPluginHookSchema, insertPluginRouteSchema, insertPluginSchema, insertSystemLogSchema, insertUserSchema, insertWorkflowHistorySchema, logConfig, media, pluginActivityLog, pluginAssets, pluginHooks, pluginRoutes, plugins, selectCollectionSchema, selectContentSchema, selectLogConfigSchema, selectMediaSchema, selectPluginActivityLogSchema, selectPluginAssetSchema, selectPluginHookSchema, selectPluginRouteSchema, selectPluginSchema, selectSystemLogSchema, selectUserSchema, selectWorkflowHistorySchema, systemLogs, users, workflowHistory } from './chunk-TBJY2FF7.js';
|
|
5
|
-
import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection } from './chunk-
|
|
6
|
-
export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-
|
|
7
|
-
import { PluginService, PLUGIN_REGISTRY } from './chunk-
|
|
8
|
-
export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-
|
|
9
|
-
export { MigrationService } from './chunk-
|
|
5
|
+
import { requireAuth, AuthManager, metricsMiddleware, bootstrapMiddleware, securityHeadersMiddleware, csrfProtection, requireRole } from './chunk-KYAF33AF.js';
|
|
6
|
+
export { AuthManager, PermissionManager, bootstrapMiddleware, cacheHeaders, compressionMiddleware, detailedLoggingMiddleware, getActivePlugins, isPluginActive, logActivity, loggingMiddleware, optionalAuth, performanceLoggingMiddleware, requireActivePlugin, requireActivePlugins, requireAnyPermission, requireAuth, requirePermission, requireRole, securityHeadersMiddleware as securityHeaders, securityLoggingMiddleware } from './chunk-KYAF33AF.js';
|
|
7
|
+
import { PluginService, PLUGIN_REGISTRY } from './chunk-W33MHOPA.js';
|
|
8
|
+
export { PluginBootstrapService, PluginService as PluginServiceClass, backfillFormSubmissions, cleanupRemovedCollections, createContentFromSubmission, deriveCollectionSchemaFromFormio, deriveSubmissionTitle, fullCollectionSync, getAvailableCollectionNames, getManagedCollections, isCollectionManaged, loadCollectionConfig, loadCollectionConfigs, mapFormStatusToContentStatus, registerCollections, syncAllFormCollections, syncCollection, syncCollections, syncFormCollection, validateCollectionConfig } from './chunk-W33MHOPA.js';
|
|
9
|
+
export { MigrationService } from './chunk-PUZMLXOJ.js';
|
|
10
10
|
export { renderFilterBar } from './chunk-ON5ZMSU4.js';
|
|
11
11
|
import { renderAdminLayout } from './chunk-XWIA3HVX.js';
|
|
12
12
|
export { getConfirmationDialogScript, renderAlert, renderConfirmationDialog, renderForm, renderFormField, renderPagination, renderTable } from './chunk-XWIA3HVX.js';
|
|
13
13
|
import { init_admin_layout_catalyst_template, renderAdminLayoutCatalyst } from './chunk-55RDMDOP.js';
|
|
14
14
|
export { HookSystemImpl, HookUtils, PluginManager as PluginManagerClass, PluginRegistryImpl, PluginValidator as PluginValidatorClass, ScopedHookSystem as ScopedHookSystemClass } from './chunk-TFNTM3OA.js';
|
|
15
|
-
import { PluginBuilder } from './chunk-EXNEW5US.js';
|
|
15
|
+
import { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
|
|
16
16
|
export { PluginBuilder, PluginHelpers } from './chunk-EXNEW5US.js';
|
|
17
|
-
import { package_default, getCoreVersion } from './chunk-
|
|
18
|
-
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-
|
|
17
|
+
import { package_default, getCoreVersion } from './chunk-JF5RQXPN.js';
|
|
18
|
+
export { QueryFilterBuilder, SONICJS_VERSION, TemplateRenderer, buildQuery, getCoreVersion, renderTemplate, templateRenderer } from './chunk-JF5RQXPN.js';
|
|
19
19
|
import './chunk-X7ZAEI5S.js';
|
|
20
20
|
export { metricsTracker } from './chunk-FICTAGD4.js';
|
|
21
21
|
export { escapeHtml, sanitizeInput, sanitizeObject } from './chunk-TQABQWOP.js';
|
|
@@ -5705,7 +5705,7 @@ var BruteForceDetector = class {
|
|
|
5705
5705
|
}
|
|
5706
5706
|
settings;
|
|
5707
5707
|
async recordFailedAttempt(ip, email) {
|
|
5708
|
-
if (!this.settings.enabled) {
|
|
5708
|
+
if (!this.settings.enabled || !this.kv) {
|
|
5709
5709
|
return { ipCount: 0, emailCount: 0, shouldLockIP: false, shouldLockEmail: false, isSuspicious: false };
|
|
5710
5710
|
}
|
|
5711
5711
|
const windowMs = this.settings.windowMinutes * 60 * 1e3;
|
|
@@ -5722,7 +5722,7 @@ var BruteForceDetector = class {
|
|
|
5722
5722
|
return { ipCount, emailCount, shouldLockIP, shouldLockEmail, isSuspicious };
|
|
5723
5723
|
}
|
|
5724
5724
|
async isLocked(ip, email) {
|
|
5725
|
-
if (!this.settings.enabled) {
|
|
5725
|
+
if (!this.settings.enabled || !this.kv) {
|
|
5726
5726
|
return { locked: false };
|
|
5727
5727
|
}
|
|
5728
5728
|
const ipLocked = await this.kv.get(`${LOCK_PREFIX}ip:${ip}`);
|
|
@@ -5736,6 +5736,7 @@ var BruteForceDetector = class {
|
|
|
5736
5736
|
return { locked: false };
|
|
5737
5737
|
}
|
|
5738
5738
|
async lockIP(ip) {
|
|
5739
|
+
if (!this.kv) return;
|
|
5739
5740
|
const ttl = this.settings.lockoutDurationMinutes * 60;
|
|
5740
5741
|
await this.kv.put(`${LOCK_PREFIX}ip:${ip}`, JSON.stringify({
|
|
5741
5742
|
lockedAt: Date.now(),
|
|
@@ -5743,6 +5744,7 @@ var BruteForceDetector = class {
|
|
|
5743
5744
|
}), { expirationTtl: ttl });
|
|
5744
5745
|
}
|
|
5745
5746
|
async lockEmail(email) {
|
|
5747
|
+
if (!this.kv) return;
|
|
5746
5748
|
const ttl = this.settings.lockoutDurationMinutes * 60;
|
|
5747
5749
|
await this.kv.put(`${LOCK_PREFIX}email:${email}`, JSON.stringify({
|
|
5748
5750
|
lockedAt: Date.now(),
|
|
@@ -5750,12 +5752,15 @@ var BruteForceDetector = class {
|
|
|
5750
5752
|
}), { expirationTtl: ttl });
|
|
5751
5753
|
}
|
|
5752
5754
|
async unlockIP(ip) {
|
|
5755
|
+
if (!this.kv) return;
|
|
5753
5756
|
await this.kv.delete(`${LOCK_PREFIX}ip:${ip}`);
|
|
5754
5757
|
}
|
|
5755
5758
|
async unlockEmail(email) {
|
|
5759
|
+
if (!this.kv) return;
|
|
5756
5760
|
await this.kv.delete(`${LOCK_PREFIX}email:${email}`);
|
|
5757
5761
|
}
|
|
5758
5762
|
async getActiveLockouts() {
|
|
5763
|
+
if (!this.kv) return [];
|
|
5759
5764
|
const ipLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}ip:` });
|
|
5760
5765
|
const emailLocks = await this.kv.list({ prefix: `${LOCK_PREFIX}email:` });
|
|
5761
5766
|
const lockouts = [];
|
|
@@ -5786,6 +5791,7 @@ var BruteForceDetector = class {
|
|
|
5786
5791
|
return lockouts;
|
|
5787
5792
|
}
|
|
5788
5793
|
async releaseLockout(key) {
|
|
5794
|
+
if (!this.kv) return;
|
|
5789
5795
|
await this.kv.delete(key);
|
|
5790
5796
|
}
|
|
5791
5797
|
isAboveAlertThreshold(count) {
|
|
@@ -6888,7 +6894,7 @@ function renderEventsPage(data) {
|
|
|
6888
6894
|
<tr>
|
|
6889
6895
|
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Time</th>
|
|
6890
6896
|
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Type</th>
|
|
6891
|
-
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Object</th>
|
|
6897
|
+
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Object / User</th>
|
|
6892
6898
|
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Status</th>
|
|
6893
6899
|
<th class="px-6 py-3 text-left text-xs font-medium text-zinc-500 dark:text-zinc-400 uppercase tracking-wider">Event ID</th>
|
|
6894
6900
|
</tr>
|
|
@@ -6947,8 +6953,20 @@ function formatTimestamp3(timestamp) {
|
|
|
6947
6953
|
second: "2-digit"
|
|
6948
6954
|
});
|
|
6949
6955
|
}
|
|
6956
|
+
function extractUserInfo(event) {
|
|
6957
|
+
try {
|
|
6958
|
+
const data = typeof event.data === "string" ? JSON.parse(event.data) : event.data;
|
|
6959
|
+
const userId = data?.metadata?.sonicjs_user_id || data?.sonicjs_user_id;
|
|
6960
|
+
const email = data?.customer_email || data?.customerEmail || data?.receipt_email;
|
|
6961
|
+
return { userId: userId || void 0, email: email || void 0 };
|
|
6962
|
+
} catch {
|
|
6963
|
+
return {};
|
|
6964
|
+
}
|
|
6965
|
+
}
|
|
6950
6966
|
function renderEventRow(event) {
|
|
6951
6967
|
const errorTooltip = event.error ? ` title="${event.error.replace(/"/g, """)}"` : "";
|
|
6968
|
+
const userInfo = extractUserInfo(event);
|
|
6969
|
+
const userLink = userInfo.userId ? `<a href="/admin/users/${userInfo.userId}" class="text-sm text-blue-600 dark:text-blue-400 hover:underline">${userInfo.email || userInfo.userId}</a>` : userInfo.email ? `<span class="text-sm text-zinc-500 dark:text-zinc-400">${userInfo.email}</span>` : "";
|
|
6952
6970
|
return `
|
|
6953
6971
|
<tr class="hover:bg-zinc-950/[0.025] dark:hover:bg-white/[0.025]"${errorTooltip}>
|
|
6954
6972
|
<td class="px-6 py-4 whitespace-nowrap text-sm text-zinc-500 dark:text-zinc-400">
|
|
@@ -6960,6 +6978,7 @@ function renderEventRow(event) {
|
|
|
6960
6978
|
<td class="px-6 py-4 whitespace-nowrap">
|
|
6961
6979
|
<div class="text-sm font-mono text-zinc-500 dark:text-zinc-400">${event.objectId || "-"}</div>
|
|
6962
6980
|
<div class="text-xs text-zinc-400 dark:text-zinc-500">${event.objectType}</div>
|
|
6981
|
+
${userLink ? `<div class="mt-1">${userLink}</div>` : ""}
|
|
6963
6982
|
</td>
|
|
6964
6983
|
<td class="px-6 py-4 whitespace-nowrap">${eventStatusBadge(event.status)}</td>
|
|
6965
6984
|
<td class="px-6 py-4 whitespace-nowrap text-xs font-mono text-zinc-400 dark:text-zinc-500">${event.stripeEventId}</td>
|
|
@@ -7762,6 +7781,672 @@ function pluginMenuMiddleware() {
|
|
|
7762
7781
|
};
|
|
7763
7782
|
}
|
|
7764
7783
|
|
|
7784
|
+
// src/plugins/types.ts
|
|
7785
|
+
var HOOKS2 = {
|
|
7786
|
+
// Request lifecycle
|
|
7787
|
+
REQUEST_START: "request:start",
|
|
7788
|
+
REQUEST_END: "request:end",
|
|
7789
|
+
USER_LOGIN: "user:login"};
|
|
7790
|
+
init_admin_layout_catalyst_template();
|
|
7791
|
+
var adminRoutes4 = new Hono();
|
|
7792
|
+
adminRoutes4.use("*", requireAuth());
|
|
7793
|
+
adminRoutes4.use("*", async (c, next) => {
|
|
7794
|
+
const user = c.get("user");
|
|
7795
|
+
if (user?.role !== "admin") {
|
|
7796
|
+
return c.text("Access denied", 403);
|
|
7797
|
+
}
|
|
7798
|
+
return next();
|
|
7799
|
+
});
|
|
7800
|
+
adminRoutes4.get("/", async (c) => {
|
|
7801
|
+
const user = c.get("user");
|
|
7802
|
+
const db = c.env.DB;
|
|
7803
|
+
let totalRequests = 0;
|
|
7804
|
+
let uniqueIPs = 0;
|
|
7805
|
+
let avgDuration = 0;
|
|
7806
|
+
let errorCount = 0;
|
|
7807
|
+
let topPages = [];
|
|
7808
|
+
let recentActivity = [];
|
|
7809
|
+
try {
|
|
7810
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
7811
|
+
const dayAgo = now - 86400;
|
|
7812
|
+
const [requestsResult, ipsResult, durationResult, errorsResult, pagesResult, activityResult] = await Promise.all([
|
|
7813
|
+
db.prepare("SELECT COUNT(*) as count FROM system_logs WHERE category = ? AND created_at > ?").bind("api", dayAgo).first(),
|
|
7814
|
+
db.prepare("SELECT COUNT(DISTINCT ip_address) as count FROM system_logs WHERE category = ? AND created_at > ?").bind("api", dayAgo).first(),
|
|
7815
|
+
db.prepare("SELECT AVG(duration) as avg FROM system_logs WHERE category = ? AND created_at > ? AND duration IS NOT NULL").bind("api", dayAgo).first(),
|
|
7816
|
+
db.prepare("SELECT COUNT(*) as count FROM system_logs WHERE level IN (?, ?) AND created_at > ?").bind("error", "fatal", dayAgo).first(),
|
|
7817
|
+
db.prepare("SELECT url, COUNT(*) as views FROM system_logs WHERE category = ? AND created_at > ? AND url IS NOT NULL GROUP BY url ORDER BY views DESC LIMIT 10").bind("api", dayAgo).all(),
|
|
7818
|
+
db.prepare("SELECT url, method, status_code, duration, created_at FROM system_logs WHERE category = ? ORDER BY created_at DESC LIMIT 20").bind("api").all()
|
|
7819
|
+
]);
|
|
7820
|
+
totalRequests = requestsResult?.count || 0;
|
|
7821
|
+
uniqueIPs = ipsResult?.count || 0;
|
|
7822
|
+
avgDuration = Math.round(durationResult?.avg || 0);
|
|
7823
|
+
errorCount = errorsResult?.count || 0;
|
|
7824
|
+
topPages = (pagesResult.results || []).map((r) => ({ path: r.url, views: r.views }));
|
|
7825
|
+
recentActivity = activityResult.results || [];
|
|
7826
|
+
} catch {
|
|
7827
|
+
}
|
|
7828
|
+
const content2 = `
|
|
7829
|
+
<div class="space-y-8">
|
|
7830
|
+
<div>
|
|
7831
|
+
<h1 class="text-2xl font-semibold text-zinc-950 dark:text-white">Analytics Dashboard</h1>
|
|
7832
|
+
<p class="mt-1 text-sm text-zinc-500 dark:text-zinc-400">Last 24 hours overview from system logs</p>
|
|
7833
|
+
</div>
|
|
7834
|
+
|
|
7835
|
+
<!-- Stats Cards -->
|
|
7836
|
+
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
7837
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7838
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Total Requests</p>
|
|
7839
|
+
<p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${totalRequests.toLocaleString()}</p>
|
|
7840
|
+
</div>
|
|
7841
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7842
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Unique Visitors</p>
|
|
7843
|
+
<p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${uniqueIPs.toLocaleString()}</p>
|
|
7844
|
+
</div>
|
|
7845
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7846
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Avg Response Time</p>
|
|
7847
|
+
<p class="mt-2 text-3xl font-semibold text-zinc-950 dark:text-white">${avgDuration}ms</p>
|
|
7848
|
+
</div>
|
|
7849
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 p-6 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7850
|
+
<p class="text-sm font-medium text-zinc-500 dark:text-zinc-400">Errors</p>
|
|
7851
|
+
<p class="mt-2 text-3xl font-semibold ${errorCount > 0 ? "text-red-600 dark:text-red-400" : "text-zinc-950 dark:text-white"}">${errorCount.toLocaleString()}</p>
|
|
7852
|
+
</div>
|
|
7853
|
+
</div>
|
|
7854
|
+
|
|
7855
|
+
<!-- Top Pages -->
|
|
7856
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7857
|
+
<div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
|
|
7858
|
+
<h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Top Pages</h2>
|
|
7859
|
+
</div>
|
|
7860
|
+
<div class="divide-y divide-zinc-950/5 dark:divide-white/10">
|
|
7861
|
+
${topPages.length > 0 ? topPages.map((p) => `
|
|
7862
|
+
<div class="flex items-center justify-between px-6 py-3">
|
|
7863
|
+
<span class="text-sm text-zinc-700 dark:text-zinc-300 font-mono truncate">${escapeHtml3(p.path)}</span>
|
|
7864
|
+
<span class="text-sm font-medium text-zinc-500 dark:text-zinc-400">${p.views}</span>
|
|
7865
|
+
</div>
|
|
7866
|
+
`).join("") : `
|
|
7867
|
+
<div class="px-6 py-8 text-center text-sm text-zinc-500 dark:text-zinc-400">
|
|
7868
|
+
No page views recorded yet. Analytics data will appear once requests are logged.
|
|
7869
|
+
</div>
|
|
7870
|
+
`}
|
|
7871
|
+
</div>
|
|
7872
|
+
</div>
|
|
7873
|
+
|
|
7874
|
+
<!-- Recent Activity -->
|
|
7875
|
+
<div class="rounded-lg bg-white dark:bg-zinc-800 ring-1 ring-zinc-950/5 dark:ring-white/10">
|
|
7876
|
+
<div class="px-6 py-4 border-b border-zinc-950/5 dark:border-white/10">
|
|
7877
|
+
<h2 class="text-lg font-semibold text-zinc-950 dark:text-white">Recent Activity</h2>
|
|
7878
|
+
</div>
|
|
7879
|
+
<div class="overflow-x-auto">
|
|
7880
|
+
<table class="w-full text-sm">
|
|
7881
|
+
<thead class="bg-zinc-50 dark:bg-zinc-800/50">
|
|
7882
|
+
<tr>
|
|
7883
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Path</th>
|
|
7884
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Method</th>
|
|
7885
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Status</th>
|
|
7886
|
+
<th class="px-6 py-2 text-left font-medium text-zinc-500 dark:text-zinc-400">Duration</th>
|
|
7887
|
+
</tr>
|
|
7888
|
+
</thead>
|
|
7889
|
+
<tbody class="divide-y divide-zinc-950/5 dark:divide-white/10">
|
|
7890
|
+
${recentActivity.length > 0 ? recentActivity.map((a) => `
|
|
7891
|
+
<tr>
|
|
7892
|
+
<td class="px-6 py-2 font-mono text-zinc-700 dark:text-zinc-300 truncate max-w-xs">${escapeHtml3(a.url || "")}</td>
|
|
7893
|
+
<td class="px-6 py-2"><span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${a.method === "GET" ? "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400" : "bg-blue-50 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"}">${a.method || ""}</span></td>
|
|
7894
|
+
<td class="px-6 py-2"><span class="inline-flex items-center rounded px-1.5 py-0.5 text-xs font-medium ${(a.status_code || 0) >= 400 ? "bg-red-50 text-red-700 dark:bg-red-900/30 dark:text-red-400" : "bg-green-50 text-green-700 dark:bg-green-900/30 dark:text-green-400"}">${a.status_code || ""}</span></td>
|
|
7895
|
+
<td class="px-6 py-2 text-zinc-500 dark:text-zinc-400">${a.duration || 0}ms</td>
|
|
7896
|
+
</tr>
|
|
7897
|
+
`).join("") : `
|
|
7898
|
+
<tr>
|
|
7899
|
+
<td colspan="4" class="px-6 py-8 text-center text-zinc-500 dark:text-zinc-400">No activity recorded yet.</td>
|
|
7900
|
+
</tr>
|
|
7901
|
+
`}
|
|
7902
|
+
</tbody>
|
|
7903
|
+
</table>
|
|
7904
|
+
</div>
|
|
7905
|
+
</div>
|
|
7906
|
+
</div>
|
|
7907
|
+
`;
|
|
7908
|
+
return c.html(renderAdminLayoutCatalyst({
|
|
7909
|
+
title: "Analytics",
|
|
7910
|
+
pageTitle: "Analytics Dashboard",
|
|
7911
|
+
currentPath: "/admin/analytics",
|
|
7912
|
+
version: c.get("appVersion"),
|
|
7913
|
+
user: user ? {
|
|
7914
|
+
name: user.email.split("@")[0] || "Admin",
|
|
7915
|
+
email: user.email,
|
|
7916
|
+
role: user.role
|
|
7917
|
+
} : void 0,
|
|
7918
|
+
content: content2,
|
|
7919
|
+
dynamicMenuItems: c.get("pluginMenuItems")
|
|
7920
|
+
}));
|
|
7921
|
+
});
|
|
7922
|
+
function escapeHtml3(str) {
|
|
7923
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
7924
|
+
}
|
|
7925
|
+
|
|
7926
|
+
// src/plugins/core-plugins/analytics/index.ts
|
|
7927
|
+
function createAnalyticsPlugin() {
|
|
7928
|
+
const builder = PluginBuilder.create({
|
|
7929
|
+
name: "core-analytics",
|
|
7930
|
+
version: "1.0.0-beta.1",
|
|
7931
|
+
description: "Core analytics tracking and reporting plugin"
|
|
7932
|
+
});
|
|
7933
|
+
builder.metadata({
|
|
7934
|
+
author: {
|
|
7935
|
+
name: "SonicJS Team",
|
|
7936
|
+
email: "team@sonicjs.com"
|
|
7937
|
+
},
|
|
7938
|
+
license: "MIT",
|
|
7939
|
+
compatibility: "^0.1.0",
|
|
7940
|
+
dependencies: ["core-auth"]
|
|
7941
|
+
// Requires auth for admin access
|
|
7942
|
+
});
|
|
7943
|
+
const analyticsAPI = new Hono();
|
|
7944
|
+
analyticsAPI.get("/stats", async (c) => {
|
|
7945
|
+
const timeRange = c.req.query("range") || "7d";
|
|
7946
|
+
c.req.query("metric") || "all";
|
|
7947
|
+
return c.json({
|
|
7948
|
+
message: "Analytics stats",
|
|
7949
|
+
data: {
|
|
7950
|
+
pageviews: 12500,
|
|
7951
|
+
uniqueVisitors: 3200,
|
|
7952
|
+
sessions: 4800,
|
|
7953
|
+
avgSessionDuration: 245,
|
|
7954
|
+
bounceRate: 0.35,
|
|
7955
|
+
topPages: [
|
|
7956
|
+
{ path: "/", views: 3200 },
|
|
7957
|
+
{ path: "/about", views: 1800 },
|
|
7958
|
+
{ path: "/contact", views: 950 }
|
|
7959
|
+
],
|
|
7960
|
+
timeRange
|
|
7961
|
+
}
|
|
7962
|
+
});
|
|
7963
|
+
});
|
|
7964
|
+
analyticsAPI.post("/track", async (c) => {
|
|
7965
|
+
const event = await c.req.json();
|
|
7966
|
+
console.info("Analytics event tracked:", event);
|
|
7967
|
+
return c.json({
|
|
7968
|
+
message: "Event tracked successfully",
|
|
7969
|
+
eventId: `event-${Date.now()}`
|
|
7970
|
+
});
|
|
7971
|
+
});
|
|
7972
|
+
analyticsAPI.get("/reports", async (c) => {
|
|
7973
|
+
const reportType = c.req.query("type") || "traffic";
|
|
7974
|
+
const startDate = c.req.query("start");
|
|
7975
|
+
const endDate = c.req.query("end");
|
|
7976
|
+
return c.json({
|
|
7977
|
+
message: "Analytics report",
|
|
7978
|
+
data: {
|
|
7979
|
+
reportType,
|
|
7980
|
+
dateRange: { start: startDate, end: endDate },
|
|
7981
|
+
data: []
|
|
7982
|
+
}
|
|
7983
|
+
});
|
|
7984
|
+
});
|
|
7985
|
+
analyticsAPI.get("/realtime", async (c) => {
|
|
7986
|
+
return c.json({
|
|
7987
|
+
message: "Real-time analytics",
|
|
7988
|
+
data: {
|
|
7989
|
+
activeUsers: 23,
|
|
7990
|
+
activePages: [
|
|
7991
|
+
{ path: "/", users: 8 },
|
|
7992
|
+
{ path: "/blog", users: 5 },
|
|
7993
|
+
{ path: "/products", users: 4 }
|
|
7994
|
+
],
|
|
7995
|
+
recentEvents: []
|
|
7996
|
+
}
|
|
7997
|
+
});
|
|
7998
|
+
});
|
|
7999
|
+
builder.addRoute("/api/analytics", analyticsAPI, {
|
|
8000
|
+
description: "Analytics tracking and reporting API",
|
|
8001
|
+
requiresAuth: true,
|
|
8002
|
+
roles: ["admin", "analytics:read"],
|
|
8003
|
+
priority: 3
|
|
8004
|
+
});
|
|
8005
|
+
builder.addRoute("/admin/analytics", adminRoutes4, {
|
|
8006
|
+
description: "Analytics admin dashboard",
|
|
8007
|
+
requiresAuth: true,
|
|
8008
|
+
priority: 50
|
|
8009
|
+
});
|
|
8010
|
+
builder.addSingleMiddleware("analytics-tracker", async (c, next) => {
|
|
8011
|
+
const start = Date.now();
|
|
8012
|
+
const path = c.req.path;
|
|
8013
|
+
const method = c.req.method;
|
|
8014
|
+
const userAgent = c.req.header("user-agent");
|
|
8015
|
+
const referer = c.req.header("referer");
|
|
8016
|
+
const ip = c.req.header("CF-Connecting-IP") || c.req.header("x-forwarded-for");
|
|
8017
|
+
await next();
|
|
8018
|
+
const duration = Date.now() - start;
|
|
8019
|
+
const status = c.res.status;
|
|
8020
|
+
const analyticsData = {
|
|
8021
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
8022
|
+
path,
|
|
8023
|
+
method,
|
|
8024
|
+
status,
|
|
8025
|
+
duration,
|
|
8026
|
+
userAgent,
|
|
8027
|
+
referer,
|
|
8028
|
+
ip,
|
|
8029
|
+
responseSize: c.res.headers.get("content-length") || 0
|
|
8030
|
+
};
|
|
8031
|
+
console.debug("Analytics tracking:", analyticsData);
|
|
8032
|
+
}, {
|
|
8033
|
+
description: "Track page views and request analytics",
|
|
8034
|
+
global: true,
|
|
8035
|
+
priority: 99
|
|
8036
|
+
// Run last to capture response data
|
|
8037
|
+
});
|
|
8038
|
+
builder.addService("analyticsService", {
|
|
8039
|
+
trackEvent: async (event) => {
|
|
8040
|
+
console.info("Tracking event:", event);
|
|
8041
|
+
return { eventId: `event-${Date.now()}` };
|
|
8042
|
+
},
|
|
8043
|
+
trackPageView: async (data) => {
|
|
8044
|
+
console.info("Tracking pageview:", data.path);
|
|
8045
|
+
return { viewId: `view-${Date.now()}` };
|
|
8046
|
+
},
|
|
8047
|
+
getStats: async (_timeRange) => {
|
|
8048
|
+
return {
|
|
8049
|
+
pageviews: 12500,
|
|
8050
|
+
sessions: 4800,
|
|
8051
|
+
uniqueVisitors: 3200
|
|
8052
|
+
};
|
|
8053
|
+
},
|
|
8054
|
+
generateReport: async (type, options) => {
|
|
8055
|
+
console.info(`Generating ${type} report with options:`, options);
|
|
8056
|
+
return { reportId: `report-${Date.now()}` };
|
|
8057
|
+
}
|
|
8058
|
+
}, {
|
|
8059
|
+
description: "Core analytics tracking service",
|
|
8060
|
+
singleton: true
|
|
8061
|
+
});
|
|
8062
|
+
const pageViewSchema = PluginHelpers.createSchema([
|
|
8063
|
+
{ name: "path", type: "string", optional: false },
|
|
8064
|
+
{ name: "title", type: "string", optional: true },
|
|
8065
|
+
{ name: "referrer", type: "string", optional: true },
|
|
8066
|
+
{ name: "userAgent", type: "string", optional: true },
|
|
8067
|
+
{ name: "ipAddress", type: "string", optional: true },
|
|
8068
|
+
{ name: "sessionId", type: "string", optional: true },
|
|
8069
|
+
{ name: "userId", type: "number", optional: true },
|
|
8070
|
+
{ name: "duration", type: "number", optional: true }
|
|
8071
|
+
]);
|
|
8072
|
+
const eventSchema = PluginHelpers.createSchema([
|
|
8073
|
+
{ name: "eventType", type: "string", optional: false },
|
|
8074
|
+
{ name: "eventName", type: "string", optional: false },
|
|
8075
|
+
{ name: "eventData", type: "object", optional: true },
|
|
8076
|
+
{ name: "path", type: "string", optional: true },
|
|
8077
|
+
{ name: "sessionId", type: "string", optional: true },
|
|
8078
|
+
{ name: "userId", type: "number", optional: true }
|
|
8079
|
+
]);
|
|
8080
|
+
const pageViewMigration = PluginHelpers.createMigration("analytics_pageviews", [
|
|
8081
|
+
{ name: "id", type: "INTEGER", primaryKey: true },
|
|
8082
|
+
{ name: "path", type: "TEXT", nullable: false },
|
|
8083
|
+
{ name: "title", type: "TEXT", nullable: true },
|
|
8084
|
+
{ name: "referrer", type: "TEXT", nullable: true },
|
|
8085
|
+
{ name: "user_agent", type: "TEXT", nullable: true },
|
|
8086
|
+
{ name: "ip_address", type: "TEXT", nullable: true },
|
|
8087
|
+
{ name: "session_id", type: "TEXT", nullable: true },
|
|
8088
|
+
{ name: "user_id", type: "INTEGER", nullable: true },
|
|
8089
|
+
{ name: "duration", type: "INTEGER", nullable: true }
|
|
8090
|
+
]);
|
|
8091
|
+
const eventMigration = PluginHelpers.createMigration("analytics_events", [
|
|
8092
|
+
{ name: "id", type: "INTEGER", primaryKey: true },
|
|
8093
|
+
{ name: "event_type", type: "TEXT", nullable: false },
|
|
8094
|
+
{ name: "event_name", type: "TEXT", nullable: false },
|
|
8095
|
+
{ name: "event_data", type: "TEXT", nullable: true },
|
|
8096
|
+
{ name: "path", type: "TEXT", nullable: true },
|
|
8097
|
+
{ name: "session_id", type: "TEXT", nullable: true },
|
|
8098
|
+
{ name: "user_id", type: "INTEGER", nullable: true }
|
|
8099
|
+
]);
|
|
8100
|
+
builder.addModel("PageView", {
|
|
8101
|
+
tableName: "analytics_pageviews",
|
|
8102
|
+
schema: pageViewSchema,
|
|
8103
|
+
migrations: [pageViewMigration]
|
|
8104
|
+
});
|
|
8105
|
+
builder.addModel("AnalyticsEvent", {
|
|
8106
|
+
tableName: "analytics_events",
|
|
8107
|
+
schema: eventSchema,
|
|
8108
|
+
migrations: [eventMigration]
|
|
8109
|
+
});
|
|
8110
|
+
builder.addHook(HOOKS2.REQUEST_START, async (data, _context) => {
|
|
8111
|
+
data.analytics = {
|
|
8112
|
+
startTime: Date.now(),
|
|
8113
|
+
sessionId: `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
|
8114
|
+
};
|
|
8115
|
+
return data;
|
|
8116
|
+
}, {
|
|
8117
|
+
priority: 1,
|
|
8118
|
+
description: "Initialize analytics tracking for requests"
|
|
8119
|
+
});
|
|
8120
|
+
builder.addHook(HOOKS2.REQUEST_END, async (data, _context) => {
|
|
8121
|
+
if (data.analytics) {
|
|
8122
|
+
const duration = Date.now() - data.analytics.startTime;
|
|
8123
|
+
console.debug(`Request completed in ${duration}ms`);
|
|
8124
|
+
}
|
|
8125
|
+
return data;
|
|
8126
|
+
}, {
|
|
8127
|
+
priority: 1,
|
|
8128
|
+
description: "Complete analytics tracking for requests"
|
|
8129
|
+
});
|
|
8130
|
+
builder.addHook(HOOKS2.USER_LOGIN, async (data, context) => {
|
|
8131
|
+
await context.services?.analyticsService?.trackEvent({
|
|
8132
|
+
eventType: "auth",
|
|
8133
|
+
eventName: "user_login",
|
|
8134
|
+
userId: data.userId,
|
|
8135
|
+
eventData: { loginMethod: data.method }
|
|
8136
|
+
});
|
|
8137
|
+
return data;
|
|
8138
|
+
}, {
|
|
8139
|
+
priority: 8,
|
|
8140
|
+
description: "Track user login events"
|
|
8141
|
+
});
|
|
8142
|
+
builder.addHook("content:view", async (data, context) => {
|
|
8143
|
+
await context.services?.analyticsService?.trackEvent({
|
|
8144
|
+
eventType: "content",
|
|
8145
|
+
eventName: "content_view",
|
|
8146
|
+
eventData: {
|
|
8147
|
+
contentId: data.id,
|
|
8148
|
+
contentType: data.type,
|
|
8149
|
+
title: data.title
|
|
8150
|
+
}
|
|
8151
|
+
});
|
|
8152
|
+
return data;
|
|
8153
|
+
}, {
|
|
8154
|
+
priority: 8,
|
|
8155
|
+
description: "Track content view events"
|
|
8156
|
+
});
|
|
8157
|
+
builder.addAdminPage(
|
|
8158
|
+
"/analytics",
|
|
8159
|
+
"Analytics Dashboard",
|
|
8160
|
+
"AnalyticsDashboardView",
|
|
8161
|
+
{
|
|
8162
|
+
description: "View analytics overview and key metrics",
|
|
8163
|
+
permissions: ["admin", "analytics:read"],
|
|
8164
|
+
icon: "chart-bar"
|
|
8165
|
+
}
|
|
8166
|
+
);
|
|
8167
|
+
builder.addAdminPage(
|
|
8168
|
+
"/analytics/reports",
|
|
8169
|
+
"Analytics Reports",
|
|
8170
|
+
"AnalyticsReportsView",
|
|
8171
|
+
{
|
|
8172
|
+
description: "Generate and view detailed analytics reports",
|
|
8173
|
+
permissions: ["admin", "analytics:read"],
|
|
8174
|
+
icon: "document-report"
|
|
8175
|
+
}
|
|
8176
|
+
);
|
|
8177
|
+
builder.addAdminPage(
|
|
8178
|
+
"/analytics/realtime",
|
|
8179
|
+
"Real-time Analytics",
|
|
8180
|
+
"AnalyticsRealtimeView",
|
|
8181
|
+
{
|
|
8182
|
+
description: "View real-time visitor activity",
|
|
8183
|
+
permissions: ["admin", "analytics:read"],
|
|
8184
|
+
icon: "lightning-bolt"
|
|
8185
|
+
}
|
|
8186
|
+
);
|
|
8187
|
+
builder.addAdminPage(
|
|
8188
|
+
"/analytics/settings",
|
|
8189
|
+
"Analytics Settings",
|
|
8190
|
+
"AnalyticsSettingsView",
|
|
8191
|
+
{
|
|
8192
|
+
description: "Configure analytics tracking and data collection",
|
|
8193
|
+
permissions: ["admin", "analytics:configure"],
|
|
8194
|
+
icon: "cog"
|
|
8195
|
+
}
|
|
8196
|
+
);
|
|
8197
|
+
builder.addMenuItem("Analytics", "/admin/analytics", {
|
|
8198
|
+
icon: "chart-bar",
|
|
8199
|
+
order: 40,
|
|
8200
|
+
permissions: ["admin", "analytics:read"]
|
|
8201
|
+
});
|
|
8202
|
+
builder.addMenuItem("Dashboard", "/admin/analytics", {
|
|
8203
|
+
icon: "chart-bar",
|
|
8204
|
+
parent: "Analytics",
|
|
8205
|
+
order: 1,
|
|
8206
|
+
permissions: ["admin", "analytics:read"]
|
|
8207
|
+
});
|
|
8208
|
+
builder.addMenuItem("Reports", "/admin/analytics/reports", {
|
|
8209
|
+
icon: "document-report",
|
|
8210
|
+
parent: "Analytics",
|
|
8211
|
+
order: 2,
|
|
8212
|
+
permissions: ["admin", "analytics:read"]
|
|
8213
|
+
});
|
|
8214
|
+
builder.addMenuItem("Real-time", "/admin/analytics/realtime", {
|
|
8215
|
+
icon: "lightning-bolt",
|
|
8216
|
+
parent: "Analytics",
|
|
8217
|
+
order: 3,
|
|
8218
|
+
permissions: ["admin", "analytics:read"]
|
|
8219
|
+
});
|
|
8220
|
+
builder.addMenuItem("Settings", "/admin/analytics/settings", {
|
|
8221
|
+
icon: "cog",
|
|
8222
|
+
parent: "Analytics",
|
|
8223
|
+
order: 4,
|
|
8224
|
+
permissions: ["admin", "analytics:configure"]
|
|
8225
|
+
});
|
|
8226
|
+
builder.lifecycle({
|
|
8227
|
+
install: async () => {
|
|
8228
|
+
console.info("Installing analytics plugin...");
|
|
8229
|
+
},
|
|
8230
|
+
activate: async () => {
|
|
8231
|
+
console.info("Activating analytics plugin...");
|
|
8232
|
+
},
|
|
8233
|
+
deactivate: async () => {
|
|
8234
|
+
console.info("Deactivating analytics plugin...");
|
|
8235
|
+
},
|
|
8236
|
+
configure: async (config) => {
|
|
8237
|
+
console.info("Configuring analytics plugin...", config);
|
|
8238
|
+
}
|
|
8239
|
+
});
|
|
8240
|
+
return builder.build();
|
|
8241
|
+
}
|
|
8242
|
+
var analyticsPlugin = createAnalyticsPlugin();
|
|
8243
|
+
|
|
8244
|
+
// src/plugins/core-plugins/analytics/services/event-tracking-service.ts
|
|
8245
|
+
var EventTrackingService = class {
|
|
8246
|
+
constructor(db) {
|
|
8247
|
+
this.db = db;
|
|
8248
|
+
}
|
|
8249
|
+
async trackEvent(input) {
|
|
8250
|
+
const id = crypto.randomUUID();
|
|
8251
|
+
const category = input.category || "user-activity";
|
|
8252
|
+
await this.db.prepare(`
|
|
8253
|
+
INSERT INTO analytics_events (id, event, category, properties, user_id, session_id, ip_address, user_agent, path)
|
|
8254
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8255
|
+
`).bind(
|
|
8256
|
+
id,
|
|
8257
|
+
input.event,
|
|
8258
|
+
category,
|
|
8259
|
+
input.properties ? JSON.stringify(input.properties) : null,
|
|
8260
|
+
input.userId || null,
|
|
8261
|
+
input.sessionId || null,
|
|
8262
|
+
input.ipAddress || null,
|
|
8263
|
+
input.userAgent || null,
|
|
8264
|
+
input.path || null
|
|
8265
|
+
).run();
|
|
8266
|
+
return id;
|
|
8267
|
+
}
|
|
8268
|
+
async trackBatch(events) {
|
|
8269
|
+
const ids = [];
|
|
8270
|
+
const stmts = events.map((input) => {
|
|
8271
|
+
const id = crypto.randomUUID();
|
|
8272
|
+
ids.push(id);
|
|
8273
|
+
const category = input.category || "user-activity";
|
|
8274
|
+
return this.db.prepare(`
|
|
8275
|
+
INSERT INTO analytics_events (id, event, category, properties, user_id, session_id, ip_address, user_agent, path)
|
|
8276
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8277
|
+
`).bind(
|
|
8278
|
+
id,
|
|
8279
|
+
input.event,
|
|
8280
|
+
category,
|
|
8281
|
+
input.properties ? JSON.stringify(input.properties) : null,
|
|
8282
|
+
input.userId || null,
|
|
8283
|
+
input.sessionId || null,
|
|
8284
|
+
input.ipAddress || null,
|
|
8285
|
+
input.userAgent || null,
|
|
8286
|
+
input.path || null
|
|
8287
|
+
);
|
|
8288
|
+
});
|
|
8289
|
+
await this.db.batch(stmts);
|
|
8290
|
+
return ids;
|
|
8291
|
+
}
|
|
8292
|
+
async queryEvents(filters = {}) {
|
|
8293
|
+
const conditions = [];
|
|
8294
|
+
const params = [];
|
|
8295
|
+
if (filters.event) {
|
|
8296
|
+
conditions.push("event = ?");
|
|
8297
|
+
params.push(filters.event);
|
|
8298
|
+
}
|
|
8299
|
+
if (filters.category) {
|
|
8300
|
+
conditions.push("category = ?");
|
|
8301
|
+
params.push(filters.category);
|
|
8302
|
+
}
|
|
8303
|
+
if (filters.userId) {
|
|
8304
|
+
conditions.push("user_id = ?");
|
|
8305
|
+
params.push(filters.userId);
|
|
8306
|
+
}
|
|
8307
|
+
if (filters.sessionId) {
|
|
8308
|
+
conditions.push("session_id = ?");
|
|
8309
|
+
params.push(filters.sessionId);
|
|
8310
|
+
}
|
|
8311
|
+
if (filters.startDate) {
|
|
8312
|
+
conditions.push("created_at >= ?");
|
|
8313
|
+
params.push(filters.startDate);
|
|
8314
|
+
}
|
|
8315
|
+
if (filters.endDate) {
|
|
8316
|
+
conditions.push("created_at <= ?");
|
|
8317
|
+
params.push(filters.endDate);
|
|
8318
|
+
}
|
|
8319
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8320
|
+
const limit = filters.limit || 50;
|
|
8321
|
+
const offset = filters.offset || 0;
|
|
8322
|
+
const [countResult, eventsResult] = await Promise.all([
|
|
8323
|
+
this.db.prepare(`SELECT COUNT(*) as total FROM analytics_events ${where}`).bind(...params).first(),
|
|
8324
|
+
this.db.prepare(`SELECT * FROM analytics_events ${where} ORDER BY created_at DESC LIMIT ? OFFSET ?`).bind(...params, limit, offset).all()
|
|
8325
|
+
]);
|
|
8326
|
+
const events = (eventsResult.results || []).map((e) => ({
|
|
8327
|
+
...e,
|
|
8328
|
+
properties: e.properties ? JSON.parse(e.properties) : null
|
|
8329
|
+
}));
|
|
8330
|
+
return { events, total: countResult?.total || 0 };
|
|
8331
|
+
}
|
|
8332
|
+
async getStats(startDate, endDate) {
|
|
8333
|
+
const conditions = [];
|
|
8334
|
+
const params = [];
|
|
8335
|
+
if (startDate) {
|
|
8336
|
+
conditions.push("created_at >= ?");
|
|
8337
|
+
params.push(startDate);
|
|
8338
|
+
}
|
|
8339
|
+
if (endDate) {
|
|
8340
|
+
conditions.push("created_at <= ?");
|
|
8341
|
+
params.push(endDate);
|
|
8342
|
+
}
|
|
8343
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
8344
|
+
const [totals, topEvents] = await Promise.all([
|
|
8345
|
+
this.db.prepare(`
|
|
8346
|
+
SELECT
|
|
8347
|
+
COUNT(*) as total_events,
|
|
8348
|
+
COUNT(DISTINCT user_id) as unique_users,
|
|
8349
|
+
COUNT(DISTINCT session_id) as unique_sessions
|
|
8350
|
+
FROM analytics_events ${where}
|
|
8351
|
+
`).bind(...params).first(),
|
|
8352
|
+
this.db.prepare(`
|
|
8353
|
+
SELECT event, COUNT(*) as count
|
|
8354
|
+
FROM analytics_events ${where}
|
|
8355
|
+
GROUP BY event ORDER BY count DESC LIMIT 20
|
|
8356
|
+
`).bind(...params).all()
|
|
8357
|
+
]);
|
|
8358
|
+
return {
|
|
8359
|
+
totalEvents: totals?.total_events || 0,
|
|
8360
|
+
uniqueUsers: totals?.unique_users || 0,
|
|
8361
|
+
uniqueSessions: totals?.unique_sessions || 0,
|
|
8362
|
+
topEvents: (topEvents.results || []).map((r) => ({ event: r.event, count: r.count }))
|
|
8363
|
+
};
|
|
8364
|
+
}
|
|
8365
|
+
};
|
|
8366
|
+
|
|
8367
|
+
// src/plugins/core-plugins/analytics/routes/api.ts
|
|
8368
|
+
var apiRoutes4 = new Hono();
|
|
8369
|
+
apiRoutes4.post("/", async (c) => {
|
|
8370
|
+
const db = c.env.DB;
|
|
8371
|
+
const service = new EventTrackingService(db);
|
|
8372
|
+
const ip = c.req.header("cf-connecting-ip") || c.req.header("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
8373
|
+
const userAgent = c.req.header("user-agent") || "";
|
|
8374
|
+
const user = c.get("user");
|
|
8375
|
+
let body;
|
|
8376
|
+
try {
|
|
8377
|
+
body = await c.req.json();
|
|
8378
|
+
} catch {
|
|
8379
|
+
return c.json({ error: "Invalid JSON body" }, 400);
|
|
8380
|
+
}
|
|
8381
|
+
if (Array.isArray(body)) {
|
|
8382
|
+
if (body.length > 100) {
|
|
8383
|
+
return c.json({ error: "Batch size limit is 100 events" }, 400);
|
|
8384
|
+
}
|
|
8385
|
+
const events = body.map((e) => ({
|
|
8386
|
+
event: e.event,
|
|
8387
|
+
category: e.category || "user-activity",
|
|
8388
|
+
properties: e.properties,
|
|
8389
|
+
userId: user?.userId || e.userId,
|
|
8390
|
+
sessionId: e.sessionId,
|
|
8391
|
+
ipAddress: ip,
|
|
8392
|
+
userAgent,
|
|
8393
|
+
path: e.path
|
|
8394
|
+
}));
|
|
8395
|
+
const invalid = events.find((e) => !e.event || typeof e.event !== "string");
|
|
8396
|
+
if (invalid) {
|
|
8397
|
+
return c.json({ error: 'Each event must have an "event" string field' }, 400);
|
|
8398
|
+
}
|
|
8399
|
+
const ids = await service.trackBatch(events);
|
|
8400
|
+
return c.json({ success: true, eventIds: ids, count: ids.length });
|
|
8401
|
+
}
|
|
8402
|
+
if (!body.event || typeof body.event !== "string") {
|
|
8403
|
+
return c.json({ error: '"event" field is required and must be a string' }, 400);
|
|
8404
|
+
}
|
|
8405
|
+
const eventId = await service.trackEvent({
|
|
8406
|
+
event: body.event,
|
|
8407
|
+
category: body.category || "user-activity",
|
|
8408
|
+
properties: body.properties,
|
|
8409
|
+
userId: user?.userId || body.userId,
|
|
8410
|
+
sessionId: body.sessionId,
|
|
8411
|
+
ipAddress: ip,
|
|
8412
|
+
userAgent,
|
|
8413
|
+
path: body.path
|
|
8414
|
+
});
|
|
8415
|
+
return c.json({ success: true, eventId });
|
|
8416
|
+
});
|
|
8417
|
+
apiRoutes4.get("/", async (c) => {
|
|
8418
|
+
const user = c.get("user");
|
|
8419
|
+
if (!user || user.role !== "admin") {
|
|
8420
|
+
return c.json({ error: "Admin access required" }, 403);
|
|
8421
|
+
}
|
|
8422
|
+
const db = c.env.DB;
|
|
8423
|
+
const service = new EventTrackingService(db);
|
|
8424
|
+
const filters = {
|
|
8425
|
+
event: c.req.query("event") || void 0,
|
|
8426
|
+
category: c.req.query("category") || void 0,
|
|
8427
|
+
userId: c.req.query("userId") || void 0,
|
|
8428
|
+
sessionId: c.req.query("sessionId") || void 0,
|
|
8429
|
+
startDate: c.req.query("startDate") ? parseInt(c.req.query("startDate")) : void 0,
|
|
8430
|
+
endDate: c.req.query("endDate") ? parseInt(c.req.query("endDate")) : void 0,
|
|
8431
|
+
limit: c.req.query("limit") ? parseInt(c.req.query("limit")) : 50,
|
|
8432
|
+
offset: c.req.query("offset") ? parseInt(c.req.query("offset")) : 0
|
|
8433
|
+
};
|
|
8434
|
+
const result = await service.queryEvents(filters);
|
|
8435
|
+
return c.json(result);
|
|
8436
|
+
});
|
|
8437
|
+
apiRoutes4.get("/stats", async (c) => {
|
|
8438
|
+
const user = c.get("user");
|
|
8439
|
+
if (!user || user.role !== "admin") {
|
|
8440
|
+
return c.json({ error: "Admin access required" }, 403);
|
|
8441
|
+
}
|
|
8442
|
+
const db = c.env.DB;
|
|
8443
|
+
const service = new EventTrackingService(db);
|
|
8444
|
+
const startDate = c.req.query("startDate") ? parseInt(c.req.query("startDate")) : void 0;
|
|
8445
|
+
const endDate = c.req.query("endDate") ? parseInt(c.req.query("endDate")) : void 0;
|
|
8446
|
+
const stats = await service.getStats(startDate, endDate);
|
|
8447
|
+
return c.json(stats);
|
|
8448
|
+
});
|
|
8449
|
+
|
|
7765
8450
|
// src/plugins/cache/services/cache-config.ts
|
|
7766
8451
|
var CACHE_CONFIGS = {
|
|
7767
8452
|
// Content (high read, low write)
|
|
@@ -9634,6 +10319,9 @@ function createSonicJSApp(config = {}) {
|
|
|
9634
10319
|
app2.use("*", middleware);
|
|
9635
10320
|
}
|
|
9636
10321
|
}
|
|
10322
|
+
const adminRoles = config.adminAccessRoles || ["admin"];
|
|
10323
|
+
app2.use("/admin/*", requireAuth());
|
|
10324
|
+
app2.use("/admin/*", requireRole(adminRoles));
|
|
9637
10325
|
app2.use("/admin/*", pluginMenuMiddleware());
|
|
9638
10326
|
app2.route("/api", api_default);
|
|
9639
10327
|
app2.route("/api/media", api_media_default);
|
|
@@ -9677,6 +10365,12 @@ function createSonicJSApp(config = {}) {
|
|
|
9677
10365
|
app2.route(route.path, route.handler);
|
|
9678
10366
|
}
|
|
9679
10367
|
}
|
|
10368
|
+
if (analyticsPlugin.routes && analyticsPlugin.routes.length > 0) {
|
|
10369
|
+
for (const route of analyticsPlugin.routes) {
|
|
10370
|
+
app2.route(route.path, route.handler);
|
|
10371
|
+
}
|
|
10372
|
+
}
|
|
10373
|
+
app2.route("/api/events", apiRoutes4);
|
|
9680
10374
|
if (stripePlugin.routes && stripePlugin.routes.length > 0) {
|
|
9681
10375
|
for (const route of stripePlugin.routes) {
|
|
9682
10376
|
app2.route(route.path, route.handler);
|