@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.
@@ -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
- 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
- (node.expression?.expression &&
52
- ts.isPropertyAccessExpression(node.expression.expression) &&
53
- node.expression.expression.expression &&
54
- ts.isThisExpression(node.expression.expression.expression)); // For class methods like this.analytics.track()
55
-
56
- return canBeCustomFunction && node.expression.getText() === customFunction;
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
- // Match function name and optional parameter list
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
- const functionName = match[1].trim();
15
- const paramsPart = match[2];
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) {