likes
comments
collection
share

一文摸清前端监控自研实践(二)行为监控

作者站长头像
站长
· 阅读数 5

前言

上篇文章我们分享了关于 页面性能监控 的内容,本文我们接着来看 用户行为监控 的方面

系列文章传送门

用户的行为特征

为什么要做用户的行为情况监控?其实也就是问:采集了用户的行为信息后我们能做什么,答案其实很简单:

  • PV、UV量,日同比、周同比等。能清晰的明白流量变化。
  • 用户热点页面、高访问量TOP10
  • 设备、浏览器语言、浏览器、活跃时间段等的用户特征
  • 用户的行为追踪:某个用户,进入了网站后的一系列操作或者跳转行为;
  • 用户自定义埋点上报用户行为:想做一些自定义事件的监听,比如播放某个视频的行为动作。
  • 多语种站点,每个语种的用户量

整体封装

跟上文一样,这边先附上整体的一个初始化封装;

数据暂存

暂存在 store 里的数据跟 文一:性能监控 基本类似,这里就附一下用户行为所多的参数

export enum metricsName {
  PI = 'page-information',
  OI = 'origin-information',
  RCR = 'router-change-record',
  CBR = 'click-behavior-record',
  CDR = 'custom-define-record',
  HT = 'http-record',
}
整体初始化
export default class UserVitals {
  private engineInstance: EngineInstance;

  // 本地暂存数据在 Map 里 (也可以自己用对象来存储)
  public metrics: UserMetricsStore;

  public breadcrumbs: BehaviorStore;

  public customHandler: Function;

  // 最大行为追踪记录数
  public maxBehaviorRecords: number;

  // 允许捕获click事件的DOM标签 eg:button div img canvas
  clickMountList: Array<string>;

  constructor(engineInstance: EngineInstance) {
    this.engineInstance = engineInstance;
    this.metrics = new UserMetricsStore();
    // 限制最大行为追踪记录数为 100,真实场景下需要外部传入自定义;
    this.maxBehaviorRecords = 100;
    // 初始化行为追踪记录
    this.breadcrumbs = new BehaviorStore({ maxBehaviorRecords: this.maxBehaviorRecords });
    // 初始化 用户自定义 事件捕获
    this.customHandler = this.initCustomerHandler();
    // 作为 真实sdk 的时候,需要在初始化时传入与默认值合并;
    this.clickMountList = ['button'].map((x) => x.toLowerCase());
    // 重写事件
    wrHistory();
    // 初始化页面基本信息
    this.initPageInfo();
    // 初始化路由跳转获取
    this.initRouteChange();
    // 初始化用户来路信息获取
    this.initOriginInfo();
    // 初始化 PV 的获取;
    this.initPV();
    // 初始化 click 事件捕获
    this.initClickHandler(this.clickMountList);
    // 初始化 Http 请求事件捕获
    this.initHttpHandler();
    // 上报策略在后几篇细说
  }

  // 封装用户行为的上报入口
  userSendHandler = (data: IMetrics) => {
    // 进行通知内核实例进行上报;
  };

  // 补齐 pathname 和 timestamp 参数
  getExtends = (): { page: string; timestamp: number | string } => {
    return {
      page: this.getPageInfo().pathname,
      timestamp: new Date().getTime(),
    };
  };

  // 初始化用户自定义埋点数据的获取上报
  initCustomerHandler = (): Function => {
    //... 详情代码在下文
  };

  // 初始化 PI 页面基本信息的获取以及返回
  initPageInfo = (): void => {
    //... 详情代码在下文
  };

  // 初始化 RCR 路由跳转的获取以及返回
  initRouteChange = (): void => {
    //... 详情代码在下文
  };

  // 初始化 PV 的获取以及返回
  initPV = (): void => {
    //... 详情代码在下文
  };

  // 初始化 OI 用户来路的获取以及返回
  initOriginInfo = (): void => {
    //... 详情代码在下文
  };

  // 初始化 CBR 点击事件的获取和返回
  initClickHandler = (mountList: Array<string>): void => {
    //... 详情代码在下文
  };

  // 初始化 http 请求的数据获取和上报
  initHttpHandler = (): void => {
    //... 详情代码在下文
  };
}

用户的基本信息

获取用户一些基本的信息;包括:当前访问的网页路径浏览器语种屏幕大小等等

export interface PageInformation {
  host: string;
  hostname: string;
  href: string;
  protocol: string;
  origin: string;
  port: string;
  pathname: string;
  search: string;
  hash: string;
  // 网页标题
  title: string;
  // 浏览器的语种 (eg:zh) ; 这里截取前两位,有需要也可以不截取
  language: string;
  // 用户 userAgent 信息
  userAgent?: string;
  // 屏幕宽高 (eg:1920x1080)  屏幕宽高意为整个显示屏的宽高
  winScreen: string;
  // 文档宽高 (eg:1388x937)   文档宽高意为当前页面显示的实际宽高(有的同学喜欢半屏显示)
  docScreen: string;
}

// 获取 PI 页面基本信息
getPageInfo = (): PageInformation => {
  const { host, hostname, href, protocol, origin, port, pathname, search, hash } = window.location;
  const { width, height } = window.screen;
  const { language, userAgent } = navigator;

  return {
    host,
    hostname,
    href,
    protocol,
    origin,
    port,
    pathname,
    search,
    hash,
    title: document.title,
    language: language.substr(0, 2),
    userAgent,
    winScreen: `${width}x${height}`,
    docScreen: `${document.documentElement.clientWidth || document.body.clientWidth}x${
      document.documentElement.clientHeight || document.body.clientHeight
    }`,
  };
};

// 初始化 PI 页面基本信息的获取以及返回
initPageInfo = (): void => {
  const info: PageInformation = this.getPageInfo();
  const metrics = info as IMetrics;
  this.metrics.set(metricsName.PI, metrics);
};

用户行为记录栈

有时候,我们需要去获取用户的一个行为追踪记录(比如说:出现了一个线上异常,我们要追溯异常如何发生),这虽然说可能算是错误监控里面的内容,不过数据捕获部分我们就放在这章 行为监控 里讲,也就是说,用户自从打开我们的网站后,看了什么,点击了什么

一般来说,我们所谈到的用户行为记录栈,需要追踪的事件包括以下

  • 路由跳转行为
  • 点击行为
  • ajax 请求行为
  • 用户自定义事件

捕获上面的四个行为,只需要在上述四个事件的代码中做数据捕获就可以了,我们放下下面的叙述中细细说明,这里我就贴一下封装 用户行为记录栈 的代码:

export interface behaviorRecordsOptions {
  maxBehaviorRecords: number;
}

export interface behaviorStack {
  name: metricsName;
  page: string;
  timestamp: number | string;
  value: Object;
}

// 暂存用户的行为记录追踪
export default class behaviorStore {
  // 数组形式的 stack
  private state: Array<behaviorStack>;

  // 记录的最大数量
  private maxBehaviorRecords: number;

  // 外部传入 options 初始化,
  constructor(options: behaviorRecordsOptions) {
    const { maxBehaviorRecords } = options;
    this.maxBehaviorRecords = maxBehaviorRecords;
    this.state = [];
  }

  // 从底部插入一个元素,且不超过 maxBehaviorRecords 限制数量
  push(value: behaviorStack) {
    if (this.length() === this.maxBehaviorRecords) {
      this.shift();
    }
    this.state.push(value);
  }

  // 从顶部删除一个元素,返回删除的元素
  shift() {
    return this.state.shift();
  }

  length() {
    return this.state.length;
  }

  get() {
    return this.state;
  }

  clear() {
    this.state = [];
  }
}

路由跳转

一般的路由跳转行为,都是针对于 SPA单页应用的,因为对于非单页应用来说,url跳转都以页面刷新的形式;

Hash 路由

hash路由的监听比较简单,大家都知道可以用hashchange来监听,

但是 hash 变化除了触发 hashchange ,也会触发 popstate 事件,而且会先触发 popstate 事件,我们可以统一监听 popstate

History 路由

接着往下阅读之前,我们先来了解一下,html5History API ,它所支持的 API 有以下五个

  • history.back()
  • history.go()
  • history.forward()
  • history.pushState()
  • history.replaceState()

同时在 History API 中还有一个 事件 ,该事件为 popstate;它有着以下特点;

  • History.back()History.forward()History.go()在被调用时,会触发 popstate事件
  • 但是History.pushState()History.replaceState()不会触发 popstate事件

所以我们需要对 replaceStatepushState,去创建新的全局Event事件。然后 window.addEventListener 监听我们加的 Event 即可

封装
  • 简单封装一下 以适合上文的整体封装
// 派发出新的 Event
const wr = (type: keyof History) => {
  const orig = history[type];
  return function (this: unknown) {
    const rv = orig.apply(this, arguments);
    const e = new Event(type);
    window.dispatchEvent(e);
    return rv;
  };
};

// 添加 pushState replaceState 事件
export const wrHistory = (): void => {
  history.pushState = wr('pushState');
  history.replaceState = wr('replaceState');
};

// 为 pushState 以及 replaceState 方法添加 Event 事件
export const proxyHistory = (handler: Function): void => {
  // 添加对 replaceState 的监听
  window.addEventListener('replaceState', (e) => handler(e), true);
  // 添加对 pushState 的监听
  window.addEventListener('pushState', (e) => handler(e), true);
};

export const proxyHash = (handler: Function): void => {
  // 添加对 hashchange 的监听
  // hash 变化除了触发 hashchange ,也会触发 popstate 事件,而且会先触发 popstate 事件,我们可以统一监听 popstate
  // 这里可以考虑是否需要监听 hashchange,或者只监听 hashchange
  window.addEventListener('hashchange', (e) => handler(e), true);
  // 添加对 popstate 的监听
  // 浏览器回退、前进行为触发的 可以自己判断是否要添加监听
  window.addEventListener('popstate', (e) => handler(e), true);
};

// 初始化 RCR 路由跳转的获取以及返回
initRouteChange = (): void => {
  const handler = (e: Event) => {
    // 正常记录
    const metrics = {
      // 跳转的方法 eg:replaceState
      jumpType: e.type,
      // 创建时间
      timestamp: new Date().getTime(),
      // 页面信息
      pageInfo: this.getPageInfo(),
    } as IMetrics;
    // 一般路由跳转的信息不会进行上报,根据业务形态决定;
    this.metrics.add(metricsName.RCR, metrics);
    // 行为记录 不需要携带 pageInfo
    delete metrics.pageInfo;
    // 记录到行为记录追踪
    const behavior = {
      category: metricsName.RCR,
      data: metrics,
      ...this.getExtends(),
    } as behaviorStack;
    this.breadcrumbs.push(behavior);
  };
  proxyHash(handler);
  // 为 pushState 以及 replaceState 方法添加 Evetn 事件
  proxyHistory(handler);
};

PV、UV

首先我们来了解一下 PV 和 UV 的定义:

  • PV 是页面访问量
  • UV 是24小时内(00:00-24:00)访问的独立用户数。

那么了解了 PV 和 UV 各是什么之后,我们应该如何做才能采集这两个数据指标呢?其实很简单

  • PV 只需要在用户每次进入页面的时候,进行上报即可,这里需要注意 SPA单页面 应用的PV上报需要结合上面的路由跳转进行;
  • UV 我们就会转为使用服务端进行采集,当服务端判断到上报的 PV所属的IP结合登录信息或者用户标志,是当天的第一次上报时,就给它记录一次 UV

那么按照这个逻辑,我们的 PV上报代码 可以像这样写,简单封装一下

// 初始化 PV 的获取以及返回
initPV = (): void => {
  const handler = () => {
    const metrics = {
      // 还有一些标识用户身份的信息,由项目使用方传入,任意拓展 eg:userId
      // 创建时间
      timestamp: new Date().getTime(),
      // 页面信息
      pageInfo: this.getPageInfo(),
      // 用户来路
      originInformation: getOriginInfo(),
    } as IMetrics;
    this.userSendHandler(metrics);
    // 一般来说, PV 可以立即上报
  };
  afterLoad(() => {
    handler();
  });
  proxyHash(handler);
  // 为 pushState 以及 replaceState 方法添加 Evetn 事件
  proxyHistory(handler);
};

点击事件

有时,我们获取用户的点击情况是非常有价值的比如说有以下场景

  • 网站的首页有三个推广广告,那么哪一个广告更能够吸引用户的点击?
  • 放在网页上的视频是否有人进行播放?播放量为多少?
  • ......等等等等

简而言之,我们如果能够捕获到用户的点击行为,是能够得到一些非常具有价值的指标数据的,当然我们也不是要获取用户的所有点击,里面会包含很多的无意义点击行为,我们需要获取的是具有一些指标意义的点击行为,这就需要一定的过滤,过滤可以根据标签、id、class等等进行过滤:

  • 简单封装一下 以适合上文的整体封装
// 初始化 CBR 点击事件的获取和返回
initClickHandler = (mountList: Array<string>): void => {
  const handler = (e: MouseEvent | any) => {
    // 这里是根据 tagName 进行是否需要捕获事件的依据,可以根据自己的需要,额外判断id\class等
    // 先判断浏览器支持 e.path ,从 path 里先取
    let target = e.path?.find((x: Element) => mountList.includes(x.tagName?.toLowerCase()));
    // 不支持 path 就再判断 target
    target = target || (mountList.includes(e.target.tagName?.toLowerCase()) ? e.target : undefined);
    if (!target) return;
    const metrics = {
      tagInfo: {
        id: target.id,
        classList: Array.from(target.classList),
        tagName: target.tagName,
        text: target.textContent,
      },
      // 创建时间
      timestamp: new Date().getTime(),
      // 页面信息
      pageInfo: this.getPageInfo(),
    } as IMetrics;
    // 除开商城业务外,一般不会特意上报点击行为的数据,都是作为辅助检查错误的数据存在;
    this.metrics.add(metricsName.CBR, metrics);
    // 行为记录 不需要携带 完整的pageInfo
    delete metrics.pageInfo;
    // 记录到行为记录追踪
    const behavior = {
      category: metricsName.CBR,
      data: metrics,
      ...this.getExtends(),
    } as behaviorStack;
    this.breadcrumbs.push(behavior);
  };
  window.addEventListener(
    'click',
    (e) => {
      handler(e);
    },
    true,
  );
};

同理,如果我们想捕获 inputkeydowndoubleClick 也是类似的写法,有兴趣的可以自行拓展

用户自定义埋点

其实用户自定义埋点这个东西,并没有那么神秘和复杂,原理也就是 SDK 内部暴露出接口供 项目使用方 调用,这样用户就可以在任意的时间段页面加载用户点击观看视频达到一半进度....等等)去调用接口 上报任意的自定义内容

而我们 SDK 暴露出的接口,可以由SDK挂载在 window 上,也可以通过暴露接口的方式给外部调用;

然后在服务端再进行数据的归类分析,就完成了用户自定义埋点的一系列流程;实现起来很简单,但是重要的是什么呢?是数据结构的定义,定义的数据结构需要能让服务端方便进行归类分析;

// 这里参考了 谷歌GA 的自定义埋点上报数据维度结构
export interface customAnalyticsData {
  // 事件类别 互动的对象 eg:Video
  eventCategory: string;
  // 事件动作 互动动作方式 eg:play
  eventAction: string;
  // 事件标签 对事件进行分类 eg:
  eventLabel: string;
  // 事件值 与事件相关的数值   eg:180min
  eventValue?: string;
}

// 初始化用户自定义埋点数据的获取上报
initCustomerHandler = (): Function => {
  const handler = (options: customAnalyticsData) => {
    // 记录到 UserMetricsStore
    this.metrics.add(metricsName.CDR, options);
    // 自定义埋点的信息一般立即上报
    this.userSendHandler(options);
    // 记录到用户行为记录栈
    this.breadcrumbs.push({
      category: metricsName.CDR,
      data: options,
      ...this.getExtends(),
    });
  };

  return handler;
};

HTTP 请求捕获

HTTP行为 也是用户行为追踪的重要一环,有的时候,页面出现问题往往是 HTTP 请求了某些数据,渲染造成的;而除了是用户行为追踪的重要一环外;采集 HTTP请求 的各种信息:包括 请求地址方法耗时请求时间响应时间响应结果等等等等...

而为了实现上述的监控需求,我们需要了解到:现在异步请求的底层原理都是调用的 XMLHttpRequest 或者 Fetch,我们只需要对这两个方法都进行 劫持 ,就可以往接口请求的过程中加入我们所需要的一些参数捕获;

XMLHttpRequest 的劫持

预期就是,我们只需要传入一个 loadHandler 方法,它就自动会在 请求 结束时给我返回该有的数据

export interface httpMetrics {
  method: string;
  url: string | URL;
  body: Document | XMLHttpRequestBodyInit | null | undefined | ReadableStream;
  requestTime: number;
  responseTime: number;
  status: number;
  statusText: string;
  response?: any;
}

// 调用 proxyXmlHttp 即可完成全局监听 XMLHttpRequest
export const proxyXmlHttp = (sendHandler: Function | null | undefined, loadHandler: Function) => {
  if ('XMLHttpRequest' in window && typeof window.XMLHttpRequest === 'function') {
    const oXMLHttpRequest = window.XMLHttpRequest;
    if (!(window as any).oXMLHttpRequest) {
      // oXMLHttpRequest 为原生的 XMLHttpRequest,可以用以 SDK 进行数据上报,区分业务
      (window as any).oXMLHttpRequest = oXMLHttpRequest;
    }
    (window as any).XMLHttpRequest = function () {
      // 覆写 window.XMLHttpRequest
      const xhr = new oXMLHttpRequest();
      const { open, send } = xhr;
      let metrics = {} as httpMetrics;
      xhr.open = (method, url) => {
        metrics.method = method;
        metrics.url = url;
        open.call(xhr, method, url, true);
      };
      xhr.send = (body) => {
        metrics.body = body || '';
        metrics.requestTime = new Date().getTime();
        // sendHandler 可以在发送 Ajax 请求之前,挂载一些信息,比如 header 请求头
        // setRequestHeader 设置请求header,用来传输关键参数等
        // xhr.setRequestHeader('xxx-id', 'VQVE-QEBQ');
        if (typeof sendHandler === 'function') sendHandler(xhr);
        send.call(xhr, body);
      };
      xhr.addEventListener('loadend', () => {
        const { status, statusText, response } = xhr;
        metrics = {
          ...metrics,
          status,
          statusText,
          response,
          responseTime: new Date().getTime(),
        };
        if (typeof loadHandler === 'function') loadHandler(metrics);
        // xhr.status 状态码
      });
      return xhr;
    };
  }
};
Fetch 的劫持
// 调用 proxyFetch 即可完成全局监听 fetch
export const proxyFetch = (sendHandler: Function | null | undefined, loadHandler: Function) => {
  if ('fetch' in window && typeof window.fetch === 'function') {
    const oFetch = window.fetch;
    if (!(window as any).oFetch) {
      (window as any).oFetch = oFetch;
    }
    (window as any).fetch = async (input: any, init: RequestInit) => {
      // init 是用户手动传入的 fetch 请求互数据,包括了 method、body、headers,要做统一拦截数据修改,直接改init即可
      if (typeof sendHandler === 'function') sendHandler(init);
      let metrics = {} as httpMetrics;

      metrics.method = init?.method || '';
      metrics.url = (input && typeof input !== 'string' ? input?.url : input) || ''; // 请求的url
      metrics.body = init?.body || '';
      metrics.requestTime = new Date().getTime();

      return oFetch.call(window, input, init).then(async (response) => {
        // clone 出一个新的 response,再用其做.text(),避免 body stream already read 问题
        const res = response.clone();
        metrics = {
          ...metrics,
          status: res.status,
          statusText: res.statusText,
          response: await res.text(),
          responseTime: new Date().getTime(),
        };
        if (typeof loadHandler === 'function') loadHandler(metrics);
        return response;
      });
    };
  }
};

简单初始化封装

上面都是 proxy 劫持接口的封装,具体的调用看如下:

// 初始化 http 请求的数据获取和上报
initHttpHandler = (): void => {
  const loadHandler = (metrics: httpMetrics) => {
    if (metrics.status < 400) {
      // 对于正常请求的 HTTP 请求来说,不需要记录 请求体 和 响应体
      delete metrics.response;
      delete metrics.body;
    }
    // 记录到 UserMetricsStore
    this.metrics.add(metricsName.HT, metrics);
    // 记录到用户行为记录栈
    this.breadcrumbs.push({
      category: metricsName.HT,
      data: metrics,
      ...this.getExtends(),
    });
  };
  proxyXmlHttp(null, loadHandler);
  proxyFetch(null, loadHandler);
};

页面停留时间

还有一项比较通用的指标,叫做页面停留时间,是通过统计用户在每个页面的停留时间而成的

这里给一个简单的采集实例代码思路(未封装)

const routeList = [];
const routeTemplate = {
  userId: '', // 用户信息等
  // 除了userId以外,还可以附带一些其余的用户特征到这里面
  url: '',
  startTime: 0,
  dulation: 0,
  endTime: 0,
};
function recordNextPage() {
  // 记录前一个页面的页面停留时间
  const time = new Date().getTime();
  routeList[routeList.length - 1].endTime = time;
  routeList[routeList.length - 1].dulation = time - routeList[routeList.length - 1].startTime;
  // 推一个新的页面停留记录
  routeList.push({
    ...routeTemplate,
    ...{ url: window.location.pathname, startTime: time, dulation: 0, endTime: 0 },
  });
}
// 第一次进入页面时,记录
window.addEventListener('load', () => {
  const time = new Date().getTime();
  routeList.push({
    ...routeTemplate,
    ...{ url: window.location.pathname, startTime: time, dulation: 0, endTime: 0 },
  });
});
// 单页面应用触发 replaceState 时的上报
window.addEventListener('replaceState', () => {
  recordNextPage();
});
// 单页面应用触发 pushState 时的上报
window.addEventListener('pushState', () => {
  recordNextPage();
});
// 浏览器回退、前进行为触发的 可以自己判断是否要上报
window.addEventListener('popstate', () => {
  recordNextPage();
});
// 关闭浏览器前记录最后的时间并上报
window.addEventListener('beforeunload', () => {
  const time = new Date().getTime();
  routeList[routeList.length - 1].endTime = time;
  routeList[routeList.length - 1].dulation = time - routeList[routeList.length - 1].startTime;
  // 记录完了离开的时间,就可以上报了
  // eg: report()
});

访客来路

有的时候,产品可能会问我们几个直击灵魂的问题

  • 我们现在的新用户流量,大部分都是从哪里引流过来的啊?
  • 线上环境404页面访问激增,我想知道用户是访问了哪个不存在页面才跳到 404 的

很简单,采集一下用户来路的数据就可以了,原理就是获取 document.referrer 以及window.performance.navigation.type

用户来路地址

我们可以直接用 document.referrer 来获取用户在我们的网页上的前一个网页地址;但是需要注意的是,有几个场景我们获取到的值会是空

  • 直接在地址栏中输入地址跳转
  • 直接通过浏览器收藏夹打开
  • 从https的网站直接进入一个http协议的网站
用户来路方式

我们可以直接使用 window.performance.navigation.type 来获取用户在我们网页上的来路方式

该属性返回一个整数值,可能有以下4种情况

  • 0: 点击链接、地址栏输入、表单提交、脚本操作等。
  • 1: 点击重新加载按钮、location.reload。
  • 2: 点击前进或后退按钮。
  • 255: 任何其他来源。即非刷新/非前进后退、非点击链接/地址栏输入/表单提交/脚本操作等。
代码封装
export interface OriginInformation {
  referrer: string;
  type: number | string;
}

// 返回 OI 用户来路信息
export const getOriginInfo = (): OriginInformation => {
  return {
    referrer: document.referrer,
    type: window.performance?.navigation.type || '',
  };
};

// 初始化 OI 用户来路的获取以及返回
initOriginInfo = (): void => {
  const info: OriginInformation = getOriginInfo();
  const metrics = info as IMetrics;
  this.metrics.set(metricsName.OI, metrics);
};

User Agent 解析

我们的 User Agent 信息里面有带有很多的信息,比如浏览器内核设备类型等等,但是解析它并不是个简单的事情,如果我们自己写的话,会用到很多的正则表达式去解析它,所以这边推荐两个现成的插件来使用:bowserua-parser-js

// nodejs 环境下 require
const parser = require('ua-parser-js');
const Bowser = require('bowser');
// 获取user-agent解析
function getFeature(userAgent) {
  const browserData = Bowser.parse(userAgent);
  const parserData = parser(userAgent);
  const browserName = browserData.browser.name || parserData.browser.name; // 浏览器名
  const browserVersion = browserData.browser.version || parserData.browser.version; // 浏览器版本号
  const osName = browserData.os.name || parserData.os.name; // 操作系统名
  const osVersion = parserData.os.version || browserData.os.version; // 操作系统版本号
  const deviceType = browserData.platform.type || parserData.device.type; // 设备类型
  const deviceVendor = browserData.platform.vendor || parserData.device.vendor || ''; // 设备所属公司
  const deviceModel = browserData.platform.model || parserData.device.model || ''; // 设备型号
  const engineName = browserData.engine.name || parserData.engine.name; // engine名
  const engineVersion = browserData.engine.version || parserData.engine.version; // engine版本号
  return {
    browserName,
    browserVersion,
    osName,
    osVersion,
    deviceType,
    deviceVendor,
    deviceModel,
    engineName,
    engineVersion,
  };
}

IP 采集解析

为什么要采集 IP 呢?其实很简单,我们可以通过解析 IP 地址,来解析出用户的地域网络运营商等信息;而解析IP我们可以使用诸如腾讯云、阿里云等各种的 三方API 来实现;但是采集 IP 信息就需要我们自己来进行实现了;

具体来说,IP 并不是一个通过 JS SDK 来采集的指标数据,我们需要在服务端去获取访问过来的IP地址,获取 IP 地址的方法我建议先阅读一下这篇文章: HTTP X-Forwarded-For 介绍 | 菜鸟教程

直接贴结论

  • 对于直接面向用户部署的 Web 应用必须使用从 TCP 连接中得到的 Remote Address
  • 对于部署了 Nginx 这样反向代理的 Web 应用,可以使用 Nginx 传过来的 X-Real-IPX-Forwarded-For 最后一节(实际上它们一定等价)。

我这边举例一下 Nginx 下的获取,这里取 x-real-ipx-forwarded-for 的最后一节即可

// nodejs 代码
const http = require('http');

function getIp(req) {
  const ip = (req.headers['x-forwarded-for'] || req.connection.remoteAddress).replace('::ffff:', '');
  // 取 x-forwarded-for 的话再做一个取最后一节的处理
  return ip === '::1' ? '127.0.0.1' : ip;
}
http
  .createServer((req, res) => {
    res.writeHead(200, { 'Content-Type': 'text/plain' });
    // 先取x-real-ip
    // 再取service里的自定义方法,取x-forwarded-for最后一节
    const ip = req.headers['x-real-ip'] || getIp(req);
    res.write(`ip: ${ip}\n`);
    res.end();
  })
  .listen(9009, '0.0.0.0');

参考链接

HTTP X-Forwarded-For 介绍 | 菜鸟教程