logger.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. import { store } from "@/store/store";
  2. import moment from "moment";
  3. import { isNil, omit } from "lodash-es";
  4. import SlsWebLogger from "js-sls-logger";
  5. import { VITE_SLS_STORE_NAME } from "@/constants/constants";
  6. import { electronLog } from "./electronLog";
  7. import getDeviceInfos from "./deviceInfo";
  8. import { isElectron } from "./nativeMethods";
  9. const aliLogger = new SlsWebLogger({
  10. host: "cn-shenzhen.log.aliyuncs.com",
  11. project: "examcloud",
  12. logstore: VITE_SLS_STORE_NAME,
  13. });
  14. type LogDetail = {
  15. /** default log */
  16. lvl?: "debug" | "log" | "warn" | "error";
  17. /** channels. 往哪些渠道放日志? default: ['console'] */
  18. cnl: ("console" | "local" | "server" | "bd")[];
  19. /** 操作的类型,方便在日志里面查找相同类型的操作 */
  20. key?: string;
  21. /** page name */
  22. pgn?: string;
  23. /** page url: AUTO => 自动从location.path获取 */
  24. pgu?: "AUTO" | `/${string}`;
  25. /** action 引发本次日志的操作 */
  26. act?: string;
  27. /** statck 错误信息的stack,单条错误信息也放此处 */
  28. stk?: string;
  29. /** detail 错误详细信息 */
  30. dtl?: string;
  31. /** api url */
  32. aul?: string;
  33. /** api status */
  34. aus?: string;
  35. /** error json */
  36. ejn?: string;
  37. /** error name */
  38. ename?: string;
  39. /** error message */
  40. emsg?: string;
  41. /** 可能是个Error对象,如果是Error,这取出标准字段,否则仅转为JSON */
  42. possibleError?: any;
  43. /** 扩展字段的集合。TODO: 提示不允许出去前面的字段 */
  44. // "not in keyof T" - exclude keyof T from string #42315
  45. // https://github.com/microsoft/TypeScript/issues/42315
  46. ext?: Record<string, any>;
  47. };
  48. /** 记录重要日志到多个source
  49. * 示例:
  50. * @param detail.key - '本地人脸比对'
  51. * @param detail.pgn - '登录页面' | '在线考试列表页面'
  52. * @param detail.act - '点击登录按钮'
  53. * @param detail.cnl - ['local', 'console']
  54. * @param detail.stk - error.stack
  55. * @param detail.dtl - '第一次点击登录' | '第二次点击登录' // 方便查询
  56. * @param detail.aul - '/api/login'
  57. * @param detail.aus - '500'
  58. * @param detail.ejn - JSON.stringify({a: 0})
  59. * @param detail.ename - error.name
  60. * @param detail.emsg - error.message
  61. * @param detail.possibleError - possibleError
  62. * @param detail.ext - {UA: 'chrome 99'}
  63. */
  64. export default function createLog(detail: LogDetail) {
  65. const user = store.user;
  66. const defaultFileds = {
  67. /** 供灰度发布识别日志 */
  68. __ver: "3.0.0",
  69. lvl: "log",
  70. uuidForEcs: localStorage.getItem("uuidForEcs"),
  71. clientDate: moment().format("YYYY-MM-DD HH:mm:ss.SSS"),
  72. ...(user?.id ? { userId: user.id } : {}),
  73. ...(detail?.cnl ? { cnl: detail.cnl } : { cnl: ["console"] }),
  74. };
  75. let possibleErrorFields = {};
  76. if (detail.possibleError instanceof Error) {
  77. possibleErrorFields = {
  78. ejn: JSON.stringify(detail.possibleError),
  79. ename: detail.possibleError.name,
  80. emsg: detail.possibleError.message,
  81. stk: detail.possibleError.stack,
  82. };
  83. } else if (!isNil(detail.possibleError)) {
  84. possibleErrorFields = { ejn: JSON.stringify(detail.possibleError) };
  85. }
  86. const newDetail: Record<string, string> = Object.assign(
  87. defaultFileds,
  88. omit(detail, "ext"),
  89. detail.ext,
  90. detail.pgu === "AUTO"
  91. ? { pgu: location.href.replace(location.origin, "") }
  92. : {},
  93. possibleErrorFields
  94. );
  95. // FIXME: 后期设置条件开启非log级别的日志,此时全部打回。
  96. // 可考虑阿里云设置一个永久的URL,URL的内容可定制,是一串逗号分割的userId,用于开启这些用户的debug级别日志
  97. if (import.meta.env.PROD && newDetail.lvl !== "log") {
  98. return;
  99. }
  100. // 开发阶段将日志全部打印出来
  101. if (import.meta.env.DEV && !detail.cnl.includes("console")) {
  102. detail.cnl.push("console");
  103. }
  104. if (detail.cnl?.includes("console")) {
  105. if (import.meta.env.DEV) {
  106. console.log(
  107. omit(newDetail, ["__ver", "cnl", "lvl", "uuidForEcs", "clientDate"])
  108. );
  109. } else {
  110. console.log(newDetail);
  111. }
  112. }
  113. if (!import.meta.env.DEV && detail.cnl?.includes("server")) {
  114. aliLogger.send(newDetail);
  115. }
  116. if (detail.cnl?.includes("local") && electronLog) {
  117. void electronLog(newDetail);
  118. }
  119. }
  120. /** 要在用户登录后调用,仅需调用一次 */
  121. export function createUserDetailLog() {
  122. const ext: Record<string, any> = {};
  123. const user = store.user;
  124. const deviceInfos = getDeviceInfos();
  125. const { displayName, identityNumber, rootOrgName, rootOrgId } = store.user;
  126. Object.assign(
  127. ext,
  128. { displayName, identityNumber, rootOrgName, rootOrgId },
  129. { userStudentCodeList: user.studentCodeList.join(",") },
  130. { UA: navigator.userAgent },
  131. deviceInfos
  132. );
  133. createLog({ cnl: ["server"], pgn: "登录页面", act: "登录成功日志", ext });
  134. }
  135. export function createEncryptLog() {
  136. // 非 electron 返回
  137. if (!isElectron()) return;
  138. try {
  139. const uuidForEcs = localStorage.getItem("uuidForEcs");
  140. // 没有 uuidForEcs 日志没法查询
  141. if (!uuidForEcs) return;
  142. let log = null;
  143. let lastLogIndex: number = +(localStorage.getItem("lastLogIndex") || 0);
  144. log = window.nodeRequire("electron-log");
  145. // const filePath = log.getFile().path;
  146. // eslint-disable-next-line @typescript-eslint/no-unsafe-call
  147. const filePath: string = log.transports.file.findLogPath();
  148. const fs: typeof import("fs") = window.nodeRequire("fs");
  149. const content = fs.readFileSync(filePath, "utf-8");
  150. const ary = content.toString().split("\r\n").join("\n").split("\n");
  151. // 重复上传没有时间的行:跨过非时间戳的行; 错误识别:全部重新执行
  152. // let lastIndex = ary.findIndex(v => v === lastLog);
  153. // console.log({ lastIndex });
  154. if (ary.length < lastLogIndex) {
  155. lastLogIndex = 0;
  156. }
  157. const logLen = 10;
  158. const newAry = ary
  159. .slice(lastLogIndex, lastLogIndex + logLen)
  160. .filter((v) => v);
  161. // 如果没有上传的内容,则不修改lastLog, 也不上传
  162. if (!newAry.length) return;
  163. lastLogIndex = lastLogIndex + newAry.length;
  164. localStorage.setItem("lastLogIndex", lastLogIndex + "");
  165. createLog({ cnl: ["server"], ext: { encryptLog: newAry.join("\n") } });
  166. } catch (error) {
  167. console.debug(error);
  168. return;
  169. }
  170. }
  171. /** 获得页面的dimension
  172. * @argument pgn - 页面名称
  173. */
  174. export function dimensionLog(pgn: string) {
  175. logger({
  176. cnl: ["server", "local"],
  177. pgn,
  178. ext: {
  179. scrollX: window.scrollX,
  180. scrollY: window.scrollY,
  181. width: window.screen.width,
  182. height: window.screen.height,
  183. screenX: window.screen.availWidth,
  184. screenY: window.screen.availHeight,
  185. clientWidth: document.documentElement.clientWidth,
  186. clientHeight: document.documentElement.clientHeight,
  187. windowInnerWidth: window.innerWidth,
  188. windowInnerHeight: window.innerHeight,
  189. windowOuterWidth: window.outerWidth,
  190. windowOuterHeight: window.outerHeight,
  191. // 是否全屏
  192. equal1:
  193. "dimesion1" +
  194. (window.screen.width === window.outerWidth &&
  195. window.screen.height === window.outerHeight),
  196. // 是否打开了调试窗口
  197. equal2:
  198. "dimesion2" +
  199. (window.innerWidth === window.outerWidth &&
  200. window.innerHeight === window.outerHeight),
  201. },
  202. });
  203. }