Skip to content

Commit 8435eb9

Browse files
feat: js-web skill (#272)
1 parent 5b40933 commit 8435eb9

File tree

5 files changed

+218
-80
lines changed

5 files changed

+218
-80
lines changed
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/* Generic JavaScript Web (client-side) wizard using posthog-agent with PostHog MCP */
2+
import type { WizardOptions } from '../utils/types';
3+
import type { FrameworkConfig } from '../lib/framework-config';
4+
import { Integration } from '../lib/constants';
5+
import * as fs from 'node:fs';
6+
import * as path from 'node:path';
7+
import { hasPackageInstalled } from '../utils/package-json';
8+
import { tryGetPackageJson } from '../utils/clack-utils';
9+
import {
10+
FRAMEWORK_PACKAGES,
11+
detectJsPackageManager,
12+
detectBundler,
13+
type JavaScriptContext,
14+
} from './utils';
15+
import { detectNodePackageManagers } from '../lib/package-manager-detection';
16+
17+
export const JAVASCRIPT_WEB_AGENT_CONFIG: FrameworkConfig<JavaScriptContext> = {
18+
metadata: {
19+
name: 'JavaScript (Web)',
20+
integration: Integration.javascript_web,
21+
beta: true,
22+
docsUrl: 'https://posthog.com/docs/libraries/js',
23+
gatherContext: (options: WizardOptions) => {
24+
const packageManagerName = detectJsPackageManager(options);
25+
const hasTypeScript = fs.existsSync(
26+
path.join(options.installDir, 'tsconfig.json'),
27+
);
28+
const hasBundler = detectBundler(options);
29+
return Promise.resolve({ packageManagerName, hasTypeScript, hasBundler });
30+
},
31+
},
32+
33+
detection: {
34+
packageName: 'posthog-js',
35+
packageDisplayName: 'JavaScript (Web)',
36+
usesPackageJson: false,
37+
getVersion: () => undefined,
38+
detectPackageManager: detectNodePackageManagers,
39+
detect: async (options) => {
40+
const packageJson = await tryGetPackageJson(options);
41+
if (!packageJson) {
42+
return false;
43+
}
44+
45+
// Exclude projects with known framework packages
46+
for (const frameworkPkg of FRAMEWORK_PACKAGES) {
47+
if (hasPackageInstalled(frameworkPkg, packageJson)) {
48+
return false;
49+
}
50+
}
51+
52+
// Ensure this is actually a JS project, not just a package.json for tooling
53+
const { installDir } = options;
54+
55+
// Check for a lockfile
56+
const hasLockfile = [
57+
'package-lock.json',
58+
'yarn.lock',
59+
'pnpm-lock.yaml',
60+
'bun.lockb',
61+
'bun.lock',
62+
].some((lockfile) => fs.existsSync(path.join(installDir, lockfile)));
63+
64+
if (hasLockfile) {
65+
return true;
66+
}
67+
68+
// Fallback: check if package.json has actual dependencies
69+
const hasDeps =
70+
(packageJson.dependencies &&
71+
Object.keys(packageJson.dependencies).length > 0) ||
72+
(packageJson.devDependencies &&
73+
Object.keys(packageJson.devDependencies).length > 0);
74+
75+
return !!hasDeps;
76+
},
77+
},
78+
79+
environment: {
80+
uploadToHosting: false,
81+
getEnvVars: (apiKey: string, host: string) => ({
82+
POSTHOG_API_KEY: apiKey,
83+
POSTHOG_HOST: host,
84+
}),
85+
},
86+
87+
analytics: {
88+
getTags: (context) => {
89+
const tags: Record<string, string> = {
90+
packageManager: context.packageManagerName ?? 'unknown',
91+
};
92+
if (context.hasBundler) {
93+
tags.bundler = context.hasBundler;
94+
}
95+
return tags;
96+
},
97+
},
98+
99+
prompts: {
100+
projectTypeDetection:
101+
'This is a JavaScript/TypeScript project. Look for package.json and lockfiles (package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb) to confirm.',
102+
packageInstallation:
103+
'Look for lockfiles to determine the package manager (npm, yarn, pnpm, bun). Do not manually edit package.json.',
104+
getAdditionalContextLines: (context) => {
105+
const lines = [
106+
`Package manager: ${context.packageManagerName ?? 'unknown'}`,
107+
`Has TypeScript: ${context.hasTypeScript ? 'yes' : 'no'}`,
108+
`Framework docs ID: js (use posthog://docs/frameworks/js for documentation if available)`,
109+
`Project type: Generic JavaScript/TypeScript application (no specific framework detected)`,
110+
];
111+
112+
if (context.hasBundler) {
113+
lines.unshift(`Bundler: ${context.hasBundler}`);
114+
}
115+
116+
return lines;
117+
},
118+
},
119+
120+
ui: {
121+
successMessage: 'PostHog integration complete',
122+
estimatedDurationMinutes: 5,
123+
getOutroChanges: (context) => {
124+
const packageManagerName =
125+
context.packageManagerName ?? 'package manager';
126+
return [
127+
`Analyzed your JavaScript project structure`,
128+
`Installed the posthog-js package using ${packageManagerName}`,
129+
`Created PostHog initialization code`,
130+
`Configured autocapture, error tracking, and event capture`,
131+
];
132+
},
133+
getOutroNextSteps: () => [
134+
'Ensure posthog.init() is called before any capture calls',
135+
'Autocapture tracks clicks, form submissions, and pageviews automatically',
136+
'Use posthog.capture() for custom events and posthog.identify() for users',
137+
'NEVER send PII in event properties (no emails, names, or user content)',
138+
'Visit your PostHog dashboard to see incoming events',
139+
],
140+
},
141+
};

src/javascript-web/utils.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import * as fs from 'node:fs';
2+
import * as path from 'node:path';
3+
import { detectAllPackageManagers } from '../utils/package-manager';
4+
import type { WizardOptions } from '../utils/types';
5+
6+
export type JavaScriptContext = {
7+
packageManagerName?: string;
8+
hasTypeScript?: boolean;
9+
hasBundler?: string;
10+
};
11+
12+
/**
13+
* Packages that indicate a specific framework integration exists.
14+
* If any of these are in package.json, we should NOT match as generic JavaScript.
15+
*
16+
* When adding a new JS framework integration to the wizard,
17+
* add its detection package here too.
18+
*/
19+
export const FRAMEWORK_PACKAGES = [
20+
'next',
21+
'nuxt',
22+
'vue',
23+
'react-router',
24+
'@tanstack/react-start',
25+
'@tanstack/react-router',
26+
'react-native',
27+
'@angular/core',
28+
'astro',
29+
'@sveltejs/kit',
30+
] as const;
31+
32+
/**
33+
* Detect the JS package manager for the project by checking lockfiles.
34+
* Reuses the existing package manager detection infrastructure.
35+
*/
36+
export function detectJsPackageManager(
37+
options: Pick<WizardOptions, 'installDir'>,
38+
): string {
39+
const detected = detectAllPackageManagers(options);
40+
if (detected.length > 0) {
41+
return detected[0].label;
42+
}
43+
return 'unknown';
44+
}
45+
46+
/**
47+
* Detect the bundler used in the project by checking package.json dependencies.
48+
*/
49+
export function detectBundler(
50+
options: Pick<WizardOptions, 'installDir'>,
51+
): string | undefined {
52+
try {
53+
const content = fs.readFileSync(
54+
path.join(options.installDir, 'package.json'),
55+
'utf-8',
56+
);
57+
const pkg = JSON.parse(content);
58+
const allDeps: Record<string, string> = {
59+
...pkg.dependencies,
60+
...pkg.devDependencies,
61+
};
62+
63+
if (allDeps['vite']) return 'vite';
64+
if (allDeps['webpack']) return 'webpack';
65+
if (allDeps['esbuild']) return 'esbuild';
66+
if (allDeps['parcel']) return 'parcel';
67+
if (allDeps['rollup']) return 'rollup';
68+
return undefined;
69+
} catch {
70+
return undefined;
71+
}
72+
}

src/lib/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export enum Integration {
2424
// Language fallbacks
2525
python = 'python',
2626
ruby = 'ruby',
27+
javascript_web = 'javascript_web',
2728
}
2829
export interface Args {
2930
debug: boolean;

src/lib/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import { ANDROID_AGENT_CONFIG } from '../android/android-wizard-agent';
1919
import { RAILS_AGENT_CONFIG } from '../rails/rails-wizard-agent';
2020
import { PYTHON_AGENT_CONFIG } from '../python/python-wizard-agent';
2121
import { RUBY_AGENT_CONFIG } from '../ruby/ruby-wizard-agent';
22+
import { JAVASCRIPT_WEB_AGENT_CONFIG } from '../javascript-web/javascript-web-wizard-agent';
2223

2324
export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
2425
[Integration.nextjs]: NEXTJS_AGENT_CONFIG,
@@ -40,4 +41,5 @@ export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
4041
[Integration.rails]: RAILS_AGENT_CONFIG,
4142
[Integration.python]: PYTHON_AGENT_CONFIG,
4243
[Integration.ruby]: RUBY_AGENT_CONFIG,
44+
[Integration.javascript_web]: JAVASCRIPT_WEB_AGENT_CONFIG,
4345
};

src/python/python-wizard-agent.ts

Lines changed: 2 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Generic Python language wizard using posthog-agent with PostHog MCP */
1+
/* Generic Python wizard using posthog-agent with PostHog MCP */
22
import type { WizardOptions } from '../utils/types';
33
import type { FrameworkConfig } from '../lib/framework-config';
44
import { PYTHON_PACKAGE_INSTALLATION } from '../lib/framework-config';
@@ -169,89 +169,11 @@ export const PYTHON_AGENT_CONFIG: FrameworkConfig<PythonContext> = {
169169
? getPackageManagerName(context.packageManager)
170170
: 'unknown';
171171

172-
const lines = [
172+
return [
173173
`Package manager: ${packageManagerName}`,
174174
`Framework docs ID: python (use posthog://docs/frameworks/python for documentation)`,
175175
`Project type: Generic Python application (CLI, script, worker, data pipeline, etc.)`,
176-
``,
177-
`## CRITICAL: Python PostHog Best Practices`,
178-
``,
179-
`### 1. Use Instance-Based API (REQUIRED)`,
180-
`Always use the Posthog() class constructor instead of module-level posthog:`,
181-
``,
182-
`CORRECT:`,
183-
`from posthog import Posthog`,
184-
`posthog_client = Posthog(`,
185-
` api_key="your_api_key",`,
186-
` host="https://us.i.posthog.com",`,
187-
` debug=False,`,
188-
` enable_exception_autocapture=True, # Auto-capture exceptions`,
189-
`)`,
190-
``,
191-
`INCORRECT (DO NOT USE):`,
192-
`import posthog`,
193-
`posthog.api_key = "your_api_key" # Don't use module-level config`,
194-
``,
195-
`### 2. Enable Exception Autocapture`,
196-
`ALWAYS include enable_exception_autocapture=True in the Posthog() initialization to automatically track exceptions.`,
197-
``,
198-
`### 3. NEVER Send PII (Personally Identifiable Information)`,
199-
`DO NOT include in event properties:`,
200-
`- Email addresses`,
201-
`- Full names`,
202-
`- Phone numbers`,
203-
`- Physical addresses`,
204-
`- IP addresses`,
205-
`- Any user-generated content (messages, comments, form submissions)`,
206-
``,
207-
`SAFE event properties:`,
208-
`posthog_client.capture('contact_form_submitted', properties={`,
209-
` 'message_length': len(message), # Metadata is OK`,
210-
` 'has_email': bool(email), # Boolean flags are OK`,
211-
` 'form_type': 'contact' # Categories are OK`,
212-
`})`,
213-
``,
214-
`UNSAFE (DO NOT DO THIS):`,
215-
`posthog_client.capture('form_submitted', properties={`,
216-
` 'email': user_email, # NEVER send actual email`,
217-
` 'message': message_content, # NEVER send user content`,
218-
` 'name': user_name # NEVER send names`,
219-
`})`,
220-
``,
221-
`### 4. Implement Graceful Shutdown`,
222-
`ALWAYS call posthog_client.shutdown() when your application exits to ensure all events are flushed:`,
223-
``,
224-
`import atexit`,
225-
`atexit.register(posthog_client.shutdown) # Ensures events are sent on exit`,
226-
``,
227-
`For Django, use AppConfig.ready() to register the shutdown handler.`,
228-
`For Flask, use @app.teardown_appcontext or atexit.`,
229-
`For scripts/workers, call shutdown() explicitly or use atexit.`,
230-
``,
231-
`### 5. For Django Projects`,
232-
`Initialize PostHog in your AppConfig.ready() method:`,
233-
``,
234-
`from django.apps import AppConfig`,
235-
`from posthog import Posthog`,
236-
``,
237-
`class YourAppConfig(AppConfig):`,
238-
` posthog_client = None`,
239-
` `,
240-
` def ready(self):`,
241-
` if YourAppConfig.posthog_client is None:`,
242-
` YourAppConfig.posthog_client = Posthog(`,
243-
` settings.POSTHOG_API_KEY,`,
244-
` host=settings.POSTHOG_HOST,`,
245-
` debug=settings.DEBUG,`,
246-
` enable_exception_autocapture=True,`,
247-
` )`,
248-
` import atexit`,
249-
` atexit.register(YourAppConfig.posthog_client.shutdown)`,
250-
``,
251-
`IMPORTANT: These best practices are MANDATORY. The implementation will fail review if they are not followed.`,
252176
];
253-
254-
return lines;
255177
},
256178
},
257179

0 commit comments

Comments
 (0)