@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
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
const ts = require('typescript');
|
|
7
|
-
const {
|
|
8
|
-
getTypeOfNode,
|
|
9
|
-
resolveTypeToProperties,
|
|
7
|
+
const {
|
|
8
|
+
getTypeOfNode,
|
|
9
|
+
resolveTypeToProperties,
|
|
10
10
|
getBasicTypeOfArrayElement,
|
|
11
|
-
isCustomType
|
|
11
|
+
isCustomType,
|
|
12
|
+
isEnumType,
|
|
13
|
+
getEnumValues,
|
|
14
|
+
resolveTypeObjectToSchema,
|
|
15
|
+
extractTypeProperties
|
|
12
16
|
} = require('../utils/type-resolver');
|
|
13
17
|
|
|
14
18
|
/**
|
|
@@ -40,7 +44,7 @@ function extractProperties(checker, node) {
|
|
|
40
44
|
Object.assign(properties, spreadProperties);
|
|
41
45
|
continue;
|
|
42
46
|
}
|
|
43
|
-
|
|
47
|
+
|
|
44
48
|
const key = getPropertyKey(prop);
|
|
45
49
|
if (!key) continue;
|
|
46
50
|
|
|
@@ -66,16 +70,16 @@ function getPropertyKey(prop) {
|
|
|
66
70
|
}
|
|
67
71
|
return null;
|
|
68
72
|
}
|
|
69
|
-
|
|
73
|
+
|
|
70
74
|
// Regular property with name
|
|
71
75
|
if (ts.isIdentifier(prop.name)) {
|
|
72
76
|
return prop.name.escapedText;
|
|
73
77
|
}
|
|
74
|
-
|
|
78
|
+
|
|
75
79
|
if (ts.isStringLiteral(prop.name)) {
|
|
76
80
|
return prop.name.text;
|
|
77
81
|
}
|
|
78
|
-
|
|
82
|
+
|
|
79
83
|
return null;
|
|
80
84
|
}
|
|
81
85
|
|
|
@@ -90,25 +94,25 @@ function extractPropertySchema(checker, prop) {
|
|
|
90
94
|
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
91
95
|
return extractShorthandPropertySchema(checker, prop);
|
|
92
96
|
}
|
|
93
|
-
|
|
97
|
+
|
|
94
98
|
// Handle property assignments with initializers
|
|
95
99
|
if (ts.isPropertyAssignment(prop)) {
|
|
96
100
|
if (prop.initializer) {
|
|
97
101
|
return extractValueSchema(checker, prop.initializer);
|
|
98
102
|
}
|
|
99
|
-
|
|
103
|
+
|
|
100
104
|
// Property with type annotation but no initializer
|
|
101
105
|
if (prop.type) {
|
|
102
106
|
const typeString = checker.typeToString(checker.getTypeFromTypeNode(prop.type));
|
|
103
107
|
return resolveTypeSchema(checker, typeString);
|
|
104
108
|
}
|
|
105
109
|
}
|
|
106
|
-
|
|
110
|
+
|
|
107
111
|
// Handle method declarations
|
|
108
112
|
if (ts.isMethodDeclaration(prop)) {
|
|
109
113
|
return { type: 'function' };
|
|
110
114
|
}
|
|
111
|
-
|
|
115
|
+
|
|
112
116
|
return null;
|
|
113
117
|
}
|
|
114
118
|
|
|
@@ -155,30 +159,11 @@ function extractShorthandPropertySchema(checker, prop) {
|
|
|
155
159
|
return { type: 'any' };
|
|
156
160
|
}
|
|
157
161
|
}
|
|
158
|
-
|
|
162
|
+
|
|
159
163
|
const propType = checker.getTypeAtLocation(prop.name);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
if (isArrayType(typeString)) {
|
|
164
|
-
return extractArrayTypeSchema(checker, propType, typeString);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Handle other types
|
|
168
|
-
const resolvedType = resolveTypeToProperties(checker, typeString);
|
|
169
|
-
|
|
170
|
-
// If it's an unresolved custom type, try to extract interface properties
|
|
171
|
-
if (resolvedType.__unresolved) {
|
|
172
|
-
const interfaceProps = extractInterfaceProperties(checker, propType);
|
|
173
|
-
if (Object.keys(interfaceProps).length > 0) {
|
|
174
|
-
return {
|
|
175
|
-
type: 'object',
|
|
176
|
-
properties: interfaceProps
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
return resolvedType;
|
|
164
|
+
|
|
165
|
+
// Use the type object resolver for better accuracy
|
|
166
|
+
return resolveTypeObjectToSchema(checker, propType);
|
|
182
167
|
}
|
|
183
168
|
|
|
184
169
|
/**
|
|
@@ -188,33 +173,29 @@ function extractShorthandPropertySchema(checker, prop) {
|
|
|
188
173
|
* @returns {PropertySchema}
|
|
189
174
|
*/
|
|
190
175
|
function extractValueSchema(checker, valueNode) {
|
|
191
|
-
// Object literal
|
|
176
|
+
// Object literal - extract inline properties
|
|
192
177
|
if (ts.isObjectLiteralExpression(valueNode)) {
|
|
193
178
|
return {
|
|
194
179
|
type: 'object',
|
|
195
180
|
properties: extractProperties(checker, valueNode)
|
|
196
181
|
};
|
|
197
182
|
}
|
|
198
|
-
|
|
183
|
+
|
|
199
184
|
// Array literal
|
|
200
185
|
if (ts.isArrayLiteralExpression(valueNode)) {
|
|
201
186
|
return extractArrayLiteralSchema(checker, valueNode);
|
|
202
187
|
}
|
|
203
|
-
|
|
204
|
-
// Identifier (variable reference)
|
|
205
|
-
if (ts.isIdentifier(valueNode)) {
|
|
206
|
-
return extractIdentifierSchema(checker, valueNode);
|
|
207
|
-
}
|
|
208
|
-
|
|
188
|
+
|
|
209
189
|
// Literal values
|
|
210
190
|
const literalType = getLiteralType(valueNode);
|
|
211
191
|
if (literalType) {
|
|
212
192
|
return { type: literalType };
|
|
213
193
|
}
|
|
214
|
-
|
|
215
|
-
// For other expressions,
|
|
216
|
-
|
|
217
|
-
|
|
194
|
+
|
|
195
|
+
// For all other expressions (identifiers, property access, etc.),
|
|
196
|
+
// use the type object resolver for accurate type resolution
|
|
197
|
+
const valueType = checker.getTypeAtLocation(valueNode);
|
|
198
|
+
return resolveTypeObjectToSchema(checker, valueType);
|
|
218
199
|
}
|
|
219
200
|
|
|
220
201
|
/**
|
|
@@ -230,17 +211,17 @@ function extractArrayLiteralSchema(checker, node) {
|
|
|
230
211
|
items: { type: 'any' }
|
|
231
212
|
};
|
|
232
213
|
}
|
|
233
|
-
|
|
214
|
+
|
|
234
215
|
// Check types of all elements
|
|
235
216
|
const elementTypes = new Set();
|
|
236
217
|
for (const element of node.elements) {
|
|
237
218
|
const elemType = getBasicTypeOfArrayElement(checker, element);
|
|
238
219
|
elementTypes.add(elemType);
|
|
239
220
|
}
|
|
240
|
-
|
|
221
|
+
|
|
241
222
|
// If all elements are the same type, use that type
|
|
242
223
|
const itemType = elementTypes.size === 1 ? Array.from(elementTypes)[0] : 'any';
|
|
243
|
-
|
|
224
|
+
|
|
244
225
|
return {
|
|
245
226
|
type: 'array',
|
|
246
227
|
items: { type: itemType }
|
|
@@ -255,28 +236,9 @@ function extractArrayLiteralSchema(checker, node) {
|
|
|
255
236
|
*/
|
|
256
237
|
function extractIdentifierSchema(checker, identifier) {
|
|
257
238
|
const identifierType = checker.getTypeAtLocation(identifier);
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
if (isArrayType(typeString)) {
|
|
262
|
-
return extractArrayTypeSchema(checker, identifierType, typeString);
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
// Handle other types
|
|
266
|
-
const resolvedType = resolveTypeToProperties(checker, typeString);
|
|
267
|
-
|
|
268
|
-
// If it's an unresolved custom type, try to extract interface properties
|
|
269
|
-
if (resolvedType.__unresolved) {
|
|
270
|
-
const interfaceProps = extractInterfaceProperties(checker, identifierType);
|
|
271
|
-
if (Object.keys(interfaceProps).length > 0) {
|
|
272
|
-
return {
|
|
273
|
-
type: 'object',
|
|
274
|
-
properties: interfaceProps
|
|
275
|
-
};
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
return resolvedType;
|
|
239
|
+
|
|
240
|
+
// Use the new type object resolver for better accuracy
|
|
241
|
+
return resolveTypeObjectToSchema(checker, identifierType);
|
|
280
242
|
}
|
|
281
243
|
|
|
282
244
|
/**
|
|
@@ -288,7 +250,7 @@ function extractIdentifierSchema(checker, identifier) {
|
|
|
288
250
|
*/
|
|
289
251
|
function extractArrayTypeSchema(checker, type, typeString) {
|
|
290
252
|
let elementType = null;
|
|
291
|
-
|
|
253
|
+
|
|
292
254
|
// Try to get type arguments for generic types
|
|
293
255
|
if (type.target && type.typeArguments && type.typeArguments.length > 0) {
|
|
294
256
|
elementType = type.typeArguments[0];
|
|
@@ -302,7 +264,7 @@ function extractArrayTypeSchema(checker, type, typeString) {
|
|
|
302
264
|
// Indexed access failed
|
|
303
265
|
}
|
|
304
266
|
}
|
|
305
|
-
|
|
267
|
+
|
|
306
268
|
if (elementType) {
|
|
307
269
|
const elementInterfaceProps = extractInterfaceProperties(checker, elementType);
|
|
308
270
|
if (Object.keys(elementInterfaceProps).length > 0) {
|
|
@@ -327,7 +289,7 @@ function extractArrayTypeSchema(checker, type, typeString) {
|
|
|
327
289
|
};
|
|
328
290
|
}
|
|
329
291
|
}
|
|
330
|
-
|
|
292
|
+
|
|
331
293
|
return {
|
|
332
294
|
type: 'array',
|
|
333
295
|
items: { type: 'any' }
|
|
@@ -342,12 +304,12 @@ function extractArrayTypeSchema(checker, type, typeString) {
|
|
|
342
304
|
*/
|
|
343
305
|
function resolveTypeSchema(checker, typeString) {
|
|
344
306
|
const resolvedType = resolveTypeToProperties(checker, typeString);
|
|
345
|
-
|
|
307
|
+
|
|
346
308
|
// Clean up any unresolved markers for simple types
|
|
347
309
|
if (resolvedType.__unresolved) {
|
|
348
310
|
delete resolvedType.__unresolved;
|
|
349
311
|
}
|
|
350
|
-
|
|
312
|
+
|
|
351
313
|
return resolvedType;
|
|
352
314
|
}
|
|
353
315
|
|
|
@@ -372,8 +334,8 @@ function getLiteralType(node) {
|
|
|
372
334
|
* @returns {boolean}
|
|
373
335
|
*/
|
|
374
336
|
function isArrayType(typeString) {
|
|
375
|
-
return typeString.includes('[]') ||
|
|
376
|
-
typeString.startsWith('Array<') ||
|
|
337
|
+
return typeString.includes('[]') ||
|
|
338
|
+
typeString.startsWith('Array<') ||
|
|
377
339
|
typeString.startsWith('ReadonlyArray<') ||
|
|
378
340
|
typeString.startsWith('readonly ');
|
|
379
341
|
}
|
|
@@ -388,13 +350,13 @@ function extractSpreadProperties(checker, spreadNode) {
|
|
|
388
350
|
if (!spreadNode.expression) {
|
|
389
351
|
return {};
|
|
390
352
|
}
|
|
391
|
-
|
|
353
|
+
|
|
392
354
|
// If the spread is an identifier, resolve it to its declaration
|
|
393
355
|
if (ts.isIdentifier(spreadNode.expression)) {
|
|
394
356
|
const symbol = checker.getSymbolAtLocation(spreadNode.expression);
|
|
395
357
|
if (symbol && symbol.declarations && symbol.declarations.length > 0) {
|
|
396
358
|
const declaration = symbol.declarations[0];
|
|
397
|
-
|
|
359
|
+
|
|
398
360
|
// If it's a variable declaration with an object literal initializer
|
|
399
361
|
if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
|
|
400
362
|
if (ts.isObjectLiteralExpression(declaration.initializer)) {
|
|
@@ -403,17 +365,17 @@ function extractSpreadProperties(checker, spreadNode) {
|
|
|
403
365
|
}
|
|
404
366
|
}
|
|
405
367
|
}
|
|
406
|
-
|
|
368
|
+
|
|
407
369
|
// Fallback to the original identifier schema extraction
|
|
408
370
|
const identifierSchema = extractIdentifierSchema(checker, spreadNode.expression);
|
|
409
371
|
return identifierSchema.properties || {};
|
|
410
372
|
}
|
|
411
|
-
|
|
373
|
+
|
|
412
374
|
// If the spread is an object literal, extract its properties
|
|
413
375
|
if (ts.isObjectLiteralExpression(spreadNode.expression)) {
|
|
414
376
|
return extractProperties(checker, spreadNode.expression);
|
|
415
377
|
}
|
|
416
|
-
|
|
378
|
+
|
|
417
379
|
// For other expressions, try to get the type and extract properties from it
|
|
418
380
|
try {
|
|
419
381
|
const spreadType = checker.getTypeAtLocation(spreadNode.expression);
|
|
@@ -432,20 +394,65 @@ function extractSpreadProperties(checker, spreadNode) {
|
|
|
432
394
|
function extractInterfaceProperties(checker, type) {
|
|
433
395
|
const properties = {};
|
|
434
396
|
const typeSymbol = type.getSymbol();
|
|
435
|
-
|
|
397
|
+
|
|
436
398
|
if (!typeSymbol) return properties;
|
|
437
|
-
|
|
399
|
+
|
|
400
|
+
// Check if this is an enum type - don't expand enum string methods
|
|
401
|
+
const typeString = checker.typeToString(type);
|
|
402
|
+
if (isEnumType(checker, typeString)) {
|
|
403
|
+
// Return empty - the caller should handle enum types specially
|
|
404
|
+
return properties;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Check if this looks like a string primitive with methods - skip it
|
|
408
|
+
if (isStringPrototype(type, checker)) {
|
|
409
|
+
return properties;
|
|
410
|
+
}
|
|
411
|
+
|
|
438
412
|
// Get all properties of the type
|
|
439
413
|
const members = checker.getPropertiesOfType(type);
|
|
440
|
-
|
|
441
|
-
|
|
414
|
+
|
|
415
|
+
// Filter out built-in methods (string prototype methods, etc.)
|
|
416
|
+
const userDefinedMembers = members.filter(member => {
|
|
417
|
+
const name = member.name;
|
|
418
|
+
// Skip common built-in method names
|
|
419
|
+
if (STRING_PROTOTYPE_METHODS.has(name)) {
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
// Skip symbols
|
|
423
|
+
if (name.startsWith('__@')) {
|
|
424
|
+
return false;
|
|
425
|
+
}
|
|
426
|
+
return true;
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
for (const member of userDefinedMembers) {
|
|
442
430
|
try {
|
|
443
431
|
const memberType = checker.getTypeOfSymbolAtLocation(member, member.valueDeclaration);
|
|
444
432
|
const memberTypeString = checker.typeToString(memberType);
|
|
445
|
-
|
|
433
|
+
|
|
434
|
+
// Skip function types
|
|
435
|
+
if (memberTypeString.includes('=>') || memberTypeString.startsWith('(')) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Check if member type is an enum
|
|
440
|
+
if (isEnumType(checker, memberTypeString, memberType)) {
|
|
441
|
+
const enumValues = getEnumValues(checker, memberTypeString, memberType);
|
|
442
|
+
if (enumValues && enumValues.length > 0) {
|
|
443
|
+
properties[member.name] = {
|
|
444
|
+
type: 'enum',
|
|
445
|
+
values: enumValues
|
|
446
|
+
};
|
|
447
|
+
} else {
|
|
448
|
+
properties[member.name] = { type: 'string' };
|
|
449
|
+
}
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
|
|
446
453
|
// Recursively resolve the member type
|
|
447
454
|
const resolvedType = resolveTypeToProperties(checker, memberTypeString);
|
|
448
|
-
|
|
455
|
+
|
|
449
456
|
// If it's an unresolved object type, try to extract its properties
|
|
450
457
|
if (resolvedType.__unresolved) {
|
|
451
458
|
const nestedProperties = extractInterfaceProperties(checker, memberType);
|
|
@@ -470,10 +477,41 @@ function extractInterfaceProperties(checker, type) {
|
|
|
470
477
|
properties[member.name] = { type: 'any' };
|
|
471
478
|
}
|
|
472
479
|
}
|
|
473
|
-
|
|
480
|
+
|
|
474
481
|
return properties;
|
|
475
482
|
}
|
|
476
483
|
|
|
484
|
+
/**
|
|
485
|
+
* Set of common string prototype method names to filter out
|
|
486
|
+
*/
|
|
487
|
+
const STRING_PROTOTYPE_METHODS = new Set([
|
|
488
|
+
'toString', 'charAt', 'charCodeAt', 'concat', 'indexOf', 'lastIndexOf',
|
|
489
|
+
'localeCompare', 'match', 'replace', 'search', 'slice', 'split',
|
|
490
|
+
'substring', 'toLowerCase', 'toLocaleLowerCase', 'toUpperCase',
|
|
491
|
+
'toLocaleUpperCase', 'trim', 'length', 'substr', 'valueOf',
|
|
492
|
+
'codePointAt', 'includes', 'endsWith', 'normalize', 'repeat',
|
|
493
|
+
'startsWith', 'anchor', 'big', 'blink', 'bold', 'fixed',
|
|
494
|
+
'fontcolor', 'fontsize', 'italics', 'link', 'small', 'strike',
|
|
495
|
+
'sub', 'sup', 'padStart', 'padEnd', 'trimEnd', 'trimStart',
|
|
496
|
+
'trimLeft', 'trimRight', 'matchAll', 'replaceAll', 'at',
|
|
497
|
+
'isWellFormed', 'toWellFormed'
|
|
498
|
+
]);
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Checks if a type is a string primitive that would have prototype methods
|
|
502
|
+
* @param {Object} type - TypeScript Type object
|
|
503
|
+
* @param {Object} checker - TypeScript type checker
|
|
504
|
+
* @returns {boolean}
|
|
505
|
+
*/
|
|
506
|
+
function isStringPrototype(type, checker) {
|
|
507
|
+
if (!type) return false;
|
|
508
|
+
const members = checker.getPropertiesOfType(type);
|
|
509
|
+
// If the type has common string methods, it's likely a string
|
|
510
|
+
const stringMethodCount = members.filter(m => STRING_PROTOTYPE_METHODS.has(m.name)).length;
|
|
511
|
+
// If more than half of the members are string methods, treat as string
|
|
512
|
+
return stringMethodCount > 10 && stringMethodCount > members.length / 2;
|
|
513
|
+
}
|
|
514
|
+
|
|
477
515
|
module.exports = {
|
|
478
516
|
extractProperties,
|
|
479
517
|
extractInterfaceProperties
|
|
@@ -168,16 +168,34 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
168
168
|
const events = [];
|
|
169
169
|
|
|
170
170
|
/**
|
|
171
|
-
* Tests if a CallExpression matches a custom function
|
|
171
|
+
* Tests if a CallExpression matches a custom function configuration
|
|
172
172
|
* @param {Object} callNode - The call expression node
|
|
173
|
-
* @param {
|
|
173
|
+
* @param {Object} customConfig - Custom function configuration object
|
|
174
174
|
* @returns {boolean} True if matches
|
|
175
175
|
*/
|
|
176
|
-
function matchesCustomFunction(callNode,
|
|
177
|
-
if (!
|
|
176
|
+
function matchesCustomFunction(callNode, customConfig) {
|
|
177
|
+
if (!customConfig || !callNode.expression) {
|
|
178
178
|
return false;
|
|
179
179
|
}
|
|
180
|
-
|
|
180
|
+
|
|
181
|
+
// Handle method-as-event pattern
|
|
182
|
+
if (customConfig.isMethodAsEvent && customConfig.objectName) {
|
|
183
|
+
if (!ts.isPropertyAccessExpression(callNode.expression)) {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
const objectExpr = callNode.expression.expression;
|
|
187
|
+
if (!ts.isIdentifier(objectExpr)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
return objectExpr.escapedText === customConfig.objectName;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Handle standard custom function pattern
|
|
194
|
+
const functionName = customConfig.functionName;
|
|
195
|
+
if (!functionName) {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
|
|
181
199
|
try {
|
|
182
200
|
return callNode.expression.getText() === functionName;
|
|
183
201
|
} catch {
|
|
@@ -197,7 +215,7 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
197
215
|
// Check for custom function matches
|
|
198
216
|
if (Array.isArray(customConfigs) && customConfigs.length > 0) {
|
|
199
217
|
for (const config of customConfigs) {
|
|
200
|
-
if (config && matchesCustomFunction(node, config
|
|
218
|
+
if (config && matchesCustomFunction(node, config)) {
|
|
201
219
|
matchedCustomConfig = config;
|
|
202
220
|
break;
|
|
203
221
|
}
|
|
@@ -211,7 +229,7 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
211
229
|
filePath,
|
|
212
230
|
matchedCustomConfig
|
|
213
231
|
);
|
|
214
|
-
|
|
232
|
+
|
|
215
233
|
if (event) {
|
|
216
234
|
events.push(event);
|
|
217
235
|
}
|
|
@@ -238,7 +256,8 @@ function findTrackingEvents(sourceFile, checker, filePath, customConfigs = []) {
|
|
|
238
256
|
*/
|
|
239
257
|
function extractTrackingEvent(node, sourceFile, checker, filePath, customConfig) {
|
|
240
258
|
// Detect the analytics source
|
|
241
|
-
|
|
259
|
+
// Pass the full customConfig object (not just functionName) to support method-as-event patterns
|
|
260
|
+
const source = detectAnalyticsSource(node, customConfig || null);
|
|
242
261
|
if (source === 'unknown') {
|
|
243
262
|
return null;
|
|
244
263
|
}
|