|  | 
|  | 1 | +# Business Metrics Implementation Guidelines | 
|  | 2 | + | 
|  | 3 | +## Table of Contents | 
|  | 4 | +- [Overview](#overview) | 
|  | 5 | +- [Core Principles](#core-principles) | 
|  | 6 | +- [Implementation Patterns](#implementation-patterns) | 
|  | 7 | +- [Performance Considerations](#performance-considerations) | 
|  | 8 | +- [Testing Requirements](#testing-requirements) | 
|  | 9 | +- [Examples and References](#examples-and-references) | 
|  | 10 | + | 
|  | 11 | +## Overview | 
|  | 12 | + | 
|  | 13 | +Business metrics are short identifiers added to the User-Agent header for telemetry tracking. They help AWS understand feature usage patterns across the SDK. This document provides guidelines for implementing business metrics in the AWS SDK for Java v2, based on team architectural decisions and performance considerations. | 
|  | 14 | + | 
|  | 15 | +**Key Concepts:** | 
|  | 16 | +- **Business Metrics**: Short string identifiers (e.g., "S", "A", "B") that represent feature usage | 
|  | 17 | +- **User-Agent Header**: HTTP header where business metrics are included for telemetry | 
|  | 18 | + | 
|  | 19 | +## Core Principles | 
|  | 20 | + | 
|  | 21 | +### Feature-Centric Placement | 
|  | 22 | + | 
|  | 23 | +**MUST** add business metrics where the feature is resolved, at the point where it is finalized that the feature is being used. Consider cases where features can be overridden - the decision is to add business metrics at the place where the feature is finalized. | 
|  | 24 | + | 
|  | 25 | +**Rationale:** Based on team discussion, this approach was chosen over centralized placement in `ApplyUserAgentStage` because: | 
|  | 26 | +- **Better separation of concerns**: `ApplyUserAgentStage` remains ignorant of internal feature implementation details | 
|  | 27 | +- **Easier maintenance**: Feature refactoring doesn't require updating multiple places | 
|  | 28 | +- **Reduced coupling**: Avoids tight coupling between stages and feature implementations | 
|  | 29 | + | 
|  | 30 | + | 
|  | 31 | +## Implementation Patterns | 
|  | 32 | + | 
|  | 33 | +For GZIP compression, we know that the request is compressed in `CompressRequestStage`, so we add the business metric there. For checksums, we know that checksum is resolved in `HttpChecksumStage`, so we add the business metric there. | 
|  | 34 | + | 
|  | 35 | +```java | 
|  | 36 | +// Example from CompressRequestStage | 
|  | 37 | +private void updateContentEncodingHeader(SdkHttpFullRequest.Builder input, | 
|  | 38 | +                                         Compressor compressor, | 
|  | 39 | +                                         ExecutionAttributes executionAttributes) { | 
|  | 40 | +    // Record business metric when compression is actually applied | 
|  | 41 | +    executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS) | 
|  | 42 | +                       .addMetric(BusinessMetricFeatureId.GZIP_REQUEST_COMPRESSION.value()); | 
|  | 43 | +     | 
|  | 44 | +    if (input.firstMatchingHeader(COMPRESSION_HEADER).isPresent()) { | 
|  | 45 | +        input.appendHeader(COMPRESSION_HEADER, compressor.compressorType()); | 
|  | 46 | +    } else { | 
|  | 47 | +        input.putHeader(COMPRESSION_HEADER, compressor.compressorType()); | 
|  | 48 | +    } | 
|  | 49 | +} | 
|  | 50 | + | 
|  | 51 | +// Example from HttpChecksumStage - showing where business metrics are recorded | 
|  | 52 | +@Override | 
|  | 53 | +public SdkHttpFullRequest.Builder execute(SdkHttpFullRequest.Builder request,  | 
|  | 54 | +                                          RequestExecutionContext context) throws Exception { | 
|  | 55 | +    // ... feature resolution logic ... | 
|  | 56 | +     | 
|  | 57 | +    SdkHttpFullRequest.Builder result = processChecksum(request, context); | 
|  | 58 | +     | 
|  | 59 | +    // Record business metrics after feature is finalized | 
|  | 60 | +    recordChecksumBusinessMetrics(context.executionAttributes()); | 
|  | 61 | +     | 
|  | 62 | +    return result; | 
|  | 63 | +} | 
|  | 64 | +``` | 
|  | 65 | + | 
|  | 66 | +## Performance Considerations | 
|  | 67 | + | 
|  | 68 | +### Avoid Request Mutation for Business Metrics | 
|  | 69 | + | 
|  | 70 | +**SHOULD NOT** use request mutation (`.toBuilder().build()`) for adding business metrics as it creates unnecessary object copies and performance overhead. | 
|  | 71 | + | 
|  | 72 | +**Avoid This Pattern** (Used in waiter/paginator implementations): | 
|  | 73 | +```java | 
|  | 74 | +// Creates new objects (performance overhead) | 
|  | 75 | +Consumer<AwsRequestOverrideConfiguration.Builder> userAgentApplier =  | 
|  | 76 | +    b -> b.addApiName(ApiName.builder().name("sdk-metrics").version("B").build()); | 
|  | 77 | + | 
|  | 78 | +AwsRequestOverrideConfiguration overrideConfiguration = | 
|  | 79 | +    request.overrideConfiguration().map(c -> c.toBuilder().applyMutation(userAgentApplier).build()) | 
|  | 80 | +    .orElse(AwsRequestOverrideConfiguration.builder().applyMutation(userAgentApplier).build()); | 
|  | 81 | + | 
|  | 82 | +return (T) request.toBuilder().overrideConfiguration(overrideConfiguration).build(); | 
|  | 83 | +``` | 
|  | 84 | + | 
|  | 85 | + **Prefer This ExecutionAttributes Pattern**: | 
|  | 86 | +```java | 
|  | 87 | +// Direct business metrics collection (no object creation) | 
|  | 88 | +private void recordFeatureBusinessMetric(ExecutionAttributes executionAttributes) { | 
|  | 89 | +    BusinessMetricCollection businessMetrics =  | 
|  | 90 | +        executionAttributes.getAttribute(SdkInternalExecutionAttribute.BUSINESS_METRICS); | 
|  | 91 | +     | 
|  | 92 | +    if (businessMetrics != null) { | 
|  | 93 | +        businessMetrics.addMetric(BusinessMetricFeatureId.FEATURE_ID.value()); | 
|  | 94 | +    } | 
|  | 95 | +} | 
|  | 96 | +``` | 
|  | 97 | + | 
|  | 98 | +## Testing Requirements | 
|  | 99 | + | 
|  | 100 | +### Functional Testing with Mock HTTP Clients | 
|  | 101 | + | 
|  | 102 | +**MUST** use functional testing with mock HTTP clients instead of interceptor-based testing. | 
|  | 103 | + | 
|  | 104 | +**Why Mock HTTP Clients:** | 
|  | 105 | +- **Reliability**: Tests are not affected by interceptor ordering changes or SDK internal modifications | 
|  | 106 | +- **End-to-end verification**: Tests verify the complete flow from feature usage to User-Agent header inclusion | 
|  | 107 | +- **Simplicity**: Direct access to the final HTTP request without interceptor setup | 
|  | 108 | +- **Maintainability**: Tests remain stable even when internal pipeline stages are refactored | 
|  | 109 | + | 
|  | 110 | +**Testing Pattern:** | 
|  | 111 | +1. Create a mock HTTP client and stub the response | 
|  | 112 | +2. Build the SDK client with the mock HTTP client | 
|  | 113 | +3. Execute the operation that should trigger the business metric | 
|  | 114 | +4. Extract the User-Agent header from the captured request | 
|  | 115 | +5. Verify the business metric is present using pattern matching | 
|  | 116 | + | 
|  | 117 | +```java | 
|  | 118 | +@Test | 
|  | 119 | +void testBusinessMetric_withMockHttpClient() { | 
|  | 120 | +    MockSyncHttpClient mockHttpClient = new MockSyncHttpClient(); | 
|  | 121 | +    mockHttpClient.stubNextResponse(HttpExecuteResponse.builder() | 
|  | 122 | +                                   .response(SdkHttpResponse.builder() | 
|  | 123 | +                                            .statusCode(200) | 
|  | 124 | +                                            .build()) | 
|  | 125 | +                                   .build()); | 
|  | 126 | +     | 
|  | 127 | +    // Create client with mock HTTP client and make request | 
|  | 128 | +    S3Client client = S3Client.builder() | 
|  | 129 | +                              .httpClient(mockHttpClient) | 
|  | 130 | +                              .build(); | 
|  | 131 | +     | 
|  | 132 | +    client.listBuckets(); | 
|  | 133 | +     | 
|  | 134 | +    // Extract User-Agent from the last request | 
|  | 135 | +    SdkHttpRequest lastRequest = mockHttpClient.getLastRequest(); | 
|  | 136 | +    String userAgent = lastRequest.firstMatchingHeader("User-Agent").orElse(""); | 
|  | 137 | +     | 
|  | 138 | +    // Verify business metric is present | 
|  | 139 | +    assertThat(userAgent).matches(METRIC_SEARCH_PATTERN.apply("A")); | 
|  | 140 | +} | 
|  | 141 | +``` | 
|  | 142 | + | 
|  | 143 | +**For Async Clients:** | 
|  | 144 | +Use `MockAsyncHttpClient` with the same pattern for testing async operations. | 
|  | 145 | + | 
|  | 146 | +### Reference Test Files | 
|  | 147 | +- `test/auth-tests/src/it/java/software/amazon/awssdk/auth/source/UserAgentProviderTest.java` | 
|  | 148 | +- `test/codegen-generated-classes-test/src/test/java/software/amazon/awssdk/services/rpcv2cbor/RpcV2CborUserAgentTest.java` | 
|  | 149 | + | 
|  | 150 | + | 
|  | 151 | +## Examples and References | 
|  | 152 | + | 
|  | 153 | +Here are some example implementations: | 
|  | 154 | + | 
|  | 155 | +### Key Files and Classes | 
|  | 156 | +- **BusinessMetricFeatureId**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/useragent/BusinessMetricFeatureId.java` | 
|  | 157 | +- **BusinessMetricsUtils**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/useragent/BusinessMetricsUtils.java` | 
|  | 158 | +- **ApplyUserAgentStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/ApplyUserAgentStage.java` | 
|  | 159 | +- **HttpChecksumStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/HttpChecksumStage.java` | 
|  | 160 | +- **CompressRequestStage**: `core/sdk-core/src/main/java/software/amazon/awssdk/core/internal/http/pipeline/stages/CompressRequestStage.java` | 
|  | 161 | +- **AuthSchemeInterceptorSpec**: `codegen/src/main/java/software/amazon/awssdk/codegen/poet/auth/scheme/AuthSchemeInterceptorSpec.java` | 
0 commit comments