@flisk/analyze-tracking 0.8.8 → 0.9.1
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 +120 -19
- package/package.json +5 -3
- package/schema.json +10 -0
- package/src/analyze/index.js +20 -13
- package/src/analyze/javascript/detectors/analytics-source.js +38 -4
- package/src/analyze/javascript/extractors/event-extractor.js +29 -12
- package/src/analyze/javascript/parser.js +26 -8
- 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 +37 -3
- package/src/analyze/typescript/extractors/event-extractor.js +59 -31
- package/src/analyze/typescript/extractors/property-extractor.js +129 -91
- package/src/analyze/typescript/parser.js +27 -8
- package/src/analyze/typescript/utils/type-resolver.js +600 -21
- package/src/analyze/utils/customFunctionParser.js +29 -0
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ npx @flisk/analyze-tracking /path/to/project [options]
|
|
|
39
39
|
|
|
40
40
|
If you have your own in-house tracker or a wrapper function that calls other tracking libraries, you can specify the function signature with the `-c` or `--customFunction` option.
|
|
41
41
|
|
|
42
|
+
#### Standard Custom Function Format
|
|
43
|
+
|
|
42
44
|
Your function signature should be in the following format:
|
|
43
45
|
```js
|
|
44
46
|
yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES, customFieldOne, customFieldTwo)
|
|
@@ -57,11 +59,45 @@ yourCustomTrackFunctionName(userId, EVENT_NAME, PROPERTIES)
|
|
|
57
59
|
|
|
58
60
|
If your function follows the standard format `yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES)`, you can simply pass in `yourCustomTrackFunctionName` to `--customFunction` as a shorthand.
|
|
59
61
|
|
|
62
|
+
#### Method-Name-as-Event Format
|
|
63
|
+
|
|
64
|
+
For tracking patterns where the method name itself is the event name (e.g., `yourClass.yourEventName({...})`), use the special `EVENT_NAME` placeholder in the method position:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
yourClass.EVENT_NAME(PROPERTIES)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This pattern tells the analyzer that:
|
|
71
|
+
- `yourClass` is the object name to match
|
|
72
|
+
- The method name after the dot (e.g., `viewItemList`, `addToCart`) is the event name
|
|
73
|
+
- `PROPERTIES` is the properties object (defaults to the first argument if not specified)
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
```typescript
|
|
77
|
+
// Code in your project:
|
|
78
|
+
yourClass.viewItemList({ items: [...] });
|
|
79
|
+
yourClass.addToCart({ item: {...}, value: 100 });
|
|
80
|
+
yourClass.purchase({ userId: '123', value: 100 });
|
|
81
|
+
|
|
82
|
+
// Command:
|
|
83
|
+
npx @flisk/analyze-tracking /path/to/project --customFunction "yourClass.EVENT_NAME(PROPERTIES)"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This will detect:
|
|
87
|
+
- Event: `viewItemList` with properties from the first argument
|
|
88
|
+
- Event: `addToCart` with properties from the first argument
|
|
89
|
+
- Event: `purchase` with properties from the first argument
|
|
90
|
+
|
|
91
|
+
_**Note:** This pattern is currently only supported for JavaScript and TypeScript code._
|
|
92
|
+
|
|
93
|
+
#### Multiple Custom Functions
|
|
94
|
+
|
|
60
95
|
You can also pass in multiple custom function signatures by passing in the `--customFunction` option multiple times or by passing in a space-separated list of function signatures.
|
|
61
96
|
|
|
62
97
|
```sh
|
|
63
98
|
npx @flisk/analyze-tracking /path/to/project --customFunction "yourFunc1" --customFunction "yourFunc2(userId, EVENT_NAME, PROPERTIES)"
|
|
64
99
|
npx @flisk/analyze-tracking /path/to/project -c "yourFunc1" "yourFunc2(userId, EVENT_NAME, PROPERTIES)"
|
|
100
|
+
npx @flisk/analyze-tracking /path/to/project -c "yourClass.EVENT_NAME(PROPERTIES)" "customTrack(EVENT_NAME, PROPERTIES)"
|
|
65
101
|
```
|
|
66
102
|
|
|
67
103
|
|
|
@@ -95,25 +131,25 @@ Use this to understand where your events live in the code and how they're being
|
|
|
95
131
|
Your LLM of choice is used for generating descriptions of events, properties, and implementations.
|
|
96
132
|
|
|
97
133
|
See [schema.json](schema.json) for a JSON Schema of the output.
|
|
98
|
-
|
|
134
|
+
|
|
99
135
|
|
|
100
136
|
## Supported tracking libraries & languages
|
|
101
137
|
|
|
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 | ✅ | ✅ | ✅ | ✅ |
|
|
138
|
+
| Library | JavaScript/TypeScript | Python | Ruby | Go | Swift |
|
|
139
|
+
|---------|:---------------------:|:------:|:----:|:--:|:--:|
|
|
140
|
+
| Google Analytics | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
141
|
+
| Google Tag Manager | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
142
|
+
| Segment | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
143
|
+
| Mixpanel | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
144
|
+
| Amplitude | ✅ | ✅ | ❌ | ✅ | ✅ |
|
|
145
|
+
| Rudderstack | ✅ | ✅ | ✳️ | ✳️ | ✅ |
|
|
146
|
+
| mParticle | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
147
|
+
| PostHog | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
148
|
+
| Pendo | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
149
|
+
| Heap | ✅ | ❌ | ❌ | ❌ | ✅ |
|
|
150
|
+
| Snowplow | ✅ | ✅ | ✅ | ✅ | ❌ |
|
|
151
|
+
| Datadog RUM | ✅ | ❌ | ❌ | ❌ | ❌ |
|
|
152
|
+
| Custom Function | ✅ | ✅ | ✅ | ✅ | ✅ |
|
|
117
153
|
|
|
118
154
|
✳️ Rudderstack's SDKs often use the same format as Segment, so Rudderstack events may be detected as Segment events.
|
|
119
155
|
|
|
@@ -129,6 +165,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
129
165
|
'<property_name>': '<property_value>'
|
|
130
166
|
});
|
|
131
167
|
```
|
|
168
|
+
|
|
169
|
+
**Swift**
|
|
170
|
+
```swift
|
|
171
|
+
Analytics.logEvent("<event_name>", parameters: [
|
|
172
|
+
"<property_name>": "<property_value>"
|
|
173
|
+
])
|
|
174
|
+
```
|
|
132
175
|
</details>
|
|
133
176
|
|
|
134
177
|
<details>
|
|
@@ -147,6 +190,11 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
147
190
|
'<property_name>': '<property_value>'
|
|
148
191
|
});
|
|
149
192
|
```
|
|
193
|
+
|
|
194
|
+
**Swift**
|
|
195
|
+
```swift
|
|
196
|
+
dataLayer.push(["event": "<event_name>", "<property_name>": "<property_value>"])
|
|
197
|
+
```
|
|
150
198
|
</details>
|
|
151
199
|
|
|
152
200
|
<details>
|
|
@@ -185,6 +233,11 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
185
233
|
Set("<property_name>", "<property_value>"),
|
|
186
234
|
})
|
|
187
235
|
```
|
|
236
|
+
|
|
237
|
+
**Swift**
|
|
238
|
+
```swift
|
|
239
|
+
analytics.track(name: "<event_name>", properties: TrackProperties("<property_name>": "<property_value>"))
|
|
240
|
+
```
|
|
188
241
|
</details>
|
|
189
242
|
|
|
190
243
|
<details>
|
|
@@ -221,6 +274,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
221
274
|
}),
|
|
222
275
|
})
|
|
223
276
|
```
|
|
277
|
+
|
|
278
|
+
**Swift**
|
|
279
|
+
```swift
|
|
280
|
+
Mixpanel.mainInstance().track(event: "<event_name>", properties: [
|
|
281
|
+
"<property_name>": "<property_value>"
|
|
282
|
+
])
|
|
283
|
+
```
|
|
224
284
|
</details>
|
|
225
285
|
|
|
226
286
|
<details>
|
|
@@ -256,6 +316,14 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
256
316
|
},
|
|
257
317
|
})
|
|
258
318
|
```
|
|
319
|
+
|
|
320
|
+
**Swift**
|
|
321
|
+
```swift
|
|
322
|
+
amplitude.track(
|
|
323
|
+
eventType: "<event_name>",
|
|
324
|
+
eventProperties: ["<property_name>": "<property_value>"]
|
|
325
|
+
)
|
|
326
|
+
```
|
|
259
327
|
</details>
|
|
260
328
|
|
|
261
329
|
<details>
|
|
@@ -295,6 +363,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
295
363
|
Set("<property_name>", "<property_value>"),
|
|
296
364
|
})
|
|
297
365
|
```
|
|
366
|
+
|
|
367
|
+
**Swift**
|
|
368
|
+
```swift
|
|
369
|
+
RSClient.sharedInstance()?.track("<event_name>", properties: [
|
|
370
|
+
"<property_name>": "<property_value>"
|
|
371
|
+
])
|
|
372
|
+
```
|
|
298
373
|
</details>
|
|
299
374
|
|
|
300
375
|
<details>
|
|
@@ -306,6 +381,15 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
306
381
|
'<property_name>': '<property_value>'
|
|
307
382
|
});
|
|
308
383
|
```
|
|
384
|
+
|
|
385
|
+
**Swift**
|
|
386
|
+
```swift
|
|
387
|
+
let event = MPEvent(name: "<event_name>", type: .other)
|
|
388
|
+
event.customAttributes = [
|
|
389
|
+
"<property_name>": "<property_value>"
|
|
390
|
+
]
|
|
391
|
+
MParticle.sharedInstance().logEvent(event)
|
|
392
|
+
```
|
|
309
393
|
</details>
|
|
310
394
|
|
|
311
395
|
<details>
|
|
@@ -353,6 +437,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
353
437
|
Set("<property_name>", "<property_value>"),
|
|
354
438
|
})
|
|
355
439
|
```
|
|
440
|
+
|
|
441
|
+
**Swift**
|
|
442
|
+
```swift
|
|
443
|
+
PostHogSDK.shared.capture("<event_name>", properties: [
|
|
444
|
+
"<property_name>": "<property_value>"
|
|
445
|
+
])
|
|
446
|
+
```
|
|
356
447
|
</details>
|
|
357
448
|
|
|
358
449
|
<details>
|
|
@@ -372,7 +463,12 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
372
463
|
})
|
|
373
464
|
```
|
|
374
465
|
|
|
375
|
-
|
|
466
|
+
**Swift**
|
|
467
|
+
```swift
|
|
468
|
+
PendoManager.shared().track("<event_name>", properties: [
|
|
469
|
+
"<property_name>": "<property_value>"
|
|
470
|
+
])
|
|
471
|
+
```
|
|
376
472
|
</details>
|
|
377
473
|
|
|
378
474
|
<details>
|
|
@@ -392,7 +488,12 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
392
488
|
})
|
|
393
489
|
```
|
|
394
490
|
|
|
395
|
-
|
|
491
|
+
**Swift**
|
|
492
|
+
```swift
|
|
493
|
+
Heap.shared.track("<event_name>", properties: [
|
|
494
|
+
"<property_name>": "<property_value>"
|
|
495
|
+
])
|
|
496
|
+
```
|
|
396
497
|
</details>
|
|
397
498
|
|
|
398
499
|
<details>
|
|
@@ -403,7 +504,7 @@ See [schema.json](schema.json) for a JSON Schema of the output.
|
|
|
403
504
|
datadogRum.addAction('<event_name>', {
|
|
404
505
|
'<property_name>': '<property_value>'
|
|
405
506
|
});
|
|
406
|
-
|
|
507
|
+
|
|
407
508
|
// Or via window
|
|
408
509
|
window.DD_RUM.addAction('<event_name>', {
|
|
409
510
|
'<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.1",
|
|
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/schema.json
CHANGED
|
@@ -142,6 +142,16 @@
|
|
|
142
142
|
"items": {
|
|
143
143
|
"$ref": "#/definitions/property",
|
|
144
144
|
"description": "Schema for array items when type is 'array'"
|
|
145
|
+
},
|
|
146
|
+
"values": {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"items": {
|
|
149
|
+
"oneOf": [
|
|
150
|
+
{ "type": "string" },
|
|
151
|
+
{ "type": "number" }
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"description": "Possible values when type is 'enum'"
|
|
145
155
|
}
|
|
146
156
|
},
|
|
147
157
|
"required": [
|
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) {
|
|
@@ -8,16 +8,25 @@ const { ANALYTICS_PROVIDERS, NODE_TYPES } = require('../constants');
|
|
|
8
8
|
/**
|
|
9
9
|
* Detects the analytics provider from a CallExpression node
|
|
10
10
|
* @param {Object} node - AST CallExpression node
|
|
11
|
-
* @param {string} [
|
|
11
|
+
* @param {string|Object} [customFunctionOrConfig] - Custom function name string or custom config object
|
|
12
12
|
* @returns {string} The detected analytics source or 'unknown'
|
|
13
13
|
*/
|
|
14
|
-
function detectAnalyticsSource(node,
|
|
14
|
+
function detectAnalyticsSource(node, customFunctionOrConfig) {
|
|
15
15
|
if (!node.callee) {
|
|
16
16
|
return 'unknown';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Check for custom function first
|
|
20
|
-
|
|
20
|
+
// Support both old string format and new config object format
|
|
21
|
+
const customConfig = typeof customFunctionOrConfig === 'object' ? customFunctionOrConfig : null;
|
|
22
|
+
const customFunction = typeof customFunctionOrConfig === 'string' ? customFunctionOrConfig : (customConfig?.functionName);
|
|
23
|
+
|
|
24
|
+
if (customConfig?.isMethodAsEvent) {
|
|
25
|
+
// Method-as-event pattern: match any method on the specified object
|
|
26
|
+
if (isMethodAsEventFunction(node, customConfig)) {
|
|
27
|
+
return 'custom';
|
|
28
|
+
}
|
|
29
|
+
} else if (customFunction && isCustomFunction(node, customFunction)) {
|
|
21
30
|
return 'custom';
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -36,6 +45,31 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
36
45
|
return 'unknown';
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Checks if the node matches a method-as-event custom function pattern
|
|
50
|
+
* @param {Object} node - AST CallExpression node
|
|
51
|
+
* @param {Object} customConfig - Custom function configuration with isMethodAsEvent: true
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isMethodAsEventFunction(node, customConfig) {
|
|
55
|
+
if (!customConfig?.isMethodAsEvent || !customConfig?.objectName) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Must be a MemberExpression: objectName.methodName(...)
|
|
60
|
+
if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The object part must match the configured objectName
|
|
65
|
+
const objectNode = node.callee.object;
|
|
66
|
+
if (objectNode.type !== NODE_TYPES.IDENTIFIER) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return objectNode.name === customConfig.objectName;
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
/**
|
|
40
74
|
* Checks if the node is a custom function call
|
|
41
75
|
* @param {Object} node - AST CallExpression node
|
|
@@ -122,7 +156,7 @@ function detectFunctionBasedProvider(node) {
|
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
const functionName = node.callee.name;
|
|
125
|
-
|
|
159
|
+
|
|
126
160
|
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
|
|
127
161
|
if (provider.type === 'function' && provider.functionName === functionName) {
|
|
128
162
|
return provider.name;
|
|
@@ -72,15 +72,15 @@ function extractSnowplowEvent(node, constantMap) {
|
|
|
72
72
|
|
|
73
73
|
// tracker.track(buildStructEvent({ action: 'event_name', ... }))
|
|
74
74
|
const firstArg = node.arguments[0];
|
|
75
|
-
|
|
76
|
-
if (firstArg.type === NODE_TYPES.CALL_EXPRESSION &&
|
|
75
|
+
|
|
76
|
+
if (firstArg.type === NODE_TYPES.CALL_EXPRESSION &&
|
|
77
77
|
firstArg.arguments.length > 0) {
|
|
78
78
|
const structEventArg = firstArg.arguments[0];
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
if (structEventArg.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
81
81
|
const actionProperty = findPropertyByKey(structEventArg, 'action');
|
|
82
82
|
const eventName = actionProperty ? getStringValue(actionProperty.value, constantMap) : null;
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
return { eventName, propertiesNode: structEventArg };
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -119,7 +119,7 @@ function extractGTMEvent(node, constantMap) {
|
|
|
119
119
|
|
|
120
120
|
// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
|
|
121
121
|
const firstArg = node.arguments[0];
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
if (firstArg.type !== NODE_TYPES.OBJECT_EXPRESSION) {
|
|
124
124
|
return { eventName: null, propertiesNode: null };
|
|
125
125
|
}
|
|
@@ -131,11 +131,11 @@ function extractGTMEvent(node, constantMap) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const eventName = getStringValue(eventProperty.value, constantMap);
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
// Create a modified properties node without the 'event' property
|
|
136
136
|
const modifiedPropertiesNode = {
|
|
137
137
|
...firstArg,
|
|
138
|
-
properties: firstArg.properties.filter(prop =>
|
|
138
|
+
properties: firstArg.properties.filter(prop =>
|
|
139
139
|
prop.key && (prop.key.name !== 'event' && prop.key.value !== 'event')
|
|
140
140
|
)
|
|
141
141
|
};
|
|
@@ -171,10 +171,27 @@ function extractDefaultEvent(node, constantMap) {
|
|
|
171
171
|
function extractCustomEvent(node, constantMap, customConfig) {
|
|
172
172
|
const args = node.arguments || [];
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
let eventName;
|
|
175
|
+
let propertiesArg;
|
|
176
|
+
|
|
177
|
+
if (customConfig?.isMethodAsEvent) {
|
|
178
|
+
// Method-as-event pattern: event name comes from the method name
|
|
179
|
+
if (node.callee.type === NODE_TYPES.MEMBER_EXPRESSION &&
|
|
180
|
+
node.callee.property.type === NODE_TYPES.IDENTIFIER) {
|
|
181
|
+
eventName = node.callee.property.name;
|
|
182
|
+
} else {
|
|
183
|
+
// Fallback: could not extract method name
|
|
184
|
+
eventName = null;
|
|
185
|
+
}
|
|
176
186
|
|
|
177
|
-
|
|
187
|
+
// Properties are at the configured index (default 0)
|
|
188
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 0];
|
|
189
|
+
} else {
|
|
190
|
+
// Standard custom function pattern: event name comes from argument
|
|
191
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
192
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
193
|
+
eventName = getStringValue(eventArg, constantMap);
|
|
194
|
+
}
|
|
178
195
|
|
|
179
196
|
const extraArgs = {};
|
|
180
197
|
if (customConfig && customConfig.extraParams) {
|
|
@@ -274,8 +291,8 @@ function getStringValue(node, constantMap = {}) {
|
|
|
274
291
|
*/
|
|
275
292
|
function findPropertyByKey(objectNode, key) {
|
|
276
293
|
if (!objectNode.properties) return null;
|
|
277
|
-
|
|
278
|
-
return objectNode.properties.find(prop =>
|
|
294
|
+
|
|
295
|
+
return objectNode.properties.find(prop =>
|
|
279
296
|
prop.key && (prop.key.name === key || prop.key.value === key)
|
|
280
297
|
);
|
|
281
298
|
}
|
|
@@ -53,7 +53,7 @@ class ParseError extends Error {
|
|
|
53
53
|
*/
|
|
54
54
|
function parseFile(filePath) {
|
|
55
55
|
let code;
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
try {
|
|
58
58
|
code = fs.readFileSync(filePath, 'utf8');
|
|
59
59
|
} catch (error) {
|
|
@@ -72,16 +72,33 @@ function parseFile(filePath) {
|
|
|
72
72
|
// ---------------------------------------------
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Determines whether a CallExpression node matches the provided custom function
|
|
76
|
-
* Supports both simple identifiers (e.g. myTrack)
|
|
75
|
+
* Determines whether a CallExpression node matches the provided custom function configuration.
|
|
76
|
+
* Supports both simple identifiers (e.g. myTrack), dot-separated members (e.g. Custom.track),
|
|
77
|
+
* and method-as-event patterns (e.g. eventCalls.EVENT_NAME).
|
|
77
78
|
* The logic mirrors isCustomFunction from detectors/analytics-source.js but is kept local to avoid
|
|
78
79
|
* circular dependencies.
|
|
79
80
|
* @param {Object} node – CallExpression AST node
|
|
80
|
-
* @param {
|
|
81
|
+
* @param {Object} customConfig – Custom function configuration object
|
|
81
82
|
* @returns {boolean}
|
|
82
83
|
*/
|
|
83
|
-
function nodeMatchesCustomFunction(node,
|
|
84
|
-
if (!
|
|
84
|
+
function nodeMatchesCustomFunction(node, customConfig) {
|
|
85
|
+
if (!customConfig || !node.callee) return false;
|
|
86
|
+
|
|
87
|
+
// Handle method-as-event pattern
|
|
88
|
+
if (customConfig.isMethodAsEvent && customConfig.objectName) {
|
|
89
|
+
if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const objectNode = node.callee.object;
|
|
93
|
+
if (objectNode.type !== NODE_TYPES.IDENTIFIER) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return objectNode.name === customConfig.objectName;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle standard custom function patterns
|
|
100
|
+
const fnName = customConfig.functionName;
|
|
101
|
+
if (!fnName) return false;
|
|
85
102
|
|
|
86
103
|
// Support chained calls in function name by stripping trailing parens from each segment
|
|
87
104
|
const parts = fnName.split('.').map(p => p.replace(/\(\s*\)$/, ''));
|
|
@@ -204,7 +221,7 @@ function findTrackingEvents(ast, filePath, customConfigs = []) {
|
|
|
204
221
|
// Attempt to match any custom function first to avoid mis-classifying built-in providers
|
|
205
222
|
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
206
223
|
for (const cfg of customConfigs) {
|
|
207
|
-
if (cfg && nodeMatchesCustomFunction(node, cfg
|
|
224
|
+
if (cfg && nodeMatchesCustomFunction(node, cfg)) {
|
|
208
225
|
matchedCustomConfig = cfg;
|
|
209
226
|
break;
|
|
210
227
|
}
|
|
@@ -237,7 +254,8 @@ function findTrackingEvents(ast, filePath, customConfigs = []) {
|
|
|
237
254
|
* @returns {Object|null} Extracted event or null
|
|
238
255
|
*/
|
|
239
256
|
function extractTrackingEvent(node, ancestors, filePath, constantMap, customConfig) {
|
|
240
|
-
|
|
257
|
+
// Pass the full customConfig object (not just functionName) to support method-as-event patterns
|
|
258
|
+
const source = detectAnalyticsSource(node, customConfig || null);
|
|
241
259
|
if (source === 'unknown') {
|
|
242
260
|
return null;
|
|
243
261
|
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Constants and helper-return collectors for Swift fixtures
|
|
3
|
+
* - Scans directory for top-level lets and enum/struct static lets
|
|
4
|
+
* - Extracts simple function returns for dict/array builders
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
function buildCrossFileConstMap(dir) {
|
|
11
|
+
const map = {};
|
|
12
|
+
try {
|
|
13
|
+
const entries = fs.readdirSync(dir).filter(f => f.endsWith('.swift'));
|
|
14
|
+
for (const f of entries) {
|
|
15
|
+
const fp = path.join(dir, f);
|
|
16
|
+
const content = fs.readFileSync(fp, 'utf8');
|
|
17
|
+
// Top-level: let NAME = "..."
|
|
18
|
+
for (const m of content.matchAll(/\blet\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
|
|
19
|
+
map[m[1]] = m[2];
|
|
20
|
+
}
|
|
21
|
+
// Enum/struct blocks: capture namespace and all static lets inside
|
|
22
|
+
let idx = 0;
|
|
23
|
+
while (idx < content.length) {
|
|
24
|
+
const head = content.slice(idx);
|
|
25
|
+
const mm = /\b(enum|struct)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/m.exec(head);
|
|
26
|
+
if (!mm) break;
|
|
27
|
+
const ns = mm[2];
|
|
28
|
+
const blockStart = idx + mm.index + mm[0].length - 1; // position at '{'
|
|
29
|
+
// Find matching closing brace
|
|
30
|
+
let depth = 0; let end = -1;
|
|
31
|
+
for (let i = blockStart; i < content.length; i++) {
|
|
32
|
+
const ch = content[i];
|
|
33
|
+
if (ch === '{') depth++;
|
|
34
|
+
else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
|
|
35
|
+
}
|
|
36
|
+
if (end === -1) break;
|
|
37
|
+
const block = content.slice(blockStart + 1, end);
|
|
38
|
+
for (const sm of block.matchAll(/\bstatic\s+let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
|
|
39
|
+
const key = sm[1];
|
|
40
|
+
const val = sm[2];
|
|
41
|
+
map[`${ns}.${key}`] = val;
|
|
42
|
+
}
|
|
43
|
+
idx = end + 1;
|
|
44
|
+
}
|
|
45
|
+
// Capture very simple helper returns
|
|
46
|
+
// func makeAddress() -> [String: Any] { return [ ... ] }
|
|
47
|
+
for (const m of content.matchAll(/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*->\s*\[[^\]]+\][^{]*\{[\s\S]*?return\s*(\[[\s\S]*?\])[\s\S]*?\}/g)) {
|
|
48
|
+
map.__dictFuncs = map.__dictFuncs || {};
|
|
49
|
+
map.__dictFuncs[m[1]] = { kind: 'dict', text: m[2] };
|
|
50
|
+
}
|
|
51
|
+
// func makeProducts() -> [[String: Any]] { return [ ... ] } (array)
|
|
52
|
+
for (const m of content.matchAll(/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*->\s*\[\[[^\]]+\]\][^{]*\{[\s\S]*?return\s*(\[[\s\S]*?\])[\s\S]*?\}/g)) {
|
|
53
|
+
map.__dictFuncs = map.__dictFuncs || {};
|
|
54
|
+
map.__dictFuncs[m[1]] = { kind: 'array', text: m[2] };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
} catch (_) {}
|
|
58
|
+
return map;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { buildCrossFileConstMap };
|