@eventra_dev/eventra-cli 0.3.9 → 0.3.11
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 +69 -185
- package/dist/analysis/extractor/extractor.js +4 -5
- package/dist/analysis/extractor/propagationExtractor.js +4 -5
- package/dist/analysis/scanner/scanner.js +1 -1
- package/dist/analysis/scanner/sinkDetector.js +17 -37
- package/dist/analysis/sdk/eventraSdk.js +42 -0
- package/dist/analysis/sdk/isEventraTrackCall.js +171 -0
- package/dist/analysis/shared/eventValidation.js +7 -0
- package/dist/cli/init.js +4 -6
- package/dist/cli/watch.js +10 -3
- package/dist/core/projectScanner.js +7 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -9,17 +9,15 @@
|
|
|
9
9
|
[](https://www.typescriptlang.org/)
|
|
10
10
|
[]()
|
|
11
11
|
|
|
12
|
-
Eventra CLI
|
|
12
|
+
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
13
|
|
|
14
14
|
---
|
|
15
15
|
|
|
16
16
|
## Overview
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
The CLI scans TypeScript and JavaScript with the TypeScript compiler API and extracts **only** calls to `Eventra.prototype.track()` on instances of `Eventra` imported from `@eventra_dev/eventra-sdk`.
|
|
19
19
|
|
|
20
|
-
It
|
|
21
|
-
|
|
22
|
-
No runtime instrumentation required.
|
|
20
|
+
It does **not** detect generic `track()`, Segment, Google Analytics, or other libraries.
|
|
23
21
|
|
|
24
22
|
---
|
|
25
23
|
|
|
@@ -31,6 +29,12 @@ npm install -D @eventra_dev/eventra-cli
|
|
|
31
29
|
pnpm add -D @eventra_dev/eventra-cli
|
|
32
30
|
```
|
|
33
31
|
|
|
32
|
+
Your project should use the runtime SDK:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pnpm add @eventra_dev/eventra-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
34
38
|
---
|
|
35
39
|
|
|
36
40
|
## Quick Start
|
|
@@ -44,180 +48,108 @@ eventra send
|
|
|
44
48
|
|
|
45
49
|
---
|
|
46
50
|
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
1. Parses your project using the TypeScript compiler API
|
|
50
|
-
2. Builds semantic indexes from AST nodes
|
|
51
|
-
3. Resolves wrappers, imports, exports, and propagation chains
|
|
52
|
-
4. Extracts concrete analytics events statically
|
|
53
|
-
5. Incrementally re-analyzes only affected files
|
|
54
|
-
|
|
55
|
-
No runtime SDK hooks.
|
|
56
|
-
No Babel transforms.
|
|
57
|
-
No instrumentation.
|
|
58
|
-
|
|
59
|
-
---
|
|
60
|
-
|
|
61
|
-
## Why Eventra
|
|
62
|
-
|
|
63
|
-
* Prevent missing or inconsistent event tracking
|
|
64
|
-
* Keep analytics schemas synchronized with source code
|
|
65
|
-
* Detect dead or unused events
|
|
66
|
-
* Analyze wrapper-heavy codebases statically
|
|
67
|
-
* Integrate validation into CI/CD pipelines
|
|
68
|
-
* Avoid runtime analytics overhead
|
|
69
|
-
|
|
70
|
-
---
|
|
71
|
-
|
|
72
|
-
## Commands
|
|
73
|
-
|
|
74
|
-
### `eventra init`
|
|
75
|
-
|
|
76
|
-
Creates `eventra.json` configuration file.
|
|
77
|
-
|
|
78
|
-
---
|
|
79
|
-
|
|
80
|
-
### `eventra sync`
|
|
81
|
-
|
|
82
|
-
Scans your project and updates the list of detected events.
|
|
51
|
+
## What gets detected
|
|
83
52
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
##### Direct calls
|
|
53
|
+
### Direct SDK calls
|
|
87
54
|
|
|
88
55
|
```ts
|
|
89
|
-
|
|
90
|
-
analytics.track("event")
|
|
91
|
-
```
|
|
56
|
+
import { Eventra } from "@eventra_dev/eventra-sdk";
|
|
92
57
|
|
|
93
|
-
|
|
58
|
+
const tracker = new Eventra({ apiKey: "YOUR_PROJECT_API_KEY" });
|
|
94
59
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
track(
|
|
60
|
+
tracker.track("checkout.completed");
|
|
61
|
+
tracker.track("app.loaded", { userId: "user_123" });
|
|
62
|
+
tracker?.track("optional.chain");
|
|
98
63
|
```
|
|
99
64
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
```ts
|
|
103
|
-
track(EVENTS.CLICK)
|
|
104
|
-
```
|
|
65
|
+
### Function wrappers (propagation)
|
|
105
66
|
|
|
106
|
-
|
|
67
|
+
Wrappers that call `Eventra.track()` inside are registered automatically:
|
|
107
68
|
|
|
108
69
|
```ts
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
##### Conditional expressions
|
|
70
|
+
function trackFeature(name: string) {
|
|
71
|
+
sdk.track(name);
|
|
72
|
+
}
|
|
113
73
|
|
|
114
|
-
|
|
115
|
-
track(condition ? "a" : "b")
|
|
74
|
+
trackFeature("purchase");
|
|
116
75
|
```
|
|
117
76
|
|
|
118
|
-
|
|
77
|
+
### Variables, templates, conditionals
|
|
119
78
|
|
|
120
79
|
```ts
|
|
121
|
-
|
|
122
|
-
|
|
80
|
+
const EVENT = "signup";
|
|
81
|
+
tracker.track(EVENT);
|
|
123
82
|
|
|
124
|
-
|
|
83
|
+
tracker.track(`feature_${type}`);
|
|
125
84
|
|
|
126
|
-
|
|
127
|
-
track({
|
|
128
|
-
event: "click"
|
|
129
|
-
})
|
|
85
|
+
tracker.track(flag ? "path.a" : "path.b");
|
|
130
86
|
```
|
|
131
87
|
|
|
132
|
-
|
|
88
|
+
### Cross-file
|
|
133
89
|
|
|
134
90
|
```ts
|
|
135
|
-
|
|
136
|
-
|
|
91
|
+
// tracker.ts
|
|
92
|
+
export function trackFeature(name: string) {
|
|
93
|
+
client.track(name);
|
|
137
94
|
}
|
|
138
95
|
|
|
139
|
-
|
|
96
|
+
// app.ts
|
|
97
|
+
import { trackFeature } from "./tracker";
|
|
98
|
+
trackFeature("purchase");
|
|
140
99
|
```
|
|
141
100
|
|
|
142
|
-
|
|
101
|
+
---
|
|
143
102
|
|
|
144
|
-
|
|
145
|
-
function trackEvent(props: {
|
|
146
|
-
event: string
|
|
147
|
-
}) {
|
|
148
|
-
track(props.event)
|
|
149
|
-
}
|
|
103
|
+
## Event name rules (aligned with SDK)
|
|
150
104
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
105
|
+
| Rule | Limit |
|
|
106
|
+
|------|--------|
|
|
107
|
+
| Max length | **64 characters** (same as SDK) |
|
|
108
|
+
| Allowed characters | `a-zA-Z0-9:_./-` |
|
|
109
|
+
| Position | First argument of `.track(name, options?)` |
|
|
155
110
|
|
|
156
|
-
|
|
111
|
+
The second argument (`userId`, `properties`) is **not** used as the event name.
|
|
157
112
|
|
|
158
|
-
|
|
159
|
-
function buildEvent(name: string) {
|
|
160
|
-
return name
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
track(buildEvent("signup"))
|
|
164
|
-
```
|
|
113
|
+
---
|
|
165
114
|
|
|
166
|
-
|
|
115
|
+
## What is ignored
|
|
167
116
|
|
|
168
117
|
```ts
|
|
169
|
-
|
|
118
|
+
// Not from @eventra_dev/eventra-sdk
|
|
119
|
+
track("legacy");
|
|
120
|
+
analytics.track("ga");
|
|
121
|
+
segment.track("x");
|
|
170
122
|
|
|
171
|
-
|
|
123
|
+
// Wrong API shape for SDK (object as first argument)
|
|
124
|
+
tracker.track({ event: "click" });
|
|
172
125
|
```
|
|
173
126
|
|
|
174
127
|
---
|
|
175
128
|
|
|
176
|
-
|
|
129
|
+
## Commands
|
|
177
130
|
|
|
178
|
-
|
|
131
|
+
### `eventra sync`
|
|
179
132
|
|
|
180
|
-
|
|
181
|
-
eventra check
|
|
182
|
-
```
|
|
133
|
+
Full project scan → updates `eventra.json`:
|
|
183
134
|
|
|
184
|
-
|
|
135
|
+
- `events` — discovered event names
|
|
136
|
+
- `functionWrappers` — detected wrapper functions
|
|
185
137
|
|
|
186
|
-
|
|
187
|
-
* Removed events
|
|
188
|
-
* Invalid or inconsistent tracking usage
|
|
138
|
+
### `eventra check`
|
|
189
139
|
|
|
190
|
-
|
|
140
|
+
Compares config with the current scan (events + wrappers). Exit code `1` on drift.
|
|
191
141
|
|
|
192
142
|
### `eventra check --fix`
|
|
193
143
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
```bash
|
|
197
|
-
eventra check --fix
|
|
198
|
-
```
|
|
199
|
-
|
|
200
|
-
---
|
|
144
|
+
Writes scan results into `eventra.json`.
|
|
201
145
|
|
|
202
146
|
### `eventra watch`
|
|
203
147
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
```bash
|
|
207
|
-
eventra watch
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Only affected files are re-analyzed after changes.
|
|
211
|
-
|
|
212
|
-
---
|
|
148
|
+
Incremental scan with the same rules as `sync`.
|
|
213
149
|
|
|
214
150
|
### `eventra send`
|
|
215
151
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
```bash
|
|
219
|
-
eventra send
|
|
220
|
-
```
|
|
152
|
+
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.
|
|
221
153
|
|
|
222
154
|
---
|
|
223
155
|
|
|
@@ -228,79 +160,31 @@ eventra send
|
|
|
228
160
|
"apiKey": "",
|
|
229
161
|
"endpoint": "",
|
|
230
162
|
"events": [],
|
|
231
|
-
"wrappers": [],
|
|
232
163
|
"functionWrappers": [],
|
|
233
164
|
"sync": {
|
|
234
165
|
"include": ["**/*.{ts,tsx,js,jsx}"],
|
|
235
|
-
"exclude": [
|
|
236
|
-
"node_modules",
|
|
237
|
-
"dist",
|
|
238
|
-
".next",
|
|
239
|
-
".git"
|
|
240
|
-
]
|
|
166
|
+
"exclude": ["node_modules", "dist", ".next", ".git"]
|
|
241
167
|
}
|
|
242
168
|
}
|
|
243
169
|
```
|
|
244
170
|
|
|
245
171
|
---
|
|
246
172
|
|
|
247
|
-
##
|
|
248
|
-
|
|
249
|
-
### Semantic propagation analysis
|
|
250
|
-
|
|
251
|
-
Eventra resolves event propagation through wrappers and returned values.
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
function createEvent(name: string) {
|
|
255
|
-
return name
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function trackEvent(name: string) {
|
|
259
|
-
track(createEvent(name))
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
---
|
|
264
|
-
|
|
265
|
-
### Incremental analysis engine
|
|
266
|
-
|
|
267
|
-
Eventra does NOT rescan the whole project after every update.
|
|
268
|
-
|
|
269
|
-
Instead it:
|
|
270
|
-
|
|
271
|
-
1. Updates changed files
|
|
272
|
-
2. Invalidates affected caches
|
|
273
|
-
3. Rebuilds TypeScript incrementally
|
|
274
|
-
4. Traverses dependency graph
|
|
275
|
-
5. Re-analyzes affected files only
|
|
276
|
-
|
|
277
|
-
---
|
|
278
|
-
|
|
279
|
-
### Static extraction
|
|
280
|
-
|
|
281
|
-
Eventra works entirely statically.
|
|
282
|
-
|
|
283
|
-
No runtime execution.
|
|
284
|
-
No monkey patching.
|
|
285
|
-
No SDK instrumentation.
|
|
286
|
-
|
|
287
|
-
---
|
|
288
|
-
|
|
289
|
-
## Supported Environments
|
|
173
|
+
## How it works
|
|
290
174
|
|
|
291
|
-
|
|
175
|
+
1. Load project files into an incremental TypeScript program (with SDK type shim).
|
|
176
|
+
2. **Phase 1** — find `Eventra` instances from `@eventra_dev/eventra-sdk` and register function wrappers that call `.track()`.
|
|
177
|
+
3. **Phase 2** — resolve static event names and wrapper propagation chains.
|
|
178
|
+
4. Write results to `eventra.json`.
|
|
292
179
|
|
|
293
|
-
|
|
294
|
-
* TypeScript
|
|
295
|
-
* Node.js projects
|
|
296
|
-
* Browser applications
|
|
297
|
-
* Monorepos
|
|
180
|
+
No runtime execution. No monkey-patching.
|
|
298
181
|
|
|
299
182
|
---
|
|
300
183
|
|
|
301
184
|
## Requirements
|
|
302
185
|
|
|
303
|
-
|
|
186
|
+
- Node.js 18+
|
|
187
|
+
- TypeScript/JavaScript source using `@eventra_dev/eventra-sdk`
|
|
304
188
|
|
|
305
189
|
---
|
|
306
190
|
|
|
@@ -8,9 +8,7 @@ const typescript_1 = __importDefault(require("typescript"));
|
|
|
8
8
|
const resolver_1 = require("../resolver/resolver");
|
|
9
9
|
const evaluationContext_1 = require("../shared/evaluationContext");
|
|
10
10
|
const propagationExtractor_1 = require("./propagationExtractor");
|
|
11
|
-
|
|
12
|
-
return (value.length > 0 && value.length < 160 && /^[a-zA-Z0-9:_./-]+$/.test(value));
|
|
13
|
-
}
|
|
11
|
+
const eventValidation_1 = require("../shared/eventValidation");
|
|
14
12
|
function extractEvents(index, checker, _config, evaluationCache, exportCache, resolvedCallCache, returnPropagationCache, wrapperRegistry) {
|
|
15
13
|
const events = new Set();
|
|
16
14
|
// direct track sinks
|
|
@@ -18,8 +16,9 @@ function extractEvents(index, checker, _config, evaluationCache, exportCache, re
|
|
|
18
16
|
for (const argument of call.trackedArguments) {
|
|
19
17
|
const resolved = (0, resolver_1.resolveNodeValue)(argument, checker, (0, evaluationContext_1.createEvaluationContext)(), new Set(), evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
20
18
|
for (const value of resolved.values) {
|
|
21
|
-
|
|
22
|
-
|
|
19
|
+
const normalized = (0, eventValidation_1.normalizeEventName)(value);
|
|
20
|
+
if (normalized) {
|
|
21
|
+
events.add(normalized);
|
|
23
22
|
}
|
|
24
23
|
}
|
|
25
24
|
}
|
|
@@ -8,9 +8,7 @@ const typescript_1 = __importDefault(require("typescript"));
|
|
|
8
8
|
const callResolver_1 = require("../resolver/callResolver");
|
|
9
9
|
const resolver_1 = require("../resolver/resolver");
|
|
10
10
|
const evaluationContext_1 = require("../shared/evaluationContext");
|
|
11
|
-
|
|
12
|
-
return (value.length > 0 && value.length < 160 && /^[a-zA-Z0-9:_./-]+$/.test(value));
|
|
13
|
-
}
|
|
11
|
+
const eventValidation_1 = require("../shared/eventValidation");
|
|
14
12
|
function resolveObjectPath(expression, path) {
|
|
15
13
|
let current = expression;
|
|
16
14
|
for (const segment of path) {
|
|
@@ -86,8 +84,9 @@ function extractPropagation(propagation, call, checker, evaluationCache, resolve
|
|
|
86
84
|
}
|
|
87
85
|
const resolved = (0, resolver_1.resolveNodeValue)(propagation.targetNode, checker, context, new Set(), evaluationCache, resolvedCallCache, returnPropagationCache, exportCache);
|
|
88
86
|
for (const value of resolved.values) {
|
|
89
|
-
|
|
90
|
-
|
|
87
|
+
const normalized = (0, eventValidation_1.normalizeEventName)(value);
|
|
88
|
+
if (normalized) {
|
|
89
|
+
events.add(normalized);
|
|
91
90
|
}
|
|
92
91
|
}
|
|
93
92
|
}
|
|
@@ -23,7 +23,7 @@ function scanSource(source, checker, wrapperRegistry) {
|
|
|
23
23
|
}
|
|
24
24
|
// sink detection
|
|
25
25
|
if (typescript_1.default.isCallExpression(node)) {
|
|
26
|
-
const sink = (0, sinkDetector_1.detectTrackSink)(node);
|
|
26
|
+
const sink = (0, sinkDetector_1.detectTrackSink)(node, checker);
|
|
27
27
|
if (sink) {
|
|
28
28
|
sinks.push(sink);
|
|
29
29
|
const trackedArguments = sink.trackedArguments.map(tracked => {
|
|
@@ -5,53 +5,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.detectTrackSink = detectTrackSink;
|
|
7
7
|
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
}
|
|
11
|
-
function isTrackIdentifier(expr) {
|
|
12
|
-
return (typescript_1.default.isIdentifier(expr) && expr.text === "track");
|
|
13
|
-
}
|
|
14
|
-
function isTrackProperty(expr) {
|
|
15
|
-
return (typescript_1.default.isPropertyAccessExpression(expr) && expr.name.text === "track");
|
|
16
|
-
}
|
|
17
|
-
function isOptionalTrackProperty(expr) {
|
|
18
|
-
return (typescript_1.default.isPropertyAccessChain(expr) && expr.name.text === "track");
|
|
19
|
-
}
|
|
8
|
+
const isEventraTrackCall_1 = require("../sdk/isEventraTrackCall");
|
|
9
|
+
/** SDK: track(name: string, options?) — event name is always the first argument. */
|
|
20
10
|
function extractTrackedArguments(call) {
|
|
21
|
-
const result = [];
|
|
22
11
|
if (call.arguments.length === 0) {
|
|
23
|
-
return
|
|
12
|
+
return [];
|
|
24
13
|
}
|
|
25
14
|
const first = call.arguments[0];
|
|
26
15
|
if (typescript_1.default.isObjectLiteralExpression(first)) {
|
|
27
|
-
|
|
28
|
-
if (!typescript_1.default.isPropertyAssignment(property) && !typescript_1.default.isShorthandPropertyAssignment(property)) {
|
|
29
|
-
continue;
|
|
30
|
-
}
|
|
31
|
-
const name = property.name.getText();
|
|
32
|
-
if (name !== "event" && name !== "name" && name !== "type") {
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
result.push({
|
|
36
|
-
index: 0,
|
|
37
|
-
propertyPath: [name],
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
return result;
|
|
16
|
+
return [];
|
|
41
17
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
index: 0,
|
|
21
|
+
propertyPath: [],
|
|
22
|
+
},
|
|
23
|
+
];
|
|
47
24
|
}
|
|
48
|
-
function detectTrackSink(call) {
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
function detectTrackSink(call, checker) {
|
|
26
|
+
if (!(0, isEventraTrackCall_1.isEventraSdkTrackCall)(call, checker)) {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const trackedArguments = extractTrackedArguments(call);
|
|
30
|
+
if (trackedArguments.length === 0) {
|
|
51
31
|
return null;
|
|
52
32
|
}
|
|
53
33
|
return {
|
|
54
34
|
call,
|
|
55
|
-
trackedArguments
|
|
35
|
+
trackedArguments,
|
|
56
36
|
};
|
|
57
37
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EVENTRA_SDK_SHIM = exports.MAX_EVENT_NAME_LENGTH = exports.EVENTRA_SDK_PACKAGE = void 0;
|
|
4
|
+
exports.isEventraSdkModuleSpecifier = isEventraSdkModuleSpecifier;
|
|
5
|
+
exports.normalizeEventName = normalizeEventName;
|
|
6
|
+
exports.isValidEventName = isValidEventName;
|
|
7
|
+
/** Mirrors @eventra_dev/eventra-sdk runtime limits (see packages/sdk/src/client.ts). */
|
|
8
|
+
exports.EVENTRA_SDK_PACKAGE = "@eventra_dev/eventra-sdk";
|
|
9
|
+
exports.MAX_EVENT_NAME_LENGTH = 64;
|
|
10
|
+
function isEventraSdkModuleSpecifier(specifier) {
|
|
11
|
+
const normalized = specifier.replace(/\\/g, "/");
|
|
12
|
+
return (normalized === exports.EVENTRA_SDK_PACKAGE ||
|
|
13
|
+
normalized.startsWith(`${exports.EVENTRA_SDK_PACKAGE}/`));
|
|
14
|
+
}
|
|
15
|
+
function normalizeEventName(value) {
|
|
16
|
+
const trimmed = value.trim();
|
|
17
|
+
if (!trimmed || trimmed.length > exports.MAX_EVENT_NAME_LENGTH) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
if (!/^[a-zA-Z0-9:_./-]+$/.test(trimmed)) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
return trimmed;
|
|
24
|
+
}
|
|
25
|
+
function isValidEventName(value) {
|
|
26
|
+
return normalizeEventName(value) !== null;
|
|
27
|
+
}
|
|
28
|
+
exports.EVENTRA_SDK_SHIM = `declare module "@eventra_dev/eventra-sdk" {
|
|
29
|
+
export interface TrackOptions {
|
|
30
|
+
userId?: string;
|
|
31
|
+
properties?: Record<string, unknown>;
|
|
32
|
+
}
|
|
33
|
+
export interface TrackerOptions {
|
|
34
|
+
apiKey: string;
|
|
35
|
+
endpoint?: string;
|
|
36
|
+
}
|
|
37
|
+
export class Eventra {
|
|
38
|
+
constructor(options: TrackerOptions);
|
|
39
|
+
track(name: string, options?: TrackOptions): void;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.isEventraSdkTrackCall = isEventraSdkTrackCall;
|
|
7
|
+
const typescript_1 = __importDefault(require("typescript"));
|
|
8
|
+
const eventraSdk_1 = require("./eventraSdk");
|
|
9
|
+
function isTrackCallee(expr) {
|
|
10
|
+
if (typescript_1.default.isPropertyAccessExpression(expr) || typescript_1.default.isPropertyAccessChain(expr)) {
|
|
11
|
+
return expr.name.text === "track";
|
|
12
|
+
}
|
|
13
|
+
if (typescript_1.default.isElementAccessExpression(expr) &&
|
|
14
|
+
expr.argumentExpression &&
|
|
15
|
+
typescript_1.default.isStringLiteral(expr.argumentExpression) &&
|
|
16
|
+
expr.argumentExpression.text === "track") {
|
|
17
|
+
return true;
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
function fileImportsEventraSdk(sourceFile) {
|
|
22
|
+
for (const stmt of sourceFile.statements) {
|
|
23
|
+
if (!typescript_1.default.isImportDeclaration(stmt) || !stmt.moduleSpecifier) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
if (!typescript_1.default.isStringLiteral(stmt.moduleSpecifier)) {
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if ((0, eventraSdk_1.isEventraSdkModuleSpecifier)(stmt.moduleSpecifier.text)) {
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
function getImportDeclarationForSpecifier(decl) {
|
|
36
|
+
let current = decl;
|
|
37
|
+
while (current) {
|
|
38
|
+
if (typescript_1.default.isImportDeclaration(current)) {
|
|
39
|
+
return current;
|
|
40
|
+
}
|
|
41
|
+
current = current.parent;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function isEventraImportDeclaration(decl) {
|
|
46
|
+
if (typescript_1.default.isImportSpecifier(decl) && decl.name.text === "Eventra") {
|
|
47
|
+
const importDecl = getImportDeclarationForSpecifier(decl);
|
|
48
|
+
if (importDecl?.moduleSpecifier && typescript_1.default.isStringLiteral(importDecl.moduleSpecifier)) {
|
|
49
|
+
return (0, eventraSdk_1.isEventraSdkModuleSpecifier)(importDecl.moduleSpecifier.text);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
if (typescript_1.default.isImportClause(decl)) {
|
|
53
|
+
const parent = decl.parent;
|
|
54
|
+
if (parent && typescript_1.default.isImportDeclaration(parent) && parent.moduleSpecifier && typescript_1.default.isStringLiteral(parent.moduleSpecifier)) {
|
|
55
|
+
return (0, eventraSdk_1.isEventraSdkModuleSpecifier)(parent.moduleSpecifier.text);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
function isEventraClassDeclaration(decl) {
|
|
61
|
+
if (!typescript_1.default.isClassDeclaration(decl) || decl.name?.text !== "Eventra") {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
const file = decl.getSourceFile().fileName.replace(/\\/g, "/");
|
|
65
|
+
return file.includes("eventra-sdk") || file.includes("@eventra_dev");
|
|
66
|
+
}
|
|
67
|
+
function isEventraSymbol(symbol, checker, depth = 0) {
|
|
68
|
+
if (depth > 10) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
if (symbol.flags & typescript_1.default.SymbolFlags.Alias) {
|
|
72
|
+
return isEventraSymbol(checker.getAliasedSymbol(symbol), checker, depth + 1);
|
|
73
|
+
}
|
|
74
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
75
|
+
if (isEventraImportDeclaration(decl) || isEventraClassDeclaration(decl)) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
function isEventraNewExpression(node, checker) {
|
|
82
|
+
const expr = node.expression;
|
|
83
|
+
if (!typescript_1.default.isIdentifier(expr)) {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
const symbol = checker.getSymbolAtLocation(expr);
|
|
87
|
+
return symbol ? isEventraSymbol(symbol, checker) : false;
|
|
88
|
+
}
|
|
89
|
+
function typeLooksLikeEventra(type, checker, depth = 0) {
|
|
90
|
+
if (depth > 8) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
const symbol = type.getSymbol();
|
|
94
|
+
if (symbol && isEventraSymbol(symbol, checker)) {
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
const typeStr = checker.typeToString(type);
|
|
98
|
+
if (typeStr === "Eventra" || /^Eventra<.*>$/.test(typeStr)) {
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
if (type.isUnionOrIntersection()) {
|
|
102
|
+
return type.types.some((t) => typeLooksLikeEventra(t, checker, depth + 1));
|
|
103
|
+
}
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
function isEventraReceiverExpression(receiver, checker) {
|
|
107
|
+
if (typescript_1.default.isNewExpression(receiver)) {
|
|
108
|
+
return isEventraNewExpression(receiver, checker);
|
|
109
|
+
}
|
|
110
|
+
if (typescript_1.default.isCallExpression(receiver) && typescript_1.default.isNewExpression(receiver.expression)) {
|
|
111
|
+
return isEventraNewExpression(receiver.expression, checker);
|
|
112
|
+
}
|
|
113
|
+
const type = checker.getTypeAtLocation(receiver);
|
|
114
|
+
if (typeLooksLikeEventra(type, checker)) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
if (typescript_1.default.isIdentifier(receiver)) {
|
|
118
|
+
const symbol = checker.getSymbolAtLocation(receiver);
|
|
119
|
+
if (!symbol) {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
for (const decl of symbol.getDeclarations() ?? []) {
|
|
123
|
+
if (typescript_1.default.isVariableDeclaration(decl) && decl.initializer) {
|
|
124
|
+
if (typescript_1.default.isNewExpression(decl.initializer) && isEventraNewExpression(decl.initializer, checker)) {
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
if (typescript_1.default.isCallExpression(decl.initializer) &&
|
|
128
|
+
typescript_1.default.isNewExpression(decl.initializer.expression) &&
|
|
129
|
+
isEventraNewExpression(decl.initializer.expression, checker)) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (typescript_1.default.isParameter(decl) && decl.type) {
|
|
134
|
+
const paramType = checker.getTypeFromTypeNode(decl.type);
|
|
135
|
+
if (typeLooksLikeEventra(paramType, checker)) {
|
|
136
|
+
return true;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (typescript_1.default.isPropertyDeclaration(decl) && decl.type) {
|
|
140
|
+
const propType = checker.getTypeFromTypeNode(decl.type);
|
|
141
|
+
if (typeLooksLikeEventra(propType, checker)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (typescript_1.default.isPropertyAccessExpression(receiver) || typescript_1.default.isPropertyAccessChain(receiver)) {
|
|
148
|
+
return isEventraReceiverExpression(receiver.expression, checker);
|
|
149
|
+
}
|
|
150
|
+
return false;
|
|
151
|
+
}
|
|
152
|
+
/** Only `instance.track(...)` where instance is Eventra from @eventra_dev/eventra-sdk. */
|
|
153
|
+
function isEventraSdkTrackCall(call, checker) {
|
|
154
|
+
const expr = call.expression;
|
|
155
|
+
if (!isTrackCallee(expr)) {
|
|
156
|
+
return false;
|
|
157
|
+
}
|
|
158
|
+
const sourceFile = call.getSourceFile();
|
|
159
|
+
if (!fileImportsEventraSdk(sourceFile)) {
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
const receiver = typescript_1.default.isPropertyAccessExpression(expr) ||
|
|
163
|
+
typescript_1.default.isPropertyAccessChain(expr) ||
|
|
164
|
+
typescript_1.default.isElementAccessExpression(expr)
|
|
165
|
+
? expr.expression
|
|
166
|
+
: undefined;
|
|
167
|
+
if (!receiver) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
return isEventraReceiverExpression(receiver, checker);
|
|
171
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MAX_EVENT_NAME_LENGTH = exports.normalizeEventName = exports.isValidEventName = void 0;
|
|
4
|
+
var eventraSdk_1 = require("../sdk/eventraSdk");
|
|
5
|
+
Object.defineProperty(exports, "isValidEventName", { enumerable: true, get: function () { return eventraSdk_1.isValidEventName; } });
|
|
6
|
+
Object.defineProperty(exports, "normalizeEventName", { enumerable: true, get: function () { return eventraSdk_1.normalizeEventName; } });
|
|
7
|
+
Object.defineProperty(exports, "MAX_EVENT_NAME_LENGTH", { enumerable: true, get: function () { return eventraSdk_1.MAX_EVENT_NAME_LENGTH; } });
|
package/dist/cli/init.js
CHANGED
|
@@ -23,12 +23,10 @@ async function init() {
|
|
|
23
23
|
},
|
|
24
24
|
]);
|
|
25
25
|
console.log("");
|
|
26
|
-
console.log(chalk_1.default.gray("Eventra
|
|
27
|
-
console.log(chalk_1.default.gray(
|
|
28
|
-
console.log(chalk_1.default.gray("•
|
|
29
|
-
console.log(chalk_1.default.gray("•
|
|
30
|
-
console.log(chalk_1.default.gray("• cross-file wrappers"));
|
|
31
|
-
console.log(chalk_1.default.gray("• parameter propagation"));
|
|
26
|
+
console.log(chalk_1.default.gray("Eventra detects @eventra_dev/eventra-sdk usage:"));
|
|
27
|
+
console.log(chalk_1.default.gray('• tracker.track("event.name")'));
|
|
28
|
+
console.log(chalk_1.default.gray("• function wrappers → Eventra.track()"));
|
|
29
|
+
console.log(chalk_1.default.gray("• cross-file propagation chains"));
|
|
32
30
|
console.log("");
|
|
33
31
|
const config = {
|
|
34
32
|
apiKey,
|
package/dist/cli/watch.js
CHANGED
|
@@ -10,8 +10,10 @@ const promises_1 = __importDefault(require("fs/promises"));
|
|
|
10
10
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
11
11
|
const path_1 = __importDefault(require("path"));
|
|
12
12
|
const config_1 = require("../config/config");
|
|
13
|
+
const eventraSdk_1 = require("../analysis/sdk/eventraSdk");
|
|
13
14
|
const EventraEngine_1 = require("../core/EventraEngine");
|
|
14
15
|
const scanResults_1 = require("../core/scanResults");
|
|
16
|
+
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
15
17
|
const processFile_1 = require("../filesystem/processFile");
|
|
16
18
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
17
19
|
async function watch() {
|
|
@@ -34,6 +36,7 @@ async function watch() {
|
|
|
34
36
|
absolute: true,
|
|
35
37
|
});
|
|
36
38
|
engine.beginPreload();
|
|
39
|
+
await engine.preloadFile(SDK_TYPES_FILE, eventraSdk_1.EVENTRA_SDK_SHIM);
|
|
37
40
|
for (const file of files) {
|
|
38
41
|
try {
|
|
39
42
|
const abs = normalize(file);
|
|
@@ -42,14 +45,16 @@ async function watch() {
|
|
|
42
45
|
cache.set(abs, parsed);
|
|
43
46
|
trackedFiles.add(abs);
|
|
44
47
|
await engine.preloadFile((0, getVirtualFile_1.getVirtualFile)(abs), parsed.content);
|
|
45
|
-
fileDeps.set(abs, new Set(parsed.dependencies
|
|
48
|
+
fileDeps.set(abs, new Set(parsed.dependencies
|
|
49
|
+
.filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
|
|
50
|
+
.map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(abs), dep)))));
|
|
46
51
|
}
|
|
47
52
|
catch {
|
|
48
53
|
// ignore invalid files
|
|
49
54
|
}
|
|
50
55
|
}
|
|
51
56
|
engine.endPreload();
|
|
52
|
-
await engine.runFullAnalysis([...trackedFiles].map((f) => (0, getVirtualFile_1.getVirtualFile)(f)), config);
|
|
57
|
+
await engine.runFullAnalysis([SDK_TYPES_FILE, ...trackedFiles].map((f) => (0, getVirtualFile_1.getVirtualFile)(f)), config);
|
|
53
58
|
await (0, scanResults_1.persistScanResults)(config, engine);
|
|
54
59
|
const initialEvents = engine.getAllEvents();
|
|
55
60
|
const initialWrappers = engine.getAllFunctionWrappers();
|
|
@@ -69,7 +74,9 @@ async function watch() {
|
|
|
69
74
|
await engine.updateFile((0, getVirtualFile_1.getVirtualFile)(file), parsed.content, config);
|
|
70
75
|
const abs = normalize(file);
|
|
71
76
|
const prevDeps = fileDeps.get(abs) ?? new Set();
|
|
72
|
-
const nextDeps = new Set(parsed.dependencies
|
|
77
|
+
const nextDeps = new Set(parsed.dependencies
|
|
78
|
+
.filter((dep) => dep.startsWith(".") || dep.startsWith("/"))
|
|
79
|
+
.map((dep) => normalize(path_1.default.resolve(path_1.default.dirname(abs), dep))));
|
|
73
80
|
for (const dep of nextDeps) {
|
|
74
81
|
if (prevDeps.has(dep))
|
|
75
82
|
continue;
|
|
@@ -7,9 +7,11 @@ exports.scanProject = scanProject;
|
|
|
7
7
|
const fast_glob_1 = __importDefault(require("fast-glob"));
|
|
8
8
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const eventraSdk_1 = require("../analysis/sdk/eventraSdk");
|
|
10
11
|
const EventraEngine_1 = require("./EventraEngine");
|
|
11
12
|
const processFile_1 = require("../filesystem/processFile");
|
|
12
13
|
const getVirtualFile_1 = require("../filesystem/getVirtualFile");
|
|
14
|
+
const SDK_TYPES_FILE = "__eventra_sdk_types__.d.ts";
|
|
13
15
|
async function getParsedFile(file, cache) {
|
|
14
16
|
const existing = cache.get(file);
|
|
15
17
|
if (existing)
|
|
@@ -36,15 +38,18 @@ async function scanProject(config) {
|
|
|
36
38
|
cache.set(file, parsed);
|
|
37
39
|
toScan.add(file);
|
|
38
40
|
for (const dep of parsed.dependencies) {
|
|
39
|
-
|
|
41
|
+
if (dep.startsWith(".") || dep.startsWith("/")) {
|
|
42
|
+
toScan.add(path_1.default.resolve(path_1.default.dirname(file), dep));
|
|
43
|
+
}
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
catch (err) {
|
|
43
47
|
console.error(`skip: ${file}`, err instanceof Error ? err.message : err);
|
|
44
48
|
}
|
|
45
49
|
}
|
|
46
|
-
const virtualFiles = [...toScan].map((f) => (0, getVirtualFile_1.getVirtualFile)(f));
|
|
50
|
+
const virtualFiles = [SDK_TYPES_FILE, ...toScan].map((f) => (0, getVirtualFile_1.getVirtualFile)(f));
|
|
47
51
|
engine.beginPreload();
|
|
52
|
+
await engine.preloadFile(SDK_TYPES_FILE, eventraSdk_1.EVENTRA_SDK_SHIM);
|
|
48
53
|
for (const file of toScan) {
|
|
49
54
|
try {
|
|
50
55
|
const parsed = await getParsedFile(file, cache);
|