sa2kit 1.0.0
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/LICENSE +22 -0
- package/README.md +298 -0
- package/dist/AliyunOSSProvider-7JLMJDXK.js +15 -0
- package/dist/AliyunOSSProvider-7JLMJDXK.js.map +1 -0
- package/dist/AliyunOSSProvider-GQMSDJGZ.mjs +6 -0
- package/dist/AliyunOSSProvider-GQMSDJGZ.mjs.map +1 -0
- package/dist/LocalStorageProvider-FVLLHBHO.mjs +6 -0
- package/dist/LocalStorageProvider-FVLLHBHO.mjs.map +1 -0
- package/dist/LocalStorageProvider-NBNHHWLY.js +15 -0
- package/dist/LocalStorageProvider-NBNHHWLY.js.map +1 -0
- package/dist/analytics/index.d.mts +1084 -0
- package/dist/analytics/index.d.ts +1084 -0
- package/dist/analytics/index.js +2595 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/index.mjs +2518 -0
- package/dist/analytics/index.mjs.map +1 -0
- package/dist/analytics/server/index.d.mts +499 -0
- package/dist/analytics/server/index.d.ts +499 -0
- package/dist/analytics/server/index.js +529 -0
- package/dist/analytics/server/index.js.map +1 -0
- package/dist/analytics/server/index.mjs +525 -0
- package/dist/analytics/server/index.mjs.map +1 -0
- package/dist/auth/client/index.d.mts +104 -0
- package/dist/auth/client/index.d.ts +104 -0
- package/dist/auth/client/index.js +21 -0
- package/dist/auth/client/index.js.map +1 -0
- package/dist/auth/client/index.mjs +4 -0
- package/dist/auth/client/index.mjs.map +1 -0
- package/dist/auth/components/index.d.mts +82 -0
- package/dist/auth/components/index.d.ts +82 -0
- package/dist/auth/components/index.js +93 -0
- package/dist/auth/components/index.js.map +1 -0
- package/dist/auth/components/index.mjs +86 -0
- package/dist/auth/components/index.mjs.map +1 -0
- package/dist/auth/hooks/index.d.mts +2 -0
- package/dist/auth/hooks/index.d.ts +2 -0
- package/dist/auth/hooks/index.js +17 -0
- package/dist/auth/hooks/index.js.map +1 -0
- package/dist/auth/hooks/index.mjs +4 -0
- package/dist/auth/hooks/index.mjs.map +1 -0
- package/dist/auth/index.d.mts +15 -0
- package/dist/auth/index.d.ts +15 -0
- package/dist/auth/index.js +110 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/auth/index.mjs +9 -0
- package/dist/auth/index.mjs.map +1 -0
- package/dist/auth/middleware/index.d.mts +75 -0
- package/dist/auth/middleware/index.d.ts +75 -0
- package/dist/auth/middleware/index.js +15 -0
- package/dist/auth/middleware/index.js.map +1 -0
- package/dist/auth/middleware/index.mjs +6 -0
- package/dist/auth/middleware/index.mjs.map +1 -0
- package/dist/auth/routes/index.d.mts +163 -0
- package/dist/auth/routes/index.d.ts +163 -0
- package/dist/auth/routes/index.js +27 -0
- package/dist/auth/routes/index.js.map +1 -0
- package/dist/auth/routes/index.mjs +6 -0
- package/dist/auth/routes/index.mjs.map +1 -0
- package/dist/auth/schema/index.d.mts +789 -0
- package/dist/auth/schema/index.d.ts +789 -0
- package/dist/auth/schema/index.js +41 -0
- package/dist/auth/schema/index.js.map +1 -0
- package/dist/auth/schema/index.mjs +4 -0
- package/dist/auth/schema/index.mjs.map +1 -0
- package/dist/auth/services/index.d.mts +47 -0
- package/dist/auth/services/index.d.ts +47 -0
- package/dist/auth/services/index.js +34 -0
- package/dist/auth/services/index.js.map +1 -0
- package/dist/auth/services/index.mjs +5 -0
- package/dist/auth/services/index.mjs.map +1 -0
- package/dist/chunk-3RFBUDRA.js +507 -0
- package/dist/chunk-3RFBUDRA.js.map +1 -0
- package/dist/chunk-3XG5OHFD.mjs +37 -0
- package/dist/chunk-3XG5OHFD.mjs.map +1 -0
- package/dist/chunk-6BL3AZGD.js +285 -0
- package/dist/chunk-6BL3AZGD.js.map +1 -0
- package/dist/chunk-6FNUWAIV.js +394 -0
- package/dist/chunk-6FNUWAIV.js.map +1 -0
- package/dist/chunk-6PRFP5EG.js +171 -0
- package/dist/chunk-6PRFP5EG.js.map +1 -0
- package/dist/chunk-6VHWOPRR.mjs +90 -0
- package/dist/chunk-6VHWOPRR.mjs.map +1 -0
- package/dist/chunk-AIKEVVDR.mjs +122 -0
- package/dist/chunk-AIKEVVDR.mjs.map +1 -0
- package/dist/chunk-APY57REU.js +300 -0
- package/dist/chunk-APY57REU.js.map +1 -0
- package/dist/chunk-BJTO5JO5.mjs +10 -0
- package/dist/chunk-BJTO5JO5.mjs.map +1 -0
- package/dist/chunk-C64RY2OW.mjs +295 -0
- package/dist/chunk-C64RY2OW.mjs.map +1 -0
- package/dist/chunk-DGUM43GV.js +12 -0
- package/dist/chunk-DGUM43GV.js.map +1 -0
- package/dist/chunk-FV3FNHQY.js +92 -0
- package/dist/chunk-FV3FNHQY.js.map +1 -0
- package/dist/chunk-GSTLV3MB.mjs +316 -0
- package/dist/chunk-GSTLV3MB.mjs.map +1 -0
- package/dist/chunk-HEMA7SWK.mjs +212 -0
- package/dist/chunk-HEMA7SWK.mjs.map +1 -0
- package/dist/chunk-HWJ34NL6.js +43 -0
- package/dist/chunk-HWJ34NL6.js.map +1 -0
- package/dist/chunk-HXFFYNIF.mjs +385 -0
- package/dist/chunk-HXFFYNIF.mjs.map +1 -0
- package/dist/chunk-KGRQNEIR.mjs +183 -0
- package/dist/chunk-KGRQNEIR.mjs.map +1 -0
- package/dist/chunk-KH6RQ4J5.js +28 -0
- package/dist/chunk-KH6RQ4J5.js.map +1 -0
- package/dist/chunk-KQGP6BTS.mjs +165 -0
- package/dist/chunk-KQGP6BTS.mjs.map +1 -0
- package/dist/chunk-NMF4ANIC.js +365 -0
- package/dist/chunk-NMF4ANIC.js.map +1 -0
- package/dist/chunk-O26VCNS3.js +216 -0
- package/dist/chunk-O26VCNS3.js.map +1 -0
- package/dist/chunk-OLHGZXN3.mjs +86 -0
- package/dist/chunk-OLHGZXN3.mjs.map +1 -0
- package/dist/chunk-QU5OT4DF.js +88 -0
- package/dist/chunk-QU5OT4DF.js.map +1 -0
- package/dist/chunk-RCNNVNLT.mjs +356 -0
- package/dist/chunk-RCNNVNLT.mjs.map +1 -0
- package/dist/chunk-ROEYW4A7.js +186 -0
- package/dist/chunk-ROEYW4A7.js.map +1 -0
- package/dist/chunk-SVWQN2LR.js +131 -0
- package/dist/chunk-SVWQN2LR.js.map +1 -0
- package/dist/chunk-TKCYPDWU.js +338 -0
- package/dist/chunk-TKCYPDWU.js.map +1 -0
- package/dist/chunk-U2L6V7KD.mjs +273 -0
- package/dist/chunk-U2L6V7KD.mjs.map +1 -0
- package/dist/chunk-YVBU7QDJ.mjs +505 -0
- package/dist/chunk-YVBU7QDJ.mjs.map +1 -0
- package/dist/chunk-ZGVB35L2.mjs +25 -0
- package/dist/chunk-ZGVB35L2.mjs.map +1 -0
- package/dist/config/index.d.mts +64 -0
- package/dist/config/index.d.ts +64 -0
- package/dist/config/index.js +136 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/index.mjs +128 -0
- package/dist/config/index.mjs.map +1 -0
- package/dist/drizzle-auth-service-Bxlovhv8.d.ts +145 -0
- package/dist/drizzle-auth-service-DZY2F1sv.d.mts +145 -0
- package/dist/enums-Dume-V5Y.d.mts +16 -0
- package/dist/enums-Dume-V5Y.d.ts +16 -0
- package/dist/i18n/index.d.mts +416 -0
- package/dist/i18n/index.d.ts +416 -0
- package/dist/i18n/index.js +671 -0
- package/dist/i18n/index.js.map +1 -0
- package/dist/i18n/index.mjs +650 -0
- package/dist/i18n/index.mjs.map +1 -0
- package/dist/index-8VoHap_4.d.mts +105 -0
- package/dist/index-8VoHap_4.d.ts +105 -0
- package/dist/index.d.mts +4 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +7 -0
- package/dist/index.mjs.map +1 -0
- package/dist/logger/index.d.mts +125 -0
- package/dist/logger/index.d.ts +125 -0
- package/dist/logger/index.js +29 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/index.mjs +4 -0
- package/dist/logger/index.mjs.map +1 -0
- package/dist/request/index.d.mts +51 -0
- package/dist/request/index.d.ts +51 -0
- package/dist/request/index.js +85 -0
- package/dist/request/index.js.map +1 -0
- package/dist/request/index.mjs +82 -0
- package/dist/request/index.mjs.map +1 -0
- package/dist/storage/index.d.mts +74 -0
- package/dist/storage/index.d.ts +74 -0
- package/dist/storage/index.js +46 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/index.mjs +5 -0
- package/dist/storage/index.mjs.map +1 -0
- package/dist/types-BINlP9MK.d.mts +286 -0
- package/dist/types-BINlP9MK.d.ts +286 -0
- package/dist/types-BaZccpvk.d.mts +48 -0
- package/dist/types-BaZccpvk.d.ts +48 -0
- package/dist/types-CbTsi9CZ.d.mts +31 -0
- package/dist/types-CbTsi9CZ.d.ts +31 -0
- package/dist/types-CoGG1rNV.d.mts +258 -0
- package/dist/types-CoGG1rNV.d.ts +258 -0
- package/dist/types-DAxQ1MeY.d.ts +70 -0
- package/dist/types-DT8LVCvE.d.mts +70 -0
- package/dist/types-DW9qar-w.d.mts +52 -0
- package/dist/types-DW9qar-w.d.ts +52 -0
- package/dist/universalExport/index.d.mts +235 -0
- package/dist/universalExport/index.d.ts +235 -0
- package/dist/universalExport/index.js +621 -0
- package/dist/universalExport/index.js.map +1 -0
- package/dist/universalExport/index.mjs +580 -0
- package/dist/universalExport/index.mjs.map +1 -0
- package/dist/universalExport/server/index.d.mts +429 -0
- package/dist/universalExport/server/index.d.ts +429 -0
- package/dist/universalExport/server/index.js +263 -0
- package/dist/universalExport/server/index.js.map +1 -0
- package/dist/universalExport/server/index.mjs +242 -0
- package/dist/universalExport/server/index.mjs.map +1 -0
- package/dist/universalFile/index.d.mts +310 -0
- package/dist/universalFile/index.d.ts +310 -0
- package/dist/universalFile/index.js +811 -0
- package/dist/universalFile/index.js.map +1 -0
- package/dist/universalFile/index.mjs +736 -0
- package/dist/universalFile/index.mjs.map +1 -0
- package/dist/universalFile/server/index.d.mts +2428 -0
- package/dist/universalFile/server/index.d.ts +2428 -0
- package/dist/universalFile/server/index.js +4578 -0
- package/dist/universalFile/server/index.js.map +1 -0
- package/dist/universalFile/server/index.mjs +4518 -0
- package/dist/universalFile/server/index.mjs.map +1 -0
- package/dist/useElectronStorage-Dj0rcorG.d.mts +65 -0
- package/dist/useElectronStorage-DwnNfIhl.d.ts +65 -0
- package/dist/utils/index.d.mts +188 -0
- package/dist/utils/index.d.ts +188 -0
- package/dist/utils/index.js +42 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/index.mjs +5 -0
- package/dist/utils/index.mjs.map +1 -0
- package/package.json +220 -0
- package/tailwind.animations.js +34 -0
|
@@ -0,0 +1,525 @@
|
|
|
1
|
+
import '../../chunk-BJTO5JO5.mjs';
|
|
2
|
+
import { pgTable, timestamp, text, jsonb, integer, index } from 'drizzle-orm/pg-core';
|
|
3
|
+
import { sql, lte, eq, gte, and, asc, count, desc, inArray } from 'drizzle-orm';
|
|
4
|
+
|
|
5
|
+
var analyticsEvents = pgTable(
|
|
6
|
+
"analytics_events",
|
|
7
|
+
{
|
|
8
|
+
id: text().primaryKey().notNull(),
|
|
9
|
+
eventType: text("event_type").notNull(),
|
|
10
|
+
eventName: text("event_name").notNull(),
|
|
11
|
+
timestamp: timestamp({ precision: 3, mode: "string" }).notNull(),
|
|
12
|
+
priority: integer().notNull(),
|
|
13
|
+
userId: text("user_id"),
|
|
14
|
+
sessionId: text("session_id").notNull(),
|
|
15
|
+
deviceId: text("device_id").notNull(),
|
|
16
|
+
pageUrl: text("page_url"),
|
|
17
|
+
pageTitle: text("page_title"),
|
|
18
|
+
referrer: text(),
|
|
19
|
+
properties: jsonb(),
|
|
20
|
+
platform: text().notNull(),
|
|
21
|
+
appVersion: text("app_version").notNull(),
|
|
22
|
+
sdkVersion: text("sdk_version").notNull(),
|
|
23
|
+
createdAt: timestamp({ precision: 3, mode: "string" }).default(sql`CURRENT_TIMESTAMP`).notNull()
|
|
24
|
+
},
|
|
25
|
+
(table) => [
|
|
26
|
+
index("analytics_events_user_id_idx").using(
|
|
27
|
+
"btree",
|
|
28
|
+
table.userId.asc().nullsLast().op("text_ops")
|
|
29
|
+
),
|
|
30
|
+
index("analytics_events_event_type_idx").using(
|
|
31
|
+
"btree",
|
|
32
|
+
table.eventType.asc().nullsLast().op("text_ops")
|
|
33
|
+
),
|
|
34
|
+
index("analytics_events_platform_idx").using(
|
|
35
|
+
"btree",
|
|
36
|
+
table.platform.asc().nullsLast().op("text_ops")
|
|
37
|
+
),
|
|
38
|
+
index("analytics_events_timestamp_idx").using("btree", table.timestamp.desc().nullsLast()),
|
|
39
|
+
index("analytics_events_session_id_idx").using(
|
|
40
|
+
"btree",
|
|
41
|
+
table.sessionId.asc().nullsLast().op("text_ops")
|
|
42
|
+
)
|
|
43
|
+
]
|
|
44
|
+
);
|
|
45
|
+
var defaultLogger = {
|
|
46
|
+
info: (message, data) => console.log(`[Analytics] ${message}`, data || ""),
|
|
47
|
+
error: (message, error) => console.error(`[Analytics] ${message}`, error)
|
|
48
|
+
};
|
|
49
|
+
function createAnalyticsService(db, analyticsEventsTable, logger = defaultLogger) {
|
|
50
|
+
const analyticsEvents2 = analyticsEventsTable;
|
|
51
|
+
return {
|
|
52
|
+
/**
|
|
53
|
+
* 批量插入埋点事件
|
|
54
|
+
*/
|
|
55
|
+
async insertAnalyticsEvents(events) {
|
|
56
|
+
try {
|
|
57
|
+
if (events.length === 0) return;
|
|
58
|
+
await db.insert(analyticsEvents2).values(events).onConflictDoNothing();
|
|
59
|
+
logger.info("Analytics events inserted", { count: events.length });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
logger.error("Failed to insert analytics events", error);
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
/**
|
|
66
|
+
* 查询埋点事件
|
|
67
|
+
*/
|
|
68
|
+
async queryAnalyticsEvents(params) {
|
|
69
|
+
try {
|
|
70
|
+
const conditions = [];
|
|
71
|
+
if (params.startDate) {
|
|
72
|
+
conditions.push(gte(analyticsEvents2.timestamp, params.startDate));
|
|
73
|
+
}
|
|
74
|
+
if (params.endDate) {
|
|
75
|
+
conditions.push(lte(analyticsEvents2.timestamp, params.endDate));
|
|
76
|
+
}
|
|
77
|
+
if (params.eventType) {
|
|
78
|
+
conditions.push(eq(analyticsEvents2.eventType, params.eventType));
|
|
79
|
+
}
|
|
80
|
+
if (params.eventTypes && params.eventTypes.length > 0) {
|
|
81
|
+
conditions.push(inArray(analyticsEvents2.eventType, params.eventTypes));
|
|
82
|
+
}
|
|
83
|
+
if (params.userId) {
|
|
84
|
+
conditions.push(eq(analyticsEvents2.userId, params.userId));
|
|
85
|
+
}
|
|
86
|
+
if (params.platform) {
|
|
87
|
+
conditions.push(eq(analyticsEvents2.platform, params.platform));
|
|
88
|
+
}
|
|
89
|
+
if (params.platforms && params.platforms.length > 0) {
|
|
90
|
+
conditions.push(inArray(analyticsEvents2.platform, params.platforms));
|
|
91
|
+
}
|
|
92
|
+
if (params.sessionId) {
|
|
93
|
+
conditions.push(eq(analyticsEvents2.sessionId, params.sessionId));
|
|
94
|
+
}
|
|
95
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : void 0;
|
|
96
|
+
const [{ total }] = await db.select({ total: count() }).from(analyticsEvents2).where(whereClause);
|
|
97
|
+
const orderByColumn = params.orderBy || "timestamp";
|
|
98
|
+
const orderDirection = params.orderDirection || "desc";
|
|
99
|
+
const orderFn = orderDirection === "asc" ? asc : desc;
|
|
100
|
+
let orderByField;
|
|
101
|
+
switch (orderByColumn) {
|
|
102
|
+
case "eventType":
|
|
103
|
+
orderByField = analyticsEvents2.eventType;
|
|
104
|
+
break;
|
|
105
|
+
case "platform":
|
|
106
|
+
orderByField = analyticsEvents2.platform;
|
|
107
|
+
break;
|
|
108
|
+
default:
|
|
109
|
+
orderByField = analyticsEvents2.timestamp;
|
|
110
|
+
}
|
|
111
|
+
const events = await db.select().from(analyticsEvents2).where(whereClause).orderBy(orderFn(orderByField)).limit(params.limit || 100).offset(params.offset || 0);
|
|
112
|
+
const formattedEvents = events.map((event) => ({
|
|
113
|
+
...event,
|
|
114
|
+
timestamp: event.timestamp.endsWith("Z") ? event.timestamp : `${event.timestamp}Z`,
|
|
115
|
+
createdAt: event.createdAt.endsWith("Z") ? event.createdAt : `${event.createdAt}Z`
|
|
116
|
+
}));
|
|
117
|
+
return {
|
|
118
|
+
events: formattedEvents,
|
|
119
|
+
total: Number(total)
|
|
120
|
+
};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
logger.error("Failed to query analytics events", error);
|
|
123
|
+
throw error;
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
/**
|
|
127
|
+
* 获取统计数据
|
|
128
|
+
*/
|
|
129
|
+
async getAnalyticsStats(startDate, endDate, platform) {
|
|
130
|
+
try {
|
|
131
|
+
const conditions = [];
|
|
132
|
+
if (startDate) {
|
|
133
|
+
conditions.push(gte(analyticsEvents2.timestamp, startDate));
|
|
134
|
+
}
|
|
135
|
+
if (endDate) {
|
|
136
|
+
conditions.push(lte(analyticsEvents2.timestamp, endDate));
|
|
137
|
+
}
|
|
138
|
+
if (platform) {
|
|
139
|
+
conditions.push(eq(analyticsEvents2.platform, platform));
|
|
140
|
+
}
|
|
141
|
+
const whereClause = conditions.length > 0 ? and(...conditions) : void 0;
|
|
142
|
+
const [{ totalEvents }] = await db.select({ totalEvents: count() }).from(analyticsEvents2).where(whereClause);
|
|
143
|
+
const [{ uniqueUsers }] = await db.select({ uniqueUsers: sql`COUNT(DISTINCT ${analyticsEvents2.userId})` }).from(analyticsEvents2).where(whereClause);
|
|
144
|
+
const [{ uniqueSessions }] = await db.select({ uniqueSessions: sql`COUNT(DISTINCT ${analyticsEvents2.sessionId})` }).from(analyticsEvents2).where(whereClause);
|
|
145
|
+
const [{ uniqueDevices }] = await db.select({ uniqueDevices: sql`COUNT(DISTINCT ${analyticsEvents2.deviceId})` }).from(analyticsEvents2).where(whereClause);
|
|
146
|
+
const eventsByType = await db.select({
|
|
147
|
+
eventType: analyticsEvents2.eventType,
|
|
148
|
+
count: count()
|
|
149
|
+
}).from(analyticsEvents2).where(whereClause).groupBy(analyticsEvents2.eventType).orderBy(desc(count())).limit(10);
|
|
150
|
+
const eventsByPlatform = await db.select({
|
|
151
|
+
platform: analyticsEvents2.platform,
|
|
152
|
+
count: count()
|
|
153
|
+
}).from(analyticsEvents2).where(whereClause).groupBy(analyticsEvents2.platform).orderBy(desc(count()));
|
|
154
|
+
const topPages = await db.select({
|
|
155
|
+
pageUrl: analyticsEvents2.pageUrl,
|
|
156
|
+
count: count()
|
|
157
|
+
}).from(analyticsEvents2).where(and(whereClause, sql`${analyticsEvents2.pageUrl} IS NOT NULL`)).groupBy(analyticsEvents2.pageUrl).orderBy(desc(count())).limit(10);
|
|
158
|
+
return {
|
|
159
|
+
totalEvents: Number(totalEvents),
|
|
160
|
+
uniqueUsers: Number(uniqueUsers),
|
|
161
|
+
uniqueSessions: Number(uniqueSessions),
|
|
162
|
+
uniqueDevices: Number(uniqueDevices),
|
|
163
|
+
eventsByType: eventsByType.map((e) => ({
|
|
164
|
+
eventType: e.eventType,
|
|
165
|
+
count: Number(e.count)
|
|
166
|
+
})),
|
|
167
|
+
eventsByPlatform: eventsByPlatform.map((e) => ({
|
|
168
|
+
platform: e.platform,
|
|
169
|
+
count: Number(e.count)
|
|
170
|
+
})),
|
|
171
|
+
topPages: topPages.filter((p) => p.pageUrl).map((p) => ({
|
|
172
|
+
pageUrl: p.pageUrl,
|
|
173
|
+
count: Number(p.count)
|
|
174
|
+
}))
|
|
175
|
+
};
|
|
176
|
+
} catch (error) {
|
|
177
|
+
logger.error("Failed to get analytics stats", error);
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
/**
|
|
182
|
+
* 获取用户行为分析
|
|
183
|
+
*/
|
|
184
|
+
async getUserBehavior(userId, startDate, endDate) {
|
|
185
|
+
try {
|
|
186
|
+
const conditions = [eq(analyticsEvents2.userId, userId)];
|
|
187
|
+
if (startDate) {
|
|
188
|
+
conditions.push(gte(analyticsEvents2.timestamp, startDate));
|
|
189
|
+
}
|
|
190
|
+
if (endDate) {
|
|
191
|
+
conditions.push(lte(analyticsEvents2.timestamp, endDate));
|
|
192
|
+
}
|
|
193
|
+
const whereClause = and(...conditions);
|
|
194
|
+
const [{ eventCount }] = await db.select({ eventCount: count() }).from(analyticsEvents2).where(whereClause);
|
|
195
|
+
if (eventCount === 0) return null;
|
|
196
|
+
const [{ lastActive }] = await db.select({ lastActive: sql`MAX(${analyticsEvents2.timestamp})` }).from(analyticsEvents2).where(whereClause);
|
|
197
|
+
const platformsResult = await db.select({ platform: analyticsEvents2.platform }).from(analyticsEvents2).where(whereClause).groupBy(analyticsEvents2.platform);
|
|
198
|
+
const topEvents = await db.select({
|
|
199
|
+
eventType: analyticsEvents2.eventType,
|
|
200
|
+
count: count()
|
|
201
|
+
}).from(analyticsEvents2).where(whereClause).groupBy(analyticsEvents2.eventType).orderBy(desc(count())).limit(5);
|
|
202
|
+
return {
|
|
203
|
+
userId,
|
|
204
|
+
eventCount: Number(eventCount),
|
|
205
|
+
lastActive,
|
|
206
|
+
platforms: platformsResult.map((p) => p.platform),
|
|
207
|
+
topEvents: topEvents.map((e) => ({
|
|
208
|
+
eventType: e.eventType,
|
|
209
|
+
count: Number(e.count)
|
|
210
|
+
}))
|
|
211
|
+
};
|
|
212
|
+
} catch (error) {
|
|
213
|
+
logger.error("Failed to get user behavior", error);
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
/**
|
|
218
|
+
* 获取会话分析
|
|
219
|
+
*/
|
|
220
|
+
async getSessionAnalytics(sessionId) {
|
|
221
|
+
try {
|
|
222
|
+
const events = await db.select().from(analyticsEvents2).where(eq(analyticsEvents2.sessionId, sessionId)).orderBy(asc(analyticsEvents2.timestamp));
|
|
223
|
+
if (events.length === 0) return null;
|
|
224
|
+
const firstEvent = events[0];
|
|
225
|
+
const lastEvent = events[events.length - 1];
|
|
226
|
+
const startTime = new Date(firstEvent.timestamp).getTime();
|
|
227
|
+
const endTime = new Date(lastEvent.timestamp).getTime();
|
|
228
|
+
const duration = endTime - startTime;
|
|
229
|
+
return {
|
|
230
|
+
sessionId,
|
|
231
|
+
userId: firstEvent.userId || void 0,
|
|
232
|
+
deviceId: firstEvent.deviceId,
|
|
233
|
+
platform: firstEvent.platform,
|
|
234
|
+
startTime: firstEvent.timestamp,
|
|
235
|
+
endTime: lastEvent.timestamp,
|
|
236
|
+
duration,
|
|
237
|
+
eventCount: events.length,
|
|
238
|
+
events
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error("Failed to get session analytics", error);
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
/**
|
|
246
|
+
* 获取漏斗分析
|
|
247
|
+
*/
|
|
248
|
+
async getFunnelAnalysis(steps, startDate, endDate) {
|
|
249
|
+
try {
|
|
250
|
+
const results = [];
|
|
251
|
+
let previousCount = 0;
|
|
252
|
+
for (let i = 0; i < steps.length; i++) {
|
|
253
|
+
const conditions = [eq(analyticsEvents2.eventType, steps[i])];
|
|
254
|
+
if (startDate) {
|
|
255
|
+
conditions.push(gte(analyticsEvents2.timestamp, startDate));
|
|
256
|
+
}
|
|
257
|
+
if (endDate) {
|
|
258
|
+
conditions.push(lte(analyticsEvents2.timestamp, endDate));
|
|
259
|
+
}
|
|
260
|
+
const whereClause = and(...conditions);
|
|
261
|
+
const [{ count: stepCount }] = await db.select({ count: sql`COUNT(DISTINCT ${analyticsEvents2.userId})` }).from(analyticsEvents2).where(whereClause);
|
|
262
|
+
const conversionRate = i === 0 ? 100 : Number(stepCount) / previousCount * 100;
|
|
263
|
+
results.push({
|
|
264
|
+
step: steps[i] || "unknown",
|
|
265
|
+
count: Number(stepCount),
|
|
266
|
+
conversionRate: Math.round(conversionRate * 100) / 100
|
|
267
|
+
});
|
|
268
|
+
previousCount = Number(stepCount);
|
|
269
|
+
}
|
|
270
|
+
return results;
|
|
271
|
+
} catch (error) {
|
|
272
|
+
logger.error("Failed to get funnel analysis", error);
|
|
273
|
+
throw error;
|
|
274
|
+
}
|
|
275
|
+
},
|
|
276
|
+
/**
|
|
277
|
+
* 删除旧数据(数据清理)
|
|
278
|
+
*/
|
|
279
|
+
async cleanOldAnalyticsEvents(daysToKeep = 90) {
|
|
280
|
+
try {
|
|
281
|
+
const cutoffDate = /* @__PURE__ */ new Date();
|
|
282
|
+
cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);
|
|
283
|
+
await db.delete(analyticsEvents2).where(lte(analyticsEvents2.timestamp, cutoffDate.toISOString()));
|
|
284
|
+
logger.info("Old analytics events cleaned", { daysToKeep });
|
|
285
|
+
return 0;
|
|
286
|
+
} catch (error) {
|
|
287
|
+
logger.error("Failed to clean old analytics events", error);
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// src/analytics/server/handlers.ts
|
|
295
|
+
function createAnalyticsHandlers(service, ResponseClass) {
|
|
296
|
+
const createResponse = ResponseClass ? (data, init) => ResponseClass.json(data, init) : (data, init) => ({ body: data, status: init?.status || 200 });
|
|
297
|
+
return {
|
|
298
|
+
/**
|
|
299
|
+
* POST /api/analytics/events
|
|
300
|
+
* 处理事件上报
|
|
301
|
+
*/
|
|
302
|
+
async handleEventsPost(request) {
|
|
303
|
+
try {
|
|
304
|
+
const body = await request.json();
|
|
305
|
+
const { events } = body;
|
|
306
|
+
if (!Array.isArray(events)) {
|
|
307
|
+
return createResponse(
|
|
308
|
+
{ success: false, message: "Invalid events format" },
|
|
309
|
+
{ status: 400 }
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
if (events.length === 0) {
|
|
313
|
+
return createResponse(
|
|
314
|
+
{ success: true, message: "No events to process" },
|
|
315
|
+
{ status: 200 }
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
for (const event of events) {
|
|
319
|
+
if (!event.event_id || !event.event_type || !event.event_name) {
|
|
320
|
+
return createResponse(
|
|
321
|
+
{ success: false, message: "Missing required event fields" },
|
|
322
|
+
{ status: 400 }
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
const formattedEvents = events.map((event) => ({
|
|
327
|
+
id: event.event_id,
|
|
328
|
+
eventType: event.event_type,
|
|
329
|
+
eventName: event.event_name,
|
|
330
|
+
timestamp: new Date(event.timestamp).toISOString(),
|
|
331
|
+
priority: event.priority,
|
|
332
|
+
userId: event.user_id || null,
|
|
333
|
+
sessionId: event.session_id,
|
|
334
|
+
deviceId: event.device_id,
|
|
335
|
+
pageUrl: event.page_url || null,
|
|
336
|
+
pageTitle: event.page_title || null,
|
|
337
|
+
referrer: event.referrer || null,
|
|
338
|
+
properties: event.properties || {},
|
|
339
|
+
platform: event.platform,
|
|
340
|
+
appVersion: event.app_version,
|
|
341
|
+
sdkVersion: event.sdk_version
|
|
342
|
+
}));
|
|
343
|
+
await service.insertAnalyticsEvents(formattedEvents);
|
|
344
|
+
return createResponse({
|
|
345
|
+
success: true,
|
|
346
|
+
message: "Events received successfully",
|
|
347
|
+
count: events.length
|
|
348
|
+
});
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error("Failed to process analytics events", error);
|
|
351
|
+
return createResponse(
|
|
352
|
+
{
|
|
353
|
+
success: false,
|
|
354
|
+
message: "Internal server error",
|
|
355
|
+
error: process.env.NODE_ENV === "development" ? error.message : void 0
|
|
356
|
+
},
|
|
357
|
+
{ status: 500 }
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
},
|
|
361
|
+
/**
|
|
362
|
+
* GET /api/analytics/query
|
|
363
|
+
* 查询埋点事件
|
|
364
|
+
*/
|
|
365
|
+
async handleQueryGet(request) {
|
|
366
|
+
try {
|
|
367
|
+
const searchParams = request.nextUrl?.searchParams || new URLSearchParams(request.url?.split("?")[1]);
|
|
368
|
+
const params = {
|
|
369
|
+
startDate: searchParams.get("startDate") || void 0,
|
|
370
|
+
endDate: searchParams.get("endDate") || void 0,
|
|
371
|
+
eventType: searchParams.get("eventType") || void 0,
|
|
372
|
+
eventTypes: searchParams.get("eventTypes")?.split(",") || void 0,
|
|
373
|
+
userId: searchParams.get("userId") || void 0,
|
|
374
|
+
platform: searchParams.get("platform") || void 0,
|
|
375
|
+
platforms: searchParams.get("platforms")?.split(",") || void 0,
|
|
376
|
+
sessionId: searchParams.get("sessionId") || void 0,
|
|
377
|
+
limit: parseInt(searchParams.get("limit") || "100"),
|
|
378
|
+
offset: parseInt(searchParams.get("offset") || "0"),
|
|
379
|
+
orderBy: searchParams.get("orderBy") || "timestamp",
|
|
380
|
+
orderDirection: searchParams.get("orderDirection") || "desc"
|
|
381
|
+
};
|
|
382
|
+
const result = await service.queryAnalyticsEvents(params);
|
|
383
|
+
return createResponse({
|
|
384
|
+
success: true,
|
|
385
|
+
data: result.events,
|
|
386
|
+
total: result.total,
|
|
387
|
+
limit: params.limit,
|
|
388
|
+
offset: params.offset
|
|
389
|
+
});
|
|
390
|
+
} catch (error) {
|
|
391
|
+
console.error("Failed to query analytics events", error);
|
|
392
|
+
return createResponse(
|
|
393
|
+
{
|
|
394
|
+
success: false,
|
|
395
|
+
message: "Internal server error",
|
|
396
|
+
error: process.env.NODE_ENV === "development" ? error.message : void 0
|
|
397
|
+
},
|
|
398
|
+
{ status: 500 }
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
},
|
|
402
|
+
/**
|
|
403
|
+
* GET /api/analytics/stats
|
|
404
|
+
* 获取统计数据
|
|
405
|
+
*/
|
|
406
|
+
async handleStatsGet(request) {
|
|
407
|
+
try {
|
|
408
|
+
const searchParams = request.nextUrl?.searchParams || new URLSearchParams(request.url?.split("?")[1]);
|
|
409
|
+
const startDate = searchParams.get("startDate") || void 0;
|
|
410
|
+
const endDate = searchParams.get("endDate") || void 0;
|
|
411
|
+
const platform = searchParams.get("platform") || void 0;
|
|
412
|
+
const stats = await service.getAnalyticsStats(startDate, endDate, platform);
|
|
413
|
+
return createResponse({
|
|
414
|
+
success: true,
|
|
415
|
+
data: stats
|
|
416
|
+
});
|
|
417
|
+
} catch (error) {
|
|
418
|
+
console.error("Failed to get analytics stats", error);
|
|
419
|
+
return createResponse(
|
|
420
|
+
{
|
|
421
|
+
success: false,
|
|
422
|
+
message: "Internal server error",
|
|
423
|
+
error: process.env.NODE_ENV === "development" ? error.message : void 0
|
|
424
|
+
},
|
|
425
|
+
{ status: 500 }
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
},
|
|
429
|
+
/**
|
|
430
|
+
* GET /api/analytics/user/[userId]
|
|
431
|
+
* 获取用户行为分析
|
|
432
|
+
*/
|
|
433
|
+
async handleUserBehaviorGet(request, params) {
|
|
434
|
+
try {
|
|
435
|
+
const { userId } = params;
|
|
436
|
+
const searchParams = request.nextUrl?.searchParams || new URLSearchParams(request.url?.split("?")[1]);
|
|
437
|
+
const startDate = searchParams.get("startDate") || void 0;
|
|
438
|
+
const endDate = searchParams.get("endDate") || void 0;
|
|
439
|
+
const behavior = await service.getUserBehavior(userId, startDate, endDate);
|
|
440
|
+
if (!behavior) {
|
|
441
|
+
return createResponse(
|
|
442
|
+
{ success: false, message: "No data found for this user" },
|
|
443
|
+
{ status: 404 }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
return createResponse({
|
|
447
|
+
success: true,
|
|
448
|
+
data: behavior
|
|
449
|
+
});
|
|
450
|
+
} catch (error) {
|
|
451
|
+
console.error("Failed to get user behavior", error);
|
|
452
|
+
return createResponse(
|
|
453
|
+
{
|
|
454
|
+
success: false,
|
|
455
|
+
message: "Internal server error",
|
|
456
|
+
error: process.env.NODE_ENV === "development" ? error.message : void 0
|
|
457
|
+
},
|
|
458
|
+
{ status: 500 }
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
},
|
|
462
|
+
/**
|
|
463
|
+
* GET /api/analytics/session/[sessionId]
|
|
464
|
+
* 获取会话分析
|
|
465
|
+
*/
|
|
466
|
+
async handleSessionAnalyticsGet(_request, params) {
|
|
467
|
+
try {
|
|
468
|
+
const { sessionId } = params;
|
|
469
|
+
const session = await service.getSessionAnalytics(sessionId);
|
|
470
|
+
if (!session) {
|
|
471
|
+
return createResponse({ success: false, message: "Session not found" }, { status: 404 });
|
|
472
|
+
}
|
|
473
|
+
return createResponse({
|
|
474
|
+
success: true,
|
|
475
|
+
data: session
|
|
476
|
+
});
|
|
477
|
+
} catch (error) {
|
|
478
|
+
console.error("Failed to get session analytics", error);
|
|
479
|
+
return createResponse(
|
|
480
|
+
{
|
|
481
|
+
success: false,
|
|
482
|
+
message: "Internal server error",
|
|
483
|
+
error: process.env.NODE_ENV === "development" ? error.message : void 0
|
|
484
|
+
},
|
|
485
|
+
{ status: 500 }
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
/**
|
|
490
|
+
* POST /api/analytics/funnel
|
|
491
|
+
* 漏斗分析
|
|
492
|
+
*/
|
|
493
|
+
async handleFunnelPost(request) {
|
|
494
|
+
try {
|
|
495
|
+
const body = await request.json();
|
|
496
|
+
const { steps, startDate, endDate } = body;
|
|
497
|
+
if (!Array.isArray(steps) || steps.length === 0) {
|
|
498
|
+
return createResponse(
|
|
499
|
+
{ success: false, message: "Invalid steps format" },
|
|
500
|
+
{ status: 400 }
|
|
501
|
+
);
|
|
502
|
+
}
|
|
503
|
+
const funnel = await service.getFunnelAnalysis(steps, startDate, endDate);
|
|
504
|
+
return createResponse({
|
|
505
|
+
success: true,
|
|
506
|
+
data: funnel
|
|
507
|
+
});
|
|
508
|
+
} catch (error) {
|
|
509
|
+
console.error("Failed to get funnel analysis", error);
|
|
510
|
+
return createResponse(
|
|
511
|
+
{
|
|
512
|
+
success: false,
|
|
513
|
+
message: "Internal server error",
|
|
514
|
+
error: process.env.NODE_ENV === "development" ? error.message : void 0
|
|
515
|
+
},
|
|
516
|
+
{ status: 500 }
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export { analyticsEvents, createAnalyticsHandlers, createAnalyticsService };
|
|
524
|
+
//# sourceMappingURL=index.mjs.map
|
|
525
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/analytics/server/schema.ts","../../../src/analytics/server/service.ts","../../../src/analytics/server/handlers.ts"],"names":["analyticsEvents","sql"],"mappings":"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","file":"index.mjs","sourcesContent":["/**\n * 埋点系统数据库 Schema\n * Analytics Database Schema\n *\n * 使用方式:\n * 在 backend/drizzle/migrations/schema.ts 中导入并导出:\n * export { analyticsEvents } from '@lyricnote/shared/analytics/server/schema'\n */\n\nimport { pgTable, text, timestamp, integer, jsonb, index } from 'drizzle-orm/pg-core';\nimport { sql } from 'drizzle-orm';\n\n/**\n * 埋点事件表\n */\nexport const analyticsEvents = pgTable(\n 'analytics_events',\n {\n id: text().primaryKey().notNull(),\n eventType: text('event_type').notNull(),\n eventName: text('event_name').notNull(),\n timestamp: timestamp({ precision: 3, mode: 'string' }).notNull(),\n priority: integer().notNull(),\n\n userId: text('user_id'),\n sessionId: text('session_id').notNull(),\n deviceId: text('device_id').notNull(),\n\n pageUrl: text('page_url'),\n pageTitle: text('page_title'),\n referrer: text(),\n\n properties: jsonb(),\n\n platform: text().notNull(),\n appVersion: text('app_version').notNull(),\n sdkVersion: text('sdk_version').notNull(),\n\n createdAt: timestamp({ precision: 3, mode: 'string' })\n .default(sql`CURRENT_TIMESTAMP`)\n .notNull(),\n },\n (table) => [\n index('analytics_events_user_id_idx').using(\n 'btree',\n table.userId.asc().nullsLast().op('text_ops')\n ),\n index('analytics_events_event_type_idx').using(\n 'btree',\n table.eventType.asc().nullsLast().op('text_ops')\n ),\n index('analytics_events_platform_idx').using(\n 'btree',\n table.platform.asc().nullsLast().op('text_ops')\n ),\n index('analytics_events_timestamp_idx').using('btree', table.timestamp.desc().nullsLast()),\n index('analytics_events_session_id_idx').using(\n 'btree',\n table.sessionId.asc().nullsLast().op('text_ops')\n ),\n ]\n);\n\n/**\n * 创建外键关系(需要在使用时手动添加)\n *\n * 在你的 schema.ts 中添加:\n *\n * foreignKey({\n * columns: [analyticsEvents.userId],\n * foreignColumns: [user.id],\n * name: \"analytics_events_userId_fkey\"\n * }).onUpdate(\"cascade\").onDelete(\"set null\")\n */\n","/**\n * 埋点分析服务\n * Analytics Service\n *\n * 使用方式:\n * import { createAnalyticsService } from '@lyricnote/shared/analytics/server'\n * const service = createAnalyticsService(db, analyticsEvents, logger)\n */\n\nimport { eq, and, gte, lte, sql, desc, asc, count, inArray } from 'drizzle-orm';\n\n// 导出类型\nexport * from './types';\nimport type {\n AnalyticsEvent,\n AnalyticsQueryParams,\n AnalyticsStats,\n UserBehavior,\n SessionAnalytics,\n DatabaseInstance,\n} from './types';\n\n// Logger 接口\ninterface Logger {\n info: (message: string, data?: any) => void;\n error: (message: string, error: Error) => void;\n}\n\n// 默认 logger(如果未提供)\nconst defaultLogger: Logger = {\n info: (message, data) => console.log(`[Analytics] ${message}`, data || ''),\n error: (message, error) => console.error(`[Analytics] ${message}`, error),\n};\n\n/**\n * 创建埋点服务实例\n * @param db - Drizzle 数据库实例\n * @param analyticsEventsTable - analytics_events 表定义\n * @param logger - 日志实例(可选)\n */\nexport function createAnalyticsService(\n db: DatabaseInstance,\n analyticsEventsTable: any,\n logger: Logger = defaultLogger\n) {\n const analyticsEvents = analyticsEventsTable;\n\n return {\n /**\n * 批量插入埋点事件\n */\n async insertAnalyticsEvents(events: AnalyticsEvent[]): Promise<void> {\n try {\n if (events.length === 0) return;\n\n // 使用 onConflictDoNothing 来忽略重复的事件 ID\n // 这样可以避免前端重试或离线队列导致的重复插入\n await db.insert(analyticsEvents).values(events).onConflictDoNothing();\n\n logger.info('Analytics events inserted', { count: events.length });\n } catch (error) {\n logger.error('Failed to insert analytics events', error as Error);\n throw error;\n }\n },\n\n /**\n * 查询埋点事件\n */\n async queryAnalyticsEvents(\n params: AnalyticsQueryParams\n ): Promise<{ events: AnalyticsEvent[]; total: number }> {\n try {\n const conditions: any[] = [];\n\n // 时间范围过滤\n if (params.startDate) {\n conditions.push(gte(analyticsEvents.timestamp, params.startDate));\n }\n if (params.endDate) {\n conditions.push(lte(analyticsEvents.timestamp, params.endDate));\n }\n\n // 事件类型过滤\n if (params.eventType) {\n conditions.push(eq(analyticsEvents.eventType, params.eventType));\n }\n if (params.eventTypes && params.eventTypes.length > 0) {\n conditions.push(inArray(analyticsEvents.eventType, params.eventTypes));\n }\n\n // 用户过滤\n if (params.userId) {\n conditions.push(eq(analyticsEvents.userId, params.userId));\n }\n\n // 平台过滤\n if (params.platform) {\n conditions.push(eq(analyticsEvents.platform, params.platform));\n }\n if (params.platforms && params.platforms.length > 0) {\n conditions.push(inArray(analyticsEvents.platform, params.platforms));\n }\n\n // 会话过滤\n if (params.sessionId) {\n conditions.push(eq(analyticsEvents.sessionId, params.sessionId));\n }\n\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined;\n\n // 查询总数\n const [{ total }] = await db\n .select({ total: count() })\n .from(analyticsEvents)\n .where(whereClause);\n\n // 排序\n const orderByColumn = params.orderBy || 'timestamp';\n const orderDirection = params.orderDirection || 'desc';\n const orderFn = orderDirection === 'asc' ? asc : desc;\n\n let orderByField;\n switch (orderByColumn) {\n case 'eventType':\n orderByField = analyticsEvents.eventType;\n break;\n case 'platform':\n orderByField = analyticsEvents.platform;\n break;\n default:\n orderByField = analyticsEvents.timestamp;\n }\n\n // 查询数据\n const events = await db\n .select()\n .from(analyticsEvents)\n .where(whereClause)\n .orderBy(orderFn(orderByField))\n .limit(params.limit || 100)\n .offset(params.offset || 0);\n\n // 修复时间戳格式:添加 'Z' 后缀表示 UTC 时间\n const formattedEvents = events.map((event: any) => ({\n ...event,\n timestamp: event.timestamp.endsWith('Z') ? event.timestamp : `${event.timestamp}Z`,\n createdAt: event.createdAt.endsWith('Z') ? event.createdAt : `${event.createdAt}Z`,\n }));\n\n return {\n events: formattedEvents as AnalyticsEvent[],\n total: Number(total),\n };\n } catch (error) {\n logger.error('Failed to query analytics events', error as Error);\n throw error;\n }\n },\n\n /**\n * 获取统计数据\n */\n async getAnalyticsStats(\n startDate?: string,\n endDate?: string,\n platform?: string\n ): Promise<AnalyticsStats> {\n try {\n const conditions: any[] = [];\n\n if (startDate) {\n conditions.push(gte(analyticsEvents.timestamp, startDate));\n }\n if (endDate) {\n conditions.push(lte(analyticsEvents.timestamp, endDate));\n }\n if (platform) {\n conditions.push(eq(analyticsEvents.platform, platform));\n }\n\n const whereClause = conditions.length > 0 ? and(...conditions) : undefined;\n\n // 总事件数\n const [{ totalEvents }] = await db\n .select({ totalEvents: count() })\n .from(analyticsEvents)\n .where(whereClause);\n\n // 唯一用户数\n const [{ uniqueUsers }] = await db\n .select({ uniqueUsers: sql<number>`COUNT(DISTINCT ${analyticsEvents.userId})` })\n .from(analyticsEvents)\n .where(whereClause);\n\n // 唯一会话数\n const [{ uniqueSessions }] = await db\n .select({ uniqueSessions: sql<number>`COUNT(DISTINCT ${analyticsEvents.sessionId})` })\n .from(analyticsEvents)\n .where(whereClause);\n\n // 唯一设备数\n const [{ uniqueDevices }] = await db\n .select({ uniqueDevices: sql<number>`COUNT(DISTINCT ${analyticsEvents.deviceId})` })\n .from(analyticsEvents)\n .where(whereClause);\n\n // 按事件类型统计\n const eventsByType = await db\n .select({\n eventType: analyticsEvents.eventType,\n count: count(),\n })\n .from(analyticsEvents)\n .where(whereClause)\n .groupBy(analyticsEvents.eventType)\n .orderBy(desc(count()))\n .limit(10);\n\n // 按平台统计\n const eventsByPlatform = await db\n .select({\n platform: analyticsEvents.platform,\n count: count(),\n })\n .from(analyticsEvents)\n .where(whereClause)\n .groupBy(analyticsEvents.platform)\n .orderBy(desc(count()));\n\n // 热门页面\n const topPages = await db\n .select({\n pageUrl: analyticsEvents.pageUrl,\n count: count(),\n })\n .from(analyticsEvents)\n .where(and(whereClause, sql`${analyticsEvents.pageUrl} IS NOT NULL`))\n .groupBy(analyticsEvents.pageUrl)\n .orderBy(desc(count()))\n .limit(10);\n\n return {\n totalEvents: Number(totalEvents),\n uniqueUsers: Number(uniqueUsers),\n uniqueSessions: Number(uniqueSessions),\n uniqueDevices: Number(uniqueDevices),\n eventsByType: eventsByType.map((e: any) => ({\n eventType: e.eventType,\n count: Number(e.count),\n })),\n eventsByPlatform: eventsByPlatform.map((e: any) => ({\n platform: e.platform,\n count: Number(e.count),\n })),\n topPages: topPages\n .filter((p: any) => p.pageUrl)\n .map((p: any) => ({\n pageUrl: p.pageUrl!,\n count: Number(p.count),\n })),\n };\n } catch (error) {\n logger.error('Failed to get analytics stats', error as Error);\n throw error;\n }\n },\n\n /**\n * 获取用户行为分析\n */\n async getUserBehavior(\n userId: string,\n startDate?: string,\n endDate?: string\n ): Promise<UserBehavior | null> {\n try {\n const conditions: any[] = [eq(analyticsEvents.userId, userId)];\n\n if (startDate) {\n conditions.push(gte(analyticsEvents.timestamp, startDate));\n }\n if (endDate) {\n conditions.push(lte(analyticsEvents.timestamp, endDate));\n }\n\n const whereClause = and(...conditions);\n\n // 事件总数\n const [{ eventCount }] = await db\n .select({ eventCount: count() })\n .from(analyticsEvents)\n .where(whereClause);\n\n if (eventCount === 0) return null;\n\n // 最后活跃时间\n const [{ lastActive }] = await db\n .select({ lastActive: sql<string>`MAX(${analyticsEvents.timestamp})` })\n .from(analyticsEvents)\n .where(whereClause);\n\n // 使用的平台\n const platformsResult = await db\n .select({ platform: analyticsEvents.platform })\n .from(analyticsEvents)\n .where(whereClause)\n .groupBy(analyticsEvents.platform);\n\n // 热门事件\n const topEvents = await db\n .select({\n eventType: analyticsEvents.eventType,\n count: count(),\n })\n .from(analyticsEvents)\n .where(whereClause)\n .groupBy(analyticsEvents.eventType)\n .orderBy(desc(count()))\n .limit(5);\n\n return {\n userId,\n eventCount: Number(eventCount),\n lastActive,\n platforms: platformsResult.map((p: any) => p.platform),\n topEvents: topEvents.map((e: any) => ({\n eventType: e.eventType,\n count: Number(e.count),\n })),\n };\n } catch (error) {\n logger.error('Failed to get user behavior', error as Error);\n throw error;\n }\n },\n\n /**\n * 获取会话分析\n */\n async getSessionAnalytics(sessionId: string): Promise<SessionAnalytics | null> {\n try {\n const events = await db\n .select()\n .from(analyticsEvents)\n .where(eq(analyticsEvents.sessionId, sessionId))\n .orderBy(asc(analyticsEvents.timestamp));\n\n if (events.length === 0) return null;\n\n const firstEvent = events[0];\n const lastEvent = events[events.length - 1];\n\n const startTime = new Date(firstEvent.timestamp).getTime();\n const endTime = new Date(lastEvent.timestamp).getTime();\n const duration = endTime - startTime;\n\n return {\n sessionId,\n userId: firstEvent.userId || undefined,\n deviceId: firstEvent.deviceId,\n platform: firstEvent.platform,\n startTime: firstEvent.timestamp,\n endTime: lastEvent.timestamp,\n duration,\n eventCount: events.length,\n events: events as AnalyticsEvent[],\n };\n } catch (error) {\n logger.error('Failed to get session analytics', error as Error);\n throw error;\n }\n },\n\n /**\n * 获取漏斗分析\n */\n async getFunnelAnalysis(\n steps: string[],\n startDate?: string,\n endDate?: string\n ): Promise<{ step: string; count: number; conversionRate: number }[]> {\n try {\n const results = [];\n let previousCount = 0;\n\n for (let i = 0; i < steps.length; i++) {\n const conditions: any[] = [eq(analyticsEvents.eventType, steps[i])];\n\n if (startDate) {\n conditions.push(gte(analyticsEvents.timestamp, startDate));\n }\n if (endDate) {\n conditions.push(lte(analyticsEvents.timestamp, endDate));\n }\n\n const whereClause = and(...conditions);\n\n const [{ count: stepCount }] = await db\n .select({ count: sql<number>`COUNT(DISTINCT ${analyticsEvents.userId})` })\n .from(analyticsEvents)\n .where(whereClause);\n\n const conversionRate = i === 0 ? 100 : (Number(stepCount) / previousCount) * 100;\n\n results.push({\n step: steps[i] || 'unknown',\n count: Number(stepCount),\n conversionRate: Math.round(conversionRate * 100) / 100,\n });\n\n previousCount = Number(stepCount);\n }\n\n return results;\n } catch (error) {\n logger.error('Failed to get funnel analysis', error as Error);\n throw error;\n }\n },\n\n /**\n * 删除旧数据(数据清理)\n */\n async cleanOldAnalyticsEvents(daysToKeep: number = 90): Promise<number> {\n try {\n const cutoffDate = new Date();\n cutoffDate.setDate(cutoffDate.getDate() - daysToKeep);\n\n await db\n .delete(analyticsEvents)\n .where(lte(analyticsEvents.timestamp, cutoffDate.toISOString()));\n\n logger.info('Old analytics events cleaned', { daysToKeep });\n\n return 0; // Drizzle doesn't return affected rows count in delete\n } catch (error) {\n logger.error('Failed to clean old analytics events', error as Error);\n throw error;\n }\n },\n };\n}\n","/**\n * 埋点 API 路由处理器\n * Analytics API Route Handlers\n *\n * 使用方式(Next.js):\n * import { createAnalyticsHandlers } from '@lyricnote/shared/analytics/server'\n * const handlers = createAnalyticsHandlers(analyticsService)\n * export const POST = handlers.handleEventsPost\n */\n\nimport type { AnalyticsEvent, AnalyticsQueryParams } from './types';\n\n// 服务接口类型\ninterface AnalyticsService {\n insertAnalyticsEvents: (events: AnalyticsEvent[]) => Promise<void>;\n queryAnalyticsEvents: (\n params: AnalyticsQueryParams\n ) => Promise<{ events: AnalyticsEvent[]; total: number }>;\n getAnalyticsStats: (startDate?: string, endDate?: string, platform?: string) => Promise<any>;\n getUserBehavior: (userId: string, startDate?: string, endDate?: string) => Promise<any>;\n getSessionAnalytics: (sessionId: string) => Promise<any>;\n getFunnelAnalysis: (steps: string[], startDate?: string, endDate?: string) => Promise<any>;\n}\n\n// Request/Response 接口(兼容多种框架)\ninterface Request {\n json: () => Promise<any>;\n url?: string;\n nextUrl?: { searchParams: URLSearchParams };\n}\n\ninterface ResponseInit {\n status?: number;\n headers?: Record<string, string>;\n}\n\n/**\n * 创建埋点 API 处理器\n */\nexport function createAnalyticsHandlers(\n service: AnalyticsService,\n ResponseClass?: any // Next.js NextResponse 或其他响应类\n) {\n // 默认使用标准响应格式\n const createResponse = ResponseClass\n ? (data: any, init?: ResponseInit) => ResponseClass.json(data, init)\n : (data: any, init?: ResponseInit) => ({ body: data, status: init?.status || 200 });\n\n return {\n /**\n * POST /api/analytics/events\n * 处理事件上报\n */\n async handleEventsPost(request: Request) {\n try {\n const body = await request.json();\n const { events } = body;\n\n // 验证数据\n if (!Array.isArray(events)) {\n return createResponse(\n { success: false, message: 'Invalid events format' },\n { status: 400 }\n );\n }\n\n if (events.length === 0) {\n return createResponse(\n { success: true, message: 'No events to process' },\n { status: 200 }\n );\n }\n\n // 验证每个事件的必填字段\n for (const event of events) {\n if (!event.event_id || !event.event_type || !event.event_name) {\n return createResponse(\n { success: false, message: 'Missing required event fields' },\n { status: 400 }\n );\n }\n }\n\n // 转换字段名(前端使用 snake_case,数据库使用 camelCase)\n const formattedEvents = events.map((event) => ({\n id: event.event_id,\n eventType: event.event_type,\n eventName: event.event_name,\n timestamp: new Date(event.timestamp).toISOString(),\n priority: event.priority,\n userId: event.user_id || null,\n sessionId: event.session_id,\n deviceId: event.device_id,\n pageUrl: event.page_url || null,\n pageTitle: event.page_title || null,\n referrer: event.referrer || null,\n properties: event.properties || {},\n platform: event.platform,\n appVersion: event.app_version,\n sdkVersion: event.sdk_version,\n }));\n\n // 插入数据库\n await service.insertAnalyticsEvents(formattedEvents);\n\n return createResponse({\n success: true,\n message: 'Events received successfully',\n count: events.length,\n });\n } catch (error) {\n console.error('Failed to process analytics events', error);\n\n return createResponse(\n {\n success: false,\n message: 'Internal server error',\n error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,\n },\n { status: 500 }\n );\n }\n },\n\n /**\n * GET /api/analytics/query\n * 查询埋点事件\n */\n async handleQueryGet(request: Request) {\n try {\n // 解析查询参数\n const searchParams =\n request.nextUrl?.searchParams || new URLSearchParams(request.url?.split('?')[1]);\n\n const params: AnalyticsQueryParams = {\n startDate: searchParams.get('startDate') || undefined,\n endDate: searchParams.get('endDate') || undefined,\n eventType: searchParams.get('eventType') || undefined,\n eventTypes: searchParams.get('eventTypes')?.split(',') || undefined,\n userId: searchParams.get('userId') || undefined,\n platform: searchParams.get('platform') || undefined,\n platforms: searchParams.get('platforms')?.split(',') || undefined,\n sessionId: searchParams.get('sessionId') || undefined,\n limit: parseInt(searchParams.get('limit') || '100'),\n offset: parseInt(searchParams.get('offset') || '0'),\n orderBy: (searchParams.get('orderBy') || 'timestamp') as any,\n orderDirection: (searchParams.get('orderDirection') || 'desc') as any,\n };\n\n // 查询数据\n const result = await service.queryAnalyticsEvents(params);\n\n return createResponse({\n success: true,\n data: result.events,\n total: result.total,\n limit: params.limit,\n offset: params.offset,\n });\n } catch (error) {\n console.error('Failed to query analytics events', error);\n\n return createResponse(\n {\n success: false,\n message: 'Internal server error',\n error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,\n },\n { status: 500 }\n );\n }\n },\n\n /**\n * GET /api/analytics/stats\n * 获取统计数据\n */\n async handleStatsGet(request: Request) {\n try {\n const searchParams =\n request.nextUrl?.searchParams || new URLSearchParams(request.url?.split('?')[1]);\n\n const startDate = searchParams.get('startDate') || undefined;\n const endDate = searchParams.get('endDate') || undefined;\n const platform = searchParams.get('platform') || undefined;\n\n // 获取统计数据\n const stats = await service.getAnalyticsStats(startDate, endDate, platform);\n\n return createResponse({\n success: true,\n data: stats,\n });\n } catch (error) {\n console.error('Failed to get analytics stats', error);\n\n return createResponse(\n {\n success: false,\n message: 'Internal server error',\n error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,\n },\n { status: 500 }\n );\n }\n },\n\n /**\n * GET /api/analytics/user/[userId]\n * 获取用户行为分析\n */\n async handleUserBehaviorGet(request: Request, params: { userId: string }) {\n try {\n const { userId } = params;\n const searchParams =\n request.nextUrl?.searchParams || new URLSearchParams(request.url?.split('?')[1]);\n\n const startDate = searchParams.get('startDate') || undefined;\n const endDate = searchParams.get('endDate') || undefined;\n\n // 获取用户行为数据\n const behavior = await service.getUserBehavior(userId, startDate, endDate);\n\n if (!behavior) {\n return createResponse(\n { success: false, message: 'No data found for this user' },\n { status: 404 }\n );\n }\n\n return createResponse({\n success: true,\n data: behavior,\n });\n } catch (error) {\n console.error('Failed to get user behavior', error);\n\n return createResponse(\n {\n success: false,\n message: 'Internal server error',\n error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,\n },\n { status: 500 }\n );\n }\n },\n\n /**\n * GET /api/analytics/session/[sessionId]\n * 获取会话分析\n */\n async handleSessionAnalyticsGet(_request: Request, params: { sessionId: string }) {\n try {\n const { sessionId } = params;\n\n // 获取会话分析数据\n const session = await service.getSessionAnalytics(sessionId);\n\n if (!session) {\n return createResponse({ success: false, message: 'Session not found' }, { status: 404 });\n }\n\n return createResponse({\n success: true,\n data: session,\n });\n } catch (error) {\n console.error('Failed to get session analytics', error);\n\n return createResponse(\n {\n success: false,\n message: 'Internal server error',\n error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,\n },\n { status: 500 }\n );\n }\n },\n\n /**\n * POST /api/analytics/funnel\n * 漏斗分析\n */\n async handleFunnelPost(request: Request) {\n try {\n const body = await request.json();\n const { steps, startDate, endDate } = body;\n\n // 验证数据\n if (!Array.isArray(steps) || steps.length === 0) {\n return createResponse(\n { success: false, message: 'Invalid steps format' },\n { status: 400 }\n );\n }\n\n // 获取漏斗分析数据\n const funnel = await service.getFunnelAnalysis(steps, startDate, endDate);\n\n return createResponse({\n success: true,\n data: funnel,\n });\n } catch (error) {\n console.error('Failed to get funnel analysis', error);\n\n return createResponse(\n {\n success: false,\n message: 'Internal server error',\n error: process.env.NODE_ENV === 'development' ? (error as Error).message : undefined,\n },\n { status: 500 }\n );\n }\n },\n };\n}\n"]}
|