@flisk/analyze-tracking 0.7.1 → 0.7.2
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 +35 -61
- package/package.json +16 -3
- package/src/analyze/analyzeJsFile.js +20 -5
- package/src/analyze/analyzePythonFile.js +1 -0
- package/src/analyze/analyzeRubyFile.js +20 -10
- package/src/analyze/analyzeTsFile.js +138 -15
- package/src/analyze/helpers.js +462 -23
- package/src/analyze/index.js +0 -1
- package/src/analyze/pythonTrackingAnalyzer.py +149 -47
- package/.github/workflows/npm-publish.yml +0 -33
- package/.github/workflows/pr-check.yml +0 -17
- package/jest.config.js +0 -7
- package/tests/detectSource.test.js +0 -20
- package/tests/extractProperties.test.js +0 -109
- package/tests/findWrappingFunction.test.js +0 -30
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @flisk/analyze-tracking
|
|
2
2
|
|
|
3
|
-
Automatically document your analytics setup by analyzing tracking code and generating data schemas from tools like Segment, Amplitude, Mixpanel, and more
|
|
3
|
+
Automatically document your analytics setup by analyzing tracking code and generating data schemas from tools like Segment, Amplitude, Mixpanel, and more 🚀
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@flisk/analyze-tracking)
|
|
5
|
+
[](https://www.npmjs.com/package/@flisk/analyze-tracking) [](https://github.com/fliskdata/analyze-tracking/actions/workflows/tests.yml)
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
## Why Use @flisk/analyze-tracking?
|
|
@@ -30,7 +30,7 @@ npx @flisk/analyze-tracking /path/to/project [options]
|
|
|
30
30
|
- `-o, --output <output_file>`: Name of the output file (default: `tracking-schema.yaml`)
|
|
31
31
|
- `-c, --customFunction <function_name>`: Specify a custom tracking function
|
|
32
32
|
|
|
33
|
-
🔑 **Important:** If you are using `generateDescription`, you must set the appropriate credentials for the provider you are using as an environment variable. OpenAI uses `OPENAI_API_KEY` and Google Vertex AI uses `GOOGLE_APPLICATION_CREDENTIALS`.
|
|
33
|
+
🔑 **Important:** If you are using `generateDescription`, you must set the appropriate credentials for the LLM provider you are using as an environment variable. OpenAI uses `OPENAI_API_KEY` and Google Vertex AI uses `GOOGLE_APPLICATION_CREDENTIALS`.
|
|
34
34
|
|
|
35
35
|
<details>
|
|
36
36
|
<summary>Note on Custom Functions 💡</summary>
|
|
@@ -96,10 +96,10 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
96
96
|
| Mixpanel | ✅ | ✅ | ✅ | ✅ |
|
|
97
97
|
| Amplitude | ✅ | ✅ | ❌ | ✅ |
|
|
98
98
|
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
|
|
99
|
-
| mParticle | ✅ |
|
|
99
|
+
| mParticle | ✅ | ❌ | ❌ | ❌ |
|
|
100
100
|
| PostHog | ✅ | ✅ | ✅ | ✅ |
|
|
101
|
-
| Pendo | ✅ |
|
|
102
|
-
| Heap | ✅ |
|
|
101
|
+
| Pendo | ✅ | ❌ | ❌ | ❌ |
|
|
102
|
+
| Heap | ✅ | ❌ | ❌ | ❌ |
|
|
103
103
|
| Snowplow | ✅ | ✅ | ✅ | ✅ |
|
|
104
104
|
| Custom Function | ✅ | ✅ | ✅ | ✅ |
|
|
105
105
|
|
|
@@ -198,16 +198,22 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
198
198
|
|
|
199
199
|
**JavaScript/TypeScript**
|
|
200
200
|
```js
|
|
201
|
-
amplitude.
|
|
201
|
+
amplitude.track('<event_name>', {
|
|
202
202
|
<event_parameters>
|
|
203
203
|
});
|
|
204
204
|
```
|
|
205
205
|
|
|
206
206
|
**Python**
|
|
207
207
|
```python
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
client.track(
|
|
209
|
+
BaseEvent(
|
|
210
|
+
event_type="<event_name>",
|
|
211
|
+
user_id="<user_id>",
|
|
212
|
+
event_properties={
|
|
213
|
+
"<property_name>": "<property_value>",
|
|
214
|
+
},
|
|
215
|
+
)
|
|
216
|
+
)
|
|
211
217
|
```
|
|
212
218
|
|
|
213
219
|
**Go**
|
|
@@ -266,19 +272,10 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
266
272
|
|
|
267
273
|
**JavaScript/TypeScript**
|
|
268
274
|
```js
|
|
269
|
-
mParticle.logEvent('<event_name>', {
|
|
275
|
+
mParticle.logEvent('<event_name>', mParticle.EventType.<event_type>, {
|
|
270
276
|
'<property_name>': '<property_value>'
|
|
271
277
|
});
|
|
272
278
|
```
|
|
273
|
-
|
|
274
|
-
**Python**
|
|
275
|
-
```python
|
|
276
|
-
mParticle.logEvent('<event_name>', {
|
|
277
|
-
'<property_name>': '<property_value>'
|
|
278
|
-
})
|
|
279
|
-
```
|
|
280
|
-
|
|
281
|
-
|
|
282
279
|
</details>
|
|
283
280
|
|
|
284
281
|
<details>
|
|
@@ -293,13 +290,9 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
293
290
|
|
|
294
291
|
**Python**
|
|
295
292
|
```python
|
|
296
|
-
posthog.capture(
|
|
297
|
-
'
|
|
298
|
-
|
|
299
|
-
{
|
|
300
|
-
'<property_name>': '<property_value>'
|
|
301
|
-
}
|
|
302
|
-
)
|
|
293
|
+
posthog.capture('distinct_id', '<event_name>', {
|
|
294
|
+
'<property_name>': '<property_value>'
|
|
295
|
+
})
|
|
303
296
|
# Or
|
|
304
297
|
posthog.capture(
|
|
305
298
|
'distinct_id',
|
|
@@ -377,61 +370,42 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
377
370
|
|
|
378
371
|
**JavaScript/TypeScript**
|
|
379
372
|
```js
|
|
380
|
-
|
|
373
|
+
tracker.track(buildStructEvent({
|
|
374
|
+
action: '<event_name>',
|
|
381
375
|
category: '<category>',
|
|
382
|
-
action: '<action>',
|
|
383
376
|
label: '<label>',
|
|
384
377
|
property: '<property>',
|
|
385
|
-
value:
|
|
386
|
-
});
|
|
378
|
+
value: <value>
|
|
379
|
+
}));
|
|
387
380
|
```
|
|
388
381
|
|
|
389
382
|
**Python**
|
|
390
383
|
```python
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
# Builder pattern
|
|
401
|
-
buildStructEvent({
|
|
402
|
-
'category': '<category>',
|
|
403
|
-
'action': '<action>',
|
|
404
|
-
'label': '<label>',
|
|
405
|
-
'property': '<property>',
|
|
406
|
-
'value': '<value>'
|
|
407
|
-
})
|
|
408
|
-
|
|
409
|
-
# Function call pattern
|
|
410
|
-
snowplow('trackStructEvent', {
|
|
411
|
-
'category': '<category>',
|
|
412
|
-
'action': '<action>',
|
|
413
|
-
'label': '<label>',
|
|
414
|
-
'property': '<property>',
|
|
415
|
-
'value': '<value>'
|
|
416
|
-
})
|
|
384
|
+
tracker.track(StructuredEvent(
|
|
385
|
+
action="<event_name>",
|
|
386
|
+
category="<category>",
|
|
387
|
+
label="<label>",
|
|
388
|
+
property_="<property>",
|
|
389
|
+
value=<value>,
|
|
390
|
+
))
|
|
417
391
|
```
|
|
418
392
|
|
|
419
393
|
**Ruby**
|
|
420
394
|
```ruby
|
|
421
395
|
tracker.track_struct_event(
|
|
396
|
+
action: '<event_name>',
|
|
422
397
|
category: '<category>',
|
|
423
|
-
action: '<action>',
|
|
424
398
|
label: '<label>',
|
|
425
399
|
property: '<property>',
|
|
426
|
-
value:
|
|
400
|
+
value: <value>
|
|
427
401
|
)
|
|
428
402
|
```
|
|
429
403
|
|
|
430
404
|
**Go**
|
|
431
405
|
```go
|
|
432
406
|
tracker.TrackStructEvent(sp.StructuredEvent{
|
|
407
|
+
Action: sp.NewString("<event_name>"),
|
|
433
408
|
Category: sp.NewString("<category>"),
|
|
434
|
-
Action: sp.NewString("<action>"),
|
|
435
409
|
Label: sp.NewString("<label>"),
|
|
436
410
|
Property: sp.NewString("<property>"),
|
|
437
411
|
Value: sp.NewFloat64(<value>),
|
|
@@ -441,4 +415,4 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
441
415
|
|
|
442
416
|
|
|
443
417
|
## Contribute
|
|
444
|
-
We're actively improving this package. Found a bug?
|
|
418
|
+
We're actively improving this package. Found a bug? Have a feature request? Open an issue or submit a pull request!
|
package/package.json
CHANGED
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk/analyze-tracking",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Analyzes tracking code in a project and generates data schemas",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"analyze-tracking": "bin/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "node tests",
|
|
11
|
+
"test:js": "node --test tests/analyzeJsFile.test.js",
|
|
12
|
+
"test:ts": "node --test tests/analyzeTsFile.test.js",
|
|
13
|
+
"test:python": "node --experimental-vm-modules --test tests/analyzePythonFile.test.js",
|
|
14
|
+
"test:ruby": "node --experimental-vm-modules --test tests/analyzeRubyFile.test.js",
|
|
15
|
+
"test:go": "node --test tests/analyzeGoFile.test.js",
|
|
16
|
+
"test:cli": "node --test tests/cli.test.js",
|
|
17
|
+
"test:schema": "node --test tests/schema.test.js"
|
|
11
18
|
},
|
|
19
|
+
"files": [
|
|
20
|
+
"bin",
|
|
21
|
+
"src",
|
|
22
|
+
"schema.json"
|
|
23
|
+
],
|
|
12
24
|
"repository": {
|
|
13
25
|
"type": "git",
|
|
14
26
|
"url": "git+https://github.com/fliskdata/analyze-tracking.git"
|
|
@@ -38,6 +50,7 @@
|
|
|
38
50
|
"zod": "^3.24.4"
|
|
39
51
|
},
|
|
40
52
|
"devDependencies": {
|
|
41
|
-
"
|
|
53
|
+
"ajv": "^8.17.1",
|
|
54
|
+
"lodash": "^4.17.21"
|
|
42
55
|
}
|
|
43
56
|
}
|
|
@@ -33,10 +33,20 @@ function analyzeJsFile(filePath, customFunction) {
|
|
|
33
33
|
if (source === 'googleanalytics' && node.arguments.length >= 3) {
|
|
34
34
|
eventName = node.arguments[1]?.value || null;
|
|
35
35
|
propertiesNode = node.arguments[2];
|
|
36
|
-
} else if (source === 'snowplow' && node.arguments.length
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
} else if (source === 'snowplow' && node.arguments.length > 0) {
|
|
37
|
+
// Snowplow pattern: tracker.track(buildStructEvent({...}))
|
|
38
|
+
const firstArg = node.arguments[0];
|
|
39
|
+
if (firstArg.type === 'CallExpression' && firstArg.arguments.length > 0) {
|
|
40
|
+
const structEventArg = firstArg.arguments[0];
|
|
41
|
+
if (structEventArg.type === 'ObjectExpression') {
|
|
42
|
+
const actionProperty = structEventArg.properties.find(prop => prop.key.name === 'action');
|
|
43
|
+
eventName = actionProperty ? actionProperty.value.value : null;
|
|
44
|
+
propertiesNode = structEventArg;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
} else if (source === 'mparticle' && node.arguments.length >= 3) {
|
|
48
|
+
eventName = node.arguments[0]?.value || null;
|
|
49
|
+
propertiesNode = node.arguments[2];
|
|
40
50
|
} else if (node.arguments.length >= 2) {
|
|
41
51
|
eventName = node.arguments[0]?.value || null;
|
|
42
52
|
propertiesNode = node.arguments[1];
|
|
@@ -46,7 +56,12 @@ function analyzeJsFile(filePath, customFunction) {
|
|
|
46
56
|
const functionName = findWrappingFunctionJs(node, ancestors);
|
|
47
57
|
|
|
48
58
|
if (eventName && propertiesNode && propertiesNode.type === 'ObjectExpression') {
|
|
49
|
-
|
|
59
|
+
let properties = extractJsProperties(propertiesNode);
|
|
60
|
+
|
|
61
|
+
// For Snowplow, remove 'action' from properties since it's used as the event name
|
|
62
|
+
if (source === 'snowplow' && properties.action) {
|
|
63
|
+
delete properties.action;
|
|
64
|
+
}
|
|
50
65
|
|
|
51
66
|
events.push({
|
|
52
67
|
eventName,
|
|
@@ -25,6 +25,7 @@ async function analyzePythonFile(filePath, customFunction) {
|
|
|
25
25
|
py.globals.set('code', code);
|
|
26
26
|
py.globals.set('filepath', filePath);
|
|
27
27
|
py.globals.set('custom_function', customFunction || null);
|
|
28
|
+
py.globals.set('__name__', null);
|
|
28
29
|
|
|
29
30
|
// Run the Python analyzer
|
|
30
31
|
py.runPython(analyzerCode);
|
|
@@ -46,12 +46,16 @@ class TrackingVisitor {
|
|
|
46
46
|
const objectName = node.receiver.name;
|
|
47
47
|
const methodName = node.name;
|
|
48
48
|
|
|
49
|
-
// Segment
|
|
50
|
-
|
|
49
|
+
// Segment and Rudderstack (both use similar format)
|
|
50
|
+
// Analytics.track (Segment) or analytics.track (Rudderstack)
|
|
51
|
+
if ((objectName === 'Analytics' || objectName === 'analytics') && methodName === 'track') {
|
|
52
|
+
// Try to determine if it's Rudderstack based on context
|
|
53
|
+
// For now, we'll treat lowercase 'analytics' as Rudderstack
|
|
54
|
+
return objectName === 'analytics' ? 'rudderstack' : 'segment';
|
|
55
|
+
}
|
|
51
56
|
|
|
52
57
|
// Mixpanel (Ruby SDK uses Mixpanel::Tracker instance)
|
|
53
|
-
if (methodName === 'track' &&
|
|
54
|
-
node.receiver.name === 'tracker') return 'mixpanel';
|
|
58
|
+
if (methodName === 'track' && objectName === 'tracker') return 'mixpanel';
|
|
55
59
|
|
|
56
60
|
// PostHog
|
|
57
61
|
if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
|
|
@@ -67,7 +71,8 @@ class TrackingVisitor {
|
|
|
67
71
|
}
|
|
68
72
|
|
|
69
73
|
extractEventName(node, source) {
|
|
70
|
-
if (source === 'segment') {
|
|
74
|
+
if (source === 'segment' || source === 'rudderstack') {
|
|
75
|
+
// Both Segment and Rudderstack use the same format
|
|
71
76
|
const params = node.arguments_.arguments_[0].elements;
|
|
72
77
|
const eventProperty = params.find(param => param?.key?.unescaped?.value === 'event');
|
|
73
78
|
return eventProperty?.value?.unescaped?.value || null;
|
|
@@ -111,7 +116,8 @@ class TrackingVisitor {
|
|
|
111
116
|
async extractProperties(node, source) {
|
|
112
117
|
const { HashNode, ArrayNode } = await import('@ruby/prism');
|
|
113
118
|
|
|
114
|
-
if (source === 'segment') {
|
|
119
|
+
if (source === 'segment' || source === 'rudderstack') {
|
|
120
|
+
// Both Segment and Rudderstack use the same format
|
|
115
121
|
const params = node.arguments_.arguments_[0].elements;
|
|
116
122
|
const properties = {};
|
|
117
123
|
|
|
@@ -158,10 +164,10 @@ class TrackingVisitor {
|
|
|
158
164
|
const args = node.arguments_.arguments_;
|
|
159
165
|
const properties = {};
|
|
160
166
|
|
|
161
|
-
// Add distinct_id as property
|
|
162
|
-
if (args && args.length > 0
|
|
167
|
+
// Add distinct_id as property (even if it's a variable)
|
|
168
|
+
if (args && args.length > 0) {
|
|
163
169
|
properties.distinct_id = {
|
|
164
|
-
type:
|
|
170
|
+
type: await this.getValueType(args[0])
|
|
165
171
|
};
|
|
166
172
|
}
|
|
167
173
|
|
|
@@ -300,7 +306,7 @@ class TrackingVisitor {
|
|
|
300
306
|
}
|
|
301
307
|
|
|
302
308
|
async visit(node) {
|
|
303
|
-
const { CallNode, ProgramNode, StatementsNode, DefNode, IfNode, BlockNode, ArgumentsNode, HashNode, AssocNode, ClassNode } = await import('@ruby/prism');
|
|
309
|
+
const { CallNode, ProgramNode, StatementsNode, DefNode, IfNode, BlockNode, ArgumentsNode, HashNode, AssocNode, ClassNode, ModuleNode } = await import('@ruby/prism');
|
|
304
310
|
if (!node) return;
|
|
305
311
|
|
|
306
312
|
this.ancestors.push(node);
|
|
@@ -344,6 +350,10 @@ class TrackingVisitor {
|
|
|
344
350
|
if (node.body) {
|
|
345
351
|
await this.visit(node.body);
|
|
346
352
|
}
|
|
353
|
+
} else if (node instanceof ModuleNode) {
|
|
354
|
+
if (node.body) {
|
|
355
|
+
await this.visit(node.body);
|
|
356
|
+
}
|
|
347
357
|
} else if (node instanceof DefNode) {
|
|
348
358
|
if (node.body) {
|
|
349
359
|
await this.visit(node.body);
|
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
const ts = require('typescript');
|
|
2
|
-
const { detectSourceTs, findWrappingFunctionTs, extractTsProperties } = require('./helpers');
|
|
2
|
+
const { detectSourceTs, findWrappingFunctionTs, extractTsProperties, resolveIdentifierToInitializer, extractInterfaceProperties } = require('./helpers');
|
|
3
|
+
|
|
4
|
+
function resolveUnresolvedTypes(properties, checker, sourceFile) {
|
|
5
|
+
const resolved = {};
|
|
6
|
+
|
|
7
|
+
for (const [key, value] of Object.entries(properties)) {
|
|
8
|
+
if (value && typeof value === 'object') {
|
|
9
|
+
if (value.__unresolved) {
|
|
10
|
+
// Try to find and resolve the type
|
|
11
|
+
const typeName = value.__unresolved;
|
|
12
|
+
delete value.__unresolved;
|
|
13
|
+
|
|
14
|
+
// This is a simplified approach - in practice, you'd need to find the actual type declaration
|
|
15
|
+
// For now, we'll keep the object type but remove the unresolved marker
|
|
16
|
+
resolved[key] = value;
|
|
17
|
+
} else if (value.type === 'array' && value.items && value.items.__unresolved) {
|
|
18
|
+
// Handle unresolved array element types
|
|
19
|
+
const itemTypeName = value.items.__unresolved;
|
|
20
|
+
delete value.items.__unresolved;
|
|
21
|
+
resolved[key] = value;
|
|
22
|
+
} else if (value.type === 'array' && value.items && typeof value.items.type === 'string' && value.items.type.includes(' ')) {
|
|
23
|
+
// Handle types like "readonly Product" - extract the actual type name
|
|
24
|
+
const typeString = value.items.type;
|
|
25
|
+
const actualType = typeString.replace(/^readonly\s+/, '').trim();
|
|
26
|
+
|
|
27
|
+
// If it looks like a custom type, mark it as object
|
|
28
|
+
if (actualType[0] === actualType[0].toUpperCase() && !actualType.includes('<')) {
|
|
29
|
+
resolved[key] = {
|
|
30
|
+
...value,
|
|
31
|
+
items: {
|
|
32
|
+
type: 'object'
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
} else {
|
|
36
|
+
resolved[key] = value;
|
|
37
|
+
}
|
|
38
|
+
} else if (value.type === 'object' && value.properties) {
|
|
39
|
+
// Recursively resolve nested properties
|
|
40
|
+
resolved[key] = {
|
|
41
|
+
...value,
|
|
42
|
+
properties: resolveUnresolvedTypes(value.properties, checker, sourceFile)
|
|
43
|
+
};
|
|
44
|
+
} else if (value.type === 'array' && value.items && value.items.properties) {
|
|
45
|
+
// Recursively resolve array item properties
|
|
46
|
+
resolved[key] = {
|
|
47
|
+
...value,
|
|
48
|
+
items: {
|
|
49
|
+
...value.items,
|
|
50
|
+
properties: value.items.properties ? resolveUnresolvedTypes(value.items.properties, checker, sourceFile) : undefined
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
} else {
|
|
54
|
+
resolved[key] = value;
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
resolved[key] = value;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return resolved;
|
|
62
|
+
}
|
|
3
63
|
|
|
4
64
|
function analyzeTsFile(filePath, program, customFunction) {
|
|
5
65
|
let events = [];
|
|
@@ -24,10 +84,50 @@ function analyzeTsFile(filePath, program, customFunction) {
|
|
|
24
84
|
if (source === 'googleanalytics' && node.arguments.length >= 3) {
|
|
25
85
|
eventName = node.arguments[1]?.text || null;
|
|
26
86
|
propertiesNode = node.arguments[2];
|
|
27
|
-
} else if (source === 'snowplow' && node.arguments.length
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
87
|
+
} else if (source === 'snowplow' && node.arguments.length > 0) {
|
|
88
|
+
// Snowplow pattern: tracker.track(buildStructEvent({...})) or tracker.track(payload)
|
|
89
|
+
const firstArg = node.arguments[0];
|
|
90
|
+
|
|
91
|
+
// Check if it's a direct buildStructEvent call
|
|
92
|
+
if (ts.isCallExpression(firstArg) &&
|
|
93
|
+
ts.isIdentifier(firstArg.expression) &&
|
|
94
|
+
firstArg.expression.escapedText === 'buildStructEvent' &&
|
|
95
|
+
firstArg.arguments.length > 0) {
|
|
96
|
+
const structEventArg = firstArg.arguments[0];
|
|
97
|
+
if (ts.isObjectLiteralExpression(structEventArg)) {
|
|
98
|
+
// Find the action property for event name
|
|
99
|
+
const actionProp = structEventArg.properties.find(
|
|
100
|
+
prop => prop.name && prop.name.escapedText === 'action'
|
|
101
|
+
);
|
|
102
|
+
if (actionProp && actionProp.initializer && ts.isStringLiteral(actionProp.initializer)) {
|
|
103
|
+
eventName = actionProp.initializer.text;
|
|
104
|
+
propertiesNode = structEventArg;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
// Check if it's a variable reference (e.g., const payload = buildStructEvent({...}))
|
|
109
|
+
else if (ts.isIdentifier(firstArg)) {
|
|
110
|
+
const resolvedNode = resolveIdentifierToInitializer(checker, firstArg, sourceFile);
|
|
111
|
+
if (resolvedNode && ts.isCallExpression(resolvedNode) &&
|
|
112
|
+
ts.isIdentifier(resolvedNode.expression) &&
|
|
113
|
+
resolvedNode.expression.escapedText === 'buildStructEvent' &&
|
|
114
|
+
resolvedNode.arguments.length > 0) {
|
|
115
|
+
const structEventArg = resolvedNode.arguments[0];
|
|
116
|
+
if (ts.isObjectLiteralExpression(structEventArg)) {
|
|
117
|
+
const actionProp = structEventArg.properties.find(
|
|
118
|
+
prop => prop.name && prop.name.escapedText === 'action'
|
|
119
|
+
);
|
|
120
|
+
if (actionProp && actionProp.initializer && ts.isStringLiteral(actionProp.initializer)) {
|
|
121
|
+
eventName = actionProp.initializer.text;
|
|
122
|
+
propertiesNode = structEventArg;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
} else if (source === 'mparticle' && node.arguments.length >= 3) {
|
|
128
|
+
// mParticle: first param is event name, second is event type (ignored), third is properties
|
|
129
|
+
eventName = node.arguments[0]?.text || null;
|
|
130
|
+
propertiesNode = node.arguments[2];
|
|
31
131
|
} else if (node.arguments.length >= 2) {
|
|
32
132
|
eventName = node.arguments[0]?.text || null;
|
|
33
133
|
propertiesNode = node.arguments[1];
|
|
@@ -36,17 +136,40 @@ function analyzeTsFile(filePath, program, customFunction) {
|
|
|
36
136
|
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
37
137
|
const functionName = findWrappingFunctionTs(node);
|
|
38
138
|
|
|
39
|
-
if (eventName && propertiesNode
|
|
139
|
+
if (eventName && propertiesNode) {
|
|
40
140
|
try {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
properties,
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
141
|
+
let properties = null;
|
|
142
|
+
|
|
143
|
+
// Check if properties is an object literal
|
|
144
|
+
if (ts.isObjectLiteralExpression(propertiesNode)) {
|
|
145
|
+
properties = extractTsProperties(checker, propertiesNode);
|
|
146
|
+
}
|
|
147
|
+
// Check if properties is an identifier (variable reference)
|
|
148
|
+
else if (ts.isIdentifier(propertiesNode)) {
|
|
149
|
+
const resolvedNode = resolveIdentifierToInitializer(checker, propertiesNode, sourceFile);
|
|
150
|
+
if (resolvedNode && ts.isObjectLiteralExpression(resolvedNode)) {
|
|
151
|
+
properties = extractTsProperties(checker, resolvedNode);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (properties) {
|
|
156
|
+
// For Snowplow, remove 'action' from properties since it's used as the event name
|
|
157
|
+
if (source === 'snowplow' && properties.action) {
|
|
158
|
+
delete properties.action;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Clean up any unresolved type markers
|
|
162
|
+
const cleanedProperties = resolveUnresolvedTypes(properties, checker, sourceFile);
|
|
163
|
+
|
|
164
|
+
events.push({
|
|
165
|
+
eventName,
|
|
166
|
+
source,
|
|
167
|
+
properties: cleanedProperties,
|
|
168
|
+
filePath,
|
|
169
|
+
line,
|
|
170
|
+
functionName
|
|
171
|
+
});
|
|
172
|
+
}
|
|
50
173
|
} catch (propertyError) {
|
|
51
174
|
console.error(`Error extracting properties in ${filePath} at line ${line}`);
|
|
52
175
|
}
|