Skip to content
Draft
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
153 changes: 153 additions & 0 deletions src/frameworks/java/java-wizard-agent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* Java (server) wizard using posthog-agent with PostHog MCP */
import fg from 'fast-glob';
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { WizardRunOptions } from '@utils/types';
import type { FrameworkConfig } from '@lib/framework-config';
import { detectJavaPackageManagers } from '@lib/detection/package-manager';
import { Integration } from '@lib/constants';

type JavaContext = {
buildTool?: 'maven' | 'gradle';
};

const GRADLE_FILES = [
'build.gradle',
'build.gradle.kts',
'settings.gradle',
'settings.gradle.kts',
];

const ANDROID_GRADLE_MARKERS = [
'com.android.application',
'com.android.library',
'com.android.tools.build:gradle',
];

const KMP_GRADLE_MARKERS = [
'kotlin("multiplatform")',
'org.jetbrains.kotlin.multiplatform',
'kotlin-multiplatform',
];

function readRootGradleFiles(installDir: string): string[] {
return GRADLE_FILES.map((name) => path.join(installDir, name))
.filter((p) => fs.existsSync(p))
.map((p) => fs.readFileSync(p, 'utf-8'));
}

function getBuildTool(installDir: string): 'maven' | 'gradle' | undefined {
if (fs.existsSync(path.join(installDir, 'pom.xml'))) {
return 'maven';
}
if (readRootGradleFiles(installDir).length > 0) {
return 'gradle';
}
return undefined;
}

export const JAVA_AGENT_CONFIG: FrameworkConfig<JavaContext> = {
metadata: {
name: 'Java',
integration: Integration.java,
docsUrl: 'https://posthog.com/docs/libraries/java',
gatherContext: (options: WizardRunOptions) => {
const buildTool = getBuildTool(options.installDir);
return Promise.resolve({ buildTool });
},
},

detection: {
packageName: 'posthog-server',
packageDisplayName: 'Java',
usesPackageJson: false,
getVersion: () => undefined,
// Maven projects (pom.xml) are unambiguously JVM backends. Gradle
// projects are only claimed when they are neither Android nor KMP —
// those detectors are ordered earlier, and this re-check keeps a
// direct java detect() call from ever claiming their projects.
detect: async (options) => {
const { installDir } = options;

// pubspec.yaml means Flutter — its gradle subtree is not a JVM backend.
if (fs.existsSync(path.join(installDir, 'pubspec.yaml'))) {
return false;
}

if (fs.existsSync(path.join(installDir, 'pom.xml'))) {
return true;
}

const gradleContents = readRootGradleFiles(installDir);
if (gradleContents.length === 0) {
return false;
}
const combined = gradleContents.join('\n');
if (
[...ANDROID_GRADLE_MARKERS, ...KMP_GRADLE_MARKERS].some((marker) =>
combined.includes(marker),
)
) {
return false;
}

const manifestFiles = await fg('**/AndroidManifest.xml', {
cwd: installDir,
ignore: ['**/build/**', '**/node_modules/**', '**/.gradle/**'],
});
return manifestFiles.length === 0;
},
detectPackageManager: detectJavaPackageManagers,
},

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

analytics: {
getTags: (context) => ({
buildTool: context.buildTool || 'unknown',
}),
},

prompts: {
projectTypeDetection:
'This is a Java server project. Look for pom.xml (Maven) or build.gradle/build.gradle.kts (Gradle), src/main/java/, and the framework in use (Spring Boot, Quarkus, Micronaut, plain servlets) to confirm.',
packageInstallation:
'Neither Maven nor Gradle has a single add command. Add the com.posthog:posthog-server dependency (pinned to the latest release on Maven Central) to pom.xml or build.gradle(.kts), matching the existing dependency format, then run `mvn install` or `gradle build` to resolve it.',
getAdditionalContextLines: (context) => {
const lines = [
`Framework docs ID: java (use posthog://docs/frameworks/java for documentation)`,
'Use the posthog-server SDK (com.posthog:posthog-server) — posthog-android is a different library and must not be used on the server.',
'Create one PostHog client per process with PostHogConfig.builder(...) and PostHog.with(config); call close() during graceful shutdown, and flush() before returning in serverless handlers.',
];
if (context.buildTool) {
lines.push(`Build tool: ${context.buildTool}`);
}
return lines;
},
},

ui: {
successMessage: 'PostHog integration complete',
estimatedDurationMinutes: 5,
getOutroChanges: (context) => [
'Analyzed your Java project structure',
`Added the posthog-server dependency to your ${
context.buildTool === 'maven' ? 'pom.xml' : 'Gradle build'
}`,
'Initialized a shared PostHog client configured from environment variables',
'Instrumented meaningful server events with posthog.capture',
],
getOutroNextSteps: () => [
'Run your Java service and trigger the instrumented code paths',
'Visit your PostHog dashboard to see incoming events',
'Use posthog.capture(distinctId, eventName, options) to track custom events',
'Keep posthog.close() in your graceful shutdown path so queued events flush',
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ const INTEGRATION_ENTRIES = [
{ id: 'integration-ruby', framework: 'ruby' },
{ id: 'integration-elixir' },
{ id: 'integration-go' },
{ id: 'integration-java', framework: 'java' },
{ id: 'integration-swift', framework: 'swift' },
{ id: 'integration-kmp', framework: 'kmp' },
{ id: 'integration-flutter' },
Expand Down
2 changes: 2 additions & 0 deletions src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ export enum Integration {
swift = 'swift',
android = 'android',
rails = 'rails',
// Must stay after kmp/swift/android: those claim gradle projects first.
java = 'java',

// Language fallbacks. Keep javascriptNode last: it matches any package.json.
python = 'python',
Expand Down
86 changes: 86 additions & 0 deletions src/lib/detection/__tests__/framework.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { detectFramework } from '@lib/detection/framework';
import { Integration } from '@lib/constants';
import { ANDROID_AGENT_CONFIG } from '../../../frameworks/android/android-wizard-agent';
import { KMP_AGENT_CONFIG } from '../../../frameworks/kmp/kmp-wizard-agent';
import { JAVA_AGENT_CONFIG } from '../../../frameworks/java/java-wizard-agent';

/** A throwaway project dir seeded with the given files. */
function makeProject(files: Record<string, string>): string {
Expand Down Expand Up @@ -54,6 +55,18 @@ describe('Integration enum order (drives first-match detection)', () => {
expect(order[order.length - 1]).toBe(Integration.javascriptNode);
});

test('java is ordered after kmp, swift, and android (they claim gradle projects first)', () => {
for (const specific of [
Integration.kmp,
Integration.swift,
Integration.android,
]) {
expect(order.indexOf(Integration.java)).toBeGreaterThan(
order.indexOf(specific),
);
}
});

test('kmp is ordered before android and swift (more specific detection wins)', () => {
// A KMP project also looks like an Android/Swift project, so KMP must be
// checked first for first-match detection to resolve it correctly.
Expand Down Expand Up @@ -137,6 +150,27 @@ describe('detectFramework (end-to-end over real project dirs)', () => {
);
});

test('a Maven project resolves to java', async () => {
const opts = project({
'pom.xml':
'<project><groupId>com.example</groupId><artifactId>app</artifactId></project>',
'src/main/java/App.java': 'class App {}',
});
await expect(detectFramework(opts.installDir)).resolves.toBe(
Integration.java,
);
});

test('a plain Gradle JVM project resolves to java, not android', async () => {
const opts = project({
'build.gradle': `plugins { id 'java' id 'org.springframework.boot' }`,
'src/main/java/App.java': 'class App {}',
});
await expect(detectFramework(opts.installDir)).resolves.toBe(
Integration.java,
);
});

test('a Flutter project is not claimed by anything', async () => {
const opts = project({
'pubspec.yaml': 'name: my_flutter_app\nenvironment:\n sdk: ^3.0.0\n',
Expand Down Expand Up @@ -169,6 +203,58 @@ describe('android detect', () => {
});
});

describe('java detect', () => {
const detect = JAVA_AGENT_CONFIG.detection.detect;

test('claims a Maven project', async () => {
const opts = project({ 'pom.xml': '<project></project>' });
await expect(detect(opts)).resolves.toBe(true);
});

test('claims a plain Gradle JVM project', async () => {
const opts = project({
'build.gradle.kts': `plugins { java }\ndependencies {}`,
});
await expect(detect(opts)).resolves.toBe(true);
});

test('does not claim an Android gradle project', async () => {
const opts = project({
'build.gradle': `plugins { id 'com.android.application' }`,
});
await expect(detect(opts)).resolves.toBe(false);
});

test('does not claim a gradle project with an AndroidManifest.xml subtree', async () => {
const opts = project({
'build.gradle': `plugins { id 'java' }`,
'app/src/main/AndroidManifest.xml': '<manifest/>',
});
await expect(detect(opts)).resolves.toBe(false);
});

test('does not claim a KMP project', async () => {
const opts = project({
'build.gradle.kts': `plugins { kotlin("multiplatform") }`,
});
await expect(detect(opts)).resolves.toBe(false);
});

test('does not claim a Flutter project', async () => {
const opts = project({
'pubspec.yaml': 'name: my_flutter_app\n',
'android/build.gradle': `plugins { id 'com.android.application' }`,
'settings.gradle': 'include ":app"',
});
await expect(detect(opts)).resolves.toBe(false);
});

test('does not claim a project with no build files', async () => {
const opts = project({ 'src/main/java/App.java': 'class App {}' });
await expect(detect(opts)).resolves.toBe(false);
});
});

describe('kmp detect', () => {
const detect = KMP_AGENT_CONFIG.detection.detect;

Expand Down
28 changes: 28 additions & 0 deletions src/lib/detection/__tests__/package-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
composerPackageManager,
swiftPackageManager,
gradlePackageManager,
detectJavaPackageManagers,
} from '@lib/detection/package-manager';

vi.mock('../../../utils/debug');
Expand Down Expand Up @@ -179,6 +180,33 @@ describe('detectPythonPackageManagers', () => {
});
});

// ---------------------------------------------------------------------------
// Java detection
// ---------------------------------------------------------------------------

describe('detectJavaPackageManagers', () => {
let tmpDir: string;

beforeEach(() => {
tmpDir = makeTmpDir();
});
afterEach(() => cleanup(tmpDir));

it('detects maven via pom.xml', async () => {
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '<project></project>');
const result = await detectJavaPackageManagers(tmpDir);
expect(result.primary?.name).toBe('maven');
expect(result.primary?.installCommand).toBe('mvn install');
expect(result.recommendation).toContain('pom.xml');
});

it('falls back to gradle without a pom.xml', async () => {
fs.writeFileSync(path.join(tmpDir, 'build.gradle'), '');
const result = await detectJavaPackageManagers(tmpDir);
expect(result.primary?.name).toBe('gradle');
});
});

// ---------------------------------------------------------------------------
// Static helpers
// ---------------------------------------------------------------------------
Expand Down
5 changes: 3 additions & 2 deletions src/lib/detection/agentic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,12 +74,13 @@ export const PROJECT_MANIFESTS: readonly string[] = [
// Ruby / PHP
'Gemfile',
'composer.json',
// Rust / Go / Elixir / JVM / .NET: no framework targets yet, but found so
// Java
'pom.xml',
// Rust / Go / Elixir / .NET: no framework targets yet, but found so
// an existing PostHog SDK is reported (feeds self-driving's "continue" path).
'Cargo.toml',
'go.mod',
'mix.exs',
'pom.xml',
'*.csproj',
// Mobile / native
'Package.swift',
Expand Down
30 changes: 30 additions & 0 deletions src/lib/detection/package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
* current framework supplies.
*/

import * as fs from 'node:fs';
import * as path from 'node:path';
import {
detectAllPackageManagers,
type PackageManager,
Expand Down Expand Up @@ -233,3 +235,31 @@ export function gradlePackageManager(): Promise<PackageManagerInfo> {
'Add dependencies to build.gradle(.kts) using implementation().',
});
}

// ---------------------------------------------------------------------------
// Java (Maven or Gradle) helper
// ---------------------------------------------------------------------------

const MAVEN: DetectedPackageManager = {
name: 'maven',
label: 'Maven',
installCommand: 'mvn install',
};

/**
* Java backends split between Maven and Gradle; pom.xml decides.
* Defaults to Gradle when neither manifest is present.
*/
export function detectJavaPackageManagers(
installDir: string,
): Promise<PackageManagerInfo> {
if (fs.existsSync(path.join(installDir, 'pom.xml'))) {
return Promise.resolve({
detected: [MAVEN],
primary: MAVEN,
recommendation:
'Use Maven. Add the dependency to pom.xml, then run mvn install to resolve it.',
});
}
return gradlePackageManager();
}
2 changes: 2 additions & 0 deletions src/lib/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { SWIFT_AGENT_CONFIG } from '@frameworks/swift/swift-wizard-agent';
import { KMP_AGENT_CONFIG } from '@frameworks/kmp/kmp-wizard-agent';
import { ANDROID_AGENT_CONFIG } from '@frameworks/android/android-wizard-agent';
import { RAILS_AGENT_CONFIG } from '@frameworks/rails/rails-wizard-agent';
import { JAVA_AGENT_CONFIG } from '@frameworks/java/java-wizard-agent';
import { PYTHON_AGENT_CONFIG } from '@frameworks/python/python-wizard-agent';
import { RUBY_AGENT_CONFIG } from '@frameworks/ruby/ruby-wizard-agent';
import { JAVASCRIPT_NODE_AGENT_CONFIG } from '@frameworks/javascript-node/javascript-node-wizard-agent';
Expand All @@ -42,6 +43,7 @@ export const FRAMEWORK_REGISTRY: Record<Integration, FrameworkConfig> = {
[Integration.swift]: SWIFT_AGENT_CONFIG,
[Integration.android]: ANDROID_AGENT_CONFIG,
[Integration.rails]: RAILS_AGENT_CONFIG,
[Integration.java]: JAVA_AGENT_CONFIG,
[Integration.python]: PYTHON_AGENT_CONFIG,
[Integration.ruby]: RUBY_AGENT_CONFIG,
[Integration.javascriptNode]: JAVASCRIPT_NODE_AGENT_CONFIG,
Expand Down
Loading