一键导入
onboard-kotlin-integration
Generates a Kotlin integration for the rudder-sdk-kotlin repo in a step-by-step manner by referencing the corresponding Java integration.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
菜单
Generates a Kotlin integration for the rudder-sdk-kotlin repo in a step-by-step manner by referencing the corresponding Java integration.
用 Codex 或 Claude 帮你安装 复制这段 Prompt,粘贴到 Codex、Claude 或其他助手里,让它检查 Skill 页面并帮你完成安装。
基于 SOC 职业分类
Validate this branch's release POMs on Maven Central against the known previous baseline. Reads each module's version from the branch's RudderStackBuildConfig.kt (never from Maven's <release>/<latest>, so betas can't leak in), fetches that version's POM for all 8 modules (core, android, adjust, appsflyer, braze, facebook, firebase, sprig), compares each against the known-good baseline, and reports pass/fail. Use when the user asks to validate release POMs, check release artifacts, verify release publish, or mentions "validate release pom", "check release", "verify release".
Validate latest snapshot POMs on Maven Central against the baseline reference. Fetches the latest snapshot for all 8 modules (core, android, adjust, appsflyer, braze, facebook, firebase, sprig), compares each against the known-good baseline, and reports pass/fail with details. Use when the user asks to validate snapshots, check snapshot POMs, verify snapshot publish, or mentions "validate pom", "check snapshot", "verify snapshot".
| name | onboard-kotlin-integration |
| description | Generates a Kotlin integration for the rudder-sdk-kotlin repo in a step-by-step manner by referencing the corresponding Java integration. |
You are an expert Android/Kotlin developer that creates Rudder integrations by converting Java integrations to Kotlin equivalents.
Your goal is to generate a new Kotlin integration in the rudder-sdk-kotlin repo by analyzing the corresponding Java integration and creating the Kotlin equivalent in a step-by-step manner.
Parse the following user input for:
--auto is present, run in auto mode (see Execution Mode below)User input: $ARGUMENTS
If integration_name is not provided, use AskUserQuestion to ask the user for it before proceeding.
The skill runs in one of two modes, controlled by the --auto flag:
The agent pauses for user approval at three checkpoints — no more:
Between checkpoints, the agent works continuously without asking for approval. The user can always interrupt with feedback in the chat — they don't need a gate to do so.
--auto)The agent runs end-to-end with no AskUserQuestion calls except:
references/value-conversion.md), the agent must propose a new one before implementing.Everything else proceeds without pausing.
Load each reference file with Read only when the corresponding step needs it (and skip if already in context). Do not paste their contents into responses — refer by filename.
references/api-mapping.md — Java v1 ↔ Kotlin SDK method mapping. Read before Step 1; consult during Steps 5–7.references/integration-plugin.md — IntegrationPlugin and StandardIntegration signatures. Read before Step 3.references/test-scaffold.md — Drop-in test structure (directory layout, build.gradle.kts test block, mockk patterns, POM verification). Read before Step 9b.references/value-conversion.md — JsonObject → destination-SDK type conversion across 5 strategies. Read before Step 5 and pick a strategy before writing helpers. The 5 strategies are not exhaustive — if none fit, propose a new one via AskUserQuestion, get approval, implement, then write it back into this reference.The skill runs from inside rudder-sdk-kotlin. Existing Kotlin integrations live under integrations/. The Java integration repo must be located — use AskUserQuestion:
rudder-integration-<integration_name>-android located?"https://github.com/rudderlabs/rudder-integration-<integration_name>-android into a temp directory"If the user picks Provide local path, ask them for the absolute path (free-form input via the "Other" choice or a follow-up). Verify the path exists with Bash.
If the user picks Clone from GitHub, run:
git clone --depth=1 https://github.com/rudderlabs/rudder-integration-<integration_name>-android /tmp/rudder-integration-<integration_name>-android
Use the cloned directory as the Java integration source. Mention to the user that this is a throwaway clone they can delete after the skill completes.
If the user picks Skip, proceed without a Java repo reference and rely on snippets the user pastes inline during each step.
Store the resolved Java repo path and reuse it throughout subsequent steps.
Locate the Java integration repo using the instructions above. If closest_example was provided, also read the existing Kotlin integration at integrations/<closest_example>/ in the current repo.
Analyze the Java integration:
RudderIntegration / Factory surface (dump, reset, flush, getUnderlyingInstance, create, key). Methods on a custom Factory subinterface, public statics, or any other host-facing API count here — they have no automatic equivalent in the Kotlin SDK and need an explicit design decision.Foo.INSTANCE.configure(...), Foo.getInstance().bar()) reflect how Java sees the destination SDK and frequently do not compile from Kotlin. Before writing any Kotlin call, open the destination SDK's actual class files (its AAR/JAR, its public sources, or its docs) and record the real Kotlin call shape for every method the integration uses. A Kotlin object is called as Foo.bar() (no INSTANCE); a class with companion object is Foo.bar() (no Companion unless @JvmStatic is absent); a regular class needs an instance; suspend / extension / nullable-receiver signatures all change the call site. Do not infer the Kotlin surface from the Java integration's call sites — read the destination SDK directly.Collect findings internally covering these sections (list only methods with non-trivial Java logic — skip unimplemented ones):
create(), getDestinationInstance(), key. Then: which event methods have logic (identify, track, screen, group, alias); which lifecycle methods have logic (update only per Step 4b, reset, flush, teardown per cleanup symmetry rule); which activity lifecycle callbacks the Java integration implements. One-line description per method.Checkpoint 1 — presenting the analysis:
Do not dump the full analysis as a large markdown block into the conversation — that consumes context for the rest of the run. Instead:
preview field of the "Approve" option in AskUserQuestion — the user can inspect it there without it persisting in conversation context.Use AskUserQuestion (interactive mode):
preview containing the full analysis) — "Analysis looks good, proceed to implementation"In auto mode, skip this gate unless there are Behavior Divergences that deviate from "preserve Java public API" — those still require explicit approval. If all divergences recommend preservation, proceed directly.
Create the basic module structure in integrations/<integration_name>/ of the current repo:
Source directory convention: use src/main/kotlin/ and src/test/kotlin/, not java/.
Create directories and basic files:
build.gradle.kts - Module build configurationsrc/main/AndroidManifest.xml - Android manifest.gitignore - Git ignore fileconsumer-rules.pro - ProGuard consumer rulesproguard-rules.pro - ProGuard rulesUpdate the config and gradle files:
RudderStackBuildConfig.kt - add the integration object here. Refer to other existing Kotlin integrations for exact patterns.settings.gradle.kts - include the integration here. Copy the pattern from other integrations.libs.versions.toml - add the dependency version here for the destination SDK. Follow existing integration examples.build.gradle.kts - add that dependency from libs.versions.toml. Use the same structure as other integrations.Dependencies: If the destination SDK's API uses Android framework types not provided by the RudderStack SDK's transitive graph (e.g., FragmentActivity, AppCompatActivity), add them as implementation — not compileOnly — since consumers of the integration need them at compile time too.
Important: When creating or updating the above config files, always refer to existing Kotlin integrations (closest_example if provided, or firebase/braze/adjust) as they follow identical patterns.
Proceed directly to Step 3 — no approval gate here. Module scaffolding is mechanical.
Create the main <IntegrationName>Integration.kt class and generate only the method stubs.
Which stubs to generate: derive the list directly from Step 1's "Methods to Implement" section. That means:
create(), getDestinationInstance(), key property.update() only if the destination SDK exposes a re-configure API (see Step 4b — most do not; the base class default is a no-op and overriding it with an empty body is dead code).flush(), reset(), or update() just to fill them in — IntegrationPlugin already provides no-op defaults for these, so an empty override is noise. teardown() is different — it has real base-class cleanup (clears plugin list, removes from plugin chain), so only override it when you need to add integration-specific cleanup (see cleanup symmetry rule in Step 4a), and always call super.teardown() first.Destination-instance field (non-negotiable): Declare a nullable private var for the destination SDK instance, default null, assigned in create() and returned by getDestinationInstance(). The Kotlin SDK uses this null-ness to decide between create() and update() on every config refresh — see android/src/main/kotlin/com/rudderstack/sdk/kotlin/android/plugins/devicemode/IntegrationPlugin.kt:197 for the gating logic. Returning a non-null literal (e.g., the destination SDK's object itself) breaks this and causes create() to never fire.
Thread-safety for mutable fields: any private var that is written on one thread and read on another must be annotated @Volatile. The most common case is an activity reference field — lifecycle callbacks write it on the main thread, while event methods (track, identify) may read it from the SDK's background thread. Mark such fields @Volatile at declaration.
Pattern (confirmed in integrations/braze, integrations/adjust, integrations/appsflyer):
class FooIntegration : StandardIntegration, IntegrationPlugin() {
override val key: String = FOO_KEY
private var foo: Foo? = null
public override fun create(destinationConfig: JsonObject) {
foo ?: run {
// parse config, then assign
foo = Foo.initialize(...)
}
}
override fun getDestinationInstance(): Any? = foo
}
Visibility: IntegrationPlugin.create() is protected. Always declare public override fun create(...) so that tests can call it directly. All existing integrations do this.
If the destination SDK is a Kotlin object (single global instance), the field still exists — typed as Foo? — and create() assigns it to the object reference after a successful configure() call. Every call site in identify/track/reset/lifecycle must use the stored field (foo?.track(...)), not the SDK class directly (Foo.track(...)), so that calls before create() is gated null-safe.
Reference Java equivalent: Show the main Java integration class method signatures alongside the Kotlin stubs.
Generate Kotlin class: Create the Kotlin equivalent with the proper class structure (StandardIntegration, IntegrationPlugin(), plus ActivityLifecycleObserver if Step 1 flagged any activity callbacks).
Proceed directly to Step 4a — no approval gate on stubs alone. The user will review the full implementation at Checkpoint 2.
Convert the Java integration initialization logic to Kotlin.
Use Step 1's "Destination SDK Kotlin API" section as the source of truth for the call site you'll write. That section already recorded the entry-point shape (Kotlin object / class with companion object / instance-required class) and the exact initialization call (Foo.configure(context, key) vs. Foo.getInstance().configure(...) vs. Foo.initialize(...).also { ... }). Do not re-derive the shape here — re-deriving the destination SDK's call shape at code-write time is the classic source of INSTANCE / Companion build failures.
Implement against the stored field: The integration class has a private var foo: Foo? = null field (Step 3). create() must:
foo ?: run { ... } so re-entry on an initialized integration is a no-op.Foo.configure(...) or equivalent) and assign the result to foo. For SDKs that don't return an instance (e.g., Kotlin object singletons), do the configure call then assign foo = Foo.(analytics as? AndroidAnalytics)?.addLifecycleObserver(this) for activity-lifecycle observers).Cleanup symmetry rule: every registration in create() — lifecycle observers, SDK event listeners, callbacks — requires a corresponding deregistration in teardown(). teardown() must always begin with super.teardown() — the base class (IntegrationPlugin) has real cleanup logic (clears plugin list and removes from plugin chain). After the super call: if create() calls addLifecycleObserver(this), then teardown() must call removeLifecycleObserver(this), null out the destination instance field, and clean up any other state (e.g., clear cached activity references). Add teardown() to Step 7's work list whenever create() registers anything — the Java integration may not have had explicit cleanup, but the Kotlin SDK's plugin lifecycle requires it.
Do not introduce a separate isInitialized flag — the foo == null check is the canonical signal and the SDK uses it directly (IntegrationPlugin.kt:197).
Always use the decode to data class strategy from references/value-conversion.md (Strategy 5) for destination config — define a @Serializable config class and a parseConfig<T> helper in Utils.kt. Even for simple configs with one or two fields, a data class is preferred — it's consistent across integrations, type-safe, and trivially extensible when new dashboard fields appear. Do not fall back to manual field-by-field parsing (getString, getBoolean, etc.) unless the config structure is genuinely dynamic (varying keys, not a fixed schema).
Reference Java initialization: Show Java constructor/factory logic alongside the Kotlin equivalent.
Implement Kotlin equivalent: Create Kotlin initialization in the create() method, using the call site recorded in Step 1 and assigning to the field declared in Step 3.
update(destinationConfig: JsonObject) is called by the Kotlin SDK when the destination's dashboard config changes at runtime, after create() has already run. The base class default is open fun update(...) {} — a no-op — so you should only override it when the destination SDK actually supports updating its configuration in-flight.
Decide between these three cases (and state which one applies in the output):
update() and call that API with the new config values. Do not re-run create() — the destination instance already exists and the SDK never calls create() while the integration's getDestinationInstance() returns non-null.update() only to refresh those cached fields — the destination SDK instance itself stays put. Braze does this with its RudderBrazeConfig cache.update() at all. An empty override is dead code.Skip ahead to Step 5 if case (3) applies. No approval gate here — proceed directly.
Convert the Java identify method to Kotlin.
Before writing this method, read references/value-conversion.md (if not already loaded in context) and pick a strategy by looking at what the destination SDK's user-attribute / identify-equivalent call accepts (Map<String, Any>, Bundle, JSONObject, typed per-key, or a @Serializable typed-trait DTO). Copy the matching helpers from the reference integration named in the file's strategy table; do not assume toAnyMap() is the default — only 2 of 5 existing integrations use it. If none of the 5 strategies fit the destination SDK's actual shape, follow the "When none of the 5 strategies fit" section in the reference: propose a new strategy via AskUserQuestion before implementing, and write it back into the reference once approved.
Utils extraction rule (applies to Steps 5–7): write the event method body (identify(), track(), etc.) in the integration class, but extract any helper functions — attribute processing, trait mapping, value conversion, validation — to Utils.kt as internal functions. Rule of thumb: if a function doesn't need this (no analytics, no mutable instance fields like currentActivity), it belongs in Utils.kt, not in the integration class. The integration class should orchestrate; Utils should transform. This matches the existing integrations — see how braze, adjust, and firebase all keep transformation logic in their respective Utils.kt.
Proceed directly to Step 6 — no approval gate.
Convert the Java track method to Kotlin with event mappings. Use the same value-conversion strategy chosen in Step 5 — do not switch strategies between event methods within one integration.
Proceed directly to Step 7 — no approval gate.
Steps 5–6 covered identify() and track() because every integration has them. This step iterates through every other method identified in Step 1's analysis that has Java logic. There is no privileged "main" method here — screen(), reset(), and ActivityLifecycleObserver are equally important; which ones apply depends entirely on what the Java integration implements.
From the Step 1 analysis, list every method below that the Java integration implements with non-trivial logic. Skip any that the Java integration leaves empty.
screen(), group(), alias(), flush()reset(), teardown()ActivityLifecycleObserver callbacks): onActivityCreated, onActivityStarted, onActivityResumed, onActivityPaused, onActivityStopped, onActivitySaveInstanceState, onActivityDestroyedImplement all methods on the work list in a single pass — in the order the Java integration declares them (or alphabetically if you have no Java source). For each method:
Do not ask for approval per method. The user reviews the full implementation at Checkpoint 2 after Step 8.
If the work list contains any ActivityLifecycleObserver callbacks, also confirm during the first sub-step for that group:
ActivityLifecycleObserver to its implements list.create() registers the integration via (analytics as? AndroidAnalytics)?.addLifecycleObserver(this) (see integrations/braze and integrations/adjust for worked examples).Application.ActivityLifecycleCallbacks directly — confirm the Kotlin port routes through the SDK's lifecycle plumbing instead (it's not a 1:1 mapping; the host registers the SDK once, and the SDK fans out to integrations).Do not add empty overrides or stubs for methods the Java integration doesn't implement — IntegrationPlugin provides defaults. create()/update()/getDestinationInstance() are Step 4; identify()/track() are Steps 5–6.
By this point, Utils.kt should already contain the value-conversion helpers and any transformation functions extracted from Steps 5–7 (per the Utils extraction rule in Step 5). This step handles anything remaining:
Utils.kt completeness: for each private/internal function in the integration class, check: does it read or write an instance field (analytics, currentActivity, etc.) directly — not via a parameter? If no, move it to Utils.kt as an internal function, passing any needed context (e.g., logger) as a parameter.@Serializable config class + parseConfig<T> helper). If it wasn't created yet, create it now.references/value-conversion.md matching the strategy picked in Step 5/6 verbatim — these are duplicated across integrations on purpose (the core helper is internal).Checkpoint 2 (interactive mode only): After completing Steps 2–8, present a summary of all generated files and key implementation decisions. Use AskUserQuestion:
In auto mode, skip this gate and proceed directly to Step 9.
./gradlew :integrations:<integration_name>:build
./gradlew :integrations:<integration_name>:detekt
If build/detekt succeeds, proceed directly to Step 9b.
If there are failures, auto-fix and re-run (up to two attempts). If failures persist after two attempts, use AskUserQuestion to present the errors — this is Checkpoint 3 and applies in both interactive and auto modes.
Read references/test-scaffold.md first (if not already loaded in context). It has the drop-in directory layout, build.gradle.kts test block, TestUtils.kt, the integration-test skeleton with mockk patterns, the UtilsTest.kt skeleton, and the PomVerificationTest.kt template. Do not invent a test structure from scratch — every existing integration follows the scaffold.
Work through the scaffold in this order — no per-sub-step approval gates:
tasks.withType<Test> block to build.gradle.kts per the scaffold.src/test/kotlin/.../TestUtils.kt with mockAnalytics, readFileAsJsonObject, mergeWithHigherPriorityTo (copy verbatim from the scaffold).src/test/resources/config/<integration_name>_config.json and new_<integration_name>_config.json fixtures mirroring the destination dashboard's payload. Use the closest existing integration's fixtures as the structural template.<Name>IntegrationTest.kt using @Nested inner classes to group tests by method (Create, Identify, Track, Reset, ActivityLifecycle, etc.) — one @Nested inner class per logical group, each containing the @Test methods for that group. Add update tests only if update() was overridden per Step 4b. Use the mockk pattern from the scaffold that matches the destination SDK's entry-point shape (recorded in Step 1's "Destination SDK Kotlin API" section). Include a getDestinationInstance test that asserts it returns null before create() runs and the configured instance after — this is the contract the Kotlin SDK relies on.UtilsTest.kt covering the value-conversion helpers from Step 8 — the test shape depends on the strategy picked in Step 5/6 (Map-based gets type-preservation tests, per-type-getter gets coercion tests, data-class-decode gets valid/invalid JSON tests).<Name>PomVerificationTest.kt — first run ./gradlew :integrations:<integration_name>:generatePomFileForReleasePublication and read the generated POM at build/publications/release/pom-default.xml. Use that actual output as the expected XML template (normalize versions per the scaffold). Do not guess the dependency list — the generated POM is the source of truth../gradlew :integrations:<integration_name>:test
If any fail, auto-fix and re-run (up to two attempts). If failures persist, use AskUserQuestion to present them (Checkpoint 3).Naming convention is non-negotiable: backticked `given X, when Y, then Z`. The scaffold's "What's out of scope" section documents what NOT to write (no Espresso, no SDK network mocking, no dashboard E2E).
Generate README.md for the integration. Refer to other integrations for examples.
| Checkpoint | When | Interactive | Auto |
|---|---|---|---|
| 1 | After Step 1 (analysis) | Always | Only if non-preserving divergences exist |
| 2 | After Step 8 (implementation) | Always | Skip |
| 3 | After Step 9 (build/test failures) | Only on persistent failures | Only on persistent failures |
Do not add AskUserQuestion calls beyond these checkpoints. The user can always interrupt with feedback — they don't need a gate to do so.
When presenting generated code for approval at checkpoints, use the preview field on AskUserQuestion options to show code inline.
?.let { ... } / ?.also { ... } chains over null-check-then-!!. Never use !! on a value you just null-checked — use the safe-call chain instead (e.g., primitive.booleanOrNull?.also { sdk.setAttribute(key, it) } not if (primitive.booleanOrNull != null) sdk.setAttribute(key, primitive.booleanOrNull!!)).for ((key, value) in map) with an if guard over filterKeys { ... }.forEach { ... } — the latter allocates an intermediate map for no benefit.= ...) for single-expression functions; block bodies for multi-statement ones.references/value-conversion.md matching the destination SDK's accepted type. Do not sprinkle inline jsonObject["key"]?.jsonPrimitive?.content calls through the integration code — that pattern is the helpers' job, and consistency with the existing integrations matters for review.After all steps completed:
## Kotlin Integration Generation Complete
### Original Java Integration:
`rudder-integration-<integration_name>-android`
### Generated Kotlin Integration:
`integrations/<integration_name>/`
### Key Business Logic Converted:
- [List main methods and their Java counterparts]
### Files Created:
- [List all generated files]
### Test Results:
- Unit tests: [PASSED/FAILED status]
- Detekt analysis: [PASSED/FAILED status]
- Build compilation: [PASSED/FAILED status]