@flisk/analyze-tracking 0.7.0 → 0.7.2

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.
@@ -13,16 +13,17 @@ function detectSourceJs(node, customFunction) {
13
13
 
14
14
  if (objectName === 'analytics' && methodName === 'track') return 'segment';
15
15
  if (objectName === 'mixpanel' && methodName === 'track') return 'mixpanel';
16
- if (objectName === 'amplitude' && methodName === 'logEvent') return 'amplitude';
16
+ if (objectName === 'amplitude' && methodName === 'track') return 'amplitude';
17
17
  if (objectName === 'rudderanalytics' && methodName === 'track') return 'rudderstack';
18
- if (objectName === 'mParticle' && methodName === 'logEvent') return 'mparticle';
18
+ if ((objectName === 'mParticle' || objectName === 'mparticle') && methodName === 'logEvent') return 'mparticle';
19
19
  if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
20
20
  if (objectName === 'pendo' && methodName === 'track') return 'pendo';
21
21
  if (objectName === 'heap' && methodName === 'track') return 'heap';
22
- }
23
-
24
- if (node.callee.type === 'Identifier' && node.callee.name === 'snowplow') {
25
- return 'snowplow';
22
+
23
+ // Check for Snowplow pattern: tracker.track(...)
24
+ if (objectName === 'tracker' && methodName === 'track') {
25
+ return 'snowplow';
26
+ }
26
27
  }
27
28
 
28
29
  if (node.callee.type === 'Identifier' && node.callee.name === customFunction) {
@@ -45,18 +46,19 @@ function detectSourceTs(node, customFunction) {
45
46
 
46
47
  if (objectName === 'analytics' && methodName === 'track') return 'segment';
47
48
  if (objectName === 'mixpanel' && methodName === 'track') return 'mixpanel';
48
- if (objectName === 'amplitude' && methodName === 'logEvent') return 'amplitude';
49
+ if (objectName === 'amplitude' && methodName === 'track') return 'amplitude';
49
50
  if (objectName === 'rudderanalytics' && methodName === 'track') return 'rudderstack';
50
- if (objectName === 'mParticle' && methodName === 'logEvent') return 'mparticle';
51
+ if ((objectName === 'mParticle' || objectName === 'mparticle') && methodName === 'logEvent') return 'mparticle';
51
52
  if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
52
53
  if (objectName === 'pendo' && methodName === 'track') return 'pendo';
53
54
  if (objectName === 'heap' && methodName === 'track') return 'heap';
55
+
56
+ // Check for Snowplow pattern: tracker.track(...)
57
+ if (objectName === 'tracker' && methodName === 'track') {
58
+ return 'snowplow';
59
+ }
54
60
  }
55
61
 
56
- if (ts.isIdentifier(node.expression) && node.expression.escapedText === 'snowplow') {
57
- return 'snowplow';
58
- }
59
-
60
62
  if (ts.isIdentifier(node.expression) && node.expression.escapedText === customFunction) {
61
63
  return 'custom';
62
64
  }
@@ -94,6 +96,11 @@ function findWrappingFunctionJs(node, ancestors) {
94
96
  return current.id ? current.id.name : 'anonymous';
95
97
  }
96
98
 
99
+ // Handle class methods
100
+ if (current.type === 'MethodDefinition') {
101
+ return current.key.name || 'anonymous';
102
+ }
103
+
97
104
  // Handle exported variable/function (e.g., export const myFunc = () => {})
98
105
  if (current.type === 'ExportNamedDeclaration' && current.declaration) {
99
106
  const declaration = current.declaration.declarations ? current.declaration.declarations[0] : null;
@@ -116,13 +123,47 @@ function extractJsProperties(node) {
116
123
  node.properties.forEach((prop) => {
117
124
  const key = prop.key?.name || prop.key?.value;
118
125
  if (key) {
119
- let valueType = typeof prop.value.value;
120
126
  if (prop.value.type === 'ObjectExpression') {
121
127
  properties[key] = {
122
128
  type: 'object',
123
129
  properties: extractJsProperties(prop.value),
124
130
  };
131
+ } else if (prop.value.type === 'ArrayExpression') {
132
+ // Handle arrays - analyze elements to determine item type
133
+ let itemType = 'any';
134
+ if (prop.value.elements && prop.value.elements.length > 0) {
135
+ // Check the types of all elements
136
+ const elementTypes = new Set();
137
+ prop.value.elements.forEach(element => {
138
+ if (element) {
139
+ if (element.type === 'Literal') {
140
+ elementTypes.add(typeof element.value);
141
+ } else if (element.type === 'ObjectExpression') {
142
+ elementTypes.add('object');
143
+ } else if (element.type === 'ArrayExpression') {
144
+ elementTypes.add('array');
145
+ } else {
146
+ elementTypes.add('any');
147
+ }
148
+ }
149
+ });
150
+
151
+ // If all elements are the same type, use that type
152
+ if (elementTypes.size === 1) {
153
+ itemType = Array.from(elementTypes)[0];
154
+ } else {
155
+ itemType = 'any';
156
+ }
157
+ }
158
+
159
+ properties[key] = {
160
+ type: 'array',
161
+ items: {
162
+ type: itemType
163
+ }
164
+ };
125
165
  } else {
166
+ let valueType = typeof prop.value.value;
126
167
  if (valueType === 'undefined') {
127
168
  valueType = 'any';
128
169
  } else if (valueType === 'object') {
@@ -147,8 +188,74 @@ function extractTsProperties(checker, node) {
147
188
  if (ts.isShorthandPropertyAssignment(prop)) {
148
189
  const symbol = checker.getSymbolAtLocation(prop.name);
149
190
  if (symbol) {
150
- valueType = getTypeOfNode(checker, symbol.valueDeclaration);
151
- properties[key] = { type: valueType };
191
+ // Get the type of the shorthand property
192
+ const propType = checker.getTypeAtLocation(prop.name);
193
+ const typeString = checker.typeToString(propType);
194
+
195
+ // Check if it's an array type
196
+ if (typeString.includes('[]') || typeString.startsWith('Array<')) {
197
+ // Handle array types
198
+ let elementType = null;
199
+
200
+ // Try to get type arguments for generic types
201
+ if (propType.target && propType.typeArguments && propType.typeArguments.length > 0) {
202
+ elementType = propType.typeArguments[0];
203
+ }
204
+ // Try indexed access for array types
205
+ else {
206
+ try {
207
+ const numberType = checker.getNumberType();
208
+ elementType = checker.getIndexedAccessType(propType, numberType);
209
+ } catch (e) {
210
+ // Indexed access failed
211
+ }
212
+ }
213
+
214
+ if (elementType) {
215
+ const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
216
+ if (Object.keys(elementInterfaceProps).length > 0) {
217
+ properties[key] = {
218
+ type: 'array',
219
+ items: {
220
+ type: 'object',
221
+ properties: elementInterfaceProps
222
+ }
223
+ };
224
+ } else {
225
+ properties[key] = {
226
+ type: 'array',
227
+ items: {
228
+ type: 'object'
229
+ }
230
+ };
231
+ }
232
+ } else {
233
+ properties[key] = {
234
+ type: 'array',
235
+ items: {
236
+ type: 'any'
237
+ }
238
+ };
239
+ }
240
+ } else {
241
+ // Not an array, handle as before
242
+ const resolvedType = resolveTypeToProperties(checker, typeString);
243
+ if (resolvedType.__unresolved) {
244
+ // Try to get the actual type and extract properties
245
+ const interfaceProps = extractInterfaceProperties(checker, propType);
246
+ if (Object.keys(interfaceProps).length > 0) {
247
+ properties[key] = {
248
+ type: 'object',
249
+ properties: interfaceProps
250
+ };
251
+ } else {
252
+ properties[key] = resolvedType;
253
+ delete properties[key].__unresolved;
254
+ }
255
+ } else {
256
+ properties[key] = resolvedType;
257
+ }
258
+ }
152
259
  }
153
260
  } else if (prop.initializer) {
154
261
  if (ts.isObjectLiteralExpression(prop.initializer)) {
@@ -157,12 +264,102 @@ function extractTsProperties(checker, node) {
157
264
  properties: extractTsProperties(checker, prop.initializer),
158
265
  };
159
266
  } else if (ts.isArrayLiteralExpression(prop.initializer)) {
160
- properties[key] = {
161
- type: 'array',
162
- items: {
163
- type: getTypeOfNode(checker, prop.initializer.elements[0]) || 'any',
164
- },
165
- };
267
+ // For array literals, we need to check the elements
268
+ const elementTypes = new Set();
269
+
270
+ if (prop.initializer.elements.length === 0) {
271
+ // Empty array
272
+ properties[key] = {
273
+ type: 'array',
274
+ items: {
275
+ type: 'any'
276
+ }
277
+ };
278
+ } else {
279
+ // Check types of all elements
280
+ for (const element of prop.initializer.elements) {
281
+ const elemType = getBasicTypeOfArrayElement(checker, element);
282
+ elementTypes.add(elemType);
283
+ }
284
+
285
+ // If all elements are the same type, use that type; otherwise use 'any'
286
+ const itemType = elementTypes.size === 1 ? Array.from(elementTypes)[0] : 'any';
287
+
288
+ properties[key] = {
289
+ type: 'array',
290
+ items: {
291
+ type: itemType
292
+ }
293
+ };
294
+ }
295
+ } else if (ts.isIdentifier(prop.initializer)) {
296
+ // Handle identifiers (variable references)
297
+ const identifierType = checker.getTypeAtLocation(prop.initializer);
298
+ const typeString = checker.typeToString(identifierType);
299
+
300
+ // Check if it's an array type
301
+ if (typeString.includes('[]') || typeString.startsWith('Array<')) {
302
+ // Extract element type and check if it's a custom interface
303
+ let elementType = null;
304
+
305
+ // Try to get type arguments for generic types
306
+ if (identifierType.target && identifierType.typeArguments && identifierType.typeArguments.length > 0) {
307
+ elementType = identifierType.typeArguments[0];
308
+ }
309
+ // Try indexed access for array types
310
+ else {
311
+ try {
312
+ const numberType = checker.getNumberType();
313
+ elementType = checker.getIndexedAccessType(identifierType, numberType);
314
+ } catch (e) {
315
+ // Indexed access failed
316
+ }
317
+ }
318
+
319
+ if (elementType) {
320
+ const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
321
+ if (Object.keys(elementInterfaceProps).length > 0) {
322
+ properties[key] = {
323
+ type: 'array',
324
+ items: {
325
+ type: 'object',
326
+ properties: elementInterfaceProps
327
+ }
328
+ };
329
+ } else {
330
+ properties[key] = {
331
+ type: 'array',
332
+ items: {
333
+ type: 'object'
334
+ }
335
+ };
336
+ }
337
+ } else {
338
+ properties[key] = {
339
+ type: 'array',
340
+ items: {
341
+ type: 'any'
342
+ }
343
+ };
344
+ }
345
+ } else {
346
+ // Not an array, resolve normally
347
+ const resolvedType = resolveTypeToProperties(checker, typeString);
348
+ if (resolvedType.__unresolved) {
349
+ const interfaceProps = extractInterfaceProperties(checker, identifierType);
350
+ if (Object.keys(interfaceProps).length > 0) {
351
+ properties[key] = {
352
+ type: 'object',
353
+ properties: interfaceProps
354
+ };
355
+ } else {
356
+ properties[key] = resolvedType;
357
+ delete properties[key].__unresolved;
358
+ }
359
+ } else {
360
+ properties[key] = resolvedType;
361
+ }
362
+ }
166
363
  } else {
167
364
  // Handle hard-coded values
168
365
  switch (prop.initializer.kind) {
@@ -190,11 +387,70 @@ function extractTsProperties(checker, node) {
190
387
  valueType = getTypeOfNode(checker, prop.initializer) || 'any';
191
388
  }
192
389
 
193
- properties[key] = { type: valueType };
390
+ // Check if this is a custom type that should be expanded
391
+ const resolvedType = resolveTypeToProperties(checker, valueType);
392
+ if (resolvedType.__unresolved) {
393
+ // Try to get the actual type and extract properties
394
+ const propType = checker.getTypeAtLocation(prop.initializer);
395
+ const interfaceProps = extractInterfaceProperties(checker, propType);
396
+ if (Object.keys(interfaceProps).length > 0) {
397
+ properties[key] = {
398
+ type: 'object',
399
+ properties: interfaceProps
400
+ };
401
+ } else {
402
+ properties[key] = resolvedType;
403
+ delete properties[key].__unresolved;
404
+ }
405
+ } else {
406
+ properties[key] = resolvedType;
407
+ }
194
408
  }
195
409
  } else if (prop.type) {
196
410
  valueType = checker.typeToString(checker.getTypeFromTypeNode(prop.type)) || 'any';
197
- properties[key] = { type: valueType };
411
+
412
+ // Check if this is a custom type that should be expanded
413
+ const resolvedType = resolveTypeToProperties(checker, valueType);
414
+
415
+ // Special handling for arrays of custom types
416
+ if (resolvedType.type === 'array') {
417
+ const propType = checker.getTypeFromTypeNode(prop.type);
418
+
419
+ // Try multiple approaches to get array element type
420
+ let elementType = null;
421
+
422
+ // First try: Check if it's a generic type reference (Array<T> or ReadonlyArray<T>)
423
+ if (propType.target && propType.typeArguments && propType.typeArguments.length > 0) {
424
+ elementType = propType.typeArguments[0];
425
+ }
426
+ // Second try: For T[] syntax, use indexed access
427
+ else {
428
+ try {
429
+ const numberType = checker.getNumberType();
430
+ elementType = checker.getIndexedAccessType(propType, numberType);
431
+ } catch (e) {
432
+ // Indexed access failed
433
+ }
434
+ }
435
+
436
+ if (elementType) {
437
+ const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
438
+ if (Object.keys(elementInterfaceProps).length > 0) {
439
+ resolvedType.items = {
440
+ type: 'object',
441
+ properties: elementInterfaceProps
442
+ };
443
+ } else {
444
+ // If no properties found but it looks like a custom type, mark as object
445
+ const elementTypeString = checker.typeToString(elementType);
446
+ if (elementTypeString[0] === elementTypeString[0].toUpperCase() && !elementTypeString.includes('<')) {
447
+ resolvedType.items = { type: 'object' };
448
+ }
449
+ }
450
+ }
451
+ }
452
+
453
+ properties[key] = resolvedType;
198
454
  }
199
455
  }
200
456
 
@@ -206,6 +462,185 @@ function getTypeOfNode(checker, node) {
206
462
  return checker.typeToString(type);
207
463
  }
208
464
 
465
+ function getBasicTypeOfArrayElement(checker, element) {
466
+ if (!element) return 'any';
467
+
468
+ // Check for literal values first
469
+ if (ts.isStringLiteral(element)) {
470
+ return 'string';
471
+ } else if (ts.isNumericLiteral(element)) {
472
+ return 'number';
473
+ } else if (element.kind === ts.SyntaxKind.TrueKeyword || element.kind === ts.SyntaxKind.FalseKeyword) {
474
+ return 'boolean';
475
+ } else if (ts.isObjectLiteralExpression(element)) {
476
+ return 'object';
477
+ } else if (ts.isArrayLiteralExpression(element)) {
478
+ return 'array';
479
+ } else if (element.kind === ts.SyntaxKind.NullKeyword) {
480
+ return 'null';
481
+ } else if (element.kind === ts.SyntaxKind.UndefinedKeyword) {
482
+ return 'undefined';
483
+ }
484
+
485
+ // For identifiers and other expressions, try to get the type
486
+ const typeString = getTypeOfNode(checker, element);
487
+
488
+ // Extract basic type from TypeScript type string
489
+ if (typeString.startsWith('"') || typeString.startsWith("'")) {
490
+ return 'string'; // String literal type
491
+ } else if (!isNaN(Number(typeString))) {
492
+ return 'number'; // Numeric literal type
493
+ } else if (typeString === 'true' || typeString === 'false') {
494
+ return 'boolean'; // Boolean literal type
495
+ } else if (typeString.includes('[]') || typeString.startsWith('Array<')) {
496
+ return 'array';
497
+ } else if (typeString === 'string' || typeString === 'number' || typeString === 'boolean' ||
498
+ typeString === 'object' || typeString === 'null' || typeString === 'undefined') {
499
+ return typeString;
500
+ } else if (typeString[0] === typeString[0].toUpperCase() && !typeString.includes('<')) {
501
+ // This looks like a custom type/interface, return 'object'
502
+ return 'object';
503
+ }
504
+
505
+ return 'any';
506
+ }
507
+
508
+ function resolveIdentifierToInitializer(checker, identifier, sourceFile) {
509
+ try {
510
+ const symbol = checker.getSymbolAtLocation(identifier);
511
+ if (!symbol || !symbol.valueDeclaration) return null;
512
+
513
+ const declaration = symbol.valueDeclaration;
514
+
515
+ // Handle variable declarations
516
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
517
+ return declaration.initializer;
518
+ }
519
+
520
+ // Handle property assignments
521
+ if (ts.isPropertyAssignment(declaration) && declaration.initializer) {
522
+ return declaration.initializer;
523
+ }
524
+
525
+ // Handle parameter with default value
526
+ if (ts.isParameter(declaration) && declaration.initializer) {
527
+ return declaration.initializer;
528
+ }
529
+
530
+ return null;
531
+ } catch (error) {
532
+ return null;
533
+ }
534
+ }
535
+
536
+ function resolveTypeToProperties(checker, typeString, visitedTypes = new Set()) {
537
+ // Prevent infinite recursion for circular references
538
+ if (visitedTypes.has(typeString)) {
539
+ return { type: typeString };
540
+ }
541
+
542
+ // Handle primitive types
543
+ if (['string', 'number', 'boolean', 'any', 'unknown', 'null', 'undefined'].includes(typeString)) {
544
+ return { type: typeString };
545
+ }
546
+
547
+ // Handle array types
548
+ const arrayMatch = typeString.match(/^(.+)\[\]$/) || typeString.match(/^Array<(.+)>$/);
549
+ if (arrayMatch) {
550
+ const elementType = arrayMatch[1].trim();
551
+ visitedTypes.add(typeString);
552
+ const elementProps = resolveTypeToProperties(checker, elementType, visitedTypes);
553
+ return {
554
+ type: 'array',
555
+ items: elementProps
556
+ };
557
+ }
558
+
559
+ // Handle readonly array types
560
+ const readonlyArrayMatch = typeString.match(/^readonly (.+)\[\]$/) || typeString.match(/^ReadonlyArray<(.+)>$/);
561
+ if (readonlyArrayMatch) {
562
+ const elementType = readonlyArrayMatch[1].trim();
563
+ visitedTypes.add(typeString);
564
+ const elementProps = resolveTypeToProperties(checker, elementType, visitedTypes);
565
+
566
+ // If element type is a custom interface/type, we need to find its properties
567
+ if (elementProps.__unresolved && checker) {
568
+ // Try to find the type by name and extract its properties
569
+ // This would require access to the program's type checker context
570
+ // For now, mark it as object type but try to get properties later
571
+ return {
572
+ type: 'array',
573
+ items: {
574
+ type: 'object',
575
+ __needsResolution: elementType
576
+ }
577
+ };
578
+ }
579
+
580
+ return {
581
+ type: 'array',
582
+ items: elementProps
583
+ };
584
+ }
585
+
586
+ // Try to find the type symbol and extract properties
587
+ try {
588
+ // This is a simplified approach - in a real implementation, we'd need access to the actual type node
589
+ // For now, we'll return the type as-is, but mark it as an object if it looks like a custom type
590
+ if (typeString[0] === typeString[0].toUpperCase() && !typeString.includes('<') && !typeString.includes('|') && !typeString.includes('&')) {
591
+ // Looks like a custom type/interface
592
+ return {
593
+ type: 'object',
594
+ __unresolved: typeString // Mark for later resolution
595
+ };
596
+ }
597
+ } catch (error) {
598
+ // Fall through
599
+ }
600
+
601
+ return { type: typeString };
602
+ }
603
+
604
+ function extractInterfaceProperties(checker, type) {
605
+ const properties = {};
606
+ const typeSymbol = type.getSymbol();
607
+
608
+ if (!typeSymbol) return properties;
609
+
610
+ // Get all properties of the type
611
+ const members = checker.getPropertiesOfType(type);
612
+
613
+ for (const member of members) {
614
+ const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
615
+ const memberTypeString = checker.typeToString(memberType);
616
+ const isOptional = member.flags & ts.SymbolFlags.Optional;
617
+
618
+ // Recursively resolve the member type
619
+ const resolvedType = resolveTypeToProperties(checker, memberTypeString);
620
+
621
+ // If it's an unresolved object type, try to extract its properties
622
+ if (resolvedType.__unresolved) {
623
+ const nestedProperties = extractInterfaceProperties(checker, memberType);
624
+ if (Object.keys(nestedProperties).length > 0) {
625
+ properties[member.name] = {
626
+ type: 'object',
627
+ properties: nestedProperties
628
+ };
629
+ } else {
630
+ properties[member.name] = resolvedType;
631
+ }
632
+ } else {
633
+ properties[member.name] = resolvedType;
634
+ // Clean up any unresolved markers
635
+ if (properties[member.name].__unresolved) {
636
+ delete properties[member.name].__unresolved;
637
+ }
638
+ }
639
+ }
640
+
641
+ return properties;
642
+ }
643
+
209
644
  module.exports = {
210
645
  detectSourceJs,
211
646
  detectSourceTs,
@@ -214,4 +649,8 @@ module.exports = {
214
649
  extractJsProperties,
215
650
  extractTsProperties,
216
651
  getTypeOfNode,
652
+ getBasicTypeOfArrayElement,
653
+ resolveIdentifierToInitializer,
654
+ resolveTypeToProperties,
655
+ extractInterfaceProperties,
217
656
  };
@@ -37,7 +37,6 @@ async function analyzeDirectory(dirPath, customFunction) {
37
37
  } else if (isGoFile) {
38
38
  events = await analyzeGoFile(file, customFunction);
39
39
  } else {
40
- console.info(`Skipping file ${file} because it is not a supported file type`);
41
40
  continue;
42
41
  }
43
42