befly 3.61.0 → 3.63.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/apis/auth/login.js +65 -13
- package/apis/tongJi/_tongJi.js +92 -41
- package/apis/tongJi/dailyReport.js +73 -51
- package/apis/tongJi/dailyStats.js +98 -125
- package/apis/tongJi/dailyStatsDistribution.js +86 -0
- package/apis/tongJi/errorList.js +5 -9
- package/apis/tongJi/errorReport.js +81 -44
- package/apis/tongJi/errorStats.js +19 -125
- package/checks/config.js +1 -4
- package/configs/beflyConfig.json +1 -4
- package/index.js +5 -0
- package/lib/logger.js +125 -296
- package/package.json +1 -1
- package/plugins/cron.js +96 -0
- package/plugins/logger.js +2 -5
- package/sql/befly.sql +53 -25
- package/tables/dailyReport.json +124 -0
- package/tables/errorReport.json +31 -48
- package/tables/loginLog.json +29 -32
- package/utils/is.js +3 -36
- package/utils/loggerUtils.js +0 -3
- package/utils/userAgent.js +229 -104
- package/apis/tongJi/_daily.js +0 -159
|
@@ -1,136 +1,120 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { getTongJiDateYmdNumber, getTongJiMonthStartDate, getTongJiRecentDateList } from "./_tongJi.js";
|
|
2
2
|
|
|
3
3
|
const DAILY_STATS_DAY_LIMIT = 30;
|
|
4
|
+
const DAILY_STATS_HISTORY_CACHE_TTL_SECONDS = 2 * 24 * 60 * 60;
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
const
|
|
6
|
+
function getPreviousMonthDateRange(reportDate) {
|
|
7
|
+
const year = Math.floor(reportDate / 10000);
|
|
8
|
+
const month = Math.floor((reportDate % 10000) / 100);
|
|
9
|
+
const previousYear = month === 1 ? year - 1 : year;
|
|
10
|
+
const previousMonth = month === 1 ? 12 : month - 1;
|
|
11
|
+
const lastDay = new Date(Date.UTC(year, month - 1, 0)).getUTCDate();
|
|
7
12
|
|
|
8
|
-
return
|
|
13
|
+
return [previousYear * 10000 + previousMonth * 100 + 1, previousYear * 10000 + previousMonth * 100 + lastDay];
|
|
9
14
|
}
|
|
10
15
|
|
|
11
|
-
function
|
|
12
|
-
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function getDayCount(befly, reportDate, productCode) {
|
|
16
|
-
return {
|
|
17
|
-
reportDate: reportDate,
|
|
18
|
-
count: await getRedisSetCount(befly, getDayMembersKey(reportDate, productCode))
|
|
19
|
-
};
|
|
20
|
-
}
|
|
16
|
+
async function loadHistory(befly, reportDate, dateStats) {
|
|
17
|
+
const cacheKey = `daily:stats:history:${reportDate}`;
|
|
21
18
|
|
|
22
|
-
|
|
23
|
-
|
|
19
|
+
if (befly.redis) {
|
|
20
|
+
const cached = await befly.redis.getObject(cacheKey);
|
|
24
21
|
|
|
25
|
-
|
|
26
|
-
|
|
22
|
+
if (cached) {
|
|
23
|
+
return cached;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
27
26
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
27
|
+
if (!befly.mysql) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
31
|
+
const res = await befly.mysql.execute("SELECT product_code as productCode, report_date as reportDate, COUNT(*) as count FROM befly_daily_report WHERE state > 0 AND report_date BETWEEN ? AND ? GROUP BY product_code, report_date", [dateStats.previousMonthStartDate, dateStats.yesterdayDate]);
|
|
32
|
+
const history = {};
|
|
33
|
+
|
|
34
|
+
for (const item of res.data || []) {
|
|
35
|
+
const code = String(item.productCode || "");
|
|
36
|
+
const date = Number(item.reportDate);
|
|
37
|
+
const count = Number(item.count) || 0;
|
|
38
|
+
const stats = history[code] || {
|
|
39
|
+
yesterday: 0,
|
|
40
|
+
dayBeforeYesterday: 0,
|
|
41
|
+
month: 0,
|
|
42
|
+
lastMonth: 0
|
|
43
|
+
};
|
|
39
44
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
}
|
|
45
|
+
if (date === dateStats.yesterdayDate) {
|
|
46
|
+
stats.yesterday = count;
|
|
47
|
+
}
|
|
48
|
+
if (date === dateStats.dayBeforeYesterdayDate) {
|
|
49
|
+
stats.dayBeforeYesterday = count;
|
|
50
|
+
}
|
|
51
|
+
if (date >= dateStats.monthStartDate && date <= dateStats.yesterdayDate) {
|
|
52
|
+
stats.month += count;
|
|
53
|
+
}
|
|
54
|
+
if (date >= dateStats.previousMonthStartDate && date <= dateStats.previousMonthEndDate) {
|
|
55
|
+
stats.lastMonth += count;
|
|
56
|
+
}
|
|
46
57
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const timeZone = befly.config?.tz;
|
|
50
|
-
const weekDateList = getDailyDateRangeList(weekStartDate, reportDate, timeZone);
|
|
51
|
-
const monthDateList = recentDateList.filter((date) => date >= monthStartDate);
|
|
52
|
-
|
|
53
|
-
const [today, yesterday, dayBeforeYesterday, week, month, recent7, recent30, days] = await Promise.all([
|
|
54
|
-
getDayCount(befly, reportDate, productCode),
|
|
55
|
-
getDayCount(befly, recentDateList[recentDateList.length - 2] || reportDate, productCode),
|
|
56
|
-
getDayCount(befly, recentDateList[recentDateList.length - 3] || recentDateList[recentDateList.length - 2] || reportDate, productCode),
|
|
57
|
-
sumDayCounts(befly, weekDateList, productCode).then((count) => buildDateRangeSummary(weekStartDate, reportDate, count)),
|
|
58
|
-
sumDayCounts(befly, monthDateList, productCode).then((count) => buildDateRangeSummary(monthStartDate, reportDate, count)),
|
|
59
|
-
sumDayCounts(befly, recent7DateList, productCode).then((count) => buildDateRangeSummary(recent7DateList[0], reportDate, count)),
|
|
60
|
-
sumDayCounts(befly, recentDateList, productCode).then((count) => buildDateRangeSummary(recentDateList[0], reportDate, count)),
|
|
61
|
-
getDayCounts(befly, recentDateList, productCode)
|
|
62
|
-
]);
|
|
58
|
+
history[code] = stats;
|
|
59
|
+
}
|
|
63
60
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
dayBeforeYesterday: dayBeforeYesterday,
|
|
68
|
-
week: week,
|
|
69
|
-
month: month,
|
|
70
|
-
recent7: recent7,
|
|
71
|
-
recent30: recent30,
|
|
72
|
-
days: days
|
|
73
|
-
};
|
|
74
|
-
}
|
|
61
|
+
if (befly.redis) {
|
|
62
|
+
await befly.redis.setObject(cacheKey, history, DAILY_STATS_HISTORY_CACHE_TTL_SECONDS);
|
|
63
|
+
}
|
|
75
64
|
|
|
76
|
-
|
|
77
|
-
return recentDateList.map((reportDate) => {
|
|
78
|
-
return {
|
|
79
|
-
reportDate: reportDate,
|
|
80
|
-
count: 0
|
|
81
|
-
};
|
|
82
|
-
});
|
|
65
|
+
return history;
|
|
83
66
|
}
|
|
84
67
|
|
|
85
|
-
function
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
68
|
+
async function getTodayCount(befly, reportDate, productCode) {
|
|
69
|
+
if (!befly.redis) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
89
72
|
|
|
90
|
-
return {
|
|
91
|
-
today: buildDaySummary(reportDate, 0),
|
|
92
|
-
yesterday: buildDaySummary(yesterdayDate, 0),
|
|
93
|
-
dayBeforeYesterday: buildDaySummary(dayBeforeYesterdayDate, 0),
|
|
94
|
-
week: buildDateRangeSummary(weekStartDate, reportDate, 0),
|
|
95
|
-
month: buildDateRangeSummary(monthStartDate, reportDate, 0),
|
|
96
|
-
recent7: buildDateRangeSummary(recent7DateList[0], reportDate, 0),
|
|
97
|
-
recent30: buildDateRangeSummary(recentDateList[0], reportDate, 0),
|
|
98
|
-
days: days
|
|
99
|
-
};
|
|
73
|
+
return Number(await befly.redis.scard(`daily:day:${reportDate}:members:${productCode}`)) || 0;
|
|
100
74
|
}
|
|
101
75
|
|
|
102
|
-
function buildProductSummary(item,
|
|
76
|
+
function buildProductSummary(item, stats, todayCount) {
|
|
103
77
|
return {
|
|
104
78
|
key: item.code,
|
|
105
79
|
productName: item.name,
|
|
106
80
|
productCode: item.code,
|
|
107
|
-
today:
|
|
108
|
-
yesterday:
|
|
109
|
-
dayBeforeYesterday:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
recent7: summary.recent7.count,
|
|
113
|
-
recent30: summary.recent30.count,
|
|
114
|
-
days: summary.days
|
|
81
|
+
today: todayCount,
|
|
82
|
+
yesterday: stats.yesterday,
|
|
83
|
+
dayBeforeYesterday: stats.dayBeforeYesterday,
|
|
84
|
+
month: stats.month,
|
|
85
|
+
lastMonth: stats.lastMonth
|
|
115
86
|
};
|
|
116
87
|
}
|
|
117
88
|
|
|
118
|
-
async function buildDailyStatsProducts(befly,
|
|
119
|
-
const
|
|
120
|
-
projectLists.map((item) => {
|
|
121
|
-
|
|
89
|
+
async function buildDailyStatsProducts(befly, projectLists, history, dateStats) {
|
|
90
|
+
const products = await Promise.all(
|
|
91
|
+
projectLists.map(async (item) => {
|
|
92
|
+
const stats = history[item.code] || { yesterday: 0, dayBeforeYesterday: 0, month: 0, lastMonth: 0 };
|
|
93
|
+
const todayCount = await getTodayCount(befly, dateStats.reportDate, item.code);
|
|
94
|
+
|
|
95
|
+
return buildProductSummary(item, stats, todayCount);
|
|
122
96
|
})
|
|
123
97
|
);
|
|
124
98
|
|
|
125
|
-
return
|
|
126
|
-
if (b.
|
|
127
|
-
return b.
|
|
99
|
+
return products.toSorted((a, b) => {
|
|
100
|
+
if (b.today !== a.today) {
|
|
101
|
+
return b.today - a.today;
|
|
128
102
|
}
|
|
129
103
|
|
|
130
|
-
return
|
|
104
|
+
return a.productName.localeCompare(b.productName, "zh-CN");
|
|
131
105
|
});
|
|
132
106
|
}
|
|
133
107
|
|
|
108
|
+
function buildFullSummary(stats, dateStats, todayCount) {
|
|
109
|
+
return {
|
|
110
|
+
today: { reportDate: dateStats.reportDate, count: todayCount },
|
|
111
|
+
yesterday: { reportDate: dateStats.yesterdayDate, count: stats.yesterday },
|
|
112
|
+
dayBeforeYesterday: { reportDate: dateStats.dayBeforeYesterdayDate, count: stats.dayBeforeYesterday },
|
|
113
|
+
month: { startDate: dateStats.monthStartDate, endDate: dateStats.yesterdayDate, count: stats.month },
|
|
114
|
+
lastMonth: { startDate: dateStats.previousMonthStartDate, endDate: dateStats.previousMonthEndDate, count: stats.lastMonth }
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
134
118
|
export default {
|
|
135
119
|
name: "获取日在线统计",
|
|
136
120
|
method: "POST",
|
|
@@ -142,36 +126,25 @@ export default {
|
|
|
142
126
|
required: [],
|
|
143
127
|
handler: async (befly, ctx) => {
|
|
144
128
|
const now = Date.now();
|
|
145
|
-
const
|
|
146
|
-
const
|
|
147
|
-
const
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
const recent7DateList = getDailyRecentDateList(now, 7, timeZone);
|
|
151
|
-
const productCode = ctx?.body?.productCode ?? "";
|
|
129
|
+
const reportDate = getTongJiDateYmdNumber(now, befly.config?.tz);
|
|
130
|
+
const monthStartDate = getTongJiMonthStartDate(now, befly.config?.tz);
|
|
131
|
+
const [previousMonthStartDate, previousMonthEndDate] = getPreviousMonthDateRange(reportDate);
|
|
132
|
+
const recentDateList = getTongJiRecentDateList(now, DAILY_STATS_DAY_LIMIT, befly.config?.tz);
|
|
133
|
+
const productCode = ctx.body?.productCode ?? "";
|
|
152
134
|
const projectLists = productCode ? [] : befly.config?.projectLists || [];
|
|
153
|
-
const
|
|
135
|
+
const dateStats = {
|
|
154
136
|
reportDate: reportDate,
|
|
137
|
+
yesterdayDate: recentDateList[recentDateList.length - 2],
|
|
138
|
+
dayBeforeYesterdayDate: recentDateList[recentDateList.length - 3],
|
|
155
139
|
monthStartDate: monthStartDate,
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
productCode: productCode
|
|
140
|
+
previousMonthStartDate: previousMonthStartDate,
|
|
141
|
+
previousMonthEndDate: previousMonthEndDate,
|
|
142
|
+
recentDateList: recentDateList
|
|
160
143
|
};
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
return befly.tool.Yes("获取成功", {
|
|
167
|
-
queryTime: now,
|
|
168
|
-
...summary,
|
|
169
|
-
products: products
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const summary = await getDailyStatsSummary(befly, params);
|
|
174
|
-
const products = await buildDailyStatsProducts(befly, params, projectLists);
|
|
144
|
+
const history = await loadHistory(befly, reportDate, dateStats);
|
|
145
|
+
const todayCount = productCode ? await getTodayCount(befly, reportDate, productCode) : 0;
|
|
146
|
+
const summary = productCode ? buildFullSummary(history[productCode] || { yesterday: 0, dayBeforeYesterday: 0, month: 0, lastMonth: 0 }, dateStats, todayCount) : null;
|
|
147
|
+
const products = await buildDailyStatsProducts(befly, projectLists, history, dateStats);
|
|
175
148
|
|
|
176
149
|
return befly.tool.Yes("获取成功", {
|
|
177
150
|
queryTime: now,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { snakeCase } from "#befly/utils/util.js";
|
|
2
|
+
|
|
3
|
+
import { getTongJiDateYmdNumber, getTongJiMonthStartDate, getTongJiRecentDateList } from "./_tongJi.js";
|
|
4
|
+
|
|
5
|
+
const DISTRIBUTION_DIMENSIONS = ["clientType", "os", "brand", "model", "productVersion"];
|
|
6
|
+
const DAILY_STATS_DISTRIBUTION_DAY_LIMIT = 30;
|
|
7
|
+
|
|
8
|
+
function getDateRangeBounds(dateRange, now, timeZone) {
|
|
9
|
+
const reportDate = getTongJiDateYmdNumber(now, timeZone);
|
|
10
|
+
const recentDateList = getTongJiRecentDateList(now, DAILY_STATS_DISTRIBUTION_DAY_LIMIT, timeZone);
|
|
11
|
+
|
|
12
|
+
if (dateRange === "today") {
|
|
13
|
+
return [reportDate, reportDate];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (dateRange === "yesterday") {
|
|
17
|
+
const yesterdayDate = recentDateList[recentDateList.length - 2];
|
|
18
|
+
|
|
19
|
+
return [yesterdayDate, yesterdayDate];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (dateRange === "dayBeforeYesterday") {
|
|
23
|
+
const dayBeforeYesterdayDate = recentDateList[recentDateList.length - 3];
|
|
24
|
+
|
|
25
|
+
return [dayBeforeYesterdayDate, dayBeforeYesterdayDate];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (dateRange === "recent30") {
|
|
29
|
+
return [recentDateList[0], recentDateList[recentDateList.length - 1]];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const monthStartDate = getTongJiMonthStartDate(now, timeZone);
|
|
33
|
+
|
|
34
|
+
return [monthStartDate, reportDate];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function queryDimensionDistribution(befly, dimension, startDate, endDate, productCode) {
|
|
38
|
+
const snakeField = snakeCase(dimension);
|
|
39
|
+
const result = await befly.mysql.execute(`SELECT ${snakeField} as name, COUNT(*) as count FROM befly_daily_report WHERE state > 0 AND report_date BETWEEN ? AND ? AND product_code = ? GROUP BY ${snakeField} ORDER BY count DESC`, [startDate, endDate, productCode]);
|
|
40
|
+
|
|
41
|
+
return (result.data || []).map((item) => {
|
|
42
|
+
return {
|
|
43
|
+
name: String(item.name || "未知"),
|
|
44
|
+
count: Number(item.count) || 0
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildEmptyDistribution() {
|
|
50
|
+
const result = {};
|
|
51
|
+
|
|
52
|
+
for (const dimension of DISTRIBUTION_DIMENSIONS) {
|
|
53
|
+
result[dimension] = [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export default {
|
|
60
|
+
name: "获取日在线分布统计",
|
|
61
|
+
method: "POST",
|
|
62
|
+
body: "none",
|
|
63
|
+
auth: true,
|
|
64
|
+
fields: {
|
|
65
|
+
productCode: { name: "产品代号", input: "string", min: 1, max: 100 },
|
|
66
|
+
dateRange: { name: "日期范围", input: "enum", check: "today|yesterday|dayBeforeYesterday|recent30|month", min: 1, max: 20 }
|
|
67
|
+
},
|
|
68
|
+
required: ["productCode", "dateRange"],
|
|
69
|
+
handler: async (befly, ctx) => {
|
|
70
|
+
const productCode = ctx.body?.productCode ?? "";
|
|
71
|
+
const dateRange = ctx.body?.dateRange ?? "";
|
|
72
|
+
|
|
73
|
+
if (!befly.mysql) {
|
|
74
|
+
return befly.tool.Yes("获取成功", buildEmptyDistribution());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const [startDate, endDate] = getDateRangeBounds(dateRange, Date.now(), befly.config?.tz);
|
|
78
|
+
const result = {};
|
|
79
|
+
|
|
80
|
+
for (const dimension of DISTRIBUTION_DIMENSIONS) {
|
|
81
|
+
result[dimension] = await queryDimensionDistribution(befly, dimension, startDate, endDate, productCode);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return befly.tool.Yes("获取成功", result);
|
|
85
|
+
}
|
|
86
|
+
};
|
package/apis/tongJi/errorList.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { queryFields } from "#befly/apis/_apis.js";
|
|
2
2
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
3
3
|
|
|
4
|
-
const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate"
|
|
5
|
-
const FILTER_FIELDS = ["errorType", "
|
|
6
|
-
const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "productName", "productCode", "productVersion"
|
|
4
|
+
const NUMBER_FIELDS = ["reportTime", "firstReportTime", "bucketTime", "bucketDate"];
|
|
5
|
+
const FILTER_FIELDS = ["errorType", "productName", "productCode", "productVersion"];
|
|
6
|
+
const KEYWORD_FIELDS = ["pagePath", "pageName", "message", "errorType", "productName", "productCode", "productVersion"];
|
|
7
7
|
|
|
8
8
|
export default {
|
|
9
9
|
name: "获取错误报告列表",
|
|
@@ -13,13 +13,9 @@ export default {
|
|
|
13
13
|
fields: {
|
|
14
14
|
...queryFields,
|
|
15
15
|
errorType: errorReportTable.errorType,
|
|
16
|
-
source: errorReportTable.source,
|
|
17
16
|
productName: errorReportTable.productName,
|
|
18
17
|
productCode: errorReportTable.productCode,
|
|
19
|
-
productVersion: errorReportTable.productVersion
|
|
20
|
-
deviceType: errorReportTable.deviceType,
|
|
21
|
-
browserName: errorReportTable.browserName,
|
|
22
|
-
osName: errorReportTable.osName
|
|
18
|
+
productVersion: errorReportTable.productVersion
|
|
23
19
|
},
|
|
24
20
|
required: [],
|
|
25
21
|
handler: async (befly, ctx) => {
|
|
@@ -65,7 +61,7 @@ export default {
|
|
|
65
61
|
const row = Object.assign({}, item);
|
|
66
62
|
|
|
67
63
|
for (const field of NUMBER_FIELDS) {
|
|
68
|
-
row[field] = Number(item[field]
|
|
64
|
+
row[field] = Number(item[field]) || 0;
|
|
69
65
|
}
|
|
70
66
|
|
|
71
67
|
return row;
|
|
@@ -1,43 +1,27 @@
|
|
|
1
|
+
import { Logger } from "#befly/lib/logger.js";
|
|
1
2
|
import errorReportTable from "#befly/tables/errorReport.json";
|
|
2
3
|
import { getDateYmdNumber, getTimeBucketStart } from "#befly/utils/datetime.js";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
import { expireRedisKeys, getErrorStatsDayBucketCountKey, getErrorStatsDayBucketsKey, getErrorStatsDayTypeCountKey, getErrorStatsDayTypesKey, getErrorStatsPeriodCountKey, getTongJiMonthStartDate, getTongJiWeekStartDate } from "./_tongJi.js";
|
|
4
|
+
import { isValidPositiveInt } from "#befly/utils/is.js";
|
|
6
5
|
|
|
7
6
|
const ERROR_STATS_BUCKET_MS = 30 * 60 * 1000;
|
|
8
|
-
const ERROR_STATS_REDIS_TTL_SECONDS =
|
|
9
|
-
|
|
10
|
-
function getErrorReportUaData(ctx) {
|
|
11
|
-
let userAgent = "";
|
|
7
|
+
const ERROR_STATS_REDIS_TTL_SECONDS = 24 * 60 * 60;
|
|
12
8
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
userAgent = ctx.headers.get("user-agent") || "";
|
|
9
|
+
function extractErrorMessage(message) {
|
|
10
|
+
if (typeof message !== "string" || message.length === 0) {
|
|
11
|
+
return "";
|
|
17
12
|
}
|
|
18
13
|
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function updateErrorStatsRedis(befly, now, bucketDate, bucketTime, errorType) {
|
|
23
|
-
if (!befly.redis) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
14
|
+
const lines = message.split(/\r?\n/);
|
|
26
15
|
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
const safeErrorType = String(errorType || "unknown");
|
|
30
|
-
const dayCountKey = getErrorStatsPeriodCountKey("day", bucketDate);
|
|
31
|
-
const weekCountKey = getErrorStatsPeriodCountKey("week", weekStartDate);
|
|
32
|
-
const monthCountKey = getErrorStatsPeriodCountKey("month", monthStartDate);
|
|
33
|
-
const dayBucketsKey = getErrorStatsDayBucketsKey(bucketDate);
|
|
34
|
-
const dayBucketCountKey = getErrorStatsDayBucketCountKey(bucketDate, bucketTime);
|
|
35
|
-
const dayTypesKey = getErrorStatsDayTypesKey(bucketDate);
|
|
36
|
-
const dayTypeCountKey = getErrorStatsDayTypeCountKey(bucketDate, safeErrorType);
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
const trimmed = line.trim();
|
|
37
18
|
|
|
38
|
-
|
|
19
|
+
if (trimmed.length > 0) {
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
39
23
|
|
|
40
|
-
|
|
24
|
+
return "";
|
|
41
25
|
}
|
|
42
26
|
|
|
43
27
|
export default {
|
|
@@ -46,9 +30,10 @@ export default {
|
|
|
46
30
|
body: "none",
|
|
47
31
|
auth: false,
|
|
48
32
|
fields: {
|
|
33
|
+
clientType: errorReportTable.clientType,
|
|
34
|
+
clientId: errorReportTable.clientId,
|
|
49
35
|
pagePath: errorReportTable.pagePath,
|
|
50
36
|
pageName: errorReportTable.pageName,
|
|
51
|
-
source: errorReportTable.source,
|
|
52
37
|
productName: errorReportTable.productName,
|
|
53
38
|
productCode: errorReportTable.productCode,
|
|
54
39
|
productVersion: errorReportTable.productVersion,
|
|
@@ -56,33 +41,85 @@ export default {
|
|
|
56
41
|
message: errorReportTable.message,
|
|
57
42
|
detail: errorReportTable.detail
|
|
58
43
|
},
|
|
59
|
-
required: [],
|
|
44
|
+
required: ["clientType"],
|
|
60
45
|
handler: async (befly, ctx) => {
|
|
61
46
|
const now = Date.now();
|
|
62
47
|
const bucketTime = getTimeBucketStart(now, ERROR_STATS_BUCKET_MS);
|
|
63
48
|
const bucketDate = getDateYmdNumber(now);
|
|
64
|
-
const uaData = getErrorReportUaData(ctx);
|
|
65
49
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
50
|
+
const body = ctx.body || {};
|
|
51
|
+
const productCode = body.productCode || "";
|
|
52
|
+
const rawMessage = body.message || "";
|
|
53
|
+
const extractedMessage = extractErrorMessage(rawMessage);
|
|
54
|
+
|
|
55
|
+
let memberMeta;
|
|
56
|
+
|
|
57
|
+
if (isValidPositiveInt(ctx?.userId)) {
|
|
58
|
+
memberMeta = { type: "user", value: `user:${ctx.userId}:${productCode}`, userId: ctx.userId };
|
|
59
|
+
} else {
|
|
60
|
+
const clientId = body.clientId || ctx?.ip || "";
|
|
61
|
+
|
|
62
|
+
if (clientId) {
|
|
63
|
+
memberMeta = { type: "client", value: `client:${clientId}:${productCode}`, clientId: clientId };
|
|
64
|
+
} else {
|
|
65
|
+
memberMeta = { type: "ip", value: `ip:${ctx?.ip || "unknown"}:${productCode}`, ip: ctx?.ip || "unknown" };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const dedupMember = [body.errorType, extractedMessage, productCode, memberMeta.value].map((item) => encodeURIComponent(String(item || ""))).join(":");
|
|
70
|
+
let first = false;
|
|
71
|
+
|
|
72
|
+
if (befly.redis) {
|
|
73
|
+
const key = `error:day:${bucketDate}:dedup:${productCode}`;
|
|
74
|
+
const added = await befly.redis.sadd(key, [dedupMember]);
|
|
75
|
+
|
|
76
|
+
if (added > 0) {
|
|
77
|
+
await befly.redis.expire(key, ERROR_STATS_REDIS_TTL_SECONDS);
|
|
78
|
+
first = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let stored = false;
|
|
83
|
+
|
|
84
|
+
if (first && befly.mysql) {
|
|
85
|
+
const userId = isValidPositiveInt(ctx.userId) ? ctx.userId : null;
|
|
86
|
+
|
|
87
|
+
const data = {
|
|
88
|
+
clientType: body.clientType,
|
|
89
|
+
clientId: memberMeta.clientId || body.clientId || null,
|
|
90
|
+
pagePath: body.pagePath || null,
|
|
91
|
+
pageName: body.pageName || null,
|
|
92
|
+
productName: body.productName || null,
|
|
93
|
+
productCode: body.productCode || null,
|
|
94
|
+
productVersion: body.productVersion || null,
|
|
95
|
+
errorType: body.errorType || null,
|
|
96
|
+
message: rawMessage || null,
|
|
97
|
+
extractedMessage: extractedMessage || null,
|
|
98
|
+
detail: body.detail || null,
|
|
99
|
+
userId: userId,
|
|
100
|
+
memberType: memberMeta.type,
|
|
101
|
+
member: memberMeta.value,
|
|
102
|
+
ip: ctx?.ip || null,
|
|
71
103
|
reportTime: now,
|
|
72
104
|
firstReportTime: now,
|
|
105
|
+
lastReportTime: now,
|
|
73
106
|
bucketTime: bucketTime,
|
|
74
|
-
bucketDate: bucketDate
|
|
75
|
-
|
|
76
|
-
}
|
|
77
|
-
});
|
|
107
|
+
bucketDate: bucketDate
|
|
108
|
+
};
|
|
78
109
|
|
|
79
|
-
|
|
110
|
+
try {
|
|
111
|
+
await befly.mysql.insData({ table: "errorReport", data: data });
|
|
112
|
+
stored = true;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
Logger.error("errorReport 插入失败", error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
80
117
|
|
|
81
118
|
return befly.tool.Yes("上报成功", {
|
|
82
119
|
reportTime: now,
|
|
83
120
|
bucketTime: bucketTime,
|
|
84
121
|
bucketDate: bucketDate,
|
|
85
|
-
stored:
|
|
122
|
+
stored: stored
|
|
86
123
|
});
|
|
87
124
|
}
|
|
88
125
|
};
|