From 5f0c7d9686ab8fae3e70b1433afaf9fb09c5ad58 Mon Sep 17 00:00:00 2001 From: Aditya Mane Date: Tue, 30 Jun 2026 15:52:25 +0530 Subject: [PATCH 1/2] Add network_logs_capture_content input to capture network log content Adds a boolean step input `network_logs_capture_content` so XCUITest builds can capture the request/response content (bodies) of network logs, not just metadata. Previously this could only be set on local/IDE runs. When enabled, the step sends networkLogsOptions.captureContent in the build payload and automatically enables networkLogs (capture content is only honoured when network logs are on). When disabled/omitted the payload is unchanged (networkLogsOptions is omitted), so it is fully backward compatible. Invalid values fail with a clear, actionable error. - step.yml: new boolean input under "Debug logs" - structs.go: NetworkLogsOptions struct + *NetworkLogsOptions field (omitempty) - util_fns.go: parseCaptureContent helper + wiring in createBuildPayload - constants.go: actionable error string for invalid values - 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 | 74 ++++++++++++++++++++++++++++++++++++++++++++++++++++ step.yml | 12 +++++++++ structs.go | 35 ++++++++++++++----------- util_fns.go | 33 +++++++++++++++++++++++ 6 files changed, 142 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2f27a5d..a90a0ea 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` | +| `Capture network logs content` | Capture request and response content (bodies) in network logs, not just metadata. Automatically enables **Network logs**. | Optional | `false` | | `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..7d8d22b 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_CAPTURE_CONTENT_INVALID = "Invalid value for network_logs_capture_content: %q. Accepted values are \"true\" or \"false\"." ) diff --git a/main_test.go b/main_test.go index ae730ce..a3af9ac 100644 --- a/main_test.go +++ b/main_test.go @@ -1,6 +1,7 @@ package main import ( + "encoding/json" "os" "testing" @@ -109,3 +110,76 @@ func TestLocateTestRunnerFileAndZip(t *testing.T) { os.Remove("test_suite.zip") } } + +func TestParseCaptureContent(t *testing.T) { + t.Log("It should treat an empty value as disabled") + { + enabled, err := parseCaptureContent("") + require.NoError(t, err) + require.False(t, enabled) + } + t.Log("It should enable on 'true'") + { + enabled, err := parseCaptureContent("true") + require.NoError(t, err) + require.True(t, enabled) + } + t.Log("It should disable on 'false'") + { + enabled, err := parseCaptureContent("false") + require.NoError(t, err) + require.False(t, enabled) + } + t.Log("It should return an actionable error on an invalid value") + { + _, err := parseCaptureContent("banana") + require.Error(t, err) + require.Contains(t, err.Error(), "network_logs_capture_content") + } +} + +func TestCreateBuildPayloadCaptureContent(t *testing.T) { + t.Setenv("devices_list", "iPhone 14-16") + + t.Log("capture content true -> networkLogs forced true + networkLogsOptions.captureContent true") + { + t.Setenv("network_logs", "false") + t.Setenv("network_logs_capture_content", "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("capture content false -> networkLogsOptions omitted from payload") + { + t.Setenv("network_logs", "true") + t.Setenv("network_logs_capture_content", "false") + + payload := createBuildPayload() + require.Nil(t, payload.NetworkLogsOptions) + + marshalled, err := json.Marshal(payload) + require.NoError(t, err) + assert.NotContains(t, string(marshalled), "networkLogsOptions") + } + + t.Log("capture content unset -> networkLogsOptions omitted (backward compatible)") + { + t.Setenv("network_logs", "true") + t.Setenv("network_logs_capture_content", "") + + 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..5e561bf 100644 --- a/step.yml +++ b/step.yml @@ -130,6 +130,18 @@ inputs: - "true" - "false" category: 'Debug logs' + - network_logs_capture_content: "false" + opts: + title: 'Capture network logs content' + summary: 'Capture the request and response content (bodies) in network logs, not just metadata.' + description: | + Capture the content (request and response bodies) of network logs, 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). + value_options: + - "true" + - "false" + 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..6389e5b 100644 --- a/util_fns.go +++ b/util_fns.go @@ -95,6 +95,23 @@ func getTestFilters(payload *BrowserStackPayload) { } } +// parseCaptureContent parses the network_logs_capture_content input. +// An empty value means "disabled". Standard boolean literals are accepted +// (true/false/1/0/...). Any other value is treated as a hard, actionable error. +func parseCaptureContent(raw string) (bool, error) { + raw = strings.TrimSpace(raw) + if raw == "" { + return false, nil + } + + enabled, err := strconv.ParseBool(raw) + if err != nil { + return false, fmt.Errorf(NETWORK_LOGS_CAPTURE_CONTENT_INVALID, raw) + } + + return enabled, 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")) + capture_content, capture_content_err := parseCaptureContent(os.Getenv("network_logs_capture_content")) + if capture_content_err != nil { + failf(capture_content_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.captureContent is only honoured by App Automate when + // networkLogs is also enabled, so enable it automatically when capture + // content is requested (avoids a backend "networkLogs not enabled" error). + if capture_content { + if !network_logs { + log.Println("network_logs auto-enabled because network_logs_capture_content is set to true") + } + payload.NetworkLogs = true + payload.NetworkLogsOptions = &NetworkLogsOptions{CaptureContent: true} + } + getTestFilters(&payload) if len(sharding_data.Mapping) != 0 && sharding_data.NumberOfShards != 0 { From 8b53e678a99ff75c3a150c9cae23eb777fdb9d9d Mon Sep 17 00:00:00 2001 From: Aditya Mane Date: Tue, 30 Jun 2026 16:54:40 +0530 Subject: [PATCH 2/2] Switch the network logs input to a networkLogsOptions JSON field Use a `network_logs_options` JSON input that mirrors the App Automate `networkLogsOptions` capability (e.g. {"captureContent": true}) instead of a boolean toggle, so the Bitrise input matches what users already pass on local/IDE runs and can carry future networkLogsOptions keys. - step.yml: network_logs_capture_content (bool) -> network_logs_options (JSON) - util_fns.go: parseCaptureContent -> parseNetworkLogsOptions (json.Unmarshal) - constants.go: error string updated for invalid JSON - main_test.go: tests updated for the JSON input - README.md: input row updated The wire payload is unchanged: networkLogs:true + networkLogsOptions:{captureContent:true}. AAP-20051 Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 2 +- constants.go | 2 +- main_test.go | 57 ++++++++++++++++++++++++---------------------------- step.yml | 15 +++++++------- util_fns.go | 36 ++++++++++++++++----------------- 5 files changed, 54 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index a90a0ea..a2e7de7 100644 --- a/README.md +++ b/README.md @@ -43,7 +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` | -| `Capture network logs content` | Capture request and response content (bodies) in network logs, not just metadata. Automatically enables **Network logs**. | 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 7d8d22b..0673d91 100644 --- a/constants.go +++ b/constants.go @@ -25,5 +25,5 @@ const ( 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_CAPTURE_CONTENT_INVALID = "Invalid value for network_logs_capture_content: %q. Accepted values are \"true\" or \"false\"." + 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 a3af9ac..777b2e4 100644 --- a/main_test.go +++ b/main_test.go @@ -111,40 +111,48 @@ func TestLocateTestRunnerFileAndZip(t *testing.T) { } } -func TestParseCaptureContent(t *testing.T) { - t.Log("It should treat an empty value as disabled") +func TestParseNetworkLogsOptions(t *testing.T) { + t.Log("It should treat an empty value as not set") { - enabled, err := parseCaptureContent("") + options, err := parseNetworkLogsOptions("") require.NoError(t, err) - require.False(t, enabled) + require.Nil(t, options) } - t.Log("It should enable on 'true'") + t.Log("It should parse captureContent true") { - enabled, err := parseCaptureContent("true") + options, err := parseNetworkLogsOptions(`{"captureContent": true}`) require.NoError(t, err) - require.True(t, enabled) + require.NotNil(t, options) + require.True(t, options.CaptureContent) } - t.Log("It should disable on 'false'") + t.Log("It should parse captureContent false") { - enabled, err := parseCaptureContent("false") + options, err := parseNetworkLogsOptions(`{"captureContent": false}`) require.NoError(t, err) - require.False(t, enabled) + require.NotNil(t, options) + require.False(t, options.CaptureContent) } - t.Log("It should return an actionable error on an invalid value") + t.Log("It should return an actionable error on malformed JSON") { - _, err := parseCaptureContent("banana") + _, err := parseNetworkLogsOptions(`{captureContent: true}`) require.Error(t, err) - require.Contains(t, err.Error(), "network_logs_capture_content") + 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 TestCreateBuildPayloadCaptureContent(t *testing.T) { +func TestCreateBuildPayloadNetworkLogsOptions(t *testing.T) { t.Setenv("devices_list", "iPhone 14-16") - t.Log("capture content true -> networkLogs forced true + networkLogsOptions.captureContent true") + t.Log("captureContent true -> networkLogs forced true + networkLogsOptions.captureContent true") { t.Setenv("network_logs", "false") - t.Setenv("network_logs_capture_content", "true") + t.Setenv("network_logs_options", `{"captureContent": true}`) payload := createBuildPayload() require.True(t, payload.NetworkLogs) @@ -157,23 +165,10 @@ func TestCreateBuildPayloadCaptureContent(t *testing.T) { assert.Contains(t, string(marshalled), `"networkLogsOptions":{"captureContent":true}`) } - t.Log("capture content false -> networkLogsOptions omitted from payload") - { - t.Setenv("network_logs", "true") - t.Setenv("network_logs_capture_content", "false") - - payload := createBuildPayload() - require.Nil(t, payload.NetworkLogsOptions) - - marshalled, err := json.Marshal(payload) - require.NoError(t, err) - assert.NotContains(t, string(marshalled), "networkLogsOptions") - } - - t.Log("capture content unset -> networkLogsOptions omitted (backward compatible)") + t.Log("options unset -> networkLogsOptions omitted (backward compatible)") { t.Setenv("network_logs", "true") - t.Setenv("network_logs_capture_content", "") + t.Setenv("network_logs_options", "") payload := createBuildPayload() require.Nil(t, payload.NetworkLogsOptions) diff --git a/step.yml b/step.yml index 5e561bf..18bf559 100644 --- a/step.yml +++ b/step.yml @@ -130,17 +130,18 @@ inputs: - "true" - "false" category: 'Debug logs' - - network_logs_capture_content: "false" + - network_logs_options: opts: - title: 'Capture network logs content' - summary: 'Capture the request and response content (bodies) in network logs, not just metadata.' + title: 'Network logs options' + summary: 'Refine network log capture as JSON. Supports captureContent to capture request/response bodies.' description: | - Capture the content (request and response bodies) of network logs, in addition to metadata such as URLs, headers, latency and status codes. + 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). - value_options: - - "true" - - "false" category: 'Debug logs' - device_logs: "false" opts: diff --git a/util_fns.go b/util_fns.go index 6389e5b..189ffc6 100644 --- a/util_fns.go +++ b/util_fns.go @@ -95,21 +95,21 @@ func getTestFilters(payload *BrowserStackPayload) { } } -// parseCaptureContent parses the network_logs_capture_content input. -// An empty value means "disabled". Standard boolean literals are accepted -// (true/false/1/0/...). Any other value is treated as a hard, actionable error. -func parseCaptureContent(raw string) (bool, error) { +// 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 false, nil + return nil, nil } - enabled, err := strconv.ParseBool(raw) - if err != nil { - return false, fmt.Errorf(NETWORK_LOGS_CAPTURE_CONTENT_INVALID, raw) + options := NetworkLogsOptions{} + if err := json.Unmarshal([]byte(raw), &options); err != nil { + return nil, fmt.Errorf(NETWORK_LOGS_OPTIONS_INVALID, err) } - return enabled, nil + return &options, nil } // this util only picks data from env and map it to the struct @@ -122,9 +122,9 @@ func createBuildPayload() BrowserStackPayload { use_local, _ := strconv.ParseBool(os.Getenv("use_local")) use_dynamic_tests, _ := strconv.ParseBool(os.Getenv("use_dynamic_tests")) - capture_content, capture_content_err := parseCaptureContent(os.Getenv("network_logs_capture_content")) - if capture_content_err != nil { - failf(capture_content_err.Error()) + 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{} @@ -148,15 +148,15 @@ func createBuildPayload() BrowserStackPayload { UseLocal: use_local, } - // networkLogsOptions.captureContent is only honoured by App Automate when - // networkLogs is also enabled, so enable it automatically when capture - // content is requested (avoids a backend "networkLogs not enabled" error). - if capture_content { + // 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_capture_content is set to true") + log.Println("network_logs auto-enabled because network_logs_options was provided") } payload.NetworkLogs = true - payload.NetworkLogsOptions = &NetworkLogsOptions{CaptureContent: true} + payload.NetworkLogsOptions = network_logs_options } getTestFilters(&payload)