@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
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const acorn = require('acorn');
|
|
3
|
-
const jsx = require('acorn-jsx');
|
|
4
|
-
const walk = require('acorn-walk');
|
|
5
|
-
const { extend } = require('acorn-jsx-walk');
|
|
6
|
-
const { detectSourceJs, findWrappingFunctionJs, extractJsProperties } = require('./helpers');
|
|
7
|
-
|
|
8
|
-
const parser = acorn.Parser.extend(jsx());
|
|
9
|
-
const parserOptions = { ecmaVersion: 'latest', sourceType: 'module', locations: true };
|
|
10
|
-
extend(walk.base);
|
|
11
|
-
|
|
12
|
-
function analyzeJsFile(filePath, customFunction) {
|
|
13
|
-
let events = [];
|
|
14
|
-
try {
|
|
15
|
-
const code = fs.readFileSync(filePath, 'utf8');
|
|
16
|
-
let ast;
|
|
17
|
-
try {
|
|
18
|
-
ast = parser.parse(code, parserOptions);
|
|
19
|
-
} catch (parseError) {
|
|
20
|
-
console.error(`Error parsing file ${filePath}`);
|
|
21
|
-
return events; // Return empty events array if parsing fails
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
walk.ancestor(ast, {
|
|
25
|
-
CallExpression(node, ancestors) {
|
|
26
|
-
try {
|
|
27
|
-
const source = detectSourceJs(node, customFunction);
|
|
28
|
-
if (source === 'unknown') return;
|
|
29
|
-
|
|
30
|
-
let eventName = null;
|
|
31
|
-
let propertiesNode = null;
|
|
32
|
-
|
|
33
|
-
if (source === 'googleanalytics' && node.arguments.length >= 3) {
|
|
34
|
-
eventName = node.arguments[1]?.value || null;
|
|
35
|
-
propertiesNode = node.arguments[2];
|
|
36
|
-
} else if (source === 'snowplow' && node.arguments.length >= 2) {
|
|
37
|
-
const actionProperty = node.arguments[1].properties.find(prop => prop.key.name === 'action');
|
|
38
|
-
eventName = actionProperty ? actionProperty.value.value : null;
|
|
39
|
-
propertiesNode = node.arguments[1];
|
|
40
|
-
} else if (node.arguments.length >= 2) {
|
|
41
|
-
eventName = node.arguments[0]?.value || null;
|
|
42
|
-
propertiesNode = node.arguments[1];
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const line = node.loc.start.line;
|
|
46
|
-
const functionName = findWrappingFunctionJs(node, ancestors);
|
|
47
|
-
|
|
48
|
-
if (eventName && propertiesNode && propertiesNode.type === 'ObjectExpression') {
|
|
49
|
-
const properties = extractJsProperties(propertiesNode);
|
|
50
|
-
|
|
51
|
-
events.push({
|
|
52
|
-
eventName,
|
|
53
|
-
source,
|
|
54
|
-
properties,
|
|
55
|
-
filePath,
|
|
56
|
-
line,
|
|
57
|
-
functionName
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
} catch (nodeError) {
|
|
61
|
-
console.error(`Error processing node in ${filePath}`);
|
|
62
|
-
}
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
} catch (fileError) {
|
|
66
|
-
console.error(`Error reading or processing file ${filePath}`);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return events;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
module.exports = { analyzeJsFile };
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
const path = require('path');
|
|
3
|
-
|
|
4
|
-
let pyodide = null;
|
|
5
|
-
|
|
6
|
-
async function initPyodide() {
|
|
7
|
-
if (!pyodide) {
|
|
8
|
-
const { loadPyodide } = await import('pyodide');
|
|
9
|
-
pyodide = await loadPyodide();
|
|
10
|
-
await pyodide.loadPackagesFromImports('import ast, json');
|
|
11
|
-
}
|
|
12
|
-
return pyodide;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function analyzePythonFile(filePath, customFunction) {
|
|
16
|
-
try {
|
|
17
|
-
const code = fs.readFileSync(filePath, 'utf8');
|
|
18
|
-
const py = await initPyodide();
|
|
19
|
-
|
|
20
|
-
// Read the Python analyzer code
|
|
21
|
-
const analyzerPath = path.join(__dirname, 'pythonTrackingAnalyzer.py');
|
|
22
|
-
const analyzerCode = fs.readFileSync(analyzerPath, 'utf8');
|
|
23
|
-
|
|
24
|
-
// Add file content and analyzer code to Python environment
|
|
25
|
-
py.globals.set('code', code);
|
|
26
|
-
py.globals.set('filepath', filePath);
|
|
27
|
-
py.globals.set('custom_function', customFunction || null);
|
|
28
|
-
|
|
29
|
-
// Run the Python analyzer
|
|
30
|
-
py.runPython(analyzerCode);
|
|
31
|
-
const result = py.runPython('analyze_python_code(code, filepath, custom_function)');
|
|
32
|
-
const events = JSON.parse(result);
|
|
33
|
-
|
|
34
|
-
return events;
|
|
35
|
-
} catch (error) {
|
|
36
|
-
console.error(`Error analyzing Python file ${filePath}:`, error);
|
|
37
|
-
return [];
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
module.exports = { analyzePythonFile };
|
|
@@ -1,409 +0,0 @@
|
|
|
1
|
-
const fs = require('fs');
|
|
2
|
-
|
|
3
|
-
let parse = null;
|
|
4
|
-
|
|
5
|
-
// Create a visitor to traverse the AST
|
|
6
|
-
class TrackingVisitor {
|
|
7
|
-
constructor(code, filePath, customFunction=null) {
|
|
8
|
-
this.code = code;
|
|
9
|
-
this.lines = code.split('\n');
|
|
10
|
-
this.ancestors = [];
|
|
11
|
-
this.events = [];
|
|
12
|
-
this.filePath = filePath;
|
|
13
|
-
this.customFunction = customFunction;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
getLineNumber(location) {
|
|
17
|
-
// Count the number of newlines before the start offset
|
|
18
|
-
const beforeStart = this.code.slice(0, location.startOffset);
|
|
19
|
-
return beforeStart.split('\n').length;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async findWrappingFunction(node, ancestors) {
|
|
23
|
-
const { DefNode, BlockNode, LambdaNode } = await import('@ruby/prism');
|
|
24
|
-
|
|
25
|
-
for (let i = ancestors.length - 1; i >= 0; i--) {
|
|
26
|
-
const current = ancestors[i];
|
|
27
|
-
|
|
28
|
-
// Handle method definitions
|
|
29
|
-
if (current instanceof DefNode) {
|
|
30
|
-
return current.name;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Handle blocks and lambdas
|
|
34
|
-
if (current instanceof BlockNode || current instanceof LambdaNode) {
|
|
35
|
-
return 'block';
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return 'global';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
detectSource(node) {
|
|
42
|
-
if (!node) return null;
|
|
43
|
-
|
|
44
|
-
// Check for analytics libraries
|
|
45
|
-
if (node.receiver) {
|
|
46
|
-
const objectName = node.receiver.name;
|
|
47
|
-
const methodName = node.name;
|
|
48
|
-
|
|
49
|
-
// Segment
|
|
50
|
-
if (objectName === 'Analytics' && methodName === 'track') return 'segment';
|
|
51
|
-
|
|
52
|
-
// Mixpanel (Ruby SDK uses Mixpanel::Tracker instance)
|
|
53
|
-
if (methodName === 'track' && node.receiver.type === 'CallNode' &&
|
|
54
|
-
node.receiver.name === 'tracker') return 'mixpanel';
|
|
55
|
-
|
|
56
|
-
// PostHog
|
|
57
|
-
if (objectName === 'posthog' && methodName === 'capture') return 'posthog';
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Snowplow (typically tracker.track_struct_event)
|
|
61
|
-
if (node.name === 'track_struct_event') return 'snowplow';
|
|
62
|
-
|
|
63
|
-
// Custom tracking function
|
|
64
|
-
if (this.customFunction && node.name === this.customFunction) return 'custom';
|
|
65
|
-
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
extractEventName(node, source) {
|
|
70
|
-
if (source === 'segment') {
|
|
71
|
-
const params = node.arguments_.arguments_[0].elements;
|
|
72
|
-
const eventProperty = params.find(param => param?.key?.unescaped?.value === 'event');
|
|
73
|
-
return eventProperty?.value?.unescaped?.value || null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (source === 'mixpanel') {
|
|
77
|
-
// Mixpanel Ruby SDK format: tracker.track('distinct_id', 'event_name', {...})
|
|
78
|
-
const args = node.arguments_.arguments_;
|
|
79
|
-
if (args && args.length > 1 && args[1]?.unescaped?.value) {
|
|
80
|
-
return args[1].unescaped.value;
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (source === 'posthog') {
|
|
85
|
-
// PostHog Ruby SDK format: posthog.capture({distinct_id: '...', event: '...', properties: {...}})
|
|
86
|
-
const hashArg = node.arguments_.arguments_[0];
|
|
87
|
-
if (hashArg && hashArg.elements) {
|
|
88
|
-
const eventProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'event');
|
|
89
|
-
return eventProperty?.value?.unescaped?.value || null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (source === 'snowplow') {
|
|
94
|
-
// Snowplow Ruby SDK: tracker.track_struct_event(category: '...', action: '...', ...)
|
|
95
|
-
const params = node.arguments_.arguments_[0].elements;
|
|
96
|
-
const actionProperty = params.find(param => param?.key?.unescaped?.value === 'action');
|
|
97
|
-
return actionProperty?.value?.unescaped?.value || null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (source === 'custom') {
|
|
101
|
-
// Custom function format: customFunction('event_name', {...})
|
|
102
|
-
const args = node.arguments_.arguments_;
|
|
103
|
-
if (args && args.length > 0 && args[0]?.unescaped?.value) {
|
|
104
|
-
return args[0].unescaped.value;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async extractProperties(node, source) {
|
|
112
|
-
const { HashNode, ArrayNode } = await import('@ruby/prism');
|
|
113
|
-
|
|
114
|
-
if (source === 'segment') {
|
|
115
|
-
const params = node.arguments_.arguments_[0].elements;
|
|
116
|
-
const properties = {};
|
|
117
|
-
|
|
118
|
-
// Process all top-level fields except 'event'
|
|
119
|
-
for (const param of params) {
|
|
120
|
-
const key = param?.key?.unescaped?.value;
|
|
121
|
-
|
|
122
|
-
if (key && key !== 'event') {
|
|
123
|
-
const value = param?.value;
|
|
124
|
-
|
|
125
|
-
if (key === 'properties' && value instanceof HashNode) {
|
|
126
|
-
// Merge properties from the 'properties' hash into the top level
|
|
127
|
-
const nestedProperties = await this.extractHashProperties(value);
|
|
128
|
-
Object.assign(properties, nestedProperties);
|
|
129
|
-
} else if (value instanceof HashNode) {
|
|
130
|
-
// Handle other nested hash objects
|
|
131
|
-
const hashProperties = await this.extractHashProperties(value);
|
|
132
|
-
properties[key] = {
|
|
133
|
-
type: 'object',
|
|
134
|
-
properties: hashProperties
|
|
135
|
-
};
|
|
136
|
-
} else if (value instanceof ArrayNode) {
|
|
137
|
-
// Handle arrays
|
|
138
|
-
const arrayItems = await this.extractArrayItemProperties(value);
|
|
139
|
-
properties[key] = {
|
|
140
|
-
type: 'array',
|
|
141
|
-
items: arrayItems
|
|
142
|
-
};
|
|
143
|
-
} else {
|
|
144
|
-
// Handle primitive values
|
|
145
|
-
const valueType = await this.getValueType(value);
|
|
146
|
-
properties[key] = {
|
|
147
|
-
type: valueType
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return properties;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
if (source === 'mixpanel') {
|
|
157
|
-
// Mixpanel Ruby SDK: tracker.track('distinct_id', 'event_name', {properties})
|
|
158
|
-
const args = node.arguments_.arguments_;
|
|
159
|
-
const properties = {};
|
|
160
|
-
|
|
161
|
-
// Add distinct_id as property
|
|
162
|
-
if (args && args.length > 0 && args[0]?.unescaped?.value) {
|
|
163
|
-
properties.distinct_id = {
|
|
164
|
-
type: 'string'
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Extract properties from third argument if it exists
|
|
169
|
-
if (args && args.length > 2 && args[2] instanceof HashNode) {
|
|
170
|
-
const propsHash = await this.extractHashProperties(args[2]);
|
|
171
|
-
Object.assign(properties, propsHash);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
return properties;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
if (source === 'posthog') {
|
|
178
|
-
// PostHog Ruby SDK: posthog.capture({distinct_id: '...', event: '...', properties: {...}})
|
|
179
|
-
const hashArg = node.arguments_.arguments_[0];
|
|
180
|
-
const properties = {};
|
|
181
|
-
|
|
182
|
-
if (hashArg && hashArg.elements) {
|
|
183
|
-
// Extract distinct_id if present
|
|
184
|
-
const distinctIdProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'distinct_id');
|
|
185
|
-
if (distinctIdProperty?.value) {
|
|
186
|
-
properties.distinct_id = {
|
|
187
|
-
type: await this.getValueType(distinctIdProperty.value)
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Extract properties
|
|
192
|
-
const propsProperty = hashArg.elements.find(elem => elem?.key?.unescaped?.value === 'properties');
|
|
193
|
-
if (propsProperty?.value instanceof HashNode) {
|
|
194
|
-
const props = await this.extractHashProperties(propsProperty.value);
|
|
195
|
-
Object.assign(properties, props);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
return properties;
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
if (source === 'snowplow') {
|
|
203
|
-
// Snowplow Ruby SDK: tracker.track_struct_event(category: '...', action: '...', ...)
|
|
204
|
-
const params = node.arguments_.arguments_[0].elements;
|
|
205
|
-
const properties = {};
|
|
206
|
-
|
|
207
|
-
// Extract all struct event parameters except 'action' (which is used as the event name)
|
|
208
|
-
for (const param of params) {
|
|
209
|
-
const key = param?.key?.unescaped?.value;
|
|
210
|
-
if (key && key !== 'action') {
|
|
211
|
-
properties[key] = {
|
|
212
|
-
type: await this.getValueType(param.value)
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return properties;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (source === 'custom') {
|
|
221
|
-
// Custom function format: customFunction('event_name', {properties})
|
|
222
|
-
const args = node.arguments_.arguments_;
|
|
223
|
-
if (args && args.length > 1 && args[1] instanceof HashNode) {
|
|
224
|
-
return await this.extractHashProperties(args[1]);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return null;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
async extractHashProperties(hashNode) {
|
|
232
|
-
const { AssocNode, HashNode, ArrayNode } = await import('@ruby/prism');
|
|
233
|
-
const properties = {};
|
|
234
|
-
|
|
235
|
-
for (const element of hashNode.elements) {
|
|
236
|
-
if (element instanceof AssocNode) {
|
|
237
|
-
const key = element.key.unescaped?.value;
|
|
238
|
-
const value = element.value;
|
|
239
|
-
|
|
240
|
-
if (key) {
|
|
241
|
-
if (value instanceof HashNode) {
|
|
242
|
-
// Handle nested hash objects
|
|
243
|
-
const nestedProperties = await this.extractHashProperties(value);
|
|
244
|
-
properties[key] = {
|
|
245
|
-
type: 'object',
|
|
246
|
-
properties: nestedProperties
|
|
247
|
-
};
|
|
248
|
-
} else if (value instanceof ArrayNode) {
|
|
249
|
-
// Handle arrays
|
|
250
|
-
const items = await this.extractArrayItemProperties(value);
|
|
251
|
-
properties[key] = {
|
|
252
|
-
type: 'array',
|
|
253
|
-
items
|
|
254
|
-
};
|
|
255
|
-
} else {
|
|
256
|
-
// Handle primitive values
|
|
257
|
-
const valueType = await this.getValueType(value);
|
|
258
|
-
properties[key] = {
|
|
259
|
-
type: valueType
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return properties;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async extractArrayItemProperties(arrayNode) {
|
|
270
|
-
const { HashNode } = await import('@ruby/prism');
|
|
271
|
-
|
|
272
|
-
if (arrayNode.elements.length === 0) {
|
|
273
|
-
return { type: 'any' };
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
const firstItem = arrayNode.elements[0];
|
|
277
|
-
if (firstItem instanceof HashNode) {
|
|
278
|
-
return {
|
|
279
|
-
type: 'object',
|
|
280
|
-
properties: this.extractHashProperties(firstItem)
|
|
281
|
-
};
|
|
282
|
-
} else {
|
|
283
|
-
const valueType = await this.getValueType(firstItem);
|
|
284
|
-
return {
|
|
285
|
-
type: valueType
|
|
286
|
-
};
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
async getValueType(node) {
|
|
291
|
-
const { StringNode, IntegerNode, FloatNode, TrueNode, FalseNode, NilNode, SymbolNode, CallNode } = await import('@ruby/prism');
|
|
292
|
-
|
|
293
|
-
if (node instanceof StringNode) return 'string';
|
|
294
|
-
if (node instanceof IntegerNode || node instanceof FloatNode) return 'number';
|
|
295
|
-
if (node instanceof TrueNode || node instanceof FalseNode) return 'boolean';
|
|
296
|
-
if (node instanceof NilNode) return 'null';
|
|
297
|
-
if (node instanceof SymbolNode) return 'string';
|
|
298
|
-
if (node instanceof CallNode) return 'any'; // Dynamic values
|
|
299
|
-
return 'any'; // Default type
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async visit(node) {
|
|
303
|
-
const { CallNode, ProgramNode, StatementsNode, DefNode, IfNode, BlockNode, ArgumentsNode, HashNode, AssocNode, ClassNode } = await import('@ruby/prism');
|
|
304
|
-
if (!node) return;
|
|
305
|
-
|
|
306
|
-
this.ancestors.push(node);
|
|
307
|
-
|
|
308
|
-
// Check if this is a tracking call
|
|
309
|
-
if (node instanceof CallNode) {
|
|
310
|
-
try {
|
|
311
|
-
const source = this.detectSource(node);
|
|
312
|
-
const eventName = this.extractEventName(node, source);
|
|
313
|
-
|
|
314
|
-
if (!source || !eventName) {
|
|
315
|
-
this.ancestors.pop();
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
const line = this.getLineNumber(node.location);
|
|
320
|
-
const functionName = await this.findWrappingFunction(node, this.ancestors);
|
|
321
|
-
const properties = await this.extractProperties(node, source);
|
|
322
|
-
|
|
323
|
-
this.events.push({
|
|
324
|
-
eventName,
|
|
325
|
-
source,
|
|
326
|
-
properties,
|
|
327
|
-
filePath: this.filePath,
|
|
328
|
-
line,
|
|
329
|
-
functionName
|
|
330
|
-
});
|
|
331
|
-
} catch (nodeError) {
|
|
332
|
-
console.error(`Error processing node in ${this.filePath}`);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// Visit all child nodes
|
|
337
|
-
if (node instanceof ProgramNode) {
|
|
338
|
-
await this.visit(node.statements);
|
|
339
|
-
} else if (node instanceof StatementsNode) {
|
|
340
|
-
for (const child of node.body) {
|
|
341
|
-
await this.visit(child);
|
|
342
|
-
}
|
|
343
|
-
} else if (node instanceof ClassNode) {
|
|
344
|
-
if (node.body) {
|
|
345
|
-
await this.visit(node.body);
|
|
346
|
-
}
|
|
347
|
-
} else if (node instanceof DefNode) {
|
|
348
|
-
if (node.body) {
|
|
349
|
-
await this.visit(node.body);
|
|
350
|
-
}
|
|
351
|
-
} else if (node instanceof IfNode) {
|
|
352
|
-
if (node.statements) {
|
|
353
|
-
await this.visit(node.statements);
|
|
354
|
-
}
|
|
355
|
-
if (node.subsequent) {
|
|
356
|
-
await this.visit(node.subsequent);
|
|
357
|
-
}
|
|
358
|
-
} else if (node instanceof BlockNode) {
|
|
359
|
-
if (node.body) {
|
|
360
|
-
await this.visit(node.body);
|
|
361
|
-
}
|
|
362
|
-
} else if (node instanceof ArgumentsNode) {
|
|
363
|
-
for (const arg of node.arguments) {
|
|
364
|
-
await this.visit(arg);
|
|
365
|
-
}
|
|
366
|
-
} else if (node instanceof HashNode) {
|
|
367
|
-
for (const element of node.elements) {
|
|
368
|
-
await this.visit(element);
|
|
369
|
-
}
|
|
370
|
-
} else if (node instanceof AssocNode) {
|
|
371
|
-
await this.visit(node.key);
|
|
372
|
-
await this.visit(node.value);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
this.ancestors.pop();
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
async function analyzeRubyFile(filePath, customFunction) {
|
|
380
|
-
// Lazy load the ruby prism parser
|
|
381
|
-
if (!parse) {
|
|
382
|
-
const { loadPrism } = await import('@ruby/prism');
|
|
383
|
-
parse = await loadPrism();
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
try {
|
|
387
|
-
const code = fs.readFileSync(filePath, 'utf8');
|
|
388
|
-
let ast;
|
|
389
|
-
try {
|
|
390
|
-
ast = await parse(code);
|
|
391
|
-
} catch (parseError) {
|
|
392
|
-
console.error(`Error parsing file ${filePath}`);
|
|
393
|
-
return []; // Return empty events array if parsing fails
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// Traverse the AST starting from the program node
|
|
397
|
-
const visitor = new TrackingVisitor(code, filePath, customFunction);
|
|
398
|
-
await visitor.visit(ast.value);
|
|
399
|
-
|
|
400
|
-
return visitor.events;
|
|
401
|
-
|
|
402
|
-
} catch (fileError) {
|
|
403
|
-
console.error(`Error reading or processing file ${filePath}`);
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return [];
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
module.exports = { analyzeRubyFile };
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
const ts = require('typescript');
|
|
2
|
-
const { detectSourceTs, findWrappingFunctionTs, extractTsProperties } = require('./helpers');
|
|
3
|
-
|
|
4
|
-
function analyzeTsFile(filePath, program, customFunction) {
|
|
5
|
-
let events = [];
|
|
6
|
-
try {
|
|
7
|
-
const sourceFile = program.getSourceFile(filePath);
|
|
8
|
-
if (!sourceFile) {
|
|
9
|
-
console.error(`Error: Unable to get source file for ${filePath}`);
|
|
10
|
-
return events;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
const checker = program.getTypeChecker();
|
|
14
|
-
|
|
15
|
-
function visit(node) {
|
|
16
|
-
try {
|
|
17
|
-
if (ts.isCallExpression(node)) {
|
|
18
|
-
const source = detectSourceTs(node, customFunction);
|
|
19
|
-
if (source === 'unknown') return;
|
|
20
|
-
|
|
21
|
-
let eventName = null;
|
|
22
|
-
let propertiesNode = null;
|
|
23
|
-
|
|
24
|
-
if (source === 'googleanalytics' && node.arguments.length >= 3) {
|
|
25
|
-
eventName = node.arguments[1]?.text || null;
|
|
26
|
-
propertiesNode = node.arguments[2];
|
|
27
|
-
} else if (source === 'snowplow' && node.arguments.length >= 2) {
|
|
28
|
-
const actionProperty = node.arguments[1].properties.find(prop => prop.name.escapedText === 'action');
|
|
29
|
-
eventName = actionProperty ? actionProperty.initializer.text : null;
|
|
30
|
-
propertiesNode = node.arguments[1];
|
|
31
|
-
} else if (node.arguments.length >= 2) {
|
|
32
|
-
eventName = node.arguments[0]?.text || null;
|
|
33
|
-
propertiesNode = node.arguments[1];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const line = sourceFile.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
37
|
-
const functionName = findWrappingFunctionTs(node);
|
|
38
|
-
|
|
39
|
-
if (eventName && propertiesNode && ts.isObjectLiteralExpression(propertiesNode)) {
|
|
40
|
-
try {
|
|
41
|
-
const properties = extractTsProperties(checker, propertiesNode);
|
|
42
|
-
events.push({
|
|
43
|
-
eventName,
|
|
44
|
-
source,
|
|
45
|
-
properties,
|
|
46
|
-
filePath,
|
|
47
|
-
line,
|
|
48
|
-
functionName
|
|
49
|
-
});
|
|
50
|
-
} catch (propertyError) {
|
|
51
|
-
console.error(`Error extracting properties in ${filePath} at line ${line}`);
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
ts.forEachChild(node, visit);
|
|
56
|
-
} catch (nodeError) {
|
|
57
|
-
console.error(`Error processing node in ${filePath}`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
ts.forEachChild(sourceFile, visit);
|
|
62
|
-
} catch (fileError) {
|
|
63
|
-
console.error(`Error analyzing TypeScript file ${filePath}`);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
return events;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
module.exports = { analyzeTsFile };
|