@flisk/analyze-tracking 0.7.6 → 0.8.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 +29 -21
- package/bin/cli.js +1 -0
- package/package.json +1 -1
- package/src/analyze/go/astTraversal.js +22 -22
- package/src/analyze/go/eventExtractor.js +10 -7
- package/src/analyze/go/index.js +39 -19
- package/src/analyze/go/propertyExtractor.js +25 -5
- package/src/analyze/go/trackingExtractor.js +5 -5
- package/src/analyze/index.js +9 -6
- package/src/analyze/javascript/detectors/analytics-source.js +55 -2
- package/src/analyze/javascript/extractors/event-extractor.js +69 -2
- package/src/analyze/javascript/index.js +14 -8
- package/src/analyze/javascript/parser.js +87 -14
- package/src/analyze/python/index.js +32 -26
- package/src/analyze/python/pythonTrackingAnalyzer.py +113 -39
- package/src/analyze/ruby/extractors.js +46 -10
- package/src/analyze/ruby/index.js +14 -7
- package/src/analyze/ruby/visitor.js +24 -7
- package/src/analyze/typescript/extractors/event-extractor.js +75 -2
- package/src/analyze/typescript/index.js +16 -10
- package/src/analyze/typescript/parser.js +37 -14
- package/src/analyze/utils/customFunctionParser.js +55 -0
- package/src/index.js +2 -2
package/README.md
CHANGED
|
@@ -23,38 +23,46 @@ Run without installation! Just use:
|
|
|
23
23
|
npx @flisk/analyze-tracking /path/to/project [options]
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
### Key Options
|
|
26
|
+
### Key Options
|
|
27
27
|
- `-g, --generateDescription`: Generate descriptions of fields (default: `false`)
|
|
28
28
|
- `-p, --provider <provider>`: Specify a provider (options: `openai`, `gemini`)
|
|
29
29
|
- `-m, --model <model>`: Specify a model (ex: `gpt-4.1-nano`, `gpt-4o-mini`, `gemini-2.0-flash-lite-001`)
|
|
30
30
|
- `-o, --output <output_file>`: Name of the output file (default: `tracking-schema.yaml`)
|
|
31
|
-
- `-c, --customFunction <
|
|
31
|
+
- `-c, --customFunction <function_signature>`: Specify the signature of your custom tracking function (see [instructions here](#custom-functions))
|
|
32
32
|
- `--format <format>`: Output format, either `yaml` (default) or `json`. If an invalid value is provided, the CLI will exit with an error.
|
|
33
33
|
- `--stdout`: Print the output to the terminal instead of writing to a file (works with both YAML and JSON)
|
|
34
34
|
|
|
35
35
|
🔑 **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`.
|
|
36
36
|
|
|
37
|
-
<details>
|
|
38
|
-
<summary>Note on Custom Functions 💡</summary>
|
|
39
37
|
|
|
40
|
-
|
|
38
|
+
### Custom Functions
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
+
|
|
42
|
+
Your function signature should be in the following format:
|
|
43
|
+
```js
|
|
44
|
+
yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES, customFieldOne, customFieldTwo)
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- `EVENT_NAME` is the name of the event you are tracking. It should be a string or a pointer to a string. This is required.
|
|
48
|
+
- `PROPERTIES` is an object of properties for that event. It should be an object / dictionary. This is optional.
|
|
49
|
+
- Any additional parameters are other fields you are tracking. They can be of any type. The names you provide for these parameters will be used as the property names in the output.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
For example, if your function has a userId parameter at the beginning, followed by the event name and properties, you would pass in the following:
|
|
53
|
+
|
|
54
|
+
```js
|
|
55
|
+
yourCustomTrackFunctionName(userId, EVENT_NAME, PROPERTIES)
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If your function follows the standard format `yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES)`, you can simply pass in `yourCustomTrackFunctionName` to `--customFunction` as a shorthand.
|
|
59
|
+
|
|
60
|
+
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
|
+
|
|
62
|
+
```sh
|
|
63
|
+
npx @flisk/analyze-tracking /path/to/project --customFunction "yourFunc1" --customFunction "yourFunc2(userId, EVENT_NAME, PROPERTIES)"
|
|
64
|
+
npx @flisk/analyze-tracking /path/to/project -c "yourFunc1" "yourFunc2(userId, EVENT_NAME, PROPERTIES)"
|
|
65
|
+
```
|
|
58
66
|
|
|
59
67
|
|
|
60
68
|
## What's Generated?
|
package/bin/cli.js
CHANGED
package/package.json
CHANGED
|
@@ -12,34 +12,34 @@ const { extractTrackingEvent } = require('./trackingExtractor');
|
|
|
12
12
|
* @param {Array<Object>} events - Array to collect found tracking events (modified in place)
|
|
13
13
|
* @param {string} filePath - Path to the file being analyzed
|
|
14
14
|
* @param {string} functionName - Name of the current function being processed
|
|
15
|
-
* @param {
|
|
15
|
+
* @param {Object|null} customConfig - Parsed custom function configuration (or null)
|
|
16
16
|
* @param {Object} typeContext - Type information context for variable resolution
|
|
17
17
|
* @param {string} currentFunction - Current function context for type lookups
|
|
18
18
|
*/
|
|
19
|
-
function extractEventsFromBody(body, events, filePath, functionName,
|
|
19
|
+
function extractEventsFromBody(body, events, filePath, functionName, customConfig, typeContext, currentFunction) {
|
|
20
20
|
for (const stmt of body) {
|
|
21
21
|
if (stmt.tag === 'exec' && stmt.expr) {
|
|
22
|
-
processExpression(stmt.expr, events, filePath, functionName,
|
|
22
|
+
processExpression(stmt.expr, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
23
23
|
} else if (stmt.tag === 'declare' && stmt.value) {
|
|
24
24
|
// Handle variable declarations with tracking calls
|
|
25
|
-
processExpression(stmt.value, events, filePath, functionName,
|
|
25
|
+
processExpression(stmt.value, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
26
26
|
} else if (stmt.tag === 'assign' && stmt.rhs) {
|
|
27
27
|
// Handle assignments with tracking calls
|
|
28
|
-
processExpression(stmt.rhs, events, filePath, functionName,
|
|
28
|
+
processExpression(stmt.rhs, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
29
29
|
} else if (stmt.tag === 'if' && stmt.body) {
|
|
30
|
-
extractEventsFromBody(stmt.body, events, filePath, functionName,
|
|
30
|
+
extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
31
31
|
} else if (stmt.tag === 'elseif' && stmt.body) {
|
|
32
|
-
extractEventsFromBody(stmt.body, events, filePath, functionName,
|
|
32
|
+
extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
33
33
|
} else if (stmt.tag === 'else' && stmt.body) {
|
|
34
|
-
extractEventsFromBody(stmt.body, events, filePath, functionName,
|
|
34
|
+
extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
35
35
|
} else if (stmt.tag === 'for' && stmt.body) {
|
|
36
|
-
extractEventsFromBody(stmt.body, events, filePath, functionName,
|
|
36
|
+
extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
37
37
|
} else if (stmt.tag === 'foreach' && stmt.body) {
|
|
38
|
-
extractEventsFromBody(stmt.body, events, filePath, functionName,
|
|
38
|
+
extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
39
39
|
} else if (stmt.tag === 'switch' && stmt.cases) {
|
|
40
40
|
for (const caseNode of stmt.cases) {
|
|
41
41
|
if (caseNode.body) {
|
|
42
|
-
extractEventsFromBody(caseNode.body, events, filePath, functionName,
|
|
42
|
+
extractEventsFromBody(caseNode.body, events, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
}
|
|
@@ -52,18 +52,18 @@ function extractEventsFromBody(body, events, filePath, functionName, customFunct
|
|
|
52
52
|
* @param {Array<Object>} events - Array to collect found tracking events (modified in place)
|
|
53
53
|
* @param {string} filePath - Path to the file being analyzed
|
|
54
54
|
* @param {string} functionName - Name of the current function being processed
|
|
55
|
-
* @param {
|
|
55
|
+
* @param {Object|null} customConfig - Parsed custom function configuration (or null)
|
|
56
56
|
* @param {Object} typeContext - Type information context for variable resolution
|
|
57
57
|
* @param {string} currentFunction - Current function context for type lookups
|
|
58
58
|
* @param {number} [depth=0] - Current recursion depth (used to prevent infinite recursion)
|
|
59
59
|
*/
|
|
60
|
-
function processExpression(expr, events, filePath, functionName,
|
|
60
|
+
function processExpression(expr, events, filePath, functionName, customConfig, typeContext, currentFunction, depth = 0) {
|
|
61
61
|
if (!expr || depth > MAX_RECURSION_DEPTH) return; // Prevent infinite recursion with depth limit
|
|
62
62
|
|
|
63
63
|
// Handle array of expressions
|
|
64
64
|
if (Array.isArray(expr)) {
|
|
65
65
|
for (const item of expr) {
|
|
66
|
-
processExpression(item, events, filePath, functionName,
|
|
66
|
+
processExpression(item, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
67
67
|
}
|
|
68
68
|
return;
|
|
69
69
|
}
|
|
@@ -71,25 +71,25 @@ function processExpression(expr, events, filePath, functionName, customFunction,
|
|
|
71
71
|
// Handle single expression with body
|
|
72
72
|
if (expr.body) {
|
|
73
73
|
for (const item of expr.body) {
|
|
74
|
-
processExpression(item, events, filePath, functionName,
|
|
74
|
+
processExpression(item, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
75
75
|
}
|
|
76
76
|
return;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
// Handle specific node types
|
|
80
80
|
if (expr.tag === 'call') {
|
|
81
|
-
const trackingCall = extractTrackingEvent(expr, filePath, functionName,
|
|
81
|
+
const trackingCall = extractTrackingEvent(expr, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
82
82
|
if (trackingCall) {
|
|
83
83
|
events.push(trackingCall);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
// Also process call arguments
|
|
87
87
|
if (expr.args) {
|
|
88
|
-
processExpression(expr.args, events, filePath, functionName,
|
|
88
|
+
processExpression(expr.args, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
89
89
|
}
|
|
90
90
|
} else if (expr.tag === 'structlit') {
|
|
91
91
|
// Check if this struct literal is a tracking event
|
|
92
|
-
const trackingCall = extractTrackingEvent(expr, filePath, functionName,
|
|
92
|
+
const trackingCall = extractTrackingEvent(expr, filePath, functionName, customConfig, typeContext, currentFunction);
|
|
93
93
|
if (trackingCall) {
|
|
94
94
|
events.push(trackingCall);
|
|
95
95
|
}
|
|
@@ -98,7 +98,7 @@ function processExpression(expr, events, filePath, functionName, customFunction,
|
|
|
98
98
|
if (!trackingCall && expr.fields) {
|
|
99
99
|
for (const field of expr.fields) {
|
|
100
100
|
if (field.value) {
|
|
101
|
-
processExpression(field.value, events, filePath, functionName,
|
|
101
|
+
processExpression(field.value, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
104
|
}
|
|
@@ -106,13 +106,13 @@ function processExpression(expr, events, filePath, functionName, customFunction,
|
|
|
106
106
|
|
|
107
107
|
// Process other common properties that might contain expressions
|
|
108
108
|
if (expr.value && expr.tag !== 'structlit') {
|
|
109
|
-
processExpression(expr.value, events, filePath, functionName,
|
|
109
|
+
processExpression(expr.value, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
110
110
|
}
|
|
111
111
|
if (expr.lhs) {
|
|
112
|
-
processExpression(expr.lhs, events, filePath, functionName,
|
|
112
|
+
processExpression(expr.lhs, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
113
113
|
}
|
|
114
114
|
if (expr.rhs) {
|
|
115
|
-
processExpression(expr.rhs, events, filePath, functionName,
|
|
115
|
+
processExpression(expr.rhs, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
|
|
@@ -10,9 +10,10 @@ const { extractStringValue, findStructLiteral, findStructField, extractSnowplowV
|
|
|
10
10
|
* Extract event name from a tracking call based on the source
|
|
11
11
|
* @param {Object} callNode - AST node representing a function call or struct literal
|
|
12
12
|
* @param {string} source - Analytics source (e.g., 'segment', 'amplitude')
|
|
13
|
+
* @param {Object|null} customConfig - Parsed custom function configuration
|
|
13
14
|
* @returns {string|null} Event name or null if not found
|
|
14
15
|
*/
|
|
15
|
-
function extractEventName(callNode, source) {
|
|
16
|
+
function extractEventName(callNode, source, customConfig = null) {
|
|
16
17
|
if (!callNode.args || callNode.args.length === 0) {
|
|
17
18
|
// For struct literals, we need to check fields instead of args
|
|
18
19
|
if (!callNode.fields || callNode.fields.length === 0) {
|
|
@@ -35,7 +36,7 @@ function extractEventName(callNode, source) {
|
|
|
35
36
|
return extractSnowplowEventName(callNode);
|
|
36
37
|
|
|
37
38
|
case ANALYTICS_SOURCES.CUSTOM:
|
|
38
|
-
return extractCustomEventName(callNode);
|
|
39
|
+
return extractCustomEventName(callNode, customConfig);
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
return null;
|
|
@@ -142,13 +143,15 @@ function extractSnowplowEventName(callNode) {
|
|
|
142
143
|
* Extract custom event name
|
|
143
144
|
* Pattern: customFunction("event_name", props)
|
|
144
145
|
* @param {Object} callNode - AST node for custom tracking function call
|
|
146
|
+
* @param {Object|null} customConfig - Custom configuration object
|
|
145
147
|
* @returns {string|null} Event name or null if not found
|
|
146
148
|
*/
|
|
147
|
-
function extractCustomEventName(callNode) {
|
|
148
|
-
if (callNode.args
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
149
|
+
function extractCustomEventName(callNode, customConfig) {
|
|
150
|
+
if (!callNode.args || callNode.args.length === 0) return null;
|
|
151
|
+
const args = callNode.args;
|
|
152
|
+
const eventIdx = customConfig?.eventIndex ?? 0;
|
|
153
|
+
const argNode = args[eventIdx];
|
|
154
|
+
return extractStringValue(argNode);
|
|
152
155
|
}
|
|
153
156
|
|
|
154
157
|
module.exports = {
|
package/src/analyze/go/index.js
CHANGED
|
@@ -12,39 +12,59 @@ const { extractEventsFromBody } = require('./astTraversal');
|
|
|
12
12
|
/**
|
|
13
13
|
* Analyze a Go file and extract tracking events
|
|
14
14
|
* @param {string} filePath - Path to the Go file to analyze
|
|
15
|
-
* @param {string|null}
|
|
15
|
+
* @param {string|null} customFunctionSignatures - Signature of custom tracking function to detect (optional)
|
|
16
16
|
* @returns {Promise<Array>} Array of tracking events found in the file
|
|
17
17
|
* @throws {Error} If the file cannot be read or parsed
|
|
18
18
|
*/
|
|
19
|
-
async function analyzeGoFile(filePath,
|
|
19
|
+
async function analyzeGoFile(filePath, customFunctionSignatures = null) {
|
|
20
20
|
try {
|
|
21
21
|
// Read the Go file
|
|
22
22
|
const source = fs.readFileSync(filePath, 'utf8');
|
|
23
|
-
|
|
24
|
-
// Parse the Go file using goAstParser
|
|
23
|
+
|
|
24
|
+
// Parse the Go file using goAstParser (once)
|
|
25
25
|
const ast = extractGoAST(source);
|
|
26
|
-
|
|
26
|
+
|
|
27
27
|
// First pass: build type information for functions and variables
|
|
28
28
|
const typeContext = buildTypeContext(ast);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
29
|
+
|
|
30
|
+
const collectEventsForConfig = (customConfig) => {
|
|
31
|
+
const events = [];
|
|
32
|
+
let currentFunction = 'global';
|
|
33
|
+
for (const node of ast) {
|
|
34
|
+
if (node.tag === 'func') {
|
|
35
|
+
currentFunction = node.name;
|
|
36
|
+
if (node.body) {
|
|
37
|
+
extractEventsFromBody(
|
|
38
|
+
node.body,
|
|
39
|
+
events,
|
|
40
|
+
filePath,
|
|
41
|
+
currentFunction,
|
|
42
|
+
customConfig,
|
|
43
|
+
typeContext,
|
|
44
|
+
currentFunction
|
|
45
|
+
);
|
|
46
|
+
}
|
|
41
47
|
}
|
|
42
48
|
}
|
|
49
|
+
return events;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
let events = [];
|
|
53
|
+
|
|
54
|
+
// Built-in providers pass (null custom config)
|
|
55
|
+
events.push(...collectEventsForConfig(null));
|
|
56
|
+
|
|
57
|
+
// Custom configs passes
|
|
58
|
+
if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) {
|
|
59
|
+
for (const customConfig of customFunctionSignatures) {
|
|
60
|
+
if (!customConfig) continue;
|
|
61
|
+
events.push(...collectEventsForConfig(customConfig));
|
|
62
|
+
}
|
|
43
63
|
}
|
|
44
|
-
|
|
64
|
+
|
|
45
65
|
// Deduplicate events based on eventName, source, and function
|
|
46
66
|
const uniqueEvents = deduplicateEvents(events);
|
|
47
|
-
|
|
67
|
+
|
|
48
68
|
return uniqueEvents;
|
|
49
69
|
} catch (error) {
|
|
50
70
|
console.error(`Error analyzing Go file ${filePath}:`, error.message);
|
|
@@ -12,9 +12,10 @@ const { extractStringValue, findStructLiteral, findStructField, extractFieldName
|
|
|
12
12
|
* @param {string} source - Analytics source (e.g., 'segment', 'amplitude')
|
|
13
13
|
* @param {Object} typeContext - Type information context for variable resolution
|
|
14
14
|
* @param {string} currentFunction - Current function context for type lookups
|
|
15
|
+
* @param {Object} customConfig - Custom configuration for property extraction
|
|
15
16
|
* @returns {Object} Object containing extracted properties with their type information
|
|
16
17
|
*/
|
|
17
|
-
function extractProperties(callNode, source, typeContext, currentFunction) {
|
|
18
|
+
function extractProperties(callNode, source, typeContext, currentFunction, customConfig) {
|
|
18
19
|
const properties = {};
|
|
19
20
|
|
|
20
21
|
switch (source) {
|
|
@@ -36,7 +37,7 @@ function extractProperties(callNode, source, typeContext, currentFunction) {
|
|
|
36
37
|
break;
|
|
37
38
|
|
|
38
39
|
case ANALYTICS_SOURCES.CUSTOM:
|
|
39
|
-
extractCustomProperties(callNode, properties, typeContext, currentFunction);
|
|
40
|
+
extractCustomProperties(callNode, properties, typeContext, currentFunction, customConfig);
|
|
40
41
|
break;
|
|
41
42
|
}
|
|
42
43
|
|
|
@@ -270,10 +271,29 @@ function extractSnowplowProperties(callNode, properties, typeContext, currentFun
|
|
|
270
271
|
* @param {Object} properties - Object to store extracted properties (modified in place)
|
|
271
272
|
* @param {Object} typeContext - Type information context for variable resolution
|
|
272
273
|
* @param {string} currentFunction - Current function context for type lookups
|
|
274
|
+
* @param {Object} customConfig - Custom configuration for property extraction
|
|
273
275
|
*/
|
|
274
|
-
function extractCustomProperties(callNode, properties, typeContext, currentFunction) {
|
|
275
|
-
if (callNode.args
|
|
276
|
-
|
|
276
|
+
function extractCustomProperties(callNode, properties, typeContext, currentFunction, customConfig) {
|
|
277
|
+
if (!callNode.args || callNode.args.length === 0) return;
|
|
278
|
+
|
|
279
|
+
const args = callNode.args;
|
|
280
|
+
|
|
281
|
+
const propsIdx = customConfig?.propertiesIndex ?? 1;
|
|
282
|
+
|
|
283
|
+
// Extract extra params first (those not event or properties)
|
|
284
|
+
if (customConfig && Array.isArray(customConfig.extraParams)) {
|
|
285
|
+
customConfig.extraParams.forEach(param => {
|
|
286
|
+
const argNode = args[param.idx];
|
|
287
|
+
if (argNode) {
|
|
288
|
+
properties[param.name] = getPropertyInfo(argNode, typeContext, currentFunction);
|
|
289
|
+
}
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Extract properties map/object (if provided)
|
|
294
|
+
const propsArg = args[propsIdx];
|
|
295
|
+
if (propsArg) {
|
|
296
|
+
extractPropertiesFromExpr(propsArg, properties, typeContext, currentFunction);
|
|
277
297
|
}
|
|
278
298
|
}
|
|
279
299
|
|
|
@@ -13,19 +13,19 @@ const { extractProperties } = require('./propertyExtractor');
|
|
|
13
13
|
* @param {Object} callNode - AST node representing a function call or struct literal
|
|
14
14
|
* @param {string} filePath - Path to the file being analyzed
|
|
15
15
|
* @param {string} functionName - Name of the function containing this tracking call
|
|
16
|
-
* @param {
|
|
16
|
+
* @param {Object|null} customConfig - Parsed custom function configuration (or null)
|
|
17
17
|
* @param {Object} typeContext - Type information context for variable resolution
|
|
18
18
|
* @param {string} currentFunction - Current function context for type lookups
|
|
19
19
|
* @returns {Object|null} Tracking event object with eventName, source, properties, etc., or null if not a tracking call
|
|
20
20
|
*/
|
|
21
|
-
function extractTrackingEvent(callNode, filePath, functionName,
|
|
22
|
-
const source = detectSource(callNode,
|
|
21
|
+
function extractTrackingEvent(callNode, filePath, functionName, customConfig, typeContext, currentFunction) {
|
|
22
|
+
const source = detectSource(callNode, customConfig ? customConfig.functionName : null);
|
|
23
23
|
if (!source) return null;
|
|
24
24
|
|
|
25
|
-
const eventName = extractEventName(callNode, source);
|
|
25
|
+
const eventName = extractEventName(callNode, source, customConfig);
|
|
26
26
|
if (!eventName) return null;
|
|
27
27
|
|
|
28
|
-
const properties = extractProperties(callNode, source, typeContext, currentFunction);
|
|
28
|
+
const properties = extractProperties(callNode, source, typeContext, currentFunction, customConfig);
|
|
29
29
|
|
|
30
30
|
// Get line number based on source type
|
|
31
31
|
let line = 0;
|
package/src/analyze/index.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
const path = require('path');
|
|
7
7
|
const ts = require('typescript');
|
|
8
|
+
const { parseCustomFunctionSignature } = require('./utils/customFunctionParser');
|
|
8
9
|
const { getAllFiles } = require('../utils/fileProcessor');
|
|
9
10
|
const { analyzeJsFile } = require('./javascript');
|
|
10
11
|
const { analyzeTsFile } = require('./typescript');
|
|
@@ -12,9 +13,11 @@ const { analyzePythonFile } = require('./python');
|
|
|
12
13
|
const { analyzeRubyFile } = require('./ruby');
|
|
13
14
|
const { analyzeGoFile } = require('./go');
|
|
14
15
|
|
|
15
|
-
async function analyzeDirectory(dirPath,
|
|
16
|
+
async function analyzeDirectory(dirPath, customFunctions) {
|
|
16
17
|
const allEvents = {};
|
|
17
18
|
|
|
19
|
+
const customFunctionSignatures = (customFunctions && customFunctions?.length > 0) ? customFunctions.map(parseCustomFunctionSignature) : null;
|
|
20
|
+
|
|
18
21
|
const files = getAllFiles(dirPath);
|
|
19
22
|
const tsFiles = files.filter(file => /\.(tsx?)$/.test(file));
|
|
20
23
|
const tsProgram = ts.createProgram(tsFiles, {
|
|
@@ -32,15 +35,15 @@ async function analyzeDirectory(dirPath, customFunction) {
|
|
|
32
35
|
const isGoFile = /\.(go)$/.test(file);
|
|
33
36
|
|
|
34
37
|
if (isJsFile) {
|
|
35
|
-
events = analyzeJsFile(file,
|
|
38
|
+
events = analyzeJsFile(file, customFunctionSignatures);
|
|
36
39
|
} else if (isTsFile) {
|
|
37
|
-
events = analyzeTsFile(file, tsProgram,
|
|
40
|
+
events = analyzeTsFile(file, tsProgram, customFunctionSignatures);
|
|
38
41
|
} else if (isPythonFile) {
|
|
39
|
-
events = await analyzePythonFile(file,
|
|
42
|
+
events = await analyzePythonFile(file, customFunctionSignatures);
|
|
40
43
|
} else if (isRubyFile) {
|
|
41
|
-
events = await analyzeRubyFile(file,
|
|
44
|
+
events = await analyzeRubyFile(file, customFunctionSignatures);
|
|
42
45
|
} else if (isGoFile) {
|
|
43
|
-
events = await analyzeGoFile(file,
|
|
46
|
+
events = await analyzeGoFile(file, customFunctionSignatures);
|
|
44
47
|
} else {
|
|
45
48
|
continue;
|
|
46
49
|
}
|
|
@@ -43,8 +43,61 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
43
43
|
* @returns {boolean}
|
|
44
44
|
*/
|
|
45
45
|
function isCustomFunction(node, customFunction) {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
if (!customFunction) return false;
|
|
47
|
+
|
|
48
|
+
// Support dot-separated names like "CustomModule.track"
|
|
49
|
+
const parts = customFunction.split('.');
|
|
50
|
+
|
|
51
|
+
// Simple identifier (no dot)
|
|
52
|
+
if (parts.length === 1) {
|
|
53
|
+
return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name === customFunction;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// For dot-separated names, the callee should be a MemberExpression chain.
|
|
57
|
+
if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return matchesMemberChain(node.callee, parts);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively verifies that a MemberExpression chain matches the expected parts.
|
|
66
|
+
* Example: parts ["CustomModule", "track"] should match `CustomModule.track()`.
|
|
67
|
+
* @param {Object} memberExpr - AST MemberExpression node
|
|
68
|
+
* @param {string[]} parts - Expected name segments (left -> right)
|
|
69
|
+
* @returns {boolean}
|
|
70
|
+
*/
|
|
71
|
+
function matchesMemberChain(memberExpr, parts) {
|
|
72
|
+
let currentNode = memberExpr;
|
|
73
|
+
let idx = parts.length - 1; // start from the rightmost property
|
|
74
|
+
|
|
75
|
+
while (currentNode && idx >= 0) {
|
|
76
|
+
const expectedPart = parts[idx];
|
|
77
|
+
|
|
78
|
+
// property should match current expectedPart
|
|
79
|
+
if (currentNode.type === NODE_TYPES.MEMBER_EXPRESSION) {
|
|
80
|
+
// Ensure property is Identifier and matches
|
|
81
|
+
if (
|
|
82
|
+
currentNode.property.type !== NODE_TYPES.IDENTIFIER ||
|
|
83
|
+
currentNode.property.name !== expectedPart
|
|
84
|
+
) {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Move to the object of the MemberExpression
|
|
89
|
+
currentNode = currentNode.object;
|
|
90
|
+
idx -= 1;
|
|
91
|
+
} else if (currentNode.type === NODE_TYPES.IDENTIFIER) {
|
|
92
|
+
// We reached the leftmost Identifier; it should match the first part
|
|
93
|
+
return idx === 0 && currentNode.name === expectedPart;
|
|
94
|
+
} else {
|
|
95
|
+
// Unexpected node type (e.g., ThisExpression, CallExpression, etc.)
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
48
101
|
}
|
|
49
102
|
|
|
50
103
|
/**
|
|
@@ -20,6 +20,7 @@ const EXTRACTION_STRATEGIES = {
|
|
|
20
20
|
googleanalytics: extractGoogleAnalyticsEvent,
|
|
21
21
|
snowplow: extractSnowplowEvent,
|
|
22
22
|
mparticle: extractMparticleEvent,
|
|
23
|
+
custom: extractCustomEvent,
|
|
23
24
|
default: extractDefaultEvent
|
|
24
25
|
};
|
|
25
26
|
|
|
@@ -27,10 +28,14 @@ const EXTRACTION_STRATEGIES = {
|
|
|
27
28
|
* Extracts event information from a CallExpression node
|
|
28
29
|
* @param {Object} node - AST CallExpression node
|
|
29
30
|
* @param {string} source - Analytics provider source
|
|
31
|
+
* @param {Object} customConfig - Parsed custom function configuration
|
|
30
32
|
* @returns {EventData} Extracted event data
|
|
31
33
|
*/
|
|
32
|
-
function extractEventData(node, source) {
|
|
34
|
+
function extractEventData(node, source, customConfig) {
|
|
33
35
|
const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
|
|
36
|
+
if (source === 'custom') {
|
|
37
|
+
return strategy(node, customConfig);
|
|
38
|
+
}
|
|
34
39
|
return strategy(node);
|
|
35
40
|
}
|
|
36
41
|
|
|
@@ -113,6 +118,30 @@ function extractDefaultEvent(node) {
|
|
|
113
118
|
return { eventName, propertiesNode };
|
|
114
119
|
}
|
|
115
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Extracts Custom function event data according to signature
|
|
123
|
+
* @param {Object} node - CallExpression node
|
|
124
|
+
* @param {Object} customConfig - Parsed custom function configuration
|
|
125
|
+
* @returns {EventData & {extraArgs:Object}} event data plus extra args map
|
|
126
|
+
*/
|
|
127
|
+
function extractCustomEvent(node, customConfig) {
|
|
128
|
+
const args = node.arguments || [];
|
|
129
|
+
|
|
130
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
131
|
+
const propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
132
|
+
|
|
133
|
+
const eventName = getStringValue(eventArg);
|
|
134
|
+
|
|
135
|
+
const extraArgs = {};
|
|
136
|
+
if (customConfig && customConfig.extraParams) {
|
|
137
|
+
customConfig.extraParams.forEach(extra => {
|
|
138
|
+
extraArgs[extra.name] = args[extra.idx];
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { eventName, propertiesNode: propertiesArg, extraArgs };
|
|
143
|
+
}
|
|
144
|
+
|
|
116
145
|
/**
|
|
117
146
|
* Processes extracted event data into final event object
|
|
118
147
|
* @param {EventData} eventData - Raw event data
|
|
@@ -120,9 +149,10 @@ function extractDefaultEvent(node) {
|
|
|
120
149
|
* @param {string} filePath - File path
|
|
121
150
|
* @param {number} line - Line number
|
|
122
151
|
* @param {string} functionName - Containing function name
|
|
152
|
+
* @param {Object} customConfig - Parsed custom function configuration
|
|
123
153
|
* @returns {Object|null} Processed event object or null
|
|
124
154
|
*/
|
|
125
|
-
function processEventData(eventData, source, filePath, line, functionName) {
|
|
155
|
+
function processEventData(eventData, source, filePath, line, functionName, customConfig) {
|
|
126
156
|
const { eventName, propertiesNode } = eventData;
|
|
127
157
|
|
|
128
158
|
if (!eventName || !propertiesNode || propertiesNode.type !== NODE_TYPES.OBJECT_EXPRESSION) {
|
|
@@ -131,6 +161,24 @@ function processEventData(eventData, source, filePath, line, functionName) {
|
|
|
131
161
|
|
|
132
162
|
let properties = extractProperties(propertiesNode);
|
|
133
163
|
|
|
164
|
+
// Handle custom extra params
|
|
165
|
+
if (source === 'custom' && customConfig && eventData.extraArgs) {
|
|
166
|
+
for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
|
|
167
|
+
if (argNode && argNode.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
168
|
+
// Extract detailed properties from object expression
|
|
169
|
+
properties[paramName] = {
|
|
170
|
+
type: 'object',
|
|
171
|
+
properties: extractProperties(argNode)
|
|
172
|
+
};
|
|
173
|
+
} else {
|
|
174
|
+
// For non-object arguments, use simple type inference
|
|
175
|
+
properties[paramName] = {
|
|
176
|
+
type: inferNodeValueType(argNode)
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
134
182
|
// Special handling for Snowplow: remove 'action' from properties
|
|
135
183
|
if (source === 'snowplow' && properties.action) {
|
|
136
184
|
delete properties.action;
|
|
@@ -173,6 +221,25 @@ function findPropertyByKey(objectNode, key) {
|
|
|
173
221
|
);
|
|
174
222
|
}
|
|
175
223
|
|
|
224
|
+
/**
|
|
225
|
+
* Infers the type of a value from an AST node (simple heuristic)
|
|
226
|
+
* @param {Object} node - AST node
|
|
227
|
+
* @returns {string} inferred type
|
|
228
|
+
*/
|
|
229
|
+
function inferNodeValueType(node) {
|
|
230
|
+
if (!node) return 'any';
|
|
231
|
+
switch (node.type) {
|
|
232
|
+
case NODE_TYPES.LITERAL:
|
|
233
|
+
return typeof node.value;
|
|
234
|
+
case NODE_TYPES.OBJECT_EXPRESSION:
|
|
235
|
+
return 'object';
|
|
236
|
+
case NODE_TYPES.ARRAY_EXPRESSION:
|
|
237
|
+
return 'array';
|
|
238
|
+
default:
|
|
239
|
+
return 'any';
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
176
243
|
module.exports = {
|
|
177
244
|
extractEventData,
|
|
178
245
|
processEventData
|