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 {