使用 GraphQL 插件

了解如何在你的 React 应用中设置 GraphQL 插件

使用 GraphQL 插件

如果你在应用中使用 GraphQL 作为 API 的查询语言,那么 GraphQL plugin 可以帮助你同时追踪针对 GraphQL 服务器执行的变更(mutations)和查询(queries)。

在本教程中,我们将使用 Apollo Boost 客户端,但只要你使用的客户端允许你设置某种 middleware,你就能在代码中使用该插件。

如果你想跟着一起操作,可以查看这个仓库,它同时包含了 GraphQL 服务器和客户端应用。

在安装 GraphQL 插件之前,你需要先安装 Tracker。如果你已经知道如何操作,请跳到下一节;否则请继续阅读。

我们会把这段代码保存在一个单独的模块中,该模块将导出两个函数:initstart

第一个函数将实例化 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 操作的新区域。

The GraphQL UI inside the Session Replay

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

Sanitized vs Not Sanitized data

虽然插件本身并不提供任何清理函数,但我们仍然可以添加代码,将个人和隐私信息从录像中隐藏起来,以帮助保护用户的隐私。

如果你查看我创建 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
});

这段代码的关键点是:

  1. 我们添加了两个函数,一个用于清理对象中的 email,另一个用于清理 GraphQL 操作的结果。
  2. map 回调(来自 link 函数)内部,我们现在不再返回 graphqlTracker 的输出,因为该函数会原封不动地返回它收到的结果值。 1. 但该结果会被返回给客户端应用,如果我们清理了结果,用户就会看到该数据集的已清理版本。因此,我们需要克隆结果,以便修改被追踪的那个并返回原始结果。
  3. sanitizeResult 函数对对象进行深克隆,因为否则修改它会改变结果本身。

Sanitized data everywhere

你可以查看这个仓库,获取一个使用 Tracker 的可运行 GraphQL 应用的完整源代码

如果你在 GraphQL 项目中设置 Tracker 时遇到任何问题,请在我们的 Slack 社区联系我们,直接向我们的开发者提问!