@flisk/analyze-tracking 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +258 -32
- package/package.json +2 -1
- package/src/analyze/analyzeGoFile.js +968 -0
- package/src/analyze/analyzePythonFile.js +41 -0
- package/src/analyze/analyzeRubyFile.js +123 -4
- package/src/analyze/go2json.js +1069 -0
- package/src/analyze/index.js +9 -1
- package/src/analyze/pythonTrackingAnalyzer.py +439 -0
|
@@ -0,0 +1,41 @@
|
|
|
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 };
|
|
@@ -4,12 +4,13 @@ let parse = null;
|
|
|
4
4
|
|
|
5
5
|
// Create a visitor to traverse the AST
|
|
6
6
|
class TrackingVisitor {
|
|
7
|
-
constructor(code, filePath) {
|
|
7
|
+
constructor(code, filePath, customFunction=null) {
|
|
8
8
|
this.code = code;
|
|
9
9
|
this.lines = code.split('\n');
|
|
10
10
|
this.ancestors = [];
|
|
11
11
|
this.events = [];
|
|
12
12
|
this.filePath = filePath;
|
|
13
|
+
this.customFunction = customFunction;
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
getLineNumber(location) {
|
|
@@ -40,13 +41,27 @@ class TrackingVisitor {
|
|
|
40
41
|
detectSource(node) {
|
|
41
42
|
if (!node) return null;
|
|
42
43
|
|
|
43
|
-
// Check for
|
|
44
|
+
// Check for analytics libraries
|
|
44
45
|
if (node.receiver) {
|
|
45
46
|
const objectName = node.receiver.name;
|
|
46
47
|
const methodName = node.name;
|
|
47
48
|
|
|
49
|
+
// Segment
|
|
48
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';
|
|
49
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';
|
|
50
65
|
|
|
51
66
|
return null;
|
|
52
67
|
}
|
|
@@ -58,6 +73,38 @@ class TrackingVisitor {
|
|
|
58
73
|
return eventProperty?.value?.unescaped?.value || null;
|
|
59
74
|
}
|
|
60
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
|
+
|
|
61
108
|
return null;
|
|
62
109
|
}
|
|
63
110
|
|
|
@@ -106,6 +153,78 @@ class TrackingVisitor {
|
|
|
106
153
|
return properties;
|
|
107
154
|
}
|
|
108
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
|
+
|
|
109
228
|
return null;
|
|
110
229
|
}
|
|
111
230
|
|
|
@@ -257,7 +376,7 @@ class TrackingVisitor {
|
|
|
257
376
|
}
|
|
258
377
|
}
|
|
259
378
|
|
|
260
|
-
async function analyzeRubyFile(filePath) {
|
|
379
|
+
async function analyzeRubyFile(filePath, customFunction) {
|
|
261
380
|
// Lazy load the ruby prism parser
|
|
262
381
|
if (!parse) {
|
|
263
382
|
const { loadPrism } = await import('@ruby/prism');
|
|
@@ -275,7 +394,7 @@ async function analyzeRubyFile(filePath) {
|
|
|
275
394
|
}
|
|
276
395
|
|
|
277
396
|
// Traverse the AST starting from the program node
|
|
278
|
-
const visitor = new TrackingVisitor(code, filePath);
|
|
397
|
+
const visitor = new TrackingVisitor(code, filePath, customFunction);
|
|
279
398
|
await visitor.visit(ast.value);
|
|
280
399
|
|
|
281
400
|
return visitor.events;
|