跳到主要内容

TypeScript SDK

一个包,六个入口。导入与你的运行时匹配的那个,摇树构建会把其余部分挡在你的产物之外。

发布位置

@prodantix/sdk on npm.

安装

Bash
bun add @prodantix/sdk

入口

每个子路径都带有自己的 ESM 与 CommonJS 构建以及各自的类型定义。根入口与运行时无关,其余入口则补上某一种运行时所需的部分。

SubpathWhat it exports
@prodantix/sdkProdantixClient, createClient, evaluateFlag, FlagsClient, MessagesClient
@prodantix/sdk/webcreateWebClient, installAutocapture, installLinkCarry, installSessionReplay, WebTransport
@prodantix/sdk/nodecreateNodeClient, readSharedIdentity
@prodantix/sdk/cspprodantixConnectSrc, prodantixImgSrc, prodantixScriptSrc
@prodantix/sdk/chatmountChat, ChatClient, ChatPanel, resolveAppearance
@prodantix/sdk/messengerfetchMessengerConfig, cachedMessengerConfig, MESSENGER_CACHE_KEY

@prodantix/sdk

客户端、功能开关求值器,以及存储与传输接口。这里没有任何代码触碰 DOM,因此你可以传入自己的存储、传输、时钟或 id 生成器,在任何地方运行它。

TypeScript
import { createClient, MemoryStorage } from '@prodantix/sdk';

const prodantix = createClient({
  apiKey: PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.api.prodantix.com',
  storage: new MemoryStorage(),
});

prodantix.capture('order.completed', { properties: { amount: 4200, currency: 'XAF' } });
await prodantix.flush();

@prodantix/sdk/web

在同一个客户端之上补齐浏览器接线:本地存储持久化、页面隐藏时用信标投递,以及页面浏览与点击的自动采集。在你给出回放主机之前,会话回放始终关闭。

TypeScript
import { createWebClient } from '@prodantix/sdk/web';

const prodantix = createWebClient({
  apiKey: PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.api.prodantix.com',
  flagsHost: 'https://api.prodantix.com',
  cookieDomain: 'example.com',
  autocaptureOptions: { clicks: true, pageviews: true },
});

prodantix.identify('user-42', { set: { plan: 'pro' } });

if (await prodantix.isFeatureEnabled('new-checkout')) {
  // ...
}

// Every per-flag read records one $feature_flag_called exposure.
const variant = await prodantix.getVariant('new-checkout');
if (variant?.key === 'treatment') {
  // ...
}

// A flag targeting a cohort reads this user's memberships from the store;
// pass them yourself to skip the lookup.
if (await prodantix.isFeatureEnabledLocal('members', { cohorts: ['<cohort id>'] })) {
  // ...
}

在网页上,访客的匿名 id 和会话保存在你可注册域名上的同一个第一方 cookie 里,因此 xyz.com、app.xyz.com 和 console.xyz.com 看到的是同一个人:营销访问、注册以及应用内的首次会话,会在应用识别出此人后落到同一条时间线上。已识别的 id 按来源保存在本地存储里,永远不会写入 cookie,因为该域名下的每个主机都能读取它。域名通过询问浏览器得出,或用 cookieDomain 指定;子域名分属不同租户的站点必须把它设为自己的主机。传入 sharedStorage 即可不写任何 cookie,例如在获得同意之前;cookie 被拒绝时,本地存储仍是后备。每次访问会生成一个会话 id,闲置三十分钟或满二十四小时后轮换,盖在每个事件上,并与会话回放共用。

cookie 无法跨越可注册域名,所以从一个站点点链接跳到另一个站点的访客本会变成两个访客。传入 linkedDomains(默认为空),客户端就会在 click 和 auxclick 时为指向这些域名的外链附上一个 _pdx 参数,参数里带三样东西:匿名 id、会话 id 和会话开始的时刻,这样三十分钟的窗口和二十四小时的上限就能跨越这一跳继续。只有匿名 id 会随行,已识别的 id 永远不会,因为目的地会自己识别这个人。指向同一可注册域名的链接、已经带有 _pdx 的链接,以及 mailto、tel、javascript 和锚点链接,一律不动。到达后 createWebClient 会在构建身份之前读取 _pdx,把带来的 id 和会话写入共享存储,并用 history.replaceState 去掉该参数,使它既不出现在 $current_url 里,也不会继续往下传;三段解析不通的参数会被忽略,地址栏保持原样。如果本主机上原本已有另一个匿名 id,客户端会发送 $create_alias,把此前记录的一切并入带来的 id;而已经识别过某人的主机会保留那个人:带来的 id 成为他的匿名 id,绝不会成为他的 distinct id。

TypeScript
import { createWebClient } from '@prodantix/sdk/web';

const prodantix = createWebClient({
  apiKey: PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.api.prodantix.com',
  linkedDomains: ['example.com', 'example-app.com'],
});

// A link to example-app.com now leaves as
// https://example-app.com/pricing?_pdx=<anonymousId>~<sessionId>~<startedAt>
// and the arriving page adopts it before it builds an identity of its own.

// The visitor's answer, from your own consent banner.
prodantix.setConsent({ identityLink: false });
prodantix.consent(); // { identityLink: false }

访客本人对这次合并的答复也放在同一个共享存储里,位于 pdx..consent 之下,因此每个子域名和读取该 cookie 的服务端看到的都是同一个答复。用 setConsent({ identityLink: false }) 记录它,用 consent() 读回它,若你的应用已经握有答复,就通过 consent 选项直接播下:同意横幅由你来建,prodantix 尊重它收集到的结果。当 identityLink 为 false 时,identify 不会发送用于并入此人的匿名 id,指向已列域名的链接也不再被装饰,因为把 id 带过去正是同一场合并的另一条路。未设置或为 true 则两者都允许,但仍取决于控制台中项目自身的设置,而那项设置由状态工作进程执行,与浏览器发送的任何内容无关。改变答复会在下一次 identify 和下一次点击时生效;已经合并的一切不会被拆开。

@prodantix/sdk/node

同一个客户端,采用服务端默认值。事件先缓冲,再由后台批量发送,因此请在进程退出前调用 shutdown,否则最后一批永远发不出去。

TypeScript
import { createNodeClient } from '@prodantix/sdk/node';

// A node client streams flag changes over Socket.IO by default and refetches
// the snapshot on push; streamFlags: false leaves only the 30s poll.
const prodantix = createNodeClient({
  apiKey: process.env.PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.api.prodantix.com',
  streamFlags: true,
});

prodantix.capture('job.completed', { properties: { queue: 'emails' } });
await prodantix.shutdown();

以服务端重定向收尾的登录不会渲染任何页面,所以浏览器自身的 identify 在那里永远不会运行。readSharedIdentity(cookieHeader, apiKey) 读取网页客户端写下的 cookie,返回匿名 id、它所处的会话以及访客的同意答复;当 cookie 不存在或不是本 SDK 的时候返回 null。随后 link(distinctId, anonymousId, traits) 就用给定的这两个 id 发出浏览器本该发出的那一条 $identify,并且不动这个客户端自己的身份。Bomdisoft 的登录在登录完成时调用两者,就在它八个登录入口早已共同调用的那一个函数里:那里正是此人的 subject 首次在服务端被知晓、而 cookie 仍在请求上的时刻。其中六个入口是没有请求对象的服务端动作,这也是该函数接收 cookie 头字符串而不是请求的原因。当读回的答复是 identityLink 为 false 时,请跳过 link。

TypeScript
import { createNodeClient, readSharedIdentity } from '@prodantix/sdk/node';

const prodantix = createNodeClient({
  apiKey: PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.api.prodantix.com',
});

// At sign-in completion, where the person is known and the browser's
// cookie is still on the request.
export const completeSignIn = (sub: string, cookieHeader: string): void => {
  const shared = readSharedIdentity(cookieHeader, PRODANTIX_PUBLIC_KEY);
  if (shared !== null && shared.consent?.identityLink !== false) {
    prodantix.link(sub, shared.anonymousId);
  }
};

@prodantix/sdk/csp

一个应用访问 Prodantix 所需的内容安全策略来源。它不从 React 或浏览器导入任何东西,因此构建配置文件可以直接读取它。

TypeScript
import { prodantixConnectSrc, prodantixImgSrc, prodantixScriptSrc } from '@prodantix/sdk/csp';

const hosts = {
  ingest: 'https://eu.api.prodantix.com',
  edge: 'https://eu.edge.prodantix.com',
  replay: 'https://eu.replay.prodantix.com',
};

const policy = [
  `connect-src ${prodantixConnectSrc(hosts)}`,
  `script-src ${prodantixScriptSrc(hosts)}`,
  `img-src ${prodantixImgSrc(hosts, ["'self'", 'data:'])}`,
].join('; ');

客服挂件从 edge 显示你团队的头像,并通过 blob: 地址绘制访客发送的文件,因此策略中的 img-src 需要同时允许这两者。prodantixImgSrc 会把它们加入你已允许的图片来源。缺少它们时,浏览器会拦截这些图片,并且只在自己的控制台中提示。

@prodantix/sdk/chat

客服挂件。挂载后你会得到一个句柄,可以打开、关闭、销毁并标识用户。它渲染在封闭的 shadow root 内,宿主页面的样式永远够不到它。

TypeScript
import { mountChat } from '@prodantix/sdk/chat';

const messenger = mountChat({
  apiKey: PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.edge.prodantix.com',
  distinctId: prodantix.distinctId,
});

document.querySelector('#help')?.addEventListener('click', () => messenger.open());

每一条访客消息都带着它写下的地方。面板在发送的那一刻读取 location 和 document.title,发送 host、path 和 title,其中 path 只是路径名:绝不含查询串,也绝不含片段,因为重置链接、魔法链接或结账回跳会把令牌放在那里,而客服收件箱不是它该去的地方。面板不会画出其中任何一项,所以访客的会话仍只是他自己的话;边缘回给 SDK 的响应同样省略它,因此读取自己会话的页面看不到控制台看到的东西。它出现的地方是收件箱:入站消息下方的一行淡色文字。

@prodantix/sdk/messenger

挂件背后的配置读取器:它拉取某个项目的消息助手配置、做缓存,并告诉你该项目是否已启用消息助手。自执行的代码片段是单独构建的,无法通过这个子路径取得。

TypeScript
import { fetchMessengerConfig } from '@prodantix/sdk/messenger';

const config = await fetchMessengerConfig({
  apiKey: PRODANTIX_PUBLIC_KEY,
  host: 'https://eu.edge.prodantix.com',
});

if (config?.enabled) {
  // the project has the messenger switched on
}