@getexception/cli 0.1.12 → 0.1.14

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.
Files changed (3) hide show
  1. package/README.md +3 -3
  2. package/dist/index.js +51 -12
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -26,9 +26,9 @@ Permanent upload tokens cannot be created or used for projects with GitLab authe
26
26
 
27
27
  Deploy the resulting `dist` unchanged. Upload failure does not undo preparation: retry within the same job using the same prepared directory. **Do not save source maps as GitLab artifacts or cache when access has not been verified.** The GitLab identity flow keeps them only in the isolated job workspace and uploads directly to GetException. A retry after that job ends requires a new build with a new job prefix; it cannot replace maps for the old build. Do not run `prepare` twice or rebuild only the maps. Never publish the private artifact directory alongside the application. Other map formats (for example CSS maps) should also be excluded from the public deploy artifact by your build configuration.
28
28
 
29
- `upload` sends an authenticated manifest, uploads files with checksums, then waits for background validation. It supports retries and resuming the same manifest, has a two-minute deadline, and exits nonzero on failure. Neither production nor preview deployment depends on GetException availability. The CI wrapper catches monitoring failures, emits a prominent production warning or a single neutral preview log line, and continues deployment after the mandatory public-output safety check. Only report successful map delivery when all batches are ready. A failed local prepare requires one clean rebuild without maps; an upload failure may deploy already prepared, verified JavaScript. Do not retain maps in GitLab artifacts as a fallback. Raw maps have no download endpoint.
29
+ `upload` sends an authenticated manifest, gzip-compresses each map in memory, uploads up to eight files concurrently with checksums, then waits for background validation. It retries bounded transient network/server failures, resumes the same manifest from its missing files, has a twenty-minute deadline per batch, and exits nonzero on failure. Neither production nor preview deployment depends on GetException availability. Deploy the safe prepared JavaScript first, then attempt map delivery from the same isolated job workspace. The CI wrapper catches monitoring failures and emits a prominent production warning or a single neutral preview log line. Only report successful map delivery when all batches are ready. A failed local prepare requires one clean rebuild without maps; a late successful upload automatically reprocesses existing errors. Do not retain maps in GitLab artifacts as a fallback. Raw maps have no download endpoint.
30
30
 
31
- Limits: 128 JS files and 128 MiB per upload, 16 MiB per map, 1 GiB per project, 10 GiB per installation. Non-indexed Source Map v3 JSON only; no archives, compression or remote source downloads. Existing events are processed after a late upload. Errors thrown in the browser console have no source file and cannot gain a source snippet from a map.
31
+ Limits: 128 JS files and 128 MiB per upload, 16 MiB per map, 1 GiB per project, 10 GiB per installation. Non-indexed Source Map v3 JSON only; no archives or remote source downloads. Gzip is used only as bounded HTTPS transfer encoding and does not change the stored map format. Existing events are processed after a late upload. Errors thrown in the browser console have no source file and cannot gain a source snippet from a map.
32
32
 
33
33
  MIT. See `THIRD-PARTY-NOTICES.md`.
34
34
 
@@ -46,6 +46,6 @@ With GitLab build authentication, use the exact `release` and `deployment` retur
46
46
 
47
47
  Debug IDs are deterministic for identical JS, maps and paths, regardless of the release SHA. The server reuses private bytes within the project and `upload` skips files it already has. Every release still sends its own manifest. Changed JavaScript requires its matching map: there is no fallback to the latest production map. Ready maps older than 30 days are eligible for cleanup only when their release has no retained events or pending inbox events. Shared files survive until all references expire.
48
48
 
49
- CI wrappers must bound subprocess duration and share a network budget across context, upload batches and registration; see the [failure handling contract](../../docs/gitlab-ci.md#обработка-сбоев-в-account). Do not mark the whole build job as allowed to fail. Compiler, deployment and public-output safety failures remain real job failures.
49
+ CI wrappers must keep `ci context` and release registration on short timeouts. Run map delivery only after the application is deployed and give each upload subprocess a hard timeout slightly longer than the CLI's twenty-minute deadline. Do not impose one small shared timeout on all batches: that makes valid larger builds fail based on transfer time. See the [failure handling contract](../../docs/gitlab-ci.md#обработка-сбоев-в-account). Do not mark the whole build job as allowed to fail. Compiler, deployment and public-output safety failures remain real job failures.
50
50
 
51
51
  On failure the CLI exits 1 and prints one `GETEXCEPTION_DIAGNOSTIC` JSON line to stderr with `version:1`, an allowlisted `code`, optional `httpStatus` and optional server UUID `requestId`. Network, response-contract and CI identity failures have separate codes. It never prints raw errors, tokens, headers or response bodies. Callers must parse and validate this diagnostic, not forward arbitrary stderr. Successful `ci context` stdout remains JSON only. Older CLI versions without this format can be handled with a generic safe failure code without blocking application deployment.
package/dist/index.js CHANGED
@@ -19217,7 +19217,9 @@ async function readPreparedMap(directory, entry) {
19217
19217
  }
19218
19218
 
19219
19219
  // packages/cli/src/upload.ts
19220
+ import { promisify } from "util";
19220
19221
  import { setTimeout as delay2 } from "timers/promises";
19222
+ import { constants, gzip } from "zlib";
19221
19223
 
19222
19224
  // packages/cli/src/api.ts
19223
19225
  import { setTimeout as delay } from "timers/promises";
@@ -19378,7 +19380,15 @@ async function httpError(response) {
19378
19380
  }
19379
19381
 
19380
19382
  // packages/cli/src/api.ts
19381
- function projectApi(address, project, token, transport = fetch) {
19383
+ function retryableNetworkError(error61) {
19384
+ return [
19385
+ "NETWORK_DNS",
19386
+ "NETWORK_CONNECTION",
19387
+ "NETWORK_TIMEOUT",
19388
+ "NETWORK_ERROR"
19389
+ ].includes(error61.code);
19390
+ }
19391
+ function projectApi(address, project, token, transport = fetch, options = {}) {
19382
19392
  let url2;
19383
19393
  try {
19384
19394
  url2 = new URL(address);
@@ -19393,8 +19403,9 @@ function projectApi(address, project, token, transport = fetch) {
19393
19403
  }
19394
19404
  const authorization = authorizationHeader(token);
19395
19405
  const base = `${url2.origin}/api/v1/projects/${project}`;
19396
- const deadline = Date.now() + 12e4;
19397
- return async function request(path, method = "GET", body) {
19406
+ const deadline = Date.now() + (options.deadlineMs ?? 12e4);
19407
+ const requestTimeout = options.requestTimeoutMs ?? 3e4;
19408
+ return async function request(path, method = "GET", body, contentEncoding) {
19398
19409
  for (let attempt = 0; attempt < 3; attempt++) {
19399
19410
  if (Date.now() >= deadline) {
19400
19411
  throw new CliError("NETWORK_TIMEOUT");
@@ -19405,15 +19416,23 @@ function projectApi(address, project, token, transport = fetch) {
19405
19416
  method,
19406
19417
  headers: {
19407
19418
  Authorization: authorization,
19408
- "Content-Type": "application/json"
19419
+ "Content-Type": "application/json",
19420
+ ...contentEncoding ? { "Content-Encoding": contentEncoding } : {}
19409
19421
  },
19410
19422
  body,
19411
19423
  redirect: "error",
19412
19424
  credentials: "omit",
19413
- signal: AbortSignal.timeout(Math.min(3e4, deadline - Date.now()))
19425
+ signal: AbortSignal.timeout(
19426
+ Math.min(requestTimeout, deadline - Date.now())
19427
+ )
19414
19428
  });
19415
19429
  } catch (error61) {
19416
- throw networkError(error61);
19430
+ const failure = networkError(error61);
19431
+ if (retryableNetworkError(failure) && attempt < 2) {
19432
+ await delay(1e3 * 2 ** attempt);
19433
+ continue;
19434
+ }
19435
+ throw failure;
19417
19436
  }
19418
19437
  if (response.ok) {
19419
19438
  return responseJson(response);
@@ -19441,10 +19460,16 @@ var receiptSchema = external_exports.object({
19441
19460
  })
19442
19461
  )
19443
19462
  });
19463
+ var uploadConcurrency = 8;
19464
+ var uploadDeadlineMs = 20 * 6e4;
19465
+ var gzipAsync = promisify(gzip);
19444
19466
  async function uploadMaps(directory, address, project, token, transport = fetch) {
19445
19467
  const manifest = await readPrepared(directory);
19446
- const api = projectApi(address, project, token, transport);
19447
- const request = (path, method = "GET", body) => api("/source-maps" + path, method, body);
19468
+ const api = projectApi(address, project, token, transport, {
19469
+ deadlineMs: uploadDeadlineMs,
19470
+ requestTimeoutMs: 6e4
19471
+ });
19472
+ const request = (path, method = "GET", body, contentEncoding) => api("/source-maps" + path, method, body, contentEncoding);
19448
19473
  const receipt = receiptSchema.parse(
19449
19474
  await request("", "POST", JSON.stringify(manifest))
19450
19475
  );
@@ -19452,6 +19477,7 @@ async function uploadMaps(directory, address, project, token, transport = fetch)
19452
19477
  return;
19453
19478
  }
19454
19479
  if (receipt.status === "receiving") {
19480
+ const pending = [];
19455
19481
  for (const entry of manifest.artifacts) {
19456
19482
  const remote = receipt.artifacts.find((item) => item.path === entry.path);
19457
19483
  if (!remote) {
@@ -19460,10 +19486,23 @@ async function uploadMaps(directory, address, project, token, transport = fetch)
19460
19486
  if (remote.uploaded) {
19461
19487
  continue;
19462
19488
  }
19463
- await request(
19464
- `/${receipt.uploadId}/${remote.id}`,
19465
- "PUT",
19466
- await readPreparedMap(directory, entry)
19489
+ pending.push({ entry, id: remote.id });
19490
+ }
19491
+ for (let start = 0; start < pending.length; start += uploadConcurrency) {
19492
+ const group = pending.slice(start, start + uploadConcurrency);
19493
+ await Promise.all(
19494
+ group.map(async ({ entry, id }) => {
19495
+ const bytes = await readPreparedMap(directory, entry);
19496
+ const compressed = await gzipAsync(bytes, {
19497
+ level: constants.Z_BEST_SPEED
19498
+ });
19499
+ return request(
19500
+ `/${receipt.uploadId}/${id}`,
19501
+ "PUT",
19502
+ compressed,
19503
+ "gzip"
19504
+ );
19505
+ })
19467
19506
  );
19468
19507
  }
19469
19508
  await request(`/${receipt.uploadId}`, "POST");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getexception/cli",
3
- "version": "0.1.12",
3
+ "version": "0.1.14",
4
4
  "description": "Private source map uploads for GetException",
5
5
  "type": "module",
6
6
  "license": "MIT",