@eventra_dev/eventra-cli 0.3.12 → 0.4.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.
Files changed (3) hide show
  1. package/README.md +65 -4
  2. package/dist/cli/send.js +110 -50
  3. package/package.json +28 -6
package/README.md CHANGED
@@ -4,10 +4,16 @@
4
4
 
5
5
  # Eventra CLI
6
6
 
7
- [![npm version](https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
8
- [![npm downloads](https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
9
- [![TypeScript](https://img.shields.io/badge/typescript-ready-blue.svg)](https://www.typescriptlang.org/)
10
- [![License](https://img.shields.io/npm/l/@eventra_dev/eventra-cli)]()
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm version" src="https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
9
+ <a href="https://www.npmjs.com/package/@eventra_dev/eventra-cli"><img alt="npm downloads" src="https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg?style=flat-square&color=blue"></a>
10
+ <a href="https://github.com/and-1991/eventra-cli/actions/workflows/test.yml"><img alt="tests" src="https://img.shields.io/github/actions/workflow/status/and-1991/eventra-cli/test.yml?branch=main&label=tests&style=flat-square&logo=vitest&logoColor=white"></a>
11
+ <img alt="unit tests" src="https://img.shields.io/badge/unit-52%20passing-brightgreen?style=flat-square&logo=vitest&logoColor=white">
12
+ <img alt="e2e fixtures" src="https://img.shields.io/badge/e2e-12%20fixtures-brightgreen?style=flat-square">
13
+ <img alt="node" src="https://img.shields.io/node/v/@eventra_dev/eventra-cli?style=flat-square&color=darkgreen&logo=node.js&logoColor=white">
14
+ <a href="https://www.typescriptlang.org/"><img alt="TypeScript" src="https://img.shields.io/badge/TypeScript-ready-blue?style=flat-square&logo=typescript&logoColor=white"></a>
15
+ <img alt="license" src="https://img.shields.io/npm/l/@eventra_dev/eventra-cli?style=flat-square&color=lightgrey">
16
+ </p>
11
17
 
12
18
  Eventra CLI statically discovers analytics events from [**@eventra_dev/eventra-sdk**](https://www.npmjs.com/package/@eventra_dev/eventra-sdk) usage in your codebase — including function wrappers and cross-file propagation chains.
13
19
 
@@ -151,6 +157,13 @@ Incremental scan with the same rules as `sync`.
151
157
 
152
158
  Uploads `events` from config to the Eventra API. Requires `apiKey` (and optionally `endpoint`) in `eventra.json`. Events are POSTed to `POST /api/v1/cli/events` and marked as non-billable on the backend.
153
159
 
160
+ Network resilience:
161
+
162
+ - Up to 4 attempts with exponential backoff + jitter (capped at 8 s)
163
+ - Retries on 429 and 5xx responses, and on transport errors (timeout / DNS / connection reset)
164
+ - Permanent failure (4xx other than 429) surfaces immediately without retry
165
+ - 10 s timeout per attempt via `AbortController`
166
+
154
167
  ---
155
168
 
156
169
  ## Configuration
@@ -188,6 +201,54 @@ No runtime execution. No monkey-patching.
188
201
 
189
202
  ---
190
203
 
204
+ ## Test Coverage
205
+
206
+ Two test layers, **52 unit tests + 12 e2e fixtures + 3 `check` exit-code scenarios**.
207
+
208
+ **Unit tests (vitest)** — 10 suites covering core modules:
209
+
210
+ | Module | Covers |
211
+ |---|---|
212
+ | `ImportGraph` | Forward/reverse edges, cycles, stale-edge cleanup, file removal |
213
+ | `Scheduler` | Batch coalescing, last-write-wins per file, sequential bursts, error propagation |
214
+ | `DocumentRegistry` | Path normalization, version bumping, no-op on identical content, `ensure()` from disk |
215
+ | `CompilerContext` | Stage/update/remove files, `resolveModule` with `tsconfig.json` `paths`, source-file enumeration |
216
+ | `EventraEngine` | Direct calls, SDK isolation, cross-file wrappers, file updates, file removal, wrapper filtering |
217
+ | `processFile` | Script-kind detection, import/export specifier extraction |
218
+ | `extractTemplateExpressions` | Vue/Svelte/Astro attribute patterns |
219
+ | `normalizeConfig` | Sort events, dedupe wrappers, defaults, preserve `apiKey` / `endpoint` / `sync` |
220
+ | `buildConfigFromScan` | Replace events + wrappers, preserve everything else |
221
+ | `hash` | Stability and uniqueness |
222
+
223
+ **End-to-end fixtures** — 12 isolated TS projects scanned via `eventra sync`:
224
+
225
+ | Fixture | Covers |
226
+ |---|---|
227
+ | `sdk/direct` | Plain `tracker.track()` calls |
228
+ | `frontend/react`, `frontend/next`, `frontend/vue` | Framework-specific code shapes |
229
+ | `backend/node`, `backend/express`, `backend/nest` | Backend wrappers and middleware chains |
230
+ | `wrappers/function` | Local wrapper functions, methods, objects, ternaries, templates |
231
+ | `wrappers/barrel` | `export * from "./tracker"` re-exports |
232
+ | `wrappers/default-export` | `export default function trackFeature` propagation |
233
+ | `wrappers/path-aliases` | `tsconfig.json` `paths` mapping (`@app/*`) |
234
+ | `watch-incremental` | Engine state across sequential file updates (matches `sync` output) |
235
+
236
+ **`eventra check` exit-code scenarios:**
237
+
238
+ - Drift → exit `1`
239
+ - `--fix` writes scan results into `eventra.json` → exit `0`
240
+ - Parity (no drift) → exit `0`
241
+
242
+ Run locally:
243
+
244
+ ```bash
245
+ pnpm --filter @eventra_dev/eventra-cli test # unit + e2e + exit codes
246
+ pnpm --filter @eventra_dev/eventra-cli test:unit # vitest only
247
+ pnpm --filter @eventra_dev/eventra-cli test:e2e # fixtures + check exit codes
248
+ ```
249
+
250
+ ---
251
+
191
252
  ## License
192
253
 
193
254
  MIT
package/dist/cli/send.js CHANGED
@@ -9,6 +9,53 @@ const inquirer_1 = __importDefault(require("inquirer"));
9
9
  const config_1 = require("../config/config");
10
10
  const package_json_1 = __importDefault(require("../../package.json"));
11
11
  const CLI_VERSION = package_json_1.default.version;
12
+ // retry/backoff tuning (mirrors SDK defaults)
13
+ const MAX_ATTEMPTS = 4;
14
+ const BASE_DELAY_MS = 500;
15
+ const MAX_DELAY_MS = 8000;
16
+ const FETCH_TIMEOUT_MS = 10_000;
17
+ // sleep helper for backoff
18
+ function sleep(ms) {
19
+ return new Promise((r) => setTimeout(r, ms));
20
+ }
21
+ // exponential backoff with jitter (capped)
22
+ function backoffMs(attempt) {
23
+ const exp = Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** attempt);
24
+ return exp * (0.5 + Math.random() * 0.5);
25
+ }
26
+ // single delivery attempt with abort timeout
27
+ async function attemptDelivery(endpoint, apiKey, payload) {
28
+ const controller = new AbortController();
29
+ const timeout = setTimeout(() => controller.abort(), FETCH_TIMEOUT_MS);
30
+ try {
31
+ const res = await fetch(endpoint, {
32
+ method: "POST",
33
+ headers: {
34
+ "Content-Type": "application/json",
35
+ "x-api-key": apiKey,
36
+ },
37
+ body: payload,
38
+ signal: controller.signal,
39
+ });
40
+ if (res.ok) {
41
+ const data = (await res.json());
42
+ return { ok: true, data };
43
+ }
44
+ let body;
45
+ try {
46
+ body = await res.text();
47
+ }
48
+ catch {
49
+ /* ignore */
50
+ }
51
+ // 429 and 5xx are transient — worth retrying
52
+ const retryable = res.status === 429 || res.status >= 500;
53
+ return { ok: false, status: res.status, body, retryable };
54
+ }
55
+ finally {
56
+ clearTimeout(timeout);
57
+ }
58
+ }
12
59
  async function send() {
13
60
  const config = await (0, config_1.loadConfig)();
14
61
  const endpoint = config?.endpoint || process.env.EVENTRA_ENDPOINT;
@@ -48,63 +95,76 @@ async function send() {
48
95
  }
49
96
  console.log("");
50
97
  console.log(chalk_1.default.blue(`Sending ${config.events.length} events...`));
51
- const controller = new AbortController();
52
- const timeout = setTimeout(() => controller.abort(), 10000);
53
- try {
54
- const res = await fetch(endpoint, {
55
- method: "POST",
56
- headers: {
57
- "Content-Type": "application/json",
58
- "x-api-key": apiKey
59
- },
60
- body: JSON.stringify({
61
- events: config.events,
62
- cli: {
63
- name: "@eventra_dev/eventra-cli",
64
- version: CLI_VERSION,
65
- runtime: "node"
66
- }
67
- }),
68
- signal: controller.signal
69
- });
70
- if (!res.ok) {
71
- console.log(chalk_1.default.red(`Request failed (${res.status})`));
72
- try {
73
- const text = await res.text();
74
- console.log(chalk_1.default.gray(text));
98
+ const payload = JSON.stringify({
99
+ events: config.events,
100
+ cli: {
101
+ name: "@eventra_dev/eventra-cli",
102
+ version: CLI_VERSION,
103
+ runtime: "node",
104
+ },
105
+ });
106
+ // attempt delivery with retry on 429/5xx/network errors
107
+ let data;
108
+ let lastFailure = null;
109
+ for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
110
+ try {
111
+ const outcome = await attemptDelivery(endpoint, apiKey, payload);
112
+ if (outcome.ok) {
113
+ data = outcome.data;
114
+ break;
75
115
  }
76
- catch {
116
+ if (!outcome.retryable) {
117
+ // permanent failure — surface immediately
118
+ console.log(chalk_1.default.red(`Request failed (${outcome.status})`));
119
+ if (outcome.body)
120
+ console.log(chalk_1.default.gray(outcome.body));
121
+ return;
122
+ }
123
+ lastFailure = { status: outcome.status, body: outcome.body };
124
+ if (attempt < MAX_ATTEMPTS - 1) {
125
+ const delay = backoffMs(attempt);
126
+ console.log(chalk_1.default.yellow(`Attempt ${attempt + 1}/${MAX_ATTEMPTS} failed (${outcome.status}). Retrying in ${Math.round(delay)} ms...`));
127
+ await sleep(delay);
77
128
  }
78
- return;
79
129
  }
80
- const data = await res.json();
81
- console.log(chalk_1.default.green("Events registered successfully"));
82
- // created
83
- if (data.created?.length) {
84
- console.log(chalk_1.default.green("\nNew events:"));
85
- data.created.forEach((e) => console.log(chalk_1.default.green(`+ ${e}`)));
130
+ catch (err) {
131
+ lastFailure = { error: err };
132
+ if (attempt < MAX_ATTEMPTS - 1) {
133
+ const delay = backoffMs(attempt);
134
+ console.log(chalk_1.default.yellow(`Attempt ${attempt + 1}/${MAX_ATTEMPTS} failed (network error). Retrying in ${Math.round(delay)} ms...`));
135
+ await sleep(delay);
136
+ }
86
137
  }
87
- // existing
88
- if (data.existing?.length) {
89
- console.log(chalk_1.default.gray("\nExisting events:"));
90
- data.existing.forEach((e) => console.log(chalk_1.default.gray(`• ${e}`)));
138
+ }
139
+ if (!data) {
140
+ console.log(chalk_1.default.red("All retries exhausted"));
141
+ if (lastFailure?.status) {
142
+ console.log(chalk_1.default.gray(`Last status: ${lastFailure.status}`));
143
+ if (lastFailure.body)
144
+ console.log(chalk_1.default.gray(lastFailure.body));
91
145
  }
92
- // processing notice
93
- if (data.created?.length) {
94
- console.log("");
95
- console.log(chalk_1.default.yellow("Events queued for processing (~2 min)"));
96
- console.log(chalk_1.default.gray("They will appear in dashboard shortly"));
146
+ else if (lastFailure?.error instanceof Error) {
147
+ console.log(chalk_1.default.gray(lastFailure.error.message));
97
148
  }
98
- console.log("");
99
- console.log(chalk_1.default.gray(`Sent ${config.events.length} events`));
149
+ return;
100
150
  }
101
- catch (err) {
102
- console.log(chalk_1.default.red("Network error"));
103
- if (err instanceof Error) {
104
- console.log(chalk_1.default.gray(err.message));
105
- }
151
+ console.log(chalk_1.default.green("Events registered successfully"));
152
+ // created events
153
+ if (data.created?.length) {
154
+ console.log(chalk_1.default.green("\nNew events:"));
155
+ data.created.forEach((e) => console.log(chalk_1.default.green(`+ ${e}`)));
106
156
  }
107
- finally {
108
- clearTimeout(timeout);
157
+ // existing events
158
+ if (data.existing?.length) {
159
+ console.log(chalk_1.default.gray("\nExisting events:"));
160
+ data.existing.forEach((e) => console.log(chalk_1.default.gray(`• ${e}`)));
161
+ }
162
+ // processing notice
163
+ if (data.created?.length) {
164
+ console.log("");
165
+ console.log(chalk_1.default.yellow("Events queued for processing (~2 min)"));
166
+ console.log(chalk_1.default.gray("They will appear in dashboard shortly"));
109
167
  }
168
+ console.log("");
169
+ console.log(chalk_1.default.gray(`Sent ${config.events.length} events`));
110
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eventra_dev/eventra-cli",
3
- "version": "0.3.12",
3
+ "version": "0.4.0",
4
4
  "description": "Static analytics event extraction CLI powered by the TypeScript compiler API",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",
@@ -17,7 +17,6 @@
17
17
  "type": "git",
18
18
  "url": "https://github.com/and-1991/eventra-cli"
19
19
  },
20
-
21
20
  "homepage": "https://eventra.dev",
22
21
  "engines": {
23
22
  "node": ">=18"
@@ -25,7 +24,9 @@
25
24
  "scripts": {
26
25
  "build": "tsc -p tsconfig.json",
27
26
  "dev": "tsx src/index.ts",
28
- "test": "pnpm run clean && pnpm run build && tsx tests/run.ts",
27
+ "test": "pnpm run clean && pnpm run build && pnpm run test:unit && tsx tests/run.ts",
28
+ "test:unit": "vitest run",
29
+ "test:e2e": "pnpm run clean && pnpm run build && tsx tests/run.ts",
29
30
  "clean": "rm -rf dist"
30
31
  },
31
32
  "dependencies": {
@@ -42,18 +43,39 @@
42
43
  "@types/inquirer": "^9.0.9",
43
44
  "@types/node": "^20.0.0",
44
45
  "cross-spawn": "^7.0.6",
45
- "tsx": "^4.7.0"
46
+ "tsx": "^4.7.0",
47
+ "vitest": "^4.1.7"
46
48
  },
47
49
  "keywords": [
48
50
  "analytics",
51
+ "analytics-cli",
49
52
  "event-tracking",
53
+ "feature-usage",
54
+ "feature-tracking",
55
+ "product-analytics",
56
+ "telemetry",
57
+ "tracking",
58
+ "events",
59
+ "event-catalog",
60
+ "event-discovery",
50
61
  "static-analysis",
62
+ "static-code-analysis",
51
63
  "typescript",
64
+ "typescript-compiler",
52
65
  "compiler-api",
53
66
  "ast",
67
+ "code-analysis",
68
+ "drift-detection",
69
+ "ci",
70
+ "ci-cd",
71
+ "lint",
72
+ "watch-mode",
54
73
  "cli",
55
- "telemetry",
56
- "eventra"
74
+ "command-line",
75
+ "developer-tools",
76
+ "devtools",
77
+ "eventra",
78
+ "eventra-cli"
57
79
  ],
58
80
  "publishConfig": {
59
81
  "access": "public"