@flisk/analyze-tracking 0.8.7 → 0.9.0
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 +84 -19
- package/package.json +5 -3
- package/src/analyze/index.js +20 -13
- package/src/analyze/javascript/detectors/analytics-source.js +25 -14
- package/src/analyze/javascript/extractors/event-extractor.js +24 -1
- package/src/analyze/javascript/parser.js +23 -13
- package/src/analyze/javascript/utils/import-resolver.js +233 -0
- package/src/analyze/swift/constants.js +61 -0
- package/src/analyze/swift/custom.js +36 -0
- package/src/analyze/swift/index.js +708 -0
- package/src/analyze/swift/providers.js +51 -0
- package/src/analyze/swift/runtime.js +46 -0
- package/src/analyze/swift/utils.js +75 -0
- package/src/analyze/typescript/detectors/analytics-source.js +47 -10
- package/src/analyze/utils/customFunctionParser.js +20 -8
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Provider detection for Swift analytics SDKs
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { normalizeChainPart, sliceRange } = require('./utils');
|
|
6
|
+
|
|
7
|
+
function detectProvider(call, source) {
|
|
8
|
+
const method = call.name;
|
|
9
|
+
const chain = Array.isArray(call.calleeChain) ? call.calleeChain : [];
|
|
10
|
+
const recvText = call.receiver || null;
|
|
11
|
+
const recvBase = recvText ? recvText.split('.')[0] : null;
|
|
12
|
+
const base = call.baseIdentifier || recvBase || (chain.length ? normalizeChainPart(chain[0]).split('.')[0] : null);
|
|
13
|
+
const methodCand = method || (chain.length ? normalizeChainPart(chain[chain.length - 1]) : null);
|
|
14
|
+
|
|
15
|
+
if (base === 'dataLayer' && (methodCand === 'append' || methodCand === 'push')) return 'gtm';
|
|
16
|
+
if (base === 'Analytics' && methodCand === 'logEvent') return 'googleanalytics';
|
|
17
|
+
if (base === 'analytics' && methodCand === 'track') return 'segment';
|
|
18
|
+
if (base === 'Mixpanel' && methodCand === 'track') return 'mixpanel';
|
|
19
|
+
if (base === 'amplitude' && methodCand === 'track') return 'amplitude';
|
|
20
|
+
if (base === 'RSClient' && methodCand === 'track') return 'rudderstack';
|
|
21
|
+
if (base === 'MParticle' && methodCand === 'logEvent') return 'mparticle';
|
|
22
|
+
if (base === 'PostHogSDK' && methodCand === 'capture') return 'posthog';
|
|
23
|
+
if (base === 'PendoManager' && methodCand === 'track') return 'pendo';
|
|
24
|
+
if (base === 'Heap' && methodCand === 'track') return 'heap';
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const text = sliceRange(source, call.range || {});
|
|
28
|
+
const t = text.replace(/\s+/g, '');
|
|
29
|
+
if (/\bdataLayer\.(append|push)\(/.test(t)) return 'gtm';
|
|
30
|
+
if (/\bAnalytics\.logEvent\(/.test(t)) return 'googleanalytics';
|
|
31
|
+
if (/\banalytics\.track\(/.test(t)) return 'segment';
|
|
32
|
+
if (/\bMixpanel\.[A-Za-z0-9_]+\(\)\.track\(/.test(t) || /\bMixpanel\.track\(/.test(t)) return 'mixpanel';
|
|
33
|
+
if (/\bamplitude\.track\(/.test(t)) return 'amplitude';
|
|
34
|
+
if (/\bRSClient\.[A-Za-z0-9_?]+\(\)?(?:\?\.|\.)track\(/.test(t) || /\bRSClient\(\)\.track\(/.test(t)) return 'rudderstack';
|
|
35
|
+
if (/\bMParticle\.[A-Za-z0-9_]+\(\)\.logEvent\(/.test(t)) return 'mparticle';
|
|
36
|
+
if (/\bPostHogSDK\.[A-Za-z0-9_]+\.capture\(/.test(t)) return 'posthog';
|
|
37
|
+
if (/\bPendoManager\.[A-Za-z0-9_]+\(\)\.track\(/.test(t)) return 'pendo';
|
|
38
|
+
if (/\bHeap\.[A-Za-z0-9_]+\.track\(/.test(t)) return 'heap';
|
|
39
|
+
} catch (_) {}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
if (methodCand === 'append') {
|
|
43
|
+
const text = sliceRange(source, call.range || {});
|
|
44
|
+
if (text.includes('event:')) return 'gtm';
|
|
45
|
+
}
|
|
46
|
+
} catch (_) {}
|
|
47
|
+
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { detectProvider };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Swift runtime bridge for @flisk/swift-ast
|
|
3
|
+
* - Provides lazy ESM/CJS loading
|
|
4
|
+
* - Serializes WASI-backed calls to avoid double-start errors
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const { pathToFileURL: pathToFileUrl } = require('url');
|
|
10
|
+
|
|
11
|
+
let __swiftAst = null;
|
|
12
|
+
async function getSwiftAst() {
|
|
13
|
+
if (__swiftAst) return __swiftAst;
|
|
14
|
+
try {
|
|
15
|
+
// Try CJS require first (when package exposes CJS entry)
|
|
16
|
+
// eslint-disable-next-line global-require
|
|
17
|
+
__swiftAst = require('@flisk/swift-ast');
|
|
18
|
+
return __swiftAst;
|
|
19
|
+
} catch (e) {
|
|
20
|
+
// Fallback to ESM dynamic import
|
|
21
|
+
try {
|
|
22
|
+
const m = await import('@flisk/swift-ast');
|
|
23
|
+
__swiftAst = m;
|
|
24
|
+
return __swiftAst;
|
|
25
|
+
} catch (_) {
|
|
26
|
+
// Final fallback: local workspace copy of swift-ast (for dev)
|
|
27
|
+
const localDist = path.resolve('/Users/sameenkarim/flisk/dev/swift-ast/dist/index.js');
|
|
28
|
+
if (fs.existsSync(localDist)) {
|
|
29
|
+
const m2 = await import(pathToFileUrl(localDist).href);
|
|
30
|
+
__swiftAst = m2;
|
|
31
|
+
return __swiftAst;
|
|
32
|
+
}
|
|
33
|
+
throw e;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Serialize WASI-backed swift-ast operations
|
|
39
|
+
let __swiftLock = Promise.resolve();
|
|
40
|
+
function withSwift(callback) {
|
|
41
|
+
const p = __swiftLock.then(callback, callback);
|
|
42
|
+
__swiftLock = p.then(() => {}, () => {});
|
|
43
|
+
return p;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { getSwiftAst, withSwift };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic Swift analyzer utilities
|
|
3
|
+
* - String inference, key resolution, text slicing, and traversal helpers
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
function normalizeChainPart(p) {
|
|
7
|
+
if (!p) return p;
|
|
8
|
+
return String(p).replace(/\s+/g, '').replace(/\(\)$/g, '');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function endsWithChain(chain, sigParts) {
|
|
12
|
+
if (sigParts.length > chain.length) return false;
|
|
13
|
+
for (let i = 1; i <= sigParts.length; i++) {
|
|
14
|
+
if (normalizeChainPart(chain[chain.length - i]) !== sigParts[sigParts.length - i]) return false;
|
|
15
|
+
}
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function extractStringLiteral(text) {
|
|
20
|
+
const m = /^\s*"([\s\S]*?)"\s*$/.exec(text || '');
|
|
21
|
+
return m ? m[1] : null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isIdentifier(text) {
|
|
25
|
+
return /^[_A-Za-z][_A-Za-z0-9\.]*$/.test(text || '');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function inferValueTypeFromText(text) {
|
|
29
|
+
const t = (text || '').trim();
|
|
30
|
+
if (/^"/.test(t)) return { type: 'string' };
|
|
31
|
+
if (/^(true|false)$/i.test(t)) return { type: 'boolean' };
|
|
32
|
+
if (/^[0-9]+(\.[0-9]+)?$/.test(t)) return { type: 'number' };
|
|
33
|
+
if (/^\[/.test(t)) {
|
|
34
|
+
const inside = t.slice(1, -1).trim();
|
|
35
|
+
if (!inside) return { type: 'array', items: { type: 'any' } };
|
|
36
|
+
if (/^(\s*"[\s\S]*?"\s*,)*\s*"[\s\S]*?"\s*$/.test(inside)) return { type: 'array', items: { type: 'string' } };
|
|
37
|
+
if (/^(\s*[0-9]+(\.[0-9]+)?\s*,)*\s*[0-9]+(\.[0-9]+)?\s*$/.test(inside)) return { type: 'array', items: { type: 'number' } };
|
|
38
|
+
return { type: 'array', items: { type: 'any' } };
|
|
39
|
+
}
|
|
40
|
+
if (/^\{/.test(t) || /\)$/.test(t)) return { type: 'object' };
|
|
41
|
+
if (isIdentifier(t)) return { type: 'string' };
|
|
42
|
+
return { type: 'any' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function splitTopLevel(s) {
|
|
46
|
+
const items = [];
|
|
47
|
+
let depthBr = 0, depthPr = 0; let cur = '';
|
|
48
|
+
for (let i = 0; i < s.length; i++) {
|
|
49
|
+
const ch = s[i];
|
|
50
|
+
if (ch === '[') depthBr++; else if (ch === ']') depthBr--;
|
|
51
|
+
else if (ch === '(') depthPr++; else if (ch === ')') depthPr--;
|
|
52
|
+
if (ch === ',' && depthBr === 0 && depthPr === 0) { items.push(cur); cur = ''; continue; }
|
|
53
|
+
cur += ch;
|
|
54
|
+
}
|
|
55
|
+
if (cur.trim()) items.push(cur);
|
|
56
|
+
return items;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sliceRange(source, range) {
|
|
60
|
+
const s = range?.start?.offset || 0; const e = range?.end?.offset || s;
|
|
61
|
+
return source.slice(s, e);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function escapeRegExp(s) { return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
normalizeChainPart,
|
|
68
|
+
endsWithChain,
|
|
69
|
+
extractStringLiteral,
|
|
70
|
+
isIdentifier,
|
|
71
|
+
inferValueTypeFromText,
|
|
72
|
+
splitTopLevel,
|
|
73
|
+
sliceRange,
|
|
74
|
+
escapeRegExp,
|
|
75
|
+
};
|
|
@@ -44,16 +44,53 @@ function detectAnalyticsSource(node, customFunction) {
|
|
|
44
44
|
* @returns {boolean}
|
|
45
45
|
*/
|
|
46
46
|
function isCustomFunction(node, customFunction) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
if (!customFunction || !node || !node.expression) return false;
|
|
48
|
+
|
|
49
|
+
// Normalize signature parts by stripping trailing parentheses from each part
|
|
50
|
+
const parts = customFunction.split('.').map(p => p.replace(/\(\s*\)$/, ''));
|
|
51
|
+
|
|
52
|
+
return matchesExpressionChain(node.expression, parts);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Recursively verify that a CallExpression/PropertyAccessExpression chain matches the expected parts.
|
|
57
|
+
* Supports patterns like getTracker().track, this.props.customTrackFunction6, tracker.track
|
|
58
|
+
*/
|
|
59
|
+
function matchesExpressionChain(expr, parts) {
|
|
60
|
+
let current = expr;
|
|
61
|
+
let idx = parts.length - 1;
|
|
62
|
+
|
|
63
|
+
while (current && idx >= 0) {
|
|
64
|
+
const expected = parts[idx];
|
|
65
|
+
|
|
66
|
+
if (ts.isPropertyAccessExpression(current)) {
|
|
67
|
+
const name = current.name?.escapedText;
|
|
68
|
+
if (name !== expected) return false;
|
|
69
|
+
current = current.expression;
|
|
70
|
+
idx -= 1;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (ts.isCallExpression(current)) {
|
|
75
|
+
// Step into the callee (e.g., getTracker() -> getTracker)
|
|
76
|
+
current = current.expression;
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (ts.isIdentifier(current)) {
|
|
81
|
+
return idx === 0 && current.escapedText === expected;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Handle `this` without relying on ts.isThisExpression for compatibility across TS versions
|
|
85
|
+
if (current.kind === ts.SyntaxKind.ThisKeyword || current.kind === ts.SyntaxKind.ThisExpression) {
|
|
86
|
+
return idx === 0 && expected === 'this';
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Unsupported expression kind for our matcher
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return false;
|
|
57
94
|
}
|
|
58
95
|
|
|
59
96
|
/**
|
|
@@ -4,15 +4,27 @@ function parseCustomFunctionSignature(signature) {
|
|
|
4
4
|
return null;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
// Supports names with module prefix like Module.func
|
|
9
|
-
const match = signature.match(/^\s*([A-Za-z0-9_.]+)\s*(?:\(([^)]*)\))?\s*$/);
|
|
10
|
-
if (!match) {
|
|
11
|
-
return null;
|
|
12
|
-
}
|
|
7
|
+
const trimmed = signature.trim();
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
// Two cases:
|
|
10
|
+
// 1) Full signature with params at the end (e.g., Module.track(EVENT_NAME, PROPERTIES)) → parse params
|
|
11
|
+
// 2) Name-only (including chains with internal calls, e.g., getService().track) → no params
|
|
12
|
+
let functionName;
|
|
13
|
+
let paramsPart = null;
|
|
14
|
+
|
|
15
|
+
if (/\)\s*$/.test(trimmed)) {
|
|
16
|
+
// Looks like it ends with a parameter list – extract the final (...) only
|
|
17
|
+
const lastOpenIdx = trimmed.lastIndexOf('(');
|
|
18
|
+
const lastCloseIdx = trimmed.lastIndexOf(')');
|
|
19
|
+
if (lastOpenIdx === -1 || lastCloseIdx < lastOpenIdx) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
functionName = trimmed.slice(0, lastOpenIdx).trim();
|
|
23
|
+
paramsPart = trimmed.slice(lastOpenIdx + 1, lastCloseIdx);
|
|
24
|
+
} else {
|
|
25
|
+
// No trailing params – treat the whole string as the function name
|
|
26
|
+
functionName = trimmed;
|
|
27
|
+
}
|
|
16
28
|
|
|
17
29
|
// Default legacy behaviour: EVENT_NAME, PROPERTIES
|
|
18
30
|
if (!paramsPart) {
|