@jtl-software/eslint-plugin-posthog 0.1.0 → 0.2.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,19 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
updates:
|
|
3
|
+
# Enable version updates for npm dependencies
|
|
4
|
+
- package-ecosystem: "npm"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "weekly"
|
|
8
|
+
open-pull-requests-limit: 5
|
|
9
|
+
groups:
|
|
10
|
+
dependencies:
|
|
11
|
+
patterns:
|
|
12
|
+
- "*"
|
|
13
|
+
|
|
14
|
+
# Enable version updates for GitHub Actions
|
|
15
|
+
- package-ecosystem: "github-actions"
|
|
16
|
+
directory: "/"
|
|
17
|
+
schedule:
|
|
18
|
+
interval: "weekly"
|
|
19
|
+
open-pull-requests-limit: 5
|
package/README.md
CHANGED
|
@@ -115,20 +115,6 @@ To test the plugin in your project before publishing:
|
|
|
115
115
|
|
|
116
116
|
3. Add the plugin to your ESLint config as shown above
|
|
117
117
|
|
|
118
|
-
### CI/CD
|
|
119
|
-
|
|
120
|
-
This project uses GitHub Actions for continuous integration and deployment:
|
|
121
|
-
|
|
122
|
-
- **Tests** run automatically on every push and pull request
|
|
123
|
-
- **Publishing to npm** happens automatically when pushing to the `main` branch
|
|
124
|
-
|
|
125
|
-
To set up automatic publishing, add an `NPM_TOKEN` secret to your GitHub repository:
|
|
126
|
-
|
|
127
|
-
1. Generate an npm access token at https://www.npmjs.com/settings/tokens
|
|
128
|
-
2. Go to your repository settings on GitHub
|
|
129
|
-
3. Navigate to Settings → Secrets and variables → Actions
|
|
130
|
-
4. Add a new repository secret named `NPM_TOKEN` with your npm token
|
|
131
|
-
|
|
132
118
|
## License
|
|
133
119
|
|
|
134
120
|
MIT
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jtl-software/eslint-plugin-posthog",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "ESLint rules for PostHog event tracking best practices",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
],
|
|
14
14
|
"author": "JTL Platform Team",
|
|
15
15
|
"license": "MIT",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/jtl-software/eslint-plugin-posthog.git"
|
|
19
|
+
},
|
|
16
20
|
"peerDependencies": {
|
|
17
21
|
"eslint": "^9.0.0"
|
|
18
22
|
},
|
|
@@ -18,6 +18,9 @@ export default {
|
|
|
18
18
|
},
|
|
19
19
|
|
|
20
20
|
create(context) {
|
|
21
|
+
const wrapperFunctions = new Map();
|
|
22
|
+
const captureCalls = [];
|
|
23
|
+
|
|
21
24
|
/**
|
|
22
25
|
* Check if a string is in camelCase
|
|
23
26
|
* @param {string} str - The string to check
|
|
@@ -43,30 +46,137 @@ export default {
|
|
|
43
46
|
);
|
|
44
47
|
}
|
|
45
48
|
|
|
49
|
+
function validateObjectProperties(objectNode) {
|
|
50
|
+
if (!objectNode || objectNode.type !== 'ObjectExpression') {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
objectNode.properties.forEach((prop) => {
|
|
55
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
56
|
+
const propertyName = prop.key.name;
|
|
57
|
+
if (!isCamelCase(propertyName)) {
|
|
58
|
+
context.report({
|
|
59
|
+
node: prop.key,
|
|
60
|
+
messageId: 'notCamelCase',
|
|
61
|
+
data: {
|
|
62
|
+
property: propertyName,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolvePropertiesArgument(propertiesArg) {
|
|
71
|
+
if (!propertiesArg) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Direct object literal
|
|
76
|
+
if (propertiesArg.type === 'ObjectExpression') {
|
|
77
|
+
return propertiesArg;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Variable reference - trace back to definition
|
|
81
|
+
if (propertiesArg.type === 'Identifier') {
|
|
82
|
+
const scope = context.sourceCode.getScope(propertiesArg);
|
|
83
|
+
const variable = scope.variables.find((v) => v.name === propertiesArg.name);
|
|
84
|
+
|
|
85
|
+
if (variable && variable.defs.length > 0) {
|
|
86
|
+
const def = variable.defs[0];
|
|
87
|
+
if (def.node.init && def.node.init.type === 'ObjectExpression') {
|
|
88
|
+
return def.node.init;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getFunctionName(node) {
|
|
97
|
+
// Regular function declaration: function foo() {}
|
|
98
|
+
if (node.type === 'FunctionDeclaration' && node.id) {
|
|
99
|
+
return node.id.name;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Variable declaration with arrow function: const foo = () => {}
|
|
103
|
+
// Variable declaration with function expression: const foo = function() {}
|
|
104
|
+
if (node.parent && node.parent.type === 'VariableDeclarator' && node.parent.id) {
|
|
105
|
+
return node.parent.id.name;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function getParentFunction(node) {
|
|
112
|
+
let current = node.parent;
|
|
113
|
+
while (current) {
|
|
114
|
+
if (
|
|
115
|
+
current.type === 'FunctionDeclaration' ||
|
|
116
|
+
current.type === 'FunctionExpression' ||
|
|
117
|
+
current.type === 'ArrowFunctionExpression'
|
|
118
|
+
) {
|
|
119
|
+
return current;
|
|
120
|
+
}
|
|
121
|
+
current = current.parent;
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const allCallExpressions = [];
|
|
127
|
+
|
|
46
128
|
return {
|
|
47
129
|
CallExpression(node) {
|
|
48
|
-
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
130
|
+
allCallExpressions.push(node);
|
|
51
131
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
if (!propertiesArg || propertiesArg.type !== 'ObjectExpression') {
|
|
55
|
-
return;
|
|
132
|
+
if (isPostHogCapture(node)) {
|
|
133
|
+
captureCalls.push(node);
|
|
56
134
|
}
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
'Program:exit'() {
|
|
138
|
+
captureCalls.forEach((captureCall) => {
|
|
139
|
+
const propertiesArg = captureCall.arguments[1];
|
|
140
|
+
|
|
141
|
+
if (propertiesArg && propertiesArg.type === 'Identifier') {
|
|
142
|
+
const parentFunc = getParentFunction(captureCall);
|
|
143
|
+
if (parentFunc) {
|
|
144
|
+
const paramName = propertiesArg.name;
|
|
145
|
+
const paramIndex = parentFunc.params.findIndex(
|
|
146
|
+
(p) => p.type === 'Identifier' && p.name === paramName,
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
if (paramIndex !== -1) {
|
|
150
|
+
const functionName = getFunctionName(parentFunc);
|
|
151
|
+
if (functionName) {
|
|
152
|
+
wrapperFunctions.set(functionName, paramIndex);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// validate direct calls
|
|
159
|
+
const objectNode = resolvePropertiesArgument(propertiesArg);
|
|
160
|
+
if (objectNode) {
|
|
161
|
+
validateObjectProperties(objectNode);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// validate calls to wrapper functions
|
|
166
|
+
allCallExpressions.forEach((callNode) => {
|
|
167
|
+
if (isPostHogCapture(callNode)) {
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
57
170
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
property: propertyName,
|
|
68
|
-
},
|
|
69
|
-
});
|
|
171
|
+
if (callNode.callee.type === 'Identifier') {
|
|
172
|
+
const functionName = callNode.callee.name;
|
|
173
|
+
if (wrapperFunctions.has(functionName)) {
|
|
174
|
+
const paramIndex = wrapperFunctions.get(functionName);
|
|
175
|
+
const propertiesArg = callNode.arguments[paramIndex];
|
|
176
|
+
const objectNode = resolvePropertiesArgument(propertiesArg);
|
|
177
|
+
if (objectNode) {
|
|
178
|
+
validateObjectProperties(objectNode);
|
|
179
|
+
}
|
|
70
180
|
}
|
|
71
181
|
}
|
|
72
182
|
});
|
|
@@ -23,6 +23,39 @@ ruleTester.run('consistent-property-naming', rule, {
|
|
|
23
23
|
code: `// Not a PostHog call
|
|
24
24
|
someOtherFunction({ user_id: '123' })`,
|
|
25
25
|
},
|
|
26
|
+
// Variable tracking - valid camelCase
|
|
27
|
+
{
|
|
28
|
+
code: `
|
|
29
|
+
const properties = { userId: '123', productName: 'Test' };
|
|
30
|
+
postHog.capture('event_name', properties);
|
|
31
|
+
`,
|
|
32
|
+
},
|
|
33
|
+
// Wrapper function - valid camelCase
|
|
34
|
+
{
|
|
35
|
+
code: `
|
|
36
|
+
function trackEvent(name, props) {
|
|
37
|
+
postHog.capture(name, props);
|
|
38
|
+
}
|
|
39
|
+
trackEvent('event_name', { userId: '123', productName: 'Test' });
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
// Nested function with wrapper - valid camelCase
|
|
43
|
+
{
|
|
44
|
+
code: `
|
|
45
|
+
function Component() {
|
|
46
|
+
const handleCapture = (eventName, properties) => {
|
|
47
|
+
postHog.capture(eventName, properties);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function handleClick() {
|
|
51
|
+
handleCapture('button_clicked', {
|
|
52
|
+
buttonId: '123',
|
|
53
|
+
buttonName: 'Submit'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
`,
|
|
58
|
+
},
|
|
26
59
|
],
|
|
27
60
|
|
|
28
61
|
invalid: [
|
|
@@ -70,5 +103,117 @@ ruleTester.run('consistent-property-naming', rule, {
|
|
|
70
103
|
},
|
|
71
104
|
],
|
|
72
105
|
},
|
|
106
|
+
// Variable tracking scenarios
|
|
107
|
+
{
|
|
108
|
+
code: `
|
|
109
|
+
const properties = { user_id: '123' };
|
|
110
|
+
postHog.capture('event_name', properties);
|
|
111
|
+
`,
|
|
112
|
+
errors: [
|
|
113
|
+
{
|
|
114
|
+
messageId: 'notCamelCase',
|
|
115
|
+
data: { property: 'user_id' },
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
code: `
|
|
121
|
+
const props = { product_id: '123', product_name: 'Test' };
|
|
122
|
+
postHog.capture('event_name', props);
|
|
123
|
+
`,
|
|
124
|
+
errors: [
|
|
125
|
+
{
|
|
126
|
+
messageId: 'notCamelCase',
|
|
127
|
+
data: { property: 'product_id' },
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
messageId: 'notCamelCase',
|
|
131
|
+
data: { property: 'product_name' },
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
code: `
|
|
137
|
+
const eventProps = {
|
|
138
|
+
userId: '123',
|
|
139
|
+
product_name: 'Test',
|
|
140
|
+
is_active: true
|
|
141
|
+
};
|
|
142
|
+
postHog.capture('event_name', eventProps);
|
|
143
|
+
`,
|
|
144
|
+
errors: [
|
|
145
|
+
{
|
|
146
|
+
messageId: 'notCamelCase',
|
|
147
|
+
data: { property: 'product_name' },
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
messageId: 'notCamelCase',
|
|
151
|
+
data: { property: 'is_active' },
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
},
|
|
155
|
+
// Wrapper function scenarios
|
|
156
|
+
{
|
|
157
|
+
code: `
|
|
158
|
+
function trackEvent(name, props) {
|
|
159
|
+
postHog.capture(name, props);
|
|
160
|
+
}
|
|
161
|
+
trackEvent('event_name', { user_id: '123' });
|
|
162
|
+
`,
|
|
163
|
+
errors: [
|
|
164
|
+
{
|
|
165
|
+
messageId: 'notCamelCase',
|
|
166
|
+
data: { property: 'user_id' },
|
|
167
|
+
},
|
|
168
|
+
],
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
code: `
|
|
172
|
+
const handleCapture = (eventName, properties) => {
|
|
173
|
+
postHog.capture(eventName, properties);
|
|
174
|
+
};
|
|
175
|
+
handleCapture('product_added', {
|
|
176
|
+
product_id: '123',
|
|
177
|
+
product_name: 'Test'
|
|
178
|
+
});
|
|
179
|
+
`,
|
|
180
|
+
errors: [
|
|
181
|
+
{
|
|
182
|
+
messageId: 'notCamelCase',
|
|
183
|
+
data: { property: 'product_id' },
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
messageId: 'notCamelCase',
|
|
187
|
+
data: { property: 'product_name' },
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
},
|
|
191
|
+
// Nested function scenario (like the original bug report)
|
|
192
|
+
{
|
|
193
|
+
code: `
|
|
194
|
+
function Component() {
|
|
195
|
+
const handleCapture = (eventName, properties) => {
|
|
196
|
+
postHog.capture(eventName, properties);
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
function handleClick() {
|
|
200
|
+
handleCapture('button_clicked', {
|
|
201
|
+
button_id: '123',
|
|
202
|
+
button_name: 'Submit'
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
`,
|
|
207
|
+
errors: [
|
|
208
|
+
{
|
|
209
|
+
messageId: 'notCamelCase',
|
|
210
|
+
data: { property: 'button_id' },
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
messageId: 'notCamelCase',
|
|
214
|
+
data: { property: 'button_name' },
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
},
|
|
73
218
|
],
|
|
74
219
|
});
|