Skip to content

Add debug event timeline support to example#393

Merged
ianrumac merged 2 commits intodevelopfrom
ir/feat/debug-event-timeline
Apr 7, 2026
Merged

Add debug event timeline support to example#393
ianrumac merged 2 commits intodevelopfrom
ir/feat/debug-event-timeline

Conversation

@ianrumac
Copy link
Copy Markdown
Collaborator

@ianrumac ianrumac commented Apr 7, 2026

Changes in this pull request

  • Add timeline to example app

Checklist

  • All unit tests pass.
  • All UI tests pass.
  • Demo project builds and runs.
  • I added/updated tests or detailed why my change isn't tested.
  • I added an entry to the CHANGELOG.md for any breaking changes, enhancements, or bug fixes.
  • I have run ktlint in the main directory and fixed any issues.
  • I have updated the SDK documentation as well as the online docs.
  • I have reviewed the contributing guide

Greptile Summary

This PR adds a debug event timeline system to the example app: a live EventTimeline in MainApplication, per-test timelines in UITestInfo, a Compose TimelineViewerActivity (reachable via double-tap), a TimelineStore registry, a EventTimelineRule JUnit watcher, and Gradle helper tasks to pull/clear timeline files from the device.

  • Line 32 of AndroidManifest.xml has a malformed android:sharedUserId attribute — the closing quote appears to swallow the adjacent tools:targetApi attribute, resulting in an invalid UID value that may cause AAPT2/build errors.
  • TimelineStore entries registered per test are never removed, causing unbounded memory growth across long test sessions.

Confidence Score: 4/5

The malformed android:sharedUserId attribute on line 32 of AndroidManifest.xml is a P0 syntax issue that may cause build failures or set an invalid UID; it must be verified/fixed before merging.

All other findings are P2 style/quality improvements. The malformed manifest attribute is the only blocking concern — it was either introduced by this PR or was pre-existing, but it's present in the current state and should be corrected. Score is 4 pending verification and fix of that single attribute.

app/src/main/AndroidManifest.xml line 32 (malformed sharedUserId attribute) requires immediate attention before merge.

Important Files Changed

Filename Overview
app/build.gradle.kts Adds pullEventTimelines and clearEventTimelines Gradle tasks for managing event timeline files via adb
app/src/androidTest/java/com/example/superapp/utils/EventTimelineRule.kt New JUnit TestWatcher writing event timelines to external storage; reliable, but redundant with stack-trace approach in TestingUtils
app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt Adds timeline writing to screenshotFlow and assertion extensions; stack-trace heuristic for test name detection is fragile and may silently overwrite files
app/src/main/AndroidManifest.xml Adds TimelineViewerActivity; line 32 has a malformed android:sharedUserId that swallows tools:targetApi, producing an invalid UID value
app/src/main/java/com/superwall/superapp/MainActivity.kt Adds double-tap gesture to open TimelineViewerActivity; not gated to debug builds
app/src/main/java/com/superwall/superapp/MainApplication.kt Adds liveTimeline EventTimeline that records all SDK delegate events into TimelineStore
app/src/main/java/com/superwall/superapp/test/EventTimeline.kt New thread-safe EventTimeline using CopyOnWriteArrayList and StateFlow; well-designed with type-safe query API
app/src/main/java/com/superwall/superapp/test/TimelineStore.kt New ConcurrentHashMap-based singleton for named timelines; entries are registered but never removed, causing unbounded growth
app/src/main/java/com/superwall/superapp/test/TimelineViewerActivity.kt New Compose UI for browsing and time-comparing events; missing BackHandler and uses hardcoded event name strings
app/src/main/java/com/superwall/superapp/test/UITestActivity.kt Integrates EventTimeline into UITestInfo with per-test registration in TimelineStore; registered timelines are never removed

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[MainApplication.onCreate] -->|register 'Live'| TS[TimelineStore]
    A -->|liveTimeline.record| LT[EventTimeline: Live]
    LT --> TS

    UITest[UITestInfo.test] -->|timeline.clear| TT[EventTimeline: Test#N]
    UITest -->|register 'Test#N'| TS
    UITest -->|delegate.handleSuperwallEvent| TT

    TS -->|timelinesFlow| TVA[TimelineViewerActivity]
    TVA --> TLS[TimelineListScreen]
    TLS -->|tap| TDS[TimelineDetailScreen]
    TDS -->|long-press events| SEL[Duration Selection]

    TT -->|EventTimelineRule / screenshotFlow| WF[writeTimelineToFile]
    WF -->|External Storage| JSON[/sdcard/Download/superwall-event-timelines/*.json]
    JSON -->|adb pull Gradle task| OUT[build/outputs/event-timelines/]
Loading

Comments Outside Diff (2)

  1. app/src/main/AndroidManifest.xml, line 32-33 (link)

    P0 Malformed android:sharedUserId attribute

    The closing quote of android:sharedUserId appears to have swallowed the tools:targetApi attribute, producing the value "com.superwall.superapp.uid tools:targetApi=". This is an invalid package name and may cause AAPT2 to reject the manifest or silently mis-set the shared UID. It should be two separate attributes:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: app/src/main/AndroidManifest.xml
    Line: 32-33
    
    Comment:
    **Malformed `android:sharedUserId` attribute**
    
    The closing quote of `android:sharedUserId` appears to have swallowed the `tools:targetApi` attribute, producing the value `"com.superwall.superapp.uid         tools:targetApi="`. This is an invalid package name and may cause AAPT2 to reject the manifest or silently mis-set the shared UID. It should be two separate attributes:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
  2. app/src/main/AndroidManifest.xml, line 8 (link)

    P2 WRITE_EXTERNAL_STORAGE missing android:maxSdkVersion

    Without android:maxSdkVersion="28", lint warns and the declaration is a no-op on API 29+ (where scoped storage applies). Add the bound so intent is explicit and the warning is suppressed:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: app/src/main/AndroidManifest.xml
    Line: 8
    
    Comment:
    **`WRITE_EXTERNAL_STORAGE` missing `android:maxSdkVersion`**
    
    Without `android:maxSdkVersion="28"`, lint warns and the declaration is a no-op on API 29+ (where scoped storage applies). Add the bound so intent is explicit and the warning is suppressed:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: app/src/main/AndroidManifest.xml
Line: 32-33

Comment:
**Malformed `android:sharedUserId` attribute**

The closing quote of `android:sharedUserId` appears to have swallowed the `tools:targetApi` attribute, producing the value `"com.superwall.superapp.uid         tools:targetApi="`. This is an invalid package name and may cause AAPT2 to reject the manifest or silently mis-set the shared UID. It should be two separate attributes:

```suggestion
        android:sharedUserId="com.superwall.superapp.uid"
        tools:targetApi="28"
        android:sharedUserMaxSdkVersion="32"
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
Line: 66-68

Comment:
**Timeline entries never removed from `TimelineStore`**

`TimelineStore.register("Test #$number", timeline)` is called on every test run, but `TimelineStore.remove(...)` is never called anywhere in the codebase. Over a long test session all test timelines (and their `CopyOnWriteArrayList` of `TimedEvent` objects) accumulate indefinitely. Add a cleanup call in the `finally` block of `screenshotFlow` in `TestingUtils.kt` after `writeTimelineToFile`:

```kotlin
} finally {
    scope.cancel()
    writeTimelineToFile(testCase, testClassName, testMethodName)
    TimelineStore.remove("Test #${testCase.number}")
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
Line: 99-107

Comment:
**Fragile stack-trace heuristic for test name detection**

Searching for `frame.className.contains("Test")` will misfire if a utility class (e.g. `TestingUtils` itself, or `AbstractTest`) appears higher in the call stack. When it misfires, every test produces the same filename (`UnknownTest_unknownMethod.json` or the wrong name), silently overwriting earlier runs. The `EventTimelineRule` already uses JUnit's `Description` API for reliable name resolution — consider consolidating on that mechanism and dropping the stack-walking path.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/test/TimelineViewerActivity.kt
Line: 157-165

Comment:
**Missing `BackHandler` — system back exits the Activity instead of returning to list**

The `< Back` text navigates within Compose state, but pressing the Android system back button exits `TimelineViewerActivity` entirely rather than returning to the timeline list. Add a `BackHandler` inside `TimelineDetailScreen`:

```kotlin
BackHandler { onBack() }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/test/TimelineViewerActivity.kt
Line: 382-388

Comment:
**Hardcoded event name strings will silently break if SDK renames events**

`"config_attributes"`, `"paywallWebviewLoad_complete"`, `"paywallPreload_start"`, etc. are string literals. If the SDK renames any of these event names, the preload section will silently produce `null` durations with no compile-time error. Consider using the type-safe `durationBetween<ConfigAttributes, PaywallWebviewLoadComplete>()` API from `EventTimeline`, or extract these strings as named constants.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/MainActivity.kt
Line: 29-38

Comment:
**Timeline viewer gesture not gated to debug builds**

The `GestureDetector` and `TimelineViewerActivity` launch are wired in the production `MainActivity`. While `TimelineViewerActivity` is `android:exported="false"`, the gesture listener adds minor overhead and exposes developer tooling to non-debug builds. Consider wrapping the setup in `if (BuildConfig.DEBUG) { … }`.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: app/src/main/AndroidManifest.xml
Line: 8

Comment:
**`WRITE_EXTERNAL_STORAGE` missing `android:maxSdkVersion`**

Without `android:maxSdkVersion="28"`, lint warns and the declaration is a no-op on API 29+ (where scoped storage applies). Add the bound so intent is explicit and the warning is suppressed:

```suggestion
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "Add debug event timeline support to exam..." | Re-trigger Greptile

Greptile also left 5 inline comments on this PR.

Context used:

  • Context used - CLAUDE.md (source)

Comment on lines +66 to 68
timeline.clear()
TimelineStore.register("Test #$number", timeline)
delay(100)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Timeline entries never removed from TimelineStore

TimelineStore.register("Test #$number", timeline) is called on every test run, but TimelineStore.remove(...) is never called anywhere in the codebase. Over a long test session all test timelines (and their CopyOnWriteArrayList of TimedEvent objects) accumulate indefinitely. Add a cleanup call in the finally block of screenshotFlow in TestingUtils.kt after writeTimelineToFile:

} finally {
    scope.cancel()
    writeTimelineToFile(testCase, testClassName, testMethodName)
    TimelineStore.remove("Test #${testCase.number}")
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/test/UITestActivity.kt
Line: 66-68

Comment:
**Timeline entries never removed from `TimelineStore`**

`TimelineStore.register("Test #$number", timeline)` is called on every test run, but `TimelineStore.remove(...)` is never called anywhere in the codebase. Over a long test session all test timelines (and their `CopyOnWriteArrayList` of `TimedEvent` objects) accumulate indefinitely. Add a cleanup call in the `finally` block of `screenshotFlow` in `TestingUtils.kt` after `writeTimelineToFile`:

```kotlin
} finally {
    scope.cancel()
    writeTimelineToFile(testCase, testClassName, testMethodName)
    TimelineStore.remove("Test #${testCase.number}")
}
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +99 to +107
val callerFrame = Thread.currentThread().stackTrace
.firstOrNull { frame ->
frame.methodName != "screenshotFlow" &&
!frame.className.startsWith("java.") &&
!frame.className.startsWith("dalvik.") &&
frame.className.contains("Test")
}
val testClassName = callerFrame?.className?.substringAfterLast('.') ?: "UnknownTest"
val testMethodName = callerFrame?.methodName ?: "unknownMethod"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Fragile stack-trace heuristic for test name detection

Searching for frame.className.contains("Test") will misfire if a utility class (e.g. TestingUtils itself, or AbstractTest) appears higher in the call stack. When it misfires, every test produces the same filename (UnknownTest_unknownMethod.json or the wrong name), silently overwriting earlier runs. The EventTimelineRule already uses JUnit's Description API for reliable name resolution — consider consolidating on that mechanism and dropping the stack-walking path.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/androidTest/java/com/example/superapp/utils/TestingUtils.kt
Line: 99-107

Comment:
**Fragile stack-trace heuristic for test name detection**

Searching for `frame.className.contains("Test")` will misfire if a utility class (e.g. `TestingUtils` itself, or `AbstractTest`) appears higher in the call stack. When it misfires, every test produces the same filename (`UnknownTest_unknownMethod.json` or the wrong name), silently overwriting earlier runs. The `EventTimelineRule` already uses JUnit's `Description` API for reliable name resolution — consider consolidating on that mechanism and dropping the stack-walking path.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +157 to +165
private fun TimelineDetailScreen(
name: String,
timeline: EventTimeline,
onBack: () -> Unit,
) {
val events by timeline.eventsFlow.collectAsState()
var firstSelected by remember { mutableStateOf<Int?>(null) }
var secondSelected by remember { mutableStateOf<Int?>(null) }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing BackHandler — system back exits the Activity instead of returning to list

The < Back text navigates within Compose state, but pressing the Android system back button exits TimelineViewerActivity entirely rather than returning to the timeline list. Add a BackHandler inside TimelineDetailScreen:

BackHandler { onBack() }
Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/test/TimelineViewerActivity.kt
Line: 157-165

Comment:
**Missing `BackHandler` — system back exits the Activity instead of returning to list**

The `< Back` text navigates within Compose state, but pressing the Android system back button exits `TimelineViewerActivity` entirely rather than returning to the timeline list. Add a `BackHandler` inside `TimelineDetailScreen`:

```kotlin
BackHandler { onBack() }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +382 to +388
val configAttr = events.firstOrNull { it.eventName == "config_attributes" }
val lastWebviewComplete = events.lastOrNull { it.eventName == "paywallWebviewLoad_complete" }
if (configAttr != null && lastWebviewComplete != null) {
lastWebviewComplete.elapsed - configAttr.elapsed
} else {
null
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Hardcoded event name strings will silently break if SDK renames events

"config_attributes", "paywallWebviewLoad_complete", "paywallPreload_start", etc. are string literals. If the SDK renames any of these event names, the preload section will silently produce null durations with no compile-time error. Consider using the type-safe durationBetween<ConfigAttributes, PaywallWebviewLoadComplete>() API from EventTimeline, or extract these strings as named constants.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/test/TimelineViewerActivity.kt
Line: 382-388

Comment:
**Hardcoded event name strings will silently break if SDK renames events**

`"config_attributes"`, `"paywallWebviewLoad_complete"`, `"paywallPreload_start"`, etc. are string literals. If the SDK renames any of these event names, the preload section will silently produce `null` durations with no compile-time error. Consider using the type-safe `durationBetween<ConfigAttributes, PaywallWebviewLoadComplete>()` API from `EventTimeline`, or extract these strings as named constants.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +29 to +38
// Double-tap anywhere to open timeline viewer
gestureDetector = GestureDetector(
this,
object : GestureDetector.SimpleOnGestureListener() {
override fun onDoubleTap(e: MotionEvent): Boolean {
startActivity(Intent(this@MainActivity, TimelineViewerActivity::class.java))
return true
}
},
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Timeline viewer gesture not gated to debug builds

The GestureDetector and TimelineViewerActivity launch are wired in the production MainActivity. While TimelineViewerActivity is android:exported="false", the gesture listener adds minor overhead and exposes developer tooling to non-debug builds. Consider wrapping the setup in if (BuildConfig.DEBUG) { … }.

Prompt To Fix With AI
This is a comment left during a code review.
Path: app/src/main/java/com/superwall/superapp/MainActivity.kt
Line: 29-38

Comment:
**Timeline viewer gesture not gated to debug builds**

The `GestureDetector` and `TimelineViewerActivity` launch are wired in the production `MainActivity`. While `TimelineViewerActivity` is `android:exported="false"`, the gesture listener adds minor overhead and exposes developer tooling to non-debug builds. Consider wrapping the setup in `if (BuildConfig.DEBUG) { … }`.

How can I resolve this? If you propose a fix, please make it concise.

@ianrumac ianrumac merged commit 737be05 into develop Apr 7, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant