使用 GraphQL 插件
如果你在应用中使用 GraphQL 作为 API 的查询语言,那么 GraphQL plugin 可以帮助你同时追踪针对 GraphQL 服务器执行的变更(mutations)和查询(queries)。
在本教程中,我们将使用 Apollo Boost 客户端,但只要你使用的客户端允许你设置某种 middleware,你就能在代码中使用该插件。
如果你想跟着一起操作,可以查看这个仓库,它同时包含了 GraphQL 服务器和客户端应用。
首先设置 Tracker
Section titled 首先设置 Tracker在安装 GraphQL 插件之前,你需要先安装 Tracker。如果你已经知道如何操作,请跳到下一节;否则请继续阅读。
我们会把这段代码保存在一个单独的模块中,该模块将导出两个函数:init 和 start。
第一个函数将实例化 tracker 并设置所有插件;第二个函数则只调用 start 方法。
import OpenReplay from '@openreplay/tracker';
let _tracker = null;
export function init({plugins}) {
_tracker = new OpenReplay({
projectKey: process.env.OPENREPLAY_PROJECT_KEY
});
let pluginResults = {}
if(plugins) {
Object.keys(plugins).forEach( pk => {
pluginResults[pk] = _tracker.use(plugins[pk]())
})
}
return pluginResults
}
export function start() {
return _tracker.start()
}
init 函数有趣的地方在于,它会返回一个由所有插件返回值组成的对象。我们的某些插件会返回一个你稍后需要使用的函数(就像 GraphQL 插件的情况)。这种方式让你可以一次性用所有插件初始化 tracker,然后在你想用的时候再使用这些返回值。
使用 npm i @openreplay/tracker-graphql 安装插件之后,使用以下代码来调用我们刚刚定义的 init 函数:
import trackerGraphQL from '@openreplay/tracker-graphql';
import {init} from './tracker/index'
const {graphqlTracker} = init({
plugins: {
graphqlTracker: trackerGraphQL
}
})
这里使用的 graphqlTracker 键可以是任何你想要的名称。只要 plugins 部分中使用的键与你从 init 函数结果中解构出来的键相同,就没有问题。
用 Apollo 客户端设置插件
Section titled 用 Apollo 客户端设置插件在本教程中,我们将使用 Apollo Boost 库,它允许你通过他们所称的 “links” 来修改每个请求的数据流。
这些 links 就像 middleware 函数一样,你可以用它们来拦截请求的数据流,在我们的例子中是用来记录它。
以下代码将使用 ApolloLink 函数创建一个新的 link。这个 link 会捕获操作的数据和结果,并调用我们的 graphqlTracker 函数(即上面 init 调用所返回的那个函数)。
const trackerApolloLink = new ApolloLink((operation, forward) => {
const operationDefinition = operation.query.definitions[0];
let {operationName, variables} = operation
const {kind, operation: op} = operationDefinition
const opKind = kind === 'OperationDefinition' ? op : 'unknown?'
let results = forward(operation).map((result) => {
return graphqlTracker(opKind, operationName, variables, result);
});
if(results.length === 0) { //if there are no results, then we've not tracked anything so far...
graphqlTracker(opKind, operationName, variables, {});
}
return results
});
搞定这些之后,我们就可以像下面这样使用这个新创建的 link:
import {ApolloClient, HttpLink } from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloLink, from } from '@apollo/client';
const link = from([
trackerApolloLink,
new HttpLink({uri: () => 'http://localhost:4000/graphql'}),
]);
const client = new ApolloClient({
link,
cache: new InMemoryCache()
});
ReactDOM.render(<ApolloProvider client={client}>
<App />
</ApolloProvider>, document.getElementById('root'));
上面的代码取自 Apollo 文档;到了这一步,tracker 和插件都已经设置完毕,所以你其实不需要再担心其他任何事情了。
完成之后,你的录像(replays)中会显示一个列出所有 GraphQL 操作的新区域。

话虽如此,被 tracker 自动清理的敏感信息(例如电子邮件地址)不会被插件清理。因此你会遇到如下情况:DOM 中是已清理的数据,但操作详情却显示实际数据。

虽然插件本身并不提供任何清理函数,但我们仍然可以添加代码,将个人和隐私信息从录像中隐藏起来,以帮助保护用户的隐私。
清理记录的数据
Section titled 清理记录的数据如果你查看我创建 trackerApolloLink 对象的那段示例代码,你会发现我所做的全部就是调用那个把信息保存到 tracker 上的 tracker 函数。
如果我不改动数据,那么一切都会原封不动地被保存下来。所以,为了在录像中清理数据同时保持操作不变,我们需要在调用 tracker 之前克隆关键变量。这意味着克隆操作的变量和结果,而这正是我们想要的全部。
所以这里有一段代码片段,它会创建 ApolloLink 并在录像数据中将数据保密:
/**
* Sanitize the result from a GraphQL operation
* @returns Returns the result object but with the sanitized fields changed.
*/
function sanitizeResult(res) {
//deep clonning needs to happen to make sure this only affects the new object and not
//the original object.
let sanitized = JSON.parse(JSON.stringify(res))
let ops = Object.keys(sanitized.data)
ops.forEach( o => {
if(Array.isArray(sanitized.data[o])) { //mutations don't really return arrays
sanitized.data[o] = sanitized.data[o].map( sanitizeData )
}
})
return sanitized
}
// We only want to hide the content of othe "email" field for now.
function sanitizeData(vars) {
let newVars = {...vars}
if(newVars.email) {
newVars.email = "****@***.***"
}
return newVars
}
const trackerApolloLink = new ApolloLink((operation, forward) => {
const operationDefinition = operation.query.definitions[0];
let {operationName, variables} = operation
const {kind, operation: op} = operationDefinition
const opKind = kind === 'OperationDefinition' ? op : 'unknown?'
let trackedVariables = sanitizeData({...variables})
let results = forward(operation).map((result) => {
let trackeresults = sanitizeResult(result)
graphqlTracker(opKind, operationName, trackedVariables, trackeresults);
return result //we have to return the original "result" object here, not the sanitized one
});
if(results.length === 0) { //if there are no results, then we've not tracked anything so far...
graphqlTracker(opKind, operationName, trackedVariables, {});
}
return results
});
这段代码的关键点是:
- 我们添加了两个函数,一个用于清理对象中的
email,另一个用于清理 GraphQL 操作的结果。 - 在
map回调(来自 link 函数)内部,我们现在不再返回 graphqlTracker 的输出,因为该函数会原封不动地返回它收到的结果值。 1. 但该结果会被返回给客户端应用,如果我们清理了结果,用户就会看到该数据集的已清理版本。因此,我们需要克隆结果,以便修改被追踪的那个并返回原始结果。 sanitizeResult函数对对象进行深克隆,因为否则修改它会改变结果本身。

有疑问吗?
Section titled 有疑问吗?你可以查看这个仓库,获取一个使用 Tracker 的可运行 GraphQL 应用的完整源代码。
如果你在 GraphQL 项目中设置 Tracker 时遇到任何问题,请在我们的 Slack 社区联系我们,直接向我们的开发者提问!