From e3fc15443dee4a7b2c9254112698bcf04109be57 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Tue, 25 Feb 2025 20:06:07 -0800 Subject: [PATCH 1/3] add dart server to package testing (FF-4065) --- .github/workflows/test-sdk-packages.yml | 33 ++-- .github/workflows/test-server-sdk.yml | 150 ++++++++++-------- package-testing/dart-sdk-relay/.env.EXAMPLE | 4 + package-testing/dart-sdk-relay/.gitignore | 4 + .../dart-sdk-relay/bin/server.dart | 101 ++++++++++++ .../dart-sdk-relay/build-and-run.sh | 38 +++++ .../lib/assignment_handler.dart | 101 ++++++++++++ .../dart-sdk-relay/lib/bandit_handler.dart | 119 ++++++++++++++ .../dart-sdk-relay/lib/config.dart | 11 ++ .../dart-sdk-relay/lib/relay_logger.dart | 30 ++++ package-testing/dart-sdk-relay/pubspec.yaml | 18 +++ package-testing/dart-sdk-relay/release.sh | 4 + 12 files changed, 536 insertions(+), 77 deletions(-) create mode 100644 package-testing/dart-sdk-relay/.env.EXAMPLE create mode 100644 package-testing/dart-sdk-relay/.gitignore create mode 100644 package-testing/dart-sdk-relay/bin/server.dart create mode 100755 package-testing/dart-sdk-relay/build-and-run.sh create mode 100644 package-testing/dart-sdk-relay/lib/assignment_handler.dart create mode 100644 package-testing/dart-sdk-relay/lib/bandit_handler.dart create mode 100644 package-testing/dart-sdk-relay/lib/config.dart create mode 100644 package-testing/dart-sdk-relay/lib/relay_logger.dart create mode 100644 package-testing/dart-sdk-relay/pubspec.yaml create mode 100755 package-testing/dart-sdk-relay/release.sh diff --git a/.github/workflows/test-sdk-packages.yml b/.github/workflows/test-sdk-packages.yml index 5fad22fd..8f849ffa 100644 --- a/.github/workflows/test-sdk-packages.yml +++ b/.github/workflows/test-sdk-packages.yml @@ -8,37 +8,52 @@ on: workflow_dispatch: jobs: + test-dart-sdk: + strategy: + fail-fast: false + matrix: + platform: ["linux"] + uses: ./.github/workflows/test-server-sdk.yml + with: + platform: ${{ matrix.platform }} + sdkName: "eppo/dart-sdk" + sdkRelayDir: "dart-sdk-relay" + setupAction: "dart-lang/setup-dart@v1" + setupWith: '{"sdk": "stable"}' + secrets: inherit + test-php-sdk: strategy: fail-fast: false matrix: - platform: ['linux'] + platform: ["linux"] uses: ./.github/workflows/test-server-sdk.yml with: platform: ${{ matrix.platform }} - sdkName: 'eppo/php-sdk' - sdkRelayDir: 'php-sdk-relay' + sdkName: "eppo/php-sdk" + sdkRelayDir: "php-sdk-relay" secrets: inherit test-python-sdk: strategy: fail-fast: false matrix: - platform: ['linux'] + platform: ["linux"] uses: ./.github/workflows/test-server-sdk.yml with: platform: ${{ matrix.platform }} - sdkName: 'eppo/python-sdk' - sdkRelayDir: 'python-sdk-relay' + sdkName: "eppo/python-sdk" + sdkRelayDir: "python-sdk-relay" secrets: inherit test-node-sdk: strategy: fail-fast: false - matrix: ['linux'] + matrix: + platform: ["linux"] uses: ./.github/workflows/test-server-sdk.yml with: platform: ${{ matrix.platform }} - sdkName: 'eppo/node-server-sdk' - sdkRelayDir: 'node-sdk-relay' + sdkName: "eppo/node-server-sdk" + sdkRelayDir: "node-sdk-relay" secrets: inherit diff --git a/.github/workflows/test-server-sdk.yml b/.github/workflows/test-server-sdk.yml index f65cec73..ea2e17e9 100644 --- a/.github/workflows/test-server-sdk.yml +++ b/.github/workflows/test-server-sdk.yml @@ -4,21 +4,28 @@ on: workflow_call: inputs: platform: - description: 'Platforms to test the SDK Relay on; linux, macos, windows' + description: "Platforms to test the SDK Relay on; linux, macos, windows" type: string required: true sdkName: - description: 'Name of the SDK' + description: "Name of the SDK" type: string required: true sdkRelayDir: - description: 'Directory of the SDK Relay server code' + description: "Directory of the SDK Relay server code" type: string required: false os: - description: 'Specific runner OS to use' + description: "Specific runner OS to use" type: string - + setupAction: + description: "Optional GitHub action to run for setup (e.g., dart-lang/setup-dart@v1)" + type: string + required: false + setupWith: + description: "Optional JSON string of parameters to pass to the setup action" + type: string + required: false jobs: test-packaged-server-sdks: @@ -37,66 +44,73 @@ jobs: GAR_LOCATION: ${{ vars.SDK_TESTING_REGION }}-docker.pkg.dev/${{ vars.SDK_TESTING_PROJECT_ID }}/sdk-testing steps: - - name: Test information header - shell: bash - run: echo "Running Test Cluster for ${SDK_NAME}" - - - name: Set some variables - id: vars - run: | - echo "::set-output name=date::$(date +'%Y-%m-%d')" - echo "SAFE_SDK_NAME=$(echo ${SDK_NAME} | sed 's/\//_/g')" >> $GITHUB_ENV - - - - name: "Checkout" - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - # Set up docker (macos runners) - - id: setup-docker - if: ${{ inputs.platform == 'macos' }} - name: Setup Docker - uses: douglascamata/setup-docker-macos-action@v1-alpha - - # Set up gCloud - - id: "auth" - uses: "google-github-actions/auth@v1" - with: - credentials_json: "${{ secrets.SERVICE_ACCOUNT_KEY }}" - - - name: "Set up Cloud SDK" - uses: "google-github-actions/setup-gcloud@v1" - - # Allow docker access to the GAR - - name: "Docker auth" - run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet - - # Pull test runner and testing api images for GCP Artifact Registry (GAR) and - # retag them locally as expected by the runner script. - - name: Pull Test Runner image - run: | - docker pull ${{ env.GAR_LOCATION }}/sdk-test-runner:latest - docker tag ${{ env.GAR_LOCATION }}/sdk-test-runner:latest Eppo-exp/sdk-test-runner:latest - docker pull ${{ env.GAR_LOCATION }}/testing-api:latest - docker tag ${{ env.GAR_LOCATION }}/testing-api:latest Eppo-exp/testing-api:latest - - - name: Run tests - run: | - pushd package-testing/sdk-test-runner - ./test-sdk.sh server ${SDK_NAME} - popd - - - name: Upload Logs - if: success() || failure() # always run even if the previous steps fail - - uses: actions/upload-artifact@v4 - with: - name: ${{ steps.date.outputs.date }}-${{ env.SAFE_SDK_NAME }}-${{ inputs.platform }}-test-logs - path: package-testing/sdk-test-runner/logs/ - - - name: Publish Test Report - uses: mikepenz/action-junit-report@v5 - if: success() || failure() # always run even if the previous steps fail - with: - report_paths: 'package-testing/sdk-test-runner/logs/results.xml' + - name: Test information header + shell: bash + run: echo "Running Test Cluster for ${SDK_NAME}" + + - name: Set some variables + id: vars + run: | + echo "::set-output name=date::$(date +'%Y-%m-%d')" + echo "SAFE_SDK_NAME=$(echo ${SDK_NAME} | sed 's/\//_/g')" >> $GITHUB_ENV + + - name: "Checkout" + uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + # Optional setup action + - name: "Run setup action" + if: ${{ inputs.setupAction != '' }} + uses: jenseng/dynamic-uses@v1 + with: + uses: ${{ inputs.setupAction }} + with: ${{ inputs.setupWith || '{}' }} + + # Set up docker (macos runners) + - id: setup-docker + if: ${{ inputs.platform == 'macos' }} + name: Setup Docker + uses: douglascamata/setup-docker-macos-action@v1-alpha + + # Set up gCloud + - id: "auth" + uses: "google-github-actions/auth@v1" + with: + credentials_json: "${{ secrets.SERVICE_ACCOUNT_KEY }}" + + - name: "Set up Cloud SDK" + uses: "google-github-actions/setup-gcloud@v1" + + # Allow docker access to the GAR + - name: "Docker auth" + run: gcloud auth configure-docker ${{ env.REGION }}-docker.pkg.dev --quiet + + # Pull test runner and testing api images for GCP Artifact Registry (GAR) and + # retag them locally as expected by the runner script. + - name: Pull Test Runner image + run: | + docker pull ${{ env.GAR_LOCATION }}/sdk-test-runner:latest + docker tag ${{ env.GAR_LOCATION }}/sdk-test-runner:latest Eppo-exp/sdk-test-runner:latest + docker pull ${{ env.GAR_LOCATION }}/testing-api:latest + docker tag ${{ env.GAR_LOCATION }}/testing-api:latest Eppo-exp/testing-api:latest + + - name: Run tests + run: | + pushd package-testing/sdk-test-runner + ./test-sdk.sh server ${SDK_NAME} + popd + + - name: Upload Logs + if: success() || failure() # always run even if the previous steps fail + + uses: actions/upload-artifact@v4 + with: + name: ${{ steps.date.outputs.date }}-${{ env.SAFE_SDK_NAME }}-${{ inputs.platform }}-test-logs + path: package-testing/sdk-test-runner/logs/ + + - name: Publish Test Report + uses: mikepenz/action-junit-report@v5 + if: success() || failure() # always run even if the previous steps fail + with: + report_paths: "package-testing/sdk-test-runner/logs/results.xml" diff --git a/package-testing/dart-sdk-relay/.env.EXAMPLE b/package-testing/dart-sdk-relay/.env.EXAMPLE new file mode 100644 index 00000000..e2085472 --- /dev/null +++ b/package-testing/dart-sdk-relay/.env.EXAMPLE @@ -0,0 +1,4 @@ +SDK_RELAY_PORT=4000 +EPPO_BASE_URL=http://localhost:5000/api +EPPO_API_KEY=NOKEYSPECIFIED +SDK_REF=main \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/.gitignore b/package-testing/dart-sdk-relay/.gitignore new file mode 100644 index 00000000..11c66bf0 --- /dev/null +++ b/package-testing/dart-sdk-relay/.gitignore @@ -0,0 +1,4 @@ +.env +.dart_tool/ +.packages +pubspec.lock \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/bin/server.dart b/package-testing/dart-sdk-relay/bin/server.dart new file mode 100644 index 00000000..de08e479 --- /dev/null +++ b/package-testing/dart-sdk-relay/bin/server.dart @@ -0,0 +1,101 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:shelf/shelf.dart'; +import 'package:shelf/shelf_io.dart'; +import 'package:shelf_router/shelf_router.dart'; +import 'package:dotenv/dotenv.dart'; +import 'package:logging/logging.dart'; + +import 'package:dart_sdk_relay/config.dart'; +import 'package:dart_sdk_relay/assignment_handler.dart'; +import 'package:dart_sdk_relay/bandit_handler.dart'; +import 'package:dart_sdk_relay/relay_logger.dart'; + +// Configure a logger +final _logger = Logger('dart_sdk_relay'); + +void main(List args) async { + // Load environment variables + var env = DotEnv(includePlatformEnvironment: true)..load(); + + // Configure logging + Logger.root.level = Level.INFO; + Logger.root.onRecord.listen((record) { + stdout.writeln('${record.level.name}: ${record.time}: ${record.message}'); + }); + + // Initialize configuration + final config = Config(); + _logger.info('Starting server with API server: ${config.apiServer}'); + + // Initialize the Eppo client + final relayLogger = RelayLogger(); + final eppoClient = await initEppoClient(config.apiKey, config.apiServer, relayLogger); + + // Create handlers + final assignmentHandler = AssignmentHandler(eppoClient, relayLogger); + final banditHandler = BanditHandler(eppoClient, relayLogger); + + // Create a router + final app = Router(); + + // Define routes + app.get('/', (Request request) { + return Response.ok('hello, world'); + }); + + app.get('/sdk/details', (Request request) { + final details = { + 'sdkName': 'eppo-dart-sdk', + 'sdkVersion': '1.0.0', // This should be dynamically determined if possible + 'supportsBandits': true, + 'supportsDynamicTyping': true, + }; + return Response.ok(jsonEncode(details), headers: {'Content-Type': 'application/json'}); + }); + + app.post('/sdk/reset', (Request request) async { + // Reset the Eppo client (clear caches) + await resetEppoClient(); + return Response.ok('Reset complete'); + }); + + app.post('/flags/v1/assignment', (Request request) async { + final payload = await request.readAsString().then(jsonDecode) as Map; + final result = await assignmentHandler.getAssignment(payload); + return Response.ok(jsonEncode(result), headers: {'Content-Type': 'application/json'}); + }); + + app.post('/bandits/v1/action', (Request request) async { + final payload = await request.readAsString().then(jsonDecode) as Map; + final result = await banditHandler.getBanditAction(payload); + return Response.ok(jsonEncode(result), headers: {'Content-Type': 'application/json'}); + }); + + // Create a handler from the router + final handler = Pipeline() + .addMiddleware(logRequests()) + .addHandler(app); + + // Get host and port from environment + final host = env['SDK_RELAY_HOST'] ?? 'localhost'; + final port = int.parse(env['SDK_RELAY_PORT'] ?? '7001'); + + // Start the server + _logger.info('Starting server on $host:$port'); + final server = await serve(handler, '0.0.0.0', port); + _logger.info('Server listening on ${server.address.host}:${server.port}'); +} + +// These functions will need to be implemented based on the Eppo Dart SDK +Future initEppoClient(String apiKey, String apiServer, RelayLogger logger) async { + // This is a placeholder - actual implementation will depend on the Eppo Dart SDK + _logger.info('Initializing Eppo client with API server: $apiServer'); + return {}; // Return a mock client for now +} + +Future resetEppoClient() async { + // This is a placeholder - actual implementation will depend on the Eppo Dart SDK + _logger.info('Resetting Eppo client'); +} \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/build-and-run.sh b/package-testing/dart-sdk-relay/build-and-run.sh new file mode 100755 index 00000000..3ec8c8c3 --- /dev/null +++ b/package-testing/dart-sdk-relay/build-and-run.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash + +# Set default values for vars +: "${SDK_REF:=main}" + +SDK="https://github.com/Eppo-exp/eppo-multiplatform.git" + +if [ -e .env ]; then + source .env +fi + +# Install dependencies +echo "Installing dependencies..." +dart pub get + +# Create tmp directory for SDK checkout +mkdir -p tmp + +echo "Cloning ${SDK}@${SDK_REF}" +git clone -b ${SDK_REF} --depth 1 --single-branch ${SDK} tmp || ( + echo "Cloning repo failed" + exit 1 +) + +# TODO: This is where you'll add the build steps for the Dart SDK +# For example: +# cd tmp/dart +# dart pub get +# dart compile exe -o eppo_dart_sdk.dart +# cp -R build/. ../lib/eppo_dart_sdk/ + +# Clean up +rm -rf tmp + +echo "Listening on ${SDK_RELAY_HOST}:${SDK_RELAY_PORT}" + +# Run the server +dart run bin/server.dart diff --git a/package-testing/dart-sdk-relay/lib/assignment_handler.dart b/package-testing/dart-sdk-relay/lib/assignment_handler.dart new file mode 100644 index 00000000..728b7962 --- /dev/null +++ b/package-testing/dart-sdk-relay/lib/assignment_handler.dart @@ -0,0 +1,101 @@ +import 'package:logging/logging.dart'; +import 'relay_logger.dart'; + +class AssignmentHandler { + final _logger = Logger('AssignmentHandler'); + final dynamic eppoClient; // This will be the actual Eppo client type + final RelayLogger relayLogger; + + // Map of assignment types to method names + static const Map _methods = { + 'INTEGER': 'getIntegerAssignment', + 'STRING': 'getStringAssignment', + 'BOOLEAN': 'getBooleanAssignment', + 'NUMERIC': 'getNumericAssignment', + 'JSON': 'getJSONAssignment', + }; + + AssignmentHandler(this.eppoClient, this.relayLogger); + + Future> getAssignment(Map payload) async { + final variationType = payload['assignmentType'] as String; + final flagKey = payload['flag'] as String; + final defaultValue = payload['defaultValue']; + final subjectKey = payload['subjectKey'] as String; + final subjectAttributes = payload['subjectAttributes'] as Map; + + _logger.info('Processing assignment for flag: $flagKey, subject: $subjectKey, type: $variationType'); + + try { + if (!_methods.containsKey(variationType)) { + throw Exception('Invalid variation type $variationType'); + } + + // This is a placeholder - actual implementation will depend on the Eppo Dart SDK + // In a real implementation, we would call the appropriate method on the eppoClient + // based on the variationType + + // Mock result for now + final result = await _mockGetAssignment( + flagKey, + subjectKey, + subjectAttributes, + defaultValue, + variationType + ); + + return { + 'subjectKey': subjectKey, + 'result': result, + 'request': payload, + 'assignmentLog': relayLogger.assignmentLogs, + }; + } catch (e) { + _logger.severe('Error getting assignment: $e'); + return { + 'subjectKey': subjectKey, + 'result': null, + 'error': e.toString(), + 'assignmentLog': relayLogger.assignmentLogs, + }; + } finally { + relayLogger.resetLogs(); + } + } + + // This is a placeholder method - will be replaced with actual SDK calls + Future _mockGetAssignment( + String flagKey, + String subjectKey, + Map subjectAttributes, + dynamic defaultValue, + String variationType + ) async { + // In a real implementation, this would call the appropriate method on the eppoClient + _logger.info('Mock getting $variationType assignment for $flagKey'); + + // Log a mock assignment event + relayLogger.logAssignment({ + 'flagKey': flagKey, + 'subjectKey': subjectKey, + 'timestamp': DateTime.now().toIso8601String(), + 'variation': 'mock-variation', + }); + + // Return a mock result based on the variation type + switch (variationType) { + case 'STRING': + return 'mock-string-value'; + case 'INTEGER': + return 42; + case 'BOOLEAN': + return true; + case 'NUMERIC': + return 3.14; + case 'JSON': + return {'key': 'value'}; + default: + return defaultValue; + } + } +} \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/lib/bandit_handler.dart b/package-testing/dart-sdk-relay/lib/bandit_handler.dart new file mode 100644 index 00000000..fad1cba3 --- /dev/null +++ b/package-testing/dart-sdk-relay/lib/bandit_handler.dart @@ -0,0 +1,119 @@ +import 'package:logging/logging.dart'; +import 'relay_logger.dart'; + +class BanditHandler { + final _logger = Logger('BanditHandler'); + final dynamic eppoClient; // This will be the actual Eppo client type + final RelayLogger relayLogger; + + BanditHandler(this.eppoClient, this.relayLogger); + + Future> getBanditAction(Map payload) async { + _logger.info('Processing Bandit action'); + _logger.fine('Payload: $payload'); + + final flagKey = payload['flag'] as String; + final defaultValue = payload['defaultValue']; + final subjectKey = payload['subjectKey'] as String; + + // Extract subject attributes + final subjectAttributes = _createAttributeSet( + payload['subjectAttributes']['numericAttributes'] as Map?, + payload['subjectAttributes']['categoricalAttributes'] as Map? + ); + + // Extract actions + final actions = >{}; + final actionsList = payload['actions'] as List; + + for (final actionItem in actionsList) { + final action = actionItem as Map; + final actionKey = action['actionKey'] as String; + + actions[actionKey] = _createAttributeSet( + action['numericAttributes'] as Map?, + action['categoricalAttributes'] as Map? + ); + } + + _logger.info('Flag: $flagKey, Subject: $subjectKey, Actions: ${actions.keys.join(', ')}'); + + try { + // This is a placeholder - actual implementation will depend on the Eppo Dart SDK + // In a real implementation, we would call the appropriate method on the eppoClient + + // Mock result for now + final result = await _mockGetBanditAction( + flagKey, + subjectKey, + subjectAttributes, + actions, + defaultValue + ); + + return { + 'subjectKey': subjectKey, + 'result': result, + 'request': payload, + 'assignmentLog': relayLogger.assignmentLogs, + 'banditLog': relayLogger.banditLogs, + }; + } catch (e) { + _logger.severe('Error getting bandit action: $e'); + return { + 'subjectKey': subjectKey, + 'result': e.toString(), + 'assignmentLog': relayLogger.assignmentLogs, + 'banditLog': relayLogger.banditLogs, + }; + } finally { + relayLogger.resetLogs(); + } + } + + // Helper method to create an attribute set + Map _createAttributeSet( + Map? numericAttributes, + Map? categoricalAttributes + ) { + return { + 'numericAttributes': numericAttributes ?? {}, + 'categoricalAttributes': categoricalAttributes ?? {}, + }; + } + + // This is a placeholder method - will be replaced with actual SDK calls + Future> _mockGetBanditAction( + String flagKey, + String subjectKey, + Map subjectAttributes, + Map> actions, + dynamic defaultValue + ) async { + // In a real implementation, this would call the appropriate method on the eppoClient + _logger.info('Mock getting bandit action for $flagKey'); + + // Log a mock assignment event + relayLogger.logAssignment({ + 'flagKey': flagKey, + 'subjectKey': subjectKey, + 'timestamp': DateTime.now().toIso8601String(), + 'variation': 'mock-variation', + }); + + // Log a mock bandit action event + final selectedAction = actions.keys.first; + relayLogger.logBanditAction({ + 'flagKey': flagKey, + 'subjectKey': subjectKey, + 'timestamp': DateTime.now().toIso8601String(), + 'action': selectedAction, + }); + + // Return a mock result + return { + 'variation': 'mock-variation', + 'action': selectedAction, + }; + } +} \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/lib/config.dart b/package-testing/dart-sdk-relay/lib/config.dart new file mode 100644 index 00000000..c7891efd --- /dev/null +++ b/package-testing/dart-sdk-relay/lib/config.dart @@ -0,0 +1,11 @@ +import 'dart:io'; +import 'package:dotenv/dotenv.dart'; + +class Config { + final String apiKey; + final String apiServer; + + Config() : + apiKey = Platform.environment['EPPO_API_KEY'] ?? DotEnv()['EPPO_API_KEY'] ?? 'NOKEYSPECIFIED', + apiServer = Platform.environment['EPPO_BASE_URL'] ?? DotEnv()['EPPO_BASE_URL'] ?? 'http://localhost:5000/api'; +} \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/lib/relay_logger.dart b/package-testing/dart-sdk-relay/lib/relay_logger.dart new file mode 100644 index 00000000..acf16572 --- /dev/null +++ b/package-testing/dart-sdk-relay/lib/relay_logger.dart @@ -0,0 +1,30 @@ +import 'package:logging/logging.dart'; + +// This class will need to be updated based on the actual Eppo Dart SDK implementation +class RelayLogger { + final _logger = Logger('RelayLogger'); + + // Store assignment logs + final List> assignmentLogs = []; + + // Store bandit logs + final List> banditLogs = []; + + // Log an assignment event + void logAssignment(Map assignmentEvent) { + _logger.fine('Logging assignment: $assignmentEvent'); + assignmentLogs.add(assignmentEvent); + } + + // Log a bandit action event + void logBanditAction(Map banditActionEvent) { + _logger.fine('Logging bandit action: $banditActionEvent'); + banditLogs.add(banditActionEvent); + } + + // Reset all logs + void resetLogs() { + assignmentLogs.clear(); + banditLogs.clear(); + } +} \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/pubspec.yaml b/package-testing/dart-sdk-relay/pubspec.yaml new file mode 100644 index 00000000..751f07e4 --- /dev/null +++ b/package-testing/dart-sdk-relay/pubspec.yaml @@ -0,0 +1,18 @@ +name: dart_sdk_relay +description: Eppo Dart SDK Relay Server +version: 1.0.0 + +environment: + sdk: '>=2.19.0 <3.0.0' + +dependencies: + shelf: ^1.4.0 + shelf_router: ^1.1.3 + http: ^0.13.5 + dotenv: ^4.1.0 + path: ^1.8.3 + logging: ^1.1.1 + args: ^2.4.0 + +dev_dependencies: + lints: ^2.0.1 diff --git a/package-testing/dart-sdk-relay/release.sh b/package-testing/dart-sdk-relay/release.sh new file mode 100755 index 00000000..475b37b7 --- /dev/null +++ b/package-testing/dart-sdk-relay/release.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash + +docker build . -t Eppo-exp/dart-sdk-relay:latest +docker tag Eppo-exp/dart-sdk-relay:latest Eppo-exp/dart-sdk-relay:$1 From 6965e8cddf18d43ec76e0022cb09b811113e8e7c Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 26 Feb 2025 11:42:24 -0800 Subject: [PATCH 2/3] compile dart from source and use real eppo client --- package-testing/dart-sdk-relay/.gitignore | 4 +- .../dart-sdk-relay/bin/server.dart | 113 +++++++++++--- .../dart-sdk-relay/build-and-run.sh | 52 +++++-- .../lib/assignment_handler.dart | 94 ++++++------ .../dart-sdk-relay/lib/bandit_handler.dart | 138 +++++++++--------- package-testing/dart-sdk-relay/pubspec.yaml | 6 +- 6 files changed, 252 insertions(+), 155 deletions(-) diff --git a/package-testing/dart-sdk-relay/.gitignore b/package-testing/dart-sdk-relay/.gitignore index 11c66bf0..97c58527 100644 --- a/package-testing/dart-sdk-relay/.gitignore +++ b/package-testing/dart-sdk-relay/.gitignore @@ -1,4 +1,6 @@ .env .dart_tool/ .packages -pubspec.lock \ No newline at end of file +pubspec.lock +vendor/ +lib/native/ diff --git a/package-testing/dart-sdk-relay/bin/server.dart b/package-testing/dart-sdk-relay/bin/server.dart index de08e479..8b28a0a1 100644 --- a/package-testing/dart-sdk-relay/bin/server.dart +++ b/package-testing/dart-sdk-relay/bin/server.dart @@ -1,11 +1,14 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:ffi'; import 'package:shelf/shelf.dart'; import 'package:shelf/shelf_io.dart'; import 'package:shelf_router/shelf_router.dart'; import 'package:dotenv/dotenv.dart'; import 'package:logging/logging.dart'; +import 'package:eppo_sdk/eppo_sdk.dart'; +import 'package:path/path.dart' as path; import 'package:dart_sdk_relay/config.dart'; import 'package:dart_sdk_relay/assignment_handler.dart'; @@ -18,7 +21,7 @@ final _logger = Logger('dart_sdk_relay'); void main(List args) async { // Load environment variables var env = DotEnv(includePlatformEnvironment: true)..load(); - + // Configure logging Logger.root.level = Level.INFO; Logger.root.onRecord.listen((record) { @@ -28,11 +31,46 @@ void main(List args) async { // Initialize configuration final config = Config(); _logger.info('Starting server with API server: ${config.apiServer}'); - + + // Set up the native library path + try { + // Get the directory where the executable is running + final scriptDir = path.dirname(Platform.script.toFilePath()); + final workingDir = Directory.current.path; + + // Try to find the native library in various locations + final possiblePaths = [ + path.join(workingDir, 'lib', 'native', 'libeppo_client.dylib'), + path.join(scriptDir, '..', 'lib', 'native', 'libeppo_client.dylib'), + path.join(workingDir, 'native', 'libeppo_client.dylib'), + ]; + + String? libraryPath; + for (final p in possiblePaths) { + if (File(p).existsSync()) { + libraryPath = p; + _logger.info('Found native library at: $libraryPath'); + break; + } + } + + if (libraryPath != null) { + // Set the environment variable for the native library + _logger.info('Setting RUST_LIBRARY_PATH to: $libraryPath'); + Platform.environment['RUST_LIBRARY_PATH'] = libraryPath; + } else { + _logger.severe( + 'Could not find native library in any of the expected locations'); + } + } catch (e) { + _logger.severe('Error setting up native library path: $e'); + } + // Initialize the Eppo client final relayLogger = RelayLogger(); - final eppoClient = await initEppoClient(config.apiKey, config.apiServer, relayLogger); - + EppoClient eppoClient = + await initEppoClient(config.apiKey, config.apiServer, relayLogger); + // Create handlers final assignmentHandler = AssignmentHandler(eppoClient, relayLogger); final banditHandler = BanditHandler(eppoClient, relayLogger); @@ -48,11 +86,13 @@ void main(List args) async { app.get('/sdk/details', (Request request) { final details = { 'sdkName': 'eppo-dart-sdk', - 'sdkVersion': '1.0.0', // This should be dynamically determined if possible + 'sdkVersion': + '1.0.0', // This should be dynamically determined if possible 'supportsBandits': true, 'supportsDynamicTyping': true, }; - return Response.ok(jsonEncode(details), headers: {'Content-Type': 'application/json'}); + return Response.ok(jsonEncode(details), + headers: {'Content-Type': 'application/json'}); }); app.post('/sdk/reset', (Request request) async { @@ -62,21 +102,23 @@ void main(List args) async { }); app.post('/flags/v1/assignment', (Request request) async { - final payload = await request.readAsString().then(jsonDecode) as Map; + final payload = + await request.readAsString().then(jsonDecode) as Map; final result = await assignmentHandler.getAssignment(payload); - return Response.ok(jsonEncode(result), headers: {'Content-Type': 'application/json'}); + return Response.ok(jsonEncode(result), + headers: {'Content-Type': 'application/json'}); }); app.post('/bandits/v1/action', (Request request) async { - final payload = await request.readAsString().then(jsonDecode) as Map; + final payload = + await request.readAsString().then(jsonDecode) as Map; final result = await banditHandler.getBanditAction(payload); - return Response.ok(jsonEncode(result), headers: {'Content-Type': 'application/json'}); + return Response.ok(jsonEncode(result), + headers: {'Content-Type': 'application/json'}); }); // Create a handler from the router - final handler = Pipeline() - .addMiddleware(logRequests()) - .addHandler(app); + final handler = Pipeline().addMiddleware(logRequests()).addHandler(app); // Get host and port from environment final host = env['SDK_RELAY_HOST'] ?? 'localhost'; @@ -88,14 +130,47 @@ void main(List args) async { _logger.info('Server listening on ${server.address.host}:${server.port}'); } -// These functions will need to be implemented based on the Eppo Dart SDK -Future initEppoClient(String apiKey, String apiServer, RelayLogger logger) async { - // This is a placeholder - actual implementation will depend on the Eppo Dart SDK +// Update the initEppoClient function +Future initEppoClient( + String apiKey, String apiServer, RelayLogger logger) async { _logger.info('Initializing Eppo client with API server: $apiServer'); - return {}; // Return a mock client for now + + // Create a custom assignment logger that forwards to our RelayLogger + final assignmentLogger = _EppoRelayLogger(logger); + + // Create the Eppo client + final client = EppoClient( + sdkKey: apiKey, + baseUrl: Uri.parse(apiServer), + logger: assignmentLogger, + ); + + // Wait for the client to be ready + await client.whenReady(); + + return client; } +// Add this class to bridge between Eppo's logger and our RelayLogger +class _EppoRelayLogger extends AssignmentLogger { + final RelayLogger relayLogger; + + _EppoRelayLogger(this.relayLogger); + + @override + void logAssignment(Map event) { + relayLogger.logAssignment(event); + } + + @override + void logBanditAction(Map event) { + relayLogger.logBanditAction(event); + } +} + +// Update the resetEppoClient function Future resetEppoClient() async { - // This is a placeholder - actual implementation will depend on the Eppo Dart SDK + // This would need to be implemented based on the Eppo SDK + // If the SDK has a reset method, call it here _logger.info('Resetting Eppo client'); -} \ No newline at end of file +} diff --git a/package-testing/dart-sdk-relay/build-and-run.sh b/package-testing/dart-sdk-relay/build-and-run.sh index 3ec8c8c3..0af8777b 100755 --- a/package-testing/dart-sdk-relay/build-and-run.sh +++ b/package-testing/dart-sdk-relay/build-and-run.sh @@ -13,26 +13,46 @@ fi echo "Installing dependencies..." dart pub get -# Create tmp directory for SDK checkout -mkdir -p tmp +# Create vendor directory for SDK checkout +mkdir -p vendor + +# Clone or update the SDK repository +if [ -d "vendor/eppo-multiplatform" ]; then + echo "Updating ${SDK}@${SDK_REF}" + cd vendor/eppo-multiplatform + git fetch + git checkout ${SDK_REF} + git pull + cd ../.. +else + echo "Cloning ${SDK}@${SDK_REF}" + git clone -b ${SDK_REF} --depth 1 --single-branch ${SDK} vendor/eppo-multiplatform || ( + echo "Cloning repo failed" + exit 1 + ) +fi + +# Create .cargo directory and config.toml for patching crates +mkdir -p vendor/eppo-multiplatform/dart-sdk/rust/.cargo +cat > vendor/eppo-multiplatform/dart-sdk/rust/.cargo/config.toml << EOF +[patch.crates-io] +eppo_core = { path = '../../eppo_core' } +EOF + +# Create native library directory +mkdir -p lib/native -echo "Cloning ${SDK}@${SDK_REF}" -git clone -b ${SDK_REF} --depth 1 --single-branch ${SDK} tmp || ( - echo "Cloning repo failed" - exit 1 -) +# Build the native library +cd vendor/eppo-multiplatform/dart-sdk +cargo build --release +cd ../../.. -# TODO: This is where you'll add the build steps for the Dart SDK -# For example: -# cd tmp/dart -# dart pub get -# dart compile exe -o eppo_dart_sdk.dart -# cp -R build/. ../lib/eppo_dart_sdk/ +# Copy the built library to the native directory +cp vendor/eppo-multiplatform/target/release/libeppo_client.dylib lib/native/ -# Clean up -rm -rf tmp +export DYLD_LIBRARY_PATH=./lib/native:$DYLD_LIBRARY_PATH echo "Listening on ${SDK_RELAY_HOST}:${SDK_RELAY_PORT}" # Run the server -dart run bin/server.dart +dart --enable-experiment=native-assets run bin/server.dart diff --git a/package-testing/dart-sdk-relay/lib/assignment_handler.dart b/package-testing/dart-sdk-relay/lib/assignment_handler.dart index 728b7962..f871eefd 100644 --- a/package-testing/dart-sdk-relay/lib/assignment_handler.dart +++ b/package-testing/dart-sdk-relay/lib/assignment_handler.dart @@ -1,9 +1,10 @@ import 'package:logging/logging.dart'; +import 'package:eppo_sdk/eppo_sdk.dart'; import 'relay_logger.dart'; class AssignmentHandler { final _logger = Logger('AssignmentHandler'); - final dynamic eppoClient; // This will be the actual Eppo client type + final EppoClient eppoClient; final RelayLogger relayLogger; // Map of assignment types to method names @@ -22,27 +23,40 @@ class AssignmentHandler { final flagKey = payload['flag'] as String; final defaultValue = payload['defaultValue']; final subjectKey = payload['subjectKey'] as String; - final subjectAttributes = payload['subjectAttributes'] as Map; + final subjectAttributes = payload['subjectAttributes'] as Map?; _logger.info('Processing assignment for flag: $flagKey, subject: $subjectKey, type: $variationType'); try { - if (!_methods.containsKey(variationType)) { - throw Exception('Invalid variation type $variationType'); - } + // Create a Subject with attributes + final subject = Subject(subjectKey); - // This is a placeholder - actual implementation will depend on the Eppo Dart SDK - // In a real implementation, we would call the appropriate method on the eppoClient - // based on the variationType + // Add attributes if they exist + if (subjectAttributes != null) { + _addAttributes(subject, subjectAttributes); + } - // Mock result for now - final result = await _mockGetAssignment( - flagKey, - subjectKey, - subjectAttributes, - defaultValue, - variationType - ); + // Get assignment based on variation type + dynamic result; + switch (variationType) { + case 'STRING': + result = eppoClient.stringAssignment(flagKey, subject, defaultValue as String? ?? ''); + break; + case 'INTEGER': + result = eppoClient.integerAssignment(flagKey, subject, defaultValue as int? ?? 0); + break; + case 'BOOLEAN': + result = eppoClient.booleanAssignment(flagKey, subject, defaultValue as bool? ?? false); + break; + case 'NUMERIC': + result = eppoClient.numericAssignment(flagKey, subject, defaultValue as double? ?? 0.0); + break; + case 'JSON': + result = eppoClient.jsonAssignment(flagKey, subject, defaultValue as Map? ?? {}); + break; + default: + throw Exception('Invalid variation type $variationType'); + } return { 'subjectKey': subjectKey, @@ -54,7 +68,7 @@ class AssignmentHandler { _logger.severe('Error getting assignment: $e'); return { 'subjectKey': subjectKey, - 'result': null, + 'result': defaultValue, 'error': e.toString(), 'assignmentLog': relayLogger.assignmentLogs, }; @@ -63,39 +77,19 @@ class AssignmentHandler { } } - // This is a placeholder method - will be replaced with actual SDK calls - Future _mockGetAssignment( - String flagKey, - String subjectKey, - Map subjectAttributes, - dynamic defaultValue, - String variationType - ) async { - // In a real implementation, this would call the appropriate method on the eppoClient - _logger.info('Mock getting $variationType assignment for $flagKey'); - - // Log a mock assignment event - relayLogger.logAssignment({ - 'flagKey': flagKey, - 'subjectKey': subjectKey, - 'timestamp': DateTime.now().toIso8601String(), - 'variation': 'mock-variation', - }); - - // Return a mock result based on the variation type - switch (variationType) { - case 'STRING': - return 'mock-string-value'; - case 'INTEGER': - return 42; - case 'BOOLEAN': - return true; - case 'NUMERIC': - return 3.14; - case 'JSON': - return {'key': 'value'}; - default: - return defaultValue; + // Helper method to add attributes to a subject + void _addAttributes(Subject subject, Map attributes) { + for (final entry in attributes.entries) { + final key = entry.key; + final value = entry.value; + + if (value is String) { + subject.stringAttribute(key, value); + } else if (value is int || value is double) { + subject.numberAttribute(key, value); + } else if (value is bool) { + subject.boolAttribute(key, value); + } } } } \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/lib/bandit_handler.dart b/package-testing/dart-sdk-relay/lib/bandit_handler.dart index fad1cba3..2c3db5d9 100644 --- a/package-testing/dart-sdk-relay/lib/bandit_handler.dart +++ b/package-testing/dart-sdk-relay/lib/bandit_handler.dart @@ -1,9 +1,10 @@ import 'package:logging/logging.dart'; +import 'package:eppo_sdk/eppo_sdk.dart'; import 'relay_logger.dart'; class BanditHandler { final _logger = Logger('BanditHandler'); - final dynamic eppoClient; // This will be the actual Eppo client type + final EppoClient eppoClient; final RelayLogger relayLogger; BanditHandler(this.eppoClient, this.relayLogger); @@ -13,47 +14,71 @@ class BanditHandler { _logger.fine('Payload: $payload'); final flagKey = payload['flag'] as String; - final defaultValue = payload['defaultValue']; + final defaultValue = payload['defaultValue'] as String?; final subjectKey = payload['subjectKey'] as String; - // Extract subject attributes - final subjectAttributes = _createAttributeSet( - payload['subjectAttributes']['numericAttributes'] as Map?, - payload['subjectAttributes']['categoricalAttributes'] as Map? - ); + // Create subject with attributes + final subject = Subject(subjectKey); - // Extract actions - final actions = >{}; + // Add subject attributes if they exist + if (payload['subjectAttributes'] != null) { + final numericAttrs = payload['subjectAttributes']['numericAttributes'] as Map?; + final categoricalAttrs = payload['subjectAttributes']['categoricalAttributes'] as Map?; + + _addNumericAttributes(subject, numericAttrs); + _addCategoricalAttributes(subject, categoricalAttrs); + } + + // Process actions final actionsList = payload['actions'] as List; + final actionAttributes = {}; for (final actionItem in actionsList) { final action = actionItem as Map; final actionKey = action['actionKey'] as String; - actions[actionKey] = _createAttributeSet( - action['numericAttributes'] as Map?, - action['categoricalAttributes'] as Map? - ); + final attrs = Attributes(); + + // Add numeric attributes + final numericAttrs = action['numericAttributes'] as Map?; + if (numericAttrs != null) { + for (final entry in numericAttrs.entries) { + attrs.numberAttribute(entry.key, entry.value); + } + } + + // Add categorical attributes + final categoricalAttrs = action['categoricalAttributes'] as Map?; + if (categoricalAttrs != null) { + for (final entry in categoricalAttrs.entries) { + if (entry.value is bool) { + attrs.boolAttribute(entry.key, entry.value); + } else { + attrs.stringAttribute(entry.key, entry.value.toString()); + } + } + } + + actionAttributes[actionKey] = attrs; } - _logger.info('Flag: $flagKey, Subject: $subjectKey, Actions: ${actions.keys.join(', ')}'); + _logger.info('Flag: $flagKey, Subject: $subjectKey, Actions: ${actionAttributes.keys.join(', ')}'); try { - // This is a placeholder - actual implementation will depend on the Eppo Dart SDK - // In a real implementation, we would call the appropriate method on the eppoClient - - // Mock result for now - final result = await _mockGetBanditAction( - flagKey, - subjectKey, - subjectAttributes, - actions, - defaultValue + // Call the real bandit action method + final banditResult = eppoClient.banditAction( + flagKey, + subject, + actionAttributes, + defaultValue ?? '', ); return { 'subjectKey': subjectKey, - 'result': result, + 'result': { + 'variation': banditResult.variation, + 'action': banditResult.action, + }, 'request': payload, 'assignmentLog': relayLogger.assignmentLogs, 'banditLog': relayLogger.banditLogs, @@ -62,7 +87,11 @@ class BanditHandler { _logger.severe('Error getting bandit action: $e'); return { 'subjectKey': subjectKey, - 'result': e.toString(), + 'result': { + 'variation': null, + 'action': defaultValue, + }, + 'error': e.toString(), 'assignmentLog': relayLogger.assignmentLogs, 'banditLog': relayLogger.banditLogs, }; @@ -71,49 +100,24 @@ class BanditHandler { } } - // Helper method to create an attribute set - Map _createAttributeSet( - Map? numericAttributes, - Map? categoricalAttributes - ) { - return { - 'numericAttributes': numericAttributes ?? {}, - 'categoricalAttributes': categoricalAttributes ?? {}, - }; + // Helper methods to add attributes to a subject + void _addNumericAttributes(Subject subject, Map? attributes) { + if (attributes == null) return; + + for (final entry in attributes.entries) { + subject.numberAttribute(entry.key, entry.value); + } } - // This is a placeholder method - will be replaced with actual SDK calls - Future> _mockGetBanditAction( - String flagKey, - String subjectKey, - Map subjectAttributes, - Map> actions, - dynamic defaultValue - ) async { - // In a real implementation, this would call the appropriate method on the eppoClient - _logger.info('Mock getting bandit action for $flagKey'); + void _addCategoricalAttributes(Subject subject, Map? attributes) { + if (attributes == null) return; - // Log a mock assignment event - relayLogger.logAssignment({ - 'flagKey': flagKey, - 'subjectKey': subjectKey, - 'timestamp': DateTime.now().toIso8601String(), - 'variation': 'mock-variation', - }); - - // Log a mock bandit action event - final selectedAction = actions.keys.first; - relayLogger.logBanditAction({ - 'flagKey': flagKey, - 'subjectKey': subjectKey, - 'timestamp': DateTime.now().toIso8601String(), - 'action': selectedAction, - }); - - // Return a mock result - return { - 'variation': 'mock-variation', - 'action': selectedAction, - }; + for (final entry in attributes.entries) { + if (entry.value is bool) { + subject.boolAttribute(entry.key, entry.value); + } else { + subject.stringAttribute(entry.key, entry.value.toString()); + } + } } } \ No newline at end of file diff --git a/package-testing/dart-sdk-relay/pubspec.yaml b/package-testing/dart-sdk-relay/pubspec.yaml index 751f07e4..628f2e87 100644 --- a/package-testing/dart-sdk-relay/pubspec.yaml +++ b/package-testing/dart-sdk-relay/pubspec.yaml @@ -8,11 +8,13 @@ environment: dependencies: shelf: ^1.4.0 shelf_router: ^1.1.3 - http: ^0.13.5 + http: ^1.3.0 dotenv: ^4.1.0 path: ^1.8.3 logging: ^1.1.1 args: ^2.4.0 + eppo_sdk: + path: ./vendor/eppo-multiplatform/dart-sdk dev_dependencies: - lints: ^2.0.1 + lints: ^5.1.1 From a7f84e4c9985abd0ed911e8a8f94b7f5093eed36 Mon Sep 17 00:00:00 2001 From: Leo Romanovsky Date: Wed, 26 Feb 2025 11:45:51 -0800 Subject: [PATCH 3/3] build first before getting deps --- package-testing/dart-sdk-relay/build-and-run.sh | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package-testing/dart-sdk-relay/build-and-run.sh b/package-testing/dart-sdk-relay/build-and-run.sh index 0af8777b..90f15afa 100755 --- a/package-testing/dart-sdk-relay/build-and-run.sh +++ b/package-testing/dart-sdk-relay/build-and-run.sh @@ -9,10 +9,6 @@ if [ -e .env ]; then source .env fi -# Install dependencies -echo "Installing dependencies..." -dart pub get - # Create vendor directory for SDK checkout mkdir -p vendor @@ -52,6 +48,10 @@ cp vendor/eppo-multiplatform/target/release/libeppo_client.dylib lib/native/ export DYLD_LIBRARY_PATH=./lib/native:$DYLD_LIBRARY_PATH +# Install dependencies +echo "Installing dependencies..." +dart pub get + echo "Listening on ${SDK_RELAY_HOST}:${SDK_RELAY_PORT}" # Run the server