@flisk/analyze-tracking 0.1.0 → 0.1.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.
@@ -0,0 +1,33 @@
1
+ name: Publish Package to NPM
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ jobs:
8
+ test:
9
+ runs-on: ubuntu-latest
10
+ steps:
11
+ - uses: actions/checkout@v4
12
+ - uses: actions/setup-node@v4
13
+ with:
14
+ node-version: 20
15
+ - run: npm ci
16
+ - run: npm test
17
+
18
+ publish:
19
+ needs: test
20
+ runs-on: ubuntu-latest
21
+ permissions:
22
+ contents: read
23
+ id-token: write
24
+ steps:
25
+ - uses: actions/checkout@v4
26
+ - uses: actions/setup-node@v4
27
+ with:
28
+ node-version: 20
29
+ registry-url: https://registry.npmjs.org/
30
+ - run: npm ci
31
+ - run: npm publish --provenance --access public
32
+ env:
33
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # @flisk/analyze-tracking
2
+
2
3
  Analyzes tracking code in a project and generates data schemas
3
4
 
5
+ [![NPM version](https://img.shields.io/npm/v/@flisk/analyze-tracking.svg)](https://www.npmjs.com/package/@flisk/analyze-tracking)
6
+
7
+
4
8
  ## Usage
5
9
  ```sh
6
10
  npx @flisk/analyze-tracking /path/to/project
package/jest.config.js ADDED
@@ -0,0 +1,7 @@
1
+ module.exports = {
2
+ testEnvironment: 'node',
3
+ verbose: true,
4
+ collectCoverage: true,
5
+ coverageDirectory: 'coverage',
6
+ testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)'],
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flisk/analyze-tracking",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Analyzes tracking code in a project and generates data schemas",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "start": "node bin/analyze-tracking.js",
11
- "test": "echo \"Error: no test specified\" && exit 1"
11
+ "test": "jest"
12
12
  },
13
13
  "repository": {
14
14
  "type": "git",
@@ -26,5 +26,8 @@
26
26
  "acorn-walk": "^8.3.3",
27
27
  "js-yaml": "^4.1.0",
28
28
  "typescript": "^5.5.4"
29
+ },
30
+ "devDependencies": {
31
+ "jest": "^29.7.0"
29
32
  }
30
33
  }
@@ -29,7 +29,7 @@ function analyzeJsFile(filePath) {
29
29
  }
30
30
 
31
31
  const line = node.loc.start.line;
32
- const functionName = findWrappingFunctionJs(ancestors[ancestors.length - 2]);
32
+ const functionName = findWrappingFunctionJs(node, ancestors);
33
33
 
34
34
  if (eventName && propertiesNode && propertiesNode.type === 'ObjectExpression') {
35
35
  const properties = extractJsProperties(propertiesNode);
@@ -53,13 +53,37 @@ function findWrappingFunctionTs(node) {
53
53
  return 'global';
54
54
  }
55
55
 
56
- function findWrappingFunctionJs(node) {
57
- let current = node;
58
- while (current) {
59
- if (current.type === 'FunctionDeclaration' || current.type === 'FunctionExpression' || current.type === 'ArrowFunctionExpression') {
56
+ function findWrappingFunctionJs(node, ancestors) {
57
+ for (let i = ancestors.length - 1; i >= 0; i--) {
58
+ const current = ancestors[i];
59
+
60
+ // Handle direct variable assignments (e.g., const myFunc = () => {})
61
+ if (current.type === 'VariableDeclarator' && current.init === node) {
62
+ return current.id.name;
63
+ }
64
+
65
+ // Handle arrow functions or function expressions assigned to variables
66
+ if (current.type === 'VariableDeclarator' && (current.init.type === 'ArrowFunctionExpression' || current.init.type === 'FunctionExpression')) {
67
+ return current.id.name;
68
+ }
69
+
70
+ // Handle named function declarations
71
+ if (current.type === 'FunctionDeclaration') {
60
72
  return current.id ? current.id.name : 'anonymous';
61
73
  }
62
- current = current.parent;
74
+
75
+ // Handle exported variable/function (e.g., export const myFunc = () => {})
76
+ if (current.type === 'ExportNamedDeclaration' && current.declaration) {
77
+ const declaration = current.declaration.declarations ? current.declaration.declarations[0] : null;
78
+ if (declaration && (declaration.init.type === 'ArrowFunctionExpression' || declaration.init.type === 'FunctionExpression')) {
79
+ return declaration.id.name;
80
+ }
81
+ }
82
+
83
+ // Handle methods within object literals
84
+ if (current.type === 'Property' && current.value === node) {
85
+ return current.key.name || current.key.value;
86
+ }
63
87
  }
64
88
  return 'global';
65
89
  }
@@ -0,0 +1,20 @@
1
+ const {
2
+ detectSourceJs,
3
+ } = require('../src/analyze/helpers');
4
+
5
+ describe('detectSourceJs', () => {
6
+ it('should detect Google Analytics', () => {
7
+ const node = { callee: { type: 'Identifier', name: 'gtag' } };
8
+ expect(detectSourceJs(node)).toBe('googleanalytics');
9
+ });
10
+
11
+ it('should detect Segment', () => {
12
+ const node = { callee: { type: 'MemberExpression', object: { name: 'analytics' }, property: { name: 'track' } } };
13
+ expect(detectSourceJs(node)).toBe('segment');
14
+ });
15
+
16
+ it('should return unknown for unrecognized source', () => {
17
+ const node = { callee: { type: 'Identifier', name: 'unknownLib' } };
18
+ expect(detectSourceJs(node)).toBe('unknown');
19
+ });
20
+ });
@@ -0,0 +1,109 @@
1
+ const ts = require('typescript');
2
+ const {
3
+ extractJsProperties,
4
+ extractProperties,
5
+ } = require('../src/analyze/helpers');
6
+
7
+ describe('extractJsProperties', () => {
8
+ it('should extract simple properties', () => {
9
+ const node = {
10
+ properties: [
11
+ { key: { name: 'userId' }, value: { value: '12345', type: 'Literal' } },
12
+ { key: { name: 'plan' }, value: { value: 'Free', type: 'Literal' } },
13
+ ],
14
+ };
15
+ const properties = extractJsProperties(node);
16
+ expect(properties).toEqual({
17
+ userId: { type: 'string' },
18
+ plan: { type: 'string' },
19
+ });
20
+ });
21
+
22
+ it('should handle nested object properties', () => {
23
+ const node = {
24
+ properties: [
25
+ {
26
+ key: { name: 'address' },
27
+ value: {
28
+ type: 'ObjectExpression',
29
+ properties: [
30
+ { key: { name: 'city' }, value: { value: 'San Francisco', type: 'Literal' } },
31
+ { key: { name: 'state' }, value: { value: 'CA', type: 'Literal' } },
32
+ ],
33
+ },
34
+ },
35
+ ],
36
+ };
37
+ const properties = extractJsProperties(node);
38
+ expect(properties).toEqual({
39
+ address: {
40
+ type: 'object',
41
+ properties: {
42
+ city: { type: 'string' },
43
+ state: { type: 'string' },
44
+ },
45
+ },
46
+ });
47
+ });
48
+
49
+ it('should handle properties with undefined type', () => {
50
+ const node = {
51
+ properties: [{ key: { name: 'undefinedProp' }, value: { value: undefined, type: 'Literal' } }],
52
+ };
53
+ const properties = extractJsProperties(node);
54
+ expect(properties).toEqual({
55
+ undefinedProp: { type: 'any' },
56
+ });
57
+ });
58
+ });
59
+
60
+ describe('extractTsProperties', () => {
61
+ it('should extract properties from TypeScript object', () => {
62
+ const node = {
63
+ properties: [
64
+ { name: { text: 'userId' }, initializer: { text: '12345', type: 'Literal' } },
65
+ { name: { text: 'plan' }, initializer: { text: 'Free', type: 'Literal' } },
66
+ ],
67
+ };
68
+ const checker = {
69
+ getTypeAtLocation: jest.fn().mockReturnValue({}),
70
+ typeToString: jest.fn().mockReturnValue('string'),
71
+ };
72
+ const properties = extractProperties(checker, node);
73
+ expect(properties).toEqual({
74
+ userId: { type: 'string' },
75
+ plan: { type: 'string' },
76
+ });
77
+ });
78
+
79
+ it('should handle nested object properties in TypeScript', () => {
80
+ const node = {
81
+ properties: [
82
+ {
83
+ name: { text: 'address' },
84
+ initializer: {
85
+ kind: ts.SyntaxKind.ObjectLiteralExpression,
86
+ properties: [
87
+ { name: { text: 'city' }, initializer: { text: 'San Francisco', type: 'Literal' } },
88
+ { name: { text: 'state' }, initializer: { text: 'CA', type: 'Literal' } },
89
+ ],
90
+ },
91
+ },
92
+ ],
93
+ };
94
+ const checker = {
95
+ getTypeAtLocation: jest.fn().mockReturnValue({}),
96
+ typeToString: jest.fn().mockReturnValue('string'),
97
+ };
98
+ const properties = extractProperties(checker, node);
99
+ expect(properties).toEqual({
100
+ address: {
101
+ type: 'object',
102
+ properties: {
103
+ city: { type: 'string' },
104
+ state: { type: 'string' },
105
+ },
106
+ },
107
+ });
108
+ });
109
+ });
@@ -0,0 +1,30 @@
1
+ const ts = require('typescript');
2
+ const {
3
+ findWrappingFunctionJs,
4
+ } = require('../src/analyze/helpers');
5
+
6
+ describe('findWrappingFunctionJs', () => {
7
+ it('should return function name for arrow function assigned to variable', () => {
8
+ const node = { type: 'ArrowFunctionExpression' };
9
+ const ancestors = [
10
+ { type: 'Program' },
11
+ { type: 'VariableDeclarator', init: node, id: { name: 'checkout' } },
12
+ ];
13
+ expect(findWrappingFunctionJs(node, ancestors)).toBe('checkout');
14
+ });
15
+
16
+ it('should return function name for function expression assigned to variable', () => {
17
+ const node = { type: 'FunctionExpression' };
18
+ const ancestors = [
19
+ { type: 'Program' },
20
+ { type: 'VariableDeclarator', init: node, id: { name: 'myFunc' } },
21
+ ];
22
+ expect(findWrappingFunctionJs(node, ancestors)).toBe('myFunc');
23
+ });
24
+
25
+ it('should return "global" if no wrapping function is found', () => {
26
+ const node = {};
27
+ const ancestors = [{ type: 'Program' }];
28
+ expect(findWrappingFunctionJs(node, ancestors)).toBe('global');
29
+ });
30
+ });