From 08ef396ec2f83f56c9cc95676c823730c5404bd8 Mon Sep 17 00:00:00 2001 From: Aditya Mane Date: Mon, 6 Jul 2026 12:23:50 +0530 Subject: [PATCH] Add network_logs_options input to capture network log content Adds a `network_logs_options` step input that mirrors the App Automate `networkLogsOptions` capability (e.g. {"captureContent": true}) so Espresso builds can capture the request/response content (bodies) of network logs, not just metadata. Previously this could only be set on local/IDE runs. The input is parsed as JSON into networkLogsOptions and forwarded in the build payload. Providing it automatically enables networkLogs (the option is only honoured when network logs are on). When empty the payload is unchanged (networkLogsOptions is omitted), so it is fully backward compatible. Invalid JSON fails with a clear, actionable error. - step.yml: new network_logs_options JSON input under "Debug logs" - structs.go: NetworkLogsOptions struct + *NetworkLogsOptions field (omitempty) - util_fns.go: parseNetworkLogsOptions helper + wiring in createBuildPayload - constants.go: actionable error string for invalid JSON - main_test.go: unit tests for parsing and payload shape - README.md: documented the new input AAP-20051 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 1 + constants.go | 2 ++ main_test.go | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++++ step.yml | 13 ++++++++++ structs.go | 43 +++++++++++++++++--------------- util_fns.go | 33 +++++++++++++++++++++++++ 6 files changed, 142 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index d16c2f0..5539fe8 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ To add and configure the "BrowserStack App Automate - Espresso" step in Bitrise, | `Devices` | Name of one or more device-OS combination in new line. For example:
`Samsung Galaxy S9 Plus-9.0`
`Google Pixel 3a-9.0` | required | | | `instrumentation_logs` | Generate instrumentation logs of the test session | | `true` | | `Network logs` | Generate network logs of your Espresso test sessions to capture network traffic, latency, etc. | | `false` | +| `Network logs options` | Refine network log capture as JSON (mirrors the `networkLogsOptions` capability). Supports `{"captureContent": true}` to capture request/response bodies, not just metadata. Automatically enables **Network logs**. | | | | `Android logcat logs` | Generate device logs (Android logcat) | | `false` | | `Capture Screenshots` | Capture the screenshots of the test execution| | `false` | | `Video recording` | Record video of the test execution | | `true` | diff --git a/constants.go b/constants.go index 2478dbf..86fe10b 100644 --- a/constants.go +++ b/constants.go @@ -20,4 +20,6 @@ const ( BUILD_FAILED_ERROR = "Failed to execute build on BrowserStack, error: %s" FETCH_BUILD_STATUS_ERROR = "Failed to fetch test results, error: %s" HTTP_ERROR = "Something went wrong while processing your request, error: %s" + + NETWORK_LOGS_OPTIONS_INVALID = "Invalid network_logs_options. Expected JSON such as {\"captureContent\": true}. Error: %s" ) diff --git a/main_test.go b/main_test.go index 5499966..069d12d 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "testing" "github.com/stretchr/testify/require" @@ -86,3 +87,71 @@ func TestGetDevices(t *testing.T) { require.Error(t, err) } } + +func TestParseNetworkLogsOptions(t *testing.T) { + t.Log("It should treat an empty value as not set") + { + options, err := parseNetworkLogsOptions("") + require.NoError(t, err) + require.Nil(t, options) + } + t.Log("It should parse captureContent true") + { + options, err := parseNetworkLogsOptions(`{"captureContent": true}`) + require.NoError(t, err) + require.NotNil(t, options) + require.True(t, options.CaptureContent) + } + t.Log("It should parse captureContent false") + { + options, err := parseNetworkLogsOptions(`{"captureContent": false}`) + require.NoError(t, err) + require.NotNil(t, options) + require.False(t, options.CaptureContent) + } + t.Log("It should return an actionable error on malformed JSON") + { + _, err := parseNetworkLogsOptions(`{captureContent: true}`) + require.Error(t, err) + require.Contains(t, err.Error(), "network_logs_options") + } + t.Log("It should return an actionable error on a wrong value type") + { + _, err := parseNetworkLogsOptions(`{"captureContent": "true"}`) + require.Error(t, err) + require.Contains(t, err.Error(), "network_logs_options") + } +} + +func TestCreateBuildPayloadNetworkLogsOptions(t *testing.T) { + t.Setenv("devices_list", "Google Pixel 6 Pro-15.0") + + t.Log("captureContent true -> networkLogs forced true + networkLogsOptions.captureContent true") + { + t.Setenv("network_logs", "false") + t.Setenv("network_logs_options", `{"captureContent": true}`) + + payload := createBuildPayload() + require.True(t, payload.NetworkLogs) + require.NotNil(t, payload.NetworkLogsOptions) + require.True(t, payload.NetworkLogsOptions.CaptureContent) + + marshalled, err := json.Marshal(payload) + require.NoError(t, err) + require.Contains(t, string(marshalled), `"networkLogs":true`) + require.Contains(t, string(marshalled), `"networkLogsOptions":{"captureContent":true}`) + } + + t.Log("options unset -> networkLogsOptions omitted (backward compatible)") + { + t.Setenv("network_logs", "true") + t.Setenv("network_logs_options", "") + + payload := createBuildPayload() + require.Nil(t, payload.NetworkLogsOptions) + + marshalled, err := json.Marshal(payload) + require.NoError(t, err) + require.NotContains(t, string(marshalled), "networkLogsOptions") + } +} diff --git a/step.yml b/step.yml index 65241a2..cbd9393 100644 --- a/step.yml +++ b/step.yml @@ -140,6 +140,19 @@ inputs: - "true" - "false" category: "Debug logs" + - network_logs_options: + opts: + title: "Network logs options" + summary: "Refine network log capture as JSON. Supports captureContent to capture request/response bodies." + description: | + Provide network log options as JSON to refine what your network logs capture. This mirrors the `networkLogsOptions` capability used in local/IDE runs. + + Currently supported: + + `{"captureContent": true}` - capture the request and response content (bodies), in addition to metadata such as URLs, headers, latency and status codes. + + Enabling this automatically enables **Network Logs** (it has no effect on its own). + category: "Debug logs" - device_logs: "false" opts: title: "Android logcat logs" diff --git a/structs.go b/structs.go index 9584efc..36bff81 100644 --- a/structs.go +++ b/structs.go @@ -12,26 +12,31 @@ type TestSharding struct { AutoStrategyDevices []string `json:"devices,omitempty"` } +type NetworkLogsOptions struct { + CaptureContent bool `json:"captureContent"` +} + type BrowserStackPayload struct { - App string `json:"app"` - TestSuite string `json:"testSuite"` - Devices []string `json:"devices"` - InstrumentationLogs bool `json:"instrumentationLogs"` - NetworkLogs bool `json:"networkLogs"` - DeviceLogs bool `json:"deviceLogs"` - DebugScreenshots bool `json:"debugscreenshots,omitempty"` - VideoRecording bool `json:"video"` - Project string `json:"project,omitempty"` - ProjectNotifyURL string `json:"projectNotifyURL,omitempty"` - UseLocal bool `json:"local,omitempty"` - ClearAppData bool `json:"clearPackageData,omitempty"` - SingleRunnerInvocation bool `json:"singleRunnerInvocation,omitempty"` - Class []string `json:"class,omitempty"` - Package []string `json:"package,omitempty"` - Annotation []string `json:"annotation,omitempty"` - Size []string `json:"size,omitempty"` - UseMockServer bool `json:"allowDeviceMockServer,omitempty"` - UseTestSharding interface{} `json:"shards,omitempty"` + App string `json:"app"` + TestSuite string `json:"testSuite"` + Devices []string `json:"devices"` + InstrumentationLogs bool `json:"instrumentationLogs"` + NetworkLogs bool `json:"networkLogs"` + NetworkLogsOptions *NetworkLogsOptions `json:"networkLogsOptions,omitempty"` + DeviceLogs bool `json:"deviceLogs"` + DebugScreenshots bool `json:"debugscreenshots,omitempty"` + VideoRecording bool `json:"video"` + Project string `json:"project,omitempty"` + ProjectNotifyURL string `json:"projectNotifyURL,omitempty"` + UseLocal bool `json:"local,omitempty"` + ClearAppData bool `json:"clearPackageData,omitempty"` + SingleRunnerInvocation bool `json:"singleRunnerInvocation,omitempty"` + Class []string `json:"class,omitempty"` + Package []string `json:"package,omitempty"` + Annotation []string `json:"annotation,omitempty"` + Size []string `json:"size,omitempty"` + UseMockServer bool `json:"allowDeviceMockServer,omitempty"` + UseTestSharding interface{} `json:"shards,omitempty"` // Apart from the inputs from UI, these are some more fields which we support. // We've mentioned the type and the json key for these field. diff --git a/util_fns.go b/util_fns.go index f9b4bd0..94a73a4 100644 --- a/util_fns.go +++ b/util_fns.go @@ -103,6 +103,23 @@ func getTestFilters(payload *BrowserStackPayload) { } } +// parseNetworkLogsOptions parses the network_logs_options input, which mirrors +// the App Automate `networkLogsOptions` capability (e.g. {"captureContent": true}). +// An empty value means "not set". Invalid JSON is a hard, actionable error. +func parseNetworkLogsOptions(raw string) (*NetworkLogsOptions, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil, nil + } + + options := NetworkLogsOptions{} + if err := json.Unmarshal([]byte(raw), &options); err != nil { + return nil, fmt.Errorf(NETWORK_LOGS_OPTIONS_INVALID, err) + } + + return &options, nil +} + // this util only picks data from env and map it to the struct func createBuildPayload() BrowserStackPayload { instrumentation_logs, _ := strconv.ParseBool(os.Getenv("instrumentation_logs")) @@ -115,6 +132,11 @@ func createBuildPayload() BrowserStackPayload { use_single_runner_invocation, _ := strconv.ParseBool(os.Getenv("use_single_runner_invocation")) use_mock_server, _ := strconv.ParseBool(os.Getenv("use_mock_server")) + network_logs_options, network_logs_options_err := parseNetworkLogsOptions(os.Getenv("network_logs_options")) + if network_logs_options_err != nil { + failf(network_logs_options_err.Error()) + } + sharding_data := TestSharding{} if os.Getenv("use_test_sharding") != "" { err := json.Unmarshal([]byte(os.Getenv("use_test_sharding")), &sharding_data) @@ -138,6 +160,17 @@ func createBuildPayload() BrowserStackPayload { UseMockServer: use_mock_server, } + // networkLogsOptions is only honoured by App Automate when networkLogs is + // also enabled, so enable it automatically when options are provided + // (avoids a backend "networkLogs not enabled" error). + if network_logs_options != nil { + if !network_logs { + log.Println("network_logs auto-enabled because network_logs_options was provided") + } + payload.NetworkLogs = true + payload.NetworkLogsOptions = network_logs_options + } + getTestFilters(&payload) if len(sharding_data.Mapping) != 0 && sharding_data.NumberOfShards != 0 {