@flisk/analyze-tracking 0.7.1 → 0.7.3

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.
Files changed (71) hide show
  1. package/README.md +35 -61
  2. package/bin/cli.js +1 -1
  3. package/package.json +18 -3
  4. package/src/analyze/go/astTraversal.js +121 -0
  5. package/src/analyze/go/constants.js +20 -0
  6. package/src/analyze/go/eventDeduplicator.js +47 -0
  7. package/src/analyze/go/eventExtractor.js +156 -0
  8. package/src/analyze/go/goAstParser/constants.js +39 -0
  9. package/src/analyze/go/goAstParser/expressionParser.js +281 -0
  10. package/src/analyze/go/goAstParser/index.js +52 -0
  11. package/src/analyze/go/goAstParser/statementParser.js +387 -0
  12. package/src/analyze/go/goAstParser/tokenizer.js +196 -0
  13. package/src/analyze/go/goAstParser/typeParser.js +202 -0
  14. package/src/analyze/go/goAstParser/utils.js +99 -0
  15. package/src/analyze/go/index.js +55 -0
  16. package/src/analyze/go/propertyExtractor.js +670 -0
  17. package/src/analyze/go/trackingDetector.js +71 -0
  18. package/src/analyze/go/trackingExtractor.js +54 -0
  19. package/src/analyze/go/typeContext.js +88 -0
  20. package/src/analyze/go/utils.js +215 -0
  21. package/src/analyze/index.js +11 -7
  22. package/src/analyze/javascript/constants.js +115 -0
  23. package/src/analyze/javascript/detectors/analytics-source.js +119 -0
  24. package/src/analyze/javascript/detectors/index.js +10 -0
  25. package/src/analyze/javascript/extractors/event-extractor.js +179 -0
  26. package/src/analyze/javascript/extractors/index.js +13 -0
  27. package/src/analyze/javascript/extractors/property-extractor.js +172 -0
  28. package/src/analyze/javascript/index.js +38 -0
  29. package/src/analyze/javascript/parser.js +126 -0
  30. package/src/analyze/javascript/utils/function-finder.js +123 -0
  31. package/src/analyze/python/index.js +111 -0
  32. package/src/analyze/python/pythonTrackingAnalyzer.py +814 -0
  33. package/src/analyze/ruby/detectors.js +46 -0
  34. package/src/analyze/ruby/extractors.js +258 -0
  35. package/src/analyze/ruby/index.js +51 -0
  36. package/src/analyze/ruby/traversal.js +123 -0
  37. package/src/analyze/ruby/types.js +30 -0
  38. package/src/analyze/ruby/visitor.js +66 -0
  39. package/src/analyze/typescript/constants.js +109 -0
  40. package/src/analyze/typescript/detectors/analytics-source.js +120 -0
  41. package/src/analyze/typescript/detectors/index.js +10 -0
  42. package/src/analyze/typescript/extractors/event-extractor.js +269 -0
  43. package/src/analyze/typescript/extractors/index.js +14 -0
  44. package/src/analyze/typescript/extractors/property-extractor.js +395 -0
  45. package/src/analyze/typescript/index.js +48 -0
  46. package/src/analyze/typescript/parser.js +131 -0
  47. package/src/analyze/typescript/utils/function-finder.js +114 -0
  48. package/src/analyze/typescript/utils/type-resolver.js +193 -0
  49. package/src/generateDescriptions/index.js +81 -0
  50. package/src/generateDescriptions/llmUtils.js +33 -0
  51. package/src/generateDescriptions/promptUtils.js +62 -0
  52. package/src/generateDescriptions/schemaUtils.js +61 -0
  53. package/src/index.js +7 -2
  54. package/src/{fileProcessor.js → utils/fileProcessor.js} +5 -0
  55. package/src/{repoDetails.js → utils/repoDetails.js} +5 -0
  56. package/src/{yamlGenerator.js → utils/yamlGenerator.js} +5 -0
  57. package/.github/workflows/npm-publish.yml +0 -33
  58. package/.github/workflows/pr-check.yml +0 -17
  59. package/jest.config.js +0 -7
  60. package/src/analyze/analyzeGoFile.js +0 -1164
  61. package/src/analyze/analyzeJsFile.js +0 -72
  62. package/src/analyze/analyzePythonFile.js +0 -41
  63. package/src/analyze/analyzeRubyFile.js +0 -409
  64. package/src/analyze/analyzeTsFile.js +0 -69
  65. package/src/analyze/go2json.js +0 -1069
  66. package/src/analyze/helpers.js +0 -217
  67. package/src/analyze/pythonTrackingAnalyzer.py +0 -439
  68. package/src/generateDescriptions.js +0 -196
  69. package/tests/detectSource.test.js +0 -20
  70. package/tests/extractProperties.test.js +0 -109
  71. package/tests/findWrappingFunction.test.js +0 -30
@@ -0,0 +1,71 @@
1
+ /**
2
+ * @fileoverview Analytics tracking detection for Go function calls
3
+ * @module analyze/go/trackingDetector
4
+ */
5
+
6
+ const { ANALYTICS_SOURCES } = require('./constants');
7
+
8
+ /**
9
+ * Detect the analytics source from a call or struct literal node
10
+ * @param {Object} callNode - AST node representing a function call or struct literal
11
+ * @param {string|null} customFunction - Name of custom tracking function to detect
12
+ * @returns {string|null} Analytics source name (e.g., 'segment', 'amplitude') or null if not recognized
13
+ */
14
+ function detectSource(callNode, customFunction) {
15
+ // Check for struct literals (Segment/Rudderstack/PostHog/Amplitude)
16
+ if (callNode.tag === 'structlit') {
17
+ if (callNode.struct) {
18
+ if (callNode.struct.tag === 'access') {
19
+ const structType = callNode.struct.member;
20
+ const namespace = callNode.struct.struct?.value;
21
+
22
+ // Check for specific struct types with their namespaces
23
+ if (structType === 'Track' && namespace === 'analytics') return ANALYTICS_SOURCES.SEGMENT;
24
+ if (structType === 'Capture' && namespace === 'posthog') return ANALYTICS_SOURCES.POSTHOG;
25
+ if (structType === 'Event' && namespace === 'amplitude') return ANALYTICS_SOURCES.AMPLITUDE;
26
+
27
+ // Fallback for struct types without namespace check (backward compatibility)
28
+ if (structType === 'Track') return ANALYTICS_SOURCES.SEGMENT;
29
+ if (structType === 'Capture') return ANALYTICS_SOURCES.POSTHOG;
30
+ }
31
+ }
32
+ return null;
33
+ }
34
+
35
+ // For function calls, check if func property exists
36
+ if (!callNode.func) return null;
37
+
38
+ // Check for method calls (e.g., client.Track, mp.Track)
39
+ if (callNode.func.tag === 'access') {
40
+ const objName = callNode.func.struct?.tag === 'ident' ? callNode.func.struct.value : null;
41
+ const methodName = callNode.func.member;
42
+
43
+ if (!objName || !methodName) return null;
44
+
45
+ // Check various analytics providers
46
+ switch (true) {
47
+ // Mixpanel: mp.Track(ctx, []*mixpanel.Event{...})
48
+ case objName === 'mp' && methodName === 'Track':
49
+ return ANALYTICS_SOURCES.MIXPANEL;
50
+
51
+ // Amplitude: client.Track(amplitude.Event{...})
52
+ case objName === 'client' && methodName === 'Track':
53
+ return ANALYTICS_SOURCES.AMPLITUDE;
54
+
55
+ // Snowplow: tracker.TrackStructEvent(...)
56
+ case objName === 'tracker' && methodName === 'TrackStructEvent':
57
+ return ANALYTICS_SOURCES.SNOWPLOW;
58
+ }
59
+ }
60
+
61
+ // Check for custom function calls
62
+ if (customFunction && callNode.func.tag === 'ident' && callNode.func.value === customFunction) {
63
+ return ANALYTICS_SOURCES.CUSTOM;
64
+ }
65
+
66
+ return null;
67
+ }
68
+
69
+ module.exports = {
70
+ detectSource
71
+ };
@@ -0,0 +1,54 @@
1
+ /**
2
+ * @fileoverview Tracking event extraction from Go function calls
3
+ * @module analyze/go/trackingExtractor
4
+ */
5
+
6
+ const { ANALYTICS_SOURCES } = require('./constants');
7
+ const { detectSource } = require('./trackingDetector');
8
+ const { extractEventName } = require('./eventExtractor');
9
+ const { extractProperties } = require('./propertyExtractor');
10
+
11
+ /**
12
+ * Extract tracking event information from a node
13
+ * @param {Object} callNode - AST node representing a function call or struct literal
14
+ * @param {string} filePath - Path to the file being analyzed
15
+ * @param {string} functionName - Name of the function containing this tracking call
16
+ * @param {string|null} customFunction - Name of custom tracking function to detect
17
+ * @param {Object} typeContext - Type information context for variable resolution
18
+ * @param {string} currentFunction - Current function context for type lookups
19
+ * @returns {Object|null} Tracking event object with eventName, source, properties, etc., or null if not a tracking call
20
+ */
21
+ function extractTrackingEvent(callNode, filePath, functionName, customFunction, typeContext, currentFunction) {
22
+ const source = detectSource(callNode, customFunction);
23
+ if (!source) return null;
24
+
25
+ const eventName = extractEventName(callNode, source);
26
+ if (!eventName) return null;
27
+
28
+ const properties = extractProperties(callNode, source, typeContext, currentFunction);
29
+
30
+ // Get line number based on source type
31
+ let line = 0;
32
+ if (source === ANALYTICS_SOURCES.SEGMENT || source === ANALYTICS_SOURCES.POSTHOG) {
33
+ // For Segment and PostHog, we need to get the line number from the struct.struct object
34
+ if (callNode.tag === 'structlit' && callNode.struct && callNode.struct.struct) {
35
+ line = callNode.struct.struct.line || 0;
36
+ }
37
+ } else {
38
+ // For other sources, use the line number from the AST node
39
+ line = callNode.line || 0;
40
+ }
41
+
42
+ return {
43
+ eventName,
44
+ source,
45
+ properties,
46
+ filePath,
47
+ line,
48
+ functionName
49
+ };
50
+ }
51
+
52
+ module.exports = {
53
+ extractTrackingEvent
54
+ };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * @fileoverview Type context builder for Go AST analysis
3
+ * @module analyze/go/typeContext
4
+ */
5
+
6
+ /**
7
+ * Build a context of type information from the AST
8
+ * @param {Array<Object>} ast - Abstract Syntax Tree of the Go file
9
+ * @returns {Object} Type context object with 'functions' and 'globals' properties
10
+ * @returns {Object} typeContext.functions - Map of function names to their parameter and local variable types
11
+ * @returns {Object} typeContext.globals - Map of global variable names to their types and values
12
+ */
13
+ function buildTypeContext(ast) {
14
+ const context = {
15
+ functions: {},
16
+ globals: {}
17
+ };
18
+
19
+ for (const node of ast) {
20
+ if (node.tag === 'func') {
21
+ // Store function parameter types
22
+ context.functions[node.name] = {
23
+ params: {},
24
+ locals: {}
25
+ };
26
+
27
+ if (node.args) {
28
+ for (const arg of node.args) {
29
+ if (arg.name && arg.type) {
30
+ context.functions[node.name].params[arg.name] = { type: arg.type };
31
+ }
32
+ }
33
+ }
34
+
35
+ // Scan function body for local variable declarations
36
+ if (node.body) {
37
+ scanForDeclarations(node.body, context.functions[node.name].locals);
38
+ }
39
+ } else if (node.tag === 'declare') {
40
+ // Global variable declarations
41
+ if (node.names && node.names.length > 0 && node.type) {
42
+ for (const name of node.names) {
43
+ context.globals[name] = { type: node.type, value: node.value };
44
+ }
45
+ }
46
+ }
47
+ }
48
+
49
+ return context;
50
+ }
51
+
52
+ /**
53
+ * Scan statements for variable declarations
54
+ * @param {Array<Object>} body - Array of AST statement nodes to scan
55
+ * @param {Object} locals - Object to store local variable declarations (modified in place)
56
+ */
57
+ function scanForDeclarations(body, locals) {
58
+ for (const stmt of body) {
59
+ if (stmt.tag === 'declare') {
60
+ if (stmt.names && stmt.type) {
61
+ for (const name of stmt.names) {
62
+ locals[name] = { type: stmt.type, value: stmt.value };
63
+ }
64
+ }
65
+ } else if (stmt.tag === 'if' && stmt.body) {
66
+ scanForDeclarations(stmt.body, locals);
67
+ } else if (stmt.tag === 'elseif' && stmt.body) {
68
+ scanForDeclarations(stmt.body, locals);
69
+ } else if (stmt.tag === 'else' && stmt.body) {
70
+ scanForDeclarations(stmt.body, locals);
71
+ } else if (stmt.tag === 'for' && stmt.body) {
72
+ scanForDeclarations(stmt.body, locals);
73
+ } else if (stmt.tag === 'foreach' && stmt.body) {
74
+ scanForDeclarations(stmt.body, locals);
75
+ } else if (stmt.tag === 'switch' && stmt.cases) {
76
+ for (const caseNode of stmt.cases) {
77
+ if (caseNode.body) {
78
+ scanForDeclarations(caseNode.body, locals);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ }
84
+
85
+ module.exports = {
86
+ buildTypeContext,
87
+ scanForDeclarations
88
+ };
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @fileoverview Utility functions for Go AST analysis and type extraction
3
+ * @module analyze/go/utils
4
+ */
5
+
6
+ /**
7
+ * Extract string value from various node types
8
+ * @param {Object} node - AST node that may contain a string value
9
+ * @returns {string|null} Extracted string value without quotes, or null if not found
10
+ */
11
+ function extractStringValue(node) {
12
+ if (!node) return null;
13
+
14
+ // Handle direct string literals
15
+ if (node.tag === 'string') {
16
+ // Remove quotes from the value
17
+ return node.value.slice(1, -1);
18
+ }
19
+
20
+ // Handle expressions that might contain a string
21
+ if (node.tag === 'expr' && node.body && node.body.length > 0) {
22
+ // Look for string literals in the expression body
23
+ for (const item of node.body) {
24
+ if (item.tag === 'string') {
25
+ return item.value.slice(1, -1);
26
+ }
27
+ }
28
+ }
29
+
30
+ return null;
31
+ }
32
+
33
+ /**
34
+ * Find a struct literal in an expression
35
+ * @param {Object} expr - AST expression node to search
36
+ * @returns {Object|null} Struct literal AST node or null if not found
37
+ */
38
+ function findStructLiteral(expr) {
39
+ if (!expr) return null;
40
+
41
+ if (expr.tag === 'structlit') {
42
+ return expr;
43
+ }
44
+
45
+ if (expr.tag === 'expr' && expr.body) {
46
+ for (const item of expr.body) {
47
+ if (item.tag === 'structlit') {
48
+ return item;
49
+ }
50
+ }
51
+ }
52
+
53
+ return null;
54
+ }
55
+
56
+ /**
57
+ * Find a field in a struct by name
58
+ * @param {Object} structlit - Struct literal AST node
59
+ * @param {string} fieldName - Name of the field to find
60
+ * @returns {Object|null} Field AST node or null if not found
61
+ */
62
+ function findStructField(structlit, fieldName) {
63
+ if (!structlit.fields) return null;
64
+
65
+ for (const field of structlit.fields) {
66
+ const name = extractFieldName(field);
67
+ if (name === fieldName) {
68
+ return field;
69
+ }
70
+ }
71
+
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Extract field name from a struct field
77
+ * @param {Object} field - Struct field AST node
78
+ * @returns {string|null} Field name or null if not found
79
+ */
80
+ function extractFieldName(field) {
81
+ if (field.name) {
82
+ return field.name;
83
+ }
84
+
85
+ if (field.value && field.value.tag === 'expr' && field.value.body) {
86
+ // Look for pattern: fieldName: value
87
+ const body = field.value.body;
88
+ if (body.length >= 3 &&
89
+ body[0].tag === 'ident' &&
90
+ body[1].tag === 'op' &&
91
+ body[1].value === ':') {
92
+ return body[0].value;
93
+ }
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Map Go types to schema types
101
+ * @param {Object} goType - Go type AST node
102
+ * @returns {Object} Schema type object with 'type' property and optionally 'items' or 'properties'
103
+ */
104
+ function mapGoTypeToSchemaType(goType) {
105
+ if (!goType) return { type: 'any' };
106
+
107
+ // Handle case where goType might be an object with a type property
108
+ if (goType.type) {
109
+ goType = goType.type;
110
+ }
111
+
112
+ // Handle simple types
113
+ if (goType.tag === 'string') return { type: 'string' };
114
+ if (goType.tag === 'bool') return { type: 'boolean' };
115
+ if (goType.tag === 'int' || goType.tag === 'int8' || goType.tag === 'int16' ||
116
+ goType.tag === 'int32' || goType.tag === 'int64' || goType.tag === 'uint' ||
117
+ goType.tag === 'uint8' || goType.tag === 'uint16' || goType.tag === 'uint32' ||
118
+ goType.tag === 'uint64' || goType.tag === 'float32' || goType.tag === 'float64' ||
119
+ goType.tag === 'byte' || goType.tag === 'rune') {
120
+ return { type: 'number' };
121
+ }
122
+
123
+ // Handle array types
124
+ if (goType.tag === 'array') {
125
+ const itemType = mapGoTypeToSchemaType(goType.item);
126
+ return {
127
+ type: 'array',
128
+ items: itemType
129
+ };
130
+ }
131
+
132
+ // Handle slice types (arrays without fixed size)
133
+ if (goType.tag === 'array' && !goType.size) {
134
+ const itemType = mapGoTypeToSchemaType(goType.item);
135
+ return {
136
+ type: 'array',
137
+ items: itemType
138
+ };
139
+ }
140
+
141
+ // Handle map types
142
+ if (goType.tag === 'map') {
143
+ return {
144
+ type: 'object',
145
+ properties: {}
146
+ };
147
+ }
148
+
149
+ // Handle pointer types by dereferencing
150
+ if (goType.tag === 'ptr') {
151
+ return mapGoTypeToSchemaType(goType.item);
152
+ }
153
+
154
+ // Default to any for complex or unknown types
155
+ return { type: 'any' };
156
+ }
157
+
158
+ /**
159
+ * Extract Snowplow values from sphelp.NewString/NewFloat64
160
+ * @param {Object} expr - Expression containing Snowplow helper function call
161
+ * @returns {string|number|null} Extracted value or null if not found
162
+ */
163
+ function extractSnowplowValue(expr) {
164
+ if (!expr) return null;
165
+
166
+ // Direct value
167
+ if (expr.tag === 'string') {
168
+ return expr.value.slice(1, -1);
169
+ }
170
+ if (expr.tag === 'number') {
171
+ return parseFloat(expr.value);
172
+ }
173
+
174
+ // Look for sphelp.NewString("value") or sphelp.NewFloat64(value)
175
+ if (expr.tag === 'expr' && expr.body) {
176
+ for (const item of expr.body) {
177
+ if (item.tag === 'call' && item.func && item.func.tag === 'access') {
178
+ if (item.func.member === 'NewString' && item.args && item.args.length > 0) {
179
+ return extractStringValue(item.args[0]);
180
+ }
181
+ if (item.func.member === 'NewFloat64' && item.args && item.args.length > 0) {
182
+ const numExpr = item.args[0];
183
+ if (numExpr.tag === 'number') {
184
+ return parseFloat(numExpr.value);
185
+ }
186
+ if (numExpr.tag === 'expr' && numExpr.body && numExpr.body[0] && numExpr.body[0].tag === 'number') {
187
+ return parseFloat(numExpr.body[0].value);
188
+ }
189
+ }
190
+ }
191
+ }
192
+ }
193
+
194
+ return null;
195
+ }
196
+
197
+ /**
198
+ * Resolves a variable to its value by looking it up in the type context
199
+ * @param {string} varName - The variable name to resolve
200
+ * @param {Object} typeContext - The type context containing variable types
201
+ * @param {string} currentFunction - The current function scope
202
+ * @returns {Object|null} The resolved value or null
203
+ */
204
+ function resolveVariable(varName, typeContext, currentFunction) {
205
+ // ... existing code ...
206
+ }
207
+
208
+ module.exports = {
209
+ extractStringValue,
210
+ findStructLiteral,
211
+ findStructField,
212
+ extractFieldName,
213
+ mapGoTypeToSchemaType,
214
+ extractSnowplowValue
215
+ };
@@ -1,11 +1,16 @@
1
+ /**
2
+ * @fileoverview Directory analyzer for detecting analytics tracking across multiple programming languages
3
+ * @module analyze-tracking/analyze
4
+ */
5
+
1
6
  const path = require('path');
2
7
  const ts = require('typescript');
3
- const { getAllFiles } = require('../fileProcessor');
4
- const { analyzeJsFile } = require('./analyzeJsFile');
5
- const { analyzeTsFile } = require('./analyzeTsFile');
6
- const { analyzePythonFile } = require('./analyzePythonFile');
7
- const { analyzeRubyFile } = require('./analyzeRubyFile');
8
- const { analyzeGoFile } = require('./analyzeGoFile');
8
+ const { getAllFiles } = require('../utils/fileProcessor');
9
+ const { analyzeJsFile } = require('./javascript');
10
+ const { analyzeTsFile } = require('./typescript');
11
+ const { analyzePythonFile } = require('./python');
12
+ const { analyzeRubyFile } = require('./ruby');
13
+ const { analyzeGoFile } = require('./go');
9
14
 
10
15
  async function analyzeDirectory(dirPath, customFunction) {
11
16
  const allEvents = {};
@@ -37,7 +42,6 @@ async function analyzeDirectory(dirPath, customFunction) {
37
42
  } else if (isGoFile) {
38
43
  events = await analyzeGoFile(file, customFunction);
39
44
  } else {
40
- console.info(`Skipping file ${file} because it is not a supported file type`);
41
45
  continue;
42
46
  }
43
47
 
@@ -0,0 +1,115 @@
1
+ /**
2
+ * @fileoverview Constants and configurations for analytics tracking providers
3
+ * @module analyze/javascript/constants
4
+ */
5
+
6
+ /**
7
+ * Analytics provider configurations
8
+ * @typedef {Object} ProviderConfig
9
+ * @property {string} name - Provider display name
10
+ * @property {string} objectName - Object name in JavaScript
11
+ * @property {string} methodName - Method name for tracking
12
+ * @property {string} type - Type of detection (member|function)
13
+ */
14
+
15
+ /**
16
+ * Supported analytics providers and their detection patterns
17
+ * @type {Object.<string, ProviderConfig>}
18
+ */
19
+ const ANALYTICS_PROVIDERS = {
20
+ SEGMENT: {
21
+ name: 'segment',
22
+ objectName: 'analytics',
23
+ methodName: 'track',
24
+ type: 'member'
25
+ },
26
+ MIXPANEL: {
27
+ name: 'mixpanel',
28
+ objectName: 'mixpanel',
29
+ methodName: 'track',
30
+ type: 'member'
31
+ },
32
+ AMPLITUDE: {
33
+ name: 'amplitude',
34
+ objectName: 'amplitude',
35
+ methodName: 'track',
36
+ type: 'member'
37
+ },
38
+ RUDDERSTACK: {
39
+ name: 'rudderstack',
40
+ objectName: 'rudderanalytics',
41
+ methodName: 'track',
42
+ type: 'member'
43
+ },
44
+ MPARTICLE: {
45
+ name: 'mparticle',
46
+ objectNames: ['mParticle', 'mparticle'],
47
+ methodName: 'logEvent',
48
+ type: 'member'
49
+ },
50
+ POSTHOG: {
51
+ name: 'posthog',
52
+ objectName: 'posthog',
53
+ methodName: 'capture',
54
+ type: 'member'
55
+ },
56
+ PENDO: {
57
+ name: 'pendo',
58
+ objectName: 'pendo',
59
+ methodName: 'track',
60
+ type: 'member'
61
+ },
62
+ HEAP: {
63
+ name: 'heap',
64
+ objectName: 'heap',
65
+ methodName: 'track',
66
+ type: 'member'
67
+ },
68
+ SNOWPLOW: {
69
+ name: 'snowplow',
70
+ objectName: 'tracker',
71
+ methodName: 'track',
72
+ type: 'member'
73
+ },
74
+ GOOGLE_ANALYTICS: {
75
+ name: 'googleanalytics',
76
+ functionName: 'gtag',
77
+ type: 'function'
78
+ }
79
+ };
80
+
81
+ /**
82
+ * Parser options for Acorn
83
+ * @type {Object}
84
+ */
85
+ const PARSER_OPTIONS = {
86
+ ecmaVersion: 'latest',
87
+ sourceType: 'module',
88
+ locations: true
89
+ };
90
+
91
+ /**
92
+ * AST node types
93
+ * @enum {string}
94
+ */
95
+ const NODE_TYPES = {
96
+ CALL_EXPRESSION: 'CallExpression',
97
+ MEMBER_EXPRESSION: 'MemberExpression',
98
+ IDENTIFIER: 'Identifier',
99
+ OBJECT_EXPRESSION: 'ObjectExpression',
100
+ ARRAY_EXPRESSION: 'ArrayExpression',
101
+ LITERAL: 'Literal',
102
+ ARROW_FUNCTION: 'ArrowFunctionExpression',
103
+ FUNCTION_EXPRESSION: 'FunctionExpression',
104
+ FUNCTION_DECLARATION: 'FunctionDeclaration',
105
+ METHOD_DEFINITION: 'MethodDefinition',
106
+ VARIABLE_DECLARATOR: 'VariableDeclarator',
107
+ EXPORT_NAMED: 'ExportNamedDeclaration',
108
+ PROPERTY: 'Property'
109
+ };
110
+
111
+ module.exports = {
112
+ ANALYTICS_PROVIDERS,
113
+ PARSER_OPTIONS,
114
+ NODE_TYPES
115
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * @fileoverview Analytics source detection module
3
+ * @module analyze/javascript/detectors/analytics-source
4
+ */
5
+
6
+ const { ANALYTICS_PROVIDERS, NODE_TYPES } = require('../constants');
7
+
8
+ /**
9
+ * Detects the analytics provider from a CallExpression node
10
+ * @param {Object} node - AST CallExpression node
11
+ * @param {string} [customFunction] - Custom function name to detect
12
+ * @returns {string} The detected analytics source or 'unknown'
13
+ */
14
+ function detectAnalyticsSource(node, customFunction) {
15
+ if (!node.callee) {
16
+ return 'unknown';
17
+ }
18
+
19
+ // Check for custom function first
20
+ if (customFunction && isCustomFunction(node, customFunction)) {
21
+ return 'custom';
22
+ }
23
+
24
+ // Check for function-based providers (e.g., gtag)
25
+ const functionSource = detectFunctionBasedProvider(node);
26
+ if (functionSource !== 'unknown') {
27
+ return functionSource;
28
+ }
29
+
30
+ // Check for member-based providers (e.g., analytics.track)
31
+ const memberSource = detectMemberBasedProvider(node);
32
+ if (memberSource !== 'unknown') {
33
+ return memberSource;
34
+ }
35
+
36
+ return 'unknown';
37
+ }
38
+
39
+ /**
40
+ * Checks if the node is a custom function call
41
+ * @param {Object} node - AST CallExpression node
42
+ * @param {string} customFunction - Custom function name
43
+ * @returns {boolean}
44
+ */
45
+ function isCustomFunction(node, customFunction) {
46
+ return node.callee.type === NODE_TYPES.IDENTIFIER &&
47
+ node.callee.name === customFunction;
48
+ }
49
+
50
+ /**
51
+ * Detects function-based analytics providers
52
+ * @param {Object} node - AST CallExpression node
53
+ * @returns {string} Provider name or 'unknown'
54
+ */
55
+ function detectFunctionBasedProvider(node) {
56
+ if (node.callee.type !== NODE_TYPES.IDENTIFIER) {
57
+ return 'unknown';
58
+ }
59
+
60
+ const functionName = node.callee.name;
61
+
62
+ for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
63
+ if (provider.type === 'function' && provider.functionName === functionName) {
64
+ return provider.name;
65
+ }
66
+ }
67
+
68
+ return 'unknown';
69
+ }
70
+
71
+ /**
72
+ * Detects member expression-based analytics providers
73
+ * @param {Object} node - AST CallExpression node
74
+ * @returns {string} Provider name or 'unknown'
75
+ */
76
+ function detectMemberBasedProvider(node) {
77
+ if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
78
+ return 'unknown';
79
+ }
80
+
81
+ const objectName = node.callee.object.name;
82
+ const methodName = node.callee.property.name;
83
+
84
+ if (!objectName || !methodName) {
85
+ return 'unknown';
86
+ }
87
+
88
+ for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
89
+ if (provider.type === 'member' && matchesMemberProvider(provider, objectName, methodName)) {
90
+ return provider.name;
91
+ }
92
+ }
93
+
94
+ return 'unknown';
95
+ }
96
+
97
+ /**
98
+ * Checks if object and method names match a provider configuration
99
+ * @param {Object} provider - Provider configuration
100
+ * @param {string} objectName - Object name from AST
101
+ * @param {string} methodName - Method name from AST
102
+ * @returns {boolean}
103
+ */
104
+ function matchesMemberProvider(provider, objectName, methodName) {
105
+ if (provider.methodName !== methodName) {
106
+ return false;
107
+ }
108
+
109
+ // Handle providers with multiple possible object names (e.g., mParticle/mparticle)
110
+ if (provider.objectNames) {
111
+ return provider.objectNames.includes(objectName);
112
+ }
113
+
114
+ return provider.objectName === objectName;
115
+ }
116
+
117
+ module.exports = {
118
+ detectAnalyticsSource
119
+ };