@cross-deck/node 1.0.0 → 1.1.1
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 +23 -0
- package/dist/auto-events/index.d.mts +1 -1
- package/dist/auto-events/index.d.ts +1 -1
- package/dist/{crossdeck-server-LvQwPKu5.d.mts → crossdeck-server-BXQaFjVx.d.mts} +22 -1
- package/dist/{crossdeck-server-LvQwPKu5.d.ts → crossdeck-server-BXQaFjVx.d.ts} +22 -1
- package/dist/index.cjs +187 -34
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +187 -34
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,29 @@ All notable changes to `@cross-deck/node` will be documented here. The
|
|
|
4
4
|
format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
|
|
5
5
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## [1.1.0] — 2026-05-13
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- **Auto-heartbeat on construction.** `new CrossdeckServer({...})` now
|
|
12
|
+
fires a heartbeat in the background the moment the SDK is
|
|
13
|
+
constructed, fire-and-forget. The dashboard's row flips LIVE within
|
|
14
|
+
~200 ms of the customer's process boot — no explicit `.heartbeat()`
|
|
15
|
+
call required in the bootstrap. Solves the cold-start serverless
|
|
16
|
+
verification problem at its root (function boot triggers SDK
|
|
17
|
+
construction triggers heartbeat; the install-verifier's URL probe
|
|
18
|
+
doubles as a cold-start waker).
|
|
19
|
+
- New option `bootHeartbeat?: boolean` (default `true`). Set `false`
|
|
20
|
+
for latency-sensitive cold paths that want the prior v1.0.0
|
|
21
|
+
caller-controlled behaviour. Implicitly disabled in `testMode`.
|
|
22
|
+
|
|
23
|
+
### Why this is non-breaking
|
|
24
|
+
|
|
25
|
+
The boot heartbeat is fire-and-forget and swallows its own errors —
|
|
26
|
+
the caller's code never blocks on it, never throws, and a failure
|
|
27
|
+
(bad key, network blip, firewall) has zero effect on subsequent
|
|
28
|
+
event flushes. Equivalent to Sentry's `Sentry.init()` boot session.
|
|
29
|
+
|
|
7
30
|
## [1.0.0] — 2026-05-13
|
|
8
31
|
|
|
9
32
|
Full three-USP server SDK release. Version-aligned with `@cross-deck/web@1.0.0`. Bank-grade quality bar — Stripe + Apple + Google VP-level QA review across two passes. 6,796 LOC of source / 6,230 LOC of tests / 398 unit tests + 19 e2e todos passing / Gate 3 fixture verifying the snippet against the built bundle. Web-SDK parity at the capability level: every Web SDK guarantee that has a server-side analogue ships here.
|
|
@@ -236,7 +236,7 @@ interface RuntimeInfo {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
declare const SDK_NAME = "@cross-deck/node";
|
|
239
|
-
declare const SDK_VERSION = "1.
|
|
239
|
+
declare const SDK_VERSION = "1.1.0";
|
|
240
240
|
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
241
241
|
declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
242
242
|
/**
|
|
@@ -430,6 +430,27 @@ interface CrossdeckServerOptions {
|
|
|
430
430
|
* platform's own SIGKILL (typically 5-10s after SIGTERM) preempts us.
|
|
431
431
|
*/
|
|
432
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;
|
|
433
454
|
/**
|
|
434
455
|
* TTL for the entitlement cache (ms). Default 60_000 (60s).
|
|
435
456
|
*
|
|
@@ -236,7 +236,7 @@ interface RuntimeInfo {
|
|
|
236
236
|
}
|
|
237
237
|
|
|
238
238
|
declare const SDK_NAME = "@cross-deck/node";
|
|
239
|
-
declare const SDK_VERSION = "1.
|
|
239
|
+
declare const SDK_VERSION = "1.1.0";
|
|
240
240
|
declare const DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
241
241
|
declare const DEFAULT_TIMEOUT_MS = 15000;
|
|
242
242
|
/**
|
|
@@ -430,6 +430,27 @@ interface CrossdeckServerOptions {
|
|
|
430
430
|
* platform's own SIGKILL (typically 5-10s after SIGTERM) preempts us.
|
|
431
431
|
*/
|
|
432
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;
|
|
433
454
|
/**
|
|
434
455
|
* TTL for the entitlement cache (ms). Default 60_000 (60s).
|
|
435
456
|
*
|
package/dist/index.cjs
CHANGED
|
@@ -365,7 +365,7 @@ function byteLength(s) {
|
|
|
365
365
|
|
|
366
366
|
// src/http.ts
|
|
367
367
|
var SDK_NAME = "@cross-deck/node";
|
|
368
|
-
var SDK_VERSION = "1.
|
|
368
|
+
var SDK_VERSION = "1.1.0";
|
|
369
369
|
var DEFAULT_BASE_URL = "https://api.cross-deck.com/v1";
|
|
370
370
|
var DEFAULT_TIMEOUT_MS = 15e3;
|
|
371
371
|
var CROSSDECK_API_VERSION = "2025-01-01";
|
|
@@ -1084,13 +1084,21 @@ function isInAppFrame(filename) {
|
|
|
1084
1084
|
if (/^internal[\\/]/.test(filename)) return false;
|
|
1085
1085
|
return true;
|
|
1086
1086
|
}
|
|
1087
|
-
function fingerprintError(message, frames) {
|
|
1087
|
+
function fingerprintError(message, frames, location) {
|
|
1088
1088
|
const inAppFrames = frames.filter((f) => f.in_app).slice(0, 3);
|
|
1089
|
-
const
|
|
1089
|
+
const parts = [
|
|
1090
1090
|
(message || "").slice(0, 200),
|
|
1091
1091
|
...inAppFrames.map((f) => `${f.function}@${f.filename}:${f.lineno}`)
|
|
1092
|
-
]
|
|
1093
|
-
|
|
1092
|
+
];
|
|
1093
|
+
if (inAppFrames.length === 0 && location) {
|
|
1094
|
+
const loc = [
|
|
1095
|
+
location.errorType ?? "",
|
|
1096
|
+
location.filename ?? "",
|
|
1097
|
+
location.lineno ?? ""
|
|
1098
|
+
].join(":");
|
|
1099
|
+
if (loc !== "::") parts.push(loc);
|
|
1100
|
+
}
|
|
1101
|
+
return djb2Hex(parts.join("|"));
|
|
1094
1102
|
}
|
|
1095
1103
|
function djb2Hex(input) {
|
|
1096
1104
|
let h = 5381;
|
|
@@ -1321,34 +1329,30 @@ var ErrorTracker = class {
|
|
|
1321
1329
|
* runtime-agnostic.
|
|
1322
1330
|
*/
|
|
1323
1331
|
buildFromUnknown(err, kind, level) {
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
message: String(err.message).slice(0, 1024),
|
|
1331
|
-
errorType: err.name,
|
|
1332
|
-
frames,
|
|
1333
|
-
rawStack: err.stack ?? null,
|
|
1334
|
-
fingerprint: fingerprintError(err.message, frames),
|
|
1335
|
-
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
1336
|
-
context: this.opts.getContext(),
|
|
1337
|
-
tags: this.opts.getTags()
|
|
1338
|
-
};
|
|
1339
|
-
}
|
|
1340
|
-
const message = safeStringify3(err).slice(0, 1024);
|
|
1332
|
+
const payload = coerceErrorPayload(err);
|
|
1333
|
+
const message = (payload.message || "Unknown error").slice(0, 1024);
|
|
1334
|
+
const stack = err instanceof Error ? err.stack ?? null : null;
|
|
1335
|
+
const frames = parseStack(stack);
|
|
1336
|
+
const errorType = payload.errorType ?? null;
|
|
1337
|
+
const context = payload.extras ? { ...this.opts.getContext(), __error_extras: payload.extras } : this.opts.getContext();
|
|
1341
1338
|
return {
|
|
1342
1339
|
timestamp: Date.now(),
|
|
1343
1340
|
kind,
|
|
1344
1341
|
level,
|
|
1345
1342
|
message,
|
|
1346
|
-
errorType
|
|
1347
|
-
frames
|
|
1348
|
-
rawStack:
|
|
1349
|
-
|
|
1343
|
+
errorType,
|
|
1344
|
+
frames,
|
|
1345
|
+
rawStack: stack,
|
|
1346
|
+
// Location fallback ensures distinct call sites stay separate
|
|
1347
|
+
// even when the message is generic and there are no parseable
|
|
1348
|
+
// frames (e.g. `throw "boom"` from a middleware).
|
|
1349
|
+
fingerprint: fingerprintError(message, frames, {
|
|
1350
|
+
filename: frames[0]?.filename ?? null,
|
|
1351
|
+
lineno: frames[0]?.lineno ?? null,
|
|
1352
|
+
errorType
|
|
1353
|
+
}),
|
|
1350
1354
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
1351
|
-
context
|
|
1355
|
+
context,
|
|
1352
1356
|
tags: this.opts.getTags()
|
|
1353
1357
|
};
|
|
1354
1358
|
}
|
|
@@ -1363,7 +1367,10 @@ var ErrorTracker = class {
|
|
|
1363
1367
|
errorType: "HTTPError",
|
|
1364
1368
|
frames: [],
|
|
1365
1369
|
rawStack: null,
|
|
1366
|
-
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, []
|
|
1370
|
+
fingerprint: fingerprintError(`HTTP ${info.status} ${info.method}`, [], {
|
|
1371
|
+
filename: info.url,
|
|
1372
|
+
errorType: "HTTPError"
|
|
1373
|
+
}),
|
|
1367
1374
|
breadcrumbs: this.opts.breadcrumbs.snapshot(),
|
|
1368
1375
|
context: this.opts.getContext(),
|
|
1369
1376
|
tags: this.opts.getTags(),
|
|
@@ -1466,16 +1473,151 @@ var ErrorTracker = class {
|
|
|
1466
1473
|
}
|
|
1467
1474
|
}
|
|
1468
1475
|
};
|
|
1469
|
-
function
|
|
1470
|
-
if (v
|
|
1471
|
-
if (
|
|
1472
|
-
if (typeof v === "
|
|
1476
|
+
function coerceErrorPayload(v) {
|
|
1477
|
+
if (v === null) return { message: "(thrown: null)", errorType: null, extras: null };
|
|
1478
|
+
if (v === void 0) return { message: "(thrown: undefined)", errorType: null, extras: null };
|
|
1479
|
+
if (typeof v === "string") {
|
|
1480
|
+
return { message: v, errorType: null, extras: null };
|
|
1481
|
+
}
|
|
1482
|
+
if (typeof v === "number" || typeof v === "boolean" || typeof v === "bigint") {
|
|
1483
|
+
return { message: String(v), errorType: typeof v, extras: null };
|
|
1484
|
+
}
|
|
1485
|
+
if (typeof v === "symbol") {
|
|
1486
|
+
return { message: v.toString(), errorType: "symbol", extras: null };
|
|
1487
|
+
}
|
|
1488
|
+
if (typeof v === "function") {
|
|
1489
|
+
return { message: `(thrown function: ${v.name || "anonymous"})`, errorType: "function", extras: null };
|
|
1490
|
+
}
|
|
1491
|
+
if (v instanceof Error) {
|
|
1492
|
+
const errorType = v.name || v.constructor?.name || "Error";
|
|
1493
|
+
const message = typeof v.message === "string" && v.message.length > 0 ? v.message : safeToString(v) || errorType;
|
|
1494
|
+
const extras = {};
|
|
1495
|
+
const causeChain = collectCauseChain(v);
|
|
1496
|
+
if (causeChain.length > 0) extras.cause = causeChain;
|
|
1497
|
+
const aggErrors = v.errors;
|
|
1498
|
+
if (Array.isArray(aggErrors)) {
|
|
1499
|
+
extras.aggregatedErrors = aggErrors.slice(0, 10).map((inner) => {
|
|
1500
|
+
if (inner instanceof Error) {
|
|
1501
|
+
return { name: inner.name || "Error", message: inner.message || "" };
|
|
1502
|
+
}
|
|
1503
|
+
return { name: "non-Error", message: safeToString(inner) };
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
for (const key of [
|
|
1507
|
+
"code",
|
|
1508
|
+
"errno",
|
|
1509
|
+
"syscall",
|
|
1510
|
+
"path",
|
|
1511
|
+
"status",
|
|
1512
|
+
"statusCode",
|
|
1513
|
+
"response",
|
|
1514
|
+
"data",
|
|
1515
|
+
"detail",
|
|
1516
|
+
"details"
|
|
1517
|
+
]) {
|
|
1518
|
+
const val = v[key];
|
|
1519
|
+
if (val !== void 0 && typeof val !== "function") {
|
|
1520
|
+
extras[key] = safeClone(val);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
for (const key of Object.keys(v)) {
|
|
1524
|
+
if (key === "message" || key === "stack" || key === "name" || key === "cause" || key === "errors") continue;
|
|
1525
|
+
if (key in extras) continue;
|
|
1526
|
+
const val = v[key];
|
|
1527
|
+
if (typeof val === "function") continue;
|
|
1528
|
+
extras[key] = safeClone(val);
|
|
1529
|
+
}
|
|
1530
|
+
return {
|
|
1531
|
+
message,
|
|
1532
|
+
errorType,
|
|
1533
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
1534
|
+
};
|
|
1535
|
+
}
|
|
1536
|
+
if (typeof Response !== "undefined" && v instanceof Response) {
|
|
1537
|
+
return {
|
|
1538
|
+
message: `HTTP ${v.status} ${v.statusText || ""}${v.url ? ` ${v.url}` : ""}`.trim(),
|
|
1539
|
+
errorType: "Response",
|
|
1540
|
+
extras: { status: v.status, statusText: v.statusText, url: v.url, type: v.type }
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
if (typeof v === "object") {
|
|
1544
|
+
const obj = v;
|
|
1545
|
+
const ctorName = obj.constructor && typeof obj.constructor === "function" && obj.constructor.name || null;
|
|
1546
|
+
const ownMessage = typeof obj.message === "string" && obj.message ? obj.message : null;
|
|
1547
|
+
const ownName = typeof obj.name === "string" && obj.name ? obj.name : null;
|
|
1548
|
+
let jsonForm = null;
|
|
1549
|
+
try {
|
|
1550
|
+
const serialised = JSON.stringify(obj);
|
|
1551
|
+
jsonForm = serialised === "{}" ? null : serialised;
|
|
1552
|
+
} catch {
|
|
1553
|
+
jsonForm = null;
|
|
1554
|
+
}
|
|
1555
|
+
const fallbackString = safeToString(obj);
|
|
1556
|
+
const message = ownMessage ?? jsonForm ?? (fallbackString && fallbackString !== "[object Object]" ? fallbackString : null) ?? (ctorName ? `(thrown ${ctorName} with no message)` : "(thrown object with no message)");
|
|
1557
|
+
const errorType = ownName ?? ctorName ?? null;
|
|
1558
|
+
const extras = {};
|
|
1559
|
+
let count = 0;
|
|
1560
|
+
for (const key of Object.keys(obj)) {
|
|
1561
|
+
if (count >= 20) break;
|
|
1562
|
+
if (key === "message" || key === "name") continue;
|
|
1563
|
+
const val = obj[key];
|
|
1564
|
+
if (typeof val === "function") continue;
|
|
1565
|
+
extras[key] = safeClone(val);
|
|
1566
|
+
count++;
|
|
1567
|
+
}
|
|
1568
|
+
return {
|
|
1569
|
+
message,
|
|
1570
|
+
errorType,
|
|
1571
|
+
extras: Object.keys(extras).length > 0 ? extras : null
|
|
1572
|
+
};
|
|
1573
|
+
}
|
|
1574
|
+
return { message: safeToString(v) || "(unstringifiable thrown value)", errorType: null, extras: null };
|
|
1575
|
+
}
|
|
1576
|
+
function collectCauseChain(err) {
|
|
1577
|
+
const out = [];
|
|
1578
|
+
let cur = err.cause;
|
|
1579
|
+
let depth = 0;
|
|
1580
|
+
while (cur != null && depth < 5) {
|
|
1581
|
+
if (cur instanceof Error) {
|
|
1582
|
+
out.push({ name: cur.name || "Error", message: cur.message || "" });
|
|
1583
|
+
cur = cur.cause;
|
|
1584
|
+
} else {
|
|
1585
|
+
out.push({ name: "non-Error", message: safeToString(cur) });
|
|
1586
|
+
cur = null;
|
|
1587
|
+
}
|
|
1588
|
+
depth++;
|
|
1589
|
+
}
|
|
1590
|
+
return out;
|
|
1591
|
+
}
|
|
1592
|
+
function safeToString(v) {
|
|
1593
|
+
try {
|
|
1594
|
+
const s = Object.prototype.toString.call(v);
|
|
1595
|
+
if (s !== "[object Object]") return s;
|
|
1596
|
+
const own = v?.toString;
|
|
1597
|
+
if (typeof own === "function" && own !== Object.prototype.toString) {
|
|
1598
|
+
const r = own.call(v);
|
|
1599
|
+
if (typeof r === "string") return r;
|
|
1600
|
+
}
|
|
1601
|
+
return s;
|
|
1602
|
+
} catch {
|
|
1603
|
+
return "(throwing toString)";
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
function safeClone(v) {
|
|
1607
|
+
if (v == null) return v;
|
|
1608
|
+
const t = typeof v;
|
|
1609
|
+
if (t === "string" || t === "number" || t === "boolean") return v;
|
|
1610
|
+
if (t === "bigint") return String(v);
|
|
1473
1611
|
try {
|
|
1474
|
-
|
|
1612
|
+
const s = JSON.stringify(v);
|
|
1613
|
+
return s === void 0 ? safeToString(v) : JSON.parse(s);
|
|
1475
1614
|
} catch {
|
|
1476
|
-
return
|
|
1615
|
+
return safeToString(v);
|
|
1477
1616
|
}
|
|
1478
1617
|
}
|
|
1618
|
+
function safeStringify3(v) {
|
|
1619
|
+
return coerceErrorPayload(v).message;
|
|
1620
|
+
}
|
|
1479
1621
|
|
|
1480
1622
|
// src/runtime-info.ts
|
|
1481
1623
|
var import_node_os = require("os");
|
|
@@ -2239,6 +2381,17 @@ var CrossdeckServer = class extends import_node_events.EventEmitter {
|
|
|
2239
2381
|
});
|
|
2240
2382
|
this.flushOnExit.install();
|
|
2241
2383
|
}
|
|
2384
|
+
if (options.testMode !== true && options.bootHeartbeat !== false) {
|
|
2385
|
+
setImmediate(() => {
|
|
2386
|
+
void this.heartbeat().catch((err) => {
|
|
2387
|
+
this.debug.emit(
|
|
2388
|
+
"sdk.boot_heartbeat_failed",
|
|
2389
|
+
"Boot heartbeat failed (non-fatal \u2014 events will still flush).",
|
|
2390
|
+
{ message: err instanceof Error ? err.message : String(err) }
|
|
2391
|
+
);
|
|
2392
|
+
});
|
|
2393
|
+
});
|
|
2394
|
+
}
|
|
2242
2395
|
}
|
|
2243
2396
|
// ============================================================
|
|
2244
2397
|
// Identity — direct HTTP (transactional, not telemetry)
|