(共556篇)
全部分类

前端埋点案例
[ 未分类 ] 

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271

import { openDB } from 'idb';
import axios from 'axios';
import { nanoid } from 'nanoid';
import CryptoJS from 'crypto-js';
import Cookie from 'js-cookie';

function debounce(fn, duration = 1000) {
  let timer = null;
  return function() {
    if (!timer) {
      fn.call(this, ...arguments);
      timer = setTimeout(() => {
        clearTimeout(timer);
        timer = null;
      }, duration);
    } else {
      clearTimeout(timer);
      timer = setTimeout(() => {
        clearTimeout(timer);
        timer = null;
      }, duration);
    }
  };
}

const http = axios.create({
  timeout: 1000 * 10,
  withCredentials: true,
});

// http.get(process.env.IP_URL).then(res => {
//   console.log(res);
// });

export class Logger {
  dbName = '';
  dbVersion = 2;
  postUrl = '';
  db = null;
  cacheLogs = [];
  cacheErrors = [];

  // 日志链id, 每次产生error级别日志时更新
  traceId = Cookie.get('LOGGER_TRACE_ID') || this.nanoid();

  upload = debounce(function(all) {
    this._upload(all);
  });

  nanoid() {
    const traceId = nanoid();
    Cookie.set('LOGGER_TRACE_ID', traceId);
    return traceId;
  }

  constructor({
    dbName,
    dbVersion = 1,
    postUrl,
    debug = false,
    expires = 2,
    uploadOnInit = true,
    encKey = '',
    encIv = '',
  }) {
    // nuxt项目中的服务端, 暂不允许初始化实例
    if (process.server) {
      return;
    }
    if (!window.indexedDB) {
      console.warn('IndexedDB not supported');
    }
    this.dbName = dbName;
    this.dbVersion = dbVersion;
    this.postUrl = postUrl;
    this.debug = debug;
    this.expires = expires;
    this.uploadOnInit = uploadOnInit;
    this.encKey = encKey;
    this.encIv = encIv;

    // 加载后直接执行一次提交事件
    // this.upload();

    // 清理超期的日志, 必须等待error日后提交之后再清理
    // this.clear();
  }

  nano() {
    let od = 0;
    let lts = new Date().getTime();
    let md = 1; // 机器码
    function hash() {
      const cts = new Date().getTime();
      if (cts < lts) {
        md = 999;
      }
      if (cts > lts) {
        if (md === 999) {
          md = 0;
        }
        od = 0;
      }
      if (cts === lts) {
        od++;
      }
      // 时间戳保存
      lts = cts;

      // 时间戳13位
      // 机器码3位, 900以上的机器码预留给时间回拨处理
      // 序列码4位, 相当于集群中每毫秒最多产生机器数量*9999个id
      // 每个机器每毫秒可以产生9999个id

      return `${cts - parseInt('16'.padEnd(13, '0'))}${(md + '').padStart(
        3,
        '0',
      )}${(od + '').padStart(4, '0')}`;
    }
    return hash();
  }

  open() {
    return openDB(this.dbName, this.dbVersion, {
      upgrade: (db, oldVersion, newVersion, transaction, event) => {
        this.db = db;
        if (db.objectStoreNames.contains('logs')) {
          db.deleteObjectStore('logs');
        }
        db.createObjectStore('logs', {
          keyPath: 'id',
        });
      },
    });
  }

  encode(data) {
    if (!this.encKey || !this.encIv) {
      console.warn(
        'encKey or encIv was empty, this will send unsafe data to your server',
      );
      return data;
    }
    const ciphertext = CryptoJS.AES.encrypt(JSON.stringify(data), this.encKey, {
      iv: this.encIv,
      mode: CryptoJS.mode.CBC,
      padding: CryptoJS.pad.Pkcs7,
    });
    return ciphertext.toString();
  }

  async _upload(all = true) {
    // 所有error级别的信息, 要配合日志链上报
    // 一次性级别的日志, 单独上报
    try {
      const store = (await this.open('logs'))
        .transaction('logs', 'readonly')
        .objectStore('logs');

      const allLogs = await store.getAll();

      // 先查找一级节点
      const nodeLogs = allLogs
        .filter(item => item.up && !item.on)
        .map(item => item.td);

      // 查找一级节点的日志链
      const upLogs = nodeLogs.length
        ? allLogs.filter(item => nodeLogs.includes(item.td))
        : [];

      // 查找一次性日志
      const onceLogs = allLogs.filter(item => item.on);

      console.log(
        `上报日志,普通日志${upLogs.length}条,强制${onceLogs.length}条`,
      );

      this.http([...upLogs, ...onceLogs]);
    } catch (error) {
      console.log('error', error);
    }
  }

  async http(logs) {
    if (!this.postUrl) {
      console.warn('logs post url not found', this);
      return;
    }
    try {
      // 压缩日志
      // const compress = pako.deflate(
      //   JSON.stringify([...errLogs, ...normalLogs]),
      // );
      // console.log(compress);

      await http.post(this.postUrl, {
        // list: this.encode(logs),
        list: logs,
      });

      // 上报成功要删除记录
      const store = (await this.open('logs'))
        .transaction('logs', 'readwrite')
        .objectStore('logs');
      logs.forEach(item => {
        store.delete(item.id);
      });

      // 每次上报都检查一下有没有过期的
      this.clear();
    } catch (error) {
      // 上报失败
      console.error('http error', error);
    }
  }

  async clear() {
    try {
      const store = (await this.open('logs'))
        .transaction('logs', 'readwrite')
        .objectStore('logs');
      const allLogs = await store.getAll();
      // 清理指定周期之前的旧日志
      allLogs
        .filter(
          item =>
            item.ts < new Date().getTime() - this.expires * 24 * 60 * 60 * 1000,
        )
        .forEach(item => {
          store.delete(item.id);
        });
    } catch (error) {
      console.log(error);
    }
  }

  // 如果有强制上传的日志, 必须有强制上传的标记, 用于区分是否查找同一跟踪链的日志
  async log(data, upload = false, once = false) {
    if (process.server) {
      return;
    }

    try {
      const store = (await this.open('logs'))
        .transaction('logs', 'readwrite')
        .objectStore('logs');
      const obj = {
        id: nanoid(),
        ts: new Date().getTime(), // timestamp
        td: this.traceId, // traceId
        ct: data, // content
        up: upload, // 标记上传节点
        on: once,
      };
      await store.add(obj);

      if (upload) {
        // 如果 需要上传, 先更新跟踪ID, 再上传
        this.traceId = this.nanoid();
        // this.upload中必须上传所有的error级别日志, 最重要的原因是要补偿某次提交失败的情况
        this.upload();
      }
    } catch (error) {
      console.log('errs error', data, error);
    }
  }
}

```