Skip to content
Merged
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
45 changes: 45 additions & 0 deletions .github/workflows/publish-github-packages.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
name: Publish to GitHub Packages

on:
# Publish whenever a GitHub Release is published.
release:
types: [published]
# Allow publishing the current VERSION_NAME manually from the Actions tab.
workflow_dispatch:

jobs:
publish:
# Runs on forks only; the upstream repo publishes to Maven Central instead.
if: ${{ github.repository != 'ChuckerTeam/chucker' }}
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout Repo
uses: actions/checkout@v6

- name: Setup Java
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: '21'

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6

- name: Publish library and library-no-op to GitHub Packages
# For a release, publish under the release tag (stripping a leading "v"), e.g. tag
# "v4.4.0-blink.1" -> version "4.4.0-blink.1". For a manual run, fall back to the
# VERSION_NAME declared in gradle.properties.
run: |
VERSION_ARG=""
TAG="${{ github.event.release.tag_name }}"
if [ -n "$TAG" ]; then
VERSION_ARG="-PVERSION_NAME=${TAG#v}"
fi
./gradlew publishReleasePublicationToGitHubPackagesRepository $VERSION_ARG --no-parallel --stacktrace
env:
GPR_USER: ${{ github.actor }}
GPR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
26 changes: 26 additions & 0 deletions gradle/gradle-github-packages.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Fork-specific: publish the `release` publication to GitHub Packages.
//
// Applied on top of gradle-mvn-push.gradle (which defines the `release` MavenPublication and
// optional signing). This only adds a target repository, so the upstream Sonatype/Maven Central
// setup is left untouched.
//
// Credentials & target repo are resolved from (in order):
// - env GPR_USER / GPR_TOKEN (set by the publish workflow)
// - env GITHUB_ACTOR / GITHUB_TOKEN (auto-provided inside GitHub Actions)
// - gradle properties gpr.user / gpr.token (for local publishing, e.g. in ~/.gradle/gradle.properties)
// The repository slug comes from GITHUB_REPOSITORY ("owner/repo", auto-set in CI) and defaults to
// the fork's slug for local runs.

publishing {
repositories {
maven {
name = "GitHubPackages"
def repoSlug = System.getenv("GITHUB_REPOSITORY") ?: (findProperty("GITHUB_REPOSITORY") ?: "simonvar/chucker")
url = uri("https://maven.pkg.github.com/${repoSlug}")
credentials {
username = System.getenv("GPR_USER") ?: System.getenv("GITHUB_ACTOR") ?: findProperty("gpr.user")
password = System.getenv("GPR_TOKEN") ?: System.getenv("GITHUB_TOKEN") ?: findProperty("gpr.token")
}
}
}
}
19 changes: 19 additions & 0 deletions library-no-op/api/library-no-op.api
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,25 @@ public final class com/chuckerteam/chucker/api/ChuckerInterceptor$Builder {
public final fun skipPaths ([Ljava/lang/String;)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder;
}

public final class com/chuckerteam/chucker/api/ChuckerWebSocketListener : okhttp3/WebSocketListener {
public fun <init> (Lokhttp3/WebSocketListener;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/Request;)V
public fun <init> (Lokhttp3/WebSocketListener;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/Request;J)V
public synthetic fun <init> (Lokhttp3/WebSocketListener;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/Request;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun onClosed (Lokhttp3/WebSocket;ILjava/lang/String;)V
public fun onClosing (Lokhttp3/WebSocket;ILjava/lang/String;)V
public fun onFailure (Lokhttp3/WebSocket;Ljava/lang/Throwable;Lokhttp3/Response;)V
public fun onMessage (Lokhttp3/WebSocket;Ljava/lang/String;)V
public fun onMessage (Lokhttp3/WebSocket;Lokio/ByteString;)V
public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V
}

public final class com/chuckerteam/chucker/api/ChuckerWebSockets {
public static final field INSTANCE Lcom/chuckerteam/chucker/api/ChuckerWebSockets;
public static final fun create (Lokhttp3/OkHttpClient;Lokhttp3/Request;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/WebSocketListener;)Lokhttp3/WebSocket;
public static final fun create (Lokhttp3/OkHttpClient;Lokhttp3/Request;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/WebSocketListener;J)Lokhttp3/WebSocket;
public static synthetic fun create$default (Lokhttp3/OkHttpClient;Lokhttp3/Request;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/WebSocketListener;JILjava/lang/Object;)Lokhttp3/WebSocket;
}

public final class com/chuckerteam/chucker/api/ExportFormat : java/lang/Enum {
public static final field HAR Lcom/chuckerteam/chucker/api/ExportFormat;
public static final field LOG Lcom/chuckerteam/chucker/api/ExportFormat;
Expand Down
1 change: 1 addition & 0 deletions library-no-op/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ dependencies {
}

apply(from = rootProject.file("gradle/gradle-mvn-push.gradle"))
apply(from = rootProject.file("gradle/gradle-github-packages.gradle"))
apply(from = rootProject.file("gradle/kotlin-static-analysis.gradle"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.chuckerteam.chucker.api

import okhttp3.Request
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString

/**
* No-op implementation.
*/
@Suppress("UnusedPrivateMember", "UNUSED_PARAMETER")
public class ChuckerWebSocketListener
@JvmOverloads
constructor(
private val delegate: WebSocketListener,
collector: ChuckerCollector,
request: Request,
maxContentLength: Long = 250_000L,
) : WebSocketListener() {
override fun onOpen(
webSocket: WebSocket,
response: Response,
): Unit = delegate.onOpen(webSocket, response)

override fun onMessage(
webSocket: WebSocket,
text: String,
): Unit = delegate.onMessage(webSocket, text)

override fun onMessage(
webSocket: WebSocket,
bytes: ByteString,
): Unit = delegate.onMessage(webSocket, bytes)

override fun onClosing(
webSocket: WebSocket,
code: Int,
reason: String,
): Unit = delegate.onClosing(webSocket, code, reason)

override fun onClosed(
webSocket: WebSocket,
code: Int,
reason: String,
): Unit = delegate.onClosed(webSocket, code, reason)

override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?,
): Unit = delegate.onFailure(webSocket, t, response)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.chuckerteam.chucker.api

import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.WebSocket
import okhttp3.WebSocketListener

/**
* No-op implementation.
*/
@Suppress("UnusedPrivateMember", "UNUSED_PARAMETER")
public object ChuckerWebSockets {
@JvmStatic
@JvmOverloads
public fun create(
client: OkHttpClient,
request: Request,
collector: ChuckerCollector,
listener: WebSocketListener,
maxContentLength: Long = 250_000L,
): WebSocket = client.newWebSocket(request, listener)
}
23 changes: 23 additions & 0 deletions library/api/library.api
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ public final class com/chuckerteam/chucker/api/ChuckerInterceptor$Builder {
public final fun skipPaths ([Ljava/lang/String;)Lcom/chuckerteam/chucker/api/ChuckerInterceptor$Builder;
}

public final class com/chuckerteam/chucker/api/ChuckerWebSocketListener : okhttp3/WebSocketListener {
public static final field Companion Lcom/chuckerteam/chucker/api/ChuckerWebSocketListener$Companion;
public fun <init> (Lokhttp3/WebSocketListener;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/Request;)V
public fun <init> (Lokhttp3/WebSocketListener;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/Request;J)V
public synthetic fun <init> (Lokhttp3/WebSocketListener;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/Request;JILkotlin/jvm/internal/DefaultConstructorMarker;)V
public fun onClosed (Lokhttp3/WebSocket;ILjava/lang/String;)V
public fun onClosing (Lokhttp3/WebSocket;ILjava/lang/String;)V
public fun onFailure (Lokhttp3/WebSocket;Ljava/lang/Throwable;Lokhttp3/Response;)V
public fun onMessage (Lokhttp3/WebSocket;Ljava/lang/String;)V
public fun onMessage (Lokhttp3/WebSocket;Lokio/ByteString;)V
public fun onOpen (Lokhttp3/WebSocket;Lokhttp3/Response;)V
}

public final class com/chuckerteam/chucker/api/ChuckerWebSocketListener$Companion {
}

public final class com/chuckerteam/chucker/api/ChuckerWebSockets {
public static final field INSTANCE Lcom/chuckerteam/chucker/api/ChuckerWebSockets;
public static final fun create (Lokhttp3/OkHttpClient;Lokhttp3/Request;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/WebSocketListener;)Lokhttp3/WebSocket;
public static final fun create (Lokhttp3/OkHttpClient;Lokhttp3/Request;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/WebSocketListener;J)Lokhttp3/WebSocket;
public static synthetic fun create$default (Lokhttp3/OkHttpClient;Lokhttp3/Request;Lcom/chuckerteam/chucker/api/ChuckerCollector;Lokhttp3/WebSocketListener;JILjava/lang/Object;)Lokhttp3/WebSocket;
}

public final class com/chuckerteam/chucker/api/ExportFormat : java/lang/Enum {
public static final field HAR Lcom/chuckerteam/chucker/api/ExportFormat;
public static final field LOG Lcom/chuckerteam/chucker/api/ExportFormat;
Expand Down
1 change: 1 addition & 0 deletions library/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -120,4 +120,5 @@ dependencies {
}

apply(from = rootProject.file("gradle/gradle-mvn-push.gradle"))
apply(from = rootProject.file("gradle/gradle-github-packages.gradle"))
apply(from = rootProject.file("gradle/kotlin-static-analysis.gradle"))
5 changes: 5 additions & 0 deletions library/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@
android:parentActivityName="com.chuckerteam.chucker.internal.ui.MainActivity"
android:theme="@style/Chucker.Theme" />

<activity
android:name="com.chuckerteam.chucker.internal.ui.websocket.WebSocketTrafficActivity"
android:parentActivityName="com.chuckerteam.chucker.internal.ui.MainActivity"
android:theme="@style/Chucker.Theme" />

<service
android:name="com.chuckerteam.chucker.internal.support.ClearDatabaseService"
android:exported="false"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import android.content.Context
import android.net.Uri
import com.chuckerteam.chucker.R
import com.chuckerteam.chucker.internal.data.entity.HttpTransaction
import com.chuckerteam.chucker.internal.data.entity.WebSocketTraffic
import com.chuckerteam.chucker.internal.data.har.log.Creator
import com.chuckerteam.chucker.internal.data.repository.RepositoryProvider
import com.chuckerteam.chucker.internal.support.HarUtils
import com.chuckerteam.chucker.internal.support.JsonConverter
import com.chuckerteam.chucker.internal.support.NotificationHelper
import com.chuckerteam.chucker.internal.support.TrafficListSharable
import com.chuckerteam.chucker.internal.support.TransactionDetailsHarSharable
import com.chuckerteam.chucker.internal.support.TransactionListDetailsSharable
import com.chuckerteam.chucker.internal.support.writeToFile
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
Expand Down Expand Up @@ -80,6 +81,27 @@ public class ChuckerCollector
}
}

/**
* Call this method when a WebSocket event (open, message, closing, closed, failure) occurs.
* This is invoked internally by [com.chuckerteam.chucker.api.ChuckerWebSocketListener] and
* the recording WebSocket returned by [com.chuckerteam.chucker.api.ChuckerWebSockets.create].
* @param traffic The captured WebSocket event.
*/
internal fun onWebSocketTraffic(traffic: WebSocketTraffic) {
scope.launch {
withContext(Dispatchers.IO) {
RepositoryProvider.webSocket().insertWebSocketTraffic(traffic)
}

if (showNotification) {
notificationHelper.show(traffic)
}
withContext(Dispatchers.IO) {
retentionManager.doMaintenance()
}
}
}

/**
* Export the Chucker transactions to a file. Please note that this function is blocking
* and performs Disk I/O. Make sure you run it on a separate thread or coroutine.
Expand All @@ -97,14 +119,20 @@ public class ChuckerCollector
): Uri? {
val transactions =
RepositoryProvider.transaction().getTransactionsInTimeRange(startTimestamp)
if (transactions.isEmpty()) {
val webSocketTraffic =
if (exportFormat == ExportFormat.LOG) {
RepositoryProvider.webSocket().getWebSocketTrafficInTimeRange(startTimestamp)
} else {
emptyList()
}
if (transactions.isEmpty() && webSocketTraffic.isEmpty()) {
return null
}

val sharableTransactions =
when (exportFormat) {
ExportFormat.LOG -> {
TransactionListDetailsSharable(transactions, encodeUrls = false)
TrafficListSharable(transactions, webSocketTraffic)
}
ExportFormat.HAR -> {
TransactionDetailsHarSharable(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package com.chuckerteam.chucker.api

import com.chuckerteam.chucker.internal.support.WebSocketTrafficRecorder
import okhttp3.Response
import okhttp3.WebSocket
import okhttp3.WebSocketListener
import okio.ByteString
import java.util.concurrent.atomic.AtomicLong

/**
* A [WebSocketListener] decorator that records every incoming message and lifecycle event
* (open / closing / closed / failure) into Chucker before delegating to the wrapped [delegate].
*
* OkHttp does not run application interceptors on the WebSocket frame stream, so this decorator
* (together with the [okhttp3.WebSocket] returned by [ChuckerWebSockets.create], which captures
* outgoing frames) is how Chucker observes WebSocket traffic.
*
* You normally do not need to instantiate this directly — use [ChuckerWebSockets.create], which
* wraps both the listener and the socket. Instantiate it directly only if you want to capture
* incoming + lifecycle events without capturing outgoing `send()` calls.
*
* @param delegate The original listener that should keep receiving all callbacks.
* @param collector The [ChuckerCollector] that persists the captured traffic.
* @param request The request used to open the WebSocket (provides url/host/path).
* @param maxContentLength Maximum number of characters stored per message. Defaults to 250000.
*/
public class ChuckerWebSocketListener
@JvmOverloads
constructor(
private val delegate: WebSocketListener,
collector: ChuckerCollector,
request: okhttp3.Request,
maxContentLength: Long = MAX_CONTENT_LENGTH,
) : WebSocketListener() {
internal val recorder =
WebSocketTrafficRecorder(
request = request,
connectionId = connectionIdGenerator.incrementAndGet(),
collector = collector,
maxContentLength = maxContentLength,
)

override fun onOpen(
webSocket: WebSocket,
response: Response,
) {
recorder.onOpen(response)
delegate.onOpen(webSocket, response)
}

override fun onMessage(
webSocket: WebSocket,
text: String,
) {
recorder.onReceiveText(text)
delegate.onMessage(webSocket, text)
}

override fun onMessage(
webSocket: WebSocket,
bytes: ByteString,
) {
recorder.onReceiveBytes(bytes)
delegate.onMessage(webSocket, bytes)
}

override fun onClosing(
webSocket: WebSocket,
code: Int,
reason: String,
) {
recorder.onClosing(code, reason)
delegate.onClosing(webSocket, code, reason)
}

override fun onClosed(
webSocket: WebSocket,
code: Int,
reason: String,
) {
recorder.onClosed(code, reason)
delegate.onClosed(webSocket, code, reason)
}

override fun onFailure(
webSocket: WebSocket,
t: Throwable,
response: Response?,
) {
recorder.onFailure(t, response)
delegate.onFailure(webSocket, t, response)
}

public companion object {
private const val MAX_CONTENT_LENGTH = 250_000L
private val connectionIdGenerator = AtomicLong(0)
}
}
Loading