@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
@@ -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 };