Skip to content

Commit 4fb4ec4

Browse files
committed
Add custom function signature parsing with flexible event extraction
1 parent f9eedde commit 4fb4ec4

File tree

25 files changed

+366
-114
lines changed

25 files changed

+366
-114
lines changed

src/analyze/go/eventExtractor.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -145,8 +145,8 @@ function extractSnowplowEventName(callNode) {
145145
* @returns {string|null} Event name or null if not found
146146
*/
147147
function extractCustomEventName(callNode) {
148-
if (callNode.args && callNode.args.length > 0) {
149-
return extractStringValue(callNode.args[0]);
148+
if (callNode.args && callNode.args.length > 1) {
149+
return extractStringValue(callNode.args[1]);
150150
}
151151
return null;
152152
}

src/analyze/go/index.js

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,21 @@ const { extractGoAST } = require('./goAstParser');
88
const { buildTypeContext } = require('./typeContext');
99
const { deduplicateEvents } = require('./eventDeduplicator');
1010
const { extractEventsFromBody } = require('./astTraversal');
11+
const { processGoFile } = require('./utils');
12+
const { parseCustomFunctionSignature } = require('../utils/customFunctionParser');
1113

1214
/**
1315
* Analyze a Go file and extract tracking events
1416
* @param {string} filePath - Path to the Go file to analyze
15-
* @param {string|null} customFunction - Name of custom tracking function to detect (optional)
17+
* @param {string|null} customFunctionSignature - Signature of custom tracking function to detect (optional)
1618
* @returns {Promise<Array>} Array of tracking events found in the file
1719
* @throws {Error} If the file cannot be read or parsed
1820
*/
19-
async function analyzeGoFile(filePath, customFunction) {
21+
async function analyzeGoFile(filePath, customFunctionSignature) {
2022
try {
23+
const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null;
24+
const customFunction = customConfig ? customConfig.functionName : null;
25+
2126
// Read the Go file
2227
const source = fs.readFileSync(filePath, 'utf8');
2328

src/analyze/go/propertyExtractor.js

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,8 +272,24 @@ function extractSnowplowProperties(callNode, properties, typeContext, currentFun
272272
* @param {string} currentFunction - Current function context for type lookups
273273
*/
274274
function extractCustomProperties(callNode, properties, typeContext, currentFunction) {
275-
if (callNode.args && callNode.args.length > 1) {
276-
extractPropertiesFromExpr(callNode.args[1], properties, typeContext, currentFunction);
275+
if (!callNode.args || callNode.args.length === 0) return;
276+
277+
// Add userId from first argument if present
278+
const userIdArg = callNode.args[0];
279+
if (userIdArg) {
280+
properties['userId'] = { type: 'string' };
281+
}
282+
283+
// Properties map is expected to be third argument in new signature, fallback to second for legacy
284+
let propsArg = null;
285+
if (callNode.args.length >= 3) {
286+
propsArg = callNode.args[2];
287+
} else if (callNode.args.length >= 2) {
288+
propsArg = callNode.args[1];
289+
}
290+
291+
if (propsArg) {
292+
extractPropertiesFromExpr(propsArg, properties, typeContext, currentFunction);
277293
}
278294
}
279295

src/analyze/javascript/extractors/event-extractor.js

Lines changed: 60 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,22 @@ const EXTRACTION_STRATEGIES = {
2020
googleanalytics: extractGoogleAnalyticsEvent,
2121
snowplow: extractSnowplowEvent,
2222
mparticle: extractMparticleEvent,
23+
custom: extractCustomEvent,
2324
default: extractDefaultEvent
2425
};
2526

2627
/**
2728
* Extracts event information from a CallExpression node
2829
* @param {Object} node - AST CallExpression node
2930
* @param {string} source - Analytics provider source
31+
* @param {Object} customConfig - Parsed custom function configuration
3032
* @returns {EventData} Extracted event data
3133
*/
32-
function extractEventData(node, source) {
34+
function extractEventData(node, source, customConfig) {
3335
const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
36+
if (source === 'custom') {
37+
return strategy(node, customConfig);
38+
}
3439
return strategy(node);
3540
}
3641

@@ -113,16 +118,41 @@ function extractDefaultEvent(node) {
113118
return { eventName, propertiesNode };
114119
}
115120

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+
116145
/**
117146
* Processes extracted event data into final event object
118147
* @param {EventData} eventData - Raw event data
119148
* @param {string} source - Analytics source
120149
* @param {string} filePath - File path
121150
* @param {number} line - Line number
122151
* @param {string} functionName - Containing function name
152+
* @param {Object} customConfig - Parsed custom function configuration
123153
* @returns {Object|null} Processed event object or null
124154
*/
125-
function processEventData(eventData, source, filePath, line, functionName) {
155+
function processEventData(eventData, source, filePath, line, functionName, customConfig) {
126156
const { eventName, propertiesNode } = eventData;
127157

128158
if (!eventName || !propertiesNode || propertiesNode.type !== NODE_TYPES.OBJECT_EXPRESSION) {
@@ -131,6 +161,15 @@ function processEventData(eventData, source, filePath, line, functionName) {
131161

132162
let properties = extractProperties(propertiesNode);
133163

164+
// Handle custom extra params
165+
if (source === 'custom' && customConfig && eventData.extraArgs) {
166+
for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
167+
properties[paramName] = {
168+
type: inferNodeValueType(argNode)
169+
};
170+
}
171+
}
172+
134173
// Special handling for Snowplow: remove 'action' from properties
135174
if (source === 'snowplow' && properties.action) {
136175
delete properties.action;
@@ -173,6 +212,25 @@ function findPropertyByKey(objectNode, key) {
173212
);
174213
}
175214

215+
/**
216+
* Infers the type of a value from an AST node (simple heuristic)
217+
* @param {Object} node - AST node
218+
* @returns {string} inferred type
219+
*/
220+
function inferNodeValueType(node) {
221+
if (!node) return 'any';
222+
switch (node.type) {
223+
case NODE_TYPES.LITERAL:
224+
return typeof node.value;
225+
case NODE_TYPES.OBJECT_EXPRESSION:
226+
return 'object';
227+
case NODE_TYPES.ARRAY_EXPRESSION:
228+
return 'array';
229+
default:
230+
return 'any';
231+
}
232+
}
233+
176234
module.exports = {
177235
extractEventData,
178236
processEventData

src/analyze/javascript/index.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,24 @@
44
*/
55

66
const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./parser');
7+
const { parseCustomFunctionSignature } = require('../utils/customFunctionParser');
78

89
/**
910
* Analyzes a JavaScript file for analytics tracking calls
1011
* @param {string} filePath - Path to the JavaScript file to analyze
1112
* @param {string} [customFunction] - Optional custom function name to detect
1213
* @returns {Array<Object>} Array of tracking events found in the file
1314
*/
14-
function analyzeJsFile(filePath, customFunction) {
15+
function analyzeJsFile(filePath, customFunctionSignature) {
1516
const events = [];
17+
const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null;
1618

1719
try {
1820
// Parse the file into an AST
1921
const ast = parseFile(filePath);
2022

2123
// Find and extract tracking events
22-
const foundEvents = findTrackingEvents(ast, filePath, customFunction);
24+
const foundEvents = findTrackingEvents(ast, filePath, customConfig);
2325
events.push(...foundEvents);
2426

2527
} catch (error) {

src/analyze/javascript/parser.js

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -70,16 +70,16 @@ function parseFile(filePath) {
7070
* Walks the AST and finds analytics tracking calls
7171
* @param {Object} ast - Parsed AST
7272
* @param {string} filePath - Path to the file being analyzed
73-
* @param {string} [customFunction] - Custom function name to detect
73+
* @param {Object} [customConfig] - Custom function configuration object
7474
* @returns {Array<Object>} Array of found events
7575
*/
76-
function findTrackingEvents(ast, filePath, customFunction) {
76+
function findTrackingEvents(ast, filePath, customConfig) {
7777
const events = [];
7878

7979
walk.ancestor(ast, {
8080
[NODE_TYPES.CALL_EXPRESSION]: (node, ancestors) => {
8181
try {
82-
const event = extractTrackingEvent(node, ancestors, filePath, customFunction);
82+
const event = extractTrackingEvent(node, ancestors, filePath, customConfig);
8383
if (event) {
8484
events.push(event);
8585
}
@@ -97,25 +97,25 @@ function findTrackingEvents(ast, filePath, customFunction) {
9797
* @param {Object} node - CallExpression node
9898
* @param {Array<Object>} ancestors - Ancestor nodes
9999
* @param {string} filePath - File path
100-
* @param {string} [customFunction] - Custom function name
100+
* @param {Object} [customConfig] - Custom function configuration object
101101
* @returns {Object|null} Extracted event or null
102102
*/
103-
function extractTrackingEvent(node, ancestors, filePath, customFunction) {
103+
function extractTrackingEvent(node, ancestors, filePath, customConfig) {
104104
// Detect the analytics source
105-
const source = detectAnalyticsSource(node, customFunction);
105+
const source = detectAnalyticsSource(node, customConfig?.functionName);
106106
if (source === 'unknown') {
107107
return null;
108108
}
109109

110110
// Extract event data based on the source
111-
const eventData = extractEventData(node, source);
111+
const eventData = extractEventData(node, source, customConfig);
112112

113113
// Get location and context information
114114
const line = node.loc.start.line;
115115
const functionName = findWrappingFunction(node, ancestors);
116116

117117
// Process the event data into final format
118-
return processEventData(eventData, source, filePath, line, functionName);
118+
return processEventData(eventData, source, filePath, line, functionName, customConfig);
119119
}
120120

121121
module.exports = {

src/analyze/python/index.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
const fs = require('fs');
77
const path = require('path');
8+
const { parseCustomFunctionSignature } = require('../utils/customFunctionParser');
89

910
// Singleton instance of Pyodide
1011
let pyodide = null;
@@ -40,7 +41,7 @@ async function initPyodide() {
4041
* libraries, extracting event names, properties, and metadata.
4142
*
4243
* @param {string} filePath - Path to the Python file to analyze
43-
* @param {string} [customFunction=null] - Name of a custom tracking function to detect
44+
* @param {string} [customFunctionSignature=null] - Signature of a custom tracking function to detect
4445
* @returns {Promise<Array<Object>>} Array of tracking events found in the file
4546
* @returns {Promise<Array>} Empty array if an error occurs
4647
*
@@ -52,7 +53,10 @@ async function initPyodide() {
5253
* // With custom tracking function
5354
* const events = await analyzePythonFile('./app.py', 'track_event');
5455
*/
55-
async function analyzePythonFile(filePath, customFunction = null) {
56+
async function analyzePythonFile(filePath, customFunctionSignature = null) {
57+
const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null;
58+
const customFunction = customConfig ? customConfig.functionName : null;
59+
5660
// Validate inputs
5761
if (!filePath || typeof filePath !== 'string') {
5862
console.error('Invalid file path provided');

src/analyze/python/pythonTrackingAnalyzer.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -430,7 +430,10 @@ def _extract_snowplow_event_name(self, node: ast.Call) -> Optional[str]:
430430

431431
def _extract_custom_event_name(self, node: ast.Call) -> Optional[str]:
432432
"""Extract event name for custom tracking function."""
433-
# Standard format: customFunction('event_name', {...})
433+
# Support signatures where event name might be second argument (userId first)
434+
if len(node.args) >= 2 and isinstance(node.args[1], ast.Constant):
435+
return node.args[1].value
436+
# Fallback to legacy format: first argument is event name
434437
if len(node.args) >= 1 and isinstance(node.args[0], ast.Constant):
435438
return node.args[0].value
436439
return None
@@ -502,6 +505,10 @@ def _extract_user_id(self, node: ast.Call, source: str) -> EventProperties:
502505
# Check if event is not anonymous and extract distinct_id
503506
user_id_props.update(self._extract_posthog_user_id(node))
504507

508+
elif source == 'custom':
509+
if len(node.args) >= 1 and self._is_non_null_value(node.args[0]):
510+
user_id_props["userId"] = {"type": "string"}
511+
505512
return user_id_props
506513

507514
def _is_non_null_value(self, node: ast.AST) -> bool:
@@ -559,10 +566,13 @@ def _get_properties_node(self, node: ast.Call, source: str) -> Optional[ast.Dict
559566
return keyword.value
560567

561568
elif source == 'custom':
562-
# Properties are in the second argument
563-
if len(node.args) > 1:
569+
# New custom function format: customFunction(userId, 'event_name', {...})
570+
if len(node.args) >= 3 and isinstance(node.args[2], ast.Dict):
571+
return node.args[2]
572+
# Fallback legacy two-arg format
573+
if len(node.args) >= 2 and isinstance(node.args[1], ast.Dict):
564574
return node.args[1]
565-
575+
566576
elif source == 'posthog':
567577
# Check named parameters first, then positional
568578
for keyword in node.keywords:

0 commit comments

Comments
 (0)