sobey-monitor-sdk 1.1.7 → 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.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
  * 性能监控配置
@@ -240,7 +258,7 @@ interface BehaviorData extends BaseData {
240
258
  */
241
259
  interface BreadCrumb {
242
260
  /** 类型 */
243
- type: 'route' | 'click' | 'request' | 'console' | 'input' | 'custom' | 'framework';
261
+ type: 'route' | 'click' | 'request' | 'console' | 'input' | 'custom' | 'framework' | 'resource';
244
262
  /** 分类 */
245
263
  category: string;
246
264
  /** 数据 */
package/dist/index.esm.js CHANGED
@@ -731,11 +731,12 @@ function installResourceErrorHandler() {
731
731
  };
732
732
  // 添加到面包屑
733
733
  context.addBreadcrumb({
734
- type: 'request',
734
+ type: 'resource',
735
735
  category: 'resource_error',
736
736
  data: {
737
737
  tagName,
738
- resourceUrl,
738
+ url: resourceUrl,
739
+ error: `Failed to load ${tagName}`,
739
740
  },
740
741
  });
741
742
  reporter.reportError(errorData);
@@ -770,6 +771,79 @@ function isSdkRequest(url) {
770
771
  return false;
771
772
  return url.includes(cfg.dsn);
772
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
+ }
773
847
  function interceptXHR() {
774
848
  XMLHttpRequest.prototype.open = function (method, url, async = true, username, password) {
775
849
  const urlStr = url.toString();
@@ -821,6 +895,8 @@ function interceptXHR() {
821
895
  },
822
896
  });
823
897
  }
898
+ const responseBody = this.responseText?.substring(0, 1000);
899
+ // HTTP 错误或业务错误
824
900
  if (status === 0 || status >= 400) {
825
901
  reportHttpError({
826
902
  method: monitorData.method,
@@ -828,9 +904,20 @@ function interceptXHR() {
828
904
  status,
829
905
  duration,
830
906
  requestBody: monitorData.requestBody,
831
- responseBody: this.responseText?.substring(0, 1000), // 限制长度
907
+ responseBody,
832
908
  });
833
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
+ }
834
921
  });
835
922
  return originalXHRSend.call(this, body);
836
923
  };
@@ -872,15 +959,16 @@ function interceptFetch() {
872
959
  },
873
960
  });
874
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 { }
875
970
  if (!response.ok) {
876
- // 克隆响应以读取 body
877
- const cloned = response.clone();
878
- let responseBody;
879
- try {
880
- responseBody = await cloned.text();
881
- responseBody = responseBody.substring(0, 1000);
882
- }
883
- catch { }
971
+ // HTTP 错误
884
972
  reportHttpError({
885
973
  method,
886
974
  url,
@@ -890,6 +978,17 @@ function interceptFetch() {
890
978
  responseBody,
891
979
  });
892
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
+ }
893
992
  return response;
894
993
  }
895
994
  catch (error) {
@@ -925,10 +1024,13 @@ function interceptFetch() {
925
1024
  /**
926
1025
  * 上报 HTTP 错误
927
1026
  */
928
- 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}`;
929
1031
  const errorData = {
930
1032
  type: 'http_error',
931
- message: `HTTP ${httpInfo.status} ${httpInfo.method} ${httpInfo.url}`,
1033
+ message,
932
1034
  httpInfo,
933
1035
  };
934
1036
  reporter.reportError(errorData);