@clickroom/sdk-node 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,49 @@
1
+ # @clickroom/sdk-node
2
+
3
+ Server-side event capture + local feature-flag evaluation for Node 22+. No autocapture — there's no
4
+ DOM to observe server-side, so events are explicit `capture`/`identify` calls.
5
+
6
+ ```ts
7
+ import { ClickroomClient } from '@clickroom/sdk-node'
8
+
9
+ const clickroom = new ClickroomClient({
10
+ writeKey: process.env.CLICKROOM_WRITE_KEY!,
11
+ readKey: process.env.CLICKROOM_READ_KEY, // optional, required for feature flags
12
+ host: 'https://i.clickroom.co',
13
+ })
14
+
15
+ clickroom.capture({
16
+ distinctId: user.id, // required — no client-side persisted identity on the server
17
+ event: 'checkout_completed',
18
+ properties: { plan: 'pro', amount: 49 },
19
+ })
20
+
21
+ clickroom.identify({ distinctId: user.id, properties: { email: user.email, plan: 'pro' } })
22
+
23
+ if (clickroom.isFeatureEnabled('new-checkout', user.id, { plan: 'pro' })) {
24
+ // ...
25
+ }
26
+ ```
27
+
28
+ ## Flags
29
+
30
+ Flags are evaluated **locally**: the client polls `GET /v1/flag-definitions` (requires `readKey`) on
31
+ `flagPollInterval` (default 30s) and evaluates in-process using the same D-05 SHA1 hash as the
32
+ browser SDK and the Rust evaluator — guaranteed identical results for the same `distinct_id`.
33
+
34
+ ## Error handling
35
+
36
+ `capture()`/`identify()` events are batched and POSTed to `{host}/v1/capture`. If the server
37
+ rejects a batch outright (HTTP 4xx other than 429, e.g. a schema-invalid event under strict
38
+ validation), the client does **not** retry — retrying a request the server will never accept
39
+ would just waste the retry budget. The batch is dropped and the failure is surfaced via
40
+ `console.warn`, including the response body (e.g. `{ accepted, errors }`) when the server returns
41
+ one. Network errors, `429` (rate limiting), and `5xx` responses are unaffected — the client
42
+ retries those with its existing bounded exponential backoff (`maxRetries`, default 5).
43
+
44
+ ## Shutdown
45
+
46
+ The client buffers events and flushes on size (`flushAt`, default 20) or interval (`flushInterval`,
47
+ default 10s). It registers `SIGTERM`/`SIGINT`/`beforeExit` handlers to flush on process exit. In
48
+ short-lived scripts (e.g. serverless handlers, CLI tools), call `await clickroom.shutdown()`
49
+ explicitly before the process exits to guarantee delivery.
@@ -0,0 +1,13 @@
1
+ import { type FlagResult } from '@clickroom/sdk-js/eval';
2
+ export interface FlagPollerOptions {
3
+ host: string;
4
+ readKey: string;
5
+ pollInterval: number;
6
+ }
7
+ export interface FlagPoller {
8
+ getFlag(key: string, distinctId: string, personProperties?: Record<string, unknown>): FlagResult;
9
+ getAllFlags(distinctId: string, personProperties?: Record<string, unknown>): Record<string, FlagResult>;
10
+ refresh(): Promise<void>;
11
+ shutdown(): void;
12
+ }
13
+ export declare function createFlagPoller(opts: FlagPollerOptions): FlagPoller;
package/dist/flags.js ADDED
@@ -0,0 +1,45 @@
1
+ // @clickroom/sdk-node — local flag evaluation: poll `GET /flag-definitions`, evaluate in-process
2
+ // with the shared D-05 hash (@clickroom/sdk-js/eval) for parity with the browser SDK + Rust.
3
+ import { evaluate } from '@clickroom/sdk-js/eval';
4
+ export function createFlagPoller(opts) {
5
+ let flags = [];
6
+ let interval = null;
7
+ const url = `${opts.host.replace(/\/+$/, '')}/v1/flag-definitions`;
8
+ const refresh = async () => {
9
+ try {
10
+ const res = await fetch(url, {
11
+ headers: { authorization: `Bearer ${opts.readKey}` },
12
+ });
13
+ if (!res.ok)
14
+ return; // keep last-known-good defs on failure
15
+ const body = (await res.json());
16
+ flags = body.flags ?? [];
17
+ }
18
+ catch {
19
+ // network/parse failure — keep whatever defs we already have
20
+ }
21
+ };
22
+ const getFlag = (key, distinctId, personProperties = {}) => {
23
+ const flag = flags.find((f) => f.key === key);
24
+ if (!flag)
25
+ return { enabled: false, variant: null, payload: null };
26
+ return evaluate(flag, distinctId, personProperties);
27
+ };
28
+ const getAllFlags = (distinctId, personProperties = {}) => {
29
+ const out = {};
30
+ for (const flag of flags) {
31
+ out[flag.key] = evaluate(flag, distinctId, personProperties);
32
+ }
33
+ return out;
34
+ };
35
+ const shutdown = () => {
36
+ if (interval !== null) {
37
+ clearInterval(interval);
38
+ interval = null;
39
+ }
40
+ };
41
+ interval = setInterval(() => void refresh(), opts.pollInterval);
42
+ interval.unref?.();
43
+ return { getFlag, getAllFlags, refresh, shutdown };
44
+ }
45
+ //# sourceMappingURL=flags.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"flags.js","sourceRoot":"","sources":["../src/flags.ts"],"names":[],"mappings":"AAAA,iGAAiG;AACjG,6FAA6F;AAE7F,OAAO,EAAE,QAAQ,EAA8B,MAAM,wBAAwB,CAAA;AAuB7E,MAAM,UAAU,gBAAgB,CAAC,IAAuB;IACtD,IAAI,KAAK,GAAW,EAAE,CAAA;IACtB,IAAI,QAAQ,GAA0C,IAAI,CAAA;IAE1D,MAAM,GAAG,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,sBAAsB,CAAA;IAElE,MAAM,OAAO,GAAG,KAAK,IAAmB,EAAE;QACxC,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE;gBAC3B,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,IAAI,CAAC,OAAO,EAAE,EAAE;aACrD,CAAC,CAAA;YACF,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,OAAM,CAAC,uCAAuC;YAC3D,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAA;YAC1D,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,EAAE,CAAA;QAC1B,CAAC;QAAC,MAAM,CAAC;YACP,6DAA6D;QAC/D,CAAC;IACH,CAAC,CAAA;IAED,MAAM,OAAO,GAAG,CACd,GAAW,EACX,UAAkB,EAClB,mBAA4C,EAAE,EAClC,EAAE;QACd,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,KAAK,GAAG,CAAC,CAAA;QAC7C,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,CAAA;QAClE,OAAO,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAA;IACrD,CAAC,CAAA;IAED,MAAM,WAAW,GAAG,CAClB,UAAkB,EAClB,mBAA4C,EAAE,EAClB,EAAE;QAC9B,MAAM,GAAG,GAA+B,EAAE,CAAA;QAC1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,UAAU,EAAE,gBAAgB,CAAC,CAAA;QAC9D,CAAC;QACD,OAAO,GAAG,CAAA;IACZ,CAAC,CAAA;IAED,MAAM,QAAQ,GAAG,GAAS,EAAE;QAC1B,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,aAAa,CAAC,QAAQ,CAAC,CAAA;YACvB,QAAQ,GAAG,IAAI,CAAA;QACjB,CAAC;IACH,CAAC,CAAA;IAED,QAAQ,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,OAAO,EAAE,EAAE,IAAI,CAAC,YAAY,CAAC,CAAA;IAC/D,QAAQ,CAAC,KAAK,EAAE,EAAE,CAAA;IAElB,OAAO,EAAE,OAAO,EAAE,WAAW,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAA;AACpD,CAAC"}
@@ -0,0 +1,20 @@
1
+ import type { FlagResult } from '@clickroom/sdk-js/eval';
2
+ import type { CaptureArgs, ClickroomNodeOptions, IdentifyArgs } from './types';
3
+ export declare class ClickroomClient {
4
+ private readonly transport;
5
+ private readonly flagPoller;
6
+ private shuttingDown;
7
+ private readonly onSignal;
8
+ constructor(opts: ClickroomNodeOptions);
9
+ capture(args: CaptureArgs): void;
10
+ identify(args: IdentifyArgs): void;
11
+ isFeatureEnabled(key: string, distinctId: string, personProperties?: Record<string, unknown>): boolean;
12
+ getFeatureFlag(key: string, distinctId: string, personProperties?: Record<string, unknown>): boolean | string;
13
+ getFeatureFlagPayload(key: string, distinctId: string, personProperties?: Record<string, unknown>): unknown;
14
+ getAllFlags(distinctId: string, personProperties?: Record<string, unknown>): Record<string, FlagResult>;
15
+ flush(): Promise<void>;
16
+ shutdown(): Promise<void>;
17
+ private requireFlags;
18
+ }
19
+ export type { FlagResult } from '@clickroom/sdk-js/eval';
20
+ export type { CaptureArgs, ClickroomNodeOptions, IdentifyArgs } from './types';
package/dist/index.js ADDED
@@ -0,0 +1,93 @@
1
+ // @clickroom/sdk-node — server-side event capture + local flag evaluation.
2
+ // No autocapture (no DOM to observe server-side) — capture/identify are explicit, distinct_id
3
+ // is required per call (no client-side persisted identity like the browser SDK).
4
+ import { createFlagPoller } from './flags';
5
+ import { createTransport } from './transport';
6
+ const DEFAULT_HOST = 'https://i.clickroom.co';
7
+ const DEFAULT_FLUSH_AT = 20;
8
+ const DEFAULT_FLUSH_INTERVAL = 10_000;
9
+ const DEFAULT_MAX_RETRIES = 5;
10
+ const DEFAULT_FLAG_POLL_INTERVAL = 30_000;
11
+ export class ClickroomClient {
12
+ transport;
13
+ flagPoller;
14
+ shuttingDown = false;
15
+ onSignal;
16
+ constructor(opts) {
17
+ const host = opts.host ?? DEFAULT_HOST;
18
+ this.transport = createTransport({
19
+ host,
20
+ writeKey: opts.writeKey,
21
+ flushAt: opts.flushAt ?? DEFAULT_FLUSH_AT,
22
+ flushInterval: opts.flushInterval ?? DEFAULT_FLUSH_INTERVAL,
23
+ maxRetries: opts.maxRetries ?? DEFAULT_MAX_RETRIES,
24
+ });
25
+ this.flagPoller = opts.readKey
26
+ ? createFlagPoller({
27
+ host,
28
+ readKey: opts.readKey,
29
+ pollInterval: opts.flagPollInterval ?? DEFAULT_FLAG_POLL_INTERVAL,
30
+ })
31
+ : null;
32
+ void this.flagPoller?.refresh();
33
+ this.onSignal = () => {
34
+ void this.shutdown();
35
+ };
36
+ process.once('SIGTERM', this.onSignal);
37
+ process.once('SIGINT', this.onSignal);
38
+ process.once('beforeExit', this.onSignal);
39
+ }
40
+ capture(args) {
41
+ this.transport.enqueue({
42
+ event: args.event,
43
+ distinct_id: args.distinctId,
44
+ timestamp: toIso(args.timestamp),
45
+ properties: args.properties,
46
+ });
47
+ }
48
+ identify(args) {
49
+ this.transport.enqueue({
50
+ event: '$identify',
51
+ distinct_id: args.distinctId,
52
+ properties: { $set: args.properties },
53
+ });
54
+ }
55
+ isFeatureEnabled(key, distinctId, personProperties) {
56
+ return this.requireFlags().getFlag(key, distinctId, personProperties).enabled;
57
+ }
58
+ getFeatureFlag(key, distinctId, personProperties) {
59
+ const result = this.requireFlags().getFlag(key, distinctId, personProperties);
60
+ return result.variant ?? result.enabled;
61
+ }
62
+ getFeatureFlagPayload(key, distinctId, personProperties) {
63
+ return this.requireFlags().getFlag(key, distinctId, personProperties).payload;
64
+ }
65
+ getAllFlags(distinctId, personProperties) {
66
+ return this.requireFlags().getAllFlags(distinctId, personProperties);
67
+ }
68
+ async flush() {
69
+ await this.transport.flush();
70
+ }
71
+ async shutdown() {
72
+ if (this.shuttingDown)
73
+ return;
74
+ this.shuttingDown = true;
75
+ process.removeListener('SIGTERM', this.onSignal);
76
+ process.removeListener('SIGINT', this.onSignal);
77
+ process.removeListener('beforeExit', this.onSignal);
78
+ this.flagPoller?.shutdown();
79
+ await this.transport.shutdown();
80
+ }
81
+ requireFlags() {
82
+ if (!this.flagPoller) {
83
+ throw new Error('@clickroom/sdk-node: flags require `readKey` in ClickroomClient options');
84
+ }
85
+ return this.flagPoller;
86
+ }
87
+ }
88
+ const toIso = (ts) => {
89
+ if (ts === undefined)
90
+ return undefined;
91
+ return typeof ts === 'string' ? ts : ts.toISOString();
92
+ };
93
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import type { EventPayload } from './types';
2
+ export interface TransportOptions {
3
+ host: string;
4
+ writeKey: string;
5
+ flushAt: number;
6
+ flushInterval: number;
7
+ maxRetries: number;
8
+ }
9
+ export interface Transport {
10
+ enqueue(event: EventPayload): void;
11
+ flush(): Promise<void>;
12
+ shutdown(): Promise<void>;
13
+ }
14
+ export declare function createTransport(opts: TransportOptions): Transport;
@@ -0,0 +1,90 @@
1
+ // @clickroom/sdk-node — batching transport (POST /capture), bounded exponential-backoff retry.
2
+ // Mirrors packages/sdk-js/src/transport.ts semantics, minus beacon/visibility (no DOM in Node).
3
+ const BATCH_MAX = 100; // contracts/capture-api.md: <=100 events/batch
4
+ const RETRY_BASE_MS = 1000;
5
+ const RETRY_CAP_MS = 30_000;
6
+ export function createTransport(opts) {
7
+ let buffer = [];
8
+ let interval = null;
9
+ const url = `${opts.host.replace(/\/+$/, '')}/v1/capture`;
10
+ // 4xx (other than 429, which is rate-limiting and retriable) means the
11
+ // server will never accept this batch as-is (e.g. schema-invalid events) —
12
+ // retrying is pointless. Surface it via console.warn (mirroring
13
+ // packages/sdk-js/src/transport.ts's equivalent behavior) instead of
14
+ // silently dropping it.
15
+ const reportRejection = async (res, batch) => {
16
+ let detail;
17
+ try {
18
+ detail = await res.json();
19
+ }
20
+ catch {
21
+ // Non-JSON or empty body — surface status only.
22
+ }
23
+ console.warn(`[clickroom] capture() batch rejected by server (HTTP ${res.status}); dropping ${batch.length} event(s)`, detail);
24
+ };
25
+ const sendBatch = async (batch) => {
26
+ let attempt = 0;
27
+ for (;;) {
28
+ try {
29
+ const res = await fetch(url, {
30
+ method: 'POST',
31
+ headers: {
32
+ 'content-type': 'application/json',
33
+ authorization: `Bearer ${opts.writeKey}`,
34
+ },
35
+ body: JSON.stringify({ batch }),
36
+ });
37
+ if (res.ok)
38
+ return;
39
+ if (res.status === 429 || res.status >= 500) {
40
+ if (attempt >= opts.maxRetries)
41
+ return; // drop after exhausting retries
42
+ const retryAfter = Number(res.headers.get('retry-after'));
43
+ const delay = Number.isFinite(retryAfter) && retryAfter >= 0
44
+ ? retryAfter * 1000
45
+ : Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_CAP_MS);
46
+ await new Promise((r) => setTimeout(r, delay));
47
+ attempt += 1;
48
+ continue;
49
+ }
50
+ // 4xx other than 429 — permanent failure, do not retry.
51
+ await reportRejection(res, batch);
52
+ return;
53
+ }
54
+ catch {
55
+ if (attempt >= opts.maxRetries)
56
+ return;
57
+ const delay = Math.min(RETRY_BASE_MS * 2 ** attempt, RETRY_CAP_MS);
58
+ await new Promise((r) => setTimeout(r, delay));
59
+ attempt += 1;
60
+ }
61
+ }
62
+ };
63
+ const flush = async () => {
64
+ if (buffer.length === 0)
65
+ return;
66
+ const pending = buffer;
67
+ buffer = [];
68
+ const chunks = [];
69
+ for (let i = 0; i < pending.length; i += BATCH_MAX) {
70
+ chunks.push(pending.slice(i, i + BATCH_MAX));
71
+ }
72
+ await Promise.all(chunks.map(sendBatch));
73
+ };
74
+ const enqueue = (event) => {
75
+ buffer.push(event);
76
+ if (buffer.length >= opts.flushAt)
77
+ void flush();
78
+ };
79
+ const shutdown = async () => {
80
+ if (interval !== null) {
81
+ clearInterval(interval);
82
+ interval = null;
83
+ }
84
+ await flush();
85
+ };
86
+ interval = setInterval(() => void flush(), opts.flushInterval);
87
+ interval.unref?.();
88
+ return { enqueue, flush, shutdown };
89
+ }
90
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,32 @@
1
+ export interface ClickroomNodeOptions {
2
+ /** Project write key — required for `/capture`. */
3
+ writeKey: string;
4
+ /** Project read key — required for local flag evaluation (`GET /flag-definitions`). */
5
+ readKey?: string;
6
+ /** API host, e.g. `https://i.clickroom.co`. */
7
+ host?: string;
8
+ /** Flush the event buffer once it reaches this many events. Default 20. */
9
+ flushAt?: number;
10
+ /** Flush the event buffer on this interval (ms). Default 10_000. */
11
+ flushInterval?: number;
12
+ /** Max retry attempts per failed batch before it is dropped. Default 5. */
13
+ maxRetries?: number;
14
+ /** Poll interval (ms) for `GET /flag-definitions`. Default 30_000. */
15
+ flagPollInterval?: number;
16
+ }
17
+ export interface EventPayload {
18
+ event: string;
19
+ distinct_id: string;
20
+ timestamp?: string;
21
+ properties?: Record<string, unknown>;
22
+ }
23
+ export interface CaptureArgs {
24
+ distinctId: string;
25
+ event: string;
26
+ properties?: Record<string, unknown>;
27
+ timestamp?: Date | string;
28
+ }
29
+ export interface IdentifyArgs {
30
+ distinctId: string;
31
+ properties: Record<string, unknown>;
32
+ }
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@clickroom/sdk-node",
3
+ "version": "0.1.0",
4
+ "description": "Node.js server SDK for Clickroom — product analytics, feature flags and experiments.",
5
+ "license": "MIT",
6
+ "homepage": "https://github.com/room-co/clickroom/tree/main/packages/sdk-node#readme",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/room-co/clickroom.git",
10
+ "directory": "packages/sdk-node"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/room-co/clickroom/issues"
14
+ },
15
+ "keywords": [
16
+ "analytics",
17
+ "product-analytics",
18
+ "feature-flags",
19
+ "experiments",
20
+ "ab-testing",
21
+ "clickroom",
22
+ "node"
23
+ ],
24
+ "engines": {
25
+ "node": ">=22"
26
+ },
27
+ "sideEffects": false,
28
+ "type": "module",
29
+ "main": "./dist/index.js",
30
+ "module": "./dist/index.js",
31
+ "types": "./dist/index.d.ts",
32
+ "publishConfig": {
33
+ "access": "public"
34
+ },
35
+ "exports": {
36
+ ".": {
37
+ "types": "./dist/index.d.ts",
38
+ "import": "./dist/index.js"
39
+ }
40
+ },
41
+ "files": [
42
+ "dist"
43
+ ],
44
+ "dependencies": {
45
+ "@clickroom/sdk-js": "0.2.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "22.20.0",
49
+ "typescript": "5.7.3",
50
+ "vitest": "^4.1.9",
51
+ "@clickroom/config": "0.0.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsc",
55
+ "typecheck": "tsc --noEmit",
56
+ "test": "vitest run",
57
+ "clean": "rm -rf dist"
58
+ }
59
+ }