@flisk/analyze-tracking 0.8.7 → 0.9.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.
- package/README.md +84 -19
- package/package.json +5 -3
- package/src/analyze/index.js +20 -13
- package/src/analyze/javascript/detectors/analytics-source.js +25 -14
- package/src/analyze/javascript/extractors/event-extractor.js +24 -1
- package/src/analyze/javascript/parser.js +23 -13
- package/src/analyze/javascript/utils/import-resolver.js +233 -0
- package/src/analyze/swift/constants.js +61 -0
- package/src/analyze/swift/custom.js +36 -0
- package/src/analyze/swift/index.js +708 -0
- package/src/analyze/swift/providers.js +51 -0
- package/src/analyze/swift/runtime.js +46 -0
- package/src/analyze/swift/utils.js +75 -0
- package/src/analyze/typescript/detectors/analytics-source.js +47 -10
- package/src/analyze/utils/customFunctionParser.js +20 -8
package/README.md
CHANGED
|
@@ -95,25 +95,25 @@ Use this to understand where your events live in the code and how they're being
|
|
|
95
95
|
Your LLM of choice is used for generating descriptions of events, properties, and implementations.
|
|
96
96
|
|
|
97
97
|
See [schema.json](schema.json) for a JSON Schema of the output.
|
|
98
|
-
|
|
98
|
+
|
|
99
99
|
|
|
100
100
|
## Supported tracking libraries & languages
|
|
101
101
|
|
|
102
|
-
| Library | JavaScript/TypeScript | Python | Ruby | Go |
|
|
103
|
-
|
|
104
|
-
| Google Analytics | ✅ | ❌ | ❌ | ❌ |
|
|
105
|
-
| Google Tag Manager | ✅ | ❌ | ❌ | ❌ |
|
|
106
|
-
| Segment | ✅ | ✅ | ✅ | ✅ |
|
|
107
|
-
| Mixpanel | ✅ | ✅ | ✅ | ✅ |
|
|
108
|
-
| Amplitude | ✅ | ✅ | ❌ | ✅ |
|
|
109
|
-
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
|
|
110
|
-
| mParticle | ✅ | ❌ | ❌ | ❌ |
|
|
111
|
-
| PostHog | ✅ | ✅ | ✅ | ✅ |
|
|
112
|
-
| Pendo | ✅ | ❌ | ❌ | ❌ |
|
|
113
|
-
| Heap | ✅ | ❌ | ❌ | ❌ |
|
|
114
|
-
| Snowplow | ✅ | ✅ | ✅ | ✅ |
|
|
115
|
-
| Datadog RUM | ✅ | ❌ | ❌ | ❌ |
|
|
116
|
-
| Custom Function | ✅ | ✅ | ✅ | ✅ |
|
|
102
|
+
| Library | JavaScript/TypeScript | Python | Ruby | Go | Swift |
|
|
103
|
+
|---------|:---------------------:|:------:|:----:|:--:|:--:|
|
|
104
|
+
| Google Analytics | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
105
|
+
| Google Tag Manager | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
106
|
+
| Segment | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
107
|
+
| Mixpanel | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
108
|
+
| Amplitude | ✅ | ✅ | ❌ | ✅ | ✅ |
|
|
109
|
+
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ | ✅ |
|
|
110
|
+
| mParticle | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
111
|
+
| PostHog | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
112
|
+
| Pendo | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
113
|
+
| Heap | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
114
|
+
| Snowplow | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
115
|
+
| Datadog RUM | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
116
|
+
| Custom Function | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
117
117
|
|
|
118
118
|
✳️ Rudderstack's SDKs often use the same format as Segment, so Rudderstack events may be detected as Segment events.
|
|
119
119
|
|
|
@@ -129,6 +129,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
129
129
|
'<property_name>': '<property_value>'
|
|
130
130
|
});
|
|
131
131
|
```
|
|
132
|
+
|
|
133
|
+
**Swift**
|
|
134
|
+
```swift
|
|
135
|
+
Analytics.logEvent("<event_name>", parameters: [
|
|
136
|
+
"<property_name>": "<property_value>"
|
|
137
|
+
])
|
|
138
|
+
```
|
|
132
139
|
</details>
|
|
133
140
|
|
|
134
141
|
<details>
|
|
@@ -147,6 +154,11 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
147
154
|
'<property_name>': '<property_value>'
|
|
148
155
|
});
|
|
149
156
|
```
|
|
157
|
+
|
|
158
|
+
**Swift**
|
|
159
|
+
```swift
|
|
160
|
+
dataLayer.push(["event": "<event_name>", "<property_name>": "<property_value>"])
|
|
161
|
+
```
|
|
150
162
|
</details>
|
|
151
163
|
|
|
152
164
|
<details>
|
|
@@ -185,6 +197,11 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
185
197
|
Set("<property_name>", "<property_value>"),
|
|
186
198
|
})
|
|
187
199
|
```
|
|
200
|
+
|
|
201
|
+
**Swift**
|
|
202
|
+
```swift
|
|
203
|
+
analytics.track(name: "<event_name>", properties: TrackProperties("<property_name>": "<property_value>"))
|
|
204
|
+
```
|
|
188
205
|
</details>
|
|
189
206
|
|
|
190
207
|
<details>
|
|
@@ -221,6 +238,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
221
238
|
}),
|
|
222
239
|
})
|
|
223
240
|
```
|
|
241
|
+
|
|
242
|
+
**Swift**
|
|
243
|
+
```swift
|
|
244
|
+
Mixpanel.mainInstance().track(event: "<event_name>", properties: [
|
|
245
|
+
"<property_name>": "<property_value>"
|
|
246
|
+
])
|
|
247
|
+
```
|
|
224
248
|
</details>
|
|
225
249
|
|
|
226
250
|
<details>
|
|
@@ -256,6 +280,14 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
256
280
|
},
|
|
257
281
|
})
|
|
258
282
|
```
|
|
283
|
+
|
|
284
|
+
**Swift**
|
|
285
|
+
```swift
|
|
286
|
+
amplitude.track(
|
|
287
|
+
eventType: "<event_name>",
|
|
288
|
+
eventProperties: ["<property_name>": "<property_value>"]
|
|
289
|
+
)
|
|
290
|
+
```
|
|
259
291
|
</details>
|
|
260
292
|
|
|
261
293
|
<details>
|
|
@@ -295,6 +327,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
295
327
|
Set("<property_name>", "<property_value>"),
|
|
296
328
|
})
|
|
297
329
|
```
|
|
330
|
+
|
|
331
|
+
**Swift**
|
|
332
|
+
```swift
|
|
333
|
+
RSClient.sharedInstance()?.track("<event_name>", properties: [
|
|
334
|
+
"<property_name>": "<property_value>"
|
|
335
|
+
])
|
|
336
|
+
```
|
|
298
337
|
</details>
|
|
299
338
|
|
|
300
339
|
<details>
|
|
@@ -306,6 +345,15 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
306
345
|
'<property_name>': '<property_value>'
|
|
307
346
|
});
|
|
308
347
|
```
|
|
348
|
+
|
|
349
|
+
**Swift**
|
|
350
|
+
```swift
|
|
351
|
+
let event = MPEvent(name: "<event_name>", type: .other)
|
|
352
|
+
event.customAttributes = [
|
|
353
|
+
"<property_name>": "<property_value>"
|
|
354
|
+
]
|
|
355
|
+
MParticle.sharedInstance().logEvent(event)
|
|
356
|
+
```
|
|
309
357
|
</details>
|
|
310
358
|
|
|
311
359
|
<details>
|
|
@@ -353,6 +401,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
353
401
|
Set("<property_name>", "<property_value>"),
|
|
354
402
|
})
|
|
355
403
|
```
|
|
404
|
+
|
|
405
|
+
**Swift**
|
|
406
|
+
```swift
|
|
407
|
+
PostHogSDK.shared.capture("<event_name>", properties: [
|
|
408
|
+
"<property_name>": "<property_value>"
|
|
409
|
+
])
|
|
410
|
+
```
|
|
356
411
|
</details>
|
|
357
412
|
|
|
358
413
|
<details>
|
|
@@ -372,7 +427,12 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
372
427
|
})
|
|
373
428
|
```
|
|
374
429
|
|
|
375
|
-
|
|
430
|
+
**Swift**
|
|
431
|
+
```swift
|
|
432
|
+
PendoManager.shared().track("<event_name>", properties: [
|
|
433
|
+
"<property_name>": "<property_value>"
|
|
434
|
+
])
|
|
435
|
+
```
|
|
376
436
|
</details>
|
|
377
437
|
|
|
378
438
|
<details>
|
|
@@ -392,7 +452,12 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
392
452
|
})
|
|
393
453
|
```
|
|
394
454
|
|
|
395
|
-
|
|
455
|
+
**Swift**
|
|
456
|
+
```swift
|
|
457
|
+
Heap.shared.track("<event_name>", properties: [
|
|
458
|
+
"<property_name>": "<property_value>"
|
|
459
|
+
])
|
|
460
|
+
```
|
|
396
461
|
</details>
|
|
397
462
|
|
|
398
463
|
<details>
|
|
@@ -403,7 +468,7 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
403
468
|
datadogRum.addAction('<event_name>', {
|
|
404
469
|
'<property_name>': '<property_value>'
|
|
405
470
|
});
|
|
406
|
-
|
|
471
|
+
|
|
407
472
|
// Or via window
|
|
408
473
|
window.DD_RUM.addAction('<event_name>', {
|
|
409
474
|
'<property_name>': '<property_value>'
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@flisk/analyze-tracking",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Analyzes tracking code in a project and generates data schemas",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"test:python": "node --experimental-vm-modules --test tests/analyzePython.test.js",
|
|
14
14
|
"test:ruby": "node --experimental-vm-modules --test tests/analyzeRuby.test.js",
|
|
15
15
|
"test:go": "node --test tests/analyzeGo.test.js",
|
|
16
|
+
"test:swift": "node --test tests/analyzeSwift.test.js",
|
|
16
17
|
"test:cli": "node --test tests/cli.test.js",
|
|
17
18
|
"test:schema": "node --test tests/schema.test.js",
|
|
18
19
|
"test:generateDescriptions": "node --test tests/generateDescriptions.test.js",
|
|
@@ -34,6 +35,7 @@
|
|
|
34
35
|
},
|
|
35
36
|
"homepage": "https://github.com/fliskdata/analyze-tracking#readme",
|
|
36
37
|
"dependencies": {
|
|
38
|
+
"@flisk/swift-ast": "^0.1.3",
|
|
37
39
|
"@langchain/core": "^0.3.56",
|
|
38
40
|
"@langchain/google-vertexai": "^0.2.9",
|
|
39
41
|
"@langchain/openai": "^0.5.10",
|
|
@@ -52,9 +54,9 @@
|
|
|
52
54
|
"zod": "^3.24.4"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
57
|
+
"@types/react": "^19.1.6",
|
|
55
58
|
"ajv": "^8.17.1",
|
|
56
59
|
"lodash": "^4.17.21",
|
|
57
|
-
"react": "^19.1.0"
|
|
58
|
-
"@types/react": "^19.1.6"
|
|
60
|
+
"react": "^19.1.0"
|
|
59
61
|
}
|
|
60
62
|
}
|
package/src/analyze/index.js
CHANGED
|
@@ -13,12 +13,13 @@ const { analyzeTsFiles } = require('./typescript');
|
|
|
13
13
|
const { analyzePythonFile } = require('./python');
|
|
14
14
|
const { analyzeRubyFile, prebuildConstantMaps } = require('./ruby');
|
|
15
15
|
const { analyzeGoFile } = require('./go');
|
|
16
|
+
const { analyzeSwiftFile } = require('./swift');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Analyzes a single file for analytics tracking calls
|
|
19
|
-
*
|
|
20
|
+
*
|
|
20
21
|
* Note: typescript files are handled separately by analyzeTsFiles, which is a batch processor
|
|
21
|
-
*
|
|
22
|
+
*
|
|
22
23
|
* @param {string} file - Path to the file to analyze
|
|
23
24
|
* @param {Array<string>} customFunctionSignatures - Custom function signatures to detect
|
|
24
25
|
* @returns {Promise<Array<Object>>} Array of events found in the file
|
|
@@ -28,19 +29,20 @@ async function analyzeFile(file, customFunctionSignatures) {
|
|
|
28
29
|
if (/\.py$/.test(file)) return analyzePythonFile(file, customFunctionSignatures)
|
|
29
30
|
if (/\.rb$/.test(file)) return analyzeRubyFile(file, customFunctionSignatures)
|
|
30
31
|
if (/\.go$/.test(file)) return analyzeGoFile(file, customFunctionSignatures)
|
|
32
|
+
if (/\.swift$/.test(file)) return analyzeSwiftFile(file, customFunctionSignatures)
|
|
31
33
|
return []
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
/**
|
|
35
37
|
* Adds an event to the events collection, merging properties if event already exists
|
|
36
|
-
*
|
|
38
|
+
*
|
|
37
39
|
* @param {Object} allEvents - Collection of all events
|
|
38
40
|
* @param {Object} event - Event to add
|
|
39
41
|
* @param {string} baseDir - Base directory for relative path calculation
|
|
40
42
|
*/
|
|
41
43
|
function addEventToCollection(allEvents, event, baseDir) {
|
|
42
44
|
const relativeFilePath = path.relative(baseDir, event.filePath);
|
|
43
|
-
|
|
45
|
+
|
|
44
46
|
const implementation = {
|
|
45
47
|
path: relativeFilePath,
|
|
46
48
|
line: event.line,
|
|
@@ -64,12 +66,12 @@ function addEventToCollection(allEvents, event, baseDir) {
|
|
|
64
66
|
|
|
65
67
|
/**
|
|
66
68
|
* Processes all files that are not TypeScript files in parallel
|
|
67
|
-
*
|
|
69
|
+
*
|
|
68
70
|
* Checks the system's file descriptor limit and uses 80% of it to avoid running out of file descriptors
|
|
69
71
|
* Creates a promise pool and launches one analysis for each file in parallel
|
|
70
72
|
* When a slot frees up, the next file is launched
|
|
71
73
|
* Waits for the remaining work to complete
|
|
72
|
-
*
|
|
74
|
+
*
|
|
73
75
|
* @param {Array<string>} files - Array of file paths
|
|
74
76
|
* @param {Object} allEvents - Collection to add events to
|
|
75
77
|
* @param {string} baseDir - Base directory for relative paths
|
|
@@ -121,10 +123,10 @@ async function processFiles(files, allEvents, baseDir, customFunctionSignatures)
|
|
|
121
123
|
|
|
122
124
|
/**
|
|
123
125
|
* Analyze a directory recursively for analytics tracking calls
|
|
124
|
-
*
|
|
126
|
+
*
|
|
125
127
|
* This function scans all supported files in a directory tree and identifies analytics tracking calls,
|
|
126
128
|
* handling different file types appropriately.
|
|
127
|
-
*
|
|
129
|
+
*
|
|
128
130
|
* @param {string} dirPath - Path to the directory to analyze
|
|
129
131
|
* @param {Array<string>} [customFunctions=null] - Array of custom tracking function signatures to detect
|
|
130
132
|
* @returns {Promise<Object>} Object mapping event names to their tracking implementations
|
|
@@ -132,17 +134,22 @@ async function processFiles(files, allEvents, baseDir, customFunctionSignatures)
|
|
|
132
134
|
async function analyzeDirectory(dirPath, customFunctions) {
|
|
133
135
|
const allEvents = {};
|
|
134
136
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
137
|
+
let customFunctionSignatures = null;
|
|
138
|
+
if (Array.isArray(customFunctions) && customFunctions.length > 0) {
|
|
139
|
+
customFunctionSignatures = customFunctions.map(cf => {
|
|
140
|
+
if (cf && typeof cf === 'object' && cf.functionName) return cf;
|
|
141
|
+
if (typeof cf === 'string') return parseCustomFunctionSignature(cf);
|
|
142
|
+
return null;
|
|
143
|
+
}).filter(Boolean);
|
|
144
|
+
}
|
|
138
145
|
|
|
139
146
|
const files = getAllFiles(dirPath);
|
|
140
|
-
|
|
147
|
+
|
|
141
148
|
// Separate TypeScript files from others for optimized processing
|
|
142
149
|
const tsFiles = [];
|
|
143
150
|
const nonTsFiles = [];
|
|
144
151
|
const rubyFiles = [];
|
|
145
|
-
|
|
152
|
+
|
|
146
153
|
for (const file of files) {
|
|
147
154
|
const isTsFile = /\.(tsx?)$/.test(file);
|
|
148
155
|
if (isTsFile) {
|
|
@@ -45,20 +45,23 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
45
45
|
function isCustomFunction(node, customFunction) {
|
|
46
46
|
if (!customFunction) return false;
|
|
47
47
|
|
|
48
|
-
// Support dot-separated names like "CustomModule.track"
|
|
49
|
-
|
|
48
|
+
// Support dot-separated names like "CustomModule.track" and chained calls like "getTrackingService().track"
|
|
49
|
+
// Normalize each segment by stripping trailing parentheses
|
|
50
|
+
const parts = customFunction.split('.').map(p => p.replace(/\(\s*\)$/, ''));
|
|
50
51
|
|
|
51
52
|
// Simple identifier (no dot)
|
|
52
53
|
if (parts.length === 1) {
|
|
53
|
-
return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name ===
|
|
54
|
+
return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name === parts[0];
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// For dot-separated names, the callee should be a MemberExpression chain
|
|
57
|
-
|
|
57
|
+
// For dot-separated names, the callee should be a MemberExpression chain,
|
|
58
|
+
// but we also allow CallExpression in the chain (e.g., getService().track)
|
|
59
|
+
const callee = node.callee;
|
|
60
|
+
if (callee.type !== NODE_TYPES.MEMBER_EXPRESSION && callee.type !== NODE_TYPES.CALL_EXPRESSION) {
|
|
58
61
|
return false;
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
return matchesMemberChain(
|
|
64
|
+
return matchesMemberChain(callee, parts);
|
|
62
65
|
}
|
|
63
66
|
|
|
64
67
|
/**
|
|
@@ -75,9 +78,8 @@ function matchesMemberChain(memberExpr, parts) {
|
|
|
75
78
|
while (currentNode && idx >= 0) {
|
|
76
79
|
const expectedPart = parts[idx];
|
|
77
80
|
|
|
78
|
-
// property should match current expectedPart
|
|
79
81
|
if (currentNode.type === NODE_TYPES.MEMBER_EXPRESSION) {
|
|
80
|
-
// Ensure property is Identifier and matches
|
|
82
|
+
// Ensure property is Identifier and matches the expected part
|
|
81
83
|
if (
|
|
82
84
|
currentNode.property.type !== NODE_TYPES.IDENTIFIER ||
|
|
83
85
|
currentNode.property.name !== expectedPart
|
|
@@ -85,16 +87,25 @@ function matchesMemberChain(memberExpr, parts) {
|
|
|
85
87
|
return false;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
|
-
// Move to the object
|
|
90
|
+
// Move to the object (which could itself be a MemberExpression, Identifier, or CallExpression)
|
|
89
91
|
currentNode = currentNode.object;
|
|
90
92
|
idx -= 1;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// If we encounter a CallExpression in the chain (e.g., getService().track),
|
|
97
|
+
// step into its callee without consuming an expected part.
|
|
98
|
+
if (currentNode.type === NODE_TYPES.CALL_EXPRESSION) {
|
|
99
|
+
currentNode = currentNode.callee;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (currentNode.type === NODE_TYPES.IDENTIFIER) {
|
|
93
104
|
return idx === 0 && currentNode.name === expectedPart;
|
|
94
|
-
} else {
|
|
95
|
-
// Unexpected node type (e.g., ThisExpression, CallExpression, etc.)
|
|
96
|
-
return false;
|
|
97
105
|
}
|
|
106
|
+
|
|
107
|
+
// Unexpected node type (e.g., ThisExpression, Literal, etc.)
|
|
108
|
+
return false;
|
|
98
109
|
}
|
|
99
110
|
|
|
100
111
|
return false;
|
|
@@ -257,7 +257,11 @@ function getStringValue(node, constantMap = {}) {
|
|
|
257
257
|
return node.value;
|
|
258
258
|
}
|
|
259
259
|
if (node.type === NODE_TYPES.MEMBER_EXPRESSION) {
|
|
260
|
-
|
|
260
|
+
const resolved = resolveMemberExpressionToString(node, constantMap);
|
|
261
|
+
if (resolved) return resolved;
|
|
262
|
+
// Fallback: return a dotted path for member expressions when we cannot
|
|
263
|
+
// resolve to a literal (e.g., imported constants like TELEMETRY_EVENTS.X)
|
|
264
|
+
return memberExpressionToPath(node);
|
|
261
265
|
}
|
|
262
266
|
return null;
|
|
263
267
|
}
|
|
@@ -315,6 +319,25 @@ function resolveMemberExpressionToString(node, constantMap) {
|
|
|
315
319
|
return null;
|
|
316
320
|
}
|
|
317
321
|
|
|
322
|
+
// Build a dotted path string for a MemberExpression (e.g., OBJ.KEY.SUBKEY)
|
|
323
|
+
function memberExpressionToPath(node) {
|
|
324
|
+
if (!node || node.type !== NODE_TYPES.MEMBER_EXPRESSION) return null;
|
|
325
|
+
const parts = [];
|
|
326
|
+
let current = node;
|
|
327
|
+
while (current && current.type === NODE_TYPES.MEMBER_EXPRESSION && !current.computed) {
|
|
328
|
+
if (current.property && current.property.type === NODE_TYPES.IDENTIFIER) {
|
|
329
|
+
parts.unshift(current.property.name);
|
|
330
|
+
} else if (current.property && current.property.type === NODE_TYPES.LITERAL) {
|
|
331
|
+
parts.unshift(String(current.property.value));
|
|
332
|
+
}
|
|
333
|
+
current = current.object;
|
|
334
|
+
}
|
|
335
|
+
if (current && current.type === NODE_TYPES.IDENTIFIER) {
|
|
336
|
+
parts.unshift(current.name);
|
|
337
|
+
}
|
|
338
|
+
return parts.length ? parts.join('.') : null;
|
|
339
|
+
}
|
|
340
|
+
|
|
318
341
|
module.exports = {
|
|
319
342
|
extractEventData,
|
|
320
343
|
processEventData
|
|
@@ -12,6 +12,7 @@ const { PARSER_OPTIONS, NODE_TYPES } = require('./constants');
|
|
|
12
12
|
const { detectAnalyticsSource } = require('./detectors');
|
|
13
13
|
const { extractEventData, processEventData } = require('./extractors');
|
|
14
14
|
const { findWrappingFunction } = require('./utils/function-finder');
|
|
15
|
+
const { collectImportedConstantStringMap } = require('./utils/import-resolver');
|
|
15
16
|
|
|
16
17
|
// Extend walker to support JSX
|
|
17
18
|
extend(walk.base);
|
|
@@ -82,19 +83,15 @@ function parseFile(filePath) {
|
|
|
82
83
|
function nodeMatchesCustomFunction(node, fnName) {
|
|
83
84
|
if (!fnName || !node.callee) return false;
|
|
84
85
|
|
|
85
|
-
|
|
86
|
+
// Support chained calls in function name by stripping trailing parens from each segment
|
|
87
|
+
const parts = fnName.split('.').map(p => p.replace(/\(\s*\)$/, ''));
|
|
86
88
|
|
|
87
89
|
// Simple identifier case
|
|
88
90
|
if (parts.length === 1) {
|
|
89
|
-
return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name ===
|
|
91
|
+
return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name === parts[0];
|
|
90
92
|
}
|
|
91
93
|
|
|
92
|
-
//
|
|
93
|
-
if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// Walk the chain from the right-most property to the leftmost object
|
|
94
|
+
// Allow MemberExpression and CallExpression within the chain (e.g., getService().track)
|
|
98
95
|
let currentNode = node.callee;
|
|
99
96
|
let idx = parts.length - 1;
|
|
100
97
|
|
|
@@ -108,13 +105,23 @@ function nodeMatchesCustomFunction(node, fnName) {
|
|
|
108
105
|
) {
|
|
109
106
|
return false;
|
|
110
107
|
}
|
|
108
|
+
// step to the object; do not decrement idx for call expressions yet
|
|
111
109
|
currentNode = currentNode.object;
|
|
112
110
|
idx -= 1;
|
|
113
|
-
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (currentNode.type === NODE_TYPES.CALL_EXPRESSION) {
|
|
115
|
+
// descend into the callee of the call without consuming a part
|
|
116
|
+
currentNode = currentNode.callee;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (currentNode.type === NODE_TYPES.IDENTIFIER) {
|
|
114
121
|
return idx === 0 && currentNode.name === expected;
|
|
115
|
-
} else {
|
|
116
|
-
return false;
|
|
117
122
|
}
|
|
123
|
+
|
|
124
|
+
return false;
|
|
118
125
|
}
|
|
119
126
|
|
|
120
127
|
return false;
|
|
@@ -183,8 +190,11 @@ function collectConstantStringMap(ast) {
|
|
|
183
190
|
function findTrackingEvents(ast, filePath, customConfigs = []) {
|
|
184
191
|
const events = [];
|
|
185
192
|
|
|
186
|
-
// Collect constant mappings once per file
|
|
187
|
-
const constantMap =
|
|
193
|
+
// Collect constant mappings once per file (locals + imported)
|
|
194
|
+
const constantMap = {
|
|
195
|
+
...collectConstantStringMap(ast),
|
|
196
|
+
...collectImportedConstantStringMap(filePath, ast)
|
|
197
|
+
};
|
|
188
198
|
|
|
189
199
|
walk.ancestor(ast, {
|
|
190
200
|
[NODE_TYPES.CALL_EXPRESSION]: (node, ancestors) => {
|