diff --git a/README.md b/README.md
index 2f27a5d..a2e7de7 100644
--- a/README.md
+++ b/README.md
@@ -43,6 +43,7 @@ Complete the following steps to configure BrowserStack's XCUI step in Bitrise:
| `Devices` | Provide one or more device-OS combination in a new line. For example:
`iPhone 11-13`
`iPhone XS-15` | Required | N/A |
| `Instrumentation logs` | Generate instrumentation logs of the test session | Optional | `true` |
| `Network logs` | Generate network logs of your test sessions to capture network traffic, latency, etc. | Optional | `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**. | Optional | N/A |
| `Device Logs` | Generate device logs | Optional | `false` |
| `Capture screenshots` | Capture the screenshots of the test execution| Optional | `false` |
| `Video recording` | Record video of the test execution | Optional | `true` |
diff --git a/constants.go b/constants.go
index c686f7d..0673d91 100644
--- a/constants.go
+++ b/constants.go
@@ -24,4 +24,6 @@ const (
RUNNER_APP_NOT_FOUND = "xcuitest_testsuite_path: couldn’t find the -Runner.app . Please add the $BITRISE_TEST_BUNDLE_PATH from Xcode Build for testing for iOS step or the absolute path of -Runner.app"
IPA_NOT_FOUND = "app_ipa_path: couldn’t find the iOS app (.ipa file). Please add the $BITRISE_IPA_PATH from Xcode Archive & Export for iOS step or the absolute path of iOS app (.ipa file)"
FILE_ZIP_ERROR = "Something went wrong while processing the test-suite, 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 ae730ce..777b2e4 100644
--- a/main_test.go
+++ b/main_test.go
@@ -1,6 +1,7 @@
package main
import (
+ "encoding/json"
"os"
"testing"
@@ -109,3 +110,71 @@ func TestLocateTestRunnerFileAndZip(t *testing.T) {
os.Remove("test_suite.zip")
}
}
+
+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", "iPhone 14-16")
+
+ 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)
+ assert.Contains(t, string(marshalled), `"networkLogs":true`)
+ assert.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)
+ assert.NotContains(t, string(marshalled), "networkLogsOptions")
+ }
+}
diff --git a/step.yml b/step.yml
index 7da4f1a..18bf559 100644
--- a/step.yml
+++ b/step.yml
@@ -130,6 +130,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: 'Device Logs'
diff --git a/structs.go b/structs.go
index 3d0741b..179bba8 100644
--- a/structs.go
+++ b/structs.go
@@ -12,22 +12,27 @@ 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"`
- SkipTesting []string `json:"skip-testing,omitempty"`
- OnlyTesting []string `json:"only-testing,omitempty"`
- DynamicTests bool `json:"dynamicTests,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"`
+ SkipTesting []string `json:"skip-testing,omitempty"`
+ OnlyTesting []string `json:"only-testing,omitempty"`
+ DynamicTests bool `json:"dynamicTests,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 0f1d7d9..189ffc6 100644
--- a/util_fns.go
+++ b/util_fns.go
@@ -95,6 +95,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"))
@@ -105,6 +122,11 @@ func createBuildPayload() BrowserStackPayload {
use_local, _ := strconv.ParseBool(os.Getenv("use_local"))
use_dynamic_tests, _ := strconv.ParseBool(os.Getenv("use_dynamic_tests"))
+ 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)
@@ -126,6 +148,17 @@ func createBuildPayload() BrowserStackPayload {
UseLocal: use_local,
}
+ // 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 {