@flisk/analyze-tracking 0.8.8 → 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 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
 
@@ -95,25 +131,25 @@ Use this to understand where your events live in the code and how they're being
95
131
  Your LLM of choice is used for generating descriptions of events, properties, and implementations.
96
132
 
97
133
  See [schema.json](schema.json) for a JSON Schema of the output.
98
-
134
+
99
135
 
100
136
  ## Supported tracking libraries & languages
101
137
 
102
- | Library | JavaScript/TypeScript | Python | Ruby | Go |
103
- |---------|:---------------------:|:------:|:----:|:--:|
104
- | Google Analytics | ✅ | ❌ | ❌ | ❌ |
105
- | Google Tag Manager | ✅ | ❌ | ❌ | ❌ |
106
- | Segment | ✅ | ✅ | ✅ | ✅ |
107
- | Mixpanel | ✅ | ✅ | ✅ | ✅ |
108
- | Amplitude | ✅ | ✅ | ❌ | ✅ |
109
- | Rudderstack | ✅ | ✅ | ✳️ | ✳️ |
110
- | mParticle | ✅ | ❌ | ❌ | ❌ |
111
- | PostHog | ✅ | ✅ | ✅ | ✅ |
112
- | Pendo | ✅ | ❌ | ❌ | ❌ |
113
- | Heap | ✅ | ❌ | ❌ | ❌ |
114
- | Snowplow | ✅ | ✅ | ✅ | ✅ |
115
- | Datadog RUM | ✅ | ❌ | ❌ | ❌ |
116
- | Custom Function | ✅ | ✅ | ✅ | ✅ |
138
+ | Library | JavaScript/TypeScript | Python | Ruby | Go | Swift |
139
+ |---------|:---------------------:|:------:|:----:|:--:|:--:|
140
+ | Google Analytics | ✅ | ❌ | ❌ | ❌ | ✅ |
141
+ | Google Tag Manager | ✅ | ❌ | ❌ | ❌ | ✅ |
142
+ | Segment | ✅ | ✅ | ✅ | ✅ | ✅ |
143
+ | Mixpanel | ✅ | ✅ | ✅ | ✅ | ✅ |
144
+ | Amplitude | ✅ | ✅ | ❌ | ✅ | ✅ |
145
+ | Rudderstack | ✅ | ✅ | ✳️ | ✳️ | ✅ |
146
+ | mParticle | ✅ | ❌ | ❌ | ❌ | ✅ |
147
+ | PostHog | ✅ | ✅ | ✅ | ✅ | ✅ |
148
+ | Pendo | ✅ | ❌ | ❌ | ❌ | ✅ |
149
+ | Heap | ✅ | ❌ | ❌ | ❌ | ✅ |
150
+ | Snowplow | ✅ | ✅ | ✅ | ✅ | ❌ |
151
+ | Datadog RUM | ✅ | ❌ | ❌ | ❌ | ❌ |
152
+ | Custom Function | ✅ | ✅ | ✅ | ✅ | ✅ |
117
153
 
118
154
  ✳️ Rudderstack's SDKs often use the same format as Segment, so Rudderstack events may be detected as Segment events.
119
155
 
@@ -129,6 +165,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
129
165
  '<property_name>': '<property_value>'
130
166
  });
131
167
  ```
168
+
169
+ **Swift**
170
+ ```swift
171
+ Analytics.logEvent("<event_name>", parameters: [
172
+ "<property_name>": "<property_value>"
173
+ ])
174
+ ```
132
175
  </details>
133
176
 
134
177
  <details>
@@ -147,6 +190,11 @@ See [schema.json](schema.json) for a JSON Schema of the output.
147
190
  '<property_name>': '<property_value>'
148
191
  });
149
192
  ```
193
+
194
+ **Swift**
195
+ ```swift
196
+ dataLayer.push(["event": "<event_name>", "<property_name>": "<property_value>"])
197
+ ```
150
198
  </details>
151
199
 
152
200
  <details>
@@ -185,6 +233,11 @@ See [schema.json](schema.json) for a JSON Schema of the output.
185
233
  Set("<property_name>", "<property_value>"),
186
234
  })
187
235
  ```
236
+
237
+ **Swift**
238
+ ```swift
239
+ analytics.track(name: "<event_name>", properties: TrackProperties("<property_name>": "<property_value>"))
240
+ ```
188
241
  </details>
189
242
 
190
243
  <details>
@@ -221,6 +274,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
221
274
  }),
222
275
  })
223
276
  ```
277
+
278
+ **Swift**
279
+ ```swift
280
+ Mixpanel.mainInstance().track(event: "<event_name>", properties: [
281
+ "<property_name>": "<property_value>"
282
+ ])
283
+ ```
224
284
  </details>
225
285
 
226
286
  <details>
@@ -256,6 +316,14 @@ See [schema.json](schema.json) for a JSON Schema of the output.
256
316
  },
257
317
  })
258
318
  ```
319
+
320
+ **Swift**
321
+ ```swift
322
+ amplitude.track(
323
+ eventType: "<event_name>",
324
+ eventProperties: ["<property_name>": "<property_value>"]
325
+ )
326
+ ```
259
327
  </details>
260
328
 
261
329
  <details>
@@ -295,6 +363,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
295
363
  Set("<property_name>", "<property_value>"),
296
364
  })
297
365
  ```
366
+
367
+ **Swift**
368
+ ```swift
369
+ RSClient.sharedInstance()?.track("<event_name>", properties: [
370
+ "<property_name>": "<property_value>"
371
+ ])
372
+ ```
298
373
  </details>
299
374
 
300
375
  <details>
@@ -306,6 +381,15 @@ See [schema.json](schema.json) for a JSON Schema of the output.
306
381
  '<property_name>': '<property_value>'
307
382
  });
308
383
  ```
384
+
385
+ **Swift**
386
+ ```swift
387
+ let event = MPEvent(name: "<event_name>", type: .other)
388
+ event.customAttributes = [
389
+ "<property_name>": "<property_value>"
390
+ ]
391
+ MParticle.sharedInstance().logEvent(event)
392
+ ```
309
393
  </details>
310
394
 
311
395
  <details>
@@ -353,6 +437,13 @@ See [schema.json](schema.json) for a JSON Schema of the output.
353
437
  Set("<property_name>", "<property_value>"),
354
438
  })
355
439
  ```
440
+
441
+ **Swift**
442
+ ```swift
443
+ PostHogSDK.shared.capture("<event_name>", properties: [
444
+ "<property_name>": "<property_value>"
445
+ ])
446
+ ```
356
447
  </details>
357
448
 
358
449
  <details>
@@ -372,7 +463,12 @@ See [schema.json](schema.json) for a JSON Schema of the output.
372
463
  })
373
464
  ```
374
465
 
375
-
466
+ **Swift**
467
+ ```swift
468
+ PendoManager.shared().track("<event_name>", properties: [
469
+ "<property_name>": "<property_value>"
470
+ ])
471
+ ```
376
472
  </details>
377
473
 
378
474
  <details>
@@ -392,7 +488,12 @@ See [schema.json](schema.json) for a JSON Schema of the output.
392
488
  })
393
489
  ```
394
490
 
395
-
491
+ **Swift**
492
+ ```swift
493
+ Heap.shared.track("<event_name>", properties: [
494
+ "<property_name>": "<property_value>"
495
+ ])
496
+ ```
396
497
  </details>
397
498
 
398
499
  <details>
@@ -403,7 +504,7 @@ See [schema.json](schema.json) for a JSON Schema of the output.
403
504
  datadogRum.addAction('<event_name>', {
404
505
  '<property_name>': '<property_value>'
405
506
  });
406
-
507
+
407
508
  // Or via window
408
509
  window.DD_RUM.addAction('<event_name>', {
409
510
  '<property_name>': '<property_value>'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk/analyze-tracking",
3
- "version": "0.8.8",
3
+ "version": "0.9.1",
4
4
  "description": "Analyzes tracking code in a project and generates data schemas",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -13,6 +13,7 @@
13
13
  "test:python": "node --experimental-vm-modules --test tests/analyzePython.test.js",
14
14
  "test:ruby": "node --experimental-vm-modules --test tests/analyzeRuby.test.js",
15
15
  "test:go": "node --test tests/analyzeGo.test.js",
16
+ "test:swift": "node --test tests/analyzeSwift.test.js",
16
17
  "test:cli": "node --test tests/cli.test.js",
17
18
  "test:schema": "node --test tests/schema.test.js",
18
19
  "test:generateDescriptions": "node --test tests/generateDescriptions.test.js",
@@ -34,6 +35,7 @@
34
35
  },
35
36
  "homepage": "https://github.com/fliskdata/analyze-tracking#readme",
36
37
  "dependencies": {
38
+ "@flisk/swift-ast": "^0.1.3",
37
39
  "@langchain/core": "^0.3.56",
38
40
  "@langchain/google-vertexai": "^0.2.9",
39
41
  "@langchain/openai": "^0.5.10",
@@ -52,9 +54,9 @@
52
54
  "zod": "^3.24.4"
53
55
  },
54
56
  "devDependencies": {
57
+ "@types/react": "^19.1.6",
55
58
  "ajv": "^8.17.1",
56
59
  "lodash": "^4.17.21",
57
- "react": "^19.1.0",
58
- "@types/react": "^19.1.6"
60
+ "react": "^19.1.0"
59
61
  }
60
62
  }
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": [
@@ -13,12 +13,13 @@ const { analyzeTsFiles } = require('./typescript');
13
13
  const { analyzePythonFile } = require('./python');
14
14
  const { analyzeRubyFile, prebuildConstantMaps } = require('./ruby');
15
15
  const { analyzeGoFile } = require('./go');
16
+ const { analyzeSwiftFile } = require('./swift');
16
17
 
17
18
  /**
18
19
  * Analyzes a single file for analytics tracking calls
19
- *
20
+ *
20
21
  * Note: typescript files are handled separately by analyzeTsFiles, which is a batch processor
21
- *
22
+ *
22
23
  * @param {string} file - Path to the file to analyze
23
24
  * @param {Array<string>} customFunctionSignatures - Custom function signatures to detect
24
25
  * @returns {Promise<Array<Object>>} Array of events found in the file
@@ -28,19 +29,20 @@ async function analyzeFile(file, customFunctionSignatures) {
28
29
  if (/\.py$/.test(file)) return analyzePythonFile(file, customFunctionSignatures)
29
30
  if (/\.rb$/.test(file)) return analyzeRubyFile(file, customFunctionSignatures)
30
31
  if (/\.go$/.test(file)) return analyzeGoFile(file, customFunctionSignatures)
32
+ if (/\.swift$/.test(file)) return analyzeSwiftFile(file, customFunctionSignatures)
31
33
  return []
32
34
  }
33
35
 
34
36
  /**
35
37
  * Adds an event to the events collection, merging properties if event already exists
36
- *
38
+ *
37
39
  * @param {Object} allEvents - Collection of all events
38
40
  * @param {Object} event - Event to add
39
41
  * @param {string} baseDir - Base directory for relative path calculation
40
42
  */
41
43
  function addEventToCollection(allEvents, event, baseDir) {
42
44
  const relativeFilePath = path.relative(baseDir, event.filePath);
43
-
45
+
44
46
  const implementation = {
45
47
  path: relativeFilePath,
46
48
  line: event.line,
@@ -64,12 +66,12 @@ function addEventToCollection(allEvents, event, baseDir) {
64
66
 
65
67
  /**
66
68
  * Processes all files that are not TypeScript files in parallel
67
- *
69
+ *
68
70
  * Checks the system's file descriptor limit and uses 80% of it to avoid running out of file descriptors
69
71
  * Creates a promise pool and launches one analysis for each file in parallel
70
72
  * When a slot frees up, the next file is launched
71
73
  * Waits for the remaining work to complete
72
- *
74
+ *
73
75
  * @param {Array<string>} files - Array of file paths
74
76
  * @param {Object} allEvents - Collection to add events to
75
77
  * @param {string} baseDir - Base directory for relative paths
@@ -121,10 +123,10 @@ async function processFiles(files, allEvents, baseDir, customFunctionSignatures)
121
123
 
122
124
  /**
123
125
  * Analyze a directory recursively for analytics tracking calls
124
- *
126
+ *
125
127
  * This function scans all supported files in a directory tree and identifies analytics tracking calls,
126
128
  * handling different file types appropriately.
127
- *
129
+ *
128
130
  * @param {string} dirPath - Path to the directory to analyze
129
131
  * @param {Array<string>} [customFunctions=null] - Array of custom tracking function signatures to detect
130
132
  * @returns {Promise<Object>} Object mapping event names to their tracking implementations
@@ -132,17 +134,22 @@ async function processFiles(files, allEvents, baseDir, customFunctionSignatures)
132
134
  async function analyzeDirectory(dirPath, customFunctions) {
133
135
  const allEvents = {};
134
136
 
135
- const customFunctionSignatures = (customFunctions?.length > 0)
136
- ? customFunctions.map(parseCustomFunctionSignature)
137
- : null;
137
+ let customFunctionSignatures = null;
138
+ if (Array.isArray(customFunctions) && customFunctions.length > 0) {
139
+ customFunctionSignatures = customFunctions.map(cf => {
140
+ if (cf && typeof cf === 'object' && cf.functionName) return cf;
141
+ if (typeof cf === 'string') return parseCustomFunctionSignature(cf);
142
+ return null;
143
+ }).filter(Boolean);
144
+ }
138
145
 
139
146
  const files = getAllFiles(dirPath);
140
-
147
+
141
148
  // Separate TypeScript files from others for optimized processing
142
149
  const tsFiles = [];
143
150
  const nonTsFiles = [];
144
151
  const rubyFiles = [];
145
-
152
+
146
153
  for (const file of files) {
147
154
  const isTsFile = /\.(tsx?)$/.test(file);
148
155
  if (isTsFile) {
@@ -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} [customFunction] - Custom function name to detect
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, customFunction) {
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
- if (customFunction && isCustomFunction(node, customFunction)) {
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
- const eventArg = args[customConfig?.eventIndex ?? 0];
175
- const propertiesArg = args[customConfig?.propertiesIndex ?? 1];
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
- const eventName = getStringValue(eventArg, constantMap);
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 name.
76
- * Supports both simple identifiers (e.g. myTrack) and dot-separated members (e.g. Custom.track).
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 {string} fnName – Custom function name (could include dots)
81
+ * @param {Object} customConfig – Custom function configuration object
81
82
  * @returns {boolean}
82
83
  */
83
- function nodeMatchesCustomFunction(node, fnName) {
84
- if (!fnName || !node.callee) return false;
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.functionName)) {
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
- const source = detectAnalyticsSource(node, customConfig?.functionName);
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
  }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Constants and helper-return collectors for Swift fixtures
3
+ * - Scans directory for top-level lets and enum/struct static lets
4
+ * - Extracts simple function returns for dict/array builders
5
+ */
6
+
7
+ const fs = require('fs');
8
+ const path = require('path');
9
+
10
+ function buildCrossFileConstMap(dir) {
11
+ const map = {};
12
+ try {
13
+ const entries = fs.readdirSync(dir).filter(f => f.endsWith('.swift'));
14
+ for (const f of entries) {
15
+ const fp = path.join(dir, f);
16
+ const content = fs.readFileSync(fp, 'utf8');
17
+ // Top-level: let NAME = "..."
18
+ for (const m of content.matchAll(/\blet\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
19
+ map[m[1]] = m[2];
20
+ }
21
+ // Enum/struct blocks: capture namespace and all static lets inside
22
+ let idx = 0;
23
+ while (idx < content.length) {
24
+ const head = content.slice(idx);
25
+ const mm = /\b(enum|struct)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\{/m.exec(head);
26
+ if (!mm) break;
27
+ const ns = mm[2];
28
+ const blockStart = idx + mm.index + mm[0].length - 1; // position at '{'
29
+ // Find matching closing brace
30
+ let depth = 0; let end = -1;
31
+ for (let i = blockStart; i < content.length; i++) {
32
+ const ch = content[i];
33
+ if (ch === '{') depth++;
34
+ else if (ch === '}') { depth--; if (depth === 0) { end = i; break; } }
35
+ }
36
+ if (end === -1) break;
37
+ const block = content.slice(blockStart + 1, end);
38
+ for (const sm of block.matchAll(/\bstatic\s+let\s+([A-Za-z_][A-Za-z0-9_]*)\s*=\s*"([\s\S]*?)"/g)) {
39
+ const key = sm[1];
40
+ const val = sm[2];
41
+ map[`${ns}.${key}`] = val;
42
+ }
43
+ idx = end + 1;
44
+ }
45
+ // Capture very simple helper returns
46
+ // func makeAddress() -> [String: Any] { return [ ... ] }
47
+ for (const m of content.matchAll(/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*->\s*\[[^\]]+\][^{]*\{[\s\S]*?return\s*(\[[\s\S]*?\])[\s\S]*?\}/g)) {
48
+ map.__dictFuncs = map.__dictFuncs || {};
49
+ map.__dictFuncs[m[1]] = { kind: 'dict', text: m[2] };
50
+ }
51
+ // func makeProducts() -> [[String: Any]] { return [ ... ] } (array)
52
+ for (const m of content.matchAll(/func\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(\)\s*->\s*\[\[[^\]]+\]\][^{]*\{[\s\S]*?return\s*(\[[\s\S]*?\])[\s\S]*?\}/g)) {
53
+ map.__dictFuncs = map.__dictFuncs || {};
54
+ map.__dictFuncs[m[1]] = { kind: 'array', text: m[2] };
55
+ }
56
+ }
57
+ } catch (_) {}
58
+ return map;
59
+ }
60
+
61
+ module.exports = { buildCrossFileConstMap };