@cekatai/event-sdk

中文文档
0.4.0发布于 2026/09/28MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 9267

The server SDK for Node.js and Bun sends Cekat events through a backend-only access token. Browser exports deliberately contain no token client, Authorization handling, or Node imports.

原始使用文档
文档目录9 节
  1. Install and initialize (Node.js and Bun)
  2. Keep tracking off the request's critical path
  3. Node request visitor context and adapters
  4. Bun
  5. Bun before 1.4
  6. Stripe metadata composition
  7. Errors and delivery outcome
  8. Browser visitor propagation
  9. Local no-publish package preparation

@cekatai/event-sdk

The server SDK for Node.js and Bun sends Cekat events through a backend-only access token. Browser exports deliberately contain no token client, Authorization handling, or Node imports.

Install and initialize (Node.js and Bun)

npm install @cekatai/event-sdk    # or: bun add @cekatai/event-sdk

Requires Node.js 22.12 or newer, or Bun 1.2.5 or newer. The same package runs on both; see Bun. The package is ESM and also loads from CommonJS through require() (for example in default NestJS projects). The Node client is exported from both @cekatai/event-sdk and @cekatai/event-sdk/node.

Framework adapterSupported majors
Express4.17+, 5
Fastify4, 5
Koa2.13+, 3
NestJS (Express or Fastify platform)10, 11, 12
Next.js (Node runtime)14, 15, 16
Fetch-style handlers: Bun.serve, Hono, Elysia (@cekatai/event-sdk/fetch)Any
import { Client } from '@cekatai/event-sdk';

// Keep the token in server-side configuration; never ship it to a browser.
const cekat = new Client(process.env.CEKAT_ACCESS_TOKEN!);

Client accepts a token only plus optional { baseURL, timeoutMs, retryCount, fetch } options. baseURL must be an absolute HTTP(S) origin without credentials, path, query, or fragment. The default is https://t.cekat.ai; each operation posts only to /api/events/ingest and identifies itself with User-Agent: cekat-event-sdk-node/<version> <runtime>/<version>, where the runtime is node or bun. The default timeout is 3 seconds per attempt and the default retry count is two after the initial request. Pass { signal } to any event call to cancel without retrying.

await cekat.userRegistration({ email: '[email protected]' });
await cekat.userLogin({ phoneNumber: '+15551234567', visitorId: 'browser-visitor' });
await cekat.orderCreated(125000, 'IDR', { email: '[email protected]', properties: { order_id: 'o-1' } });
await cekat.formSubmitted({ email: '[email protected]', properties: { form_id: 'contact' } });
await cekat.orderPaid(125000, 'IDR', { email: '[email protected]', properties: { order_id: 'o-1' } });
await cekat.customEvent('wishlist_updated', { email: '[email protected]', contactName: 'Ada' });

formSubmitted(event) sends the common form_submitted event (is_common: true). orderCreated(amount, currency, event) and orderPaid(amount, currency, event) require a finite amount and a nonblank currency, sent as the amount and currency properties; do not also put those keys in event.properties. They differ only in their event key.

A nonblank explicit visitorId takes precedence over request context; a blank explicit value falls back to context. Email and phone are retained as submitted but at least one must be nonblank. Event properties are JSON values only (finite safe numbers, arrays, and plain objects); convert values such as Date to strings first.

Every event carries an event_id and an occurred_at timestamp. When eventId is blank the SDK generates a random UUID, and when occurredAt (a Date) is omitted it uses the time of the call. Both are fixed before the first attempt and reused by every retry, so Cekat can recognize retried deliveries. Supply your own eventId (for example an order or webhook ID) when the same business event may be sent more than once:

await cekat.orderPaid(order.total, order.currency, { email: order.email, eventId: `order-paid-${order.id}`, occurredAt: order.paidAt });

Keep tracking off the request's critical path

Event methods always return a promise; invalid input rejects it rather than throwing synchronously. To avoid adding tracking latency to a user-facing request, you can skip await — but always attach a .catch. An unhandled rejection terminates the Node.js process by default, so a Cekat outage must never reach it:

cekat.userLogin({ email: user.email }).catch((error) => logger.warn({ error }, 'cekat user_login failed'));

The call reads the request's visitor scope synchronously, so this works inside adapters.

A successful Acknowledgement means the ingest service accepted the event for asynchronous processing. It is not a promise of idempotency, durable persistence, identity resolution, delivery state, or special common-event behavior.

Node request visitor context and adapters

X-Cekat-Visitor-ID wins over the _cekat_visitor_id cookie. Values are trimmed; blank values are absent. Visitor IDs are untrusted correlation data from the browser: never use them for authentication or authorization. For non-framework code, establish a request-local scope explicitly:

import { runWithVisitorId } from '@cekatai/event-sdk/node';

await runWithVisitorId('visitor-123', () => cekat.customEvent('page_server_rendered', { email: '[email protected]' }));

Use one adapter at the server boundary. Context is request-local, including asynchronous work started within the handler.

// Express
import express from 'express';
import { visitorMiddleware } from '@cekatai/event-sdk/express';
express().use(visitorMiddleware());

// Fastify
import Fastify from 'fastify';
import { visitorPlugin } from '@cekatai/event-sdk/fastify';
await Fastify().register(visitorPlugin);

// Koa
import Koa from 'koa';
import { visitorMiddleware as koaVisitorMiddleware } from '@cekatai/event-sdk/koa';
new Koa().use(koaVisitorMiddleware());

// NestJS
import { CekatVisitorMiddleware } from '@cekatai/event-sdk/nestjs';
// consumer.apply(CekatVisitorMiddleware).forRoutes('*');

Next.js support is Node runtime only. Declare the runtime in a route before importing the Next helper; Edge route module graphs must not import this subpath.

export const runtime = 'nodejs';

import { withCekatVisitor } from '@cekatai/event-sdk/nextjs';
export default withCekatVisitor(async (request, response) => { /* ... */ });

For App Router handlers use runWithCekatVisitor(request, () => handler()) in the same kind of Node-runtime route.

Bun

On Bun 1.2.5 or newer the client, the visitor scope, and the Express, Fastify, Koa, and NestJS adapters run unchanged; their test suites pass with Bun as the runtime. The Next.js helpers' tests also pass on Bun, but whether Next.js itself runs on Bun is up to Next.js. No separate import or build is needed. Bun-native servers pass a Web Request to a fetch handler, so use @cekatai/event-sdk/fetch:

import { Client } from '@cekatai/event-sdk';
import { withCekatVisitor } from '@cekatai/event-sdk/fetch';

const cekat = new Client(Bun.env.CEKAT_ACCESS_TOKEN!);

Bun.serve({
  // `this` and extra arguments such as `server` pass through unchanged.
  fetch: withCekatVisitor(async (request, server) => {
    const { email } = await request.json();
    await cekat.userLogin({ email }); // sends the request's visitor_id
    return new Response('ok');
  }),
  // Route handlers can be wrapped individually:
  // routes: { '/login': { POST: withCekatVisitor(login) } },
});

Hono and Elysia expose a standard fetch, so wrap it, or use Hono middleware:

import { Hono } from 'hono';
import { runWithCekatVisitor, withCekatVisitor } from '@cekatai/event-sdk/fetch';

const hono = new Hono();
hono.use((c, next) => runWithCekatVisitor(c.req.raw, next));
export default { fetch: hono.fetch };

// Elysia: serve its fetch handler with the wrapper instead of app.listen().
Bun.serve({ fetch: withCekatVisitor(elysiaApp.fetch) });

@cekatai/event-sdk/fetch needs only a Web Request and AsyncLocalStorage, so it also works with Hono on Node.js. The scope covers the handler and everything it awaits or starts. A streaming response body that is generated after the handler returns may not observe it: read currentVisitorId() in the handler and pass the value along.

On Bun, AsyncLocalStorage is not restored inside AbortSignal.timeout() listeners or MessagePort message handlers (it is on Node.js). Calls made from those callbacks send no visitor unless you capture it first and pass it explicitly: const visitorId = currentVisitorId(); then cekat.userLogin({ email, visitorId }). Every other asynchronous boundary the test suite covers behaves as on Node.js.

Bun before 1.4

Bun 1.2.5 through 1.3.x are supported with two differences, both caused by Bun's HTTP client and fixed in Bun 1.4.0:

  • No connection reuse. Those releases can hand a new request a pooled connection that a server closed mid-response, which makes the request hang, read part of another response, or be sent twice. The SDK therefore opens a new connection for every attempt on Bun before 1.4 (keepalive: false). This adds a TCP (and TLS) handshake to each event; keep tracking off the request's critical path as shown above.
  • A truncated response looks like a network failure. If the connection closes after Cekat's response headers but before the full body, Bun rejects the request without exposing the status. Instead of ResponseDecodeError (received, not retried), the SDK sees a TransportError, retries with the same event_id, and reports deliveryOutcomeUnknown: true if retries are exhausted. A retry may therefore deliver the same event again, as with any network failure. On Bun 1.4.0+ and Node.js the received response is classified exactly.

Bun older than 1.2.5 is not tested. See docs/compatibility.md for the evidence.

Stripe metadata composition

Use the helper with the merchant's Stripe client; it does not create a Stripe request or send a Cekat event.

import { mergeMetadata, metadataFromCurrentVisitor } from '@cekatai/event-sdk/stripe';

const metadata = mergeMetadata(merchantMetadata, metadataFromCurrentVisitor()['cekat_' + 'visitor_id']);
await stripe.paymentIntents.create({ amount: 1200, currency: 'usd', metadata });
await stripe.checkout.sessions.create({ mode: 'payment', metadata, payment_intent_data: { metadata } });

Only a trimmed 1–128 character [A-Za-z0-9_-] visitor becomes the Stripe visitor metadata entry. Merge returns a fresh object, preserving merchant keys and leaving invalid or absent visitor input unchanged.

Errors and delivery outcome

Invalid local input rejects with ValidationError before network I/O. Received responses have known delivery outcome and reject with AuthenticationError (401), EventDefinitionNotFoundError (404), ApiError (other non-200), or ResponseDecodeError (malformed, truncated, or unreadable 200). TransportError means outcome is unknown after transport failure or SDK timeout. HTTP bodies retained in errors are bounded to 65,536 bytes.

Transport failures, SDK timeouts, and HTTP 429, 500, 502, 503, and 504 are retried with capped exponential full jitter (up to 100ms, 200ms, 400ms, 800ms, then 1s). Other statuses, including 400, 401, and 404, are not retried. A valid Retry-After header raises the delay to the server's value; if the server asks for more than 5 seconds, the call rejects immediately instead of blocking. A received 200 is never retried, even if its body cannot be read, because the event was already accepted. Retries after an unknown outcome can create duplicate events; they reuse the same event_id, but the SDK does not guarantee server-side deduplication.

Browser visitor propagation

Browser helpers read the exact _cekat_visitor_id cookie; they are SSR-safe and return no visitor when document is unavailable. They never make backend token-bearing requests.

Use explicit enrichment as the default escape hatch:

import { withVisitor, withVisitorRequest } from '@cekatai/event-sdk/browser';

await fetch('https://api.example.test/events', withVisitor({ method: 'POST' }));
await fetch(withVisitorRequest(new Request('https://api.example.test/events')));

Existing X-Cekat-Visitor-ID headers are preserved case-insensitively. If the target is cross-origin, its CORS policy must allow the X-Cekat-Visitor-ID request header.

Axios is optional and should be attached only to a dedicated instance, not a process-wide/default instance:

import axios from 'axios';
import { createAxiosVisitorInterceptor } from '@cekatai/event-sdk/browser/axios';

const api = axios.create({ baseURL: 'https://api.example.test' });
api.interceptors.request.use(createAxiosVisitorInterceptor());

Automatic propagation is disabled by default. Enable it only with parsed, explicit HTTP(S) origin/path allowlist entries. Matching requires exact normalized URL.origin equality and URL.pathname.startsWith(pathPrefix); it is not a raw URL-prefix match. One active installation is allowed and disable() restores the original globals.

import { enableAutoPropagation } from '@cekatai/event-sdk/browser';

const disable = enableAutoPropagation({
  allowedTargets: [{ origin: 'https://api.example.test', pathPrefix: '/v1/' }],
});
// Later, for example during application teardown:
disable();

The automatic interceptor covers browser global fetch and XMLHttpRequest only. It does not cover server fetches, Axios unless the dedicated interceptor is registered, WebSocket/EventSource traffic, navigation, forms, service-worker-controlled requests, or arbitrary third-party HTTP clients.

Local no-publish package preparation

./scripts/package --version 0.4.0 --output /absolute/empty-directory

Bun support is verified separately with npm run test:bun (the test suites on Bun, including the Bun-only test/bun servers) and CEKAT_NODE_RUNTIME=bun scripts/conformance (the shared conformance fixtures on Bun). Both use Node.js and npm for installation and type checking.

The command runs dependency installation, official compatibility verification, production and full dependency audits, the approved-range dependency gate, tests, type checks, builds, export checks, and browser tests; it then creates one local .tgz plus an atomic SHA-256 manifest.json. It never publishes, signs, tags, or pushes. Releases to npm run from the repository's release-node.yml workflow when a node/vX.Y.Z tag is pushed; see the root release checklist.

版本与发布记录

@cekatai/event-sdk 共发布 5 个版本,首次发布于 2026/09/17;过去 12 个月发布了 5 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.4.0latest2026/09/281 个月前
0.3.02026/09/231 个月前
0.2.02026/09/211 个月前
0.1.02026/09/171 个月前
0.0.0-bootstrap.02026/09/171 个月前

关于 @cekatai/event-sdk 的常见问题

@cekatai/event-sdk 是什么?

The server SDK for Node.js and Bun sends Cekat events through a backend-only access token. Browser exports deliberately contain no token client, Authorization handling, or Node imports.

如何安装 @cekatai/event-sdk?

在项目目录运行 npm install @cekatai/event-sdk,或使用 pnpm add / yarn add / bun add。

@cekatai/event-sdk 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@cekatai/event-sdk 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@cekatai/event-sdk 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.12.0 <28.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@cekatai/event-sdk 可以商用吗?

@cekatai/event-sdk 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@cekatai/event-sdk 的下载量有多少?

根据 npm 官方下载统计,@cekatai/event-sdk 在 2026/10/10 – 2026/11/08 这 30 天内被下载 9,267 次,日均约 309 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@cekatai/event-sdk 还在维护吗?

最新版本 0.4.0 发布于 2026/09/28(1 个月前);至今共发布 5 个版本,其中过去 12 个月发布了 5 个;首次发布于 2026/09/17。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。