humanbehavior-js
Version:
SDK for HumanBehavior session and event recording
1,590 lines (1,361 loc) • 54 kB
text/typescript
/**
* HumanBehavior SDK Auto-Installation Wizard
*
* This wizard automatically detects the user's framework and modifies their codebase
* to integrate the SDK with minimal user intervention.
*/
import * as fs from 'fs';
import * as path from 'path';
export interface FrameworkInfo {
name: string;
type: 'react' | 'vue' | 'angular' | 'svelte' | 'nextjs' | 'nuxt' | 'remix' | 'vanilla' | 'astro' | 'gatsby' | 'node' | 'auto';
bundler?: 'vite' | 'webpack' | 'esbuild' | 'rollup';
packageManager?: 'npm' | 'yarn' | 'pnpm';
hasTypeScript?: boolean;
hasRouter?: boolean;
projectRoot?: string;
version?: string;
majorVersion?: number;
features?: {
hasReact18?: boolean;
hasVue3?: boolean;
hasNuxt3?: boolean;
hasAngularStandalone?: boolean;
hasNextAppRouter?: boolean;
hasSvelteKit?: boolean;
};
}
export interface CodeModification {
filePath: string;
action: 'create' | 'modify' | 'append';
content: string;
description: string;
}
export interface InstallationResult {
success: boolean;
framework: FrameworkInfo;
modifications: CodeModification[];
errors: string[];
nextSteps: string[];
}
export class AutoInstallationWizard {
protected apiKey: string;
protected projectRoot: string;
protected framework: FrameworkInfo | null = null;
constructor(apiKey: string, projectRoot: string = process.cwd()) {
this.apiKey = apiKey;
this.projectRoot = projectRoot;
}
/**
* Simple version comparison utility
*/
private compareVersions(version1: string, version2: string): number {
const v1Parts = version1.split('.').map(Number);
const v2Parts = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
const v1 = v1Parts[i] || 0;
const v2 = v2Parts[i] || 0;
if (v1 > v2) return 1;
if (v1 < v2) return -1;
}
return 0;
}
private isVersionGte(version: string, target: string): boolean {
return this.compareVersions(version, target) >= 0;
}
private getMajorVersion(version: string): number {
return parseInt(version.split('.')[0]) || 0;
}
/**
* Main installation method - detects framework and auto-installs
*/
async install(): Promise<InstallationResult> {
try {
// Step 1: Detect framework
this.framework = await this.detectFramework();
// Step 2: Install package
await this.installPackage();
// Step 3: Generate and apply code modifications
const modifications = await this.generateModifications();
await this.applyModifications(modifications);
// Step 4: Generate next steps
const nextSteps = this.generateNextSteps();
return {
success: true,
framework: this.framework,
modifications,
errors: [],
nextSteps
};
} catch (error) {
return {
success: false,
framework: this.framework || { name: 'unknown', type: 'vanilla' },
modifications: [],
errors: [error instanceof Error ? error.message : 'Unknown error'],
nextSteps: []
};
}
}
/**
* Detect the current framework and project setup
*/
public async detectFramework(): Promise<FrameworkInfo> {
const packageJsonPath = path.join(this.projectRoot, 'package.json');
if (!fs.existsSync(packageJsonPath)) {
return {
name: 'vanilla',
type: 'vanilla',
projectRoot: this.projectRoot
};
}
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const dependencies = {
...packageJson.dependencies,
...packageJson.devDependencies
};
// Detect framework with version information
let framework: FrameworkInfo = {
name: 'vanilla',
type: 'vanilla',
projectRoot: this.projectRoot,
features: {}
};
if (dependencies.nuxt) {
const nuxtVersion = dependencies.nuxt;
const isNuxt3 = this.isVersionGte(nuxtVersion, '3.0.0');
framework = {
name: 'nuxt',
type: 'nuxt',
version: nuxtVersion,
majorVersion: this.getMajorVersion(nuxtVersion),
hasTypeScript: !!dependencies.typescript,
hasRouter: true,
projectRoot: this.projectRoot,
features: {
hasNuxt3: isNuxt3
}
};
} else if (dependencies.next) {
const nextVersion = dependencies.next;
const isNext13 = this.isVersionGte(nextVersion, '13.0.0');
framework = {
name: 'nextjs',
type: 'nextjs',
version: nextVersion,
majorVersion: this.getMajorVersion(nextVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['@types/node'],
hasRouter: true,
projectRoot: this.projectRoot,
features: {
hasNextAppRouter: isNext13
}
};
} else if (dependencies['@remix-run/react'] || dependencies['@remix-run/dev']) {
const remixVersion = dependencies['@remix-run/react'] || dependencies['@remix-run/dev'];
framework = {
name: 'remix',
type: 'remix',
version: remixVersion,
majorVersion: this.getMajorVersion(remixVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'],
hasRouter: true,
projectRoot: this.projectRoot,
features: {}
};
} else if (dependencies.react) {
const reactVersion = dependencies.react;
const isReact18 = this.isVersionGte(reactVersion, '18.0.0');
framework = {
name: 'react',
type: 'react',
version: reactVersion,
majorVersion: this.getMajorVersion(reactVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'],
hasRouter: !!dependencies['react-router-dom'] || !!dependencies['react-router'],
projectRoot: this.projectRoot,
features: {
hasReact18: isReact18
}
};
} else if (dependencies.vue) {
const vueVersion = dependencies.vue;
const isVue3 = this.isVersionGte(vueVersion, '3.0.0');
framework = {
name: 'vue',
type: 'vue',
version: vueVersion,
majorVersion: this.getMajorVersion(vueVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['@vue/cli-service'],
hasRouter: !!dependencies['vue-router'],
projectRoot: this.projectRoot,
features: {
hasVue3: isVue3
}
};
} else if (dependencies['@angular/core']) {
const angularVersion = dependencies['@angular/core'];
const isAngular17 = this.isVersionGte(angularVersion, '17.0.0');
framework = {
name: 'angular',
type: 'angular',
version: angularVersion,
majorVersion: this.getMajorVersion(angularVersion),
hasTypeScript: true,
hasRouter: true,
projectRoot: this.projectRoot,
features: {
hasAngularStandalone: isAngular17
}
};
} else if (dependencies.svelte) {
const svelteVersion = dependencies.svelte;
const isSvelteKit = !!dependencies['@sveltejs/kit'];
framework = {
name: 'svelte',
type: 'svelte',
version: svelteVersion,
majorVersion: this.getMajorVersion(svelteVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['svelte-check'],
hasRouter: !!dependencies['svelte-routing'] || !!dependencies['@sveltejs/kit'],
projectRoot: this.projectRoot,
features: {
hasSvelteKit: isSvelteKit
}
};
} else if (dependencies.astro) {
const astroVersion = dependencies.astro;
framework = {
name: 'astro',
type: 'astro',
version: astroVersion,
majorVersion: this.getMajorVersion(astroVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['@astrojs/ts-plugin'],
hasRouter: true,
projectRoot: this.projectRoot,
features: {}
};
} else if (dependencies.gatsby) {
const gatsbyVersion = dependencies.gatsby;
framework = {
name: 'gatsby',
type: 'gatsby',
version: gatsbyVersion,
majorVersion: this.getMajorVersion(gatsbyVersion),
hasTypeScript: !!dependencies.typescript || !!dependencies['@types/react'],
hasRouter: true,
projectRoot: this.projectRoot,
features: {}
};
}
// Detect bundler
if (dependencies.vite) {
framework.bundler = 'vite';
} else if (dependencies.webpack) {
framework.bundler = 'webpack';
} else if (dependencies.esbuild) {
framework.bundler = 'esbuild';
} else if (dependencies.rollup) {
framework.bundler = 'rollup';
}
// Detect package manager
if (fs.existsSync(path.join(this.projectRoot, 'yarn.lock'))) {
framework.packageManager = 'yarn';
} else if (fs.existsSync(path.join(this.projectRoot, 'pnpm-lock.yaml'))) {
framework.packageManager = 'pnpm';
} else {
framework.packageManager = 'npm';
}
return framework;
}
/**
* Install the SDK package
*/
protected async installPackage(): Promise<void> {
const { execSync } = await import('child_process');
// Build base command
let command = this.framework?.packageManager === 'yarn'
? 'yarn add humanbehavior-js'
: this.framework?.packageManager === 'pnpm'
? 'pnpm add humanbehavior-js'
: 'npm install humanbehavior-js';
// Add legacy peer deps flag for npm to handle dependency conflicts
if (this.framework?.packageManager !== 'yarn' && this.framework?.packageManager !== 'pnpm') {
command += ' --legacy-peer-deps';
}
try {
execSync(command, { cwd: this.projectRoot, stdio: 'inherit' });
} catch (error) {
throw new Error(`Failed to install humanbehavior-js: ${error}`);
}
}
/**
* Generate code modifications based on framework
*/
protected async generateModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
switch (this.framework?.type) {
case 'react':
modifications.push(...await this.generateReactModifications());
break;
case 'nextjs':
modifications.push(...await this.generateNextJSModifications());
break;
case 'nuxt':
modifications.push(...await this.generateNuxtModifications());
break;
case 'astro':
modifications.push(...await this.generateAstroModifications());
break;
case 'gatsby':
modifications.push(...await this.generateGatsbyModifications());
break;
case 'remix':
modifications.push(...await this.generateRemixModifications());
break;
case 'vue':
modifications.push(...await this.generateVueModifications());
break;
case 'angular':
modifications.push(...await this.generateAngularModifications());
break;
case 'svelte':
modifications.push(...await this.generateSvelteModifications());
break;
default:
modifications.push(...await this.generateVanillaModifications());
}
return modifications;
}
/**
* Generate React-specific modifications
*/
private async generateReactModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Find main App component or index file
const appFile = this.findReactAppFile();
if (appFile) {
const content = fs.readFileSync(appFile, 'utf8');
const modifiedContent = this.injectReactProvider(content, appFile);
modifications.push({
filePath: appFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehaviorProvider to React app'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Next.js-specific modifications
*/
private async generateNextJSModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Check for App Router
const appLayoutFile = path.join(this.projectRoot, 'src', 'app', 'layout.tsx');
const pagesLayoutFile = path.join(this.projectRoot, 'src', 'pages', '_app.tsx');
if (fs.existsSync(appLayoutFile)) {
// Create providers.tsx file for App Router
modifications.push({
filePath: path.join(this.projectRoot, 'src', 'app', 'providers.tsx'),
action: 'create',
content: `'use client';
import { HumanBehaviorProvider } from 'humanbehavior-js/react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<HumanBehaviorProvider apiKey={process.env.NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY}>
{children}
</HumanBehaviorProvider>
);
}`,
description: 'Created providers.tsx file for Next.js App Router'
});
// Modify layout.tsx to use the provider
const content = fs.readFileSync(appLayoutFile, 'utf8');
const modifiedContent = this.injectNextJSAppRouter(content);
modifications.push({
filePath: appLayoutFile,
action: 'modify',
content: modifiedContent,
description: 'Added Providers wrapper to Next.js App Router layout'
});
} else if (fs.existsSync(pagesLayoutFile)) {
// Create providers.tsx file for Pages Router
modifications.push({
filePath: path.join(this.projectRoot, 'src', 'components', 'providers.tsx'),
action: 'create',
content: `'use client';
import { HumanBehaviorProvider } from 'humanbehavior-js/react';
export function Providers({ children }: { children: React.ReactNode }) {
return (
<HumanBehaviorProvider apiKey={process.env.NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY}>
{children}
</HumanBehaviorProvider>
);
}`,
description: 'Created providers.tsx file for Pages Router'
});
// Modify _app.tsx to use the provider
const content = fs.readFileSync(pagesLayoutFile, 'utf8');
const modifiedContent = this.injectNextJSPagesRouter(content);
modifications.push({
filePath: pagesLayoutFile,
action: 'modify',
content: modifiedContent,
description: 'Added Providers wrapper to Next.js Pages Router'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Astro-specific modifications
*/
private async generateAstroModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Create Astro component for HumanBehavior
const astroComponentPath = path.join(this.projectRoot, 'src', 'components', 'HumanBehavior.astro');
const astroComponentContent = `---
// This component will only run on the client side
---
<script>
import { HumanBehaviorTracker } from 'humanbehavior-js';
// Get API key from environment variable
const apiKey = import.meta.env.PUBLIC_HUMANBEHAVIOR_API_KEY;
console.log('HumanBehavior: API key found:', apiKey ? 'Yes' : 'No');
if (apiKey) {
try {
const tracker = HumanBehaviorTracker.init(apiKey);
console.log('HumanBehavior: Tracker initialized successfully');
// Test event to verify tracking is working
setTimeout(() => {
tracker.customEvent('astro_page_view', {
page: window.location.pathname,
framework: 'astro'
}).then(() => {
console.log('HumanBehavior: Test event sent successfully');
}).catch((error) => {
console.error('HumanBehavior: Failed to send test event:', error);
});
}, 1000);
} catch (error) {
console.error('HumanBehavior: Failed to initialize tracker:', error);
}
} else {
console.error('HumanBehavior: No API key found');
}
</script>`;
modifications.push({
filePath: astroComponentPath,
action: 'create',
content: astroComponentContent,
description: 'Created Astro component for HumanBehavior SDK'
});
// Find and update layout file
const layoutFiles = [
path.join(this.projectRoot, 'src', 'layouts', 'Layout.astro'),
path.join(this.projectRoot, 'src', 'layouts', 'layout.astro'),
path.join(this.projectRoot, 'src', 'layouts', 'BaseLayout.astro')
];
let layoutFile = null;
for (const file of layoutFiles) {
if (fs.existsSync(file)) {
layoutFile = file;
break;
}
}
if (layoutFile) {
const content = fs.readFileSync(layoutFile, 'utf8');
const modifiedContent = this.injectAstroLayout(content);
modifications.push({
filePath: layoutFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior component to Astro layout'
});
}
// Add environment variable
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Nuxt-specific modifications
*/
private async generateNuxtModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Create plugin file for Nuxt (in app directory)
const pluginFile = path.join(this.projectRoot, 'app', 'plugins', 'humanbehavior.client.ts');
modifications.push({
filePath: pluginFile,
action: 'create',
content: `import { HumanBehaviorTracker } from 'humanbehavior-js';
export default defineNuxtPlugin(() => {
const config = useRuntimeConfig();
// Initialize HumanBehavior SDK (client-side only)
if (typeof window !== 'undefined') {
const apiKey = config.public.humanBehaviorApiKey;
console.log('HumanBehavior: API key:', apiKey ? 'present' : 'missing');
if (apiKey) {
try {
const tracker = HumanBehaviorTracker.init(apiKey);
console.log('HumanBehavior: Tracker initialized successfully');
} catch (error) {
console.error('HumanBehavior: Failed to initialize tracker:', error);
}
} else {
console.error('HumanBehavior: No API key found in runtime config');
}
}
});`,
description: 'Created Nuxt plugin for HumanBehavior SDK in app directory'
});
// Create environment configuration
const nuxtConfigFile = path.join(this.projectRoot, 'nuxt.config.ts');
if (fs.existsSync(nuxtConfigFile)) {
const content = fs.readFileSync(nuxtConfigFile, 'utf8');
const modifiedContent = this.injectNuxtConfig(content);
modifications.push({
filePath: nuxtConfigFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior runtime config to Nuxt config'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Remix-specific modifications
*/
private async generateRemixModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Find root.tsx file
const rootFile = path.join(this.projectRoot, 'app', 'root.tsx');
if (fs.existsSync(rootFile)) {
const content = fs.readFileSync(rootFile, 'utf8');
const modifiedContent = this.injectRemixProvider(content);
modifications.push({
filePath: rootFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehaviorProvider to Remix root component'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Vue-specific modifications
*/
private async generateVueModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Find main.js or main.ts
const mainFile = this.findVueMainFile();
if (mainFile) {
const content = fs.readFileSync(mainFile, 'utf8');
const modifiedContent = this.injectVuePlugin(content);
modifications.push({
filePath: mainFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehaviorPlugin to Vue app'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Angular-specific modifications
*/
private async generateAngularModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Check for modern Angular (standalone components) vs legacy (NgModule)
const appModuleFile = path.join(this.projectRoot, 'src', 'app', 'app.module.ts');
const appComponentFile = path.join(this.projectRoot, 'src', 'app', 'app.ts');
const mainFile = path.join(this.projectRoot, 'src', 'main.ts');
const isModernAngular = fs.existsSync(appComponentFile) && !fs.existsSync(appModuleFile);
if (isModernAngular) {
// Modern Angular 17+ with standalone components
if (fs.existsSync(mainFile)) {
const content = fs.readFileSync(mainFile, 'utf8');
const modifiedContent = this.injectAngularStandaloneInit(content);
modifications.push({
filePath: mainFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior initialization to Angular main.ts'
});
}
} else if (fs.existsSync(appModuleFile)) {
// Legacy Angular with NgModule
const content = fs.readFileSync(appModuleFile, 'utf8');
const modifiedContent = this.injectAngularModule(content);
modifications.push({
filePath: appModuleFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehaviorModule to Angular app'
});
}
// Handle Angular environment file (legacy structure)
const envFile = path.join(this.projectRoot, 'src', 'environments', 'environment.ts');
if (fs.existsSync(envFile)) {
const content = fs.readFileSync(envFile, 'utf8');
if (!content.includes('humanBehaviorApiKey')) {
const modifiedContent = content.replace(
/export const environment = {([\s\S]*?)};/,
`export const environment = {
$1,
humanBehaviorApiKey: process.env['HUMANBEHAVIOR_API_KEY'] || ''
};`
);
modifications.push({
filePath: envFile,
action: 'modify',
content: modifiedContent,
description: 'Added API key to Angular environment'
});
}
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Svelte-specific modifications
*/
private async generateSvelteModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Check for SvelteKit
const svelteConfigFile = path.join(this.projectRoot, 'svelte.config.js');
const isSvelteKit = fs.existsSync(svelteConfigFile);
if (isSvelteKit) {
// SvelteKit - create layout file
const layoutFile = path.join(this.projectRoot, 'src', 'routes', '+layout.svelte');
if (fs.existsSync(layoutFile)) {
const content = fs.readFileSync(layoutFile, 'utf8');
const modifiedContent = this.injectSvelteKitLayout(content);
modifications.push({
filePath: layoutFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior store to SvelteKit layout'
});
}
} else {
// Regular Svelte - modify main file
const mainFile = this.findSvelteMainFile();
if (mainFile) {
const content = fs.readFileSync(mainFile, 'utf8');
const modifiedContent = this.injectSvelteStore(content);
modifications.push({
filePath: mainFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior store to Svelte app'
});
}
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate vanilla JS/TS modifications
*/
private async generateVanillaModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Find HTML file to inject script
const htmlFile = this.findHTMLFile();
if (htmlFile) {
const content = fs.readFileSync(htmlFile, 'utf8');
const modifiedContent = this.injectVanillaScript(content);
modifications.push({
filePath: htmlFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior CDN script to HTML file'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Generate Gatsby-specific modifications
*/
private async generateGatsbyModifications(): Promise<CodeModification[]> {
const modifications: CodeModification[] = [];
// Modify or create gatsby-browser.js for Gatsby
const gatsbyBrowserFile = path.join(this.projectRoot, 'gatsby-browser.js');
if (fs.existsSync(gatsbyBrowserFile)) {
const content = fs.readFileSync(gatsbyBrowserFile, 'utf8');
const modifiedContent = this.injectGatsbyBrowser(content);
modifications.push({
filePath: gatsbyBrowserFile,
action: 'modify',
content: modifiedContent,
description: 'Added HumanBehavior initialization to Gatsby browser'
});
} else {
// Create gatsby-browser.js if it doesn't exist
modifications.push({
filePath: gatsbyBrowserFile,
action: 'create',
content: `import { HumanBehaviorTracker } from 'humanbehavior-js';
export const onClientEntry = () => {
console.log('Gatsby browser entry point loaded');
const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY;
console.log('API Key found:', apiKey ? 'Yes' : 'No');
if (apiKey) {
const tracker = HumanBehaviorTracker.init(apiKey);
console.log('HumanBehavior SDK initialized for Gatsby');
} else {
console.log('No API key found in environment variables');
}
};`,
description: 'Created gatsby-browser.js with HumanBehavior initialization'
});
}
// Create or append to environment file
modifications.push(this.createEnvironmentModification(this.framework!));
return modifications;
}
/**
* Apply modifications to the codebase
*/
protected async applyModifications(modifications: CodeModification[]): Promise<void> {
for (const modification of modifications) {
try {
const dir = path.dirname(modification.filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
switch (modification.action) {
case 'create':
fs.writeFileSync(modification.filePath, modification.content);
break;
case 'modify':
fs.writeFileSync(modification.filePath, modification.content);
break;
case 'append':
fs.appendFileSync(modification.filePath, '\n' + modification.content);
break;
}
} catch (error) {
throw new Error(`Failed to apply modification to ${modification.filePath}: ${error}`);
}
}
}
/**
* Generate next steps for the user
*/
private generateNextSteps(): string[] {
const steps = [
'✅ SDK installed and configured automatically!',
'🚀 Your app is now tracking user behavior',
'📊 View sessions in your HumanBehavior dashboard',
'🔧 Customize tracking in your code as needed'
];
if (this.framework?.type === 'react' || this.framework?.type === 'nextjs') {
steps.push('💡 Use the useHumanBehavior() hook to track custom events');
}
return steps;
}
// Helper methods for file detection and content injection
private findReactAppFile(): string | null {
const possibleFiles = [
'src/App.jsx', 'src/App.js', 'src/App.tsx', 'src/App.ts',
'src/index.js', 'src/index.tsx', 'src/main.js', 'src/main.tsx'
];
for (const file of possibleFiles) {
const fullPath = path.join(this.projectRoot, file);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
private findVueMainFile(): string | null {
const possibleFiles = [
'src/main.js', 'src/main.ts', 'src/main.jsx', 'src/main.tsx'
];
for (const file of possibleFiles) {
const fullPath = path.join(this.projectRoot, file);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
private findSvelteMainFile(): string | null {
const possibleFiles = [
'src/main.js', 'src/main.ts', 'src/main.svelte'
];
for (const file of possibleFiles) {
const fullPath = path.join(this.projectRoot, file);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
private findHTMLFile(): string | null {
const possibleFiles = ['index.html', 'public/index.html', 'dist/index.html'];
for (const file of possibleFiles) {
const fullPath = path.join(this.projectRoot, file);
if (fs.existsSync(fullPath)) {
return fullPath;
}
}
return null;
}
private injectReactProvider(content: string, filePath: string): string {
const isTypeScript = filePath.endsWith('.tsx') || filePath.endsWith('.ts');
// Check if already has HumanBehaviorProvider
if (content.includes('HumanBehaviorProvider')) {
return content;
}
// Determine the correct environment variable syntax based on bundler
const isVite = this.framework?.bundler === 'vite';
const envVar = isVite
? 'import.meta.env.VITE_HUMANBEHAVIOR_API_KEY!'
: 'process.env.HUMANBEHAVIOR_API_KEY!';
const importStatement = `import { HumanBehaviorProvider } from 'humanbehavior-js/react';`;
// Enhanced parsing for React 18+ features
const hasReact18 = this.framework?.features?.hasReact18;
// Handle different React patterns
if (content.includes('function App()') || content.includes('const App =')) {
// Add import statement
let modifiedContent = content.replace(
/(import.*?from.*?['"]react['"];?)/,
`$1\n${importStatement}`
);
// If no React import found, add it at the top
if (!modifiedContent.includes(importStatement)) {
modifiedContent = `${importStatement}\n\n${modifiedContent}`;
}
// Wrap the App component return with HumanBehaviorProvider
modifiedContent = modifiedContent.replace(
/(return\s*\([\s\S]*?\)\s*;)/,
`return (
<HumanBehaviorProvider apiKey={${envVar}}>
$1
</HumanBehaviorProvider>
);`
);
return modifiedContent;
}
// Handle React 18+ createRoot pattern
if (hasReact18 && content.includes('createRoot')) {
let modifiedContent = content.replace(
/(import.*?from.*?['"]react['"];?)/,
`$1\n${importStatement}`
);
if (!modifiedContent.includes(importStatement)) {
modifiedContent = `${importStatement}\n\n${modifiedContent}`;
}
// Wrap the root render with HumanBehaviorProvider
modifiedContent = modifiedContent.replace(
/(root\.render\s*\([\s\S]*?\)\s*;)/,
`root.render(
<HumanBehaviorProvider apiKey={${envVar}}>
$1
</HumanBehaviorProvider>
);`
);
return modifiedContent;
}
// Fallback: simple injection
return `${importStatement}\n\n${content}`;
}
private injectNextJSAppRouter(content: string): string {
if (content.includes('Providers')) {
return content;
}
const importStatement = `import { Providers } from './providers';`;
// First, add the import statement
let modifiedContent = content.replace(
/export default function RootLayout/,
`${importStatement}\n\nexport default function RootLayout`
);
// Then wrap the body content with Providers
// Use a more specific approach to handle the body content
modifiedContent = modifiedContent.replace(
/<body([^>]*)>([\s\S]*?)<\/body>/,
(match, bodyAttrs, bodyContent) => {
// Trim whitespace and newlines from bodyContent
const trimmedContent = bodyContent.trim();
return `<body${bodyAttrs}>
<Providers>
${trimmedContent}
</Providers>
</body>`;
}
);
return modifiedContent;
}
private injectNextJSPagesRouter(content: string): string {
if (content.includes('Providers')) {
return content;
}
const importStatement = `import { Providers } from '../components/providers';`;
return content.replace(
/function MyApp/,
`${importStatement}\n\nfunction MyApp`
).replace(
/return \(([\s\S]*?)\);/,
`return (
<Providers>
$1
</Providers>
);`
);
}
private injectRemixProvider(content: string): string {
if (content.includes('HumanBehaviorProvider')) {
return content;
}
const importStatement = `import { HumanBehaviorProvider, createHumanBehaviorLoader } from 'humanbehavior-js/remix';`;
const useLoaderDataImport = `import { useLoaderData } from "@remix-run/react";`;
// Add imports more robustly
let modifiedContent = content;
// Add HumanBehaviorProvider import - find the last import and add after it
if (!content.includes('HumanBehaviorProvider')) {
const lastImportIndex = modifiedContent.lastIndexOf('import');
if (lastImportIndex !== -1) {
const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex);
if (nextLineIndex !== -1) {
modifiedContent = modifiedContent.slice(0, nextLineIndex + 1) +
importStatement + '\n' +
modifiedContent.slice(nextLineIndex + 1);
} else {
modifiedContent = modifiedContent + '\n' + importStatement;
}
} else {
modifiedContent = importStatement + '\n' + modifiedContent;
}
}
// Add useLoaderData import - find the last import and add after it
if (!content.includes('useLoaderData')) {
const lastImportIndex = modifiedContent.lastIndexOf('import');
if (lastImportIndex !== -1) {
const nextLineIndex = modifiedContent.indexOf('\n', lastImportIndex);
if (nextLineIndex !== -1) {
modifiedContent = modifiedContent.slice(0, nextLineIndex + 1) +
useLoaderDataImport + '\n' +
modifiedContent.slice(nextLineIndex + 1);
} else {
modifiedContent = modifiedContent + '\n' + useLoaderDataImport;
}
} else {
modifiedContent = useLoaderDataImport + '\n' + modifiedContent;
}
}
// Add loader function before the App component
if (!content.includes('export const loader')) {
modifiedContent = modifiedContent.replace(
/export default function App\(\)/,
`export const loader = createHumanBehaviorLoader();
export default function App()`
);
}
// Wrap the App component content with HumanBehaviorProvider
modifiedContent = modifiedContent.replace(
/export default function App\(\) \{[\s\S]*?return \(([\s\S]*?)\);[\s\S]*?\}/,
`export default function App() {
const data = useLoaderData<typeof loader>();
return (
<HumanBehaviorProvider apiKey={data.ENV.HUMANBEHAVIOR_API_KEY}>
$1
</HumanBehaviorProvider>
);
}`
);
return modifiedContent;
}
private injectVuePlugin(content: string): string {
if (content.includes('HumanBehaviorPlugin')) {
return content;
}
const importStatement = `import { HumanBehaviorPlugin } from 'humanbehavior-js/vue';`;
// Enhanced Vue 3 support with version detection
const hasVue3 = this.framework?.features?.hasVue3;
if (hasVue3) {
// Vue 3 with Composition API
const pluginUsage = `app.use(HumanBehaviorPlugin, {
apiKey: import.meta.env.VITE_HUMANBEHAVIOR_API_KEY
});`;
let modifiedContent = content;
// Add import statement
if (!content.includes(importStatement)) {
modifiedContent = content.replace(
/(import.*?from.*?['"]vue['"];?)/,
`$1\n${importStatement}`
);
// If no Vue import found, add it at the top
if (!modifiedContent.includes(importStatement)) {
modifiedContent = `${importStatement}\n\n${modifiedContent}`;
}
}
// Handle createApp pattern
if (content.includes('createApp')) {
modifiedContent = modifiedContent.replace(
/(app\.mount\(.*?\))/,
`${pluginUsage}\n\n$1`
);
}
return modifiedContent;
} else {
// Vue 2 with Options API
const pluginUsage = `Vue.use(HumanBehaviorPlugin, {
apiKey: process.env.VUE_APP_HUMANBEHAVIOR_API_KEY
});`;
let modifiedContent = content;
// Add import statement
if (!content.includes(importStatement)) {
modifiedContent = content.replace(
/(import.*?from.*?['"]vue['"];?)/,
`$1\n${importStatement}`
);
if (!modifiedContent.includes(importStatement)) {
modifiedContent = `${importStatement}\n\n${modifiedContent}`;
}
}
// Handle new Vue pattern
if (content.includes('new Vue')) {
modifiedContent = modifiedContent.replace(
/(new Vue\(.*?\))/,
`${pluginUsage}\n\n$1`
);
}
return modifiedContent;
}
}
private injectAngularModule(content: string): string {
if (content.includes('HumanBehaviorModule')) {
return content;
}
const importStatement = `import { HumanBehaviorModule } from 'humanbehavior-js/angular';`;
const environmentImport = `import { environment } from '../environments/environment';`;
// Add environment import if not present
let modifiedContent = content;
if (!content.includes('environment')) {
modifiedContent = content.replace(
/import.*from.*['"]@angular/,
`${environmentImport}\n$&`
);
}
return modifiedContent.replace(
/imports:\s*\[([\s\S]*?)\]/,
`imports: [
$1,
HumanBehaviorModule.forRoot({
apiKey: environment.humanBehaviorApiKey
})
]`
).replace(
/import.*from.*['"]@angular/,
`$&\n${importStatement}`
);
}
private injectAngularStandaloneInit(content: string): string {
if (content.includes('initializeHumanBehavior')) {
return content;
}
const importStatement = `import { initializeHumanBehavior } from 'humanbehavior-js/angular';`;
// Add import at the top
let modifiedContent = content.replace(
/import.*from.*['"]@angular/,
`${importStatement}\n$&`
);
// Add initialization after bootstrapApplication
modifiedContent = modifiedContent.replace(
/(bootstrapApplication\([^}]+\}?\)(?:\s*\.catch[^;]+;)?)/,
`$1
// Initialize HumanBehavior SDK (client-side only)
if (typeof window !== 'undefined') {
const tracker = initializeHumanBehavior(
'${this.apiKey}'
);
}`
);
return modifiedContent;
}
private injectSvelteStore(content: string): string {
if (content.includes('humanBehaviorStore')) {
return content;
}
const importStatement = `import { humanBehaviorStore } from 'humanbehavior-js/svelte';`;
const initCode = `humanBehaviorStore.init(process.env.PUBLIC_HUMANBEHAVIOR_API_KEY || '');`;
return `${importStatement}\n${initCode}\n\n${content}`;
}
private injectSvelteKitLayout(content: string): string {
if (content.includes('humanBehaviorStore')) {
return content;
}
const importStatement = `import { humanBehaviorStore } from 'humanbehavior-js/svelte';`;
const envImport = `import { PUBLIC_HUMANBEHAVIOR_API_KEY } from '$env/static/public';`;
const initCode = `humanBehaviorStore.init(PUBLIC_HUMANBEHAVIOR_API_KEY || '');`;
// Add to script section - handle different script tag patterns
if (content.includes('<script lang="ts">')) {
return content.replace(
/<script lang="ts">/,
`<script lang="ts">\n\t${envImport}\n\t${importStatement}\n\t${initCode}`
);
} else if (content.includes('<script>')) {
return content.replace(
/<script>/,
`<script>\n\t${envImport}\n\t${importStatement}\n\t${initCode}`
);
} else if (content.includes('<script context="module">')) {
return content.replace(
/<script\s+context="module">/,
`<script context="module">\n\t${envImport}\n\t${importStatement}\n\t${initCode}`
);
} else {
// If no script tag found, add one at the beginning
return content.replace(
/<svelte:head>/,
`<script lang="ts">\n\t${envImport}\n\t${importStatement}\n\t${initCode}\n</script>\n\n<svelte:head>`
);
}
}
private injectVanillaScript(content: string): string {
if (content.includes('humanbehavior-js')) {
return content;
}
const cdnScript = `<script src="https://unpkg.com/humanbehavior-js@latest/dist/index.min.js"></script>`;
const initScript = `<script>
// Initialize HumanBehavior SDK
// Note: For vanilla HTML, the API key must be hardcoded since env vars aren't available
const tracker = HumanBehaviorTracker.init('${this.apiKey}');
</script>`;
return content.replace(
/<\/head>/,
` ${cdnScript}\n ${initScript}\n</head>`
);
}
/**
* Inject Astro layout with HumanBehavior component
*/
private injectAstroLayout(content: string): string {
// Check if HumanBehavior component is already imported
if (content.includes('HumanBehavior') || content.includes('humanbehavior-js')) {
return content; // Already has HumanBehavior
}
// Add import inside frontmatter if not present
let modifiedContent = content;
if (!content.includes('import HumanBehavior')) {
const importStatement = 'import HumanBehavior from \'../components/HumanBehavior.astro\';';
const frontmatterEndIndex = content.indexOf('---', 3);
if (frontmatterEndIndex !== -1) {
// Insert import inside frontmatter, before the closing ---
modifiedContent = content.slice(0, frontmatterEndIndex) + '\n' + importStatement + '\n' + content.slice(frontmatterEndIndex);
} else {
// No frontmatter, add at the very beginning
modifiedContent = '---\n' + importStatement + '\n---\n\n' + content;
}
}
// Find the closing </body> tag and add HumanBehavior component before it
const bodyCloseIndex = modifiedContent.lastIndexOf('</body>');
if (bodyCloseIndex === -1) {
// No body tag found, append to end
return modifiedContent + '\n\n<HumanBehavior />';
}
// Add component before closing body tag
return modifiedContent.slice(0, bodyCloseIndex) + ' <HumanBehavior />\n' + modifiedContent.slice(bodyCloseIndex);
}
private injectNuxtConfig(content: string): string {
if (content.includes('humanBehaviorApiKey')) {
return content;
}
// Enhanced Nuxt 3 support with version detection
const hasNuxt3 = this.framework?.features?.hasNuxt3;
if (hasNuxt3) {
// Nuxt 3 with runtime config
return content.replace(
/export default defineNuxtConfig\(\{/,
`export default defineNuxtConfig({
runtimeConfig: {
public: {
humanBehaviorApiKey: process.env.NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY
}
},`
);
} else {
// Nuxt 2 with env config
return content.replace(
/export default \{/,
`export default {
env: {
humanBehaviorApiKey: process.env.HUMANBEHAVIOR_API_KEY
},`
);
}
}
private injectGatsbyLayout(content: string): string {
if (content.includes('HumanBehavior')) {
return content;
}
const importStatement = `import HumanBehavior from './HumanBehavior';`;
const componentUsage = `<HumanBehavior apiKey={process.env.GATSBY_HUMANBEHAVIOR_API_KEY || ''} />`;
// Add import at the top
let modifiedContent = content.replace(
/import.*from.*['"]\./,
`${importStatement}\n$&`
);
// Add component before closing body tag
modifiedContent = modifiedContent.replace(
/(\s*<\/body>)/,
`\n ${componentUsage}\n$1`
);
return modifiedContent;
}
private injectGatsbyBrowser(content: string): string {
if (content.includes('HumanBehaviorTracker')) {
return content;
}
const importStatement = `import { HumanBehaviorTracker } from 'humanbehavior-js';`;
const initCode = `
// Initialize HumanBehavior SDK
export const onClientEntry = () => {
console.log('Gatsby browser entry point loaded');
const apiKey = process.env.GATSBY_HUMANBEHAVIOR_API_KEY;
console.log('API Key found:', apiKey ? 'Yes' : 'No');
if (apiKey) {
const tracker = HumanBehaviorTracker.init(apiKey);
console.log('HumanBehavior SDK initialized for Gatsby');
} else {
console.log('No API key found in environment variables');
}
};`;
// If the file already has content, add the import and init code
if (content.trim()) {
return `${importStatement}${initCode}\n\n${content}`;
} else {
// If file is empty, just return the new content
return `${importStatement}${initCode}`;
}
}
/**
* Helper method to find the best environment file for a framework
*/
private findBestEnvFile(framework: FrameworkInfo): { filePath: string; envVarName: string } {
const possibleEnvFiles = [
'.env.local',
'.env.development.local',
'.env.development',
'.env.local.development',
'.env',
'.env.production',
'.env.staging'
];
// Framework-specific environment variable names
const getEnvVarName = (framework: FrameworkInfo) => {
// Handle React+Vite specifically
if (framework.type === 'react' && framework.bundler === 'vite') {
return 'VITE_HUMANBEHAVIOR_API_KEY';
}
// Framework-specific mappings
const envVarNames = {
react: 'HUMANBEHAVIOR_API_KEY',
nextjs: 'NEXT_PUBLIC_HUMANBEHAVIOR_API_KEY',
vue: 'VITE_HUMANBEHAVIOR_API_KEY',
svelte: 'PUBLIC_HUMANBEHAVIOR_API_KEY',
angular: 'HUMANBEHAVIOR_API_KEY',
nuxt: 'NUXT_PUBLIC_HUMANBEHAVIOR_API_KEY',
remix: 'HUMANBEHAVIOR_API_KEY',
vanilla: 'HUMANBEHAVIOR_API_KEY',
astro: 'PUBLIC_HUMANBEHAVIOR_API_KEY',
gatsby: 'GATSBY_HUMANBEHAVIOR_API_KEY',
node: 'HUMANBEHAVIOR_API_KEY',
auto: 'HUMANBEHAVIOR_API_KEY'
};
return envVarNames[framework.type] || 'HUMANBEHAVIOR_API_KEY';
};
const envVarName = getEnvVarName(framework);
// Check for existing files
for (const envFile of possibleEnvFiles) {
const fullPath = path.join(this.projectRoot, envFile);
if (fs.existsSync(fullPath)) {
return { filePath: fullPath, envVarName };
}
}
// Framework-specific default file creation
const defaultFiles = {
react: '.env.local',
nextjs: '.env.local',
vue: '.env.local',
svelte: '.env',
angular: '.env',
nuxt: '.env',
remix: '.env.local',
vanilla: '.env',
astro: '.env',
gatsby: '.env.development',
node: '.env',
auto: '.env'
};
const defaultFile = defaultFiles[framework.type] || '.env';
return {
filePath: path.join(this.projectRoot, defaultFile),
envVarName
};
}
/**
* Helper method to create or append to environment files
*/
private createEnvironmentModification(framework: FrameworkInfo): CodeModification {
const { filePath, envVarName } = this.findBestEnvFile(framework);
// Clean the API key to prevent formatting issues
const cleanApiKey = this.apiKey.trim();
if (fs.existsSync(filePath)) {
// Check if the variable already exists
const content = fs.readFileSync(filePath, 'utf8');
if (content.includes(envVarName)) {
// Variable exists, don't modify
return {
filePath,
action: 'modify',
content: content, // No change
description: `API key already exists in ${path.basename(filePath)}`
};
} else {
// Append to existing file
return {
filePath,
action: 'append',
content: `\n${envVarName}=${cleanApiKey}`,
description: `Added API key to existing ${path.basename(filePath)}`
};
}
} else {
// Create new file
return {
filePath,
action: 'create',
content: `${envVarName}=${cleanApiKey}`,
description: `Created ${path.basename(filePath)} with API key`
};
}
}
}
/**
* Browser-based auto-installation wizard
*/
export class BrowserAutoInstallationWizard {
private apiKey: string;
constructor(apiKey: string) {
this.apiKey = apiKey;
}
async install(): Promise<InstallationResult> {
try {
// Detect framework in browser
const framework = this.detectFram