fetchApi拦截与数据上报

背景

之前的文章有分享过 flogger 和 arms 对前端请求数据进行拦截和上报的原理,当时只做了对 xhr 的拦截和上报,现在新的框架基本都使用 fetch 作为数据请求的工具。请求日志需要对 fetch 请求进行拦截和上报。

原理

线上代码

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
function fetchHandler() {
const self = this;
if (!("Proxy" in window)) return;

const textTypes = new Set([
"image/svg",
"application/xml",
"application/xhtml",
"application/html",
]);
const JSON_RE = /^application\/(?:[\w!#$%&*.^`~-]*\+)?json(;.+)?$/i;
const nullBodyResponses = new Set([101, 204, 205, 304]);
function detectResponseType(_contentType = "") {
if (!_contentType) {
return "json";
}
const contentType = _contentType.split(";").shift() || "";
if (JSON_RE.test(contentType)) {
return "json";
}
if (textTypes.has(contentType) || contentType.startsWith("text/")) {
return "text";
}
return "blob";
}

window.fetch = new Proxy(window.fetch, {
apply(target, thisArg, args) {
let _context = null;
let req = null;
// 请求开始时间
const startTime = new Date();
try {
req = new Request(...args);
const urlObj = self._resolveUrl(req.url);
let pathname = urlObj.pathname || "";
if (pathname[0] !== "/") {
pathname = "/" + pathname;
}
// 处理请求的body
let reqBody = args[1]?.body || "";
if (reqBody instanceof Blob) {
reqBody = "Blob:" + reqBody.type;
} else if (typeof reqBody === "object") {
try {
reqBody = JSON.stringify(reqBody);
} catch (e) {
reqBody = "unknown object";
}
}
if (typeof reqBody !== "string") {
reqBody = "unknown";
}

_context = {
requestContext: {
host: urlObj.host, // host名
pathname, // 接口路径
query: urlObj.search, // query参数
method: req.method, // 请求方式
body: reqBody, // 请求体
},
// 其他上报数据...
};
} catch (e) {}
const fetchInstance = target.apply(thisArg, args);

if (req && _context) {
fetchInstance
.then((response) => {
// 请求结束时间
const endTime = new Date();

// 请求耗时
const costTime = (endTime - startTime) / 1000;
const resType = detectResponseType(
response.headers.get("content-type") || ""
);
const _readBody =
response.body &&
!nullBodyResponses.has(response.status) &&
(args[1]?.responseType === "json" ||
resType === "json" ||
resType === "text")
? response.clone().text()
: Promise.resolve("unknown");
_readBody.then((resBody) => {
if (_context) {
Object.assign(_context.requestContext, {
statusCode: response.status || 200, // 返回的状态码
responseTime: costTime, // 请求消耗时间
responseText: resBody,
});
// 上报数据...
}
});
})
.catch((error) => {
const endTime = new Date();
// 请求耗时
const costTime = (endTime - startTime) / 1000;
Object.assign(_context.requestContext, {
responseTime: costTime, // 请求消耗时间
responseText: (error.message || "") + (error.stack || ""), // 响应
});
// 上报数据...
throw error;
});
}
return fetchInstance;
},
});
}

注意点

需要关注 MDN 上对于 fetch 的定义,注意参数和返回值的处理: https://developer.mozilla.org/zh-CN/docs/Web/API/Window/fetch

  1. fetch 请求的第一个参数支持 string/URL/Request 三种形式,两个参数中都有可能有 query 参数,可以直接用 Request 封装一个新的对象,将参数全部传入,这样就能得到一个完整的 query
  2. fetch 的返回值是一个可读流(ReadableStream),无法直接获取到响应体,如果在代理中直接调用方法获取到响应数据,会将流状态置为锁定状态,后续就无法再读取了。需要调用 response.clone 方法复制一份数据进行操作
  3. detectResponseType 方法是判断接口返回的数据格式,如果返回头比较标准,则使用 content-type 头判断,如果不是特别标准,在 fetch 入参中指定 responseType 也可以判断(基本上第三方库都会支持)
  4. fetch 返回的 promise,接口只要响应就会 resolve,与请求状态码无关,只有接口因为一些参数错误导致的错误才会进入 error。具体原因可以参考 MDN 文档。

fetchApi拦截与数据上报
https://www.wobushi.top/2024/fetchApi拦截与数据上报/
作者
Pride Su
发布于
2024年9月4日
许可协议