@flisk/analyze-tracking 0.9.0 → 0.9.1
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 +36 -0
- package/package.json +1 -1
- package/schema.json +10 -0
- package/src/analyze/javascript/detectors/analytics-source.js +38 -4
- package/src/analyze/javascript/extractors/event-extractor.js +29 -12
- package/src/analyze/javascript/parser.js +26 -8
- package/src/analyze/typescript/detectors/analytics-source.js +37 -3
- package/src/analyze/typescript/extractors/event-extractor.js +59 -31
- package/src/analyze/typescript/extractors/property-extractor.js +129 -91
- package/src/analyze/typescript/parser.js +27 -8
- package/src/analyze/typescript/utils/type-resolver.js +600 -21
- package/src/analyze/utils/customFunctionParser.js +29 -0
package/README.md
CHANGED
|
@@ -39,6 +39,8 @@ npx @flisk/analyze-tracking /path/to/project [options]
|
|
|
39
39
|
|
|
40
40
|
If you have your own in-house tracker or a wrapper function that calls other tracking libraries, you can specify the function signature with the `-c` or `--customFunction` option.
|
|
41
41
|
|
|
42
|
+
#### Standard Custom Function Format
|
|
43
|
+
|
|
42
44
|
Your function signature should be in the following format:
|
|
43
45
|
```js
|
|
44
46
|
yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES, customFieldOne, customFieldTwo)
|
|
@@ -57,11 +59,45 @@ yourCustomTrackFunctionName(userId, EVENT_NAME, PROPERTIES)
|
|
|
57
59
|
|
|
58
60
|
If your function follows the standard format `yourCustomTrackFunctionName(EVENT_NAME, PROPERTIES)`, you can simply pass in `yourCustomTrackFunctionName` to `--customFunction` as a shorthand.
|
|
59
61
|
|
|
62
|
+
#### Method-Name-as-Event Format
|
|
63
|
+
|
|
64
|
+
For tracking patterns where the method name itself is the event name (e.g., `yourClass.yourEventName({...})`), use the special `EVENT_NAME` placeholder in the method position:
|
|
65
|
+
|
|
66
|
+
```js
|
|
67
|
+
yourClass.EVENT_NAME(PROPERTIES)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
This pattern tells the analyzer that:
|
|
71
|
+
- `yourClass` is the object name to match
|
|
72
|
+
- The method name after the dot (e.g., `viewItemList`, `addToCart`) is the event name
|
|
73
|
+
- `PROPERTIES` is the properties object (defaults to the first argument if not specified)
|
|
74
|
+
|
|
75
|
+
**Example:**
|
|
76
|
+
```typescript
|
|
77
|
+
// Code in your project:
|
|
78
|
+
yourClass.viewItemList({ items: [...] });
|
|
79
|
+
yourClass.addToCart({ item: {...}, value: 100 });
|
|
80
|
+
yourClass.purchase({ userId: '123', value: 100 });
|
|
81
|
+
|
|
82
|
+
// Command:
|
|
83
|
+
npx @flisk/analyze-tracking /path/to/project --customFunction "yourClass.EVENT_NAME(PROPERTIES)"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
This will detect:
|
|
87
|
+
- Event: `viewItemList` with properties from the first argument
|
|
88
|
+
- Event: `addToCart` with properties from the first argument
|
|
89
|
+
- Event: `purchase` with properties from the first argument
|
|
90
|
+
|
|
91
|
+
_**Note:** This pattern is currently only supported for JavaScript and TypeScript code._
|
|
92
|
+
|
|
93
|
+
#### Multiple Custom Functions
|
|
94
|
+
|
|
60
95
|
You can also pass in multiple custom function signatures by passing in the `--customFunction` option multiple times or by passing in a space-separated list of function signatures.
|
|
61
96
|
|
|
62
97
|
```sh
|
|
63
98
|
npx @flisk/analyze-tracking /path/to/project --customFunction "yourFunc1" --customFunction "yourFunc2(userId, EVENT_NAME, PROPERTIES)"
|
|
64
99
|
npx @flisk/analyze-tracking /path/to/project -c "yourFunc1" "yourFunc2(userId, EVENT_NAME, PROPERTIES)"
|
|
100
|
+
npx @flisk/analyze-tracking /path/to/project -c "yourClass.EVENT_NAME(PROPERTIES)" "customTrack(EVENT_NAME, PROPERTIES)"
|
|
65
101
|
```
|
|
66
102
|
|
|
67
103
|
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -142,6 +142,16 @@
|
|
|
142
142
|
"items": {
|
|
143
143
|
"$ref": "#/definitions/property",
|
|
144
144
|
"description": "Schema for array items when type is 'array'"
|
|
145
|
+
},
|
|
146
|
+
"values": {
|
|
147
|
+
"type": "array",
|
|
148
|
+
"items": {
|
|
149
|
+
"oneOf": [
|
|
150
|
+
{ "type": "string" },
|
|
151
|
+
{ "type": "number" }
|
|
152
|
+
]
|
|
153
|
+
},
|
|
154
|
+
"description": "Possible values when type is 'enum'"
|
|
145
155
|
}
|
|
146
156
|
},
|
|
147
157
|
"required": [
|
|
@@ -8,16 +8,25 @@ const { ANALYTICS_PROVIDERS, NODE_TYPES } = require('../constants');
|
|
|
8
8
|
/**
|
|
9
9
|
* Detects the analytics provider from a CallExpression node
|
|
10
10
|
* @param {Object} node - AST CallExpression node
|
|
11
|
-
* @param {string} [
|
|
11
|
+
* @param {string|Object} [customFunctionOrConfig] - Custom function name string or custom config object
|
|
12
12
|
* @returns {string} The detected analytics source or 'unknown'
|
|
13
13
|
*/
|
|
14
|
-
function detectAnalyticsSource(node,
|
|
14
|
+
function detectAnalyticsSource(node, customFunctionOrConfig) {
|
|
15
15
|
if (!node.callee) {
|
|
16
16
|
return 'unknown';
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
// Check for custom function first
|
|
20
|
-
|
|
20
|
+
// Support both old string format and new config object format
|
|
21
|
+
const customConfig = typeof customFunctionOrConfig === 'object' ? customFunctionOrConfig : null;
|
|
22
|
+
const customFunction = typeof customFunctionOrConfig === 'string' ? customFunctionOrConfig : (customConfig?.functionName);
|
|
23
|
+
|
|
24
|
+
if (customConfig?.isMethodAsEvent) {
|
|
25
|
+
// Method-as-event pattern: match any method on the specified object
|
|
26
|
+
if (isMethodAsEventFunction(node, customConfig)) {
|
|
27
|
+
return 'custom';
|
|
28
|
+
}
|
|
29
|
+
} else if (customFunction && isCustomFunction(node, customFunction)) {
|
|
21
30
|
return 'custom';
|
|
22
31
|
}
|
|
23
32
|
|
|
@@ -36,6 +45,31 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
36
45
|
return 'unknown';
|
|
37
46
|
}
|
|
38
47
|
|
|
48
|
+
/**
|
|
49
|
+
* Checks if the node matches a method-as-event custom function pattern
|
|
50
|
+
* @param {Object} node - AST CallExpression node
|
|
51
|
+
* @param {Object} customConfig - Custom function configuration with isMethodAsEvent: true
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
function isMethodAsEventFunction(node, customConfig) {
|
|
55
|
+
if (!customConfig?.isMethodAsEvent || !customConfig?.objectName) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Must be a MemberExpression: objectName.methodName(...)
|
|
60
|
+
if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The object part must match the configured objectName
|
|
65
|
+
const objectNode = node.callee.object;
|
|
66
|
+
if (objectNode.type !== NODE_TYPES.IDENTIFIER) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return objectNode.name === customConfig.objectName;
|
|
71
|
+
}
|
|
72
|
+
|
|
39
73
|
/**
|
|
40
74
|
* Checks if the node is a custom function call
|
|
41
75
|
* @param {Object} node - AST CallExpression node
|
|
@@ -122,7 +156,7 @@ function detectFunctionBasedProvider(node) {
|
|
|
122
156
|
}
|
|
123
157
|
|
|
124
158
|
const functionName = node.callee.name;
|
|
125
|
-
|
|
159
|
+
|
|
126
160
|
for (const provider of Object.values(ANALYTICS_PROVIDERS)) {
|
|
127
161
|
if (provider.type === 'function' && provider.functionName === functionName) {
|
|
128
162
|
return provider.name;
|
|
@@ -72,15 +72,15 @@ function extractSnowplowEvent(node, constantMap) {
|
|
|
72
72
|
|
|
73
73
|
// tracker.track(buildStructEvent({ action: 'event_name', ... }))
|
|
74
74
|
const firstArg = node.arguments[0];
|
|
75
|
-
|
|
76
|
-
if (firstArg.type === NODE_TYPES.CALL_EXPRESSION &&
|
|
75
|
+
|
|
76
|
+
if (firstArg.type === NODE_TYPES.CALL_EXPRESSION &&
|
|
77
77
|
firstArg.arguments.length > 0) {
|
|
78
78
|
const structEventArg = firstArg.arguments[0];
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
if (structEventArg.type === NODE_TYPES.OBJECT_EXPRESSION) {
|
|
81
81
|
const actionProperty = findPropertyByKey(structEventArg, 'action');
|
|
82
82
|
const eventName = actionProperty ? getStringValue(actionProperty.value, constantMap) : null;
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
return { eventName, propertiesNode: structEventArg };
|
|
85
85
|
}
|
|
86
86
|
}
|
|
@@ -119,7 +119,7 @@ function extractGTMEvent(node, constantMap) {
|
|
|
119
119
|
|
|
120
120
|
// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
|
|
121
121
|
const firstArg = node.arguments[0];
|
|
122
|
-
|
|
122
|
+
|
|
123
123
|
if (firstArg.type !== NODE_TYPES.OBJECT_EXPRESSION) {
|
|
124
124
|
return { eventName: null, propertiesNode: null };
|
|
125
125
|
}
|
|
@@ -131,11 +131,11 @@ function extractGTMEvent(node, constantMap) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
const eventName = getStringValue(eventProperty.value, constantMap);
|
|
134
|
-
|
|
134
|
+
|
|
135
135
|
// Create a modified properties node without the 'event' property
|
|
136
136
|
const modifiedPropertiesNode = {
|
|
137
137
|
...firstArg,
|
|
138
|
-
properties: firstArg.properties.filter(prop =>
|
|
138
|
+
properties: firstArg.properties.filter(prop =>
|
|
139
139
|
prop.key && (prop.key.name !== 'event' && prop.key.value !== 'event')
|
|
140
140
|
)
|
|
141
141
|
};
|
|
@@ -171,10 +171,27 @@ function extractDefaultEvent(node, constantMap) {
|
|
|
171
171
|
function extractCustomEvent(node, constantMap, customConfig) {
|
|
172
172
|
const args = node.arguments || [];
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
let eventName;
|
|
175
|
+
let propertiesArg;
|
|
176
|
+
|
|
177
|
+
if (customConfig?.isMethodAsEvent) {
|
|
178
|
+
// Method-as-event pattern: event name comes from the method name
|
|
179
|
+
if (node.callee.type === NODE_TYPES.MEMBER_EXPRESSION &&
|
|
180
|
+
node.callee.property.type === NODE_TYPES.IDENTIFIER) {
|
|
181
|
+
eventName = node.callee.property.name;
|
|
182
|
+
} else {
|
|
183
|
+
// Fallback: could not extract method name
|
|
184
|
+
eventName = null;
|
|
185
|
+
}
|
|
176
186
|
|
|
177
|
-
|
|
187
|
+
// Properties are at the configured index (default 0)
|
|
188
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 0];
|
|
189
|
+
} else {
|
|
190
|
+
// Standard custom function pattern: event name comes from argument
|
|
191
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
192
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
193
|
+
eventName = getStringValue(eventArg, constantMap);
|
|
194
|
+
}
|
|
178
195
|
|
|
179
196
|
const extraArgs = {};
|
|
180
197
|
if (customConfig && customConfig.extraParams) {
|
|
@@ -274,8 +291,8 @@ function getStringValue(node, constantMap = {}) {
|
|
|
274
291
|
*/
|
|
275
292
|
function findPropertyByKey(objectNode, key) {
|
|
276
293
|
if (!objectNode.properties) return null;
|
|
277
|
-
|
|
278
|
-
return objectNode.properties.find(prop =>
|
|
294
|
+
|
|
295
|
+
return objectNode.properties.find(prop =>
|
|
279
296
|
prop.key && (prop.key.name === key || prop.key.value === key)
|
|
280
297
|
);
|
|
281
298
|
}
|
|
@@ -53,7 +53,7 @@ class ParseError extends Error {
|
|
|
53
53
|
*/
|
|
54
54
|
function parseFile(filePath) {
|
|
55
55
|
let code;
|
|
56
|
-
|
|
56
|
+
|
|
57
57
|
try {
|
|
58
58
|
code = fs.readFileSync(filePath, 'utf8');
|
|
59
59
|
} catch (error) {
|
|
@@ -72,16 +72,33 @@ function parseFile(filePath) {
|
|
|
72
72
|
// ---------------------------------------------
|
|
73
73
|
|
|
74
74
|
/**
|
|
75
|
-
* Determines whether a CallExpression node matches the provided custom function
|
|
76
|
-
* Supports both simple identifiers (e.g. myTrack)
|
|
75
|
+
* Determines whether a CallExpression node matches the provided custom function configuration.
|
|
76
|
+
* Supports both simple identifiers (e.g. myTrack), dot-separated members (e.g. Custom.track),
|
|
77
|
+
* and method-as-event patterns (e.g. eventCalls.EVENT_NAME).
|
|
77
78
|
* The logic mirrors isCustomFunction from detectors/analytics-source.js but is kept local to avoid
|
|
78
79
|
* circular dependencies.
|
|
79
80
|
* @param {Object} node – CallExpression AST node
|
|
80
|
-
* @param {
|
|
81
|
+
* @param {Object} customConfig – Custom function configuration object
|
|
81
82
|
* @returns {boolean}
|
|
82
83
|
*/
|
|
83
|
-
function nodeMatchesCustomFunction(node,
|
|
84
|
-
if (!
|
|
84
|
+
function nodeMatchesCustomFunction(node, customConfig) {
|
|
85
|
+
if (!customConfig || !node.callee) return false;
|
|
86
|
+
|
|
87
|
+
// Handle method-as-event pattern
|
|
88
|
+
if (customConfig.isMethodAsEvent && customConfig.objectName) {
|
|
89
|
+
if (node.callee.type !== NODE_TYPES.MEMBER_EXPRESSION) {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const objectNode = node.callee.object;
|
|
93
|
+
if (objectNode.type !== NODE_TYPES.IDENTIFIER) {
|
|
94
|
+
return false;
|
|
95
|
+
}
|
|
96
|
+
return objectNode.name === customConfig.objectName;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Handle standard custom function patterns
|
|
100
|
+
const fnName = customConfig.functionName;
|
|
101
|
+
if (!fnName) return false;
|
|
85
102
|
|
|
86
103
|
// Support chained calls in function name by stripping trailing parens from each segment
|
|
87
104
|
const parts = fnName.split('.').map(p => p.replace(/\(\s*\)$/, ''));
|
|
@@ -204,7 +221,7 @@ function findTrackingEvents(ast, filePath, customConfigs = []) {
|
|
|
204
221
|
// Attempt to match any custom function first to avoid mis-classifying built-in providers
|
|
205
222
|
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
206
223
|
for (const cfg of customConfigs) {
|
|
207
|
-
if (cfg && nodeMatchesCustomFunction(node, cfg
|
|
224
|
+
if (cfg && nodeMatchesCustomFunction(node, cfg)) {
|
|
208
225
|
matchedCustomConfig = cfg;
|
|
209
226
|
break;
|
|
210
227
|
}
|
|
@@ -237,7 +254,8 @@ function findTrackingEvents(ast, filePath, customConfigs = []) {
|
|
|
237
254
|
* @returns {Object|null} Extracted event or null
|
|
238
255
|
*/
|
|
239
256
|
function extractTrackingEvent(node, ancestors, filePath, constantMap, customConfig) {
|
|
240
|
-
|
|
257
|
+
// Pass the full customConfig object (not just functionName) to support method-as-event patterns
|
|
258
|
+
const source = detectAnalyticsSource(node, customConfig || null);
|
|
241
259
|
if (source === 'unknown') {
|
|
242
260
|
return null;
|
|
243
261
|
}
|
|
@@ -9,16 +9,25 @@ const { ANALYTICS_PROVIDERS } = require('../constants');
|
|
|
9
9
|
/**
|
|
10
10
|
* Detects the analytics provider from a CallExpression node
|
|
11
11
|
* @param {Object} node - TypeScript CallExpression node
|
|
12
|
-
* @param {string} [
|
|
12
|
+
* @param {string|Object} [customFunctionOrConfig] - Custom function name string or custom config object
|
|
13
13
|
* @returns {string} The detected analytics source or 'unknown'
|
|
14
14
|
*/
|
|
15
|
-
function detectAnalyticsSource(node,
|
|
15
|
+
function detectAnalyticsSource(node, customFunctionOrConfig) {
|
|
16
16
|
if (!node.expression) {
|
|
17
17
|
return 'unknown';
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
// Check for custom function first
|
|
21
|
-
|
|
21
|
+
// Support both old string format and new config object format
|
|
22
|
+
const customConfig = typeof customFunctionOrConfig === 'object' ? customFunctionOrConfig : null;
|
|
23
|
+
const customFunction = typeof customFunctionOrConfig === 'string' ? customFunctionOrConfig : (customConfig?.functionName);
|
|
24
|
+
|
|
25
|
+
if (customConfig?.isMethodAsEvent) {
|
|
26
|
+
// Method-as-event pattern: match any method on the specified object
|
|
27
|
+
if (isMethodAsEventFunction(node, customConfig)) {
|
|
28
|
+
return 'custom';
|
|
29
|
+
}
|
|
30
|
+
} else if (customFunction && isCustomFunction(node, customFunction)) {
|
|
22
31
|
return 'custom';
|
|
23
32
|
}
|
|
24
33
|
|
|
@@ -37,6 +46,31 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
37
46
|
return 'unknown';
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Checks if the node matches a method-as-event custom function pattern
|
|
51
|
+
* @param {Object} node - TypeScript CallExpression node
|
|
52
|
+
* @param {Object} customConfig - Custom function configuration with isMethodAsEvent: true
|
|
53
|
+
* @returns {boolean}
|
|
54
|
+
*/
|
|
55
|
+
function isMethodAsEventFunction(node, customConfig) {
|
|
56
|
+
if (!customConfig?.isMethodAsEvent || !customConfig?.objectName) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Must be a PropertyAccessExpression: objectName.methodName(...)
|
|
61
|
+
if (!ts.isPropertyAccessExpression(node.expression)) {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// The object part must match the configured objectName
|
|
66
|
+
const objectExpr = node.expression.expression;
|
|
67
|
+
if (!ts.isIdentifier(objectExpr)) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return objectExpr.escapedText === customConfig.objectName;
|
|
72
|
+
}
|
|
73
|
+
|
|
40
74
|
/**
|
|
41
75
|
* Checks if the node is a custom function call
|
|
42
76
|
* @param {Object} node - TypeScript CallExpression node
|
|
@@ -76,10 +76,10 @@ function extractSnowplowEvent(node, checker, sourceFile) {
|
|
|
76
76
|
|
|
77
77
|
// tracker.track(buildStructEvent({ action: 'event_name', ... }))
|
|
78
78
|
const firstArg = node.arguments[0];
|
|
79
|
-
|
|
79
|
+
|
|
80
80
|
// Check if it's a direct buildStructEvent call
|
|
81
|
-
if (ts.isCallExpression(firstArg) &&
|
|
82
|
-
ts.isIdentifier(firstArg.expression) &&
|
|
81
|
+
if (ts.isCallExpression(firstArg) &&
|
|
82
|
+
ts.isIdentifier(firstArg.expression) &&
|
|
83
83
|
firstArg.expression.escapedText === 'buildStructEvent' &&
|
|
84
84
|
firstArg.arguments.length > 0) {
|
|
85
85
|
const structEventArg = firstArg.arguments[0];
|
|
@@ -141,7 +141,7 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
141
141
|
|
|
142
142
|
// dataLayer.push({ event: 'event_name', property1: 'value1', property2: 'value2' })
|
|
143
143
|
const firstArg = node.arguments[0];
|
|
144
|
-
|
|
144
|
+
|
|
145
145
|
if (!ts.isObjectLiteralExpression(firstArg)) {
|
|
146
146
|
return { eventName: null, propertiesNode: null };
|
|
147
147
|
}
|
|
@@ -153,7 +153,7 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
153
153
|
}
|
|
154
154
|
|
|
155
155
|
const eventName = getStringValue(eventProperty.initializer, checker, sourceFile);
|
|
156
|
-
|
|
156
|
+
|
|
157
157
|
// Create a modified properties node without the 'event' property
|
|
158
158
|
const modifiedProperties = firstArg.properties.filter(prop => {
|
|
159
159
|
if (ts.isPropertyAssignment(prop) && prop.name) {
|
|
@@ -169,7 +169,7 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
169
169
|
|
|
170
170
|
// Create a synthetic object literal with the filtered properties
|
|
171
171
|
const modifiedPropertiesNode = ts.factory.createObjectLiteralExpression(modifiedProperties);
|
|
172
|
-
|
|
172
|
+
|
|
173
173
|
// Copy source positions for proper analysis
|
|
174
174
|
if (firstArg.pos !== undefined) {
|
|
175
175
|
modifiedPropertiesNode.pos = firstArg.pos;
|
|
@@ -192,10 +192,31 @@ function extractGTMEvent(node, checker, sourceFile) {
|
|
|
192
192
|
function extractCustomEvent(node, checker, sourceFile, customConfig) {
|
|
193
193
|
const args = node.arguments || [];
|
|
194
194
|
|
|
195
|
-
|
|
196
|
-
|
|
195
|
+
let eventName;
|
|
196
|
+
let propertiesArg;
|
|
197
|
+
|
|
198
|
+
if (customConfig?.isMethodAsEvent) {
|
|
199
|
+
// Method-as-event pattern: event name comes from the method name
|
|
200
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
201
|
+
const methodName = node.expression.name;
|
|
202
|
+
if (methodName && ts.isIdentifier(methodName)) {
|
|
203
|
+
eventName = methodName.escapedText || methodName.text;
|
|
204
|
+
} else {
|
|
205
|
+
// Fallback: could not extract method name
|
|
206
|
+
eventName = null;
|
|
207
|
+
}
|
|
208
|
+
} else {
|
|
209
|
+
eventName = null;
|
|
210
|
+
}
|
|
197
211
|
|
|
198
|
-
|
|
212
|
+
// Properties are at the configured index (default 0)
|
|
213
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 0];
|
|
214
|
+
} else {
|
|
215
|
+
// Standard custom function pattern: event name comes from argument
|
|
216
|
+
const eventArg = args[customConfig?.eventIndex ?? 0];
|
|
217
|
+
propertiesArg = args[customConfig?.propertiesIndex ?? 1];
|
|
218
|
+
eventName = getStringValue(eventArg, checker, sourceFile);
|
|
219
|
+
}
|
|
199
220
|
|
|
200
221
|
const extraArgs = {};
|
|
201
222
|
if (customConfig && customConfig.extraParams) {
|
|
@@ -320,22 +341,22 @@ function processEventData(eventData, source, filePath, line, functionName, check
|
|
|
320
341
|
*/
|
|
321
342
|
function getStringValue(node, checker, sourceFile) {
|
|
322
343
|
if (!node) return null;
|
|
323
|
-
|
|
344
|
+
|
|
324
345
|
// Handle string literals (existing behavior)
|
|
325
346
|
if (ts.isStringLiteral(node)) {
|
|
326
347
|
return node.text;
|
|
327
348
|
}
|
|
328
|
-
|
|
349
|
+
|
|
329
350
|
// Handle property access expressions like TRACKING_EVENTS.ECOMMERCE_PURCHASE
|
|
330
351
|
if (ts.isPropertyAccessExpression(node)) {
|
|
331
352
|
return resolvePropertyAccessToString(node, checker, sourceFile);
|
|
332
353
|
}
|
|
333
|
-
|
|
354
|
+
|
|
334
355
|
// Handle identifiers that might reference constants
|
|
335
356
|
if (ts.isIdentifier(node)) {
|
|
336
357
|
return resolveIdentifierToString(node, checker, sourceFile);
|
|
337
358
|
}
|
|
338
|
-
|
|
359
|
+
|
|
339
360
|
return null;
|
|
340
361
|
}
|
|
341
362
|
|
|
@@ -438,39 +459,39 @@ function resolveIdentifierToString(node, checker, sourceFile) {
|
|
|
438
459
|
if (!symbol) {
|
|
439
460
|
return null;
|
|
440
461
|
}
|
|
441
|
-
|
|
462
|
+
|
|
442
463
|
// First try to resolve through value declaration
|
|
443
464
|
if (symbol.valueDeclaration) {
|
|
444
465
|
const declaration = symbol.valueDeclaration;
|
|
445
|
-
|
|
466
|
+
|
|
446
467
|
// Handle variable declarations with string literal initializers
|
|
447
|
-
if (ts.isVariableDeclaration(declaration) &&
|
|
468
|
+
if (ts.isVariableDeclaration(declaration) &&
|
|
448
469
|
declaration.initializer &&
|
|
449
470
|
ts.isStringLiteral(declaration.initializer)) {
|
|
450
471
|
return declaration.initializer.text;
|
|
451
472
|
}
|
|
452
|
-
|
|
473
|
+
|
|
453
474
|
// Handle const declarations with object literals containing string properties
|
|
454
|
-
if (ts.isVariableDeclaration(declaration) &&
|
|
475
|
+
if (ts.isVariableDeclaration(declaration) &&
|
|
455
476
|
declaration.initializer &&
|
|
456
477
|
ts.isObjectLiteralExpression(declaration.initializer)) {
|
|
457
478
|
// This case is handled by property access resolution
|
|
458
479
|
return null;
|
|
459
480
|
}
|
|
460
481
|
}
|
|
461
|
-
|
|
482
|
+
|
|
462
483
|
// If value declaration doesn't exist or doesn't help, try type resolution
|
|
463
484
|
// This handles imported constants that are resolved through TypeScript's type system
|
|
464
485
|
const type = checker.getTypeOfSymbolAtLocation(symbol, node);
|
|
465
486
|
if (type && type.isStringLiteral && typeof type.isStringLiteral === 'function' && type.isStringLiteral()) {
|
|
466
487
|
return type.value;
|
|
467
488
|
}
|
|
468
|
-
|
|
489
|
+
|
|
469
490
|
// Alternative approach for string literal types (different TypeScript versions)
|
|
470
491
|
if (type && type.flags && (type.flags & ts.TypeFlags.StringLiteral)) {
|
|
471
492
|
return type.value;
|
|
472
493
|
}
|
|
473
|
-
|
|
494
|
+
|
|
474
495
|
return null;
|
|
475
496
|
} catch (error) {
|
|
476
497
|
return null;
|
|
@@ -485,7 +506,7 @@ function resolveIdentifierToString(node, checker, sourceFile) {
|
|
|
485
506
|
*/
|
|
486
507
|
function findPropertyByKey(objectNode, key) {
|
|
487
508
|
if (!objectNode.properties) return null;
|
|
488
|
-
|
|
509
|
+
|
|
489
510
|
return objectNode.properties.find(prop => {
|
|
490
511
|
if (prop.name) {
|
|
491
512
|
if (ts.isIdentifier(prop.name)) {
|
|
@@ -506,30 +527,37 @@ function findPropertyByKey(objectNode, key) {
|
|
|
506
527
|
*/
|
|
507
528
|
function cleanupProperties(properties) {
|
|
508
529
|
const cleaned = {};
|
|
509
|
-
|
|
530
|
+
|
|
510
531
|
for (const [key, value] of Object.entries(properties)) {
|
|
511
532
|
if (value && typeof value === 'object') {
|
|
512
|
-
// Remove __unresolved marker
|
|
533
|
+
// Remove __unresolved marker from the value itself
|
|
513
534
|
if (value.__unresolved) {
|
|
514
535
|
delete value.__unresolved;
|
|
515
536
|
}
|
|
516
|
-
|
|
537
|
+
|
|
517
538
|
// Recursively clean nested properties
|
|
518
539
|
if (value.properties) {
|
|
519
540
|
value.properties = cleanupProperties(value.properties);
|
|
520
541
|
}
|
|
521
|
-
|
|
522
|
-
// Clean array item properties
|
|
523
|
-
if (value.type === 'array' && value.items
|
|
524
|
-
|
|
542
|
+
|
|
543
|
+
// Clean array item properties and __unresolved markers
|
|
544
|
+
if (value.type === 'array' && value.items) {
|
|
545
|
+
// Remove __unresolved from items directly
|
|
546
|
+
if (value.items.__unresolved) {
|
|
547
|
+
delete value.items.__unresolved;
|
|
548
|
+
}
|
|
549
|
+
// Clean nested properties in items
|
|
550
|
+
if (value.items.properties) {
|
|
551
|
+
value.items.properties = cleanupProperties(value.items.properties);
|
|
552
|
+
}
|
|
525
553
|
}
|
|
526
|
-
|
|
554
|
+
|
|
527
555
|
cleaned[key] = value;
|
|
528
556
|
} else {
|
|
529
557
|
cleaned[key] = value;
|
|
530
558
|
}
|
|
531
559
|
}
|
|
532
|
-
|
|
560
|
+
|
|
533
561
|
return cleaned;
|
|
534
562
|
}
|
|
535
563
|
|