@eventra_dev/eventra-cli 0.0.4 → 0.0.5

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 CHANGED
@@ -4,69 +4,68 @@
4
4
 
5
5
  # Eventra CLI
6
6
 
7
- [![npm
8
- version](https://img.shields.io/npm/v/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
9
- [![npm
10
- downloads](https://img.shields.io/npm/dm/@eventra_dev/eventra-cli.svg)](https://www.npmjs.com/package/@eventra_dev/eventra-cli)
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)
11
9
  [![TypeScript](https://img.shields.io/badge/typescript-ready-blue.svg)](https://www.typescriptlang.org/)
12
10
 
13
- Eventra CLI automatically discovers feature usage events in your
14
- codebase and syncs them with Eventra.
11
+ Eventra CLI automatically discovers feature usage events in your codebase and syncs them with Eventra.
15
12
 
16
13
  Eventra CLI helps you:
17
14
 
18
- - Discover feature usage automatically
19
- - Detect wrapper components
20
- - Keep events in sync
21
- - Register features in Eventra
22
- - Maintain consistent event naming
15
+ - Discover feature usage automatically
16
+ - Detect wrapper components
17
+ - Detect wrapper functions
18
+ - Keep events in sync
19
+ - Register features in Eventra
20
+ - Maintain consistent event naming
23
21
 
24
22
  It is designed to be:
25
23
 
26
- - framework-agnostic
27
- - static analysis based
28
- - zero runtime overhead
29
- - production-safe
24
+ - framework-agnostic
25
+ - static analysis based
26
+ - zero runtime overhead
27
+ - production-safe
28
+ - backend + frontend compatible
30
29
 
31
- ------------------------------------------------------------------------
30
+ ---
32
31
 
33
32
  # Installation
34
33
 
35
34
  ### npm
36
35
 
37
- ``` bash
36
+ ```bash
38
37
  npm install -D @eventra_dev/eventra-cli
39
38
  ```
40
39
 
41
40
  ### pnpm
42
41
 
43
- ``` bash
42
+ ```bash
44
43
  pnpm add -D @eventra_dev/eventra-cli
45
44
  ```
46
45
 
47
46
  ### yarn
48
47
 
49
- ``` bash
48
+ ```bash
50
49
  yarn add -D @eventra_dev/eventra-cli
51
50
  ```
52
51
 
53
52
  ### npx
54
53
 
55
- ``` bash
54
+ ```bash
56
55
  npx eventra init
57
56
  ```
58
57
 
59
- ------------------------------------------------------------------------
58
+ ---
60
59
 
61
60
  # Quick Start
62
61
 
63
- ``` bash
62
+ ```bash
64
63
  eventra init
65
64
  eventra sync
66
65
  eventra send
67
66
  ```
68
67
 
69
- ------------------------------------------------------------------------
68
+ ---
70
69
 
71
70
  # Commands
72
71
 
@@ -74,60 +73,140 @@ eventra send
74
73
 
75
74
  Creates `eventra.json` configuration file.
76
75
 
77
- ``` bash
76
+ ```bash
78
77
  eventra init
79
78
  ```
80
79
 
81
- ------------------------------------------------------------------------
80
+ ---
82
81
 
83
82
  ## eventra sync
84
83
 
85
84
  Scans your project and discovers events automatically.
86
85
 
87
- ``` ts
86
+ ### track() detection
87
+
88
+ ```ts
88
89
  tracker.track("feature_created")
89
90
  ```
90
91
 
91
- ``` tsx
92
+ ```ts
93
+ track("user_signup")
94
+ ```
95
+
96
+ ---
97
+
98
+ ### Component wrappers
99
+
100
+ ```tsx
92
101
  <TrackedButton event="feature_created" />
93
102
  ```
94
103
 
95
- ``` tsx
104
+ ```tsx
96
105
  <MyComponent event="user_signup" />
97
106
  ```
98
107
 
99
- ------------------------------------------------------------------------
108
+ ---
109
+
110
+ ### Function wrappers
111
+
112
+ ```ts
113
+ trackFeature("feature_created")
114
+ ```
115
+
116
+ ```ts
117
+ analytics.trackFeature("user_signup")
118
+ ```
119
+
120
+ ---
100
121
 
101
122
  ## eventra send
102
123
 
103
124
  Send events to Eventra backend.
104
125
 
105
- ``` bash
126
+ ```bash
106
127
  eventra send
107
128
  ```
108
129
 
109
- ------------------------------------------------------------------------
130
+ New events are queued for processing and will appear in dashboard shortly.
131
+
132
+ Processing typically takes:
133
+
134
+ ~1-2 minutes
135
+
136
+ This delay ensures reliable event ingestion and aggregation.
137
+
138
+ ---
139
+
140
+ # Configuration
141
+
142
+ Eventra CLI creates `eventra.json`
143
+
144
+ Example:
145
+
146
+ ```json
147
+ {
148
+ "apiKey": "",
149
+ "events": [],
150
+ "wrappers": [],
151
+ "functionWrappers": [],
152
+ "sync": {
153
+ "include": ["**/*.{ts,tsx,js,jsx}"],
154
+ "exclude": [
155
+ "node_modules",
156
+ "dist",
157
+ ".next",
158
+ ".git"
159
+ ]
160
+ }
161
+ }
162
+ ```
163
+
164
+ ---
110
165
 
111
166
  # Supported Frameworks
112
167
 
113
- - React
114
- - Next.js
115
- - Vue
116
- - Nuxt
117
- - Svelte
118
- - Astro
119
- - Node.js
120
- - Express
121
- - NestJS
168
+ Frontend:
169
+
170
+ - React
171
+ - Next.js
172
+ - Vue
173
+ - Nuxt
174
+ - Svelte
175
+ - Astro
176
+
177
+ Backend:
178
+
179
+ - Node.js
180
+ - Express
181
+ - NestJS
182
+ - Fastify
183
+ - Hono
184
+ - Bun
185
+ - Deno
186
+
187
+ ---
188
+
189
+ # How It Works
190
+
191
+ Eventra CLI:
192
+
193
+ 1. Scans your codebase
194
+ 2. Detects tracking calls
195
+ 3. Detects wrapper components
196
+ 4. Detects wrapper functions
197
+ 5. Syncs discovered events
198
+ 6. Registers events in Eventra
199
+
200
+ No runtime SDK required.
122
201
 
123
- ------------------------------------------------------------------------
202
+ ---
124
203
 
125
204
  # Requirements
126
205
 
127
- - Node.js 18+
128
- - JavaScript / TypeScript project
206
+ - Node.js 18+
207
+ - JavaScript / TypeScript project
129
208
 
130
- ------------------------------------------------------------------------
209
+ ---
131
210
 
132
211
  # License
133
212
 
@@ -27,6 +27,7 @@ async function init() {
27
27
  apiKey: answers.apiKey || "",
28
28
  events: [],
29
29
  wrappers: [],
30
+ functionWrappers: [],
30
31
  sync: {
31
32
  include: ["**/*.{ts,tsx,js,jsx}"],
32
33
  exclude: [
@@ -7,15 +7,17 @@ exports.send = send;
7
7
  const chalk_1 = __importDefault(require("chalk"));
8
8
  const inquirer_1 = __importDefault(require("inquirer"));
9
9
  const config_1 = require("../utils/config");
10
+ const package_json_1 = __importDefault(require("../../package.json"));
10
11
  const EVENTRA_ENDPOINT = process.env.EVENTRA_ENDPOINT ?? "";
11
- const CLI_VERSION = "0.0.1";
12
+ const CLI_VERSION = package_json_1.default.version;
12
13
  async function send() {
13
- let config = await (0, config_1.loadConfig)();
14
+ const config = await (0, config_1.loadConfig)();
14
15
  if (!config) {
15
16
  console.log(chalk_1.default.red("eventra.json not found. Run 'eventra init'"));
16
17
  return;
17
18
  }
18
19
  let apiKey = config.apiKey;
20
+ // ask api key
19
21
  if (!apiKey) {
20
22
  const answers = await inquirer_1.default.prompt([
21
23
  {
@@ -29,11 +31,21 @@ async function send() {
29
31
  await (0, config_1.saveConfig)(config);
30
32
  console.log(chalk_1.default.green("API key saved"));
31
33
  }
32
- if (!config.events.length) {
34
+ // no events
35
+ if (!config.events?.length) {
33
36
  console.log(chalk_1.default.yellow("No events found. Run 'eventra sync'"));
34
37
  return;
35
38
  }
36
- console.log(chalk_1.default.blue("Sending events..."));
39
+ if (!apiKey) {
40
+ console.log(chalk_1.default.red("API key required"));
41
+ return;
42
+ }
43
+ if (!EVENTRA_ENDPOINT) {
44
+ console.log(chalk_1.default.red("EVENTRA_ENDPOINT not configured"));
45
+ return;
46
+ }
47
+ console.log("");
48
+ console.log(chalk_1.default.blue(`Sending ${config.events.length} events...`));
37
49
  try {
38
50
  const res = await fetch(EVENTRA_ENDPOINT, {
39
51
  method: "POST",
@@ -50,18 +62,40 @@ async function send() {
50
62
  }
51
63
  })
52
64
  });
53
- if (res.status >= 400) {
54
- console.log(chalk_1.default.red(`Failed (${res.status})`));
65
+ if (!res.ok) {
66
+ console.log(chalk_1.default.red(`Request failed (${res.status})`));
67
+ try {
68
+ const text = await res.text();
69
+ console.log(chalk_1.default.gray(text));
70
+ }
71
+ catch { }
55
72
  return;
56
73
  }
57
74
  const data = await res.json();
58
75
  console.log(chalk_1.default.green("Events registered successfully"));
76
+ // created
59
77
  if (data.created?.length) {
60
78
  console.log(chalk_1.default.green("\nNew events:"));
61
79
  data.created.forEach((e) => console.log(chalk_1.default.green(`+ ${e}`)));
62
80
  }
81
+ // existing
82
+ if (data.existing?.length) {
83
+ console.log(chalk_1.default.gray("\nExisting events:"));
84
+ data.existing.forEach((e) => console.log(chalk_1.default.gray(`• ${e}`)));
85
+ }
86
+ // processing notice
87
+ if (data.created?.length) {
88
+ console.log("");
89
+ console.log(chalk_1.default.yellow("Events queued for processing (~2 min)"));
90
+ console.log(chalk_1.default.gray("They will appear in dashboard shortly"));
91
+ }
92
+ console.log("");
93
+ console.log(chalk_1.default.gray(`Sent ${config.events.length} events`));
63
94
  }
64
- catch {
95
+ catch (err) {
65
96
  console.log(chalk_1.default.red("Network error"));
97
+ if (err instanceof Error) {
98
+ console.log(chalk_1.default.gray(err.message));
99
+ }
66
100
  }
67
101
  }
@@ -4,213 +4,46 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.sync = sync;
7
- const ts_morph_1 = require("ts-morph");
8
- const fast_glob_1 = __importDefault(require("fast-glob"));
9
7
  const chalk_1 = __importDefault(require("chalk"));
10
- const inquirer_1 = __importDefault(require("inquirer"));
8
+ const fast_glob_1 = __importDefault(require("fast-glob"));
9
+ const ts_morph_1 = require("ts-morph");
10
+ const promises_1 = __importDefault(require("fs/promises"));
11
11
  const config_1 = require("../utils/config");
12
+ const router_1 = require("../utils/parsers/router");
13
+ const vue_1 = require("../utils/parsers/vue");
14
+ const svelte_1 = require("../utils/parsers/svelte");
15
+ const astro_1 = require("../utils/parsers/astro");
16
+ const track_1 = require("../utils/scanners/track");
17
+ const function_wrappers_1 = require("../utils/scanners/function-wrappers");
18
+ const component_wrappers_1 = require("../utils/scanners/component-wrappers");
12
19
  async function sync() {
13
- let config = await (0, config_1.loadConfig)();
20
+ const config = await (0, config_1.loadConfig)();
14
21
  if (!config) {
15
- console.log(chalk_1.default.red("Run 'eventra init' first"));
22
+ console.log(chalk_1.default.red("Run 'eventra init'"));
16
23
  return;
17
24
  }
18
- const events = new Set();
19
- const project = new ts_morph_1.Project();
20
25
  console.log(chalk_1.default.blue("Scanning project..."));
26
+ const project = new ts_morph_1.Project();
27
+ const events = new Set();
21
28
  const files = await (0, fast_glob_1.default)(config.sync.include, {
22
29
  ignore: config.sync.exclude
23
30
  });
24
- // track()
25
31
  for (const file of files) {
26
- const sourceFile = project.addSourceFileAtPath(file);
27
- const calls = sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression);
28
- for (const call of calls) {
29
- const expression = call.getExpression();
30
- if (expression.getKind() ===
31
- ts_morph_1.SyntaxKind.PropertyAccessExpression) {
32
- const prop = expression.asKind(ts_morph_1.SyntaxKind.PropertyAccessExpression);
33
- if (!prop)
34
- continue;
35
- if (prop.getName() !== "track")
36
- continue;
37
- const args = call.getArguments();
38
- const eventArg = args[0];
39
- if (!eventArg)
40
- continue;
41
- let event = null;
42
- // "event"
43
- if (eventArg.getKind() ===
44
- ts_morph_1.SyntaxKind.StringLiteral) {
45
- event =
46
- eventArg
47
- .asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral)
48
- .getLiteralText();
49
- }
50
- // `event`
51
- if (eventArg.getKind() ===
52
- ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral) {
53
- event =
54
- eventArg
55
- .asKindOrThrow(ts_morph_1.SyntaxKind.NoSubstitutionTemplateLiteral)
56
- .getLiteralText();
57
- }
58
- if (event) {
59
- events.add(event);
60
- }
61
- }
62
- }
63
- }
64
- console.log(chalk_1.default.green(`Found ${events.size} track events`));
65
- // wrappers setup
66
- if (!config.wrappers.length) {
67
- const { useWrapper } = await inquirer_1.default.prompt([
68
- {
69
- type: "confirm",
70
- name: "useWrapper",
71
- message: "Use wrapper components?",
72
- default: true
73
- }
74
- ]);
75
- if (useWrapper) {
76
- const wrappers = [];
77
- let addMore = true;
78
- while (addMore) {
79
- const answers = await inquirer_1.default.prompt([
80
- {
81
- type: "input",
82
- name: "name",
83
- message: "Wrapper component name:"
84
- },
85
- {
86
- type: "input",
87
- name: "prop",
88
- message: "Event prop name:"
89
- }
90
- ]);
91
- wrappers.push({
92
- name: answers.name,
93
- prop: answers.prop
94
- });
95
- const more = await inquirer_1.default.prompt([
96
- {
97
- type: "confirm",
98
- name: "more",
99
- message: "Add another wrapper?",
100
- default: false
101
- }
102
- ]);
103
- addMore = more.more;
104
- }
105
- config.wrappers = wrappers;
106
- }
32
+ const parser = (0, router_1.detectParser)(file);
33
+ let content = await promises_1.default.readFile(file, "utf-8");
34
+ if (parser === "vue")
35
+ content = (0, vue_1.parseVue)(content);
36
+ if (parser === "svelte")
37
+ content = (0, svelte_1.parseSvelte)(content);
38
+ if (parser === "astro")
39
+ content = (0, astro_1.parseAstro)(content);
40
+ const source = project.createSourceFile(file, content, { overwrite: true });
41
+ (0, track_1.scanTrack)(source).forEach((e) => events.add(e));
42
+ (0, function_wrappers_1.scanFunctionWrappers)(source, config.functionWrappers ?? []).forEach((e) => events.add(e));
43
+ (0, component_wrappers_1.scanComponentWrappers)(source, config.wrappers ?? []).forEach((e) => events.add(e));
107
44
  }
108
- // scan wrappers
109
- if (config.wrappers.length) {
110
- console.log(chalk_1.default.blue("Scanning wrappers..."));
111
- for (const file of files) {
112
- const sourceFile = project.addSourceFileAtPath(file);
113
- const elements = [
114
- ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxOpeningElement),
115
- ...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.JsxSelfClosingElement)
116
- ];
117
- for (const element of elements) {
118
- const tagName = element
119
- .getTagNameNode()
120
- .getText()
121
- .toLowerCase();
122
- for (const wrapper of config.wrappers) {
123
- if (tagName !==
124
- wrapper.name.toLowerCase())
125
- continue;
126
- const attrs = element.getAttributes();
127
- for (const attr of attrs) {
128
- if (attr.getKind() !==
129
- ts_morph_1.SyntaxKind.JsxAttribute)
130
- continue;
131
- const attrNode = attr.asKind(ts_morph_1.SyntaxKind.JsxAttribute);
132
- if (!attrNode)
133
- continue;
134
- const attrName = attrNode
135
- .getNameNode()
136
- .getText()
137
- .toLowerCase();
138
- if (attrName !==
139
- wrapper.prop.toLowerCase())
140
- continue;
141
- const initializer = attrNode.getInitializer();
142
- if (!initializer)
143
- continue;
144
- let value = null;
145
- // event="signup"
146
- if (initializer.getKind() ===
147
- ts_morph_1.SyntaxKind.StringLiteral) {
148
- value =
149
- initializer
150
- .asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral)
151
- .getLiteralText();
152
- }
153
- // event={"signup"}
154
- if (initializer.getKind() ===
155
- ts_morph_1.SyntaxKind.JsxExpression) {
156
- const expr = initializer
157
- .asKindOrThrow(ts_morph_1.SyntaxKind.JsxExpression)
158
- .getExpression();
159
- if (expr?.getKind() ===
160
- ts_morph_1.SyntaxKind.StringLiteral) {
161
- value =
162
- expr
163
- .asKindOrThrow(ts_morph_1.SyntaxKind.StringLiteral)
164
- .getLiteralText();
165
- }
166
- }
167
- if (value) {
168
- events.add(value);
169
- }
170
- }
171
- }
172
- }
173
- }
174
- }
175
- // results
176
45
  const list = [...events].sort();
177
- console.log("");
178
- console.log(chalk_1.default.green("Found events:"));
179
- list.forEach((e) => console.log(chalk_1.default.gray(`- ${e}`)));
180
- console.log("");
181
- // diff
182
- const previous = config.events ?? [];
183
- const added = list.filter((e) => !previous.includes(e));
184
- const removed = previous.filter((e) => !list.includes(e));
185
- if (added.length || removed.length) {
186
- console.log(chalk_1.default.blue("Changes:"));
187
- if (added.length) {
188
- console.log(chalk_1.default.green("New events:"));
189
- added.forEach((e) => console.log(chalk_1.default.green(`+ ${e}`)));
190
- }
191
- if (removed.length) {
192
- console.log(chalk_1.default.red("Removed events:"));
193
- removed.forEach((e) => console.log(chalk_1.default.red(`- ${e}`)));
194
- }
195
- console.log("");
196
- }
197
- else {
198
- console.log(chalk_1.default.gray("No changes detected"));
199
- }
200
- // confirm
201
- const { confirm } = await inquirer_1.default.prompt([
202
- {
203
- type: "confirm",
204
- name: "confirm",
205
- message: "Sync these events?",
206
- default: true
207
- }
208
- ]);
209
- if (!confirm) {
210
- console.log(chalk_1.default.yellow("Sync cancelled"));
211
- return;
212
- }
213
46
  config.events = list;
214
47
  await (0, config_1.saveConfig)(config);
215
- console.log(chalk_1.default.green("eventra.json updated"));
48
+ console.log(chalk_1.default.green(`Found ${list.length} events`));
216
49
  }
package/dist/index.js CHANGED
File without changes
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -15,8 +15,11 @@ function normalizeConfig(config) {
15
15
  apiKey: config.apiKey ?? "",
16
16
  events: config.events ?? [],
17
17
  wrappers: config.wrappers ?? [],
18
+ functionWrappers: config.functionWrappers ?? [],
18
19
  sync: config.sync ?? {
19
- include: ["**/*.{ts,tsx,js,jsx}"],
20
+ include: [
21
+ "**/*.{ts,tsx,js,jsx}"
22
+ ],
20
23
  exclude: [
21
24
  "node_modules",
22
25
  "dist",
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.extractEvent = extractEvent;
4
+ const ts_morph_1 = require("ts-morph");
5
+ function extractEvent(call, path) {
6
+ const parts = path.split(".");
7
+ let node = call.getArguments()[Number(parts[0])];
8
+ if (!node)
9
+ return null;
10
+ for (let i = 1; i < parts.length; i++) {
11
+ if (ts_morph_1.Node.isObjectLiteralExpression(node)) {
12
+ const obj = node;
13
+ const prop = obj.getProperty(parts[i]);
14
+ if (!prop)
15
+ return null;
16
+ if (ts_morph_1.Node.isPropertyAssignment(prop)) {
17
+ const initializer = prop.getInitializer();
18
+ if (!initializer)
19
+ return null;
20
+ node = initializer;
21
+ }
22
+ }
23
+ }
24
+ if (node &&
25
+ ts_morph_1.Node.isStringLiteral(node)) {
26
+ return node.getLiteralText();
27
+ }
28
+ return null;
29
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseAstro = parseAstro;
4
+ function parseAstro(content) {
5
+ return content;
6
+ }
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.detectParser = detectParser;
4
+ function detectParser(file) {
5
+ if (file.endsWith(".vue"))
6
+ return "vue";
7
+ if (file.endsWith(".svelte"))
8
+ return "svelte";
9
+ if (file.endsWith(".astro"))
10
+ return "astro";
11
+ return "ts";
12
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSvelte = parseSvelte;
4
+ function parseSvelte(content) {
5
+ return content;
6
+ }
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseVue = parseVue;
4
+ function parseVue(content) {
5
+ const template = content.match(/<template[\s\S]*?>([\s\S]*?)<\/template>/);
6
+ const script = content.match(/<script[\s\S]*?>([\s\S]*?)<\/script>/);
7
+ return ((template?.[1] ?? "") +
8
+ "\n" +
9
+ (script?.[1] ?? ""));
10
+ }