@yqg/api-metrics
Advanced tools
+1
-1
| { | ||
| "name": "@yqg/api-metrics", | ||
| "version": "1.0.0", | ||
| "version": "1.0.1", | ||
| "description": "YQG ApiMetrics", | ||
@@ -5,0 +5,0 @@ "main": "dist/index.js", |
| /* | ||
| * @Author: ruiwang | ||
| * @Date: 2024-08-15 17:20:14 | ||
| * @Last Modified by: ruiwang | ||
| * @Last Modified time: 2024-08-15 17:21:07 | ||
| */ | ||
| export const CountryMap = { | ||
| 'prod': 'CN', | ||
| 'test': 'CN', | ||
| 'prod-indo': 'IDN', | ||
| 'prod-mex-huawei': 'MEX', | ||
| 'prod-phi': 'PHI', | ||
| 'prod-sea': 'THA', | ||
| 'prod-eu': 'POL', | ||
| 'prod-indo-rta': 'IDN', | ||
| 'prod-hill': 'IDN', | ||
| 'test-indo': 'IDN', | ||
| 'test-mex': 'MEX', | ||
| 'test-sea': 'PHI', | ||
| 'test-eu': 'POL' | ||
| }; |
-237
| /* | ||
| * @Author: ruiwang | ||
| * @Date: 2024-08-08 14:39:30 | ||
| * @Last Modified by: ruiwang | ||
| * @Last Modified time: 2024-12-03 14:33:06 | ||
| */ | ||
| import {CountryMap} from './constant/country'; | ||
| import MetricReporter from './log-metrics'; | ||
| type Stage = keyof (typeof CountryMap); | ||
| interface Metric { | ||
| metricsType: string; | ||
| ua: string; | ||
| referer?: string; | ||
| url: string; | ||
| spans: number; | ||
| recevieTime?: number; | ||
| timeToFetch?: number; | ||
| requestTime?: number; | ||
| waitTime?: number; | ||
| compressed?: boolean; | ||
| startTime?: number; | ||
| fetchStart?: number; | ||
| requestStart?: number; | ||
| responseStart?: number; | ||
| responseEnd?: number; | ||
| status?: string; | ||
| encodedBodySize?: number; | ||
| decodedBodySize?: number; | ||
| transferSize?: number; | ||
| nextHopProtocol?: string; | ||
| params?: any; | ||
| } | ||
| interface Options { | ||
| appId: string; | ||
| stage: Stage; | ||
| size?: number; // 单次上报最大数据量,默认 20 | ||
| time?: number; // 轮询上报时间间隔,默认 1000ms | ||
| limitRate?: number; // 采样率,默认 0.1 | ||
| isProd?: boolean; | ||
| ignoreList?: string[]; // 配置不上报的接口 | ||
| generateReferer?: any; // 获取referrer 的方法 | ||
| } | ||
| const getUA = () => { | ||
| try { | ||
| return (navigator as any).userAgent; | ||
| } catch { | ||
| return ''; | ||
| } | ||
| }; | ||
| const generateReferer = () => { | ||
| try { | ||
| return (location as any).href; | ||
| } catch (e) { | ||
| return ''; | ||
| } | ||
| }; | ||
| export default class ApiMetrics { | ||
| static instance: any; | ||
| options: Options | undefined; | ||
| apiMetricsInterceptor: any; | ||
| apiMetricsReqInterceptor: any; | ||
| timer: any; | ||
| reporter: MetricReporter | undefined; | ||
| observer: any; | ||
| constructor(options: Options) { | ||
| if (ApiMetrics.instance) { | ||
| return ApiMetrics.instance; | ||
| } | ||
| this.timer = null; | ||
| this.options = options; | ||
| const { | ||
| appId, | ||
| stage, | ||
| size = 20, | ||
| time = 1000, | ||
| limitRate = 0.1, | ||
| } = options; | ||
| const random = Math.random(); | ||
| if (random > limitRate) return; | ||
| const countryCode = CountryMap[stage]; | ||
| if (!countryCode) return; | ||
| const isProd = /prod/i.test(stage); | ||
| // 模板唯一键 (这里最终和influxdb模板measurement字段设置一致),命名规范app_metrics_for_{业务标识},例如:app_metrics_for_activity | ||
| const measurement = 'app_metrics_for_api_metric'; | ||
| try { | ||
| this.reporter = new MetricReporter({ | ||
| appId, | ||
| countryCode, | ||
| size, | ||
| time, | ||
| isProd, | ||
| measurement, | ||
| postUplaod: true | ||
| }); | ||
| } catch (e) { | ||
| /* handle error */ | ||
| } | ||
| this.initApiMetricsObserver(); | ||
| ApiMetrics.instance = this; | ||
| } | ||
| initApiMetricsObserver() { | ||
| try { | ||
| this.observer = new PerformanceObserver((list: any) => { | ||
| setTimeout(() => { | ||
| list.getEntries() | ||
| .filter(({initiatorType}: any) => initiatorType === 'xmlhttprequest') | ||
| .forEach((entry: any) => { | ||
| const metric = this.genMetricInfo(entry); | ||
| if (metric) { | ||
| this.reporter?.report(metric); | ||
| } | ||
| }); | ||
| }, 0); | ||
| }); | ||
| this.observer.observe({type: 'resource', buffered: true}); | ||
| if (window) { | ||
| (window as any).addEventListener('unload', () => { | ||
| // 这里执行清理操作 | ||
| this.clearObserver(); | ||
| }); | ||
| } | ||
| } catch (e) { | ||
| /* handle error */ | ||
| } | ||
| } | ||
| genMetricInfo(entry: any) { | ||
| if (!entry) return null; | ||
| const { | ||
| name: fullUrl, | ||
| startTime, | ||
| fetchStart, | ||
| requestStart, | ||
| responseStart, | ||
| responseEnd, | ||
| responseStatus, | ||
| encodedBodySize, | ||
| decodedBodySize, | ||
| transferSize, | ||
| nextHopProtocol, | ||
| duration | ||
| } = entry; | ||
| const urlObj = new URL(fullUrl); | ||
| const {pathname, searchParams, search} = urlObj; | ||
| const {options: {ignoreList = ['/logMetrics']}} = this; | ||
| if (ignoreList?.find((item: string) => pathname.startsWith(item))) { | ||
| return null; | ||
| } | ||
| const spans = Math.ceil(duration); | ||
| const compressed = decodedBodySize !== encodedBodySize; | ||
| const timeToFetch = responseStart - requestStart; | ||
| const requestTime = responseStart - requestStart; | ||
| const recevieTime = responseEnd - responseStart; | ||
| const waitTime = requestStart - startTime; | ||
| const ua = getUA(); | ||
| const uaGenerator = this.options?.generateReferer || generateReferer; | ||
| const referer = uaGenerator(); | ||
| const metricsType = 'api_metric'; | ||
| const metric: Metric = { | ||
| metricsType, | ||
| ua, | ||
| referer, | ||
| url: pathname, | ||
| spans, | ||
| recevieTime, | ||
| timeToFetch, | ||
| requestTime, | ||
| waitTime, | ||
| compressed, | ||
| startTime, | ||
| fetchStart, | ||
| requestStart, | ||
| responseStart, | ||
| responseEnd, | ||
| status: responseStatus === 200 ? 'success' : 'fail', | ||
| encodedBodySize, | ||
| decodedBodySize, | ||
| transferSize, | ||
| nextHopProtocol | ||
| }; | ||
| if (search) { | ||
| const params: {[key: string]: any} = {}; | ||
| for (const [key, value] of searchParams.entries()) { | ||
| params[key] = value; | ||
| } | ||
| Object.assign(metric, {params}); | ||
| } | ||
| return metric; | ||
| } | ||
| clearObserver() { | ||
| try { | ||
| if (this.observer) { | ||
| setTimeout(() => { | ||
| const unprocessedEntries = this.observer.takeRecords(); | ||
| unprocessedEntries | ||
| .filter(({initiatorType}: any) => initiatorType === 'xmlhttprequest') | ||
| .forEach((entry: any) => { | ||
| const metric = this.genMetricInfo(entry); | ||
| if (metric) { | ||
| this.reporter?.report(metric); | ||
| } | ||
| }); | ||
| this.observer?.disconnect(); | ||
| }, 0); | ||
| } | ||
| } catch (error) { | ||
| // ignore | ||
| } | ||
| } | ||
| } |
| /* | ||
| * @Author: ruiwang | ||
| * @Date: 2024-08-12 14:04:04 | ||
| * @Last Modified by: ruiwang | ||
| * @Last Modified time: 2024-09-09 11:48:21 | ||
| */ | ||
| interface Config { | ||
| appId?: string; | ||
| countryCode?: string; | ||
| size?: number; // 单次上报最大数据量,默认 20 | ||
| time?: number; // 轮询上报时间间隔,默认 1000ms | ||
| isProd?: boolean; | ||
| measurement?: string; | ||
| limitRate?: number; // 采样率,默认 0.1 | ||
| postUplaod?: boolean; // 是否收集多个log延时上报 | ||
| } | ||
| interface ReportData {[key: string]: any} | ||
| const reportUrlMap = { | ||
| 'test': 'https://event-tracking-api-test.yangqianguan.com/logMetrics', | ||
| 'prod': 'https://event-tracking-api.yangqianguan.com/logMetrics', | ||
| 'prod-indo': 'https://event-tracking-api.easycash.id/logMetrics' | ||
| }; | ||
| export default class MetricReporter { | ||
| metricData: any[] | undefined | null; | ||
| metricTimer: any; | ||
| reportConfig: Config; | ||
| constructor( | ||
| config: Config | ||
| ) { | ||
| this.reportConfig = config; | ||
| } | ||
| send (data: any, config: any, callback: any) { | ||
| const {countryCode, isProd} = this.reportConfig; | ||
| const urlKey = (!isProd && 'test') || (countryCode === 'IDN' && 'prod-indo') || 'prod'; | ||
| const reportUrl = reportUrlMap[urlKey]; | ||
| const xhr = new XMLHttpRequest(); | ||
| xhr.withCredentials = true; | ||
| xhr.open('POST', reportUrl, true); | ||
| xhr.setRequestHeader('Content-Type', 'application/json'); | ||
| xhr.setRequestHeader('YQG-PLATFORM-SDK-TYPE', config.appId); | ||
| if (config.userToken) { | ||
| xhr.setRequestHeader('userToken', config.userToken); | ||
| } | ||
| xhr.setRequestHeader('Country', config.countryCode); | ||
| xhr.send(JSON.stringify(data)); | ||
| xhr.onreadystatechange = function () { | ||
| if (xhr.readyState === 4) { | ||
| if (xhr.status === 200) { | ||
| callback?.(null, xhr); | ||
| } else { | ||
| callback?.(new Error(xhr.statusText), xhr); | ||
| } | ||
| } | ||
| }; | ||
| } | ||
| report( | ||
| reportData: ReportData, | ||
| callback?: any | ||
| ) { | ||
| const {reportConfig} = this; | ||
| const {appId, countryCode, size = 20, time = 1000, measurement, limitRate, postUplaod = false} = reportConfig; | ||
| if (typeof limitRate !== 'undefined') { | ||
| const random = Math.random(); | ||
| if (random > limitRate) return; | ||
| } | ||
| const {metricsType, userToken, ...parameter} = reportData; | ||
| this.metricData = this.metricData || []; | ||
| const metric = { | ||
| appId, | ||
| metricsType, | ||
| uuid: userToken, | ||
| measurement, | ||
| parameter | ||
| }; | ||
| if (!postUplaod) { | ||
| this.send({ | ||
| level: 'INFO', | ||
| logs: [metric] | ||
| }, { | ||
| appId, | ||
| countryCode, | ||
| userToken | ||
| }, (err: any, res: any) => { | ||
| if (!err) { | ||
| callback?.(err, res); | ||
| } else { | ||
| callback?.(err, res); | ||
| } | ||
| }); | ||
| return; | ||
| } | ||
| this.metricData.push(metric); | ||
| const processor = () => { | ||
| if (this.metricData?.length) { | ||
| const dataSlice = this.metricData.splice(0, size); | ||
| this.send({ | ||
| level: 'INFO', | ||
| logs: dataSlice | ||
| }, { | ||
| appId, | ||
| countryCode, | ||
| userToken | ||
| }, (err: any, res: any) => { | ||
| if (!err) { | ||
| this.metricTimer = setTimeout(processor, time); | ||
| callback?.(err, res); | ||
| } else { | ||
| this.metricData?.unshift(...dataSlice); | ||
| callback?.(err, res); | ||
| } | ||
| }); | ||
| } else { | ||
| if (this.metricTimer) { | ||
| clearTimeout(this.metricTimer); | ||
| this.metricTimer = null; | ||
| } | ||
| } | ||
| }; | ||
| if (!this.metricTimer) { | ||
| this.metricTimer = setTimeout(processor, time); | ||
| } | ||
| } | ||
| } |
| { | ||
| "extends": "@yqg/tsconfig", | ||
| "exclude": ["dist/**/*", "node_modules"], | ||
| "compilerOptions": { | ||
| "declaration": true, | ||
| "declarationDir": "./dist", | ||
| "rootDir": "./src", | ||
| "outDir": "./dist", | ||
| "esModuleInterop": true, | ||
| "target": "ES2018", | ||
| "module": "CommonJS", | ||
| "skipLibCheck": true, | ||
| } | ||
| } |
21830
-34.69%10
-28.57%639
-36.48%