Skip to content

Commit 1d84566

Browse files
feat(client): improve logging
Logging is now: 1. Streaming 4. Configurable in-memory 5. Generally more robust 6. Usable with any underlying http client
1 parent 1ff82f3 commit 1d84566

10 files changed

Lines changed: 1739 additions & 27 deletions

File tree

README.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -526,8 +526,6 @@ The SDK throws custom unchecked exception types:
526526

527527
## Logging
528528

529-
The SDK uses the standard [OkHttp logging interceptor](https://github.com/square/okhttp/tree/master/okhttp-logging-interceptor).
530-
531529
Enable logging by setting the `STAGEHAND_LOG` environment variable to `info`:
532530

533531
```sh
@@ -540,6 +538,19 @@ Or to `debug` for more verbose logging:
540538
export STAGEHAND_LOG=debug
541539
```
542540

541+
Or configure the client manually using the `logLevel` method:
542+
543+
```java
544+
import com.browserbase.api.client.StagehandClient;
545+
import com.browserbase.api.client.okhttp.StagehandOkHttpClient;
546+
import com.browserbase.api.core.LogLevel;
547+
548+
StagehandClient client = StagehandOkHttpClient.builder()
549+
.fromEnv()
550+
.logLevel(LogLevel.INFO)
551+
.build();
552+
```
553+
543554
## ProGuard and R8
544555

545556
Although the SDK uses reflection, it is still usable with [ProGuard](https://github.com/Guardsquare/proguard) and [R8](https://developer.android.com/topic/performance/app-optimization/enable-app-optimization) because `stagehand-java-core` is published with a [configuration file](stagehand-java-core/src/main/resources/META-INF/proguard/stagehand-java-core.pro) containing [keep rules](https://www.guardsquare.com/manual/configuration/usage).

stagehand-java-client-okhttp/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ dependencies {
77
api(project(":stagehand-java-core"))
88

99
implementation("com.squareup.okhttp3:okhttp:4.12.0")
10-
implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")
1110

1211
testImplementation(kotlin("test"))
1312
testImplementation("org.assertj:assertj-core:3.27.7")

stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/OkHttpClient.kt

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ import okhttp3.Request
3535
import okhttp3.RequestBody
3636
import okhttp3.RequestBody.Companion.toRequestBody
3737
import okhttp3.Response
38-
import okhttp3.logging.HttpLoggingInterceptor
3938
import okio.BufferedSink
4039
import okio.buffer
4140
import okio.sink
@@ -93,21 +92,6 @@ internal constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClie
9392
private fun newCall(request: HttpRequest, requestOptions: RequestOptions): Call {
9493
val clientBuilder = okHttpClient.newBuilder()
9594

96-
val logLevel =
97-
when (System.getenv("STAGEHAND_LOG")?.lowercase()) {
98-
"info" -> HttpLoggingInterceptor.Level.BASIC
99-
"debug" -> HttpLoggingInterceptor.Level.BODY
100-
else -> null
101-
}
102-
if (logLevel != null) {
103-
clientBuilder.addNetworkInterceptor(
104-
HttpLoggingInterceptor().setLevel(logLevel).apply {
105-
redactHeader("x-bb-api-key")
106-
redactHeader("x-model-api-key")
107-
}
108-
)
109-
}
110-
11195
requestOptions.timeout?.let {
11296
clientBuilder
11397
.connectTimeout(it.connect())

stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClient.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.browserbase.api.client.okhttp
55
import com.browserbase.api.client.StagehandClient
66
import com.browserbase.api.client.StagehandClientImpl
77
import com.browserbase.api.core.ClientOptions
8+
import com.browserbase.api.core.LogLevel
89
import com.browserbase.api.core.Sleeper
910
import com.browserbase.api.core.Timeout
1011
import com.browserbase.api.core.http.AsyncStreamResponse
@@ -290,6 +291,15 @@ class StagehandOkHttpClient private constructor() {
290291
*/
291292
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
292293

294+
/**
295+
* The level at which to log request and response information.
296+
*
297+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
298+
*
299+
* Defaults to [LogLevel.fromEnv].
300+
*/
301+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
302+
293303
/** Your [Browserbase API Key](https://www.browserbase.com/settings) */
294304
fun browserbaseApiKey(browserbaseApiKey: String) = apply {
295305
clientOptions.browserbaseApiKey(browserbaseApiKey)

stagehand-java-client-okhttp/src/main/kotlin/com/browserbase/api/client/okhttp/StagehandOkHttpClientAsync.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.browserbase.api.client.okhttp
55
import com.browserbase.api.client.StagehandClientAsync
66
import com.browserbase.api.client.StagehandClientAsyncImpl
77
import com.browserbase.api.core.ClientOptions
8+
import com.browserbase.api.core.LogLevel
89
import com.browserbase.api.core.Sleeper
910
import com.browserbase.api.core.Timeout
1011
import com.browserbase.api.core.http.AsyncStreamResponse
@@ -290,6 +291,15 @@ class StagehandOkHttpClientAsync private constructor() {
290291
*/
291292
fun maxRetries(maxRetries: Int) = apply { clientOptions.maxRetries(maxRetries) }
292293

294+
/**
295+
* The level at which to log request and response information.
296+
*
297+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
298+
*
299+
* Defaults to [LogLevel.fromEnv].
300+
*/
301+
fun logLevel(logLevel: LogLevel) = apply { clientOptions.logLevel(logLevel) }
302+
293303
/** Your [Browserbase API Key](https://www.browserbase.com/settings) */
294304
fun browserbaseApiKey(browserbaseApiKey: String) = apply {
295305
clientOptions.browserbaseApiKey(browserbaseApiKey)

stagehand-java-core/src/main/kotlin/com/browserbase/api/core/ClientOptions.kt

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.browserbase.api.core
55
import com.browserbase.api.core.http.AsyncStreamResponse
66
import com.browserbase.api.core.http.Headers
77
import com.browserbase.api.core.http.HttpClient
8+
import com.browserbase.api.core.http.LoggingHttpClient
89
import com.browserbase.api.core.http.PhantomReachableClosingHttpClient
910
import com.browserbase.api.core.http.QueryParams
1011
import com.browserbase.api.core.http.RetryingHttpClient
@@ -110,6 +111,14 @@ private constructor(
110111
* Defaults to 2.
111112
*/
112113
@get:JvmName("maxRetries") val maxRetries: Int,
114+
/**
115+
* The level at which to log request and response information.
116+
*
117+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
118+
*
119+
* Defaults to [LogLevel.fromEnv].
120+
*/
121+
@get:JvmName("logLevel") val logLevel: LogLevel,
113122
/** Your [Browserbase API Key](https://www.browserbase.com/settings) */
114123
@get:JvmName("browserbaseApiKey") val browserbaseApiKey: String,
115124
private val browserbaseProjectId: String?,
@@ -176,6 +185,7 @@ private constructor(
176185
private var responseValidation: Boolean = false
177186
private var timeout: Timeout = Timeout.default()
178187
private var maxRetries: Int = 2
188+
private var logLevel: LogLevel = LogLevel.fromEnv()
179189
private var browserbaseApiKey: String? = null
180190
private var browserbaseProjectId: String? = null
181191
private var modelApiKey: String? = null
@@ -194,6 +204,7 @@ private constructor(
194204
responseValidation = clientOptions.responseValidation
195205
timeout = clientOptions.timeout
196206
maxRetries = clientOptions.maxRetries
207+
logLevel = clientOptions.logLevel
197208
browserbaseApiKey = clientOptions.browserbaseApiKey
198209
browserbaseProjectId = clientOptions.browserbaseProjectId
199210
modelApiKey = clientOptions.modelApiKey
@@ -320,14 +331,23 @@ private constructor(
320331
*/
321332
fun maxRetries(maxRetries: Int) = apply { this.maxRetries = maxRetries }
322333

334+
/**
335+
* The level at which to log request and response information.
336+
*
337+
* [fromEnv] will set the level from environment variables. See [LogLevel.fromEnv].
338+
*
339+
* Defaults to [LogLevel.fromEnv].
340+
*/
341+
fun logLevel(logLevel: LogLevel) = apply { this.logLevel = logLevel }
342+
323343
/** Your [Browserbase API Key](https://www.browserbase.com/settings) */
324344
fun browserbaseApiKey(browserbaseApiKey: String) = apply {
325345
this.browserbaseApiKey = browserbaseApiKey
326346
}
327347

328348
/**
329349
* Deprecated. Browserbase API keys are now project-scoped, so this value is no longer
330-
* required. Accepted for backwards compatibility; it is ignored.
350+
* required.
331351
*/
332352
fun browserbaseProjectId(browserbaseProjectId: String?) = apply {
333353
this.browserbaseProjectId = browserbaseProjectId
@@ -430,23 +450,28 @@ private constructor(
430450
*
431451
* See this table for the available options:
432452
*
433-
* |Setter |System property |Environment variable |Required|Default value |
434-
* |-------------------|-----------------------------|---------------------|--------|-----------------------------------------|
435-
* |`browserbaseApiKey`|`stagehand.browserbaseApiKey`|`BROWSERBASE_API_KEY`|true |- |
436-
* |`modelApiKey` |`stagehand.modelApiKey` |`MODEL_API_KEY` |true |- |
437-
* |`baseUrl` |`stagehand.baseUrl` |`STAGEHAND_API_URL` |false |`"https://api.stagehand.browserbase.com"`|
453+
* |Setter |System property |Environment variable |Required|Default value |
454+
* |----------------------|--------------------------------|------------------------|--------|-----------------------------------------|
455+
* |`browserbaseApiKey` |`stagehand.browserbaseApiKey` |`BROWSERBASE_API_KEY` |true |- |
456+
* |`browserbaseProjectId`|`stagehand.browserbaseProjectId`|`BROWSERBASE_PROJECT_ID`|false |- |
457+
* |`modelApiKey` |`stagehand.modelApiKey` |`MODEL_API_KEY` |true |- |
458+
* |`baseUrl` |`stagehand.baseUrl` |`STAGEHAND_BASE_URL` |true |`"https://api.stagehand.browserbase.com"`|
438459
*
439460
* System properties take precedence over environment variables.
440461
*/
441462
fun fromEnv() = fromEnv(System::getenv)
442463

443-
internal fun fromEnv(getEnv: (String) -> String?) = apply {
464+
fun fromEnv(getEnv: (String) -> String?) = apply {
465+
logLevel(LogLevel.fromEnv())
444466
(System.getProperty("stagehand.baseUrl")
445467
?: getEnv("STAGEHAND_API_URL")
446468
?: getEnv("STAGEHAND_BASE_URL"))
447469
?.let { baseUrl(it) }
448470
(System.getProperty("stagehand.browserbaseApiKey") ?: getEnv("BROWSERBASE_API_KEY"))
449471
?.let { browserbaseApiKey(it) }
472+
(System.getProperty("stagehand.browserbaseProjectId")
473+
?: getEnv("BROWSERBASE_PROJECT_ID"))
474+
?.let { browserbaseProjectId(it) }
450475
(System.getProperty("stagehand.modelApiKey") ?: getEnv("MODEL_API_KEY"))?.let {
451476
modelApiKey(it)
452477
}
@@ -525,7 +550,13 @@ private constructor(
525550
return ClientOptions(
526551
httpClient,
527552
RetryingHttpClient.builder()
528-
.httpClient(httpClient)
553+
.httpClient(
554+
LoggingHttpClient.builder()
555+
.httpClient(httpClient)
556+
.clock(clock)
557+
.level(logLevel)
558+
.build()
559+
)
529560
.sleeper(sleeper)
530561
.clock(clock)
531562
.maxRetries(maxRetries)
@@ -541,6 +572,7 @@ private constructor(
541572
responseValidation,
542573
timeout,
543574
maxRetries,
575+
logLevel,
544576
browserbaseApiKey,
545577
browserbaseProjectId,
546578
modelApiKey,
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// File generated from our OpenAPI spec by Stainless.
2+
3+
package com.browserbase.api.core
4+
5+
/** The level at which to log request and response information. */
6+
enum class LogLevel {
7+
/** No logging. */
8+
OFF,
9+
/** Minimal request and response summary logs. No headers or bodies are logged. */
10+
INFO,
11+
/** [INFO] logs plus details about request failures. */
12+
ERROR,
13+
/**
14+
* Full request and response logs. Sensitive headers are redacted, but sensitive data in request
15+
* and response bodies may still be visible.
16+
*/
17+
DEBUG;
18+
19+
/** Returns whether this level is at or higher than the given [level]. */
20+
fun shouldLog(level: LogLevel): Boolean = ordinal >= level.ordinal
21+
22+
companion object {
23+
24+
/** Returns a [LogLevel] based on the `STAGEHAND_LOG` environment variable. */
25+
fun fromEnv() =
26+
when (System.getenv("STAGEHAND_LOG")?.lowercase()) {
27+
"info" -> INFO
28+
"error" -> ERROR
29+
"debug" -> DEBUG
30+
else -> OFF
31+
}
32+
}
33+
}

stagehand-java-core/src/main/kotlin/com/browserbase/api/core/Utils.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package com.browserbase.api.core
55
import com.browserbase.api.errors.StagehandInvalidDataException
66
import java.util.Collections
77
import java.util.SortedMap
8+
import java.util.SortedSet
89
import java.util.concurrent.CompletableFuture
910
import java.util.concurrent.locks.Lock
1011

@@ -16,6 +17,11 @@ internal fun <T : Any> T?.getOrThrow(name: String): T =
1617
internal fun <T> List<T>.toImmutable(): List<T> =
1718
if (isEmpty()) Collections.emptyList() else Collections.unmodifiableList(toList())
1819

20+
@JvmSynthetic
21+
internal fun <V : Comparable<V>> SortedSet<V>.toImmutable(): SortedSet<V> =
22+
if (isEmpty()) Collections.emptySortedSet()
23+
else Collections.unmodifiableSortedSet(toSortedSet(comparator() ?: Comparator.naturalOrder()))
24+
1925
@JvmSynthetic
2026
internal fun <K, V> Map<K, V>.toImmutable(): Map<K, V> =
2127
if (isEmpty()) immutableEmptyMap() else Collections.unmodifiableMap(toMap())

0 commit comments

Comments
 (0)