@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.
- package/README.md +36 -0
- package/dist/cli.js +108 -4
- 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.
|
|
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
|
-
|
|
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;
|
|
7784
|
+
`Could not reach ${info.baseUrl}: check APEX_BASE_URL (or DRIP_BASE_URL) / network; ${message}.`,
|
|
7744
7785
|
EXIT_CODES.general,
|
|
7745
|
-
{
|
|
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();
|