sobey-monitor-sdk 1.1.8 → 1.1.9
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/index.cjs.js +112 -11
- package/dist/index.cjs.js.map +1 -1
- package/dist/index.d.ts +18 -0
- package/dist/index.esm.js +112 -11
- package/dist/index.esm.js.map +1 -1
- package/dist/index.umd.js +112 -11
- package/dist/index.umd.js.map +1 -1
- package/dist/types/index.d.ts +18 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -70,6 +70,18 @@ interface ReportConfig {
|
|
|
70
70
|
/** 请求超时(ms) */
|
|
71
71
|
timeout?: number;
|
|
72
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* 业务错误检测规则
|
|
75
|
+
* 用于检测 HTTP 200 响应中的业务层错误
|
|
76
|
+
*/
|
|
77
|
+
interface BusinessErrorRule {
|
|
78
|
+
/** 字段路径,如 "status" 或 "data.code" */
|
|
79
|
+
field: string;
|
|
80
|
+
/** 比较操作符 */
|
|
81
|
+
operator: 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'exists';
|
|
82
|
+
/** 比较值(当 operator 为 'exists' 时可不填) */
|
|
83
|
+
value?: string | number | boolean;
|
|
84
|
+
}
|
|
73
85
|
/**
|
|
74
86
|
* 错误监控配置
|
|
75
87
|
*/
|
|
@@ -86,6 +98,12 @@ interface ErrorMonitorConfig {
|
|
|
86
98
|
httpError?: boolean;
|
|
87
99
|
/** 错误过滤函数 */
|
|
88
100
|
filter?: (error: ErrorData) => boolean;
|
|
101
|
+
/**
|
|
102
|
+
* 业务错误检测规则
|
|
103
|
+
* 当 HTTP 状态码为 200 时,根据这些规则检测响应体中的业务错误
|
|
104
|
+
* 任一规则匹配时将上报 http_error
|
|
105
|
+
*/
|
|
106
|
+
businessErrorRules?: BusinessErrorRule[];
|
|
89
107
|
}
|
|
90
108
|
/**
|
|
91
109
|
* 性能监控配置
|
package/dist/index.esm.js
CHANGED
|
@@ -771,6 +771,79 @@ function isSdkRequest(url) {
|
|
|
771
771
|
return false;
|
|
772
772
|
return url.includes(cfg.dsn);
|
|
773
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* 获取嵌套对象的字段值
|
|
776
|
+
* @param obj 对象
|
|
777
|
+
* @param path 字段路径,如 "status" 或 "data.code"
|
|
778
|
+
*/
|
|
779
|
+
function getNestedValue(obj, path) {
|
|
780
|
+
if (!obj || typeof obj !== 'object')
|
|
781
|
+
return undefined;
|
|
782
|
+
const keys = path.split('.');
|
|
783
|
+
let value = obj;
|
|
784
|
+
for (const key of keys) {
|
|
785
|
+
if (value === null || value === undefined)
|
|
786
|
+
return undefined;
|
|
787
|
+
value = value[key];
|
|
788
|
+
}
|
|
789
|
+
return value;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* 检查单条规则是否匹配
|
|
793
|
+
*/
|
|
794
|
+
function matchRule(data, rule) {
|
|
795
|
+
const value = getNestedValue(data, rule.field);
|
|
796
|
+
switch (rule.operator) {
|
|
797
|
+
case 'exists':
|
|
798
|
+
return value !== undefined && value !== null;
|
|
799
|
+
case 'eq':
|
|
800
|
+
return value === rule.value;
|
|
801
|
+
case 'ne':
|
|
802
|
+
return value !== rule.value;
|
|
803
|
+
case 'gt':
|
|
804
|
+
return typeof value === 'number' && typeof rule.value === 'number' && value > rule.value;
|
|
805
|
+
case 'gte':
|
|
806
|
+
return typeof value === 'number' && typeof rule.value === 'number' && value >= rule.value;
|
|
807
|
+
case 'lt':
|
|
808
|
+
return typeof value === 'number' && typeof rule.value === 'number' && value < rule.value;
|
|
809
|
+
case 'lte':
|
|
810
|
+
return typeof value === 'number' && typeof rule.value === 'number' && value <= rule.value;
|
|
811
|
+
default:
|
|
812
|
+
return false;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/**
|
|
816
|
+
* 检测业务错误
|
|
817
|
+
* @param responseBody 响应体字符串
|
|
818
|
+
* @returns 是否为业务错误
|
|
819
|
+
*/
|
|
820
|
+
function detectBusinessError(responseBody) {
|
|
821
|
+
if (!responseBody)
|
|
822
|
+
return false;
|
|
823
|
+
if (!config.isInitialized())
|
|
824
|
+
return false;
|
|
825
|
+
const cfg = config.get();
|
|
826
|
+
const rules = cfg.error?.businessErrorRules;
|
|
827
|
+
if (!rules || rules.length === 0)
|
|
828
|
+
return false;
|
|
829
|
+
try {
|
|
830
|
+
const data = JSON.parse(responseBody);
|
|
831
|
+
// 任一规则匹配时返回 true
|
|
832
|
+
for (const rule of rules) {
|
|
833
|
+
if (matchRule(data, rule)) {
|
|
834
|
+
if (cfg.debug) {
|
|
835
|
+
console.log('[Monitor] Business error detected by rule:', rule, 'data:', data);
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
catch {
|
|
843
|
+
// JSON 解析失败,不是业务错误
|
|
844
|
+
return false;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
774
847
|
function interceptXHR() {
|
|
775
848
|
XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
|
|
776
849
|
const urlStr = url.toString();
|
|
@@ -822,6 +895,8 @@ function interceptXHR() {
|
|
|
822
895
|
},
|
|
823
896
|
});
|
|
824
897
|
}
|
|
898
|
+
const responseBody = this.responseText?.substring(0, 1000);
|
|
899
|
+
// HTTP 错误或业务错误
|
|
825
900
|
if (status === 0 || status >= 400) {
|
|
826
901
|
reportHttpError({
|
|
827
902
|
method: monitorData.method,
|
|
@@ -829,9 +904,20 @@ function interceptXHR() {
|
|
|
829
904
|
status,
|
|
830
905
|
duration,
|
|
831
906
|
requestBody: monitorData.requestBody,
|
|
832
|
-
responseBody
|
|
907
|
+
responseBody,
|
|
833
908
|
});
|
|
834
909
|
}
|
|
910
|
+
else if (status >= 200 && status < 300 && detectBusinessError(responseBody)) {
|
|
911
|
+
// HTTP 200 但检测到业务错误
|
|
912
|
+
reportHttpError({
|
|
913
|
+
method: monitorData.method,
|
|
914
|
+
url: monitorData.url,
|
|
915
|
+
status,
|
|
916
|
+
duration,
|
|
917
|
+
requestBody: monitorData.requestBody,
|
|
918
|
+
responseBody,
|
|
919
|
+
}, true);
|
|
920
|
+
}
|
|
835
921
|
});
|
|
836
922
|
return originalXHRSend.call(this, body);
|
|
837
923
|
};
|
|
@@ -873,15 +959,16 @@ function interceptFetch() {
|
|
|
873
959
|
},
|
|
874
960
|
});
|
|
875
961
|
}
|
|
962
|
+
// 克隆响应以读取 body(无论成功与否都需要,用于业务错误检测)
|
|
963
|
+
const cloned = response.clone();
|
|
964
|
+
let responseBody;
|
|
965
|
+
try {
|
|
966
|
+
responseBody = await cloned.text();
|
|
967
|
+
responseBody = responseBody.substring(0, 1000);
|
|
968
|
+
}
|
|
969
|
+
catch { }
|
|
876
970
|
if (!response.ok) {
|
|
877
|
-
//
|
|
878
|
-
const cloned = response.clone();
|
|
879
|
-
let responseBody;
|
|
880
|
-
try {
|
|
881
|
-
responseBody = await cloned.text();
|
|
882
|
-
responseBody = responseBody.substring(0, 1000);
|
|
883
|
-
}
|
|
884
|
-
catch { }
|
|
971
|
+
// HTTP 错误
|
|
885
972
|
reportHttpError({
|
|
886
973
|
method,
|
|
887
974
|
url,
|
|
@@ -891,6 +978,17 @@ function interceptFetch() {
|
|
|
891
978
|
responseBody,
|
|
892
979
|
});
|
|
893
980
|
}
|
|
981
|
+
else if (detectBusinessError(responseBody)) {
|
|
982
|
+
// HTTP 200 但检测到业务错误
|
|
983
|
+
reportHttpError({
|
|
984
|
+
method,
|
|
985
|
+
url,
|
|
986
|
+
status,
|
|
987
|
+
duration,
|
|
988
|
+
requestBody,
|
|
989
|
+
responseBody,
|
|
990
|
+
}, true);
|
|
991
|
+
}
|
|
894
992
|
return response;
|
|
895
993
|
}
|
|
896
994
|
catch (error) {
|
|
@@ -926,10 +1024,13 @@ function interceptFetch() {
|
|
|
926
1024
|
/**
|
|
927
1025
|
* 上报 HTTP 错误
|
|
928
1026
|
*/
|
|
929
|
-
function reportHttpError(httpInfo) {
|
|
1027
|
+
function reportHttpError(httpInfo, isBusinessError = false) {
|
|
1028
|
+
const message = isBusinessError
|
|
1029
|
+
? `Business Error ${httpInfo.method} ${httpInfo.url}`
|
|
1030
|
+
: `HTTP ${httpInfo.status} ${httpInfo.method} ${httpInfo.url}`;
|
|
930
1031
|
const errorData = {
|
|
931
1032
|
type: 'http_error',
|
|
932
|
-
message
|
|
1033
|
+
message,
|
|
933
1034
|
httpInfo,
|
|
934
1035
|
};
|
|
935
1036
|
reporter.reportError(errorData);
|