@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.
@@ -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 other analytics libraries
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;