Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
95d6afc
Test hardening, bug fixes on iOS, RN, and Android. Add CI testing for…
akfreas May 22, 2026
48f103a
Consolidate the duplicated iOS and Android JS bridge packages into a …
akfreas May 22, 2026
0144b89
Repoint the iOS build script and the iOS and Android SDK docs at the …
akfreas May 22, 2026
fd8665d
Restore the Android JS bridge bundle to its minified build output, re…
akfreas May 22, 2026
46a89c2
Port the getMergeTagValue and flag bridge methods into the merged opt…
akfreas May 22, 2026
6a59652
prevent CI from silently passing when Android instrumentation runner …
akfreas May 25, 2026
02739e8
Drop set -o pipefail from the emulator-runner script — the action inv…
akfreas May 25, 2026
8d8de1e
satisfy strict ESLint in OptimizationProvider injected-sdk RN tests: …
akfreas May 25, 2026
af1dcf9
satisfy strict ESLint in OptimizationProvider RN test: use Promise.wi…
akfreas May 25, 2026
998ec0c
satisfy strict ESLint in OptimizedEntry RN test: extract 1234 ms dwel…
akfreas May 25, 2026
25407e1
satisfy strict ESLint in OptimizationProvider onStatesReady react-web…
akfreas May 25, 2026
6afc512
satisfy strict ESLint in OptimizationProvider trackEntryInteraction r…
akfreas May 25, 2026
a41b0fd
Tighten the two isContentfulOptimization predicates in the RN provide…
akfreas May 25, 2026
c5ada6a
Add the missing testID='preview-panel-scroll' to the PreviewPanel Scr…
akfreas May 25, 2026
c502e57
Apply prettier formatting to the Reflect.get type assertion on the cl…
akfreas May 25, 2026
d119016
Replace reset-all Alert.alert with inline confirmation view in RN pre…
akfreas May 17, 2026
54e44b4
Surface a preview-refresh-button in the RN preview panel and wire it …
akfreas May 19, 2026
d77c1f3
Scroll the audience-toggle row back into view between the deactivate …
akfreas May 25, 2026
bded3b6
Make scrollPanelToElement tolerate the StaleObjectException that the …
akfreas May 25, 2026
b7938ac
Sort preview-panel audiences by name only (drop the qualified-first t…
akfreas May 26, 2026
07d191c
Remove the scenario 3 scroll-between-taps workaround in PreviewPanelO…
akfreas May 26, 2026
433b355
Harden the Android UI Automator test infra against the UiAutomator-pl…
akfreas May 26, 2026
f624d9a
Remove the stale Zipline references from the Android SDK docs and Pro…
akfreas May 26, 2026
2466eee
Rewrite PreviewPanelOverridesTests.scrollPanelToElement to use UiObje…
akfreas May 27, 2026
e9ab042
Fail-fast on first failing Android E2E test instead of letting am ins…
akfreas May 27, 2026
0c9711f
Collapse the Android E2E fail-fast pipeline onto a single physical YA…
akfreas May 27, 2026
d85979e
Cache the Android E2E AVD across CI runs to skip the 1-3 minute cold …
akfreas May 27, 2026
b549e8e
trigger CI
akfreas May 27, 2026
4cfdca7
Revert "Cache the Android E2E AVD across CI runs to skip the 1-3 minu…
akfreas May 27, 2026
de8816c
Restore documentation/concepts/react-native-sdk-interaction-tracking-…
akfreas May 27, 2026
419413a
Restore .github/workflows/notify-slack.yml to its main-branch state —…
akfreas May 27, 2026
48aa661
Revert the stale eslint.config.ts narrowing on this branch and the tw…
akfreas May 27, 2026
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
186 changes: 186 additions & 0 deletions .github/workflows/main-pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
e2e_web_sdk_react: ${{ steps.filter.outputs.e2e_web_sdk_react }}
e2e_react_web_sdk: ${{ steps.filter.outputs.e2e_react_web_sdk }}
e2e_react_native_android: ${{ steps.filter.outputs.e2e_react_native_android }}
e2e_android: ${{ steps.filter.outputs.e2e_android }}
e2e_ios: ${{ steps.filter.outputs.e2e_ios }}
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1
Expand Down Expand Up @@ -126,11 +127,21 @@ jobs:
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# Android native implementation E2E coverage scope.
e2e_android:
- 'implementations/android-sdk/**'
- 'lib/mocks/**'
- 'packages/android/**'
- 'packages/universal/optimization-js-bridge/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
# iOS native implementation E2E coverage scope.
e2e_ios:
- 'implementations/ios-sdk/**'
- 'lib/mocks/**'
- 'packages/ios/**'
- 'packages/universal/optimization-js-bridge/**'
- 'package.json'
- 'pnpm-lock.yaml'
- '.github/workflows/main-pipeline.yaml'
Expand Down Expand Up @@ -659,6 +670,181 @@ jobs:
if-no-files-found: error
retention-days: 1

e2e-android-sdk:
name: 🤖 E2E Android Native
runs-on: namespace-profile-linux-16-vcpu-32-gb-ram-optimal
timeout-minutes: 45
needs: [setup, changes]
if: needs.changes.outputs.e2e_android == 'true'
env:
CI: 'true'
GRADLE_OPTS: >-
-Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.jvmargs=-Xmx4g
-Dkotlin.daemon.jvm.options=-Xmx2g
steps:
- uses: namespacelabs/nscloud-checkout-action@938f5d2d403d6224d9a0c0dc559b1dae09c2ede4 # v8.1.1

- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: '.nvmrc'
package-manager-cache: false

- uses: pnpm/action-setup@903f9c1a6ebcba6cf41d87230be49611ac97822e # v6.0.3

- name: Set Android SDK environment variables
run: |
echo "ANDROID_SDK_ROOT=$HOME/.android/sdk" >> "$GITHUB_ENV"
echo "ANDROID_HOME=$HOME/.android/sdk" >> "$GITHUB_ENV"

- name: Prepare cache directories
run: |
mkdir -p "$HOME/.android/sdk" "$HOME/.android/avd" "$HOME/.android/cache"

- name: Set up caches (Namespace)
uses: namespacelabs/nscloud-cache-action@15799a6b54e5765f85b2aac25b3f0df43ed571c0 # v1.4.3
with:
cache: |
pnpm
gradle
path: |
~/.android/sdk
~/.android/avd
~/.android/cache

- name: Install system dependencies (Android emulator)
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
ca-certificates curl unzip zip git \
netcat-openbsd cpu-checker \
libgl1 libnss3 libx11-6 libx11-xcb1 libxcomposite1 libxdamage1 libxrandr2 libxtst6 \
libxi6 libxrender1 libxkbcommon0 libgbm1 libdbus-1-3 libdrm2 libpulse0
sudo apt-get install -y --no-install-recommends libasound2 || sudo apt-get install -y --no-install-recommends libasound2t64

- name: Verify KVM is available
run: |
if [ ! -e /dev/kvm ]; then
echo "/dev/kvm not found; Android hardware acceleration will not work." >&2
exit 1
fi
ls -l /dev/kvm
sudo kvm-ok || true

- name: Setup Java
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
with:
distribution: 'temurin'
java-version: '17'

- name: Setup Android SDK
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1

- name: Install JS dependencies
run: pnpm install --prefer-offline --frozen-lockfile

- name: Build the JS bridge bundles
run: pnpm --filter @contentful/optimization-js-bridge build

- name: Build app and test APKs
working-directory: implementations/android-sdk
run: ./gradlew :app:assembleDebug :uitests:assembleDebug

- name: Start Mock Server
run: |
pnpm --dir lib/mocks serve > /tmp/mock-server.log 2>&1 &
echo $! > /tmp/mock-server.pid
for i in {1..60}; do
if nc -z localhost 8000 2>/dev/null; then
echo "Mock server is ready"
break
fi
echo "Waiting for mock server... ($i/60)"
sleep 1
done
if ! nc -z localhost 8000 2>/dev/null; then
echo "Mock server failed to start:"
cat /tmp/mock-server.log
exit 1
fi

- name: Run Android E2E Tests (emulator)
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
api-level: 35
arch: x86_64
target: google_apis
profile: pixel_7
avd-name: test
force-avd-creation: true
emulator-boot-timeout: 600
cores: 6
ram-size: 4096M
disk-size: 8G
disable-animations: true
emulator-options: -no-window -no-audio -no-boot-anim -gpu swiftshader_indirect
script: |
# The emulator-runner action invokes the script via `/usr/bin/sh -c`, not bash, so
# `set -o pipefail` would error out with "Illegal option -o pipefail" and terminate
# the script before any test ran. The grep-on-test-output checks below already
# detect instrumentation failures regardless of pipe-status propagation.
echo "Disabling animations..."
adb shell settings put global window_animation_scale 0
adb shell settings put global transition_animation_scale 0
adb shell settings put global animator_duration_scale 0
echo "Installing APKs..."
adb install -r implementations/android-sdk/app/build/outputs/apk/debug/app-debug.apk
adb install -r implementations/android-sdk/uitests/build/outputs/apk/debug/uitests-debug.apk
echo "Setting up adb reverse port forwarding..."
adb reverse tcp:8000 tcp:8000
sleep 3
adb shell "for i in 1 2 3 4 5 6 7 8 9 10; do nc -z localhost 8000 2>/dev/null && echo 'Mock server tunnel verified' && exit 0; sleep 1; done; echo 'WARNING: tunnel verification timed out'"
echo "Running UI Automator 2 E2E tests..."
# Fail-fast: stream am instrument output through awk; on the first
# "Error in test" or "Process crashed" line, force-stop the test
# process to abort the remaining suite. AndroidJUnitRunner has no
# built-in early-exit, so without this every subsequent @Before
# waits its full 20-30s waitForElement timeout and one root-cause
# failure in an early class burns the whole 45-minute job budget.
# force-stop on the .uitests process kills the instrumentation;
# the remote `am instrument -w` exits and the local pipeline
# collapses. fflush keeps the GitHub Actions log live so we see
# the failure when it happens, not at the end.
#
# IMPORTANT: this MUST be a single physical YAML line. The
# reactivecircus/android-emulator-runner action's parseScript()
# splits the multi-line `script:` block on every newline and runs
# each line as a separate `sh -c`, so a multi-line pipeline with
# `\` continuations gets broken into three independent commands —
# `am instrument` runs standalone and the `| tee` / `| awk` lines
# are dropped as no-op syntax errors. Verified in the action's
# src/script-parser.ts:
# rawScript.trim().split(/\r\n|\n|\r/)
adb shell am instrument -w com.contentful.optimization.uitests/androidx.test.runner.AndroidJUnitRunner 2>&1 | tee /tmp/test-output.log | awk 'BEGIN{aborted=0} /Error in test|Process crashed/{if(!aborted){aborted=1;print;print "::error::Test failure detected — aborting remaining suite";fflush();system("adb shell am force-stop com.contentful.optimization.uitests >/dev/null 2>&1");exit 1} next} {print;fflush()}'
grep -q "FAILURES\|Error in test" /tmp/test-output.log && { echo "::error::Android UI tests failed"; exit 1; } || true
grep -q "Process crashed" /tmp/test-output.log && { echo "::error::Test process crashed"; exit 1; } || true
grep -q "OK (" /tmp/test-output.log || { echo "::error::Android UI tests did not complete successfully (missing OK status)"; exit 1; }

- name: Upload logs on failure
if: failure()
run: |
echo "=== Mock Server Logs ==="
cat /tmp/mock-server.log || echo "No mock server logs found"

- name: Stop Mock Server
if: always()
run: |
kill $(cat /tmp/mock-server.pid) 2>/dev/null || true

- uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: ci-results-android-sdk
path: |
implementations/android-sdk/logs/
/tmp/mock-server.log
/tmp/test-output.log
retention-days: 7

e2e-ios-sdk-build:
name: 🍎 Build iOS UI Test Bundles
runs-on: namespace-profile-macos-apple-silicon-arm64-6-cpu-14-gb
Expand Down
14 changes: 6 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,12 @@ local.properties
*.keystore
!debug.keystore
!**/gradle/wrapper/gradle-wrapper.jar
**/android-sdk/.gradle/
**/android-sdk/app/build/
**/android-sdk/uitests/build/
**/android-sdk/local.properties
**/android-sdk/logs/

# Android Native
triage-out
.gradle/
app/build/
uitests/build/
local.properties
logs/

# node.js
#
Expand Down Expand Up @@ -122,4 +120,4 @@ yarn-error.log
# Local environment configuration - commented out since we use safe defaults
# Uncomment this line if you need to override with local secrets
# env.config.ts

triage-out
3 changes: 2 additions & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ pnpm-lock.yaml
**/android/.gradle/**
**/.bundle/**
**/node_modules/**
packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/optimization-ios-bridge.umd.js
packages/ios/ContentfulOptimization/Sources/ContentfulOptimization/Resources/optimization-ios-bridge.umd.js
packages/android/ContentfulOptimization/src/main/assets/optimization-android-bridge.umd.js
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -92,8 +92,8 @@ React Native support is available through
[`@contentful/optimization-react-native`](./packages/react-native-sdk/README.md).

Native iOS work is also present in this repository as a pre-release Swift Package under
[`packages/ios`](./packages/ios/README.md), backed by the
[`@contentful/optimization-ios-bridge`](./packages/ios/ios-jsc-bridge/README.md) JavaScriptCore
[`packages/ios`](./packages/ios/README.md), backed by the shared
[`@contentful/optimization-js-bridge`](./packages/universal/optimization-js-bridge/README.md)
adapter and the [iOS reference app](./implementations/ios-sdk/README.md). Treat this surface as
alpha implementation work rather than a stable public native SDK.

Expand Down
3 changes: 3 additions & 0 deletions eslint.config.ts
Comment thread
phobetron marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export default defineConfig(
'**/dist',
'docs/media/**',
'**/ios/**',
// Engine-targeted JS bridge glue compiled into the native SDKs; consolidated
// from the ios/android bridge packages, which were ignored under the rules above.
'**/optimization-js-bridge/**',
'**/node_modules',
],
},
Expand Down
6 changes: 3 additions & 3 deletions implementations/android-sdk/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ build.

- Keep this app focused on validating native Android integration behavior. Reusable SDK behavior
belongs in `packages/android/ContentfulOptimization`, and TypeScript bridge behavior belongs in
`packages/android/android-zipline-bridge`.
`packages/universal/optimization-js-bridge`.
- The mock server must be running at `http://localhost:8000` before running the app. Use
`adb reverse tcp:8000 tcp:8000` to forward the port to the emulator.
- The app references the SDK via Gradle `include` + `project.dir` in `settings.gradle.kts`. After
Expand All @@ -45,7 +45,7 @@ build.
- `pnpm serve:mocks` (from monorepo root)
- From `implementations/android-sdk/`: `./gradlew :app:assembleDebug`
- From `implementations/android-sdk/`: `./scripts/bootstrap.sh`
- Build bridge first: `pnpm --filter @contentful/optimization-android-bridge build`
- Build bridge first: `pnpm --filter @contentful/optimization-js-bridge build`
- Build UI test APK: `./gradlew :uitests:assembleDebug`
- Run all UI tests: `./gradlew :uitests:connectedAndroidTest`
- Run single test class:
Expand All @@ -65,5 +65,5 @@ build.

- Run the app on emulator after changes to verify UI renders correctly.
- Verify accessibility identifiers match iOS counterparts when changing UI structure.
- Rebuild `@contentful/optimization-android-bridge` before testing when bridge source changed.
- Rebuild `@contentful/optimization-js-bridge` before testing when bridge source changed.
- After UI structure changes, run `./gradlew :uitests:assembleDebug` to verify test APK compiles.
8 changes: 4 additions & 4 deletions implementations/android-sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ integration pattern using Jetpack Compose and serves as a test target for UI Aut
- Nested entry resolution and recursive rendering
- Navigation with screen tracking via `ScreenTrackingEffect`
- Live updates behavior: default (global), explicit live, and locked variants
- `PreviewPanelOverlay` with audience/variant override controls
- `PreviewPanelConfig` preview panel with audience/variant override controls
- Analytics event display for debugging tracked events
- All accessibility identifiers aligned with the iOS SwiftUI implementation for cross-platform E2E
parity
Expand All @@ -40,15 +40,15 @@ integration pattern using Jetpack Compose and serves as a test target for UI Aut
- Android emulator or connected device
- `adb` in PATH
- pnpm dependencies installed at monorepo root (`pnpm install`)
- Android bridge built: `pnpm --filter @contentful/optimization-android-bridge build`
- Android bridge built: `pnpm --filter @contentful/optimization-js-bridge build`

## Setup

From the monorepo root:

```sh
pnpm install
pnpm --filter @contentful/optimization-android-bridge build
pnpm --filter @contentful/optimization-js-bridge build
```

## Running locally
Expand Down Expand Up @@ -98,7 +98,7 @@ Before running anything from the IDE, in a separate terminal:

```sh
# From the monorepo root, build the bridge once (or after bridge source changes):
pnpm --filter @contentful/optimization-android-bridge build
pnpm --filter @contentful/optimization-js-bridge build

# Then start the mock server and leave it running:
pnpm --dir lib/mocks serve
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import androidx.compose.ui.semantics.testTagsAsResourceId
import com.contentful.optimization.app.screens.MainScreen
import com.contentful.optimization.compose.OptimizationRoot
import com.contentful.optimization.core.OptimizationConfig
import com.contentful.optimization.preview.PreviewPanelOverlay
import com.contentful.optimization.preview.PreviewPanelConfig

class MainActivity : ComponentActivity() {

Expand Down Expand Up @@ -50,10 +50,11 @@ class MainActivity : ComponentActivity() {
),
trackViews = true,
trackTaps = true,
previewPanel = PreviewPanelConfig(
contentfulClient = MockPreviewContentfulClient(),
),
) {
PreviewPanelOverlay(contentfulClient = MockPreviewContentfulClient()) {
MainScreen(simulateOffline = simulateOffline)
}
MainScreen(simulateOffline = simulateOffline)
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,29 +4,20 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.contentful.optimization.app.EventStore
import com.contentful.optimization.compose.LocalOptimizationClient

@Composable
fun AnalyticsEventDisplay() {
val client = LocalOptimizationClient.current
val events by EventStore.events.collectAsState()
val componentStats by EventStore.componentStats.collectAsState()
val scope = rememberCoroutineScope()

LaunchedEffect(Unit) {
EventStore.subscribe(client.events, scope)
}

Column(
modifier = Modifier
Expand Down
Loading
Loading