diff --git a/README.md b/README.md index 32c8785..99608aa 100644 --- a/README.md +++ b/README.md @@ -23,38 +23,39 @@ Run without installation! Just use: npx @flisk/analyze-tracking /path/to/project [options] ``` -### Key Options: +### Key Options - `-g, --generateDescription`: Generate descriptions of fields (default: `false`) - `-p, --provider `: Specify a provider (options: `openai`, `gemini`) - `-m, --model `: Specify a model (ex: `gpt-4.1-nano`, `gpt-4o-mini`, `gemini-2.0-flash-lite-001`) - `-o, --output `: Name of the output file (default: `tracking-schema.yaml`) -- `-c, --customFunction `: Specify a custom tracking function +- `-c, --customFunction `: Specify the signature of your custom tracking function (see [instructions here](#custom-functions)) - `--format `: Output format, either `yaml` (default) or `json`. If an invalid value is provided, the CLI will exit with an error. - `--stdout`: Print the output to the terminal instead of writing to a file (works with both YAML and JSON) 🔑  **Important:** If you are using `generateDescription`, you must set the appropriate credentials for the LLM provider you are using as an environment variable. OpenAI uses `OPENAI_API_KEY` and Google Vertex AI uses `GOOGLE_APPLICATION_CREDENTIALS`. -
- Note on Custom Functions 💡 - Use this if you have your own in-house tracker or a wrapper function that calls other tracking libraries. +### Custom Functions - We currently only support functions that follow the following format: - - **JavaScript/TypeScript/Python/Ruby:** - ```js - yourCustomTrackFunctionName('', { - - }); - ``` - - **Go:** - ```go - yourCustomTrackFunctionName("", map[string]any{}{ - "": "", - }) - ``` -
+If you have your own in-house tracker or a wrapper function that calls other tracking libraries, you can specify the function signature with the `-c` or `--customFunction` option. + +Your function signature should be in the following format: +```js +yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES, customFieldOne, customFieldTwo) +``` + +- `EVENT_NAME` is the name of the event you are tracking. It should be a string or a pointer to a string. This is required. +- `PROPERTIES` is an object of properties for that event. It should be an object / dictionary. This is optional. +- Any additional parameters are other fields you are tracking. They can be of any type. The names you provide for these parameters will be used as the property names in the output. + + +For example, if your function has a userId parameter at the beginning, followed by the event name and properties, you would pass in the following: + +```js +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. ## What's Generated? diff --git a/src/analyze/go/astTraversal.js b/src/analyze/go/astTraversal.js index 7d58fbf..c00bad6 100644 --- a/src/analyze/go/astTraversal.js +++ b/src/analyze/go/astTraversal.js @@ -12,34 +12,34 @@ const { extractTrackingEvent } = require('./trackingExtractor'); * @param {Array} events - Array to collect found tracking events (modified in place) * @param {string} filePath - Path to the file being analyzed * @param {string} functionName - Name of the current function being processed - * @param {string|null} customFunction - Name of custom tracking function to detect + * @param {Object|null} customConfig - Parsed custom function configuration (or null) * @param {Object} typeContext - Type information context for variable resolution * @param {string} currentFunction - Current function context for type lookups */ -function extractEventsFromBody(body, events, filePath, functionName, customFunction, typeContext, currentFunction) { +function extractEventsFromBody(body, events, filePath, functionName, customConfig, typeContext, currentFunction) { for (const stmt of body) { if (stmt.tag === 'exec' && stmt.expr) { - processExpression(stmt.expr, events, filePath, functionName, customFunction, typeContext, currentFunction); + processExpression(stmt.expr, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'declare' && stmt.value) { // Handle variable declarations with tracking calls - processExpression(stmt.value, events, filePath, functionName, customFunction, typeContext, currentFunction); + processExpression(stmt.value, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'assign' && stmt.rhs) { // Handle assignments with tracking calls - processExpression(stmt.rhs, events, filePath, functionName, customFunction, typeContext, currentFunction); + processExpression(stmt.rhs, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'if' && stmt.body) { - extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction); + extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'elseif' && stmt.body) { - extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction); + extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'else' && stmt.body) { - extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction); + extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'for' && stmt.body) { - extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction); + extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'foreach' && stmt.body) { - extractEventsFromBody(stmt.body, events, filePath, functionName, customFunction, typeContext, currentFunction); + extractEventsFromBody(stmt.body, events, filePath, functionName, customConfig, typeContext, currentFunction); } else if (stmt.tag === 'switch' && stmt.cases) { for (const caseNode of stmt.cases) { if (caseNode.body) { - extractEventsFromBody(caseNode.body, events, filePath, functionName, customFunction, typeContext, currentFunction); + extractEventsFromBody(caseNode.body, events, filePath, functionName, customConfig, typeContext, currentFunction); } } } @@ -52,18 +52,18 @@ function extractEventsFromBody(body, events, filePath, functionName, customFunct * @param {Array} events - Array to collect found tracking events (modified in place) * @param {string} filePath - Path to the file being analyzed * @param {string} functionName - Name of the current function being processed - * @param {string|null} customFunction - Name of custom tracking function to detect + * @param {Object|null} customConfig - Parsed custom function configuration (or null) * @param {Object} typeContext - Type information context for variable resolution * @param {string} currentFunction - Current function context for type lookups * @param {number} [depth=0] - Current recursion depth (used to prevent infinite recursion) */ -function processExpression(expr, events, filePath, functionName, customFunction, typeContext, currentFunction, depth = 0) { +function processExpression(expr, events, filePath, functionName, customConfig, typeContext, currentFunction, depth = 0) { if (!expr || depth > MAX_RECURSION_DEPTH) return; // Prevent infinite recursion with depth limit // Handle array of expressions if (Array.isArray(expr)) { for (const item of expr) { - processExpression(item, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(item, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } return; } @@ -71,25 +71,25 @@ function processExpression(expr, events, filePath, functionName, customFunction, // Handle single expression with body if (expr.body) { for (const item of expr.body) { - processExpression(item, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(item, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } return; } // Handle specific node types if (expr.tag === 'call') { - const trackingCall = extractTrackingEvent(expr, filePath, functionName, customFunction, typeContext, currentFunction); + const trackingCall = extractTrackingEvent(expr, filePath, functionName, customConfig, typeContext, currentFunction); if (trackingCall) { events.push(trackingCall); } // Also process call arguments if (expr.args) { - processExpression(expr.args, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(expr.args, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } } else if (expr.tag === 'structlit') { // Check if this struct literal is a tracking event - const trackingCall = extractTrackingEvent(expr, filePath, functionName, customFunction, typeContext, currentFunction); + const trackingCall = extractTrackingEvent(expr, filePath, functionName, customConfig, typeContext, currentFunction); if (trackingCall) { events.push(trackingCall); } @@ -98,7 +98,7 @@ function processExpression(expr, events, filePath, functionName, customFunction, if (!trackingCall && expr.fields) { for (const field of expr.fields) { if (field.value) { - processExpression(field.value, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(field.value, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } } } @@ -106,13 +106,13 @@ function processExpression(expr, events, filePath, functionName, customFunction, // Process other common properties that might contain expressions if (expr.value && expr.tag !== 'structlit') { - processExpression(expr.value, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(expr.value, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } if (expr.lhs) { - processExpression(expr.lhs, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(expr.lhs, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } if (expr.rhs) { - processExpression(expr.rhs, events, filePath, functionName, customFunction, typeContext, currentFunction, depth + 1); + processExpression(expr.rhs, events, filePath, functionName, customConfig, typeContext, currentFunction, depth + 1); } } diff --git a/src/analyze/go/eventExtractor.js b/src/analyze/go/eventExtractor.js index 8059610..5b54d4d 100644 --- a/src/analyze/go/eventExtractor.js +++ b/src/analyze/go/eventExtractor.js @@ -10,9 +10,10 @@ const { extractStringValue, findStructLiteral, findStructField, extractSnowplowV * Extract event name from a tracking call based on the source * @param {Object} callNode - AST node representing a function call or struct literal * @param {string} source - Analytics source (e.g., 'segment', 'amplitude') + * @param {Object|null} customConfig - Parsed custom function configuration * @returns {string|null} Event name or null if not found */ -function extractEventName(callNode, source) { +function extractEventName(callNode, source, customConfig = null) { if (!callNode.args || callNode.args.length === 0) { // For struct literals, we need to check fields instead of args if (!callNode.fields || callNode.fields.length === 0) { @@ -35,7 +36,7 @@ function extractEventName(callNode, source) { return extractSnowplowEventName(callNode); case ANALYTICS_SOURCES.CUSTOM: - return extractCustomEventName(callNode); + return extractCustomEventName(callNode, customConfig); } return null; @@ -142,13 +143,15 @@ function extractSnowplowEventName(callNode) { * Extract custom event name * Pattern: customFunction("event_name", props) * @param {Object} callNode - AST node for custom tracking function call + * @param {Object|null} customConfig - Custom configuration object * @returns {string|null} Event name or null if not found */ -function extractCustomEventName(callNode) { - if (callNode.args && callNode.args.length > 0) { - return extractStringValue(callNode.args[0]); - } - return null; +function extractCustomEventName(callNode, customConfig) { + if (!callNode.args || callNode.args.length === 0) return null; + const args = callNode.args; + const eventIdx = customConfig?.eventIndex ?? 0; + const argNode = args[eventIdx]; + return extractStringValue(argNode); } module.exports = { diff --git a/src/analyze/go/index.js b/src/analyze/go/index.js index cfc6670..812e5b7 100644 --- a/src/analyze/go/index.js +++ b/src/analyze/go/index.js @@ -8,16 +8,20 @@ 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} customFunction - Name of custom tracking function to detect (optional) + * @param {string|null} customFunctionSignature - 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, customFunction) { +async function analyzeGoFile(filePath, customFunctionSignature) { try { + const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; + // Read the Go file const source = fs.readFileSync(filePath, 'utf8'); @@ -37,7 +41,7 @@ async function analyzeGoFile(filePath, customFunction) { currentFunction = node.name; // Process the function body if (node.body) { - extractEventsFromBody(node.body, events, filePath, currentFunction, customFunction, typeContext, currentFunction); + extractEventsFromBody(node.body, events, filePath, currentFunction, customConfig, typeContext, currentFunction); } } } diff --git a/src/analyze/go/propertyExtractor.js b/src/analyze/go/propertyExtractor.js index b0071a7..fa7601d 100644 --- a/src/analyze/go/propertyExtractor.js +++ b/src/analyze/go/propertyExtractor.js @@ -12,9 +12,10 @@ const { extractStringValue, findStructLiteral, findStructField, extractFieldName * @param {string} source - Analytics source (e.g., 'segment', 'amplitude') * @param {Object} typeContext - Type information context for variable resolution * @param {string} currentFunction - Current function context for type lookups + * @param {Object} customConfig - Custom configuration for property extraction * @returns {Object} Object containing extracted properties with their type information */ -function extractProperties(callNode, source, typeContext, currentFunction) { +function extractProperties(callNode, source, typeContext, currentFunction, customConfig) { const properties = {}; switch (source) { @@ -36,7 +37,7 @@ function extractProperties(callNode, source, typeContext, currentFunction) { break; case ANALYTICS_SOURCES.CUSTOM: - extractCustomProperties(callNode, properties, typeContext, currentFunction); + extractCustomProperties(callNode, properties, typeContext, currentFunction, customConfig); break; } @@ -270,10 +271,29 @@ function extractSnowplowProperties(callNode, properties, typeContext, currentFun * @param {Object} properties - Object to store extracted properties (modified in place) * @param {Object} typeContext - Type information context for variable resolution * @param {string} currentFunction - Current function context for type lookups + * @param {Object} customConfig - Custom configuration for property extraction */ -function extractCustomProperties(callNode, properties, typeContext, currentFunction) { - if (callNode.args && callNode.args.length > 1) { - extractPropertiesFromExpr(callNode.args[1], properties, typeContext, currentFunction); +function extractCustomProperties(callNode, properties, typeContext, currentFunction, customConfig) { + if (!callNode.args || callNode.args.length === 0) return; + + const args = callNode.args; + + const propsIdx = customConfig?.propertiesIndex ?? 1; + + // Extract extra params first (those not event or properties) + if (customConfig && Array.isArray(customConfig.extraParams)) { + customConfig.extraParams.forEach(param => { + const argNode = args[param.idx]; + if (argNode) { + properties[param.name] = getPropertyInfo(argNode, typeContext, currentFunction); + } + }); + } + + // Extract properties map/object (if provided) + const propsArg = args[propsIdx]; + if (propsArg) { + extractPropertiesFromExpr(propsArg, properties, typeContext, currentFunction); } } diff --git a/src/analyze/go/trackingExtractor.js b/src/analyze/go/trackingExtractor.js index 3af4837..7b88ad0 100644 --- a/src/analyze/go/trackingExtractor.js +++ b/src/analyze/go/trackingExtractor.js @@ -13,19 +13,19 @@ const { extractProperties } = require('./propertyExtractor'); * @param {Object} callNode - AST node representing a function call or struct literal * @param {string} filePath - Path to the file being analyzed * @param {string} functionName - Name of the function containing this tracking call - * @param {string|null} customFunction - Name of custom tracking function to detect + * @param {Object|null} customConfig - Parsed custom function configuration (or null) * @param {Object} typeContext - Type information context for variable resolution * @param {string} currentFunction - Current function context for type lookups * @returns {Object|null} Tracking event object with eventName, source, properties, etc., or null if not a tracking call */ -function extractTrackingEvent(callNode, filePath, functionName, customFunction, typeContext, currentFunction) { - const source = detectSource(callNode, customFunction); +function extractTrackingEvent(callNode, filePath, functionName, customConfig, typeContext, currentFunction) { + const source = detectSource(callNode, customConfig ? customConfig.functionName : null); if (!source) return null; - const eventName = extractEventName(callNode, source); + const eventName = extractEventName(callNode, source, customConfig); if (!eventName) return null; - const properties = extractProperties(callNode, source, typeContext, currentFunction); + const properties = extractProperties(callNode, source, typeContext, currentFunction, customConfig); // Get line number based on source type let line = 0; diff --git a/src/analyze/javascript/detectors/analytics-source.js b/src/analyze/javascript/detectors/analytics-source.js index 4b94514..26ef558 100644 --- a/src/analyze/javascript/detectors/analytics-source.js +++ b/src/analyze/javascript/detectors/analytics-source.js @@ -43,8 +43,61 @@ function detectAnalyticsSource(node, customFunction) { * @returns {boolean} */ function isCustomFunction(node, customFunction) { - return node.callee.type === NODE_TYPES.IDENTIFIER && - node.callee.name === customFunction; + if (!customFunction) return false; + + // Support dot-separated names like "CustomModule.track" + const parts = customFunction.split('.'); + + // Simple identifier (no dot) + if (parts.length === 1) { + return node.callee.type === NODE_TYPES.IDENTIFIER && node.callee.name === customFunction; + } + + // For dot-separated names, the callee should be a MemberExpression chain. + if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) { + return false; + } + + return matchesMemberChain(node.callee, parts); +} + +/** + * Recursively verifies that a MemberExpression chain matches the expected parts. + * Example: parts ["CustomModule", "track"] should match `CustomModule.track()`. + * @param {Object} memberExpr - AST MemberExpression node + * @param {string[]} parts - Expected name segments (left -> right) + * @returns {boolean} + */ +function matchesMemberChain(memberExpr, parts) { + let currentNode = memberExpr; + let idx = parts.length - 1; // start from the rightmost property + + while (currentNode && idx >= 0) { + const expectedPart = parts[idx]; + + // property should match current expectedPart + if (currentNode.type === NODE_TYPES.MEMBER_EXPRESSION) { + // Ensure property is Identifier and matches + if ( + currentNode.property.type !== NODE_TYPES.IDENTIFIER || + currentNode.property.name !== expectedPart + ) { + return false; + } + + // Move to the object of the MemberExpression + currentNode = currentNode.object; + idx -= 1; + } else if (currentNode.type === NODE_TYPES.IDENTIFIER) { + // We reached the leftmost Identifier; it should match the first part + return idx === 0 && currentNode.name === expectedPart; + } else { + // Unexpected node type (e.g., ThisExpression, CallExpression, etc.) + return false; + } + } + + return false; } /** diff --git a/src/analyze/javascript/extractors/event-extractor.js b/src/analyze/javascript/extractors/event-extractor.js index d2971b7..071f437 100644 --- a/src/analyze/javascript/extractors/event-extractor.js +++ b/src/analyze/javascript/extractors/event-extractor.js @@ -20,6 +20,7 @@ const EXTRACTION_STRATEGIES = { googleanalytics: extractGoogleAnalyticsEvent, snowplow: extractSnowplowEvent, mparticle: extractMparticleEvent, + custom: extractCustomEvent, default: extractDefaultEvent }; @@ -27,10 +28,14 @@ const EXTRACTION_STRATEGIES = { * Extracts event information from a CallExpression node * @param {Object} node - AST CallExpression node * @param {string} source - Analytics provider source + * @param {Object} customConfig - Parsed custom function configuration * @returns {EventData} Extracted event data */ -function extractEventData(node, source) { +function extractEventData(node, source, customConfig) { const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default; + if (source === 'custom') { + return strategy(node, customConfig); + } return strategy(node); } @@ -113,6 +118,30 @@ function extractDefaultEvent(node) { return { eventName, propertiesNode }; } +/** + * Extracts Custom function event data according to signature + * @param {Object} node - CallExpression node + * @param {Object} customConfig - Parsed custom function configuration + * @returns {EventData & {extraArgs:Object}} event data plus extra args map + */ +function extractCustomEvent(node, customConfig) { + const args = node.arguments || []; + + const eventArg = args[customConfig?.eventIndex ?? 0]; + const propertiesArg = args[customConfig?.propertiesIndex ?? 1]; + + const eventName = getStringValue(eventArg); + + const extraArgs = {}; + if (customConfig && customConfig.extraParams) { + customConfig.extraParams.forEach(extra => { + extraArgs[extra.name] = args[extra.idx]; + }); + } + + return { eventName, propertiesNode: propertiesArg, extraArgs }; +} + /** * Processes extracted event data into final event object * @param {EventData} eventData - Raw event data @@ -120,9 +149,10 @@ function extractDefaultEvent(node) { * @param {string} filePath - File path * @param {number} line - Line number * @param {string} functionName - Containing function name + * @param {Object} customConfig - Parsed custom function configuration * @returns {Object|null} Processed event object or null */ -function processEventData(eventData, source, filePath, line, functionName) { +function processEventData(eventData, source, filePath, line, functionName, customConfig) { const { eventName, propertiesNode } = eventData; if (!eventName || !propertiesNode || propertiesNode.type !== NODE_TYPES.OBJECT_EXPRESSION) { @@ -131,6 +161,15 @@ function processEventData(eventData, source, filePath, line, functionName) { let properties = extractProperties(propertiesNode); + // Handle custom extra params + if (source === 'custom' && customConfig && eventData.extraArgs) { + for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) { + properties[paramName] = { + type: inferNodeValueType(argNode) + }; + } + } + // Special handling for Snowplow: remove 'action' from properties if (source === 'snowplow' && properties.action) { delete properties.action; @@ -173,6 +212,25 @@ function findPropertyByKey(objectNode, key) { ); } +/** + * Infers the type of a value from an AST node (simple heuristic) + * @param {Object} node - AST node + * @returns {string} inferred type + */ +function inferNodeValueType(node) { + if (!node) return 'any'; + switch (node.type) { + case NODE_TYPES.LITERAL: + return typeof node.value; + case NODE_TYPES.OBJECT_EXPRESSION: + return 'object'; + case NODE_TYPES.ARRAY_EXPRESSION: + return 'array'; + default: + return 'any'; + } +} + module.exports = { extractEventData, processEventData diff --git a/src/analyze/javascript/index.js b/src/analyze/javascript/index.js index 8389013..8a3a08f 100644 --- a/src/analyze/javascript/index.js +++ b/src/analyze/javascript/index.js @@ -4,6 +4,7 @@ */ const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./parser'); +const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); /** * Analyzes a JavaScript file for analytics tracking calls @@ -11,15 +12,16 @@ const { parseFile, findTrackingEvents, FileReadError, ParseError } = require('./ * @param {string} [customFunction] - Optional custom function name to detect * @returns {Array} Array of tracking events found in the file */ -function analyzeJsFile(filePath, customFunction) { +function analyzeJsFile(filePath, customFunctionSignature) { const events = []; + const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; try { // Parse the file into an AST const ast = parseFile(filePath); // Find and extract tracking events - const foundEvents = findTrackingEvents(ast, filePath, customFunction); + const foundEvents = findTrackingEvents(ast, filePath, customConfig); events.push(...foundEvents); } catch (error) { diff --git a/src/analyze/javascript/parser.js b/src/analyze/javascript/parser.js index 7efd026..2616b4b 100644 --- a/src/analyze/javascript/parser.js +++ b/src/analyze/javascript/parser.js @@ -70,16 +70,16 @@ function parseFile(filePath) { * Walks the AST and finds analytics tracking calls * @param {Object} ast - Parsed AST * @param {string} filePath - Path to the file being analyzed - * @param {string} [customFunction] - Custom function name to detect + * @param {Object} [customConfig] - Custom function configuration object * @returns {Array} Array of found events */ -function findTrackingEvents(ast, filePath, customFunction) { +function findTrackingEvents(ast, filePath, customConfig) { const events = []; walk.ancestor(ast, { [NODE_TYPES.CALL_EXPRESSION]: (node, ancestors) => { try { - const event = extractTrackingEvent(node, ancestors, filePath, customFunction); + const event = extractTrackingEvent(node, ancestors, filePath, customConfig); if (event) { events.push(event); } @@ -97,25 +97,25 @@ function findTrackingEvents(ast, filePath, customFunction) { * @param {Object} node - CallExpression node * @param {Array} ancestors - Ancestor nodes * @param {string} filePath - File path - * @param {string} [customFunction] - Custom function name + * @param {Object} [customConfig] - Custom function configuration object * @returns {Object|null} Extracted event or null */ -function extractTrackingEvent(node, ancestors, filePath, customFunction) { +function extractTrackingEvent(node, ancestors, filePath, customConfig) { // Detect the analytics source - const source = detectAnalyticsSource(node, customFunction); + const source = detectAnalyticsSource(node, customConfig?.functionName); if (source === 'unknown') { return null; } // Extract event data based on the source - const eventData = extractEventData(node, source); + const eventData = extractEventData(node, source, customConfig); // Get location and context information const line = node.loc.start.line; const functionName = findWrappingFunction(node, ancestors); // Process the event data into final format - return processEventData(eventData, source, filePath, line, functionName); + return processEventData(eventData, source, filePath, line, functionName, customConfig); } module.exports = { diff --git a/src/analyze/python/index.js b/src/analyze/python/index.js index 104f0ad..0c1a0cd 100644 --- a/src/analyze/python/index.js +++ b/src/analyze/python/index.js @@ -5,6 +5,7 @@ const fs = require('fs'); const path = require('path'); +const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); // Singleton instance of Pyodide let pyodide = null; @@ -40,7 +41,7 @@ async function initPyodide() { * libraries, extracting event names, properties, and metadata. * * @param {string} filePath - Path to the Python file to analyze - * @param {string} [customFunction=null] - Name of a custom tracking function to detect + * @param {string} [customFunctionSignature=null] - Signature of a custom tracking function to detect * @returns {Promise>} Array of tracking events found in the file * @returns {Promise} Empty array if an error occurs * @@ -52,7 +53,9 @@ async function initPyodide() { * // With custom tracking function * const events = await analyzePythonFile('./app.py', 'track_event'); */ -async function analyzePythonFile(filePath, customFunction = null) { +async function analyzePythonFile(filePath, customFunctionSignature = null) { + const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; + // Validate inputs if (!filePath || typeof filePath !== 'string') { console.error('Invalid file path provided'); @@ -83,15 +86,17 @@ async function analyzePythonFile(filePath, customFunction = null) { // Set up Python environment with necessary variables py.globals.set('code', code); py.globals.set('filepath', filePath); - py.globals.set('custom_function', customFunction); + 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 py.globals.set('__name__', null); // Load and run the analyzer py.runPython(analyzerCode); - // Execute the analysis and parse results - const result = py.runPython('analyze_python_code(code, filepath, custom_function)'); + // Execute the analysis and parse result + const result = py.runPython('analyze_python_code(code, filepath, custom_config)'); const events = JSON.parse(result); return events; diff --git a/src/analyze/python/pythonTrackingAnalyzer.py b/src/analyze/python/pythonTrackingAnalyzer.py index 9020d5f..6ebf9aa 100644 --- a/src/analyze/python/pythonTrackingAnalyzer.py +++ b/src/analyze/python/pythonTrackingAnalyzer.py @@ -53,6 +53,14 @@ # Container types that map to objects OBJECT_TYPES = {'Dict', 'dict'} +def _safe_id(node: ast.AST) -> Optional[str]: + """Return the .id attribute of a node if present.""" + return getattr(node, 'id', None) + +# ------------------------------------------- +# Tracking Visitor +# ------------------------------------------- + class TrackingVisitor(ast.NodeVisitor): """ AST visitor that identifies and extracts analytics tracking calls from Python code. @@ -68,16 +76,16 @@ class TrackingVisitor(ast.NodeVisitor): function_stack: Stack of function contexts for nested functions var_types: Dictionary of variable types in the current scope var_types_stack: Stack of variable type scopes - custom_function: Optional name of a custom tracking function + custom_config: Optional custom configuration for custom tracking functions """ - def __init__(self, filepath: str, custom_function: Optional[str] = None): + def __init__(self, filepath: str, custom_config: Optional[Dict[str, Any]] = None): """ Initialize the tracking visitor. Args: filepath: Path to the Python file being analyzed - custom_function: Optional name of a custom tracking function to detect + custom_config: Optional custom configuration for custom tracking functions """ self.events: List[AnalyticsEvent] = [] self.filepath = filepath @@ -85,7 +93,18 @@ def __init__(self, filepath: str, custom_function: Optional[str] = None): self.function_stack: List[str] = [] self.var_types: Dict[str, PropertyType] = {} self.var_types_stack: List[Dict[str, PropertyType]] = [] - self.custom_function = custom_function + self.custom_config = custom_config or None + # Store convenience attributes if config provided + if self.custom_config: + self._custom_fn_name: str = self.custom_config.get('functionName', '') + self._event_idx: int = self.custom_config.get('eventIndex', 0) + self._props_idx: int = self.custom_config.get('propertiesIndex', 1) + self._extra_params = self.custom_config.get('extraParams', []) + else: + self._custom_fn_name = None + self._event_idx = 0 + self._props_idx = 1 + self._extra_params = [] def visit_FunctionDef(self, node: ast.FunctionDef) -> None: """ @@ -164,8 +183,11 @@ def extract_type_annotation(self, annotation: ast.AST) -> PropertyType: elif isinstance(annotation, ast.Subscript): # Handle generic types like List[int], Dict[str, int] - if hasattr(annotation.value, 'id'): - container_type = annotation.value.id + container_type = getattr(annotation.value, 'id', None) + if container_type is None and isinstance(annotation.value, ast.Attribute): + container_type = getattr(annotation.value, 'attr', None) + + if container_type: if container_type in ARRAY_TYPES: # Try to get the type parameter for arrays @@ -272,11 +294,12 @@ def detect_source(self, node: ast.Call) -> Optional[str]: def _detect_method_call_source(self, node: ast.Call) -> Optional[str]: """Helper method to detect analytics source from method calls.""" - if not hasattr(node.func.value, 'id'): + obj_val = getattr(node.func, 'value', None) + if obj_val is None: return None - obj_id = node.func.value.id - method_name = node.func.attr + obj_id = _safe_id(obj_val) or '' + method_name = getattr(node.func, 'attr', '') # Check standard analytics libraries for source, config in ANALYTICS_SOURCES.items(): @@ -292,11 +315,17 @@ def _detect_method_call_source(self, node: ast.Call) -> Optional[str]: if method_name == 'track' and self._is_snowplow_tracker_call(node): return 'snowplow' + # Handle dot-separated custom function names like CustomModule.track + if self._custom_fn_name and '.' in self._custom_fn_name: + full_name = f"{obj_id}.{method_name}" + if full_name == self._custom_fn_name: + return 'custom' + return None def _detect_function_call_source(self, node: ast.Call) -> Optional[str]: """Helper method to detect analytics source from direct function calls.""" - func_name = node.func.id + func_name = _safe_id(node.func) or '' # Check for Snowplow direct functions if func_name in ['trackStructEvent', 'buildStructEvent']: @@ -307,7 +336,7 @@ def _detect_function_call_source(self, node: ast.Call) -> Optional[str]: return 'snowplow' # Check for custom tracking function - if self.custom_function and func_name == self.custom_function: + if self._custom_fn_name and func_name == self._custom_fn_name: return 'custom' return None @@ -319,7 +348,7 @@ def _is_amplitude_call(self, node: ast.Call) -> bool: first_arg = node.args[0] if isinstance(first_arg, ast.Call) and isinstance(first_arg.func, ast.Name): - return first_arg.func.id == 'BaseEvent' + return _safe_id(first_arg.func) == 'BaseEvent' return False def _is_snowplow_tracker_call(self, node: ast.Call) -> bool: @@ -330,7 +359,7 @@ def _is_snowplow_tracker_call(self, node: ast.Call) -> bool: first_arg = node.args[0] # Check if first argument is StructuredEvent if isinstance(first_arg, ast.Call) and isinstance(first_arg.func, ast.Name): - return first_arg.func.id == 'StructuredEvent' + return _safe_id(first_arg.func) == 'StructuredEvent' # Also check if it might be a variable (simple heuristic) if isinstance(first_arg, ast.Name) and hasattr(node.func, 'value'): @@ -418,7 +447,7 @@ def _extract_snowplow_event_name(self, node: ast.Call) -> Optional[str]: if len(node.args) >= 1: first_arg = node.args[0] if isinstance(first_arg, ast.Call) and isinstance(first_arg.func, ast.Name): - if first_arg.func.id == 'StructuredEvent': + if _safe_id(first_arg.func) == 'StructuredEvent': # Look for action in keyword arguments for keyword in first_arg.keywords: if keyword.arg == 'action' and isinstance(keyword.value, ast.Constant): @@ -430,9 +459,15 @@ def _extract_snowplow_event_name(self, node: ast.Call) -> Optional[str]: def _extract_custom_event_name(self, node: ast.Call) -> Optional[str]: """Extract event name for custom tracking function.""" - # Standard format: customFunction('event_name', {...}) - if len(node.args) >= 1 and isinstance(node.args[0], ast.Constant): - return node.args[0].value + args = node.args + + # Use configured index if available + if len(args) > self._event_idx and isinstance(args[self._event_idx], ast.Constant): + return args[self._event_idx].value + + # Fallback heuristics + if len(args) >= 1 and isinstance(args[0], ast.Constant): + return args[0].value return None def extract_properties(self, node: ast.Call, source: str) -> EventProperties: @@ -502,6 +537,19 @@ def _extract_user_id(self, node: ast.Call, source: str) -> EventProperties: # Check if event is not anonymous and extract distinct_id user_id_props.update(self._extract_posthog_user_id(node)) + elif source == 'custom': + # Populate extra params defined in custom config as properties + if self._extra_params: + for extra in self._extra_params: + idx = extra.get('idx') + name = extra.get('name') + if idx is None or name is None: + continue + if idx < len(node.args): + prop_type = self._extract_property_type(node.args[idx]) + if prop_type: + user_id_props[name] = prop_type + return user_id_props def _is_non_null_value(self, node: ast.AST) -> bool: @@ -559,10 +607,13 @@ def _get_properties_node(self, node: ast.Call, source: str) -> Optional[ast.Dict return keyword.value elif source == 'custom': - # Properties are in the second argument - if len(node.args) > 1: + # Use configured indices where possible + if len(node.args) > self._props_idx and isinstance(node.args[self._props_idx], ast.Dict): + return node.args[self._props_idx] + # Fallbacks (legacy) + if len(node.args) >= 2 and isinstance(node.args[1], ast.Dict): return node.args[1] - + elif source == 'posthog': # Check named parameters first, then positional for keyword in node.keywords: @@ -576,7 +627,7 @@ def _get_properties_node(self, node: ast.Call, source: str) -> Optional[ast.Dict if len(node.args) >= 1: first_arg = node.args[0] if isinstance(first_arg, ast.Call) and isinstance(first_arg.func, ast.Name): - if first_arg.func.id == 'StructuredEvent': + if _safe_id(first_arg.func) == 'StructuredEvent': # Return None as properties are handled differently for Snowplow return None @@ -618,7 +669,7 @@ def _extract_snowplow_properties(self, node: ast.Call) -> EventProperties: if len(node.args) >= 1: first_arg = node.args[0] if isinstance(first_arg, ast.Call) and isinstance(first_arg.func, ast.Name): - if first_arg.func.id == 'StructuredEvent': + if _safe_id(first_arg.func) == 'StructuredEvent': # Extract all keyword arguments except 'action' for keyword in first_arg.keywords: if keyword.arg and keyword.arg != 'action': @@ -755,7 +806,7 @@ def get_value_type(self, value: Any) -> str: return "null" return "any" -def analyze_python_code(code: str, filepath: str, custom_function: Optional[str] = None) -> str: +def analyze_python_code(code: str, filepath: str, custom_config: Optional[dict[str, any]] = None) -> str: """ Analyze Python code for analytics tracking calls. @@ -765,7 +816,7 @@ def analyze_python_code(code: str, filepath: str, custom_function: Optional[str] Args: code: The Python source code to analyze filepath: Path to the file being analyzed - custom_function: Optional name of a custom tracking function + custom_config: Optional custom configuration for custom tracking functions Returns: JSON string containing array of tracking events @@ -775,7 +826,7 @@ def analyze_python_code(code: str, filepath: str, custom_function: Optional[str] tree = ast.parse(code) # Create visitor and analyze - visitor = TrackingVisitor(filepath, custom_function) + visitor = TrackingVisitor(filepath, custom_config) visitor.visit(tree) # Return events as JSON diff --git a/src/analyze/ruby/extractors.js b/src/analyze/ruby/extractors.js index b86f99a..52a7184 100644 --- a/src/analyze/ruby/extractors.js +++ b/src/analyze/ruby/extractors.js @@ -9,9 +9,10 @@ const { getValueType } = require('./types'); * Extracts the event name from a tracking call based on the source * @param {Object} node - The AST CallNode * @param {string} source - The detected analytics source + * @param {Object} customConfig - Custom configuration for custom functions * @returns {string|null} - The extracted event name or null */ -function extractEventName(node, source) { +function extractEventName(node, source, customConfig = null) { if (source === 'segment' || source === 'rudderstack') { // Both Segment and Rudderstack use the same format const params = node.arguments_?.arguments_?.[0]?.elements; @@ -50,11 +51,21 @@ function extractEventName(node, source) { } if (source === 'custom') { - // Custom function format: customFunction('event_name', {...}) - const args = node.arguments_?.arguments_; - if (args && args.length > 0 && args[0]?.unescaped?.value) { - return args[0].unescaped.value; + const args = node.arguments_?.arguments_ || []; + + if (!customConfig) { + // Fallback: first argument string literal event name + if (args[0]?.unescaped?.value) { + return args[0].unescaped.value; + } + return null; } + + const eventArg = args[customConfig.eventIndex]; + if (eventArg?.unescaped?.value) { + return eventArg.unescaped.value; + } + return null; } return null; @@ -64,9 +75,10 @@ function extractEventName(node, source) { * Extracts properties from a tracking call based on the source * @param {Object} node - The AST CallNode * @param {string} source - The detected analytics source + * @param {Object} customConfig - Custom configuration for custom functions * @returns {Object|null} - The extracted properties or null */ -async function extractProperties(node, source) { +async function extractProperties(node, source, customConfig = null) { const { HashNode, ArrayNode } = await import('@ruby/prism'); if (source === 'segment' || source === 'rudderstack') { @@ -183,11 +195,35 @@ async function extractProperties(node, source) { } if (source === 'custom') { - // Custom function format: customFunction('event_name', {properties}) - const args = node.arguments_?.arguments_; - if (args && args.length > 1 && args[1] instanceof HashNode) { - return await extractHashProperties(args[1]); + const args = node.arguments_?.arguments_ || []; + + if (!customConfig) { + // Legacy fallback behavior + if (args.length > 1 && args[1] instanceof HashNode) { + return await extractHashProperties(args[1]); + } + return null; } + + const properties = {}; + + // Handle extra params first + for (const extra of customConfig.extraParams) { + const argNode = args[extra.idx]; + if (!argNode) continue; + properties[extra.name] = { + type: await getValueType(argNode) + }; + } + + // Handle properties object + const propsArg = args[customConfig.propertiesIndex]; + if (propsArg instanceof HashNode) { + const hashProps = await extractHashProperties(propsArg); + Object.assign(properties, hashProps); + } + + return Object.keys(properties).length > 0 ? properties : null; } return null; diff --git a/src/analyze/ruby/index.js b/src/analyze/ruby/index.js index d1efe53..3bd2467 100644 --- a/src/analyze/ruby/index.js +++ b/src/analyze/ruby/index.js @@ -5,6 +5,7 @@ const fs = require('fs'); const TrackingVisitor = require('./visitor'); +const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); // Lazy-loaded parse function from Ruby Prism let parse = null; @@ -16,7 +17,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, customFunction) { +async function analyzeRubyFile(filePath, customFunctionSignature) { // Lazy load the Ruby Prism parser if (!parse) { const { loadPrism } = await import('@ruby/prism'); @@ -37,7 +38,8 @@ async function analyzeRubyFile(filePath, customFunction) { } // Create a visitor and analyze the AST - const visitor = new TrackingVisitor(code, filePath, customFunction); + const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; + const visitor = new TrackingVisitor(code, filePath, customConfig); const events = await visitor.analyze(ast); return events; diff --git a/src/analyze/ruby/visitor.js b/src/analyze/ruby/visitor.js index 75c5c95..120ab4c 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, customFunction = null) { + constructor(code, filePath, customConfig = null) { this.code = code; this.filePath = filePath; - this.customFunction = customFunction; + this.customConfig = customConfig; this.events = []; } @@ -22,10 +22,10 @@ class TrackingVisitor { */ async processCallNode(node, ancestors) { try { - const source = detectSource(node, this.customFunction); + const source = detectSource(node, this.customConfig?.functionName); if (!source) return; - const eventName = extractEventName(node, source); + const eventName = extractEventName(node, source, this.customConfig); if (!eventName) return; const line = getLineNumber(this.code, node.location); @@ -33,13 +33,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.customFunction && this.customFunction.includes('.')) { - functionName = this.customFunction; + if (source === 'custom' && this.customConfig && this.customConfig.functionName.includes('.')) { + functionName = this.customConfig.functionName; } else { functionName = await findWrappingFunction(node, ancestors); } - const properties = await extractProperties(node, source); + const properties = await extractProperties(node, source, this.customConfig); this.events.push({ eventName, diff --git a/src/analyze/typescript/extractors/event-extractor.js b/src/analyze/typescript/extractors/event-extractor.js index 6b01916..1e70e38 100644 --- a/src/analyze/typescript/extractors/event-extractor.js +++ b/src/analyze/typescript/extractors/event-extractor.js @@ -21,6 +21,7 @@ const EXTRACTION_STRATEGIES = { googleanalytics: extractGoogleAnalyticsEvent, snowplow: extractSnowplowEvent, mparticle: extractMparticleEvent, + custom: extractCustomEvent, default: extractDefaultEvent }; @@ -30,10 +31,14 @@ const EXTRACTION_STRATEGIES = { * @param {string} source - Analytics provider source * @param {Object} checker - TypeScript type checker * @param {Object} sourceFile - TypeScript source file + * @param {Object} customConfig - Custom configuration for custom extraction * @returns {EventData} Extracted event data */ -function extractEventData(node, source, checker, sourceFile) { +function extractEventData(node, source, checker, sourceFile, customConfig) { const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default; + if (source === 'custom') { + return strategy(node, checker, sourceFile, customConfig); + } return strategy(node, checker, sourceFile); } @@ -121,6 +126,32 @@ function extractMparticleEvent(node, checker, sourceFile) { return { eventName, propertiesNode }; } +/** + * Custom extraction + * @param {Object} node - CallExpression node + * @param {Object} checker - TypeScript type checker + * @param {Object} sourceFile - TypeScript source file + * @param {Object} customConfig - Custom configuration for custom extraction + * @returns {EventData} + */ +function extractCustomEvent(node, checker, sourceFile, customConfig) { + const args = node.arguments || []; + + const eventArg = args[customConfig?.eventIndex ?? 0]; + const propertiesArg = args[customConfig?.propertiesIndex ?? 1]; + + const eventName = getStringValue(eventArg, checker, sourceFile); + + const extraArgs = {}; + if (customConfig && customConfig.extraParams) { + customConfig.extraParams.forEach(extra => { + extraArgs[extra.name] = args[extra.idx]; + }); + } + + return { eventName, propertiesNode: propertiesArg, extraArgs }; +} + /** * Default event extraction for standard providers * @param {Object} node - CallExpression node @@ -149,9 +180,10 @@ function extractDefaultEvent(node, checker, sourceFile) { * @param {string} functionName - Containing function name * @param {Object} checker - TypeScript type checker * @param {Object} sourceFile - TypeScript source file + * @param {Object} customConfig - Custom configuration for custom extraction * @returns {Object|null} Processed event object or null */ -function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile) { +function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile, customConfig) { const { eventName, propertiesNode } = eventData; if (!eventName || !propertiesNode) { @@ -184,6 +216,15 @@ function processEventData(eventData, source, filePath, line, functionName, check // Clean up any unresolved type markers const cleanedProperties = cleanupProperties(properties); + // Handle custom extra params + if (source === 'custom' && customConfig && eventData.extraArgs) { + for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) { + cleanedProperties[paramName] = { + type: inferNodeValueType(argNode) + }; + } + } + return { eventName, source, @@ -368,6 +409,16 @@ function cleanupProperties(properties) { return cleaned; } +function inferNodeValueType(node) { + if (!node) return 'any'; + if (ts.isStringLiteral(node)) return 'string'; + if (ts.isNumericLiteral(node)) return 'number'; + if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean'; + if (ts.isArrayLiteralExpression(node)) return 'array'; + if (ts.isObjectLiteralExpression(node)) return 'object'; + return 'any'; +} + module.exports = { extractEventData, processEventData diff --git a/src/analyze/typescript/index.js b/src/analyze/typescript/index.js index aff9844..708ef7c 100644 --- a/src/analyze/typescript/index.js +++ b/src/analyze/typescript/index.js @@ -4,16 +4,18 @@ */ const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = require('./parser'); +const { parseCustomFunctionSignature } = require('../utils/customFunctionParser'); /** * Analyzes a TypeScript file for analytics tracking calls * @param {string} filePath - Path to the TypeScript file to analyze * @param {Object} [program] - Optional existing TypeScript program to reuse - * @param {string} [customFunction] - Optional custom function name to detect + * @param {string} [customFunctionSignature] - Optional custom function signature to detect * @returns {Array} Array of tracking events found in the file */ -function analyzeTsFile(filePath, program, customFunction) { +function analyzeTsFile(filePath, program = null, customFunctionSignature = null) { const events = []; + const customConfig = customFunctionSignature ? parseCustomFunctionSignature(customFunctionSignature) : null; try { // Get or create TypeScript program @@ -29,7 +31,7 @@ function analyzeTsFile(filePath, program, customFunction) { const checker = tsProgram.getTypeChecker(); // Find and extract tracking events - const foundEvents = findTrackingEvents(sourceFile, checker, filePath, customFunction); + const foundEvents = findTrackingEvents(sourceFile, checker, filePath, customConfig); events.push(...foundEvents); } catch (error) { diff --git a/src/analyze/typescript/parser.js b/src/analyze/typescript/parser.js index ae863d3..bd60e07 100644 --- a/src/analyze/typescript/parser.js +++ b/src/analyze/typescript/parser.js @@ -65,10 +65,10 @@ 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 {string} [customFunction] - Custom function name to detect + * @param {Object} [customConfig] - Custom function configuration * @returns {Array} Array of found events */ -function findTrackingEvents(sourceFile, checker, filePath, customFunction) { +function findTrackingEvents(sourceFile, checker, filePath, customConfig) { const events = []; /** @@ -78,7 +78,7 @@ function findTrackingEvents(sourceFile, checker, filePath, customFunction) { function visit(node) { try { if (ts.isCallExpression(node)) { - const event = extractTrackingEvent(node, sourceFile, checker, filePath, customFunction); + const event = extractTrackingEvent(node, sourceFile, checker, filePath, customConfig); if (event) { events.push(event); } @@ -102,25 +102,25 @@ function findTrackingEvents(sourceFile, checker, filePath, customFunction) { * @param {Object} sourceFile - TypeScript source file * @param {Object} checker - TypeScript type checker * @param {string} filePath - File path - * @param {string} [customFunction] - Custom function name + * @param {Object} [customConfig] - Custom function configuration * @returns {Object|null} Extracted event or null */ -function extractTrackingEvent(node, sourceFile, checker, filePath, customFunction) { +function extractTrackingEvent(node, sourceFile, checker, filePath, customConfig) { // Detect the analytics source - const source = detectAnalyticsSource(node, customFunction); + const source = detectAnalyticsSource(node, customConfig?.functionName); if (source === 'unknown') { return null; } // Extract event data based on the source - const eventData = extractEventData(node, source, checker, sourceFile); + const eventData = extractEventData(node, source, checker, sourceFile, customConfig); // Get location and context information const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1; const functionName = findWrappingFunction(node); // Process the event data into final format - return processEventData(eventData, source, filePath, line, functionName, checker, sourceFile); + return processEventData(eventData, source, filePath, line, functionName, checker, sourceFile, customConfig); } module.exports = { diff --git a/src/analyze/utils/customFunctionParser.js b/src/analyze/utils/customFunctionParser.js new file mode 100644 index 0000000..149e919 --- /dev/null +++ b/src/analyze/utils/customFunctionParser.js @@ -0,0 +1,55 @@ +// Create new file with parser implementation +function parseCustomFunctionSignature(signature) { + if (!signature || typeof signature !== 'string') { + return null; + } + + // Match function name and optional parameter list + // Supports names with module prefix like Module.func + const match = signature.match(/^\s*([A-Za-z0-9_.]+)\s*(?:\(([^)]*)\))?\s*$/); + if (!match) { + return null; + } + + const functionName = match[1].trim(); + const paramsPart = match[2]; + + // Default legacy behaviour: EVENT_NAME, PROPERTIES + if (!paramsPart) { + return { + functionName, + eventIndex: 0, + propertiesIndex: 1, + extraParams: [] + }; + } + + // Split params by comma, trimming whitespace + const params = paramsPart.split(',').map(p => p.trim()).filter(Boolean); + + const eventIndex = params.findIndex(p => p.toUpperCase() === 'EVENT_NAME'); + let propertiesIndex = params.findIndex(p => p.toUpperCase() === 'PROPERTIES'); + + if (eventIndex === -1) { + throw new Error('EVENT_NAME is required in custom function signature'); + } + + if (propertiesIndex === -1) { + // If PROPERTIES is missing, assume it's at the end of the parameters + propertiesIndex = params.length; + } + + const extraParams = params.map((name, idx) => ({ idx, name })) + .filter(p => !(p.idx === eventIndex || p.idx === propertiesIndex)); + + return { + functionName, + eventIndex, + propertiesIndex, + extraParams + }; +} + +module.exports = { + parseCustomFunctionSignature +}; diff --git a/tests/analyzeGo.test.js b/tests/analyzeGo.test.js index f6b9073..02858ef 100644 --- a/tests/analyzeGo.test.js +++ b/tests/analyzeGo.test.js @@ -8,7 +8,7 @@ test.describe('analyzeGoFile', () => { const testFilePath = path.join(fixturesDir, 'go', 'main.go'); test('should correctly analyze Go file with multiple tracking providers', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeGoFile(testFilePath, customFunction); // Sort events by eventName for consistent ordering @@ -21,7 +21,7 @@ test.describe('analyzeGoFile', () => { assert.ok(amplitudeEvent); assert.strictEqual(amplitudeEvent.source, 'amplitude'); assert.strictEqual(amplitudeEvent.functionName, 'amplitudeTrack'); - assert.strictEqual(amplitudeEvent.line, 44); + assert.strictEqual(amplitudeEvent.line, 52); assert.deepStrictEqual(amplitudeEvent.properties, { UserID: { type: 'string' }, name: { type: 'string' }, @@ -35,7 +35,7 @@ test.describe('analyzeGoFile', () => { assert.ok(segmentEvent); assert.strictEqual(segmentEvent.source, 'segment'); assert.strictEqual(segmentEvent.functionName, 'segmentTrack'); - assert.strictEqual(segmentEvent.line, 21); + assert.strictEqual(segmentEvent.line, 29); assert.deepStrictEqual(segmentEvent.properties, { UserId: { type: 'string' }, plan: { type: 'string' }, @@ -47,7 +47,7 @@ test.describe('analyzeGoFile', () => { assert.ok(mixpanelEvent); assert.strictEqual(mixpanelEvent.source, 'mixpanel'); assert.strictEqual(mixpanelEvent.functionName, 'mixpanelTrack'); - assert.strictEqual(mixpanelEvent.line, 33); + assert.strictEqual(mixpanelEvent.line, 41); assert.deepStrictEqual(mixpanelEvent.properties, { DistinctId: { type: 'string' }, plan: { type: 'string' }, @@ -59,7 +59,7 @@ test.describe('analyzeGoFile', () => { assert.ok(posthogEvent); assert.strictEqual(posthogEvent.source, 'posthog'); assert.strictEqual(posthogEvent.functionName, 'posthogTrack'); - assert.strictEqual(posthogEvent.line, 64); + assert.strictEqual(posthogEvent.line, 72); assert.deepStrictEqual(posthogEvent.properties, { DistinctId: { type: 'string' }, login_type: { type: 'string' }, @@ -72,7 +72,7 @@ test.describe('analyzeGoFile', () => { assert.ok(snowplowEvent); assert.strictEqual(snowplowEvent.source, 'snowplow'); assert.strictEqual(snowplowEvent.functionName, 'snowplowTrack'); - assert.strictEqual(snowplowEvent.line, 81); + assert.strictEqual(snowplowEvent.line, 89); assert.deepStrictEqual(snowplowEvent.properties, { Category: { type: 'string' }, Property: { type: 'string' }, @@ -84,8 +84,9 @@ test.describe('analyzeGoFile', () => { assert.ok(customEvent); assert.strictEqual(customEvent.source, 'custom'); assert.strictEqual(customEvent.functionName, 'main'); - assert.strictEqual(customEvent.line, 105); + assert.strictEqual(customEvent.line, 113); assert.deepStrictEqual(customEvent.properties, { + userId: { type: 'string' }, foo: { type: 'string' }, baz: { type: 'number' }, list: { type: 'array', items: { type: 'string' } }, @@ -115,7 +116,7 @@ test.describe('analyzeGoFile', () => { }); test('should handle nested property types correctly', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeGoFile(testFilePath, customFunction); const customEvent = events.find(e => e.eventName === 'custom_event'); @@ -139,7 +140,7 @@ test.describe('analyzeGoFile', () => { }); test('should match expected tracking-schema.yaml output', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeGoFile(testFilePath, customFunction); // Create a map of events by name for easier verification @@ -162,7 +163,7 @@ test.describe('analyzeGoFile', () => { assert.ok(eventMap['Signed Up']); assert.strictEqual(eventMap['Signed Up'].eventName, 'Signed Up'); assert.strictEqual(eventMap['Signed Up'].source, 'segment'); - assert.strictEqual(eventMap['Signed Up'].line, 21); + assert.strictEqual(eventMap['Signed Up'].line, 29); assert.strictEqual(eventMap['Signed Up'].functionName, 'segmentTrack'); assert.deepStrictEqual(eventMap['Signed Up'].properties, { UserId: { type: 'string' }, @@ -173,7 +174,7 @@ test.describe('analyzeGoFile', () => { assert.ok(eventMap['some_event']); assert.strictEqual(eventMap['some_event'].eventName, 'some_event'); assert.strictEqual(eventMap['some_event'].source, 'mixpanel'); - assert.strictEqual(eventMap['some_event'].line, 33); + assert.strictEqual(eventMap['some_event'].line, 41); assert.strictEqual(eventMap['some_event'].functionName, 'mixpanelTrack'); assert.deepStrictEqual(eventMap['some_event'].properties, { DistinctId: { type: 'string' }, @@ -184,7 +185,7 @@ test.describe('analyzeGoFile', () => { assert.ok(eventMap['Button Clicked']); assert.strictEqual(eventMap['Button Clicked'].eventName, 'Button Clicked'); assert.strictEqual(eventMap['Button Clicked'].source, 'amplitude'); - assert.strictEqual(eventMap['Button Clicked'].line, 44); + assert.strictEqual(eventMap['Button Clicked'].line, 52); assert.strictEqual(eventMap['Button Clicked'].functionName, 'amplitudeTrack'); assert.deepStrictEqual(eventMap['Button Clicked'].properties, { UserID: { type: 'string' }, @@ -197,7 +198,7 @@ test.describe('analyzeGoFile', () => { assert.ok(eventMap['user_signed_up']); assert.strictEqual(eventMap['user_signed_up'].eventName, 'user_signed_up'); assert.strictEqual(eventMap['user_signed_up'].source, 'posthog'); - assert.strictEqual(eventMap['user_signed_up'].line, 64); + assert.strictEqual(eventMap['user_signed_up'].line, 72); assert.strictEqual(eventMap['user_signed_up'].functionName, 'posthogTrack'); assert.deepStrictEqual(eventMap['user_signed_up'].properties, { DistinctId: { type: 'string' }, @@ -209,7 +210,7 @@ test.describe('analyzeGoFile', () => { assert.ok(eventMap['add-to-basket']); assert.strictEqual(eventMap['add-to-basket'].eventName, 'add-to-basket'); assert.strictEqual(eventMap['add-to-basket'].source, 'snowplow'); - assert.strictEqual(eventMap['add-to-basket'].line, 81); + assert.strictEqual(eventMap['add-to-basket'].line, 89); assert.strictEqual(eventMap['add-to-basket'].functionName, 'snowplowTrack'); assert.deepStrictEqual(eventMap['add-to-basket'].properties, { Category: { type: 'string' }, @@ -220,9 +221,10 @@ test.describe('analyzeGoFile', () => { assert.ok(eventMap['custom_event']); assert.strictEqual(eventMap['custom_event'].eventName, 'custom_event'); assert.strictEqual(eventMap['custom_event'].source, 'custom'); - assert.strictEqual(eventMap['custom_event'].line, 105); + assert.strictEqual(eventMap['custom_event'].line, 113); assert.strictEqual(eventMap['custom_event'].functionName, 'main'); assert.deepStrictEqual(eventMap['custom_event'].properties, { + userId: { type: 'string' }, foo: { type: 'string' }, baz: { type: 'number' }, list: { @@ -239,4 +241,20 @@ test.describe('analyzeGoFile', () => { } }); }); + + test('should detect events for all custom function signature variations', async () => { + const variants = [ + { sig: 'customTrackFunction0', event: 'custom_event0' }, + { sig: 'customTrackFunction1(EVENT_NAME, PROPERTIES)', event: 'custom_event1' }, + { sig: 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', event: 'custom_event2' }, + { sig: 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', event: 'custom_event3' }, + { sig: 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', event: 'custom_event4' }, + ]; + + for (const { sig, event } of variants) { + const events = await analyzeGoFile(testFilePath, sig); + const found = events.find(e => e.eventName === event && e.source === 'custom'); + assert.ok(found, `Should detect ${event} for signature ${sig}`); + } + }); }); diff --git a/tests/analyzeJavaScript.test.js b/tests/analyzeJavaScript.test.js index 2175303..993aaab 100644 --- a/tests/analyzeJavaScript.test.js +++ b/tests/analyzeJavaScript.test.js @@ -8,7 +8,7 @@ test.describe('analyzeJsFile', () => { const testFilePath = path.join(fixturesDir, 'javascript', 'main.js'); test('should correctly analyze JavaScript file with multiple tracking providers', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); // Sort events by line number for consistent ordering @@ -189,6 +189,7 @@ test.describe('analyzeJsFile', () => { assert.strictEqual(customEvent.functionName, 'global'); assert.strictEqual(customEvent.line, 152); assert.deepStrictEqual(customEvent.properties, { + userId: { type: 'string' }, order_id: { type: 'string' }, value: { type: 'number' }, list: { @@ -219,7 +220,7 @@ test.describe('analyzeJsFile', () => { }); test('should handle nested property types correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); // Test nested object properties @@ -243,7 +244,7 @@ test.describe('analyzeJsFile', () => { }); test('should detect array types correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); // Test array of objects @@ -264,7 +265,7 @@ test.describe('analyzeJsFile', () => { }); test('should handle different function contexts correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); // Test function declaration @@ -285,7 +286,7 @@ test.describe('analyzeJsFile', () => { }); test('should handle case variations in provider names', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); // mParticle is used with lowercase 'p' in the test file @@ -295,7 +296,7 @@ test.describe('analyzeJsFile', () => { }); test('should exclude action field from Snowplow properties', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); const snowplowEvent = events.find(e => e.source === 'snowplow'); @@ -307,7 +308,7 @@ test.describe('analyzeJsFile', () => { }); test('should handle mParticle three-parameter format', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = analyzeJsFile(testFilePath, customFunction); const mparticleEvent = events.find(e => e.source === 'mparticle'); @@ -316,4 +317,21 @@ test.describe('analyzeJsFile', () => { // Event name is first param, properties are third param assert.ok(mparticleEvent.properties.order_id); }); + + test('should detect events for all custom function signature variations', () => { + const variants = [ + { sig: 'customTrackFunction0', event: 'custom_event0' }, + { sig: 'customTrackFunction1(EVENT_NAME, PROPERTIES)', event: 'custom_event1' }, + { sig: 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', event: 'custom_event2' }, + { sig: 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', event: 'custom_event3' }, + { sig: 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', event: 'custom_event4' }, + { sig: 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)', event: 'custom_module_event' }, + ]; + + variants.forEach(({ sig, event }) => { + const events = analyzeJsFile(testFilePath, sig); + const found = events.find(e => e.eventName === event && e.source === 'custom'); + assert.ok(found, `Should detect ${event} for signature ${sig}`); + }); + }); }); diff --git a/tests/analyzePython.test.js b/tests/analyzePython.test.js index a48b027..984c88c 100644 --- a/tests/analyzePython.test.js +++ b/tests/analyzePython.test.js @@ -9,7 +9,7 @@ test.describe('analyzePythonFile', () => { const testFilePath = path.join(fixturesDir, 'python', 'main.py'); test('should correctly analyze Python file with multiple tracking providers', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzePythonFile(testFilePath, customFunction); // Sort events by eventName for consistent ordering @@ -22,7 +22,7 @@ test.describe('analyzePythonFile', () => { assert.ok(segmentEvent); assert.strictEqual(segmentEvent.source, 'segment'); assert.strictEqual(segmentEvent.functionName, 'segment_track'); - assert.strictEqual(segmentEvent.line, 11); + assert.strictEqual(segmentEvent.line, 80); assert.deepStrictEqual(segmentEvent.properties, { user_id: { type: 'string' }, method: { type: 'string' }, @@ -35,7 +35,7 @@ test.describe('analyzePythonFile', () => { assert.ok(mixpanelEvent); assert.strictEqual(mixpanelEvent.source, 'mixpanel'); assert.strictEqual(mixpanelEvent.functionName, 'mixpanel_track'); - assert.strictEqual(mixpanelEvent.line, 21); + assert.strictEqual(mixpanelEvent.line, 90); assert.deepStrictEqual(mixpanelEvent.properties, { distinct_id: { type: 'string' }, plan: { type: 'string' }, @@ -48,7 +48,7 @@ test.describe('analyzePythonFile', () => { assert.ok(amplitudeEvent); assert.strictEqual(amplitudeEvent.source, 'amplitude'); assert.strictEqual(amplitudeEvent.functionName, 'amplitude_track'); - assert.strictEqual(amplitudeEvent.line, 31); + assert.strictEqual(amplitudeEvent.line, 100); assert.deepStrictEqual(amplitudeEvent.properties, { user_id: { type: 'string' }, color: { type: 'string' }, @@ -60,7 +60,7 @@ test.describe('analyzePythonFile', () => { assert.ok(rudderstackEvent); assert.strictEqual(rudderstackEvent.source, 'rudderstack'); assert.strictEqual(rudderstackEvent.functionName, 'rudderstack_track'); - assert.strictEqual(rudderstackEvent.line, 47); + assert.strictEqual(rudderstackEvent.line, 116); assert.deepStrictEqual(rudderstackEvent.properties, { user_id: { type: 'string' }, timestamp: { type: 'number' }, @@ -73,7 +73,7 @@ test.describe('analyzePythonFile', () => { assert.ok(posthogEvent1); assert.strictEqual(posthogEvent1.source, 'posthog'); assert.strictEqual(posthogEvent1.functionName, 'posthog_capture'); - assert.strictEqual(posthogEvent1.line, 58); + assert.strictEqual(posthogEvent1.line, 127); assert.deepStrictEqual(posthogEvent1.properties, { method: { type: 'string' }, is_free_trial: { type: 'boolean' }, @@ -84,7 +84,7 @@ test.describe('analyzePythonFile', () => { assert.ok(posthogEvent2); assert.strictEqual(posthogEvent2.source, 'posthog'); assert.strictEqual(posthogEvent2.functionName, 'posthog_capture'); - assert.strictEqual(posthogEvent2.line, 64); + assert.strictEqual(posthogEvent2.line, 133); assert.deepStrictEqual(posthogEvent2.properties, { method: { type: 'string' }, is_free_trial: { type: 'boolean' }, @@ -96,7 +96,7 @@ test.describe('analyzePythonFile', () => { assert.ok(snowplowEvent); assert.strictEqual(snowplowEvent.source, 'snowplow'); assert.strictEqual(snowplowEvent.functionName, 'snowplow_track_events'); - assert.strictEqual(snowplowEvent.line, 74); + assert.strictEqual(snowplowEvent.line, 143); assert.deepStrictEqual(snowplowEvent.properties, { category: { type: 'string' }, label: { type: 'string' }, @@ -109,8 +109,9 @@ test.describe('analyzePythonFile', () => { assert.ok(customEvent); assert.strictEqual(customEvent.source, 'custom'); assert.strictEqual(customEvent.functionName, 'main'); - assert.strictEqual(customEvent.line, 89); + assert.strictEqual(customEvent.line, 158); assert.deepStrictEqual(customEvent.properties, { + userId: { type: 'string' }, key: { type: 'string' }, nested: { type: 'object', @@ -136,7 +137,7 @@ test.describe('analyzePythonFile', () => { }); test('should handle nested property types correctly', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzePythonFile(testFilePath, customFunction); const customEvent = events.find(e => e.eventName === 'custom_event'); @@ -152,7 +153,7 @@ test.describe('analyzePythonFile', () => { }); test('should match expected tracking-schema.yaml output', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzePythonFile(testFilePath, customFunction); // Create a map of events by name for easier verification @@ -177,7 +178,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['User Signed Up']); assert.strictEqual(eventMap['User Signed Up'].eventName, 'User Signed Up'); assert.strictEqual(eventMap['User Signed Up'].source, 'segment'); - assert.strictEqual(eventMap['User Signed Up'].line, 11); + assert.strictEqual(eventMap['User Signed Up'].line, 80); assert.strictEqual(eventMap['User Signed Up'].functionName, 'segment_track'); assert.deepStrictEqual(eventMap['User Signed Up'].properties, { user_id: { type: 'string' }, @@ -189,7 +190,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['Purchase Completed']); assert.strictEqual(eventMap['Purchase Completed'].eventName, 'Purchase Completed'); assert.strictEqual(eventMap['Purchase Completed'].source, 'mixpanel'); - assert.strictEqual(eventMap['Purchase Completed'].line, 21); + assert.strictEqual(eventMap['Purchase Completed'].line, 90); assert.strictEqual(eventMap['Purchase Completed'].functionName, 'mixpanel_track'); assert.deepStrictEqual(eventMap['Purchase Completed'].properties, { distinct_id: { type: 'string' }, @@ -201,7 +202,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['Button Clicked']); assert.strictEqual(eventMap['Button Clicked'].eventName, 'Button Clicked'); assert.strictEqual(eventMap['Button Clicked'].source, 'amplitude'); - assert.strictEqual(eventMap['Button Clicked'].line, 31); + assert.strictEqual(eventMap['Button Clicked'].line, 100); assert.strictEqual(eventMap['Button Clicked'].functionName, 'amplitude_track'); assert.deepStrictEqual(eventMap['Button Clicked'].properties, { user_id: { type: 'string' }, @@ -212,7 +213,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['User Logged In']); assert.strictEqual(eventMap['User Logged In'].eventName, 'User Logged In'); assert.strictEqual(eventMap['User Logged In'].source, 'rudderstack'); - assert.strictEqual(eventMap['User Logged In'].line, 47); + assert.strictEqual(eventMap['User Logged In'].line, 116); assert.strictEqual(eventMap['User Logged In'].functionName, 'rudderstack_track'); assert.deepStrictEqual(eventMap['User Logged In'].properties, { user_id: { type: 'string' }, @@ -224,7 +225,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['user_signed_up']); assert.strictEqual(eventMap['user_signed_up'].eventName, 'user_signed_up'); assert.strictEqual(eventMap['user_signed_up'].source, 'posthog'); - assert.strictEqual(eventMap['user_signed_up'].line, 58); + assert.strictEqual(eventMap['user_signed_up'].line, 127); assert.strictEqual(eventMap['user_signed_up'].functionName, 'posthog_capture'); assert.deepStrictEqual(eventMap['user_signed_up'].properties, { method: { type: 'string' }, @@ -235,7 +236,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['user_cancelled_subscription']); assert.strictEqual(eventMap['user_cancelled_subscription'].eventName, 'user_cancelled_subscription'); assert.strictEqual(eventMap['user_cancelled_subscription'].source, 'posthog'); - assert.strictEqual(eventMap['user_cancelled_subscription'].line, 64); + assert.strictEqual(eventMap['user_cancelled_subscription'].line, 133); assert.strictEqual(eventMap['user_cancelled_subscription'].functionName, 'posthog_capture'); assert.deepStrictEqual(eventMap['user_cancelled_subscription'].properties, { method: { type: 'string' }, @@ -246,7 +247,7 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['add-to-basket']); assert.strictEqual(eventMap['add-to-basket'].eventName, 'add-to-basket'); assert.strictEqual(eventMap['add-to-basket'].source, 'snowplow'); - assert.strictEqual(eventMap['add-to-basket'].line, 74); + assert.strictEqual(eventMap['add-to-basket'].line, 143); assert.strictEqual(eventMap['add-to-basket'].functionName, 'snowplow_track_events'); assert.deepStrictEqual(eventMap['add-to-basket'].properties, { category: { type: 'string' }, @@ -258,9 +259,10 @@ test.describe('analyzePythonFile', () => { assert.ok(eventMap['custom_event']); assert.strictEqual(eventMap['custom_event'].eventName, 'custom_event'); assert.strictEqual(eventMap['custom_event'].source, 'custom'); - assert.strictEqual(eventMap['custom_event'].line, 89); + assert.strictEqual(eventMap['custom_event'].line, 158); assert.strictEqual(eventMap['custom_event'].functionName, 'main'); assert.deepStrictEqual(eventMap['custom_event'].properties, { + userId: { type: 'string' }, key: { type: 'string' }, nested: { type: 'object', @@ -281,18 +283,18 @@ from typing import List, Dict, Any def track_with_types() -> None: items: List[int] = [1, 2, 3] config: Dict[str, Any] = {"enabled": True} - customTrackFunction("types_test", { + customTrackFunction("user111", "types_test", { "items": items, "config": config, "inline_list": [1, 2, 3], "inline_dict": {"a": 1, "b": "two"} }) -def customTrackFunction(event_name: str, params: Dict[str, Any]) -> None: +def customTrackFunction(user_id: str, event_name: str, params: Dict[str, Any]) -> None: pass `); - const events = await analyzePythonFile(typeTestFile, 'customTrackFunction'); + const events = await analyzePythonFile(typeTestFile, 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'); assert.strictEqual(events.length, 1); const event = events[0]; @@ -311,4 +313,21 @@ def customTrackFunction(event_name: str, params: Dict[str, Any]) -> None: // Clean up fs.unlinkSync(typeTestFile); }); + + test('should detect events for all custom function signature variations', async () => { + const variants = [ + { sig: 'customTrackFunction0', event: 'custom_event0' }, + { sig: 'customTrackFunction1(EVENT_NAME, PROPERTIES)', event: 'custom_event1' }, + { sig: 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', event: 'custom_event2' }, + { sig: 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', event: 'custom_event3' }, + { sig: 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', event: 'custom_event4' }, + { sig: 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)', event: 'custom_module_event' }, + ]; + + for (const { sig, event } of variants) { + const events = await analyzePythonFile(testFilePath, sig); + const found = events.find(e => e.eventName === event && e.source === 'custom'); + assert.ok(found, `Should detect ${event} for signature ${sig}`); + } + }); }); diff --git a/tests/analyzeRuby.test.js b/tests/analyzeRuby.test.js index 6829f39..5781985 100644 --- a/tests/analyzeRuby.test.js +++ b/tests/analyzeRuby.test.js @@ -8,7 +8,7 @@ test.describe('analyzeRubyFile', () => { const testFilePath = path.join(fixturesDir, 'ruby', 'main.rb'); test('should correctly analyze Ruby file with multiple tracking providers', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeRubyFile(testFilePath, customFunction); // Sort events by eventName for consistent ordering @@ -86,6 +86,7 @@ test.describe('analyzeRubyFile', () => { assert.strictEqual(customEvent.functionName, 'custom_track_event'); assert.strictEqual(customEvent.line, 79); assert.deepStrictEqual(customEvent.properties, { + userId: { type: 'string' }, key: { type: 'string' }, nested: { type: 'object', @@ -125,7 +126,7 @@ test.describe('analyzeRubyFile', () => { }); test('should handle nested property types correctly', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeRubyFile(testFilePath, customFunction); const customEvent = events.find(e => e.eventName === 'custom_event'); @@ -157,7 +158,7 @@ test.describe('analyzeRubyFile', () => { }); test('should handle all property types correctly', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeRubyFile(testFilePath, customFunction); // Test string properties @@ -179,7 +180,7 @@ test.describe('analyzeRubyFile', () => { }); test('should correctly identify function names in different contexts', async () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeRubyFile(testFilePath, customFunction); // Verify function names are correctly identified @@ -196,7 +197,7 @@ test.describe('analyzeRubyFile', () => { }); test('should detect custom functions that are methods of a module', async () => { - const customFunction = 'CustomModule.track'; + const customFunction = 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)'; const events = await analyzeRubyFile(testFilePath, customFunction); // Should find the CustomModule.track call @@ -205,6 +206,7 @@ test.describe('analyzeRubyFile', () => { assert.strictEqual(customModuleEvent.eventName, 'custom_event'); assert.strictEqual(customModuleEvent.line, 98); assert.deepStrictEqual(customModuleEvent.properties, { + userId: { type: 'string' }, key: { type: 'string' }, nested: { type: 'object', @@ -231,4 +233,20 @@ test.describe('analyzeRubyFile', () => { assert.ok(rudderstackEvent); assert.strictEqual(rudderstackEvent.source, 'rudderstack'); }); + + test('should detect events for all custom function signature variations', async () => { + const variants = [ + { sig: 'customTrackFunction0', event: 'custom_event0' }, + { sig: 'customTrackFunction1(EVENT_NAME, PROPERTIES)', event: 'custom_event1' }, + { sig: 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', event: 'custom_event2' }, + { sig: 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', event: 'custom_event3' }, + { sig: 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', event: 'custom_event4' }, + ]; + + for (const { sig, event } of variants) { + const events = await analyzeRubyFile(testFilePath, sig); + const found = events.find(e => e.eventName === event && e.source === 'custom'); + assert.ok(found, `Should detect ${event} for signature ${sig}`); + } + }); }); diff --git a/tests/analyzeTypeScript.test.js b/tests/analyzeTypeScript.test.js index 9acce13..cb9fbf9 100644 --- a/tests/analyzeTypeScript.test.js +++ b/tests/analyzeTypeScript.test.js @@ -22,7 +22,7 @@ test.describe('analyzeTsFile', () => { } test('should correctly analyze TypeScript file with multiple tracking providers', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -36,7 +36,7 @@ test.describe('analyzeTsFile', () => { assert.ok(gaEvent); assert.strictEqual(gaEvent.source, 'googleanalytics'); assert.strictEqual(gaEvent.functionName, 'trackOrderCompletedGA'); - assert.strictEqual(gaEvent.line, 104); + assert.strictEqual(gaEvent.line, 105); assert.deepStrictEqual(gaEvent.properties, { order_id: { type: 'string' }, products: { @@ -68,7 +68,7 @@ test.describe('analyzeTsFile', () => { assert.ok(segmentEvent); assert.strictEqual(segmentEvent.source, 'segment'); assert.strictEqual(segmentEvent.functionName, 'checkout'); - assert.strictEqual(segmentEvent.line, 120); + assert.strictEqual(segmentEvent.line, 121); assert.deepStrictEqual(segmentEvent.properties, { stage: { type: 'string' }, method: { type: 'string' }, @@ -80,7 +80,7 @@ test.describe('analyzeTsFile', () => { assert.ok(mixpanelEvent); assert.strictEqual(mixpanelEvent.source, 'mixpanel'); assert.strictEqual(mixpanelEvent.functionName, 'confirmPurchaseMixpanel'); - assert.strictEqual(mixpanelEvent.line, 129); + assert.strictEqual(mixpanelEvent.line, 130); assert.deepStrictEqual(mixpanelEvent.properties, { order_id: { type: 'string' }, items: { @@ -103,7 +103,7 @@ test.describe('analyzeTsFile', () => { assert.ok(amplitudeEvent); assert.strictEqual(amplitudeEvent.source, 'amplitude'); assert.strictEqual(amplitudeEvent.functionName, 'checkout'); - assert.strictEqual(amplitudeEvent.line, 134); + assert.strictEqual(amplitudeEvent.line, 135); assert.deepStrictEqual(amplitudeEvent.properties, { order_id: { type: 'string' }, items: { @@ -135,7 +135,7 @@ test.describe('analyzeTsFile', () => { assert.ok(rudderstackEvent); assert.strictEqual(rudderstackEvent.source, 'rudderstack'); assert.strictEqual(rudderstackEvent.functionName, 'checkout'); - assert.strictEqual(rudderstackEvent.line, 149); + assert.strictEqual(rudderstackEvent.line, 150); assert.deepStrictEqual(rudderstackEvent.properties, { order_id: { type: 'string' }, items: { @@ -166,7 +166,7 @@ test.describe('analyzeTsFile', () => { assert.ok(mparticleEvent); assert.strictEqual(mparticleEvent.source, 'mparticle'); assert.strictEqual(mparticleEvent.functionName, 'checkout2'); - assert.strictEqual(mparticleEvent.line, 175); + assert.strictEqual(mparticleEvent.line, 176); assert.deepStrictEqual(mparticleEvent.properties, { order_id: { type: 'string' }, items: { @@ -197,7 +197,7 @@ test.describe('analyzeTsFile', () => { assert.ok(posthogEvent); assert.strictEqual(posthogEvent.source, 'posthog'); assert.strictEqual(posthogEvent.functionName, 'checkout2'); - assert.strictEqual(posthogEvent.line, 194); + assert.strictEqual(posthogEvent.line, 195); assert.deepStrictEqual(posthogEvent.properties, { order_id: { type: 'string' }, retry: { type: 'number' }, @@ -229,7 +229,7 @@ test.describe('analyzeTsFile', () => { assert.ok(pendoEvent); assert.strictEqual(pendoEvent.source, 'pendo'); assert.strictEqual(pendoEvent.functionName, 'checkout3'); - assert.strictEqual(pendoEvent.line, 215); + assert.strictEqual(pendoEvent.line, 216); assert.deepStrictEqual(pendoEvent.properties, { order_id: { type: 'string' }, products: { @@ -260,7 +260,7 @@ test.describe('analyzeTsFile', () => { assert.ok(heapEvent); assert.strictEqual(heapEvent.source, 'heap'); assert.strictEqual(heapEvent.functionName, 'checkout3'); - assert.strictEqual(heapEvent.line, 229); + assert.strictEqual(heapEvent.line, 230); assert.deepStrictEqual(heapEvent.properties, { user_id: { type: 'string' }, email: { type: 'string' }, @@ -276,7 +276,7 @@ test.describe('analyzeTsFile', () => { assert.ok(snowplowEvent1); assert.strictEqual(snowplowEvent1.source, 'snowplow'); assert.strictEqual(snowplowEvent1.functionName, 'trackSnowplow'); - assert.strictEqual(snowplowEvent1.line, 246); + assert.strictEqual(snowplowEvent1.line, 247); assert.deepStrictEqual(snowplowEvent1.properties, { category: { type: 'string' }, label: { type: 'string' }, @@ -288,15 +288,16 @@ test.describe('analyzeTsFile', () => { assert.ok(snowplowEvent2); assert.strictEqual(snowplowEvent2.source, 'snowplow'); assert.strictEqual(snowplowEvent2.functionName, 'trackSnowplow2'); - assert.strictEqual(snowplowEvent2.line, 250); + assert.strictEqual(snowplowEvent2.line, 251); // Test custom function event const customEvent = events.find(e => e.eventName === 'custom_event_v2'); assert.ok(customEvent); assert.strictEqual(customEvent.source, 'custom'); assert.strictEqual(customEvent.functionName, 'global'); - assert.strictEqual(customEvent.line, 279); + assert.strictEqual(customEvent.line, 280); assert.deepStrictEqual(customEvent.properties, { + userId: { type: 'string' }, order_id: { type: 'string' }, value: { type: 'number' }, list: { @@ -317,8 +318,9 @@ test.describe('analyzeTsFile', () => { assert.ok(constantEvent); assert.strictEqual(constantEvent.source, 'custom'); assert.strictEqual(constantEvent.functionName, 'global'); - assert.strictEqual(constantEvent.line, 290); + assert.strictEqual(constantEvent.line, 291); assert.deepStrictEqual(constantEvent.properties, { + userId: { type: 'string' }, orderId: { type: 'string' }, total: { type: 'number' }, items: { @@ -332,7 +334,7 @@ test.describe('analyzeTsFile', () => { assert.ok(importedConstantEvent); assert.strictEqual(importedConstantEvent.source, 'segment'); assert.strictEqual(importedConstantEvent.functionName, 'global'); - assert.strictEqual(importedConstantEvent.line, 292); + assert.strictEqual(importedConstantEvent.line, 293); assert.deepStrictEqual(importedConstantEvent.properties, { orderId: { type: 'string' }, total: { type: 'number' }, @@ -366,7 +368,7 @@ test.describe('analyzeTsFile', () => { }); test('should handle nested property types correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -400,7 +402,7 @@ test.describe('analyzeTsFile', () => { }); test('should detect and expand interface types correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -428,7 +430,7 @@ test.describe('analyzeTsFile', () => { }); test('should handle shorthand property assignments correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -442,7 +444,7 @@ test.describe('analyzeTsFile', () => { }); test('should handle variable references correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -457,7 +459,7 @@ test.describe('analyzeTsFile', () => { }); test('should exclude action field from Snowplow properties', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -469,7 +471,7 @@ test.describe('analyzeTsFile', () => { }); test('should handle mParticle three-parameter format', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -481,7 +483,7 @@ test.describe('analyzeTsFile', () => { }); test('should handle readonly array types correctly', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -495,7 +497,7 @@ test.describe('analyzeTsFile', () => { }); test('should handle exported vs non-exported interfaces', () => { - const customFunction = 'customTrackFunction'; + const customFunction = 'customTrackFunction(userId, EVENT_NAME, PROPERTIES)'; const program = createProgram(testFilePath); const events = analyzeTsFile(testFilePath, program, customFunction); @@ -847,4 +849,22 @@ test.describe('analyzeTsFile', () => { documentType: { type: 'string' } }); }); + + test('should detect events for all custom function signature variations', () => { + const variants = [ + { sig: 'customTrackFunction0', event: 'custom_event0' }, + { sig: 'customTrackFunction1(EVENT_NAME, PROPERTIES)', event: 'custom_event1' }, + { sig: 'customTrackFunction2(userId, EVENT_NAME, PROPERTIES)', event: 'custom_event2' }, + { sig: 'customTrackFunction3(EVENT_NAME, PROPERTIES, userEmail)', event: 'custom_event3' }, + { sig: 'customTrackFunction4(userId, EVENT_NAME, userAddress, PROPERTIES, userEmail)', event: 'custom_event4' }, + { sig: 'CustomModule.track(userId, EVENT_NAME, PROPERTIES)', event: 'custom_module_event' }, + ]; + + variants.forEach(({ sig, event }) => { + const program = createProgram(testFilePath); + const events = analyzeTsFile(testFilePath, program, sig); + const found = events.find(e => e.eventName === event && e.source === 'custom'); + assert.ok(found, `Should detect ${event} for signature ${sig}`); + }); + }); }); diff --git a/tests/cli.test.js b/tests/cli.test.js index e9bb4bd..0c1b9e1 100644 --- a/tests/cli.test.js +++ b/tests/cli.test.js @@ -8,6 +8,8 @@ const _ = require('lodash'); const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js'); +const customFunctionSignature = 'customTrackFunction(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}"`; @@ -106,7 +108,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'go', 'tracking-schema-go.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -122,7 +124,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'javascript', 'tracking-schema-javascript.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -138,7 +140,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'typescript', 'tracking-schema-typescript.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -154,7 +156,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'python', 'tracking-schema-python.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -170,7 +172,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'ruby', 'tracking-schema-ruby.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -200,7 +202,7 @@ test.describe('CLI End-to-End Tests', () => { ); // Run CLI on the directory with only the empty file - const success = runCLI(tempLangDir, 'customTrackFunction', outputFile); + const success = runCLI(tempLangDir, customFunctionSignature, outputFile); assert.ok(success, `CLI should run successfully for ${lang} empty file`); // Check output file exists @@ -228,7 +230,7 @@ test.describe('CLI End-to-End Tests', () => { const expectedFile = path.join(fixturesDir, 'tracking-schema-all.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -241,7 +243,7 @@ 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 "customTrackFunction" --stdout`; + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -265,7 +267,7 @@ 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 "customTrackFunction" --stdout`; + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -279,7 +281,7 @@ 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 "customTrackFunction" --stdout --format json`; + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout --format json`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -313,7 +315,7 @@ 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 "customTrackFunction" --output "${outputFile}" --format json`; + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --output "${outputFile}" --format json`; let stdout; try { stdout = execSync(command, { encoding: 'utf8' }); @@ -345,7 +347,7 @@ 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 "customTrackFunction" --stdout --format xml`; + const command = `node --no-warnings=ExperimentalWarning "${CLI_PATH}" "${targetDir}" --customFunction "${customFunctionSignature}" --stdout --format xml`; let errorCaught = false; try { execSync(command, { encoding: 'utf8', stdio: 'pipe' }); diff --git a/tests/fixtures/go/main.go b/tests/fixtures/go/main.go index 90f216c..bd13eb8 100644 --- a/tests/fixtures/go/main.go +++ b/tests/fixtures/go/main.go @@ -12,8 +12,16 @@ import ( ) // Custom tracking function for your own hooks -func customTrackFunction(eventName string, params map[string]any) { - log.Printf("Custom track: %s - %+v\n", eventName, params) +func customTrackFunction(userId string, eventName string, params map[string]any) { + log.Printf("Custom track: %s - %s - %+v\n", userId, eventName, params) +} + +// Stub custom tracking variants for tests +func customTrackFunction0(eventName string, params map[string]any) {} +func customTrackFunction1(eventName string, params map[string]any) {} +func customTrackFunction2(userId string, eventName string, params map[string]any) {} +func customTrackFunction3(eventName string, params map[string]any, userEmail string) {} +func customTrackFunction4(userId string, eventName string, userAddress map[string]any, params map[string]any, userEmail string) { } func segmentTrack(userId string) { @@ -102,10 +110,17 @@ func main() { "b": 2, "c": test, } - customTrackFunction("custom_event", map[string]any{ + customTrackFunction("user888", "custom_event", map[string]any{ "foo": "bar", "baz": baz, "list": list, "obj": obj, }) + + // Calls for additional custom tracking variants + customTrackFunction0("custom_event0", map[string]any{"foo": "bar"}) + customTrackFunction1("custom_event1", map[string]any{"foo": "bar"}) + customTrackFunction2("user101", "custom_event2", map[string]any{"foo": "bar"}) + customTrackFunction3("custom_event3", map[string]any{"foo": "bar"}, "user@example.com") + customTrackFunction4("user202", "custom_event4", map[string]any{"city": "San Francisco"}, map[string]any{"foo": "bar"}, "user@example.com") } diff --git a/tests/fixtures/go/tracking-schema-go.yaml b/tests/fixtures/go/tracking-schema-go.yaml index ebf0b3c..a5e317c 100644 --- a/tests/fixtures/go/tracking-schema-go.yaml +++ b/tests/fixtures/go/tracking-schema-go.yaml @@ -1,14 +1,14 @@ # yaml-language-server: $schema=https://raw.githubusercontent.com/fliskdata/analyze-tracking/main/schema.json version: 1 source: - repository: git@github.com:fliskdata/example-repo-go.git - commit: 760a9a4bd8a4b0c48e56def39a44a0397348839a - timestamp: '2025-05-28T00:12:46Z' + repository: git@github.com:fliskdata/analyze-tracking.git + commit: f9eeddec7c52731efddc5ca5f8fbd8bf4c6a89cf + timestamp: '2025-06-25T03:57:29Z' events: Signed Up: implementations: - path: main.go - line: 21 + line: 29 function: segmentTrack destination: segment properties: @@ -21,7 +21,7 @@ events: some_event: implementations: - path: main.go - line: 33 + line: 41 function: mixpanelTrack destination: mixpanel properties: @@ -34,7 +34,7 @@ events: Button Clicked: implementations: - path: main.go - line: 44 + line: 52 function: amplitudeTrack destination: amplitude properties: @@ -51,7 +51,7 @@ events: user_signed_up: implementations: - path: main.go - line: 64 + line: 72 function: posthogTrack destination: posthog properties: @@ -66,7 +66,7 @@ events: add-to-basket: implementations: - path: main.go - line: 81 + line: 89 function: snowplowTrack destination: snowplow properties: @@ -79,10 +79,12 @@ events: custom_event: implementations: - path: main.go - line: 105 + line: 113 function: main destination: custom properties: + userId: + type: string foo: type: string baz: diff --git a/tests/fixtures/javascript/main.js b/tests/fixtures/javascript/main.js index a137b03..bd0d27f 100644 --- a/tests/fixtures/javascript/main.js +++ b/tests/fixtures/javascript/main.js @@ -149,8 +149,33 @@ const myClass = new MyClass(12345678); myClass.trackSnowplow(); // custom tracking example -customTrackFunction('customEvent', { +customTrackFunction('user999', 'customEvent', { order_id: 'order123', value: 12345678, list: ['item1', 'item2', 'item3'] }); + +// ----------------------------------------------------------------------------- +// Additional custom tracking function variants for testing +// ----------------------------------------------------------------------------- + +customTrackFunction0('custom_event0', { foo: 'bar' }); +customTrackFunction1('custom_event1', { foo: 'bar' }); +customTrackFunction2('user101', 'custom_event2', { foo: 'bar' }); +customTrackFunction3('custom_event3', { foo: 'bar' }, 'user@example.com'); +customTrackFunction4('user202', 'custom_event4', { city: 'San Francisco' }, { foo: 'bar' }, 'user@example.com'); + +// ----------------------------------------------------------------------------- +// Dot-separated custom tracking function (module-style) +// ----------------------------------------------------------------------------- + +const CustomModule = { + track(userId, eventName, params) { + console.log('CustomModule.track', userId, eventName, params); + } +}; + +CustomModule.track('user321', 'custom_module_event', { + order_id: 'order123', + foo: 'bar' +}); diff --git a/tests/fixtures/javascript/tracking-schema-javascript.yaml b/tests/fixtures/javascript/tracking-schema-javascript.yaml index 2acd43d..8b17e03 100644 --- a/tests/fixtures/javascript/tracking-schema-javascript.yaml +++ b/tests/fixtures/javascript/tracking-schema-javascript.yaml @@ -2,8 +2,8 @@ version: 1 source: repository: git@github.com:fliskdata/analyze-tracking.git - commit: 798853671e0a716d89a37d483ab94110022a4eea - timestamp: '2025-05-29T12:43:49Z' + commit: f9eeddec7c52731efddc5ca5f8fbd8bf4c6a89cf + timestamp: '2025-06-25T03:57:29Z' events: purchase: implementations: @@ -196,3 +196,5 @@ events: type: array items: type: string + userId: + type: string diff --git a/tests/fixtures/python/main.py b/tests/fixtures/python/main.py index 2de5671..56ba817 100644 --- a/tests/fixtures/python/main.py +++ b/tests/fixtures/python/main.py @@ -1,8 +1,77 @@ from typing import Any, Dict, List +# Stub imports for external analytics SDKs so linters/type checkers don't complain. +# They are not actually executed in tests. +try: + import segment # type: ignore + import mixpanel # type: ignore + import amplitude # type: ignore + import posthog # type: ignore + import rudderstack # type: ignore + import snowplow_tracker # type: ignore +except ImportError: # pragma: no cover + import sys + from types import ModuleType + + _stub_modules: List[str] = [ + 'segment', + 'segment.analytics', + 'mixpanel', + 'amplitude', + 'amplitude.Amplitude', + 'amplitude.BaseEvent', + 'posthog', + 'rudderstack', + 'rudderstack.analytics', + 'snowplow_tracker', + ] + + def _ensure_module(name: str) -> ModuleType: # type: ignore[return-value] + """Return existing or newly created stub module (supports dotted names).""" + if name in sys.modules: + return sys.modules[name] + if '.' in name: + parent_name, _, child = name.partition('.') + parent = _ensure_module(parent_name) + mod = ModuleType(name) + setattr(parent, child, mod) # type: ignore[attr-defined] + sys.modules[name] = mod + return mod + mod = ModuleType(name) + sys.modules[name] = mod + return mod + + for _mod_name in _stub_modules: + _ensure_module(_mod_name) + + # Add minimal class stubs used in tests + amplitude_mod = sys.modules['amplitude'] + if not hasattr(amplitude_mod, 'Amplitude'): + class Amplitude: # type: ignore[too-many-instance-attributes] + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + def track(self, *args: Any, **kwargs: Any) -> None: # noqa: D401 + pass + class BaseEvent: # type: ignore[too-many-instance-attributes] + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + + amplitude_mod.Amplitude = Amplitude # type: ignore[attr-defined] + amplitude_mod.BaseEvent = BaseEvent # type: ignore[attr-defined] + + posthog_mod = sys.modules['posthog'] + if not hasattr(posthog_mod, 'Posthog'): + class Posthog: # type: ignore[too-many-instance-attributes] + def __init__(self, *args: Any, **kwargs: Any) -> None: + pass + def capture(self, *args: Any, **kwargs: Any) -> None: # noqa: D401 + pass + + posthog_mod.Posthog = Posthog # type: ignore[attr-defined] + # Custom tracking function stub -def customTrackFunction(event_name: str, params: Dict[str, Any]) -> None: - print(f"Custom track: {event_name} - {params}") +def customTrackFunction(user_id: str, event_name: str, params: Dict[str, Any]) -> None: + print(f"Custom track: {user_id} - {event_name} - {params}") # Segment tracking example def segment_track(user_id: str, plan: str) -> None: @@ -82,8 +151,38 @@ def snowplow_track_events(category: str, value: float) -> None: def main() -> None: segment_track("user123", plan="Pro") mixpanel_track("user123", 9.99, ["apple", "banana"]) - amplitude_track("ButtonClicked", {"color": "red", "size": 12}) + amplitude_track("user123", 12) rudderstack_track("user123", "iOS", 14) posthog_capture("user123", "email", True, "premium") snowplow_track_events("shop", 2) - customTrackFunction("custom_event", {"key": "value", "nested": {"a": [1,2,3]}}) + customTrackFunction("user999", "custom_event", {"key": "value", "nested": {"a": [1,2,3]}}) + + # Additional custom tracking function variant calls for testing + customTrackFunction0("custom_event0", {"foo": "bar"}) + customTrackFunction1("custom_event1", {"foo": "bar"}) + customTrackFunction2("user101", "custom_event2", {"foo": "bar"}) + customTrackFunction3("custom_event3", {"foo": "bar"}, "user@example.com") + customTrackFunction4("user202", "custom_event4", {"city": "San Francisco"}, {"foo": "bar"}, "user@example.com") + + # Dot-separated custom tracking function (module-style) + class CustomModule: + @staticmethod + def track(user_id: str, event_name: str, params: Dict[str, Any]) -> None: # type: ignore[return-value] + print("CustomModule.track", user_id, event_name, params) + + CustomModule.track("user444", "custom_module_event", { + "order_id": "order_xyz", + "foo": "bar" + }) + +# Stub variant definitions to satisfy linters (not executed) + +def customTrackFunction0(event_name: str, params: Dict[str, Any]) -> None: ... + +def customTrackFunction1(event_name: str, params: Dict[str, Any]) -> None: ... + +def customTrackFunction2(user_id: str, event_name: str, params: Dict[str, Any]) -> None: ... + +def customTrackFunction3(event_name: str, params: Dict[str, Any], user_email: str) -> None: ... + +def customTrackFunction4(user_id: str, event_name: str, user_address: Dict[str, Any], params: Dict[str, Any], user_email: str) -> None: ... diff --git a/tests/fixtures/python/tracking-schema-python.yaml b/tests/fixtures/python/tracking-schema-python.yaml index c52af18..5b73ed8 100644 --- a/tests/fixtures/python/tracking-schema-python.yaml +++ b/tests/fixtures/python/tracking-schema-python.yaml @@ -2,13 +2,13 @@ version: 1 source: repository: git@github.com:fliskdata/analyze-tracking.git - commit: 271fcbec42330eda86add4be3a606e5d63d85eff - timestamp: '2025-05-28T19:02:47Z' + commit: f9eeddec7c52731efddc5ca5f8fbd8bf4c6a89cf + timestamp: '2025-06-25T03:57:29Z' events: User Signed Up: implementations: - path: main.py - line: 11 + line: 80 function: segment_track destination: segment properties: @@ -23,7 +23,7 @@ events: Purchase Completed: implementations: - path: main.py - line: 21 + line: 90 function: mixpanel_track destination: mixpanel properties: @@ -40,7 +40,7 @@ events: Button Clicked: implementations: - path: main.py - line: 31 + line: 100 function: amplitude_track destination: amplitude properties: @@ -53,7 +53,7 @@ events: User Logged In: implementations: - path: main.py - line: 47 + line: 116 function: rudderstack_track destination: rudderstack properties: @@ -68,7 +68,7 @@ events: user_signed_up: implementations: - path: main.py - line: 58 + line: 127 function: posthog_capture destination: posthog properties: @@ -81,7 +81,7 @@ events: user_cancelled_subscription: implementations: - path: main.py - line: 64 + line: 133 function: posthog_capture destination: posthog properties: @@ -94,7 +94,7 @@ events: add-to-basket: implementations: - path: main.py - line: 74 + line: 143 function: snowplow_track_events destination: snowplow properties: @@ -109,10 +109,12 @@ events: custom_event: implementations: - path: main.py - line: 89 + line: 158 function: main destination: custom properties: + userId: + type: string key: type: string nested: diff --git a/tests/fixtures/ruby/main.rb b/tests/fixtures/ruby/main.rb index f4a4163..0989fdb 100644 --- a/tests/fixtures/ruby/main.rb +++ b/tests/fixtures/ruby/main.rb @@ -76,7 +76,7 @@ def posthog_track(is_free_trial, plan) # Custom tracking function def custom_track_event - customTrackFunction('custom_event', { + customTrackFunction('user456', 'custom_event', { key: 'value', nested: { a: [1, 2, 3] } }) @@ -84,18 +84,18 @@ def custom_track_event private - def customTrackFunction(event_name, params = {}) - puts "Custom track: #{event_name} - #{params}" + def customTrackFunction(user_id, event_name, params = {}) + puts "Custom track: #{user_id} - #{event_name} - #{params}" end module CustomModule - def track(event_name, params = {}) + def track(user_id, event_name, params = {}) # Mock implementation end end def custom_track_module - CustomModule.track('custom_event', { + CustomModule.track('user789', 'custom_event', { key: 'value', nested: { a: [1, 2, 3] } }) @@ -127,3 +127,13 @@ def self.track_something ) end end + +# ----------------------------------------------------------------------------- +# Additional custom tracking function variant calls for testing +# ----------------------------------------------------------------------------- + +customTrackFunction0('custom_event0', { foo: 'bar' }) +customTrackFunction1('custom_event1', { foo: 'bar' }) +customTrackFunction2('user101', 'custom_event2', { foo: 'bar' }) +customTrackFunction3('custom_event3', { foo: 'bar' }, 'user@example.com') +customTrackFunction4('user202', 'custom_event4', { city: 'San Francisco' }, { foo: 'bar' }, 'user@example.com') diff --git a/tests/fixtures/ruby/tracking-schema-ruby.yaml b/tests/fixtures/ruby/tracking-schema-ruby.yaml index 7f427f1..ae6bb6e 100644 --- a/tests/fixtures/ruby/tracking-schema-ruby.yaml +++ b/tests/fixtures/ruby/tracking-schema-ruby.yaml @@ -2,8 +2,8 @@ version: 1 source: repository: git@github.com:fliskdata/analyze-tracking.git - commit: 0c20fe5e71e403c5226b7d8685b6263a89a6b8eb - timestamp: '2025-05-29T01:28:30Z' + commit: f9eeddec7c52731efddc5ca5f8fbd8bf4c6a89cf + timestamp: '2025-06-25T03:57:29Z' events: User Signed Up: implementations: @@ -68,6 +68,8 @@ events: function: custom_track_event destination: custom properties: + userId: + type: string key: type: string nested: diff --git a/tests/fixtures/tracking-schema-all.yaml b/tests/fixtures/tracking-schema-all.yaml index a9fd27f..bf46b5d 100644 --- a/tests/fixtures/tracking-schema-all.yaml +++ b/tests/fixtures/tracking-schema-all.yaml @@ -2,13 +2,13 @@ version: 1 source: repository: git@github.com:fliskdata/analyze-tracking.git - commit: 9ae0c52c8416a1e8ef447ac443bb877c9e42a052 - timestamp: '2025-05-29T20:41:34Z' + commit: f9eeddec7c52731efddc5ca5f8fbd8bf4c6a89cf + timestamp: '2025-06-25T03:57:29Z' events: Signed Up: implementations: - path: go/main.go - line: 21 + line: 29 function: segmentTrack destination: segment properties: @@ -21,7 +21,7 @@ events: some_event: implementations: - path: go/main.go - line: 33 + line: 41 function: mixpanelTrack destination: mixpanel properties: @@ -34,11 +34,11 @@ events: Button Clicked: implementations: - path: go/main.go - line: 44 + line: 52 function: amplitudeTrack destination: amplitude - path: python/main.py - line: 31 + line: 100 function: amplitude_track destination: amplitude properties: @@ -61,11 +61,11 @@ events: user_signed_up: implementations: - path: go/main.go - line: 64 + line: 72 function: posthogTrack destination: posthog - path: python/main.py - line: 58 + line: 127 function: posthog_capture destination: posthog - path: ruby/main.rb @@ -88,11 +88,11 @@ events: add-to-basket: implementations: - path: go/main.go - line: 81 + line: 89 function: snowplowTrack destination: snowplow - path: python/main.py - line: 74 + line: 143 function: snowplow_track_events destination: snowplow - path: ruby/main.rb @@ -117,11 +117,11 @@ events: custom_event: implementations: - path: go/main.go - line: 105 + line: 113 function: main destination: custom - path: python/main.py - line: 89 + line: 158 function: main destination: custom - path: ruby/main.rb @@ -129,6 +129,8 @@ events: function: custom_track_event destination: custom properties: + userId: + type: string foo: type: string baz: @@ -346,10 +348,12 @@ events: type: array items: type: string + userId: + type: string User Signed Up: implementations: - path: python/main.py - line: 11 + line: 80 function: segment_track destination: segment - path: ruby/main.rb @@ -368,7 +372,7 @@ events: Purchase Completed: implementations: - path: python/main.py - line: 21 + line: 90 function: mixpanel_track destination: mixpanel - path: ruby/main.rb @@ -389,7 +393,7 @@ events: User Logged In: implementations: - path: python/main.py - line: 47 + line: 116 function: rudderstack_track destination: rudderstack properties: @@ -404,7 +408,7 @@ events: user_cancelled_subscription: implementations: - path: python/main.py - line: 64 + line: 133 function: posthog_capture destination: posthog properties: @@ -441,7 +445,7 @@ events: order_completed: implementations: - path: typescript/main.ts - line: 104 + line: 105 function: trackOrderCompletedGA destination: googleanalytics properties: @@ -476,7 +480,7 @@ events: user_checkout: implementations: - path: typescript/main.ts - line: 120 + line: 121 function: checkout destination: segment properties: @@ -489,7 +493,7 @@ events: purchase_confirmed: implementations: - path: typescript/main.ts - line: 129 + line: 130 function: confirmPurchaseMixpanel destination: mixpanel properties: @@ -513,7 +517,7 @@ events: checkout_initiated: implementations: - path: typescript/main.ts - line: 134 + line: 135 function: checkout destination: amplitude properties: @@ -548,7 +552,7 @@ events: order_finalized: implementations: - path: typescript/main.ts - line: 149 + line: 150 function: checkout destination: rudderstack properties: @@ -581,7 +585,7 @@ events: BuyNow: implementations: - path: typescript/main.ts - line: 175 + line: 176 function: checkout2 destination: mparticle properties: @@ -614,7 +618,7 @@ events: user_action: implementations: - path: typescript/main.ts - line: 194 + line: 195 function: checkout2 destination: posthog properties: @@ -649,7 +653,7 @@ events: customer_checkout: implementations: - path: typescript/main.ts - line: 215 + line: 216 function: checkout3 destination: pendo properties: @@ -682,7 +686,7 @@ events: user_login: implementations: - path: typescript/main.ts - line: 229 + line: 230 function: checkout3 destination: heap properties: @@ -699,7 +703,7 @@ events: item_view: implementations: - path: typescript/main.ts - line: 246 + line: 247 function: trackSnowplow destination: snowplow properties: @@ -714,7 +718,7 @@ events: button_click: implementations: - path: typescript/main.ts - line: 250 + line: 251 function: trackSnowplow2 destination: snowplow properties: @@ -729,7 +733,7 @@ events: custom_event_v2: implementations: - path: typescript/main.ts - line: 279 + line: 280 function: global destination: custom properties: @@ -748,10 +752,12 @@ events: type: string retry: type: boolean + userId: + type: string ecommerce_purchase: implementations: - path: typescript/main.ts - line: 290 + line: 291 function: global destination: custom properties: @@ -763,10 +769,12 @@ events: type: array items: type: string + userId: + type: string ecommerce_purchase_v2: implementations: - path: typescript/main.ts - line: 292 + line: 293 function: global destination: segment properties: @@ -810,7 +818,7 @@ events: product_id: type: string product_name: - type: string + type: string price: type: number item_added: diff --git a/tests/fixtures/typescript/main.ts b/tests/fixtures/typescript/main.ts index e870580..ad94c46 100644 --- a/tests/fixtures/typescript/main.ts +++ b/tests/fixtures/typescript/main.ts @@ -61,6 +61,7 @@ declare function buildStructEvent(payload: { }): unknown; declare function customTrackFunction( + userId: string, eventName: string, params: Record ): void; @@ -276,7 +277,7 @@ const customParams: CustomParams = { list: ['itemA', 'itemB'], metadata: { source: 'unit_test', retry: false }, }; -customTrackFunction('custom_event_v2', customParams); +customTrackFunction('user888', 'custom_event_v2', customParams); // ----------------------------------------------------------------------------- // Event name is a const/pointer, not a string literal @@ -287,6 +288,38 @@ const purchaseEvent = { total: 99.99, items: ['sku_1', 'sku_2'] }; -customTrackFunction(TRACKING_EVENTS.ECOMMERCE_PURCHASE, purchaseEvent); +customTrackFunction('user555', TRACKING_EVENTS.ECOMMERCE_PURCHASE, purchaseEvent); analytics.track(ECOMMERCE_PURCHASE_V2, {...purchaseEvent}); + +// ----------------------------------------------------------------------------- +// Additional custom tracking function variants for testing +// ----------------------------------------------------------------------------- + +declare function customTrackFunction0(EVENT_NAME: string, PROPERTIES: Record): void; +declare function customTrackFunction1(EVENT_NAME: string, PROPERTIES: Record): void; +declare function customTrackFunction2(userId: string, EVENT_NAME: string, PROPERTIES: Record): void; +declare function customTrackFunction3(EVENT_NAME: string, PROPERTIES: Record, userEmail: string): void; +declare function customTrackFunction4(userId: string, EVENT_NAME: string, userAddress: Record, PROPERTIES: Record, userEmail: string): void; + +// Calls for each variant so tests can verify detection +customTrackFunction0('custom_event0', { foo: 'bar' }); +customTrackFunction1('custom_event1', { foo: 'bar' }); +customTrackFunction2('user101', 'custom_event2', { foo: 'bar' }); +customTrackFunction3('custom_event3', { foo: 'bar' }, 'user@example.com'); +customTrackFunction4('user202', 'custom_event4', { city: 'San Francisco' }, { foo: 'bar' }, 'user@example.com'); + +// ----------------------------------------------------------------------------- +// Dot-separated custom tracking function (module-style) +// ----------------------------------------------------------------------------- + +namespace CustomModule { + export function track(userId: string, eventName: string, params: Record): void { + console.log('CustomModule.track', userId, eventName, params); + } +} + +CustomModule.track('user333', 'custom_module_event', { + order_id: 'order_xyz', + foo: 'bar' +}); diff --git a/tests/fixtures/typescript/tracking-schema-typescript.yaml b/tests/fixtures/typescript/tracking-schema-typescript.yaml index 442d641..b3a00dd 100644 --- a/tests/fixtures/typescript/tracking-schema-typescript.yaml +++ b/tests/fixtures/typescript/tracking-schema-typescript.yaml @@ -2,13 +2,13 @@ version: 1 source: repository: git@github.com:fliskdata/analyze-tracking.git - commit: d3774b76a3d1528c76c4bdb500dc32ee508e6381 - timestamp: '2025-05-29T15:55:59Z' + commit: f9eeddec7c52731efddc5ca5f8fbd8bf4c6a89cf + timestamp: '2025-06-25T03:57:29Z' events: order_completed: implementations: - path: main.ts - line: 104 + line: 105 function: trackOrderCompletedGA destination: googleanalytics properties: @@ -43,7 +43,7 @@ events: user_checkout: implementations: - path: main.ts - line: 120 + line: 121 function: checkout destination: segment properties: @@ -56,7 +56,7 @@ events: purchase_confirmed: implementations: - path: main.ts - line: 129 + line: 130 function: confirmPurchaseMixpanel destination: mixpanel properties: @@ -80,7 +80,7 @@ events: checkout_initiated: implementations: - path: main.ts - line: 134 + line: 135 function: checkout destination: amplitude properties: @@ -115,7 +115,7 @@ events: order_finalized: implementations: - path: main.ts - line: 149 + line: 150 function: checkout destination: rudderstack properties: @@ -148,7 +148,7 @@ events: BuyNow: implementations: - path: main.ts - line: 175 + line: 176 function: checkout2 destination: mparticle properties: @@ -181,7 +181,7 @@ events: user_action: implementations: - path: main.ts - line: 194 + line: 195 function: checkout2 destination: posthog properties: @@ -216,7 +216,7 @@ events: customer_checkout: implementations: - path: main.ts - line: 215 + line: 216 function: checkout3 destination: pendo properties: @@ -249,7 +249,7 @@ events: user_login: implementations: - path: main.ts - line: 229 + line: 230 function: checkout3 destination: heap properties: @@ -266,7 +266,7 @@ events: item_view: implementations: - path: main.ts - line: 246 + line: 247 function: trackSnowplow destination: snowplow properties: @@ -281,7 +281,7 @@ events: button_click: implementations: - path: main.ts - line: 250 + line: 251 function: trackSnowplow2 destination: snowplow properties: @@ -296,7 +296,7 @@ events: custom_event_v2: implementations: - path: main.ts - line: 279 + line: 280 function: global destination: custom properties: @@ -315,10 +315,12 @@ events: type: string retry: type: boolean + userId: + type: string ecommerce_purchase: implementations: - path: main.ts - line: 290 + line: 291 function: global destination: custom properties: @@ -330,10 +332,12 @@ events: type: array items: type: string + userId: + type: string ecommerce_purchase_v2: implementations: - path: main.ts - line: 292 + line: 293 function: global destination: segment properties: diff --git a/tests/schema.test.js b/tests/schema.test.js index b4f93dc..678c426 100644 --- a/tests/schema.test.js +++ b/tests/schema.test.js @@ -9,6 +9,8 @@ const Ajv = require('ajv'); const CLI_PATH = path.join(__dirname, '..', 'bin', 'cli.js'); const SCHEMA_PATH = path.join(__dirname, '..', 'schema.json'); +const customFunctionSignature = 'customTrackFunction(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}"`; @@ -70,7 +72,7 @@ test.describe('Schema Validation Tests', () => { const outputFile = path.join(tempDir, 'tracking-schema-validation-test.yaml'); // Run CLI on entire fixtures directory - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Check output file exists @@ -101,7 +103,7 @@ test.describe('Schema Validation Tests', () => { const outputFile = path.join(tempDir, `tracking-schema-${lang}-validation-test.yaml`); // Run CLI for each language directory - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, `CLI should run successfully for ${lang}`); // Validate the generated YAML against the schema @@ -124,7 +126,7 @@ test.describe('Schema Validation Tests', () => { const outputFile = path.join(tempDir, 'tracking-schema-required-fields-test.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Read and parse the YAML @@ -166,7 +168,7 @@ test.describe('Schema Validation Tests', () => { const outputFile = path.join(tempDir, 'tracking-schema-enum-test.yaml'); // Run CLI - const success = runCLI(targetDir, 'customTrackFunction', outputFile); + const success = runCLI(targetDir, customFunctionSignature, outputFile); assert.ok(success, 'CLI should run successfully'); // Read and parse the YAML