@jtl-software/eslint-plugin-posthog 0.1.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/.github/workflows/ci.yml +94 -0
- package/.prettierignore +5 -0
- package/.prettierrc +9 -0
- package/.tool-versions +2 -0
- package/README.md +134 -0
- package/docs/rules/consistent-property-naming.md +44 -0
- package/docs/rules/valid-event-names.md +104 -0
- package/package.json +36 -0
- package/src/index.js +24 -0
- package/src/rules/consistent-property-naming.js +76 -0
- package/src/rules/valid-event-names.js +318 -0
- package/tests/rules/consistent-property-naming.test.js +74 -0
- package/tests/rules/valid-event-names.test.js +197 -0
- package/tests/setup.js +13 -0
- package/vitest.config.js +14 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- '**'
|
|
7
|
+
pull_request:
|
|
8
|
+
branches:
|
|
9
|
+
- main
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
test:
|
|
13
|
+
name: Test
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout code
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- name: Read .tool-versions
|
|
21
|
+
id: tool-versions
|
|
22
|
+
run: |
|
|
23
|
+
NODE_VERSION=$(grep nodejs .tool-versions | awk '{print $2}')
|
|
24
|
+
PNPM_VERSION=$(grep pnpm .tool-versions | awk '{print $2}')
|
|
25
|
+
echo "node-version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
|
26
|
+
echo "pnpm-version=$PNPM_VERSION" >> $GITHUB_OUTPUT
|
|
27
|
+
|
|
28
|
+
- name: Setup Node.js
|
|
29
|
+
uses: actions/setup-node@v4
|
|
30
|
+
with:
|
|
31
|
+
node-version: ${{ steps.tool-versions.outputs.node-version }}
|
|
32
|
+
|
|
33
|
+
- name: Setup pnpm
|
|
34
|
+
uses: pnpm/action-setup@v4
|
|
35
|
+
with:
|
|
36
|
+
version: ${{ steps.tool-versions.outputs.pnpm-version }}
|
|
37
|
+
|
|
38
|
+
- name: Install dependencies
|
|
39
|
+
run: pnpm install --frozen-lockfile
|
|
40
|
+
|
|
41
|
+
- name: Run tests
|
|
42
|
+
run: pnpm test
|
|
43
|
+
|
|
44
|
+
publish:
|
|
45
|
+
name: Publish to npm
|
|
46
|
+
needs: test
|
|
47
|
+
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
|
|
48
|
+
runs-on: ubuntu-latest
|
|
49
|
+
|
|
50
|
+
steps:
|
|
51
|
+
- name: Checkout code
|
|
52
|
+
uses: actions/checkout@v4
|
|
53
|
+
|
|
54
|
+
- name: Read .tool-versions
|
|
55
|
+
id: tool-versions
|
|
56
|
+
run: |
|
|
57
|
+
NODE_VERSION=$(grep nodejs .tool-versions | awk '{print $2}')
|
|
58
|
+
PNPM_VERSION=$(grep pnpm .tool-versions | awk '{print $2}')
|
|
59
|
+
echo "node-version=$NODE_VERSION" >> $GITHUB_OUTPUT
|
|
60
|
+
echo "pnpm-version=$PNPM_VERSION" >> $GITHUB_OUTPUT
|
|
61
|
+
|
|
62
|
+
- name: Setup Node.js
|
|
63
|
+
uses: actions/setup-node@v4
|
|
64
|
+
with:
|
|
65
|
+
node-version: ${{ steps.tool-versions.outputs.node-version }}
|
|
66
|
+
registry-url: 'https://registry.npmjs.org'
|
|
67
|
+
|
|
68
|
+
- name: Setup pnpm
|
|
69
|
+
uses: pnpm/action-setup@v4
|
|
70
|
+
with:
|
|
71
|
+
version: ${{ steps.tool-versions.outputs.pnpm-version }}
|
|
72
|
+
|
|
73
|
+
- name: Install dependencies
|
|
74
|
+
run: pnpm install --frozen-lockfile
|
|
75
|
+
|
|
76
|
+
- name: Check if version exists on npm
|
|
77
|
+
id: version-check
|
|
78
|
+
run: |
|
|
79
|
+
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
80
|
+
echo "package-version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT
|
|
81
|
+
|
|
82
|
+
if npm view @jtl-software/eslint-plugin-posthog@$PACKAGE_VERSION version 2>/dev/null; then
|
|
83
|
+
echo "should-publish=false" >> $GITHUB_OUTPUT
|
|
84
|
+
echo "Version $PACKAGE_VERSION already exists on npm, skipping publish"
|
|
85
|
+
else
|
|
86
|
+
echo "should-publish=true" >> $GITHUB_OUTPUT
|
|
87
|
+
echo "Version $PACKAGE_VERSION not found on npm, will publish"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
- name: Publish to npm
|
|
91
|
+
if: steps.version-check.outputs.should-publish == 'true'
|
|
92
|
+
run: pnpm publish --access public --no-git-checks
|
|
93
|
+
env:
|
|
94
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/.tool-versions
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# @jtl-software/eslint-plugin-posthog
|
|
2
|
+
|
|
3
|
+
ESLint plugin for enforcing PostHog event tracking best practices.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --save-dev @jtl-software/eslint-plugin-posthog
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
yarn add -D @jtl-software/eslint-plugin-posthog
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pnpm add -D @jtl-software/eslint-plugin-posthog
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### Flat Config (ESLint 9+)
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// eslint.config.js
|
|
25
|
+
import posthog from '@jtl-software/eslint-plugin-posthog';
|
|
26
|
+
|
|
27
|
+
export default [
|
|
28
|
+
posthog.configs.recommended,
|
|
29
|
+
{
|
|
30
|
+
// Your other config here
|
|
31
|
+
},
|
|
32
|
+
];
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Legacy Config (.eslintrc)
|
|
36
|
+
|
|
37
|
+
For ESLint 8 and below, add to your `.eslintrc.js` or `.eslintrc.json`:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
module.exports = {
|
|
41
|
+
plugins: ['@jtl-software/eslint-plugin-posthog'],
|
|
42
|
+
rules: {
|
|
43
|
+
'@jtl-software/posthog/consistent-property-naming': 'error',
|
|
44
|
+
'@jtl-software/posthog/valid-event-names': 'error',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Rules
|
|
50
|
+
|
|
51
|
+
| Rule | Description | Severity |
|
|
52
|
+
| ------------------------------------------------------------------------ | -------------------------------------- | -------- |
|
|
53
|
+
| [consistent-property-naming](./docs/rules/consistent-property-naming.md) | Enforce camelCase for property names | error |
|
|
54
|
+
| [valid-event-names](./docs/rules/valid-event-names.md) | Enforce valid event naming conventions | error |
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
## Examples
|
|
58
|
+
|
|
59
|
+
### Before
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
// ❌ Multiple issues
|
|
63
|
+
postHog.capture('user clicked button', {
|
|
64
|
+
user_id: '123', // snake_case instead of camelCase
|
|
65
|
+
ButtonName: 'Submit', // PascalCase
|
|
66
|
+
});
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### After
|
|
70
|
+
|
|
71
|
+
```js
|
|
72
|
+
// ✅ Following best practices
|
|
73
|
+
const EVENTS = {
|
|
74
|
+
BUTTON_CLICKED: 'button_clicked',
|
|
75
|
+
PAGE_VIEWED: 'page_viewed',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
postHog.capture(EVENTS.BUTTON_CLICKED, {
|
|
79
|
+
userId: '123',
|
|
80
|
+
buttonName: 'Submit',
|
|
81
|
+
page: 'checkout',
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
postHog.capture(EVENTS.PAGE_VIEWED, {
|
|
85
|
+
pagePath: '/checkout',
|
|
86
|
+
referrer: document.referrer,
|
|
87
|
+
});
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
### Running Tests
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
pnpm test
|
|
96
|
+
# or
|
|
97
|
+
pnpm test:watch
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Testing Locally
|
|
101
|
+
|
|
102
|
+
To test the plugin in your project before publishing:
|
|
103
|
+
|
|
104
|
+
1. In this directory, run:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
npm link
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
2. In your project directory:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
npm link @jtl-software/eslint-plugin-posthog
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
3. Add the plugin to your ESLint config as shown above
|
|
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
|
+
## License
|
|
133
|
+
|
|
134
|
+
MIT
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# consistent-property-naming
|
|
2
|
+
|
|
3
|
+
Enforce consistent property naming convention (camelCase) in PostHog capture calls.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule enforces camelCase naming for all properties passed to `postHog.capture()` calls. Consistent naming conventions make it easier to query and analyze events in PostHog.
|
|
8
|
+
|
|
9
|
+
## Examples
|
|
10
|
+
|
|
11
|
+
### ❌ Incorrect
|
|
12
|
+
|
|
13
|
+
```js
|
|
14
|
+
postHog.capture('user_action', {
|
|
15
|
+
user_id: '123', // snake_case
|
|
16
|
+
ProductName: 'Test', // PascalCase
|
|
17
|
+
'user-email': 'test@example.com', // kebab-case
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
### ✅ Correct
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
postHog.capture('user_action', {
|
|
25
|
+
userId: '123',
|
|
26
|
+
productName: 'Test',
|
|
27
|
+
userEmail: 'test@example.com',
|
|
28
|
+
});
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why?
|
|
32
|
+
|
|
33
|
+
- **Consistency**: Makes queries and analysis easier
|
|
34
|
+
- **Schema validation**: Aligns with code property naming conventions
|
|
35
|
+
- **Discoverability**: Properties with consistent naming are easier to find
|
|
36
|
+
|
|
37
|
+
## When Not To Use It
|
|
38
|
+
|
|
39
|
+
If your team has established a different naming convention (e.g., snake_case) across all tracking implementations, you may want to disable this rule.
|
|
40
|
+
|
|
41
|
+
## Related Rules
|
|
42
|
+
|
|
43
|
+
- [require-event-schema](./require-event-schema.md)
|
|
44
|
+
- [no-literal-event-names](./no-literal-event-names.md)
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# valid-event-names
|
|
2
|
+
|
|
3
|
+
Enforce valid event naming conventions for PostHog events.
|
|
4
|
+
|
|
5
|
+
## Rule Details
|
|
6
|
+
|
|
7
|
+
This rule enforces that event names follow PostHog's recommended naming conventions:
|
|
8
|
+
|
|
9
|
+
1. **snake_case**: Event names must use lowercase with underscores
|
|
10
|
+
2. **Object-verb pattern**: Events should follow the `[object]_[verb]` format (e.g., `button_clicked`, `user_created`)
|
|
11
|
+
3. **Verb ending**: Events must end with a verb to describe the action
|
|
12
|
+
|
|
13
|
+
## Examples
|
|
14
|
+
|
|
15
|
+
### ❌ Incorrect
|
|
16
|
+
|
|
17
|
+
```js
|
|
18
|
+
// Not snake_case (camelCase)
|
|
19
|
+
postHog.capture('buttonClicked', { userId: '123' });
|
|
20
|
+
|
|
21
|
+
// Not snake_case (PascalCase)
|
|
22
|
+
postHog.capture('ButtonClicked', { userId: '123' });
|
|
23
|
+
|
|
24
|
+
// Not snake_case (kebab-case)
|
|
25
|
+
postHog.capture('button-clicked', { userId: '123' });
|
|
26
|
+
|
|
27
|
+
// Too short (missing object or verb)
|
|
28
|
+
postHog.capture('clicked', { userId: '123' });
|
|
29
|
+
postHog.capture('button', { userId: '123' });
|
|
30
|
+
|
|
31
|
+
// Doesn't end with verb
|
|
32
|
+
postHog.capture('button_color', { color: 'red' });
|
|
33
|
+
postHog.capture('user_profile', { userId: '123' });
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### ✅ Correct
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
// Past tense verbs
|
|
40
|
+
postHog.capture('button_clicked', { userId: '123' });
|
|
41
|
+
postHog.capture('user_created', { userId: '123' });
|
|
42
|
+
postHog.capture('page_viewed', { path: '/home' });
|
|
43
|
+
|
|
44
|
+
// Present participle
|
|
45
|
+
postHog.capture('video_playing', { videoId: '456' });
|
|
46
|
+
postHog.capture('data_loading', { count: 10 });
|
|
47
|
+
|
|
48
|
+
// Present tense
|
|
49
|
+
postHog.capture('button_click', { buttonId: 'submit' });
|
|
50
|
+
postHog.capture('page_view', { path: '/about' });
|
|
51
|
+
postHog.capture('user_login', { method: 'oauth' });
|
|
52
|
+
postHog.capture('form_submit', { formId: 'contact' });
|
|
53
|
+
|
|
54
|
+
// With category prefix (optional)
|
|
55
|
+
postHog.capture('account:settings_updated', { field: 'email' });
|
|
56
|
+
postHog.capture('signup:button_clicked', { step: 1 });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## Verb Patterns
|
|
60
|
+
|
|
61
|
+
The rule recognizes several verb patterns:
|
|
62
|
+
|
|
63
|
+
### Endings
|
|
64
|
+
|
|
65
|
+
- **-ed**: Past tense (clicked, created, viewed, updated)
|
|
66
|
+
- **-ing**: Present participle (clicking, creating, viewing)
|
|
67
|
+
- **-e**: Present tense ending in 'e' (create, delete, update)
|
|
68
|
+
- **-ize/-ise**: Verbs (initialize, customize)
|
|
69
|
+
- **-ate**: Verbs (validate, activate)
|
|
70
|
+
- **-ify**: Verbs (notify, verify)
|
|
71
|
+
|
|
72
|
+
### Common Verbs
|
|
73
|
+
|
|
74
|
+
The rule includes a list of common present-tense verbs like: click, view, submit, open, close, add, remove, start, stop, play, pause, load, save, send, login, logout, etc.
|
|
75
|
+
|
|
76
|
+
## Category Prefix (Optional)
|
|
77
|
+
|
|
78
|
+
The rule supports PostHog's enhanced framework with optional category prefixes:
|
|
79
|
+
|
|
80
|
+
```js
|
|
81
|
+
postHog.capture('category:object_action', { ... });
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Examples:
|
|
85
|
+
|
|
86
|
+
- `account_settings:password_changed`
|
|
87
|
+
- `signup_flow:form_submitted`
|
|
88
|
+
- `checkout:payment_completed`
|
|
89
|
+
|
|
90
|
+
## Why?
|
|
91
|
+
|
|
92
|
+
- **Consistency**: Makes events easier to find and filter in PostHog
|
|
93
|
+
- **Best practice**: Follows PostHog's official recommendations
|
|
94
|
+
- **Clarity**: Object-verb pattern clearly describes what happened
|
|
95
|
+
- **Analysis**: Consistent naming enables better querying and grouping
|
|
96
|
+
|
|
97
|
+
## When Not To Use It
|
|
98
|
+
|
|
99
|
+
If your team has established a different event naming convention and can't migrate, you may want to disable this rule. However, following PostHog's recommendations will make your analytics more maintainable.
|
|
100
|
+
|
|
101
|
+
## Related Rules
|
|
102
|
+
|
|
103
|
+
- [no-literal-event-names](./no-literal-event-names.md)
|
|
104
|
+
- [consistent-property-naming](./consistent-property-naming.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@jtl-software/eslint-plugin-posthog",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "ESLint rules for PostHog event tracking best practices",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"eslint",
|
|
9
|
+
"eslint-plugin",
|
|
10
|
+
"posthog",
|
|
11
|
+
"analytics",
|
|
12
|
+
"event-tracking"
|
|
13
|
+
],
|
|
14
|
+
"author": "JTL Platform Team",
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"peerDependencies": {
|
|
17
|
+
"eslint": "^9.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@eslint/js": "^9.0.0",
|
|
21
|
+
"eslint": "^9.0.0",
|
|
22
|
+
"prettier": "^3.7.4",
|
|
23
|
+
"typescript-eslint": "^8.0.0",
|
|
24
|
+
"vitest": "^3.0.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=18.0.0"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"test": "vitest run",
|
|
31
|
+
"test:watch": "vitest",
|
|
32
|
+
"lint": "eslint src tests",
|
|
33
|
+
"format": "prettier --write .",
|
|
34
|
+
"format:check": "prettier --check ."
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import consistentPropertyNaming from './rules/consistent-property-naming.js';
|
|
2
|
+
import validEventNames from './rules/valid-event-names.js';
|
|
3
|
+
|
|
4
|
+
const plugin = {
|
|
5
|
+
meta: {
|
|
6
|
+
name: '@jtl-software/eslint-plugin-posthog',
|
|
7
|
+
version: '0.1.0',
|
|
8
|
+
},
|
|
9
|
+
rules: {
|
|
10
|
+
'consistent-property-naming': consistentPropertyNaming,
|
|
11
|
+
'valid-event-names': validEventNames,
|
|
12
|
+
},
|
|
13
|
+
configs: {
|
|
14
|
+
recommended: {
|
|
15
|
+
plugins: ['posthog'],
|
|
16
|
+
rules: {
|
|
17
|
+
'posthog/consistent-property-naming': 'error',
|
|
18
|
+
'posthog/valid-event-names': 'error',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default plugin;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce consistent property naming convention (camelCase) in PostHog events
|
|
3
|
+
* @author JTL Platform Team
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
'Enforce consistent property naming convention (camelCase) in PostHog capture calls',
|
|
12
|
+
recommended: true,
|
|
13
|
+
},
|
|
14
|
+
messages: {
|
|
15
|
+
notCamelCase: 'Property "{{property}}" should use camelCase naming convention',
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
/**
|
|
22
|
+
* Check if a string is in camelCase
|
|
23
|
+
* @param {string} str - The string to check
|
|
24
|
+
* @returns {boolean}
|
|
25
|
+
*/
|
|
26
|
+
function isCamelCase(str) {
|
|
27
|
+
// Allow camelCase: starts with lowercase, can contain uppercase letters and numbers
|
|
28
|
+
// No underscores or hyphens allowed
|
|
29
|
+
return /^[a-z][a-zA-Z0-9]*$/.test(str);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Check if node is a PostHog capture call
|
|
34
|
+
* @param {Object} node - AST node
|
|
35
|
+
* @returns {boolean}
|
|
36
|
+
*/
|
|
37
|
+
function isPostHogCapture(node) {
|
|
38
|
+
return (
|
|
39
|
+
node.type === 'CallExpression' &&
|
|
40
|
+
node.callee.type === 'MemberExpression' &&
|
|
41
|
+
node.callee.property.name === 'capture' &&
|
|
42
|
+
node.callee.object.name === 'postHog'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
CallExpression(node) {
|
|
48
|
+
if (!isPostHogCapture(node)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Second argument should be the properties object
|
|
53
|
+
const propertiesArg = node.arguments[1];
|
|
54
|
+
if (!propertiesArg || propertiesArg.type !== 'ObjectExpression') {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Check each property in the object
|
|
59
|
+
propertiesArg.properties.forEach((prop) => {
|
|
60
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
61
|
+
const propertyName = prop.key.name;
|
|
62
|
+
if (!isCamelCase(propertyName)) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: prop.key,
|
|
65
|
+
messageId: 'notCamelCase',
|
|
66
|
+
data: {
|
|
67
|
+
property: propertyName,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Enforce valid event naming conventions for PostHog events
|
|
3
|
+
* @author JTL Platform Team
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'suggestion',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Enforce snake_case and object-verb pattern for PostHog event names',
|
|
11
|
+
recommended: true,
|
|
12
|
+
},
|
|
13
|
+
messages: {
|
|
14
|
+
notSnakeCase: 'Event name "{{eventName}}" should use snake_case naming convention',
|
|
15
|
+
missingVerb:
|
|
16
|
+
'Event name "{{eventName}}" should end with a verb (e.g., "object_clicked", "user_created", "page_view")',
|
|
17
|
+
tooShort:
|
|
18
|
+
'Event name "{{eventName}}" should follow the [object][verb] pattern with at least two parts',
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
/**
|
|
25
|
+
* Check if a string is in snake_case
|
|
26
|
+
* @param {string} str - The string to check
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isSnakeCase(str) {
|
|
30
|
+
// Allow snake_case: lowercase letters, numbers, and underscores
|
|
31
|
+
// Can also start with a category prefix like "category:object_action"
|
|
32
|
+
return /^[a-z][a-z0-9_:]*$/.test(str);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Check if the last word in an event name looks like a verb
|
|
37
|
+
* @param {string} eventName - The event name to check
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
function endsWithVerb(eventName) {
|
|
41
|
+
// Remove category prefix if present (e.g., "account:button_clicked" -> "button_clicked")
|
|
42
|
+
const nameWithoutCategory = eventName.includes(':') ? eventName.split(':')[1] : eventName;
|
|
43
|
+
|
|
44
|
+
// Split by underscore and get the last word
|
|
45
|
+
const words = nameWithoutCategory.split('_');
|
|
46
|
+
const lastWord = words[words.length - 1];
|
|
47
|
+
|
|
48
|
+
// Check for common verb patterns
|
|
49
|
+
const verbPatterns = [/ed$/, /ing$/, /ize$/, /ise$/, /ate$/, /ify$/];
|
|
50
|
+
|
|
51
|
+
// Common present tense verbs without specific patterns
|
|
52
|
+
const commonVerbs = [
|
|
53
|
+
'click',
|
|
54
|
+
'view',
|
|
55
|
+
'submit',
|
|
56
|
+
'open',
|
|
57
|
+
'close',
|
|
58
|
+
'add',
|
|
59
|
+
'remove',
|
|
60
|
+
'start',
|
|
61
|
+
'stop',
|
|
62
|
+
'play',
|
|
63
|
+
'pause',
|
|
64
|
+
'load',
|
|
65
|
+
'save',
|
|
66
|
+
'send',
|
|
67
|
+
'get',
|
|
68
|
+
'post',
|
|
69
|
+
'put',
|
|
70
|
+
'patch',
|
|
71
|
+
'delete',
|
|
72
|
+
'fetch',
|
|
73
|
+
'push',
|
|
74
|
+
'pull',
|
|
75
|
+
'set',
|
|
76
|
+
'reset',
|
|
77
|
+
'clear',
|
|
78
|
+
'show',
|
|
79
|
+
'hide',
|
|
80
|
+
'toggle',
|
|
81
|
+
'select',
|
|
82
|
+
'deselect',
|
|
83
|
+
'check',
|
|
84
|
+
'uncheck',
|
|
85
|
+
'enable',
|
|
86
|
+
'disable',
|
|
87
|
+
'expand',
|
|
88
|
+
'collapse',
|
|
89
|
+
'scroll',
|
|
90
|
+
'hover',
|
|
91
|
+
'focus',
|
|
92
|
+
'blur',
|
|
93
|
+
'drag',
|
|
94
|
+
'drop',
|
|
95
|
+
'upload',
|
|
96
|
+
'download',
|
|
97
|
+
'export',
|
|
98
|
+
'import',
|
|
99
|
+
'print',
|
|
100
|
+
'copy',
|
|
101
|
+
'paste',
|
|
102
|
+
'cut',
|
|
103
|
+
'undo',
|
|
104
|
+
'redo',
|
|
105
|
+
'refresh',
|
|
106
|
+
'reload',
|
|
107
|
+
'login',
|
|
108
|
+
'logout',
|
|
109
|
+
'signup',
|
|
110
|
+
'signin',
|
|
111
|
+
'signout',
|
|
112
|
+
'create',
|
|
113
|
+
'update',
|
|
114
|
+
'archive',
|
|
115
|
+
'unarchive',
|
|
116
|
+
'like',
|
|
117
|
+
'share',
|
|
118
|
+
'subscribe',
|
|
119
|
+
'unsubscribe',
|
|
120
|
+
'favorite',
|
|
121
|
+
'unfavorite',
|
|
122
|
+
'mute',
|
|
123
|
+
'unmute',
|
|
124
|
+
'change',
|
|
125
|
+
'complete',
|
|
126
|
+
'execute',
|
|
127
|
+
'terminate',
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
return (
|
|
131
|
+
verbPatterns.some((pattern) => pattern.test(lastWord)) || commonVerbs.includes(lastWord)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if event name follows [object][verb] pattern
|
|
137
|
+
* @param {string} eventName - The event name to check
|
|
138
|
+
* @returns {boolean}
|
|
139
|
+
*/
|
|
140
|
+
function hasObjectVerbPattern(eventName) {
|
|
141
|
+
// Remove category prefix if present
|
|
142
|
+
const nameWithoutCategory = eventName.includes(':') ? eventName.split(':')[1] : eventName;
|
|
143
|
+
|
|
144
|
+
// Should have at least 2 parts (object + verb)
|
|
145
|
+
const parts = nameWithoutCategory.split('_');
|
|
146
|
+
return parts.length >= 2;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Check if node is a PostHog capture call
|
|
151
|
+
* @param {Object} node - AST node
|
|
152
|
+
* @returns {boolean}
|
|
153
|
+
*/
|
|
154
|
+
function isPostHogCapture(node) {
|
|
155
|
+
return (
|
|
156
|
+
node.type === 'CallExpression' &&
|
|
157
|
+
node.callee.type === 'MemberExpression' &&
|
|
158
|
+
node.callee.property.name === 'capture' &&
|
|
159
|
+
node.callee.object.name === 'postHog'
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Track identifiers that refer to postHog.capture
|
|
164
|
+
const captureAliases = new Set();
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Check if a CallExpression node is a capture call (direct or aliased)
|
|
168
|
+
* @param {Object} node - AST node
|
|
169
|
+
* @returns {boolean}
|
|
170
|
+
*/
|
|
171
|
+
function isCaptureCall(node) {
|
|
172
|
+
if (node.type !== 'CallExpression') {
|
|
173
|
+
return false;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Direct postHog.capture()
|
|
177
|
+
if (isPostHogCapture(node)) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Aliased or imported capture()
|
|
182
|
+
if (node.callee.type === 'Identifier' && captureAliases.has(node.callee.name)) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return false;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Validate an event name argument
|
|
191
|
+
* @param {Object} eventNameArg - The event name argument node
|
|
192
|
+
*/
|
|
193
|
+
function validateEventName(eventNameArg) {
|
|
194
|
+
// Only check literal strings (constants will be checked at their definition)
|
|
195
|
+
if (eventNameArg.type === 'Literal' && typeof eventNameArg.value === 'string') {
|
|
196
|
+
const eventName = eventNameArg.value;
|
|
197
|
+
|
|
198
|
+
// Check snake_case
|
|
199
|
+
if (!isSnakeCase(eventName)) {
|
|
200
|
+
context.report({
|
|
201
|
+
node: eventNameArg,
|
|
202
|
+
messageId: 'notSnakeCase',
|
|
203
|
+
data: { eventName },
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check object-verb pattern (at least 2 parts)
|
|
209
|
+
if (!hasObjectVerbPattern(eventName)) {
|
|
210
|
+
context.report({
|
|
211
|
+
node: eventNameArg,
|
|
212
|
+
messageId: 'tooShort',
|
|
213
|
+
data: { eventName },
|
|
214
|
+
});
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Check that it ends with a verb
|
|
219
|
+
if (!endsWithVerb(eventName)) {
|
|
220
|
+
context.report({
|
|
221
|
+
node: eventNameArg,
|
|
222
|
+
messageId: 'missingVerb',
|
|
223
|
+
data: { eventName },
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Also check if it's an Identifier (constant reference)
|
|
229
|
+
if (eventNameArg.type === 'Identifier') {
|
|
230
|
+
// Find the constant definition
|
|
231
|
+
const scope = context.sourceCode.getScope(eventNameArg);
|
|
232
|
+
const variable = scope.variables.find((v) => v.name === eventNameArg.name);
|
|
233
|
+
|
|
234
|
+
if (variable && variable.defs.length > 0) {
|
|
235
|
+
const def = variable.defs[0];
|
|
236
|
+
if (def.node.init && def.node.init.type === 'Literal') {
|
|
237
|
+
const eventName = def.node.init.value;
|
|
238
|
+
|
|
239
|
+
if (typeof eventName === 'string') {
|
|
240
|
+
// Check snake_case
|
|
241
|
+
if (!isSnakeCase(eventName)) {
|
|
242
|
+
context.report({
|
|
243
|
+
node: def.node.init,
|
|
244
|
+
messageId: 'notSnakeCase',
|
|
245
|
+
data: { eventName },
|
|
246
|
+
});
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Check object-verb pattern
|
|
251
|
+
if (!hasObjectVerbPattern(eventName)) {
|
|
252
|
+
context.report({
|
|
253
|
+
node: def.node.init,
|
|
254
|
+
messageId: 'tooShort',
|
|
255
|
+
data: { eventName },
|
|
256
|
+
});
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Check that it ends with a verb
|
|
261
|
+
if (!endsWithVerb(eventName)) {
|
|
262
|
+
context.report({
|
|
263
|
+
node: def.node.init,
|
|
264
|
+
messageId: 'missingVerb',
|
|
265
|
+
data: { eventName },
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
// Track imports of capture from posthog packages
|
|
276
|
+
ImportDeclaration(node) {
|
|
277
|
+
if (
|
|
278
|
+
node.source.value === 'posthog-js' ||
|
|
279
|
+
node.source.value === 'posthog' ||
|
|
280
|
+
node.source.value.startsWith('@posthog/')
|
|
281
|
+
) {
|
|
282
|
+
node.specifiers.forEach((specifier) => {
|
|
283
|
+
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'capture') {
|
|
284
|
+
captureAliases.add(specifier.local.name);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
|
|
290
|
+
// Track variable assignments of postHog.capture
|
|
291
|
+
VariableDeclarator(node) {
|
|
292
|
+
if (
|
|
293
|
+
node.init &&
|
|
294
|
+
node.init.type === 'MemberExpression' &&
|
|
295
|
+
node.init.object.name === 'postHog' &&
|
|
296
|
+
node.init.property.name === 'capture' &&
|
|
297
|
+
node.id.type === 'Identifier'
|
|
298
|
+
) {
|
|
299
|
+
captureAliases.add(node.id.name);
|
|
300
|
+
}
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
CallExpression(node) {
|
|
304
|
+
if (!isCaptureCall(node)) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// First argument should be the event name
|
|
309
|
+
const eventNameArg = node.arguments[0];
|
|
310
|
+
if (!eventNameArg) {
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
validateEventName(eventNameArg);
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
},
|
|
318
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../../src/rules/consistent-property-naming.js';
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
ecmaVersion: 2022,
|
|
7
|
+
sourceType: 'module',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
ruleTester.run('consistent-property-naming', rule, {
|
|
12
|
+
valid: [
|
|
13
|
+
{
|
|
14
|
+
code: `postHog.capture('event_name', { userId: '123', productName: 'Test' })`,
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
code: `postHog.capture('event_name', { count: 5, isActive: true })`,
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
code: `postHog.capture('event_name', { a: 1, bc: 2, def: 3 })`,
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
code: `// Not a PostHog call
|
|
24
|
+
someOtherFunction({ user_id: '123' })`,
|
|
25
|
+
},
|
|
26
|
+
],
|
|
27
|
+
|
|
28
|
+
invalid: [
|
|
29
|
+
{
|
|
30
|
+
code: `postHog.capture('event_name', { user_id: '123' })`,
|
|
31
|
+
errors: [
|
|
32
|
+
{
|
|
33
|
+
messageId: 'notCamelCase',
|
|
34
|
+
data: { property: 'user_id' },
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
code: `postHog.capture('event_name', { product_name: 'Test', userId: '123' })`,
|
|
40
|
+
errors: [
|
|
41
|
+
{
|
|
42
|
+
messageId: 'notCamelCase',
|
|
43
|
+
data: { property: 'product_name' },
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
code: `postHog.capture('event_name', { UserID: '123', productName: 'Test' })`,
|
|
49
|
+
errors: [
|
|
50
|
+
{
|
|
51
|
+
messageId: 'notCamelCase',
|
|
52
|
+
data: { property: 'UserID' },
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
code: `postHog.capture('event_name', {
|
|
58
|
+
userId: '123',
|
|
59
|
+
product_name: 'Test',
|
|
60
|
+
is_active: true
|
|
61
|
+
})`,
|
|
62
|
+
errors: [
|
|
63
|
+
{
|
|
64
|
+
messageId: 'notCamelCase',
|
|
65
|
+
data: { property: 'product_name' },
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
messageId: 'notCamelCase',
|
|
69
|
+
data: { property: 'is_active' },
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
});
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../../src/rules/valid-event-names.js';
|
|
3
|
+
|
|
4
|
+
const ruleTester = new RuleTester({
|
|
5
|
+
languageOptions: {
|
|
6
|
+
ecmaVersion: 2022,
|
|
7
|
+
sourceType: 'module',
|
|
8
|
+
},
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
ruleTester.run('valid-event-names', rule, {
|
|
12
|
+
valid: [
|
|
13
|
+
// snake_case with object-verb pattern (past tense)
|
|
14
|
+
{
|
|
15
|
+
code: `postHog.capture('button_clicked', { userId: '123' })`,
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
code: `postHog.capture('user_created', { userId: '123' })`,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
code: `postHog.capture('page_viewed', { path: '/home' })`,
|
|
22
|
+
},
|
|
23
|
+
// snake_case with object-verb pattern (present participle)
|
|
24
|
+
{
|
|
25
|
+
code: `postHog.capture('video_playing', { videoId: '456' })`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: `postHog.capture('data_loading', { count: 10 })`,
|
|
29
|
+
},
|
|
30
|
+
// snake_case with object-verb pattern (present tense)
|
|
31
|
+
{
|
|
32
|
+
code: `postHog.capture('button_click', { buttonId: 'submit' })`,
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
code: `postHog.capture('page_view', { path: '/about' })`,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: `postHog.capture('user_login', { method: 'oauth' })`,
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: `postHog.capture('form_submit', { formId: 'contact' })`,
|
|
42
|
+
},
|
|
43
|
+
// snake_case with category prefix
|
|
44
|
+
{
|
|
45
|
+
code: `postHog.capture('account:settings_updated', { field: 'email' })`,
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
code: `postHog.capture('signup:button_clicked', { step: 1 })`,
|
|
49
|
+
},
|
|
50
|
+
// common verb endings
|
|
51
|
+
{
|
|
52
|
+
code: `postHog.capture('user_initialize', { userId: '123' })`,
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
code: `postHog.capture('form_validate', { formId: 'signup' })`,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
code: `postHog.capture('account_activate', { accountId: '789' })`,
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
code: `postHog.capture('user_notify', { message: 'test' })`,
|
|
62
|
+
},
|
|
63
|
+
// using constants (won't be checked)
|
|
64
|
+
{
|
|
65
|
+
code: `const EVENT_NAME = 'button_clicked';
|
|
66
|
+
postHog.capture(EVENT_NAME, { userId: '123' })`,
|
|
67
|
+
},
|
|
68
|
+
// not a PostHog call
|
|
69
|
+
{
|
|
70
|
+
code: `someOtherFunction('InvalidName', { data: 'test' })`,
|
|
71
|
+
},
|
|
72
|
+
// aliased postHog.capture
|
|
73
|
+
{
|
|
74
|
+
code: `const capture = postHog.capture;
|
|
75
|
+
capture('button_clicked', { userId: '123' })`,
|
|
76
|
+
},
|
|
77
|
+
// imported capture from posthog-js
|
|
78
|
+
{
|
|
79
|
+
code: `import { capture } from 'posthog-js';
|
|
80
|
+
capture('user_created', { userId: '456' })`,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
|
|
84
|
+
invalid: [
|
|
85
|
+
// not snake_case (camelCase)
|
|
86
|
+
{
|
|
87
|
+
code: `postHog.capture('buttonClicked', { userId: '123' })`,
|
|
88
|
+
errors: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'notSnakeCase',
|
|
91
|
+
data: { eventName: 'buttonClicked' },
|
|
92
|
+
},
|
|
93
|
+
],
|
|
94
|
+
},
|
|
95
|
+
// not snake_case (PascalCase)
|
|
96
|
+
{
|
|
97
|
+
code: `postHog.capture('ButtonClicked', { userId: '123' })`,
|
|
98
|
+
errors: [
|
|
99
|
+
{
|
|
100
|
+
messageId: 'notSnakeCase',
|
|
101
|
+
data: { eventName: 'ButtonClicked' },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
// not snake_case (kebab-case)
|
|
106
|
+
{
|
|
107
|
+
code: `postHog.capture('button-clicked', { userId: '123' })`,
|
|
108
|
+
errors: [
|
|
109
|
+
{
|
|
110
|
+
messageId: 'notSnakeCase',
|
|
111
|
+
data: { eventName: 'button-clicked' },
|
|
112
|
+
},
|
|
113
|
+
],
|
|
114
|
+
},
|
|
115
|
+
// too short (missing object or verb)
|
|
116
|
+
{
|
|
117
|
+
code: `postHog.capture('clicked', { userId: '123' })`,
|
|
118
|
+
errors: [
|
|
119
|
+
{
|
|
120
|
+
messageId: 'tooShort',
|
|
121
|
+
data: { eventName: 'clicked' },
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
code: `postHog.capture('button', { userId: '123' })`,
|
|
127
|
+
errors: [
|
|
128
|
+
{
|
|
129
|
+
messageId: 'tooShort',
|
|
130
|
+
data: { eventName: 'button' },
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
// doesn't end with verb
|
|
135
|
+
{
|
|
136
|
+
code: `postHog.capture('button_color', { color: 'red' })`,
|
|
137
|
+
errors: [
|
|
138
|
+
{
|
|
139
|
+
messageId: 'missingVerb',
|
|
140
|
+
data: { eventName: 'button_color' },
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
code: `postHog.capture('user_profile', { userId: '123' })`,
|
|
146
|
+
errors: [
|
|
147
|
+
{
|
|
148
|
+
messageId: 'missingVerb',
|
|
149
|
+
data: { eventName: 'user_profile' },
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
// constant with a bad name
|
|
154
|
+
{
|
|
155
|
+
code: `const EVENT_NAME = 'BadName';
|
|
156
|
+
postHog.capture(EVENT_NAME, { userId: '123' })`,
|
|
157
|
+
errors: [
|
|
158
|
+
{
|
|
159
|
+
messageId: 'notSnakeCase',
|
|
160
|
+
data: { eventName: 'BadName' },
|
|
161
|
+
},
|
|
162
|
+
],
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
code: `const EVENT_NAME = 'button_color';
|
|
166
|
+
postHog.capture(EVENT_NAME, { userId: '123' })`,
|
|
167
|
+
errors: [
|
|
168
|
+
{
|
|
169
|
+
messageId: 'missingVerb',
|
|
170
|
+
data: { eventName: 'button_color' },
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
// aliased postHog.capture with bad event name
|
|
175
|
+
{
|
|
176
|
+
code: `const capture = postHog.capture;
|
|
177
|
+
capture('InvalidName', { data: 'test' })`,
|
|
178
|
+
errors: [
|
|
179
|
+
{
|
|
180
|
+
messageId: 'notSnakeCase',
|
|
181
|
+
data: { eventName: 'InvalidName' },
|
|
182
|
+
},
|
|
183
|
+
],
|
|
184
|
+
},
|
|
185
|
+
// imported capture with bad event name
|
|
186
|
+
{
|
|
187
|
+
code: `import { capture } from 'posthog-js';
|
|
188
|
+
capture('userClicked', { userId: '123' })`,
|
|
189
|
+
errors: [
|
|
190
|
+
{
|
|
191
|
+
messageId: 'notSnakeCase',
|
|
192
|
+
data: { eventName: 'userClicked' },
|
|
193
|
+
},
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
});
|
package/tests/setup.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vitest test setup
|
|
3
|
+
* Configures ESLint's RuleTester to work with Vitest
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { RuleTester } from 'eslint';
|
|
7
|
+
import { describe, it, afterAll } from 'vitest';
|
|
8
|
+
|
|
9
|
+
// Configure RuleTester to work with Vitest
|
|
10
|
+
RuleTester.describe = describe;
|
|
11
|
+
RuleTester.it = it;
|
|
12
|
+
RuleTester.itOnly = it.only;
|
|
13
|
+
RuleTester.afterAll = afterAll;
|
package/vitest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
test: {
|
|
5
|
+
globals: true,
|
|
6
|
+
environment: 'node',
|
|
7
|
+
setupFiles: ['./tests/setup.js'],
|
|
8
|
+
coverage: {
|
|
9
|
+
provider: 'v8',
|
|
10
|
+
reporter: ['text', 'json', 'html'],
|
|
11
|
+
exclude: ['node_modules/', 'tests/', '*.config.js'],
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
});
|