🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@yqg/api-metrics

Package Overview
Dependencies
Maintainers
19
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@yqg/api-metrics - npm Package Compare versions

Comparing version
1.0.0
to
1.0.1
+1
-1
package.json
{
"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'
};
/*
* @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,
}
}