diff --git a/README.md b/README.md index 99608aa..332f69c 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,13 @@ yourCustomTrackFunctionName(userId, EVENT_NAME, PROPERTIES) If your function follows the standard format `yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES)`, you can simply pass in `yourCustomTrackFunctionName` to `--customFunction` as a shorthand. +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. + +```sh +npx @flisk/analyze-tracking /path/to/project --customFunction "yourFunc1" --customFunction "yourFunc2(userId, EVENT_NAME, PROPERTIES)" +npx @flisk/analyze-tracking /path/to/project -c "yourFunc1" "yourFunc2(userId, EVENT_NAME, PROPERTIES)" +``` + ## What's Generated? A clear YAML schema that shows where your events are tracked, their properties, and more. diff --git a/bin/cli.js b/bin/cli.js index b6c17e7..b4e6d30 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -43,6 +43,7 @@ const optionDefinitions = [ name: 'customFunction', alias: 'c', type: String, + multiple: true, }, { name: 'repositoryUrl', diff --git a/src/analyze/go/index.js b/src/analyze/go/index.js index 812e5b7..cfe7248 100644 --- a/src/analyze/go/index.js +++ b/src/analyze/go/index.js @@ -8,47 +8,63 @@ const { extractGoAST } = require('./goAstParser'); const { buildTypeContext } = require('./typeContext'); const { deduplicateEvents } = require('./eventDeduplicator'); const { extractEventsFromBody } = require('./astTraversal'); -const { processGoFile } = require('./utils'); -const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); /** * Analyze a Go file and extract tracking events * @param {string} filePath - Path to the Go file to analyze - * @param {string|null} customFunctionSignature - Signature of custom tracking function to detect (optional) + * @param {string|null} customFunctionSignatures - Signature of custom tracking function to detect (optional) * @returns {Promise} Array of tracking events found in the file * @throws {Error} If the file cannot be read or parsed */ -async function analyzeGoFile(filePath, customFunctionSignature) { +async function analyzeGoFile(filePath, customFunctionSignatures = null) { try { - const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; - // Read the Go file const source = fs.readFileSync(filePath, 'utf8'); - - // Parse the Go file using goAstParser + + // Parse the Go file using goAstParser (once) const ast = extractGoAST(source); - + // First pass: build type information for functions and variables const typeContext = buildTypeContext(ast); - - // Extract tracking events from the AST - const events = []; - let currentFunction = 'global'; - - // Walk through the AST - for (const node of ast) { - if (node.tag === 'func') { - currentFunction = node.name; - // Process the function body - if (node.body) { - extractEventsFromBody(node.body, events, filePath, currentFunction, customConfig, typeContext, currentFunction); + + const collectEventsForConfig = (customConfig) => { + const events = []; + let currentFunction = 'global'; + for (const node of ast) { + if (node.tag === 'func') { + currentFunction = node.name; + if (node.body) { + extractEventsFromBody( + node.body, + events, + filePath, + currentFunction, + customConfig, + typeContext, + currentFunction + ); + } } } + return events; + }; + + let events = []; + + // Built-in providers pass (null custom config) + events.push(...collectEventsForConfig(null)); + + // Custom configs passes + if (Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0) { + for (const customConfig of customFunctionSignatures) { + if (!customConfig) continue; + events.push(...collectEventsForConfig(customConfig)); + } } - + // Deduplicate events based on eventName, source, and function const uniqueEvents = deduplicateEvents(events); - + return uniqueEvents; } catch (error) { console.error(`Error analyzing Go file ${filePath}:`, error.message); diff --git a/src/analyze/index.js b/src/analyze/index.js index b47f0e5..f78ae4b 100644 --- a/src/analyze/index.js +++ b/src/analyze/index.js @@ -5,6 +5,7 @@ const path = require('path'); const ts = require('typescript'); +const { parseCustomFunctionSignature } = require('./utils/customFunctionParser'); const { getAllFiles } = require('../utils/fileProcessor'); const { analyzeJsFile } = require('./javascript'); const { analyzeTsFile } = require('./typescript'); @@ -12,9 +13,11 @@ const { analyzePythonFile } = require('./python'); const { analyzeRubyFile } = require('./ruby'); const { analyzeGoFile } = require('./go'); -async function analyzeDirectory(dirPath, customFunction) { +async function analyzeDirectory(dirPath, customFunctions) { const allEvents = {}; + const customFunctionSignatures = (customFunctions && customFunctions?.length > 0) ? customFunctions.map(parseCustomFunctionSignature) : null; + const files = getAllFiles(dirPath); const tsFiles = files.filter(file => /\.(tsx?)$/.test(file)); const tsProgram = ts.createProgram(tsFiles, { @@ -32,15 +35,15 @@ async function analyzeDirectory(dirPath, customFunction) { const isGoFile = /\.(go)$/.test(file); if (isJsFile) { - events = analyzeJsFile(file, customFunction); + events = analyzeJsFile(file, customFunctionSignatures); } else if (isTsFile) { - events = analyzeTsFile(file, tsProgram, customFunction); + events = analyzeTsFile(file, tsProgram, customFunctionSignatures); } else if (isPythonFile) { - events = await analyzePythonFile(file, customFunction); + events = await analyzePythonFile(file, customFunctionSignatures); } else if (isRubyFile) { - events = await analyzeRubyFile(file, customFunction); + events = await analyzeRubyFile(file, customFunctionSignatures); } else if (isGoFile) { - events = await analyzeGoFile(file, customFunction); + events = await analyzeGoFile(file, customFunctionSignatures); } else { continue; } diff --git a/src/analyze/javascript/extractors/event-extractor.js b/src/analyze/javascript/extractors/event-extractor.js index 071f437..eaa7f7e 100644 --- a/src/analyze/javascript/extractors/event-extractor.js +++ b/src/analyze/javascript/extractors/event-extractor.js @@ -164,9 +164,18 @@ function processEventData(eventData, source, filePath, line, functionName, custo // Handle custom extra params if (source === 'custom' && customConfig && eventData.extraArgs) { for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) { - properties[paramName] = { - type: inferNodeValueType(argNode) - }; + if (argNode && argNode.type === NODE_TYPES.OBJECT_EXPRESSION) { + // Extract detailed properties from object expression + properties[paramName] = { + type: 'object', + properties: extractProperties(argNode) + }; + } else { + // For non-object arguments, use simple type inference + properties[paramName] = { + type: inferNodeValueType(argNode) + }; + } } } diff --git a/src/analyze/javascript/index.js b/src/analyze/javascript/index.js index 8a3a08f..582e426 100644 --- a/src/analyze/javascript/index.js +++ b/src/analyze/javascript/index.js @@ -4,7 +4,6 @@ */ const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./parser'); -const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); /** * Analyzes a JavaScript file for analytics tracking calls @@ -12,17 +11,22 @@ const { parseCustomFunctionSignature } = require('../utils/customFunctionParser' * @param {string} [customFunction] - Optional custom function name to detect * @returns {Array} Array of tracking events found in the file */ -function analyzeJsFile(filePath, customFunctionSignature) { - const events = []; - const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; - +function analyzeJsFile(filePath, customFunctionSignatures = null) { try { - // Parse the file into an AST + // Parse the file into an AST once const ast = parseFile(filePath); - // Find and extract tracking events - const foundEvents = findTrackingEvents(ast, filePath, customConfig); - events.push(...foundEvents); + // Single pass extraction covering built-in + all custom configs + const events = findTrackingEvents(ast, filePath, customFunctionSignatures || []); + + // Deduplicate events (by source | eventName | line | functionName) + const unique = new Map(); + for (const evt of events) { + const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`; + if (!unique.has(key)) unique.set(key, evt); + } + + return Array.from(unique.values()); } catch (error) { if (error instanceof FileReadError) { @@ -34,7 +38,7 @@ function analyzeJsFile(filePath, customFunctionSignature) { } } - return events; + return []; } module.exports = { analyzeJsFile }; diff --git a/src/analyze/javascript/parser.js b/src/analyze/javascript/parser.js index 2616b4b..ce3e8e2 100644 --- a/src/analyze/javascript/parser.js +++ b/src/analyze/javascript/parser.js @@ -66,22 +66,95 @@ function parseFile(filePath) { } } +// --------------------------------------------- +// Helper – custom function matcher +// --------------------------------------------- + /** - * Walks the AST and finds analytics tracking calls - * @param {Object} ast - Parsed AST - * @param {string} filePath - Path to the file being analyzed - * @param {Object} [customConfig] - Custom function configuration object - * @returns {Array} Array of found events + * Determines whether a CallExpression node matches the provided custom function name. + * Supports both simple identifiers (e.g. myTrack) and dot-separated members (e.g. Custom.track). + * The logic mirrors isCustomFunction from detectors/analytics-source.js but is kept local to avoid + * circular dependencies. + * @param {Object} node – CallExpression AST node + * @param {string} fnName – Custom function name (could include dots) + * @returns {boolean} */ -function findTrackingEvents(ast, filePath, customConfig) { +function nodeMatchesCustomFunction(node, fnName) { + if (!fnName || !node.callee) return false; + + const parts = fnName.split('.'); + + // Simple identifier case + if (parts.length === 1) { + return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name === fnName; + } + + // Member expression chain case + if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) { + return false; + } + + // Walk the chain from the right-most property to the leftmost object + let currentNode = node.callee; + let idx = parts.length - 1; + + while (currentNode && idx >= 0) { + const expected = parts[idx]; + + if (currentNode.type === NODE_TYPES.MEMBER_EXPRESSION) { + if ( + currentNode.property.type !== NODE_TYPES.IDENTIFIER || + currentNode.property.name !== expected + ) { + return false; + } + currentNode = currentNode.object; + idx -= 1; + } else if (currentNode.type === NODE_TYPES.IDENTIFIER) { + return idx === 0 && currentNode.name === expected; + } else { + return false; + } + } + + return false; +} + +/** + * Walk the AST once and find tracking events for built-in providers plus any number of custom + * function configurations. This avoids the previous O(n * customConfigs) behaviour. + * + * @param {Object} ast – Parsed AST of the source file + * @param {string} filePath – Absolute/relative path to the source file + * @param {Object[]} [customConfigs=[]] – Array of parsed custom function configurations + * @returns {Array} – List of extracted tracking events + */ +function findTrackingEvents(ast, filePath, customConfigs = []) { const events = []; walk.ancestor(ast, { [NODE_TYPES.CALL_EXPRESSION]: (node, ancestors) => { try { - const event = extractTrackingEvent(node, ancestors, filePath, customConfig); - if (event) { - events.push(event); + let matchedCustomConfig = null; + + // Attempt to match any custom function first to avoid mis-classifying built-in providers + if (Array.isArray(customConfigs) && customConfigs.length > 0) { + for (const cfg of customConfigs) { + if (cfg && nodeMatchesCustomFunction(node, cfg.functionName)) { + matchedCustomConfig = cfg; + break; + } + } + } + + if (matchedCustomConfig) { + // Force source to 'custom' and use matched config + const event = extractTrackingEvent(node, ancestors, filePath, matchedCustomConfig); + if (event) events.push(event); + } else { + // Let built-in detector figure out source (pass undefined customFunction) + const event = extractTrackingEvent(node, ancestors, filePath, null); + if (event) events.push(event); } } catch (error) { console.error(`Error processing node in ${filePath}:`, error.message); diff --git a/src/analyze/python/index.js b/src/analyze/python/index.js index 0c1a0cd..80eb304 100644 --- a/src/analyze/python/index.js +++ b/src/analyze/python/index.js @@ -5,7 +5,6 @@ const fs = require('fs'); const path = require('path'); -const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); // Singleton instance of Pyodide let pyodide = null; @@ -53,52 +52,54 @@ async function initPyodide() { * // With custom tracking function * const events = await analyzePythonFile('./app.py', 'track_event'); */ -async function analyzePythonFile(filePath, customFunctionSignature = null) { - const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; - +async function analyzePythonFile(filePath, customFunctionSignatures = null) { // Validate inputs if (!filePath || typeof filePath !== 'string') { console.error('Invalid file path provided'); return []; } - try { - // Check if file exists before reading - if (!fs.existsSync(filePath)) { - console.error(`File not found: ${filePath}`); - return []; - } + // Check if file exists before reading + if (!fs.existsSync(filePath)) { + console.error(`File not found: ${filePath}`); + return []; + } - // Read the Python file + try { + // Read the Python file only once const code = fs.readFileSync(filePath, 'utf8'); - + // Initialize Pyodide if not already done const py = await initPyodide(); - - // Load the Python analyzer code + + // Load the Python analyzer code (idempotent – redefining functions is fine) const analyzerPath = path.join(__dirname, 'pythonTrackingAnalyzer.py'); if (!fs.existsSync(analyzerPath)) { throw new Error(`Python analyzer not found at: ${analyzerPath}`); } - const analyzerCode = fs.readFileSync(analyzerPath, 'utf8'); - - // Set up Python environment with necessary variables - py.globals.set('code', code); - py.globals.set('filepath', filePath); - py.globals.set('custom_config_json', customConfig ? JSON.stringify(customConfig) : null); - py.runPython('import json'); - py.runPython('custom_config = None if custom_config_json == None else json.loads(custom_config_json)'); - // Set __name__ to null to prevent execution of main block + // Prevent the analyzer from executing any __main__ blocks that expect CLI usage py.globals.set('__name__', null); - - // Load and run the analyzer py.runPython(analyzerCode); - - // Execute the analysis and parse result - const result = py.runPython('analyze_python_code(code, filepath, custom_config)'); - const events = JSON.parse(result); - + + // Helper to run analysis with a given custom config (can be null) + const runAnalysis = (customConfig) => { + py.globals.set('code', code); + py.globals.set('filepath', filePath); + py.globals.set('custom_config_json', customConfig ? JSON.stringify(customConfig) : null); + py.runPython('import json'); + py.runPython('custom_config = None if custom_config_json == None else json.loads(custom_config_json)'); + const result = py.runPython('analyze_python_code(code, filepath, custom_config)'); + return JSON.parse(result); + }; + + // Prepare config argument (array or null) + const configArg = Array.isArray(customFunctionSignatures) && customFunctionSignatures.length > 0 + ? customFunctionSignatures + : null; + + const events = runAnalysis(configArg); + return events; } catch (error) { // Log detailed error information for debugging diff --git a/src/analyze/python/pythonTrackingAnalyzer.py b/src/analyze/python/pythonTrackingAnalyzer.py index 6ebf9aa..2464cb4 100644 --- a/src/analyze/python/pythonTrackingAnalyzer.py +++ b/src/analyze/python/pythonTrackingAnalyzer.py @@ -806,33 +806,56 @@ def get_value_type(self, value: Any) -> str: return "null" return "any" -def analyze_python_code(code: str, filepath: str, custom_config: Optional[dict[str, any]] = None) -> str: +def analyze_python_code(code: str, filepath: str, custom_config: Optional[any] = None) -> str: """ Analyze Python code for analytics tracking calls. - - This function parses Python code and identifies analytics tracking calls, - extracting event names, properties, and metadata. - + + The function supports either a single custom configuration object or a list + of such objects, allowing detection of multiple custom tracking functions + without parsing the source code multiple times. + Args: code: The Python source code to analyze filepath: Path to the file being analyzed - custom_config: Optional custom configuration for custom tracking functions - + custom_config: None, a single custom config dict, or a list of configs + Returns: JSON string containing array of tracking events """ try: - # Parse the Python code + # Parse the Python code only once tree = ast.parse(code) - - # Create visitor and analyze - visitor = TrackingVisitor(filepath, custom_config) - visitor.visit(tree) - - # Return events as JSON - return json.dumps(visitor.events) - except Exception as e: - # Return empty array on parse errors + + events: List[AnalyticsEvent] = [] + + def run_visitor(cfg: Optional[dict]) -> None: + vis = TrackingVisitor(filepath, cfg) + vis.visit(tree) + events.extend(vis.events) + + # Built-in providers pass (no custom config) + run_visitor(None) + + # Handle list or single custom configuration + if custom_config: + if isinstance(custom_config, list): + for cfg in custom_config: + if cfg: + run_visitor(cfg) + else: + run_visitor(custom_config) # single config for backward compat + + # Deduplicate events (source|eventName|line|functionName) + unique: Dict[str, AnalyticsEvent] = {} + for evt in events: + key = f"{evt['source']}|{evt['eventName']}|{evt['line']}|{evt['functionName']}" + if key not in unique: + unique[key] = evt + + return json.dumps(list(unique.values())) + + except Exception: + # Return empty array on failure return json.dumps([]) # Command-line interface diff --git a/src/analyze/ruby/index.js b/src/analyze/ruby/index.js index 3bd2467..54a5320 100644 --- a/src/analyze/ruby/index.js +++ b/src/analyze/ruby/index.js @@ -5,7 +5,6 @@ const fs = require('fs'); const TrackingVisitor = require('./visitor'); -const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); // Lazy-loaded parse function from Ruby Prism let parse = null; @@ -17,7 +16,7 @@ let parse = null; * @returns {Promise} Array of tracking events found in the file * @throws {Error} If the file cannot be read or parsed */ -async function analyzeRubyFile(filePath, customFunctionSignature) { +async function analyzeRubyFile(filePath, customFunctionSignatures = null) { // Lazy load the Ruby Prism parser if (!parse) { const { loadPrism } = await import('@ruby/prism'); @@ -27,22 +26,28 @@ async function analyzeRubyFile(filePath, customFunctionSignature) { try { // Read the file content const code = fs.readFileSync(filePath, 'utf8'); - - // Parse the Ruby code into an AST + + // Parse the Ruby code into an AST once let ast; try { ast = await parse(code); } catch (parseError) { console.error(`Error parsing file ${filePath}:`, parseError.message); - return []; // Return empty events array if parsing fails + return []; } - // Create a visitor and analyze the AST - const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; - const visitor = new TrackingVisitor(code, filePath, customConfig); + // Single visitor pass covering all custom configs + const visitor = new TrackingVisitor(code, filePath, customFunctionSignatures || []); const events = await visitor.analyze(ast); - return events; + // Deduplicate events + const unique = new Map(); + for (const evt of events) { + const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`; + if (!unique.has(key)) unique.set(key, evt); + } + + return Array.from(unique.values()); } catch (fileError) { console.error(`Error reading or processing file ${filePath}:`, fileError.message); diff --git a/src/analyze/ruby/visitor.js b/src/analyze/ruby/visitor.js index 120ab4c..e29ca36 100644 --- a/src/analyze/ruby/visitor.js +++ b/src/analyze/ruby/visitor.js @@ -8,10 +8,10 @@ const { extractEventName, extractProperties } = require('./extractors'); const { findWrappingFunction, traverseNode, getLineNumber } = require('./traversal'); class TrackingVisitor { - constructor(code, filePath, customConfig = null) { + constructor(code, filePath, customConfigs = []) { this.code = code; this.filePath = filePath; - this.customConfig = customConfig; + this.customConfigs = Array.isArray(customConfigs) ? customConfigs : []; this.events = []; } @@ -22,10 +22,27 @@ class TrackingVisitor { */ async processCallNode(node, ancestors) { try { - const source = detectSource(node, this.customConfig?.functionName); + let matchedConfig = null; + let source = null; + + // Try to match any custom config first + for (const cfg of this.customConfigs) { + if (!cfg) continue; + if (detectSource(node, cfg.functionName) === 'custom') { + matchedConfig = cfg; + source = 'custom'; + break; + } + } + + // If no custom match, attempt built-in providers + if (!source) { + source = detectSource(node, null); + } + if (!source) return; - const eventName = extractEventName(node, source, this.customConfig); + const eventName = extractEventName(node, source, matchedConfig); if (!eventName) return; const line = getLineNumber(this.code, node.location); @@ -33,13 +50,13 @@ class TrackingVisitor { // For module-scoped custom functions, use the custom function name as the functionName // For simple custom functions, use the wrapping function name let functionName; - if (source === 'custom' && this.customConfig && this.customConfig.functionName.includes('.')) { - functionName = this.customConfig.functionName; + if (source === 'custom' && matchedConfig && matchedConfig.functionName.includes('.')) { + functionName = matchedConfig.functionName; } else { functionName = await findWrappingFunction(node, ancestors); } - const properties = await extractProperties(node, source, this.customConfig); + const properties = await extractProperties(node, source, matchedConfig); this.events.push({ eventName, diff --git a/src/analyze/typescript/extractors/event-extractor.js b/src/analyze/typescript/extractors/event-extractor.js index 1e70e38..a944a50 100644 --- a/src/analyze/typescript/extractors/event-extractor.js +++ b/src/analyze/typescript/extractors/event-extractor.js @@ -219,9 +219,31 @@ function processEventData(eventData, source, filePath, line, functionName, check // Handle custom extra params if (source === 'custom' && customConfig && eventData.extraArgs) { for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) { - cleanedProperties[paramName] = { - type: inferNodeValueType(argNode) - }; + if (argNode && ts.isObjectLiteralExpression(argNode)) { + // Extract detailed properties from object literal expression + cleanedProperties[paramName] = { + type: 'object', + properties: extractProperties(checker, argNode) + }; + } else if (argNode && ts.isIdentifier(argNode)) { + // Handle identifier references to objects + const resolvedNode = resolveIdentifierToInitializer(checker, argNode, sourceFile); + if (resolvedNode && ts.isObjectLiteralExpression(resolvedNode)) { + cleanedProperties[paramName] = { + type: 'object', + properties: extractProperties(checker, resolvedNode) + }; + } else { + cleanedProperties[paramName] = { + type: inferNodeValueType(argNode) + }; + } + } else { + // For non-object arguments, use simple type inference + cleanedProperties[paramName] = { + type: inferNodeValueType(argNode) + }; + } } } diff --git a/src/analyze/typescript/index.js b/src/analyze/typescript/index.js index 708ef7c..03de75d 100644 --- a/src/analyze/typescript/index.js +++ b/src/analyze/typescript/index.js @@ -4,7 +4,6 @@ */ const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = require('./parser'); -const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); /** * Analyzes a TypeScript file for analytics tracking calls @@ -13,14 +12,11 @@ const { parseCustomFunctionSignature } = require('../utils/customFunctionParser' * @param {string} [customFunctionSignature] - Optional custom function signature to detect * @returns {Array} Array of tracking events found in the file */ -function analyzeTsFile(filePath, program = null, customFunctionSignature = null) { - const events = []; - const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; - +function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) { try { - // Get or create TypeScript program + // Get or create TypeScript program (only once) const tsProgram = getProgram(filePath, program); - + // Get source file from program const sourceFile = tsProgram.getSourceFile(filePath); if (!sourceFile) { @@ -30,9 +26,17 @@ function analyzeTsFile(filePath, program = null, customFunctionSignature = null) // Get type checker const checker = tsProgram.getTypeChecker(); - // Find and extract tracking events - const foundEvents = findTrackingEvents(sourceFile, checker, filePath, customConfig); - events.push(...foundEvents); + // Single-pass collection covering built-in + all custom configs + const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []); + + // Deduplicate events + const unique = new Map(); + for (const evt of events) { + const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`; + if (!unique.has(key)) unique.set(key, evt); + } + + return Array.from(unique.values()); } catch (error) { if (error instanceof ProgramError) { @@ -44,7 +48,7 @@ function analyzeTsFile(filePath, program = null, customFunctionSignature = null) } } - return events; + return []; } module.exports = { analyzeTsFile }; diff --git a/src/analyze/typescript/parser.js b/src/analyze/typescript/parser.js index bd60e07..1ce3778 100644 --- a/src/analyze/typescript/parser.js +++ b/src/analyze/typescript/parser.js @@ -65,32 +65,55 @@ function getProgram(filePath, existingProgram) { * @param {Object} sourceFile - TypeScript source file * @param {Object} checker - TypeScript type checker * @param {string} filePath - Path to the file being analyzed - * @param {Object} [customConfig] - Custom function configuration + * @param {Array} [customConfigs] - Array of custom function configurations * @returns {Array} Array of found events */ -function findTrackingEvents(sourceFile, checker, filePath, customConfig) { +function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) { const events = []; /** - * Visitor function for AST traversal - * @param {Object} node - Current AST node + * Helper to test if a CallExpression matches a custom function name. + * We simply rely on node.expression.getText() which preserves the fully qualified name. */ + const matchesCustomFn = (callNode, fnName) => { + if (!fnName) return false; + try { + return callNode.expression && callNode.expression.getText() === fnName; + } catch { + return false; + } + }; + function visit(node) { try { if (ts.isCallExpression(node)) { - const event = extractTrackingEvent(node, sourceFile, checker, filePath, customConfig); - if (event) { - events.push(event); + let matchedCustom = null; + + if (Array.isArray(customConfigs) && customConfigs.length > 0) { + for (const cfg of customConfigs) { + if (cfg && matchesCustomFn(node, cfg.functionName)) { + matchedCustom = cfg; + break; + } + } } + + const event = extractTrackingEvent( + node, + sourceFile, + checker, + filePath, + matchedCustom /* may be null */ + ); + if (event) events.push(event); } - // Continue traversing the AST + ts.forEachChild(node, visit); } catch (error) { console.error(`Error processing node in ${filePath}:`, error.message); } } - // Start traversal from the root ts.forEachChild(sourceFile, visit); return events; diff --git a/src/index.js b/src/index.js index ea1822b..fb2e3fe 100644 --- a/src/index.js +++ b/src/index.js @@ -11,8 +11,8 @@ const { generateDescriptions } = require('./generateDescriptions'); const { ChatOpenAI } = require('@langchain/openai'); const { ChatVertexAI } = require('@langchain/google-vertexai'); -async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model, stdout, format) { - let events = await analyzeDirectory(targetDir, customFunction); +async function run(targetDir, outputPath, customFunctions, customSourceDetails, generateDescription, provider, model, stdout, format) { + let events = await analyzeDirectory(targetDir, customFunctions); if (generateDescription) { let llm; if (provider === 'openai') { diff --git a/tests/analyzeGo.test.js b/tests/analyzeGo.test.js index 02858ef..344f221 100644 --- a/tests/analyzeGo.test.js +++ b/tests/analyzeGo.test.js @@ -2,6 +2,7 @@ const test = require('node:test'); const assert = require('node:assert'); const path = require('path'); const { analyzeGoFile } = require('../src/analyze/go'); +const { parseCustomFunctionSignature } = require('../src/analyze/utils/customFunctionParser'); test.describe('analyzeGoFile', () => { const fixturesDir = path.join(__dirname, 'fixtures'); @@ -9,7 +10,8 @@ test.describe('analyzeGoFile', () => { test('should correctly analyze Go file with multiple tracking providers', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeGoFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeGoFile(testFilePath, customFunctionSignatures); // Sort events by eventName for consistent ordering events.sort((a, b) => a.eventName.localeCompare(b.eventName)); @@ -103,7 +105,8 @@ test.describe('analyzeGoFile', () => { test('should handle files without tracking events', async () => { const emptyTestFile = path.join(fixturesDir, 'go', 'empty.go'); - const events = await analyzeGoFile(emptyTestFile, 'customTrack'); + const customFunctionSignatures = [parseCustomFunctionSignature('customTrack')]; + const events = await analyzeGoFile(emptyTestFile, customFunctionSignatures); assert.deepStrictEqual(events, []); }); @@ -117,7 +120,8 @@ test.describe('analyzeGoFile', () => { test('should handle nested property types correctly', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeGoFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeGoFile(testFilePath, customFunctionSignatures); const customEvent = events.find(e => e.eventName === 'custom_event'); assert.ok(customEvent); @@ -141,7 +145,8 @@ test.describe('analyzeGoFile', () => { test('should match expected tracking-schema.yaml output', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeGoFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeGoFile(testFilePath, customFunctionSignatures); // Create a map of events by name for easier verification const eventMap = {}; @@ -252,9 +257,41 @@ test.describe('analyzeGoFile', () => { ]; for (const { sig, event } of variants) { - const events = await analyzeGoFile(testFilePath, sig); + const customFunctionSignatures = [parseCustomFunctionSignature(sig)]; + const events = await analyzeGoFile(testFilePath, customFunctionSignatures); const found = events.find(e => e.eventName === event && e.source === 'custom'); assert.ok(found, `Should detect ${event} for signature ${sig}`); } }); + + test('should detect events when multiple custom function signatures are provided together', async () => { + const variants = [ + 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction0', + 'customTrackFunction1(EVENT_NAME, PROPERTIES)', + 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', + 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)' + ]; + + const customFunctionSignatures = variants.map(parseCustomFunctionSignature); + const events = await analyzeGoFile(testFilePath, customFunctionSignatures); + + const expectedEventNames = [ + 'custom_event', + 'custom_event0', + 'custom_event1', + 'custom_event2', + 'custom_event3', + 'custom_event4' + ]; + + expectedEventNames.forEach(eventName => { + const evt = events.find(e => e.eventName === eventName && e.source === 'custom'); + assert.ok(evt, `Expected to find event ${eventName}`); + }); + + const builtInCount = events.filter(e => e.source !== 'custom').length; + assert.ok(builtInCount >= 5, 'Should still include built-in events'); + }); }); diff --git a/tests/analyzeJavaScript.test.js b/tests/analyzeJavaScript.test.js index 993aaab..1a7a265 100644 --- a/tests/analyzeJavaScript.test.js +++ b/tests/analyzeJavaScript.test.js @@ -2,6 +2,7 @@ const test = require('node:test'); const assert = require('node:assert'); const path = require('path'); const { analyzeJsFile } = require('../src/analyze/javascript'); +const { parseCustomFunctionSignature } = require('../src/analyze/utils/customFunctionParser'); test.describe('analyzeJsFile', () => { const fixturesDir = path.join(__dirname, 'fixtures'); @@ -9,7 +10,8 @@ test.describe('analyzeJsFile', () => { test('should correctly analyze JavaScript file with multiple tracking providers', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); // Sort events by line number for consistent ordering events.sort((a, b) => a.line - b.line); @@ -207,7 +209,8 @@ test.describe('analyzeJsFile', () => { fs.writeFileSync(emptyTestFile, '// Empty file\n'); } - const events = analyzeJsFile(emptyTestFile, 'customTrack'); + const customFunctionSignatures = [parseCustomFunctionSignature('customTrack')]; + const events = analyzeJsFile(emptyTestFile, customFunctionSignatures); assert.deepStrictEqual(events, []); }); @@ -221,7 +224,8 @@ test.describe('analyzeJsFile', () => { test('should handle nested property types correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); // Test nested object properties const eventWithNestedObj = events.find(e => e.properties.address); @@ -245,7 +249,8 @@ test.describe('analyzeJsFile', () => { test('should detect array types correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); // Test array of objects const pendoEvent = events.find(e => e.eventName === 'customer checkout'); @@ -266,7 +271,8 @@ test.describe('analyzeJsFile', () => { test('should handle different function contexts correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); // Test function declaration const funcDeclEvent = events.find(e => e.functionName === 'test12345678'); @@ -287,7 +293,8 @@ test.describe('analyzeJsFile', () => { test('should handle case variations in provider names', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); // mParticle is used with lowercase 'p' in the test file const mparticleEvent = events.find(e => e.source === 'mparticle'); @@ -297,7 +304,8 @@ test.describe('analyzeJsFile', () => { test('should exclude action field from Snowplow properties', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); const snowplowEvent = events.find(e => e.source === 'snowplow'); assert.ok(snowplowEvent); @@ -309,7 +317,8 @@ test.describe('analyzeJsFile', () => { test('should handle mParticle three-parameter format', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = analyzeJsFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); const mparticleEvent = events.find(e => e.source === 'mparticle'); assert.ok(mparticleEvent); @@ -329,9 +338,45 @@ test.describe('analyzeJsFile', () => { ]; variants.forEach(({ sig, event }) => { - const events = analyzeJsFile(testFilePath, sig); + const customFunctionSignatures = [parseCustomFunctionSignature(sig)]; + const events = analyzeJsFile(testFilePath, customFunctionSignatures); const found = events.find(e => e.eventName === event && e.source === 'custom'); assert.ok(found, `Should detect ${event} for signature ${sig}`); }); }); + + test('should detect events when multiple custom function signatures are provided together', () => { + const variants = [ + 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction0', + 'customTrackFunction1(EVENT_NAME, PROPERTIES)', + 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', + 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', + 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)' + ]; + + const customFunctionSignatures = variants.map(parseCustomFunctionSignature); + const events = analyzeJsFile(testFilePath, customFunctionSignatures); + + // Each variant triggers exactly one event in the fixture file + const expectedEventNames = [ + 'customEvent', + 'custom_event0', + 'custom_event1', + 'custom_event2', + 'custom_event3', + 'custom_event4', + 'custom_module_event' + ]; + + expectedEventNames.forEach(eventName => { + const evt = events.find(e => e.eventName === eventName && e.source === 'custom'); + assert.ok(evt, `Expected to find event ${eventName}`); + }); + + // Sanity check – ensure we did not lose built-in provider events + const builtInProvidersCount = events.filter(e => e.source !== 'custom').length; + assert.ok(builtInProvidersCount >= 10, 'Should still include built-in events'); + }); }); diff --git a/tests/analyzePython.test.js b/tests/analyzePython.test.js index 984c88c..36a45cb 100644 --- a/tests/analyzePython.test.js +++ b/tests/analyzePython.test.js @@ -3,6 +3,7 @@ const assert = require('node:assert'); const path = require('path'); const fs = require('fs'); const { analyzePythonFile } = require('../src/analyze/python'); +const { parseCustomFunctionSignature } = require('../src/analyze/utils/customFunctionParser'); test.describe('analyzePythonFile', () => { const fixturesDir = path.join(__dirname, 'fixtures'); @@ -10,7 +11,8 @@ test.describe('analyzePythonFile', () => { test('should correctly analyze Python file with multiple tracking providers', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzePythonFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzePythonFile(testFilePath, customFunctionSignatures); // Sort events by eventName for consistent ordering events.sort((a, b) => a.eventName.localeCompare(b.eventName)); @@ -124,7 +126,8 @@ test.describe('analyzePythonFile', () => { test('should handle files without tracking events', async () => { const emptyTestFile = path.join(fixturesDir, 'python', 'empty.py'); - const events = await analyzePythonFile(emptyTestFile, 'customTrack'); + const customFunctionSignatures = [parseCustomFunctionSignature('customTrack')]; + const events = await analyzePythonFile(emptyTestFile, customFunctionSignatures); assert.deepStrictEqual(events, []); }); @@ -138,7 +141,8 @@ test.describe('analyzePythonFile', () => { test('should handle nested property types correctly', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzePythonFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzePythonFile(testFilePath, customFunctionSignatures); const customEvent = events.find(e => e.eventName === 'custom_event'); assert.ok(customEvent); @@ -154,7 +158,8 @@ test.describe('analyzePythonFile', () => { test('should match expected tracking-schema.yaml output', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzePythonFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzePythonFile(testFilePath, customFunctionSignatures); // Create a map of events by name for easier verification const eventMap = {}; @@ -294,7 +299,8 @@ def customTrackFunction(user_id: str, event_name: str, params: Dict[str, Any]) - pass `); - const events = await analyzePythonFile(typeTestFile, 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'); + const customFunctionSignatures = [parseCustomFunctionSignature('customTrackFunction(userId, EVENT_NAME, PROPERTIES)')]; + const events = await analyzePythonFile(typeTestFile, customFunctionSignatures); assert.strictEqual(events.length, 1); const event = events[0]; @@ -325,9 +331,43 @@ def customTrackFunction(user_id: str, event_name: str, params: Dict[str, Any]) - ]; for (const { sig, event } of variants) { - const events = await analyzePythonFile(testFilePath, sig); + const customFunctionSignatures = [parseCustomFunctionSignature(sig)]; + const events = await analyzePythonFile(testFilePath, customFunctionSignatures); const found = events.find(e => e.eventName === event && e.source === 'custom'); assert.ok(found, `Should detect ${event} for signature ${sig}`); } }); + + test('should detect events when multiple custom function signatures are provided together', async () => { + const variants = [ + 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction0', + 'customTrackFunction1(EVENT_NAME, PROPERTIES)', + 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', + 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', + 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)' + ]; + + const customFunctionSignatures = variants.map(parseCustomFunctionSignature); + const events = await analyzePythonFile(testFilePath, customFunctionSignatures); + + const expectedEventNames = [ + 'custom_event', + 'custom_event0', + 'custom_event1', + 'custom_event2', + 'custom_event3', + 'custom_event4', + 'custom_module_event' + ]; + + expectedEventNames.forEach(eventName => { + const evt = events.find(e => e.eventName === eventName && e.source === 'custom'); + assert.ok(evt, `Expected to find event ${eventName}`); + }); + + const builtInCount = events.filter(e => e.source !== 'custom').length; + assert.ok(builtInCount >= 7, 'Should still include built-in events'); + }); }); diff --git a/tests/analyzeRuby.test.js b/tests/analyzeRuby.test.js index 5781985..25468e0 100644 --- a/tests/analyzeRuby.test.js +++ b/tests/analyzeRuby.test.js @@ -2,6 +2,7 @@ const test = require('node:test'); const assert = require('node:assert'); const path = require('path'); const { analyzeRubyFile } = require('../src/analyze/ruby'); +const { parseCustomFunctionSignature } = require('../src/analyze/utils/customFunctionParser'); test.describe('analyzeRubyFile', () => { const fixturesDir = path.join(__dirname, 'fixtures'); @@ -9,7 +10,8 @@ test.describe('analyzeRubyFile', () => { test('should correctly analyze Ruby file with multiple tracking providers', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeRubyFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); // Sort events by eventName for consistent ordering events.sort((a, b) => a.eventName.localeCompare(b.eventName)); @@ -113,7 +115,8 @@ test.describe('analyzeRubyFile', () => { test('should handle files without tracking events', async () => { const emptyTestFile = path.join(fixturesDir, 'ruby', 'empty.rb'); - const events = await analyzeRubyFile(emptyTestFile, 'customTrack'); + const customFunctionSignatures = [parseCustomFunctionSignature('customTrack')]; + const events = await analyzeRubyFile(emptyTestFile, customFunctionSignatures); assert.deepStrictEqual(events, []); }); @@ -127,7 +130,8 @@ test.describe('analyzeRubyFile', () => { test('should handle nested property types correctly', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeRubyFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); const customEvent = events.find(e => e.eventName === 'custom_event'); assert.ok(customEvent); @@ -159,7 +163,8 @@ test.describe('analyzeRubyFile', () => { test('should handle all property types correctly', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeRubyFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); // Test string properties const signupEvent = events.find(e => e.eventName === 'User Signed Up'); @@ -181,7 +186,8 @@ test.describe('analyzeRubyFile', () => { test('should correctly identify function names in different contexts', async () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeRubyFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); // Verify function names are correctly identified const functionNames = events.map(e => e.functionName).sort(); @@ -198,7 +204,8 @@ test.describe('analyzeRubyFile', () => { test('should detect custom functions that are methods of a module', async () => { const customFunction = 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)'; - const events = await analyzeRubyFile(testFilePath, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); // Should find the CustomModule.track call const customModuleEvent = events.find(e => e.source === 'custom' && e.functionName === 'CustomModule.track'); @@ -244,9 +251,42 @@ test.describe('analyzeRubyFile', () => { ]; for (const { sig, event } of variants) { - const events = await analyzeRubyFile(testFilePath, sig); + const customFunctionSignatures = [parseCustomFunctionSignature(sig)]; + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); const found = events.find(e => e.eventName === event && e.source === 'custom'); assert.ok(found, `Should detect ${event} for signature ${sig}`); } }); + + test('should detect events when multiple custom function signatures are provided together', async () => { + const variants = [ + 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction0', + 'customTrackFunction1(EVENT_NAME, PROPERTIES)', + 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', + 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', + 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)' + ]; + + const customFunctionSignatures = variants.map(parseCustomFunctionSignature); + const events = await analyzeRubyFile(testFilePath, customFunctionSignatures); + + const expectedEventNames = [ + 'custom_event', + 'custom_event0', + 'custom_event1', + 'custom_event2', + 'custom_event3', + 'custom_event4' + ]; + + expectedEventNames.forEach(eventName => { + const evt = events.find(e => e.eventName === eventName && e.source === 'custom'); + assert.ok(evt, `Expected to find event ${eventName}`); + }); + + const builtInCount = events.filter(e => e.source !== 'custom').length; + assert.ok(builtInCount >= 6, 'Should still include built-in events'); + }); }); diff --git a/tests/analyzeTypeScript.test.js b/tests/analyzeTypeScript.test.js index cb9fbf9..904d11f 100644 --- a/tests/analyzeTypeScript.test.js +++ b/tests/analyzeTypeScript.test.js @@ -3,6 +3,7 @@ const assert = require('node:assert'); const path = require('path'); const ts = require('typescript'); const { analyzeTsFile } = require('../src/analyze/typescript'); +const { parseCustomFunctionSignature } = require('../src/analyze/utils/customFunctionParser'); test.describe('analyzeTsFile', () => { const fixturesDir = path.join(__dirname, 'fixtures'); @@ -23,8 +24,9 @@ test.describe('analyzeTsFile', () => { test('should correctly analyze TypeScript file with multiple tracking providers', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Sort events by line number for consistent ordering events.sort((a, b) => a.line - b.line); @@ -354,7 +356,8 @@ test.describe('analyzeTsFile', () => { } const program = createProgram(emptyTestFile); - const events = analyzeTsFile(emptyTestFile, program, 'customTrack'); + const customFunctionSignatures = [parseCustomFunctionSignature('customTrack')]; + const events = analyzeTsFile(emptyTestFile, program, customFunctionSignatures); assert.deepStrictEqual(events, []); }); @@ -369,8 +372,9 @@ test.describe('analyzeTsFile', () => { test('should handle nested property types correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Test nested object properties with interfaces expanded const eventWithNestedObj = events.find(e => e.properties.location); @@ -403,8 +407,9 @@ test.describe('analyzeTsFile', () => { test('should detect and expand interface types correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Test that Address interface is expanded const eventWithAddress = events.find(e => e.properties.address || e.properties.location); @@ -431,8 +436,9 @@ test.describe('analyzeTsFile', () => { test('should handle shorthand property assignments correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Test that shorthand 'items' property is correctly expanded const mixpanelEvent = events.find(e => e.eventName === 'purchase_confirmed'); @@ -445,8 +451,9 @@ test.describe('analyzeTsFile', () => { test('should handle variable references correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Test that variable references like segmentProps are resolved const segmentEvent = events.find(e => e.eventName === 'user_checkout'); @@ -460,8 +467,9 @@ test.describe('analyzeTsFile', () => { test('should exclude action field from Snowplow properties', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); const snowplowEvent = events.find(e => e.source === 'snowplow'); assert.ok(snowplowEvent); @@ -472,8 +480,9 @@ test.describe('analyzeTsFile', () => { test('should handle mParticle three-parameter format', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); const mparticleEvent = events.find(e => e.source === 'mparticle'); assert.ok(mparticleEvent); @@ -484,8 +493,9 @@ test.describe('analyzeTsFile', () => { test('should handle readonly array types correctly', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Test ReadonlyArray in checkout3 const pendoEvent = events.find(e => e.eventName === 'customer_checkout'); @@ -498,8 +508,9 @@ test.describe('analyzeTsFile', () => { test('should handle exported vs non-exported interfaces', () => { const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, customFunction); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); // Both exported Product and non-exported Address should be expanded const eventWithBoth = events.find(e => e.properties.items && e.properties.location); @@ -646,7 +657,8 @@ test.describe('analyzeTsFile', () => { test('should correctly analyze React TypeScript file with custom function', () => { const reactFilePath = path.join(fixturesDir, 'typescript-react', 'main.tsx'); const program = createProgram(reactFilePath); - const events = analyzeTsFile(reactFilePath, program, 'tracker.track'); + const customFunctionSignatures = [parseCustomFunctionSignature('tracker.track')]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); // Should find both tracker.track events (cart_update and complex_operation) const trackEvents = events.filter(e => e.source === 'custom'); @@ -692,7 +704,8 @@ test.describe('analyzeTsFile', () => { // This was the specific case that was causing "Cannot read properties of undefined (reading 'kind')" assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, 'track'); + const customFunctionSignatures = [parseCustomFunctionSignature('track')]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); // Should find the analytics.track call when looking for 'track' custom function @@ -723,7 +736,8 @@ test.describe('analyzeTsFile', () => { customFunctionTests.forEach(customFunction => { assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); }, `Should not throw error with custom function: ${customFunction}`); }); @@ -743,7 +757,8 @@ test.describe('analyzeTsFile', () => { complexCustomFunctions.forEach(customFunction => { assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); }, `Should not crash with complex custom function: ${customFunction}`); }); @@ -769,7 +784,8 @@ test.describe('analyzeTsFile', () => { // The file has complex type intersections: MappedProps & ExplicitProps & ActionProps // This should not cause AST traversal issues assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, 'uploadError'); + const customFunctionSignatures = [parseCustomFunctionSignature('uploadError')]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); }); }); @@ -780,7 +796,8 @@ test.describe('analyzeTsFile', () => { // The file uses React.createRef() which creates complex AST nodes assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, 'open'); + const customFunctionSignatures = [parseCustomFunctionSignature('open')]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); }); }); @@ -791,7 +808,8 @@ test.describe('analyzeTsFile', () => { // Should work without errors for file containing both patterns assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, 'track'); + const customFunctionSignatures = [parseCustomFunctionSignature('track')]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); @@ -822,7 +840,8 @@ test.describe('analyzeTsFile', () => { edgeCaseCustomFunctions.forEach(customFunction => { assert.doesNotThrow(() => { - const events = analyzeTsFile(reactFilePath, program, customFunction); + const customFunctionSignatures = [parseCustomFunctionSignature(customFunction)]; + const events = analyzeTsFile(reactFilePath, program, customFunctionSignatures); assert.ok(Array.isArray(events)); }, `Should handle edge case custom function: ${customFunction}`); }); @@ -862,9 +881,46 @@ test.describe('analyzeTsFile', () => { variants.forEach(({ sig, event }) => { const program = createProgram(testFilePath); - const events = analyzeTsFile(testFilePath, program, sig); + const customFunctionSignatures = [parseCustomFunctionSignature(sig)]; + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); const found = events.find(e => e.eventName === event && e.source === 'custom'); assert.ok(found, `Should detect ${event} for signature ${sig}`); }); }); + + test('should detect events when multiple custom function signatures are provided together', () => { + const variants = [ + 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction0', + 'customTrackFunction1(EVENT_NAME, PROPERTIES)', + 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', + 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', + 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)' + ]; + + const customFunctionSignatures = variants.map(parseCustomFunctionSignature); + const program = createProgram(testFilePath); + const events = analyzeTsFile(testFilePath, program, customFunctionSignatures); + + const expectedEventNames = [ + 'custom_event_v2', + 'ecommerce_purchase', + 'custom_event0', + 'custom_event1', + 'custom_event2', + 'custom_event3', + 'custom_event4', + 'custom_module_event' + ]; + + expectedEventNames.forEach(eventName => { + const evt = events.find(e => e.eventName === eventName && e.source === 'custom'); + assert.ok(evt, `Expected to find event ${eventName}`); + }); + + // Ensure built-in provider events remain unaffected + const builtInCount = events.filter(e => e.source !== 'custom').length; + assert.ok(builtInCount >= 12, 'Should still include built-in provider events'); + }); }); diff --git a/tests/cli.test.js b/tests/cli.test.js index 0c1b9e1..4fd8050 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -8,11 +8,21 @@ const _ = require('lodash'); const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js'); -const customFunctionSignature = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; +// Multiple custom function signatures for comprehensive testing +const customFunctionSignatures = [ + 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction0', + 'customTrackFunction1(EVENT_NAME, PROPERTIES)', + 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', + 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', + 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', + 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)' +]; // Helper function to run CLI and capture output -function runCLI(targetDir, customFunction, outputFile) { - const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunction}" --output "${outputFile}"`; +function runCLI(targetDir, customFunctions, outputFile) { + const customFunctionArgs = customFunctions.map(func => `--customFunction "${func}"`).join(' '); + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" ${customFunctionArgs} --output "${outputFile}"`; try { execSync(command, { encoding: 'utf8' }); return true; @@ -108,7 +118,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'go', 'tracking-schema-go.yaml'); // Run CLI - const success = runCLI(targetDir, customFunctionSignature, outputFile); + const success = runCLI(targetDir, customFunctionSignatures, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -124,7 +134,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml'); // Run CLI - const success = runCLI(targetDir, customFunctionSignature, outputFile); + const success = runCLI(targetDir, customFunctionSignatures, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -140,7 +150,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'typescript', 'tracking-schema-typescript.yaml'); // Run CLI - const success = runCLI(targetDir, customFunctionSignature, outputFile); + const success = runCLI(targetDir, customFunctionSignatures, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -156,7 +166,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'python', 'tracking-schema-python.yaml'); // Run CLI - const success = runCLI(targetDir, customFunctionSignature, outputFile); + const success = runCLI(targetDir, customFunctionSignatures, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -172,7 +182,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'ruby', 'tracking-schema-ruby.yaml'); // Run CLI - const success = runCLI(targetDir, customFunctionSignature, outputFile); + const success = runCLI(targetDir, customFunctionSignatures, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -202,7 +212,7 @@ test.describe('CLI End-to-End Tests', () => { ); // Run CLI on the directory with only the empty file - const success = runCLI(tempLangDir, customFunctionSignature, outputFile); + const success = runCLI(tempLangDir, customFunctionSignatures, outputFile); assert.ok(success, `CLI should run successfully for ${lang} empty file`); // Check output file exists @@ -230,7 +240,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'tracking-schema-all.yaml'); // Run CLI - const success = runCLI(targetDir, customFunctionSignature, outputFile); + const success = runCLI(targetDir, customFunctionSignatures, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -243,7 +253,8 @@ test.describe('CLI End-to-End Tests', () => { test('should print YAML to stdout when --stdout is used', async () => { const targetDir = path.join(fixturesDir, 'javascript'); const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml'); - const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout`; + const customFunctionArgs = customFunctionSignatures.map(func => `--customFunction "${func}"`).join(' '); + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" ${customFunctionArgs} --stdout`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -267,7 +278,8 @@ test.describe('CLI End-to-End Tests', () => { test('should not print output file message when --stdout is used', async () => { const targetDir = path.join(fixturesDir, 'javascript'); - const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout`; + const customFunctionArgs = customFunctionSignatures.map(func => `--customFunction "${func}"`).join(' '); + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" ${customFunctionArgs} --stdout`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -281,7 +293,8 @@ test.describe('CLI End-to-End Tests', () => { test('should print JSON to stdout when --format json is used', async () => { const targetDir = path.join(fixturesDir, 'javascript'); const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml'); - const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout --format json`; + const customFunctionArgs = customFunctionSignatures.map(func => `--customFunction "${func}"`).join(' '); + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" ${customFunctionArgs} --stdout --format json`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -315,7 +328,8 @@ test.describe('CLI End-to-End Tests', () => { const targetDir = path.join(fixturesDir, 'javascript'); const outputFile = path.join(tempDir, 'tracking-schema-javascript-test.json'); const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml'); - const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --output "${outputFile}" --format json`; + const customFunctionArgs = customFunctionSignatures.map(func => `--customFunction "${func}"`).join(' '); + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" ${customFunctionArgs} --output "${outputFile}" --format json`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -347,7 +361,8 @@ test.describe('CLI End-to-End Tests', () => { test('should fail with a clear error if --format is not yaml or json', async () => { const targetDir = path.join(fixturesDir, 'javascript'); - const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout --format xml`; + const customFunctionArgs = customFunctionSignatures.map(func => `--customFunction "${func}"`).join(' '); + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" ${customFunctionArgs} --stdout --format xml`; let errorCaught = false; try { execSync(command, { encoding: 'utf8', stdio: 'pipe' }); diff --git a/tests/fixtures/go/tracking-schema-go.yaml b/tests/fixtures/go/tracking-schema-go.yaml index a5e317c..47330ee 100644 --- a/tests/fixtures/go/tracking-schema-go.yaml +++ b/tests/fixtures/go/tracking-schema-go.yaml @@ -102,3 +102,61 @@ events: type: number c: type: string + custom_event0: + implementations: + - path: main.go + line: 121 + function: main + destination: custom + properties: + foo: + type: string + custom_event1: + implementations: + - path: main.go + line: 122 + function: main + destination: custom + properties: + foo: + type: string + custom_event2: + implementations: + - path: main.go + line: 123 + function: main + destination: custom + properties: + userId: + type: string + foo: + type: string + custom_event3: + implementations: + - path: main.go + line: 124 + function: main + destination: custom + properties: + foo: + type: string + userEmail: + type: string + custom_event4: + implementations: + - path: main.go + line: 125 + function: main + destination: custom + properties: + userId: + type: string + userAddress: + type: object + properties: + city: + type: string + foo: + type: string + userEmail: + type: string diff --git a/tests/fixtures/javascript/tracking-schema-javascript.yaml b/tests/fixtures/javascript/tracking-schema-javascript.yaml index 8b17e03..b2acffb 100644 --- a/tests/fixtures/javascript/tracking-schema-javascript.yaml +++ b/tests/fixtures/javascript/tracking-schema-javascript.yaml @@ -198,3 +198,74 @@ events: type: string userId: type: string + custom_event0: + implementations: + - path: main.js + line: 162 + function: global + destination: custom + properties: + foo: + type: string + custom_event1: + implementations: + - path: main.js + line: 163 + function: global + destination: custom + properties: + foo: + type: string + custom_event2: + implementations: + - path: main.js + line: 164 + function: global + destination: custom + properties: + foo: + type: string + userId: + type: string + custom_event3: + implementations: + - path: main.js + line: 165 + function: global + destination: custom + properties: + foo: + type: string + userEmail: + type: string + custom_event4: + implementations: + - path: main.js + line: 166 + function: global + destination: custom + properties: + foo: + type: string + userId: + type: string + userAddress: + type: object + properties: + city: + type: string + userEmail: + type: string + custom_module_event: + implementations: + - path: main.js + line: 178 + function: global + destination: custom + properties: + order_id: + type: string + foo: + type: string + userId: + type: string diff --git a/tests/fixtures/python/tracking-schema-python.yaml b/tests/fixtures/python/tracking-schema-python.yaml index 5b73ed8..f7ae117 100644 --- a/tests/fixtures/python/tracking-schema-python.yaml +++ b/tests/fixtures/python/tracking-schema-python.yaml @@ -124,3 +124,74 @@ events: type: array items: type: number + custom_event0: + implementations: + - path: main.py + line: 161 + function: main + destination: custom + properties: + foo: + type: string + custom_event1: + implementations: + - path: main.py + line: 162 + function: main + destination: custom + properties: + foo: + type: string + custom_event2: + implementations: + - path: main.py + line: 163 + function: main + destination: custom + properties: + userId: + type: string + foo: + type: string + custom_event3: + implementations: + - path: main.py + line: 164 + function: main + destination: custom + properties: + foo: + type: string + userEmail: + type: string + custom_event4: + implementations: + - path: main.py + line: 165 + function: main + destination: custom + properties: + userId: + type: string + userAddress: + type: object + properties: + city: + type: string + foo: + type: string + userEmail: + type: string + custom_module_event: + implementations: + - path: main.py + line: 173 + function: main + destination: custom + properties: + userId: + type: string + order_id: + type: string + foo: + type: string diff --git a/tests/fixtures/ruby/tracking-schema-ruby.yaml b/tests/fixtures/ruby/tracking-schema-ruby.yaml index ae6bb6e..27263a1 100644 --- a/tests/fixtures/ruby/tracking-schema-ruby.yaml +++ b/tests/fixtures/ruby/tracking-schema-ruby.yaml @@ -67,6 +67,10 @@ events: line: 79 function: custom_track_event destination: custom + - path: main.rb + line: 98 + function: CustomModule.track + destination: custom properties: userId: type: string @@ -105,3 +109,58 @@ events: type: string from_module: type: boolean + custom_event0: + implementations: + - path: main.rb + line: 135 + function: global + destination: custom + properties: + foo: + type: string + custom_event1: + implementations: + - path: main.rb + line: 136 + function: global + destination: custom + properties: + foo: + type: string + custom_event2: + implementations: + - path: main.rb + line: 137 + function: global + destination: custom + properties: + userId: + type: string + foo: + type: string + custom_event3: + implementations: + - path: main.rb + line: 138 + function: global + destination: custom + properties: + foo: + type: string + userEmail: + type: string + custom_event4: + implementations: + - path: main.rb + line: 139 + function: global + destination: custom + properties: + userId: + type: string + userAddress: + type: any + userEmail: + type: string + foo: + type: string diff --git a/tests/fixtures/tracking-schema-all.yaml b/tests/fixtures/tracking-schema-all.yaml index bf46b5d..48a3bf2 100644 --- a/tests/fixtures/tracking-schema-all.yaml +++ b/tests/fixtures/tracking-schema-all.yaml @@ -128,6 +128,10 @@ events: line: 79 function: custom_track_event destination: custom + - path: ruby/main.rb + line: 98 + function: CustomModule.track + destination: custom properties: userId: type: string @@ -920,3 +924,162 @@ events: type: string checkout_step: type: number + custom_event0: + implementations: + - path: go/main.go + line: 121 + function: main + destination: custom + - path: javascript/main.js + line: 162 + function: global + destination: custom + - path: python/main.py + line: 161 + function: main + destination: custom + - path: ruby/main.rb + line: 135 + function: global + destination: custom + - path: typescript/main.ts + line: 306 + function: global + destination: custom + properties: + foo: + type: string + custom_event1: + implementations: + - path: go/main.go + line: 122 + function: main + destination: custom + - path: javascript/main.js + line: 163 + function: global + destination: custom + - path: python/main.py + line: 162 + function: main + destination: custom + - path: ruby/main.rb + line: 136 + function: global + destination: custom + - path: typescript/main.ts + line: 307 + function: global + destination: custom + properties: + foo: + type: string + custom_event2: + implementations: + - path: go/main.go + line: 123 + function: main + destination: custom + - path: javascript/main.js + line: 164 + function: global + destination: custom + - path: python/main.py + line: 163 + function: main + destination: custom + - path: ruby/main.rb + line: 137 + function: global + destination: custom + - path: typescript/main.ts + line: 308 + function: global + destination: custom + properties: + userId: + type: string + foo: + type: string + custom_event3: + implementations: + - path: go/main.go + line: 124 + function: main + destination: custom + - path: javascript/main.js + line: 165 + function: global + destination: custom + - path: python/main.py + line: 164 + function: main + destination: custom + - path: ruby/main.rb + line: 138 + function: global + destination: custom + - path: typescript/main.ts + line: 309 + function: global + destination: custom + properties: + foo: + type: string + userEmail: + type: string + custom_event4: + implementations: + - path: go/main.go + line: 125 + function: main + destination: custom + - path: javascript/main.js + line: 166 + function: global + destination: custom + - path: python/main.py + line: 165 + function: main + destination: custom + - path: ruby/main.rb + line: 139 + function: global + destination: custom + - path: typescript/main.ts + line: 310 + function: global + destination: custom + properties: + userId: + type: string + userAddress: + type: object + properties: + city: + type: string + userEmail: + type: string + foo: + type: string + custom_module_event: + implementations: + - path: javascript/main.js + line: 178 + function: global + destination: custom + - path: python/main.py + line: 173 + function: main + destination: custom + - path: typescript/main.ts + line: 322 + function: global + destination: custom + properties: + userId: + type: string + order_id: + type: string + foo: + type: string diff --git a/tests/fixtures/typescript/tracking-schema-typescript.yaml b/tests/fixtures/typescript/tracking-schema-typescript.yaml index b3a00dd..d1b8b71 100644 --- a/tests/fixtures/typescript/tracking-schema-typescript.yaml +++ b/tests/fixtures/typescript/tracking-schema-typescript.yaml @@ -349,3 +349,74 @@ events: type: array items: type: string + custom_event0: + implementations: + - path: main.ts + line: 306 + function: global + destination: custom + properties: + foo: + type: string + custom_event1: + implementations: + - path: main.ts + line: 307 + function: global + destination: custom + properties: + foo: + type: string + custom_event2: + implementations: + - path: main.ts + line: 308 + function: global + destination: custom + properties: + foo: + type: string + userId: + type: string + custom_event3: + implementations: + - path: main.ts + line: 309 + function: global + destination: custom + properties: + foo: + type: string + userEmail: + type: string + custom_event4: + implementations: + - path: main.ts + line: 310 + function: global + destination: custom + properties: + foo: + type: string + userId: + type: string + userAddress: + type: object + properties: + city: + type: string + userEmail: + type: string + custom_module_event: + implementations: + - path: main.ts + line: 322 + function: global + destination: custom + properties: + order_id: + type: string + foo: + type: string + userId: + type: string