Sentry 开发者贡献指南 - SDK 开发(性能监控)

时间:2022-01-16 16:58:38

Sentry 开发者贡献指南 - SDK 开发(性能监控)

性能监控指南

本文档介绍了 SDK 应如何通过分布式跟踪添加对性能监控的支持。

  • https://docs.sentry.io/product/performance/distributed-tracing/

这应该提供 SDK 需要实现的 API 的概述,而不强制要求内部实现细节。

参考实现:

  • @sentry/tracing (JavaScript)
  • https://github.com/getsentry/sentry-javascript/tree/master/packages/tracing

Python SDK

  • https://github.com/getsentry/sentry-python/blob/master/sentry_sdk/tracing.py

SDK 配置

通过设置两个新的 SDK 配置选项之一来启用跟踪,tracesSampleRate 和 tracesSampler。如果未设置,则两者都默认为 undefined,从而选择如何加入跟踪。

tracesSampleRate

这应该是介于 0.0 和 1.0(含)之间的 float/double,表示任何给定 transaction 将被发送到 Sentry 的百分比机会。因此,0.0 是 0% 的机会(不会发送),而 1.0 是 100% 的机会(都将发送)。此 rate 同样适用于所有 transaction;换句话说,每个 transaction 都应该有相同的随机机会以 sampled = true 结束,等于 tracesSampleRate。

tracesSampler

这应该是一个 callback,在 transaction 开始时调用,它将被赋予一个 samplingContext 对象,并且应该返回一个介于 0.0 和 1.0 之间的采样率_对于所讨论的 transaction_。此采样率的行为方式应与上面的 tracesSampleRate 相同,不同之处在于它仅适用于新创建的 transaction,因此可以以不同的 rate 对不同的 transaction 进行采样。返回 0.0 应该强制删除 transaction(设置为 sampled = false),返回 1.0 应该强制发送 transaction(设置 sampled = true)。

可选地,tracesSampler callback 也可以返回一个布尔值来强制进行采样决策(false 等同于 0.0,true 等同于 1.0)。如果返回两种不同的数据类型在实现语言中不是一个选项,则可以安全地省略这种可能性。

maxSpans

由于 transaction payload 在摄取端强制执行最大大小,因此 SDK 应限制附加到事务的 span 数。这类似于如何限制面包屑和其他任意大小的列表以防止意外误用。如果在达到最大值后添加新的 span,SDK 应删除 span 并理想地使用内部日志记录来帮助调试。

maxSpans 应该作为一个内部的、不可配置的、默认为 1000 的常量来实现。如果在给定的平台中有理由,它可能会变得可配置。

maxSpans 限制还可以帮助避免永远不会完成的 transaction(在 span 打开时保持 transaction 打开的平台中),防止 OOM 错误,并通常避免降低应用程序性能。

Event 变更

在撰写本文时,transaction 是作为 Event 模型的扩展实现的。

Transaction 的显着特征是 type: "transaction"。

除此之外,Event 获得了新的字段:spans、contexts.TraceContext。

新的 Span 和 Transaction 类

在内存中,span 构建了一个定时操作的概念树(conceptual tree)。我们称整个 span tree 为 transaction。有时我们使用术语 "transaction" 来指代作为整棵树的 span tree,有时特指树的 root span。

通过网络,transaction 被序列化为 JSON 作为增强的 Event,并作为 envelope 发送。不同的 envelope 类型用于优化摄取(因此我们可以以不同于其他事件的方式路由 “transaction events”,主要是 “error events”)。

在 Sentry UI 中,您可以使用 Discover 查看所有类型的事件,并使用 Issues 和 Performance 部分分别深入研究 errors 和 transactions。 面向用户的跟踪文档解释了更多产品级别的概念。

  • https://docs.sentry.io/product/performance/distributed-tracing/#traces-transactions-and-spans

Span 类将每个单独的 span 存储在 trace 中。

Transaction 类就像一个 span,有几个主要区别:

  • Transaction 有 name,span 没有。
  • 在 span 上调用 finish 方法会记录 span 的结束时间戳。对于 transaction,finish 方法另外向 Sentry 发送一个事件。

Transaction 类可能继承自 Span,但这是一个实现细节。从语义上讲,transaction 既表示 span tree 的 top-level span,也表示向 Sentry 报告的单位。

Span 接口

  • 创建 Span 时,将 startTimestamp 设置为当前时间
  • SpanContext 是 Span 的属性集合(可以是一个实现细节)。如果可能,SpanContext 应该是不可变的。
  • Span 应该有一个方法 startChild,它使用当前 span 的 id 作为新 span 的 parentSpanId 创建一个新的 span,并将当前 span 的 sampled 值复制到新 span 的 sampled 属性
  • startChild 方法应遵守 maxSpans 限制,一旦达到限制,SDK 不应为给定的 transaction 创建新的子 span。
  • Span 应该有一个名为 toSentryTrace 的方法,它返回一个字符串,该字符串可以作为名为 sentry-trace 的 header 发送。
  • Span 应该有一个名为 iterHeaders(适应平台的命名约定)的方法,它返回一个可迭代的或 header 名称和值的映射。这是一个包含 return {"sentry-trace": toSentryTrace()} 的薄 wrapper。请参阅 continueFromHeaders 以了解为什么存在这种情况,并且在编写集成(integration)时应该首选。

Transaction 接口

  • 一个 Transaction 内部包含一个子 Span 的平面列表(不是树结构)
  • Transaction 还有一个 setName 方法来设置 transaction 的名称
  • Transaction 在创建时收到一个 TransactionContext(新属性与 SpanContext 是 name)
  • 由于 Transaction 继承了 Span,因此它具有所有 Span 可用的函数并且可以像 Span 一样进行交互
  • 一个 transaction 要么被采样(sampled = true),要么被取消采样(sampled = false),一个在 transaction 的生命周期中被继承或设置一次的决定,并且在任何一种情况下都会传播给所有的 children。不应将未抽样的 transaction 发送给 Sentry。
  • TransactionContext 应该有一个叫做 fromSentryTrace 的 static/ctor 方法,它用从 sentry-trace header值接收的数据预填充一个 TransactionContext
  • TransactionContext 应该有一个名为 continueFromHeaders(headerMap) 的 static/ctor 方法,它现在实际上只是一个围绕 fromSentryTrace(headerMap.get("sentry-trace")) 的薄 wrapper。integration/framework-sdk 的作者应该更喜欢 fromSentryTrace,因为它隐藏了核心 sdk 中更深层次使用的确切 header 名称,并为将来使用其他 header(来自 W3C)留下了机会,而无需更改所有集成。

Span.finish()

  • 只需将 endTimestamp 设置为当前时间(在 payload timestamp 中)

Transaction.finish()

  • super.finish() (在 Span 上调用 finish)
  • 仅当 sampled == true 时才将其发送给 Sentry
  • 一个 Transaction 需要被包裹在一个 Envelope 中并发送到 Envelope Endpoint
  • Transport 应该为 Transactions/Events 使用相同的内部队列
  • Transport 应该实现基于类别的速率限制 →
  • Transport 应该处理在内部将 Transaction 包装在 Envelope 中

采样

每个 transaction 都有一个 “抽样决策”,即一个布尔值,指示是否应该将其发送给 Sentry。这应该在 transaction 的生命周期内只设置一次,并且应该存储在内部的 sampled 布尔值中。

transaction 可以通过多种方式结束抽样决策(sampling decision):

  • 根据 tracesSampleRate 中设置的静态采样率随机采样
  • 根据 tracesSampler 返回的动态采样率进行随机采样
  • tracesSampler 返回的绝对决策(100% 概率或 0% 概率)
  • 如果 transaction 有父级,继承其父级的抽样决策
  • 传递给 startTransaction 的绝对决策

当其中一个以上发挥作用的可能性时,应适用以下优先规则:

  1. 如果将抽样决策传递给 startTransaction (startTransaction({name: "my transaction", sampled: true})),则将使用该决策,而不管其他任何事情。
  2. 如果定义了 tracesSampler,则将使用其决策。它可以选择保留或忽略任何父采样决策,或使用采样上下文数据做出自己的决策或为 transaction 选择采样率。
  3. 如果未定义 tracesSampler,但存在父采样决策,则将使用父采样决策。
  4. 如果没有定义 tracesSampler 并且没有父采样决策,则将使用 tracesSampleRate。

Transaction 应仅通过 tracesSampleRate 或 tracesSampler 进行采样。sampleRate 配置用于 error 事件,不应应用于 transaction。

采样上下文

如果定义,tracesSampler 回调应该传递一个 samplingContext 对象,该对象至少应该包括:

  • 创建 transaction 的 transactionContext
  • 一个布尔值 parentSampled,包含从父级传递过来的采样决策,如果有的话
  • 来自可选的 customSamplingContext 对象的数据在手动调用时传递给 startTransaction

根据平台,可能包含其他默认数据。(例如,对于服务器框架,包含与 transaction 正在测量的请求相对应的 request 对象是有意义的。)

传播

transaction 的抽样决策应传递给其所有子项,包括跨服务边界。这可以在相同服务子项的 startChild 方法中完成,并为不同服务中的子项使用 senry-trace header。

Header sentry-trace

Header 用于跟踪传播。SDK 使用 header 继续跟踪来自上游服务(传入 HTTP 请求),并将跟踪信息传播到下游服务(传出 HTTP 请求)。

sentry-trace = traceid-spanid-sampled

sampled 是可选的。所以至少,它是预期的:

sentry-trace = traceid-spanid

为了与 W3C traceparent header(没有版本前缀) 和 Zipkin's b3 headers(考虑 64 位和 128 位的 traceId 有效)提供最小的兼容性, sentry-trace header 应具有以 32 个十六进制字符编码的 128 位的 traceId 以及以 16 个十六进制字符编码的 64 位 spanId。为了避免与 W3C traceparent header(我们的 header 相似但不相同)混淆, 我们将其简称为 sentry-trace。header 中没有定义版本。

  • https://www.w3.org/TR/trace-context/#traceparent-header
  • https://zipkin.io/pages/instrumenting#communicating-trace-information

sampled 值

为简化处理,该值由单个(可选)字符组成。可能的值为:

  1. - No value means defer
  2.  
  3. 0 - Don't sample
  4.  
  5. 1 - Sampled

与 b3 header 不同,sentry-trace header 不应该只包含一个采样决策,没有 traceid 或 spanid 值。有很好的理由 无论采样决策如何,始终包含 traceid 和 spanid,这样做也简化了实现。

  • https://github.com/apache/incubator-zipkin-b3-propagation/blob/bc937b6854ea30e46b3e85fbf147d8f4de685dd5/README.md#why-send-trace-ids-with-a-reject-sampling-decision

除了在 Sentry 的情况下使用 *defer 的通常原因外, 还有一个原因是下游系统使用 Sentry 捕获 error 事件。可以在那时做出决定,对跟踪进行采样,以便为报告的崩溃提供跟踪数据。

  • https://github.com/apache/incubator-zipkin-b3-propagation/blob/bc937b6854ea30e46b3e85fbf147d8f4de685dd5/README.md#why-defer-a-sampling-decision

sentry-trace = sampled

这实际上对于代理将其设置为 0 并选择退出跟踪很有用。

Static API 变更

Sentry.startTransaction 函数应该接受两个参数 - 传递给 Transaction 构造函数的 transactionContext 和一个包含要传递给 tracesSampler(如果已定义)的数据的可选的 customSamplingContext 对象。

它创建一个绑定到当前 hub 的 Transaction 并返回实例。用户与实例交互以创建子 span,因此,必须自己跟踪它。

Hub 变更

引入一个名为 traceHeaders 的方法

  • 此函数返回 header(string)sentry-trace
  • 该值应该是当前在 Scope 上的 Span 的 trace header 字符串

引入一个名为 startTransaction 的方法

  • 采用与 Sentry.startTransaction 相同的两个参数
  • 创建一个新的 Transaction 实例
  • 应按照本文档 'Sampling' 部分中更详细的描述实施抽样

修改名为 captureEvent 或 captureTransaction 的方法

  • 不要为 transaction 设置 lastEventId

Scope 变更

Scope 持有对当前 Span 或 Transaction 的引用。

Scope 引入 setSpan

  • 这可以在内部使用,来传递 Span / Transaction,以便集成可以将子项附加到它
  • 在 Scope(旧版)上设置 transaction 属性应该覆盖存储在 Scope 中的 Transaction 的名称,如果有的话。这样,即使用户无法直接访问 Transaction 的实例,我们也可以让用户选择更改 transaction 名称。

与 beforeSend 和事件处理器的交互

beforeSend 回调是我们认为最重要的特殊 Event Processor。适当的事件处理器通常被认为是内部的。

Transaction 应该不通过 beforeSend。但是,它们仍然由事件处理器处理。这是在将 transaction 作为 event 的当前实现处理的一些灵活性与为 transaction 和 span 的不同生命周期 hook 留出空间之间的折衷。

动机:

  1. 面向未来:如果用户依赖 beforeSend 进行 transaction, 这将使最终在不破坏用户代码的情况下实现单个 span 摄取变得复杂。在撰写本文时,transaction 作为 event 发送,但这被视为实现细节。
  2. API 兼容性:用户拥有他们现有的 beforeSend 实现,只需要处理错误事件。我们将 transaction 作为一种新型 event 引入。当用户升级到新的 SDK 版本并开始使用跟踪时,他们的 beforeSend 将开始看到他们的代码不打算处理的新类型。在 transaction 之前,他们根本不必关心不同的事件类型。有几种可能的后果:破坏用户应用程序;默默地和无意地放弃 transaction; transaction 事件以令人惊讶的方式修改。
  3. 就可用性而言,beforeSend 不适合删除 transaction,就像删除 error 一样。 Error 是一个时间点事件。当 error 发生时,用户在 beforeSend 中有完整的上下文, 并且可以在它进入 Sentry 之前修改/丢弃事件。对于交易,transaction 是不同的。创建 transaction,然后将它们打开一段时间,同时创建 child span 并将其附加到它。同时传出的 HTTP 请求包括当前 transaction 与其他服务的采样决策。在 span 和 transaction 完成后,将 transaction 放入类似 beforeSend 的钩子中会在跟踪中留下来自其他服务的孤立 transaction。同样,在此后期将采样决策修改为 "yes" 也会产生不一致的痕迹。

跟踪上下文(实验性)

为了对跟踪进行采样,我们需要沿着调用链传递 trace id 以及做出采样决策所需的信息,即所谓的 跟踪上下文(trace context)。

协议

Trace 信息作为编码的 tracestate header 在 SDK 之间传递,SDK 预计会拦截和传播这些 header。

对于向 sentry 提交的事件,trace context 作为嵌入在 Envelope header 中的 JSON 对象发送,key 为 trace。

跟踪上下文

无论采用何种传输机制,trace context 都是具有以下字段的 JSON 对象:

  • trace_id (string, required) - UUID V4 编码为不带破折号的十六进制序列(例如771a43a4192642f0b136d5159a501700),它是一个由 32 个十六进制数字组成的序列。这必须与提交的 transaction item 的 trace id 匹配。
  • public_key (string, required) - 来自 SDK 使用的 DSN 的 Public key。它允许 Sentry 通过基于起始项目解析相同的规则集来对跨多个项目的跟踪进行采样。
  • release (string, optional) - 客户端选项中指定的版本名称,通常是:package@x.y.z+build。 这应该与 transaction event payload 的 release 属性匹配*
  • environment - 客户端选项中指定的 environment 名称,例如 staging。 这应该与 transaction event payload 的 environment 属性匹配*
  • user (object, optional) - 包含以下字段的 scope 的 user context 的子集:
  • id (string, optional) - 用户上下文的 id 属性。
  • segment (string, optional) - 用户数据包中的 segment 属性值(如果存在)。将来,该字段可能会被提升为用户上下文的适当属性。
  • transaction (string, optional) - 在 scope 上设置的 transaction 名称。这应该与 transaction event payload 的 transaction 属性匹配*

例子:

  1. {
  2. "trace_id": "771a43a4192642f0b136d5159a501700",
  3. "public_key": "49d0f7386ad645858ae85020e393bef3",
  4. "release": "myapp@1.1.2",
  5. "environment": "production",
  6. "user": {
  7. "id": "7efa4978da177713df088f846f8c484d",
  8. "segment": "vip"
  9. },
  10. "transaction": "/api/0/project_details"
  11. }

Envelope Headers(信封头)

当通过 Envelope 向 Sentry 发送 transaction 事件时,必须在 trace 字段下的 envelope header 中设置 trace 信息。

这是一个包含 trace context 的最小 envelope header 的示例(尽管 header 不包含换行符,但在下面的示例中添加了换行符以提高可读性):

  1. {
  2. "event_id": "12c2d058d58442709aa2eca08bf20986",
  3. "trace": {
  4. "trace_id": "771a43a4192642f0b136d5159a501700",
  5. "public_key": "49d0f7386ad645858ae85020e393bef3"
  6. // other trace attributes
  7. }
  8. }

Tracestate Headers(跟踪状态头)

将跟踪上下文传播到其他 SDK 时,Sentry 使用 W3C tracestate header。有关如何将这些 header 传播到其他 SDK 的更多信息,请参阅 "Trace Propagation"。

  • https://www.w3.org/TR/trace-context/#trace-context-http-headers-format

Tracestate header 包含几个特定于供应商的不透明数据。根据 HTTP 规范,这些多个 header 值可以通过两种方式给出,通常由 HTTP 库和开箱即用的框架支持:

用逗号连接:

  1. tracestate: sentry=,other=

重复:

  1. tracestate: sentry=
  2. tracestate: other=

要创建 tracestate header 的内容:

  1. 将完整的 trace context 序列化为 JSON,包括 trace_id。
  2. 如果字符串在平台上的表示方式不同,则将生成的 JSON 字符串编码为 UTF-8。
  3. 使用 base64 对 UTF-8 字符串进行编码。
  4. 去除尾随填充字符 (=),因为这是一个保留字符。
  5. 在前面加上 "sentry=",导致 "sentry= "。
  6. 如上所述加入 header。

通过去除尾随填充,默认的 base64 解析器可能会检测到不完整的 payload。选择允许丢失 = 或允许截断 payload 的解析模式。

例如:

  1. {
  2. "trace_id": "771a43a4192642f0b136d5159a501700",
  3. "public_key": "49d0f7386ad645858ae85020e393bef3",
  4. "release": "1.1.22",
  5. "environment": "dev",
  6. "user": {
  7. "segment": "vip",
  8. "id": "7efa4978da177713df088f846f8c484d"
  9. }
  10. }

将编码为

  1. ewogICJ0cmFjZV9pZCI6ICI3NzFhNDNhNDE5MjY0MmYwYjEzNmQ1MTU5YTUwMTcwMCIsCiAgInB1YmxpY19rZXkiOiAiNDlkMGY3Mzg2YWQ2NDU4NThhZTg1MDIwZTM5M2JlZjMiLAogICJyZWxlYXNlIjogIjEuMS4yMiIsCiAgImVudmlyb25tZW50IjogImRldiIsCiAgInVzZXIiOiB7CiAgICAic2VnbWVudCI6ICJ2aXAiLAogICAgImlkIjogIjdlZmE0OTc4ZGExNzc3MTNkZjA4OGY4NDZmOGM0ODRkIgogIH0KfQ

并导致 header

  1. tracestate: sentry=ewogIC...IH0KfQ,other=[omitted]

(注意 header 末尾的第三方条目;新的或修改的条目总是添加到左侧,因此我们将 sentry= 值放在那里。另请注意,尽管此处为了清晰起见省略了编码值, 在真正的 header 中,将使用完整的值。)

实施指南

支持此 header 的 SDK 必须:

  • 创建新的 trace context 时使用 scope 信息
  • 为包含 transaction 的 envelope 添加带有 trace context 的 envelope header
  • 将 tracestate HTTP header 添加到传出的 HTTP 请求以进行传播
  • 在适用的情况下拦截对 tracestate HTTP header 的传入 HTTP 请求,并将它们应用到 local trace context

背景

这是性能指南涵盖的 trace ID 传播的扩展。根据统一 API 跟踪规范,Sentry SDK 通过集成向传出请求添加 HTTP header sentry-trace。最重要的是,此 header 包含 trace ID,它必须与 transaction event 的 trace id 以及下面的 trace context 的 trace id 匹配。

trace context 应在 W3C traceparent header 中定义的附加 tracestate header 中传播。请注意,我们必须保持与 W3C 规范的兼容性,而不是专有的 sentry-trace header。除了 Sentry SDK 放置的内容之外,tracestate header 还包含供应商特定的不透明数据。

  • https://www.w3.org/TR/trace-context/#trace-context-http-headers-format

Client 选项

虽然 trace context 正在开发中,但它们应该在内部 trace_sampling 布尔值 client 选项后面进行门控。该选项默认为 false,不应在 Sentry 文档中记录。

根据平台命名指南,该选项应该适当地区分大小写:

  • trace_sampling (snake case)
  • traceSampling (camel case)
  • TraceSampling (pascal case)
  • setTraceSampling (Java-style setters)

添加 Envelope Header

在以下任何一种情况下,SDK 应将 envelope header 添加到传出 envelope 中:

  1. envelope 包含 transaction event。
  2. scope 有一个 transaction 绑定。

具体来说,这意味着即使没有 transaction 的 envelope 也可以包含 trace envelope header, 从而允许 Sentry 最终对属于 transaction 的 attachment 进行采样。当 envelope 包含 transaction 且 scope 有绑定 transaction 时, SDK 应使用 envelope 的 transaction 来创建 trace envelope header。

冻结上下文

为了确保 trace 中所有 transaction 的 trace context 完全一致,一旦通过网络发送 trace context,就不能更改 trace context,即使 scope 或 options 之后发生更改。也就是说,一旦计算出 trace context 就不再更新。即使应用程序调用 setRelease,旧版本仍保留在 context 中。

为了弥补对 setTransaction 和 setUser 等函数的延迟调用, 可以认为 trace context 处于两种状态:NEW 和 SENT。最初,context 处于 NEW 状态并且可以修改。第一次发送后,它将变为 SENT 并且不能再更改。

我们建议 trace context 应该在第一次需要时即时计算:

  • 创建 Envelope
  • 传播到传出的 HTTP 请求

Trace context 必须保留,直到用户开始新的 trace,此时 SDK 必须计算新的 trace context。

建议 SDK 记录在 trace context 冻结时会导致 trace context 更改的属性修改,例如 user.id,以简化常见动态采样陷阱的调试。

传入上下文

与拦截来自入站 HTTP 请求的 trace ID 相同,SDK 应读取 tracestate header 并假设 Sentry 跟踪上下文(如果指定)。这样的上下文立即冻结在 SENT 状态,不应再允许修改。

平台细节

在 JavaScript 中编码

如前所述,我们需要使用 UTF-8 字符串对 JSON trace context 进行编码。JavaScript 内部使用 UTF16,因此我们需要做一些工作来进行转换。

Base64 encoding and decoding in JavaScript(以及 Using Javascript's atob to decode base64 doesn't properly decode utf-8 strings)介绍了基本思想。

  • https://attacomsian.com/blog/javascript-base64-encode-decode
  • https://*.com/questions/30106476/using-javascripts-atob-to-decode-base64-doesnt-properly-decode-utf-8-strings

简而言之,这是将 context 转换为可以保存在 tracestate 中的 base64 字符串的函数。最后我们采用了一个更简单的实现,但想法是一样的:

  1. // Compact form
  2. function objToB64(obj) {
  3. const utf16Json = JSON.stringify(obj);
  4. const b64 = btoa(
  5. encodeURIComponent(utf16Json).replace(
  6. /%([0-9A-F]{2})/g,
  7. function toSolidBytes(match, p1) {
  8. return String.fromCharCode("0x" + p1);
  9. }
  10. )
  11. );
  12. const len = b64.length;
  13. if (b64[len - 2] === "=") {
  14. return b64.substr(0, len - 2);
  15. } else if (b64[len - 1] === "=") {
  16. return b64.substr(0, len - 1);
  17. }
  18. return b64;
  19. }
  20.  
  21. // Commented
  22. function objToB64(obj) {
  23. // object to JSON string
  24. const utf16Json = JSON.stringify(obj);
  25. // still utf16 string but with non ASCI escaped as UTF-8 numbers)
  26. const encodedUtf8 = encodeURIComponent(utf16Json);
  27.  
  28. // replace the escaped code points with utf16
  29. // in the first 256 code points (the most wierd part)
  30. const b64 = btoa(
  31. endcodedUtf8.replace(/%([0-9A-F]{2})/g, function toSolidBytes(match, p1) {
  32. return String.fromCharCode("0x" + p1);
  33. })
  34. );
  35.  
  36. // drop the '=' or '==' padding from base64
  37. const len = b64.length;
  38. if (b64[len - 2] === "=") {
  39. return b64.substr(0, len - 2);
  40. } else if (b64[len - 1] === "=") {
  41. return b64.substr(0, len - 1);
  42. }
  43. return b64;
  44. }
  45. // const test = {"x":"a-