├── .github ├── ISSUE_TEMPLATE │ ├── bug_report--ldai.md │ ├── bug_report--ldotel.md │ ├── bug_report.md │ ├── config.yml │ ├── feature_request--ldai.md │ ├── feature_request--ldotel.md │ └── feature_request.md ├── actions │ ├── benchmarks │ │ └── action.yml │ ├── coverage │ │ └── action.yml │ ├── get-go-version │ │ └── action.yml │ └── unit-tests │ │ └── action.yml ├── pull_request_template.md ├── variables │ └── go-versions.env └── workflows │ ├── check-go-versions.yml │ ├── ci.yml │ ├── common_ci.yml │ ├── go-versions.yml │ ├── ldai-ci.yml │ ├── ldotel-ci.yml │ ├── release-please.yml │ └── stale.yml ├── .gitignore ├── .golangci.yml ├── .release-please-manifest.json ├── .sdk_metadata.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODEOWNERS ├── CONTRIBUTING.md ├── LICENSE.txt ├── Makefile ├── README.md ├── SECURITY.md ├── client_context_from_config.go ├── config.go ├── config_test.go ├── event_processor_benchmark_test.go ├── go.mod ├── go.sum ├── interfaces ├── application_info.go ├── big_segment_store_status_provider.go ├── client_interface.go ├── data_source_status_provider.go ├── data_source_status_provider_types_test.go ├── data_store_status_provider.go ├── flag_tracker.go ├── flagstate │ ├── flags_state.go │ ├── flags_state_test.go │ └── package_info.go ├── package_info.go └── service_endpoints.go ├── internal ├── bigsegments │ ├── big_segment_store_status_provider_impl.go │ ├── big_segment_store_status_provider_impl_test.go │ ├── package_info.go │ └── user_hash.go ├── broadcasters.go ├── broadcasters_test.go ├── client_context_impl.go ├── concurrent.go ├── concurrent_test.go ├── datakinds │ ├── data_kind_internal.go │ ├── data_kinds_impl.go │ ├── data_kinds_impl_test.go │ └── package_info.go ├── datasource │ ├── data_model_dependencies.go │ ├── data_model_dependencies_test.go │ ├── data_source_status_provider_impl.go │ ├── data_source_status_provider_impl_test.go │ ├── data_source_test_helpers_test.go │ ├── data_source_update_sink_impl.go │ ├── data_source_update_sink_impl_test.go │ ├── helpers.go │ ├── helpers_test.go │ ├── null_data_source.go │ ├── null_data_source_test.go │ ├── package_info.go │ ├── polling_data_source.go │ ├── polling_data_source_test.go │ ├── polling_http_request.go │ ├── polling_http_request_test.go │ ├── streaming_data_source.go │ ├── streaming_data_source_events.go │ ├── streaming_data_source_events_test.go │ └── streaming_data_source_test.go ├── datasourcev2 │ ├── fdv1_polling_data_source.go │ ├── helpers.go │ ├── package_info.go │ ├── polling_data_source.go │ ├── polling_data_source_test.go │ ├── polling_http_request.go │ ├── streaming_data_source.go │ └── streaming_data_source_test.go ├── datastore │ ├── data_store_eval_impl.go │ ├── data_store_eval_impl_test.go │ ├── data_store_status_poller.go │ ├── data_store_status_provider_impl.go │ ├── data_store_status_provider_impl_test.go │ ├── data_store_test_helpers_test.go │ ├── data_store_update_sink_impl.go │ ├── data_store_update_sink_impl_test.go │ ├── in_memory_data_store_impl.go │ ├── in_memory_data_store_impl_benchmark_test.go │ ├── in_memory_data_store_impl_test.go │ ├── package_info.go │ ├── persistent_data_store_wrapper.go │ ├── persistent_data_store_wrapper_status_test.go │ └── persistent_data_store_wrapper_test.go ├── datasystem │ ├── data_availability.go │ ├── data_availability_test.go │ ├── data_model_dependencies.go │ ├── data_model_dependencies_test.go │ ├── fdv1_datasystem.go │ ├── fdv2_datasystem.go │ ├── package.go │ ├── store.go │ └── store_test.go ├── endpoints │ ├── configure_endpoints.go │ ├── configure_endpoints_test.go │ ├── package_info.go │ └── standard_endpoints.go ├── flag_tracker_impl.go ├── flag_tracker_impl_test.go ├── hooks │ ├── evaluation_execution.go │ ├── evaluation_execution_test.go │ ├── iterator.go │ ├── iterator_test.go │ ├── package_info.go │ ├── runner.go │ └── runner_test.go ├── log_messages.go ├── memorystorev2 │ ├── memory_store.go │ ├── memory_store_benchmark_test.go │ └── memory_store_test.go ├── package_info.go ├── sharedtest │ ├── data_helpers.go │ ├── events.go │ ├── hooks.go │ ├── mocks │ │ ├── assert.go │ │ ├── mock_big_segment_store.go │ │ ├── mock_components.go │ │ ├── mock_data.go │ │ ├── mock_data_destination.go │ │ ├── mock_data_selector.go │ │ ├── mock_data_source_updates.go │ │ ├── mock_data_store.go │ │ ├── mock_persistent_data_store.go │ │ ├── mock_polling_request.go │ │ ├── mock_status_reporter.go │ │ ├── package_info.go │ │ ├── spy_data_store.go │ │ └── spy_event_processor.go │ ├── package_info.go │ ├── test_context.go │ └── test_log_level.go ├── toposort │ └── toposort.go └── version.go ├── ldai ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── client.go ├── client_test.go ├── config.go ├── datamodel │ └── datamodel.go ├── go.mod ├── go.sum ├── package_info.go ├── tracker.go └── tracker_test.go ├── ldclient.go ├── ldclient_big_segments_test.go ├── ldclient_end_to_end_fdv2_test.go ├── ldclient_end_to_end_test.go ├── ldclient_evaluation_all_flags_test.go ├── ldclient_evaluation_benchmark_test.go ├── ldclient_evaluation_test.go ├── ldclient_events.go ├── ldclient_events_test.go ├── ldclient_external_updates_only_test.go ├── ldclient_hooks_test.go ├── ldclient_listeners_fdv2_test.go ├── ldclient_listeners_test.go ├── ldclient_migration_test.go ├── ldclient_offline_test.go ├── ldclient_service_endpoints_test.go ├── ldclient_test.go ├── ldclient_test_data_source_test.go ├── ldcomponents ├── big_segments_configuration_builder.go ├── big_segments_configuration_builder_test.go ├── data_system_configuration_builder.go ├── external_updates_data_source.go ├── external_updates_data_source_test.go ├── fdv1_polling_data_source_builder.go ├── http_configuration_builder.go ├── http_configuration_builder_test.go ├── in_memory_data_store.go ├── in_memory_data_store_test.go ├── logging_configuration_builder.go ├── logging_configuration_builder_test.go ├── no_events.go ├── no_events_test.go ├── package_info.go ├── persistent_data_store_builder.go ├── persistent_data_store_builder_test.go ├── polling_data_source_builder.go ├── polling_data_source_builder_test.go ├── polling_data_source_builder_v2.go ├── polling_data_source_builder_v2_test.go ├── send_events.go ├── send_events_test.go ├── service_endpoints.go ├── service_endpoints_test.go ├── streaming_data_source_builder.go ├── streaming_data_source_builder_test.go ├── streaming_data_source_builder_v2.go ├── streaming_data_source_builder_v2_test.go └── test_helpers_test.go ├── ldfiledata ├── file_data_source_builder.go ├── file_data_source_impl.go ├── file_data_source_test.go └── package_info.go ├── ldfiledatav2 ├── file_data_source_builder.go ├── file_data_source_impl.go ├── file_data_source_test.go └── package_info.go ├── ldfilewatch ├── package_info.go ├── watched_file_data_source.go └── watched_file_data_source_test.go ├── ldhooks ├── evaluation_series_context.go ├── evaluation_series_data.go ├── evaluation_series_data_test.go ├── hooks.go ├── metadata.go └── package_info.go ├── ldhttp ├── http_transport.go └── http_transport_test.go ├── ldntlm ├── ntlm_proxy.go └── ntlm_proxy_test.go ├── ldotel ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── go.mod ├── go.sum ├── package_info.go ├── tracing_hook.go └── tracing_hook_test.go ├── migration_op_tracker.go ├── migration_op_tracker_test.go ├── migrator.go ├── migrator_base.go ├── migrator_builder.go ├── migrator_test.go ├── package_info.go ├── proxytest ├── http_transport_proxy_test.go └── ldclient_proxy_test.go ├── release-please-config.json ├── server_side_diagnostics.go ├── server_side_diagnostics_test.go ├── subsystems ├── big_segments.go ├── changeset.go ├── changeset_test.go ├── client_context.go ├── component_configurer.go ├── data_destination.go ├── data_source.go ├── data_source_status_reporter.go ├── data_source_update_sink.go ├── data_store.go ├── data_store_mode.go ├── data_store_update_sink.go ├── datasystem_configuration.go ├── deltas_to_storable_items.go ├── diagnostic_description.go ├── events.go ├── http_configuration.go ├── ldstoreimpl │ ├── big_segment_store_wrapper.go │ ├── big_segment_store_wrapper_test.go │ ├── big_segments_config_extra.go │ ├── big_segments_membership.go │ ├── big_segments_membership_test.go │ ├── data_kinds.go │ ├── data_kinds_test.go │ ├── data_store_eval.go │ ├── data_store_eval_test.go │ └── package_info.go ├── ldstoretypes │ ├── data_store_types.go │ └── package_info.go ├── logging_configuration.go ├── package_info.go ├── payloads.go ├── persistent_data_store.go ├── raw_event.go ├── read_only_store.go └── selector.go ├── testhelpers ├── client_context.go ├── ldservices │ ├── events_service.go │ ├── events_service_test.go │ ├── package_info.go │ ├── polling_service.go │ ├── polling_service_test.go │ ├── server_sdk_data.go │ ├── server_sdk_data_test.go │ ├── streaming_service.go │ └── streaming_service_test.go ├── ldservicesv2 │ ├── package.go │ ├── server_sdk_data.go │ └── streaming_protocol_builder.go ├── ldtestdata │ ├── package_info.go │ ├── test_data_source.go │ ├── test_data_source_flag.go │ ├── test_data_source_flag_test.go │ └── test_data_source_test.go ├── ldtestdatav2 │ ├── package_info.go │ ├── test_data_source.go │ ├── test_data_source_flag.go │ └── test_data_source_test.go ├── package_info.go └── storetest │ ├── big_segment_store_test_suite.go │ ├── big_segment_store_test_suite_test.go │ ├── package_info.go │ ├── persistent_data_store_test_suite.go │ └── persistent_data_store_test_suite_test.go └── testservice ├── .gitignore ├── README.md ├── big_segment_store_fixture.go ├── callback_service.go ├── go.mod ├── go.sum ├── sdk_client_entity.go ├── service.go ├── servicedef ├── callbackfixtures │ ├── big_segment_store.go │ └── persistent_data_store.go ├── command_params.go ├── sdk_config.go └── service_params.go └── test_hook.go /.github/ISSUE_TEMPLATE/bug_report--ldai.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report for the ldai module 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'ldai, enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a support request?** 11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com. 12 | 13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Logs** 25 | If applicable, add any log output related to your problem. 26 | 27 | **SDK version** 28 | The version of this SDK that you are using. 29 | 30 | **Language version, developer tools** 31 | For instance, Go 1.22. 32 | 33 | **OS/platform** 34 | For instance, Ubuntu 16.04, or Windows 10. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report--ldotel.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report for the ldotel module 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'ldotel, enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a support request?** 11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com. 12 | 13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Logs** 25 | If applicable, add any log output related to your problem. 26 | 27 | **SDK version** 28 | The version of this SDK that you are using. 29 | 30 | **Language version, developer tools** 31 | For instance, Go 1.22. 32 | 33 | **OS/platform** 34 | For instance, Ubuntu 16.04, or Windows 10. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: 'server-sdk, bug' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is this a support request?** 11 | This issue tracker is maintained by LaunchDarkly SDK developers and is intended for feedback on the SDK code. If you're not sure whether the problem you are having is specifically related to the SDK, or to the LaunchDarkly service overall, it may be more appropriate to contact the LaunchDarkly support team; they can help to investigate the problem and will consult the SDK team if necessary. You can submit a support request by going [here](https://support.launchdarkly.com/hc/en-us/requests/new) or by emailing support@launchdarkly.com. 12 | 13 | Note that issues filed on this issue tracker are publicly accessible. Do not provide any private account information on your issues. If your problem is specific to your account, you should submit a support request as described above. 14 | 15 | **Describe the bug** 16 | A clear and concise description of what the bug is. 17 | 18 | **To reproduce** 19 | Steps to reproduce the behavior. 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Logs** 25 | If applicable, add any log output related to your problem. 26 | 27 | **SDK version** 28 | The version of this SDK that you are using. 29 | 30 | **Language version, developer tools** 31 | For instance, Go 1.11 or Ruby 2.5.3. If you are using a language that requires a separate compiler, such as C, please include the name and version of the compiler too. 32 | 33 | **OS/platform** 34 | For instance, Ubuntu 16.04, Windows 10, or Android 4.0.3. If your code is running in a browser, please also include the browser type and version. 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: Support request 4 | url: https://support.launchdarkly.com/hc/en-us/requests/new 5 | about: File your support requests with LaunchDarkly's support team 6 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request--ldai.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request for the ldai module 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'ldai, enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request--ldotel.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request for the ldotel module 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'ldotel, enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: 'server-sdk, enhancement' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I would love to see the SDK [...does something new...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/actions/benchmarks/action.yml: -------------------------------------------------------------------------------- 1 | name: Benchmarks 2 | description: "Runs the SDK's performance benchmarks." 3 | 4 | runs: 5 | using: composite 6 | steps: 7 | - uses: ./.github/actions/get-go-version 8 | id: go-version 9 | 10 | - name: Run Benchmarks 11 | id: benchmarks 12 | shell: bash 13 | run: make benchmarks | tee benchmarks.txt 14 | 15 | - name: Upload Results 16 | if: steps.benchmarks.outcome == 'success' 17 | uses: actions/upload-artifact@v4 18 | with: 19 | name: Benchmarks-${{ steps.go-version.outputs.version }} 20 | path: benchmarks.txt 21 | -------------------------------------------------------------------------------- /.github/actions/coverage/action.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | description: "Runs Go Server SDK's code-coverage checker." 3 | inputs: 4 | enforce: 5 | description: 'Whether to enforce coverage thresholds.' 6 | required: false 7 | default: 'false' 8 | 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - uses: ./.github/actions/get-go-version 14 | id: go-version 15 | 16 | - name: Test with coverage 17 | shell: bash 18 | id: test-coverage 19 | run: | 20 | set +e 21 | make test-coverage 22 | status=$? 23 | echo "coverage_status=$status" >> $GITHUB_OUTPUT 24 | 25 | - name: Upload coverage results 26 | uses: actions/upload-artifact@v4 27 | with: 28 | name: Coverage-result-${{ steps.go-version.outputs.version }} 29 | path: build/coverage* 30 | 31 | - name: Enforce coverage 32 | shell: bash 33 | run: | 34 | if [ "${{ steps.test-coverage.outputs.coverage_status }}" != "0" ]; then 35 | echo "Code isn't fully covered!" 36 | if [ "${{ inputs.enforce }}" == "true" ]; then 37 | exit 1 38 | fi 39 | else 40 | echo "Code is fully covered!" 41 | fi 42 | -------------------------------------------------------------------------------- /.github/actions/get-go-version/action.yml: -------------------------------------------------------------------------------- 1 | name: Get Go Version 2 | description: "Gets the currently installed Go version." 3 | outputs: 4 | version: 5 | description: 'The currently installed Go version.' 6 | value: ${{ steps.go-version.outputs.value }} 7 | 8 | runs: 9 | using: composite 10 | steps: 11 | - name: Get Go version 12 | id: go-version 13 | shell: bash 14 | run: | 15 | echo "value=$(go version | awk '{print $3}')" >> $GITHUB_OUTPUT 16 | -------------------------------------------------------------------------------- /.github/actions/unit-tests/action.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | description: "Runs Go Server SDK's unit tests + linters and optionally gathers coverage." 3 | inputs: 4 | lint: 5 | description: 'Whether to run linters.' 6 | required: false 7 | default: 'false' 8 | test-target: 9 | description: 'The test target to run.' 10 | required: true 11 | 12 | runs: 13 | using: composite 14 | steps: 15 | - uses: ./.github/actions/get-go-version 16 | id: go-version 17 | - name: Lint 18 | if: inputs.lint == 'true' 19 | shell: bash 20 | run: make lint 21 | 22 | - name: Test 23 | shell: bash 24 | id: test 25 | run: make ${{ inputs.test-target }} | tee raw_report.txt 26 | 27 | - name: Process test results 28 | if: steps.test.outcome == 'success' 29 | id: process-test 30 | shell: bash 31 | run: go run github.com/jstemmer/go-junit-report@v0.9.1 < raw_report.txt > junit_report.xml 32 | 33 | - name: Upload test results 34 | if: steps.process-test.outcome == 'success' 35 | uses: actions/upload-artifact@v4 36 | with: 37 | name: Test-result-${{ inputs.test-target }}${{ steps.go-version.outputs.version }} 38 | path: junit_report.xml 39 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Requirements** 2 | 3 | - [ ] I have added test coverage for new or changed functionality 4 | - [ ] I have followed the repository's [pull request submission guidelines](../blob/v5/CONTRIBUTING.md#submitting-pull-requests) 5 | - [ ] I have validated my changes against all supported platform versions 6 | 7 | **Related issues** 8 | 9 | Provide links to any issues in this repository or elsewhere relating to this pull request. 10 | 11 | **Describe the solution you've provided** 12 | 13 | Provide a clear and concise description of what you expect to happen. 14 | 15 | **Describe alternatives you've considered** 16 | 17 | Provide a clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | 21 | Add any other context about the pull request here. 22 | -------------------------------------------------------------------------------- /.github/variables/go-versions.env: -------------------------------------------------------------------------------- 1 | latest=1.24 2 | penultimate=1.23 3 | min=1.23 4 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test SDK 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: [ 'v7', 'feat/**' ] 7 | paths-ignore: 8 | - '**.md' # Don't run CI on markdown changes. 9 | pull_request: 10 | branches: [ 'v7', 'feat/**' ] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | go-versions: 16 | uses: ./.github/workflows/go-versions.yml 17 | 18 | # Runs the common tasks (unit tests, lint, contract tests) for each Go version. 19 | test-linux: 20 | name: ${{ format('Linux, Go {0}', matrix.go-version) }} 21 | needs: go-versions 22 | strategy: 23 | # Let jobs fail independently, in case it's a single version that's broken. 24 | fail-fast: false 25 | matrix: 26 | go-version: ${{ fromJSON(needs.go-versions.outputs.matrix) }} 27 | uses: ./.github/workflows/common_ci.yml 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | 31 | test-windows: 32 | name: ${{ format('Windows, Go {0}', matrix.go-version) }} 33 | runs-on: windows-2022 34 | needs: go-versions 35 | strategy: 36 | fail-fast: false 37 | matrix: 38 | go-version: ${{ fromJSON(needs.go-versions.outputs.matrix) }} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - name: Setup Go ${{ matrix.go-version }} 42 | uses: actions/setup-go@v5 43 | with: 44 | go-version: ${{ matrix.go-version }} 45 | - name: Test 46 | run: go test -race ./... 47 | -------------------------------------------------------------------------------- /.github/workflows/common_ci.yml: -------------------------------------------------------------------------------- 1 | name: Common CI 2 | on: 3 | workflow_call: 4 | inputs: 5 | go-version: 6 | description: "Go version to use for the jobs." 7 | required: true 8 | type: string 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | unit-test-and-coverage: 14 | runs-on: ubuntu-latest 15 | name: 'Unit Tests and Coverage' 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Setup Go ${{ inputs.go-version }} 19 | uses: actions/setup-go@v5 20 | with: 21 | go-version: ${{ inputs.go-version }} 22 | - uses: ./.github/actions/unit-tests 23 | with: 24 | lint: 'true' 25 | test-target: sdk-test 26 | - uses: ./.github/actions/coverage 27 | with: 28 | enforce: 'false' 29 | 30 | contract-tests: 31 | runs-on: ubuntu-latest 32 | name: 'Contract Tests' 33 | env: 34 | TEST_SERVICE_PORT: 10000 35 | 36 | services: 37 | redis: 38 | image: redis 39 | ports: 40 | - 6379:6379 41 | dynamodb: 42 | image: amazon/dynamodb-local 43 | ports: 44 | - 8000:8000 45 | consul: 46 | image: hashicorp/consul 47 | ports: 48 | - 8500:8500 49 | steps: 50 | - uses: actions/checkout@v4 51 | - name: Setup Go ${{ inputs.go-version }} 52 | uses: actions/setup-go@v5 53 | with: 54 | go-version: ${{ inputs.go-version }} 55 | - uses: ./.github/actions/get-go-version 56 | id: go-version 57 | - name: Make workspace 58 | run: make workspace 59 | - name: Build test service 60 | run: make build-contract-tests 61 | - name: Start test service in background 62 | run: make start-contract-test-service-bg 63 | - uses: launchdarkly/gh-actions/actions/contract-tests@contract-tests-v1.2.0 64 | with: 65 | enable_persistence_tests: 'true' 66 | test_service_port: ${{ env.TEST_SERVICE_PORT }} 67 | token: ${{ secrets.GITHUB_TOKEN }} 68 | - name: Upload test service logs 69 | uses: actions/upload-artifact@v4 70 | with: 71 | name: Contract-test-service-logs-${{ steps.go-version.outputs.version }} 72 | path: /tmp/sdk-contract-test-service.log 73 | 74 | benchmarks: 75 | name: 'Benchmarks' 76 | runs-on: ubuntu-latest 77 | steps: 78 | - uses: actions/checkout@v4 79 | - name: Setup Go ${{ inputs.go-version }} 80 | uses: actions/setup-go@v5 81 | with: 82 | go-version: ${{ inputs.go-version }} 83 | - uses: ./.github/actions/benchmarks 84 | -------------------------------------------------------------------------------- /.github/workflows/go-versions.yml: -------------------------------------------------------------------------------- 1 | # The following chunk of yml boils down to pulling two Go version numbers out of a file and 2 | # making them available to the other workflows in a convenient fashion. 3 | # 4 | # It's a reusable workflow instead of an action so that its output can be used in a matrix strategy 5 | # of another job. 6 | # 7 | # The idea is to define the most recent, and penultimate, Go versions that should be used to test Relay. 8 | # Ideally we'd define these in a single place - otherwise we'd need to update many different places in 9 | # each workflow. This single place is .github/variables/go-versions.env. 10 | # 11 | # This reusable workflow grabs them out of the file, then sets them as outputs. As a convenience, it 12 | # also wraps each version in an array, so it can be directly used in a matrix strategy. Single-item matrices 13 | # are nice because you can tell instantly in the Github UI which version is being tested without needing 14 | # to inspect logs. 15 | # 16 | # To use a matrix output, e.g. latest version, do: 17 | # strategy: 18 | # matrix: ${{ fromJSON(this-job.outputs.latest-matrix) }} 19 | # 20 | name: Go Versions 21 | permissions: 22 | contents: read 23 | on: 24 | workflow_call: 25 | outputs: 26 | latest: 27 | description: 'The most recent Go version to test' 28 | value: ${{ jobs.go-versions.outputs.latest }} 29 | penultimate: 30 | description: 'The second most recent Go version to test' 31 | value: ${{ jobs.go-versions.outputs.penultimate }} 32 | min: 33 | description: 'The minimum Go version to test' 34 | value: ${{ jobs.go-versions.outputs.min }} 35 | matrix: 36 | description: 'All Go versions to test as a matrix' 37 | value: ${{ jobs.go-versions.outputs.all }} 38 | 39 | jobs: 40 | go-versions: 41 | runs-on: ubuntu-latest 42 | outputs: 43 | latest: ${{ steps.set-env.outputs.latest }} 44 | penultimate: ${{ steps.set-env.outputs.penultimate }} 45 | all: ${{ steps.set-matrix.outputs.all }} 46 | steps: 47 | - uses: actions/checkout@v4 48 | - name: Set Go Versions 49 | id: set-env 50 | run: cat ./.github/variables/go-versions.env > $GITHUB_OUTPUT 51 | - name: Set Go Version Matrices 52 | id: set-matrix 53 | run: | 54 | if [ "${{ steps.set-env.outputs.penultimate }}" == "${{ steps.set-env.outputs.min }}" ]; then 55 | echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\"]" >> $GITHUB_OUTPUT 56 | else 57 | echo "all=[\"${{ steps.set-env.outputs.latest }}\",\"${{ steps.set-env.outputs.penultimate }}\",\"${{ steps.set-env.outputs.min }}\"]" >> $GITHUB_OUTPUT 58 | fi 59 | -------------------------------------------------------------------------------- /.github/workflows/ldai-ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test ldai 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: [ 'v7', 'feat/**' ] 7 | paths-ignore: 8 | - '**.md' # Don't run CI on markdown changes. 9 | pull_request: 10 | branches: [ 'v7', 'feat/**' ] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | go-versions: 16 | uses: ./.github/workflows/go-versions.yml 17 | 18 | # Runs the common tasks (unit tests, lint, contract tests) for each Go version. 19 | test-linux: 20 | name: ${{ format('ldai Linux, Go {0}', matrix.go-version) }} 21 | needs: go-versions 22 | strategy: 23 | # Let jobs fail independently, in case it's a single version that's broken. 24 | fail-fast: false 25 | matrix: 26 | go-version: ${{ fromJSON(needs.go-versions.outputs.matrix) }} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Setup Go ${{ inputs.go-version }} 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | - uses: ./.github/actions/unit-tests 35 | with: 36 | lint: 'true' 37 | test-target: ldai-test 38 | - uses: ./.github/actions/coverage 39 | with: 40 | enforce: 'false' 41 | -------------------------------------------------------------------------------- /.github/workflows/ldotel-ci.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test ldotel 2 | permissions: 3 | contents: read 4 | on: 5 | push: 6 | branches: [ 'v7', 'feat/**' ] 7 | paths-ignore: 8 | - '**.md' # Don't run CI on markdown changes. 9 | pull_request: 10 | branches: [ 'v7', 'feat/**' ] 11 | paths-ignore: 12 | - '**.md' 13 | 14 | jobs: 15 | go-versions: 16 | uses: ./.github/workflows/go-versions.yml 17 | 18 | # Runs the common tasks (unit tests, lint, contract tests) for each Go version. 19 | test-linux: 20 | name: ${{ format('ldotel Linux, Go {0}', matrix.go-version) }} 21 | needs: go-versions 22 | strategy: 23 | # Let jobs fail independently, in case it's a single version that's broken. 24 | fail-fast: false 25 | matrix: 26 | go-version: ${{ fromJSON(needs.go-versions.outputs.matrix) }} 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | - name: Setup Go ${{ inputs.go-version }} 31 | uses: actions/setup-go@v5 32 | with: 33 | go-version: ${{ matrix.go-version }} 34 | - uses: ./.github/actions/unit-tests 35 | with: 36 | lint: 'true' 37 | test-target: ldotel-test 38 | - uses: ./.github/actions/coverage 39 | with: 40 | enforce: 'false' 41 | -------------------------------------------------------------------------------- /.github/workflows/release-please.yml: -------------------------------------------------------------------------------- 1 | name: Run Release Please 2 | 3 | on: 4 | push: 5 | branches: 6 | - v7 7 | workflow_dispatch: 8 | 9 | jobs: 10 | release-please: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | pull-requests: write 15 | steps: 16 | - uses: googleapis/release-please-action@v4 17 | with: 18 | target-branch: v7 19 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs" 2 | permissions: 3 | actions: write 4 | contents: read 5 | issues: write 6 | pull-requests: write 7 | on: 8 | workflow_dispatch: 9 | schedule: 10 | # Happen once per day at 1:30 AM 11 | - cron: "30 1 * * *" 12 | 13 | jobs: 14 | sdk-close-stale: 15 | uses: launchdarkly/gh-actions/.github/workflows/sdk-stale.yml@main 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | go-server-sdk.test 4 | allocations.out 5 | .idea 6 | .vscode 7 | go.work 8 | go.work.sum 9 | coverage.out 10 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | deadline: 120s 3 | tests: false 4 | 5 | linters: 6 | enable: 7 | - bodyclose 8 | - dupl 9 | - errcheck 10 | - goconst 11 | - gochecknoglobals 12 | - gochecknoinits 13 | - goconst 14 | - gocritic 15 | - gocyclo 16 | - godox 17 | - gofmt 18 | - goimports 19 | - gosec 20 | - gosimple 21 | - govet 22 | - ineffassign 23 | - lll 24 | - misspell 25 | - nakedret 26 | - nolintlint 27 | - prealloc 28 | - revive 29 | - staticcheck 30 | - stylecheck 31 | - typecheck 32 | - unconvert 33 | - unparam 34 | - unused 35 | - whitespace 36 | fast: false 37 | 38 | linters-settings: 39 | gofmt: 40 | simplify: false 41 | goimports: 42 | local-prefixes: gopkg.in/launchdarkly,github.com/launchdarkly 43 | revive: 44 | rules: 45 | - name: exported 46 | arguments: 47 | - disableStutteringCheck 48 | 49 | issues: 50 | exclude-use-default: false 51 | max-same-issues: 1000 52 | -------------------------------------------------------------------------------- /.release-please-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | ".": "7.10.2", 3 | "ldotel": "1.1.0", 4 | "ldai": "0.7.0" 5 | } 6 | -------------------------------------------------------------------------------- /.sdk_metadata.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "sdks": { 4 | "go-server-sdk": { 5 | "name": "Go Server SDK", 6 | "type": "server-side", 7 | "languages": [ 8 | "Go" 9 | ], 10 | "userAgents": ["GoClient"] 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "go.lintTool": "golangci-lint" 3 | } -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Repository Maintainers 2 | * @launchdarkly/team-sdk-go 3 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2016 Catamorphic, Co. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Reporting and Fixing Security Issues 2 | 3 | Please report all security issues to the LaunchDarkly security team by submitting a bug bounty report to our [HackerOne program](https://hackerone.com/launchdarkly?type=team). LaunchDarkly will triage and address all valid security issues following the response targets defined in our program policy. Valid security issues may be eligible for a bounty. 4 | 5 | Please do not open issues or pull requests for security issues. This makes the problem immediately visible to everyone, including potentially malicious actors. 6 | -------------------------------------------------------------------------------- /client_context_from_config.go: -------------------------------------------------------------------------------- 1 | package ldclient 2 | 3 | import ( 4 | "errors" 5 | "regexp" 6 | 7 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 8 | "github.com/launchdarkly/go-server-sdk/v7/internal" 9 | "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" 10 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 11 | ) 12 | 13 | var validTagKeyOrValueRegex = regexp.MustCompile(`(?s)^[\w.-]*$`) 14 | 15 | func newClientContextFromConfig( 16 | sdkKey string, 17 | config Config, 18 | ) (*internal.ClientContextImpl, error) { 19 | if !stringIsValidHTTPHeaderValue(sdkKey) { 20 | // We want to fail fast in this case, because if we got as far as trying to make an HTTP request 21 | // to LaunchDarkly with a malformed key, the Go HTTP client unfortunately would include the 22 | // actual Authorization header value in its error message, which could end up in logs - and the 23 | // value might be a real SDK key that just has (for instance) a newline at the end of it, so it 24 | // would be sensitive information. 25 | return nil, errors.New("SDK key contains invalid characters") 26 | } 27 | 28 | basicConfig := subsystems.BasicClientContext{ 29 | SDKKey: sdkKey, 30 | Offline: config.Offline, 31 | ServiceEndpoints: config.ServiceEndpoints, 32 | } 33 | 34 | loggingFactory := config.Logging 35 | if loggingFactory == nil { 36 | loggingFactory = ldcomponents.Logging() 37 | } 38 | logging, err := loggingFactory.Build(basicConfig) 39 | if err != nil { 40 | return nil, err 41 | } 42 | basicConfig.Logging = logging 43 | 44 | basicConfig.ApplicationInfo.ApplicationID = validateTagValue(config.ApplicationInfo.ApplicationID, 45 | "ApplicationID", logging.Loggers) 46 | basicConfig.ApplicationInfo.ApplicationVersion = validateTagValue(config.ApplicationInfo.ApplicationVersion, 47 | "ApplicationVersion", logging.Loggers) 48 | 49 | httpFactory := config.HTTP 50 | if httpFactory == nil { 51 | httpFactory = ldcomponents.HTTPConfiguration() 52 | } 53 | http, err := httpFactory.Build(basicConfig) 54 | if err != nil { 55 | return nil, err 56 | } 57 | basicConfig.HTTP = http 58 | 59 | return &internal.ClientContextImpl{BasicClientContext: basicConfig}, nil 60 | } 61 | 62 | func stringIsValidHTTPHeaderValue(s string) bool { 63 | for _, ch := range s { 64 | if ch < 32 || ch > 127 { 65 | return false 66 | } 67 | } 68 | return true 69 | } 70 | 71 | func validateTagValue(value, name string, loggers ldlog.Loggers) string { 72 | if value == "" { 73 | return "" 74 | } 75 | if len(value) > 64 { 76 | loggers.Warnf("Value of Config.ApplicationInfo.%s was longer than 64 characters and was discarded", name) 77 | return "" 78 | } 79 | if !validTagKeyOrValueRegex.MatchString(value) { 80 | loggers.Warnf("Value of Config.ApplicationInfo.%s contained invalid characters and was discarded", name) 81 | return "" 82 | } 83 | return value 84 | } 85 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package ldclient 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | type urlAppendingHTTPTransport string 8 | 9 | func urlAppendingHTTPClientFactory(suffix string) func(Config) http.Client { 10 | return func(Config) http.Client { 11 | return http.Client{Transport: urlAppendingHTTPTransport(suffix)} 12 | } 13 | } 14 | 15 | func (t urlAppendingHTTPTransport) RoundTrip(r *http.Request) (*http.Response, error) { 16 | req := *r 17 | req.URL.Path = req.URL.Path + string(t) 18 | return http.DefaultTransport.RoundTrip(&req) 19 | } 20 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/launchdarkly/go-server-sdk/v7 2 | 3 | go 1.23.0 4 | 5 | require ( 6 | github.com/fsnotify/fsnotify v1.4.7 7 | github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f 8 | github.com/launchdarkly/ccache v1.1.0 9 | github.com/launchdarkly/eventsource v1.9.1 10 | github.com/launchdarkly/go-jsonstream/v3 v3.1.0 11 | github.com/launchdarkly/go-ntlm-proxy-auth v1.0.2 12 | github.com/launchdarkly/go-sdk-common/v3 v3.1.0 13 | github.com/launchdarkly/go-sdk-events/v3 v3.5.0 14 | github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.1 15 | github.com/launchdarkly/go-test-helpers/v3 v3.0.2 16 | github.com/patrickmn/go-cache v2.1.0+incompatible 17 | github.com/stretchr/testify v1.9.0 18 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa 19 | golang.org/x/sync v0.8.0 20 | gopkg.in/ghodss/yaml.v1 v1.0.0 21 | ) 22 | 23 | require ( 24 | github.com/davecgh/go-spew v1.1.1 // indirect 25 | github.com/google/uuid v1.1.1 // indirect 26 | github.com/josharian/intern v1.0.0 // indirect 27 | github.com/launchdarkly/go-ntlmssp v1.0.2 // indirect 28 | github.com/launchdarkly/go-semver v1.0.3 // indirect 29 | github.com/mailru/easyjson v0.7.7 // indirect 30 | github.com/pmezard/go-difflib v1.0.0 // indirect 31 | github.com/rogpeppe/go-internal v1.9.0 // indirect 32 | golang.org/x/crypto v0.36.0 // indirect 33 | golang.org/x/sys v0.31.0 // indirect 34 | gopkg.in/yaml.v2 v2.3.0 // indirect 35 | gopkg.in/yaml.v3 v3.0.1 // indirect 36 | ) 37 | -------------------------------------------------------------------------------- /interfaces/application_info.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // ApplicationInfo allows configuration of application metadata. 4 | // 5 | // Application metadata may be used in LaunchDarkly analytics or other product features, but does not 6 | // affect feature flag evaluations. 7 | // 8 | // If you want to set non-default values for any of these fields, set the ApplicationInfo field 9 | // in the SDK's [github.com/launchdarkly/go-server-sdk/v7.Config] struct. 10 | type ApplicationInfo struct { 11 | // ApplicationID is a unique identifier representing the application where the LaunchDarkly SDK is 12 | // running. 13 | // 14 | // This can be specified as any string value as long as it only uses the following characters: ASCII 15 | // letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be 16 | // ignored. 17 | ApplicationID string 18 | 19 | // ApplicationVersion is a unique identifier representing the version of the application where the 20 | // LaunchDarkly SDK is running. 21 | // 22 | // This can be specified as any string value as long as it only uses the following characters: ASCII 23 | // letters, ASCII digits, period, hyphen, underscore. A string containing any other characters will be 24 | // ignored. 25 | ApplicationVersion string 26 | } 27 | -------------------------------------------------------------------------------- /interfaces/data_source_status_provider_types_test.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDataSourceStatusProviderTypes(t *testing.T) { 11 | t.Run("status string representation", func(t *testing.T) { 12 | now := time.Now() 13 | 14 | s1 := DataSourceStatus{State: DataSourceStateValid, StateSince: now} 15 | assert.Equal(t, "Status(VALID,"+now.Format(time.RFC3339)+",)", s1.String()) 16 | 17 | e := DataSourceErrorInfo{Kind: DataSourceErrorKindErrorResponse, StatusCode: 401, Time: time.Now()} 18 | s2 := DataSourceStatus{State: DataSourceStateInterrupted, StateSince: now, LastError: e} 19 | assert.Equal(t, "Status(INTERRUPTED,"+now.Format(time.RFC3339)+","+e.String()+")", s2.String()) 20 | }) 21 | 22 | t.Run("error string representation", func(t *testing.T) { 23 | now := time.Now() 24 | 25 | e1 := DataSourceErrorInfo{Kind: DataSourceErrorKindErrorResponse, StatusCode: 401, Time: time.Now()} 26 | assert.Equal(t, "ERROR_RESPONSE(401)@"+now.Format(time.RFC3339), e1.String()) 27 | 28 | e2 := DataSourceErrorInfo{Kind: DataSourceErrorKindErrorResponse, StatusCode: 401, 29 | Message: "nope", Time: time.Now()} 30 | assert.Equal(t, "ERROR_RESPONSE(401,nope)@"+now.Format(time.RFC3339), e2.String()) 31 | 32 | e3 := DataSourceErrorInfo{Kind: DataSourceErrorKindNetworkError, 33 | Message: "nope", Time: time.Now()} 34 | assert.Equal(t, "NETWORK_ERROR(nope)@"+now.Format(time.RFC3339), e3.String()) 35 | 36 | e4 := DataSourceErrorInfo{Kind: DataSourceErrorKindStoreError, Time: time.Now()} 37 | assert.Equal(t, "STORE_ERROR@"+now.Format(time.RFC3339), e4.String()) 38 | 39 | e5 := DataSourceErrorInfo{Kind: DataSourceErrorKindUnknown, Time: time.Now()} 40 | assert.Equal(t, "UNKNOWN@"+now.Format(time.RFC3339), e5.String()) 41 | 42 | e6 := DataSourceErrorInfo{Kind: DataSourceErrorKindUnknown} 43 | assert.Equal(t, "UNKNOWN", e6.String()) 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /interfaces/flagstate/package_info.go: -------------------------------------------------------------------------------- 1 | // Package flagstate contains the data types used by the LDClient.AllFlagsState() method. 2 | // These types are used when obtaining a snapshot of a set of feature flags at once. 3 | package flagstate 4 | -------------------------------------------------------------------------------- /interfaces/package_info.go: -------------------------------------------------------------------------------- 1 | // Package interfaces contains types that are part of the public API, but not needed for basic 2 | // use of the SDK. 3 | package interfaces 4 | -------------------------------------------------------------------------------- /interfaces/service_endpoints.go: -------------------------------------------------------------------------------- 1 | package interfaces 2 | 3 | // ServiceEndpoints allow configuration of custom service URIs. 4 | // 5 | // If you want to set non-default values for any of these fields, 6 | // set the ServiceEndpoints field in the SDK's 7 | // [github.com/launchdarkly/go-server-sdk/v7.Config] struct. 8 | // You may set individual values such as Streaming, or use the helper method 9 | // [github.com/launchdarkly/go-server-sdk/v7/ldcomponents.RelayProxyEndpoints]. 10 | // 11 | // Important note: if one or more URI is set to a custom value, then 12 | // all URIs should be set to custom values. Otherwise, the SDK will emit 13 | // an error-level log to surface this potential misconfiguration, while 14 | // using default values for the unset URIs. 15 | // 16 | // There are some scenarios where it is desirable to set only some of the 17 | // fields, but this is not recommended for general usage. If your scenario 18 | // requires it, you can call [WithPartialSpecification] to suppress the 19 | // error message. 20 | // 21 | // See Config.ServiceEndpoints for more details. 22 | type ServiceEndpoints struct { 23 | Streaming string 24 | Polling string 25 | Events string 26 | allowPartialSpecification bool 27 | } 28 | 29 | // WithPartialSpecification returns a copy of this ServiceEndpoints that will 30 | // not trigger an error-level log message if only some, but not all the fields 31 | // are set to custom values. This is an advanced configuration and likely not 32 | // necessary for most use-cases. 33 | func (s ServiceEndpoints) WithPartialSpecification() ServiceEndpoints { 34 | s.allowPartialSpecification = true 35 | return s 36 | } 37 | 38 | // PartialSpecificationRequested returns true if this ServiceEndpoints should not 39 | // be treated as malformed if some, but not all fields are set. 40 | func (s ServiceEndpoints) PartialSpecificationRequested() bool { 41 | return s.allowPartialSpecification 42 | } 43 | -------------------------------------------------------------------------------- /internal/bigsegments/big_segment_store_status_provider_impl.go: -------------------------------------------------------------------------------- 1 | package bigsegments 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 5 | "github.com/launchdarkly/go-server-sdk/v7/internal" 6 | ) 7 | 8 | // This is the standard implementation of BigSegmentStoreStatusProvider. Most of the work is done by 9 | // BigSegmentStoreManager, which exposes the methods that other SDK components need to access the store. 10 | // 11 | // We always create this component regardless of whether there really is a store. If there is no store (so 12 | // there is no BigSegmentStoreManager) then we won't actually be doing any Big Segments stuff, or sending 13 | // any status updates, but this API object still exists so your app won't crash if you try to use 14 | // GetStatus or AddStatusListener. 15 | type bigSegmentStoreStatusProviderImpl struct { 16 | getStatusFn func() interfaces.BigSegmentStoreStatus 17 | broadcaster *internal.Broadcaster[interfaces.BigSegmentStoreStatus] 18 | } 19 | 20 | // NewBigSegmentStoreStatusProviderImpl creates the internal implementation of 21 | // BigSegmentStoreStatusProvider. The manager parameter can be nil if there is no Big Segment store. 22 | func NewBigSegmentStoreStatusProviderImpl( 23 | getStatusFn func() interfaces.BigSegmentStoreStatus, 24 | broadcaster *internal.Broadcaster[interfaces.BigSegmentStoreStatus], 25 | ) interfaces.BigSegmentStoreStatusProvider { 26 | return &bigSegmentStoreStatusProviderImpl{ 27 | getStatusFn: getStatusFn, 28 | broadcaster: broadcaster, 29 | } 30 | } 31 | 32 | func (b *bigSegmentStoreStatusProviderImpl) GetStatus() interfaces.BigSegmentStoreStatus { 33 | if b.getStatusFn == nil { 34 | return interfaces.BigSegmentStoreStatus{Available: false} 35 | } 36 | return b.getStatusFn() 37 | } 38 | 39 | func (b *bigSegmentStoreStatusProviderImpl) AddStatusListener() <-chan interfaces.BigSegmentStoreStatus { 40 | return b.broadcaster.AddListener() 41 | } 42 | 43 | func (b *bigSegmentStoreStatusProviderImpl) RemoveStatusListener( 44 | ch <-chan interfaces.BigSegmentStoreStatus, 45 | ) { 46 | b.broadcaster.RemoveListener(ch) 47 | } 48 | -------------------------------------------------------------------------------- /internal/bigsegments/big_segment_store_status_provider_impl_test.go: -------------------------------------------------------------------------------- 1 | package bigsegments 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" 8 | 9 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 10 | "github.com/launchdarkly/go-server-sdk/v7/internal" 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestGetStatusWhenStatusFunctionIsUndefined(t *testing.T) { 15 | provider := NewBigSegmentStoreStatusProviderImpl(nil, nil) 16 | 17 | status := provider.GetStatus() 18 | assert.False(t, status.Available) 19 | assert.False(t, status.Stale) 20 | } 21 | 22 | func TestStatusListener(t *testing.T) { 23 | broadcaster := internal.NewBroadcaster[interfaces.BigSegmentStoreStatus]() 24 | defer broadcaster.Close() 25 | provider := NewBigSegmentStoreStatusProviderImpl(nil, broadcaster) 26 | 27 | ch1 := provider.AddStatusListener() 28 | ch2 := provider.AddStatusListener() 29 | ch3 := provider.AddStatusListener() 30 | provider.RemoveStatusListener(ch2) 31 | 32 | status := interfaces.BigSegmentStoreStatus{Available: false, Stale: false} 33 | broadcaster.Broadcast(status) 34 | mocks.ExpectBigSegmentStoreStatus(t, ch1, nil, time.Second, status) 35 | mocks.ExpectBigSegmentStoreStatus(t, ch3, nil, time.Second, status) 36 | assert.Len(t, ch2, 0) 37 | } 38 | -------------------------------------------------------------------------------- /internal/bigsegments/package_info.go: -------------------------------------------------------------------------------- 1 | // Package bigsegments is an internal package containing implementation details for the SDK's Big 2 | // Segment functionality, not including the part that is in go-server-sdk-evaluation. These are 3 | // not visible from outside of the SDK. 4 | // 5 | // This does not include implementations of specific Big Segment store integrations such as Redis. 6 | // Those are implemented in separate repositories such as 7 | // https://github.com/launchdarkly/go-server-sdk-redis-redigo. 8 | package bigsegments 9 | -------------------------------------------------------------------------------- /internal/bigsegments/user_hash.go: -------------------------------------------------------------------------------- 1 | package bigsegments 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/base64" 6 | ) 7 | 8 | // HashForContextKey computes the hash that we use in the Big Segment store. This function is exported 9 | // for use in LDClient tests. 10 | func HashForContextKey(key string) string { 11 | hashBytes := sha256.Sum256([]byte(key)) 12 | return base64.StdEncoding.EncodeToString(hashBytes[:]) 13 | } 14 | -------------------------------------------------------------------------------- /internal/client_context_impl.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | ldevents "github.com/launchdarkly/go-sdk-events/v3" 5 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 6 | ) 7 | 8 | // ClientContextImpl is the SDK's standard implementation of interfaces.ClientContext. 9 | type ClientContextImpl struct { 10 | subsystems.BasicClientContext 11 | // Used internally to share a diagnosticsManager instance between components. 12 | DiagnosticsManager *ldevents.DiagnosticsManager 13 | } 14 | -------------------------------------------------------------------------------- /internal/concurrent.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import "sync/atomic" 4 | 5 | // AtomicBoolean is a simple atomic boolean type based on sync/atomic. Since sync/atomic supports 6 | // only integer types, the implementation uses an int32. (Note: we should be able to get rid of 7 | // this once our minimum Go version becomes 1.19 or higher.) 8 | type AtomicBoolean struct { 9 | value int32 10 | } 11 | 12 | // Get returns the current value. 13 | func (a *AtomicBoolean) Get() bool { 14 | return int32ToBoolean(atomic.LoadInt32(&a.value)) 15 | } 16 | 17 | // Set updates the value. 18 | func (a *AtomicBoolean) Set(value bool) { 19 | atomic.StoreInt32(&a.value, booleanToInt32(value)) 20 | } 21 | 22 | // GetAndSet atomically updates the value and returns the previous value. 23 | func (a *AtomicBoolean) GetAndSet(value bool) bool { 24 | return int32ToBoolean(atomic.SwapInt32(&a.value, booleanToInt32(value))) 25 | } 26 | 27 | func booleanToInt32(value bool) int32 { 28 | if value { 29 | return 1 30 | } 31 | return 0 32 | } 33 | 34 | func int32ToBoolean(value int32) bool { 35 | return value != 0 36 | } 37 | -------------------------------------------------------------------------------- /internal/concurrent_test.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestAtomicBoolean(t *testing.T) { 10 | t.Run("defaults to false", func(t *testing.T) { 11 | var b AtomicBoolean 12 | assert.False(t, b.Get()) 13 | }) 14 | 15 | t.Run("Set", func(t *testing.T) { 16 | var b AtomicBoolean 17 | b.Set(true) 18 | assert.True(t, b.Get()) 19 | b.Set(false) 20 | assert.False(t, b.Get()) 21 | }) 22 | 23 | t.Run("GetAndSet", func(t *testing.T) { 24 | var b AtomicBoolean 25 | assert.False(t, b.GetAndSet(true)) 26 | assert.True(t, b.Get()) 27 | assert.True(t, b.GetAndSet(false)) 28 | assert.False(t, b.Get()) 29 | }) 30 | 31 | t.Run("data race", func(t *testing.T) { 32 | // should be flagged by race detector if our implementation is unsafer 33 | done := make(chan struct{}) 34 | var b AtomicBoolean 35 | go func() { 36 | for i := 0; i < 100; i++ { 37 | b.Set(true) 38 | } 39 | done <- struct{}{} 40 | }() 41 | go func() { 42 | for i := 0; i < 100; i++ { 43 | b.Get() 44 | } 45 | done <- struct{}{} 46 | }() 47 | <-done 48 | <-done 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /internal/datakinds/data_kind_internal.go: -------------------------------------------------------------------------------- 1 | package datakinds 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 5 | 6 | "github.com/launchdarkly/go-jsonstream/v3/jreader" 7 | ) 8 | 9 | // DataKindInternal is implemented along with DataKind to provide more efficient jsonstream-based 10 | // deserialization for our built-in data kinds. 11 | type DataKindInternal interface { 12 | ldstoretypes.DataKind 13 | DeserializeFromJSONReader(reader *jreader.Reader) (ldstoretypes.ItemDescriptor, error) 14 | } 15 | -------------------------------------------------------------------------------- /internal/datakinds/package_info.go: -------------------------------------------------------------------------------- 1 | // Package datakinds contains the implementations of ldstoretypes.DataKind for flags and segments. 2 | // 3 | // These have their own package because they are used in many places and therefore need to be in a 4 | // package that doesn't have other things with other dependencies, to avoid cyclic references. 5 | package datakinds 6 | -------------------------------------------------------------------------------- /internal/datasource/data_model_dependencies.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/internal/toposort" 5 | st "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 6 | ) 7 | 8 | // Maintains a bidirectional dependency graph that can be updated whenever an item has changed. 9 | type dependencyTracker struct { 10 | dependenciesFrom toposort.AdjacencyList 11 | dependenciesTo toposort.AdjacencyList 12 | } 13 | 14 | func newDependencyTracker() *dependencyTracker { 15 | return &dependencyTracker{ 16 | make(toposort.AdjacencyList), 17 | make(toposort.AdjacencyList), 18 | } 19 | } 20 | 21 | // Updates the dependency graph when an item has changed. 22 | func (d *dependencyTracker) updateDependenciesFrom( 23 | kind st.DataKind, 24 | fromKey string, 25 | fromItem st.ItemDescriptor, 26 | ) { 27 | fromWhat := toposort.NewVertex(kind, fromKey) 28 | updatedDependencies := toposort.GetNeighbors(kind, fromItem) 29 | 30 | oldDependencySet := d.dependenciesFrom[fromWhat] 31 | for oldDep := range oldDependencySet { 32 | depsToThisOldDep := d.dependenciesTo[oldDep] 33 | if depsToThisOldDep != nil { 34 | delete(depsToThisOldDep, fromWhat) 35 | } 36 | } 37 | 38 | d.dependenciesFrom[fromWhat] = updatedDependencies 39 | for newDep := range updatedDependencies { 40 | depsToThisNewDep := d.dependenciesTo[newDep] 41 | if depsToThisNewDep == nil { 42 | depsToThisNewDep = make(toposort.Neighbors) 43 | d.dependenciesTo[newDep] = depsToThisNewDep 44 | } 45 | depsToThisNewDep.Add(fromWhat) 46 | } 47 | } 48 | 49 | func (d *dependencyTracker) reset() { 50 | d.dependenciesFrom = make(toposort.AdjacencyList) 51 | d.dependenciesTo = make(toposort.AdjacencyList) 52 | } 53 | 54 | // Populates the given set with the union of the initial item and all items that directly or indirectly 55 | // depend on it (based on the current state of the dependency graph). 56 | func (d *dependencyTracker) addAffectedItems(itemsOut toposort.Neighbors, initialModifiedItem toposort.Vertex) { 57 | if !itemsOut.Contains(initialModifiedItem) { 58 | itemsOut.Add(initialModifiedItem) 59 | affectedItems := d.dependenciesTo[initialModifiedItem] 60 | for affectedItem := range affectedItems { 61 | d.addAffectedItems(itemsOut, affectedItem) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/datasource/data_source_status_provider_impl.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 7 | "github.com/launchdarkly/go-server-sdk/v7/internal" 8 | ) 9 | 10 | // dataSourceStatusProviderImpl is the internal implementation of DataSourceStatusProvider. It's not 11 | // exported because the rest of the SDK code only interacts with the public interface. 12 | type dataSourceStatusProviderImpl struct { 13 | broadcaster *internal.Broadcaster[interfaces.DataSourceStatus] 14 | dataSourceUpdates *DataSourceUpdateSinkImpl 15 | } 16 | 17 | // NewDataSourceStatusProviderImpl creates the internal implementation of DataSourceStatusProvider. 18 | func NewDataSourceStatusProviderImpl( 19 | broadcaster *internal.Broadcaster[interfaces.DataSourceStatus], 20 | dataSourceUpdates *DataSourceUpdateSinkImpl, 21 | ) interfaces.DataSourceStatusProvider { 22 | return &dataSourceStatusProviderImpl{broadcaster, dataSourceUpdates} 23 | } 24 | 25 | func (d *dataSourceStatusProviderImpl) GetStatus() interfaces.DataSourceStatus { 26 | return d.dataSourceUpdates.GetLastStatus() 27 | } 28 | 29 | func (d *dataSourceStatusProviderImpl) AddStatusListener() <-chan interfaces.DataSourceStatus { 30 | return d.broadcaster.AddListener() 31 | } 32 | 33 | func (d *dataSourceStatusProviderImpl) RemoveStatusListener(listener <-chan interfaces.DataSourceStatus) { 34 | d.broadcaster.RemoveListener(listener) 35 | } 36 | 37 | func (d *dataSourceStatusProviderImpl) WaitFor(desiredState interfaces.DataSourceState, timeout time.Duration) bool { 38 | return d.dataSourceUpdates.waitFor(desiredState, timeout) 39 | } 40 | -------------------------------------------------------------------------------- /internal/datasource/data_source_test_helpers_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "net/http" 5 | "testing" 6 | "time" 7 | 8 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" 9 | 10 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 11 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 12 | 13 | th "github.com/launchdarkly/go-test-helpers/v3" 14 | ) 15 | 16 | func withMockDataSourceUpdates(action func(*mocks.MockDataSourceUpdates)) { 17 | d := mocks.NewMockDataSourceUpdates(datastore.NewInMemoryDataStore(sharedtest.NewTestLoggers())) 18 | // currently don't need to defer any cleanup actions 19 | action(d) 20 | } 21 | 22 | func waitForReadyWithTimeout(t *testing.T, closeWhenReady <-chan struct{}, timeout time.Duration) { 23 | if !th.AssertChannelClosed(t, closeWhenReady, timeout) { 24 | t.FailNow() 25 | } 26 | } 27 | 28 | type urlAppendingHTTPTransport string 29 | 30 | func urlAppendingHTTPClientFactory(suffix string) func() *http.Client { 31 | return func() *http.Client { 32 | return &http.Client{Transport: urlAppendingHTTPTransport(suffix)} 33 | } 34 | } 35 | 36 | func (t urlAppendingHTTPTransport) RoundTrip(r *http.Request) (*http.Response, error) { 37 | req := *r 38 | req.URL.Path = req.URL.Path + string(t) 39 | return http.DefaultTransport.RoundTrip(&req) 40 | } 41 | -------------------------------------------------------------------------------- /internal/datasource/helpers_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "strconv" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHTTPStatusError(t *testing.T) { 11 | var error = httpStatusError{Message: "message", Code: 500} 12 | assert.Equal(t, "message", error.Error()) 13 | } 14 | 15 | func TestIsHTTPErrorRecoverable(t *testing.T) { 16 | for i := 400; i < 500; i++ { 17 | assert.Equal(t, i == 400 || i == 408 || i == 429, isHTTPErrorRecoverable(i), strconv.Itoa(i)) 18 | } 19 | for i := 500; i < 600; i++ { 20 | assert.True(t, isHTTPErrorRecoverable(i)) 21 | } 22 | } 23 | 24 | func TestHTTPErrorDescription(t *testing.T) { 25 | assert.Equal(t, "HTTP error 400", httpErrorDescription(400)) 26 | assert.Equal(t, "HTTP error 401 (invalid SDK key)", httpErrorDescription(401)) 27 | assert.Equal(t, "HTTP error 403 (invalid SDK key)", httpErrorDescription(403)) 28 | assert.Equal(t, "HTTP error 500", httpErrorDescription(500)) 29 | } 30 | 31 | // filterTest represents the expected URL query parameter that should 32 | // be generated for a particular filter key. For example, filter 'foo' should generate 33 | // query parameter 'filter=foo'. 34 | type filterTest struct { 35 | key string 36 | query string 37 | } 38 | 39 | // testWithFilters generates a nested test for a set of relevant filters. 40 | // The 'test' function is executed with the requested filter, and the expected query parameter 41 | // for that filter. 42 | func testWithFilters(t *testing.T, test func(t *testing.T, filterTest filterTest)) { 43 | testCases := map[string]filterTest{ 44 | "no filter": {"", ""}, 45 | "filter requires no encoding": {"microservice-1", "filter=microservice-1"}, 46 | "filter requires urlencoding": {"micro service 1", "filter=micro+service+1"}, 47 | } 48 | for name, params := range testCases { 49 | t.Run(name, func(t *testing.T) { 50 | test(t, params) 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/datasource/null_data_source.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import "github.com/launchdarkly/go-server-sdk/v7/subsystems" 4 | 5 | // NewNullDataSource returns a stub implementation of DataSource. 6 | func NewNullDataSource() subsystems.DataSource { 7 | return nullDataSource{} 8 | } 9 | 10 | type nullDataSource struct{} 11 | 12 | func (n nullDataSource) IsInitialized() bool { 13 | return true 14 | } 15 | 16 | func (n nullDataSource) Close() error { 17 | return nil 18 | } 19 | 20 | func (n nullDataSource) Start(closeWhenReady chan<- struct{}) { 21 | close(closeWhenReady) 22 | } 23 | -------------------------------------------------------------------------------- /internal/datasource/null_data_source_test.go: -------------------------------------------------------------------------------- 1 | package datasource 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestNullDataSource(t *testing.T) { 10 | d := NewNullDataSource() 11 | assert.True(t, d.IsInitialized()) 12 | 13 | ch := make(chan struct{}) 14 | d.Start(ch) 15 | _, ok := <-ch 16 | assert.False(t, ok) 17 | 18 | assert.Nil(t, d.Close()) 19 | } 20 | -------------------------------------------------------------------------------- /internal/datasource/package_info.go: -------------------------------------------------------------------------------- 1 | // Package datasource is an internal package containing implementation types for the SDK's data source 2 | // implementations (streaming, polling, etc.) and related functionality. These types are not visible 3 | // from outside of the SDK. 4 | // 5 | // This does not include the file data source, which is in the ldfiledata package. 6 | package datasource 7 | -------------------------------------------------------------------------------- /internal/datasourcev2/fdv1_polling_data_source.go: -------------------------------------------------------------------------------- 1 | package datasourcev2 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | 7 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 8 | "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" 9 | "github.com/launchdarkly/go-server-sdk/v7/internal/datasource" 10 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 11 | ) 12 | 13 | type fdv1ToFDv2Requester struct { 14 | requester *datasource.PollingRequester 15 | loggers ldlog.Loggers 16 | } 17 | 18 | func (r *fdv1ToFDv2Requester) Request( 19 | ctx context.Context, 20 | selector subsystems.Selector, 21 | ) (*subsystems.ChangeSet, error) { 22 | data, cached, err := r.requester.Request() 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | code := subsystems.IntentTransferFull 28 | reason := "cant-catchup" 29 | 30 | if cached { 31 | code = subsystems.IntentNone 32 | reason = "cached" 33 | } 34 | 35 | changeSetBuilder := subsystems.NewChangeSetBuilder() 36 | err = changeSetBuilder.Start(subsystems.ServerIntent{ 37 | Payload: subsystems.Payload{ 38 | ID: "arbitrary-id", 39 | Target: 0, 40 | Code: code, 41 | Reason: reason, 42 | }, 43 | }) 44 | if err != nil { 45 | return nil, err 46 | } 47 | 48 | for _, item := range data { 49 | kind := subsystems.FlagKind 50 | if item.Kind == datakinds.Segments { 51 | kind = subsystems.SegmentKind 52 | } 53 | 54 | for _, keyedItem := range item.Items { 55 | if keyedItem.Item.Item == nil { 56 | continue 57 | } 58 | 59 | bytes, err := json.Marshal(keyedItem.Item.Item) 60 | if err != nil { 61 | r.loggers.Warn("Error marshalling v1 item to JSON: %s", err) 62 | return nil, err 63 | } 64 | 65 | changeSetBuilder.AddPut(kind, keyedItem.Key, keyedItem.Item.Version, bytes) 66 | } 67 | } 68 | 69 | return changeSetBuilder.Finish(subsystems.NewSelector("state", 1)) 70 | } 71 | 72 | func (r *fdv1ToFDv2Requester) BaseURI() string { 73 | return r.requester.BaseURI() 74 | } 75 | 76 | func (r *fdv1ToFDv2Requester) FilterKey() string { 77 | return r.requester.FilterKey() 78 | } 79 | 80 | // NewFDv1PollingProcessor creates the internal implementation of the polling data source. 81 | func NewFDv1PollingProcessor( 82 | context subsystems.ClientContext, 83 | cfg datasource.PollingConfig, 84 | ) *PollingProcessor { 85 | requester := &fdv1ToFDv2Requester{ 86 | requester: datasource.NewPollingRequester(context, context.GetHTTP().CreateHTTPClient(), cfg.BaseURI, cfg.FilterKey), 87 | loggers: context.GetLogging().Loggers, 88 | } 89 | return newPollingProcessor(context, requester, cfg.PollInterval) 90 | } 91 | -------------------------------------------------------------------------------- /internal/datasourcev2/helpers.go: -------------------------------------------------------------------------------- 1 | package datasourcev2 2 | 3 | //nolint: godox 4 | // TODO: This was copied from datasource/helpers.go. We should extract these 5 | // out into a common module, or if we decide we don't need these later in the 6 | // v2 implementation, we should clean this up. 7 | 8 | import ( 9 | "fmt" 10 | "net/http" 11 | 12 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 13 | ) 14 | 15 | type httpStatusError struct { 16 | Message string 17 | Code int 18 | Header http.Header 19 | } 20 | 21 | func (e httpStatusError) Error() string { 22 | return e.Message 23 | } 24 | 25 | // Tests whether an HTTP error status represents a condition that might resolve on its own if we retry, 26 | // or at least should not make us permanently stop sending requests. 27 | func isHTTPErrorRecoverable(statusCode int) bool { 28 | if statusCode >= 400 && statusCode < 500 { 29 | switch statusCode { 30 | case 400: // bad request 31 | return true 32 | case 408: // request timeout 33 | return true 34 | case 429: // too many requests 35 | return true 36 | default: 37 | return false // all other 4xx errors are unrecoverable 38 | } 39 | } 40 | return true 41 | } 42 | 43 | func httpErrorDescription(statusCode int) string { 44 | message := "" 45 | if statusCode == 401 || statusCode == 403 { 46 | message = " (invalid SDK key)" 47 | } 48 | return fmt.Sprintf("HTTP error %d%s", statusCode, message) 49 | } 50 | 51 | // Logs an HTTP error or network error at the appropriate level and determines whether it is recoverable 52 | // (as defined by isHTTPErrorRecoverable). 53 | func checkIfErrorIsRecoverableAndLog( 54 | loggers ldlog.Loggers, 55 | errorDesc, errorContext string, 56 | statusCode int, 57 | recoverableMessage string, 58 | ) bool { 59 | if statusCode > 0 && !isHTTPErrorRecoverable(statusCode) { 60 | loggers.Errorf("Error %s (giving up permanently): %s", errorContext, errorDesc) 61 | return false 62 | } 63 | loggers.Warnf("Error %s (%s): %s", errorContext, recoverableMessage, errorDesc) 64 | return true 65 | } 66 | 67 | func checkForHTTPError(statusCode int, header http.Header, url string) error { 68 | if statusCode == http.StatusUnauthorized { 69 | return httpStatusError{ 70 | Message: fmt.Sprintf("Invalid SDK key when accessing URL: %s. Verify that your SDK key is correct.", url), 71 | Code: statusCode, 72 | Header: header, 73 | } 74 | } 75 | 76 | if statusCode == http.StatusNotFound { 77 | return httpStatusError{ 78 | Message: fmt.Sprintf("Resource not found when accessing URL: %s. Verify that this resource exists.", url), 79 | Code: statusCode, 80 | Header: header, 81 | } 82 | } 83 | 84 | if statusCode/100 != 2 { 85 | return httpStatusError{ 86 | Message: fmt.Sprintf("Unexpected response code: %d when accessing URL: %s", statusCode, url), 87 | Code: statusCode, 88 | Header: header, 89 | } 90 | } 91 | return nil 92 | } 93 | -------------------------------------------------------------------------------- /internal/datasourcev2/package_info.go: -------------------------------------------------------------------------------- 1 | // Package datasourcev2 is an internal package containing implementation types for the SDK's data source 2 | // implementations (streaming, polling, etc.) and related functionality. These types are not visible 3 | // from outside of the SDK. 4 | // 5 | // WARNING: This particular implementation supports the upcoming flag delivery v2 format which is not 6 | // publicly available. 7 | // 8 | // This does not include the file data source, which is in the ldfiledata package. 9 | package datasourcev2 10 | -------------------------------------------------------------------------------- /internal/datastore/data_store_eval_impl.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 5 | ldeval "github.com/launchdarkly/go-server-sdk-evaluation/v3" 6 | "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldmodel" 7 | "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" 8 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 9 | ) 10 | 11 | type dataStoreEvaluatorDataProviderImpl struct { 12 | store subsystems.ReadOnlyStore 13 | loggers ldlog.Loggers 14 | } 15 | 16 | // NewDataStoreEvaluatorDataProviderImpl creates the internal implementation of the adapter that connects 17 | // the Evaluator (from go-server-sdk-evaluation) with the data store. 18 | func NewDataStoreEvaluatorDataProviderImpl(store subsystems.ReadOnlyStore, loggers ldlog.Loggers) ldeval.DataProvider { 19 | return dataStoreEvaluatorDataProviderImpl{store, loggers} 20 | } 21 | 22 | func (d dataStoreEvaluatorDataProviderImpl) GetFeatureFlag(key string) *ldmodel.FeatureFlag { 23 | item, err := d.store.Get(datakinds.Features, key) 24 | if err == nil && item.Item != nil { 25 | data := item.Item 26 | if flag, ok := data.(*ldmodel.FeatureFlag); ok { 27 | return flag 28 | } 29 | d.loggers.Errorf("unexpected data type (%T) found in store for feature key: %s. Returning default value", data, key) 30 | } 31 | return nil 32 | } 33 | 34 | func (d dataStoreEvaluatorDataProviderImpl) GetSegment(key string) *ldmodel.Segment { 35 | item, err := d.store.Get(datakinds.Segments, key) 36 | if err == nil && item.Item != nil { 37 | data := item.Item 38 | if segment, ok := data.(*ldmodel.Segment); ok { 39 | return segment 40 | } 41 | d.loggers.Errorf("unexpected data type (%T) found in store for segment key: %s. Returning default value", data, key) 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/datastore/data_store_status_provider_impl.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 5 | ) 6 | 7 | // StatusMonitorable means a data store is capable of having its status monitored over time. 8 | type StatusMonitorable interface { 9 | // IsStatusMonitoringEnabled returns true if this data store implementation supports status 10 | // monitoring. 11 | // 12 | // This is normally only true for persistent data stores created with ldcomponents.PersistentDataStore(), 13 | // but it could also be true for any custom DataStore implementation that makes use of the 14 | // statusUpdater parameter provided to the DataStoreFactory. Returning true means that the store 15 | // guarantees that if it ever enters an invalid state (that is, an operation has failed or it knows 16 | // that operations cannot succeed at the moment), it will publish a status update, and will then 17 | // publish another status update once it has returned to a valid state. 18 | // 19 | // The same value will be returned from DataStoreStatusProvider.IsStatusMonitoringEnabled(). 20 | IsStatusMonitoringEnabled() bool 21 | } 22 | 23 | // dataStoreStatusProviderImpl is the internal implementation of DataStoreStatusProvider. It's not 24 | // exported because the rest of the SDK code only interacts with the public interface. 25 | type dataStoreStatusProviderImpl struct { 26 | store StatusMonitorable 27 | dataStoreUpdates *DataStoreUpdateSinkImpl 28 | } 29 | 30 | // NewDataStoreStatusProviderImpl creates the internal implementation of DataStoreStatusProvider. 31 | func NewDataStoreStatusProviderImpl( 32 | store StatusMonitorable, 33 | dataStoreUpdates *DataStoreUpdateSinkImpl, 34 | ) interfaces.DataStoreStatusProvider { 35 | return &dataStoreStatusProviderImpl{ 36 | store: store, 37 | dataStoreUpdates: dataStoreUpdates, 38 | } 39 | } 40 | 41 | func (d *dataStoreStatusProviderImpl) GetStatus() interfaces.DataStoreStatus { 42 | return d.dataStoreUpdates.getStatus() 43 | } 44 | 45 | func (d *dataStoreStatusProviderImpl) IsStatusMonitoringEnabled() bool { 46 | return d.store.IsStatusMonitoringEnabled() 47 | } 48 | 49 | func (d *dataStoreStatusProviderImpl) AddStatusListener() <-chan interfaces.DataStoreStatus { 50 | return d.dataStoreUpdates.getBroadcaster().AddListener() 51 | } 52 | 53 | func (d *dataStoreStatusProviderImpl) RemoveStatusListener(ch <-chan interfaces.DataStoreStatus) { 54 | d.dataStoreUpdates.getBroadcaster().RemoveListener(ch) 55 | } 56 | -------------------------------------------------------------------------------- /internal/datastore/data_store_status_provider_impl_test.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 12 | "github.com/launchdarkly/go-server-sdk/v7/internal" 13 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 14 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 15 | ) 16 | 17 | type dataStoreStatusProviderTestParams struct { 18 | dataStore *mocks.CapturingDataStore 19 | dataStoreUpdates subsystems.DataStoreUpdateSink 20 | dataStoreStatusProvider interfaces.DataStoreStatusProvider 21 | } 22 | 23 | func dataStoreStatusProviderTest(action func(dataStoreStatusProviderTestParams)) { 24 | p := dataStoreStatusProviderTestParams{} 25 | p.dataStore = mocks.NewCapturingDataStore(NewInMemoryDataStore(sharedtest.NewTestLoggers())) 26 | broadcaster := internal.NewBroadcaster[interfaces.DataStoreStatus]() 27 | defer broadcaster.Close() 28 | dataStoreUpdates := NewDataStoreUpdateSinkImpl(broadcaster) 29 | p.dataStoreUpdates = dataStoreUpdates 30 | p.dataStoreStatusProvider = NewDataStoreStatusProviderImpl(p.dataStore, dataStoreUpdates) 31 | 32 | action(p) 33 | } 34 | 35 | func TestDataStoreStatusProviderImpl(t *testing.T) { 36 | t.Run("GetStatus", func(t *testing.T) { 37 | dataStoreStatusProviderTest(func(p dataStoreStatusProviderTestParams) { 38 | assert.Equal(t, interfaces.DataStoreStatus{Available: true}, p.dataStoreStatusProvider.GetStatus()) 39 | 40 | newStatus := interfaces.DataStoreStatus{Available: false} 41 | p.dataStoreUpdates.UpdateStatus(newStatus) 42 | 43 | assert.Equal(t, newStatus, p.dataStoreStatusProvider.GetStatus()) 44 | }) 45 | }) 46 | 47 | t.Run("IsStatusMonitoringEnabled", func(t *testing.T) { 48 | dataStoreStatusProviderTest(func(p dataStoreStatusProviderTestParams) { 49 | p.dataStore.SetStatusMonitoringEnabled(true) 50 | assert.True(t, p.dataStoreStatusProvider.IsStatusMonitoringEnabled()) 51 | }) 52 | 53 | dataStoreStatusProviderTest(func(p dataStoreStatusProviderTestParams) { 54 | p.dataStore.SetStatusMonitoringEnabled(false) 55 | assert.False(t, p.dataStoreStatusProvider.IsStatusMonitoringEnabled()) 56 | }) 57 | }) 58 | 59 | t.Run("listeners", func(t *testing.T) { 60 | dataStoreStatusProviderTest(func(p dataStoreStatusProviderTestParams) { 61 | ch1 := p.dataStoreStatusProvider.AddStatusListener() 62 | ch2 := p.dataStoreStatusProvider.AddStatusListener() 63 | ch3 := p.dataStoreStatusProvider.AddStatusListener() 64 | p.dataStoreStatusProvider.RemoveStatusListener(ch2) 65 | 66 | newStatus := interfaces.DataStoreStatus{Available: false} 67 | p.dataStoreUpdates.UpdateStatus(newStatus) 68 | 69 | require.Len(t, ch1, 1) 70 | require.Len(t, ch2, 0) 71 | require.Len(t, ch3, 1) 72 | assert.Equal(t, newStatus, <-ch1) 73 | assert.Equal(t, newStatus, <-ch3) 74 | }) 75 | }) 76 | } 77 | -------------------------------------------------------------------------------- /internal/datastore/data_store_test_helpers_test.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 7 | ) 8 | 9 | type unknownDataKind struct{} 10 | 11 | func (k unknownDataKind) GetName() string { 12 | return "unknown" 13 | } 14 | 15 | func (k unknownDataKind) Serialize(item ldstoretypes.ItemDescriptor) []byte { 16 | return nil 17 | } 18 | 19 | func (k unknownDataKind) Deserialize(data []byte) (ldstoretypes.ItemDescriptor, error) { 20 | return ldstoretypes.ItemDescriptor{}, errors.New("not implemented") 21 | } 22 | -------------------------------------------------------------------------------- /internal/datastore/data_store_update_sink_impl.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 7 | "github.com/launchdarkly/go-server-sdk/v7/internal" 8 | ) 9 | 10 | // DataStoreUpdateSinkImpl is the internal implementation of DataStoreUpdateSink. It is exported 11 | // because the actual implementation type, rather than the interface, is required as a dependency 12 | // of other SDK components. 13 | type DataStoreUpdateSinkImpl struct { 14 | lastStatus interfaces.DataStoreStatus 15 | broadcaster *internal.Broadcaster[interfaces.DataStoreStatus] 16 | lock sync.Mutex 17 | } 18 | 19 | // NewDataStoreUpdateSinkImpl creates the internal implementation of DataStoreUpdateSink. 20 | func NewDataStoreUpdateSinkImpl( 21 | broadcaster *internal.Broadcaster[interfaces.DataStoreStatus], 22 | ) *DataStoreUpdateSinkImpl { 23 | return &DataStoreUpdateSinkImpl{ 24 | lastStatus: interfaces.DataStoreStatus{Available: true}, 25 | broadcaster: broadcaster, 26 | } 27 | } 28 | 29 | func (d *DataStoreUpdateSinkImpl) getStatus() interfaces.DataStoreStatus { 30 | d.lock.Lock() 31 | defer d.lock.Unlock() 32 | return d.lastStatus 33 | } 34 | 35 | func (d *DataStoreUpdateSinkImpl) getBroadcaster() *internal.Broadcaster[interfaces.DataStoreStatus] { 36 | return d.broadcaster 37 | } 38 | 39 | // UpdateStatus is called from the data store to push a status update. 40 | func (d *DataStoreUpdateSinkImpl) UpdateStatus(newStatus interfaces.DataStoreStatus) { 41 | d.lock.Lock() 42 | modified := false 43 | if newStatus != d.lastStatus { 44 | d.lastStatus = newStatus 45 | modified = true 46 | } 47 | d.lock.Unlock() 48 | if modified { 49 | d.broadcaster.Broadcast(newStatus) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/datastore/data_store_update_sink_impl_test.go: -------------------------------------------------------------------------------- 1 | package datastore 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 9 | "github.com/launchdarkly/go-server-sdk/v7/internal" 10 | ) 11 | 12 | func TestDataStoreUpdateSinkImpl(t *testing.T) { 13 | t.Run("getStatus", func(t *testing.T) { 14 | dataStoreUpdates := NewDataStoreUpdateSinkImpl(internal.NewBroadcaster[interfaces.DataStoreStatus]()) 15 | 16 | assert.Equal(t, interfaces.DataStoreStatus{Available: true}, dataStoreUpdates.getStatus()) 17 | 18 | newStatus := interfaces.DataStoreStatus{Available: true} 19 | dataStoreUpdates.UpdateStatus(newStatus) 20 | 21 | assert.Equal(t, newStatus, dataStoreUpdates.getStatus()) 22 | }) 23 | 24 | t.Run("UpdateStatus", func(t *testing.T) { 25 | broadcaster := internal.NewBroadcaster[interfaces.DataStoreStatus]() 26 | defer broadcaster.Close() 27 | 28 | ch := broadcaster.AddListener() 29 | 30 | dataStoreUpdates := NewDataStoreUpdateSinkImpl(broadcaster) 31 | 32 | newStatus := interfaces.DataStoreStatus{Available: false} 33 | dataStoreUpdates.UpdateStatus(newStatus) 34 | 35 | assert.Equal(t, newStatus, <-ch) 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /internal/datastore/package_info.go: -------------------------------------------------------------------------------- 1 | // Package datastore is an internal package containing implementation types for the SDK's data store 2 | // implementations (in-memory vs. cached persistent store) and related functionality. These types are 3 | // not visible from outside of the SDK. 4 | // 5 | // This does not include implementations of specific database integrations such as Redis. Those are 6 | // implemented in separate repositories such as https://github.com/launchdarkly/go-server-sdk-redis-redigo. 7 | package datastore 8 | -------------------------------------------------------------------------------- /internal/datasystem/data_availability.go: -------------------------------------------------------------------------------- 1 | package datasystem 2 | 3 | // DataAvailability represents the availability of data in the SDK. 4 | type DataAvailability string 5 | 6 | const ( 7 | // Defaults means the SDK has no data and will evaluate flags using the application-provided default values. 8 | Defaults = DataAvailability("defaults") 9 | // Cached means the SDK has data, not necessarily the latest, which will be used to evaluate flags. 10 | Cached = DataAvailability("cached") 11 | // Refreshed means the SDK has obtained, at least once, the latest known data from LaunchDarkly. 12 | Refreshed = DataAvailability("refreshed") 13 | ) 14 | 15 | // AtLeast returns true if the DataAvailability is at least as good as the 16 | // other DataAvailability in terms of data quality. 17 | func (da DataAvailability) AtLeast(other DataAvailability) bool { 18 | if da == other { 19 | return true 20 | } 21 | 22 | return da == Refreshed || (da == Cached && other == Defaults) 23 | } 24 | -------------------------------------------------------------------------------- /internal/datasystem/data_availability_test.go: -------------------------------------------------------------------------------- 1 | package datasystem 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestDataAvailibilityAtLeast(t *testing.T) { 10 | assert.True(t, Refreshed.AtLeast(Refreshed)) 11 | assert.True(t, Refreshed.AtLeast(Cached)) 12 | assert.True(t, Refreshed.AtLeast(Defaults)) 13 | 14 | assert.False(t, Cached.AtLeast(Refreshed)) 15 | assert.True(t, Cached.AtLeast(Cached)) 16 | assert.True(t, Cached.AtLeast(Defaults)) 17 | 18 | assert.False(t, Defaults.AtLeast(Refreshed)) 19 | assert.False(t, Defaults.AtLeast(Cached)) 20 | assert.True(t, Defaults.AtLeast(Defaults)) 21 | } 22 | -------------------------------------------------------------------------------- /internal/datasystem/data_model_dependencies.go: -------------------------------------------------------------------------------- 1 | package datasystem 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/internal/toposort" 5 | st "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 6 | ) 7 | 8 | // Maintains a bidirectional dependency graph that can be updated whenever an item has changed. 9 | // 10 | // This type is duplicated from internal.datasource. This duplication will be 11 | // removed when FDv1 is removed. 12 | type dependencyTracker struct { 13 | dependenciesFrom toposort.AdjacencyList 14 | dependenciesTo toposort.AdjacencyList 15 | } 16 | 17 | func newDependencyTracker() *dependencyTracker { 18 | return &dependencyTracker{ 19 | make(toposort.AdjacencyList), 20 | make(toposort.AdjacencyList), 21 | } 22 | } 23 | 24 | // Updates the dependency graph when an item has changed. 25 | func (d *dependencyTracker) updateDependenciesFrom( 26 | kind st.DataKind, 27 | fromKey string, 28 | fromItem st.ItemDescriptor, 29 | ) { 30 | fromWhat := toposort.NewVertex(kind, fromKey) 31 | updatedDependencies := toposort.GetNeighbors(kind, fromItem) 32 | 33 | oldDependencySet := d.dependenciesFrom[fromWhat] 34 | for oldDep := range oldDependencySet { 35 | depsToThisOldDep := d.dependenciesTo[oldDep] 36 | if depsToThisOldDep != nil { 37 | delete(depsToThisOldDep, fromWhat) 38 | } 39 | } 40 | 41 | d.dependenciesFrom[fromWhat] = updatedDependencies 42 | for newDep := range updatedDependencies { 43 | depsToThisNewDep := d.dependenciesTo[newDep] 44 | if depsToThisNewDep == nil { 45 | depsToThisNewDep = make(toposort.Neighbors) 46 | d.dependenciesTo[newDep] = depsToThisNewDep 47 | } 48 | depsToThisNewDep.Add(fromWhat) 49 | } 50 | } 51 | 52 | func (d *dependencyTracker) reset() { 53 | d.dependenciesFrom = make(toposort.AdjacencyList) 54 | d.dependenciesTo = make(toposort.AdjacencyList) 55 | } 56 | 57 | // Populates the given set with the union of the initial item and all items that directly or indirectly 58 | // depend on it (based on the current state of the dependency graph). 59 | func (d *dependencyTracker) addAffectedItems(itemsOut toposort.Neighbors, initialModifiedItem toposort.Vertex) { 60 | if !itemsOut.Contains(initialModifiedItem) { 61 | itemsOut.Add(initialModifiedItem) 62 | affectedItems := d.dependenciesTo[initialModifiedItem] 63 | for affectedItem := range affectedItems { 64 | d.addAffectedItems(itemsOut, affectedItem) 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /internal/datasystem/package.go: -------------------------------------------------------------------------------- 1 | // Package datasystem encapsulates the interactions between the SDK's data store, data source, and other related 2 | // components. 3 | // Currently, there is only one data system implementation, FDv1, which represents the functionality of the SDK 4 | // before the FDv2 protocol was introduced. 5 | package datasystem 6 | -------------------------------------------------------------------------------- /internal/endpoints/package_info.go: -------------------------------------------------------------------------------- 1 | // Package endpoints contains internal constants and functions for computing service endpoint URIs. 2 | package endpoints 3 | -------------------------------------------------------------------------------- /internal/endpoints/standard_endpoints.go: -------------------------------------------------------------------------------- 1 | package endpoints 2 | 3 | const ( 4 | // DefaultStreamingBaseURI is the default base URI of the streaming service. 5 | DefaultStreamingBaseURI = "https://stream.launchdarkly.com/" 6 | 7 | // DefaultPollingBaseURI is the default base URI of the polling service. 8 | DefaultPollingBaseURI = "https://sdk.launchdarkly.com/" 9 | 10 | // DefaultEventsBaseURI is the default base URI of the events service. 11 | DefaultEventsBaseURI = "https://events.launchdarkly.com/" 12 | 13 | // StreamingRequestPath is the URL path for the server-side streaming endpoint. 14 | StreamingRequestPath = "/all" 15 | 16 | // StreamingRequestV2Path is the URL path for the server-side streaming endpoint v2. 17 | StreamingRequestV2Path = "/sdk/stream" 18 | 19 | // PollingRequestPath is the URL path for the server-side polling endpoint. 20 | PollingRequestPath = "/sdk/latest-all" 21 | 22 | // PollingRequestV2Path is the URL path for the server-side polling endpoint v2. 23 | PollingRequestV2Path = "/sdk/poll" 24 | 25 | // Events service paths are defined in the go-sdk-events package 26 | ) 27 | -------------------------------------------------------------------------------- /internal/hooks/evaluation_execution.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | gocontext "context" 5 | 6 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 7 | "github.com/launchdarkly/go-sdk-common/v3/ldreason" 8 | "github.com/launchdarkly/go-server-sdk/v7/ldhooks" 9 | ) 10 | 11 | // EvaluationExecution represents the state of a running series of evaluation stages. 12 | type EvaluationExecution struct { 13 | hooks []ldhooks.Hook 14 | data []ldhooks.EvaluationSeriesData 15 | context ldhooks.EvaluationSeriesContext 16 | loggers *ldlog.Loggers 17 | } 18 | 19 | // BeforeEvaluation executes the BeforeEvaluation stage of registered hooks. 20 | func (e *EvaluationExecution) BeforeEvaluation(ctx gocontext.Context) { 21 | e.executeStage( 22 | false, 23 | "BeforeEvaluation", 24 | func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { 25 | return hook.BeforeEvaluation(ctx, e.context, data) 26 | }) 27 | } 28 | 29 | // AfterEvaluation executes the AfterEvaluation stage of registered hooks. 30 | func (e *EvaluationExecution) AfterEvaluation( 31 | ctx gocontext.Context, 32 | detail ldreason.EvaluationDetail, 33 | ) { 34 | e.executeStage( 35 | true, 36 | "AfterEvaluation", 37 | func(hook ldhooks.Hook, data ldhooks.EvaluationSeriesData) (ldhooks.EvaluationSeriesData, error) { 38 | return hook.AfterEvaluation(ctx, e.context, data, detail) 39 | }) 40 | } 41 | 42 | func (e *EvaluationExecution) executeStage( 43 | reverse bool, 44 | stageName string, 45 | fn func( 46 | hook ldhooks.Hook, 47 | data ldhooks.EvaluationSeriesData, 48 | ) (ldhooks.EvaluationSeriesData, error)) { 49 | returnData := make([]ldhooks.EvaluationSeriesData, len(e.hooks)) 50 | iterator := newIterator(reverse, e.hooks) 51 | for iterator.hasNext() { 52 | i, hook := iterator.getNext() 53 | 54 | outData, err := fn(hook, e.data[i]) 55 | if err != nil { 56 | returnData[i] = e.data[i] 57 | e.loggers.Errorf( 58 | "During evaluation of flag \"%s\", an error was encountered in \"%s\" of the \"%s\" hook: %s", 59 | e.context.FlagKey(), 60 | stageName, 61 | hook.Metadata().Name(), 62 | err.Error()) 63 | continue 64 | } 65 | returnData[i] = outData 66 | } 67 | e.data = returnData 68 | } 69 | -------------------------------------------------------------------------------- /internal/hooks/iterator.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/ldhooks" 5 | ) 6 | 7 | type iterator struct { 8 | reverse bool 9 | cursor int 10 | collection []ldhooks.Hook 11 | } 12 | 13 | // newIterator creates a new hook iterator which can iterate hooks forward or reverse. 14 | // 15 | // The collection being iterated should not be modified during iteration. 16 | // 17 | // Example: 18 | // it := newIterator(false, hooks) 19 | // 20 | // for it.hasNext() { 21 | // hook := it.getNext() 22 | // } 23 | func newIterator(reverse bool, hooks []ldhooks.Hook) *iterator { 24 | cursor := -1 25 | if reverse { 26 | cursor = len(hooks) 27 | } 28 | return &iterator{ 29 | reverse: reverse, 30 | cursor: cursor, 31 | collection: hooks, 32 | } 33 | } 34 | 35 | func (it *iterator) hasNext() bool { 36 | nextCursor := it.getNextIndex() 37 | return it.inBounds(nextCursor) 38 | } 39 | 40 | func (it *iterator) inBounds(nextCursor int) bool { 41 | inBounds := nextCursor < len(it.collection) && nextCursor >= 0 42 | return inBounds 43 | } 44 | 45 | func (it *iterator) getNextIndex() int { 46 | var nextCursor int 47 | if it.reverse { 48 | nextCursor = it.cursor - 1 49 | } else { 50 | nextCursor = it.cursor + 1 51 | } 52 | return nextCursor 53 | } 54 | 55 | func (it *iterator) getNext() (int, ldhooks.Hook) { 56 | i := it.getNextIndex() 57 | if it.inBounds(i) { 58 | it.cursor = i 59 | return it.cursor, it.collection[it.cursor] 60 | } 61 | return it.cursor, nil 62 | } 63 | -------------------------------------------------------------------------------- /internal/hooks/iterator_test.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 8 | "github.com/launchdarkly/go-server-sdk/v7/ldhooks" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestIterator(t *testing.T) { 13 | testCases := []bool{false, true} 14 | for _, reverse := range testCases { 15 | t.Run(fmt.Sprintf("reverse: %v", reverse), func(t *testing.T) { 16 | t.Run("empty collection", func(t *testing.T) { 17 | 18 | var hooks []ldhooks.Hook 19 | it := newIterator(reverse, hooks) 20 | 21 | assert.False(t, it.hasNext()) 22 | 23 | _, value := it.getNext() 24 | assert.Zero(t, value) 25 | 26 | }) 27 | 28 | t.Run("collection with items", func(t *testing.T) { 29 | hooks := []ldhooks.Hook{ 30 | sharedtest.NewTestHook("a"), 31 | sharedtest.NewTestHook("b"), 32 | sharedtest.NewTestHook("c"), 33 | } 34 | 35 | it := newIterator(reverse, hooks) 36 | 37 | var cursor int 38 | count := 0 39 | if reverse { 40 | cursor = 2 41 | } else { 42 | cursor += 0 43 | } 44 | for it.hasNext() { 45 | index, value := it.getNext() 46 | assert.Equal(t, cursor, index) 47 | assert.Equal(t, hooks[cursor].Metadata().Name(), value.Metadata().Name()) 48 | 49 | count += 1 50 | 51 | if reverse { 52 | cursor -= 1 53 | } else { 54 | cursor += 1 55 | } 56 | 57 | } 58 | assert.Equal(t, 3, count) 59 | assert.False(t, it.hasNext()) 60 | }) 61 | }) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/hooks/package_info.go: -------------------------------------------------------------------------------- 1 | // Package hooks is an internal package containing implementations to run hooks. 2 | package hooks 3 | -------------------------------------------------------------------------------- /internal/hooks/runner.go: -------------------------------------------------------------------------------- 1 | package hooks 2 | 3 | import ( 4 | gocontext "context" 5 | 6 | "github.com/launchdarkly/go-sdk-common/v3/ldcontext" 7 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 8 | "github.com/launchdarkly/go-sdk-common/v3/ldreason" 9 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 10 | "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldmodel" 11 | "github.com/launchdarkly/go-server-sdk/v7/ldhooks" 12 | ) 13 | 14 | // Runner manages the registration and execution of hooks. 15 | type Runner struct { 16 | hooks []ldhooks.Hook 17 | loggers ldlog.Loggers 18 | } 19 | 20 | // NewRunner creates a new hook runner. 21 | func NewRunner(loggers ldlog.Loggers, hooks []ldhooks.Hook) *Runner { 22 | return &Runner{ 23 | loggers: loggers, 24 | hooks: hooks, 25 | } 26 | } 27 | 28 | // RunEvaluation runs the evaluation series surrounding the given evaluation function. 29 | func (h *Runner) RunEvaluation( 30 | ctx gocontext.Context, 31 | flagKey string, 32 | evalContext ldcontext.Context, 33 | defaultVal ldvalue.Value, 34 | method string, 35 | fn func() (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error), 36 | ) (ldreason.EvaluationDetail, *ldmodel.FeatureFlag, error) { 37 | if len(h.hooks) == 0 { 38 | return fn() 39 | } 40 | e := h.prepareEvaluationSeries(flagKey, evalContext, defaultVal, method) 41 | e.BeforeEvaluation(ctx) 42 | detail, flag, err := fn() 43 | e.AfterEvaluation(ctx, detail) 44 | return detail, flag, err 45 | } 46 | 47 | // PrepareEvaluationSeries creates an EvaluationExecution suitable for executing evaluation stages. 48 | func (h *Runner) prepareEvaluationSeries( 49 | flagKey string, 50 | evalContext ldcontext.Context, 51 | defaultVal ldvalue.Value, 52 | method string, 53 | ) *EvaluationExecution { 54 | returnData := make([]ldhooks.EvaluationSeriesData, len(h.hooks)) 55 | for i := range h.hooks { 56 | returnData[i] = ldhooks.EmptyEvaluationSeriesData() 57 | } 58 | return &EvaluationExecution{ 59 | hooks: h.hooks, 60 | data: returnData, 61 | context: ldhooks.NewEvaluationSeriesContext(flagKey, evalContext, defaultVal, method), 62 | loggers: &h.loggers, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /internal/log_messages.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | ) 7 | 8 | // This file contains helper functions for generating various kinds of standardized log warnings/errors. 9 | // In some cases, these need to be written directly to os.Stderr instead of using our ldlog.Loggers API 10 | // because they are for conditions where we don't have access to any configured SDK components. 11 | 12 | // LogErrorNilPointerMethod prints a message to os.Stderr to indicate that the application tried to call 13 | // a method on a nil pointer receiver. 14 | func LogErrorNilPointerMethod(typeName string) { 15 | fmt.Fprintf(os.Stderr, "[LaunchDarkly] ERROR: tried to call a method on a nil pointer of type *%s\n", typeName) 16 | } 17 | -------------------------------------------------------------------------------- /internal/package_info.go: -------------------------------------------------------------------------------- 1 | // Package internal contains SDK implementation details that are shared between packages, 2 | // but are not exposed to application code. The datasource and datastore subpackages contain 3 | // implementation components specific to their areas of functionality. 4 | package internal 5 | -------------------------------------------------------------------------------- /internal/sharedtest/events.go: -------------------------------------------------------------------------------- 1 | package sharedtest 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 8 | 9 | th "github.com/launchdarkly/go-test-helpers/v3" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // ExpectFlagChangeEvents asserts that a channel receives flag change events for the specified keys (in 15 | // any order) and then does not receive any more events for the next 100ms. 16 | func ExpectFlagChangeEvents(t *testing.T, ch <-chan interfaces.FlagChangeEvent, keys ...string) { 17 | expectedChangedFlagKeys := make(map[string]bool) 18 | for _, key := range keys { 19 | expectedChangedFlagKeys[key] = true 20 | } 21 | actualChangedFlagKeys := make(map[string]bool) 22 | ReadLoop: 23 | for i := 0; i < len(keys); i++ { 24 | select { 25 | case event, ok := <-ch: 26 | if !ok { 27 | break ReadLoop 28 | } 29 | actualChangedFlagKeys[event.Key] = true 30 | case <-time.After(time.Second): 31 | assert.Fail(t, "did not receive expected event", "expected: %v, received: %v", 32 | expectedChangedFlagKeys, actualChangedFlagKeys) 33 | return 34 | } 35 | } 36 | assert.Equal(t, expectedChangedFlagKeys, actualChangedFlagKeys) 37 | th.AssertNoMoreValues(t, ch, time.Millisecond*100) 38 | } 39 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/assert.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "reflect" 5 | ) 6 | 7 | // AssertNotNil forces a panic if the specified value is nil (either a nil interface value, or a 8 | // nil pointer). 9 | func AssertNotNil(i interface{}) { 10 | if i != nil { 11 | val := reflect.ValueOf(i) 12 | if val.Kind() != reflect.Ptr || !val.IsNil() { 13 | return 14 | } 15 | } 16 | panic("unexpected nil pointer or nil interface value") 17 | } 18 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/mock_components.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "github.com/launchdarkly/go-server-sdk/v7/subsystems" 4 | 5 | // SingleComponentConfigurer is a test implementation of ComponentConfigurer that always returns the same 6 | // pre-existing instance. 7 | type SingleComponentConfigurer[T any] struct { 8 | Instance T 9 | } 10 | 11 | // Build builds the component. 12 | func (c SingleComponentConfigurer[T]) Build(clientContext subsystems.ClientContext) (T, error) { 13 | return c.Instance, nil 14 | } 15 | 16 | // ComponentConfigurerThatReturnsError is a test implementation of ComponentConfigurer that always returns 17 | // an error. 18 | type ComponentConfigurerThatReturnsError[T any] struct { 19 | Err error 20 | } 21 | 22 | // Build builds the component. 23 | func (c ComponentConfigurerThatReturnsError[T]) Build(clientContext subsystems.ClientContext) (T, error) { 24 | var empty T 25 | return empty, c.Err 26 | } 27 | 28 | // ComponentConfigurerThatCapturesClientContext is a test decorator for a ComponentConfigurer that allows 29 | // tests to see the ClientContext that was passed to it. 30 | type ComponentConfigurerThatCapturesClientContext[T any] struct { 31 | Configurer subsystems.ComponentConfigurer[T] 32 | ReceivedClientContext subsystems.ClientContext 33 | } 34 | 35 | // Build builds the component. 36 | func (c *ComponentConfigurerThatCapturesClientContext[T]) Build(clientContext subsystems.ClientContext) (T, error) { 37 | c.ReceivedClientContext = clientContext 38 | return c.Configurer.Build(clientContext) 39 | } 40 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/mock_data_selector.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 7 | ) 8 | 9 | // MockDataSelector is a mock implementation of the DataSelector interface. 10 | type MockDataSelector struct { 11 | mutex sync.Mutex 12 | selector subsystems.Selector 13 | } 14 | 15 | // NewMockDataSelector creates a new MockDataSelector with the given selector. 16 | func NewMockDataSelector(selector subsystems.Selector) *MockDataSelector { 17 | return &MockDataSelector{ 18 | selector: selector, 19 | } 20 | } 21 | 22 | // Selector returns the selector that this mock data selector holds. 23 | func (m *MockDataSelector) Selector() subsystems.Selector { 24 | m.mutex.Lock() 25 | defer m.mutex.Unlock() 26 | return m.selector 27 | } 28 | 29 | // SetSelector safely updates the selector. 30 | func (m *MockDataSelector) SetSelector(selector subsystems.Selector) { 31 | m.mutex.Lock() 32 | defer m.mutex.Unlock() 33 | m.selector = selector 34 | } 35 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/mock_data_store.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 5 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 6 | ) 7 | 8 | // DataSourceFactoryWithData is a test implementation of ComponentConfigurer that will cause the data 9 | // source to provide a specific set of data when it starts. 10 | type DataSourceFactoryWithData struct { 11 | Data []ldstoretypes.Collection 12 | } 13 | 14 | func (f DataSourceFactoryWithData) Build( //nolint:revive 15 | context subsystems.ClientContext, 16 | ) (subsystems.DataSource, error) { 17 | return &dataSourceWithData{f.Data, context.GetDataSourceUpdateSink(), false}, nil 18 | } 19 | 20 | type dataSourceWithData struct { 21 | data []ldstoretypes.Collection 22 | dataSourceUpdates subsystems.DataSourceUpdateSink 23 | inited bool 24 | } 25 | 26 | func (d *dataSourceWithData) IsInitialized() bool { 27 | return d.inited 28 | } 29 | 30 | func (d *dataSourceWithData) Close() error { 31 | return nil 32 | } 33 | 34 | func (d *dataSourceWithData) Start(closeWhenReady chan<- struct{}) { 35 | d.dataSourceUpdates.Init(d.data) 36 | d.inited = true 37 | close(closeWhenReady) 38 | } 39 | 40 | // DataSourceThatIsAlwaysInitialized returns a test component factory that produces a data source 41 | // that immediately reports success on startup, although it does not provide any data. 42 | func DataSourceThatIsAlwaysInitialized() subsystems.ComponentConfigurer[subsystems.DataSource] { 43 | return SingleComponentConfigurer[subsystems.DataSource]{Instance: mockDataSource{Initialized: true}} 44 | } 45 | 46 | // DataSourceThatNeverInitializes returns a test component factory that produces a data source 47 | // that immediately starts up in a failed state and does not provide any data. 48 | func DataSourceThatNeverInitializes() subsystems.ComponentConfigurer[subsystems.DataSource] { 49 | return SingleComponentConfigurer[subsystems.DataSource]{Instance: mockDataSource{Initialized: false}} 50 | } 51 | 52 | type mockDataSource struct { 53 | Initialized bool 54 | CloseFn func() error 55 | StartFn func(chan<- struct{}) 56 | } 57 | 58 | func (u mockDataSource) IsInitialized() bool { 59 | return u.Initialized 60 | } 61 | 62 | func (u mockDataSource) Close() error { 63 | if u.CloseFn == nil { 64 | return nil 65 | } 66 | return u.CloseFn() 67 | } 68 | 69 | func (u mockDataSource) Start(closeWhenReady chan<- struct{}) { 70 | if u.StartFn == nil { 71 | close(closeWhenReady) 72 | } else { 73 | u.StartFn(closeWhenReady) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/mock_polling_request.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 4 | 5 | // Requester is a mock used in polling_data_source_test.go, to satisfy the 6 | // datasource.Requester interface (used by PollingProcessor). 7 | // Its purpose is to allow the PollingProcessor to be tested without involving actual HTTP operations. 8 | type Requester struct { 9 | RequestAllRespCh chan RequestAllResponse 10 | PollsCh chan struct{} 11 | CloserCh chan struct{} 12 | } 13 | 14 | // RequestAllResponse is used to inject custom responses into the Requester, 15 | // which will subsequently return them to the object under test. 16 | type RequestAllResponse struct { 17 | Data []ldstoretypes.Collection 18 | Cached bool 19 | Err error 20 | } 21 | 22 | // NewPollingRequester constructs a Requester. 23 | func NewPollingRequester() *Requester { 24 | return &Requester{ 25 | RequestAllRespCh: make(chan RequestAllResponse, 100), 26 | PollsCh: make(chan struct{}, 100), 27 | CloserCh: make(chan struct{}), 28 | } 29 | } 30 | 31 | // Close closes the Requester's CloserCh. 32 | func (r *Requester) Close() { 33 | close(r.CloserCh) 34 | } 35 | 36 | // BaseURI exists to fulfil the datasource.Requester interface; here it returns an empty string. 37 | func (r *Requester) BaseURI() string { 38 | return "" 39 | } 40 | 41 | // FilterKey exists to fulfil the datasource.Requester interface; here it returns an empty string. 42 | func (r *Requester) FilterKey() string { 43 | return "" 44 | } 45 | 46 | // Request blocks until a mock request is available on the RequestAllRespCh, or until closing 47 | // via Close(). 48 | func (r *Requester) Request() ([]ldstoretypes.Collection, bool, error) { 49 | select { 50 | case resp := <-r.RequestAllRespCh: 51 | r.PollsCh <- struct{}{} 52 | return resp.Data, resp.Cached, resp.Err 53 | case <-r.CloserCh: 54 | return nil, false, nil 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/mock_status_reporter.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "sync" 5 | "testing" 6 | "time" 7 | 8 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 9 | th "github.com/launchdarkly/go-test-helpers/v3" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | // MockStatusReporter is a mock implementation of DataSourceUpdates for testing data sources. 15 | type MockStatusReporter struct { 16 | Statuses chan interfaces.DataSourceStatus 17 | lastStatus interfaces.DataSourceStatus 18 | lock sync.Mutex 19 | } 20 | 21 | // NewMockStatusReporter creates an instance of MockStatusReporter. 22 | // 23 | // The DataStoreStatusProvider can be nil if we are not doing a test that requires manipulation of that 24 | // component. 25 | func NewMockStatusReporter() *MockStatusReporter { 26 | return &MockStatusReporter{ 27 | Statuses: make(chan interfaces.DataSourceStatus, 10), 28 | } 29 | } 30 | 31 | // UpdateStatus in this test implementation, pushes a value onto the Statuses channel. 32 | func (d *MockStatusReporter) UpdateStatus( 33 | newState interfaces.DataSourceState, 34 | newError interfaces.DataSourceErrorInfo, 35 | ) { 36 | d.lock.Lock() 37 | defer d.lock.Unlock() 38 | if newState != d.lastStatus.State || newError.Kind != "" { 39 | d.lastStatus = interfaces.DataSourceStatus{State: newState, LastError: newError} 40 | d.Statuses <- d.lastStatus 41 | } 42 | } 43 | 44 | // RequireStatusOf blocks until a new data source status is available, and verifies its state. 45 | func (d *MockStatusReporter) RequireStatusOf( 46 | t *testing.T, 47 | newState interfaces.DataSourceState, 48 | ) interfaces.DataSourceStatus { 49 | status := d.RequireStatus(t) 50 | assert.Equal(t, string(newState), string(status.State)) 51 | // string conversion is due to a bug in assert with type aliases 52 | return status 53 | } 54 | 55 | // RequireStatus blocks until a new data source status is available. 56 | func (d *MockStatusReporter) RequireStatus(t *testing.T) interfaces.DataSourceStatus { 57 | return th.RequireValue(t, d.Statuses, time.Second, "timed out waiting for new data source status") 58 | } 59 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/package_info.go: -------------------------------------------------------------------------------- 1 | // Package mocks contains mocks/spies used within SDK unit tests. 2 | package mocks 3 | -------------------------------------------------------------------------------- /internal/sharedtest/mocks/spy_event_processor.go: -------------------------------------------------------------------------------- 1 | package mocks 2 | 3 | import ( 4 | "encoding/json" 5 | "time" 6 | 7 | ldevents "github.com/launchdarkly/go-sdk-events/v3" 8 | ) 9 | 10 | // CapturingEventProcessor is a test implementation of EventProcessor that accumulates all events. 11 | type CapturingEventProcessor struct { 12 | Events []interface{} 13 | } 14 | 15 | func (c *CapturingEventProcessor) RecordEvaluation(e ldevents.EvaluationData) { //nolint:revive 16 | c.Events = append(c.Events, e) 17 | } 18 | 19 | func (c *CapturingEventProcessor) RecordIdentifyEvent(e ldevents.IdentifyEventData) { //nolint:revive 20 | c.Events = append(c.Events, e) 21 | } 22 | 23 | func (c *CapturingEventProcessor) RecordCustomEvent(e ldevents.CustomEventData) { //nolint:revive 24 | c.Events = append(c.Events, e) 25 | } 26 | 27 | func (c *CapturingEventProcessor) RecordMigrationOpEvent(e ldevents.MigrationOpEventData) { //nolint:revive 28 | c.Events = append(c.Events, e) 29 | } 30 | 31 | func (c *CapturingEventProcessor) RecordRawEvent(e json.RawMessage) { //nolint:revive 32 | c.Events = append(c.Events, e) 33 | } 34 | 35 | func (c *CapturingEventProcessor) Flush() {} //nolint:revive 36 | 37 | func (c *CapturingEventProcessor) FlushBlocking(time.Duration) bool { return true } //nolint:revive 38 | 39 | func (c *CapturingEventProcessor) Close() error { //nolint:revive 40 | return nil 41 | } 42 | -------------------------------------------------------------------------------- /internal/sharedtest/package_info.go: -------------------------------------------------------------------------------- 1 | // Package sharedtest contains types and functions used by SDK unit tests in multiple packages. 2 | // 3 | // Since it is inside internal/, none of this code can be seen by application code and it can be freely 4 | // changed without breaking any public APIs. Test helpers that we want to be available to application code 5 | // should be in testhelpers/ instead. 6 | // 7 | // It is important that no non-test code ever imports this package, so that it will not be compiled into 8 | // applications as a transitive dependency. 9 | // 10 | // Note that this package is not allowed to reference the "internal" package, because the tests in that 11 | // package use sharedtest helpers so it would be a circular reference. 12 | package sharedtest 13 | -------------------------------------------------------------------------------- /internal/sharedtest/test_context.go: -------------------------------------------------------------------------------- 1 | package sharedtest 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 5 | ) 6 | 7 | // TestSDKKey is a test SDK key that can be used in test code. 8 | const TestSDKKey = "test-sdk-key" 9 | 10 | // BasicClientContext returns a basic implementation of interfaces.ClientContext for use in test code. 11 | func BasicClientContext() subsystems.ClientContext { 12 | return NewSimpleTestContext(TestSDKKey) 13 | } 14 | 15 | // NewSimpleTestContext returns a basic implementation of interfaces.ClientContext for use in test code. 16 | func NewSimpleTestContext(sdkKey string) subsystems.ClientContext { 17 | return NewTestContext(sdkKey, nil, nil) 18 | } 19 | 20 | // NewTestContext returns a basic implementation of interfaces.ClientContext for use in test code. 21 | // We can't use internal.NewClientContextImpl for this because of circular references. 22 | func NewTestContext( 23 | sdkKey string, 24 | optHTTPConfig *subsystems.HTTPConfiguration, 25 | optLoggingConfig *subsystems.LoggingConfiguration, 26 | ) subsystems.BasicClientContext { 27 | ret := subsystems.BasicClientContext{SDKKey: sdkKey} 28 | if optHTTPConfig != nil { 29 | ret.HTTP = *optHTTPConfig 30 | } 31 | if optLoggingConfig != nil { 32 | ret.Logging = *optLoggingConfig 33 | } else { 34 | ret.Logging = TestLoggingConfig() 35 | } 36 | return ret 37 | } 38 | 39 | // TestLoggingConfig returns a LoggingConfiguration corresponding to NewTestLoggers(). 40 | func TestLoggingConfig() subsystems.LoggingConfiguration { 41 | return subsystems.LoggingConfiguration{Loggers: NewTestLoggers()} 42 | } 43 | -------------------------------------------------------------------------------- /internal/sharedtest/test_log_level.go: -------------------------------------------------------------------------------- 1 | //nolint:gochecknoglobals 2 | package sharedtest 3 | 4 | import ( 5 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 6 | ) 7 | 8 | var testLogLevel = ldlog.None 9 | 10 | // NewTestLoggers returns a standardized logger instance used by unit tests. If you want to temporarily 11 | // enable log output for tests, change testLogLevel to for instance ldlog.Debug. Note that "go test" 12 | // normally suppresses output anyway unless a test fails. 13 | func NewTestLoggers() ldlog.Loggers { 14 | ret := ldlog.NewDefaultLoggers() 15 | ret.SetMinLevel(testLogLevel) 16 | return ret 17 | } 18 | -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | // SDKVersion is the current version string of the SDK. This is updated by our release scripts. 4 | const SDKVersion = "7.10.2" // {{ x-release-please-version }} 5 | -------------------------------------------------------------------------------- /ldai/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 Catamorphic, Co. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /ldai/datamodel/datamodel.go: -------------------------------------------------------------------------------- 1 | package datamodel 2 | 3 | import "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 4 | 5 | // Meta defines the serialization format for config metadata. 6 | type Meta struct { 7 | // VariationKey is the variation key. 8 | VariationKey string `json:"variationKey,omitempty"` 9 | 10 | // Enabled is true if the config is enabled. 11 | Enabled bool `json:"enabled,omitempty"` 12 | 13 | // Version is the version of the Variation. 14 | Version *int `json:"version,omitempty"` 15 | } 16 | 17 | // Model defines the serialization format for a model. 18 | type Model struct { 19 | // Name identifies the model. 20 | Name string `json:"name"` 21 | 22 | // Parameters are the model parameters, generally provided by LaunchDarkly. 23 | Parameters map[string]ldvalue.Value `json:"parameters,omitempty"` 24 | 25 | // Custom are custom model parameters, generally provided by the user. 26 | Custom map[string]ldvalue.Value `json:"custom,omitempty"` 27 | } 28 | 29 | // Provider defines the serialization format for a model provider. 30 | type Provider struct { 31 | // Name identifies the provider. 32 | Name string `json:"name"` 33 | } 34 | 35 | // Role defines the role of a message. 36 | type Role string 37 | 38 | const ( 39 | // User represents the user. 40 | User Role = "user" 41 | 42 | // System represents the system. 43 | System Role = "system" 44 | 45 | // Assistant represents an assistant. 46 | Assistant Role = "assistant" 47 | ) 48 | 49 | // Message defines the serialization format for a message which may be passed to an AI model provider. 50 | type Message struct { 51 | // Content is the message content. 52 | Content string `json:"content"` 53 | 54 | // Role is the role of the message. 55 | Role Role `json:"role"` 56 | } 57 | 58 | // Config defines the serialization format for an AI Config. 59 | type Config struct { 60 | // Messages is a list of messages. The messages received from LaunchDarkly are uninterpolated. 61 | Messages []Message `json:"messages,omitempty"` 62 | 63 | // Meta is the config metadata. 64 | Meta Meta `json:"_ldMeta,omitempty"` 65 | 66 | // Model is the model. 67 | Model Model `json:"model,omitempty"` 68 | 69 | // Provider is the provider. 70 | Provider Provider `json:"provider,omitempty"` 71 | } 72 | -------------------------------------------------------------------------------- /ldai/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/launchdarkly/go-server-sdk/ldai 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/alexkappa/mustache v1.0.0 7 | github.com/launchdarkly/go-sdk-common/v3 v3.3.0 8 | github.com/launchdarkly/go-server-sdk/v7 v7.7.0 9 | github.com/stretchr/testify v1.9.0 10 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa 11 | ) 12 | 13 | require ( 14 | github.com/davecgh/go-spew v1.1.1 // indirect 15 | github.com/google/uuid v1.1.1 // indirect 16 | github.com/josharian/intern v1.0.0 // indirect 17 | github.com/launchdarkly/go-jsonstream/v3 v3.1.0 // indirect 18 | github.com/launchdarkly/go-sdk-events/v3 v3.4.0 // indirect 19 | github.com/mailru/easyjson v0.7.7 // indirect 20 | github.com/pmezard/go-difflib v1.0.0 // indirect 21 | gopkg.in/yaml.v3 v3.0.1 // indirect 22 | ) 23 | -------------------------------------------------------------------------------- /ldai/go.sum: -------------------------------------------------------------------------------- 1 | github.com/alexkappa/mustache v1.0.0 h1:GeF7AKKpKKVq8emIwYRQPsKPDWSGYQGWWSsddge62M4= 2 | github.com/alexkappa/mustache v1.0.0/go.mod h1:6v0WNoCZEQ8K5OZAv82ScIARg2bDqFD+Jl0LWxnApas= 3 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 4 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= 6 | github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 7 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 8 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 9 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 10 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 11 | github.com/launchdarkly/go-jsonstream/v3 v3.1.0 h1:U/7/LplZO72XefBQ+FzHf6o4FwLHVqBE+4V58Ornu/E= 12 | github.com/launchdarkly/go-jsonstream/v3 v3.1.0/go.mod h1:2Pt4BR5AwWgsuVTCcIpB6Os04JFIKWfoA+7faKkZB5E= 13 | github.com/launchdarkly/go-sdk-common/v3 v3.3.0 h1:kkf78wcKX+DOXzNjG29i+py/P+XMIw8/mXS7eEWGQwU= 14 | github.com/launchdarkly/go-sdk-common/v3 v3.3.0/go.mod h1:mXFmDGEh4ydK3QilRhrAyKuf9v44VZQWnINyhqbbOd0= 15 | github.com/launchdarkly/go-sdk-events/v3 v3.4.0 h1:22sVSEDEXpdOEK3UBtmThwsUHqc+cbbe/pJfsliBAA4= 16 | github.com/launchdarkly/go-sdk-events/v3 v3.4.0/go.mod h1:oepYWQ2RvvjfL2WxkE1uJJIuRsIMOP4WIVgUpXRPcNI= 17 | github.com/launchdarkly/go-server-sdk/v7 v7.7.0 h1:UZ1Fn28UiIsINLcxnKEmOvBUgrqR2f4zY4WNPaScL0A= 18 | github.com/launchdarkly/go-server-sdk/v7 v7.7.0/go.mod h1:rf/K2E4s5OjkB8Nn3ATDOR6W6S3U7D8FJ3WAKLxSTIQ= 19 | github.com/launchdarkly/go-test-helpers/v3 v3.0.2 h1:rh0085g1rVJM5qIukdaQ8z1XTWZztbJ49vRZuveqiuU= 20 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 21 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 22 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 23 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 24 | github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= 25 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 26 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 27 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= 28 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= 29 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 30 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 31 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 32 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 33 | -------------------------------------------------------------------------------- /ldai/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldai contains an AI SDK suitable for usage with generative AI applications. 2 | package ldai 3 | 4 | // Version is the current version string of the ldai package. This is updated by our release scripts. 5 | const Version = "0.7.0" // {{ x-release-please-version }} 6 | -------------------------------------------------------------------------------- /ldclient_external_updates_only_test.go: -------------------------------------------------------------------------------- 1 | package ldclient 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" 7 | 8 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 9 | "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" 10 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 11 | "github.com/launchdarkly/go-server-sdk-evaluation/v3/ldbuilders" 12 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 13 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 14 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 15 | "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" 16 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 17 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoreimpl" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | type clientExternalUpdatesTestParams struct { 23 | client *LDClient 24 | store subsystems.DataStore 25 | mockLog *ldlogtest.MockLog 26 | } 27 | 28 | func withClientExternalUpdatesTestParams(callback func(clientExternalUpdatesTestParams)) { 29 | p := clientExternalUpdatesTestParams{} 30 | p.store = datastore.NewInMemoryDataStore(ldlog.NewDisabledLoggers()) 31 | p.mockLog = ldlogtest.NewMockLog() 32 | config := Config{ 33 | DataSource: ldcomponents.ExternalUpdatesOnly(), 34 | DataStore: mocks.SingleComponentConfigurer[subsystems.DataStore]{Instance: p.store}, 35 | Logging: ldcomponents.Logging().Loggers(p.mockLog.Loggers), 36 | } 37 | p.client, _ = MakeCustomClient("sdk_key", config, 0) 38 | defer p.client.Close() 39 | callback(p) 40 | } 41 | 42 | func TestClientExternalUpdatesMode(t *testing.T) { 43 | t.Run("is initialized", func(t *testing.T) { 44 | withClientExternalUpdatesTestParams(func(p clientExternalUpdatesTestParams) { 45 | assert.True(t, p.client.Initialized()) 46 | assert.Equal(t, interfaces.DataSourceStateValid, 47 | p.client.GetDataSourceStatusProvider().GetStatus().State) 48 | }) 49 | }) 50 | 51 | t.Run("reports non-offline status", func(t *testing.T) { 52 | withClientExternalUpdatesTestParams(func(p clientExternalUpdatesTestParams) { 53 | assert.False(t, p.client.IsOffline()) 54 | }) 55 | }) 56 | 57 | t.Run("logs appropriate message at startup", func(t *testing.T) { 58 | withClientExternalUpdatesTestParams(func(p clientExternalUpdatesTestParams) { 59 | assert.Contains( 60 | t, 61 | p.mockLog.GetOutput(ldlog.Info), 62 | "LaunchDarkly client will not connect to Launchdarkly for feature flag data", 63 | ) 64 | }) 65 | }) 66 | 67 | t.Run("uses data from store", func(t *testing.T) { 68 | flag := ldbuilders.NewFlagBuilder("flagkey").SingleVariation(ldvalue.Bool(true)).Build() 69 | 70 | withClientExternalUpdatesTestParams(func(p clientExternalUpdatesTestParams) { 71 | _, _ = p.store.Upsert(ldstoreimpl.Features(), flag.Key, sharedtest.FlagDescriptor(flag)) 72 | result, err := p.client.BoolVariation(flag.Key, evalTestUser, false) 73 | assert.NoError(t, err) 74 | assert.True(t, result) 75 | }) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /ldclient_offline_test.go: -------------------------------------------------------------------------------- 1 | package ldclient 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" 7 | 8 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 9 | "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" 10 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 11 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 12 | "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" 13 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 14 | 15 | "github.com/stretchr/testify/assert" 16 | ) 17 | 18 | type clientOfflineTestParams struct { 19 | client *LDClient 20 | store subsystems.DataStore 21 | mockLog *ldlogtest.MockLog 22 | } 23 | 24 | func withClientOfflineTestParams(callback func(clientExternalUpdatesTestParams)) { 25 | p := clientExternalUpdatesTestParams{} 26 | p.store = datastore.NewInMemoryDataStore(ldlog.NewDisabledLoggers()) 27 | p.mockLog = ldlogtest.NewMockLog() 28 | config := Config{ 29 | Offline: true, 30 | DataStore: mocks.SingleComponentConfigurer[subsystems.DataStore]{Instance: p.store}, 31 | Logging: ldcomponents.Logging().Loggers(p.mockLog.Loggers), 32 | } 33 | p.client, _ = MakeCustomClient("sdk_key", config, 0) 34 | defer p.client.Close() 35 | callback(p) 36 | } 37 | 38 | func TestClientOfflineMode(t *testing.T) { 39 | t.Run("is initialized", func(t *testing.T) { 40 | withClientOfflineTestParams(func(p clientExternalUpdatesTestParams) { 41 | assert.True(t, p.client.Initialized()) 42 | assert.Equal(t, interfaces.DataSourceStateValid, 43 | p.client.GetDataSourceStatusProvider().GetStatus().State) 44 | }) 45 | }) 46 | 47 | t.Run("reports offline status", func(t *testing.T) { 48 | withClientOfflineTestParams(func(p clientExternalUpdatesTestParams) { 49 | assert.True(t, p.client.IsOffline()) 50 | }) 51 | }) 52 | 53 | t.Run("logs appropriate message at startup", func(t *testing.T) { 54 | withClientOfflineTestParams(func(p clientExternalUpdatesTestParams) { 55 | assert.Contains( 56 | t, 57 | p.mockLog.GetOutput(ldlog.Info), 58 | "Starting LaunchDarkly client in offline mode", 59 | ) 60 | }) 61 | }) 62 | 63 | t.Run("returns default values", func(t *testing.T) { 64 | withClientOfflineTestParams(func(p clientExternalUpdatesTestParams) { 65 | result, err := p.client.BoolVariation("flagkey", evalTestUser, false) 66 | assert.NoError(t, err) 67 | assert.False(t, result) 68 | }) 69 | }) 70 | 71 | t.Run("returns invalid state from AllFlagsState", func(t *testing.T) { 72 | withClientOfflineTestParams(func(p clientExternalUpdatesTestParams) { 73 | result := p.client.AllFlagsState(evalTestUser) 74 | assert.False(t, result.IsValid()) 75 | }) 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /ldclient_test_data_source_test.go: -------------------------------------------------------------------------------- 1 | package ldclient 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/launchdarkly/go-sdk-common/v3/ldcontext" 8 | "github.com/launchdarkly/go-server-sdk/v7/ldcomponents" 9 | "github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | // This is a basic smoke test to verify that TestDataSource works correctly inside of an LDClient instance. 16 | // It is in the main package, rather than the testhelpers package, to avoid a circular package reference. 17 | 18 | func TestClientWithTestDataSource(t *testing.T) { 19 | td := ldtestdata.DataSource() 20 | td.Update(td.Flag("flagkey").On(true)) 21 | 22 | config := Config{ 23 | DataSource: td, 24 | Events: ldcomponents.NoEvents(), 25 | } 26 | client, err := MakeCustomClient("", config, time.Second) 27 | require.NoError(t, err) 28 | defer client.Close() 29 | 30 | value, err := client.BoolVariation("flagkey", ldcontext.New("userkey"), false) 31 | require.NoError(t, err) 32 | assert.True(t, value) 33 | 34 | td.Update(td.Flag("flagkey").On(false)) 35 | value, err = client.BoolVariation("flagkey", ldcontext.New("userkey"), false) 36 | require.NoError(t, err) 37 | assert.False(t, value) 38 | } 39 | -------------------------------------------------------------------------------- /ldcomponents/big_segments_configuration_builder_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "errors" 5 | "testing" 6 | "time" 7 | 8 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | type mockBigSegmentStoreFactory struct { 15 | fakeError error 16 | } 17 | 18 | func (m mockBigSegmentStoreFactory) Build(subsystems.ClientContext) (subsystems.BigSegmentStore, error) { 19 | return mockBigSegmentStore{}, m.fakeError 20 | } 21 | 22 | type mockBigSegmentStore struct{} 23 | 24 | func (m mockBigSegmentStore) Close() error { return nil } 25 | 26 | func (m mockBigSegmentStore) GetMetadata() (subsystems.BigSegmentStoreMetadata, error) { 27 | return subsystems.BigSegmentStoreMetadata{}, nil 28 | } 29 | 30 | func (m mockBigSegmentStore) GetMembership(string) (subsystems.BigSegmentMembership, error) { 31 | return nil, nil 32 | } 33 | 34 | func TestBigSegmentsConfigurationBuilder(t *testing.T) { 35 | context := basicClientContext() 36 | 37 | t.Run("defaults", func(t *testing.T) { 38 | c, err := BigSegments(mockBigSegmentStoreFactory{}).Build(context) 39 | require.NoError(t, err) 40 | 41 | assert.Equal(t, mockBigSegmentStore{}, c.GetStore()) 42 | assert.Equal(t, DefaultBigSegmentsContextCacheSize, c.GetContextCacheSize()) 43 | assert.Equal(t, DefaultBigSegmentsContextCacheTime, c.GetContextCacheTime()) 44 | assert.Equal(t, DefaultBigSegmentsStatusPollInterval, c.GetStatusPollInterval()) 45 | assert.Equal(t, DefaultBigSegmentsStaleAfter, c.GetStaleAfter()) 46 | }) 47 | 48 | t.Run("store creation fails", func(t *testing.T) { 49 | fakeError := errors.New("sorry") 50 | storeFactory := mockBigSegmentStoreFactory{fakeError: fakeError} 51 | _, err := BigSegments(storeFactory).Build(context) 52 | require.Equal(t, fakeError, err) 53 | }) 54 | 55 | t.Run("ContextCacheSize", func(t *testing.T) { 56 | c, err := BigSegments(mockBigSegmentStoreFactory{}). 57 | ContextCacheSize(999). 58 | Build(context) 59 | require.NoError(t, err) 60 | assert.Equal(t, 999, c.GetContextCacheSize()) 61 | }) 62 | 63 | t.Run("ContextCacheTime", func(t *testing.T) { 64 | c, err := BigSegments(mockBigSegmentStoreFactory{}). 65 | ContextCacheTime(time.Second * 999). 66 | Build(context) 67 | require.NoError(t, err) 68 | assert.Equal(t, time.Second*999, c.GetContextCacheTime()) 69 | }) 70 | 71 | t.Run("StatusPollInterval", func(t *testing.T) { 72 | c, err := BigSegments(mockBigSegmentStoreFactory{}). 73 | StatusPollInterval(time.Second * 999). 74 | Build(context) 75 | require.NoError(t, err) 76 | assert.Equal(t, time.Second*999, c.GetStatusPollInterval()) 77 | }) 78 | 79 | t.Run("StaleAfter", func(t *testing.T) { 80 | c, err := BigSegments(mockBigSegmentStoreFactory{}). 81 | StaleAfter(time.Second * 999). 82 | Build(context) 83 | require.NoError(t, err) 84 | assert.Equal(t, time.Second*999, c.GetStaleAfter()) 85 | }) 86 | } 87 | -------------------------------------------------------------------------------- /ldcomponents/external_updates_data_source.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 5 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/datasource" 7 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 8 | ) 9 | 10 | type nullDataSourceFactory struct{} 11 | 12 | // ExternalUpdatesOnly returns a configuration object that disables a direct connection with LaunchDarkly 13 | // for feature flag updates. 14 | // 15 | // Storing this in the DataSource field of [github.com/launchdarkly/go-server-sdk/v7.Config] causes the 16 | // SDK not to retrieve feature flag data from LaunchDarkly, regardless of any other configuration. This is 17 | // normally done if you are using the Relay Proxy (https://docs.launchdarkly.com/home/relay-proxy) in 18 | // "daemon mode", where an external process-- the Relay Proxy-- connects to LaunchDarkly and populates a 19 | // persistent data store with the feature flag data. The data store could also be populated by another 20 | // process that is running the LaunchDarkly SDK. If there is no external process updating the data store, 21 | // then the SDK will not have any feature flag data and will return application default values only. 22 | // 23 | // config := ld.Config{ 24 | // DataSource: ldcomponents.ExternalUpdatesOnly(), 25 | // } 26 | func ExternalUpdatesOnly() subsystems.ComponentConfigurer[subsystems.DataSource] { 27 | return nullDataSourceFactory{} 28 | } 29 | 30 | // DataSourceFactory implementation 31 | func (f nullDataSourceFactory) Build( 32 | context subsystems.ClientContext, 33 | ) (subsystems.DataSource, error) { 34 | context.GetLogging().Loggers.Info("LaunchDarkly client will not connect to Launchdarkly for feature flag data") 35 | if context.GetDataSourceUpdateSink() != nil { 36 | context.GetDataSourceUpdateSink().UpdateStatus(interfaces.DataSourceStateValid, interfaces.DataSourceErrorInfo{}) 37 | } 38 | return datasource.NewNullDataSource(), nil 39 | } 40 | 41 | // DiagnosticDescription implementation 42 | func (f nullDataSourceFactory) DescribeConfiguration(context subsystems.ClientContext) ldvalue.Value { 43 | // This information is only used for diagnostic events, and if we're able to send diagnostic events, 44 | // then by definition we're not completely offline so we must be using daemon mode. 45 | return ldvalue.ObjectBuild(). 46 | SetBool("usingRelayDaemon", true). 47 | Build() 48 | } 49 | -------------------------------------------------------------------------------- /ldcomponents/external_updates_data_source_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest/mocks" 7 | 8 | "github.com/stretchr/testify/assert" 9 | "github.com/stretchr/testify/require" 10 | 11 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 12 | "github.com/launchdarkly/go-server-sdk/v7/internal/datasource" 13 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 14 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 15 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 16 | ) 17 | 18 | func TestExternalUpdatesOnly(t *testing.T) { 19 | dsu := mocks.NewMockDataSourceUpdates(datastore.NewInMemoryDataStore(sharedtest.NewTestLoggers())) 20 | context := subsystems.BasicClientContext{DataSourceUpdateSink: dsu} 21 | ds, err := ExternalUpdatesOnly().Build(context) 22 | require.NoError(t, err) 23 | defer ds.Close() 24 | 25 | assert.Equal(t, datasource.NewNullDataSource(), ds) 26 | assert.True(t, ds.IsInitialized()) 27 | 28 | dsu.RequireStatusOf(t, interfaces.DataSourceStateValid) 29 | } 30 | -------------------------------------------------------------------------------- /ldcomponents/in_memory_data_store.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 5 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 7 | ) 8 | 9 | type inMemoryDataStoreFactory struct{} 10 | 11 | func (f inMemoryDataStoreFactory) Build(context subsystems.ClientContext) (subsystems.DataStore, error) { 12 | loggers := context.GetLogging().Loggers 13 | loggers.SetPrefix("InMemoryDataStore:") 14 | return datastore.NewInMemoryDataStore(loggers), nil 15 | } 16 | 17 | // DiagnosticDescription implementation 18 | func (f inMemoryDataStoreFactory) DescribeConfiguration(context subsystems.ClientContext) ldvalue.Value { 19 | return ldvalue.String("memory") 20 | } 21 | 22 | // InMemoryDataStore returns the default in-memory DataStore implementation factory. 23 | func InMemoryDataStore() subsystems.ComponentConfigurer[subsystems.DataStore] { 24 | return inMemoryDataStoreFactory{} 25 | } 26 | -------------------------------------------------------------------------------- /ldcomponents/in_memory_data_store_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 7 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestInMemoryDataStoreFactory(t *testing.T) { 14 | factory := InMemoryDataStore() 15 | store, err := factory.Build(basicClientContext()) 16 | require.NoError(t, err) 17 | require.NotNil(t, store) 18 | assert.IsType(t, datastore.NewInMemoryDataStore(sharedtest.NewTestLoggers()), store) 19 | } 20 | -------------------------------------------------------------------------------- /ldcomponents/logging_configuration_builder_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 8 | "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" 9 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | func TestLoggingConfigurationBuilder(t *testing.T) { 15 | basicConfig := subsystems.BasicClientContext{} 16 | 17 | t.Run("defaults", func(t *testing.T) { 18 | c, err := Logging().Build(basicConfig) 19 | assert.Nil(t, err) 20 | assert.False(t, c.LogEvaluationErrors) 21 | assert.False(t, c.LogContextKeyInErrors) 22 | }) 23 | 24 | t.Run("LogDataSourceOutageAsErrorAfter", func(t *testing.T) { 25 | c, err := Logging().LogDataSourceOutageAsErrorAfter(time.Hour).Build(basicConfig) 26 | assert.Nil(t, err) 27 | assert.Equal(t, time.Hour, c.LogDataSourceOutageAsErrorAfter) 28 | }) 29 | 30 | t.Run("LogEvaluationErrors", func(t *testing.T) { 31 | c, err := Logging().LogEvaluationErrors(true).Build(basicConfig) 32 | assert.Nil(t, err) 33 | assert.True(t, c.LogEvaluationErrors) 34 | }) 35 | 36 | t.Run("LogContextKeyInErrors", func(t *testing.T) { 37 | c, err := Logging().LogContextKeyInErrors(true).Build(basicConfig) 38 | assert.Nil(t, err) 39 | assert.True(t, c.LogContextKeyInErrors) 40 | }) 41 | 42 | t.Run("Loggers", func(t *testing.T) { 43 | mockLoggers := ldlogtest.NewMockLog() 44 | c, err := Logging().Loggers(mockLoggers.Loggers).Build(basicConfig) 45 | assert.Nil(t, err) 46 | assert.Equal(t, mockLoggers.Loggers, c.Loggers) 47 | }) 48 | 49 | t.Run("MinLevel", func(t *testing.T) { 50 | mockLoggers := ldlogtest.NewMockLog() 51 | c, err := Logging().Loggers(mockLoggers.Loggers).MinLevel(ldlog.Error).Build(basicConfig) 52 | assert.Nil(t, err) 53 | c.Loggers.Info("suppress this message") 54 | c.Loggers.Error("log this message") 55 | assert.Len(t, mockLoggers.GetOutput(ldlog.Info), 0) 56 | assert.Equal(t, []string{"log this message"}, mockLoggers.GetOutput(ldlog.Error)) 57 | }) 58 | 59 | t.Run("NoLogging", func(t *testing.T) { 60 | c, err := NoLogging().Build(basicConfig) 61 | assert.Nil(t, err) 62 | assert.Equal(t, ldlog.NewDisabledLoggers(), c.Loggers) 63 | }) 64 | 65 | t.Run("nil safety", func(t *testing.T) { 66 | var b *LoggingConfigurationBuilder = nil 67 | b = b.LogContextKeyInErrors(true).LogDataSourceOutageAsErrorAfter(0).LogEvaluationErrors(true). 68 | Loggers(ldlog.NewDefaultLoggers()).MinLevel(ldlog.Debug) 69 | _, _ = b.Build(subsystems.BasicClientContext{}) 70 | }) 71 | } 72 | -------------------------------------------------------------------------------- /ldcomponents/no_events.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | ldevents "github.com/launchdarkly/go-sdk-events/v3" 5 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 6 | ) 7 | 8 | type nullEventProcessorFactory struct{} 9 | 10 | // NoEvents returns a configuration object that disables analytics events. 11 | // 12 | // Storing this in the Events field of [github.com/launchdarkly/go-server-sdk/v7.Config] causes the 13 | // SDK to discard all analytics events and not send them to LaunchDarkly, regardless of any other configuration. 14 | // 15 | // config := ld.Config{ 16 | // Events: ldcomponents.NoEvents(), 17 | // } 18 | func NoEvents() subsystems.ComponentConfigurer[ldevents.EventProcessor] { 19 | return nullEventProcessorFactory{} 20 | } 21 | 22 | func (f nullEventProcessorFactory) Build( 23 | context subsystems.ClientContext, 24 | ) (ldevents.EventProcessor, error) { 25 | return ldevents.NewNullEventProcessor(), nil 26 | } 27 | 28 | // This method implements a hidden interface in ldclient_events.go, as a hint to the SDK that this is 29 | // the stub implementation of EventProcessorFactory and therefore LDClient does not need to bother 30 | // generating events at all. 31 | func (f nullEventProcessorFactory) IsNullEventProcessorFactory() bool { 32 | return true 33 | } 34 | -------------------------------------------------------------------------------- /ldcomponents/no_events_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/require" 7 | 8 | "github.com/launchdarkly/go-sdk-common/v3/lduser" 9 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 10 | ldevents "github.com/launchdarkly/go-sdk-events/v3" 11 | ) 12 | 13 | func TestNoEvents(t *testing.T) { 14 | ep, err := NoEvents().Build(basicClientContext()) 15 | require.NoError(t, err) 16 | defer ep.Close() 17 | ef := ldevents.NewEventFactory(false, nil) 18 | ep.RecordIdentifyEvent(ef.NewIdentifyEventData(ldevents.Context(lduser.NewUser("key")), ldvalue.OptionalInt{})) 19 | ep.Flush() 20 | } 21 | -------------------------------------------------------------------------------- /ldcomponents/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldcomponents provides the standard implementations and configuration options of LaunchDarkly components. 2 | // 3 | // Some configuration options are represented as fields in the main Config struct; but others are specific to one 4 | // area of functionality, such as how the SDK receives feature flag updates or processes analytics events. For the 5 | // latter, the standard way to specify a configuration is to call one of the functions in ldcomponents (such as 6 | // [StreamingDataSource]), apply any desired configuration change to the object that that method returns (such as 7 | // StreamingDataSourceBuilder.InitialReconnectDelay), and then put the configured component builder into the 8 | // corresponding Config field (such as Config.DataSource) to use that configuration in the SDK. 9 | package ldcomponents 10 | -------------------------------------------------------------------------------- /ldcomponents/service_endpoints.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import "github.com/launchdarkly/go-server-sdk/v7/interfaces" 4 | 5 | // RelayProxyEndpoints specifies a single base URI for a Relay Proxy instance, telling the SDK to 6 | // use the Relay Proxy for all services. 7 | // 8 | // When using the LaunchDarkly Relay Proxy (https://docs.launchdarkly.com/home/relay-proxy), the SDK 9 | // only needs to know the single base URI of the Relay Proxy, which will provide all of the proxied 10 | // service endpoints. 11 | // 12 | // Store this value in the ServiceEndpoints field of [github.com/launchdarkly/go-server-sdk/v7.Config]. 13 | // For example: 14 | // 15 | // relayURI := "http://my-relay-hostname:8080" 16 | // config := ld.Config{ 17 | // ServiceEndpoints: ldcomponents.RelayProxyEndpoints(relayURI), 18 | // } 19 | // 20 | // If analytics events are enabled, this will also cause the SDK to forward events through the 21 | // Relay Proxy. If you have not enabled event forwarding in your Relay Proxy configuration and you 22 | // want the SDK to send events directly to LaunchDarkly instead, use [RelayProxyEndpointsWithoutEvents]. 23 | // 24 | // See Config.ServiceEndpoints for more details. 25 | func RelayProxyEndpoints(relayProxyBaseURI string) interfaces.ServiceEndpoints { 26 | return interfaces.ServiceEndpoints{ 27 | Streaming: relayProxyBaseURI, 28 | Polling: relayProxyBaseURI, 29 | Events: relayProxyBaseURI, 30 | } 31 | } 32 | 33 | // RelayProxyEndpointsWithoutEvents specifies a single base URI for a Relay Proxy instance, telling 34 | // the SDK to use the Relay Proxy for all services except analytics events. Note that this does not disable events, it 35 | // instead means events (if enabled) will be sent directly to LaunchDarkly. 36 | // 37 | // When using the LaunchDarkly Relay Proxy (https://docs.launchdarkly.com/home/relay-proxy), the SDK 38 | // only needs to know the single base URI of the Relay Proxy, which will provide all of the proxied 39 | // service endpoints. 40 | // 41 | // Store this value in the ServiceEndpoints field of your SDK configuration. For example: 42 | // 43 | // relayURI := "http://my-relay-hostname:8080" 44 | // config := ld.Config{ 45 | // ServiceEndpoints: ldcomponents.RelayProxyEndpointsWithoutEvents(relayURI), 46 | // } 47 | // 48 | // If you do want events to be forwarded through the Relay Proxy, use [RelayProxyEndpoints] instead. 49 | // 50 | // See Config.ServiceEndpoints for more details. 51 | func RelayProxyEndpointsWithoutEvents(relayProxyBaseURI string) interfaces.ServiceEndpoints { 52 | return interfaces.ServiceEndpoints{ 53 | Streaming: relayProxyBaseURI, 54 | Polling: relayProxyBaseURI, 55 | }.WithPartialSpecification() 56 | } 57 | -------------------------------------------------------------------------------- /ldcomponents/service_endpoints_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestRelayProxyEndpoints(t *testing.T) { 10 | uri := "http://relay:8080" 11 | e := RelayProxyEndpoints(uri) 12 | assert.Equal(t, uri, e.Streaming) 13 | assert.Equal(t, uri, e.Polling) 14 | assert.Equal(t, uri, e.Events) 15 | } 16 | 17 | func TestRelayProxyEndpointsWithoutEvents(t *testing.T) { 18 | uri := "http://relay:8080" 19 | e := RelayProxyEndpointsWithoutEvents(uri) 20 | assert.Equal(t, uri, e.Streaming) 21 | assert.Equal(t, uri, e.Polling) 22 | assert.Equal(t, "", e.Events) 23 | } 24 | -------------------------------------------------------------------------------- /ldcomponents/test_helpers_test.go: -------------------------------------------------------------------------------- 1 | package ldcomponents 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/internal" 5 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 7 | ) 8 | 9 | const testSdkKey = "test-sdk-key" 10 | 11 | func basicClientContext() subsystems.ClientContext { 12 | return sharedtest.NewSimpleTestContext(testSdkKey) 13 | } 14 | 15 | // Returns a basic context where all of the service endpoints point to the specified URI. 16 | func makeTestContextWithBaseURIs(uri string) *internal.ClientContextImpl { 17 | return &internal.ClientContextImpl{ 18 | BasicClientContext: subsystems.BasicClientContext{ 19 | SDKKey: testSdkKey, 20 | Logging: sharedtest.TestLoggingConfig(), 21 | ServiceEndpoints: RelayProxyEndpoints(uri), 22 | }, 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ldfilewatch/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldfilewatch allows the LaunchDarkly client to read feature flag data from a file 2 | // that will be automatically reloaded if the file changes. 3 | // 4 | // It should be used in conjunction with the [github.com/launchdarkly/go-server-sdk/v7/ldfiledata] 5 | // package: 6 | // 7 | // config := ld.Config{ 8 | // DataSource: ldfiledata.DataSource(). 9 | // FilePaths(filePaths). 10 | // Reloader(ldfilewatch.WatchFiles), 11 | // } 12 | // 13 | // The two packages are separate so as to avoid bringing additional dependencies for users who 14 | // do not need automatic reloading. 15 | package ldfilewatch 16 | -------------------------------------------------------------------------------- /ldhooks/evaluation_series_context.go: -------------------------------------------------------------------------------- 1 | package ldhooks 2 | 3 | import ( 4 | "github.com/launchdarkly/go-sdk-common/v3/ldcontext" 5 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 6 | ) 7 | 8 | // EvaluationSeriesContext contains contextual information for the execution of stages in the evaluation series. 9 | type EvaluationSeriesContext struct { 10 | flagKey string 11 | context ldcontext.Context 12 | defaultValue ldvalue.Value 13 | method string 14 | } 15 | 16 | // NewEvaluationSeriesContext create a new EvaluationSeriesContext. Hook implementations do not need to use this 17 | // function. 18 | func NewEvaluationSeriesContext(flagKey string, evalContext ldcontext.Context, 19 | defaultValue ldvalue.Value, method string) EvaluationSeriesContext { 20 | return EvaluationSeriesContext{ 21 | flagKey: flagKey, 22 | context: evalContext, 23 | defaultValue: defaultValue, 24 | method: method, 25 | } 26 | } 27 | 28 | // FlagKey gets the key of the flag being evaluated. 29 | func (c EvaluationSeriesContext) FlagKey() string { 30 | return c.flagKey 31 | } 32 | 33 | // Context gets the evaluation context the flag is being evaluated for. 34 | func (c EvaluationSeriesContext) Context() ldcontext.Context { 35 | return c.context 36 | } 37 | 38 | // DefaultValue gets the default value for the evaluation. 39 | func (c EvaluationSeriesContext) DefaultValue() ldvalue.Value { 40 | return c.defaultValue 41 | } 42 | 43 | // Method gets a string represent of the LDClient method being executed. 44 | func (c EvaluationSeriesContext) Method() string { 45 | return c.method 46 | } 47 | -------------------------------------------------------------------------------- /ldhooks/evaluation_series_data.go: -------------------------------------------------------------------------------- 1 | package ldhooks 2 | 3 | // EvaluationSeriesData is an immutable data type used for passing implementation-specific data between stages in the 4 | // evaluation series. 5 | type EvaluationSeriesData struct { 6 | data map[string]any 7 | } 8 | 9 | // EvaluationSeriesDataBuilder should be used by hook implementers to append data 10 | type EvaluationSeriesDataBuilder struct { 11 | data map[string]any 12 | } 13 | 14 | // EmptyEvaluationSeriesData returns empty series data. This function is not intended for use by hook implementors. 15 | // Hook implementations should always use NewEvaluationSeriesBuilder. 16 | func EmptyEvaluationSeriesData() EvaluationSeriesData { 17 | return EvaluationSeriesData{ 18 | data: make(map[string]any), 19 | } 20 | } 21 | 22 | // Get gets the value associated with the given key. If there is no value, then ok will be false. 23 | func (b EvaluationSeriesData) Get(key string) (value any, ok bool) { 24 | val, ok := b.data[key] 25 | return val, ok 26 | } 27 | 28 | // AsAnyMap returns a copy of the contents of the series data as a map. 29 | func (b EvaluationSeriesData) AsAnyMap() map[string]any { 30 | ret := make(map[string]any) 31 | for key, value := range b.data { 32 | ret[key] = value 33 | } 34 | return ret 35 | } 36 | 37 | // NewEvaluationSeriesBuilder creates an EvaluationSeriesDataBuilder based on the provided EvaluationSeriesData. 38 | // 39 | // func(h MyHook) BeforeEvaluation(seriesContext EvaluationSeriesContext, 40 | // data EvaluationSeriesData) EvaluationSeriesData { 41 | // // Some hook functionality. 42 | // return NewEvaluationSeriesBuilder(data).Set("my-key", myValue).Build() 43 | // } 44 | func NewEvaluationSeriesBuilder(data EvaluationSeriesData) *EvaluationSeriesDataBuilder { 45 | newData := make(map[string]any, len(data.data)) 46 | for k, v := range data.data { 47 | newData[k] = v 48 | } 49 | return &EvaluationSeriesDataBuilder{ 50 | data: newData, 51 | } 52 | } 53 | 54 | // Set sets the given key to the given value. 55 | func (b *EvaluationSeriesDataBuilder) Set(key string, value any) *EvaluationSeriesDataBuilder { 56 | b.data[key] = value 57 | return b 58 | } 59 | 60 | // Merge copies the keys and values from the given map to the builder. 61 | func (b *EvaluationSeriesDataBuilder) Merge(newValues map[string]any) *EvaluationSeriesDataBuilder { 62 | for k, v := range newValues { 63 | b.data[k] = v 64 | } 65 | return b 66 | } 67 | 68 | // Build builds an EvaluationSeriesData based on the contents of the builder. 69 | func (b *EvaluationSeriesDataBuilder) Build() EvaluationSeriesData { 70 | newData := make(map[string]any, len(b.data)) 71 | for k, v := range b.data { 72 | newData[k] = v 73 | } 74 | return EvaluationSeriesData{ 75 | data: newData, 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ldhooks/evaluation_series_data_test.go: -------------------------------------------------------------------------------- 1 | package ldhooks 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestCanCreateEmptyData(t *testing.T) { 10 | empty := EmptyEvaluationSeriesData() 11 | assert.Empty(t, empty.data) 12 | } 13 | 14 | func TestCanCreateNewDataWithSetFields(t *testing.T) { 15 | withEntries := NewEvaluationSeriesBuilder(EmptyEvaluationSeriesData()). 16 | Set("A", "a"). 17 | Set("B", "b").Build() 18 | 19 | assert.Len(t, withEntries.data, 2) 20 | 21 | aVal, aPresent := withEntries.Get("A") 22 | assert.True(t, aPresent) 23 | assert.Equal(t, "a", aVal) 24 | 25 | bVal, bPresent := withEntries.Get("B") 26 | assert.True(t, bPresent) 27 | assert.Equal(t, "b", bVal) 28 | } 29 | 30 | func TestCanAccessAMissingEntry(t *testing.T) { 31 | empty := EmptyEvaluationSeriesData() 32 | val, present := empty.Get("something") 33 | assert.Zero(t, val) 34 | assert.False(t, present) 35 | } 36 | 37 | func TestDataBuiltFromOtherDataDoesNotAffectOriginal(t *testing.T) { 38 | original := NewEvaluationSeriesBuilder(EmptyEvaluationSeriesData()). 39 | Set("A", "a"). 40 | Set("B", "b").Build() 41 | 42 | derivative := NewEvaluationSeriesBuilder(original). 43 | Set("A", "AAA").Build() 44 | 45 | originalA, _ := original.Get("A") 46 | assert.Equal(t, "a", originalA) 47 | 48 | derivativeA, _ := derivative.Get("A") 49 | assert.Equal(t, "AAA", derivativeA) 50 | } 51 | 52 | func TestCanMergeDataFromMap(t *testing.T) { 53 | original := NewEvaluationSeriesBuilder(EmptyEvaluationSeriesData()). 54 | Set("A", "a"). 55 | Set("B", "b").Build() 56 | 57 | merged := NewEvaluationSeriesBuilder(original). 58 | Merge(map[string]any{ 59 | "A": "AAA", 60 | "C": "c", 61 | }).Build() 62 | 63 | originalA, _ := original.Get("A") 64 | assert.Equal(t, "a", originalA) 65 | 66 | originalC, originalCPresent := original.Get("C") 67 | assert.Zero(t, originalC) 68 | assert.False(t, originalCPresent) 69 | 70 | derivativeA, _ := merged.Get("A") 71 | assert.Equal(t, "AAA", derivativeA) 72 | 73 | derivativeC, _ := merged.Get("C") 74 | assert.Equal(t, "c", derivativeC) 75 | } 76 | -------------------------------------------------------------------------------- /ldhooks/metadata.go: -------------------------------------------------------------------------------- 1 | package ldhooks 2 | 3 | // Metadata contains information about a specific hook implementation. 4 | type Metadata struct { 5 | name string 6 | } 7 | 8 | // HookMetadataOption represents a functional means of setting additional, optional, attributes of the Metadata. 9 | type HookMetadataOption func(hook *Metadata) 10 | 11 | // Implementation note: Currently the hook metadata only contains a name, but it may contain additional, and likely 12 | // optional, fields in the future. The HookMetadataOption will allow for additional options to be added without 13 | // breaking callsites. 14 | // 15 | // Example: 16 | // NewMetadata("my-hook", WithVendorName("LaunchDarkly")) 17 | // 18 | 19 | // NewMetadata creates Metadata with the provided name. 20 | func NewMetadata(name string, opts ...HookMetadataOption) Metadata { 21 | metadata := Metadata{ 22 | name: name, 23 | } 24 | for _, opt := range opts { 25 | opt(&metadata) 26 | } 27 | return metadata 28 | } 29 | 30 | // Name gets the name of the hook implementation. 31 | func (m Metadata) Name() string { 32 | return m.name 33 | } 34 | -------------------------------------------------------------------------------- /ldhooks/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldhooks allows for writing extensions to the LaunchDarkly client functionality for purposes such as 2 | // telemetry. 3 | // 4 | // A developer does not need to use this package in the typical operation of the LaunchDarkly SDK. 5 | package ldhooks 6 | -------------------------------------------------------------------------------- /ldntlm/ntlm_proxy.go: -------------------------------------------------------------------------------- 1 | // Package ldntlm allows you to configure the SDK to connect to LaunchDarkly through a proxy server that 2 | // uses NTLM authentication. The standard Go HTTP client proxy mechanism does not support this. The 3 | // implementation uses this package: github.com/launchdarkly/go-ntlm-proxy-auth 4 | // 5 | // See NewNTLMProxyHTTPClientFactory for more details. 6 | package ldntlm 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "net/http" 12 | "net/url" 13 | 14 | ntlm "github.com/launchdarkly/go-ntlm-proxy-auth" 15 | 16 | "github.com/launchdarkly/go-server-sdk/v7/ldhttp" 17 | ) 18 | 19 | // NewNTLMProxyHTTPClientFactory returns a factory function for creating an HTTP client that will 20 | // connect through an NTLM-authenticated proxy server. 21 | // 22 | // To use this with the SDK, pass the factory function to HTTPConfigurationBuilder.HTTPClientFactory: 23 | // 24 | // clientFactory, err := ldntlm.NewNTLMProxyHTTPClientFactory("http://my-proxy.com", "username", 25 | // "password", "domain") 26 | // if err != nil { 27 | // // there's some configuration problem such as an invalid proxy URL 28 | // } 29 | // config := ld.Config{ 30 | // HTTP: ldcomponents.HTTPConfiguration().HTTPClientFactory(clientFactory), 31 | // } 32 | // client, err := ld.MakeCustomClient("sdk-key", config, 5*time.Second) 33 | // 34 | // You can also specify TLS configuration options from the ldhttp package, if you are connecting to 35 | // the proxy securely: 36 | // 37 | // clientFactory, err := ldntlm.NewNTLMProxyHTTPClientFactory("http://my-proxy.com", "username", 38 | // "password", "domain", ldhttp.CACertFileOption("extra-ca-cert.pem")) 39 | func NewNTLMProxyHTTPClientFactory(proxyURL, username, password, domain string, 40 | options ...ldhttp.TransportOption) (func() *http.Client, error) { 41 | if proxyURL == "" || username == "" || password == "" { 42 | return nil, errors.New("ProxyURL, username, and password are required") 43 | } 44 | parsedProxyURL, err := url.Parse(proxyURL) 45 | if err != nil { 46 | return nil, fmt.Errorf("invalid proxy URL %s: %s", proxyURL, err) 47 | } 48 | // Try creating a transport with these options just to make sure it's valid before we get any farther 49 | if _, _, err := ldhttp.NewHTTPTransport(options...); err != nil { 50 | return nil, err 51 | } 52 | return func() *http.Client { 53 | client := *http.DefaultClient 54 | if transport, dialer, err := ldhttp.NewHTTPTransport(options...); err == nil { 55 | transport.DialContext = ntlm.NewNTLMProxyDialContext(dialer, *parsedProxyURL, 56 | username, password, domain, transport.TLSClientConfig) 57 | client.Transport = transport 58 | } 59 | return &client 60 | }, nil 61 | } 62 | -------------------------------------------------------------------------------- /ldotel/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.1.0](https://github.com/launchdarkly/go-server-sdk/compare/ldotel/v1.0.2...ldotel/v1.1.0) (2025-01-23) 4 | 5 | 6 | ### Features 7 | 8 | * Update minimum go version to 1.20. ([46c9694](https://github.com/launchdarkly/go-server-sdk/commit/46c9694e733356cf4d051e7b72241b0a6e330a37)) 9 | 10 | ## [1.0.2](https://github.com/launchdarkly/go-server-sdk/compare/ldotel/v1.0.1...ldotel/v1.0.2) (2024-08-28) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * **ldotel:** bump go-server-sdk to v7.6.2 ([#183](https://github.com/launchdarkly/go-server-sdk/issues/183)) ([cdd19cb](https://github.com/launchdarkly/go-server-sdk/commit/cdd19cb8a31e20e2e400d12c686451f0e66403ce)) 16 | 17 | ## [1.0.1](https://github.com/launchdarkly/go-server-sdk/compare/ldotel/v1.0.0...ldotel/v1.0.1) (2024-08-20) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * add support for testing minimum Go version ([c0707a3](https://github.com/launchdarkly/go-server-sdk/commit/c0707a3c9eaab854815062f0d817b97b2b654edd)) 23 | 24 | ## 1.0.0 (2024-04-11) 25 | 26 | 27 | ### Features 28 | 29 | * Implement otel tracing hook. ([#130](https://github.com/launchdarkly/go-server-sdk/issues/130)) ([f5675c1](https://github.com/launchdarkly/go-server-sdk/commit/f5675c1d20976dc1f9dbb4064ab648abfd7765c2)) 30 | * Update to use go-server-sdk 7.4.0 ([#147](https://github.com/launchdarkly/go-server-sdk/issues/147)) ([0603a9a](https://github.com/launchdarkly/go-server-sdk/commit/0603a9a0bc2189a3938f988068d626e52fe76c99)) 31 | -------------------------------------------------------------------------------- /ldotel/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2024 Catamorphic, Co. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /ldotel/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/launchdarkly/go-server-sdk/ldotel 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/launchdarkly/go-sdk-common/v3 v3.1.0 7 | github.com/launchdarkly/go-server-sdk/v7 v7.6.2 8 | github.com/stretchr/testify v1.9.0 9 | go.opentelemetry.io/otel v1.24.0 10 | go.opentelemetry.io/otel/sdk v1.24.0 11 | go.opentelemetry.io/otel/trace v1.24.0 12 | ) 13 | 14 | require ( 15 | github.com/davecgh/go-spew v1.1.1 // indirect 16 | github.com/go-logr/logr v1.4.1 // indirect 17 | github.com/go-logr/stdr v1.2.2 // indirect 18 | github.com/google/uuid v1.1.1 // indirect 19 | github.com/gregjones/httpcache v0.0.0-20171119193500-2bcd89a1743f // indirect 20 | github.com/josharian/intern v1.0.0 // indirect 21 | github.com/launchdarkly/ccache v1.1.0 // indirect 22 | github.com/launchdarkly/eventsource v1.6.2 // indirect 23 | github.com/launchdarkly/go-jsonstream/v3 v3.1.0 // indirect 24 | github.com/launchdarkly/go-sdk-events/v3 v3.4.0 // indirect 25 | github.com/launchdarkly/go-semver v1.0.3 // indirect 26 | github.com/launchdarkly/go-server-sdk-evaluation/v3 v3.0.1 // indirect 27 | github.com/mailru/easyjson v0.7.7 // indirect 28 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 29 | github.com/pmezard/go-difflib v1.0.0 // indirect 30 | go.opentelemetry.io/otel/metric v1.24.0 // indirect 31 | golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa // indirect 32 | golang.org/x/sync v0.8.0 // indirect 33 | golang.org/x/sys v0.17.0 // indirect 34 | gopkg.in/yaml.v3 v3.0.1 // indirect 35 | ) 36 | -------------------------------------------------------------------------------- /ldotel/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldotel contains OpenTelemetry specific implementations of hooks. 2 | // 3 | // For instance, to use LaunchDarkly with OpenTelemetry tracing, one would use the TracingHook: 4 | // 5 | // client, _ = ld.MakeCustomClient("sdk-key", ld.Config{ 6 | // Hooks: []ldhooks.Hook{ldotel.NewTracingHook()}, 7 | // }, 5*time.Second) 8 | package ldotel 9 | 10 | // Version is the current version string of the ldotel package. This is updated by our release scripts. 11 | const Version = "1.1.0" // {{ x-release-please-version }} 12 | -------------------------------------------------------------------------------- /package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldclient is the main package for the LaunchDarkly SDK. 2 | // 3 | // This package contains the types and methods for the SDK client ([LDClient]) and its overall 4 | // configuration ([Config]). 5 | // 6 | // Subpackages in the same repository provide additional functionality for specific features of the 7 | // client. Most applications that need to change any configuration settings will use the package 8 | // [github.com/launchdarkly/go-server-sdk/v7/ldcomponents]. 9 | // 10 | // The SDK also uses types from the go-sdk-common repository and its subpackages 11 | // ([github.com/launchdarkly/go-sdk-common/v3) that represent standard data structures 12 | // in the LaunchDarkly model. All applications that evaluate feature flags will use the ldcontext 13 | // package ([github.com/launchdarkly/go-sdk-common/v3/ldcontext]); for some features such 14 | // as custom attributes with complex data types, the ldvalue package is also helpful 15 | // ([github.com/launchdarkly/go-sdk-common/v3/ldvalue]). 16 | // 17 | // For more information and code examples, see the Go SDK Reference: 18 | // https://docs.launchdarkly.com/sdk/server-side/go 19 | package ldclient 20 | -------------------------------------------------------------------------------- /proxytest/http_transport_proxy_test.go: -------------------------------------------------------------------------------- 1 | //go:build proxytest1 2 | // +build proxytest1 3 | 4 | // Note, the tests in this package must be run one at a time in separate "go test" invocations, because 5 | // (depending on the platform) Go may cache the value of HTTP_PROXY. Therefore, we have a separate build 6 | // tag for each test and the Makefile runs this package once for each tag. 7 | 8 | package proxytest 9 | 10 | import ( 11 | "net/http" 12 | "net/http/httptest" 13 | "os" 14 | "testing" 15 | 16 | "github.com/stretchr/testify/assert" 17 | "github.com/stretchr/testify/require" 18 | 19 | "github.com/launchdarkly/go-test-helpers/v3/httphelpers" 20 | 21 | "github.com/launchdarkly/go-server-sdk/v7/ldhttp" 22 | ) 23 | 24 | func TestDefaultTransportUsesProxyEnvVars(t *testing.T) { 25 | oldHttpProxy := os.Getenv("HTTP_PROXY") 26 | defer os.Setenv("HTTP_PROXY", oldHttpProxy) 27 | 28 | targetURL := "http://badhost/url" 29 | 30 | // Create an extremely minimal fake proxy server that doesn't actually do any proxying, just to 31 | // verify that we are connecting to it. If the HTTP_PROXY setting is ignored, then it will try 32 | // to connect directly to the nonexistent host "badhost" instead and get an error. 33 | handler, requestsCh := httphelpers.RecordingHandler(httphelpers.HandlerWithStatus(200)) 34 | httphelpers.WithServer(handler, func(proxy *httptest.Server) { 35 | // Note that in normal usage, we will be connecting to secure LaunchDarkly endpoints, so it's 36 | // really HTTPS_PROXY that is relevant. But support for HTTP_PROXY and HTTPS_PROXY comes from the 37 | // same mechanism, so it's simpler to just test against an insecure proxy. 38 | os.Setenv("HTTP_PROXY", proxy.URL) 39 | 40 | transport, _, err := ldhttp.NewHTTPTransport() 41 | require.NoError(t, err) 42 | 43 | client := *http.DefaultClient 44 | client.Transport = transport 45 | resp, err := client.Get(targetURL) 46 | require.NoError(t, err) 47 | 48 | assert.Equal(t, 200, resp.StatusCode) 49 | assert.Equal(t, 1, len(requestsCh)) 50 | r := <-requestsCh 51 | assert.Equal(t, targetURL, r.Request.URL.String()) 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /release-please-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "separate-pull-requests": true, 3 | "include-component-in-tag" : true, 4 | "packages" : { 5 | "." : { 6 | "release-type" : "go", 7 | "bump-minor-pre-major" : true, 8 | "versioning" : "default", 9 | "include-component-in-tag" : false, 10 | "bootstrap-sha" : "25fcfa1ba34928c6dc6d1ee29f4504b7614fa55c", 11 | "extra-files" : [ 12 | "internal/version.go" 13 | ], 14 | "exclude-paths": [ 15 | ".github", 16 | ".vscode", 17 | "ldotel", 18 | "ldai" 19 | ] 20 | }, 21 | "ldotel" : { 22 | "package-name": "ldotel", 23 | "release-type" : "go", 24 | "tag-separator": "/", 25 | "versioning" : "default", 26 | "extra-files" : [ 27 | "package_info.go" 28 | ] 29 | }, 30 | "ldai" : { 31 | "package-name": "ldai", 32 | "release-type" : "go", 33 | "bump-minor-pre-major" : true, 34 | "tag-separator": "/", 35 | "versioning" : "default", 36 | "extra-files" : [ 37 | "package_info.go" 38 | ] 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /subsystems/changeset_test.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestChangeSetBuilder_New(t *testing.T) { 10 | builder := NewChangeSetBuilder() 11 | assert.NotNil(t, builder) 12 | } 13 | 14 | func TestChangeSetBuilder_MustStartToFinish(t *testing.T) { 15 | builder := NewChangeSetBuilder() 16 | selector := NewSelector("foo", 1) 17 | _, err := builder.Finish(selector) 18 | assert.Error(t, err) 19 | 20 | assert.NoError(t, builder.Start(ServerIntent{Payload: Payload{Code: IntentNone}})) 21 | 22 | _, err = builder.Finish(selector) 23 | assert.NoError(t, err) 24 | } 25 | 26 | func TestChangeSetBuilder_Changes(t *testing.T) { 27 | builder := NewChangeSetBuilder() 28 | err := builder.Start(ServerIntent{Payload: Payload{Code: IntentTransferChanges}}) 29 | assert.NoError(t, err) 30 | 31 | builder.AddPut("foo", "bar", 1, []byte("baz")) 32 | builder.AddDelete("foo", "bar", 1) 33 | 34 | selector := NewSelector("foo", 1) 35 | changeSet, err := builder.Finish(selector) 36 | assert.NoError(t, err) 37 | assert.NotNil(t, changeSet) 38 | 39 | changes := changeSet.Changes() 40 | assert.Equal(t, 2, len(changes)) 41 | assert.Equal(t, Change{Action: ChangeTypePut, Kind: "foo", Key: "bar", Version: 1, Object: []byte("baz")}, changes[0]) 42 | assert.Equal(t, Change{Action: ChangeTypeDelete, Kind: "foo", Key: "bar", Version: 1}, changes[1]) 43 | 44 | assert.Equal(t, IntentTransferChanges, changeSet.IntentCode()) 45 | assert.Equal(t, selector, changeSet.Selector()) 46 | 47 | } 48 | 49 | // After receiving an intent, the SDK may receive 1 or more objects before receiving a payload-transferred. 50 | // At that point, LaunchDarkly may send more objects followed by another payload-transferred. These objects 51 | // should be regarded as part of an implicit "xfer-changes" intent, even though the server doesn't actually send one. 52 | // If the server intends to use an xfer-full instead (for efficiency or other reasons), it will need to explicitly 53 | // send one. 54 | func TestChangeSetBuilder_ImplicitIntentXferChanges(t *testing.T) { 55 | builder := NewChangeSetBuilder() 56 | err := builder.Start(ServerIntent{Payload: Payload{Code: IntentTransferFull}}) 57 | assert.NoError(t, err) 58 | 59 | changes1, err := builder.Finish(NewSelector("foo", 1)) 60 | assert.NoError(t, err) 61 | assert.Equal(t, IntentTransferFull, changes1.IntentCode()) 62 | 63 | builder.AddPut("foo", "bar", 1, []byte("baz")) 64 | changes2, err := builder.Finish(NewSelector("bar", 2)) 65 | assert.NoError(t, err) 66 | 67 | assert.Equal(t, IntentTransferChanges, changes2.IntentCode()) 68 | } 69 | 70 | func TestChangeSetBuilder_NoChanges(t *testing.T) { 71 | builder := NewChangeSetBuilder() 72 | changeSet := builder.NoChanges() 73 | assert.NotNil(t, changeSet) 74 | 75 | intent := changeSet.IntentCode() 76 | assert.NotNil(t, intent) 77 | 78 | assert.Equal(t, IntentNone, intent) 79 | 80 | assert.False(t, changeSet.Selector().IsDefined()) 81 | assert.Equal(t, NoSelector(), changeSet.Selector()) 82 | } 83 | -------------------------------------------------------------------------------- /subsystems/component_configurer.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | // ComponentConfigurer is a common interface for SDK component factories and configuration builders. 4 | // Applications should not need to implement this interface. 5 | type ComponentConfigurer[T any] interface { 6 | // Build is called internally by the SDK to create an implementation instance. Applications 7 | // should not need to call this method. 8 | Build(clientContext ClientContext) (T, error) 9 | } 10 | -------------------------------------------------------------------------------- /subsystems/data_destination.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | // DataDestination handles data obtained from a data source and maintains a 4 | // record of the last selector applied. 5 | // 6 | // This interface is not stable, and not subject to any backwards 7 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 8 | // 9 | // Do not use it. 10 | // You have been warned. 11 | type DataDestination interface { 12 | // Selector returns the last known selector for the data store. If no previous selector is known, 13 | // this should return subsystems.NoSelector() 14 | Selector() Selector 15 | 16 | // SetBasis defines a new basis for the data store. This means the store must 17 | // be emptied of any existing data before applying the events. This operation should be 18 | // atomic with respect to any other operations that modify the store. 19 | // 20 | // The selector defines the version of the basis. 21 | // 22 | // If persist is true, it indicates that the data should be propagated to any connected persistent 23 | // store. 24 | SetBasis(events []Change, selector Selector, persist bool) 25 | 26 | // ApplyDelta applies a set of changes to an existing basis. This operation should be atomic with 27 | // respect to any other operations that modify the store. 28 | // 29 | // The selector defines the new version of the basis. 30 | // 31 | // If persist is true, it indicates that the changes should be propagated to any connected persistent 32 | // store. 33 | ApplyDelta(events []Change, selector Selector, persist bool) 34 | } 35 | -------------------------------------------------------------------------------- /subsystems/data_source_status_reporter.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/interfaces" 5 | ) 6 | 7 | // DataSourceStatusReporter allows a data source to report its status to the SDK. 8 | // 9 | // This interface is not stable, and not subject to any backwards 10 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 11 | // 12 | // Do not use it. 13 | // You have been warned. 14 | type DataSourceStatusReporter interface { 15 | // UpdateStatus informs the SDK of a change in the data source's status. 16 | // 17 | // Data source implementations should use this method if they have any concept of being in a valid 18 | // state, a temporarily disconnected state, or a permanently stopped state. 19 | // 20 | // If newState is different from the previous state, and/or newError is non-empty, the SDK 21 | // will start returning the new status (adding a timestamp for the change) from 22 | // DataSourceStatusProvider.GetStatus(), and will trigger status change events to any 23 | // registered listeners. 24 | // 25 | // A special case is that if newState is DataSourceStateInterrupted, but the previous state was 26 | // DataSourceStateInitializing, the state will remain at Initializing because Interrupted is 27 | // only meaningful after a successful startup. 28 | UpdateStatus(newState interfaces.DataSourceState, newError interfaces.DataSourceErrorInfo) 29 | } 30 | -------------------------------------------------------------------------------- /subsystems/data_store.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "io" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 7 | ) 8 | 9 | // DataStore is an interface for a data store that holds feature flags and related data received by 10 | // the SDK. 11 | // 12 | // Ordinarily, the only implementations of this interface are the default in-memory implementation, 13 | // which holds references to actual SDK data model objects, and the persistent data store 14 | // implementation that delegates to a PersistentDataStore. 15 | type DataStore interface { 16 | io.Closer 17 | 18 | ReadOnlyStore 19 | 20 | // Init overwrites the store's contents with a set of items for each collection. 21 | // 22 | // All previous data should be discarded, regardless of versioning. 23 | // 24 | // The update should be done atomically. If it cannot be done atomically, then the store 25 | // must first add or update each item in the same order that they are given in the input 26 | // data, and then delete any previously stored items that were not in the input data. 27 | Init(allData []ldstoretypes.Collection) error 28 | 29 | // Upsert updates or inserts an item in the specified collection. For updates, the object will only be 30 | // updated if the existing version is less than the new version. 31 | // 32 | // The SDK may pass an ItemDescriptor whose Item is nil, to represent a placeholder for a deleted 33 | // item. In that case, assuming the version is greater than any existing version of that item, the 34 | // store should retain that placeholder rather than simply not storing anything. 35 | // 36 | // The method returns true if the item was updated, or false if it was not updated because the store 37 | // contains an equal or greater version. 38 | Upsert(kind ldstoretypes.DataKind, key string, item ldstoretypes.ItemDescriptor) (bool, error) 39 | 40 | // IsStatusMonitoringEnabled returns true if this data store implementation supports status 41 | // monitoring. 42 | // 43 | // This is normally only true for persistent data stores created with ldcomponents.PersistentDataStore(), 44 | // but it could also be true for any custom DataStore implementation that makes use of the 45 | // statusUpdater parameter provided to the DataStoreFactory. Returning true means that the store 46 | // guarantees that if it ever enters an invalid state (that is, an operation has failed or it knows 47 | // that operations cannot succeed at the moment), it will publish a status update, and will then 48 | // publish another status update once it has returned to a valid state. 49 | // 50 | // The same value will be returned from DataStoreStatusProvider.IsStatusMonitoringEnabled(). 51 | IsStatusMonitoringEnabled() bool 52 | } 53 | -------------------------------------------------------------------------------- /subsystems/data_store_mode.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | // DataStoreMode represents the mode of operation of a Data Store in FDV2 mode. 4 | // 5 | // This enum is not stable, and not subject to any backwards 6 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 7 | // 8 | // Do not use it. 9 | // You have been warned. 10 | type DataStoreMode int 11 | 12 | const ( 13 | // DataStoreModeRead indicates that the data store is read-only. Data will never be written back to the store by 14 | // the SDK. 15 | DataStoreModeRead = 0 16 | // DataStoreModeReadWrite indicates that the data store is read-write. Data from initializers/synchronizers may be 17 | // written to the store as necessary. 18 | DataStoreModeReadWrite = 1 19 | ) 20 | -------------------------------------------------------------------------------- /subsystems/data_store_update_sink.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import "github.com/launchdarkly/go-server-sdk/v7/interfaces" 4 | 5 | // DataStoreUpdateSink is an interface that a data store implementation can use to report information 6 | // back to the SDK. 7 | // 8 | // Application code does not need to use this type. It is for data store implementations. 9 | // 10 | // The SDK passes this in the ClientContext when it is creating a data store component. 11 | type DataStoreUpdateSink interface { 12 | // UpdateStatus informs the SDK of a change in the data store's operational status. 13 | // 14 | // This is what makes the status monitoring mechanisms in DataStoreStatusProvider work. 15 | UpdateStatus(newStatus interfaces.DataStoreStatus) 16 | } 17 | -------------------------------------------------------------------------------- /subsystems/datasystem_configuration.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | // SynchronizersConfiguration represents the config for the primary and secondary synchronizers. 4 | type SynchronizersConfiguration struct { 5 | // The builder for the synchronizer that is primarily active. 6 | PrimaryBuilder func() (DataSynchronizer, error) 7 | // A fallback builder for the synchronizer if the primary fails. 8 | SecondaryBuilder func() (DataSynchronizer, error) 9 | // A temporarily supported FDv1 fallback builder for the synchronizer as an 10 | // alternative fallback option. 11 | FDv1FallbackBuilder func() (DataSynchronizer, error) 12 | } 13 | 14 | // DataSystemConfiguration represents the configuration for the data system. 15 | type DataSystemConfiguration struct { 16 | // Store is the (optional) persistent data store. 17 | Store DataStore 18 | // StoreMode specifies the mode in which the persistent store should operate, if present. 19 | StoreMode DataStoreMode 20 | // Initializers obtain data for the SDK in a one-shot manner at startup. Their job is to get the SDK 21 | // into a state where it is serving somewhat fresh values as fast as possible. 22 | Initializers []DataInitializer 23 | // Synchronizers keep the SDK's data up-to-date continuously. 24 | Synchronizers SynchronizersConfiguration 25 | } 26 | -------------------------------------------------------------------------------- /subsystems/deltas_to_storable_items.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "github.com/launchdarkly/go-jsonstream/v3/jreader" 5 | "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 7 | ) 8 | 9 | // ToStorableItems converts a list of FDv2 events to a list of collections suitable for insertion 10 | // into a data store. 11 | // 12 | // This function is not stable, and not subject to any backwards 13 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 14 | // 15 | // Do not use it. 16 | // You have been warned. 17 | func ToStorableItems(deltas []Change) ([]ldstoretypes.Collection, error) { 18 | collections := make(kindMap) 19 | for _, event := range deltas { 20 | kind, ok := event.Kind.ToFDV1() 21 | if !ok { 22 | // If we don't recognize this kind, it's not an error and should be ignored for forwards 23 | // compatibility. 24 | continue 25 | } 26 | 27 | switch event.Action { 28 | case ChangeTypePut: 29 | // A put requires deserializing the item. We delegate to the optimized streaming JSON 30 | // parser. 31 | reader := jreader.NewReader(event.Object) 32 | item, err := kind.DeserializeFromJSONReader(&reader) 33 | if err != nil { 34 | return nil, err 35 | } 36 | collections[kind] = append(collections[kind], ldstoretypes.KeyedItemDescriptor{ 37 | Key: event.Key, 38 | Item: item, 39 | }) 40 | case ChangeTypeDelete: 41 | // A deletion is represented by a tombstone, which is an ItemDescriptor with a version and nil item. 42 | collections[kind] = append(collections[kind], ldstoretypes.KeyedItemDescriptor{ 43 | Key: event.Key, 44 | Item: ldstoretypes.ItemDescriptor{Version: event.Version, Item: nil}, 45 | }) 46 | default: 47 | // An unknown action isn't an error, and should be ignored for forwards compatibility. 48 | continue 49 | } 50 | } 51 | 52 | return collections.flatten(), nil 53 | } 54 | 55 | type kindMap map[datakinds.DataKindInternal][]ldstoretypes.KeyedItemDescriptor 56 | 57 | func (k kindMap) flatten() (result []ldstoretypes.Collection) { 58 | for kind, items := range k { 59 | result = append(result, ldstoretypes.Collection{ 60 | Kind: kind, 61 | Items: items, 62 | }) 63 | } 64 | return 65 | } 66 | -------------------------------------------------------------------------------- /subsystems/diagnostic_description.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 4 | 5 | // DiagnosticDescription is an optional interface for components to describe their own configuration. 6 | // 7 | // The SDK uses a simplified JSON representation of its configuration when recording diagnostics data. 8 | // Any component type that implements ComponentConfigurer may choose to contribute values to this 9 | // representation, although the SDK may or may not use them. 10 | type DiagnosticDescription interface { 11 | // DescribeConfiguration should return a JSON value or ldvalue.Null(). 12 | // 13 | // For custom components, this must be a string value that describes the basic nature of this component 14 | // implementation (e.g. "Redis"). Built-in LaunchDarkly components may instead return a JSON object 15 | // containing multiple properties specific to the LaunchDarkly diagnostic schema. 16 | DescribeConfiguration(context ClientContext) ldvalue.Value 17 | } 18 | -------------------------------------------------------------------------------- /subsystems/http_configuration.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "net/http" 5 | ) 6 | 7 | // HTTPConfiguration encapsulates top-level HTTP configuration that applies to all SDK components. 8 | // 9 | // See ldcomponents.HTTPConfigurationBuilder for more details on these properties. 10 | type HTTPConfiguration struct { 11 | // DefaultHeaders contains the basic headers that should be added to all HTTP requests from SDK 12 | // components to LaunchDarkly services, based on the current SDK configuration. This map is never 13 | // modified once created. 14 | DefaultHeaders http.Header 15 | 16 | // CreateHTTPClient is a function that returns a new HTTP client instance based on the SDK configuration. 17 | // 18 | // The SDK will ensure that this field is non-nil before passing it to any component. 19 | CreateHTTPClient func() *http.Client 20 | } 21 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/big_segments_config_extra.go: -------------------------------------------------------------------------------- 1 | package ldstoreimpl 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 7 | ) 8 | 9 | // BigSegmentsConfigurationProperties encapsulates the SDK's configuration with regard to Big Segments. 10 | // 11 | // This struct implements the BigSegmentsConfiguration interface, but allows for addition of new 12 | // properties. In a future version, BigSegmentsConfigurationBuilder and other configuration builders 13 | // may be changed to use concrete types instead of interfaces. 14 | type BigSegmentsConfigurationProperties struct { 15 | // Store the data store instance that is used for Big Segments data. If nil, Big Segments are disabled. 16 | Store subsystems.BigSegmentStore 17 | 18 | // ContextCacheSize is the maximum number of contexts whose Big Segment state will be cached by the SDK 19 | // at any given time. 20 | ContextCacheSize int 21 | 22 | // ContextCacheTime is the maximum length of time that the Big Segment state for a context will be cached 23 | // by the SDK. 24 | ContextCacheTime time.Duration 25 | 26 | // StatusPollInterval is the interval at which the SDK will poll the Big Segment store to make sure 27 | // it is available and to determine how long ago it was updated 28 | StatusPollInterval time.Duration 29 | 30 | // StaleAfter is the maximum length of time between updates of the Big Segments data before the data 31 | // is considered out of date. 32 | StaleAfter time.Duration 33 | 34 | // StartPolling is true if the polling task should be started immediately. Otherwise, it will only 35 | // start after calling BigSegmentsStoreWrapper.SetPollingActive(true). This property is always true 36 | // in regular use of the SDK; the Relay Proxy may set it to false. 37 | StartPolling bool 38 | } 39 | 40 | func (p BigSegmentsConfigurationProperties) GetStore() subsystems.BigSegmentStore { //nolint:revive 41 | return p.Store 42 | } 43 | 44 | func (p BigSegmentsConfigurationProperties) GetContextCacheSize() int { //nolint:revive 45 | return p.ContextCacheSize 46 | } 47 | 48 | func (p BigSegmentsConfigurationProperties) GetContextCacheTime() time.Duration { //nolint:revive 49 | return p.ContextCacheTime 50 | } 51 | 52 | func (p BigSegmentsConfigurationProperties) GetStatusPollInterval() time.Duration { //nolint:revive 53 | return p.StatusPollInterval 54 | } 55 | 56 | func (p BigSegmentsConfigurationProperties) GetStaleAfter() time.Duration { //nolint:revive 57 | return p.StaleAfter 58 | } 59 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/big_segments_membership_test.go: -------------------------------------------------------------------------------- 1 | package ldstoreimpl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestMembershipWithNilKeys(t *testing.T) { 12 | m := NewBigSegmentMembershipFromSegmentRefs(nil, nil) 13 | assert.Equal(t, bigSegmentMembershipMapImpl(nil), m) 14 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key")) 15 | } 16 | 17 | func TestMembershipWithEmptySliceKeys(t *testing.T) { 18 | m := NewBigSegmentMembershipFromSegmentRefs(nil, nil) 19 | assert.Equal(t, bigSegmentMembershipMapImpl(nil), m) 20 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key")) 21 | } 22 | 23 | func TestMembershipWithSingleIncludedKey(t *testing.T) { 24 | m := NewBigSegmentMembershipFromSegmentRefs([]string{"key1"}, nil) 25 | assert.Equal(t, bigSegmentMembershipSingleInclude("key1"), m) 26 | assert.Equal(t, ldvalue.NewOptionalBool(true), m.CheckMembership("key1")) 27 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key2")) 28 | } 29 | 30 | func TestMembershipWithSingleExcludedKey(t *testing.T) { 31 | m := NewBigSegmentMembershipFromSegmentRefs(nil, []string{"key1"}) 32 | assert.Equal(t, bigSegmentMembershipSingleExclude("key1"), m) 33 | assert.Equal(t, ldvalue.NewOptionalBool(false), m.CheckMembership("key1")) 34 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key2")) 35 | } 36 | 37 | func TestMembershipWithMultipleIncludedKeys(t *testing.T) { 38 | m := NewBigSegmentMembershipFromSegmentRefs([]string{"key1", "key2"}, nil) 39 | assert.Equal(t, bigSegmentMembershipMapImpl(map[string]bool{"key1": true, "key2": true}), m) 40 | assert.Equal(t, ldvalue.NewOptionalBool(true), m.CheckMembership("key1")) 41 | assert.Equal(t, ldvalue.NewOptionalBool(true), m.CheckMembership("key2")) 42 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key3")) 43 | } 44 | 45 | func TestMembershipWithMultipleExcludedKeys(t *testing.T) { 46 | m := NewBigSegmentMembershipFromSegmentRefs(nil, []string{"key1", "key2"}) 47 | assert.Equal(t, bigSegmentMembershipMapImpl(map[string]bool{"key1": false, "key2": false}), m) 48 | assert.Equal(t, ldvalue.NewOptionalBool(false), m.CheckMembership("key1")) 49 | assert.Equal(t, ldvalue.NewOptionalBool(false), m.CheckMembership("key2")) 50 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key3")) 51 | } 52 | 53 | func TestMembershipWithIncludedAndExcludedKeys(t *testing.T) { 54 | m := NewBigSegmentMembershipFromSegmentRefs([]string{"key1", "key2"}, []string{"key2", "key3"}) 55 | // key1 is included; key2 is included and excluded, therefore it's included; key3 is excluded 56 | assert.Equal(t, bigSegmentMembershipMapImpl(map[string]bool{"key1": true, "key2": true, "key3": false}), m) 57 | assert.Equal(t, ldvalue.NewOptionalBool(true), m.CheckMembership("key1")) 58 | assert.Equal(t, ldvalue.NewOptionalBool(true), m.CheckMembership("key2")) 59 | assert.Equal(t, ldvalue.NewOptionalBool(false), m.CheckMembership("key3")) 60 | assert.Equal(t, ldvalue.OptionalBool{}, m.CheckMembership("key4")) 61 | } 62 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/data_kinds.go: -------------------------------------------------------------------------------- 1 | package ldstoreimpl 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" 5 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 6 | ) 7 | 8 | // This file contains the public API for accessing things in internal/datakinds. We need to export 9 | // these things in order to support development of custom database integrations and internal LD 10 | // components, but we don't want to expose the underlying global variables. 11 | 12 | // AllKinds returns a list of supported StoreDataKinds. Among other things, this list might 13 | // be used by data stores to know what data (namespaces) to expect. 14 | func AllKinds() []ldstoretypes.DataKind { 15 | return datakinds.AllDataKinds() 16 | } 17 | 18 | // Features returns the StoreDataKind instance corresponding to feature flag data. 19 | func Features() ldstoretypes.DataKind { 20 | return datakinds.Features 21 | } 22 | 23 | // Segments returns the StoreDataKind instance corresponding to user segment data. 24 | func Segments() ldstoretypes.DataKind { 25 | return datakinds.Segments 26 | } 27 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/data_kinds_test.go: -------------------------------------------------------------------------------- 1 | package ldstoreimpl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | 8 | "github.com/launchdarkly/go-server-sdk/v7/internal/datakinds" 9 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 10 | ) 11 | 12 | func TestDataKinds(t *testing.T) { 13 | // Here we're just verifying that the public API returns the same instances that we're using internally. 14 | // The behavior of those instances is tested in internal/datakinds where they are implemented. 15 | 16 | assert.Equal(t, datakinds.Features, Features()) 17 | assert.Equal(t, datakinds.Segments, Segments()) 18 | assert.Equal(t, []ldstoretypes.DataKind{Features(), Segments()}, AllKinds()) 19 | } 20 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/data_store_eval.go: -------------------------------------------------------------------------------- 1 | package ldstoreimpl 2 | 3 | import ( 4 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 5 | ldeval "github.com/launchdarkly/go-server-sdk-evaluation/v3" 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 7 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 8 | ) 9 | 10 | // This file contains the public API for creating the adapter that bridges Evaluator to DataStore. The 11 | // actual implementation is in internal/datastore, but we expose it because it is helpful when we 12 | // evaluate flags outside of the SDK in ld-relay. 13 | 14 | // NewDataStoreEvaluatorDataProvider provides an adapter for using a DataStore with the Evaluator type 15 | // in go-server-sdk-evaluation. 16 | // 17 | // Normal use of the SDK does not require this type. It is provided for use by other LaunchDarkly 18 | // components that use DataStore and Evaluator separately from the SDK. 19 | func NewDataStoreEvaluatorDataProvider(store subsystems.ReadOnlyStore, loggers ldlog.Loggers) ldeval.DataProvider { 20 | return datastore.NewDataStoreEvaluatorDataProviderImpl(store, loggers) 21 | } 22 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/data_store_eval_test.go: -------------------------------------------------------------------------------- 1 | package ldstoreimpl 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/launchdarkly/go-server-sdk/v7/internal/datastore" 7 | 8 | "github.com/stretchr/testify/assert" 9 | 10 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 11 | ) 12 | 13 | func TestDataStoreEvaluatorDataProvider(t *testing.T) { 14 | // The underlying implementation type is tested in the internal/datastore package, so this test 15 | // just verifies that we are in fact constructing that type. 16 | loggers := ldlog.NewDisabledLoggers() 17 | store := datastore.NewInMemoryDataStore(loggers) 18 | provider := NewDataStoreEvaluatorDataProvider(store, loggers) 19 | expected := datastore.NewDataStoreEvaluatorDataProviderImpl(store, loggers) 20 | assert.Equal(t, expected, provider) 21 | } 22 | -------------------------------------------------------------------------------- /subsystems/ldstoreimpl/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldstoreimpl contains SDK data store implementation objects that may be used by external 2 | // code such as custom data store integrations and internal LaunchDarkly components. Application code 3 | // normally will not use this package. 4 | package ldstoreimpl 5 | -------------------------------------------------------------------------------- /subsystems/ldstoretypes/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldstoretypes contains types that are only needed when implementing custom components. 2 | // Specifically, these types describe how data is passed to and from a DataStore. 3 | package ldstoretypes 4 | -------------------------------------------------------------------------------- /subsystems/logging_configuration.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/launchdarkly/go-sdk-common/v3/ldlog" 7 | ) 8 | 9 | // LoggingConfiguration encapsulates the SDK's general logging configuration. 10 | // 11 | // See ldcomponents.LoggingConfigurationBuilder for more details on these properties. 12 | type LoggingConfiguration struct { 13 | // Loggers is a configured ldlog.Loggers instance for general SDK logging. 14 | Loggers ldlog.Loggers 15 | 16 | // LogDataSourceOutageAsErrorAfter is the time threshold, if any, after which the SDK 17 | // will log a data source outage at Error level instead of Warn level. See 18 | // LoggingConfigurationBuilderLogDataSourceOutageAsErrorAfter(). 19 | LogDataSourceOutageAsErrorAfter time.Duration 20 | 21 | // LogEvaluationErrors is true if evaluation errors should be logged. 22 | LogEvaluationErrors bool 23 | 24 | // LogContextKeyInErrors is true if context keys may be included in logging. 25 | LogContextKeyInErrors bool 26 | } 27 | -------------------------------------------------------------------------------- /subsystems/package_info.go: -------------------------------------------------------------------------------- 1 | // Package subsystems contains interfaces for implementation of custom LaunchDarkly components. 2 | // 3 | // Most applications will not need to refer to these types. You will use them if you are creating a 4 | // plug-in component, such as a database integration, or a test fixture. They are also used as 5 | // interfaces for the built-in SDK components, so that plugin components can be used interchangeably 6 | // with those: for instance, Config.DataStore uses the type subsystems.DataStore as an abstraction 7 | // for the data store component. 8 | // 9 | // The package also includes concrete types that are used as parameters within these interfaces. 10 | package subsystems 11 | -------------------------------------------------------------------------------- /subsystems/payloads.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | // Payload represents a payload delivered in a streaming response. 4 | // 5 | // This type is not stable, and not subject to any backwards 6 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 7 | // 8 | // Do not use it. 9 | // You have been warned. 10 | type Payload struct { 11 | // The id here doesn't seem to match the state that is included in the 12 | // Payload transferred object. 13 | 14 | // It would be nice if we had the same value available in both so we could 15 | // use that as the key consistently throughout the the process. 16 | ID string `json:"id"` 17 | Target int `json:"target"` 18 | Code IntentCode `json:"intentCode"` 19 | Reason string `json:"reason"` 20 | } 21 | 22 | // PollingPayload represents a payload that is delivered in a polling response. 23 | // 24 | // This type is not stable, and not subject to any backwards 25 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 26 | // 27 | // Do not use it. 28 | // You have been warned. 29 | type PollingPayload struct { 30 | // Note: the first event in a PollingPayload should be a Payload. 31 | Events []RawEvent `json:"events"` 32 | } 33 | -------------------------------------------------------------------------------- /subsystems/raw_event.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | // RawEvent is a partially deserialized event that allows the event name to be extracted before 8 | // the rest of the event is deserialized. 9 | // 10 | // This type is not stable, and not subject to any backwards 11 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 12 | // 13 | // Do not use it. 14 | // You have been warned. 15 | type RawEvent struct { 16 | Name EventName `json:"event"` 17 | Data json.RawMessage `json:"data"` 18 | } 19 | -------------------------------------------------------------------------------- /subsystems/read_only_store.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "github.com/launchdarkly/go-server-sdk/v7/subsystems/ldstoretypes" 5 | ) 6 | 7 | // ReadOnlyStore represents a read-only data store that can be used to retrieve 8 | // any of the SDK's supported DataKinds. 9 | type ReadOnlyStore interface { 10 | // Get retrieves an item from the specified collection, if available. 11 | // 12 | // If the specified key does not exist in the collection, it should return an ItemDescriptor 13 | // whose Version is -1. 14 | // 15 | // If the item has been deleted and the store contains a placeholder, it should return an 16 | // ItemDescriptor whose Version is the version of the placeholder, and whose Item is nil. 17 | Get(kind ldstoretypes.DataKind, key string) (ldstoretypes.ItemDescriptor, error) 18 | 19 | // GetAll retrieves all items from the specified collection. 20 | // 21 | // If the store contains placeholders for deleted items, it should include them in the results, 22 | // not filter them out. 23 | GetAll(kind ldstoretypes.DataKind) ([]ldstoretypes.KeyedItemDescriptor, error) 24 | 25 | // IsInitialized returns true if the data store contains a data set, meaning that Init has been 26 | // called at least once. 27 | // 28 | // In a shared data store, it should be able to detect this even if Init was called in a 29 | // different process: that is, the test should be based on looking at what is in the data store. 30 | // Once this has been determined to be true, it can continue to return true without having to 31 | // check the store again; this method should be as fast as possible since it may be called during 32 | // feature flag evaluations. 33 | IsInitialized() bool 34 | } 35 | 36 | // ReadOnlyDataStore represents a read-only data store that can be used to 37 | // retrieve any of the SDK's supported DataKinds. 38 | // 39 | // This field is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. 40 | // It is not suitable for production usage. Do not use it. You have been warned. 41 | type ReadOnlyDataStore interface { 42 | ReadOnlyStore 43 | 44 | // Snapshot returns a snapshot of the current state of the data store. The 45 | // snapshot includes both the map representing the data in the active store, 46 | // but also a selector that can be used as a basis for the FDv2 protocol. 47 | Snapshot() (map[ldstoretypes.DataKind][]ldstoretypes.KeyedItemDescriptor, Selector, error) 48 | } 49 | -------------------------------------------------------------------------------- /subsystems/selector.go: -------------------------------------------------------------------------------- 1 | package subsystems 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | // Selector represents a particular snapshot of data. 9 | // 10 | // This type is not stable, and not subject to any backwards 11 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 12 | // 13 | // Do not use it. 14 | // You have been warned. 15 | type Selector struct { 16 | state string 17 | version int 18 | } 19 | 20 | // NoSelector returns an empty Selector. 21 | func NoSelector() Selector { 22 | return Selector{} 23 | } 24 | 25 | // IsDefined returns true if the Selector has a value. 26 | func (s Selector) IsDefined() bool { 27 | return s != NoSelector() 28 | } 29 | 30 | //nolint:revive // Event method. 31 | func (s Selector) Name() EventName { 32 | return EventPayloadTransferred 33 | } 34 | 35 | // NewSelector creates a new Selector from a state string and version. 36 | // 37 | // This function is not stable, and not subject to any backwards 38 | // compatibility guarantees or semantic versioning. It is not suitable for production usage. 39 | // 40 | // Do not use it. 41 | // You have been warned. 42 | func NewSelector(state string, version int) Selector { 43 | return Selector{state: state, version: version} 44 | } 45 | 46 | // State returns the state string of the Selector. This cannot be called if the Selector is nil. 47 | func (s Selector) State() string { 48 | return s.state 49 | } 50 | 51 | // Version returns the version of the Selector. This cannot be called if the Selector is nil. 52 | func (s Selector) Version() int { 53 | return s.version 54 | } 55 | 56 | // UnmarshalJSON unmarshals a Selector from JSON. 57 | func (s *Selector) UnmarshalJSON(data []byte) error { 58 | var raw map[string]interface{} 59 | if err := json.Unmarshal(data, &raw); err != nil { 60 | return err 61 | } 62 | 63 | if state, ok := raw["state"].(string); ok { 64 | s.state = state 65 | } else { 66 | return errors.New("unmarshal selector: missing state field") 67 | } 68 | if version, ok := raw["version"].(float64); ok { 69 | s.version = int(version) 70 | } else { 71 | return errors.New("unmarshal selector: missing version field") 72 | } 73 | return nil 74 | } 75 | 76 | // MarshalJSON marshals a Selector to JSON. 77 | func (s Selector) MarshalJSON() ([]byte, error) { 78 | return json.Marshal(map[string]interface{}{ 79 | "state": s.state, 80 | "version": s.version, 81 | }) 82 | } 83 | -------------------------------------------------------------------------------- /testhelpers/client_context.go: -------------------------------------------------------------------------------- 1 | package testhelpers 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" 7 | "github.com/launchdarkly/go-server-sdk/v7/internal/sharedtest" 8 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 9 | ) 10 | 11 | // Fallible is a general interface for anything with a Failed method. This is used by test helpers to 12 | // generalize between *testing.T, assert.T, etc. when all that we care about is detecting test failure. 13 | type Fallible interface { 14 | Failed() bool 15 | } 16 | 17 | // WithMockLoggingContext creates a ClientContext that writes to a MockLogger, executes the specified 18 | // action, and then dumps the captured output to the console only if there's been a test failure. 19 | func WithMockLoggingContext(t Fallible, action func(subsystems.ClientContext)) { 20 | mockLog := ldlogtest.NewMockLog() 21 | context := sharedtest.NewTestContext("", nil, 22 | &subsystems.LoggingConfiguration{Loggers: mockLog.Loggers}) 23 | defer func() { 24 | if t.Failed() { 25 | mockLog.Dump(os.Stdout) 26 | } 27 | // There's already a similar DumpLogIfTestFailed defined in the ldlogtest package, but it requires 28 | // specifically a *testing.T. 29 | }() 30 | action(context) 31 | } 32 | -------------------------------------------------------------------------------- /testhelpers/ldservices/events_service.go: -------------------------------------------------------------------------------- 1 | package ldservices 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/launchdarkly/go-test-helpers/v3/httphelpers" 7 | ) 8 | 9 | const ( 10 | serverSideEventsPath = "/bulk" 11 | serverSideDiagnosticEventsPath = "/diagnostic" 12 | ) 13 | 14 | // ServerSideEventsServiceHandler creates an HTTP handler to mimic the LaunchDarkly server-side events service. 15 | // It returns a 202 status for POSTs to the /bulk and /diagnostic paths, otherwise a 404. 16 | func ServerSideEventsServiceHandler() http.Handler { 17 | return httphelpers.HandlerForPath( 18 | serverSideEventsPath, 19 | httphelpers.HandlerForMethod("POST", httphelpers.HandlerWithStatus(202), nil), 20 | httphelpers.HandlerForPath( 21 | serverSideDiagnosticEventsPath, 22 | httphelpers.HandlerForMethod("POST", httphelpers.HandlerWithStatus(202), nil), 23 | nil, 24 | ), 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /testhelpers/ldservices/events_service_test.go: -------------------------------------------------------------------------------- 1 | package ldservices 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/launchdarkly/go-test-helpers/v3/httphelpers" 8 | 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestServerSideEventsEndpoint(t *testing.T) { 14 | handler := ServerSideEventsServiceHandler() 15 | client := httphelpers.ClientFromHandler(handler) 16 | 17 | resp, err := client.Post("http://fake/bulk", "text/plain", bytes.NewBufferString("hello")) 18 | assert.NoError(t, err) 19 | require.NotNil(t, resp) 20 | assert.Equal(t, 202, resp.StatusCode) 21 | } 22 | 23 | func TestServerSideDiagnosticEventsEndpoint(t *testing.T) { 24 | handler := ServerSideEventsServiceHandler() 25 | client := httphelpers.ClientFromHandler(handler) 26 | 27 | resp, err := client.Post("http://fake/diagnostic", "text/plain", bytes.NewBufferString("hello")) 28 | assert.NoError(t, err) 29 | require.NotNil(t, resp) 30 | assert.Equal(t, 202, resp.StatusCode) 31 | } 32 | 33 | func TestServerSideEventsHandlerReturns404ForWrongURL(t *testing.T) { 34 | handler := ServerSideEventsServiceHandler() 35 | client := httphelpers.ClientFromHandler(handler) 36 | 37 | resp, err := client.Post("http://fake/other", "text/plain", bytes.NewBufferString("hello")) 38 | assert.NoError(t, err) 39 | require.NotNil(t, resp) 40 | assert.Equal(t, 404, resp.StatusCode) 41 | } 42 | 43 | func TestServerSideEventsEndpointReturns405ForWrongMethod(t *testing.T) { 44 | handler := ServerSideEventsServiceHandler() 45 | client := httphelpers.ClientFromHandler(handler) 46 | 47 | resp, err := client.Get("http://fake/bulk") 48 | assert.NoError(t, err) 49 | require.NotNil(t, resp) 50 | assert.Equal(t, 405, resp.StatusCode) 51 | } 52 | 53 | func TestServerSideDiagnosticEventsEndpointReturns405ForWrongMethod(t *testing.T) { 54 | handler := ServerSideEventsServiceHandler() 55 | client := httphelpers.ClientFromHandler(handler) 56 | 57 | resp, err := client.Get("http://fake/diagnostic") 58 | assert.NoError(t, err) 59 | require.NotNil(t, resp) 60 | assert.Equal(t, 405, resp.StatusCode) 61 | } 62 | -------------------------------------------------------------------------------- /testhelpers/ldservices/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldservices provides HTTP handlers that simulate the behavior of LaunchDarkly service endpoints. 2 | // 3 | // This is mainly intended for use in the Go SDK's unit tests. It is also used in unit tests for the 4 | // LaunchDarkly Relay Proxy, and could be useful in testing other applications that use the Go SDK if it 5 | // is desirable to use real HTTP rather than other kinds of test fixtures. 6 | package ldservices 7 | -------------------------------------------------------------------------------- /testhelpers/ldservices/polling_service.go: -------------------------------------------------------------------------------- 1 | package ldservices 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/launchdarkly/go-test-helpers/v3/httphelpers" 7 | ) 8 | 9 | const serverSideSDKPollingPath = "/sdk/latest-all" 10 | const serverSideSDKV2PollingPath = "/sdk/poll" 11 | 12 | // ServerSidePollingServiceHandler creates an HTTP handler to mimic the LaunchDarkly server-side polling service. 13 | // 14 | // This handler returns JSON data for requests to /sdk/latest-all, and a 404 error for all other requests. 15 | // 16 | // Since this package cannot depend on the LaunchDarkly data model types, the caller is responsible for providing an 17 | // object that can be marshaled to JSON (such as ServerSDKData). If the data parameter is nil, the default response 18 | // is an empty JSON object {}. The data is marshalled again for each request. 19 | // 20 | // data := NewServerSDKData().Flags(flag1, flag2) 21 | // handler := ServerSidePollingServiceHandler(data) 22 | // 23 | // If you want the mock service to return different responses at different points during a test, you can either 24 | // provide a *ServerSDKData and modify its properties, or use a DelegatingHandler or SequentialHandler that can 25 | // be made to delegate to the ServerSidePollingServiceHandler at one time but a different handler at another time. 26 | func ServerSidePollingServiceHandler(data interface{}) http.Handler { 27 | return serverSidePollingHandler(data, serverSideSDKPollingPath) 28 | } 29 | 30 | // ServerSidePollingV2ServiceHandler creates an HTTP handler to mimic the LaunchDarkly server-side polling 31 | // service for FDv2. 32 | // 33 | // This handler returns JSON data for requests to /sdk/poll, and a 404 error for all other requests. 34 | // 35 | // Since this package cannot depend on the LaunchDarkly data model types, the caller is responsible for providing an 36 | // object that can be marshaled to JSON (such as ServerSDKData). If the data parameter is nil, the default response 37 | // is an empty JSON object {}. The data is marshalled again for each request. 38 | // 39 | // data := NewServerSDKData().Flags(flag1, flag2) 40 | // handler := ServerSidePollingV2ServiceHandler(data) 41 | // 42 | // If you want the mock service to return different responses at different points during a test, you can either 43 | // provide a *ServerSDKData and modify its properties, or use a DelegatingHandler or SequentialHandler that can 44 | // be made to delegate to the ServerSidePollingServiceHandler at one time but a different handler at another time. 45 | func ServerSidePollingV2ServiceHandler(data interface{}) http.Handler { 46 | return serverSidePollingHandler(data, serverSideSDKV2PollingPath) 47 | } 48 | 49 | func serverSidePollingHandler(data any, path string) http.Handler { 50 | if data == nil { 51 | data = map[string]any{} // default is an empty JSON object rather than null 52 | } 53 | return httphelpers.HandlerForPath(path, 54 | httphelpers.HandlerForMethod("GET", httphelpers.HandlerWithJSONResponse(data, nil), nil), nil) 55 | } 56 | -------------------------------------------------------------------------------- /testhelpers/ldservices/polling_service_test.go: -------------------------------------------------------------------------------- 1 | package ldservices 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/launchdarkly/go-test-helpers/v3/httphelpers" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestServerSidePollingEndpoint(t *testing.T) { 15 | data := "fake data" 16 | handler := ServerSidePollingServiceHandler(data) 17 | client := httphelpers.ClientFromHandler(handler) 18 | 19 | resp, err := client.Get(serverSideSDKPollingPath) 20 | assert.NoError(t, err) 21 | require.NotNil(t, resp) 22 | assert.Equal(t, 200, resp.StatusCode) 23 | assert.Equal(t, "application/json", resp.Header.Get("Content-Type")) 24 | bytes, err := io.ReadAll(resp.Body) 25 | assert.NoError(t, err) 26 | assert.Equal(t, `"fake data"`, string(bytes)) // the extra quotes are because the value was marshalled to JSON 27 | } 28 | 29 | func TestServerSidePollingReturns404ForWrongURL(t *testing.T) { 30 | data := "fake data" 31 | handler := ServerSidePollingServiceHandler(data) 32 | client := httphelpers.ClientFromHandler(handler) 33 | 34 | resp, err := client.Get("/other/path") 35 | assert.NoError(t, err) 36 | require.NotNil(t, resp) 37 | assert.Equal(t, 404, resp.StatusCode) 38 | } 39 | 40 | func TestServerSidePollingReturns405ForWrongMethod(t *testing.T) { 41 | data := "fake data" 42 | handler := ServerSidePollingServiceHandler(data) 43 | client := httphelpers.ClientFromHandler(handler) 44 | 45 | resp, err := client.Post(serverSideSDKPollingPath, "text/plain", bytes.NewBufferString("hello")) 46 | assert.NoError(t, err) 47 | require.NotNil(t, resp) 48 | assert.Equal(t, 405, resp.StatusCode) 49 | } 50 | 51 | func TestServerSidePollingMarshalsDataAgainForEachRequest(t *testing.T) { 52 | data := NewServerSDKData() 53 | handler := ServerSidePollingServiceHandler(data) 54 | client := httphelpers.ClientFromHandler(handler) 55 | 56 | resp, err := client.Get(serverSideSDKPollingPath) 57 | assert.NoError(t, err) 58 | assert.NotNil(t, resp) 59 | bytes, err := io.ReadAll(resp.Body) 60 | assert.NoError(t, err) 61 | assert.Equal(t, `{"flags":{},"segments":{}}`, string(bytes)) 62 | 63 | data.Flags(KeyAndVersionItem("flagkey", 1)) 64 | resp, err = client.Get(serverSideSDKPollingPath) 65 | assert.NoError(t, err) 66 | assert.NotNil(t, resp) 67 | bytes, err = io.ReadAll(resp.Body) 68 | assert.NoError(t, err) 69 | assert.Equal(t, `{"flags":{"flagkey":{"key":"flagkey","version":1}},"segments":{}}`, string(bytes)) 70 | } 71 | -------------------------------------------------------------------------------- /testhelpers/ldservices/server_sdk_data_test.go: -------------------------------------------------------------------------------- 1 | package ldservices 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestKeyAndVersionItem(t *testing.T) { 11 | f := KeyAndVersionItem("my-key", 2) 12 | bytes, err := json.Marshal(f) 13 | assert.NoError(t, err) 14 | assert.JSONEq(t, `{"key":"my-key","version":2}`, string(bytes)) 15 | } 16 | 17 | func TestEmptyServerSDKData(t *testing.T) { 18 | expectedJSON := `{"flags":{},"segments":{}}` 19 | data := NewServerSDKData() 20 | bytes, err := json.Marshal(data) 21 | assert.NoError(t, err) 22 | assert.JSONEq(t, expectedJSON, string(bytes)) 23 | } 24 | 25 | func TestSDKDataWithAllDataKinds(t *testing.T) { 26 | flag1 := KeyAndVersionItem("flagkey1", 1) 27 | flag2 := KeyAndVersionItem("flagkey2", 2) 28 | segment1 := KeyAndVersionItem("segkey1", 3) 29 | segment2 := KeyAndVersionItem("segkey2", 4) 30 | data := NewServerSDKData().Flags(flag1, flag2).Segments(segment1, segment2) 31 | 32 | expectedJSON := `{ 33 | "flags": { 34 | "flagkey1": { 35 | "key": "flagkey1", 36 | "version": 1 37 | }, 38 | "flagkey2": { 39 | "key": "flagkey2", 40 | "version": 2 41 | } 42 | }, 43 | "segments": { 44 | "segkey1": { 45 | "key": "segkey1", 46 | "version": 3 47 | }, 48 | "segkey2": { 49 | "key": "segkey2", 50 | "version": 4 51 | } 52 | } 53 | }` 54 | bytes, err := json.Marshal(data) 55 | assert.NoError(t, err) 56 | assert.JSONEq(t, expectedJSON, string(bytes)) 57 | } 58 | -------------------------------------------------------------------------------- /testhelpers/ldservices/streaming_service_test.go: -------------------------------------------------------------------------------- 1 | package ldservices 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | 13 | helpers "github.com/launchdarkly/go-test-helpers/v3" 14 | "github.com/launchdarkly/go-test-helpers/v3/httphelpers" 15 | ) 16 | 17 | func TestServerSideStreamingServiceHandler(t *testing.T) { 18 | initialEvent := httphelpers.SSEEvent{Event: "put", Data: "my data"} 19 | handler, stream := ServerSideStreamingServiceHandler(initialEvent) 20 | defer stream.Close() 21 | 22 | httphelpers.WithServer(handler, func(server *httptest.Server) { 23 | t.Run("sends events", func(t *testing.T) { 24 | resp, err := http.DefaultClient.Get(server.URL + ServerSideSDKStreamingPath) 25 | require.NoError(t, err) 26 | defer resp.Body.Close() 27 | 28 | expected := initialEvent.Bytes() 29 | assert.Equal(t, string(expected), string(helpers.ReadWithTimeout(resp.Body, len(expected), time.Second))) 30 | 31 | event2 := httphelpers.SSEEvent{Event: "patch", Data: "more data"} 32 | stream.Send(event2) 33 | 34 | expected = event2.Bytes() 35 | assert.Equal(t, string(expected), string(helpers.ReadWithTimeout(resp.Body, len(expected), time.Second))) 36 | }) 37 | 38 | t.Run("returns 404 for wrong URL", func(t *testing.T) { 39 | resp, err := http.DefaultClient.Get(server.URL + "/some/other/path") 40 | assert.NoError(t, err) 41 | assert.Equal(t, 404, resp.StatusCode) 42 | }) 43 | 44 | t.Run("returns 405 for wrong method", func(t *testing.T) { 45 | resp, err := http.DefaultClient.Post(server.URL+ServerSideSDKStreamingPath, "text/plain", bytes.NewBufferString("hello")) 46 | assert.NoError(t, err) 47 | assert.Equal(t, 405, resp.StatusCode) 48 | }) 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /testhelpers/ldservicesv2/package.go: -------------------------------------------------------------------------------- 1 | // Package ldservicesv2 provides test helpers for generating fdv2 protocol data. 2 | package ldservicesv2 3 | -------------------------------------------------------------------------------- /testhelpers/ldtestdata/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldtestdata provides a mechanism for providing dynamically updatable feature flag state in a 2 | // simplified form to an SDK client in test scenarios. The entry point for using this feature is 3 | // [DataSource]. 4 | // 5 | // Unlike the file data source (in the [github.com/launchdarkly/go-server-sdk/v7/ldfiledata] package), 6 | // this mechanism does not use any external resources. It provides only the data that the application 7 | // has put into it using the Update method. 8 | // 9 | // td := ldtestdata.DataSource() 10 | // td.Update(td.Flag("flag-key-1").BooleanFlag().VariationForAll(true)) 11 | // 12 | // config := ld.Config{ 13 | // DataSource: td, 14 | // } 15 | // client := ld.MakeCustomClient(sdkKey, config, timeout) 16 | // 17 | // // flags can be updated at any time: 18 | // td.Update(td.Flag("flag-key-2"). 19 | // VariationForUser("some-user-key", true). 20 | // FallthroughVariation(false)) 21 | // 22 | // The above example uses a simple boolean flag, but more complex configurations are possible using 23 | // the methods of the [FlagBuilder] that is returned by [TestDataSource.Flag]. FlagBuilder supports many of 24 | // the ways a flag can be configured on the LaunchDarkly dashboard, but does not currently support 1. 25 | // rule operators other than "in" and "not in", or 2. percentage rollouts. 26 | // 27 | // If the same TestDataSource instance is used to configure multiple LDClient instances, any change 28 | // made to the data will propagate to all of the LDClients. 29 | package ldtestdata 30 | -------------------------------------------------------------------------------- /testhelpers/ldtestdatav2/package_info.go: -------------------------------------------------------------------------------- 1 | // Package ldtestdatav2 provides a mechanism for providing dynamically updatable feature flag state in a 2 | // simplified form to an SDK client in test scenarios. The entry point for using this feature is 3 | // [DataSource]. 4 | // 5 | // Unlike the file data source (in the [github.com/launchdarkly/go-server-sdk/v7/ldfiledatav2] package), 6 | // this mechanism does not use any external resources. It provides only the data that the application 7 | // has put into it using the Update method. 8 | // 9 | // td := ldtestdatav2.DataSource() 10 | // td.Update(td.Flag("flag-key-1").BooleanFlag().VariationForAll(true)) 11 | // 12 | // config := ld.Config{ 13 | // DataSystem: ldcomponents.DataSystem().Custom().Synchronizers(td, nil) 14 | // } 15 | // client := ld.MakeCustomClient(sdkKey, config, timeout) 16 | // 17 | // // flags can be updated at any time: 18 | // td.Update(td.Flag("flag-key-2"). 19 | // VariationForUser("some-user-key", true). 20 | // FallthroughVariation(false)) 21 | // 22 | // The above example uses a simple boolean flag, but more complex configurations are possible using 23 | // the methods of the [FlagBuilder] that is returned by [TestDataSynchronizer.Flag]. FlagBuilder supports many of 24 | // the ways a flag can be configured on the LaunchDarkly dashboard, but does not currently support 1. 25 | // rule operators other than "in" and "not in", or 2. percentage rollouts. 26 | // 27 | // If the same TestDataSource instance is used to configure multiple LDClient instances, any change 28 | // made to the data will propagate to all of the LDClients. 29 | // 30 | // WARNING: This particular implementation supports the upcoming flag delivery v2 format which is not 31 | // publicly available. 32 | // 33 | // This package is not stable, and not subject to any backwards compatibility guarantees or semantic versioning. 34 | // It is not suitable for production usage. Do not use it. You have been warned. 35 | package ldtestdatav2 36 | -------------------------------------------------------------------------------- /testhelpers/package_info.go: -------------------------------------------------------------------------------- 1 | // Package testhelpers contains types and functions that may be useful in testing SDK functionality or 2 | // custom integrations. 3 | // 4 | // It contains two subpackages: 5 | // - [github.com/launchdarkly/go-server-sdk/v7/testhelpers/ldtestdata], which provides a test fixture 6 | // for setting flag values programmatically; 7 | // - [github.com/launchdarkly/go-server-sdk/v7/testhelpers/storetest], which provides a standard test 8 | // suite for custom persistent data store implementations. 9 | // 10 | // The APIs in this package and its subpackages are supported as part of the SDK. 11 | package testhelpers 12 | 13 | // Implementation note: the types and functions in this package are mainly meant for external use, but may 14 | // be useful in SDK tests. Anything that is *only* for SDK tests should be in internal/sharedtest instead. 15 | // Avoid putting anything here that depends on any packages other than interfaces, since then it might not 16 | // be possible to use it in other areas of the SDK without causing a cyclic reference (that's why storetest 17 | // is a separate package, so it can reference the main package). 18 | -------------------------------------------------------------------------------- /testhelpers/storetest/package_info.go: -------------------------------------------------------------------------------- 1 | // Package storetest contains the standard test suite for persistent data store implementations. 2 | // 3 | // If you are writing your own database integration, use this test suite to ensure that it is being 4 | // fully tested in the same way that all of the built-in ones are tested. 5 | // 6 | // Due to its dependencies, this package can only be used when building with module support. 7 | package storetest 8 | -------------------------------------------------------------------------------- /testservice/.gitignore: -------------------------------------------------------------------------------- 1 | testservice 2 | -------------------------------------------------------------------------------- /testservice/README.md: -------------------------------------------------------------------------------- 1 | # SDK contract test service 2 | 3 | This directory contains an implementation of the cross-platform SDK testing protocol defined by https://github.com/launchdarkly/sdk-test-harness. See that project's `README` for details of this protocol, and the kinds of SDK capabilities that are relevant to the contract tests. This code should not need to be updated unless the SDK has added or removed such capabilities. 4 | 5 | To run these tests locally, run `make contract-tests` from the SDK project root directory. This downloads the correct version of the test harness tool automatically. 6 | 7 | Or, to test against an in-progress local version of the test harness, run `make start-contract-test-service` from the SDK project root directory; then, in the root directory of the `sdk-test-harness` project, build the test harness and run it from the command line. 8 | -------------------------------------------------------------------------------- /testservice/big_segment_store_fixture.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/launchdarkly/go-sdk-common/v3/ldvalue" 5 | "github.com/launchdarkly/go-server-sdk/v7/subsystems" 6 | cf "github.com/launchdarkly/go-server-sdk/v7/testservice/servicedef/callbackfixtures" 7 | ) 8 | 9 | type BigSegmentStoreFixture struct { 10 | service *callbackService 11 | } 12 | 13 | type bigSegmentMembershipMap map[string]bool 14 | 15 | func (b *BigSegmentStoreFixture) Build(context subsystems.ClientContext) (subsystems.BigSegmentStore, error) { 16 | return b, nil 17 | } 18 | 19 | func (b *BigSegmentStoreFixture) Close() error { 20 | return b.service.close() 21 | } 22 | 23 | func (b *BigSegmentStoreFixture) GetMetadata() (subsystems.BigSegmentStoreMetadata, error) { 24 | var resp cf.BigSegmentStoreGetMetadataResponse 25 | if err := b.service.post(cf.BigSegmentStorePathGetMetadata, nil, &resp); err != nil { 26 | return subsystems.BigSegmentStoreMetadata{}, err 27 | } 28 | return subsystems.BigSegmentStoreMetadata(resp), nil 29 | } 30 | 31 | func (b *BigSegmentStoreFixture) GetMembership(contextHash string) (subsystems.BigSegmentMembership, error) { 32 | params := cf.BigSegmentStoreGetMembershipParams{ContextHash: contextHash, UserHash: contextHash} 33 | var resp cf.BigSegmentStoreGetMembershipResponse 34 | if err := b.service.post(cf.BigSegmentStorePathGetMembership, params, &resp); err != nil { 35 | return nil, err 36 | } 37 | return bigSegmentMembershipMap(resp.Values), nil 38 | } 39 | 40 | func (m bigSegmentMembershipMap) CheckMembership(segmentRef string) ldvalue.OptionalBool { 41 | if value, ok := m[segmentRef]; ok { 42 | return ldvalue.NewOptionalBool(value) 43 | } 44 | return ldvalue.OptionalBool{} 45 | } 46 | -------------------------------------------------------------------------------- /testservice/callback_service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | type callbackService struct { 13 | baseURL string 14 | } 15 | 16 | func (c *callbackService) close() error { 17 | req, _ := http.NewRequest("DELETE", c.baseURL, nil) 18 | _, err := http.DefaultClient.Do(req) 19 | return err 20 | } 21 | 22 | func (c *callbackService) post(path string, params interface{}, responseOut interface{}) error { 23 | url := c.baseURL + path 24 | var postBody io.Reader 25 | if params != nil { 26 | data, _ := json.Marshal(params) 27 | postBody = bytes.NewBuffer(data) 28 | } 29 | resp, err := http.DefaultClient.Post(url, "application/json", postBody) 30 | if err != nil { 31 | //e.logger.Printf("Callback to %s failed: %s", url, err) 32 | return err 33 | } 34 | var body []byte 35 | if resp.Body != nil { 36 | body, err = io.ReadAll(resp.Body) 37 | if err != nil { 38 | return err 39 | } 40 | _ = resp.Body.Close() 41 | } 42 | if resp.StatusCode >= 300 { 43 | message := "" 44 | if body != nil { 45 | message = " (" + string(body) + ")" 46 | } 47 | return fmt.Errorf("callback returned HTTP status %d%s", resp.StatusCode, message) 48 | } 49 | if responseOut != nil { 50 | if body == nil { 51 | return errors.New("expected a response body but got none") 52 | } 53 | if err = json.Unmarshal(body, responseOut); err != nil { 54 | return err 55 | } 56 | } 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /testservice/servicedef/callbackfixtures/big_segment_store.go: -------------------------------------------------------------------------------- 1 | package callbackfixtures 2 | 3 | import "github.com/launchdarkly/go-sdk-common/v3/ldtime" 4 | 5 | const ( 6 | BigSegmentStorePathGetMetadata = "/getMetadata" 7 | BigSegmentStorePathGetMembership = "/getMembership" 8 | ) 9 | 10 | type BigSegmentStoreGetMetadataResponse struct { 11 | LastUpToDate ldtime.UnixMillisecondTime `json:"lastUpToDate"` 12 | } 13 | 14 | type BigSegmentStoreGetMembershipParams struct { 15 | ContextHash string `json:"contextHash"` 16 | UserHash string `json:"userHash"` // temporary, for compatibility with current sdk-test-harness 17 | } 18 | 19 | type BigSegmentStoreGetMembershipResponse struct { 20 | Values map[string]bool `json:"values,omitempty"` 21 | } 22 | -------------------------------------------------------------------------------- /testservice/servicedef/callbackfixtures/persistent_data_store.go: -------------------------------------------------------------------------------- 1 | package callbackfixtures 2 | 3 | type DataStoreSerializedCollection struct { 4 | Kind string `json:"kind"` 5 | Items []DataStoreSerializedKeyedItem `json:"items"` 6 | } 7 | 8 | type DataStoreSerializedKeyedItem struct { 9 | Key string `json:"key"` 10 | Item DataStoreSerializedItem `json:"item"` 11 | } 12 | 13 | type DataStoreSerializedItem struct { 14 | Version int `json:"version"` 15 | SerializedItem string `json:"serializedItem,omitempty"` 16 | } 17 | 18 | type DataStoreStatusResponse struct { 19 | Available bool `json:"available"` 20 | Initialized bool `json:"initialized"` 21 | } 22 | 23 | type DataStoreInitParams struct { 24 | AllData []DataStoreSerializedCollection 25 | } 26 | 27 | type DataStoreGetParams struct { 28 | Kind string `json:"kind"` 29 | Key string `json:"key"` 30 | } 31 | 32 | type DataStoreGetResponse struct { 33 | Item *DataStoreSerializedItem `json:"item,omitempty"` 34 | } 35 | 36 | type DataStoreGetAllParams struct { 37 | Kind string `json:"kind"` 38 | } 39 | 40 | type DataStoreGetAllResponse struct { 41 | Items []DataStoreSerializedKeyedItem `json:"items"` 42 | } 43 | 44 | type DataStoreUpsertParams struct { 45 | Kind string `json:"kind"` 46 | Key string `json:"key"` 47 | Item DataStoreSerializedItem `json:"item"` 48 | } 49 | 50 | type DataStoreUpsertResponse struct { 51 | Updated bool `json:"updated"` 52 | } 53 | -------------------------------------------------------------------------------- /testservice/servicedef/service_params.go: -------------------------------------------------------------------------------- 1 | package servicedef 2 | 3 | const ( 4 | CapabilityClientSide = "client-side" 5 | CapabilityServerSide = "server-side" 6 | CapabilityStronglyTyped = "strongly-typed" 7 | 8 | CapabilityAllFlagsWithReasons = "all-flags-with-reasons" 9 | CapabilityAllFlagsClientSideOnly = "all-flags-client-side-only" 10 | CapabilityAllFlagsDetailsOnlyForTrackedFlags = "all-flags-details-only-for-tracked-flags" 11 | 12 | CapabilityBigSegments = "big-segments" 13 | CapabilitySecureModeHash = "secure-mode-hash" 14 | CapabilityServerSidePolling = "server-side-polling" 15 | CapabilityServiceEndpoints = "service-endpoints" 16 | CapabilityTags = "tags" 17 | CapabilityFiltering = "filtering" 18 | CapabilityContextType = "context-type" 19 | CapabilityMigrations = "migrations" 20 | CapabilityEventSampling = "event-sampling" 21 | CapabilityInlineContextAll = "inline-context-all" 22 | CapabilityAnonymousRedaction = "anonymous-redaction" 23 | CapabilityEvaluationHooks = "evaluation-hooks" 24 | CapabilityOmitAnonymousContexts = "omit-anonymous-contexts" 25 | CapabilityEventGzip = "event-gzip" 26 | CapabilityOptionalEventGzip = "optional-event-gzip" 27 | CapabilityClientPrereqEvents = "client-prereq-events" 28 | CapabilityPersistentDataStoreRedis = "persistent-data-store-redis" 29 | CapabilityPersistentDataStoreConsul = "persistent-data-store-consul" 30 | CapabilityPersistentDataStoreDynamoDB = "persistent-data-store-dynamodb" 31 | ) 32 | 33 | type StatusRep struct { 34 | // Name is the name of the project that the test service is testing, such as "go-server-sdk". 35 | Name string `json:"name"` 36 | 37 | // Capabilities is a list of strings representing optional features of the test service. 38 | Capabilities []string `json:"capabilities"` 39 | 40 | ClientVersion string `json:"clientVersion"` 41 | } 42 | 43 | type CreateInstanceParams struct { 44 | Configuration SDKConfigParams `json:"configuration"` 45 | Tag string `json:"tag"` 46 | } 47 | --------------------------------------------------------------------------------