@flisk/analyze-tracking 0.7.5 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,7 @@ let parse = null;
16
16
  * @returns {Promise<Array>} Array of tracking events found in the file
17
17
  * @throws {Error} If the file cannot be read or parsed
18
18
  */
19
- async function analyzeRubyFile(filePath, customFunction) {
19
+ async function analyzeRubyFile(filePath, customFunctionSignatures = null) {
20
20
  // Lazy load the Ruby Prism parser
21
21
  if (!parse) {
22
22
  const { loadPrism } = await import('@ruby/prism');
@@ -26,21 +26,28 @@ async function analyzeRubyFile(filePath, customFunction) {
26
26
  try {
27
27
  // Read the file content
28
28
  const code = fs.readFileSync(filePath, 'utf8');
29
-
30
- // Parse the Ruby code into an AST
29
+
30
+ // Parse the Ruby code into an AST once
31
31
  let ast;
32
32
  try {
33
33
  ast = await parse(code);
34
34
  } catch (parseError) {
35
35
  console.error(`Error parsing file ${filePath}:`, parseError.message);
36
- return []; // Return empty events array if parsing fails
36
+ return [];
37
37
  }
38
38
 
39
- // Create a visitor and analyze the AST
40
- const visitor = new TrackingVisitor(code, filePath, customFunction);
39
+ // Single visitor pass covering all custom configs
40
+ const visitor = new TrackingVisitor(code, filePath, customFunctionSignatures || []);
41
41
  const events = await visitor.analyze(ast);
42
42
 
43
- return events;
43
+ // Deduplicate events
44
+ const unique = new Map();
45
+ for (const evt of events) {
46
+ const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
47
+ if (!unique.has(key)) unique.set(key, evt);
48
+ }
49
+
50
+ return Array.from(unique.values());
44
51
 
45
52
  } catch (fileError) {
46
53
  console.error(`Error reading or processing file ${filePath}:`, fileError.message);
@@ -8,10 +8,10 @@ const { extractEventName, extractProperties } = require('./extractors');
8
8
  const { findWrappingFunction, traverseNode, getLineNumber } = require('./traversal');
9
9
 
10
10
  class TrackingVisitor {
11
- constructor(code, filePath, customFunction = null) {
11
+ constructor(code, filePath, customConfigs = []) {
12
12
  this.code = code;
13
13
  this.filePath = filePath;
14
- this.customFunction = customFunction;
14
+ this.customConfigs = Array.isArray(customConfigs) ? customConfigs : [];
15
15
  this.events = [];
16
16
  }
17
17
 
@@ -22,10 +22,27 @@ class TrackingVisitor {
22
22
  */
23
23
  async processCallNode(node, ancestors) {
24
24
  try {
25
- const source = detectSource(node, this.customFunction);
25
+ let matchedConfig = null;
26
+ let source = null;
27
+
28
+ // Try to match any custom config first
29
+ for (const cfg of this.customConfigs) {
30
+ if (!cfg) continue;
31
+ if (detectSource(node, cfg.functionName) === 'custom') {
32
+ matchedConfig = cfg;
33
+ source = 'custom';
34
+ break;
35
+ }
36
+ }
37
+
38
+ // If no custom match, attempt built-in providers
39
+ if (!source) {
40
+ source = detectSource(node, null);
41
+ }
42
+
26
43
  if (!source) return;
27
44
 
28
- const eventName = extractEventName(node, source);
45
+ const eventName = extractEventName(node, source, matchedConfig);
29
46
  if (!eventName) return;
30
47
 
31
48
  const line = getLineNumber(this.code, node.location);
@@ -33,13 +50,13 @@ class TrackingVisitor {
33
50
  // For module-scoped custom functions, use the custom function name as the functionName
34
51
  // For simple custom functions, use the wrapping function name
35
52
  let functionName;
36
- if (source === 'custom' && this.customFunction && this.customFunction.includes('.')) {
37
- functionName = this.customFunction;
53
+ if (source === 'custom' && matchedConfig && matchedConfig.functionName.includes('.')) {
54
+ functionName = matchedConfig.functionName;
38
55
  } else {
39
56
  functionName = await findWrappingFunction(node, ancestors);
40
57
  }
41
58
 
42
- const properties = await extractProperties(node, source);
59
+ const properties = await extractProperties(node, source, matchedConfig);
43
60
 
44
61
  this.events.push({
45
62
  eventName,
@@ -48,7 +48,10 @@ function isCustomFunction(node, customFunction) {
48
48
  ts.isPropertyAccessExpression(node.expression) ||
49
49
  ts.isCallExpression(node.expression) || // For chained calls like getTracker().track()
50
50
  ts.isElementAccessExpression(node.expression) || // For array/object access like trackers['analytics'].track()
51
- (ts.isPropertyAccessExpression(node.expression?.expression) && ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
51
+ (node.expression?.expression &&
52
+ ts.isPropertyAccessExpression(node.expression.expression) &&
53
+ node.expression.expression.expression &&
54
+ ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
52
55
 
53
56
  return canBeCustomFunction && node.expression.getText() === customFunction;
54
57
  }
@@ -21,6 +21,7 @@ const EXTRACTION_STRATEGIES = {
21
21
  googleanalytics: extractGoogleAnalyticsEvent,
22
22
  snowplow: extractSnowplowEvent,
23
23
  mparticle: extractMparticleEvent,
24
+ custom: extractCustomEvent,
24
25
  default: extractDefaultEvent
25
26
  };
26
27
 
@@ -30,10 +31,14 @@ const EXTRACTION_STRATEGIES = {
30
31
  * @param {string} source - Analytics provider source
31
32
  * @param {Object} checker - TypeScript type checker
32
33
  * @param {Object} sourceFile - TypeScript source file
34
+ * @param {Object} customConfig - Custom configuration for custom extraction
33
35
  * @returns {EventData} Extracted event data
34
36
  */
35
- function extractEventData(node, source, checker, sourceFile) {
37
+ function extractEventData(node, source, checker, sourceFile, customConfig) {
36
38
  const strategy = EXTRACTION_STRATEGIES[source] || EXTRACTION_STRATEGIES.default;
39
+ if (source === 'custom') {
40
+ return strategy(node, checker, sourceFile, customConfig);
41
+ }
37
42
  return strategy(node, checker, sourceFile);
38
43
  }
39
44
 
@@ -50,7 +55,7 @@ function extractGoogleAnalyticsEvent(node, checker, sourceFile) {
50
55
  }
51
56
 
52
57
  // gtag('event', 'event_name', { properties })
53
- const eventName = getStringValue(node.arguments[1]);
58
+ const eventName = getStringValue(node.arguments[1], checker, sourceFile);
54
59
  const propertiesNode = node.arguments[2];
55
60
 
56
61
  return { eventName, propertiesNode };
@@ -79,7 +84,7 @@ function extractSnowplowEvent(node, checker, sourceFile) {
79
84
  const structEventArg = firstArg.arguments[0];
80
85
  if (ts.isObjectLiteralExpression(structEventArg)) {
81
86
  const actionProperty = findPropertyByKey(structEventArg, 'action');
82
- const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
87
+ const eventName = actionProperty ? getStringValue(actionProperty.initializer, checker, sourceFile) : null;
83
88
  return { eventName, propertiesNode: structEventArg };
84
89
  }
85
90
  }
@@ -93,7 +98,7 @@ function extractSnowplowEvent(node, checker, sourceFile) {
93
98
  const structEventArg = resolvedNode.arguments[0];
94
99
  if (ts.isObjectLiteralExpression(structEventArg)) {
95
100
  const actionProperty = findPropertyByKey(structEventArg, 'action');
96
- const eventName = actionProperty ? getStringValue(actionProperty.initializer) : null;
101
+ const eventName = actionProperty ? getStringValue(actionProperty.initializer, checker, sourceFile) : null;
97
102
  return { eventName, propertiesNode: structEventArg };
98
103
  }
99
104
  }
@@ -115,12 +120,38 @@ function extractMparticleEvent(node, checker, sourceFile) {
115
120
  }
116
121
 
117
122
  // mParticle.logEvent('event_name', mParticle.EventType.Navigation, { properties })
118
- const eventName = getStringValue(node.arguments[0]);
123
+ const eventName = getStringValue(node.arguments[0], checker, sourceFile);
119
124
  const propertiesNode = node.arguments[2];
120
125
 
121
126
  return { eventName, propertiesNode };
122
127
  }
123
128
 
129
+ /**
130
+ * Custom extraction
131
+ * @param {Object} node - CallExpression node
132
+ * @param {Object} checker - TypeScript type checker
133
+ * @param {Object} sourceFile - TypeScript source file
134
+ * @param {Object} customConfig - Custom configuration for custom extraction
135
+ * @returns {EventData}
136
+ */
137
+ function extractCustomEvent(node, checker, sourceFile, customConfig) {
138
+ const args = node.arguments || [];
139
+
140
+ const eventArg = args[customConfig?.eventIndex ?? 0];
141
+ const propertiesArg = args[customConfig?.propertiesIndex ?? 1];
142
+
143
+ const eventName = getStringValue(eventArg, checker, sourceFile);
144
+
145
+ const extraArgs = {};
146
+ if (customConfig && customConfig.extraParams) {
147
+ customConfig.extraParams.forEach(extra => {
148
+ extraArgs[extra.name] = args[extra.idx];
149
+ });
150
+ }
151
+
152
+ return { eventName, propertiesNode: propertiesArg, extraArgs };
153
+ }
154
+
124
155
  /**
125
156
  * Default event extraction for standard providers
126
157
  * @param {Object} node - CallExpression node
@@ -134,7 +165,7 @@ function extractDefaultEvent(node, checker, sourceFile) {
134
165
  }
135
166
 
136
167
  // provider.track('event_name', { properties })
137
- const eventName = getStringValue(node.arguments[0]);
168
+ const eventName = getStringValue(node.arguments[0], checker, sourceFile);
138
169
  const propertiesNode = node.arguments[1];
139
170
 
140
171
  return { eventName, propertiesNode };
@@ -149,9 +180,10 @@ function extractDefaultEvent(node, checker, sourceFile) {
149
180
  * @param {string} functionName - Containing function name
150
181
  * @param {Object} checker - TypeScript type checker
151
182
  * @param {Object} sourceFile - TypeScript source file
183
+ * @param {Object} customConfig - Custom configuration for custom extraction
152
184
  * @returns {Object|null} Processed event object or null
153
185
  */
154
- function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile) {
186
+ function processEventData(eventData, source, filePath, line, functionName, checker, sourceFile, customConfig) {
155
187
  const { eventName, propertiesNode } = eventData;
156
188
 
157
189
  if (!eventName || !propertiesNode) {
@@ -184,6 +216,37 @@ function processEventData(eventData, source, filePath, line, functionName, check
184
216
  // Clean up any unresolved type markers
185
217
  const cleanedProperties = cleanupProperties(properties);
186
218
 
219
+ // Handle custom extra params
220
+ if (source === 'custom' && customConfig && eventData.extraArgs) {
221
+ for (const [paramName, argNode] of Object.entries(eventData.extraArgs)) {
222
+ if (argNode && ts.isObjectLiteralExpression(argNode)) {
223
+ // Extract detailed properties from object literal expression
224
+ cleanedProperties[paramName] = {
225
+ type: 'object',
226
+ properties: extractProperties(checker, argNode)
227
+ };
228
+ } else if (argNode && ts.isIdentifier(argNode)) {
229
+ // Handle identifier references to objects
230
+ const resolvedNode = resolveIdentifierToInitializer(checker, argNode, sourceFile);
231
+ if (resolvedNode && ts.isObjectLiteralExpression(resolvedNode)) {
232
+ cleanedProperties[paramName] = {
233
+ type: 'object',
234
+ properties: extractProperties(checker, resolvedNode)
235
+ };
236
+ } else {
237
+ cleanedProperties[paramName] = {
238
+ type: inferNodeValueType(argNode)
239
+ };
240
+ }
241
+ } else {
242
+ // For non-object arguments, use simple type inference
243
+ cleanedProperties[paramName] = {
244
+ type: inferNodeValueType(argNode)
245
+ };
246
+ }
247
+ }
248
+ }
249
+
187
250
  return {
188
251
  eventName,
189
252
  source,
@@ -197,16 +260,121 @@ function processEventData(eventData, source, filePath, line, functionName, check
197
260
  /**
198
261
  * Gets string value from a TypeScript AST node
199
262
  * @param {Object} node - TypeScript AST node
263
+ * @param {Object} checker - TypeScript type checker
264
+ * @param {Object} sourceFile - TypeScript source file
200
265
  * @returns {string|null} String value or null
201
266
  */
202
- function getStringValue(node) {
267
+ function getStringValue(node, checker, sourceFile) {
203
268
  if (!node) return null;
269
+
270
+ // Handle string literals (existing behavior)
204
271
  if (ts.isStringLiteral(node)) {
205
272
  return node.text;
206
273
  }
274
+
275
+ // Handle property access expressions like TRACKING_EVENTS.ECOMMERCE_PURCHASE
276
+ if (ts.isPropertyAccessExpression(node)) {
277
+ return resolvePropertyAccessToString(node, checker, sourceFile);
278
+ }
279
+
280
+ // Handle identifiers that might reference constants
281
+ if (ts.isIdentifier(node)) {
282
+ return resolveIdentifierToString(node, checker, sourceFile);
283
+ }
284
+
207
285
  return null;
208
286
  }
209
287
 
288
+ /**
289
+ * Resolves a property access expression to its string value
290
+ * @param {Object} node - PropertyAccessExpression node
291
+ * @param {Object} checker - TypeScript type checker
292
+ * @param {Object} sourceFile - TypeScript source file
293
+ * @returns {string|null} String value or null
294
+ */
295
+ function resolvePropertyAccessToString(node, checker, sourceFile) {
296
+ try {
297
+ // Get the symbol for the property access
298
+ const symbol = checker.getSymbolAtLocation(node);
299
+ if (!symbol || !symbol.valueDeclaration) {
300
+ return null;
301
+ }
302
+
303
+ // Check if it's a property assignment with a string initializer
304
+ if (ts.isPropertyAssignment(symbol.valueDeclaration) &&
305
+ symbol.valueDeclaration.initializer &&
306
+ ts.isStringLiteral(symbol.valueDeclaration.initializer)) {
307
+ return symbol.valueDeclaration.initializer.text;
308
+ }
309
+
310
+ // Check if it's a variable declaration property
311
+ if (ts.isPropertySignature(symbol.valueDeclaration) ||
312
+ ts.isMethodSignature(symbol.valueDeclaration)) {
313
+ // Try to get the type and see if it's a string literal type
314
+ const type = checker.getTypeAtLocation(node);
315
+ if (type.isStringLiteral && type.isStringLiteral()) {
316
+ return type.value;
317
+ }
318
+ }
319
+
320
+ return null;
321
+ } catch (error) {
322
+ return null;
323
+ }
324
+ }
325
+
326
+ /**
327
+ * Resolves an identifier to its string value
328
+ * @param {Object} node - Identifier node
329
+ * @param {Object} checker - TypeScript type checker
330
+ * @param {Object} sourceFile - TypeScript source file
331
+ * @returns {string|null} String value or null
332
+ */
333
+ function resolveIdentifierToString(node, checker, sourceFile) {
334
+ try {
335
+ const symbol = checker.getSymbolAtLocation(node);
336
+ if (!symbol) {
337
+ return null;
338
+ }
339
+
340
+ // First try to resolve through value declaration
341
+ if (symbol.valueDeclaration) {
342
+ const declaration = symbol.valueDeclaration;
343
+
344
+ // Handle variable declarations with string literal initializers
345
+ if (ts.isVariableDeclaration(declaration) &&
346
+ declaration.initializer &&
347
+ ts.isStringLiteral(declaration.initializer)) {
348
+ return declaration.initializer.text;
349
+ }
350
+
351
+ // Handle const declarations with object literals containing string properties
352
+ if (ts.isVariableDeclaration(declaration) &&
353
+ declaration.initializer &&
354
+ ts.isObjectLiteralExpression(declaration.initializer)) {
355
+ // This case is handled by property access resolution
356
+ return null;
357
+ }
358
+ }
359
+
360
+ // If value declaration doesn't exist or doesn't help, try type resolution
361
+ // This handles imported constants that are resolved through TypeScript's type system
362
+ const type = checker.getTypeOfSymbolAtLocation(symbol, node);
363
+ if (type && type.isStringLiteral && typeof type.isStringLiteral === 'function' && type.isStringLiteral()) {
364
+ return type.value;
365
+ }
366
+
367
+ // Alternative approach for string literal types (different TypeScript versions)
368
+ if (type && type.flags && (type.flags & ts.TypeFlags.StringLiteral)) {
369
+ return type.value;
370
+ }
371
+
372
+ return null;
373
+ } catch (error) {
374
+ return null;
375
+ }
376
+ }
377
+
210
378
  /**
211
379
  * Finds a property by key in an ObjectLiteralExpression
212
380
  * @param {Object} objectNode - ObjectLiteralExpression node
@@ -263,6 +431,16 @@ function cleanupProperties(properties) {
263
431
  return cleaned;
264
432
  }
265
433
 
434
+ function inferNodeValueType(node) {
435
+ if (!node) return 'any';
436
+ if (ts.isStringLiteral(node)) return 'string';
437
+ if (ts.isNumericLiteral(node)) return 'number';
438
+ if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
439
+ if (ts.isArrayLiteralExpression(node)) return 'array';
440
+ if (ts.isObjectLiteralExpression(node)) return 'object';
441
+ return 'any';
442
+ }
443
+
266
444
  module.exports = {
267
445
  extractEventData,
268
446
  processEventData
@@ -34,6 +34,13 @@ function extractProperties(checker, node) {
34
34
  const properties = {};
35
35
 
36
36
  for (const prop of node.properties) {
37
+ // Handle spread assignments like {...object}
38
+ if (ts.isSpreadAssignment(prop)) {
39
+ const spreadProperties = extractSpreadProperties(checker, prop);
40
+ Object.assign(properties, spreadProperties);
41
+ continue;
42
+ }
43
+
37
44
  const key = getPropertyKey(prop);
38
45
  if (!key) continue;
39
46
 
@@ -350,7 +357,7 @@ function resolveTypeSchema(checker, typeString) {
350
357
  * @returns {string|null} Literal type or null
351
358
  */
352
359
  function getLiteralType(node) {
353
- if (!node) return null;
360
+ if (!node || typeof node.kind === 'undefined') return null;
354
361
  if (ts.isStringLiteral(node)) return 'string';
355
362
  if (ts.isNumericLiteral(node)) return 'number';
356
363
  if (node.kind === ts.SyntaxKind.TrueKeyword || node.kind === ts.SyntaxKind.FalseKeyword) return 'boolean';
@@ -371,6 +378,51 @@ function isArrayType(typeString) {
371
378
  typeString.startsWith('readonly ');
372
379
  }
373
380
 
381
+ /**
382
+ * Extracts properties from a spread assignment
383
+ * @param {Object} checker - TypeScript type checker
384
+ * @param {Object} spreadNode - SpreadAssignment node
385
+ * @returns {Object.<string, PropertySchema>}
386
+ */
387
+ function extractSpreadProperties(checker, spreadNode) {
388
+ if (!spreadNode.expression) {
389
+ return {};
390
+ }
391
+
392
+ // If the spread is an identifier, resolve it to its declaration
393
+ if (ts.isIdentifier(spreadNode.expression)) {
394
+ const symbol = checker.getSymbolAtLocation(spreadNode.expression);
395
+ if (symbol && symbol.declarations && symbol.declarations.length > 0) {
396
+ const declaration = symbol.declarations[0];
397
+
398
+ // If it's a variable declaration with an object literal initializer
399
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
400
+ if (ts.isObjectLiteralExpression(declaration.initializer)) {
401
+ // Extract properties directly from the object literal
402
+ return extractProperties(checker, declaration.initializer);
403
+ }
404
+ }
405
+ }
406
+
407
+ // Fallback to the original identifier schema extraction
408
+ const identifierSchema = extractIdentifierSchema(checker, spreadNode.expression);
409
+ return identifierSchema.properties || {};
410
+ }
411
+
412
+ // If the spread is an object literal, extract its properties
413
+ if (ts.isObjectLiteralExpression(spreadNode.expression)) {
414
+ return extractProperties(checker, spreadNode.expression);
415
+ }
416
+
417
+ // For other expressions, try to get the type and extract properties from it
418
+ try {
419
+ const spreadType = checker.getTypeAtLocation(spreadNode.expression);
420
+ return extractInterfaceProperties(checker, spreadType);
421
+ } catch (error) {
422
+ return {};
423
+ }
424
+ }
425
+
374
426
  /**
375
427
  * Extracts properties from a TypeScript interface or type
376
428
  * @param {Object} checker - TypeScript type checker
@@ -9,16 +9,14 @@ const { getProgram, findTrackingEvents, ProgramError, SourceFileError } = requir
9
9
  * Analyzes a TypeScript file for analytics tracking calls
10
10
  * @param {string} filePath - Path to the TypeScript file to analyze
11
11
  * @param {Object} [program] - Optional existing TypeScript program to reuse
12
- * @param {string} [customFunction] - Optional custom function name to detect
12
+ * @param {string} [customFunctionSignature] - Optional custom function signature to detect
13
13
  * @returns {Array<Object>} Array of tracking events found in the file
14
14
  */
15
- function analyzeTsFile(filePath, program, customFunction) {
16
- const events = [];
17
-
15
+ function analyzeTsFile(filePath, program = null, customFunctionSignatures = null) {
18
16
  try {
19
- // Get or create TypeScript program
17
+ // Get or create TypeScript program (only once)
20
18
  const tsProgram = getProgram(filePath, program);
21
-
19
+
22
20
  // Get source file from program
23
21
  const sourceFile = tsProgram.getSourceFile(filePath);
24
22
  if (!sourceFile) {
@@ -28,9 +26,17 @@ function analyzeTsFile(filePath, program, customFunction) {
28
26
  // Get type checker
29
27
  const checker = tsProgram.getTypeChecker();
30
28
 
31
- // Find and extract tracking events
32
- const foundEvents = findTrackingEvents(sourceFile, checker, filePath, customFunction);
33
- events.push(...foundEvents);
29
+ // Single-pass collection covering built-in + all custom configs
30
+ const events = findTrackingEvents(sourceFile, checker, filePath, customFunctionSignatures || []);
31
+
32
+ // Deduplicate events
33
+ const unique = new Map();
34
+ for (const evt of events) {
35
+ const key = `${evt.source}|${evt.eventName}|${evt.line}|${evt.functionName}`;
36
+ if (!unique.has(key)) unique.set(key, evt);
37
+ }
38
+
39
+ return Array.from(unique.values());
34
40
 
35
41
  } catch (error) {
36
42
  if (error instanceof ProgramError) {
@@ -42,7 +48,7 @@ function analyzeTsFile(filePath, program, customFunction) {
42
48
  }
43
49
  }
44
50
 
45
- return events;
51
+ return [];
46
52
  }
47
53
 
48
54
  module.exports = { analyzeTsFile };
@@ -65,32 +65,55 @@ function getProgram(filePath, existingProgram) {
65
65
  * @param {Object} sourceFile - TypeScript source file
66
66
  * @param {Object} checker - TypeScript type checker
67
67
  * @param {string} filePath - Path to the file being analyzed
68
- * @param {string} [customFunction] - Custom function name to detect
68
+ * @param {Array<Object>} [customConfigs] - Array of custom function configurations
69
69
  * @returns {Array<Object>} Array of found events
70
70
  */
71
- function findTrackingEvents(sourceFile, checker, filePath, customFunction) {
71
+ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
72
72
  const events = [];
73
73
 
74
74
  /**
75
- * Visitor function for AST traversal
76
- * @param {Object} node - Current AST node
75
+ * Helper to test if a CallExpression matches a custom function name.
76
+ * We simply rely on node.expression.getText() which preserves the fully qualified name.
77
77
  */
78
+ const matchesCustomFn = (callNode, fnName) => {
79
+ if (!fnName) return false;
80
+ try {
81
+ return callNode.expression && callNode.expression.getText() === fnName;
82
+ } catch {
83
+ return false;
84
+ }
85
+ };
86
+
78
87
  function visit(node) {
79
88
  try {
80
89
  if (ts.isCallExpression(node)) {
81
- const event = extractTrackingEvent(node, sourceFile, checker, filePath, customFunction);
82
- if (event) {
83
- events.push(event);
90
+ let matchedCustom = null;
91
+
92
+ if (Array.isArray(customConfigs) && customConfigs.length > 0) {
93
+ for (const cfg of customConfigs) {
94
+ if (cfg && matchesCustomFn(node, cfg.functionName)) {
95
+ matchedCustom = cfg;
96
+ break;
97
+ }
98
+ }
84
99
  }
100
+
101
+ const event = extractTrackingEvent(
102
+ node,
103
+ sourceFile,
104
+ checker,
105
+ filePath,
106
+ matchedCustom /* may be null */
107
+ );
108
+ if (event) events.push(event);
85
109
  }
86
- // Continue traversing the AST
110
+
87
111
  ts.forEachChild(node, visit);
88
112
  } catch (error) {
89
113
  console.error(`Error processing node in ${filePath}:`, error.message);
90
114
  }
91
115
  }
92
116
 
93
- // Start traversal from the root
94
117
  ts.forEachChild(sourceFile, visit);
95
118
 
96
119
  return events;
@@ -102,25 +125,25 @@ function findTrackingEvents(sourceFile, checker, filePath, customFunction) {
102
125
  * @param {Object} sourceFile - TypeScript source file
103
126
  * @param {Object} checker - TypeScript type checker
104
127
  * @param {string} filePath - File path
105
- * @param {string} [customFunction] - Custom function name
128
+ * @param {Object} [customConfig] - Custom function configuration
106
129
  * @returns {Object|null} Extracted event or null
107
130
  */
108
- function extractTrackingEvent(node, sourceFile, checker, filePath, customFunction) {
131
+ function extractTrackingEvent(node, sourceFile, checker, filePath, customConfig) {
109
132
  // Detect the analytics source
110
- const source = detectAnalyticsSource(node, customFunction);
133
+ const source = detectAnalyticsSource(node, customConfig?.functionName);
111
134
  if (source === 'unknown') {
112
135
  return null;
113
136
  }
114
137
 
115
138
  // Extract event data based on the source
116
- const eventData = extractEventData(node, source, checker, sourceFile);
139
+ const eventData = extractEventData(node, source, checker, sourceFile, customConfig);
117
140
 
118
141
  // Get location and context information
119
142
  const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
120
143
  const functionName = findWrappingFunction(node);
121
144
 
122
145
  // Process the event data into final format
123
- return processEventData(eventData, source, filePath, line, functionName, checker, sourceFile);
146
+ return processEventData(eventData, source, filePath, line, functionName, checker, sourceFile, customConfig);
124
147
  }
125
148
 
126
149
  module.exports = {
@@ -110,6 +110,16 @@ function findParentFunctionName(node) {
110
110
  }
111
111
  }
112
112
 
113
+ // Property declaration in class: myFunc = () => {}
114
+ if (ts.isPropertyDeclaration(parent) && parent.name) {
115
+ if (ts.isIdentifier(parent.name)) {
116
+ return parent.name.escapedText;
117
+ }
118
+ if (ts.isStringLiteral(parent.name)) {
119
+ return parent.name.text;
120
+ }
121
+ }
122
+
113
123
  // Method property in object literal: { myFunc() {} }
114
124
  if (ts.isMethodDeclaration(parent) && parent.name) {
115
125
  return parent.name.escapedText;
@@ -117,6 +127,7 @@ function findParentFunctionName(node) {
117
127
 
118
128
  // Binary expression assignment: obj.myFunc = () => {}
119
129
  if (ts.isBinaryExpression(parent) &&
130
+ parent.operatorToken &&
120
131
  parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
121
132
  if (ts.isPropertyAccessExpression(parent.left)) {
122
133
  return parent.left.name.escapedText;
@@ -144,7 +144,7 @@ function isCustomType(typeString) {
144
144
  * @returns {string} Basic type string
145
145
  */
146
146
  function getBasicTypeOfArrayElement(checker, element) {
147
- if (!element) return 'any';
147
+ if (!element || typeof element.kind === 'undefined') return 'any';
148
148
 
149
149
  // Check for literal values first
150
150
  if (ts.isStringLiteral(element)) {