@flisk/analyze-tracking 0.7.3 → 0.7.4

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 CHANGED
@@ -29,6 +29,8 @@ npx @flisk/analyze-tracking /path/to/project [options]
29
29
  - `-m, --model <model>`: Specify a model (ex: `gpt-4.1-nano`, `gpt-4o-mini`, `gemini-2.0-flash-lite-001`)
30
30
  - `-o, --output <output_file>`: Name of the output file (default: `tracking-schema.yaml`)
31
31
  - `-c, --customFunction <function_name>`: Specify a custom tracking function
32
+ - `--format <format>`: Output format, either `yaml` (default) or `json`. If an invalid value is provided, the CLI will exit with an error.
33
+ - `--stdout`: Print the output to the terminal instead of writing to a file (works with both YAML and JSON)
32
34
 
33
35
  🔑&nbsp; **Important:** If you are using `generateDescription`, you must set the appropriate credentials for the LLM provider you are using as an environment variable. OpenAI uses `OPENAI_API_KEY` and Google Vertex AI uses `GOOGLE_APPLICATION_CREDENTIALS`.
34
36
 
@@ -416,3 +418,5 @@ See [schema.json](schema.json) for a JSON Schema of the output.
416
418
 
417
419
  ## Contribute
418
420
  We're actively improving this package. Found a bug? Have a feature request? Open an issue or submit a pull request!
421
+
422
+ [![Slack](https://img.shields.io/badge/Join%20Us%20on%20Slack-Flisk%20Community-611f69.svg?logo=slack)](https://join.slack.com/t/fliskcommunity/shared_invite/zt-354hesfnm-BbNzveERo9C4JwVQEWvXoA)
package/bin/cli.js CHANGED
@@ -64,6 +64,17 @@ const optionDefinitions = [
64
64
  alias: 'h',
65
65
  type: Boolean,
66
66
  },
67
+ {
68
+ name: 'stdout',
69
+ type: Boolean,
70
+ defaultValue: false,
71
+ },
72
+ {
73
+ name: 'format',
74
+ alias: 'f',
75
+ type: String,
76
+ defaultValue: 'yaml',
77
+ },
67
78
  ]
68
79
  const options = commandLineArgs(optionDefinitions);
69
80
  const {
@@ -77,6 +88,8 @@ const {
77
88
  commitHash,
78
89
  commitTimestamp,
79
90
  help,
91
+ stdout,
92
+ format,
80
93
  } = options;
81
94
 
82
95
  if (help) {
@@ -113,4 +126,19 @@ if (generateDescription) {
113
126
  }
114
127
  }
115
128
 
116
- run(path.resolve(targetDir), output, customFunction, customSourceDetails, generateDescription, provider, model);
129
+ if (format !== 'yaml' && format !== 'json') {
130
+ console.error(`Invalid format: ${format}. Please use --format yaml or --format json.`);
131
+ process.exit(1);
132
+ }
133
+
134
+ run(
135
+ path.resolve(targetDir),
136
+ output,
137
+ customFunction,
138
+ customSourceDetails,
139
+ generateDescription,
140
+ provider,
141
+ model,
142
+ stdout,
143
+ format
144
+ );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk/analyze-tracking",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Analyzes tracking code in a project and generates data schemas",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -53,6 +53,8 @@
53
53
  },
54
54
  "devDependencies": {
55
55
  "ajv": "^8.17.1",
56
- "lodash": "^4.17.21"
56
+ "lodash": "^4.17.21",
57
+ "react": "^19.1.0",
58
+ "@types/react": "^19.1.6"
57
59
  }
58
60
  }
@@ -44,8 +44,13 @@ function detectAnalyticsSource(node, customFunction) {
44
44
  * @returns {boolean}
45
45
  */
46
46
  function isCustomFunction(node, customFunction) {
47
- return ts.isIdentifier(node.expression) &&
48
- node.expression.escapedText === customFunction;
47
+ const canBeCustomFunction = ts.isIdentifier(node.expression) ||
48
+ ts.isPropertyAccessExpression(node.expression) ||
49
+ ts.isCallExpression(node.expression) || // For chained calls like getTracker().track()
50
+ ts.isElementAccessExpression(node.expression) || // For array/object access like trackers['analytics'].track()
51
+ (ts.isPropertyAccessExpression(node.expression?.expression) && ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
52
+
53
+ return canBeCustomFunction && node.expression.getText() === customFunction;
49
54
  }
50
55
 
51
56
  /**
@@ -59,7 +64,7 @@ function detectFunctionBasedProvider(node) {
59
64
  }
60
65
 
61
66
  const functionName = node.expression.escapedText;
62
-
67
+
63
68
  for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
64
69
  if (provider.type === 'function' && provider.functionName === functionName) {
65
70
  return provider.name;
@@ -116,6 +116,38 @@ function extractShorthandPropertySchema(checker, prop) {
116
116
  if (!symbol) {
117
117
  return { type: 'any' };
118
118
  }
119
+ const declarations = symbol.declarations || [];
120
+ for (const decl of declarations) {
121
+ // Detect destructuring from useState: const [state, setState] = useState<Type>(...)
122
+ if (
123
+ ts.isBindingElement(decl) &&
124
+ decl.parent &&
125
+ ts.isArrayBindingPattern(decl.parent) &&
126
+ decl.parent.parent &&
127
+ ts.isVariableDeclaration(decl.parent.parent) &&
128
+ decl.parent.parent.initializer &&
129
+ ts.isCallExpression(decl.parent.parent.initializer) &&
130
+ ts.isIdentifier(decl.parent.parent.initializer.expression) &&
131
+ decl.parent.parent.initializer.expression.escapedText === 'useState'
132
+ ) {
133
+ // Try to get type from generic argument
134
+ const callExpr = decl.parent.parent.initializer;
135
+ if (callExpr.typeArguments && callExpr.typeArguments.length > 0) {
136
+ const typeNode = callExpr.typeArguments[0];
137
+ const type = checker.getTypeFromTypeNode(typeNode);
138
+ const typeString = checker.typeToString(type);
139
+ return resolveTypeToProperties(checker, typeString);
140
+ }
141
+ // Fallback: get type from initial value
142
+ if (callExpr.arguments && callExpr.arguments.length > 0) {
143
+ const initType = checker.getTypeAtLocation(callExpr.arguments[0]);
144
+ const typeString = checker.typeToString(initType);
145
+ return resolveTypeToProperties(checker, typeString);
146
+ }
147
+ // Default to any
148
+ return { type: 'any' };
149
+ }
150
+ }
119
151
 
120
152
  const propType = checker.getTypeAtLocation(prop.name);
121
153
  const typeString = checker.typeToString(propType);
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  const ts = require('typescript');
7
+ const { isReactHookCall } = require('./type-resolver');
7
8
 
8
9
  /**
9
10
  * Finds the name of the function that wraps a given node
@@ -12,17 +13,17 @@ const ts = require('typescript');
12
13
  */
13
14
  function findWrappingFunction(node) {
14
15
  let current = node;
15
-
16
+
16
17
  while (current) {
17
18
  const functionName = extractFunctionName(current);
18
-
19
+
19
20
  if (functionName) {
20
21
  return functionName;
21
22
  }
22
-
23
+
23
24
  current = current.parent;
24
25
  }
25
-
26
+
26
27
  return 'global';
27
28
  }
28
29
 
@@ -36,27 +37,27 @@ function extractFunctionName(node) {
36
37
  if (ts.isFunctionDeclaration(node)) {
37
38
  return node.name ? node.name.escapedText : 'anonymous';
38
39
  }
39
-
40
+
40
41
  // Method declaration in class
41
42
  if (ts.isMethodDeclaration(node)) {
42
43
  return node.name ? node.name.escapedText : 'anonymous';
43
44
  }
44
-
45
+
45
46
  // Arrow function or function expression
46
47
  if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
47
48
  return findParentFunctionName(node) || 'anonymous';
48
49
  }
49
-
50
+
50
51
  // Constructor
51
52
  if (ts.isConstructorDeclaration(node)) {
52
53
  return 'constructor';
53
54
  }
54
-
55
+
55
56
  // Getter/Setter
56
57
  if (ts.isGetAccessorDeclaration(node) || ts.isSetAccessorDeclaration(node)) {
57
58
  return node.name ? `${ts.isGetAccessorDeclaration(node) ? 'get' : 'set'} ${node.name.escapedText}` : 'anonymous';
58
59
  }
59
-
60
+
60
61
  return null;
61
62
  }
62
63
 
@@ -67,14 +68,38 @@ function extractFunctionName(node) {
67
68
  */
68
69
  function findParentFunctionName(node) {
69
70
  const parent = node.parent;
70
-
71
+
71
72
  if (!parent) return null;
72
-
73
+
74
+ if (
75
+ ts.isCallExpression(parent) &&
76
+ ts.isIdentifier(parent.expression) &&
77
+ isReactHookCall(parent)
78
+ ) {
79
+ if (
80
+ parent.parent &&
81
+ ts.isVariableDeclaration(parent.parent) &&
82
+ parent.parent.name
83
+ ) {
84
+ return `${parent.expression.escapedText}(${parent.parent.name.escapedText})`;
85
+ }
86
+ return `${parent.expression.escapedText}()`;
87
+ }
88
+
73
89
  // Variable declaration: const myFunc = () => {}
74
90
  if (ts.isVariableDeclaration(parent) && parent.name) {
91
+ // Check if initializer is a recognized React hook call
92
+ if (
93
+ parent.initializer &&
94
+ ts.isCallExpression(parent.initializer) &&
95
+ ts.isIdentifier(parent.initializer.expression) &&
96
+ REACT_HOOKS.has(parent.initializer.expression.escapedText)
97
+ ) {
98
+ return `${parent.initializer.expression.escapedText}(${parent.name.escapedText})`;
99
+ }
75
100
  return parent.name.escapedText;
76
101
  }
77
-
102
+
78
103
  // Property assignment: { myFunc: () => {} }
79
104
  if (ts.isPropertyAssignment(parent) && parent.name) {
80
105
  if (ts.isIdentifier(parent.name)) {
@@ -84,20 +109,20 @@ function findParentFunctionName(node) {
84
109
  return parent.name.text;
85
110
  }
86
111
  }
87
-
112
+
88
113
  // Method property in object literal: { myFunc() {} }
89
114
  if (ts.isMethodDeclaration(parent) && parent.name) {
90
115
  return parent.name.escapedText;
91
116
  }
92
-
117
+
93
118
  // Binary expression assignment: obj.myFunc = () => {}
94
- if (ts.isBinaryExpression(parent) &&
95
- parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
119
+ if (ts.isBinaryExpression(parent) &&
120
+ parent.operatorToken.kind === ts.SyntaxKind.EqualsToken) {
96
121
  if (ts.isPropertyAccessExpression(parent.left)) {
97
122
  return parent.left.name.escapedText;
98
123
  }
99
124
  }
100
-
125
+
101
126
  // Call expression argument: someFunc(() => {})
102
127
  if (ts.isCallExpression(parent)) {
103
128
  const argIndex = parent.arguments.indexOf(node);
@@ -105,7 +130,7 @@ function findParentFunctionName(node) {
105
130
  return `anonymous-callback-${argIndex}`;
106
131
  }
107
132
  }
108
-
133
+
109
134
  return null;
110
135
  }
111
136
 
@@ -184,10 +184,25 @@ function getBasicTypeOfArrayElement(checker, element) {
184
184
  return 'any';
185
185
  }
186
186
 
187
+ /**
188
+ * Checks if a CallExpression is a React hook (useCallback, useState, etc)
189
+ * @param {Object} node - CallExpression node
190
+ * @param {string[]} hookNames - List of hook names to check
191
+ * @returns {boolean}
192
+ */
193
+ function isReactHookCall(node, hookNames = ['useCallback', 'useState', 'useEffect', 'useMemo', 'useReducer']) {
194
+ if (!node || !node.expression) return false;
195
+ if (ts.isIdentifier(node.expression)) {
196
+ return hookNames.includes(node.expression.escapedText);
197
+ }
198
+ return false;
199
+ }
200
+
187
201
  module.exports = {
188
202
  resolveIdentifierToInitializer,
189
203
  getTypeOfNode,
190
204
  resolveTypeToProperties,
191
205
  isCustomType,
192
- getBasicTypeOfArrayElement
206
+ getBasicTypeOfArrayElement,
207
+ isReactHookCall
193
208
  };
package/src/index.js CHANGED
@@ -5,13 +5,13 @@
5
5
 
6
6
  const { analyzeDirectory } = require('./analyze');
7
7
  const { getRepoDetails } = require('./utils/repoDetails');
8
- const { generateYamlSchema } = require('./utils/yamlGenerator');
8
+ const { generateYamlSchema, generateJsonSchema } = require('./utils/yamlGenerator');
9
9
  const { generateDescriptions } = require('./generateDescriptions');
10
10
 
11
11
  const { ChatOpenAI } = require('@langchain/openai');
12
12
  const { ChatVertexAI } = require('@langchain/google-vertexai');
13
13
 
14
- async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model) {
14
+ async function run(targetDir, outputPath, customFunction, customSourceDetails, generateDescription, provider, model, stdout, format) {
15
15
  let events = await analyzeDirectory(targetDir, customFunction);
16
16
  if (generateDescription) {
17
17
  let llm;
@@ -35,7 +35,11 @@ async function run(targetDir, outputPath, customFunction, customSourceDetails, g
35
35
  events = await generateDescriptions(events, targetDir, llm);
36
36
  }
37
37
  const repoDetails = await getRepoDetails(targetDir, customSourceDetails);
38
- generateYamlSchema(events, repoDetails, outputPath);
38
+ if (format === 'json') {
39
+ generateJsonSchema(events, repoDetails, outputPath, stdout);
40
+ } else {
41
+ generateYamlSchema(events, repoDetails, outputPath, stdout);
42
+ }
39
43
  }
40
44
 
41
45
  module.exports = { run };
@@ -9,7 +9,7 @@ const yaml = require('js-yaml');
9
9
  const VERSION = 1
10
10
  const SCHEMA_URL = "https://raw.githubusercontent.com/fliskdata/analyze-tracking/main/schema.json";
11
11
 
12
- function generateYamlSchema(events, repository, outputPath) {
12
+ function generateYamlSchema(events, repository, outputPath, stdout = false) {
13
13
  const schema = {
14
14
  version: VERSION,
15
15
  source: repository,
@@ -21,8 +21,27 @@ function generateYamlSchema(events, repository, outputPath) {
21
21
  };
22
22
  const yamlOutput = yaml.dump(schema, options);
23
23
  const yamlFile = `# yaml-language-server: $schema=${SCHEMA_URL}\n${yamlOutput}`;
24
- fs.writeFileSync(outputPath, yamlFile, 'utf8');
25
- console.log(`Tracking schema YAML file generated: ${outputPath}`);
24
+ if (stdout) {
25
+ process.stdout.write(yamlFile);
26
+ } else {
27
+ fs.writeFileSync(outputPath, yamlFile, 'utf8');
28
+ console.log(`Tracking schema YAML file generated: ${outputPath}`);
29
+ }
26
30
  }
27
31
 
28
- module.exports = { generateYamlSchema };
32
+ function generateJsonSchema(events, repository, outputPath, stdout = false) {
33
+ const schema = {
34
+ version: VERSION,
35
+ source: repository,
36
+ events,
37
+ };
38
+ const jsonFile = JSON.stringify(schema, null, 2);
39
+ if (stdout) {
40
+ process.stdout.write(jsonFile);
41
+ } else {
42
+ fs.writeFileSync(outputPath, jsonFile, 'utf8');
43
+ console.log(`Tracking schema JSON file generated: ${outputPath}`);
44
+ }
45
+ }
46
+
47
+ module.exports = { generateYamlSchema, generateJsonSchema };