@drip-apex/apex-cli 0.11.0 → 0.12.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.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/dist/cli.js +108 -4
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -342,3 +342,39 @@ optimistic concurrency token.
342
342
  QA status creates a run only when none exists. Reading an existing run never writes
343
343
  to it: `updatedAt` is its last stored mutation time, while `computedAt` is the ISO
344
344
  timestamp when the current checklist and summary were computed.
345
+
346
+ ### Decision latency
347
+
348
+ `apex tracking latency <experiment-id-or-key> [--days <1..30>] [--qa-session <id>] [--json]`
349
+ compares forced-preview and natural decision latency before launch. The default
350
+ window is seven days. The table shows sample counts, decision p50/p95/p99,
351
+ config-fetch p50/p95 (milliseconds), and inline/cache/network/none counts.
352
+ JSON output includes the API body in the standard envelope and assignment-ready
353
+ percentiles. Fewer than five measured samples have null percentiles. Natural
354
+ loads are shop-wide non-QA loads without forced IDs; a QA-session bucket can
355
+ overlap forced loads. Analytics failures return errors. The API position is
356
+ `runtime_inline_no_change`.
357
+
358
+ The runtime remains unchanged. Natural runtime decides within tens of milliseconds
359
+ of SDK start when config ships inline with the snippet (measured p50 12 ms /
360
+ p95 54 ms on a large storefront). QA previews, screenshot-mode previews and
361
+ cookie-forced sessions take the QA config lane and fetch fresh config over the
362
+ network (measured p50 1.3 s / p95 7 s); they are not representative of launch
363
+ behaviour. URL-forced sessions without a `qa` parameter keep the inline or cached
364
+ config, so these measurements do not describe them. No config preload or
365
+ anti-flicker hold is planned.
366
+
367
+ ### Connection troubleshooting
368
+
369
+ Connection errors retain the underlying failure. JSON output exposes
370
+ `error.details.reason` and, when available, `error.details.code`; the numeric
371
+ `error.code` remains the CLI exit code.
372
+
373
+ | Reason | Meaning and usual fix |
374
+ | --- | --- |
375
+ | `dns` | The host could not be resolved. Check `APEX_BASE_URL` (or `DRIP_BASE_URL`), DNS, and whether your sandbox permits network access. |
376
+ | `refused` | The host rejected the connection. Check the base URL, port, and whether the server is running. |
377
+ | `reset` | The connection was interrupted. Check network stability and proxy settings before retrying. |
378
+ | `tls` | Certificate or TLS validation failed. Check expired certificates and your corporate TLS proxy's trusted CA configuration. |
379
+ | `timeout` | The request was aborted or exceeded its deadline. Check server availability and network access. |
380
+ | `unknown` | Another network failure occurred. Use the included cause message to diagnose network or sandbox restrictions. |
package/dist/cli.js CHANGED
@@ -7112,7 +7112,7 @@ var import_node_os3 = __toESM(require("node:os"));
7112
7112
  // package.json
7113
7113
  var package_default = {
7114
7114
  name: "@drip-apex/apex-cli",
7115
- version: "0.11.0",
7115
+ version: "0.12.1",
7116
7116
  license: "MIT",
7117
7117
  description: "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
7118
7118
  private: false,
@@ -7666,7 +7666,16 @@ function createHttpClient(creds, options) {
7666
7666
  try {
7667
7667
  response = await fetch(`${creds.baseUrl}${path24}`, { method, headers, body: form ? body5 : body5 ? JSON.stringify(body5) : void 0, signal: AbortSignal.timeout(timeoutMs) });
7668
7668
  } catch (error) {
7669
- return options.onNetworkError({ baseUrl: creds.baseUrl, timeoutMs, cause: error instanceof Error ? error.message : String(error) });
7669
+ const outer = error && typeof error === "object" ? error : void 0;
7670
+ const nested = outer?.cause && typeof outer.cause === "object" ? outer.cause : void 0;
7671
+ return options.onNetworkError({
7672
+ baseUrl: creds.baseUrl,
7673
+ timeoutMs,
7674
+ cause: error instanceof Error ? error.message : String(error),
7675
+ code: typeof nested?.code === "string" ? nested.code : typeof outer?.code === "string" ? outer.code : void 0,
7676
+ name: typeof outer?.name === "string" ? outer.name : void 0,
7677
+ causeMessage: typeof nested?.message === "string" ? nested.message : void 0
7678
+ });
7670
7679
  }
7671
7680
  inspectVersion(response.headers);
7672
7681
  const json = await response.json().catch(() => null);
@@ -7739,10 +7748,42 @@ function sharedClient(creds) {
7739
7748
  transport: testTransport,
7740
7749
  testTransportVersion: () => testTransport ? process.env[["APEX", "CLI", "MOCK", "API", "VERSION"].join("_")] : void 0,
7741
7750
  onNetworkError: (info) => {
7751
+ let url;
7752
+ try {
7753
+ url = new URL(info.baseUrl);
7754
+ } catch {
7755
+ }
7756
+ const host = url?.hostname ?? info.baseUrl;
7757
+ const code = info.code;
7758
+ let reason = "unknown";
7759
+ let message = info.causeMessage ?? info.cause;
7760
+ if (info.name === "TimeoutError" || info.name === "AbortError" || code === "UND_ERR_CONNECT_TIMEOUT") {
7761
+ reason = "timeout";
7762
+ message = `server did not respond within ${info.timeoutMs / 1e3}s`;
7763
+ } else if (code === "ENOTFOUND" || code === "EAI_AGAIN") {
7764
+ reason = "dns";
7765
+ message = `could not resolve host ${host}`;
7766
+ } else if (code === "ECONNREFUSED") {
7767
+ reason = "refused";
7768
+ message = `connection refused by ${host}:${url?.port || (url?.protocol === "https:" ? "443" : "80")}`;
7769
+ } else if (code === "ECONNRESET" || code === "EPIPE") {
7770
+ reason = "reset";
7771
+ message = `connection reset by ${host}`;
7772
+ } else if (code?.startsWith("CERT_") || code?.startsWith("ERR_TLS_") || [
7773
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
7774
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
7775
+ "SELF_SIGNED_CERT_IN_CHAIN",
7776
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
7777
+ "UNABLE_TO_GET_ISSUER_CERT",
7778
+ "HOSTNAME_MISMATCH"
7779
+ ].includes(code ?? "")) {
7780
+ reason = "tls";
7781
+ message = `TLS error (${code})`;
7782
+ }
7742
7783
  throw new CliError(
7743
- `Could not reach ${info.baseUrl}: check APEX_BASE_URL (or DRIP_BASE_URL) / network; server did not respond within ${info.timeoutMs / 1e3}s.`,
7784
+ `Could not reach ${info.baseUrl}: check APEX_BASE_URL (or DRIP_BASE_URL) / network; ${message}.`,
7744
7785
  EXIT_CODES.general,
7745
- { ...info }
7786
+ { reason, code, name: info.name, baseUrl: info.baseUrl, timeoutMs: info.timeoutMs }
7746
7787
  );
7747
7788
  }
7748
7789
  });
@@ -7985,6 +8026,11 @@ function trackingEventProperties(event) {
7985
8026
  if (event.goal_id != null) properties.goalId = event.goal_id;
7986
8027
  return properties;
7987
8028
  }
8029
+ function getDecisionLatency(creds, experimentId, opts) {
8030
+ const params = new URLSearchParams({ experiment_id: experimentId, days: String(opts.days) });
8031
+ if (opts.qaSessionId) params.set("qa_session_id", opts.qaSessionId);
8032
+ return request(creds, "GET", `/api/v1/tracking/latency?${params}`);
8033
+ }
7988
8034
  function verifyTracking(creds, opts) {
7989
8035
  const params = new URLSearchParams();
7990
8036
  if (opts?.minutes) params.set("minutes", String(opts.minutes));
@@ -13096,6 +13142,64 @@ trackingCommand.command("verify").description("Verify live tracking events for t
13096
13142
  }
13097
13143
  });
13098
13144
  });
13145
+ trackingCommand.command("latency").description("Measure forced-preview versus natural decision latency before launch").argument("<experiment-id-or-key>", "Experiment ID or test key").option("--days <n>", "Lookback window in days (1\u201330)", "7").option("--qa-session <id>", "Include a QA session bucket").action(async function(experimentId, opts) {
13146
+ const days = Number(opts.days);
13147
+ if (!Number.isInteger(days) || days < 1 || days > 30) {
13148
+ fail(this, "days must be an integer from 1 to 30", classifyStatus(400));
13149
+ }
13150
+ const result = await getDecisionLatency(requireCredentials(), experimentId, {
13151
+ days,
13152
+ qaSessionId: opts.qaSession
13153
+ });
13154
+ if (!result.ok || !result.data) {
13155
+ fail(
13156
+ this,
13157
+ result.error ?? "Failed to read decision latency",
13158
+ classifyStatus(result.status),
13159
+ result.errorDetails
13160
+ );
13161
+ }
13162
+ const summary = result.data;
13163
+ output(this, summary, () => {
13164
+ const buckets = [
13165
+ ["forced", summary.forced],
13166
+ ["natural", summary.natural],
13167
+ ...summary.qa_session ? [["qa_session", summary.qa_session]] : []
13168
+ ];
13169
+ const ms = (value) => value === null ? "-" : String(Math.round(value));
13170
+ printTable(
13171
+ [
13172
+ "Bucket",
13173
+ "Samples",
13174
+ "Decision p50",
13175
+ "p95",
13176
+ "p99",
13177
+ "Config p50",
13178
+ "p95",
13179
+ "inline/cache/network/none"
13180
+ ],
13181
+ buckets.map(([name, bucket]) => [
13182
+ name,
13183
+ String(bucket.samples),
13184
+ ms(bucket.decision_latency_ms.p50),
13185
+ ms(bucket.decision_latency_ms.p95),
13186
+ ms(bucket.decision_latency_ms.p99),
13187
+ ms(bucket.config_fetch_ms.p50),
13188
+ ms(bucket.config_fetch_ms.p95),
13189
+ [
13190
+ bucket.config_sources.inline,
13191
+ bucket.config_sources.cache,
13192
+ bucket.config_sources.network,
13193
+ bucket.config_sources.none
13194
+ ].join("/")
13195
+ ])
13196
+ );
13197
+ console.log("Times in ms. Fewer than 5 measured samples: -.");
13198
+ console.log(
13199
+ "Position: runtime unchanged (inline config decides within tens of ms; forced previews fetch config over the network and are not representative)"
13200
+ );
13201
+ });
13202
+ });
13099
13203
 
13100
13204
  // src/commands/goals.ts
13101
13205
  init_source();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@drip-apex/apex-cli",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "license": "MIT",
5
5
  "description": "Apex CLI for managing experiments, approved roadmap ideas, and draft workspaces.",
6
6
  "private": false,