@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.
- package/README.md +35 -61
- package/bin/cli.js +1 -1
- package/package.json +18 -3
- package/src/analyze/go/astTraversal.js +121 -0
- package/src/analyze/go/constants.js +20 -0
- package/src/analyze/go/eventDeduplicator.js +47 -0
- package/src/analyze/go/eventExtractor.js +156 -0
- package/src/analyze/go/goAstParser/constants.js +39 -0
- package/src/analyze/go/goAstParser/expressionParser.js +281 -0
- package/src/analyze/go/goAstParser/index.js +52 -0
- package/src/analyze/go/goAstParser/statementParser.js +387 -0
- package/src/analyze/go/goAstParser/tokenizer.js +196 -0
- package/src/analyze/go/goAstParser/typeParser.js +202 -0
- package/src/analyze/go/goAstParser/utils.js +99 -0
- package/src/analyze/go/index.js +55 -0
- package/src/analyze/go/propertyExtractor.js +670 -0
- package/src/analyze/go/trackingDetector.js +71 -0
- package/src/analyze/go/trackingExtractor.js +54 -0
- package/src/analyze/go/typeContext.js +88 -0
- package/src/analyze/go/utils.js +215 -0
- package/src/analyze/index.js +11 -7
- package/src/analyze/javascript/constants.js +115 -0
- package/src/analyze/javascript/detectors/analytics-source.js +119 -0
- package/src/analyze/javascript/detectors/index.js +10 -0
- package/src/analyze/javascript/extractors/event-extractor.js +179 -0
- package/src/analyze/javascript/extractors/index.js +13 -0
- package/src/analyze/javascript/extractors/property-extractor.js +172 -0
- package/src/analyze/javascript/index.js +38 -0
- package/src/analyze/javascript/parser.js +126 -0
- package/src/analyze/javascript/utils/function-finder.js +123 -0
- package/src/analyze/python/index.js +111 -0
- package/src/analyze/python/pythonTrackingAnalyzer.py +814 -0
- package/src/analyze/ruby/detectors.js +46 -0
- package/src/analyze/ruby/extractors.js +258 -0
- package/src/analyze/ruby/index.js +51 -0
- package/src/analyze/ruby/traversal.js +123 -0
- package/src/analyze/ruby/types.js +30 -0
- package/src/analyze/ruby/visitor.js +66 -0
- package/src/analyze/typescript/constants.js +109 -0
- package/src/analyze/typescript/detectors/analytics-source.js +120 -0
- package/src/analyze/typescript/detectors/index.js +10 -0
- package/src/analyze/typescript/extractors/event-extractor.js +269 -0
- package/src/analyze/typescript/extractors/index.js +14 -0
- package/src/analyze/typescript/extractors/property-extractor.js +395 -0
- package/src/analyze/typescript/index.js +48 -0
- package/src/analyze/typescript/parser.js +131 -0
- package/src/analyze/typescript/utils/function-finder.js +114 -0
- package/src/analyze/typescript/utils/type-resolver.js +193 -0
- package/src/generateDescriptions/index.js +81 -0
- package/src/generateDescriptions/llmUtils.js +33 -0
- package/src/generateDescriptions/promptUtils.js +62 -0
- package/src/generateDescriptions/schemaUtils.js +61 -0
- package/src/index.js +7 -2
- package/src/{fileProcessor.js → utils/fileProcessor.js} +5 -0
- package/src/{repoDetails.js → utils/repoDetails.js} +5 -0
- package/src/{yamlGenerator.js → utils/yamlGenerator.js} +5 -0
- package/.github/workflows/npm-publish.yml +0 -33
- package/.github/workflows/pr-check.yml +0 -17
- package/jest.config.js +0 -7
- package/src/analyze/analyzeGoFile.js +0 -1164
- package/src/analyze/analyzeJsFile.js +0 -72
- package/src/analyze/analyzePythonFile.js +0 -41
- package/src/analyze/analyzeRubyFile.js +0 -409
- package/src/analyze/analyzeTsFile.js +0 -69
- package/src/analyze/go2json.js +0 -1069
- package/src/analyze/helpers.js +0 -217
- package/src/analyze/pythonTrackingAnalyzer.py +0 -439
- package/src/generateDescriptions.js +0 -196
- package/tests/detectSource.test.js +0 -20
- package/tests/extractProperties.test.js +0 -109
- package/tests/findWrappingFunction.test.js +0 -30
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Analytics source detection for Ruby tracking calls
|
|
3
|
+
* @module analyze/ruby/detectors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Detects the analytics source from a Ruby AST CallNode
|
|
8
|
+
* @param {Object} node - The AST CallNode to analyze
|
|
9
|
+
* @param {string} customFunction - Optional custom function name to detect
|
|
10
|
+
* @returns {string|null} - The detected source or null
|
|
11
|
+
*/
|
|
12
|
+
function detectSource(node, customFunction = null) {
|
|
13
|
+
if (!node) return null;
|
|
14
|
+
|
|
15
|
+
// Check for analytics libraries
|
|
16
|
+
if (node.receiver) {
|
|
17
|
+
const objectName = node.receiver.name;
|
|
18
|
+
const methodName = node.name;
|
|
19
|
+
|
|
20
|
+
// Segment and Rudderstack (both use similar format)
|
|
21
|
+
// Analytics.track (Segment) or analytics.track (Rudderstack)
|
|
22
|
+
if ((objectName === 'Analytics' || objectName === 'analytics') && methodName === 'track') {
|
|
23
|
+
// Try to determine if it's Rudderstack based on context
|
|
24
|
+
// For now, we'll treat lowercase 'analytics' as Rudderstack
|
|
25
|
+
return objectName === 'analytics' ? 'rudderstack' : 'segment';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Mixpanel (Ruby SDK uses Mixpanel::Tracker instance)
|
|
29
|
+
if (methodName === 'track' && objectName === 'tracker') return 'mixpanel';
|
|
30
|
+
|
|
31
|
+
// PostHog
|
|
32
|
+
if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Snowplow (typically tracker.track_struct_event)
|
|
36
|
+
if (node.name === 'track_struct_event') return 'snowplow';
|
|
37
|
+
|
|
38
|
+
// Custom tracking function
|
|
39
|
+
if (customFunction && node.name === customFunction) return 'custom';
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = {
|
|
45
|
+
detectSource
|
|
46
|
+
};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Event and property extraction utilities for Ruby analytics
|
|
3
|
+
* @module analyze/ruby/extractors
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { getValueType } = require('./types');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts the event name from a tracking call based on the source
|
|
10
|
+
* @param {Object} node - The AST CallNode
|
|
11
|
+
* @param {string} source - The detected analytics source
|
|
12
|
+
* @returns {string|null} - The extracted event name or null
|
|
13
|
+
*/
|
|
14
|
+
function extractEventName(node, source) {
|
|
15
|
+
if (source === 'segment' || source === 'rudderstack') {
|
|
16
|
+
// Both Segment and Rudderstack use the same format
|
|
17
|
+
const params = node.arguments_.arguments_[0].elements;
|
|
18
|
+
const eventProperty = params.find(param => param?.key?.unescaped?.value === 'event');
|
|
19
|
+
return eventProperty?.value?.unescaped?.value || null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (source === 'mixpanel') {
|
|
23
|
+
// Mixpanel Ruby SDK format: tracker.track('distinct_id', 'event_name', {...})
|
|
24
|
+
const args = node.arguments_.arguments_;
|
|
25
|
+
if (args && args.length > 1 && args[1]?.unescaped?.value) {
|
|
26
|
+
return args[1].unescaped.value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (source === 'posthog') {
|
|
31
|
+
// PostHog Ruby SDK format: posthog.capture({distinct_id: '...', event: '...', properties: {...}})
|
|
32
|
+
const hashArg = node.arguments_.arguments_[0];
|
|
33
|
+
if (hashArg && hashArg.elements) {
|
|
34
|
+
const eventProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'event');
|
|
35
|
+
return eventProperty?.value?.unescaped?.value || null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (source === 'snowplow') {
|
|
40
|
+
// Snowplow Ruby SDK: tracker.track_struct_event(category: '...', action: '...', ...)
|
|
41
|
+
const params = node.arguments_.arguments_[0].elements;
|
|
42
|
+
const actionProperty = params.find(param => param?.key?.unescaped?.value === 'action');
|
|
43
|
+
return actionProperty?.value?.unescaped?.value || null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (source === 'custom') {
|
|
47
|
+
// Custom function format: customFunction('event_name', {...})
|
|
48
|
+
const args = node.arguments_.arguments_;
|
|
49
|
+
if (args && args.length > 0 && args[0]?.unescaped?.value) {
|
|
50
|
+
return args[0].unescaped.value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Extracts properties from a tracking call based on the source
|
|
59
|
+
* @param {Object} node - The AST CallNode
|
|
60
|
+
* @param {string} source - The detected analytics source
|
|
61
|
+
* @returns {Object|null} - The extracted properties or null
|
|
62
|
+
*/
|
|
63
|
+
async function extractProperties(node, source) {
|
|
64
|
+
const { HashNode, ArrayNode } = await import('@ruby/prism');
|
|
65
|
+
|
|
66
|
+
if (source === 'segment' || source === 'rudderstack') {
|
|
67
|
+
// Both Segment and Rudderstack use the same format
|
|
68
|
+
const params = node.arguments_.arguments_[0].elements;
|
|
69
|
+
const properties = {};
|
|
70
|
+
|
|
71
|
+
// Process all top-level fields except 'event'
|
|
72
|
+
for (const param of params) {
|
|
73
|
+
const key = param?.key?.unescaped?.value;
|
|
74
|
+
|
|
75
|
+
if (key && key !== 'event') {
|
|
76
|
+
const value = param?.value;
|
|
77
|
+
|
|
78
|
+
if (key === 'properties' && value instanceof HashNode) {
|
|
79
|
+
// Merge properties from the 'properties' hash into the top level
|
|
80
|
+
const nestedProperties = await extractHashProperties(value);
|
|
81
|
+
Object.assign(properties, nestedProperties);
|
|
82
|
+
} else if (value instanceof HashNode) {
|
|
83
|
+
// Handle other nested hash objects
|
|
84
|
+
const hashProperties = await extractHashProperties(value);
|
|
85
|
+
properties[key] = {
|
|
86
|
+
type: 'object',
|
|
87
|
+
properties: hashProperties
|
|
88
|
+
};
|
|
89
|
+
} else if (value instanceof ArrayNode) {
|
|
90
|
+
// Handle arrays
|
|
91
|
+
const arrayItems = await extractArrayItemProperties(value);
|
|
92
|
+
properties[key] = {
|
|
93
|
+
type: 'array',
|
|
94
|
+
items: arrayItems
|
|
95
|
+
};
|
|
96
|
+
} else {
|
|
97
|
+
// Handle primitive values
|
|
98
|
+
const valueType = await getValueType(value);
|
|
99
|
+
properties[key] = {
|
|
100
|
+
type: valueType
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return properties;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (source === 'mixpanel') {
|
|
110
|
+
// Mixpanel Ruby SDK: tracker.track('distinct_id', 'event_name', {properties})
|
|
111
|
+
const args = node.arguments_.arguments_;
|
|
112
|
+
const properties = {};
|
|
113
|
+
|
|
114
|
+
// Add distinct_id as property (even if it's a variable)
|
|
115
|
+
if (args && args.length > 0) {
|
|
116
|
+
properties.distinct_id = {
|
|
117
|
+
type: await getValueType(args[0])
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Extract properties from third argument if it exists
|
|
122
|
+
if (args && args.length > 2 && args[2] instanceof HashNode) {
|
|
123
|
+
const propsHash = await extractHashProperties(args[2]);
|
|
124
|
+
Object.assign(properties, propsHash);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return properties;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (source === 'posthog') {
|
|
131
|
+
// PostHog Ruby SDK: posthog.capture({distinct_id: '...', event: '...', properties: {...}})
|
|
132
|
+
const hashArg = node.arguments_.arguments_[0];
|
|
133
|
+
const properties = {};
|
|
134
|
+
|
|
135
|
+
if (hashArg && hashArg.elements) {
|
|
136
|
+
// Extract distinct_id if present
|
|
137
|
+
const distinctIdProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'distinct_id');
|
|
138
|
+
if (distinctIdProperty?.value) {
|
|
139
|
+
properties.distinct_id = {
|
|
140
|
+
type: await getValueType(distinctIdProperty.value)
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Extract properties
|
|
145
|
+
const propsProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'properties');
|
|
146
|
+
if (propsProperty?.value instanceof HashNode) {
|
|
147
|
+
const props = await extractHashProperties(propsProperty.value);
|
|
148
|
+
Object.assign(properties, props);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return properties;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (source === 'snowplow') {
|
|
156
|
+
// Snowplow Ruby SDK: tracker.track_struct_event(category: '...', action: '...', ...)
|
|
157
|
+
const params = node.arguments_.arguments_[0].elements;
|
|
158
|
+
const properties = {};
|
|
159
|
+
|
|
160
|
+
// Extract all struct event parameters except 'action' (which is used as the event name)
|
|
161
|
+
for (const param of params) {
|
|
162
|
+
const key = param?.key?.unescaped?.value;
|
|
163
|
+
if (key && key !== 'action') {
|
|
164
|
+
properties[key] = {
|
|
165
|
+
type: await getValueType(param.value)
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return properties;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (source === 'custom') {
|
|
174
|
+
// Custom function format: customFunction('event_name', {properties})
|
|
175
|
+
const args = node.arguments_.arguments_;
|
|
176
|
+
if (args && args.length > 1 && args[1] instanceof HashNode) {
|
|
177
|
+
return await extractHashProperties(args[1]);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Extracts properties from a HashNode
|
|
186
|
+
* @param {Object} hashNode - The HashNode to extract properties from
|
|
187
|
+
* @returns {Object} - The extracted properties
|
|
188
|
+
*/
|
|
189
|
+
async function extractHashProperties(hashNode) {
|
|
190
|
+
const { AssocNode, HashNode, ArrayNode } = await import('@ruby/prism');
|
|
191
|
+
const properties = {};
|
|
192
|
+
|
|
193
|
+
for (const element of hashNode.elements) {
|
|
194
|
+
if (element instanceof AssocNode) {
|
|
195
|
+
const key = element.key.unescaped?.value;
|
|
196
|
+
const value = element.value;
|
|
197
|
+
|
|
198
|
+
if (key) {
|
|
199
|
+
if (value instanceof HashNode) {
|
|
200
|
+
// Handle nested hash objects
|
|
201
|
+
const nestedProperties = await extractHashProperties(value);
|
|
202
|
+
properties[key] = {
|
|
203
|
+
type: 'object',
|
|
204
|
+
properties: nestedProperties
|
|
205
|
+
};
|
|
206
|
+
} else if (value instanceof ArrayNode) {
|
|
207
|
+
// Handle arrays
|
|
208
|
+
const items = await extractArrayItemProperties(value);
|
|
209
|
+
properties[key] = {
|
|
210
|
+
type: 'array',
|
|
211
|
+
items
|
|
212
|
+
};
|
|
213
|
+
} else {
|
|
214
|
+
// Handle primitive values
|
|
215
|
+
const valueType = await getValueType(value);
|
|
216
|
+
properties[key] = {
|
|
217
|
+
type: valueType
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return properties;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Extracts property information from array items
|
|
229
|
+
* @param {Object} arrayNode - The ArrayNode to analyze
|
|
230
|
+
* @returns {Object} - Type information for array items
|
|
231
|
+
*/
|
|
232
|
+
async function extractArrayItemProperties(arrayNode) {
|
|
233
|
+
const { HashNode } = await import('@ruby/prism');
|
|
234
|
+
|
|
235
|
+
if (arrayNode.elements.length === 0) {
|
|
236
|
+
return { type: 'any' };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const firstItem = arrayNode.elements[0];
|
|
240
|
+
if (firstItem instanceof HashNode) {
|
|
241
|
+
return {
|
|
242
|
+
type: 'object',
|
|
243
|
+
properties: await extractHashProperties(firstItem)
|
|
244
|
+
};
|
|
245
|
+
} else {
|
|
246
|
+
const valueType = await getValueType(firstItem);
|
|
247
|
+
return {
|
|
248
|
+
type: valueType
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
module.exports = {
|
|
254
|
+
extractEventName,
|
|
255
|
+
extractProperties,
|
|
256
|
+
extractHashProperties,
|
|
257
|
+
extractArrayItemProperties
|
|
258
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Ruby analytics tracking analyzer - main entry point
|
|
3
|
+
* @module analyze/ruby
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const TrackingVisitor = require('./visitor');
|
|
8
|
+
|
|
9
|
+
// Lazy-loaded parse function from Ruby Prism
|
|
10
|
+
let parse = null;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Analyzes a Ruby file for analytics tracking calls
|
|
14
|
+
* @param {string} filePath - Path to the Ruby file to analyze
|
|
15
|
+
* @param {string} customFunction - Optional custom tracking function name
|
|
16
|
+
* @returns {Promise<Array>} Array of tracking events found in the file
|
|
17
|
+
* @throws {Error} If the file cannot be read or parsed
|
|
18
|
+
*/
|
|
19
|
+
async function analyzeRubyFile(filePath, customFunction) {
|
|
20
|
+
// Lazy load the Ruby Prism parser
|
|
21
|
+
if (!parse) {
|
|
22
|
+
const { loadPrism } = await import('@ruby/prism');
|
|
23
|
+
parse = await loadPrism();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Read the file content
|
|
28
|
+
const code = fs.readFileSync(filePath, 'utf8');
|
|
29
|
+
|
|
30
|
+
// Parse the Ruby code into an AST
|
|
31
|
+
let ast;
|
|
32
|
+
try {
|
|
33
|
+
ast = await parse(code);
|
|
34
|
+
} catch (parseError) {
|
|
35
|
+
console.error(`Error parsing file ${filePath}:`, parseError.message);
|
|
36
|
+
return []; // Return empty events array if parsing fails
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Create a visitor and analyze the AST
|
|
40
|
+
const visitor = new TrackingVisitor(code, filePath, customFunction);
|
|
41
|
+
const events = await visitor.analyze(ast);
|
|
42
|
+
|
|
43
|
+
return events;
|
|
44
|
+
|
|
45
|
+
} catch (fileError) {
|
|
46
|
+
console.error(`Error reading or processing file ${filePath}:`, fileError.message);
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { analyzeRubyFile };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AST traversal utilities for Ruby code analysis
|
|
3
|
+
* @module analyze/ruby/traversal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Finds the wrapping function for a given node
|
|
8
|
+
* @param {Object} node - The current AST node
|
|
9
|
+
* @param {Array} ancestors - The ancestor nodes stack
|
|
10
|
+
* @returns {string} - The function name or 'global'/'block'
|
|
11
|
+
*/
|
|
12
|
+
async function findWrappingFunction(node, ancestors) {
|
|
13
|
+
const { DefNode, BlockNode, LambdaNode } = await import('@ruby/prism');
|
|
14
|
+
|
|
15
|
+
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
16
|
+
const current = ancestors[i];
|
|
17
|
+
|
|
18
|
+
// Handle method definitions
|
|
19
|
+
if (current instanceof DefNode) {
|
|
20
|
+
return current.name;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Handle blocks and lambdas
|
|
24
|
+
if (current instanceof BlockNode || current instanceof LambdaNode) {
|
|
25
|
+
return 'block';
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return 'global';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Recursively traverses the AST tree
|
|
33
|
+
* @param {Object} node - The current AST node
|
|
34
|
+
* @param {Function} nodeVisitor - Function to call for each node
|
|
35
|
+
* @param {Array} ancestors - The ancestor nodes stack
|
|
36
|
+
*/
|
|
37
|
+
async function traverseNode(node, nodeVisitor, ancestors = []) {
|
|
38
|
+
const {
|
|
39
|
+
ProgramNode,
|
|
40
|
+
StatementsNode,
|
|
41
|
+
DefNode,
|
|
42
|
+
IfNode,
|
|
43
|
+
BlockNode,
|
|
44
|
+
ArgumentsNode,
|
|
45
|
+
HashNode,
|
|
46
|
+
AssocNode,
|
|
47
|
+
ClassNode,
|
|
48
|
+
ModuleNode,
|
|
49
|
+
CallNode
|
|
50
|
+
} = await import('@ruby/prism');
|
|
51
|
+
|
|
52
|
+
if (!node) return;
|
|
53
|
+
|
|
54
|
+
ancestors.push(node);
|
|
55
|
+
|
|
56
|
+
// Call the visitor for this node
|
|
57
|
+
if (node instanceof CallNode) {
|
|
58
|
+
await nodeVisitor(node, ancestors);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Visit all child nodes based on node type
|
|
62
|
+
if (node instanceof ProgramNode) {
|
|
63
|
+
await traverseNode(node.statements, nodeVisitor, ancestors);
|
|
64
|
+
} else if (node instanceof StatementsNode) {
|
|
65
|
+
for (const child of node.body) {
|
|
66
|
+
await traverseNode(child, nodeVisitor, ancestors);
|
|
67
|
+
}
|
|
68
|
+
} else if (node instanceof ClassNode) {
|
|
69
|
+
if (node.body) {
|
|
70
|
+
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
71
|
+
}
|
|
72
|
+
} else if (node instanceof ModuleNode) {
|
|
73
|
+
if (node.body) {
|
|
74
|
+
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
75
|
+
}
|
|
76
|
+
} else if (node instanceof DefNode) {
|
|
77
|
+
if (node.body) {
|
|
78
|
+
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
79
|
+
}
|
|
80
|
+
} else if (node instanceof IfNode) {
|
|
81
|
+
if (node.statements) {
|
|
82
|
+
await traverseNode(node.statements, nodeVisitor, ancestors);
|
|
83
|
+
}
|
|
84
|
+
if (node.subsequent) {
|
|
85
|
+
await traverseNode(node.subsequent, nodeVisitor, ancestors);
|
|
86
|
+
}
|
|
87
|
+
} else if (node instanceof BlockNode) {
|
|
88
|
+
if (node.body) {
|
|
89
|
+
await traverseNode(node.body, nodeVisitor, ancestors);
|
|
90
|
+
}
|
|
91
|
+
} else if (node instanceof ArgumentsNode) {
|
|
92
|
+
for (const arg of node.arguments) {
|
|
93
|
+
await traverseNode(arg, nodeVisitor, ancestors);
|
|
94
|
+
}
|
|
95
|
+
} else if (node instanceof HashNode) {
|
|
96
|
+
for (const element of node.elements) {
|
|
97
|
+
await traverseNode(element, nodeVisitor, ancestors);
|
|
98
|
+
}
|
|
99
|
+
} else if (node instanceof AssocNode) {
|
|
100
|
+
await traverseNode(node.key, nodeVisitor, ancestors);
|
|
101
|
+
await traverseNode(node.value, nodeVisitor, ancestors);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
ancestors.pop();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Gets the line number for a given location in the code
|
|
109
|
+
* @param {string} code - The full source code
|
|
110
|
+
* @param {Object} location - The location object with startOffset
|
|
111
|
+
* @returns {number} - The line number (1-indexed)
|
|
112
|
+
*/
|
|
113
|
+
function getLineNumber(code, location) {
|
|
114
|
+
// Count the number of newlines before the start offset
|
|
115
|
+
const beforeStart = code.slice(0, location.startOffset);
|
|
116
|
+
return beforeStart.split('\n').length;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = {
|
|
120
|
+
findWrappingFunction,
|
|
121
|
+
traverseNode,
|
|
122
|
+
getLineNumber
|
|
123
|
+
};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Type definitions and constants for Ruby analytics analysis
|
|
3
|
+
* @module analyze/ruby/types
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
async function getValueType(node) {
|
|
7
|
+
const {
|
|
8
|
+
StringNode,
|
|
9
|
+
IntegerNode,
|
|
10
|
+
FloatNode,
|
|
11
|
+
TrueNode,
|
|
12
|
+
FalseNode,
|
|
13
|
+
NilNode,
|
|
14
|
+
SymbolNode,
|
|
15
|
+
CallNode
|
|
16
|
+
} = await import('@ruby/prism');
|
|
17
|
+
|
|
18
|
+
if (node instanceof StringNode) return 'string';
|
|
19
|
+
if (node instanceof IntegerNode || node instanceof FloatNode) return 'number';
|
|
20
|
+
if (node instanceof TrueNode || node instanceof FalseNode) return 'boolean';
|
|
21
|
+
if (node instanceof NilNode) return 'null';
|
|
22
|
+
if (node instanceof SymbolNode) return 'string';
|
|
23
|
+
if (node instanceof CallNode) return 'any'; // Dynamic values
|
|
24
|
+
|
|
25
|
+
return 'any'; // Default type
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
getValueType
|
|
30
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview AST visitor for analyzing Ruby tracking events
|
|
3
|
+
* @module analyze/ruby/visitor
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { detectSource } = require('./detectors');
|
|
7
|
+
const { extractEventName, extractProperties } = require('./extractors');
|
|
8
|
+
const { findWrappingFunction, traverseNode, getLineNumber } = require('./traversal');
|
|
9
|
+
|
|
10
|
+
class TrackingVisitor {
|
|
11
|
+
constructor(code, filePath, customFunction = null) {
|
|
12
|
+
this.code = code;
|
|
13
|
+
this.filePath = filePath;
|
|
14
|
+
this.customFunction = customFunction;
|
|
15
|
+
this.events = [];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Processes a call node to extract tracking event information
|
|
20
|
+
* @param {Object} node - The CallNode to process
|
|
21
|
+
* @param {Array} ancestors - The ancestor nodes stack
|
|
22
|
+
*/
|
|
23
|
+
async processCallNode(node, ancestors) {
|
|
24
|
+
try {
|
|
25
|
+
const source = detectSource(node, this.customFunction);
|
|
26
|
+
if (!source) return;
|
|
27
|
+
|
|
28
|
+
const eventName = extractEventName(node, source);
|
|
29
|
+
if (!eventName) return;
|
|
30
|
+
|
|
31
|
+
const line = getLineNumber(this.code, node.location);
|
|
32
|
+
const functionName = await findWrappingFunction(node, ancestors);
|
|
33
|
+
const properties = await extractProperties(node, source);
|
|
34
|
+
|
|
35
|
+
this.events.push({
|
|
36
|
+
eventName,
|
|
37
|
+
source,
|
|
38
|
+
properties,
|
|
39
|
+
filePath: this.filePath,
|
|
40
|
+
line,
|
|
41
|
+
functionName
|
|
42
|
+
});
|
|
43
|
+
} catch (nodeError) {
|
|
44
|
+
console.error(`Error processing node in ${this.filePath}:`, nodeError.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Analyzes the AST to find tracking events
|
|
50
|
+
* @param {Object} ast - The parsed AST
|
|
51
|
+
* @returns {Array} - Array of tracking events found
|
|
52
|
+
*/
|
|
53
|
+
async analyze(ast) {
|
|
54
|
+
// Create a visitor function that will be called for each CallNode
|
|
55
|
+
const nodeVisitor = async (node, ancestors) => {
|
|
56
|
+
await this.processCallNode(node, ancestors);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
// Traverse the AST starting from the program node
|
|
60
|
+
await traverseNode(ast.value, nodeVisitor);
|
|
61
|
+
|
|
62
|
+
return this.events;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = TrackingVisitor;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Constants and configurations for analytics tracking providers
|
|
3
|
+
* @module analyze/typescript/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
|
+
* TypeScript syntax kinds for node types we care about
|
|
83
|
+
* @enum {number}
|
|
84
|
+
*/
|
|
85
|
+
const TS_NODE_KINDS = {
|
|
86
|
+
CALL_EXPRESSION: 'CallExpression',
|
|
87
|
+
PROPERTY_ACCESS: 'PropertyAccessExpression',
|
|
88
|
+
IDENTIFIER: 'Identifier',
|
|
89
|
+
OBJECT_LITERAL: 'ObjectLiteralExpression',
|
|
90
|
+
ARRAY_LITERAL: 'ArrayLiteralExpression',
|
|
91
|
+
STRING_LITERAL: 'StringLiteral',
|
|
92
|
+
NUMERIC_LITERAL: 'NumericLiteral',
|
|
93
|
+
TRUE_KEYWORD: 'TrueKeyword',
|
|
94
|
+
FALSE_KEYWORD: 'FalseKeyword',
|
|
95
|
+
NULL_KEYWORD: 'NullKeyword',
|
|
96
|
+
UNDEFINED_KEYWORD: 'UndefinedKeyword',
|
|
97
|
+
FUNCTION_DECLARATION: 'FunctionDeclaration',
|
|
98
|
+
METHOD_DECLARATION: 'MethodDeclaration',
|
|
99
|
+
ARROW_FUNCTION: 'ArrowFunction',
|
|
100
|
+
VARIABLE_DECLARATION: 'VariableDeclaration',
|
|
101
|
+
PROPERTY_ASSIGNMENT: 'PropertyAssignment',
|
|
102
|
+
SHORTHAND_PROPERTY: 'ShorthandPropertyAssignment',
|
|
103
|
+
PARAMETER: 'Parameter'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
module.exports = {
|
|
107
|
+
ANALYTICS_PROVIDERS,
|
|
108
|
+
TS_NODE_KINDS
|
|
109
|
+
};
|