@baukit/analytics-core

中文文档
0.18.3发布于 2026/11/07MITTypeScriptES Module零依赖构建来源已验证近 30 天下载 9069

Provider-neutral, privacy-first product analytics port and safety policy.

原始使用文档
文档目录6 节
  1. Why a port, not a provider wrapper
  2. Typed setup
  3. Consent
  4. Identity
  5. Scrubbing
  6. Buffering and failures

@baukit/analytics-core

Provider-neutral, privacy-first product analytics for Baukit TypeScript applications. The package defines the port and the safety policy; PostHog support belongs in the separate web and native adapter packages. It has no runtime dependencies.

Why a port, not a provider wrapper

Product code should own event meaning and compile-time shape without importing a provider SDK. The core accepts a product event union, applies consent and privacy controls, and sends neutral command envelopes to a Transport. A provider adapter only translates those already-sanitized envelopes.

The required order for captured properties is:

  1. Keep only keys in that event's allowlist.
  2. Scrub blocked keys and sensitive string patterns.
  3. Stamp the required common context and identity.
  4. Enqueue the sanitized envelope for transport.

Typed setup

import {
  AnalyticsClient,
  type AnalyticsEvent,
  type EventAllowlist,
  type Transport,
} from '@baukit/analytics-core';

type ProductEvent =
  | {
      name: 'onboarding_started';
      properties: { source: 'organic' | 'invite' };
    }
  | {
      name: 'onboarding_completed';
      properties: { duration_seconds: number };
    };

const allowlist = {
  onboarding_started: ['source'],
  onboarding_completed: ['duration_seconds'],
} as const satisfies EventAllowlist<ProductEvent>;

declare const transport: Transport<ProductEvent>; // for example, a PostHog adapter

const analytics = new AnalyticsClient<ProductEvent>({
  context: {
    schema_version: 1,
    app: 'architecture-health',
    app_version: '1.0.0',
    platform: 'web',
    environment: 'production',
    locale: 'de-DE',
  },
  allowlist,
  transport,
});

analytics.capture({
  name: 'onboarding_started',
  properties: { source: 'organic' },
});

AnalyticsClientOptions.context is required, so constructing a client without schema_version, app, app_version, platform, environment, or locale does not type-check. Adding a name to ProductEvent also makes the matching allowlist entry required. Runtime input can still contain excess keys (for example, after an unsafe cast or across a JavaScript boundary), so the allowlist drops them before transport.

Event names are a product-owned convention: stable, past-tense snake_case, such as onboarding_completed. Encoding English past tense in template-literal types would reject valid irregular forms and create false confidence. Instead, the client uses a conservative development warning (snake case plus a past-tense heuristic); it never changes runtime delivery. Tracking-plan purpose and ownership remain product responsibilities.

Consent starts as unknown. Captures, identify calls, and alias calls made while consent is unknown or denied are dropped immediately and are never buffered for later opt-in.

analytics.setConsent('granted'); // persist opt-in, then analytics may run
analytics.capture({
  name: 'onboarding_completed',
  properties: { duration_seconds: 42 },
});

analytics.setConsent('denied'); // persist withdrawal and discard every unsent command

setConsent is not itself an analytics event. Consent is read synchronously during construction, before the first capture is possible. InMemoryAnalyticsStorage is the dependency-free default; products should inject a small synchronous AnalyticsStorage backed by device storage. If the platform's native storage API is asynchronous, hydrate a synchronous cache before constructing the client. On React Native, HydratedAnalyticsStorage from @baukit/analytics-posthog-native/storage is that cache; this package does not ship one. Storage failures fail privacy-safe: unreadable consent becomes unknown, and analytics methods do not throw because persistence diagnostics fail.

Withdrawal clears commands that have not been handed to the transport. A batch already in flight cannot be recalled, which is a transport boundary rather than hidden buffering.

Identity

The client creates and persists an anonymous UUID with globalThis.crypto.randomUUID(). Runtimes without it, including some React Native configurations, must inject a cryptographically secure uuidFactory; there is intentionally no Math.random() fallback.

analytics.setConsent('granted');
const anonymousId = analytics.anonymousId;

analytics.identify(internalUserUuid, { plan: 'free' });
analytics.alias(anonymousId, internalUserUuid); // accepted once for this anonymous identity

analytics.reset(); // logout: clear known identity and alias guard, rotate anonymous UUID

Only UUID-shaped user IDs are accepted. This cannot prove which system issued a UUID, so products must pass their internal user UUID—not an email, name, or provider subject. Switching directly between known users is rejected; call reset() first. The alias guard is persisted and admits at most one alias command for an anonymous identity, including across client reconstruction. Traits are scrubbed with the same blocked-key and sensitive-value rules as event properties.

AnalyticsClient stores the consent state, anonymous UUID, known user UUID, and alias guard under storageKeyPrefix. analyticsStorageKeys(prefix) returns those four keys, so a persistent storage adapter can list the ones it keeps.

Scrubbing

The default key blocklist is email, name, token, password, authorization, cookie, phone, and address. Matching is case/separator insensitive and deliberately conservative, so keys such as displayName and auth_token are redacted. Products can add terms through blockedKeys. Email-shaped strings, JWT-shaped strings, and long hex/base64-like strings become "[redacted]". Nested arrays and plain objects are traversed without mutating the input.

Short keys need exact matching, because ip as a substring would also redact zip_code and description. DEFAULT_EXACT_BLOCKED_KEYS redacts ip, ip_address, remote_addr, x_forwarded_for, and x_real_ip only when the normalized key equals one of them. Add product keys, such as q, through exactBlockedKeys on scrubProperties or AnalyticsClient.

scrubProperties(properties, { blockedKeys: ['notes'], exactBlockedKeys: ['q'] });

scrubErrorEvent applies the same rules to a crash report, for example in Sentry's beforeSend. It also redacts the keys in ERROR_EVENT_BLOCKED_KEYS (headers, data, query_string, body, vars, geo, and env) at any depth, which covers request payloads, breadcrumb data, and stack frame variables. String values under ERROR_EVENT_PRESERVED_KEYS stay intact, so event, trace, and span IDs survive the long-hex rule and stack frames keep filename, abs_path, function, and module. The top-level sdk object keeps its keys, so the SDK name stays readable. Other name keys, including OS and device names, are still redacted.

Sentry.init({
  dsn,
  sendDefaultPii: false,
  beforeSend: (event) => scrubErrorEvent(event, { blockedKeys: ['notes'] }),
});

The scrubber is only a last line of defense. Event properties must remain bounded metadata: never health or food values, conversation or prompt content, free text, precise location, credentials, or raw identifying URLs.

Buffering and failures

Accepted commands enter an in-memory queue with these defaults:

  • maximum 100 waiting commands;
  • flush at 20 commands or after 5 seconds;
  • oldest waiting command dropped on overflow;
  • two retries after the initial transport attempt, with short exponential delays;
  • a failed batch dropped after the retry cap.

capture, identify, alias, reset, and setConsent are synchronous and never wait for a provider. Size-triggered delivery is scheduled in a microtask; interval delivery uses an unref'd timer where the runtime supports it. Transport rejection and synchronous transport exceptions are caught internally and never escape into the user journey. onTransportFailure is available for bounded diagnostics, and even exceptions from that callback are contained.

flush() is an explicit best-effort boundary that always resolves, useful for tests and app lifecycle hooks. dispose() performs a best-effort flush and clears its timer. Queues are not persisted: application restarts do not resurrect analytics commands, and unknown/denied events can never appear after later consent.

NoopTransport safely discards commands. InMemoryTransport retains inspectable envelopes for tests. PostHog web and React Native packages plug in by implementing Transport<E> and translating the neutral capture, identify, alias, and reset envelope variants; product code and privacy policy stay unchanged.

版本与发布记录

@baukit/analytics-core 共发布 67 个版本,首次发布于 2026/08/26;过去 12 个月发布了 67 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.18.3latest2026/11/07今天
0.18.22026/11/061 天前
0.18.12026/11/062 天前
0.18.02026/11/052 天前
0.17.12026/11/034 天前
0.17.02026/11/025 天前

关于 @baukit/analytics-core 的常见问题

@baukit/analytics-core 是什么?

Provider-neutral, privacy-first product analytics port and safety policy.

如何安装 @baukit/analytics-core?

在项目目录运行 npm install @baukit/analytics-core,或使用 pnpm add / yarn add / bun add。

@baukit/analytics-core 支持 TypeScript 吗?

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

@baukit/analytics-core 支持 ESM 还是 CommonJS?

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

@baukit/analytics-core 需要什么版本的 Node.js?

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

@baukit/analytics-core 可以商用吗?

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

@baukit/analytics-core 的下载量有多少?

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

@baukit/analytics-core 还在维护吗?

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