@superblocksteam/sdk 2.0.129-next.0 → 2.0.130-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 +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
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
} from "@superblocksteam/shared";
|
|
16
16
|
import {
|
|
17
17
|
bucketNpmRegistryHost,
|
|
18
|
+
recordNpmInstall,
|
|
18
19
|
redactNpmRegistryHostsFromText,
|
|
19
20
|
} from "@superblocksteam/telemetry";
|
|
20
21
|
import type { LockService } from "@superblocksteam/vite-plugin-file-sync/lock-service";
|
|
@@ -31,6 +32,9 @@ import {
|
|
|
31
32
|
clearCliVersionCache,
|
|
32
33
|
} from "./version-detection.js";
|
|
33
34
|
|
|
35
|
+
// Child exit code that asks the `superblocks dev` parent supervisor to re-exec
|
|
36
|
+
// the dev server after the CLI upgrades its own on-disk binary. Full exit-code
|
|
37
|
+
// contract: packages/cli/packages/cli/src/commands/dev-parent.mts.
|
|
34
38
|
export const AUTO_UPGRADE_EXIT_CODE = 99;
|
|
35
39
|
|
|
36
40
|
/**
|
|
@@ -172,6 +176,12 @@ async function upgradeCliWithOclif(targetVersion: string): Promise<boolean> {
|
|
|
172
176
|
}
|
|
173
177
|
}
|
|
174
178
|
|
|
179
|
+
// Returns `true` when an install command actually ran and post-install
|
|
180
|
+
// validation passed (every other failure throws). Returns `false` only when no
|
|
181
|
+
// global install command could be resolved for the detected package manager — a
|
|
182
|
+
// no-op the caller marks as a failed span WITHOUT throwing, so the live-edit
|
|
183
|
+
// pod degrades on the current version instead of crash-looping. Proper graceful
|
|
184
|
+
// degrade + surfacing the failed upgrade to the user is tracked by APPS-4457.
|
|
175
185
|
async function upgradeCliWithPackageManager(
|
|
176
186
|
pm: DetectResult,
|
|
177
187
|
targetVersion: string,
|
|
@@ -181,7 +191,7 @@ async function upgradeCliWithPackageManager(
|
|
|
181
191
|
// `.superblocks/logs`). Optional so callers/tests that don't care keep
|
|
182
192
|
// npm's default `_logs` location.
|
|
183
193
|
logsDir?: string,
|
|
184
|
-
): Promise<
|
|
194
|
+
): Promise<boolean> {
|
|
185
195
|
const packageName = alias
|
|
186
196
|
? `@superblocksteam/cli@npm:${alias}@${targetVersion}`
|
|
187
197
|
: `@superblocksteam/cli@${targetVersion}`;
|
|
@@ -191,12 +201,57 @@ async function upgradeCliWithPackageManager(
|
|
|
191
201
|
const installCommand = resolveCommand(pm.agent, "global", installArgs);
|
|
192
202
|
|
|
193
203
|
if (!installCommand) {
|
|
204
|
+
// APPS-4544 (Bugbot 613adfc7): we couldn't even build a global install
|
|
205
|
+
// command for the detected package manager, so no upgrade happened.
|
|
206
|
+
// Before this PR added the `upgradeCli` span `outcome` attribute and the
|
|
207
|
+
// `superblocks.npm.install.*` metric, this no-op left no telemetry: the
|
|
208
|
+
// span kept its default `outcome=success` and no install metric was
|
|
209
|
+
// emitted, so an auto-upgrade that did nothing looked successful.
|
|
210
|
+
//
|
|
211
|
+
// Emit a failure sample so the histogram carries a `runner="auto_upgrade"`
|
|
212
|
+
// failure sample for this path, then return `false` so the caller flips
|
|
213
|
+
// the `upgradeCli` span to `outcome=failure`. `durationMs` is 0 because no
|
|
214
|
+
// install process ran; `outcome="other"` mirrors the unclassified-failure
|
|
215
|
+
// fallback used by the exec/validation paths below.
|
|
216
|
+
//
|
|
217
|
+
// We deliberately do NOT throw: a rejected upgrade promise routes through
|
|
218
|
+
// `dev.mts` to `process.exit(1)`, crash-looping the live-edit pod. The
|
|
219
|
+
// no-op dev-server restart that still follows (`cliUpdated` is set
|
|
220
|
+
// unconditionally at enqueue time in `checkVersionsAndWritePackageJson`),
|
|
221
|
+
// plus degrading gracefully (continue on the current version) and
|
|
222
|
+
// surfacing the failed upgrade to the user, are tracked by APPS-4457.
|
|
194
223
|
logger.error("Could not determine how to upgrade Superblocks packages.");
|
|
195
|
-
|
|
224
|
+
recordNpmInstall({
|
|
225
|
+
runner: "auto_upgrade",
|
|
226
|
+
outcome: "other",
|
|
227
|
+
configured: hasAnyRegistryConfigured ?? false,
|
|
228
|
+
registryHost: undefined,
|
|
229
|
+
durationMs: 0,
|
|
230
|
+
});
|
|
231
|
+
return false;
|
|
196
232
|
}
|
|
197
233
|
|
|
198
234
|
const { command, args } = installCommand;
|
|
199
235
|
logger.info(`Running ${command} ${args.join(" ")}...`);
|
|
236
|
+
// APPS-4544: emit `superblocks.npm.install.*` for the CLI auto-upgrade
|
|
237
|
+
// install. Until now this path's only failure signal was a structured log
|
|
238
|
+
// line + the rejected promise; metrics/dashboards distinguishing a
|
|
239
|
+
// perimeter-blocked upgrade from a working one had no `runner="auto_upgrade"`
|
|
240
|
+
// facet because no call site emitted it. `installStart` covers exec +
|
|
241
|
+
// post-install version validation so the histogram answers the operator's
|
|
242
|
+
// real question ("how long did the upgrade take end-to-end"), not just
|
|
243
|
+
// "how long did pnpm spend".
|
|
244
|
+
//
|
|
245
|
+
// `outcome` / `registryHost` are mutated from inside the exec callback
|
|
246
|
+
// because that's where the classifier runs; the `finally` then emits with
|
|
247
|
+
// whatever the callback (or a post-install validation throw below) left
|
|
248
|
+
// behind. `recordNpmInstall` itself coerces `outcome` through
|
|
249
|
+
// `normalizeInstallOutcome` and `registryHost` through `bucketNpmRegistryHost`,
|
|
250
|
+
// so raw `NpmInstallBlocked.reason` / hostnames can't leak past the emit
|
|
251
|
+
// boundary even if a future caller forgets to pre-sanitize.
|
|
252
|
+
const installStart = Date.now();
|
|
253
|
+
let outcome: string = "success";
|
|
254
|
+
let registryHost: string | undefined;
|
|
200
255
|
let resolver: (value: void | PromiseLike<void>) => void;
|
|
201
256
|
let rejecter: (reason?: any) => void;
|
|
202
257
|
const promise = new Promise<void>((resolve, reject) => {
|
|
@@ -233,6 +288,21 @@ async function upgradeCliWithPackageManager(
|
|
|
233
288
|
requestedPackages: [{ name: packageName }],
|
|
234
289
|
hasAnyRegistryConfigured,
|
|
235
290
|
});
|
|
291
|
+
// Surface the structured `NpmInstallBlocked.reason` (e.g.
|
|
292
|
+
// `registry_unreachable`, `registry_auth_failed`) and raw host to
|
|
293
|
+
// the install metric below; the emitter coerces both at the emit
|
|
294
|
+
// boundary (`normalizeInstallOutcome` / `bucketNpmRegistryHost`),
|
|
295
|
+
// so a reason that isn't in `NPM_INSTALL_OUTCOMES` folds to
|
|
296
|
+
// `other` and a private host buckets to `private`. When the
|
|
297
|
+
// classifier doesn't match (network glitch, lockfile conflict, …)
|
|
298
|
+
// we still need to flip outcome off `success`, since the install
|
|
299
|
+
// did fail; default to `other` so the metric reflects the failure.
|
|
300
|
+
outcome = blocked?.reason ?? "other";
|
|
301
|
+
// `blocked?.registryHost` is already `string | undefined` (optional
|
|
302
|
+
// chaining yields `undefined` when `blocked` is null, and
|
|
303
|
+
// `NpmInstallBlocked.registryHost` is `readonly registryHost?: string`),
|
|
304
|
+
// so no `?? undefined` fallback is needed.
|
|
305
|
+
registryHost = blocked?.registryHost;
|
|
236
306
|
if (blocked) {
|
|
237
307
|
// Structured fields are grep-able from the message body because
|
|
238
308
|
// the local logger contract limits the meta arg to
|
|
@@ -296,28 +366,61 @@ async function upgradeCliWithPackageManager(
|
|
|
296
366
|
logger.warn(data);
|
|
297
367
|
});
|
|
298
368
|
|
|
299
|
-
|
|
300
|
-
|
|
369
|
+
try {
|
|
370
|
+
// Wait for the install to complete, then validate the version
|
|
371
|
+
await promise;
|
|
301
372
|
|
|
302
|
-
|
|
303
|
-
|
|
373
|
+
// Clear cache to force actual version detection (not just returning cached value)
|
|
374
|
+
clearCliVersionCache();
|
|
304
375
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
376
|
+
const currentCliInfo = await getCurrentCliVersion();
|
|
377
|
+
if (!currentCliInfo) {
|
|
378
|
+
throw new Error("Could not get validate CLI version post-install");
|
|
379
|
+
}
|
|
309
380
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
381
|
+
if (currentCliInfo.version !== targetVersion) {
|
|
382
|
+
throw new Error(
|
|
383
|
+
`CLI version mismatch after upgrade: ${currentCliInfo.version} !== ${targetVersion}`,
|
|
384
|
+
);
|
|
385
|
+
}
|
|
315
386
|
|
|
316
|
-
|
|
317
|
-
|
|
387
|
+
// Now that we've validated, cache the correct version (avoids re-detection on next call)
|
|
388
|
+
clearCliVersionCache({ version: targetVersion, alias });
|
|
389
|
+
|
|
390
|
+
// This log line is used to end the upgrade spinner
|
|
391
|
+
logger.info(`Successfully upgraded packages to ${targetVersion}`);
|
|
392
|
+
} catch (err) {
|
|
393
|
+
// If outcome is still "success", the install itself succeeded and we
|
|
394
|
+
// failed during post-install validation (missing binary / version
|
|
395
|
+
// mismatch). The user-visible effect is the same — auto-upgrade
|
|
396
|
+
// failed — so the metric flips to `other`. Exec-time failures already
|
|
397
|
+
// set outcome from the classified `NpmInstallBlocked.reason` (or
|
|
398
|
+
// `other` for unclassified errors) in the callback above.
|
|
399
|
+
if (outcome === "success") {
|
|
400
|
+
outcome = "other";
|
|
401
|
+
}
|
|
402
|
+
throw err;
|
|
403
|
+
} finally {
|
|
404
|
+
// Both the happy path and every throw above (exec failure → callback
|
|
405
|
+
// sets outcome, post-install validation throw → catch sets outcome to
|
|
406
|
+
// `other`) flow through here. The earlier `resolveCommand` failure
|
|
407
|
+
// returns before `installStart`, so it never reaches this `finally`; it
|
|
408
|
+
// emits its own `outcome="other"` failure sample inline (with
|
|
409
|
+
// `durationMs: 0`, since no install ran) so that path isn't a metric gap
|
|
410
|
+
// either. No double-emit results because that early return skips this block.
|
|
411
|
+
recordNpmInstall({
|
|
412
|
+
runner: "auto_upgrade",
|
|
413
|
+
registryHost,
|
|
414
|
+
outcome,
|
|
415
|
+
configured: hasAnyRegistryConfigured ?? false,
|
|
416
|
+
durationMs: Date.now() - installStart,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
318
419
|
|
|
319
|
-
//
|
|
320
|
-
|
|
420
|
+
// Reached only when the install ran and post-install validation passed
|
|
421
|
+
// (every failure above throws). Signals the caller that a real upgrade
|
|
422
|
+
// happened so it leaves the `upgradeCli` span `outcome=success`.
|
|
423
|
+
return true;
|
|
321
424
|
}
|
|
322
425
|
|
|
323
426
|
export async function checkVersionsAndWritePackageJson(
|
|
@@ -437,49 +540,143 @@ export async function checkVersionsAndWritePackageJson(
|
|
|
437
540
|
const cliUpgradePromise = traceFunction({
|
|
438
541
|
name: "upgradeCli",
|
|
439
542
|
tracer,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
await traceFunction({
|
|
454
|
-
name: "
|
|
543
|
+
// APPS-4544: capture the span so the `outcome` attribute can be
|
|
544
|
+
// set in `finally` below. Adds a single trace-side fact so
|
|
545
|
+
// operators can correlate a failed upgrade span with the pod
|
|
546
|
+
// that crash-looped, complementing the
|
|
547
|
+
// `superblocks.npm.install.*` metric the package-manager and
|
|
548
|
+
// oclif paths emit below.
|
|
549
|
+
fn: async (span) => {
|
|
550
|
+
let outcome: "success" | "failure" = "success";
|
|
551
|
+
try {
|
|
552
|
+
logger.info(
|
|
553
|
+
`Beginning CLI upgrade from ${currentCliInfo.version} to ${targetVersions.cli}`,
|
|
554
|
+
);
|
|
555
|
+
const oclifStart = Date.now();
|
|
556
|
+
const oclifUpgradeSucceeded = await traceFunction({
|
|
557
|
+
name: "upgradeCliWithOclif",
|
|
455
558
|
tracer,
|
|
456
559
|
fn: async () => {
|
|
457
|
-
await
|
|
458
|
-
packageManager,
|
|
459
|
-
targetVersions.cli,
|
|
460
|
-
currentCliInfo.alias,
|
|
461
|
-
undefined,
|
|
462
|
-
superblocksLogsPath(appDir),
|
|
463
|
-
);
|
|
560
|
+
return await upgradeCliWithOclif(targetVersions.cli);
|
|
464
561
|
},
|
|
465
562
|
});
|
|
466
|
-
|
|
563
|
+
if (oclifUpgradeSucceeded) {
|
|
564
|
+
// APPS-4544: oclif succeeded outright, so the package
|
|
565
|
+
// manager fallback below is skipped — record the
|
|
566
|
+
// upgrade in `superblocks.npm.install.*` here so the
|
|
567
|
+
// dashboard's "auto-upgrade succeeded" count includes
|
|
568
|
+
// oclif-channel installs. Failures and "not updatable"
|
|
569
|
+
// both flow through the fallback, which already
|
|
570
|
+
// records the outcome with the correct attributes
|
|
571
|
+
// pulled from the classifier; double-emitting from
|
|
572
|
+
// both branches would double-count those failures.
|
|
573
|
+
recordNpmInstall({
|
|
574
|
+
runner: "auto_upgrade",
|
|
575
|
+
outcome: "success",
|
|
576
|
+
// oclif's update channel is the public superblocks
|
|
577
|
+
// tarball location (not a customer-configured npm
|
|
578
|
+
// registry), so `configured` is false here even
|
|
579
|
+
// when the app shell has a private registry — the
|
|
580
|
+
// CLI binary upgrade does not flow through that
|
|
581
|
+
// registry, only library/dependency installs do.
|
|
582
|
+
configured: false,
|
|
583
|
+
// oclif resolves its tarball via its update channel,
|
|
584
|
+
// not a configurable npm registry, so there's no
|
|
585
|
+
// single host to attribute the install to. Leave
|
|
586
|
+
// unset and let `bucketNpmRegistryHost` map it to
|
|
587
|
+
// `unknown` at the emit boundary.
|
|
588
|
+
registryHost: undefined,
|
|
589
|
+
durationMs: Date.now() - oclifStart,
|
|
590
|
+
});
|
|
591
|
+
// Operator log anchor symmetric with the package-manager
|
|
592
|
+
// path's "Successfully upgraded packages to ..." line: the
|
|
593
|
+
// oclif branch records a `runner=auto_upgrade, outcome=success`
|
|
594
|
+
// metric but, without this, left no log entry to correlate
|
|
595
|
+
// the increment against on the oclif channel.
|
|
596
|
+
logger.info(
|
|
597
|
+
`Successfully upgraded CLI to ${targetVersions.cli} via oclif`,
|
|
598
|
+
);
|
|
599
|
+
} else {
|
|
600
|
+
logger.info(
|
|
601
|
+
`Falling back to package manager upgrade for CLI`,
|
|
602
|
+
);
|
|
603
|
+
const cliUpgradeRan = await traceFunction({
|
|
604
|
+
name: "upgradePackageWithPackageManager - cli",
|
|
605
|
+
tracer,
|
|
606
|
+
fn: async () => {
|
|
607
|
+
return await upgradeCliWithPackageManager(
|
|
608
|
+
packageManager,
|
|
609
|
+
targetVersions.cli,
|
|
610
|
+
currentCliInfo.alias,
|
|
611
|
+
undefined,
|
|
612
|
+
superblocksLogsPath(appDir),
|
|
613
|
+
);
|
|
614
|
+
},
|
|
615
|
+
});
|
|
616
|
+
if (!cliUpgradeRan) {
|
|
617
|
+
// No global install command could be resolved → the CLI
|
|
618
|
+
// upgrade was a no-op (the failure metric was already
|
|
619
|
+
// emitted inline). Flip the span to `failure` so the trace
|
|
620
|
+
// is accurate, but do NOT throw: throwing routes through
|
|
621
|
+
// `dev.mts` to `process.exit(1)` and crash-loops the pod.
|
|
622
|
+
// The no-op dev-server restart that still follows
|
|
623
|
+
// (`cliUpdated = true` below), graceful degrade, and user
|
|
624
|
+
// surfacing of the failed upgrade are tracked by APPS-4457.
|
|
625
|
+
outcome = "failure";
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// The freshly fetched tarball may ship bundled lockfiles with
|
|
629
|
+
// stale public-host `resolved` URLs; strip them so the next
|
|
630
|
+
// install does not bypass the active registry. Non-fatal —
|
|
631
|
+
// a failed post-step must not crash the pod after a working
|
|
632
|
+
// upgrade, and (APPS-4544) must not flip the span outcome
|
|
633
|
+
// to `failure` either, since the actual upgrade succeeded.
|
|
634
|
+
// The inner traceFunction still records the strip-level
|
|
635
|
+
// exception on its own span, so we don't lose the signal.
|
|
636
|
+
try {
|
|
637
|
+
await traceFunction({
|
|
638
|
+
name: "stripUpgradedCliLockfiles",
|
|
639
|
+
tracer,
|
|
640
|
+
fn: async () => {
|
|
641
|
+
await stripUpgradedCliLockfiles(logger);
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
} catch (stripErr) {
|
|
645
|
+
logger.warn(
|
|
646
|
+
`stripUpgradedCliLockfiles failed (non-fatal): ${
|
|
647
|
+
isNativeError(stripErr) ? stripErr.message : stripErr
|
|
648
|
+
}`,
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
} catch (err) {
|
|
652
|
+
outcome = "failure";
|
|
653
|
+
throw err;
|
|
654
|
+
} finally {
|
|
655
|
+
// Closed enum string keeps the trace attribute
|
|
656
|
+
// queryable in TraceQL without cardinality concerns;
|
|
657
|
+
// `traceFunction` already records the exception itself
|
|
658
|
+
// via `recordException`, so this attribute is just a
|
|
659
|
+
// semantic label for grouping.
|
|
660
|
+
span.setAttribute("outcome", outcome);
|
|
467
661
|
}
|
|
468
|
-
// The freshly fetched tarball may ship bundled lockfiles with
|
|
469
|
-
// stale public-host `resolved` URLs; strip them so the next
|
|
470
|
-
// install does not bypass the active registry. Non-fatal — a
|
|
471
|
-
// failed post-step must not crash the pod after a working
|
|
472
|
-
// upgrade.
|
|
473
|
-
await traceFunction({
|
|
474
|
-
name: "stripUpgradedCliLockfiles",
|
|
475
|
-
tracer,
|
|
476
|
-
fn: async () => {
|
|
477
|
-
await stripUpgradedCliLockfiles(logger);
|
|
478
|
-
},
|
|
479
|
-
});
|
|
480
662
|
},
|
|
481
663
|
});
|
|
482
664
|
upgradePromises.push(cliUpgradePromise);
|
|
665
|
+
// APPS-4554: mark the CLI upgrade as pending SYNCHRONOUSLY here,
|
|
666
|
+
// not inside the async promise above. `dev.mts` reads
|
|
667
|
+
// `result.cliUpdated` as a value snapshot immediately after this
|
|
668
|
+
// function returns (to decide whether to restart the dev server),
|
|
669
|
+
// but `cliUpgradePromise` suspends at its first `await exec(...)`
|
|
670
|
+
// and resolves long after the return — so a `cliUpdated = true`
|
|
671
|
+
// set inside either the oclif or package-manager branch never
|
|
672
|
+
// reaches the returned snapshot. Setting it at schedule time means
|
|
673
|
+
// "a CLI upgrade was enqueued; restart once it succeeds." The
|
|
674
|
+
// success gate stays in `dev.mts`, which awaits this promise via
|
|
675
|
+
// `joinPackageUpgrade` before restarting and exits (no restart) if
|
|
676
|
+
// the upgrade rejects. This fixes both the oclif-success path
|
|
677
|
+
// (Bugbot: it set the flag in neither branch reliably) and the
|
|
678
|
+
// pre-existing package-manager race.
|
|
679
|
+
cliUpdated = true;
|
|
483
680
|
}
|
|
484
681
|
|
|
485
682
|
// Skip if we're in local development
|
|
@@ -1190,27 +1190,26 @@ export async function dev(options: {
|
|
|
1190
1190
|
// auto-upgrade fires. With the file in place, the auto-upgrade's
|
|
1191
1191
|
// `npm install -g @superblocksteam/cli@…` resolves through the
|
|
1192
1192
|
// customer's private registry instead of `registry.npmjs.org`.
|
|
1193
|
-
|
|
1193
|
+
// `syncHomeNpmrc` owns its own `npm_registry.sync_home_npmrc`
|
|
1194
|
+
// span (APPS-4378), so no outer span is created here.
|
|
1195
|
+
if (!aiService) {
|
|
1196
|
+
logger.info(
|
|
1197
|
+
"[home-npmrc] skipped: AiService unavailable at startup",
|
|
1198
|
+
);
|
|
1199
|
+
} else {
|
|
1194
1200
|
try {
|
|
1195
|
-
if (!aiService) {
|
|
1196
|
-
logger.info(
|
|
1197
|
-
"[home-npmrc] skipped: AiService unavailable at startup",
|
|
1198
|
-
);
|
|
1199
|
-
return;
|
|
1200
|
-
}
|
|
1201
1201
|
await syncHomeNpmrc({
|
|
1202
1202
|
npmRegistryClient: aiService.getNpmRegistryClient(),
|
|
1203
1203
|
logger,
|
|
1204
|
+
orgId: currentUser.user.currentOrganizationId,
|
|
1204
1205
|
});
|
|
1205
1206
|
} catch (error) {
|
|
1206
1207
|
logger.warn(
|
|
1207
1208
|
"[home-npmrc] sync step failed unexpectedly; ~/.superblocks/npmrc left untouched",
|
|
1208
1209
|
getErrorMeta(error),
|
|
1209
1210
|
);
|
|
1210
|
-
} finally {
|
|
1211
|
-
span.end();
|
|
1212
1211
|
}
|
|
1213
|
-
}
|
|
1212
|
+
}
|
|
1214
1213
|
|
|
1215
1214
|
let hasCliUpdated = false;
|
|
1216
1215
|
let upgradePromises: Promise<void>[] = [];
|
|
@@ -2,6 +2,14 @@ import { mkdir } from "node:fs/promises";
|
|
|
2
2
|
import * as os from "node:os";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
|
|
5
|
+
import { trace } from "@opentelemetry/api";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
bucketNpmRegistryHost,
|
|
9
|
+
NPM_REGISTRY_SPAN,
|
|
10
|
+
NPM_REGISTRY_SPAN_ATTR,
|
|
11
|
+
withNpmRegistrySpan,
|
|
12
|
+
} from "@superblocksteam/telemetry";
|
|
5
13
|
import {
|
|
6
14
|
type NpmRegistryClient,
|
|
7
15
|
type NpmRegistryFetchResult,
|
|
@@ -177,6 +185,12 @@ export interface SyncHomeNpmrcDeps {
|
|
|
177
185
|
* leave it unset.
|
|
178
186
|
*/
|
|
179
187
|
homeDir?: string;
|
|
188
|
+
/**
|
|
189
|
+
* Organization id, surfaced on the `npm_registry.sync_home_npmrc` span
|
|
190
|
+
* (per-trace context, never a metric label). Absent in tests / when no org
|
|
191
|
+
* context is available at the call site.
|
|
192
|
+
*/
|
|
193
|
+
orgId?: string;
|
|
180
194
|
}
|
|
181
195
|
|
|
182
196
|
/**
|
|
@@ -230,6 +244,28 @@ export interface SyncHomeNpmrcDeps {
|
|
|
230
244
|
*/
|
|
231
245
|
export async function syncHomeNpmrc(
|
|
232
246
|
deps: SyncHomeNpmrcDeps,
|
|
247
|
+
): Promise<SyncHomeNpmrcResult> {
|
|
248
|
+
// `npm_registry.sync_home_npmrc` span (APPS-4378). org_id rides on the span;
|
|
249
|
+
// the precise 5-value outcome and resolved `source` are attached once the
|
|
250
|
+
// sync resolves (registry_host is set inside the impl, after the fetch).
|
|
251
|
+
return withNpmRegistrySpan(
|
|
252
|
+
NPM_REGISTRY_SPAN.SYNC_HOME_NPMRC,
|
|
253
|
+
deps.orgId ? { [NPM_REGISTRY_SPAN_ATTR.ORG_ID]: deps.orgId } : {},
|
|
254
|
+
async (span) => {
|
|
255
|
+
const result = await syncHomeNpmrcImpl(deps);
|
|
256
|
+
span.setAttributes({
|
|
257
|
+
[NPM_REGISTRY_SPAN_ATTR.OUTCOME]: result.outcome,
|
|
258
|
+
...(result.source
|
|
259
|
+
? { [NPM_REGISTRY_SPAN_ATTR.SOURCE]: result.source }
|
|
260
|
+
: {}),
|
|
261
|
+
});
|
|
262
|
+
return result;
|
|
263
|
+
},
|
|
264
|
+
);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
async function syncHomeNpmrcImpl(
|
|
268
|
+
deps: SyncHomeNpmrcDeps,
|
|
233
269
|
): Promise<SyncHomeNpmrcResult> {
|
|
234
270
|
const targetPath = superblocksNpmrcPath(deps.homeDir);
|
|
235
271
|
const backupPath = superblocksNpmrcBackupPath(deps.homeDir);
|
|
@@ -250,6 +286,15 @@ export async function syncHomeNpmrc(
|
|
|
250
286
|
return { outcome: "error", path: targetPath };
|
|
251
287
|
}
|
|
252
288
|
|
|
289
|
+
// Attach the bucketed registry_host to the active sync span now that the
|
|
290
|
+
// config has resolved (the default registry URL is only known here).
|
|
291
|
+
trace
|
|
292
|
+
.getActiveSpan()
|
|
293
|
+
?.setAttribute(
|
|
294
|
+
NPM_REGISTRY_SPAN_ATTR.REGISTRY_HOST,
|
|
295
|
+
bucketNpmRegistryHost(result.config.default?.url),
|
|
296
|
+
);
|
|
297
|
+
|
|
253
298
|
if (result.source === "unreachable") {
|
|
254
299
|
deps.logger.warn(
|
|
255
300
|
`${LOG_PREFIX} registry server unreachable with no cached config; ~/.superblocks/npmrc left unchanged`,
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { fetchBillingUsageRecordsResponse } from "./client.js";
|
|
4
|
+
|
|
5
|
+
const { mockAxios } = vi.hoisted(() => ({
|
|
6
|
+
mockAxios: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
|
|
9
|
+
vi.mock("axios", async () => {
|
|
10
|
+
const actual = await vi.importActual("axios");
|
|
11
|
+
return {
|
|
12
|
+
...actual,
|
|
13
|
+
default: mockAxios,
|
|
14
|
+
};
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("fetchBillingUsageRecordsResponse", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
mockAxios.mockReset();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("returns the full billing usage records payload", async () => {
|
|
23
|
+
const payload = {
|
|
24
|
+
agentUsage: [
|
|
25
|
+
{ agentId: "agent-1", name: "Support agent", creditsUsed: 50 },
|
|
26
|
+
],
|
|
27
|
+
applicationUsageRows: [
|
|
28
|
+
{
|
|
29
|
+
applicationId: "00000000-0000-4000-8000-000000000001",
|
|
30
|
+
applicationName: "Operations",
|
|
31
|
+
creditsUsed: 75,
|
|
32
|
+
usageType: "ai_credits",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
dollarLedgerRows: [
|
|
36
|
+
{
|
|
37
|
+
amountCents: 1234,
|
|
38
|
+
applicationId: "app-1",
|
|
39
|
+
applicationName: "Operations",
|
|
40
|
+
chargeKind: "committed",
|
|
41
|
+
creditsUsed: 62,
|
|
42
|
+
date: "2026-03-24",
|
|
43
|
+
email: "alice@example.com",
|
|
44
|
+
entryType: "ai_credit_debit",
|
|
45
|
+
name: "Alice",
|
|
46
|
+
usageType: "ai_credits",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
rows: [
|
|
50
|
+
{
|
|
51
|
+
actorId: "user-1",
|
|
52
|
+
actorType: "user",
|
|
53
|
+
creditsUsed: 100,
|
|
54
|
+
date: "2026-03-24",
|
|
55
|
+
email: "alice@example.com",
|
|
56
|
+
name: "Alice",
|
|
57
|
+
source: "seat",
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
mockAxios.mockResolvedValue({ data: { data: payload } });
|
|
62
|
+
|
|
63
|
+
await expect(
|
|
64
|
+
fetchBillingUsageRecordsResponse({
|
|
65
|
+
cliVersion: "1.2.3",
|
|
66
|
+
endDate: "2026-03-25",
|
|
67
|
+
startDate: "2026-03-24",
|
|
68
|
+
superblocksBaseUrl: "https://staging.superblocks.com",
|
|
69
|
+
token: "test-token",
|
|
70
|
+
}),
|
|
71
|
+
).resolves.toEqual(payload);
|
|
72
|
+
});
|
|
73
|
+
});
|
package/src/client.ts
CHANGED
|
@@ -2789,6 +2789,8 @@ export interface DailyUsageRow {
|
|
|
2789
2789
|
}
|
|
2790
2790
|
|
|
2791
2791
|
export interface UsageRecordRow {
|
|
2792
|
+
actorId?: string;
|
|
2793
|
+
actorType?: "ai_agent" | "user";
|
|
2792
2794
|
date: string;
|
|
2793
2795
|
email: string;
|
|
2794
2796
|
name: string;
|
|
@@ -2796,6 +2798,42 @@ export interface UsageRecordRow {
|
|
|
2796
2798
|
source: string;
|
|
2797
2799
|
}
|
|
2798
2800
|
|
|
2801
|
+
export interface AgentUsageRow {
|
|
2802
|
+
agentId: string;
|
|
2803
|
+
name: string;
|
|
2804
|
+
creditsUsed: number;
|
|
2805
|
+
}
|
|
2806
|
+
|
|
2807
|
+
export interface ApplicationUsageRow {
|
|
2808
|
+
applicationId: string;
|
|
2809
|
+
applicationName: string;
|
|
2810
|
+
creditsUsed: number;
|
|
2811
|
+
usageType: "ai_credits";
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
export interface DollarLedgerRow {
|
|
2815
|
+
amountCents: number;
|
|
2816
|
+
applicationId: string | null;
|
|
2817
|
+
applicationName: string | null;
|
|
2818
|
+
chargeKind: "committed" | "overage";
|
|
2819
|
+
creditsUsed: number | null;
|
|
2820
|
+
date: string;
|
|
2821
|
+
email: string | null;
|
|
2822
|
+
entryType: string;
|
|
2823
|
+
name: string | null;
|
|
2824
|
+
usageType: "ai_credits" | "deployed_app";
|
|
2825
|
+
}
|
|
2826
|
+
|
|
2827
|
+
export interface BillingUsageRecordsResponse {
|
|
2828
|
+
agentUsage?: AgentUsageRow[];
|
|
2829
|
+
applicationUsageRows?: ApplicationUsageRow[];
|
|
2830
|
+
dollarCommitUsedCents?: number;
|
|
2831
|
+
dollarLedgerRows?: DollarLedgerRow[];
|
|
2832
|
+
enterpriseBillingWindows?: unknown[];
|
|
2833
|
+
rows: UsageRecordRow[];
|
|
2834
|
+
seatAssignments?: unknown[];
|
|
2835
|
+
}
|
|
2836
|
+
|
|
2799
2837
|
export interface UserCreditLimit {
|
|
2800
2838
|
amountLimitCents: number | null;
|
|
2801
2839
|
creditLimit: number | null;
|
|
@@ -2922,6 +2960,29 @@ export async function fetchBillingUsageRecords({
|
|
|
2922
2960
|
startDate: string;
|
|
2923
2961
|
endDate: string;
|
|
2924
2962
|
}): Promise<UsageRecordRow[]> {
|
|
2963
|
+
const response = await fetchBillingUsageRecordsResponse({
|
|
2964
|
+
cliVersion,
|
|
2965
|
+
token,
|
|
2966
|
+
superblocksBaseUrl,
|
|
2967
|
+
startDate,
|
|
2968
|
+
endDate,
|
|
2969
|
+
});
|
|
2970
|
+
return response.rows;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
export async function fetchBillingUsageRecordsResponse({
|
|
2974
|
+
cliVersion,
|
|
2975
|
+
token,
|
|
2976
|
+
superblocksBaseUrl,
|
|
2977
|
+
startDate,
|
|
2978
|
+
endDate,
|
|
2979
|
+
}: {
|
|
2980
|
+
cliVersion: string;
|
|
2981
|
+
token: string;
|
|
2982
|
+
superblocksBaseUrl: string;
|
|
2983
|
+
startDate: string;
|
|
2984
|
+
endDate: string;
|
|
2985
|
+
}): Promise<BillingUsageRecordsResponse> {
|
|
2925
2986
|
try {
|
|
2926
2987
|
const url = new URL(
|
|
2927
2988
|
`${BASE_SERVER_API_URL_V1}/billing/usage-records`,
|
|
@@ -2939,7 +3000,11 @@ export async function fetchBillingUsageRecords({
|
|
|
2939
3000
|
},
|
|
2940
3001
|
};
|
|
2941
3002
|
const response = await axios(config);
|
|
2942
|
-
|
|
3003
|
+
const data = response.data.data as Partial<BillingUsageRecordsResponse>;
|
|
3004
|
+
if (!Array.isArray(data.rows)) {
|
|
3005
|
+
throw new Error("Billing usage records response did not include rows");
|
|
3006
|
+
}
|
|
3007
|
+
return data as BillingUsageRecordsResponse;
|
|
2943
3008
|
} catch (e: any) {
|
|
2944
3009
|
let message: string;
|
|
2945
3010
|
if (e instanceof AxiosError) {
|