@flisk/analyze-tracking 0.7.6 → 0.8.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 +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 +109 -12
- package/src/analyze/javascript/index.js +14 -8
- package/src/analyze/javascript/parser.js +140 -21
- 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 +141 -20
- 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
|
@@ -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,25 +28,31 @@ 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} constantMap - Collected constant map
|
|
32
|
+
* @param {Object} customConfig - Parsed custom function configuration
|
|
30
33
|
* @returns {EventData} Extracted event data
|
|
31
34
|
*/
|
|
32
|
-
function extractEventData(node, source) {
|
|
35
|
+
function extractEventData(node, source, constantMap = {}, customConfig) {
|
|
33
36
|
const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
|
|
34
|
-
|
|
37
|
+
if (source === 'custom') {
|
|
38
|
+
return strategy(node, constantMap, customConfig);
|
|
39
|
+
}
|
|
40
|
+
return strategy(node, constantMap);
|
|
35
41
|
}
|
|
36
42
|
|
|
37
43
|
/**
|
|
38
44
|
* Extracts Google Analytics event data
|
|
39
45
|
* @param {Object} node - CallExpression node
|
|
46
|
+
* @param {Object} constantMap - Collected constant map
|
|
40
47
|
* @returns {EventData}
|
|
41
48
|
*/
|
|
42
|
-
function extractGoogleAnalyticsEvent(node) {
|
|
49
|
+
function extractGoogleAnalyticsEvent(node, constantMap) {
|
|
43
50
|
if (!node.arguments || node.arguments.length < 3) {
|
|
44
51
|
return { eventName: null, propertiesNode: null };
|
|
45
52
|
}
|
|
46
53
|
|
|
47
54
|
// gtag('event', 'event_name', { properties })
|
|
48
|
-
const eventName = getStringValue(node.arguments[1]);
|
|
55
|
+
const eventName = getStringValue(node.arguments[1], constantMap);
|
|
49
56
|
const propertiesNode = node.arguments[2];
|
|
50
57
|
|
|
51
58
|
return { eventName, propertiesNode };
|
|
@@ -54,9 +61,10 @@ function extractGoogleAnalyticsEvent(node) {
|
|
|
54
61
|
/**
|
|
55
62
|
* Extracts Snowplow event data
|
|
56
63
|
* @param {Object} node - CallExpression node
|
|
64
|
+
* @param {Object} constantMap - Collected constant map
|
|
57
65
|
* @returns {EventData}
|
|
58
66
|
*/
|
|
59
|
-
function extractSnowplowEvent(node) {
|
|
67
|
+
function extractSnowplowEvent(node, constantMap) {
|
|
60
68
|
if (!node.arguments || node.arguments.length === 0) {
|
|
61
69
|
return { eventName: null, propertiesNode: null };
|
|
62
70
|
}
|
|
@@ -70,7 +78,7 @@ function extractSnowplowEvent(node) {
|
|
|
70
78
|
|
|
71
79
|
if (structEventArg.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
72
80
|
const actionProperty = findPropertyByKey(structEventArg, 'action');
|
|
73
|
-
const eventName = actionProperty ? getStringValue(actionProperty.value) : null;
|
|
81
|
+
const eventName = actionProperty ? getStringValue(actionProperty.value, constantMap) : null;
|
|
74
82
|
|
|
75
83
|
return { eventName, propertiesNode: structEventArg };
|
|
76
84
|
}
|
|
@@ -82,15 +90,16 @@ function extractSnowplowEvent(node) {
|
|
|
82
90
|
/**
|
|
83
91
|
* Extracts mParticle event data
|
|
84
92
|
* @param {Object} node - CallExpression node
|
|
93
|
+
* @param {Object} constantMap - Collected constant map
|
|
85
94
|
* @returns {EventData}
|
|
86
95
|
*/
|
|
87
|
-
function extractMparticleEvent(node) {
|
|
96
|
+
function extractMparticleEvent(node, constantMap) {
|
|
88
97
|
if (!node.arguments || node.arguments.length < 3) {
|
|
89
98
|
return { eventName: null, propertiesNode: null };
|
|
90
99
|
}
|
|
91
100
|
|
|
92
101
|
// mParticle.logEvent('event_name', mParticle.EventType.Navigation, { properties })
|
|
93
|
-
const eventName = getStringValue(node.arguments[0]);
|
|
102
|
+
const eventName = getStringValue(node.arguments[0], constantMap);
|
|
94
103
|
const propertiesNode = node.arguments[2];
|
|
95
104
|
|
|
96
105
|
return { eventName, propertiesNode };
|
|
@@ -99,20 +108,46 @@ function extractMparticleEvent(node) {
|
|
|
99
108
|
/**
|
|
100
109
|
* Default event extraction for standard providers
|
|
101
110
|
* @param {Object} node - CallExpression node
|
|
111
|
+
* @param {Object} constantMap - Collected constant map
|
|
102
112
|
* @returns {EventData}
|
|
103
113
|
*/
|
|
104
|
-
function extractDefaultEvent(node) {
|
|
114
|
+
function extractDefaultEvent(node, constantMap) {
|
|
105
115
|
if (!node.arguments || node.arguments.length < 2) {
|
|
106
116
|
return { eventName: null, propertiesNode: null };
|
|
107
117
|
}
|
|
108
118
|
|
|
109
119
|
// provider.track('event_name', { properties })
|
|
110
|
-
const eventName = getStringValue(node.arguments[0]);
|
|
120
|
+
const eventName = getStringValue(node.arguments[0], constantMap);
|
|
111
121
|
const propertiesNode = node.arguments[1];
|
|
112
122
|
|
|
113
123
|
return { eventName, propertiesNode };
|
|
114
124
|
}
|
|
115
125
|
|
|
126
|
+
/**
|
|
127
|
+
* Extracts Custom function event data according to signature
|
|
128
|
+
* @param {Object} node - CallExpression node
|
|
129
|
+
* @param {Object} constantMap - Collected constant map
|
|
130
|
+
* @param {Object} customConfig - Parsed custom function configuration
|
|
131
|
+
* @returns {EventData & {extraArgs:Object}} event data plus extra args map
|
|
132
|
+
*/
|
|
133
|
+
function extractCustomEvent(node, constantMap, customConfig) {
|
|
134
|
+
const args = node.arguments || [];
|
|
135
|
+
|
|
136
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
137
|
+
const propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
138
|
+
|
|
139
|
+
const eventName = getStringValue(eventArg, constantMap);
|
|
140
|
+
|
|
141
|
+
const extraArgs = {};
|
|
142
|
+
if (customConfig && customConfig.extraParams) {
|
|
143
|
+
customConfig.extraParams.forEach(extra => {
|
|
144
|
+
extraArgs[extra.name] = args[extra.idx];
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return { eventName, propertiesNode: propertiesArg, extraArgs };
|
|
149
|
+
}
|
|
150
|
+
|
|
116
151
|
/**
|
|
117
152
|
* Processes extracted event data into final event object
|
|
118
153
|
* @param {EventData} eventData - Raw event data
|
|
@@ -120,9 +155,10 @@ function extractDefaultEvent(node) {
|
|
|
120
155
|
* @param {string} filePath - File path
|
|
121
156
|
* @param {number} line - Line number
|
|
122
157
|
* @param {string} functionName - Containing function name
|
|
158
|
+
* @param {Object} customConfig - Parsed custom function configuration
|
|
123
159
|
* @returns {Object|null} Processed event object or null
|
|
124
160
|
*/
|
|
125
|
-
function processEventData(eventData, source, filePath, line, functionName) {
|
|
161
|
+
function processEventData(eventData, source, filePath, line, functionName, customConfig) {
|
|
126
162
|
const { eventName, propertiesNode } = eventData;
|
|
127
163
|
|
|
128
164
|
if (!eventName || !propertiesNode || propertiesNode.type !== NODE_TYPES.OBJECT_EXPRESSION) {
|
|
@@ -131,6 +167,24 @@ function processEventData(eventData, source, filePath, line, functionName) {
|
|
|
131
167
|
|
|
132
168
|
let properties = extractProperties(propertiesNode);
|
|
133
169
|
|
|
170
|
+
// Handle custom extra params
|
|
171
|
+
if (source === 'custom' && customConfig && eventData.extraArgs) {
|
|
172
|
+
for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
|
|
173
|
+
if (argNode && argNode.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
174
|
+
// Extract detailed properties from object expression
|
|
175
|
+
properties[paramName] = {
|
|
176
|
+
type: 'object',
|
|
177
|
+
properties: extractProperties(argNode)
|
|
178
|
+
};
|
|
179
|
+
} else {
|
|
180
|
+
// For non-object arguments, use simple type inference
|
|
181
|
+
properties[paramName] = {
|
|
182
|
+
type: inferNodeValueType(argNode)
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
134
188
|
// Special handling for Snowplow: remove 'action' from properties
|
|
135
189
|
if (source === 'snowplow' && properties.action) {
|
|
136
190
|
delete properties.action;
|
|
@@ -149,13 +203,17 @@ function processEventData(eventData, source, filePath, line, functionName) {
|
|
|
149
203
|
/**
|
|
150
204
|
* Gets string value from an AST node
|
|
151
205
|
* @param {Object} node - AST node
|
|
206
|
+
* @param {Object} constantMap - Collected constant map
|
|
152
207
|
* @returns {string|null} String value or null
|
|
153
208
|
*/
|
|
154
|
-
function getStringValue(node) {
|
|
209
|
+
function getStringValue(node, constantMap = {}) {
|
|
155
210
|
if (!node) return null;
|
|
156
211
|
if (node.type === NODE_TYPES.LITERAL && typeof node.value === 'string') {
|
|
157
212
|
return node.value;
|
|
158
213
|
}
|
|
214
|
+
if (node.type === NODE_TYPES.MEMBER_EXPRESSION) {
|
|
215
|
+
return resolveMemberExpressionToString(node, constantMap);
|
|
216
|
+
}
|
|
159
217
|
return null;
|
|
160
218
|
}
|
|
161
219
|
|
|
@@ -173,6 +231,45 @@ function findPropertyByKey(objectNode, key) {
|
|
|
173
231
|
);
|
|
174
232
|
}
|
|
175
233
|
|
|
234
|
+
/**
|
|
235
|
+
* Infers the type of a value from an AST node (simple heuristic)
|
|
236
|
+
* @param {Object} node - AST node
|
|
237
|
+
* @returns {string} inferred type
|
|
238
|
+
*/
|
|
239
|
+
function inferNodeValueType(node) {
|
|
240
|
+
if (!node) return 'any';
|
|
241
|
+
switch (node.type) {
|
|
242
|
+
case NODE_TYPES.LITERAL:
|
|
243
|
+
return typeof node.value;
|
|
244
|
+
case NODE_TYPES.OBJECT_EXPRESSION:
|
|
245
|
+
return 'object';
|
|
246
|
+
case NODE_TYPES.ARRAY_EXPRESSION:
|
|
247
|
+
return 'array';
|
|
248
|
+
default:
|
|
249
|
+
return 'any';
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Helper to resolve MemberExpression (CONST.KEY) to string using collected constant map
|
|
254
|
+
function resolveMemberExpressionToString(node, constantMap) {
|
|
255
|
+
if (!node || node.type !== NODE_TYPES.MEMBER_EXPRESSION) return null;
|
|
256
|
+
if (node.computed) return null; // Only support dot notation
|
|
257
|
+
|
|
258
|
+
const object = node.object;
|
|
259
|
+
const property = node.property;
|
|
260
|
+
|
|
261
|
+
if (object.type !== NODE_TYPES.IDENTIFIER) return null;
|
|
262
|
+
if (property.type !== NODE_TYPES.IDENTIFIER) return null;
|
|
263
|
+
|
|
264
|
+
const objName = object.name;
|
|
265
|
+
const propName = property.name;
|
|
266
|
+
|
|
267
|
+
if (constantMap && constantMap[objName] && typeof constantMap[objName][propName] === 'string') {
|
|
268
|
+
return constantMap[objName][propName];
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
176
273
|
module.exports = {
|
|
177
274
|
extractEventData,
|
|
178
275
|
processEventData
|
|
@@ -11,16 +11,22 @@ const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./
|
|
|
11
11
|
* @param {string} [customFunction] - Optional custom function name to detect
|
|
12
12
|
* @returns {Array<Object>} Array of tracking events found in the file
|
|
13
13
|
*/
|
|
14
|
-
function analyzeJsFile(filePath,
|
|
15
|
-
const events = [];
|
|
16
|
-
|
|
14
|
+
function analyzeJsFile(filePath, customFunctionSignatures = null) {
|
|
17
15
|
try {
|
|
18
|
-
// Parse the file into an AST
|
|
16
|
+
// Parse the file into an AST once
|
|
19
17
|
const ast = parseFile(filePath);
|
|
20
18
|
|
|
21
|
-
//
|
|
22
|
-
const
|
|
23
|
-
|
|
19
|
+
// Single pass extraction covering built-in + all custom configs
|
|
20
|
+
const events = findTrackingEvents(ast, filePath, customFunctionSignatures || []);
|
|
21
|
+
|
|
22
|
+
// Deduplicate events (by source | eventName | line | functionName)
|
|
23
|
+
const unique = new Map();
|
|
24
|
+
for (const evt of events) {
|
|
25
|
+
const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
|
|
26
|
+
if (!unique.has(key)) unique.set(key, evt);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return Array.from(unique.values());
|
|
24
30
|
|
|
25
31
|
} catch (error) {
|
|
26
32
|
if (error instanceof FileReadError) {
|
|
@@ -32,7 +38,7 @@ function analyzeJsFile(filePath, customFunction) {
|
|
|
32
38
|
}
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
return
|
|
41
|
+
return [];
|
|
36
42
|
}
|
|
37
43
|
|
|
38
44
|
module.exports = { analyzeJsFile };
|
|
@@ -66,22 +66,147 @@ function parseFile(filePath) {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
// ---------------------------------------------
|
|
70
|
+
// Helper – custom function matcher
|
|
71
|
+
// ---------------------------------------------
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Determines whether a CallExpression node matches the provided custom function name.
|
|
75
|
+
* Supports both simple identifiers (e.g. myTrack) and dot-separated members (e.g. Custom.track).
|
|
76
|
+
* The logic mirrors isCustomFunction from detectors/analytics-source.js but is kept local to avoid
|
|
77
|
+
* circular dependencies.
|
|
78
|
+
* @param {Object} node – CallExpression AST node
|
|
79
|
+
* @param {string} fnName – Custom function name (could include dots)
|
|
80
|
+
* @returns {boolean}
|
|
81
|
+
*/
|
|
82
|
+
function nodeMatchesCustomFunction(node, fnName) {
|
|
83
|
+
if (!fnName || !node.callee) return false;
|
|
84
|
+
|
|
85
|
+
const parts = fnName.split('.');
|
|
86
|
+
|
|
87
|
+
// Simple identifier case
|
|
88
|
+
if (parts.length === 1) {
|
|
89
|
+
return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name === fnName;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Member expression chain case
|
|
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
|
|
98
|
+
let currentNode = node.callee;
|
|
99
|
+
let idx = parts.length - 1;
|
|
100
|
+
|
|
101
|
+
while (currentNode && idx >= 0) {
|
|
102
|
+
const expected = parts[idx];
|
|
103
|
+
|
|
104
|
+
if (currentNode.type === NODE_TYPES.MEMBER_EXPRESSION) {
|
|
105
|
+
if (
|
|
106
|
+
currentNode.property.type !== NODE_TYPES.IDENTIFIER ||
|
|
107
|
+
currentNode.property.name !== expected
|
|
108
|
+
) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
currentNode = currentNode.object;
|
|
112
|
+
idx -= 1;
|
|
113
|
+
} else if (currentNode.type === NODE_TYPES.IDENTIFIER) {
|
|
114
|
+
return idx === 0 && currentNode.name === expected;
|
|
115
|
+
} else {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// -----------------------------------------------------------------------------
|
|
124
|
+
// Utility – collect constants defined as plain objects or Object.freeze({...})
|
|
125
|
+
// -----------------------------------------------------------------------------
|
|
126
|
+
function collectConstantStringMap(ast) {
|
|
127
|
+
const map = {};
|
|
128
|
+
|
|
129
|
+
walk.simple(ast, {
|
|
130
|
+
VariableDeclaration(node) {
|
|
131
|
+
// Only consider const declarations
|
|
132
|
+
if (node.kind !== 'const') return;
|
|
133
|
+
node.declarations.forEach(decl => {
|
|
134
|
+
if (decl.id.type !== NODE_TYPES.IDENTIFIER || !decl.init) return;
|
|
135
|
+
const name = decl.id.name;
|
|
136
|
+
let objLiteral = null;
|
|
137
|
+
|
|
138
|
+
if (decl.init.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
139
|
+
objLiteral = decl.init;
|
|
140
|
+
} else if (decl.init.type === NODE_TYPES.CALL_EXPRESSION) {
|
|
141
|
+
// Check for Object.freeze({...})
|
|
142
|
+
const callee = decl.init.callee;
|
|
143
|
+
if (
|
|
144
|
+
callee &&
|
|
145
|
+
callee.type === NODE_TYPES.MEMBER_EXPRESSION &&
|
|
146
|
+
callee.object.type === NODE_TYPES.IDENTIFIER &&
|
|
147
|
+
callee.object.name === 'Object' &&
|
|
148
|
+
callee.property.type === NODE_TYPES.IDENTIFIER &&
|
|
149
|
+
callee.property.name === 'freeze' &&
|
|
150
|
+
decl.init.arguments.length > 0 &&
|
|
151
|
+
decl.init.arguments[0].type === NODE_TYPES.OBJECT_EXPRESSION
|
|
152
|
+
) {
|
|
153
|
+
objLiteral = decl.init.arguments[0];
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (objLiteral) {
|
|
158
|
+
map[name] = {};
|
|
159
|
+
objLiteral.properties.forEach(prop => {
|
|
160
|
+
if (!prop.key || !prop.value) return;
|
|
161
|
+
const keyName = prop.key.name || prop.key.value;
|
|
162
|
+
if (prop.value.type === NODE_TYPES.LITERAL && typeof prop.value.value === 'string') {
|
|
163
|
+
map[name][keyName] = prop.value.value;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
return map;
|
|
172
|
+
}
|
|
173
|
+
|
|
69
174
|
/**
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
* @param {
|
|
74
|
-
* @
|
|
175
|
+
* Walk the AST once and find tracking events for built-in providers plus any number of custom
|
|
176
|
+
* function configurations. This avoids the previous O(n * customConfigs) behaviour.
|
|
177
|
+
*
|
|
178
|
+
* @param {Object} ast – Parsed AST of the source file
|
|
179
|
+
* @param {string} filePath – Absolute/relative path to the source file
|
|
180
|
+
* @param {Object[]} [customConfigs=[]] – Array of parsed custom function configurations
|
|
181
|
+
* @returns {Array<Object>} – List of extracted tracking events
|
|
75
182
|
*/
|
|
76
|
-
function findTrackingEvents(ast, filePath,
|
|
183
|
+
function findTrackingEvents(ast, filePath, customConfigs = []) {
|
|
77
184
|
const events = [];
|
|
78
185
|
|
|
186
|
+
// Collect constant mappings once per file
|
|
187
|
+
const constantMap = collectConstantStringMap(ast);
|
|
188
|
+
|
|
79
189
|
walk.ancestor(ast, {
|
|
80
190
|
[NODE_TYPES.CALL_EXPRESSION]: (node, ancestors) => {
|
|
81
191
|
try {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
192
|
+
let matchedCustomConfig = null;
|
|
193
|
+
|
|
194
|
+
// Attempt to match any custom function first to avoid mis-classifying built-in providers
|
|
195
|
+
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
196
|
+
for (const cfg of customConfigs) {
|
|
197
|
+
if (cfg && nodeMatchesCustomFunction(node, cfg.functionName)) {
|
|
198
|
+
matchedCustomConfig = cfg;
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (matchedCustomConfig) {
|
|
205
|
+
const event = extractTrackingEvent(node, ancestors, filePath, constantMap, matchedCustomConfig);
|
|
206
|
+
if (event) events.push(event);
|
|
207
|
+
} else {
|
|
208
|
+
const event = extractTrackingEvent(node, ancestors, filePath, constantMap, null);
|
|
209
|
+
if (event) events.push(event);
|
|
85
210
|
}
|
|
86
211
|
} catch (error) {
|
|
87
212
|
console.error(`Error processing node in ${filePath}:`, error.message);
|
|
@@ -97,25 +222,19 @@ function findTrackingEvents(ast, filePath, customFunction) {
|
|
|
97
222
|
* @param {Object} node - CallExpression node
|
|
98
223
|
* @param {Array<Object>} ancestors - Ancestor nodes
|
|
99
224
|
* @param {string} filePath - File path
|
|
100
|
-
* @param {
|
|
225
|
+
* @param {Object} constantMap - Constant string map
|
|
226
|
+
* @param {Object} [customConfig] - Custom function configuration object
|
|
101
227
|
* @returns {Object|null} Extracted event or null
|
|
102
228
|
*/
|
|
103
|
-
function extractTrackingEvent(node, ancestors, filePath,
|
|
104
|
-
|
|
105
|
-
const source = detectAnalyticsSource(node, customFunction);
|
|
229
|
+
function extractTrackingEvent(node, ancestors, filePath, constantMap, customConfig) {
|
|
230
|
+
const source = detectAnalyticsSource(node, customConfig?.functionName);
|
|
106
231
|
if (source === 'unknown') {
|
|
107
232
|
return null;
|
|
108
233
|
}
|
|
109
|
-
|
|
110
|
-
// Extract event data based on the source
|
|
111
|
-
const eventData = extractEventData(node, source);
|
|
112
|
-
|
|
113
|
-
// Get location and context information
|
|
234
|
+
const eventData = extractEventData(node, source, constantMap, customConfig);
|
|
114
235
|
const line = node.loc.start.line;
|
|
115
236
|
const functionName = findWrappingFunction(node, ancestors);
|
|
116
|
-
|
|
117
|
-
// Process the event data into final format
|
|
118
|
-
return processEventData(eventData, source, filePath, line, functionName);
|
|
237
|
+
return processEventData(eventData, source, filePath, line, functionName, customConfig);
|
|
119
238
|
}
|
|
120
239
|
|
|
121
240
|
module.exports = {
|
|
@@ -40,7 +40,7 @@ async function initPyodide() {
|
|
|
40
40
|
* libraries, extracting event names, properties, and metadata.
|
|
41
41
|
*
|
|
42
42
|
* @param {string} filePath - Path to the Python file to analyze
|
|
43
|
-
* @param {string} [
|
|
43
|
+
* @param {string} [customFunctionSignature=null] - Signature of a custom tracking function to detect
|
|
44
44
|
* @returns {Promise<Array<Object>>} Array of tracking events found in the file
|
|
45
45
|
* @returns {Promise<Array>} Empty array if an error occurs
|
|
46
46
|
*
|
|
@@ -52,48 +52,54 @@ async function initPyodide() {
|
|
|
52
52
|
* // With custom tracking function
|
|
53
53
|
* const events = await analyzePythonFile('./app.py', 'track_event');
|
|
54
54
|
*/
|
|
55
|
-
async function analyzePythonFile(filePath,
|
|
55
|
+
async function analyzePythonFile(filePath, customFunctionSignatures = null) {
|
|
56
56
|
// Validate inputs
|
|
57
57
|
if (!filePath || typeof filePath !== 'string') {
|
|
58
58
|
console.error('Invalid file path provided');
|
|
59
59
|
return [];
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
}
|
|
62
|
+
// Check if file exists before reading
|
|
63
|
+
if (!fs.existsSync(filePath)) {
|
|
64
|
+
console.error(`File not found: ${filePath}`);
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
68
67
|
|
|
69
|
-
|
|
68
|
+
try {
|
|
69
|
+
// Read the Python file only once
|
|
70
70
|
const code = fs.readFileSync(filePath, 'utf8');
|
|
71
|
-
|
|
71
|
+
|
|
72
72
|
// Initialize Pyodide if not already done
|
|
73
73
|
const py = await initPyodide();
|
|
74
|
-
|
|
75
|
-
// Load the Python analyzer code
|
|
74
|
+
|
|
75
|
+
// Load the Python analyzer code (idempotent – redefining functions is fine)
|
|
76
76
|
const analyzerPath = path.join(__dirname, 'pythonTrackingAnalyzer.py');
|
|
77
77
|
if (!fs.existsSync(analyzerPath)) {
|
|
78
78
|
throw new Error(`Python analyzer not found at: ${analyzerPath}`);
|
|
79
79
|
}
|
|
80
|
-
|
|
81
80
|
const analyzerCode = fs.readFileSync(analyzerPath, 'utf8');
|
|
82
|
-
|
|
83
|
-
// Set up Python environment with necessary variables
|
|
84
|
-
py.globals.set('code', code);
|
|
85
|
-
py.globals.set('filepath', filePath);
|
|
86
|
-
py.globals.set('custom_function', customFunction);
|
|
87
|
-
// Set __name__ to null to prevent execution of main block
|
|
81
|
+
// Prevent the analyzer from executing any __main__ blocks that expect CLI usage
|
|
88
82
|
py.globals.set('__name__', null);
|
|
89
|
-
|
|
90
|
-
// Load and run the analyzer
|
|
91
83
|
py.runPython(analyzerCode);
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
84
|
+
|
|
85
|
+
// Helper to run analysis with a given custom config (can be null)
|
|
86
|
+
const runAnalysis = (customConfig) => {
|
|
87
|
+
py.globals.set('code', code);
|
|
88
|
+
py.globals.set('filepath', filePath);
|
|
89
|
+
py.globals.set('custom_config_json', customConfig ? JSON.stringify(customConfig) : null);
|
|
90
|
+
py.runPython('import json');
|
|
91
|
+
py.runPython('custom_config = None if custom_config_json == None else json.loads(custom_config_json)');
|
|
92
|
+
const result = py.runPython('analyze_python_code(code, filepath, custom_config)');
|
|
93
|
+
return JSON.parse(result);
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Prepare config argument (array or null)
|
|
97
|
+
const configArg = Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0
|
|
98
|
+
? customFunctionSignatures
|
|
99
|
+
: null;
|
|
100
|
+
|
|
101
|
+
const events = runAnalysis(configArg);
|
|
102
|
+
|
|
97
103
|
return events;
|
|
98
104
|
} catch (error) {
|
|
99
105
|
// Log detailed error information for debugging
|