@superblocksteam/sdk 2.0.151 → 2.0.152-next.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/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/automatic-upgrades.d.ts.map +1 -1
- package/dist/cli-replacement/automatic-upgrades.js +8 -6
- package/dist/cli-replacement/automatic-upgrades.js.map +1 -1
- package/dist/cli-replacement/dev-s3-restore.test.mjs +1 -0
- package/dist/cli-replacement/dev-s3-restore.test.mjs.map +1 -1
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs +1 -0
- package/dist/cli-replacement/dev-startup-git-before-dbfs-order.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.d.mts +148 -1
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.interception.test.mjs +259 -4
- package/dist/cli-replacement/dev.interception.test.mjs.map +1 -1
- package/dist/cli-replacement/dev.mjs +429 -28
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/client.billing-usage.test.js +25 -0
- package/dist/client.billing-usage.test.js.map +1 -1
- package/dist/client.d.ts +19 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js.map +1 -1
- package/dist/collect-sdk-apis.d.mts +61 -3
- package/dist/collect-sdk-apis.d.mts.map +1 -1
- package/dist/collect-sdk-apis.mjs +97 -6
- package/dist/collect-sdk-apis.mjs.map +1 -1
- package/dist/collect-sdk-apis.test.mjs +277 -1
- package/dist/collect-sdk-apis.test.mjs.map +1 -1
- package/dist/dev-utils/dev-server-metrics.d.mts +17 -0
- package/dist/dev-utils/dev-server-metrics.d.mts.map +1 -1
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts +2 -0
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.d.mts.map +1 -0
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs +108 -0
- package/dist/dev-utils/dev-server-metrics.dependency-failure.test.mjs.map +1 -0
- package/dist/dev-utils/dev-server-metrics.mjs +16 -2
- package/dist/dev-utils/dev-server-metrics.mjs.map +1 -1
- package/dist/dev-utils/dev-server.d.mts +9 -0
- package/dist/dev-utils/dev-server.d.mts.map +1 -1
- package/dist/dev-utils/dev-server.mjs +44 -27
- package/dist/dev-utils/dev-server.mjs.map +1 -1
- package/dist/dev-utils/dev-server.status.test.mjs +31 -1
- package/dist/dev-utils/dev-server.status.test.mjs.map +1 -1
- package/dist/dev-utils/fatal-exit.d.mts +17 -4
- package/dist/dev-utils/fatal-exit.d.mts.map +1 -1
- package/dist/dev-utils/fatal-exit.mjs +4 -3
- package/dist/dev-utils/fatal-exit.mjs.map +1 -1
- package/dist/extract-api-integrations.d.mts +1 -1
- package/dist/extract-api-integrations.d.mts.map +1 -1
- package/dist/extract-api-integrations.mjs +1 -1
- package/dist/extract-api-integrations.mjs.map +1 -1
- package/dist/flag.d.ts +5 -0
- package/dist/flag.d.ts.map +1 -1
- package/dist/flag.js +7 -0
- package/dist/flag.js.map +1 -1
- package/dist/flag.test.js +18 -0
- package/dist/flag.test.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.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/dist/vite-plugin-generate-api-build-manifest.d.mts.map +1 -1
- package/dist/vite-plugin-generate-api-build-manifest.mjs +46 -0
- package/dist/vite-plugin-generate-api-build-manifest.mjs.map +1 -1
- package/package.json +6 -6
- package/src/cli-replacement/automatic-upgrades.ts +8 -6
- package/src/cli-replacement/dev-s3-restore.test.mts +1 -0
- package/src/cli-replacement/dev-startup-git-before-dbfs-order.test.mts +1 -0
- package/src/cli-replacement/dev.interception.test.mts +334 -10
- package/src/cli-replacement/dev.mts +499 -33
- package/src/client.billing-usage.test.ts +26 -0
- package/src/client.ts +21 -0
- package/src/collect-sdk-apis.mts +141 -5
- package/src/collect-sdk-apis.test.mts +380 -1
- package/src/dev-utils/dev-server-metrics.dependency-failure.test.mts +149 -0
- package/src/dev-utils/dev-server-metrics.mts +25 -1
- package/src/dev-utils/dev-server.mts +52 -29
- package/src/dev-utils/dev-server.status.test.mts +42 -0
- package/src/dev-utils/fatal-exit.mts +18 -4
- package/src/extract-api-integrations.mts +3 -0
- package/src/flag.test.ts +21 -0
- package/src/flag.ts +8 -0
- package/src/index.ts +1 -0
- package/src/types/common.ts +1 -0
- package/src/vite-plugin-generate-api-build-manifest.mts +74 -4
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import type { devServerMetrics as DevServerMetricsSingleton } from "./dev-server-metrics.mjs";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Dependency-failure counter emission tests.
|
|
7
|
+
*
|
|
8
|
+
* These assert on what the COUNTER receives, not on what the recorder method
|
|
9
|
+
* was called with. That distinction is the whole point of this file: an earlier
|
|
10
|
+
* revision of `recordUpgradeFailure` accepted an `outcome` argument and passed
|
|
11
|
+
* it down, but the shared `recordDependencyFailure` helper rebuilt its own
|
|
12
|
+
* `labels` object from a fixed destructure — so `outcome` was silently dropped
|
|
13
|
+
* on the way to `counter.add()`. A test that spies on `recordUpgradeFailure`
|
|
14
|
+
* passes against that bug; only a test that listens at the meter boundary
|
|
15
|
+
* catches it.
|
|
16
|
+
*
|
|
17
|
+
* The fake meter is captured at `getMeter()`, mirroring
|
|
18
|
+
* `dev-server-metrics.boot.test.mts`. The only substitutions are the two
|
|
19
|
+
* telemetry-bootstrap boundaries the module depends on
|
|
20
|
+
* (`isTelemetryInitialized`, `getMeter`).
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
type Add = { name: string; value: number; attrs: Record<string, unknown> };
|
|
24
|
+
|
|
25
|
+
const h = vi.hoisted((): { adds: Add[]; telemetryUp: boolean } => ({
|
|
26
|
+
adds: [],
|
|
27
|
+
telemetryUp: true,
|
|
28
|
+
}));
|
|
29
|
+
|
|
30
|
+
vi.mock("@superblocksteam/telemetry", () => ({
|
|
31
|
+
isTelemetryInitialized: () => h.telemetryUp,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
vi.mock("../telemetry/index.js", () => ({
|
|
35
|
+
getMeter: () => ({
|
|
36
|
+
createCounter: (name: string) => ({
|
|
37
|
+
add: (value: number, attrs: Record<string, unknown>) =>
|
|
38
|
+
h.adds.push({ name, value, attrs }),
|
|
39
|
+
}),
|
|
40
|
+
createHistogram: () => ({ record: () => {} }),
|
|
41
|
+
}),
|
|
42
|
+
}));
|
|
43
|
+
|
|
44
|
+
const addsFor = (name: string) => h.adds.filter((a) => a.name === name);
|
|
45
|
+
const UPGRADE_COUNTER = "dev_server_auto_upgrade_failure_total";
|
|
46
|
+
const INSTALL_COUNTER = "dev_server_initial_install_failure_total";
|
|
47
|
+
|
|
48
|
+
describe("devServerMetrics dependency-failure counters", () => {
|
|
49
|
+
let devServerMetrics: typeof DevServerMetricsSingleton;
|
|
50
|
+
|
|
51
|
+
beforeEach(async () => {
|
|
52
|
+
h.adds = [];
|
|
53
|
+
h.telemetryUp = true;
|
|
54
|
+
vi.resetModules();
|
|
55
|
+
({ devServerMetrics } = await import("./dev-server-metrics.mjs"));
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it("puts `outcome` on the auto-upgrade counter the exporter actually sees", () => {
|
|
59
|
+
devServerMetrics.recordUpgradeFailure({
|
|
60
|
+
category: "unknown",
|
|
61
|
+
npmErrorCode: "ETARGET",
|
|
62
|
+
hasAnyRegistryConfigured: true,
|
|
63
|
+
outcome: "exit",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const upgrades = addsFor(UPGRADE_COUNTER);
|
|
67
|
+
expect(upgrades).toHaveLength(1);
|
|
68
|
+
expect(upgrades[0].value).toBe(1);
|
|
69
|
+
expect(upgrades[0].attrs).toEqual({
|
|
70
|
+
category: "unknown",
|
|
71
|
+
npm_error_code: "ETARGET",
|
|
72
|
+
has_any_registry_configured: "true",
|
|
73
|
+
outcome: "exit",
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("distinguishes the degrade outcome from the exit outcome on the counter", () => {
|
|
78
|
+
devServerMetrics.recordUpgradeFailure({
|
|
79
|
+
category: "registry_unreachable",
|
|
80
|
+
outcome: "degrade",
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const upgrades = addsFor(UPGRADE_COUNTER);
|
|
84
|
+
expect(upgrades).toHaveLength(1);
|
|
85
|
+
expect(upgrades[0].attrs).toMatchObject({ outcome: "degrade" });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("omits `outcome` entirely when the caller did not supply one", () => {
|
|
89
|
+
// A missing outcome must not mint an `outcome: undefined` facet — an
|
|
90
|
+
// undefined attribute value is not the same series as no attribute.
|
|
91
|
+
devServerMetrics.recordUpgradeFailure({ category: "tls_failed" });
|
|
92
|
+
|
|
93
|
+
const upgrades = addsFor(UPGRADE_COUNTER);
|
|
94
|
+
expect(upgrades).toHaveLength(1);
|
|
95
|
+
expect(upgrades[0].attrs).not.toHaveProperty("outcome");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("keeps the initial-install counter's label set unchanged (no `outcome`)", () => {
|
|
99
|
+
// The install path has no exit/degrade split, so adding an empty `outcome`
|
|
100
|
+
// facet there would split the existing series for no diagnostic gain.
|
|
101
|
+
devServerMetrics.recordInitialInstallFailure({
|
|
102
|
+
category: "dependency_conflict",
|
|
103
|
+
npmErrorCode: "ERESOLVE",
|
|
104
|
+
hasAnyRegistryConfigured: false,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const installs = addsFor(INSTALL_COUNTER);
|
|
108
|
+
expect(installs).toHaveLength(1);
|
|
109
|
+
expect(installs[0].attrs).toEqual({
|
|
110
|
+
category: "dependency_conflict",
|
|
111
|
+
npm_error_code: "ERESOLVE",
|
|
112
|
+
has_any_registry_configured: "false",
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("still bounds npm_error_code and has_any_registry_configured with `outcome` present", () => {
|
|
117
|
+
devServerMetrics.recordUpgradeFailure({
|
|
118
|
+
category: "unknown",
|
|
119
|
+
npmErrorCode: "`;rm -rf /#",
|
|
120
|
+
outcome: "exit",
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const upgrades = addsFor(UPGRADE_COUNTER);
|
|
124
|
+
expect(upgrades[0].attrs).toEqual({
|
|
125
|
+
category: "unknown",
|
|
126
|
+
npm_error_code: "other",
|
|
127
|
+
has_any_registry_configured: "unknown",
|
|
128
|
+
outcome: "exit",
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it("replays a buffered upgrade failure with its `outcome` intact", () => {
|
|
133
|
+
// The exit path records while telemetry may still be down; the label has to
|
|
134
|
+
// survive the buffer round-trip, not just the direct call.
|
|
135
|
+
h.telemetryUp = false;
|
|
136
|
+
devServerMetrics.recordUpgradeFailure({
|
|
137
|
+
category: "unknown",
|
|
138
|
+
outcome: "exit",
|
|
139
|
+
});
|
|
140
|
+
expect(addsFor(UPGRADE_COUNTER)).toHaveLength(0);
|
|
141
|
+
|
|
142
|
+
h.telemetryUp = true;
|
|
143
|
+
devServerMetrics.flush();
|
|
144
|
+
|
|
145
|
+
const upgrades = addsFor(UPGRADE_COUNTER);
|
|
146
|
+
expect(upgrades).toHaveLength(1);
|
|
147
|
+
expect(upgrades[0].attrs).toMatchObject({ outcome: "exit" });
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -414,6 +414,15 @@ class DevServerMetrics {
|
|
|
414
414
|
* - `npm_error_code` is a bounded npm error code string (e.g. "E401",
|
|
415
415
|
* "ERESOLVE") or `"none"` when absent.
|
|
416
416
|
* - `has_any_registry_configured` is `"true"` / `"false"` / `"unknown"`.
|
|
417
|
+
* - `outcome`, when the caller supplies one, is a closed enum owned by that
|
|
418
|
+
* caller (see `recordUpgradeFailure`).
|
|
419
|
+
*
|
|
420
|
+
* Every label a caller passes must be destructured here AND placed into
|
|
421
|
+
* `labels`. This helper is the only thing that reaches `counter.add`, so a
|
|
422
|
+
* field accepted by a public wrapper but omitted here is silently dropped on
|
|
423
|
+
* the way to the exporter — and TypeScript cannot catch it, because `labels`
|
|
424
|
+
* is a typed variable rather than a fresh object literal at the call site.
|
|
425
|
+
* That is exactly how `outcome` went missing.
|
|
417
426
|
*
|
|
418
427
|
* Do NOT pass raw package names, registry hostnames, or rawError — those are
|
|
419
428
|
* high-cardinality or contain customer-infra identifiers.
|
|
@@ -425,10 +434,12 @@ class DevServerMetrics {
|
|
|
425
434
|
category,
|
|
426
435
|
npmErrorCode,
|
|
427
436
|
hasAnyRegistryConfigured,
|
|
437
|
+
outcome,
|
|
428
438
|
}: {
|
|
429
439
|
category: string;
|
|
430
440
|
npmErrorCode?: string;
|
|
431
441
|
hasAnyRegistryConfigured?: boolean;
|
|
442
|
+
outcome?: string;
|
|
432
443
|
},
|
|
433
444
|
): void {
|
|
434
445
|
const labels = {
|
|
@@ -438,6 +449,11 @@ class DevServerMetrics {
|
|
|
438
449
|
hasAnyRegistryConfigured === undefined
|
|
439
450
|
? "unknown"
|
|
440
451
|
: String(hasAnyRegistryConfigured),
|
|
452
|
+
// Spread conditionally rather than assigning `outcome` directly: the
|
|
453
|
+
// initial-install counter has no exit/degrade split, and emitting an
|
|
454
|
+
// `outcome: undefined` attribute there would fork its existing series
|
|
455
|
+
// for no diagnostic gain.
|
|
456
|
+
...(outcome === undefined ? {} : { outcome }),
|
|
441
457
|
};
|
|
442
458
|
this.record(() => {
|
|
443
459
|
getMeter().createCounter(counterName, { description }).add(1, labels);
|
|
@@ -463,10 +479,18 @@ class DevServerMetrics {
|
|
|
463
479
|
category: string;
|
|
464
480
|
npmErrorCode?: string;
|
|
465
481
|
hasAnyRegistryConfigured?: boolean;
|
|
482
|
+
/**
|
|
483
|
+
* What the failure did to this process: `degrade` kept the current version
|
|
484
|
+
* running, `exit` terminated so the sandbox is replaced. Without this the
|
|
485
|
+
* two outcomes are indistinguishable, so a crash-loop of exiting pods looks
|
|
486
|
+
* identical to a fleet of pods quietly serving the older version.
|
|
487
|
+
* Low-cardinality: a closed two-value enum.
|
|
488
|
+
*/
|
|
489
|
+
outcome?: "degrade" | "exit";
|
|
466
490
|
}): void {
|
|
467
491
|
this.recordDependencyFailure(
|
|
468
492
|
"dev_server_auto_upgrade_failure_total",
|
|
469
|
-
"Count of
|
|
493
|
+
"Count of dev-server startups where a best-effort CLI/library auto-upgrade failed, labelled by outcome: `degrade` (kept the current version) or `exit` (terminated so the sandbox is replaced).",
|
|
470
494
|
labels,
|
|
471
495
|
);
|
|
472
496
|
}
|
|
@@ -90,6 +90,36 @@ export function getDevServerTracer() {
|
|
|
90
90
|
return getTracer();
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
/**
|
|
94
|
+
* Every `/_sb_*` control endpoint, keyed by its low-cardinality metrics label.
|
|
95
|
+
* The request path is always `/` + the label.
|
|
96
|
+
*
|
|
97
|
+
* The value is why the endpoint must answer even when Vite is not up, which
|
|
98
|
+
* exempts it from the "hold the request until Vite is initialized" middleware,
|
|
99
|
+
* or null when the endpoint is held like ordinary traffic. Typed as an
|
|
100
|
+
* exhaustive `Record` over `DevServerEndpoint`, so an endpoint cannot be added
|
|
101
|
+
* without declaring its hold behavior. Both `endpointForUrl` and
|
|
102
|
+
* `isDevServerControlPath` read this table, so the metrics label and the
|
|
103
|
+
* exemption cannot drift apart.
|
|
104
|
+
*/
|
|
105
|
+
const DEV_SERVER_ENDPOINTS: Readonly<Record<DevServerEndpoint, string | null>> =
|
|
106
|
+
{
|
|
107
|
+
_sb_activate: null,
|
|
108
|
+
_sb_activity: "idle-reaper poll; reads the activity tracker, not Vite",
|
|
109
|
+
_sb_connect: "carries the Vite init failure the editor surfaces",
|
|
110
|
+
_sb_disconnect: "teardown has to work when Vite never came up",
|
|
111
|
+
_sb_health:
|
|
112
|
+
"the editor gates page load on it; holding it hides the failure",
|
|
113
|
+
_sb_persist: "archives the workspace off disk; never touches Vite",
|
|
114
|
+
_sb_ready: null,
|
|
115
|
+
_sb_status: "reports serverErrors, which only matter once Vite failed",
|
|
116
|
+
_sb_update_token: null,
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
function isDevServerEndpoint(label: string): label is DevServerEndpoint {
|
|
120
|
+
return Object.hasOwn(DEV_SERVER_ENDPOINTS, label);
|
|
121
|
+
}
|
|
122
|
+
|
|
93
123
|
/**
|
|
94
124
|
* Maps a dev-server request URL onto a low-cardinality endpoint label for
|
|
95
125
|
* metrics. Returns undefined for non-`/_sb_*` URLs (Vite-proxied traffic).
|
|
@@ -99,28 +129,22 @@ function endpointForUrl(
|
|
|
99
129
|
): DevServerEndpoint | undefined {
|
|
100
130
|
if (!url) return undefined;
|
|
101
131
|
const path = url.split("?", 1)[0];
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
return "_sb_persist";
|
|
119
|
-
case "/_sb_ready":
|
|
120
|
-
return "_sb_ready";
|
|
121
|
-
default:
|
|
122
|
-
return undefined;
|
|
123
|
-
}
|
|
132
|
+
if (!path.startsWith("/")) return undefined;
|
|
133
|
+
const label = path.slice(1);
|
|
134
|
+
return isDevServerEndpoint(label) ? label : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* True when a request must be answered even if Vite never initialized, so the
|
|
139
|
+
* hold middleware lets it through. `DEV_SERVER_ENDPOINTS` says which endpoints
|
|
140
|
+
* qualify and why.
|
|
141
|
+
*
|
|
142
|
+
* Compares the path, not the raw URL: the editor polls `/_sb_health?t=<epoch>`
|
|
143
|
+
* with a cache-buster, so a raw-URL comparison drops that poll into the hold.
|
|
144
|
+
*/
|
|
145
|
+
export function isDevServerControlPath(url: string | undefined): boolean {
|
|
146
|
+
const endpoint = endpointForUrl(url);
|
|
147
|
+
return endpoint !== undefined && DEV_SERVER_ENDPOINTS[endpoint] !== null;
|
|
124
148
|
}
|
|
125
149
|
|
|
126
150
|
/**
|
|
@@ -911,13 +935,7 @@ export async function createDevServer({
|
|
|
911
935
|
res.setHeader("Cache-Control", "no-store, max-age=0");
|
|
912
936
|
res.setHeader("x-csb-no-sw-proxy", "1");
|
|
913
937
|
|
|
914
|
-
if (
|
|
915
|
-
req.url === "/_sb_connect" ||
|
|
916
|
-
req.url === "/_sb_disconnect" ||
|
|
917
|
-
req.url === "/_sb_health" ||
|
|
918
|
-
req.url === "/_sb_status" ||
|
|
919
|
-
req.url === "/_sb_activity"
|
|
920
|
-
) {
|
|
938
|
+
if (isDevServerControlPath(req.url)) {
|
|
921
939
|
return next();
|
|
922
940
|
}
|
|
923
941
|
|
|
@@ -1441,6 +1459,10 @@ export async function createDevServer({
|
|
|
1441
1459
|
featureFlags?.inPlaceTemplateRestartEnabled(),
|
|
1442
1460
|
"SUPERBLOCKS_DEV_SERVER_IN_PLACE_TEMPLATE_RESTART_ENABLED",
|
|
1443
1461
|
),
|
|
1462
|
+
autoCommitCheckpointsToGitEnabled: flagOrEnv(
|
|
1463
|
+
featureFlags?.removeManualCommitsEnabled(),
|
|
1464
|
+
"SUPERBLOCKS_REMOVE_MANUAL_COMMITS_ENABLED",
|
|
1465
|
+
),
|
|
1444
1466
|
},
|
|
1445
1467
|
});
|
|
1446
1468
|
// Note: vitePromise itself gets a no-op .catch() at its construction site
|
|
@@ -1517,6 +1539,7 @@ async function startVite({
|
|
|
1517
1539
|
features: {
|
|
1518
1540
|
enableSessionRecording: boolean;
|
|
1519
1541
|
inPlaceTemplateRestartEnabled: boolean;
|
|
1542
|
+
autoCommitCheckpointsToGitEnabled: boolean;
|
|
1520
1543
|
};
|
|
1521
1544
|
prepareInPlaceRestart: (options?: { flipRestart?: true }) => Promise<void>;
|
|
1522
1545
|
}): Promise<{
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
buildStatusPayload,
|
|
11
11
|
DEV_SERVER_BOOT_ID,
|
|
12
12
|
DEV_SERVER_STARTED_AT,
|
|
13
|
+
isDevServerControlPath,
|
|
13
14
|
} from "./dev-server.mjs";
|
|
14
15
|
|
|
15
16
|
describe("buildStatusPayload", () => {
|
|
@@ -90,3 +91,44 @@ describe("buildHealthResponse", () => {
|
|
|
90
91
|
expect(health.startedAt).toBe(DEV_SERVER_STARTED_AT);
|
|
91
92
|
});
|
|
92
93
|
});
|
|
94
|
+
|
|
95
|
+
describe("isDevServerControlPath", () => {
|
|
96
|
+
it.each([
|
|
97
|
+
"/_sb_connect",
|
|
98
|
+
"/_sb_disconnect",
|
|
99
|
+
"/_sb_health",
|
|
100
|
+
"/_sb_status",
|
|
101
|
+
"/_sb_activity",
|
|
102
|
+
"/_sb_persist",
|
|
103
|
+
])("treats %s as a control path", (url) => {
|
|
104
|
+
expect(isDevServerControlPath(url)).toBe(true);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it.each(["/_sb_activate", "/_sb_ready", "/_sb_update_token"])(
|
|
108
|
+
"leaves %s subject to the vite hold",
|
|
109
|
+
(url) => {
|
|
110
|
+
expect(isDevServerControlPath(url)).toBe(false);
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
it("treats a control path carrying a query string as a control path", () => {
|
|
115
|
+
expect(isDevServerControlPath("/_sb_health?t=1785959985730")).toBe(true);
|
|
116
|
+
expect(isDevServerControlPath("/_sb_connect?foo=bar&baz=1")).toBe(true);
|
|
117
|
+
expect(isDevServerControlPath("/_sb_persist?t=1")).toBe(true);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it("does not claim vite-proxied traffic", () => {
|
|
121
|
+
expect(isDevServerControlPath("/src/main.tsx")).toBe(false);
|
|
122
|
+
expect(isDevServerControlPath("/_sb_healthcheck")).toBe(false);
|
|
123
|
+
expect(isDevServerControlPath("/_sb_health/extra")).toBe(false);
|
|
124
|
+
expect(isDevServerControlPath(undefined)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// The lookup is by object key, so Object.prototype members must not match.
|
|
128
|
+
it.each(["/toString", "/constructor", "/hasOwnProperty", "/__proto__"])(
|
|
129
|
+
"does not treat %s as a control path",
|
|
130
|
+
(url) => {
|
|
131
|
+
expect(isDevServerControlPath(url)).toBe(false);
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
});
|
|
@@ -25,9 +25,10 @@
|
|
|
25
25
|
* `handler=containerExit` line for the same exit. A signal kill / OOM emits only
|
|
26
26
|
* the `containerExit` line (no JS handler ran); a JS crash where shutdown exits
|
|
27
27
|
* first with code 0 emits only the JS-handler line (the supervisor sees a clean
|
|
28
|
-
* exit).
|
|
29
|
-
*
|
|
30
|
-
* count
|
|
28
|
+
* exit). A deliberate startup exit emits `handler=startupFailure` from the child
|
|
29
|
+
* plus the supervisor's `containerExit` line, i.e. the same double shape. So a
|
|
30
|
+
* raw `count(event=dev_server_fatal_exit)` over-counts the double case:
|
|
31
|
+
* deduplicate by pod within a short time window for a true unique-crash count.
|
|
31
32
|
*
|
|
32
33
|
* Signal-kill field note: for a signal kill the cold parent emits
|
|
33
34
|
* `exit_code=null signal=SIGKILL` (Node's `child.on("exit")` gives a null code +
|
|
@@ -49,11 +50,24 @@
|
|
|
49
50
|
* and the warm entrypoint shell emit this
|
|
50
51
|
* value. They cannot import this module,
|
|
51
52
|
* so they repeat the same string.
|
|
53
|
+
* startupFailure `dev()` decided at startup that it
|
|
54
|
+
* cannot serve (sync/lock/setup failure,
|
|
55
|
+
* or a dependency upgrade that may have
|
|
56
|
+
* tainted the live tree in a disposable
|
|
57
|
+
* sandbox) and is exiting deliberately.
|
|
58
|
+
* No JS error handler is involved; the
|
|
59
|
+
* adjacent `[dev-server] Startup failed
|
|
60
|
+
* during …` line names the stage.
|
|
61
|
+
*
|
|
62
|
+
* Adding a VALUE here (as `startupFailure` was) needs no change in the two
|
|
63
|
+
* emitters that cannot import this module — they each emit their own fixed
|
|
64
|
+
* value. Only adding a FIELD does; see the SYNC note on `buildFatalExitLog`.
|
|
52
65
|
*/
|
|
53
66
|
type FatalExitHandler =
|
|
54
67
|
| "uncaughtException"
|
|
55
68
|
| "unhandledRejection"
|
|
56
|
-
| "containerExit"
|
|
69
|
+
| "containerExit"
|
|
70
|
+
| "startupFailure";
|
|
57
71
|
|
|
58
72
|
/** Fixed token so a log-based metric or a log search can pick out these lines. */
|
|
59
73
|
export const FATAL_EXIT_EVENT = "dev_server_fatal_exit";
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
export {
|
|
2
|
+
extractApiIntegrations,
|
|
2
3
|
extractIntegrationIdsFromSource,
|
|
3
4
|
extractIntegrationsFromSource,
|
|
5
|
+
resolveRelativeImportPath,
|
|
6
|
+
type ExtractApiIntegrationsResult,
|
|
4
7
|
type IntegrationInfo,
|
|
5
8
|
} from "@superblocksteam/vite-plugin-file-sync/extract-api-integrations";
|
package/src/flag.test.ts
CHANGED
|
@@ -41,6 +41,27 @@ describe("flagOrEnv", () => {
|
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
44
|
+
describe("FeatureFlags.removeManualCommitsEnabled", () => {
|
|
45
|
+
it("defaults to false when the flag is absent", () => {
|
|
46
|
+
const flags = new FeatureFlags({});
|
|
47
|
+
expect(flags.removeManualCommitsEnabled()).toBe(false);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("returns true when the flag is enabled", () => {
|
|
51
|
+
const flags = new FeatureFlags({
|
|
52
|
+
"superblocks.remove-manual-commits.enabled": true,
|
|
53
|
+
});
|
|
54
|
+
expect(flags.removeManualCommitsEnabled()).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns false when the flag is explicitly disabled", () => {
|
|
58
|
+
const flags = new FeatureFlags({
|
|
59
|
+
"superblocks.remove-manual-commits.enabled": false,
|
|
60
|
+
});
|
|
61
|
+
expect(flags.removeManualCommitsEnabled()).toBe(false);
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
44
65
|
describe("FeatureFlags.inPlaceTemplateRestartEnabled", () => {
|
|
45
66
|
it("defaults to false when the flag is absent", () => {
|
|
46
67
|
const flags = new FeatureFlags({});
|
package/src/flag.ts
CHANGED
|
@@ -59,6 +59,14 @@ export class FeatureFlags {
|
|
|
59
59
|
return this.flags["superblocks.dev-server.memory-metrics.enabled"] ?? false;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Gates the unified version history work. While off, autosave keeps its
|
|
64
|
+
* original fixed-window cadence so dev servers behave exactly as before.
|
|
65
|
+
*/
|
|
66
|
+
removeManualCommitsEnabled(): boolean {
|
|
67
|
+
return this.flags["superblocks.remove-manual-commits.enabled"] ?? false;
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
/**
|
|
63
71
|
* Gates the in-place dev-server process restart after a v2->v3 template
|
|
64
72
|
* flip. When enabled (and a restart supervisor is present), the
|
package/src/index.ts
CHANGED
package/src/types/common.ts
CHANGED
|
@@ -20,6 +20,7 @@ export interface FlagBootstrap {
|
|
|
20
20
|
"superblocks.dev-server.memory-metrics.enabled"?: boolean;
|
|
21
21
|
"superblocks.dev-server.in-place-template-restart.enabled"?: boolean;
|
|
22
22
|
"superblocks.deploy.include-dot-superblocks"?: boolean;
|
|
23
|
+
"superblocks.remove-manual-commits.enabled"?: boolean;
|
|
23
24
|
}
|
|
24
25
|
|
|
25
26
|
export interface TokenConfig {
|
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
|
|
18
18
|
import { collectSdkApisFromRegistry } from "./collect-sdk-apis.mjs";
|
|
19
19
|
import { getLogger } from "./telemetry/logging.js";
|
|
20
|
+
import type { Logger } from "./telemetry/logging.js";
|
|
20
21
|
|
|
21
22
|
const BUILD_MANIFEST_SUFFIX = path.join(
|
|
22
23
|
"lib",
|
|
@@ -35,6 +36,32 @@ function isBuildManifestModule(id: string): boolean {
|
|
|
35
36
|
);
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Emits a structured WARN when the logger supports it, falling back to a
|
|
41
|
+
* plain `warn` with just the human-readable message otherwise.
|
|
42
|
+
* `warnStructured` is optional on the `Logger` type (future/alternate
|
|
43
|
+
* implementations may omit it), so calling it with `?.` alone would let an
|
|
44
|
+
* auth-relevant warning disappear silently on such a logger. Both call
|
|
45
|
+
* sites in this plugin route through here instead of duplicating the
|
|
46
|
+
* fallback branch.
|
|
47
|
+
*
|
|
48
|
+
* `message` must be the plain, uncolored text — it becomes the OTel log
|
|
49
|
+
* body when `warnStructured` is available, and ANSI escapes have no place
|
|
50
|
+
* in a structured body. Coloring is applied only in the console-facing
|
|
51
|
+
* fallback branch below.
|
|
52
|
+
*/
|
|
53
|
+
function warnStructured(
|
|
54
|
+
logger: Logger,
|
|
55
|
+
message: string,
|
|
56
|
+
attributes: Record<string, string | number | boolean>,
|
|
57
|
+
): void {
|
|
58
|
+
if (logger.warnStructured) {
|
|
59
|
+
logger.warnStructured(message, attributes);
|
|
60
|
+
} else {
|
|
61
|
+
logger.warn(yellow(message));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
38
65
|
/**
|
|
39
66
|
* Vite plugin that generates a build manifest for the application.
|
|
40
67
|
*
|
|
@@ -108,10 +135,53 @@ export function generateApiBuildManifestPlugin(root: string) {
|
|
|
108
135
|
);
|
|
109
136
|
|
|
110
137
|
const sdkApis = sdkApiEnabled
|
|
111
|
-
? await collectSdkApisFromRegistry(
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
138
|
+
? await collectSdkApisFromRegistry(
|
|
139
|
+
root,
|
|
140
|
+
{
|
|
141
|
+
pathExists: (p) => fs.pathExists(p),
|
|
142
|
+
readFile: (p, enc) => fs.readFile(p, enc),
|
|
143
|
+
realpath: (p) => fs.realpath(p),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
onUnresolvedIntegrations: (
|
|
147
|
+
apiName,
|
|
148
|
+
entryPoint,
|
|
149
|
+
unresolvedKeys,
|
|
150
|
+
) => {
|
|
151
|
+
warnStructured(
|
|
152
|
+
logger,
|
|
153
|
+
`[superblocks] Could not resolve integration(s) ${unresolvedKeys
|
|
154
|
+
.map((k) => `"${k}"`)
|
|
155
|
+
.join(
|
|
156
|
+
", ",
|
|
157
|
+
)} declared in ${entryPoint} (API "${apiName}") — check-auth may be skipped for this API in deployed/preview mode. Declare the integration or its ID in the same file.`,
|
|
158
|
+
{
|
|
159
|
+
// Bounded/numeric only — api name, entry point, and the
|
|
160
|
+
// unresolved keys themselves are free-form text and stay
|
|
161
|
+
// in the message body (searchable, not faceted) to keep
|
|
162
|
+
// OTel log attribute cardinality low. See the
|
|
163
|
+
// `LogAttributes` doc comment in ./telemetry/logging.ts.
|
|
164
|
+
"sdk.integrations.unresolved_count": unresolvedKeys.length,
|
|
165
|
+
},
|
|
166
|
+
);
|
|
167
|
+
},
|
|
168
|
+
onExtractionError: (apiName, entryPoint, error) => {
|
|
169
|
+
const errorMessage =
|
|
170
|
+
error instanceof Error ? error.message : String(error);
|
|
171
|
+
warnStructured(
|
|
172
|
+
logger,
|
|
173
|
+
`[superblocks] Failed to extract integrations from ${entryPoint} (API "${apiName}"): ${errorMessage} — check-auth may be skipped for this API in deployed/preview mode.`,
|
|
174
|
+
{
|
|
175
|
+
// Bounded constant only — api name, entry point, and the
|
|
176
|
+
// raw error message are free-form text and stay in the
|
|
177
|
+
// message body. See the `LogAttributes` doc comment in
|
|
178
|
+
// ./telemetry/logging.ts.
|
|
179
|
+
"sdk.extraction.error_category": "read_or_extract_failed",
|
|
180
|
+
},
|
|
181
|
+
);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
)
|
|
115
185
|
: {};
|
|
116
186
|
|
|
117
187
|
manifestCode = `export default ${JSON.stringify({
|