Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 127 additions & 0 deletions src/android/android-wizard-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/* Android (Kotlin) wizard using posthog-agent with PostHog MCP */
import type { WizardOptions } from '../utils/types';
import type { FrameworkConfig } from '../lib/framework-config';
import { Integration } from '../lib/constants';
import fg from 'fast-glob';
import * as fs from 'node:fs';
import * as path from 'node:path';
import {
getKotlinVersion,
getKotlinVersionBucket,
getMinSdkVersion,
} from './utils';

type AndroidContext = {
kotlinVersion?: string;
};

export const ANDROID_AGENT_CONFIG: FrameworkConfig<AndroidContext> = {
metadata: {
name: 'Android (Kotlin)',
integration: Integration.android,
beta: true,
docsUrl: 'https://posthog.com/docs/libraries/android',
gatherContext: (options: WizardOptions) => {
const kotlinVersion = getKotlinVersion(options);
return Promise.resolve({ kotlinVersion });
},
},

detection: {
packageName: 'posthog-android',
packageDisplayName: 'Android (Kotlin)',
usesPackageJson: false,
getVersion: () => undefined,
getVersionBucket: (version: string) => getKotlinVersionBucket(version),
// This is actually pretty high for a minimum, but android apis aren't super stable.
minimumVersion: '21.0.0',
getInstalledVersion: (options: WizardOptions) => getMinSdkVersion(options),
detect: async (options) => {
const { installDir } = options;

// Strategy 1: Check for build.gradle(.kts) with Android plugin
for (const name of ['build.gradle', 'build.gradle.kts']) {
const buildGradlePath = path.join(installDir, name);
if (fs.existsSync(buildGradlePath)) {
const content = fs.readFileSync(buildGradlePath, 'utf-8');
if (
content.includes('com.android.application') ||
content.includes('com.android.library') ||
content.includes('com.android.tools.build:gradle')
) {
return true;
}
}
}

// Strategy 2: Check for AndroidManifest.xml with Kotlin source files
// This could be an issue if we have Flutter in the mix, but we'll figure that out later.
const manifestFiles = await fg('**/AndroidManifest.xml', {
cwd: installDir,
ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'],
});

if (manifestFiles.length > 0) {
const kotlinFiles = await fg('**/*.kt', {
cwd: installDir,
ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'],
});
if (kotlinFiles.length > 0) {
return true;
}
}

return false;
},
},

environment: {
uploadToHosting: false,
getEnvVars: (apiKey: string, host: string) => ({
POSTHOG_API_KEY: apiKey,
POSTHOG_HOST: host,
}),
},

analytics: {
getTags: (context) => ({
...(context.kotlinVersion
? { kotlinVersion: getKotlinVersionBucket(context.kotlinVersion) }
: {}),
}),
},

prompts: {
projectTypeDetection:
'This is an Android/Kotlin project. Look for build.gradle or build.gradle.kts files, AndroidManifest.xml, and Kotlin source files (.kt) to confirm.',
packageInstallation:
'Add the PostHog Android SDK dependency to the app-level build.gradle(.kts) file. Use implementation("com.posthog:posthog-android:<VERSION>"). Check the existing dependency format (Groovy vs Kotlin DSL) and match it.',
getAdditionalContextLines: (context) => {
const lines = [
`Framework docs ID: android (use posthog://docs/frameworks/android for documentation)`,
];

if (context.kotlinVersion) {
lines.push(`Kotlin version: ${context.kotlinVersion}`);
}

return lines;
},
},

ui: {
successMessage: 'PostHog integration complete',
estimatedDurationMinutes: 5,
getOutroChanges: () => [
`Analyzed your Android project structure`,
`Added the PostHog Android SDK dependency`,
`Configured PostHog initialization in your Application class`,
`Added event capture and user identification`,
],
getOutroNextSteps: () => [
'Build and run your app to see PostHog in action',
'Visit your PostHog dashboard to see incoming events',
'Check out the PostHog Android docs for advanced features like feature flags and session replay',
],
},
};
65 changes: 65 additions & 0 deletions src/android/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import type { WizardOptions } from '../utils/types';
import { createVersionBucket } from '../utils/semver';
import fg from 'fast-glob';
import * as fs from 'node:fs';
import * as path from 'node:path';

const IGNORE_PATTERNS = ['**/build/**', '**/.gradle/**', '**/node_modules/**'];

/**
* Extract minSdk from the app-level build.gradle(.kts).
* Returns the value as a semver-like string (e.g. "24" from minSdk = 24).
*/
export async function getMinSdkVersion(
options: WizardOptions,
): Promise<string | undefined> {
const { installDir } = options;

const buildFiles = await fg(['**/build.gradle', '**/build.gradle.kts'], {
cwd: installDir,
ignore: IGNORE_PATTERNS,
});

for (const file of buildFiles) {
try {
const content = fs.readFileSync(path.join(installDir, file), 'utf-8');
// Match: minSdk = 24, minSdkVersion 21, minSdkVersion = 21
const match = content.match(/minSdk(?:Version)?\s*=?\s*(\d+)/);
if (match) return match[1];
} catch {
continue;
}
}

return undefined;
}

export const getKotlinVersionBucket = createVersionBucket();

/**
* Read the root or app-level build.gradle(.kts) and extract the Kotlin version.
*/
export function getKotlinVersion(options: WizardOptions): string | undefined {
const { installDir } = options;

for (const name of [
'build.gradle',
'build.gradle.kts',
'gradle/libs.versions.toml',
]) {
const filePath = path.join(installDir, name);
if (!fs.existsSync(filePath)) continue;

const content = fs.readFileSync(filePath, 'utf-8');

// build.gradle: kotlinVersion = "2.0.21" or ext.kotlin_version = '1.9.0'
const match = content.match(/kotlin[_-]?[Vv]ersion\s*=\s*["']([^"']+)["']/);
if (match) return match[1];

// libs.versions.toml: kotlin = "2.0.21"
const tomlMatch = content.match(/^kotlin\s*=\s*["']([^"']+)["']/m);
if (tomlMatch) return tomlMatch[1];
}

return undefined;
}
1 change: 1 addition & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum Integration {
laravel = 'laravel',
sveltekit = 'sveltekit',
swift = 'swift',
android = 'android',

// Language fallbacks
python = 'python',
Expand Down
2 changes: 2 additions & 0 deletions src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { FASTAPI_AGENT_CONFIG } from '../fastapi/fastapi-wizard-agent';
import { LARAVEL_AGENT_CONFIG } from '../laravel/laravel-wizard-agent';
import { SVELTEKIT_AGENT_CONFIG } from '../svelte/svelte-wizard-agent';
import { SWIFT_AGENT_CONFIG } from '../swift/swift-wizard-agent';
import { ANDROID_AGENT_CONFIG } from '../android/android-wizard-agent';
import { PYTHON_AGENT_CONFIG } from '../python/python-wizard-agent';

export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
Expand All @@ -33,5 +34,6 @@ export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
[Integration.laravel]: LARAVEL_AGENT_CONFIG,
[Integration.sveltekit]: SVELTEKIT_AGENT_CONFIG,
[Integration.swift]: SWIFT_AGENT_CONFIG,
[Integration.android]: ANDROID_AGENT_CONFIG,
[Integration.python]: PYTHON_AGENT_CONFIG,
};
Loading