@yqg/multiple-click
Version:
Monitor user's multiple click behavior and report
207 lines (171 loc) • 6.48 kB
text/typescript
import {
CONFIG_CENTER_HOST_MAP,
DEFAULT_TRIGGER_CONTINUOUS_COUNT,
DEFAULT_TRIGGER_INTERVAL,
DEFAULT_TRIGGER_RANGE,
EVENT_TRACKING_HOST_PROD_INDO,
EVENT_TRACKING_HOST_PROD,
EVENT_TRACKING_HOST_TEST,
OUTER_HTML_MAX_LENGTH
} from './constant';
import {
ENV_TYPE,
ExtraTrackingInfo,
IExposeMultipleClickTrackingConfig,
MultipleClickTrackingConfig,
RemoteMultipleClickTrackingConfig
} from './type';
import {castArray, getCountry, getOsType, htmlTreeAsString, pick} from './utils';
/** 获取上报的url */
const getTrackingUrl = ({env}: IExposeMultipleClickTrackingConfig) => {
let trackingHost;
if (env === ENV_TYPE.PROD_INDO) {
trackingHost = EVENT_TRACKING_HOST_PROD_INDO;
} else if (env.startsWith('prod')) {
trackingHost = EVENT_TRACKING_HOST_PROD;
} else if (env !== ENV_TYPE.DEV) {
trackingHost = EVENT_TRACKING_HOST_TEST;
}
const trackingUrl = trackingHost ? `${trackingHost}/logMetrics` : null;
return trackingUrl;
};
/** 拉取远程配置 */
const fetchRemoteConfig = ({
appId,
env
}: IExposeMultipleClickTrackingConfig): Promise<RemoteMultipleClickTrackingConfig> => {
const configCenterHost = CONFIG_CENTER_HOST_MAP[env];
if (!configCenterHost) {
return Promise.resolve({});
}
const configUrl = `${configCenterHost}/api-web/hostCond?app=web-multiple-click&prefix=${appId}`;
return fetch(configUrl)
.then(async res => {
const data = (await res.json())?.body?.[appId];
const config: RemoteMultipleClickTrackingConfig = data ? JSON.parse(data) : {};
return pick(config, ['disabled', 'interval', 'continuousCount', 'range', 'excludeRules']);
})
.catch(() => ({}));
};
/** 日志上报方法的工厂函数 */
const uploadTrackingInfoFactory = (config: IExposeMultipleClickTrackingConfig) => {
const {appId, appVersion, customTrackingInfo} = config;
// 信息上报url
const trackingUrl = getTrackingUrl(config);
// 操作系统类型
const osType = getOsType();
// 请求头
const headers = {
// eslint-disable-next-line quote-props
Country: getCountry(),
'YQG-PLATFORM-SDK-TYPE': appId,
'CONTENT-TYPE': 'application/json;charset=UTF-8'
};
// 事件上报
const extraInfoGetter = ({target: _target}: MouseEvent): ExtraTrackingInfo => {
const target = _target as HTMLElement;
const {outerHTML} = target;
const {x: clientLeft, y: clientTop} = target.getBoundingClientRect();
return {
url: location.href,
outerHTML: outerHTML.slice(0, OUTER_HTML_MAX_LENGTH),
selector: htmlTreeAsString(target),
offsetTop: clientTop + window.scrollY,
offsetLeft: clientLeft + window.scrollX,
clientTop,
clientLeft
};
};
const uploadTrackingInfo = (event: MouseEvent) => {
const trackingInfo = customTrackingInfo(event, extraInfoGetter(event));
if (!trackingUrl) {
// eslint-disable-next-line no-console
console.info('无需上报多次点击事件,事件信息:', trackingInfo);
return;
}
const info = {
level: 'INFO',
logs: [
{
appId,
appVersion: appVersion || null,
osType,
measurement: 'app_metrics_for_multiple_click',
metricsType: 'metricsType1',
time: Date.now().toString(),
message: 'multiple click',
parameter: trackingInfo
}
]
};
fetch(trackingUrl, {method: 'post', headers, body: JSON.stringify(info)}).catch(err =>
// eslint-disable-next-line no-console
console.error('多次点击上报异常', err)
);
};
return uploadTrackingInfo;
};
/** 配置的检查、转换 */
export const processTrackingConfig = async (
config: IExposeMultipleClickTrackingConfig
): Promise<MultipleClickTrackingConfig> => {
const {disabled, ...remoteConfig} = await fetchRemoteConfig(config);
if (disabled) {
return null;
}
const defaultConfig = {
interval: DEFAULT_TRIGGER_INTERVAL,
continuousCount: DEFAULT_TRIGGER_CONTINUOUS_COUNT,
range: DEFAULT_TRIGGER_RANGE,
customTrackingInfo: (_: Event, extra: ExtraTrackingInfo) => extra,
filter: () => false
};
const mergedConfig = {
...defaultConfig,
...config,
...remoteConfig,
excludeRules: [...castArray(config.excludeRules || []), ...castArray(remoteConfig.excludeRules || [])]
} as Required<IExposeMultipleClickTrackingConfig>;
const {appId, appVersion, env, interval, continuousCount, range, customTrackingInfo, filter, excludeRules} =
mergedConfig;
// 配置检查
if (typeof appId !== 'string' || !appId) {
throw new Error('appId需为非空字符串');
}
if (appVersion && typeof appVersion !== 'string') {
throw new Error('appVersion需为字符串');
}
const envTypeList = Object.values(ENV_TYPE);
if (!envTypeList.includes(env)) {
throw new Error(`env需为${envTypeList.join('、')}中的一个`);
}
if (typeof interval !== 'number' || interval <= 0) {
throw new Error('interval需为大于0的数字');
}
if (typeof continuousCount !== 'number' || continuousCount <= 1) {
throw new Error('continuousCount需为大于1的数字');
}
if (typeof range !== 'number' || range <= 0) {
throw new Error('range需为大于0的数字');
}
if (typeof customTrackingInfo !== 'function') {
throw new Error('customTrackingInfo需为函数');
}
if (typeof filter !== 'function') {
throw new Error('filter需为函数');
}
const validatedConfig = {
...config,
interval,
continuousCount,
range,
customTrackingInfo,
filter
};
const uploadTrackingInfo = uploadTrackingInfoFactory(validatedConfig);
const parsedExcludeRules = excludeRules.map(({page, ...rule}) => ({
...rule,
page: page ? castArray(page) : []
}));
return {interval, continuousCount, range, uploadTrackingInfo, filter, excludeRules: parsedExcludeRules};
};