@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 +4 -0
- package/bin/cli.js +29 -1
- package/package.json +4 -2
- package/src/analyze/typescript/detectors/analytics-source.js +8 -3
- package/src/analyze/typescript/extractors/property-extractor.js +32 -0
- package/src/analyze/typescript/utils/function-finder.js +43 -18
- package/src/analyze/typescript/utils/type-resolver.js +16 -1
- package/src/index.js +7 -3
- package/src/utils/yamlGenerator.js +23 -4
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
|
🔑 **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
|
+
[](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
|
-
|
|
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
|
+
"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
|
-
|
|
48
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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 };
|