@cross-deck/node 0.1.0 → 1.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/CHANGELOG.md +139 -0
- package/README.md +406 -124
- package/dist/auto-events/index.cjs +354 -0
- package/dist/auto-events/index.cjs.map +1 -0
- package/dist/auto-events/index.d.mts +316 -0
- package/dist/auto-events/index.d.ts +316 -0
- package/dist/auto-events/index.mjs +322 -0
- package/dist/auto-events/index.mjs.map +1 -0
- package/dist/crossdeck-server-BXQaFjVx.d.mts +1414 -0
- package/dist/crossdeck-server-BXQaFjVx.d.ts +1414 -0
- package/dist/index.cjs +3069 -179
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +339 -178
- package/dist/index.d.ts +339 -178
- package/dist/index.mjs +3054 -180
- package/dist/index.mjs.map +1 -1
- package/package.json +18 -4
|
@@ -0,0 +1,1414 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
|
|
3
|
+
type CrossdeckErrorType = "authentication_error" | "permission_error" | "invalid_request_error" | "rate_limit_error" | "internal_error" | "network_error" | "configuration_error";
|
|
4
|
+
interface CrossdeckErrorPayload {
|
|
5
|
+
type: CrossdeckErrorType;
|
|
6
|
+
/**
|
|
7
|
+
* Error code. The canonical set is the `CrossdeckErrorCode` literal
|
|
8
|
+
* union exported from `./error-codes` (derived from
|
|
9
|
+
* `CROSSDECK_ERROR_CODES`). Typed as `string` here so the SDK can
|
|
10
|
+
* still surface server-returned codes that aren't (yet) in the
|
|
11
|
+
* catalogue without a wholesale recompile of every consumer.
|
|
12
|
+
*
|
|
13
|
+
* For type-safe code comparisons in caller code, use:
|
|
14
|
+
* `import { CrossdeckErrorCode, isCrossdeckErrorCode } from "@cross-deck/node"`
|
|
15
|
+
* `if (isCrossdeckErrorCode(err.code) && err.code === "webhook_invalid_signature") {}`
|
|
16
|
+
*/
|
|
17
|
+
code: string;
|
|
18
|
+
message: string;
|
|
19
|
+
requestId?: string;
|
|
20
|
+
status?: number;
|
|
21
|
+
retryAfterMs?: number;
|
|
22
|
+
}
|
|
23
|
+
declare class CrossdeckError extends Error {
|
|
24
|
+
readonly type: CrossdeckErrorType;
|
|
25
|
+
readonly code: string;
|
|
26
|
+
readonly requestId?: string;
|
|
27
|
+
readonly status?: number;
|
|
28
|
+
readonly retryAfterMs?: number;
|
|
29
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
30
|
+
/**
|
|
31
|
+
* JSON representation suitable for structured loggers. Without this,
|
|
32
|
+
* `console.log(err)` and most log frameworks (Pino, Winston) emit
|
|
33
|
+
* only `name` + `message` + `stack` — losing `type`, `code`,
|
|
34
|
+
* `requestId`, `status`, `retryAfterMs`. With `toJSON`, calling
|
|
35
|
+
* `JSON.stringify(err)` or passing the error to a logger that
|
|
36
|
+
* serialises via JSON includes the full diagnostic surface.
|
|
37
|
+
*
|
|
38
|
+
* Stripe pattern. Critical for production observability.
|
|
39
|
+
*/
|
|
40
|
+
toJSON(): Record<string, unknown>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Authentication failure — the secret key is missing, invalid, or
|
|
44
|
+
* revoked. Maps to `type: "authentication_error"`. Includes codes:
|
|
45
|
+
* `invalid_secret_key`, `webhook_invalid_signature`,
|
|
46
|
+
* `webhook_replay_window_exceeded`, and any 401 from the backend.
|
|
47
|
+
*
|
|
48
|
+
* if (err instanceof CrossdeckAuthenticationError) { ... }
|
|
49
|
+
*
|
|
50
|
+
* Stripe pattern — typed subclasses make caller error-handling
|
|
51
|
+
* clean and let TypeScript narrow on `instanceof`.
|
|
52
|
+
*/
|
|
53
|
+
declare class CrossdeckAuthenticationError extends CrossdeckError {
|
|
54
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Caller is authenticated but doesn't have permission for the
|
|
58
|
+
* requested resource. Maps to `type: "permission_error"`.
|
|
59
|
+
*/
|
|
60
|
+
declare class CrossdeckPermissionError extends CrossdeckError {
|
|
61
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Request is malformed or violates a validation rule. Maps to
|
|
65
|
+
* `type: "invalid_request_error"`. Includes codes like
|
|
66
|
+
* `missing_user_id`, `missing_event_name`, `serialization_failed`,
|
|
67
|
+
* and any 4xx (other than 401/403/429) from the backend.
|
|
68
|
+
*/
|
|
69
|
+
declare class CrossdeckValidationError extends CrossdeckError {
|
|
70
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Rate limit exceeded. Maps to `type: "rate_limit_error"`. Carries
|
|
74
|
+
* `retryAfterMs` from the server's `Retry-After` header — caller
|
|
75
|
+
* should back off and retry only after that delay.
|
|
76
|
+
*/
|
|
77
|
+
declare class CrossdeckRateLimitError extends CrossdeckError {
|
|
78
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Network-layer failure — `fetch` threw, the request timed out, or
|
|
82
|
+
* the response body was unparseable. Maps to `type: "network_error"`
|
|
83
|
+
* with codes `fetch_failed`, `request_timeout`, or `internal_error`
|
|
84
|
+
* (`invalid_json_response`). Almost always transient; the SDK auto-
|
|
85
|
+
* retries event-queue flushes.
|
|
86
|
+
*/
|
|
87
|
+
declare class CrossdeckNetworkError extends CrossdeckError {
|
|
88
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Backend returned a 5xx or the SDK detected an unexpected
|
|
92
|
+
* internal state. Maps to `type: "internal_error"`.
|
|
93
|
+
*/
|
|
94
|
+
declare class CrossdeckInternalError extends CrossdeckError {
|
|
95
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Misconfigured SDK options at construction time. Maps to
|
|
99
|
+
* `type: "configuration_error"`. Includes codes like
|
|
100
|
+
* `invalid_secret_key`, `webhook_missing_secret`. Never retryable —
|
|
101
|
+
* always a developer fix.
|
|
102
|
+
*/
|
|
103
|
+
declare class CrossdeckConfigurationError extends CrossdeckError {
|
|
104
|
+
constructor(payload: CrossdeckErrorPayload);
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Construct the right `CrossdeckError` subclass for a given payload's
|
|
108
|
+
* `type`. Used by `crossdeckErrorFromResponse` + by any internal call
|
|
109
|
+
* site that throws — gives every thrown error its semantic subclass
|
|
110
|
+
* without forcing every call site to know the mapping.
|
|
111
|
+
*/
|
|
112
|
+
declare function makeCrossdeckError(payload: CrossdeckErrorPayload): CrossdeckError;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Breadcrumb ring buffer — context attached to every error report.
|
|
116
|
+
*
|
|
117
|
+
* Sentry / Datadog / Bugsnag all ship the same idea: keep a rolling
|
|
118
|
+
* record of the last N "things the process did" (HTTP calls, queued
|
|
119
|
+
* events, custom log lines, function invocations). When an error fires,
|
|
120
|
+
* attach the buffer so the engineer reading the error can see exactly
|
|
121
|
+
* how the process got into the broken state. The single most powerful
|
|
122
|
+
* debugging signal in error monitoring — without breadcrumbs, errors
|
|
123
|
+
* are stack traces with no story.
|
|
124
|
+
*
|
|
125
|
+
* Implementation: a circular buffer with a fixed cap. Old entries are
|
|
126
|
+
* evicted as new ones arrive. The default cap (50) is enough to cover
|
|
127
|
+
* ~5 minutes of typical request activity without ballooning the error
|
|
128
|
+
* payload. Sentry uses 100 by default but the SDK is more aggressive
|
|
129
|
+
* about size since we ship breadcrumbs over the wire with every error,
|
|
130
|
+
* not as a separate batch.
|
|
131
|
+
*
|
|
132
|
+
* Verbatim port of `@cross-deck/web/src/breadcrumbs.ts`. The data
|
|
133
|
+
* structure has zero browser dependencies; same code works in Node.
|
|
134
|
+
*
|
|
135
|
+
* Privacy: breadcrumbs from `track()` calls auto-flow through the same
|
|
136
|
+
* property sanitiser (`event-validation.ts`) before reaching this
|
|
137
|
+
* buffer, so a function/symbol/Error-shape in a tracked property won't
|
|
138
|
+
* crash subsequent error reports.
|
|
139
|
+
*/
|
|
140
|
+
type BreadcrumbCategory = "navigation" | "ui.click" | "ui.input" | "http" | "console" | "custom" | "info";
|
|
141
|
+
type BreadcrumbLevel = "debug" | "info" | "warning" | "error";
|
|
142
|
+
interface Breadcrumb {
|
|
143
|
+
/** epoch ms */
|
|
144
|
+
timestamp: number;
|
|
145
|
+
category: BreadcrumbCategory;
|
|
146
|
+
level?: BreadcrumbLevel;
|
|
147
|
+
/** Short human-readable description. */
|
|
148
|
+
message?: string;
|
|
149
|
+
/** Arbitrary key/value context for the crumb. */
|
|
150
|
+
data?: Record<string, unknown>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Stack-trace parser — normalises V8 / Firefox / Safari stack strings
|
|
155
|
+
* into a common frame shape.
|
|
156
|
+
*
|
|
157
|
+
* Why hand-rolled, not `stack-trace-js` / `error-stack-parser`: those
|
|
158
|
+
* weigh 5–15 KB after minification and we'd be pulling in their full
|
|
159
|
+
* feature matrix just for the parser. The patterns below cover the
|
|
160
|
+
* three shapes any modern runtime emits, totalling ~80 lines.
|
|
161
|
+
*
|
|
162
|
+
* Port of `@cross-deck/web/src/stack-parser.ts`. Two differences:
|
|
163
|
+
* 1. `isInAppFrame` heuristics are Node-aware (`node_modules/`,
|
|
164
|
+
* `node:` core URLs, `internal/` Node internals,
|
|
165
|
+
* `@cross-deck/node` self-skip) instead of browser-aware
|
|
166
|
+
* (extension URLs, CDN hostnames).
|
|
167
|
+
* 2. Path separator handling accepts both `/` (Unix / V8 standard)
|
|
168
|
+
* and `\` (Windows native paths sometimes leak into `error.stack`
|
|
169
|
+
* on Node-for-Windows deployments).
|
|
170
|
+
*
|
|
171
|
+
* Defensive: never throws. An unparseable line becomes a `raw` frame
|
|
172
|
+
* with just the literal text. Engineers reading errors still get the
|
|
173
|
+
* raw stack as fallback.
|
|
174
|
+
*/
|
|
175
|
+
interface StackFrame {
|
|
176
|
+
/** Function name, or "?" if anonymous / unparseable. */
|
|
177
|
+
function: string;
|
|
178
|
+
/** Source file URL the frame ran in. Empty when unknown. */
|
|
179
|
+
filename: string;
|
|
180
|
+
/** 1-indexed line number, or 0 when unknown. */
|
|
181
|
+
lineno: number;
|
|
182
|
+
/** 1-indexed column number, or 0 when unknown. */
|
|
183
|
+
colno: number;
|
|
184
|
+
/**
|
|
185
|
+
* True when the frame is in the app's own code (best-effort:
|
|
186
|
+
* detected by URL not in node_modules/, not a node: core URL, etc.).
|
|
187
|
+
* Powers the dashboard's "your code vs library code" view.
|
|
188
|
+
*/
|
|
189
|
+
in_app: boolean;
|
|
190
|
+
/** Raw line from the stack string for debugging when parse fails. */
|
|
191
|
+
raw: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Runtime info enrichment — the Node SDK equivalent of
|
|
196
|
+
* `@cross-deck/web/src/device-info.ts`.
|
|
197
|
+
*
|
|
198
|
+
* Detects the host platform (Lambda / Firebase Functions v1 / v2 /
|
|
199
|
+
* Cloud Run / Vercel / plain Node), region, service name + version,
|
|
200
|
+
* and instance ID. Auto-merged into every event's `properties` and
|
|
201
|
+
* every captured-error's `runtime` block.
|
|
202
|
+
*
|
|
203
|
+
* Privacy posture (parity with web's device-info.ts):
|
|
204
|
+
* - No fingerprinting / hardware identifiers.
|
|
205
|
+
* - No precise geolocation (region only — the platform's own metadata).
|
|
206
|
+
* - No IP collection (backend logs the request IP for rate-limit
|
|
207
|
+
* purposes; not stored on the event document).
|
|
208
|
+
*
|
|
209
|
+
* Detection runs ONCE per process — the returned `RuntimeInfo` is a
|
|
210
|
+
* frozen reference cached at module level. Zero per-event overhead.
|
|
211
|
+
* Caller-supplied overrides (serviceName / serviceVersion / appVersion
|
|
212
|
+
* via `CrossdeckServer` options) win over env-derived values on the
|
|
213
|
+
* first call — that's the SDK constructor.
|
|
214
|
+
*/
|
|
215
|
+
type RuntimeHost = "aws-lambda" | "azure-functions" | "google-app-engine" | "firebase-functions-v1" | "firebase-functions-v2" | "cloud-run" | "vercel" | "netlify" | "heroku" | "render" | "railway" | "fly" | "kubernetes" | "node";
|
|
216
|
+
interface RuntimeInfo {
|
|
217
|
+
nodeVersion: string;
|
|
218
|
+
/** `os.platform()` — "darwin" | "linux" | "win32" | … */
|
|
219
|
+
platform: string;
|
|
220
|
+
/** `os.release()` — kernel release string, e.g. "5.15.0-1071-aws". */
|
|
221
|
+
platformRelease: string;
|
|
222
|
+
hostname: string;
|
|
223
|
+
host: RuntimeHost;
|
|
224
|
+
region: string | null;
|
|
225
|
+
serviceName: string | null;
|
|
226
|
+
serviceVersion: string | null;
|
|
227
|
+
/**
|
|
228
|
+
* Process-stable ID. Lambda log stream name when on Lambda; revision +
|
|
229
|
+
* pid on Cloud Run / Firebase v2; pid as string otherwise. Used by the
|
|
230
|
+
* dashboard to distinguish events from different instances of the same
|
|
231
|
+
* function name + version.
|
|
232
|
+
*/
|
|
233
|
+
instanceId: string | null;
|
|
234
|
+
/** Caller-supplied app version. Attached as `appVersion` on every event. */
|
|
235
|
+
appVersion: string | null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
declare const SDK_NAME = "@cross-deck/node";
|
|
239
|
+
declare const SDK_VERSION = "1.1.0";
|
|
240
|
+
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
241
|
+
declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
242
|
+
/**
|
|
243
|
+
* Pinned Crossdeck API version sent on every request as
|
|
244
|
+
* `Crossdeck-Api-Version`. Forward-compat with backend evolution —
|
|
245
|
+
* server-side breaking changes ship under a new version date; pinning
|
|
246
|
+
* means the SDK keeps speaking the version it was built against until
|
|
247
|
+
* the SDK explicitly bumps. Stripe pattern (`Stripe-Version`).
|
|
248
|
+
*
|
|
249
|
+
* Bump this in lockstep with backend version releases. Document the
|
|
250
|
+
* deprecation policy in CHANGELOG.
|
|
251
|
+
*/
|
|
252
|
+
declare const CROSSDECK_API_VERSION = "2025-01-01";
|
|
253
|
+
interface HttpRetriesConfig {
|
|
254
|
+
/** Max attempts INCLUSIVE of the first call. Default 3 (1 initial + 2 retries). 1 disables retries. */
|
|
255
|
+
maxAttempts?: number;
|
|
256
|
+
/** Statuses considered retryable. Default: 408, 500, 502, 503, 504. */
|
|
257
|
+
retryableStatuses?: number[];
|
|
258
|
+
}
|
|
259
|
+
interface HttpRequestInfo {
|
|
260
|
+
method: "GET" | "POST";
|
|
261
|
+
url: string;
|
|
262
|
+
headers: Record<string, string>;
|
|
263
|
+
/** The serialised body string, when one was set. */
|
|
264
|
+
bodyPreview?: string;
|
|
265
|
+
/** Attempt number, starting at 1. Useful for distinguishing retries. */
|
|
266
|
+
attempt: number;
|
|
267
|
+
}
|
|
268
|
+
interface HttpResponseInfo {
|
|
269
|
+
method: "GET" | "POST";
|
|
270
|
+
url: string;
|
|
271
|
+
status: number;
|
|
272
|
+
durationMs: number;
|
|
273
|
+
attempt: number;
|
|
274
|
+
/** True if the request was a synthetic test-mode response. */
|
|
275
|
+
testMode: boolean;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Public wire types for @cross-deck/node.
|
|
280
|
+
*
|
|
281
|
+
* These mirror the v1 backend API (see backend/src/api/v1-types.ts) so the
|
|
282
|
+
* Node SDK speaks the same JSON shape the Web SDK + dashboard + workers do.
|
|
283
|
+
* Per-module types (Breadcrumb, CapturedError, StackFrame, RuntimeInfo,
|
|
284
|
+
* DebugSignal) live in the module that defines them — types.ts only carries
|
|
285
|
+
* the shared / wire-level surface.
|
|
286
|
+
*
|
|
287
|
+
* Keep in lockstep with `sdks/web/src/types.ts`. Same field names, same
|
|
288
|
+
* nullability. Where Node intentionally diverges from the web wire shape
|
|
289
|
+
* (no anonymousId-by-default, env implied by secret key prefix), the
|
|
290
|
+
* comment explains why.
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
type Environment = "production" | "sandbox";
|
|
294
|
+
type AuditRail = "apple" | "stripe" | "google" | "manual";
|
|
295
|
+
interface PublicEntitlement {
|
|
296
|
+
object: "entitlement";
|
|
297
|
+
key: string;
|
|
298
|
+
isActive: boolean;
|
|
299
|
+
validUntil?: number | null;
|
|
300
|
+
source: {
|
|
301
|
+
rail: AuditRail;
|
|
302
|
+
productId: string;
|
|
303
|
+
subscriptionId: string;
|
|
304
|
+
};
|
|
305
|
+
updatedAt: number;
|
|
306
|
+
}
|
|
307
|
+
interface EntitlementsListResponse {
|
|
308
|
+
object: "list";
|
|
309
|
+
data: PublicEntitlement[];
|
|
310
|
+
crossdeckCustomerId: string;
|
|
311
|
+
env: Environment;
|
|
312
|
+
}
|
|
313
|
+
interface AliasResult {
|
|
314
|
+
object: "alias_result";
|
|
315
|
+
crossdeckCustomerId: string;
|
|
316
|
+
linked: Array<{
|
|
317
|
+
type: "developer";
|
|
318
|
+
id: string;
|
|
319
|
+
} | {
|
|
320
|
+
type: "anonymous";
|
|
321
|
+
id: string;
|
|
322
|
+
}>;
|
|
323
|
+
mergePending: boolean;
|
|
324
|
+
env: Environment;
|
|
325
|
+
}
|
|
326
|
+
interface IngestResponse {
|
|
327
|
+
object: "list";
|
|
328
|
+
received: number;
|
|
329
|
+
env: Environment;
|
|
330
|
+
throttled?: {
|
|
331
|
+
dropped: number;
|
|
332
|
+
sampleRate: number;
|
|
333
|
+
retryAfterMs: number;
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
interface PurchaseResult {
|
|
337
|
+
object: "purchase_result";
|
|
338
|
+
crossdeckCustomerId: string;
|
|
339
|
+
env: Environment;
|
|
340
|
+
entitlements: PublicEntitlement[];
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Response shape from `GET /v1/sdk/heartbeat`. Used by
|
|
344
|
+
* `server.heartbeat()` to validate the secret key at boot and surface
|
|
345
|
+
* the backend's view of which project + app the key maps to. Clock
|
|
346
|
+
* skew between client + server can be detected from `serverTime`.
|
|
347
|
+
*/
|
|
348
|
+
interface HeartbeatResponse {
|
|
349
|
+
object: "heartbeat";
|
|
350
|
+
ok: true;
|
|
351
|
+
projectId: string;
|
|
352
|
+
appId: string;
|
|
353
|
+
platform: "node" | "web" | "ios" | "android";
|
|
354
|
+
env: Environment;
|
|
355
|
+
/** Server's view of `Date.now()` at the moment the response was sent. */
|
|
356
|
+
serverTime: number;
|
|
357
|
+
}
|
|
358
|
+
interface ForgetResult {
|
|
359
|
+
object: "forgot";
|
|
360
|
+
crossdeckCustomerId: string | null;
|
|
361
|
+
queuedAt: number;
|
|
362
|
+
env: Environment;
|
|
363
|
+
}
|
|
364
|
+
/**
|
|
365
|
+
* Options for `new CrossdeckServer(...)`. The trio of `secretKey` (required)
|
|
366
|
+
* + a sensible set of opt-in knobs covers the v1.0.0 surface.
|
|
367
|
+
*
|
|
368
|
+
* Defaults are tuned for serverless deployment (the dominant Node deployment
|
|
369
|
+
* shape today): flush-on-exit ON, error capture ON, entitlement TTL 60s,
|
|
370
|
+
* idempotent retried event queue, generous timeouts.
|
|
371
|
+
*/
|
|
372
|
+
interface CrossdeckServerOptions {
|
|
373
|
+
/** Secret API key. MUST start with `cd_sk_test_` or `cd_sk_live_`. Required. */
|
|
374
|
+
secretKey: string;
|
|
375
|
+
/** Override the API base URL. Default `https://api.cross-deck.com/v1`. */
|
|
376
|
+
baseUrl?: string;
|
|
377
|
+
/**
|
|
378
|
+
* Per-request abort timeout (ms). Default 15_000.
|
|
379
|
+
*
|
|
380
|
+
* On expiry: `CrossdeckError({ type: "network_error", code: "request_timeout" })`.
|
|
381
|
+
* Pass `0` to disable; per-call overrides allowed via the HTTP layer. A
|
|
382
|
+
* captive portal or hung connection would otherwise inherit the runtime's
|
|
383
|
+
* default and lock up the queue.
|
|
384
|
+
*/
|
|
385
|
+
timeoutMs?: number;
|
|
386
|
+
/** Override the SDK version reported on the wire. Default: package version. */
|
|
387
|
+
sdkVersion?: string;
|
|
388
|
+
/**
|
|
389
|
+
* Optional informational appId stamped onto event batches. The server
|
|
390
|
+
* trusts the API key's resolved app routing — this is best-effort metadata,
|
|
391
|
+
* not the source of truth.
|
|
392
|
+
*/
|
|
393
|
+
appId?: string;
|
|
394
|
+
/**
|
|
395
|
+
* Error capture configuration. Default: ON with `onUncaughtException` +
|
|
396
|
+
* `onUnhandledRejection` + `wrapFetch` all enabled.
|
|
397
|
+
*
|
|
398
|
+
* Pass `false` to disable error capture entirely (the SDK still ships
|
|
399
|
+
* the manual `captureError(err)` API, it just doesn't auto-wire process
|
|
400
|
+
* handlers). Pass a partial object to override individual defaults.
|
|
401
|
+
*
|
|
402
|
+
* Setting `false` is the right call if you have a separate error tracker
|
|
403
|
+
* (Sentry, Datadog) and don't want duplicates. Setting `true` (the
|
|
404
|
+
* default) is the right call for everyone else — that's why you installed
|
|
405
|
+
* a backend SDK.
|
|
406
|
+
*/
|
|
407
|
+
errorCapture?: boolean | Partial<ErrorCaptureConfig>;
|
|
408
|
+
/** Maximum events buffered before forced flush. Default 20. Parity with web SDK. */
|
|
409
|
+
eventFlushBatchSize?: number;
|
|
410
|
+
/** Idle ms after the last track() before flushing. Default 1500. Parity with web SDK. */
|
|
411
|
+
eventFlushIntervalMs?: number;
|
|
412
|
+
/**
|
|
413
|
+
* Install `process.on('beforeExit')` + `SIGTERM` + `SIGINT` handlers that
|
|
414
|
+
* synchronously drain the event queue before exit. Default `true`.
|
|
415
|
+
*
|
|
416
|
+
* **Critical for Cloud Functions / Lambda.** Without this, a function
|
|
417
|
+
* cold-starts, fires 3 events, and exits before the HTTP POSTs complete —
|
|
418
|
+
* the events vanish silently. With it, the queue drains bounded by
|
|
419
|
+
* `flushOnExitTimeoutMs` before the process is allowed to terminate.
|
|
420
|
+
*
|
|
421
|
+
* Set `false` only if your runtime already manages SDK shutdown
|
|
422
|
+
* explicitly (some test harnesses, custom signal handlers).
|
|
423
|
+
*/
|
|
424
|
+
flushOnExit?: boolean;
|
|
425
|
+
/**
|
|
426
|
+
* Bounded timeout for the on-exit drain (ms). Default 2000.
|
|
427
|
+
*
|
|
428
|
+
* Two seconds is enough to flush a handful of events over a healthy
|
|
429
|
+
* network without holding up the function teardown so long that the
|
|
430
|
+
* platform's own SIGKILL (typically 5-10s after SIGTERM) preempts us.
|
|
431
|
+
*/
|
|
432
|
+
flushOnExitTimeoutMs?: number;
|
|
433
|
+
/**
|
|
434
|
+
* Fire a heartbeat in the background the moment the SDK is
|
|
435
|
+
* constructed. Default `true`.
|
|
436
|
+
*
|
|
437
|
+
* This is what makes the dashboard's "Verify install" surface
|
|
438
|
+
* actually work in cold-start serverless: the moment the customer's
|
|
439
|
+
* process boots and runs `new CrossdeckServer({...})`, we phone
|
|
440
|
+
* home, the dashboard row flips LIVE, and the caller doesn't have
|
|
441
|
+
* to add an explicit `await server.heartbeat()` to their bootstrap.
|
|
442
|
+
*
|
|
443
|
+
* Fire-and-forget. Failures are swallowed (the SDK still works for
|
|
444
|
+
* events even if this boot ping can't reach the backend). The
|
|
445
|
+
* caller's process never blocks on this.
|
|
446
|
+
*
|
|
447
|
+
* Set `false` if you want the prior v1.0.0 behaviour where the
|
|
448
|
+
* caller controlled when (or whether) the first network ping fired
|
|
449
|
+
* — e.g., very latency-sensitive cold paths, or environments where
|
|
450
|
+
* the very first request must not race with an SDK-initiated call.
|
|
451
|
+
* `testMode: true` also disables this implicitly.
|
|
452
|
+
*/
|
|
453
|
+
bootHeartbeat?: boolean;
|
|
454
|
+
/**
|
|
455
|
+
* TTL for the entitlement cache (ms). Default 60_000 (60s).
|
|
456
|
+
*
|
|
457
|
+
* Once `getEntitlements()` has warmed the cache, subsequent
|
|
458
|
+
* `isEntitled(key)` calls are memory reads for the next `ttlMs` — no
|
|
459
|
+
* HTTP round-trip. Without this, a hot-path entitlement gate adds
|
|
460
|
+
* 50-200ms per request. Stripe + Mixpanel ship the same TTL pattern
|
|
461
|
+
* server-side for the same reason.
|
|
462
|
+
*
|
|
463
|
+
* Pass `0` to disable caching (every `isEntitled` requires a fresh
|
|
464
|
+
* `getEntitlements()` call to populate the cache — useful for tests).
|
|
465
|
+
*/
|
|
466
|
+
entitlementCacheTtlMs?: number;
|
|
467
|
+
/**
|
|
468
|
+
* Service name for runtime enrichment. Attached to every event + error
|
|
469
|
+
* as `properties.serviceName`. Default: env-detected via
|
|
470
|
+
* `K_SERVICE` (Cloud Run / Cloud Functions v2) /
|
|
471
|
+
* `AWS_LAMBDA_FUNCTION_NAME` (Lambda) /
|
|
472
|
+
* `FUNCTION_NAME` (Cloud Functions v1). Falls back to `process.pid` if
|
|
473
|
+
* no env signal is present.
|
|
474
|
+
*/
|
|
475
|
+
serviceName?: string;
|
|
476
|
+
/**
|
|
477
|
+
* Service version. Default: env-detected via `K_REVISION` /
|
|
478
|
+
* `AWS_LAMBDA_FUNCTION_VERSION`. Surfaces in dashboards as the build
|
|
479
|
+
* cohort the event/error originated from.
|
|
480
|
+
*/
|
|
481
|
+
serviceVersion?: string;
|
|
482
|
+
/**
|
|
483
|
+
* App version attached as `appVersion` on every event/error. Parity
|
|
484
|
+
* with `@cross-deck/web`'s `appVersion` option — same role.
|
|
485
|
+
*/
|
|
486
|
+
appVersion?: string;
|
|
487
|
+
/**
|
|
488
|
+
* Enable verbose diagnostic logging via NorthStar §16 debug signal
|
|
489
|
+
* vocabulary. Default `false`. Equivalent to `server.setDebugMode(true)`
|
|
490
|
+
* after construction.
|
|
491
|
+
*/
|
|
492
|
+
debug?: boolean;
|
|
493
|
+
/**
|
|
494
|
+
* Breadcrumb buffer size. Default 50 (parity with web SDK). The last
|
|
495
|
+
* N tracked events + manual breadcrumbs are attached to every error
|
|
496
|
+
* report — "what was the request doing right before it failed."
|
|
497
|
+
*/
|
|
498
|
+
breadcrumbsMaxSize?: number;
|
|
499
|
+
/**
|
|
500
|
+
* **Test mode.** When `true`, every HTTP call short-circuits to a
|
|
501
|
+
* synthetic success response — no network goes out. The synthetic
|
|
502
|
+
* shape matches each endpoint's contract (e.g. `getEntitlements`
|
|
503
|
+
* returns `{ object: "list", data: [], … }`). For caller test
|
|
504
|
+
* suites that don't want to mock `globalThis.fetch` directly.
|
|
505
|
+
*
|
|
506
|
+
* `onRequest` / `onResponse` hooks still fire in test mode so
|
|
507
|
+
* audit pipelines can observe synthetic traffic.
|
|
508
|
+
*
|
|
509
|
+
* Default `false`. Never enable in production.
|
|
510
|
+
*/
|
|
511
|
+
testMode?: boolean;
|
|
512
|
+
/**
|
|
513
|
+
* Inspection hook fired BEFORE every HTTP request (including
|
|
514
|
+
* retries). Use for debugging, audit logging, custom metrics.
|
|
515
|
+
* Synchronous — errors thrown by the hook are swallowed, the
|
|
516
|
+
* request continues.
|
|
517
|
+
*/
|
|
518
|
+
onRequest?: (info: HttpRequestInfo) => void;
|
|
519
|
+
/**
|
|
520
|
+
* Inspection hook fired AFTER every HTTP response. Same contract
|
|
521
|
+
* as `onRequest`. Carries `durationMs`, `attempt` number, and
|
|
522
|
+
* `testMode` flag (true if the response was synthetic).
|
|
523
|
+
*/
|
|
524
|
+
onResponse?: (info: HttpResponseInfo) => void;
|
|
525
|
+
/**
|
|
526
|
+
* Retry config for idempotent GET requests. Default: 3 attempts
|
|
527
|
+
* with exponential backoff + full jitter, retrying on 408 + 5xx
|
|
528
|
+
* (except 501) and on network failures. POST retries are handled
|
|
529
|
+
* by the EventQueue separately (with batch-level Idempotency-Key
|
|
530
|
+
* reuse). Set `maxAttempts: 1` to disable GET retries.
|
|
531
|
+
*/
|
|
532
|
+
httpRetries?: HttpRetriesConfig;
|
|
533
|
+
/**
|
|
534
|
+
* Override the runtime token in the `User-Agent` header
|
|
535
|
+
* (`@cross-deck/node/<sdk-version> <runtimeToken>`). Default
|
|
536
|
+
* detects `node/<process.versions.node> <process.platform>`.
|
|
537
|
+
* Override for custom builds (Bun, Deno-shim, electron) that want
|
|
538
|
+
* to report a more specific runtime label.
|
|
539
|
+
*/
|
|
540
|
+
runtimeToken?: string;
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Per-call options accepted by every async public method on
|
|
544
|
+
* `CrossdeckServer`. Carries cancellation + per-call timeout
|
|
545
|
+
* overrides. Inherits Stripe's pattern of "request options" as a
|
|
546
|
+
* trailing arg.
|
|
547
|
+
*
|
|
548
|
+
* const ctrl = new AbortController();
|
|
549
|
+
* const flight = server.heartbeat({ signal: ctrl.signal });
|
|
550
|
+
* setTimeout(() => ctrl.abort(), 100);
|
|
551
|
+
* await flight; // throws CrossdeckNetworkError({ code: "request_aborted" })
|
|
552
|
+
*/
|
|
553
|
+
interface RequestOptions {
|
|
554
|
+
/**
|
|
555
|
+
* Caller-supplied AbortSignal. When aborted, the in-flight `fetch`
|
|
556
|
+
* is cancelled and the call throws
|
|
557
|
+
* `CrossdeckNetworkError({ code: "request_aborted" })`. Composes
|
|
558
|
+
* with the per-request timeout — whichever fires first wins.
|
|
559
|
+
*/
|
|
560
|
+
signal?: AbortSignal;
|
|
561
|
+
/**
|
|
562
|
+
* Per-call timeout override (ms). Defaults to the client's
|
|
563
|
+
* `timeoutMs`. Pass `0` to disable.
|
|
564
|
+
*/
|
|
565
|
+
timeoutMs?: number;
|
|
566
|
+
}
|
|
567
|
+
interface IdentityHints {
|
|
568
|
+
customerId?: string;
|
|
569
|
+
userId?: string;
|
|
570
|
+
anonymousId?: string;
|
|
571
|
+
}
|
|
572
|
+
interface IdentifyOptions {
|
|
573
|
+
email?: string;
|
|
574
|
+
traits?: Record<string, unknown>;
|
|
575
|
+
}
|
|
576
|
+
interface AliasIdentityInput extends IdentifyOptions {
|
|
577
|
+
userId: string;
|
|
578
|
+
anonymousId: string;
|
|
579
|
+
}
|
|
580
|
+
type ErrorLevel = "error" | "warning" | "info";
|
|
581
|
+
/** Properties payload for `track()`. Arbitrary JSON-serialisable bag, ≤ 8 KB. */
|
|
582
|
+
type EventProperties = Record<string, unknown>;
|
|
583
|
+
interface ServerEvent {
|
|
584
|
+
eventId?: string;
|
|
585
|
+
name: string;
|
|
586
|
+
timestamp?: number;
|
|
587
|
+
properties?: EventProperties;
|
|
588
|
+
developerUserId?: string;
|
|
589
|
+
anonymousId?: string;
|
|
590
|
+
crossdeckCustomerId?: string;
|
|
591
|
+
level?: ErrorLevel;
|
|
592
|
+
tags?: Record<string, string>;
|
|
593
|
+
categoryTags?: string[];
|
|
594
|
+
}
|
|
595
|
+
interface IngestOptions extends RequestOptions {
|
|
596
|
+
idempotencyKey?: string;
|
|
597
|
+
}
|
|
598
|
+
interface SyncPurchaseInput {
|
|
599
|
+
rail?: "apple";
|
|
600
|
+
signedTransactionInfo: string;
|
|
601
|
+
signedRenewalInfo?: string;
|
|
602
|
+
appAccountToken?: string;
|
|
603
|
+
}
|
|
604
|
+
type GrantDuration = "P30D" | "P90D" | "P1Y" | "lifetime";
|
|
605
|
+
interface GrantEntitlementInput {
|
|
606
|
+
customerId: string;
|
|
607
|
+
entitlementKey: string;
|
|
608
|
+
duration: GrantDuration;
|
|
609
|
+
reason: string;
|
|
610
|
+
}
|
|
611
|
+
interface RevokeEntitlementInput {
|
|
612
|
+
customerId: string;
|
|
613
|
+
entitlementKey: string;
|
|
614
|
+
reason: string;
|
|
615
|
+
}
|
|
616
|
+
interface EntitlementMutationResult {
|
|
617
|
+
object: "entitlement_mutation";
|
|
618
|
+
action: "grant" | "revoke";
|
|
619
|
+
crossdeckCustomerId: string;
|
|
620
|
+
entitlement: PublicEntitlement;
|
|
621
|
+
env: Environment;
|
|
622
|
+
}
|
|
623
|
+
type AuditDecision = "applied" | "no_op" | "rejected";
|
|
624
|
+
interface AuditEntry {
|
|
625
|
+
eventId: string;
|
|
626
|
+
rail: AuditRail;
|
|
627
|
+
env: Environment;
|
|
628
|
+
eventType: string;
|
|
629
|
+
projectId: string;
|
|
630
|
+
subscriptionId?: string;
|
|
631
|
+
customerId?: string;
|
|
632
|
+
fromState?: string;
|
|
633
|
+
toState?: string;
|
|
634
|
+
decision: AuditDecision;
|
|
635
|
+
reason?: string;
|
|
636
|
+
derivedSignal?: string;
|
|
637
|
+
signatureVerified: boolean;
|
|
638
|
+
reconciledWithProvider: boolean;
|
|
639
|
+
rawEventReceivedAt: number;
|
|
640
|
+
processedAt: number;
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* Diagnostic snapshot returned by `server.diagnostics()`. Stable shape
|
|
644
|
+
* regardless of init state — callers don't need to narrow.
|
|
645
|
+
*
|
|
646
|
+
* Differs from web SDK's Diagnostics in two ways:
|
|
647
|
+
* - No `anonymousId` / `crossdeckCustomerId` / `developerUserId` (Node
|
|
648
|
+
* SDK has no per-device identity — identity is per-request).
|
|
649
|
+
* - No `clock` block (no heartbeat round-trip — Node SDK doesn't ship
|
|
650
|
+
* one).
|
|
651
|
+
* - Adds `runtime` (Node version, OS, host, region, service) and
|
|
652
|
+
* `errors` (session count, fingerprints tracked).
|
|
653
|
+
*/
|
|
654
|
+
interface Diagnostics {
|
|
655
|
+
sdkVersion: string;
|
|
656
|
+
baseUrl: string;
|
|
657
|
+
/** First 12 chars of the secret key (incl. `cd_sk_test_` / `cd_sk_live_` prefix). For correlation in support tickets. */
|
|
658
|
+
secretKeyPrefix: string;
|
|
659
|
+
env: Environment;
|
|
660
|
+
runtime: {
|
|
661
|
+
nodeVersion: string;
|
|
662
|
+
platform: string;
|
|
663
|
+
hostname: string;
|
|
664
|
+
/** Which serverless / hosting platform the SDK detected. "node" if no platform signal. */
|
|
665
|
+
host: RuntimeHost;
|
|
666
|
+
region: string | null;
|
|
667
|
+
serviceName: string | null;
|
|
668
|
+
serviceVersion: string | null;
|
|
669
|
+
instanceId: string | null;
|
|
670
|
+
};
|
|
671
|
+
entitlements: {
|
|
672
|
+
count: number;
|
|
673
|
+
lastUpdated: number;
|
|
674
|
+
ttlMs: number;
|
|
675
|
+
/** Cumulative count of listener invocations that threw. Swallowed inside the cache; surfaced here. */
|
|
676
|
+
listenerErrors: number;
|
|
677
|
+
};
|
|
678
|
+
events: {
|
|
679
|
+
buffered: number;
|
|
680
|
+
dropped: number;
|
|
681
|
+
inFlight: number;
|
|
682
|
+
lastFlushAt: number;
|
|
683
|
+
lastError: string | null;
|
|
684
|
+
consecutiveFailures: number;
|
|
685
|
+
nextRetryAt: number | null;
|
|
686
|
+
};
|
|
687
|
+
errors: {
|
|
688
|
+
/** Total error reports captured in this process lifetime. */
|
|
689
|
+
sessionCount: number;
|
|
690
|
+
/** Number of distinct fingerprints currently inside the rate-limit window. */
|
|
691
|
+
fingerprintsTracked: number;
|
|
692
|
+
/** Whether the global handlers (uncaughtException / unhandledRejection) are installed. */
|
|
693
|
+
handlersInstalled: boolean;
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
/**
|
|
698
|
+
* Error capture — the third Crossdeck USP, the headline reason backend
|
|
699
|
+
* developers install observability SDKs.
|
|
700
|
+
*
|
|
701
|
+
* Catches every error source a Node process can hand us and ships them
|
|
702
|
+
* as Crossdeck events. The pipeline reuses the analytics queue:
|
|
703
|
+
* - Same retry-with-backoff + Idempotency-Key (duplicate batches
|
|
704
|
+
* dedup server-side)
|
|
705
|
+
* - Same property sanitisation (one bad context blob can't poison
|
|
706
|
+
* the batch)
|
|
707
|
+
* - Same on-the-wire enrichment via runtime-info
|
|
708
|
+
*
|
|
709
|
+
* Error sources captured (each toggleable):
|
|
710
|
+
* 1. `process.on('uncaughtException')` — uncaught synchronous errors
|
|
711
|
+
* 2. `process.on('unhandledRejection')` — unhandled promise rejections
|
|
712
|
+
* 3. `globalThis.fetch` wrap — 5xx + network failures
|
|
713
|
+
* 4. `console.error` wrap (default OFF) — noisy, opt-in
|
|
714
|
+
* 5. `server.captureError(err)` — manual try/catch API
|
|
715
|
+
* 6. `server.captureMessage(msg)` — non-error signals
|
|
716
|
+
*
|
|
717
|
+
* Adapted from `@cross-deck/web/src/error-capture.ts`. Three runtime
|
|
718
|
+
* differences:
|
|
719
|
+
* - `window.onerror` → `process.on('uncaughtException')`. Node's
|
|
720
|
+
* uncaught-exception handler receives an `Error` directly, not an
|
|
721
|
+
* `ErrorEvent` wrapper — `buildFromUnknown` handles both.
|
|
722
|
+
* - `window.onunhandledrejection` → `process.on('unhandledRejection')`.
|
|
723
|
+
* Same shape (the rejection's `reason`); same handler logic.
|
|
724
|
+
* - `XMLHttpRequest` wrap → dropped (no XHR in Node).
|
|
725
|
+
*
|
|
726
|
+
* Defensive design rules (parity with web):
|
|
727
|
+
* - The error handler must NEVER throw — if our own code crashed
|
|
728
|
+
* while reporting an error, we'd take down the host's last-resort
|
|
729
|
+
* error path. Every callback wrapped in try/swallow.
|
|
730
|
+
* - Recursion guard: a `_reporting` flag prevents reporting our own
|
|
731
|
+
* errors recursively forever.
|
|
732
|
+
* - Rate limited per-fingerprint: max N reports per minute to defend
|
|
733
|
+
* against runaway loops (e.g. an error in a per-request middleware).
|
|
734
|
+
* - Session cap (per process lifetime): hard limit after which we
|
|
735
|
+
* stop reporting. The dashboard sees "1 unique error" instead of
|
|
736
|
+
* a million events.
|
|
737
|
+
* - Self-skip for `api.cross-deck.com` requests so a Crossdeck
|
|
738
|
+
* outage doesn't self-amplify back into the queue.
|
|
739
|
+
*/
|
|
740
|
+
|
|
741
|
+
interface CapturedError {
|
|
742
|
+
/** When the error fired (epoch ms). */
|
|
743
|
+
timestamp: number;
|
|
744
|
+
/** error.unhandled | error.unhandledrejection | error.handled | error.message | error.http */
|
|
745
|
+
kind: "error.unhandled" | "error.unhandledrejection" | "error.handled" | "error.message" | "error.http";
|
|
746
|
+
level: ErrorLevel;
|
|
747
|
+
message: string;
|
|
748
|
+
/** The error class name when we have it (TypeError, ReferenceError, etc.) */
|
|
749
|
+
errorType: string | null;
|
|
750
|
+
/** Parsed stack frames, empty when unavailable. */
|
|
751
|
+
frames: StackFrame[];
|
|
752
|
+
/** Raw stack string for fallback display. */
|
|
753
|
+
rawStack: string | null;
|
|
754
|
+
/** djb2 hash of message + top frames — groups identical errors. */
|
|
755
|
+
fingerprint: string;
|
|
756
|
+
/** Snapshot of the breadcrumb buffer at the moment the error fired. */
|
|
757
|
+
breadcrumbs: Breadcrumb[];
|
|
758
|
+
/** Free-form context attached via `server.setContext()`. */
|
|
759
|
+
context: Record<string, unknown>;
|
|
760
|
+
/** Free-form tags attached via `server.setTag()`. */
|
|
761
|
+
tags: Record<string, string>;
|
|
762
|
+
/** Set only on `error.http` — the request that failed. */
|
|
763
|
+
http?: {
|
|
764
|
+
url: string;
|
|
765
|
+
method: string;
|
|
766
|
+
status: number;
|
|
767
|
+
statusText?: string;
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
interface ErrorCaptureConfig {
|
|
771
|
+
/** Master switch. Default true. */
|
|
772
|
+
enabled: boolean;
|
|
773
|
+
/** Hook `process.on('uncaughtException')`. Default true. */
|
|
774
|
+
onUncaughtException: boolean;
|
|
775
|
+
/** Hook `process.on('unhandledRejection')`. Default true. */
|
|
776
|
+
onUnhandledRejection: boolean;
|
|
777
|
+
/** Wrap `globalThis.fetch` to capture 5xx + network failures. Default true. */
|
|
778
|
+
wrapFetch: boolean;
|
|
779
|
+
/** Wrap `console.error`. Default false (noisy). */
|
|
780
|
+
captureConsole: boolean;
|
|
781
|
+
/**
|
|
782
|
+
* Drop errors matching these substrings / regexes. Tested against
|
|
783
|
+
* `message`. Default: empty (Node has no equivalent of the
|
|
784
|
+
* browser's `ResizeObserver` / `Script error.` noise).
|
|
785
|
+
*/
|
|
786
|
+
ignoreErrors: Array<string | RegExp>;
|
|
787
|
+
/**
|
|
788
|
+
* Only capture errors whose top in-app frame filename matches one
|
|
789
|
+
* of these. Empty array means "no allowlist — capture everything".
|
|
790
|
+
*/
|
|
791
|
+
allowPaths: Array<string | RegExp>;
|
|
792
|
+
/**
|
|
793
|
+
* Drop errors whose top frame filename matches any of these.
|
|
794
|
+
* Default: SDK self-skip pattern (`@cross-deck/node`).
|
|
795
|
+
*/
|
|
796
|
+
denyPaths: Array<string | RegExp>;
|
|
797
|
+
/**
|
|
798
|
+
* Sample rate, 0–1. 1.0 = send every error. 0.5 = send half (per
|
|
799
|
+
* fingerprint, deterministically — so a given fingerprint always
|
|
800
|
+
* either always sends or never does, no flapping). Default 1.0.
|
|
801
|
+
*/
|
|
802
|
+
sampleRate: number;
|
|
803
|
+
/**
|
|
804
|
+
* Maximum errors per fingerprint per minute. Defends against
|
|
805
|
+
* runaway loops. Default 5.
|
|
806
|
+
*/
|
|
807
|
+
maxPerFingerprintPerMinute: number;
|
|
808
|
+
/**
|
|
809
|
+
* Total cap per process lifetime, regardless of fingerprint. Hard
|
|
810
|
+
* limit after which we stop reporting. Default 100.
|
|
811
|
+
*/
|
|
812
|
+
maxPerSession: number;
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Super-properties + group analytics — Mixpanel pattern.
|
|
817
|
+
*
|
|
818
|
+
* Super properties are key/value pairs the developer registers ONCE
|
|
819
|
+
* via `server.register({ tenant: "acme" })` that get attached to
|
|
820
|
+
* every subsequent event of THIS SDK instance. They're the single
|
|
821
|
+
* most-used feature in Mixpanel-style analytics: "every event from
|
|
822
|
+
* this process should have `tenant` and `serviceName` on it" instead
|
|
823
|
+
* of remembering to pass them on every `track()` call.
|
|
824
|
+
*
|
|
825
|
+
* Groups are organisational identifiers: a customer might belong to
|
|
826
|
+
* an `org` ("acme"), a `team` ("design"), and a `plan` ("enterprise").
|
|
827
|
+
* Each event carries `$groups.{type}: id` so B2B dashboards can pivot:
|
|
828
|
+
* "Acme's team:design fired 142 paywall_shown events this week".
|
|
829
|
+
*
|
|
830
|
+
* Node port differences from `@cross-deck/web/src/super-properties.ts`:
|
|
831
|
+
* - No `KeyValueStorage` backing. In-memory only. Node processes are
|
|
832
|
+
* short-lived (Lambda freezes between invocations, Cloud Functions
|
|
833
|
+
* tear down containers); super-properties typically belong to the
|
|
834
|
+
* SDK instance lifetime, not persistence-across-process.
|
|
835
|
+
* - The Store reset clears both bags (parity with web's clear()).
|
|
836
|
+
*
|
|
837
|
+
* The store is reset on `server.shutdown()` — both super properties
|
|
838
|
+
* and groups are cleared because their lifetime is tied to the SDK
|
|
839
|
+
* instance, not to the process.
|
|
840
|
+
*/
|
|
841
|
+
interface GroupMembership {
|
|
842
|
+
id: string;
|
|
843
|
+
traits?: Record<string, unknown>;
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Per-customer entitlement cache with TTL — the third Crossdeck USP
|
|
848
|
+
* on the server.
|
|
849
|
+
*
|
|
850
|
+
* Why this exists: server-side gating code looks like
|
|
851
|
+
*
|
|
852
|
+
* if (server.isEntitled(customerId, "pro")) { … }
|
|
853
|
+
*
|
|
854
|
+
* inside a request handler. Without a cache, every request makes an
|
|
855
|
+
* HTTP round-trip to `GET /v1/entitlements?customerId=…` — 50-200ms
|
|
856
|
+
* per request, every request, for every customer. The cache makes
|
|
857
|
+
* `isEntitled()` a `Map.get()` after the first warm.
|
|
858
|
+
*
|
|
859
|
+
* Differences from `@cross-deck/web/src/entitlement-cache.ts`:
|
|
860
|
+
* - **Per-customer**, not singleton. Web SDK has one user per browser
|
|
861
|
+
* tab; Node SDK has many users hitting one server. The cache is
|
|
862
|
+
* keyed by `crossdeckCustomerId`.
|
|
863
|
+
* - **TTL-bounded**. Each customer's entry expires after `ttlMs`
|
|
864
|
+
* (default 60_000) and the next read returns `false` until
|
|
865
|
+
* `getEntitlements()` refreshes. Stripe + Mixpanel ship the same
|
|
866
|
+
* pattern server-side.
|
|
867
|
+
* - **Subscriber API unchanged** — `subscribe(listener)` fires after
|
|
868
|
+
* any mutation (set / clear / per-customer expiry-driven eviction
|
|
869
|
+
* is NOT considered a mutation, by design — listeners shouldn't
|
|
870
|
+
* re-render just because a TTL elapsed).
|
|
871
|
+
*
|
|
872
|
+
* The cache holds only ACTIVE entitlements — `setForCustomer` filters.
|
|
873
|
+
* `isEntitled()` returns `false` when:
|
|
874
|
+
* - the customer has no cached entry
|
|
875
|
+
* - the entry has expired
|
|
876
|
+
* - the requested key isn't in the active set
|
|
877
|
+
*/
|
|
878
|
+
|
|
879
|
+
type EntitlementsListener = (customerId: string, entitlements: PublicEntitlement[]) => void;
|
|
880
|
+
interface EntitlementCacheOptions {
|
|
881
|
+
/** TTL in ms. Default 60_000 (60s). 0 disables caching (every read is cold). */
|
|
882
|
+
ttlMs?: number;
|
|
883
|
+
/**
|
|
884
|
+
* Maximum number of customers cached at once. Long-running multi-tenant
|
|
885
|
+
* servers handling a long tail of customers would otherwise leak Map
|
|
886
|
+
* entries forever. Default 10_000 — enough for any realistic deployment.
|
|
887
|
+
* When the cap is reached, the OLDEST entry (by insertion / refresh
|
|
888
|
+
* order) is evicted to make room. Eviction does NOT fire listeners
|
|
889
|
+
* (passive eviction is not a mutation by design).
|
|
890
|
+
*/
|
|
891
|
+
maxCustomers?: number;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* @cross-deck/node — `CrossdeckServer`, the orchestrator.
|
|
896
|
+
*
|
|
897
|
+
* v1.0.0 expands beyond the v0.1.0 thin HTTP client to ship the three
|
|
898
|
+
* Crossdeck USPs on the server:
|
|
899
|
+
*
|
|
900
|
+
* 1. Errors — `captureError`, `captureMessage`, auto-wired
|
|
901
|
+
* `process.on('uncaughtException')` + `process.on('unhandledRejection')`,
|
|
902
|
+
* `globalThis.fetch` wrap, stack-frame parsing, breadcrumb
|
|
903
|
+
* attachment, fingerprint dedup, rate-limit per fingerprint.
|
|
904
|
+
* [USP 1 — landed v1.0.0]
|
|
905
|
+
*
|
|
906
|
+
* 2. Analytics — `track()` switches from sync-HTTP-per-event to
|
|
907
|
+
* enqueue-and-batch via `EventQueue` (durable, retried, idempotent
|
|
908
|
+
* per batch). `flush-on-exit` drains before Cloud Function / Lambda
|
|
909
|
+
* teardown so events don't vanish silently.
|
|
910
|
+
* [USP 1 ships queue + flush-on-exit; super-props + auto-events
|
|
911
|
+
* arrive in USP 2]
|
|
912
|
+
*
|
|
913
|
+
* 3. Entitlements — TTL-cached `isEntitled()` so hot-path gates are
|
|
914
|
+
* memory reads after first warm.
|
|
915
|
+
* [USP 3 — pending]
|
|
916
|
+
*
|
|
917
|
+
* Cross-cutting: every event + error carries `runtime.*` enrichment
|
|
918
|
+
* (Node version, OS, host, region, function name, instance ID) auto-
|
|
919
|
+
* attached via `collectRuntimeInfo()`.
|
|
920
|
+
*
|
|
921
|
+
* The non-event endpoints (identify / aliasIdentity / forget /
|
|
922
|
+
* getEntitlements / getCustomerEntitlements / syncPurchases /
|
|
923
|
+
* grantEntitlement / revokeEntitlement / getAuditEntry) stay as direct
|
|
924
|
+
* HTTP — they're transactional, not telemetry. Only `track()` changed
|
|
925
|
+
* to queue-based.
|
|
926
|
+
*/
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Typed event names + payloads emitted by `CrossdeckServer`. Caller
|
|
930
|
+
* subscribes via the standard EventEmitter API:
|
|
931
|
+
*
|
|
932
|
+
* server.on("queue.flush_failed", ({ error, attempt }) => { ... });
|
|
933
|
+
* server.once("sdk.shutdown", () => { ... });
|
|
934
|
+
*
|
|
935
|
+
* The typed `on` / `off` / `emit` overloads narrow the listener
|
|
936
|
+
* arguments to the right shape. Untyped event names still work for
|
|
937
|
+
* forward compat with any backend-side additions.
|
|
938
|
+
*/
|
|
939
|
+
interface CrossdeckServerEvents {
|
|
940
|
+
/** Fired once per batch on successful flush. */
|
|
941
|
+
"queue.flush_succeeded": [info: {
|
|
942
|
+
batchSize: number;
|
|
943
|
+
durationMs: number;
|
|
944
|
+
}];
|
|
945
|
+
/** Fired on every failed flush attempt. */
|
|
946
|
+
"queue.flush_failed": [info: {
|
|
947
|
+
error: CrossdeckError | string;
|
|
948
|
+
attempt: number;
|
|
949
|
+
nextRetryMs: number;
|
|
950
|
+
}];
|
|
951
|
+
/** Fired when the queue drops oldest events due to HARD_BUFFER_CAP. */
|
|
952
|
+
"queue.dropped": [info: {
|
|
953
|
+
count: number;
|
|
954
|
+
}];
|
|
955
|
+
/** Fired when the buffer changes size — used by callers wanting backpressure-aware tracking. */
|
|
956
|
+
"queue.buffer_changed": [info: {
|
|
957
|
+
size: number;
|
|
958
|
+
}];
|
|
959
|
+
/** Fired when an error is captured (manual or auto). */
|
|
960
|
+
"error.captured": [info: {
|
|
961
|
+
fingerprint: string;
|
|
962
|
+
kind: string;
|
|
963
|
+
message: string;
|
|
964
|
+
}];
|
|
965
|
+
/** Fired once after `getEntitlements()` warms the cache for a customer. */
|
|
966
|
+
"entitlements.warmed": [info: {
|
|
967
|
+
customerId: string;
|
|
968
|
+
count: number;
|
|
969
|
+
}];
|
|
970
|
+
/** Fired on `shutdown()` / `[Symbol.dispose]` / `[Symbol.asyncDispose]`. */
|
|
971
|
+
"sdk.shutdown": [info: {
|
|
972
|
+
reason: "shutdown" | "dispose" | "asyncDispose";
|
|
973
|
+
}];
|
|
974
|
+
}
|
|
975
|
+
declare class CrossdeckServer extends EventEmitter {
|
|
976
|
+
private readonly http;
|
|
977
|
+
private readonly sdkVersion;
|
|
978
|
+
private readonly baseUrl;
|
|
979
|
+
private readonly appId;
|
|
980
|
+
private readonly env;
|
|
981
|
+
private readonly secretKeyPrefix;
|
|
982
|
+
/**
|
|
983
|
+
* Process-stable pseudo-anonymous ID. Used as the default identity
|
|
984
|
+
* for `track()` / `captureError()` calls where the caller doesn't
|
|
985
|
+
* supply one (e.g. an `uncaughtException` handler has no per-request
|
|
986
|
+
* context). Stable for the SDK instance's lifetime so events from
|
|
987
|
+
* the same process correlate.
|
|
988
|
+
*/
|
|
989
|
+
private readonly processAnonymousId;
|
|
990
|
+
private readonly runtime;
|
|
991
|
+
private readonly runtimeProperties;
|
|
992
|
+
private readonly breadcrumbs;
|
|
993
|
+
private readonly eventQueue;
|
|
994
|
+
private readonly errorTracker;
|
|
995
|
+
private readonly flushOnExit;
|
|
996
|
+
private readonly superProps;
|
|
997
|
+
private readonly entitlementCache;
|
|
998
|
+
private readonly debug;
|
|
999
|
+
/**
|
|
1000
|
+
* Alias map — `developerUserId` / `anonymousId` → canonical
|
|
1001
|
+
* `crossdeckCustomerId`. Populated by `getEntitlements()` so a
|
|
1002
|
+
* subsequent `isEntitled({ userId }, "pro")` resolves to the same
|
|
1003
|
+
* cache entry the prior `getEntitlements({ userId })` populated.
|
|
1004
|
+
*
|
|
1005
|
+
* Bounded by `MAX_CUSTOMER_ID_ALIASES` (matches the entitlement
|
|
1006
|
+
* cache's default max-customers for symmetry — if the underlying
|
|
1007
|
+
* cache entry was evicted, a stale alias is dead weight anyway).
|
|
1008
|
+
* Long-running multi-tenant servers handling a long tail of customers
|
|
1009
|
+
* are the failure mode this bound defends against.
|
|
1010
|
+
*/
|
|
1011
|
+
private customerIdAliases;
|
|
1012
|
+
/** Mutable error-state — modified by setTag / setContext / setErrorBeforeSend. */
|
|
1013
|
+
private errorContext;
|
|
1014
|
+
private errorTags;
|
|
1015
|
+
private errorBeforeSend;
|
|
1016
|
+
constructor(options: CrossdeckServerOptions);
|
|
1017
|
+
identify(userId: string, anonymousId: string, options?: IdentifyOptions & RequestOptions): Promise<AliasResult>;
|
|
1018
|
+
aliasIdentity(input: AliasIdentityInput, options?: RequestOptions): Promise<AliasResult>;
|
|
1019
|
+
forget(hints: IdentityHints, options?: RequestOptions): Promise<ForgetResult>;
|
|
1020
|
+
getEntitlements(hints: IdentityHints, options?: RequestOptions): Promise<EntitlementsListResponse>;
|
|
1021
|
+
getCustomerEntitlements(customerId: string, options?: RequestOptions): Promise<EntitlementsListResponse>;
|
|
1022
|
+
/**
|
|
1023
|
+
* Synchronous entitlement check. Returns `true` iff the customer
|
|
1024
|
+
* has the entitlement AND the cache entry is fresh (within
|
|
1025
|
+
* `entitlementCacheTtlMs`, default 60s). Returns `false` when the
|
|
1026
|
+
* cache is cold or expired.
|
|
1027
|
+
*
|
|
1028
|
+
* The hint can be any combination of `customerId` / `userId` /
|
|
1029
|
+
* `anonymousId`. After `getEntitlements({ userId })` populates the
|
|
1030
|
+
* cache, subsequent `isEntitled({ userId }, "pro")` calls within
|
|
1031
|
+
* TTL are memory reads (no HTTP). The "warm cache" pattern that
|
|
1032
|
+
* makes hot-path entitlement gates cheap.
|
|
1033
|
+
*
|
|
1034
|
+
* await server.getEntitlements({ userId }); // warm
|
|
1035
|
+
* if (server.isEntitled({ userId }, "pro")) { // synchronous
|
|
1036
|
+
* // ...
|
|
1037
|
+
* }
|
|
1038
|
+
*
|
|
1039
|
+
* Caller is responsible for re-warming after TTL elapses. The cache
|
|
1040
|
+
* does NOT auto-refresh on read (would block the hot path).
|
|
1041
|
+
*/
|
|
1042
|
+
isEntitled(hint: IdentityHints | string, key: string): boolean;
|
|
1043
|
+
/**
|
|
1044
|
+
* Snapshot of the customer's cached entitlements. Returns `[]` when
|
|
1045
|
+
* the cache is cold or expired. Same hint resolution as
|
|
1046
|
+
* `isEntitled()`.
|
|
1047
|
+
*/
|
|
1048
|
+
listEntitlements(hint: IdentityHints | string): PublicEntitlement[];
|
|
1049
|
+
/**
|
|
1050
|
+
* Subscribe to entitlement-cache mutations. Listener fires after
|
|
1051
|
+
* `getEntitlements()` populates the cache or `shutdown()` clears
|
|
1052
|
+
* it. Returns an idempotent unsubscribe function.
|
|
1053
|
+
*
|
|
1054
|
+
* Used by callers that want to react to entitlement changes (e.g.
|
|
1055
|
+
* a websocket layer notifying connected clients of plan upgrades).
|
|
1056
|
+
* Listener errors are swallowed — surfaced via
|
|
1057
|
+
* `diagnostics().entitlements.listenerErrors`.
|
|
1058
|
+
*/
|
|
1059
|
+
onEntitlementsChange(listener: EntitlementsListener): () => void;
|
|
1060
|
+
/**
|
|
1061
|
+
* Queue an event for batched delivery. Returns synchronously — the
|
|
1062
|
+
* HTTP round-trip happens in the background.
|
|
1063
|
+
*
|
|
1064
|
+
* Behaviour parity with `@cross-deck/web`'s `track()`:
|
|
1065
|
+
* - Synchronous return, void.
|
|
1066
|
+
* - Throws sync on `missing_event_name`.
|
|
1067
|
+
* - Property bag sanitised through `validateEventProperties`.
|
|
1068
|
+
* - Runtime info (`runtime.*`) auto-merged into every event's
|
|
1069
|
+
* properties. Caller-supplied properties win on key collision.
|
|
1070
|
+
* - Breadcrumb auto-emitted (unless the name starts with `error.`,
|
|
1071
|
+
* which would cause a cycle).
|
|
1072
|
+
*
|
|
1073
|
+
* Differences from `@cross-deck/web`:
|
|
1074
|
+
* - Single-argument signature `track(event)` instead of
|
|
1075
|
+
* `track(name, properties)` — the Node wire shape needs the full
|
|
1076
|
+
* `ServerEvent` (identity hint, optional level + tags + categoryTags).
|
|
1077
|
+
* - Auto-fills `anonymousId` with `this.processAnonymousId` when no
|
|
1078
|
+
* identity hint is supplied. A captureError from
|
|
1079
|
+
* `uncaughtException` has no per-request context; without the
|
|
1080
|
+
* auto-fill, the event would be rejected at queue enqueue.
|
|
1081
|
+
*/
|
|
1082
|
+
track(event: ServerEvent): void;
|
|
1083
|
+
/**
|
|
1084
|
+
* Immediate POST of one or more events. For bulk imports / replay
|
|
1085
|
+
* scenarios where the caller wants synchronous confirmation. Bypasses
|
|
1086
|
+
* the queue — no batching, no auto-fill of identity, no
|
|
1087
|
+
* runtime-enrichment.
|
|
1088
|
+
*
|
|
1089
|
+
* Use `track()` for the standard fire-and-forget telemetry path.
|
|
1090
|
+
* Use `ingest()` when you need:
|
|
1091
|
+
* - The IngestResponse synchronously.
|
|
1092
|
+
* - Strict per-event identity validation (no auto-fill).
|
|
1093
|
+
* - Caller-controlled idempotency key.
|
|
1094
|
+
*/
|
|
1095
|
+
ingest(events: ServerEvent[], options?: IngestOptions): Promise<IngestResponse>;
|
|
1096
|
+
/**
|
|
1097
|
+
* Validate the secret key against the Crossdeck API and return the
|
|
1098
|
+
* resolved project + app metadata. Useful at boot to fail fast on a
|
|
1099
|
+
* misconfigured deployment — without this, a wrong secret key only
|
|
1100
|
+
* surfaces on the first event flush attempt, which may be minutes
|
|
1101
|
+
* after process start.
|
|
1102
|
+
*
|
|
1103
|
+
* const { projectId, appId, env, serverTime } = await server.heartbeat();
|
|
1104
|
+
*
|
|
1105
|
+
* Throws `CrossdeckError` on:
|
|
1106
|
+
* - `authentication_error` — secret key invalid / revoked
|
|
1107
|
+
* - `network_error` — couldn't reach the backend
|
|
1108
|
+
* - `request_timeout` — backend slow / unreachable
|
|
1109
|
+
*
|
|
1110
|
+
* Side effect: success records `(serverTime, clientTime)` for clock-
|
|
1111
|
+
* skew detection in `diagnostics().clock` (Phase 2 — not yet exposed
|
|
1112
|
+
* in this SDK release but the data is captured).
|
|
1113
|
+
*
|
|
1114
|
+
* Not auto-called. Caller decides whether the trade-off (one extra
|
|
1115
|
+
* boot request + ~50ms p50 latency) is worth the early-failure
|
|
1116
|
+
* signal. For serverless cold-starts, it usually is — cheap
|
|
1117
|
+
* compared to the cost of a silent broken secret in production.
|
|
1118
|
+
*/
|
|
1119
|
+
heartbeat(options?: RequestOptions): Promise<HeartbeatResponse>;
|
|
1120
|
+
/**
|
|
1121
|
+
* Drain the event queue. Resolves when the in-flight batch completes
|
|
1122
|
+
* (success or failure). On failure, events stay queued for the next
|
|
1123
|
+
* scheduled retry — the resolved promise does NOT throw.
|
|
1124
|
+
*
|
|
1125
|
+
* Typical callers:
|
|
1126
|
+
* - End of a Lambda handler: `await server.flush()` before return
|
|
1127
|
+
* so events land before the platform freezes the process.
|
|
1128
|
+
* - Express server shutdown: `await server.flush()` inside the
|
|
1129
|
+
* SIGTERM handler.
|
|
1130
|
+
* - Tests: drain between assertions.
|
|
1131
|
+
*
|
|
1132
|
+
* Idempotent — flush on an empty queue is a no-op.
|
|
1133
|
+
*/
|
|
1134
|
+
flush(): Promise<void>;
|
|
1135
|
+
syncPurchases(input: SyncPurchaseInput, options?: RequestOptions): Promise<PurchaseResult>;
|
|
1136
|
+
grantEntitlement(input: GrantEntitlementInput, options?: RequestOptions): Promise<EntitlementMutationResult>;
|
|
1137
|
+
/**
|
|
1138
|
+
* Grant multiple entitlements in one logical call. Backend lacks a
|
|
1139
|
+
* bulk endpoint today, so this is a client-side fan-out — each
|
|
1140
|
+
* grant fires a separate request. Results return as a
|
|
1141
|
+
* settled-promise array so partial failures don't drop the rest:
|
|
1142
|
+
* the caller decides how to handle each `{ ok, value }` /
|
|
1143
|
+
* `{ ok: false, error }` entry.
|
|
1144
|
+
*
|
|
1145
|
+
* Use for ops sweeps (e.g. "grant the entire `pro` tier a one-time
|
|
1146
|
+
* `pro_q1_bonus` entitlement"). The bounded concurrency (default
|
|
1147
|
+
* `maxConcurrency: 5`) avoids hammering the backend; the rate-
|
|
1148
|
+
* limit policy on the server still kicks in if needed.
|
|
1149
|
+
*/
|
|
1150
|
+
bulkGrantEntitlement(grants: GrantEntitlementInput[], options?: RequestOptions & {
|
|
1151
|
+
maxConcurrency?: number;
|
|
1152
|
+
}): Promise<Array<{
|
|
1153
|
+
input: GrantEntitlementInput;
|
|
1154
|
+
ok: true;
|
|
1155
|
+
value: EntitlementMutationResult;
|
|
1156
|
+
} | {
|
|
1157
|
+
input: GrantEntitlementInput;
|
|
1158
|
+
ok: false;
|
|
1159
|
+
error: CrossdeckError;
|
|
1160
|
+
}>>;
|
|
1161
|
+
revokeEntitlement(input: RevokeEntitlementInput, options?: RequestOptions): Promise<EntitlementMutationResult>;
|
|
1162
|
+
/**
|
|
1163
|
+
* Revoke multiple entitlements in one logical call. Same
|
|
1164
|
+
* settled-array contract as `bulkGrantEntitlement` — see that
|
|
1165
|
+
* doc for behaviour notes.
|
|
1166
|
+
*/
|
|
1167
|
+
bulkRevokeEntitlement(revokes: RevokeEntitlementInput[], options?: RequestOptions & {
|
|
1168
|
+
maxConcurrency?: number;
|
|
1169
|
+
}): Promise<Array<{
|
|
1170
|
+
input: RevokeEntitlementInput;
|
|
1171
|
+
ok: true;
|
|
1172
|
+
value: EntitlementMutationResult;
|
|
1173
|
+
} | {
|
|
1174
|
+
input: RevokeEntitlementInput;
|
|
1175
|
+
ok: false;
|
|
1176
|
+
error: CrossdeckError;
|
|
1177
|
+
}>>;
|
|
1178
|
+
getAuditEntry(eventId: string, options?: RequestOptions): Promise<AuditEntry>;
|
|
1179
|
+
/**
|
|
1180
|
+
* Manually capture an error from a try/catch block.
|
|
1181
|
+
*
|
|
1182
|
+
* try { … } catch (err) {
|
|
1183
|
+
* server.captureError(err, { context: { jobId }, tags: { flow: "checkout" } });
|
|
1184
|
+
* }
|
|
1185
|
+
*
|
|
1186
|
+
* The error ships through the same event queue analytics rides on
|
|
1187
|
+
* (retried, idempotent, runtime-enriched). Returns silently — never
|
|
1188
|
+
* throws, even if error capture is disabled.
|
|
1189
|
+
*/
|
|
1190
|
+
captureError(error: unknown, options?: {
|
|
1191
|
+
context?: Record<string, unknown>;
|
|
1192
|
+
tags?: Record<string, string>;
|
|
1193
|
+
level?: ErrorLevel;
|
|
1194
|
+
}): void;
|
|
1195
|
+
/**
|
|
1196
|
+
* Capture a non-error signal as an issue. Sentry's `captureMessage`
|
|
1197
|
+
* pattern — for "we hit the deprecated code path" / "soft-warning
|
|
1198
|
+
* triggered" signals where there's no Error to throw.
|
|
1199
|
+
*/
|
|
1200
|
+
captureMessage(message: string, level?: ErrorLevel): void;
|
|
1201
|
+
/**
|
|
1202
|
+
* Attach a tag to every subsequent error report. Sentry pattern.
|
|
1203
|
+
* Tags are flat string key/value (queryable in the dashboard);
|
|
1204
|
+
* use `setContext()` for structured blobs.
|
|
1205
|
+
*/
|
|
1206
|
+
setTag(key: string, value: string): void;
|
|
1207
|
+
/** Bulk-set tags. Merges with existing tags. */
|
|
1208
|
+
setTags(tags: Record<string, string>): void;
|
|
1209
|
+
/**
|
|
1210
|
+
* Attach a structured context blob to every subsequent error report.
|
|
1211
|
+
* Unlike tags (flat key/value), context is a named bag of arbitrary
|
|
1212
|
+
* JSON-serialisable data.
|
|
1213
|
+
*
|
|
1214
|
+
* server.setContext("cart", { items: 3, total: 42.99 });
|
|
1215
|
+
*/
|
|
1216
|
+
setContext(name: string, data: Record<string, unknown>): void;
|
|
1217
|
+
/**
|
|
1218
|
+
* Add a custom breadcrumb to the rolling buffer. The last 50
|
|
1219
|
+
* breadcrumbs are attached to every subsequent error report —
|
|
1220
|
+
* "what was the request doing right before things broke."
|
|
1221
|
+
*/
|
|
1222
|
+
addBreadcrumb(crumb: Breadcrumb): void;
|
|
1223
|
+
/**
|
|
1224
|
+
* Install a pre-send hook for errors. Return null to drop the report,
|
|
1225
|
+
* or a modified `CapturedError` to scrub fields. Sentry's
|
|
1226
|
+
* `beforeSend` pattern — the only place to add app-specific PII
|
|
1227
|
+
* redaction (auth tokens in URLs, etc.) before the report leaves the
|
|
1228
|
+
* process.
|
|
1229
|
+
*
|
|
1230
|
+
* The hook is called LAST, after rate-limit + sampling + path gates
|
|
1231
|
+
* already passed. A throwing hook falls back to the original error.
|
|
1232
|
+
*/
|
|
1233
|
+
setErrorBeforeSend(hook: ((err: CapturedError) => CapturedError | null) | null): void;
|
|
1234
|
+
/**
|
|
1235
|
+
* Register super-properties — every subsequent event carries these
|
|
1236
|
+
* keys on its `properties` bag automatically. Mixpanel pattern.
|
|
1237
|
+
*
|
|
1238
|
+
* server.register({ tenant: "acme", plan: "pro" });
|
|
1239
|
+
* server.track({ name: "paywall_shown", developerUserId: userId });
|
|
1240
|
+
* // ^ event carries `tenant` + `plan` in properties
|
|
1241
|
+
*
|
|
1242
|
+
* Values that are `null` are deleted (the explicit "stop tracking
|
|
1243
|
+
* this key" idiom). Sanitised through `validateEventProperties` so
|
|
1244
|
+
* a `{ avatar: <Buffer> }` payload can't poison the queue.
|
|
1245
|
+
*
|
|
1246
|
+
* Returns a defensive snapshot of the resulting bag.
|
|
1247
|
+
*
|
|
1248
|
+
* **Multi-tenant servers — read carefully.** Super-properties are
|
|
1249
|
+
* PROCESS-SCOPED. In a single Node process handling requests for
|
|
1250
|
+
* many tenants (the common multi-tenant SaaS shape), calling
|
|
1251
|
+
* `server.register({ tenant: "acme" })` taints EVERY subsequent
|
|
1252
|
+
* event from that process — including ones serving tenant "beta".
|
|
1253
|
+
* That's almost never what you want.
|
|
1254
|
+
*
|
|
1255
|
+
* The right pattern for per-request properties is to pass them on
|
|
1256
|
+
* the `track()` call itself:
|
|
1257
|
+
*
|
|
1258
|
+
* server.track({
|
|
1259
|
+
* name: "paywall_shown",
|
|
1260
|
+
* developerUserId: req.user.id,
|
|
1261
|
+
* properties: { tenant: req.tenantId, plan: req.user.plan },
|
|
1262
|
+
* });
|
|
1263
|
+
*
|
|
1264
|
+
* Reserve `register()` for properties that genuinely apply to every
|
|
1265
|
+
* event from this process — e.g. service version, region, build
|
|
1266
|
+
* commit. For those, `runtime-info` already provides
|
|
1267
|
+
* `runtime.serviceVersion` etc. automatically.
|
|
1268
|
+
*/
|
|
1269
|
+
register(properties: Record<string, unknown>): Record<string, unknown>;
|
|
1270
|
+
/** Remove a single super-property key. Idempotent. */
|
|
1271
|
+
unregister(key: string): void;
|
|
1272
|
+
/** Snapshot of the current super-property bag. */
|
|
1273
|
+
getSuperProperties(): Record<string, unknown>;
|
|
1274
|
+
/**
|
|
1275
|
+
* Associate the current SDK instance with a group (org, team,
|
|
1276
|
+
* account, plan). Mixpanel / Segment Group Analytics pattern.
|
|
1277
|
+
*
|
|
1278
|
+
* server.group("org", "acme_inc");
|
|
1279
|
+
* server.group("team", "design", { headcount: 12 });
|
|
1280
|
+
*
|
|
1281
|
+
* Once set, every subsequent event carries `$groups.<type>: id` on
|
|
1282
|
+
* its `properties` bag, enabling B2B dashboard pivots. Pass
|
|
1283
|
+
* `id: null` to clear a group membership.
|
|
1284
|
+
*
|
|
1285
|
+
* Group traits are sanitised through `validateEventProperties`.
|
|
1286
|
+
*/
|
|
1287
|
+
group(type: string, id: string | null, traits?: Record<string, unknown>): void;
|
|
1288
|
+
/** Snapshot of current group memberships keyed by type. */
|
|
1289
|
+
getGroups(): Record<string, GroupMembership>;
|
|
1290
|
+
diagnostics(): Diagnostics;
|
|
1291
|
+
/**
|
|
1292
|
+
* Tear down handlers and clear in-memory state. Tests + custom
|
|
1293
|
+
* lifecycle callers only. Production code should rely on
|
|
1294
|
+
* `flush-on-exit` instead.
|
|
1295
|
+
*/
|
|
1296
|
+
shutdown(reason?: "shutdown" | "dispose" | "asyncDispose"): void;
|
|
1297
|
+
/**
|
|
1298
|
+
* Convert a `CapturedError` into a `ServerEvent` and push through
|
|
1299
|
+
* `track()`. Goes through the same queue / enrichment / breadcrumb
|
|
1300
|
+
* pipeline analytics events do.
|
|
1301
|
+
*/
|
|
1302
|
+
on<K extends keyof CrossdeckServerEvents>(event: K, listener: (...args: CrossdeckServerEvents[K]) => void): this;
|
|
1303
|
+
on(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
1304
|
+
once<K extends keyof CrossdeckServerEvents>(event: K, listener: (...args: CrossdeckServerEvents[K]) => void): this;
|
|
1305
|
+
once(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
1306
|
+
off<K extends keyof CrossdeckServerEvents>(event: K, listener: (...args: CrossdeckServerEvents[K]) => void): this;
|
|
1307
|
+
off(event: string | symbol, listener: (...args: unknown[]) => void): this;
|
|
1308
|
+
emit<K extends keyof CrossdeckServerEvents>(event: K, ...args: CrossdeckServerEvents[K]): boolean;
|
|
1309
|
+
emit(event: string | symbol, ...args: unknown[]): boolean;
|
|
1310
|
+
/**
|
|
1311
|
+
* Synchronous readiness check — "is the SDK in a state where it
|
|
1312
|
+
* should accept new traffic?". Used by Kubernetes readiness probes
|
|
1313
|
+
* and backpressure-aware callers.
|
|
1314
|
+
*
|
|
1315
|
+
* Returns `false` if:
|
|
1316
|
+
* - The event queue is in a sustained retry storm
|
|
1317
|
+
* (`consecutiveFailures >= 5`).
|
|
1318
|
+
* - The event queue's buffered count is at >= 80% of HARD_BUFFER_CAP.
|
|
1319
|
+
*
|
|
1320
|
+
* Otherwise `true`. The default isn't "perfectly healthy" — the
|
|
1321
|
+
* SDK is happy to enqueue events even during transient flush
|
|
1322
|
+
* failures because the queue's retry path handles them. Only
|
|
1323
|
+
* sustained failure flips this to `false`.
|
|
1324
|
+
*/
|
|
1325
|
+
isReady(): boolean;
|
|
1326
|
+
/**
|
|
1327
|
+
* Async wait until `isReady()` returns true OR the timeout elapses.
|
|
1328
|
+
* Resolves `true` on ready, `false` on timeout. Polls every 50ms by
|
|
1329
|
+
* default — backpressure for callers writing high-volume servers.
|
|
1330
|
+
*
|
|
1331
|
+
* if (!(await server.awaitReady(2000))) {
|
|
1332
|
+
* // shed load — SDK is in a retry storm, don't queue more
|
|
1333
|
+
* }
|
|
1334
|
+
*/
|
|
1335
|
+
awaitReady(timeoutMs?: number, pollIntervalMs?: number): Promise<boolean>;
|
|
1336
|
+
/**
|
|
1337
|
+
* Snapshot for Kubernetes liveness + readiness probes. `healthy`
|
|
1338
|
+
* stays true unless the SDK is in a catastrophic state (which
|
|
1339
|
+
* currently can't happen without crashing the process). `ready`
|
|
1340
|
+
* matches `isReady()`.
|
|
1341
|
+
*
|
|
1342
|
+
* app.get("/healthz", (_req, res) => {
|
|
1343
|
+
* const h = server.getHealth();
|
|
1344
|
+
* res.status(h.healthy ? 200 : 503).json(h);
|
|
1345
|
+
* });
|
|
1346
|
+
*/
|
|
1347
|
+
getHealth(): {
|
|
1348
|
+
ready: boolean;
|
|
1349
|
+
healthy: boolean;
|
|
1350
|
+
bufferedEvents: number;
|
|
1351
|
+
inFlight: number;
|
|
1352
|
+
consecutiveFailures: number;
|
|
1353
|
+
lastFlushAt: number;
|
|
1354
|
+
lastError: string | null;
|
|
1355
|
+
errorHandlersInstalled: boolean;
|
|
1356
|
+
};
|
|
1357
|
+
/**
|
|
1358
|
+
* Sync disposal hook — runs when a `using` declaration exits scope
|
|
1359
|
+
* (TC39 explicit-resource-management, Node 20+ / TS 5.2+).
|
|
1360
|
+
*
|
|
1361
|
+
* using server = new CrossdeckServer({ ... });
|
|
1362
|
+
* // ... use server ...
|
|
1363
|
+
* // at end of block, server[Symbol.dispose]() runs automatically
|
|
1364
|
+
*
|
|
1365
|
+
* `Symbol.dispose` is synchronous so we can't await `flush()` here
|
|
1366
|
+
* — for that, use `await using` + `[Symbol.asyncDispose]()`. This
|
|
1367
|
+
* sync variant just calls `shutdown()` (handler cleanup +
|
|
1368
|
+
* in-memory state wipe).
|
|
1369
|
+
*/
|
|
1370
|
+
[Symbol.dispose](): void;
|
|
1371
|
+
/**
|
|
1372
|
+
* Async disposal hook — runs when an `await using` declaration
|
|
1373
|
+
* exits scope. Awaits `flush()` THEN runs `shutdown()`. Use this
|
|
1374
|
+
* variant when the caller needs the queue drained before exit
|
|
1375
|
+
* (the common case for serverless handlers).
|
|
1376
|
+
*
|
|
1377
|
+
* await using server = new CrossdeckServer({ ... });
|
|
1378
|
+
*/
|
|
1379
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
1380
|
+
private reportCapturedError;
|
|
1381
|
+
/**
|
|
1382
|
+
* Populate the entitlement cache from a fresh server response.
|
|
1383
|
+
* Records aliases so `userId` / `anonymousId` hints supplied to
|
|
1384
|
+
* `getEntitlements()` resolve to the same cache entry on subsequent
|
|
1385
|
+
* `isEntitled({ userId }, ...)` calls.
|
|
1386
|
+
*
|
|
1387
|
+
* Bounds the alias map at MAX_CUSTOMER_ID_ALIASES — once full, the
|
|
1388
|
+
* oldest aliases (by insertion order) are evicted FIFO. Symmetric
|
|
1389
|
+
* with the entitlement cache's max-customers cap.
|
|
1390
|
+
*/
|
|
1391
|
+
private populateEntitlementCache;
|
|
1392
|
+
private touchAlias;
|
|
1393
|
+
/**
|
|
1394
|
+
* Resolve any hint shape (canonical customerId / userId hint /
|
|
1395
|
+
* anonymousId hint / raw string) to a `crossdeckCustomerId` if we
|
|
1396
|
+
* have a cache entry for it.
|
|
1397
|
+
*/
|
|
1398
|
+
private resolveCacheCustomerId;
|
|
1399
|
+
private identityPayload;
|
|
1400
|
+
/**
|
|
1401
|
+
* Resolve event identity. Caller-supplied wins; falls back to
|
|
1402
|
+
* `processAnonymousId` so events from `captureError` /
|
|
1403
|
+
* uncaughtException always have at least one identity hint.
|
|
1404
|
+
*/
|
|
1405
|
+
private resolveIdentity;
|
|
1406
|
+
/**
|
|
1407
|
+
* Strict normalisation for `ingest()` — no auto-fill of identity,
|
|
1408
|
+
* caller must supply at least one hint per event. Matches v0.1.0
|
|
1409
|
+
* behaviour for backward compatibility with bulk-import callers.
|
|
1410
|
+
*/
|
|
1411
|
+
private normalizeIngestEvent;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
export { type StackFrame as $, type AliasIdentityInput as A, type Breadcrumb as B, CROSSDECK_API_VERSION as C, DEFAULT_BASE_URL as D, type EntitlementCacheOptions as E, type EventProperties as F, type ForgetResult as G, type GrantDuration as H, type GrantEntitlementInput as I, type GroupMembership as J, type HeartbeatResponse as K, type HttpRequestInfo as L, type HttpResponseInfo as M, type HttpRetriesConfig as N, type IdentifyOptions as O, type IdentityHints as P, type IngestOptions as Q, type IngestResponse as R, type PublicEntitlement as S, type PurchaseResult as T, type RequestOptions as U, type RevokeEntitlementInput as V, type RuntimeHost as W, type RuntimeInfo as X, SDK_NAME as Y, SDK_VERSION as Z, type ServerEvent as _, type AliasResult as a, type SyncPurchaseInput as a0, makeCrossdeckError as a1, type AuditDecision as b, type AuditEntry as c, type BreadcrumbCategory as d, type BreadcrumbLevel as e, type CapturedError as f, CrossdeckAuthenticationError as g, CrossdeckConfigurationError as h, CrossdeckError as i, type CrossdeckErrorPayload as j, type CrossdeckErrorType as k, CrossdeckInternalError as l, CrossdeckNetworkError as m, CrossdeckPermissionError as n, CrossdeckRateLimitError as o, CrossdeckServer as p, type CrossdeckServerOptions as q, CrossdeckValidationError as r, DEFAULT_TIMEOUT_MS as s, type Diagnostics as t, type EntitlementMutationResult as u, type EntitlementsListResponse as v, type EntitlementsListener as w, type Environment as x, type ErrorCaptureConfig as y, type ErrorLevel as z };
|