@superblocksteam/sdk 2.0.129 → 2.0.130-next.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/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +235 -42
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.test.js +406 -3
- package/dist/cli-replacement/automatic-upgrades.test.js.map +1 -1
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +8 -9
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/home-npmrc.d.mts +6 -0
- package/dist/cli-replacement/home-npmrc.d.mts.map +1 -1
- package/dist/cli-replacement/home-npmrc.mjs +22 -0
- package/dist/cli-replacement/home-npmrc.mjs.map +1 -1
- package/dist/client.billing-usage.test.d.ts +2 -0
- package/dist/client.billing-usage.test.d.ts.map +1 -0
- package/dist/client.billing-usage.test.js +66 -0
- package/dist/client.billing-usage.test.js.map +1 -0
- package/dist/client.d.ts +41 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +15 -1
- package/dist/client.js.map +1 -1
- package/dist/dev-utils/dev-server-metrics.boot.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.boot.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.boot.test.mjs +55 -0
- package/dist/dev-utils/dev-server-metrics.boot.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.d.mts +32 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.mjs +60 -0
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.test.mjs +18 -1
- package/dist/dev-utils/dev-server-metrics.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +28 -2
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +68 -7
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.mjs +28 -1
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -1
- package/dist/flag.d.ts +14 -0
- package/dist/flag.d.ts.map +1 -1
- package/dist/flag.js +17 -0
- package/dist/flag.js.map +1 -1
- package/dist/flag.test.d.ts +2 -0
- package/dist/flag.test.d.ts.map +1 -0
- package/dist/flag.test.js +54 -0
- package/dist/flag.test.js.map +1 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/sdk.d.ts +3 -1
- package/dist/sdk.d.ts.map +1 -1
- package/dist/sdk.js +11 -1
- package/dist/sdk.js.map +1 -1
- package/dist/types/common.d.ts +1 -0
- package/dist/types/common.d.ts.map +1 -1
- package/dist/types/common.js.map +1 -1
- package/package.json +8 -8
- package/src/cli-replacement/automatic-upgrades.test.ts +556 -3
- package/src/cli-replacement/automatic-upgrades.ts +251 -54
- package/src/cli-replacement/dev.mts +9 -10
- package/src/cli-replacement/home-npmrc.mts +45 -0
- package/src/client.billing-usage.test.ts +73 -0
- package/src/client.ts +66 -1
- package/src/dev-utils/dev-server-metrics.boot.test.mts +83 -0
- package/src/dev-utils/dev-server-metrics.mts +65 -0
- package/src/dev-utils/dev-server-metrics.test.mts +24 -1
- package/src/dev-utils/dev-server.mts +96 -7
- package/src/dev-utils/dev-server.status.test.mts +35 -1
- package/src/flag.test.ts +63 -0
- package/src/flag.ts +24 -0
- package/src/index.ts +9 -0
- package/src/sdk.ts +20 -0
- package/src/types/common.ts +1 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/turbo.json +2 -1
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { devServerMetrics as DevServerMetricsSingleton } from "./dev-server-metrics.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Boot-completion-signal emission tests.
|
|
7
|
+
*
|
|
8
|
+
* These exercise the real `DevServerMetrics.recordBoot` / `flush` logic against
|
|
9
|
+
* a fake meter captured at the `getMeter()` boundary — i.e. we "listen" to
|
|
10
|
+
* exactly the metric increments and labels that would be flushed to the OTLP
|
|
11
|
+
* exporter, without standing up a network collector. The only substitutions are
|
|
12
|
+
* the two telemetry-bootstrap boundaries the module depends on
|
|
13
|
+
* (`isTelemetryInitialized`, `getMeter`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
type Add = { name: string; value: number; attrs: Record<string, unknown> };
|
|
17
|
+
|
|
18
|
+
const h = vi.hoisted(() => ({
|
|
19
|
+
adds: [] as Add[],
|
|
20
|
+
telemetryUp: true,
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock("@superblocksteam/telemetry", () => ({
|
|
24
|
+
isTelemetryInitialized: () => h.telemetryUp,
|
|
25
|
+
}));
|
|
26
|
+
|
|
27
|
+
vi.mock("../telemetry/index.js", () => ({
|
|
28
|
+
getMeter: () => ({
|
|
29
|
+
createCounter: (name: string) => ({
|
|
30
|
+
add: (value: number, attrs: Record<string, unknown>) =>
|
|
31
|
+
h.adds.push({ name, value, attrs }),
|
|
32
|
+
}),
|
|
33
|
+
createHistogram: () => ({ record: () => {} }),
|
|
34
|
+
}),
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
const bootAdds = () => h.adds.filter((a) => a.name === "dev_server_boot_total");
|
|
38
|
+
|
|
39
|
+
describe("devServerMetrics.recordBoot (in-place-restart completion signal)", () => {
|
|
40
|
+
let devServerMetrics: typeof DevServerMetricsSingleton;
|
|
41
|
+
|
|
42
|
+
beforeEach(async () => {
|
|
43
|
+
h.adds = [];
|
|
44
|
+
h.telemetryUp = true;
|
|
45
|
+
// Fresh module (and fresh singleton) per test so the once-per-process
|
|
46
|
+
// boot guard does not leak across cases.
|
|
47
|
+
vi.resetModules();
|
|
48
|
+
({ devServerMetrics } = await import("./dev-server-metrics.mjs"));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("emits dev_server_boot_total once with the flip_restart boot kind", () => {
|
|
52
|
+
devServerMetrics.recordBoot("flip_restart");
|
|
53
|
+
|
|
54
|
+
const boots = bootAdds();
|
|
55
|
+
expect(boots).toHaveLength(1);
|
|
56
|
+
expect(boots[0].value).toBe(1);
|
|
57
|
+
expect(boots[0].attrs).toEqual({ boot_kind: "flip_restart" });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("emits at most one boot per process even when called from multiple sites", () => {
|
|
61
|
+
// initializeTelemetry runs on cold boot, token hot-reload, and warm
|
|
62
|
+
// activation, and createDevServer may also call it — the boot signal must
|
|
63
|
+
// still increment exactly once for the process.
|
|
64
|
+
devServerMetrics.recordBoot("flip_restart");
|
|
65
|
+
devServerMetrics.recordBoot("flip_restart");
|
|
66
|
+
devServerMetrics.recordBoot(undefined);
|
|
67
|
+
|
|
68
|
+
expect(bootAdds()).toHaveLength(1);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("buffers the boot when telemetry is down and replays it once on flush", () => {
|
|
72
|
+
h.telemetryUp = false;
|
|
73
|
+
devServerMetrics.recordBoot("flip_restart");
|
|
74
|
+
expect(bootAdds()).toHaveLength(0);
|
|
75
|
+
|
|
76
|
+
h.telemetryUp = true;
|
|
77
|
+
devServerMetrics.flush();
|
|
78
|
+
|
|
79
|
+
const boots = bootAdds();
|
|
80
|
+
expect(boots).toHaveLength(1);
|
|
81
|
+
expect(boots[0].attrs).toEqual({ boot_kind: "flip_restart" });
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* warm-standby observations are not silently dropped into the no-op meter.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
+
import { RESTART_REASON_FLIP_RESTART } from "@superblocksteam/library-shared/restart-contract";
|
|
15
16
|
import { isTelemetryInitialized } from "@superblocksteam/telemetry";
|
|
16
17
|
|
|
17
18
|
import { getMeter } from "../telemetry/index.js";
|
|
@@ -72,6 +73,29 @@ export function normalizeNpmErrorCodeTag(code: string | undefined): string {
|
|
|
72
73
|
return KNOWN_NPM_ERROR_CODES.has(collapsed) ? collapsed : "other";
|
|
73
74
|
}
|
|
74
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Normalize the CLI parent's `SUPERBLOCKS_RESTART_REASON` env into a bounded
|
|
78
|
+
* `boot_kind` metric label for `dev_server_boot_total`:
|
|
79
|
+
* - absent/empty/whitespace -> `"cold_start"` (first boot or a non-restart
|
|
80
|
+
* respawn; the parent only stamps the reason on a post-flip respawn)
|
|
81
|
+
* - `RESTART_REASON_FLIP_RESTART` -> passes through (the post-flip in-place
|
|
82
|
+
* restart we want to prove actually booted)
|
|
83
|
+
* - anything else -> `"other"`
|
|
84
|
+
*
|
|
85
|
+
* The reason comes from process env, which an operator or a stale value could
|
|
86
|
+
* set to anything, so the unknown branch collapses to `"other"` to keep the
|
|
87
|
+
* `boot_kind` series count bounded (cardinality guard).
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeBootKindTag(
|
|
90
|
+
restartReason: string | undefined,
|
|
91
|
+
): string {
|
|
92
|
+
const trimmed = restartReason?.trim();
|
|
93
|
+
if (!trimmed) return "cold_start";
|
|
94
|
+
return trimmed === RESTART_REASON_FLIP_RESTART
|
|
95
|
+
? RESTART_REASON_FLIP_RESTART
|
|
96
|
+
: "other";
|
|
97
|
+
}
|
|
98
|
+
|
|
75
99
|
/** Dev-server endpoints that produce per-request metrics. */
|
|
76
100
|
export type DevServerEndpoint =
|
|
77
101
|
| "_sb_health"
|
|
@@ -150,6 +174,10 @@ class DevServerMetrics {
|
|
|
150
174
|
*/
|
|
151
175
|
private bufferedRecorders: Array<() => void> = [];
|
|
152
176
|
|
|
177
|
+
/** Set once `recordBoot` has been called, so the boot metric is emitted at
|
|
178
|
+
* most once per process regardless of how many init paths invoke it. */
|
|
179
|
+
private booted = false;
|
|
180
|
+
|
|
153
181
|
/**
|
|
154
182
|
* Routes a recorder to the live OTel meter when telemetry is up, or buffers
|
|
155
183
|
* it for replay when telemetry has not yet initialized.
|
|
@@ -193,6 +221,43 @@ class DevServerMetrics {
|
|
|
193
221
|
}
|
|
194
222
|
}
|
|
195
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Records a dev-server process boot, tagged by `boot_kind`.
|
|
226
|
+
*
|
|
227
|
+
* Emitted once per process at startup. Pass the raw
|
|
228
|
+
* `SUPERBLOCKS_RESTART_REASON` env value (the CLI parent stamps
|
|
229
|
+
* `RESTART_REASON_FLIP_RESTART` on a post-flip in-place respawn and leaves it
|
|
230
|
+
* unset otherwise); it is normalized to a bounded `boot_kind` label
|
|
231
|
+
* (`cold_start` | `flip_restart` | `other`).
|
|
232
|
+
*
|
|
233
|
+
* This is the completion half of the in-place-restart telemetry: the
|
|
234
|
+
* server-side decision metric proves we *chose* to self-restart, and a
|
|
235
|
+
* `dev_server_boot_total{boot_kind="flip_restart"}` increment proves the
|
|
236
|
+
* child actually booted afterward, so the two can be compared in Datadog.
|
|
237
|
+
*/
|
|
238
|
+
recordBoot(restartReason: string | undefined): void {
|
|
239
|
+
// Once per process. `recordBoot` is driven from every telemetry-init path
|
|
240
|
+
// (cold boot, post-flip respawn, warm activation, and auth hot-reload all
|
|
241
|
+
// call `initializeTelemetry`, which can run more than once per process), so
|
|
242
|
+
// the guard keeps `dev_server_boot_total` at exactly one increment per boot
|
|
243
|
+
// no matter how many call sites or re-inits fire. The first call wins and
|
|
244
|
+
// captures the boot kind; whether it emits immediately or buffers for
|
|
245
|
+
// `flush()`, later calls are no-ops.
|
|
246
|
+
if (this.booted) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
this.booted = true;
|
|
250
|
+
const bootKind = normalizeBootKindTag(restartReason);
|
|
251
|
+
this.record(() => {
|
|
252
|
+
getMeter()
|
|
253
|
+
.createCounter("dev_server_boot_total", {
|
|
254
|
+
description:
|
|
255
|
+
"Count of dev-server process boots by boot kind (cold_start | flip_restart | other).",
|
|
256
|
+
})
|
|
257
|
+
.add(1, { boot_kind: bootKind });
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
196
261
|
/**
|
|
197
262
|
* Records the outcome of a dev-server HTTP endpoint request.
|
|
198
263
|
*/
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
normalizeBootKindTag,
|
|
5
|
+
normalizeNpmErrorCodeTag,
|
|
6
|
+
} from "./dev-server-metrics.mjs";
|
|
7
|
+
|
|
8
|
+
describe("normalizeBootKindTag (metric cardinality bound)", () => {
|
|
9
|
+
it("passes through the known flip-restart reason", () => {
|
|
10
|
+
expect(normalizeBootKindTag("flip_restart")).toBe("flip_restart");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("maps an absent reason to 'cold_start' (no parent restart marker)", () => {
|
|
14
|
+
expect(normalizeBootKindTag(undefined)).toBe("cold_start");
|
|
15
|
+
expect(normalizeBootKindTag("")).toBe("cold_start");
|
|
16
|
+
expect(normalizeBootKindTag(" ")).toBe("cold_start");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("maps unknown / arbitrary / injected tokens to 'other' (cardinality guard)", () => {
|
|
20
|
+
// SUPERBLOCKS_RESTART_REASON is process env, so an operator or a stale
|
|
21
|
+
// value could set anything; it must never mint a new time series.
|
|
22
|
+
expect(normalizeBootKindTag("auto_upgrade")).toBe("other");
|
|
23
|
+
expect(normalizeBootKindTag("some-random-token")).toBe("other");
|
|
24
|
+
expect(normalizeBootKindTag("`;rm -rf /#")).toBe("other");
|
|
25
|
+
});
|
|
26
|
+
});
|
|
4
27
|
|
|
5
28
|
describe("normalizeNpmErrorCodeTag (metric cardinality bound)", () => {
|
|
6
29
|
it("passes through known npm codes", () => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as child_process from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { existsSync, readdirSync } from "node:fs";
|
|
3
4
|
import type http from "node:http";
|
|
4
5
|
import net from "node:net";
|
|
@@ -14,6 +15,10 @@ import type { HmrOptions, Plugin, UserConfig } from "vite";
|
|
|
14
15
|
import type { ViteDevServer } from "vite";
|
|
15
16
|
import tsconfigPaths from "vite-tsconfig-paths";
|
|
16
17
|
|
|
18
|
+
import {
|
|
19
|
+
RESTART_EXIT_CODE,
|
|
20
|
+
RESTART_REASON_ENV,
|
|
21
|
+
} from "@superblocksteam/library-shared/restart-contract";
|
|
17
22
|
import type { ServerError } from "@superblocksteam/library-shared/types";
|
|
18
23
|
import {
|
|
19
24
|
JwtVerifier,
|
|
@@ -35,6 +40,7 @@ import type { OperationQueue } from "@superblocksteam/vite-plugin-file-sync/oper
|
|
|
35
40
|
import type { SyncService } from "@superblocksteam/vite-plugin-file-sync/sync-service";
|
|
36
41
|
|
|
37
42
|
import pkg from "../../package.json" with { type: "json" };
|
|
43
|
+
import { flagOrEnv } from "../flag.js";
|
|
38
44
|
import type { SuperblocksSdk } from "../sdk.js";
|
|
39
45
|
import { getTracer } from "../telemetry/index.js";
|
|
40
46
|
import { getErrorMeta, getLogger } from "../telemetry/logging.js";
|
|
@@ -353,7 +359,10 @@ export function isWorkspacePersistUploadHeaders(
|
|
|
353
359
|
return true;
|
|
354
360
|
}
|
|
355
361
|
|
|
356
|
-
|
|
362
|
+
// Re-exported so existing consumers (sdk index, dev.mts) keep importing
|
|
363
|
+
// RESTART_EXIT_CODE from the SDK; the canonical definition lives in
|
|
364
|
+
// `@superblocksteam/library-shared/restart-contract`.
|
|
365
|
+
export { RESTART_EXIT_CODE };
|
|
357
366
|
|
|
358
367
|
function getJwksUriWithBaseUrl(superblocksBaseUrl?: string): string {
|
|
359
368
|
if (process.env.SUPERBLOCKS_JWKS_URI) {
|
|
@@ -494,6 +503,43 @@ export function buildStatusPayload<T extends object>(
|
|
|
494
503
|
return { ...base, serverErrors: devServerStatus?.serverErrors ?? [] };
|
|
495
504
|
}
|
|
496
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Stable per-process identity for the dev-server runtime. `bootId` is generated
|
|
508
|
+
* once at module load, so it changes on every process start (including an
|
|
509
|
+
* in-place restart). Together with an unchanged sandbox/port, these let an e2e
|
|
510
|
+
* spec prove "same sandbox, new process" after a post-flip restart without log
|
|
511
|
+
* scraping (testability hook for the post-flip restart e2e spec).
|
|
512
|
+
*/
|
|
513
|
+
export const DEV_SERVER_BOOT_ID = randomUUID();
|
|
514
|
+
export const DEV_SERVER_STARTED_AT = new Date().toISOString();
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Builds the `/_sb_health` payload, including the per-process boot identity.
|
|
518
|
+
* Extracted as a pure function so the boot-identity contract can be unit
|
|
519
|
+
* tested without booting the full dev server.
|
|
520
|
+
*/
|
|
521
|
+
export function buildHealthResponse(args: {
|
|
522
|
+
applicationId: string;
|
|
523
|
+
branch: string | null | undefined;
|
|
524
|
+
cliVersion: string;
|
|
525
|
+
}): {
|
|
526
|
+
status: "healthy";
|
|
527
|
+
applicationId: string;
|
|
528
|
+
branch: string | null | undefined;
|
|
529
|
+
cliVersion: string;
|
|
530
|
+
bootId: string;
|
|
531
|
+
startedAt: string;
|
|
532
|
+
} {
|
|
533
|
+
return {
|
|
534
|
+
status: "healthy",
|
|
535
|
+
applicationId: args.applicationId,
|
|
536
|
+
branch: args.branch,
|
|
537
|
+
cliVersion: args.cliVersion,
|
|
538
|
+
bootId: DEV_SERVER_BOOT_ID,
|
|
539
|
+
startedAt: DEV_SERVER_STARTED_AT,
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
|
|
497
543
|
// The "Dev Server" is a long-running HTTP server that manages the Vite server
|
|
498
544
|
export async function createDevServer({
|
|
499
545
|
root,
|
|
@@ -527,13 +573,47 @@ export async function createDevServer({
|
|
|
527
573
|
}
|
|
528
574
|
const currentBranch = await getCurrentGitBranchIfGit();
|
|
529
575
|
|
|
530
|
-
const healthResponse = {
|
|
531
|
-
status: "healthy",
|
|
576
|
+
const healthResponse = buildHealthResponse({
|
|
532
577
|
applicationId: resourceConfig.id,
|
|
533
578
|
branch: currentBranch,
|
|
534
|
-
// TODO(code-mode): add branch name
|
|
535
579
|
cliVersion: pkg.version,
|
|
536
|
-
};
|
|
580
|
+
});
|
|
581
|
+
|
|
582
|
+
// Anchor the per-process boot identity in stdout so operators can correlate
|
|
583
|
+
// log lines from before and after an in-place restart (same sandbox, new
|
|
584
|
+
// process) without polling the /_sb_health endpoint.
|
|
585
|
+
logger.info(
|
|
586
|
+
`Dev server process started (bootId=${healthResponse.bootId}, startedAt=${healthResponse.startedAt})`,
|
|
587
|
+
);
|
|
588
|
+
|
|
589
|
+
// Emit a boot metric tagged with how this process started. The CLI parent
|
|
590
|
+
// stamps SUPERBLOCKS_RESTART_REASON=flip_restart when it respawns the child
|
|
591
|
+
// after an in-place v2->v3 template flip, so a
|
|
592
|
+
// `dev_server_boot_total{boot_kind="flip_restart"}` increment proves the child
|
|
593
|
+
// actually booted back up after the flip — the completion signal that pairs
|
|
594
|
+
// with the server-side "decided to self-restart" metric.
|
|
595
|
+
//
|
|
596
|
+
// NOTE: this child-side metric is BEST-EFFORT, not durable. recordBoot buffers
|
|
597
|
+
// when telemetry isn't up yet, but the buffer only flushes if a later
|
|
598
|
+
// initializeTelemetry() call succeeds; if the child stalls or exits before
|
|
599
|
+
// telemetry initializes, the buffered increment is lost (the exact gap we saw
|
|
600
|
+
// in Datadog). The reliable completion signal is the parent-emitted
|
|
601
|
+
// `dev_server_child_respawn_completed_total`, driven by the devServerStarted
|
|
602
|
+
// IPC sent just below — keep both so server/parent/child can be cross-checked.
|
|
603
|
+
devServerMetrics.recordBoot(process.env[RESTART_REASON_ENV]);
|
|
604
|
+
|
|
605
|
+
// Tell the supervising CLI parent (dev-parent.mts) we reached dev-server boot.
|
|
606
|
+
// The parent emits the RELIABLE in-place-restart-completed metric from this
|
|
607
|
+
// signal, because the child's own `dev_server_boot_total` is buffered through
|
|
608
|
+
// the telemetry pipeline and is lost if telemetry never initializes (the gap
|
|
609
|
+
// we saw in Datadog: a respawned child went dark before its boot metric ever
|
|
610
|
+
// flushed). `process.send` is only defined when spawned with an IPC channel —
|
|
611
|
+
// i.e. under the lightweight parent supervisor; warm single-process and
|
|
612
|
+
// shell-supervised pods have no IPC parent and simply skip this. Sent as a
|
|
613
|
+
// plain object; the parent owns the typed `ChildToParentMessage` contract.
|
|
614
|
+
if (process.send) {
|
|
615
|
+
process.send({ type: "devServerStarted" });
|
|
616
|
+
}
|
|
537
617
|
|
|
538
618
|
let viteCreationResults: {
|
|
539
619
|
viteServer: ViteDevServer;
|
|
@@ -1136,6 +1216,12 @@ export async function createDevServer({
|
|
|
1136
1216
|
superblocksBaseUrl: explicitBaseUrl || localToken?.superblocksBaseUrl,
|
|
1137
1217
|
features: {
|
|
1138
1218
|
enableSessionRecording: featureFlags?.enableSessionRecording() ?? false,
|
|
1219
|
+
// Gate the in-place restart on the LD flag OR the env escape hatch
|
|
1220
|
+
// (cloud-prem/local), mirroring the memory-metrics pattern below.
|
|
1221
|
+
inPlaceTemplateRestartEnabled: flagOrEnv(
|
|
1222
|
+
featureFlags?.inPlaceTemplateRestartEnabled(),
|
|
1223
|
+
"SUPERBLOCKS_DEV_SERVER_IN_PLACE_TEMPLATE_RESTART_ENABLED",
|
|
1224
|
+
),
|
|
1139
1225
|
},
|
|
1140
1226
|
});
|
|
1141
1227
|
// Note: vitePromise itself gets a no-op .catch() at its construction site
|
|
@@ -1154,8 +1240,10 @@ export async function createDevServer({
|
|
|
1154
1240
|
// Start memory metrics only for claimed pods (after Vite init, not warm standby).
|
|
1155
1241
|
// Gated by either the LD flag (cloud) or the SABS-injected env var (cloud-prem/local).
|
|
1156
1242
|
if (
|
|
1157
|
-
|
|
1158
|
-
|
|
1243
|
+
flagOrEnv(
|
|
1244
|
+
featureFlags?.devServerMemoryMetricsEnabled(),
|
|
1245
|
+
"SUPERBLOCKS_DEV_SERVER_MEMORY_METRICS_ENABLED",
|
|
1246
|
+
)
|
|
1159
1247
|
) {
|
|
1160
1248
|
startMemoryMetrics();
|
|
1161
1249
|
}
|
|
@@ -1205,6 +1293,7 @@ async function startVite({
|
|
|
1205
1293
|
checkAuthorization: FileSyncPluginParams["checkAuthorization"];
|
|
1206
1294
|
features: {
|
|
1207
1295
|
enableSessionRecording: boolean;
|
|
1296
|
+
inPlaceTemplateRestartEnabled: boolean;
|
|
1208
1297
|
};
|
|
1209
1298
|
}): Promise<{
|
|
1210
1299
|
viteServer: ViteDevServer;
|
|
@@ -5,7 +5,12 @@ import type { ServerError } from "@superblocksteam/library-shared/types";
|
|
|
5
5
|
// Static import so the slow `dev-server.mjs` module graph (vite, react plugin,
|
|
6
6
|
// workspace deps) loads during file evaluation rather than inside each test,
|
|
7
7
|
// where the cold-import cost easily exceeds the default 5s test timeout.
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
buildHealthResponse,
|
|
10
|
+
buildStatusPayload,
|
|
11
|
+
DEV_SERVER_BOOT_ID,
|
|
12
|
+
DEV_SERVER_STARTED_AT,
|
|
13
|
+
} from "./dev-server.mjs";
|
|
9
14
|
|
|
10
15
|
describe("buildStatusPayload", () => {
|
|
11
16
|
it("merges serverErrors from devServerStatus onto the base payload", () => {
|
|
@@ -56,3 +61,32 @@ describe("buildStatusPayload", () => {
|
|
|
56
61
|
});
|
|
57
62
|
});
|
|
58
63
|
});
|
|
64
|
+
|
|
65
|
+
describe("buildHealthResponse", () => {
|
|
66
|
+
it("includes the per-process boot identity so an e2e can prove 'same sandbox, new process'", () => {
|
|
67
|
+
const health = buildHealthResponse({
|
|
68
|
+
applicationId: "app-1",
|
|
69
|
+
branch: "main",
|
|
70
|
+
cliVersion: "1.2.3",
|
|
71
|
+
});
|
|
72
|
+
expect(health).toEqual({
|
|
73
|
+
status: "healthy",
|
|
74
|
+
applicationId: "app-1",
|
|
75
|
+
branch: "main",
|
|
76
|
+
cliVersion: "1.2.3",
|
|
77
|
+
bootId: DEV_SERVER_BOOT_ID,
|
|
78
|
+
startedAt: DEV_SERVER_STARTED_AT,
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("passes through an undefined branch (non-git checkout)", () => {
|
|
83
|
+
const health = buildHealthResponse({
|
|
84
|
+
applicationId: "app-2",
|
|
85
|
+
branch: undefined,
|
|
86
|
+
cliVersion: "9.9.9",
|
|
87
|
+
});
|
|
88
|
+
expect(health.branch).toBeUndefined();
|
|
89
|
+
expect(health.bootId).toBe(DEV_SERVER_BOOT_ID);
|
|
90
|
+
expect(health.startedAt).toBe(DEV_SERVER_STARTED_AT);
|
|
91
|
+
});
|
|
92
|
+
});
|
package/src/flag.test.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { FeatureFlags, flagOrEnv } from "./flag.js";
|
|
4
|
+
|
|
5
|
+
describe("flagOrEnv", () => {
|
|
6
|
+
const ENV = "SUPERBLOCKS_TEST_FLAG_OR_ENV";
|
|
7
|
+
let original: string | undefined;
|
|
8
|
+
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
original = process.env[ENV];
|
|
11
|
+
delete process.env[ENV];
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
if (original === undefined) {
|
|
16
|
+
delete process.env[ENV];
|
|
17
|
+
} else {
|
|
18
|
+
process.env[ENV] = original;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns false when the flag is undefined and the env is unset", () => {
|
|
23
|
+
expect(flagOrEnv(undefined, ENV)).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns true when the flag value is true", () => {
|
|
27
|
+
expect(flagOrEnv(true, ENV)).toBe(true);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("returns true when the flag is falsy but the env is exactly 'true'", () => {
|
|
31
|
+
process.env[ENV] = "true";
|
|
32
|
+
expect(flagOrEnv(false, ENV)).toBe(true);
|
|
33
|
+
expect(flagOrEnv(undefined, ENV)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("returns false when the env is set to anything other than 'true'", () => {
|
|
37
|
+
process.env[ENV] = "1";
|
|
38
|
+
expect(flagOrEnv(false, ENV)).toBe(false);
|
|
39
|
+
process.env[ENV] = "TRUE";
|
|
40
|
+
expect(flagOrEnv(undefined, ENV)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("FeatureFlags.inPlaceTemplateRestartEnabled", () => {
|
|
45
|
+
it("defaults to false when the flag is absent", () => {
|
|
46
|
+
const flags = new FeatureFlags({});
|
|
47
|
+
expect(flags.inPlaceTemplateRestartEnabled()).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns true when the flag is enabled", () => {
|
|
51
|
+
const flags = new FeatureFlags({
|
|
52
|
+
"superblocks.dev-server.in-place-template-restart.enabled": true,
|
|
53
|
+
});
|
|
54
|
+
expect(flags.inPlaceTemplateRestartEnabled()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns false when the flag is explicitly disabled", () => {
|
|
58
|
+
const flags = new FeatureFlags({
|
|
59
|
+
"superblocks.dev-server.in-place-template-restart.enabled": false,
|
|
60
|
+
});
|
|
61
|
+
expect(flags.inPlaceTemplateRestartEnabled()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
package/src/flag.ts
CHANGED
|
@@ -2,6 +2,17 @@ import { DEFAULT_LINES_FOR_LARGE_STEPS } from "@superblocksteam/util";
|
|
|
2
2
|
|
|
3
3
|
import type { FlagBootstrap } from "./types/index.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Resolves a feature gate from its LaunchDarkly flag value OR a process-env
|
|
7
|
+
* escape hatch (used by cloud-prem/local, where LD is unavailable). Centralizes
|
|
8
|
+
* the `flag ?? false || env === "true"` shape so each gate names its
|
|
9
|
+
* flag-getter/env-name pairing in exactly one place at the call site.
|
|
10
|
+
*/
|
|
11
|
+
export const flagOrEnv = (
|
|
12
|
+
flagValue: boolean | undefined,
|
|
13
|
+
envName: string,
|
|
14
|
+
): boolean => (flagValue ?? false) || process.env[envName] === "true";
|
|
15
|
+
|
|
5
16
|
export const signingEnabled = (flags: FlagBootstrap): boolean => {
|
|
6
17
|
return flags["ui.enable-resource-signing"] ?? false;
|
|
7
18
|
};
|
|
@@ -47,4 +58,17 @@ export class FeatureFlags {
|
|
|
47
58
|
devServerMemoryMetricsEnabled(): boolean {
|
|
48
59
|
return this.flags["superblocks.dev-server.memory-metrics.enabled"] ?? false;
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Gates the in-place dev-server process restart after a v2->v3 template
|
|
64
|
+
* flip. When enabled (and a restart supervisor is present), the
|
|
65
|
+
* dev server self-restarts its own process so the pod stays alive instead of
|
|
66
|
+
* being terminated and replaced. Flag off restores today's pod-swap behavior.
|
|
67
|
+
*/
|
|
68
|
+
inPlaceTemplateRestartEnabled(): boolean {
|
|
69
|
+
return (
|
|
70
|
+
this.flags["superblocks.dev-server.in-place-template-restart.enabled"] ??
|
|
71
|
+
false
|
|
72
|
+
);
|
|
73
|
+
}
|
|
50
74
|
}
|
package/src/index.ts
CHANGED
|
@@ -8,7 +8,9 @@ export {
|
|
|
8
8
|
type CreateApplicationResult,
|
|
9
9
|
type CurrentBranch,
|
|
10
10
|
type BillingUsageLimitsResponse,
|
|
11
|
+
type BillingUsageRecordsResponse,
|
|
11
12
|
type DailyUsageRow,
|
|
13
|
+
type DollarLedgerRow,
|
|
12
14
|
type GetCommitsResponseBody,
|
|
13
15
|
type IntegrationFilters,
|
|
14
16
|
type IntegrationSummary,
|
|
@@ -68,6 +70,13 @@ export {
|
|
|
68
70
|
|
|
69
71
|
export { AUTO_UPGRADE_EXIT_CODE } from "./cli-replacement/automatic-upgrades.js";
|
|
70
72
|
export { RESTART_EXIT_CODE } from "./dev-utils/dev-server.mjs";
|
|
73
|
+
// Surfaces the canonical restart-contract markers alongside RESTART_EXIT_CODE so
|
|
74
|
+
// the builtins-only CLI parent copies can be drift-checked against it in tests.
|
|
75
|
+
export {
|
|
76
|
+
RESTART_REASON_ENV,
|
|
77
|
+
RESTART_REASON_FLIP_RESTART,
|
|
78
|
+
RESTART_SUPERVISED_ENV,
|
|
79
|
+
} from "@superblocksteam/library-shared/restart-contract";
|
|
71
80
|
|
|
72
81
|
export { createDevServer, preWarmViteCache } from "./dev-utils/dev-server.mjs";
|
|
73
82
|
export {
|
package/src/sdk.ts
CHANGED
|
@@ -41,6 +41,7 @@ import {
|
|
|
41
41
|
fetchBillingUsageDaily,
|
|
42
42
|
fetchBillingUsageLimits,
|
|
43
43
|
fetchBillingUsageRecords,
|
|
44
|
+
fetchBillingUsageRecordsResponse,
|
|
44
45
|
fetchCurrentUser,
|
|
45
46
|
fetchFact,
|
|
46
47
|
fetchFacts,
|
|
@@ -76,6 +77,7 @@ import type {
|
|
|
76
77
|
CreateApplicationResult,
|
|
77
78
|
CreateRbacAssignmentsRequest,
|
|
78
79
|
BillingUsageLimitsResponse,
|
|
80
|
+
BillingUsageRecordsResponse,
|
|
79
81
|
DailyUsageRow,
|
|
80
82
|
FolderMutationPayload,
|
|
81
83
|
FolderSummary,
|
|
@@ -108,6 +110,11 @@ import type { DeploymentHistoryEntryDto, UserMeDto } from "./types/index.js";
|
|
|
108
110
|
|
|
109
111
|
// Exporting here instead of index.ts because only sdk.ts is exposed outside in package.json
|
|
110
112
|
export * from "./errors.js";
|
|
113
|
+
export {
|
|
114
|
+
RESTART_REASON_ENV,
|
|
115
|
+
RESTART_REASON_FLIP_RESTART,
|
|
116
|
+
RESTART_SUPERVISED_ENV,
|
|
117
|
+
} from "@superblocksteam/library-shared/restart-contract";
|
|
111
118
|
|
|
112
119
|
export class SuperblocksSdk {
|
|
113
120
|
token = "";
|
|
@@ -891,6 +898,19 @@ export class SuperblocksSdk {
|
|
|
891
898
|
});
|
|
892
899
|
}
|
|
893
900
|
|
|
901
|
+
async fetchBillingUsageRecordsResponse(
|
|
902
|
+
startDate: string,
|
|
903
|
+
endDate: string,
|
|
904
|
+
): Promise<BillingUsageRecordsResponse> {
|
|
905
|
+
return fetchBillingUsageRecordsResponse({
|
|
906
|
+
cliVersion: this.cliVersion,
|
|
907
|
+
token: this.token,
|
|
908
|
+
superblocksBaseUrl: this.superblocksBaseUrl,
|
|
909
|
+
startDate,
|
|
910
|
+
endDate,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
894
914
|
async fetchBillingPlanSummary(): Promise<PlanSummary> {
|
|
895
915
|
return fetchBillingPlanSummary({
|
|
896
916
|
cliVersion: this.cliVersion,
|
package/src/types/common.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface FlagBootstrap {
|
|
|
18
18
|
"superblocks.dev-server.inactivity-timeout.local"?: number;
|
|
19
19
|
"superblocks.dev-server.llmobs.enabled"?: boolean;
|
|
20
20
|
"superblocks.dev-server.memory-metrics.enabled"?: boolean;
|
|
21
|
+
"superblocks.dev-server.in-place-template-restart.enabled"?: boolean;
|
|
21
22
|
"superblocks.deploy.include-dot-superblocks"?: boolean;
|
|
22
23
|
}
|
|
23
24
|
|