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
65 changes: 60 additions & 5 deletions .cursor/rules/development-workflow.mdc
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,30 @@
alwaysApply: true
---

# Development Workflow
# Development Workflow — Ketch Android SDK

## Prerequisites

- JDK 17 (JitPack also builds with JDK 17 — keep parity)
- Android SDK with an emulator or device on API 26+ (API 28+ recommended for the instrumented suite)
- `local.properties` with `sdk.dir` pointing at the Android SDK
- **Android Studio** or Android SDK with an emulator or device on API 26+ (API 28+ recommended for the instrumented suite)
- **JDK 17** (JitPack also builds with JDK 17 — keep parity)
- `local.properties` with `sdk.dir` pointing at the Android SDK; emulator/device with `adb` on `PATH`

## Sample apps

| Module | UI stack | Manual QA entry |
| --- | --- | --- |
| `sample-app-compose` | Jetpack Compose | `MainActivity` + `KetchSampleApp` |
| `sample-app-standard` | XML + ViewBinding | `activity_main.xml` + `MainActivity` |
| `integration-tests` | XML (Espresso host) | Automation only; layout reference for dashboard |

Build and run a sample:

```bash
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose local
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh standard local
```

See `ketch-android-run-sample` skill for prerequisites, remote/local SDK toggle, and manual QA checklist.

## Common Commands

Expand All @@ -19,6 +36,10 @@ alwaysApply: true
| `./gradlew build` | Assemble + unit-test all modules |
| `./gradlew test` | Run JVM unit tests (all variants) |
| `./gradlew :ketchsdk:testDebugUnitTest` | Run SDK unit tests only |
| `./gradlew :sample-app-compose:assembleDebug` | Build Compose sample |
| `./gradlew :sample-app-standard:assembleDebug` | Build standard sample |
| `bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose local` | Build, install, launch Compose sample (local SDK) |
| `bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh standard local` | Build, install, launch standard sample (local SDK) |
| `./gradlew :integration-tests:connectedDebugAndroidTest` | Run the instrumented Espresso suite on a connected device/emulator |
| `./gradlew ktlintCheck` | Kotlin lint check |
| `./gradlew ktlintFormat` | Auto-format Kotlin sources |
Expand All @@ -27,14 +48,48 @@ alwaysApply: true
| `./gradlew dokkaHtmlMultiModule` | Generate API docs |
| `./run-integration-tests.sh` | Clean build + full instrumented run (checks for a connected device) |

## SDK Health Dashboard (manual QA)

Both `sample-app-compose` and `sample-app-standard` include an on-screen **SDK Health Dashboard** (pattern from `integration-tests`).

| Section | What to verify |
| --- | --- |
| **Connection** | Init, status, `ethansch061226` / `website_smart_tag` / `production` |
| **WebView / Experience** | Load state, visibility, dismiss reason (ATT N/A on Android) |
| **Privacy / Consent** | Environment, jurisdiction, region, consent, US Privacy / TCF / GPP |
| **Headless** | **Fetch Location**, **Fetch Bootstrap**, **Cold Start** — inline OK/Error |
| **Event log** | Timestamped callback trace (~50 lines) |

Manual golden path: **Load** → privacy fields update → **Show Consent** → visibility/dismiss rows change → **Fetch Bootstrap** shows success or error on-screen.

Defaults: org `ethansch061226`, property `website_smart_tag`, environment `production` (constants in `MainActivity`; editable org/property UI is a future enhancement).

## Local tag URL overrides (non-prod scripts)

Use `webResourceUrlOverrides` to redirect exact CDN URLs (e.g. UAT `plugins.js` / `ketch.js`) to local dev servers. This is separate from `dataCenter` / `ketchUrl`, which control the boot.js base path.

```kotlin
KetchSdk.create(
// ...
webResourceUrlOverrides = mapOf(
"https://cdn.uat.ketchjs.com/ketchtag/stable/v2.12/ketch-sdk.js" to "http://localhost:9000/ketch-sdk.js",
),
)
// or after create:
ketch.setWebResourceUrlOverrides(overrides)
```

Android uses native `shouldInterceptRequest` plus a JS hook in index HTML. Sample apps: set `DevUrlOverrides.ENABLED = true` in `DevUrlOverrides.kt`.

## Validation Checklist

Run before considering any change complete:

1. `./gradlew :ketchsdk:assembleDebug` — verify the SDK compiles
2. `./gradlew ktlintCheck` — Kotlin style
3. `./gradlew test` — unit tests
4. `./gradlew :integration-tests:connectedDebugAndroidTest` — instrumented suite (requires a running emulator/device); run this when changing SDK runtime behavior, the lifecycle tracker, or the WebView/dialog flow
4. Build affected sample module(s)
5. `./gradlew :integration-tests:connectedDebugAndroidTest` — instrumented suite (requires a running emulator/device); run this when changing SDK runtime behavior, the lifecycle tracker, the WebView/dialog flow, or headless/listener contracts

## Testing

Expand Down
67 changes: 67 additions & 0 deletions .cursor/skills/ketch-android-run-sample/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
---
name: ketch-android-run-sample
description: Configures the in-repo Android sample apps (Compose or standard) for either the published com.ketch.android:ketchsdk Maven artifact or the local :ketchsdk module, boots an emulator when needed, builds, installs, launches, and streams filtered logcat. Use when the user runs /ketch-android-run-sample or wants manual SDK Health Dashboard QA on Android.
---

# ketch-android-run-sample

## Instructions

When the user invokes **`/ketch-android-run-sample`** (or asks to run the Android sample with production / local SDK):

1. `cd` to the `ketch-android` repository root.

2. Run the helper script with a **required** sample variant:

**Local SDK (default for SDK development)** — sample `build.gradle` uses `project(':ketchsdk')`:

```bash
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose local
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh standard local
```

**Published Maven artifact** — sample depends on `com.ketch.android:ketchsdk` (version from `KETCH_ANDROID_SDK_VERSION` or `3.0`):

```bash
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh standard
```

The script boots an emulator when no `adb` device is connected, runs `./gradlew :sample-app-*:assembleDebug`, installs the APK, launches `MainActivity`, and streams filtered logcat (`Ketch`, sample tag). Stop with `Ctrl-C`.

## Manual testing basics

**Needs:** Android Studio or SDK, emulator or USB device, `adb` on PATH, network for CDN/headless steps.

**Launch:** `bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose local` from the `ketch-android` repo root.

**In the app:** **SDK Health Dashboard** is the first section in the scroll view. Connection row uses org `ethansch061226`, property `website_smart_tag`, environment `production`. Use dashboard panels and event log — logcat from the script is optional for smoke QA.

**Smoke flow:** **Load** → privacy rows update → **Show Consent** → visibility/dismiss rows change → **Fetch Bootstrap** under Headless. ATT does not apply on Android.

## Other options

```bash
DEVICE_ID=emulator-5554 bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose local
AVD_NAME="Pixel_7_API_34" bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh standard local
KETCH_ANDROID_SDK_VERSION=3.0 bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh compose local --build-only
bash .cursor/skills/ketch-android-run-sample/scripts/run-sample-app.sh standard local --full-system-logs
```

## Manual QA checklist (SDK Health Dashboard)

Verify on-screen panels (no logcat required):

1. **Connection** — Init/status; connection row shows `ethansch061226 / website_smart_tag / production`.
2. **Load** — Tap **Load** → Load row `loading`; environment/consent fields update from callbacks.
3. **WebView / Experience** — **Show Consent** → visibility/dismiss rows change (ATT N/A on Android).
4. **Privacy / Consent** — Environment, jurisdiction, region, consent, US Privacy / TCF / GPP populate after load.
5. **Headless** — **Fetch Location**, **Fetch Bootstrap**, **Cold Start** show inline OK/Error.
6. **Event log** — Timestamped trace (~50 lines max).

## Notes

- `integration-tests` is the Espresso automation host; use **compose** or **standard** samples for manual QA.
- Headless steps require network access to the live Ketch CDN.
- Remote mode needs the pinned Maven artifact to be resolvable from your configured repositories.
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/usr/bin/env python3
"""Switch sample apps between the local :ketchsdk project and a published Maven artifact."""

from __future__ import annotations

import os
import re
import sys
from pathlib import Path

GROUP = "com.ketch.android"
ARTIFACT = "ketchsdk"
DEFAULT_VERSION = "3.0"

SAMPLE_GRADLE_FILES = (
"sample-app-compose/build.gradle",
"sample-app-standard/build.gradle",
)

LOCAL_DEP = "implementation project(':ketchsdk')"
REMOTE_DEP_TEMPLATE = "implementation '{group}:{artifact}:{version}'"


def remote_dep(version: str) -> str:
return REMOTE_DEP_TEMPLATE.format(group=GROUP, artifact=ARTIFACT, version=version)


def configure_file(path: Path, mode: str, version: str) -> bool:
text = path.read_text(encoding="utf-8")
target = LOCAL_DEP if mode == "local" else remote_dep(version)
pattern = re.compile(
r"implementation\s+(?:project\(':ketchsdk'\)|'"
+ re.escape(GROUP)
+ r":"
+ re.escape(ARTIFACT)
+ r":[^']+')"
)
if not pattern.search(text):
raise SystemExit(f"Could not find ketchsdk dependency in {path}")

new_text, count = pattern.subn(target, text, count=1)
if count != 1:
raise SystemExit(f"Expected one ketchsdk dependency in {path}, replaced {count}")

if new_text == text:
print(f"Already {mode} in {path.name}")
return False

path.write_text(new_text, encoding="utf-8")
print(f"Updated {path.name} to {mode} ({target})")
return True


def main() -> None:
if len(sys.argv) != 3:
raise SystemExit(f"Usage: {sys.argv[0]} <local|remote> <repo_root>")

mode = sys.argv[1]
if mode not in {"local", "remote"}:
raise SystemExit("mode must be local or remote")

repo_root = Path(sys.argv[2]).resolve()
version = os.environ.get("KETCH_ANDROID_SDK_VERSION", DEFAULT_VERSION)

changed = False
for rel in SAMPLE_GRADLE_FILES:
path = repo_root / rel
if not path.is_file():
raise SystemExit(f"Missing {path}")
if configure_file(path, mode, version):
changed = True

if not changed:
print(f"No changes needed (already {mode})")


if __name__ == "__main__":
main()
Loading
Loading