diff --git a/.github/workflows/browser.yml b/.github/workflows/browser.yml new file mode 100644 index 000000000..da12b51e5 --- /dev/null +++ b/.github/workflows/browser.yml @@ -0,0 +1,33 @@ +name: sdk/browser + +on: + push: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' + +jobs: + build-test-browser: + runs-on: ubuntu-latest + + strategy: + matrix: + # Node versions to run on. + version: [18, 21] + + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.version }} + registry-url: 'https://registry.npmjs.org' + - id: shared + name: Shared CI Steps + uses: ./actions/ci + with: + workspace_name: '@launchdarkly/js-client-sdk' + workspace_path: packages/sdk/browser diff --git a/.github/workflows/manual-publish-docs.yml b/.github/workflows/manual-publish-docs.yml index a56d43212..397bc1a58 100644 --- a/.github/workflows/manual-publish-docs.yml +++ b/.github/workflows/manual-publish-docs.yml @@ -19,6 +19,7 @@ on: - packages/store/node-server-sdk-redis - packages/store/node-server-sdk-dynamodb - packages/telemetry/node-server-sdk-otel + - packages/sdk/browser name: Publish Documentation jobs: build-publish: diff --git a/.github/workflows/manual-publish.yml b/.github/workflows/manual-publish.yml index f8a463ff8..c8ba253ad 100644 --- a/.github/workflows/manual-publish.yml +++ b/.github/workflows/manual-publish.yml @@ -32,6 +32,7 @@ on: - packages/store/node-server-sdk-dynamodb - packages/telemetry/node-server-sdk-otel - packages/tooling/jest + - packages/sdk/browser prerelease: description: 'Is this a prerelease. If so, then the latest tag will not be updated in npm.' type: boolean diff --git a/.github/workflows/react-native-detox.yml b/.github/workflows/react-native-detox.yml new file mode 100644 index 000000000..d42886a67 --- /dev/null +++ b/.github/workflows/react-native-detox.yml @@ -0,0 +1,95 @@ +name: sdk/react-native/example + +# The example builds independently of react-native because of the duration of the build. +# We limit it to only build under specific circumstances. +# Additionally this does allow for scheduled builds of just the example, to handle changes in expo, +# should they be desired. + +on: + push: + branches: [main, 'feat/**'] + paths-ignore: + - '**.md' #Do not need to run CI for markdown changes. + pull_request: + branches: [main, 'feat/**'] + paths: + - 'packages/shared/common/**' + - 'packages/shared/sdk-client/**' + - 'packages/sdk/react-native/**' + - 'packages/shared/mocks/**' + +jobs: + detox-android: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + defaults: + run: + working-directory: packages/sdk/react-native/example + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + + - name: Install deps + run: yarn workspaces focus + - name: Build + run: yarn workspaces foreach -pR --topological-dev --from 'react-native-example' run build + + - uses: ./actions/release-secrets + name: 'Get mobile key' + with: + aws_assume_role: ${{ vars.AWS_ROLE_ARN_EXAMPLES }} + ssm_parameter_pairs: '/sdk/common/hello-apps/mobile-key = MOBILE_KEY, + /sdk/common/hello-apps/boolean-flag-key = LAUNCHDARKLY_FLAG_KEY' + + - name: Create .env file. + run: echo "MOBILE_KEY=$MOBILE_KEY" > .env + + - name: Enable KVM group perms (for performance) + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - name: Expo Prebuild + run: npx expo prebuild + + # Java setup is after checkout and expo prebuild so that it can locate the + # gradle configuration. + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 17 + cache: 'gradle' + + - name: Detox build + run: yarn detox build --configuration android.emu.release + + - name: Get android emulator device name + id: device + run: node -e "console.log('AVD_NAME=' + require('./.detoxrc').devices.emulator.device.avdName)" >> $GITHUB_OUTPUT + + - name: Make space for the emulator. + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be + with: + android: false # We need android. + + - name: Detox test + uses: reactivecircus/android-emulator-runner@f0d1ed2dcad93c7479e8b2f2226c83af54494915 + with: + api-level: 31 + arch: x86_64 + avd-name: ${{ steps.device.outputs.AVD_NAME }} + working-directory: packages/sdk/react-native/example + script: yarn detox test --configuration android.emu.release --headless --record-logs all + + - name: Upload artifacts + if: always() + uses: actions/upload-artifact@v4 + with: + name: detox-artifacts + path: packages/sdk/react-native/example/artifacts diff --git a/.github/workflows/react-native.yml b/.github/workflows/react-native.yml index a9eb6945e..de88b8e1e 100644 --- a/.github/workflows/react-native.yml +++ b/.github/workflows/react-native.yml @@ -22,65 +22,3 @@ jobs: with: workspace_name: '@launchdarkly/react-native-client-sdk' workspace_path: packages/sdk/react-native - detox-ios: - # TODO: disable detox for now because it's unstable. - if: false - # macos-latest uses macos-12 and we need macos-14 to get xcode 15. - # https://github.com/actions/runner-images/blob/main/README.md - runs-on: macos-14 - permissions: - id-token: write - contents: read - defaults: - run: - working-directory: packages/sdk/react-native/example - steps: - - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 - - name: Install deps - run: yarn workspaces focus - - name: Build - run: yarn workspaces foreach -pR --topological-dev --from 'react-native-example' run build - - name: Install macOS dependencies - run: | - brew tap wix/brew - brew install applesimutils - env: - HOMEBREW_NO_AUTO_UPDATE: 1 - HOMEBREW_NO_INSTALL_CLEANUP: 1 - - - name: Cache Detox build - id: cache-detox-build - uses: actions/cache@v4 - with: - path: ios/build - key: ${{ runner.os }}-detox-build - restore-keys: | - ${{ runner.os }}-detox-build - - - name: Detox rebuild framework cache - run: yarn detox rebuild-framework-cache - - - uses: ./actions/release-secrets - name: 'Get mobile key' - with: - aws_assume_role: ${{ vars.AWS_ROLE_ARN }} - ssm_parameter_pairs: '/sdk/detox/mobile-key = MOBILE_KEY' - - - name: Set mobile key - run: echo "MOBILE_KEY=$MOBILE_KEY" > .env - - - name: Expo prebuild - # HACK: Deleting ios/.xcode.env.local is needed to solve an xcode build issue with rn 0.73 - # https://github.com/facebook/react-native/issues/42112#issuecomment-1884536225 - run: | - export NO_FLIPPER=1 - yarn expo-prebuild - rm -rf ./ios/.xcode.env.local - - - name: Detox build - run: yarn detox build --configuration ios.sim.release - - - name: Detox test - run: yarn detox test --configuration ios.sim.release --cleanup --headless --record-logs all --take-screenshots failing diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 99a941690..08f1798d6 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -52,11 +52,11 @@ jobs: release-sdk-client: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-common'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-sdk-client-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-sdk-client-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -72,11 +72,11 @@ jobs: release-sdk-server: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-common'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-sdk-server-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-sdk-server-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -92,11 +92,11 @@ jobs: release-sdk-server-edge: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-sdk-server'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-sdk-server-edge-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-sdk-server-edge-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -112,11 +112,11 @@ jobs: release-akamai-edgeworker-sdk: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-sdk-server'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-akamai-edgeworker-sdk-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-akamai-edgeworker-sdk-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -132,11 +132,11 @@ jobs: release-cloudflare: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-sdk-server-edge'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-cloudflare-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-cloudflare-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -152,11 +152,11 @@ jobs: release-react-native: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-sdk-client'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-react-native-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-react-native-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -172,11 +172,11 @@ jobs: release-server-node: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-sdk-server'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-server-node-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-server-node-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -192,11 +192,11 @@ jobs: release-vercel: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-sdk-server-edge'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-vercel-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-vercel-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -212,11 +212,11 @@ jobs: release-akamai-base: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-akamai-edgeworker-sdk'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-akamai-base-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-akamai-base-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -232,11 +232,11 @@ jobs: release-akamai-edgekv: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-akamai-edgeworker-sdk'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-akamai-edgekv-released == 'true'}} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-akamai-edgekv-released == 'true'}} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -252,11 +252,11 @@ jobs: release-node-server-sdk-redis: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-server-node'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-node-server-sdk-redis-release == 'true' }} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-node-server-sdk-redis-release == 'true' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -276,11 +276,11 @@ jobs: release-node-server-sdk-dynamodb: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-server-node'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-node-server-sdk-dynamodb-release == 'true' }} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-node-server-sdk-dynamodb-release == 'true' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -298,11 +298,11 @@ jobs: release-node-server-sdk-otel: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-server-node'] permissions: id-token: write contents: write - if: ${{ needs.release-please.outputs.package-node-server-sdk-otel-release == 'true' }} + if: ${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-node-server-sdk-otel-release == 'true' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 @@ -339,11 +339,11 @@ jobs: release-tooling-react-universal: runs-on: ubuntu-latest - needs: ['release-please'] + needs: ['release-please', 'release-server-node', 'release-sdk-client'] permissions: id-token: write contents: write - if: false #${{ needs.release-please.outputs.package-react-universal-release == 'true' }} + if: false #${{ always() && !failure() && !cancelled() && needs.release-please.outputs.package-react-universal-release == 'true' }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 41c8f8c48..65537cbda 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,16 +1,16 @@ { - "packages/shared/common": "2.6.0", - "packages/shared/sdk-server": "2.4.5", - "packages/sdk/server-node": "9.5.1", - "packages/sdk/cloudflare": "2.5.10", - "packages/shared/sdk-server-edge": "2.3.6", - "packages/sdk/vercel": "1.3.13", - "packages/sdk/akamai-base": "2.1.12", - "packages/sdk/akamai-edgekv": "1.1.12", - "packages/shared/akamai-edgeworker-sdk": "1.1.12", - "packages/store/node-server-sdk-dynamodb": "6.1.18", - "packages/store/node-server-sdk-redis": "4.1.18", - "packages/shared/sdk-client": "1.5.0", - "packages/sdk/react-native": "10.5.1", - "packages/telemetry/node-server-sdk-otel": "1.0.10" + "packages/shared/common": "2.8.0", + "packages/shared/sdk-server": "2.6.1", + "packages/sdk/server-node": "9.5.4", + "packages/sdk/cloudflare": "2.5.13", + "packages/shared/sdk-server-edge": "2.3.9", + "packages/sdk/vercel": "1.3.16", + "packages/sdk/akamai-base": "2.1.15", + "packages/sdk/akamai-edgekv": "1.1.15", + "packages/shared/akamai-edgeworker-sdk": "1.1.15", + "packages/store/node-server-sdk-dynamodb": "6.1.21", + "packages/store/node-server-sdk-redis": "4.1.21", + "packages/shared/sdk-client": "1.7.0", + "packages/sdk/react-native": "10.6.1", + "packages/telemetry/node-server-sdk-otel": "1.0.13" } diff --git a/.sdk_metadata.json b/.sdk_metadata.json index 22fb38ba7..fe6500fad 100644 --- a/.sdk_metadata.json +++ b/.sdk_metadata.json @@ -5,10 +5,7 @@ "name": "Akamai SDK", "type": "edge", "path": "packages/sdk/akamai-base", - "languages": [ - "JavaScript", - "TypeScript" - ], + "languages": ["JavaScript", "TypeScript"], "releases": { "tag-prefix": "akamai-server-base-sdk-" } @@ -17,10 +14,7 @@ "name": "Akamai SDK for EdgeKV", "type": "edge", "path": "packages/sdk/akamai-edgekv", - "languages": [ - "JavaScript", - "TypeScript" - ], + "languages": ["JavaScript", "TypeScript"], "releases": { "tag-prefix": "akamai-server-edgekv-sdk-" } @@ -29,10 +23,7 @@ "name": "Cloudflare SDK", "type": "edge", "path": "packages/sdk/cloudflare", - "languages": [ - "JavaScript", - "TypeScript" - ], + "languages": ["JavaScript", "TypeScript"], "releases": { "tag-prefix": "cloudflare-server-sdk-" } @@ -41,10 +32,7 @@ "name": "React Native SDK", "type": "client-side", "path": "packages/sdk/react-native", - "languages": [ - "JavaScript", - "TypeScript" - ], + "languages": ["JavaScript", "TypeScript"], "releases": { "tag-prefix": "react-native-client-sdk-" } @@ -53,10 +41,7 @@ "name": "Node.js Server SDK", "type": "server-side", "path": "packages/sdk/server-node", - "languages": [ - "JavaScript", - "TypeScript" - ], + "languages": ["JavaScript", "TypeScript"], "releases": { "tag-prefix": "node-server-sdk-" } @@ -65,10 +50,7 @@ "name": "Vercel Edge SDK", "type": "edge", "path": "packages/sdk/vercel", - "languages": [ - "JavaScript", - "TypeScript" - ], + "languages": ["JavaScript", "TypeScript"], "releases": { "tag-prefix": "vercel-server-sdk-" } diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3cde4e2eb..dbcde575e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -27,14 +27,20 @@ To install project dependencies, from the project root directory: yarn ``` -### Testing +### Build -To run all unit tests: +To build all projects, from the root directory: ``` -yarn test +yarn build ``` +### Testing + +Unit tests should be implemented in a `__tests__` folder in the root of the package. The directory structure inside of `__tests__` should mirror that of the source directory. + +Each package has its own testing requirements and tests should be only ran for single projects. + To run the SDK contract test suite (see [`contract-tests/README.md`](./contract-tests/README.md)): The SDK contract test suite will run the Node.js Server version of the SDK. diff --git a/PROVENANCE.md b/PROVENANCE.md index 2698c41a3..b47226935 100644 --- a/PROVENANCE.md +++ b/PROVENANCE.md @@ -2,6 +2,6 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. -As part of [SLSA requirements for level 3 compliance](https://slsa.dev/spec/v1.0/requirements), LaunchDarkly publishes provenance attestations about our SDK package builds to npm for distribution alongside our packages. +As part of [SLSA requirements for level 3 compliance](https://slsa.dev/spec/v1.0/requirements), LaunchDarkly publishes provenance attestations about our SDK package builds to npm for distribution alongside our packages. -For npm packages that are published with provenance, npm automatically [verifies the authenticity of the package using Sigstore](https://docs.npmjs.com/generating-provenance-statements#about-npm-provenance). +For npm packages that are published with provenance, npm automatically [verifies the authenticity of the package using Sigstore](https://docs.npmjs.com/generating-provenance-statements#about-npm-provenance). diff --git a/README.md b/README.md index c50a78700..fd6ef6088 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,11 @@ This includes shared libraries, used by SDKs and other tools, as well as SDKs. | [@launchdarkly/vercel-server-sdk](packages/sdk/vercel/README.md) | [![NPM][sdk-vercel-npm-badge]][sdk-vercel-npm-link] | [Vercel][package-sdk-vercel-issues] | [![Actions Status][sdk-vercel-ci-badge]][sdk-vercel-ci] | | [@launchdarkly/react-native-client-sdk](packages/sdk/react-native/README.md) | [![NPM][sdk-react-native-npm-badge]][sdk-react-native-npm-link] | [React-Native][package-sdk-react-native-issues] | [![Actions Status][sdk-react-native-ci-badge]][sdk-react-native-ci] | + + | Shared packages | npm | issues | tests | | ------------------------------------------------------------------------------------ | ------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------- | | [@launchdarkly/js-sdk-common](packages/shared/common/README.md) | [![NPM][common-npm-badge]][common-npm-link] | [Common][package-shared-common-issues] | [![Actions Status][shared-common-ci-badge]][shared-common-ci] | @@ -26,9 +31,9 @@ This includes shared libraries, used by SDKs and other tools, as well as SDKs. | [@launchdarkly/node-server-sdk-redis](packages/store/node-server-sdk-redis/README.md) | [![NPM][node-redis-npm-badge]][node-redis-npm-link] | [Node Redis][node-redis-issues] | [![Actions Status][node-redis-ci-badge]][node-redis-ci] | | [@launchdarkly/node-server-sdk-dynamodb](packages/store/node-server-sdk-dynamodb/README.md) | [![NPM][node-dynamodb-npm-badge]][node-dynamodb-npm-link] | [Node DynamoDB][node-dynamodb-issues] | [![Actions Status][node-dynamodb-ci-badge]][node-dynamodb-ci] | -| Telemetry Packages | npm | issues | tests | -| ------------------------------------------------------------------------------------------- | --------------------------------------------------------- | ------------------------------------- | ------------------------------------------------------------- | -| [@launchdarkly/node-server-sdk-otel](packages/telemetry/node-server-sdk-otel/README.md) | [![NPM][node-otel-npm-badge]][node-otel-npm-link] | [Node OTel][node-otel-issues] | [![Actions Status][node-otel-ci-badge]][node-otel-ci] | +| Telemetry Packages | npm | issues | tests | +| --------------------------------------------------------------------------------------- | ------------------------------------------------- | ----------------------------- | ----------------------------------------------------- | +| [@launchdarkly/node-server-sdk-otel](packages/telemetry/node-server-sdk-otel/README.md) | [![NPM][node-otel-npm-badge]][node-otel-npm-link] | [Node OTel][node-otel-issues] | [![Actions Status][node-otel-ci-badge]][node-otel-ci] | ## Organization @@ -62,7 +67,7 @@ We encourage pull requests and other contributions from the community. Check out - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly @@ -168,4 +173,14 @@ We encourage pull requests and other contributions from the community. Check out [node-otel-ci]: https://github.com/launchdarkly/js-core/actions/workflows/node-otel.yml [node-otel-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/node-server-sdk-otel.svg?style=flat-square [node-otel-npm-link]: https://www.npmjs.com/package/@launchdarkly/node-server-sdk-otel -[node-otel-issues]: https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+telemetry%2Fnode-server-sdk-otel%22+ \ No newline at end of file +[node-otel-issues]: https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+telemetry%2Fnode-server-sdk-otel%22+ +[//]: # 'sdk/browser' +[sdk-browser-ci-badge]: https://github.com/launchdarkly/js-core/actions/workflows/browser.yml/badge.svg +[sdk-browser-ci]: https://github.com/launchdarkly/js-core/actions/workflows/browser.yml +[sdk-browser-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/js-client-sdk.svg?style=flat-square +[sdk-browser-npm-link]: https://www.npmjs.com/package/@launchdarkly/js-client-sdk +[sdk-browser-ghp-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8 +[sdk-browser-ghp-link]: https://launchdarkly.github.io/js-core/packages/sdk/browser/docs/ +[sdk-browser-dm-badge]: https://img.shields.io/npm/dm/@launchdarkly/js-client-sdk.svg?style=flat-square +[sdk-browser-dt-badge]: https://img.shields.io/npm/dt/@launchdarkly/js-client-sdk.svg?style=flat-square +[package-sdk-browser-issues]: https://github.com/launchdarkly/js-core/issues?q=is%3Aissue+is%3Aopen+label%3A%22package%3A+sdk%2Fbrowser%22+ diff --git a/contract-tests/TestHook.js b/contract-tests/TestHook.js index 0c3457eb3..548971b36 100644 --- a/contract-tests/TestHook.js +++ b/contract-tests/TestHook.js @@ -24,7 +24,7 @@ export default class TestHook { } beforeEvaluation(hookContext, data) { - if(this._errors?.beforeEvaluation) { + if (this._errors?.beforeEvaluation) { throw new Error(this._errors.beforeEvaluation); } this._safePost({ @@ -36,7 +36,7 @@ export default class TestHook { } afterEvaluation(hookContext, data, detail) { - if(this._errors?.afterEvaluation) { + if (this._errors?.afterEvaluation) { throw new Error(this._errors.afterEvaluation); } this._safePost({ @@ -46,7 +46,6 @@ export default class TestHook { evaluationDetail: detail, }); - return { ...data, ...(this._data?.['afterEvaluation'] || {}) }; } } diff --git a/contract-tests/index.js b/contract-tests/index.js index 3b507ffca..e3716a367 100644 --- a/contract-tests/index.js +++ b/contract-tests/index.js @@ -27,6 +27,8 @@ app.get('/', (req, res) => { 'all-flags-with-reasons', 'tags', 'big-segments', + 'filtering', + 'filtering-strict', 'user-type', 'migrations', 'event-sampling', @@ -35,7 +37,7 @@ app.get('/', (req, res) => { 'inline-context', 'anonymous-redaction', 'evaluation-hooks', - 'wrapper' + 'wrapper', ], }); }); diff --git a/contract-tests/sdkClientEntity.js b/contract-tests/sdkClientEntity.js index 930afdeeb..7301ffacf 100644 --- a/contract-tests/sdkClientEntity.js +++ b/contract-tests/sdkClientEntity.js @@ -26,11 +26,17 @@ export function makeSdkConfig(options, tag) { if (options.streaming) { cf.streamUri = options.streaming.baseUri; cf.streamInitialReconnectDelay = maybeTime(options.streaming.initialRetryDelayMs); + if (options.streaming.filter) { + cf.payloadFilterKey = options.streaming.filter; + } } if (options.polling) { cf.stream = false; cf.baseUri = options.polling.baseUri; cf.pollInterface = options.polling.pollIntervalMs / 1000; + if (options.polling.filter) { + cf.payloadFilterKey = options.polling.filter; + } } if (options.events) { cf.allAttributesPrivate = options.events.allAttributesPrivate; @@ -68,10 +74,10 @@ export function makeSdkConfig(options, tag) { ); } if (options.wrapper) { - if(options.wrapper.name) { + if (options.wrapper.name) { cf.wrapperName = options.wrapper.name; } - if(options.wrapper.version) { + if (options.wrapper.version) { cf.wrapperVersion = options.wrapper.version; } } @@ -117,7 +123,7 @@ export async function newSdkClientEntity(options) { makeSdkConfig(options.configuration, options.tag), ); try { - await client.waitForInitialization({timeout: timeout}); + await client.waitForInitialization({ timeout: timeout }); } catch (_) { // if waitForInitialization() rejects, the client failed to initialize, see next line } diff --git a/package.json b/package.json index 6789419f7..3fa5d4200 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "packages/store/node-server-sdk-redis", "packages/store/node-server-sdk-dynamodb", "packages/telemetry/node-server-sdk-otel", - "packages/tooling/jest" + "packages/tooling/jest", + "packages/sdk/browser" ], "private": true, "scripts": { diff --git a/packages/sdk/akamai-base/CHANGELOG.md b/packages/sdk/akamai-base/CHANGELOG.md index 55b5f9a81..743a33cde 100644 --- a/packages/sdk/akamai-base/CHANGELOG.md +++ b/packages/sdk/akamai-base/CHANGELOG.md @@ -30,6 +30,36 @@ All notable changes to the LaunchDarkly SDK for Akamai Workers will be documente * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.1 to ^1.1.2 * @launchdarkly/js-server-sdk-common bumped from ^2.2.1 to ^2.2.2 +## [2.1.15](https://github.com/launchdarkly/js-core/compare/akamai-server-base-sdk-v2.1.14...akamai-server-base-sdk-v2.1.15) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.14 to ^1.1.15 + * @launchdarkly/js-server-sdk-common bumped from ^2.6.0 to ^2.6.1 + +## [2.1.14](https://github.com/launchdarkly/js-core/compare/akamai-server-base-sdk-v2.1.13...akamai-server-base-sdk-v2.1.14) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.13 to ^1.1.14 + * @launchdarkly/js-server-sdk-common bumped from ^2.5.0 to ^2.6.0 + +## [2.1.13](https://github.com/launchdarkly/js-core/compare/akamai-server-base-sdk-v2.1.12...akamai-server-base-sdk-v2.1.13) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.12 to ^1.1.13 + * @launchdarkly/js-server-sdk-common bumped from ^2.4.5 to ^2.5.0 + ## [2.1.12](https://github.com/launchdarkly/js-core/compare/akamai-server-base-sdk-v2.1.11...akamai-server-base-sdk-v2.1.12) (2024-08-12) diff --git a/packages/sdk/akamai-base/README.md b/packages/sdk/akamai-base/README.md index 1ab47e5dd..b38e2699b 100644 --- a/packages/sdk/akamai-base/README.md +++ b/packages/sdk/akamai-base/README.md @@ -32,7 +32,7 @@ yarn test ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -40,7 +40,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/sdk/akamai-base/src/__tests__/index.test.ts b/packages/sdk/akamai-base/__tests__/index.test.ts similarity index 98% rename from packages/sdk/akamai-base/src/__tests__/index.test.ts rename to packages/sdk/akamai-base/__tests__/index.test.ts index f70caae05..e62833ea2 100644 --- a/packages/sdk/akamai-base/src/__tests__/index.test.ts +++ b/packages/sdk/akamai-base/__tests__/index.test.ts @@ -1,4 +1,4 @@ -import { EdgeProvider, init } from '../index'; +import { EdgeProvider, init } from '../src/index'; import * as testData from './testData.json'; const sdkKey = 'test-sdk-key'; diff --git a/packages/sdk/akamai-base/src/__tests__/testData.json b/packages/sdk/akamai-base/__tests__/testData.json similarity index 100% rename from packages/sdk/akamai-base/src/__tests__/testData.json rename to packages/sdk/akamai-base/__tests__/testData.json diff --git a/packages/sdk/akamai-base/package.json b/packages/sdk/akamai-base/package.json index 5d5fd1b42..f054b557d 100644 --- a/packages/sdk/akamai-base/package.json +++ b/packages/sdk/akamai-base/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/akamai-server-base-sdk", - "version": "2.1.12", + "version": "2.1.15", "description": "Akamai LaunchDarkly EdgeWorker SDK", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/akamai-base", "repository": { @@ -73,7 +73,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/akamai-edgeworker-sdk-common": "^1.1.12", - "@launchdarkly/js-server-sdk-common": "^2.4.5" + "@launchdarkly/akamai-edgeworker-sdk-common": "^1.1.15", + "@launchdarkly/js-server-sdk-common": "^2.6.1" } } diff --git a/packages/sdk/akamai-edgekv/CHANGELOG.md b/packages/sdk/akamai-edgekv/CHANGELOG.md index 64fed2afa..aa93d41ce 100644 --- a/packages/sdk/akamai-edgekv/CHANGELOG.md +++ b/packages/sdk/akamai-edgekv/CHANGELOG.md @@ -31,6 +31,36 @@ All notable changes to the LaunchDarkly SDK for Akamai Workers will be documente * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.1 to ^1.1.2 * @launchdarkly/js-server-sdk-common bumped from ^2.2.1 to ^2.2.2 +## [1.1.15](https://github.com/launchdarkly/js-core/compare/akamai-server-edgekv-sdk-v1.1.14...akamai-server-edgekv-sdk-v1.1.15) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.14 to ^1.1.15 + * @launchdarkly/js-server-sdk-common bumped from ^2.6.0 to ^2.6.1 + +## [1.1.14](https://github.com/launchdarkly/js-core/compare/akamai-server-edgekv-sdk-v1.1.13...akamai-server-edgekv-sdk-v1.1.14) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.13 to ^1.1.14 + * @launchdarkly/js-server-sdk-common bumped from ^2.5.0 to ^2.6.0 + +## [1.1.13](https://github.com/launchdarkly/js-core/compare/akamai-server-edgekv-sdk-v1.1.12...akamai-server-edgekv-sdk-v1.1.13) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/akamai-edgeworker-sdk-common bumped from ^1.1.12 to ^1.1.13 + * @launchdarkly/js-server-sdk-common bumped from ^2.4.5 to ^2.5.0 + ## [1.1.12](https://github.com/launchdarkly/js-core/compare/akamai-server-edgekv-sdk-v1.1.11...akamai-server-edgekv-sdk-v1.1.12) (2024-08-12) diff --git a/packages/sdk/akamai-edgekv/README.md b/packages/sdk/akamai-edgekv/README.md index 72b38b184..48b77ff45 100644 --- a/packages/sdk/akamai-edgekv/README.md +++ b/packages/sdk/akamai-edgekv/README.md @@ -32,7 +32,7 @@ yarn test ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -40,7 +40,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/sdk/akamai-edgekv/src/__tests__/edgekv/edgeKVProvider.test.ts b/packages/sdk/akamai-edgekv/__tests__/edgekv/edgeKVProvider.test.ts similarity index 85% rename from packages/sdk/akamai-edgekv/src/__tests__/edgekv/edgeKVProvider.test.ts rename to packages/sdk/akamai-edgekv/__tests__/edgekv/edgeKVProvider.test.ts index 13d61d3d8..b97366dd2 100644 --- a/packages/sdk/akamai-edgekv/src/__tests__/edgekv/edgeKVProvider.test.ts +++ b/packages/sdk/akamai-edgekv/__tests__/edgekv/edgeKVProvider.test.ts @@ -1,7 +1,7 @@ -import { EdgeKV } from '../../edgekv/edgekv'; -import EdgeKVProvider from '../../edgekv/edgeKVProvider'; +import { EdgeKV } from '../../src/edgekv/edgekv'; +import EdgeKVProvider from '../../src/edgekv/edgeKVProvider'; -jest.mock('../../edgekv/edgekv', () => ({ +jest.mock('../../src/edgekv/edgekv', () => ({ EdgeKV: jest.fn(), })); diff --git a/packages/sdk/akamai-edgekv/src/__tests__/index.test.ts b/packages/sdk/akamai-edgekv/__tests__/index.test.ts similarity index 96% rename from packages/sdk/akamai-edgekv/src/__tests__/index.test.ts rename to packages/sdk/akamai-edgekv/__tests__/index.test.ts index 778a37e2a..90fd9e924 100644 --- a/packages/sdk/akamai-edgekv/src/__tests__/index.test.ts +++ b/packages/sdk/akamai-edgekv/__tests__/index.test.ts @@ -1,8 +1,8 @@ -import EdgeKVProvider from '../edgekv/edgeKVProvider'; -import { init as initWithEdgeKV, LDClient, LDContext } from '../index'; +import EdgeKVProvider from '../src/edgekv/edgeKVProvider'; +import { init as initWithEdgeKV, LDClient, LDContext } from '../src/index'; import * as testData from './testData.json'; -jest.mock('../edgekv/edgekv', () => ({ +jest.mock('../src/edgekv/edgekv', () => ({ EdgeKV: jest.fn(), })); diff --git a/packages/sdk/akamai-edgekv/src/__tests__/testData.json b/packages/sdk/akamai-edgekv/__tests__/testData.json similarity index 100% rename from packages/sdk/akamai-edgekv/src/__tests__/testData.json rename to packages/sdk/akamai-edgekv/__tests__/testData.json diff --git a/packages/sdk/akamai-edgekv/package.json b/packages/sdk/akamai-edgekv/package.json index d0234a560..aaf01a649 100644 --- a/packages/sdk/akamai-edgekv/package.json +++ b/packages/sdk/akamai-edgekv/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/akamai-server-edgekv-sdk", - "version": "1.1.12", + "version": "1.1.15", "description": "Akamai LaunchDarkly EdgeWorker SDK for EdgeKV feature store", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/akamai-edgekv", "repository": { @@ -73,7 +73,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/akamai-edgeworker-sdk-common": "^1.1.12", - "@launchdarkly/js-server-sdk-common": "^2.4.5" + "@launchdarkly/akamai-edgeworker-sdk-common": "^1.1.15", + "@launchdarkly/js-server-sdk-common": "^2.6.1" } } diff --git a/packages/sdk/browser/LICENSE b/packages/sdk/browser/LICENSE new file mode 100644 index 000000000..50add35e7 --- /dev/null +++ b/packages/sdk/browser/LICENSE @@ -0,0 +1,13 @@ +Copyright 2024 Catamorphic, Co. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/packages/sdk/browser/README.md b/packages/sdk/browser/README.md new file mode 100644 index 000000000..d171ca1d9 --- /dev/null +++ b/packages/sdk/browser/README.md @@ -0,0 +1,59 @@ +# LaunchDarkly JavaScript SDK for Browsers + + + +> [!CAUTION] +> This library is a beta version and should not be considered ready for production use while this message is visible. + + + +## Getting started + +Refer to the [SDK documentation](https://docs.launchdarkly.com/sdk/client-side/javascript#getting-started) for instructions on getting started with using the SDK. + +Note: _If you are using JavaScript in a non-browser environment, please check our other SDK packages in [js-core](https://github.com/launchdarkly/js-core)_ +Please note that the JavaScript SDK has two special requirements in terms of your LaunchDarkly environment. First, in terms of the credentials for your environment that appear on your [Account Settings](https://app.launchdarkly.com/settings/projects) dashboard, the JavaScript SDK uses the "Client-side ID"-- not the "SDK key" or the "Mobile key". Second, for any feature flag that you will be using in JavaScript code, you must check the "Make this flag available to client-side SDKs" box on that flag's Settings page. + +## Verifying SDK build provenance with the SLSA framework + +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). + +## About LaunchDarkly + +- LaunchDarkly is a continuous delivery platform that provides feature flags as a service and allows developers to iterate quickly and safely. We allow you to easily flag your features and manage them from the LaunchDarkly dashboard. With LaunchDarkly, you can: + - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. + - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). + - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Disable parts of your application to facilitate maintenance, without taking everything offline. +- LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. +- Explore LaunchDarkly + - [launchdarkly.com](https://www.launchdarkly.com/ 'LaunchDarkly Main Website') for more information + - [docs.launchdarkly.com](https://docs.launchdarkly.com/ 'LaunchDarkly Documentation') for our documentation and SDK reference guides + - [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ 'LaunchDarkly API Documentation') for our API documentation + - [blog.launchdarkly.com](https://blog.launchdarkly.com/ 'LaunchDarkly Blog Documentation') for the latest product updates + +[browser-sdk-ci-badge]: https://github.com/launchdarkly/js-core/actions/workflows/browser.yml/badge.svg +[browser-sdk-ci]: https://github.com/launchdarkly/js-core/actions/workflows/browser.yml +[browser-sdk-npm-badge]: https://img.shields.io/npm/v/@launchdarkly/js-client-sdk.svg?style=flat-square +[browser-sdk-npm-link]: https://www.npmjs.com/package/@launchdarkly/js-client-sdk +[browser-sdk-ghp-badge]: https://img.shields.io/static/v1?label=GitHub+Pages&message=API+reference&color=00add8 +[browser-sdk-ghp-link]: https://launchdarkly.github.io/js-core/packages/sdk/browser/docs/ +[browser-sdk-dm-badge]: https://img.shields.io/npm/dm/@launchdarkly/js-client-sdk.svg?style=flat-square +[browser-sdk-dt-badge]: https://img.shields.io/npm/dt/@launchdarkly/js-client-sdk.svg?style=flat-square diff --git a/packages/sdk/browser/__tests__/index.test.ts b/packages/sdk/browser/__tests__/index.test.ts new file mode 100644 index 000000000..1c44f4715 --- /dev/null +++ b/packages/sdk/browser/__tests__/index.test.ts @@ -0,0 +1,3 @@ +it('can run tests', () => { + expect(true).toBe(true); +}); diff --git a/packages/sdk/browser/__tests__/platform/LocalStorage.test.ts b/packages/sdk/browser/__tests__/platform/LocalStorage.test.ts new file mode 100644 index 000000000..477b348f0 --- /dev/null +++ b/packages/sdk/browser/__tests__/platform/LocalStorage.test.ts @@ -0,0 +1,139 @@ +import LocalStorage from '../../src/platform/LocalStorage'; + +it('can set values', async () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + // Storage here needs to be the global browser 'Storage' not the interface + // for our platform. + const spy = jest.spyOn(Storage.prototype, 'setItem'); + + const storage = new LocalStorage(logger); + storage.set('test-key', 'test-value'); + expect(spy).toHaveBeenCalledWith('test-key', 'test-value'); + + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('can handle an error setting a value', async () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + // Storage here needs to be the global browser 'Storage' not the interface + // for our platform. + const spy = jest.spyOn(Storage.prototype, 'setItem'); + spy.mockImplementation(() => { + throw new Error('bad'); + }); + + const storage = new LocalStorage(logger); + storage.set('test-key', 'test-value'); + + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + 'Error setting key in localStorage: test-key, reason: Error: bad', + ); +}); + +it('can get values', async () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + // Storage here needs to be the global browser 'Storage' not the interface + // for our platform. + const spy = jest.spyOn(Storage.prototype, 'getItem'); + + const storage = new LocalStorage(logger); + storage.get('test-key'); + expect(spy).toHaveBeenCalledWith('test-key'); + + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('can handle an error getting a value', async () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + // Storage here needs to be the global browser 'Storage' not the interface + // for our platform. + const spy = jest.spyOn(Storage.prototype, 'getItem'); + spy.mockImplementation(() => { + throw new Error('bad'); + }); + + const storage = new LocalStorage(logger); + storage.get('test-key'); + + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + 'Error getting key from localStorage: test-key, reason: Error: bad', + ); +}); + +it('can clear values', async () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + // Storage here needs to be the global browser 'Storage' not the interface + // for our platform. + const spy = jest.spyOn(Storage.prototype, 'removeItem'); + + const storage = new LocalStorage(logger); + storage.clear('test-key'); + expect(spy).toHaveBeenCalledWith('test-key'); + + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).not.toHaveBeenCalled(); +}); + +it('can handle an error clearing a value', async () => { + const logger = { + debug: jest.fn(), + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }; + // Storage here needs to be the global browser 'Storage' not the interface + // for our platform. + const spy = jest.spyOn(Storage.prototype, 'removeItem'); + spy.mockImplementation(() => { + throw new Error('bad'); + }); + + const storage = new LocalStorage(logger); + storage.clear('test-key'); + + expect(logger.debug).not.toHaveBeenCalled(); + expect(logger.info).not.toHaveBeenCalled(); + expect(logger.warn).not.toHaveBeenCalled(); + expect(logger.error).toHaveBeenCalledWith( + 'Error clearing key from localStorage: test-key, reason: Error: bad', + ); +}); diff --git a/packages/sdk/browser/jest.config.js b/packages/sdk/browser/jest.config.js new file mode 100644 index 000000000..364918be3 --- /dev/null +++ b/packages/sdk/browser/jest.config.js @@ -0,0 +1,12 @@ + +export default { + preset: 'ts-jest', + testEnvironment: 'jest-environment-jsdom', + transform: { + "^.+\\.tsx?$": "ts-jest" + // process `*.tsx` files with `ts-jest` + }, + moduleNameMapper: { + '\\.(gif|ttf|eot|svg|png)$': '/test/__ mocks __/fileMock.js', + }, +} diff --git a/packages/sdk/browser/package.json b/packages/sdk/browser/package.json new file mode 100644 index 000000000..8723526a3 --- /dev/null +++ b/packages/sdk/browser/package.json @@ -0,0 +1,64 @@ +{ + "name": "@launchdarkly/js-client-sdk", + "version": "0.0.0", + "description": "LaunchDarkly SDK for JavaScript in Browsers", + "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/browser", + "repository": { + "type": "git", + "url": "https://github.com/launchdarkly/js-core.git" + }, + "license": "Apache-2.0", + "packageManager": "yarn@3.4.1", + "keywords": [ + "launchdarkly", + "feature flags", + "feature toggles", + "feature management", + "sdk" + ], + "exports": { + "types": "./dist/src/index.d.ts", + "require": "./dist/index.cjs.js", + "import": "./dist/index.es.js" + }, + "type": "module", + "files": [ + "dist" + ], + "scripts": { + "clean": "rimraf dist", + "build": "vite build", + "lint": "eslint . --ext .ts,.tsx", + "prettier": "prettier --write '**/*.@(js|ts|tsx|json|css)' --ignore-path ../../../.prettierignore", + "test": "jest", + "coverage": "yarn test --coverage", + "check": "yarn prettier && yarn lint && yarn build && yarn test" + }, + "dependencies": { + "@launchdarkly/js-client-sdk-common": "1.5.0" + }, + "devDependencies": { + "@launchdarkly/private-js-mocks": "0.0.1", + "@trivago/prettier-plugin-sort-imports": "^4.1.1", + "@types/jest": "^29.5.11", + "@typescript-eslint/eslint-plugin": "^6.20.0", + "@typescript-eslint/parser": "^6.20.0", + "eslint": "^8.45.0", + "eslint-config-airbnb-base": "^15.0.0", + "eslint-config-airbnb-typescript": "^17.1.0", + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-import": "^2.27.5", + "eslint-plugin-jest": "^27.6.3", + "eslint-plugin-prettier": "^5.0.0", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "prettier": "^3.0.0", + "rimraf": "^5.0.5", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typedoc": "0.25.0", + "typescript": "^5.5.3", + "vite": "^5.4.1", + "vite-plugin-dts": "^4.0.3" + } +} diff --git a/packages/sdk/browser/src/index.ts b/packages/sdk/browser/src/index.ts new file mode 100644 index 000000000..a31204dbd --- /dev/null +++ b/packages/sdk/browser/src/index.ts @@ -0,0 +1,4 @@ +export function Hello() { + // eslint-disable-next-line no-console + console.log('HELLO'); +} diff --git a/packages/sdk/browser/src/platform/BrowserPlatform.ts b/packages/sdk/browser/src/platform/BrowserPlatform.ts new file mode 100644 index 000000000..419840d5d --- /dev/null +++ b/packages/sdk/browser/src/platform/BrowserPlatform.ts @@ -0,0 +1,22 @@ +import { + LDOptions, + Storage, + /* platform */ +} from '@launchdarkly/js-client-sdk-common'; + +import LocalStorage, { isLocalStorageSupported } from './LocalStorage'; + +export default class BrowserPlatform /* implements platform.Platform */ { + // encoding?: Encoding; + // info: Info; + // fileSystem?: Filesystem; + // crypto: Crypto; + // requests: Requests; + storage?: Storage; + + constructor(options: LDOptions) { + if (isLocalStorageSupported()) { + this.storage = new LocalStorage(options.logger); + } + } +} diff --git a/packages/sdk/browser/src/platform/LocalStorage.ts b/packages/sdk/browser/src/platform/LocalStorage.ts new file mode 100644 index 000000000..75e8be6de --- /dev/null +++ b/packages/sdk/browser/src/platform/LocalStorage.ts @@ -0,0 +1,42 @@ +import type { LDLogger, Storage } from '@launchdarkly/js-client-sdk-common'; + +export function isLocalStorageSupported() { + // Checking a symbol using typeof is safe, but directly accessing a symbol + // which is not defined would be an error. + return typeof localStorage !== 'undefined'; +} + +/** + * Implementation of Storage using localStorage for the browser. + * + * The Storage API is async, and localStorage is synchronous. This is fine, + * and none of the methods need to internally await their operations. + */ +export default class PlatformStorage implements Storage { + constructor(private readonly logger?: LDLogger) {} + async clear(key: string): Promise { + try { + localStorage.removeItem(key); + } catch (error) { + this.logger?.error(`Error clearing key from localStorage: ${key}, reason: ${error}`); + } + } + + async get(key: string): Promise { + try { + const value = localStorage.getItem(key); + return value ?? null; + } catch (error) { + this.logger?.error(`Error getting key from localStorage: ${key}, reason: ${error}`); + return null; + } + } + + async set(key: string, value: string): Promise { + try { + localStorage.setItem(key, value); + } catch (error) { + this.logger?.error(`Error setting key in localStorage: ${key}, reason: ${error}`); + } + } +} diff --git a/packages/sdk/browser/tsconfig.eslint.json b/packages/sdk/browser/tsconfig.eslint.json new file mode 100644 index 000000000..8241f86c3 --- /dev/null +++ b/packages/sdk/browser/tsconfig.eslint.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["/**/*.ts", "/**/*.tsx"], + "exclude": ["node_modules"] +} diff --git a/packages/sdk/browser/tsconfig.json b/packages/sdk/browser/tsconfig.json new file mode 100644 index 000000000..b1c92fdd9 --- /dev/null +++ b/packages/sdk/browser/tsconfig.json @@ -0,0 +1,35 @@ +{ + "compilerOptions": { + "allowSyntheticDefaultImports": true, + "declaration": true, + "declarationMap": true, + "jsx": "react-jsx", + "lib": ["es6", "dom"], + "module": "ES6", + "moduleResolution": "node", + "noImplicitOverride": true, + "resolveJsonModule": true, + // Uses "." so it can load package.json. + "rootDir": ".", + "skipLibCheck": true, + // enables importers to jump to source + "sourceMap": true, + "strict": true, + "stripInternal": true, + "target": "ES2017", + "types": ["node", "jest"], + "allowJs": true + }, + "exclude": [ + "vite.config.ts", + "__tests__", + "dist", + "docs", + "example", + "node_modules", + "babel.config.js", + "jest.config.ts", + "jestSetupFile.ts", + "**/*.test.ts*" + ] +} diff --git a/packages/sdk/browser/tsconfig.ref.json b/packages/sdk/browser/tsconfig.ref.json new file mode 100644 index 000000000..3925f645b --- /dev/null +++ b/packages/sdk/browser/tsconfig.ref.json @@ -0,0 +1,7 @@ +{ + "extends": "./tsconfig.json", + "include": ["src/**/*", "package.json", "__tests__/index.test.ts"], + "compilerOptions": { + "composite": true + } +} diff --git a/packages/sdk/browser/tsconfig.test.json b/packages/sdk/browser/tsconfig.test.json new file mode 100644 index 000000000..2c617dcaa --- /dev/null +++ b/packages/sdk/browser/tsconfig.test.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "esModuleInterop": true, + "jsx": "react-jsx", + "lib": ["es6", "dom"], + "module": "ES6", + "moduleResolution": "node", + "resolveJsonModule": true, + "rootDir": ".", + "strict": true, + "types": ["jest", "node"] + }, + "exclude": ["dist", "node_modules", "__tests__", "example"] +} diff --git a/packages/sdk/browser/typedoc.json b/packages/sdk/browser/typedoc.json new file mode 100644 index 000000000..7ac616b54 --- /dev/null +++ b/packages/sdk/browser/typedoc.json @@ -0,0 +1,5 @@ +{ + "extends": ["../../../typedoc.base.json"], + "entryPoints": ["src/index.ts"], + "out": "docs" +} diff --git a/packages/sdk/browser/vite.config.ts b/packages/sdk/browser/vite.config.ts new file mode 100644 index 000000000..74378ce66 --- /dev/null +++ b/packages/sdk/browser/vite.config.ts @@ -0,0 +1,18 @@ +/* eslint-disable import/no-extraneous-dependencies */ +// This file intentionally uses dev dependencies as it is a build file. +import { resolve } from 'path'; +import { defineConfig } from 'vite'; +import dts from 'vite-plugin-dts'; + +export default defineConfig({ + plugins: [dts()], + build: { + lib: { + entry: resolve(__dirname, 'src/index.ts'), + name: '@launchdarkly/js-client-sdk', + fileName: (format) => `index.${format}.js`, + formats: ['cjs', 'es'], + }, + rollupOptions: {}, + }, +}); diff --git a/packages/sdk/cloudflare/CHANGELOG.md b/packages/sdk/cloudflare/CHANGELOG.md index de793abab..91a927695 100644 --- a/packages/sdk/cloudflare/CHANGELOG.md +++ b/packages/sdk/cloudflare/CHANGELOG.md @@ -21,6 +21,33 @@ All notable changes to the LaunchDarkly SDK for Cloudflare Workers will be docum * devDependencies * @launchdarkly/js-server-sdk-common-edge bumped from 2.2.1 to 2.2.2 +## [2.5.13](https://github.com/launchdarkly/js-core/compare/cloudflare-server-sdk-v2.5.12...cloudflare-server-sdk-v2.5.13) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.3.8 to 2.3.9 + +## [2.5.12](https://github.com/launchdarkly/js-core/compare/cloudflare-server-sdk-v2.5.11...cloudflare-server-sdk-v2.5.12) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.3.7 to 2.3.8 + +## [2.5.11](https://github.com/launchdarkly/js-core/compare/cloudflare-server-sdk-v2.5.10...cloudflare-server-sdk-v2.5.11) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.3.6 to 2.3.7 + ## [2.5.10](https://github.com/launchdarkly/js-core/compare/cloudflare-server-sdk-v2.5.9...cloudflare-server-sdk-v2.5.10) (2024-08-12) diff --git a/packages/sdk/cloudflare/README.md b/packages/sdk/cloudflare/README.md index 56171ce9f..d94abd168 100644 --- a/packages/sdk/cloudflare/README.md +++ b/packages/sdk/cloudflare/README.md @@ -72,7 +72,7 @@ yarn test ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -80,7 +80,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/sdk/cloudflare/src/index.test.ts b/packages/sdk/cloudflare/__tests__/index.test.ts similarity index 98% rename from packages/sdk/cloudflare/src/index.test.ts rename to packages/sdk/cloudflare/__tests__/index.test.ts index 747bc75e1..941438c0a 100644 --- a/packages/sdk/cloudflare/src/index.test.ts +++ b/packages/sdk/cloudflare/__tests__/index.test.ts @@ -3,7 +3,7 @@ import { Miniflare } from 'miniflare'; import { LDClient, LDContext } from '@launchdarkly/js-server-sdk-common-edge'; -import { init } from './index'; +import { init } from '../src/index'; import * as allFlagsSegments from './testData.json'; const mf = new Miniflare({ diff --git a/packages/sdk/cloudflare/src/testData.json b/packages/sdk/cloudflare/__tests__/testData.json similarity index 100% rename from packages/sdk/cloudflare/src/testData.json rename to packages/sdk/cloudflare/__tests__/testData.json diff --git a/packages/sdk/cloudflare/example/README.md b/packages/sdk/cloudflare/example/README.md index 4a9655202..73d54f2b2 100644 --- a/packages/sdk/cloudflare/example/README.md +++ b/packages/sdk/cloudflare/example/README.md @@ -40,8 +40,8 @@ npx wrangler kv:key get --binding=LD_KV "LD-Env-test-client-side-id" --preview 5. Edit [index.ts](https://github.com/launchdarkly/js-core/blob/main/packages/sdk/cloudflare/example/src/index.ts#L6) to use your clientSideID and a valid flag key from the test data you just inserted. ```ts - const clientSideID = 'test-client-side-id'; - const flagKey = 'test-boolean-flag'; +const clientSideID = 'test-client-side-id'; +const flagKey = 'test-boolean-flag'; ``` 6. Finally: diff --git a/packages/sdk/cloudflare/jsr.json b/packages/sdk/cloudflare/jsr.json index 65144b552..8f0b8980b 100644 --- a/packages/sdk/cloudflare/jsr.json +++ b/packages/sdk/cloudflare/jsr.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/cloudflare-server-sdk", - "version": "2.5.10", + "version": "2.5.13", "exports": "./src/index.ts", "publish": { "include": [ diff --git a/packages/sdk/cloudflare/package.json b/packages/sdk/cloudflare/package.json index 266774b4a..975678f66 100644 --- a/packages/sdk/cloudflare/package.json +++ b/packages/sdk/cloudflare/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/cloudflare-server-sdk", - "version": "2.5.10", + "version": "2.5.13", "description": "Cloudflare LaunchDarkly SDK", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/cloudflare", "repository": { @@ -44,7 +44,7 @@ "crypto-js": "^4.1.1" }, "devDependencies": { - "@launchdarkly/js-server-sdk-common-edge": "2.3.6", + "@launchdarkly/js-server-sdk-common-edge": "2.3.9", "@rollup/plugin-commonjs": "^25.0.4", "@rollup/plugin-json": "^6.0.0", "@rollup/plugin-node-resolve": "^15.2.1", diff --git a/packages/sdk/cloudflare/src/createPlatformInfo.ts b/packages/sdk/cloudflare/src/createPlatformInfo.ts index 41109a32c..34c96cf3f 100644 --- a/packages/sdk/cloudflare/src/createPlatformInfo.ts +++ b/packages/sdk/cloudflare/src/createPlatformInfo.ts @@ -1,7 +1,7 @@ import type { Info, PlatformData, SdkData } from '@launchdarkly/js-server-sdk-common-edge'; const name = '@launchdarkly/cloudflare-server-sdk'; -const version = '2.5.10'; // x-release-please-version +const version = '2.5.13'; // x-release-please-version class CloudflarePlatformInfo implements Info { platformData(): PlatformData { diff --git a/packages/sdk/react-native/CHANGELOG.md b/packages/sdk/react-native/CHANGELOG.md index fbc03b8d9..3a31bd0c7 100644 --- a/packages/sdk/react-native/CHANGELOG.md +++ b/packages/sdk/react-native/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## [10.6.1](https://github.com/launchdarkly/js-core/compare/react-native-client-sdk-v10.6.0...react-native-client-sdk-v10.6.1) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-client-sdk-common bumped from 1.6.0 to 1.7.0 + +## [10.6.0](https://github.com/launchdarkly/js-core/compare/react-native-client-sdk-v10.5.1...react-native-client-sdk-v10.6.0) (2024-08-28) + + +### Features + +* custom storage option for React Native SDK ([#539](https://github.com/launchdarkly/js-core/issues/539)) ([115bd82](https://github.com/launchdarkly/js-core/commit/115bd828c665731084665b5d94bb3836942332b1)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-client-sdk-common bumped from 1.5.0 to 1.6.0 + ## [10.5.1](https://github.com/launchdarkly/js-core/compare/react-native-client-sdk-v10.5.0...react-native-client-sdk-v10.5.1) (2024-08-19) diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.storage.test.ts b/packages/sdk/react-native/__tests__/ReactNativeLDClient.storage.test.ts similarity index 93% rename from packages/sdk/react-native/src/ReactNativeLDClient.storage.test.ts rename to packages/sdk/react-native/__tests__/ReactNativeLDClient.storage.test.ts index 26b6dd7c4..53bde5c12 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.storage.test.ts +++ b/packages/sdk/react-native/__tests__/ReactNativeLDClient.storage.test.ts @@ -1,6 +1,6 @@ import { AutoEnvAttributes, LDLogger } from '@launchdarkly/js-client-sdk-common'; -import ReactNativeLDClient from './ReactNativeLDClient'; +import ReactNativeLDClient from '../src/ReactNativeLDClient'; it('uses custom storage', async () => { // This test just validates that the custom storage instance is being called. diff --git a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts b/packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts similarity index 95% rename from packages/sdk/react-native/src/ReactNativeLDClient.test.ts rename to packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts index 8f79acc41..7a4f594c6 100644 --- a/packages/sdk/react-native/src/ReactNativeLDClient.test.ts +++ b/packages/sdk/react-native/__tests__/ReactNativeLDClient.test.ts @@ -1,11 +1,11 @@ import { AutoEnvAttributes, LDLogger, Response } from '@launchdarkly/js-client-sdk-common'; -import createPlatform from './platform'; -import PlatformCrypto from './platform/crypto'; -import PlatformEncoding from './platform/PlatformEncoding'; -import PlatformInfo from './platform/PlatformInfo'; -import PlatformStorage from './platform/PlatformStorage'; -import ReactNativeLDClient from './ReactNativeLDClient'; +import createPlatform from '../src/platform'; +import PlatformCrypto from '../src/platform/crypto'; +import PlatformEncoding from '../src/platform/PlatformEncoding'; +import PlatformInfo from '../src/platform/PlatformInfo'; +import PlatformStorage from '../src/platform/PlatformStorage'; +import ReactNativeLDClient from '../src/ReactNativeLDClient'; function mockResponse(value: string, statusCode: number) { const response: Response = { @@ -34,7 +34,7 @@ function mockFetch(value: string, statusCode: number = 200) { return f; } -jest.mock('./platform', () => ({ +jest.mock('../src/platform', () => ({ __esModule: true, default: jest.fn((logger: LDLogger) => ({ crypto: new PlatformCrypto(), diff --git a/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.test.ts b/packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts similarity index 85% rename from packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.test.ts rename to packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts index c39dda61e..f6c4173ad 100644 --- a/packages/sdk/react-native/src/fromExternal/react-native-sse/EventSource.test.ts +++ b/packages/sdk/react-native/__tests__/fromExternal/react-native-sse/EventSource.test.ts @@ -1,7 +1,10 @@ import { type EventName } from '@launchdarkly/js-client-sdk-common'; import { createLogger } from '@launchdarkly/private-js-mocks'; -import EventSource, { backoff, jitter } from './EventSource'; +import EventSource, { + backoff, + jitter, +} from '../../../src/fromExternal/react-native-sse/EventSource'; let logger: ReturnType; @@ -24,14 +27,14 @@ describe('EventSource', () => { .mockImplementationOnce(() => 0.888) .mockImplementationOnce(() => 0.999); - mockXhr = { - open: jest.fn(), - send: jest.fn(), - setRequestHeader: jest.fn(), - abort: jest.fn(), - }; + mockXhr = { + open: jest.fn(), + send: jest.fn(), + setRequestHeader: jest.fn(), + abort: jest.fn(), + }; - jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => mockXhr as XMLHttpRequest); + jest.spyOn(window, 'XMLHttpRequest').mockImplementation(() => mockXhr as XMLHttpRequest); eventSource = new EventSource(uri, { logger }); eventSource.onclose = jest.fn(); @@ -84,7 +87,9 @@ describe('EventSource', () => { test('initial connection', () => { jest.runAllTimers(); - expect(logger.debug).toHaveBeenCalledWith(expect.stringMatching(/\[EventSource\] opening new connection./)); + expect(logger.debug).toHaveBeenCalledWith( + expect.stringMatching(/\[EventSource\] opening new connection./), + ); expect(mockXhr.open).toHaveBeenCalledTimes(1); expect(eventSource.onclose).toHaveBeenCalledTimes(0); }); diff --git a/packages/sdk/react-native/src/options.test.ts b/packages/sdk/react-native/__tests__/options.test.ts similarity index 96% rename from packages/sdk/react-native/src/options.test.ts rename to packages/sdk/react-native/__tests__/options.test.ts index 91f179a02..e219d28aa 100644 --- a/packages/sdk/react-native/src/options.test.ts +++ b/packages/sdk/react-native/__tests__/options.test.ts @@ -1,7 +1,7 @@ import { LDLogger } from '@launchdarkly/js-client-sdk-common'; -import validateOptions, { filterToBaseOptions } from './options'; -import { RNStorage } from './RNOptions'; +import validateOptions, { filterToBaseOptions } from '../src/options'; +import { RNStorage } from '../src/RNOptions'; it('logs no warnings when all configuration is valid', () => { const logger: LDLogger = { diff --git a/packages/sdk/react-native/src/platform/ConnectionManager.test.ts b/packages/sdk/react-native/__tests__/platform/ConnectionManager.test.ts similarity index 99% rename from packages/sdk/react-native/src/platform/ConnectionManager.test.ts rename to packages/sdk/react-native/__tests__/platform/ConnectionManager.test.ts index 2ce2db3d2..9bc8e20c1 100644 --- a/packages/sdk/react-native/src/platform/ConnectionManager.test.ts +++ b/packages/sdk/react-native/__tests__/platform/ConnectionManager.test.ts @@ -6,7 +6,7 @@ import { ConnectionManager, NetworkState, StateDetector, -} from './ConnectionManager'; +} from '../../src/platform/ConnectionManager'; function mockDestination(): ConnectionDestination { return { diff --git a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts b/packages/sdk/react-native/__tests__/platform/crypto/PlatformHasher.test.ts similarity index 96% rename from packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts rename to packages/sdk/react-native/__tests__/platform/crypto/PlatformHasher.test.ts index 38135983c..ae416add2 100644 --- a/packages/sdk/react-native/src/platform/crypto/PlatformHasher.test.ts +++ b/packages/sdk/react-native/__tests__/platform/crypto/PlatformHasher.test.ts @@ -1,4 +1,4 @@ -import PlatformHasher from './PlatformHasher'; +import PlatformHasher from '../../../src/platform/crypto/PlatformHasher'; /** * The links below are different from js-sha256 and are useful to verify the diff --git a/packages/sdk/react-native/src/provider/LDProvider.test.tsx b/packages/sdk/react-native/__tests__/provider/LDProvider.test.tsx similarity index 84% rename from packages/sdk/react-native/src/provider/LDProvider.test.tsx rename to packages/sdk/react-native/__tests__/provider/LDProvider.test.tsx index 4d5d9a5db..fb208b947 100644 --- a/packages/sdk/react-native/src/provider/LDProvider.test.tsx +++ b/packages/sdk/react-native/__tests__/provider/LDProvider.test.tsx @@ -2,13 +2,13 @@ import { render } from '@testing-library/react'; import { AutoEnvAttributes, LDContext, LDOptions } from '@launchdarkly/js-client-sdk-common'; -import { useLDClient } from '../hooks'; -import ReactNativeLDClient from '../ReactNativeLDClient'; -import LDProvider from './LDProvider'; -import setupListeners from './setupListeners'; +import { useLDClient } from '../../src/hooks'; +import LDProvider from '../../src/provider/LDProvider'; +import setupListeners from '../../src/provider/setupListeners'; +import ReactNativeLDClient from '../../src/ReactNativeLDClient'; -jest.mock('../ReactNativeLDClient'); -jest.mock('./setupListeners'); +jest.mock('../../src/ReactNativeLDClient'); +jest.mock('../../src/provider/setupListeners'); const TestApp = () => { const ldClient = useLDClient(); diff --git a/packages/sdk/react-native/src/provider/setupListeners.test.ts b/packages/sdk/react-native/__tests__/provider/setupListeners.test.ts similarity index 82% rename from packages/sdk/react-native/src/provider/setupListeners.test.ts rename to packages/sdk/react-native/__tests__/provider/setupListeners.test.ts index 4d6878a29..98a45a990 100644 --- a/packages/sdk/react-native/src/provider/setupListeners.test.ts +++ b/packages/sdk/react-native/__tests__/provider/setupListeners.test.ts @@ -1,11 +1,11 @@ import { AutoEnvAttributes } from '@launchdarkly/js-client-sdk-common'; -import ReactNativeLDClient from '../ReactNativeLDClient'; -import setupListeners from './setupListeners'; +import setupListeners from '../../src/provider/setupListeners'; +import ReactNativeLDClient from '../../src/ReactNativeLDClient'; import resetAllMocks = jest.resetAllMocks; -jest.mock('../ReactNativeLDClient'); +jest.mock('../../src/ReactNativeLDClient'); describe('setupListeners', () => { let ldc: ReactNativeLDClient; diff --git a/packages/sdk/react-native/example/.detoxrc.js b/packages/sdk/react-native/example/.detoxrc.js index d6b62c9e2..3dc2ceb12 100644 --- a/packages/sdk/react-native/example/.detoxrc.js +++ b/packages/sdk/react-native/example/.detoxrc.js @@ -50,7 +50,7 @@ module.exports = { emulator: { type: 'android.emulator', device: { - avdName: 'Pixel_3a_API_33_arm64-v8a', + avdName: 'Pixel_4_API_30', }, }, }, diff --git a/packages/sdk/react-native/example/.gitignore b/packages/sdk/react-native/example/.gitignore index 877c0f430..017aa78aa 100644 --- a/packages/sdk/react-native/example/.gitignore +++ b/packages/sdk/react-native/example/.gitignore @@ -39,3 +39,6 @@ ios android !yarn.lock + +# detox +artifacts diff --git a/packages/sdk/react-native/example/app.json b/packages/sdk/react-native/example/app.json index 681b269d3..fd5b4ed4d 100644 --- a/packages/sdk/react-native/example/app.json +++ b/packages/sdk/react-native/example/app.json @@ -25,6 +25,7 @@ }, "web": { "favicon": "./assets/favicon.png" - } + }, + "plugins": ["@config-plugins/detox"] } } diff --git a/packages/sdk/react-native/example/e2e/starter.test.ts b/packages/sdk/react-native/example/e2e/starter.test.ts index af4f80c74..bd6461249 100644 --- a/packages/sdk/react-native/example/e2e/starter.test.ts +++ b/packages/sdk/react-native/example/e2e/starter.test.ts @@ -1,42 +1,37 @@ import { by, device, element, expect, waitFor } from 'detox'; -describe('Example', () => { +describe('given the example application', () => { beforeAll(async () => { await device.launchApp({ newInstance: true, launchArgs: { - detoxURLBlacklistRegex: '\\("^https://clientstream.launchdarkly.com/meval"\\)', + // Detox will wait for HTTP requests to complete. This prevents detox from waiting for + // requests matching this URL to complete. + detoxURLBlacklistRegex: '\\("^https://clientstream.launchdarkly.com/meval.*"\\)', }, }); }); - // For speed, all tests are sequential and dependent. - // beforeEach(async () => { - // await device.reloadReactNative(); - // }); - - afterAll(async () => { - await device.terminateApp(); - }); - - test('app loads and renders correctly', async () => { + it('loads and renders correctly with default values', async () => { await expect(element(by.text(/welcome to launchdarkly/i))).toBeVisible(); - await expect(element(by.text(/my-boolean-flag-1: false/i))).toBeVisible(); + await expect(element(by.text(/sample-feature: false/i))).toBeVisible(); }); - test('identify', async () => { - await element(by.id('userKey')).typeText('test-user'); + it('can identify and evaluate with non-default values', async () => { + const featureFlagKey = process.env.LAUNCHDARKLY_FLAG_KEY ?? 'sample-feature'; + await element(by.id('userKey')).typeText('example-user-key'); + await element(by.id('flagKey')).replaceText(featureFlagKey); await element(by.text(/identify/i)).tap(); - await waitFor(element(by.text(/my-boolean-flag-1: true/i))) + await waitFor(element(by.text(new RegExp(`${featureFlagKey}: true`)))) .toBeVisible() .withTimeout(2000); }); - test('variation', async () => { - await element(by.id('flagKey')).replaceText('my-boolean-flag-2'); + it('can set a flag and has defaults for a non-existent flag', async () => { + await element(by.id('flagKey')).replaceText('not-found-flag'); - await waitFor(element(by.text(/my-boolean-flag-2: true/i))) + await waitFor(element(by.text(/not-found-flag: false/i))) .toBeVisible() .withTimeout(2000); }); diff --git a/packages/sdk/react-native/example/package.json b/packages/sdk/react-native/example/package.json index b11df52ff..a79b3d473 100644 --- a/packages/sdk/react-native/example/package.json +++ b/packages/sdk/react-native/example/package.json @@ -23,7 +23,7 @@ "dependencies": { "@launchdarkly/react-native-client-sdk": "workspace:^", "@react-native-async-storage/async-storage": "^1.21.0", - "expo": "51.0.21", + "expo": "51.0.31", "expo-status-bar": "~1.11.1", "react": "18.2.0", "react-native": "0.74.3", @@ -31,6 +31,7 @@ }, "devDependencies": { "@babel/core": "^7.20.0", + "@config-plugins/detox": "^8.0.0", "@types/detox": "^18.1.0", "@types/jest": "^29.5.11", "@types/node": "^20.10.5", diff --git a/packages/sdk/react-native/example/src/welcome.tsx b/packages/sdk/react-native/example/src/welcome.tsx index 496cfc7f0..f726fe865 100644 --- a/packages/sdk/react-native/example/src/welcome.tsx +++ b/packages/sdk/react-native/example/src/welcome.tsx @@ -5,7 +5,7 @@ import { ConnectionMode } from '@launchdarkly/js-client-sdk-common'; import { useBoolVariation, useLDClient } from '@launchdarkly/react-native-client-sdk'; export default function Welcome() { - const [flagKey, setFlagKey] = useState('my-boolean-flag-1'); + const [flagKey, setFlagKey] = useState('sample-feature'); const [userKey, setUserKey] = useState(''); const flagValue = useBoolVariation(flagKey, false); const ldc = useLDClient(); @@ -16,7 +16,6 @@ export default function Welcome() { .catch((e: any) => console.error(`error identifying ${userKey}: ${e}`)); }; - const setConnectionMode = (m: ConnectionMode) => { ldc.setConnectionMode(m); }; @@ -51,7 +50,10 @@ export default function Welcome() { testID="flagKey" /> - setConnectionMode('offline')}> + setConnectionMode('offline')} + > Set offline { private tryConnect(initialConnection: boolean = false) { let delay = initialConnection ? 0 : this.getNextRetryDelay(); if (initialConnection) { - this.logger?.debug(`[EventSource] opening new connection.`) + this.logger?.debug(`[EventSource] opening new connection.`); } else { this.logger?.debug(`[EventSource] Will open new connection in ${delay} ms.`); this.dispatch('retry', { type: 'retry', delayMillis: delay }); @@ -138,7 +138,8 @@ export default class EventSource { } this.logger?.debug( - `[EventSource][onreadystatechange] ReadyState: ${XMLReadyStateMap[this.xhr.readyState] || 'Unknown' + `[EventSource][onreadystatechange] ReadyState: ${ + XMLReadyStateMap[this.xhr.readyState] || 'Unknown' }(${this.xhr.readyState}), status: ${this.xhr.status}`, ); @@ -349,8 +350,8 @@ export default class EventSource { return this.status; } - onopen() { } - onclose() { } - onerror(_err: any) { } - onretrying(_e: any) { } + onopen() {} + onclose() {} + onerror(_err: any) {} + onretrying(_e: any) {} } diff --git a/packages/sdk/react-universal/src/index.test.ts b/packages/sdk/react-universal/__tests__/index.test.ts similarity index 100% rename from packages/sdk/react-universal/src/index.test.ts rename to packages/sdk/react-universal/__tests__/index.test.ts diff --git a/packages/sdk/server-node/CHANGELOG.md b/packages/sdk/server-node/CHANGELOG.md index cc4ec22f2..b54a2bf17 100644 --- a/packages/sdk/server-node/CHANGELOG.md +++ b/packages/sdk/server-node/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to `@launchdarkly/node-server-sdk` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [9.5.4](https://github.com/launchdarkly/js-core/compare/node-server-sdk-v9.5.3...node-server-sdk-v9.5.4) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.6.0 to 2.6.1 + +## [9.5.3](https://github.com/launchdarkly/js-core/compare/node-server-sdk-v9.5.2...node-server-sdk-v9.5.3) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.5.0 to 2.6.0 + +## [9.5.2](https://github.com/launchdarkly/js-core/compare/node-server-sdk-v9.5.1...node-server-sdk-v9.5.2) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.4.5 to 2.5.0 + ## [9.5.1](https://github.com/launchdarkly/js-core/compare/node-server-sdk-v9.5.0...node-server-sdk-v9.5.1) (2024-08-12) diff --git a/packages/sdk/server-node/README.md b/packages/sdk/server-node/README.md index 5a53949ec..cf595d3c4 100644 --- a/packages/sdk/server-node/README.md +++ b/packages/sdk/server-node/README.md @@ -38,7 +38,7 @@ We encourage pull requests and other contributions from the community. Check out ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -46,7 +46,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/sdk/server-node/package.json b/packages/sdk/server-node/package.json index f9fcf0760..c5c0b2b29 100644 --- a/packages/sdk/server-node/package.json +++ b/packages/sdk/server-node/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk", - "version": "9.5.1", + "version": "9.5.4", "description": "LaunchDarkly Server-Side SDK for Node.js", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/server-node", "repository": { @@ -45,7 +45,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@launchdarkly/js-server-sdk-common": "2.4.5", + "@launchdarkly/js-server-sdk-common": "2.6.1", "https-proxy-agent": "^5.0.1", "launchdarkly-eventsource": "2.0.3" }, diff --git a/packages/sdk/vercel/CHANGELOG.md b/packages/sdk/vercel/CHANGELOG.md index cd4da0217..7c262ae43 100644 --- a/packages/sdk/vercel/CHANGELOG.md +++ b/packages/sdk/vercel/CHANGELOG.md @@ -20,6 +20,33 @@ All notable changes to the LaunchDarkly SDK for Vercel Edge Config will be docum * dependencies * @launchdarkly/js-server-sdk-common-edge bumped from 2.2.1 to 2.2.2 +## [1.3.16](https://github.com/launchdarkly/js-core/compare/vercel-server-sdk-v1.3.15...vercel-server-sdk-v1.3.16) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.3.8 to 2.3.9 + +## [1.3.15](https://github.com/launchdarkly/js-core/compare/vercel-server-sdk-v1.3.14...vercel-server-sdk-v1.3.15) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.3.7 to 2.3.8 + +## [1.3.14](https://github.com/launchdarkly/js-core/compare/vercel-server-sdk-v1.3.13...vercel-server-sdk-v1.3.14) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common-edge bumped from 2.3.6 to 2.3.7 + ## [1.3.13](https://github.com/launchdarkly/js-core/compare/vercel-server-sdk-v1.3.12...vercel-server-sdk-v1.3.13) (2024-08-12) diff --git a/packages/sdk/vercel/README.md b/packages/sdk/vercel/README.md index 0635f7d60..19b8aa2f1 100644 --- a/packages/sdk/vercel/README.md +++ b/packages/sdk/vercel/README.md @@ -49,13 +49,13 @@ const flagValue = await ldClient.variation('my-flag', ldContext, true); To learn more, see the [examples](examples/README.md) in this repository or head straight to the [complete reference guide for this SDK](https://docs.launchdarkly.com/sdk/server-side/vercel). - > **⚠️ experimental** -> -> This SDK has experimental support for sending analytic events to LaunchDarkly. It can be enabled by setting `sendEvents` to `true` in the SDK options. ->```typescript -> const client = initLD(sdkKey, env.LD_KV, {sendEvents: true}); ->``` +> +> This SDK has experimental support for sending analytic events to LaunchDarkly. It can be enabled by setting `sendEvents` to `true` in the SDK options. +> +> ```typescript +> const client = initLD(sdkKey, env.LD_KV, { sendEvents: true }); +> ``` ## Developing this SDK @@ -68,7 +68,7 @@ yarn test ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -76,7 +76,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Read [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/sdk/vercel/src/createPlatformInfo.test.ts b/packages/sdk/vercel/__tests__/createPlatformInfo.test.ts similarity index 88% rename from packages/sdk/vercel/src/createPlatformInfo.test.ts rename to packages/sdk/vercel/__tests__/createPlatformInfo.test.ts index 6db8f24f9..5378dee11 100644 --- a/packages/sdk/vercel/src/createPlatformInfo.test.ts +++ b/packages/sdk/vercel/__tests__/createPlatformInfo.test.ts @@ -1,4 +1,4 @@ -import createPlatformInfo from './createPlatformInfo'; +import createPlatformInfo from '../src/createPlatformInfo'; const packageJson = require('../package.json'); diff --git a/packages/sdk/vercel/src/index.test.ts b/packages/sdk/vercel/__tests__/index.test.ts similarity index 98% rename from packages/sdk/vercel/src/index.test.ts rename to packages/sdk/vercel/__tests__/index.test.ts index 5c631f7f0..d95d2b107 100644 --- a/packages/sdk/vercel/src/index.test.ts +++ b/packages/sdk/vercel/__tests__/index.test.ts @@ -1,6 +1,6 @@ import { LDClient, LDContext } from '@launchdarkly/js-server-sdk-common-edge'; -import { init } from './index'; +import { init } from '../src/index'; import mockEdgeConfigClient from './utils/mockEdgeConfigClient'; import * as testData from './utils/testData.json'; diff --git a/packages/sdk/vercel/src/utils/mockEdgeConfigClient.ts b/packages/sdk/vercel/__tests__/utils/mockEdgeConfigClient.ts similarity index 100% rename from packages/sdk/vercel/src/utils/mockEdgeConfigClient.ts rename to packages/sdk/vercel/__tests__/utils/mockEdgeConfigClient.ts diff --git a/packages/sdk/vercel/src/utils/testData.json b/packages/sdk/vercel/__tests__/utils/testData.json similarity index 100% rename from packages/sdk/vercel/src/utils/testData.json rename to packages/sdk/vercel/__tests__/utils/testData.json diff --git a/packages/sdk/vercel/package.json b/packages/sdk/vercel/package.json index ad381190a..acc757e07 100644 --- a/packages/sdk/vercel/package.json +++ b/packages/sdk/vercel/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/vercel-server-sdk", - "version": "1.3.13", + "version": "1.3.16", "description": "LaunchDarkly Server-Side SDK for Vercel Edge", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/sdk/vercel", "repository": { @@ -36,7 +36,7 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test" }, "dependencies": { - "@launchdarkly/js-server-sdk-common-edge": "2.3.6", + "@launchdarkly/js-server-sdk-common-edge": "2.3.9", "@vercel/edge-config": "^1.1.0", "crypto-js": "^4.1.1" }, diff --git a/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md b/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md index 229e70737..5b8738265 100644 --- a/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md +++ b/packages/shared/akamai-edgeworker-sdk/CHANGELOG.md @@ -86,6 +86,33 @@ All notable changes to the LaunchDarkly SDK for Akamai Workers will be documente * dependencies * @launchdarkly/js-server-sdk-common bumped from ^2.2.1 to ^2.2.2 +## [1.1.15](https://github.com/launchdarkly/js-core/compare/akamai-edgeworker-sdk-common-v1.1.14...akamai-edgeworker-sdk-common-v1.1.15) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from ^2.6.0 to ^2.6.1 + +## [1.1.14](https://github.com/launchdarkly/js-core/compare/akamai-edgeworker-sdk-common-v1.1.13...akamai-edgeworker-sdk-common-v1.1.14) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from ^2.5.0 to ^2.6.0 + +## [1.1.13](https://github.com/launchdarkly/js-core/compare/akamai-edgeworker-sdk-common-v1.1.12...akamai-edgeworker-sdk-common-v1.1.13) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from ^2.4.5 to ^2.5.0 + ## [1.1.12](https://github.com/launchdarkly/js-core/compare/akamai-edgeworker-sdk-common-v1.1.11...akamai-edgeworker-sdk-common-v1.1.12) (2024-08-12) diff --git a/packages/shared/akamai-edgeworker-sdk/README.md b/packages/shared/akamai-edgeworker-sdk/README.md index 2221e05be..6ce424aaf 100644 --- a/packages/shared/akamai-edgeworker-sdk/README.md +++ b/packages/shared/akamai-edgeworker-sdk/README.md @@ -12,7 +12,7 @@ See [Contributing](../CONTRIBUTING.md). - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/featureStore/index.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/featureStore/index.test.ts similarity index 98% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/featureStore/index.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/featureStore/index.test.ts index e692e3b71..ce243c6ed 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/featureStore/index.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/featureStore/index.test.ts @@ -1,6 +1,6 @@ import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common'; -import { EdgeFeatureStore, EdgeProvider } from '../../featureStore'; +import { EdgeFeatureStore, EdgeProvider } from '../../src/featureStore'; import * as testData from '../testData.json'; describe('EdgeFeatureStore', () => { diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/index.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/index.test.ts similarity index 99% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/index.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/index.test.ts index aa8f16fe9..9b0264f82 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/index.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/index.test.ts @@ -1,4 +1,4 @@ -import { EdgeProvider, init, LDLogger, LDMultiKindContext, LDSingleKindContext } from '../..'; +import { EdgeProvider, init, LDLogger, LDMultiKindContext, LDSingleKindContext } from '../dist'; import * as testData from './testData.json'; const createClient = (sdkKey: string, mockLogger: LDLogger, mockEdgeProvider: EdgeProvider) => diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/platform/info/index.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/platform/info/index.test.ts similarity index 84% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/platform/info/index.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/platform/info/index.test.ts index 9890146d6..7e6ef0abc 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/platform/info/index.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/platform/info/index.test.ts @@ -1,6 +1,6 @@ -import createPlatformInfo from '../../../platform/info'; +import createPlatformInfo from '../../../src/platform/info'; -const packageJson = require('../../../../package.json'); +const packageJson = require('../../../package.json'); describe('Akamai Platform Info', () => { const { name, version } = packageJson; diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/platform/requests.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/platform/requests.test.ts similarity index 95% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/platform/requests.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/platform/requests.test.ts index e96e1ecb3..e70836b07 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/platform/requests.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/platform/requests.test.ts @@ -1,6 +1,6 @@ import { EventSourceInitDict } from '@launchdarkly/js-server-sdk-common'; -import EdgeRequests from '../../platform/requests'; +import EdgeRequests from '../../src/platform/requests'; const TEXT_RESPONSE = ''; const JSON_RESPONSE = {}; diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/testData.json b/packages/shared/akamai-edgeworker-sdk/__tests__/testData.json similarity index 100% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/testData.json rename to packages/shared/akamai-edgeworker-sdk/__tests__/testData.json diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/createCallbacks.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/utils/createCallbacks.test.ts similarity index 89% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/createCallbacks.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/utils/createCallbacks.test.ts index 818fe553d..ef5b7963a 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/createCallbacks.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/utils/createCallbacks.test.ts @@ -1,4 +1,4 @@ -import { createCallbacks } from '../../utils/createCallbacks'; +import { createCallbacks } from '../../src/utils/createCallbacks'; describe('create callback', () => { it('creates valid callbacks', () => { diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/createOptions.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/utils/createOptions.test.ts similarity index 87% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/createOptions.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/utils/createOptions.test.ts index 66895ab38..735a2c9f3 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/createOptions.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/utils/createOptions.test.ts @@ -1,4 +1,4 @@ -import { createOptions, defaultOptions } from '../../utils/createOptions'; +import { createOptions, defaultOptions } from '../../src/utils/createOptions'; describe('create options', () => { it('returns default options', () => { diff --git a/packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/validateOptions.test.ts b/packages/shared/akamai-edgeworker-sdk/__tests__/utils/validateOptions.test.ts similarity index 94% rename from packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/validateOptions.test.ts rename to packages/shared/akamai-edgeworker-sdk/__tests__/utils/validateOptions.test.ts index 22615875c..4126a9fe8 100644 --- a/packages/shared/akamai-edgeworker-sdk/src/__tests__/utils/validateOptions.test.ts +++ b/packages/shared/akamai-edgeworker-sdk/__tests__/utils/validateOptions.test.ts @@ -1,7 +1,7 @@ import { BasicLogger } from '@launchdarkly/js-server-sdk-common'; -import { EdgeFeatureStore } from '../../featureStore'; -import { LDOptionsInternal, validateOptions } from '../../utils/validateOptions'; +import { EdgeFeatureStore } from '../../src/featureStore'; +import { LDOptionsInternal, validateOptions } from '../../src/utils/validateOptions'; const SDK_KEY = 'test-key'; diff --git a/packages/shared/akamai-edgeworker-sdk/package.json b/packages/shared/akamai-edgeworker-sdk/package.json index b6d8d9536..a7c386e16 100644 --- a/packages/shared/akamai-edgeworker-sdk/package.json +++ b/packages/shared/akamai-edgeworker-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/akamai-edgeworker-sdk-common", - "version": "1.1.12", + "version": "1.1.15", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/akamai-edge-sdk", "repository": { "type": "git", @@ -55,7 +55,7 @@ "typescript": "5.1.6" }, "dependencies": { - "@launchdarkly/js-server-sdk-common": "^2.4.5", + "@launchdarkly/js-server-sdk-common": "^2.6.1", "crypto-js": "^4.1.1" } } diff --git a/packages/shared/common/CHANGELOG.md b/packages/shared/common/CHANGELOG.md index 900dd96cc..0948810e6 100644 --- a/packages/shared/common/CHANGELOG.md +++ b/packages/shared/common/CHANGELOG.md @@ -2,6 +2,20 @@ All notable changes to `@launchdarkly/js-sdk-common` will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org). +## [2.8.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.7.0...js-sdk-common-v2.8.0) (2024-09-03) + + +### Features + +* Add support for Payload Filtering ([#551](https://github.com/launchdarkly/js-core/issues/551)) ([6f44383](https://github.com/launchdarkly/js-core/commit/6f4438323baed802d8f951ac82494e6cfa9932c5)) + +## [2.7.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.6.0...js-sdk-common-v2.7.0) (2024-08-28) + + +### Features + +* Correct client evaluation typings. ([#554](https://github.com/launchdarkly/js-core/issues/554)) ([64ab88d](https://github.com/launchdarkly/js-core/commit/64ab88d278308564b4cd7b6433870c7adb09142a)) + ## [2.6.0](https://github.com/launchdarkly/js-core/compare/js-sdk-common-v2.5.0...js-sdk-common-v2.6.0) (2024-08-12) diff --git a/packages/shared/common/README.md b/packages/shared/common/README.md index d9bac9202..2b33019f9 100644 --- a/packages/shared/common/README.md +++ b/packages/shared/common/README.md @@ -12,7 +12,7 @@ See [Contributing](../CONTRIBUTING.md). ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -20,7 +20,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/shared/common/__tests__/options/ServiceEndpoints.test.ts b/packages/shared/common/__tests__/options/ServiceEndpoints.test.ts index ffa7bf5bc..c08d09f8d 100644 --- a/packages/shared/common/__tests__/options/ServiceEndpoints.test.ts +++ b/packages/shared/common/__tests__/options/ServiceEndpoints.test.ts @@ -1,4 +1,8 @@ -import ServiceEndpoints from '../../src/options/ServiceEndpoints'; +import ServiceEndpoints, { + getEventsUri, + getPollingUri, + getStreamingUri, +} from '../../src/options/ServiceEndpoints'; describe.each([ [ @@ -33,3 +37,26 @@ describe.each([ expect(endpoints.events).toEqual(expected.eventsUri); }); }); + +it('applies payload filter to polling and streaming endpoints', () => { + const endpoints = new ServiceEndpoints( + 'https://stream.launchdarkly.com', + 'https://sdk.launchdarkly.com', + 'https://events.launchdarkly.com', + '/bulk', + '/diagnostic', + true, + 'filterKey', + ); + + expect(getStreamingUri(endpoints, '/all', [])).toEqual( + 'https://stream.launchdarkly.com/all?filter=filterKey', + ); + expect(getPollingUri(endpoints, '/sdk/latest-all', [])).toEqual( + 'https://sdk.launchdarkly.com/sdk/latest-all?filter=filterKey', + ); + expect( + getPollingUri(endpoints, '/sdk/latest-all', [{ key: 'withReasons', value: 'true' }]), + ).toEqual('https://sdk.launchdarkly.com/sdk/latest-all?withReasons=true&filter=filterKey'); + expect(getEventsUri(endpoints, '/bulk', [])).toEqual('https://events.launchdarkly.com/bulk'); +}); diff --git a/packages/shared/common/package.json b/packages/shared/common/package.json index a0da2808a..29faa31b4 100644 --- a/packages/shared/common/package.json +++ b/packages/shared/common/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-sdk-common", - "version": "2.6.0", + "version": "2.8.0", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/shared/common/src/api/data/LDEvaluationDetail.ts b/packages/shared/common/src/api/data/LDEvaluationDetail.ts index 0a4f11dd7..84f08d5d4 100644 --- a/packages/shared/common/src/api/data/LDEvaluationDetail.ts +++ b/packages/shared/common/src/api/data/LDEvaluationDetail.ts @@ -1,6 +1,8 @@ import { LDEvaluationReason } from './LDEvaluationReason'; import { LDFlagValue } from './LDFlagValue'; +// TODO: On major version change "variationIndex" to only be optional and not nullable. + /** * An object that combines the result of a feature flag evaluation with information about * how it was calculated. diff --git a/packages/shared/common/src/internal/evaluation/evaluationDetail.ts b/packages/shared/common/src/internal/evaluation/evaluationDetail.ts deleted file mode 100644 index c08cc8687..000000000 --- a/packages/shared/common/src/internal/evaluation/evaluationDetail.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { LDEvaluationReason, LDFlagValue } from '../../api'; -import ErrorKinds from './ErrorKinds'; - -export const createErrorEvaluationDetail = (errorKind: ErrorKinds, def?: LDFlagValue) => ({ - value: def ?? null, - variationIndex: null, - reason: { kind: 'ERROR', errorKind }, -}); - -export const createSuccessEvaluationDetail = ( - value: LDFlagValue, - variationIndex?: number, - reason?: LDEvaluationReason, -) => ({ - value, - variationIndex: variationIndex ?? null, - reason: reason ?? null, -}); diff --git a/packages/shared/common/src/internal/evaluation/index.ts b/packages/shared/common/src/internal/evaluation/index.ts index 175c5d4e9..9882812df 100644 --- a/packages/shared/common/src/internal/evaluation/index.ts +++ b/packages/shared/common/src/internal/evaluation/index.ts @@ -1,11 +1,4 @@ import ErrorKinds from './ErrorKinds'; -import { createErrorEvaluationDetail, createSuccessEvaluationDetail } from './evaluationDetail'; import EventFactoryBase, { EvalEventArgs } from './EventFactoryBase'; -export { - createSuccessEvaluationDetail, - createErrorEvaluationDetail, - ErrorKinds, - EvalEventArgs, - EventFactoryBase, -}; +export { ErrorKinds, EvalEventArgs, EventFactoryBase }; diff --git a/packages/shared/common/src/internal/events/EventSender.ts b/packages/shared/common/src/internal/events/EventSender.ts index 30b161aba..afee52df1 100644 --- a/packages/shared/common/src/internal/events/EventSender.ts +++ b/packages/shared/common/src/internal/events/EventSender.ts @@ -10,7 +10,7 @@ import { isHttpRecoverable, LDUnexpectedResponseError, } from '../../errors'; -import { ClientContext } from '../../options'; +import { ClientContext, getEventsUri } from '../../options'; import { defaultHeaders, httpErrorMessage, sleep } from '../../utils'; export default class EventSender implements LDEventSender { @@ -26,19 +26,18 @@ export default class EventSender implements LDEventSender { const { basicConfiguration, platform } = clientContext; const { sdkKey, - serviceEndpoints: { - events, - analyticsEventPath, - diagnosticEventPath, - includeAuthorizationHeader, - }, + serviceEndpoints: { analyticsEventPath, diagnosticEventPath, includeAuthorizationHeader }, tags, } = basicConfiguration; const { crypto, info, requests } = platform; this.defaultHeaders = defaultHeaders(sdkKey, info, tags, includeAuthorizationHeader); - this.eventsUri = `${events}${analyticsEventPath}`; - this.diagnosticEventsUri = `${events}${diagnosticEventPath}`; + this.eventsUri = getEventsUri(basicConfiguration.serviceEndpoints, analyticsEventPath, []); + this.diagnosticEventsUri = getEventsUri( + basicConfiguration.serviceEndpoints, + diagnosticEventPath, + [], + ); this.requests = requests; this.crypto = crypto; } diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts index 0fd17d9f4..e91b93e16 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.test.ts @@ -107,6 +107,7 @@ describe('given a stream processor with mock event source', () => { platform: basicPlatform, }, '/all', + [], listeners, diagnosticsManager, mockErrorHandler, @@ -142,6 +143,7 @@ describe('given a stream processor with mock event source', () => { platform: basicPlatform, }, '/all', + [], listeners, diagnosticsManager, mockErrorHandler, diff --git a/packages/shared/common/src/internal/stream/StreamingProcessor.ts b/packages/shared/common/src/internal/stream/StreamingProcessor.ts index db67c4647..d9ccfaab4 100644 --- a/packages/shared/common/src/internal/stream/StreamingProcessor.ts +++ b/packages/shared/common/src/internal/stream/StreamingProcessor.ts @@ -9,6 +9,7 @@ import { import { LDStreamProcessor } from '../../api/subsystem'; import { LDStreamingError } from '../../errors'; import { ClientContext } from '../../options'; +import { getStreamingUri } from '../../options/ServiceEndpoints'; import { defaultHeaders, httpErrorMessage, shouldRetry } from '../../utils'; import { DiagnosticsManager } from '../diagnostics'; import { StreamingErrorHandler } from './types'; @@ -37,6 +38,7 @@ class StreamingProcessor implements LDStreamProcessor { sdkKey: string, clientContext: ClientContext, streamUriPath: string, + parameters: { key: string; value: string }[], private readonly listeners: Map, private readonly diagnosticsManager?: DiagnosticsManager, private readonly errorHandler?: StreamingErrorHandler, @@ -49,7 +51,11 @@ class StreamingProcessor implements LDStreamProcessor { this.headers = defaultHeaders(sdkKey, info, tags); this.logger = logger; this.requests = requests; - this.streamUri = `${basicConfiguration.serviceEndpoints.streaming}${streamUriPath}`; + this.streamUri = getStreamingUri( + basicConfiguration.serviceEndpoints, + streamUriPath, + parameters, + ); } private logConnectionStarted() { diff --git a/packages/shared/common/src/options/ServiceEndpoints.ts b/packages/shared/common/src/options/ServiceEndpoints.ts index 4debefa46..d0781b0a9 100644 --- a/packages/shared/common/src/options/ServiceEndpoints.ts +++ b/packages/shared/common/src/options/ServiceEndpoints.ts @@ -2,6 +2,10 @@ function canonicalizeUri(uri: string): string { return uri.replace(/\/+$/, ''); } +function canonicalizePath(path: string): string { + return path.replace(/^\/+/, '').replace(/\?$/, ''); +} + /** * Specifies the base service URIs used by SDK components. */ @@ -11,6 +15,7 @@ export default class ServiceEndpoints { public readonly streaming: string; public readonly polling: string; public readonly events: string; + public readonly payloadFilterKey?: string; /** Valid paths are: * /bulk @@ -36,6 +41,7 @@ export default class ServiceEndpoints { analyticsEventPath: string = '/bulk', diagnosticEventPath: string = '/diagnostic', includeAuthorizationHeader: boolean = true, + payloadFilterKey?: string, ) { this.streaming = canonicalizeUri(streaming); this.polling = canonicalizeUri(polling); @@ -43,5 +49,76 @@ export default class ServiceEndpoints { this.analyticsEventPath = analyticsEventPath; this.diagnosticEventPath = diagnosticEventPath; this.includeAuthorizationHeader = includeAuthorizationHeader; + this.payloadFilterKey = payloadFilterKey; } } + +function getWithParams(uri: string, parameters: { key: string; value: string }[]) { + if (parameters.length === 0) { + return uri; + } + + const parts = parameters.map(({ key, value }) => `${key}=${value}`); + return `${uri}?${parts.join('&')}`; +} + +/** + * Get the URI for the streaming endpoint. + * + * @param endpoints The service endpoints. + * @param path The path to the resource, devoid of any query parameters or hrefs. + * @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you. + */ +export function getStreamingUri( + endpoints: ServiceEndpoints, + path: string, + parameters: { key: string; value: string }[], +): string { + const canonicalizedPath = canonicalizePath(path); + + const combinedParameters = [...parameters]; + if (endpoints.payloadFilterKey) { + combinedParameters.push({ key: 'filter', value: endpoints.payloadFilterKey }); + } + + return getWithParams(`${endpoints.streaming}/${canonicalizedPath}`, combinedParameters); +} + +/** + * Get the URI for the polling endpoint. + * + * @param endpoints The service endpoints. + * @param path The path to the resource, devoid of any query parameters or hrefs. + * @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you. + */ +export function getPollingUri( + endpoints: ServiceEndpoints, + path: string, + parameters: { key: string; value: string }[], +): string { + const canonicalizedPath = canonicalizePath(path); + + const combinedParameters = [...parameters]; + if (endpoints.payloadFilterKey) { + combinedParameters.push({ key: 'filter', value: endpoints.payloadFilterKey }); + } + + return getWithParams(`${endpoints.polling}/${canonicalizedPath}`, combinedParameters); +} + +/** + * Get the URI for the events endpoint. + * + * @param endpoints The service endpoints. + * @param path The path to the resource, devoid of any query parameters or hrefs. + * @param parameters The query parameters. These query parameters must already have the appropriate encoding applied. This function WILL NOT apply it for you. + */ +export function getEventsUri( + endpoints: ServiceEndpoints, + path: string, + parameters: { key: string; value: string }[], +): string { + const canonicalizedPath = canonicalizePath(path); + + return getWithParams(`${endpoints.events}/${canonicalizedPath}`, parameters); +} diff --git a/packages/shared/common/src/options/index.ts b/packages/shared/common/src/options/index.ts index 17d7859e3..04181131c 100644 --- a/packages/shared/common/src/options/index.ts +++ b/packages/shared/common/src/options/index.ts @@ -1,6 +1,14 @@ import ApplicationTags from './ApplicationTags'; import ClientContext from './ClientContext'; import OptionMessages from './OptionMessages'; -import ServiceEndpoints from './ServiceEndpoints'; +import ServiceEndpoints, { getEventsUri, getPollingUri, getStreamingUri } from './ServiceEndpoints'; -export { ApplicationTags, OptionMessages, ServiceEndpoints, ClientContext }; +export { + ApplicationTags, + OptionMessages, + ServiceEndpoints, + ClientContext, + getStreamingUri, + getPollingUri, + getEventsUri, +}; diff --git a/packages/shared/common/src/validators.ts b/packages/shared/common/src/validators.ts index 5ae8f0c0c..d294643bd 100644 --- a/packages/shared/common/src/validators.ts +++ b/packages/shared/common/src/validators.ts @@ -118,7 +118,7 @@ export class StringMatchingRegex extends Type { } override is(u: unknown): u is string { - return !!(u as string).match(this.expression); + return typeof u === 'string' && !!(u as string).match(this.expression); } } diff --git a/packages/shared/mocks/README.md b/packages/shared/mocks/README.md index dd8c01864..2d178b398 100644 --- a/packages/shared/mocks/README.md +++ b/packages/shared/mocks/README.md @@ -118,7 +118,7 @@ See [Contributing](../shared/CONTRIBUTING.md). - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/shared/mocks/src/platform.ts b/packages/shared/mocks/src/platform.ts index 3fe5ecfef..ef138ebd5 100644 --- a/packages/shared/mocks/src/platform.ts +++ b/packages/shared/mocks/src/platform.ts @@ -1,11 +1,7 @@ -import type { Encoding, Platform, PlatformData, Requests, SdkData, Storage } from '@common'; +import type { PlatformData, SdkData } from '@common'; import { setupCrypto } from './crypto'; -const encoding: Encoding = { - btoa: (s: string) => Buffer.from(s).toString('base64'), -}; - const setupInfo = () => ({ platformData: jest.fn( (): PlatformData => ({ diff --git a/packages/shared/mocks/src/streamingProcessor.ts b/packages/shared/mocks/src/streamingProcessor.ts index 2de19efc8..e596b443f 100644 --- a/packages/shared/mocks/src/streamingProcessor.ts +++ b/packages/shared/mocks/src/streamingProcessor.ts @@ -25,6 +25,7 @@ export const setupMockStreamingProcessor = ( sdkKey: string, clientContext: ClientContext, streamUriPath: string, + parameters: { key: string; value: string }[], listeners: Map, diagnosticsManager: internal.DiagnosticsManager, errorHandler: internal.StreamingErrorHandler, diff --git a/packages/shared/sdk-client/CHANGELOG.md b/packages/shared/sdk-client/CHANGELOG.md index 7a2a3ac06..b1f8039df 100644 --- a/packages/shared/sdk-client/CHANGELOG.md +++ b/packages/shared/sdk-client/CHANGELOG.md @@ -1,5 +1,34 @@ # Changelog +## [1.7.0](https://github.com/launchdarkly/js-core/compare/js-client-sdk-common-v1.6.0...js-client-sdk-common-v1.7.0) (2024-09-03) + + +### Features + +* Add support for Payload Filtering ([#551](https://github.com/launchdarkly/js-core/issues/551)) ([6f44383](https://github.com/launchdarkly/js-core/commit/6f4438323baed802d8f951ac82494e6cfa9932c5)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-sdk-common bumped from 2.7.0 to 2.8.0 + +## [1.6.0](https://github.com/launchdarkly/js-core/compare/js-client-sdk-common-v1.5.0...js-client-sdk-common-v1.6.0) (2024-08-28) + + +### Features + +* Correct client evaluation typings. ([#554](https://github.com/launchdarkly/js-core/issues/554)) ([64ab88d](https://github.com/launchdarkly/js-core/commit/64ab88d278308564b4cd7b6433870c7adb09142a)) +* Make timeout optional in LDIdentifyOptions. ([#552](https://github.com/launchdarkly/js-core/issues/552)) ([fa247b2](https://github.com/launchdarkly/js-core/commit/fa247b2db821d11c8360752ba5f28b4ecff493c7)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-sdk-common bumped from 2.6.0 to 2.7.0 + ## [1.5.0](https://github.com/launchdarkly/js-core/compare/js-client-sdk-common-v1.4.0...js-client-sdk-common-v1.5.0) (2024-08-19) diff --git a/packages/shared/sdk-client/README.md b/packages/shared/sdk-client/README.md index 59e5046e3..6bbc864aa 100644 --- a/packages/shared/sdk-client/README.md +++ b/packages/shared/sdk-client/README.md @@ -19,7 +19,7 @@ See [Contributing](../CONTRIBUTING.md). - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/shared/sdk-client/src/LDClientImpl.events.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts similarity index 97% rename from packages/shared/sdk-client/src/LDClientImpl.events.test.ts rename to packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts index 0e0aef2c4..e9d4677ac 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.events.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.events.test.ts @@ -13,10 +13,10 @@ import { MockEventProcessor, } from '@launchdarkly/private-js-mocks'; +import LDClientImpl from '../src/LDClientImpl'; +import { MockEventSource } from '../src/LDClientImpl.mocks'; +import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; -import LDClientImpl from './LDClientImpl'; -import { MockEventSource } from './LDClientImpl.mocks'; -import { Flags } from './types'; type InputCustomEvent = internal.InputCustomEvent; type InputIdentifyEvent = internal.InputIdentifyEvent; diff --git a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts similarity index 98% rename from packages/shared/sdk-client/src/LDClientImpl.storage.test.ts rename to packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts index 864cb813b..218841f66 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.storage.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.storage.test.ts @@ -1,12 +1,12 @@ import { AutoEnvAttributes, clone, Encoding, type LDContext } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; -import LDEmitter from './api/LDEmitter'; -import { toMulti } from './context/addAutoEnv'; +import { toMulti } from '../src/context/addAutoEnv'; +import { MockEventSource } from '../src/LDClientImpl.mocks'; +import LDEmitter from '../src/LDEmitter'; +import { Flags, PatchFlag } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; -import LDClientImpl from './LDClientImpl'; -import { MockEventSource } from './LDClientImpl.mocks'; -import { Flags, PatchFlag } from './types'; +import LDClientImpl from '../src/LDClientImpl'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/src/LDClientImpl.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts similarity index 98% rename from packages/shared/sdk-client/src/LDClientImpl.test.ts rename to packages/shared/sdk-client/__tests__/LDClientImpl.test.ts index ad9df36b3..e096d4f3c 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.test.ts @@ -1,10 +1,10 @@ import { AutoEnvAttributes, clone, Encoding, Hasher, LDContext } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; +import LDClientImpl from '../src/LDClientImpl'; +import { MockEventSource } from '../src/LDClientImpl.mocks'; +import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; -import LDClientImpl from './LDClientImpl'; -import { MockEventSource } from './LDClientImpl.mocks'; -import { Flags } from './types'; const testSdkKey = 'test-sdk-key'; const context: LDContext = { kind: 'org', key: 'Testy Pizza' }; @@ -113,7 +113,6 @@ describe('sdk-client object', () => { expect(all).toMatchObject({ 'dev-test-flag': true, }); - expect(mockCreateEventSource).toHaveBeenCalledWith( expect.stringContaining('/stream/path'), expect.anything(), diff --git a/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts similarity index 96% rename from packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts rename to packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts index 5ca99b65e..d562c8353 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.timeout.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.timeout.test.ts @@ -1,11 +1,11 @@ import { AutoEnvAttributes, clone, Encoding, LDContext } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; -import { toMulti } from './context/addAutoEnv'; +import { toMulti } from '../src/context/addAutoEnv'; +import LDClientImpl from '../src/LDClientImpl'; +import { MockEventSource } from '../src/LDClientImpl.mocks'; +import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; -import LDClientImpl from './LDClientImpl'; -import { MockEventSource } from './LDClientImpl.mocks'; -import { Flags } from './types'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts b/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts similarity index 95% rename from packages/shared/sdk-client/src/LDClientImpl.variation.test.ts rename to packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts index 8caa4db0c..0b708738a 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.variation.test.ts +++ b/packages/shared/sdk-client/__tests__/LDClientImpl.variation.test.ts @@ -7,10 +7,10 @@ import { } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; +import LDClientImpl from '../src/LDClientImpl'; +import { MockEventSource } from '../src/LDClientImpl.mocks'; +import { Flags } from '../src/types'; import * as mockResponseJson from './evaluation/mockResponse.json'; -import LDClientImpl from './LDClientImpl'; -import { MockEventSource } from './LDClientImpl.mocks'; -import { Flags } from './types'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/src/api/LDEmitter.test.ts b/packages/shared/sdk-client/__tests__/LDEmitter.test.ts similarity index 99% rename from packages/shared/sdk-client/src/api/LDEmitter.test.ts rename to packages/shared/sdk-client/__tests__/LDEmitter.test.ts index def8355a3..66a2a57d9 100644 --- a/packages/shared/sdk-client/src/api/LDEmitter.test.ts +++ b/packages/shared/sdk-client/__tests__/LDEmitter.test.ts @@ -1,6 +1,6 @@ import { LDContext, LDLogger } from '@launchdarkly/js-sdk-common'; -import LDEmitter from './LDEmitter'; +import LDEmitter from '../src/LDEmitter'; describe('LDEmitter', () => { const error = { type: 'network', message: 'unreachable' }; diff --git a/packages/shared/sdk-client/src/configuration/Configuration.test.ts b/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts similarity index 67% rename from packages/shared/sdk-client/src/configuration/Configuration.test.ts rename to packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts index f86709e2f..6ce292295 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.test.ts +++ b/packages/shared/sdk-client/__tests__/configuration/Configuration.test.ts @@ -1,5 +1,5 @@ /* eslint-disable no-console */ -import Configuration from './Configuration'; +import Configuration from '../../src/configuration/Configuration'; describe('Configuration', () => { beforeEach(() => { @@ -7,7 +7,7 @@ describe('Configuration', () => { console.error = jest.fn(); }); - test('defaults', () => { + it('has valid default values', () => { const config = new Configuration(); expect(config).toMatchObject({ @@ -20,7 +20,6 @@ describe('Configuration', () => { withReasons: false, eventsUri: 'https://events.launchdarkly.com', flushInterval: 30, - inspectors: [], logger: { destination: console.error, logLevel: 1, @@ -37,12 +36,12 @@ describe('Configuration', () => { expect(console.error).not.toHaveBeenCalled(); }); - test('specified options should be set', () => { + it('allows specifying valid wrapperName', () => { const config = new Configuration({ wrapperName: 'test' }); expect(config).toMatchObject({ wrapperName: 'test' }); }); - test('unknown option', () => { + it('warns and ignored invalid keys', () => { // @ts-ignore const config = new Configuration({ baseballUri: 1 }); @@ -50,7 +49,7 @@ describe('Configuration', () => { expect(console.error).toHaveBeenCalledWith(expect.stringContaining('unknown config option')); }); - test('wrong type for boolean should be converted', () => { + it('converts boolean types', () => { // @ts-ignore const config = new Configuration({ sendEvents: 0 }); @@ -60,7 +59,7 @@ describe('Configuration', () => { ); }); - test('wrong type for number should use default', () => { + it('ignores wrong type for number and logs appropriately', () => { // @ts-ignore const config = new Configuration({ capacity: true }); @@ -70,7 +69,7 @@ describe('Configuration', () => { ); }); - test('enforce minimum flushInterval', () => { + it('enforces minimum flushInterval', () => { const config = new Configuration({ flushInterval: 1 }); expect(config.flushInterval).toEqual(2); @@ -80,25 +79,14 @@ describe('Configuration', () => { ); }); - test('invalid bootstrap should use default', () => { - // @ts-ignore - const config = new Configuration({ bootstrap: 'localStora' }); - - expect(config.bootstrap).toBeUndefined(); - expect(console.error).toHaveBeenNthCalledWith( - 1, - expect.stringMatching(/should be of type LDFlagSet, got string/i), - ); - }); - - test('recognize maxCachedContexts', () => { + it('allows setting a valid maxCachedContexts', () => { const config = new Configuration({ maxCachedContexts: 3 }); expect(config.maxCachedContexts).toBeDefined(); expect(console.error).not.toHaveBeenCalled(); }); - test('enforce minimum maxCachedContext', () => { + it('enforces minimum maxCachedContext', () => { const config = new Configuration({ maxCachedContexts: -1 }); expect(config.maxCachedContexts).toBeDefined(); @@ -107,4 +95,28 @@ describe('Configuration', () => { expect.stringContaining('had invalid value of -1'), ); }); + + it.each([ + ['1'], + ['camelCaseWorks'], + ['PascalCaseWorks'], + ['kebab-case-works'], + ['snake_case_works'], + ])('allow setting valid payload filter keys', (filter) => { + const config = new Configuration({ payloadFilterKey: filter }); + expect(config.payloadFilterKey).toEqual(filter); + expect(console.error).toHaveBeenCalledTimes(0); + }); + + it.each([['invalid-@-filter'], ['_invalid-filter'], ['-invalid-filter']])( + 'ignores invalid filters and logs a warning', + (filter) => { + const config = new Configuration({ payloadFilterKey: filter }); + expect(config.payloadFilterKey).toBeUndefined(); + expect(console.error).toHaveBeenNthCalledWith( + 1, + expect.stringMatching(/should be of type string matching/i), + ); + }, + ); }); diff --git a/packages/shared/sdk-client/src/context/addAutoEnv.test.ts b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts similarity index 98% rename from packages/shared/sdk-client/src/context/addAutoEnv.test.ts rename to packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts index a36379455..555f7d0b2 100644 --- a/packages/shared/sdk-client/src/context/addAutoEnv.test.ts +++ b/packages/shared/sdk-client/__tests__/context/addAutoEnv.test.ts @@ -7,8 +7,13 @@ import { } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform, createLogger } from '@launchdarkly/private-js-mocks'; -import Configuration from '../configuration'; -import { addApplicationInfo, addAutoEnv, addDeviceInfo, toMulti } from './addAutoEnv'; +import Configuration from '../../src/configuration'; +import { + addApplicationInfo, + addAutoEnv, + addDeviceInfo, + toMulti, +} from '../../src/context/addAutoEnv'; let mockPlatform: ReturnType; let logger: ReturnType; diff --git a/packages/shared/sdk-client/src/context/ensureKey.test.ts b/packages/shared/sdk-client/__tests__/context/ensureKey.test.ts similarity index 98% rename from packages/shared/sdk-client/src/context/ensureKey.test.ts rename to packages/shared/sdk-client/__tests__/context/ensureKey.test.ts index a985427c0..2faf60177 100644 --- a/packages/shared/sdk-client/src/context/ensureKey.test.ts +++ b/packages/shared/sdk-client/__tests__/context/ensureKey.test.ts @@ -7,7 +7,7 @@ import type { } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform } from '@launchdarkly/private-js-mocks'; -import { ensureKey } from './ensureKey'; +import { ensureKey } from '../../src/context/ensureKey'; let mockPlatform: ReturnType; diff --git a/packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.test.ts b/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts similarity index 93% rename from packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.test.ts rename to packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts index aa70b134e..1a30e21e1 100644 --- a/packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.test.ts +++ b/packages/shared/sdk-client/__tests__/diagnostics/createDiagnosticsInitConfig.test.ts @@ -1,9 +1,9 @@ import { secondsToMillis } from '@launchdarkly/js-sdk-common'; -import Configuration from '../configuration'; +import Configuration from '../../src/configuration'; import createDiagnosticsInitConfig, { type DiagnosticsInitConfig, -} from './createDiagnosticsInitConfig'; +} from '../../src/diagnostics/createDiagnosticsInitConfig'; describe('createDiagnosticsInitConfig', () => { let initConfig: DiagnosticsInitConfig; diff --git a/packages/shared/sdk-client/src/evaluation/mockResponse.json b/packages/shared/sdk-client/__tests__/evaluation/mockResponse.json similarity index 100% rename from packages/shared/sdk-client/src/evaluation/mockResponse.json rename to packages/shared/sdk-client/__tests__/evaluation/mockResponse.json diff --git a/packages/shared/sdk-client/src/flag-manager/ContextIndex.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/ContextIndex.test.ts similarity index 98% rename from packages/shared/sdk-client/src/flag-manager/ContextIndex.test.ts rename to packages/shared/sdk-client/__tests__/flag-manager/ContextIndex.test.ts index aa9bd45de..5edb004c4 100644 --- a/packages/shared/sdk-client/src/flag-manager/ContextIndex.test.ts +++ b/packages/shared/sdk-client/__tests__/flag-manager/ContextIndex.test.ts @@ -1,4 +1,4 @@ -import ContextIndex from './ContextIndex'; +import ContextIndex from '../../src/flag-manager/ContextIndex'; describe('ContextIndex tests', () => { test('notice adds to index', async () => { diff --git a/packages/shared/sdk-client/src/flag-manager/FlagPersistence.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts similarity index 97% rename from packages/shared/sdk-client/src/flag-manager/FlagPersistence.test.ts rename to packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts index 2cd26cd1b..c0daf17c6 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagPersistence.test.ts +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagPersistence.test.ts @@ -1,11 +1,14 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ import { Context, Crypto, Hasher, LDLogger, Platform, Storage } from '@launchdarkly/js-sdk-common'; -import { namespaceForContextData, namespaceForContextIndex } from '../storage/namespaceUtils'; -import { Flag, Flags } from '../types'; -import FlagPersistence from './FlagPersistence'; -import { DefaultFlagStore } from './FlagStore'; -import FlagUpdater from './FlagUpdater'; +import FlagPersistence from '../../src/flag-manager/FlagPersistence'; +import { DefaultFlagStore } from '../../src/flag-manager/FlagStore'; +import FlagUpdater from '../../src/flag-manager/FlagUpdater'; +import { + namespaceForContextData, + namespaceForContextIndex, +} from '../../src/storage/namespaceUtils'; +import { Flag, Flags } from '../../src/types'; const TEST_NAMESPACE = 'TestNamespace'; diff --git a/packages/shared/sdk-client/__tests__/flag-manager/FlagStore.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagStore.test.ts new file mode 100644 index 000000000..8cb3389ec --- /dev/null +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagStore.test.ts @@ -0,0 +1,60 @@ +import { DefaultFlagStore } from '../../src/flag-manager/FlagStore'; + +describe('given an empty flag store', () => { + let store: DefaultFlagStore; + + beforeEach(() => { + store = new DefaultFlagStore(); + }); + + it.each(['unknown', 'toString', 'length'])( + 'gets undefined for a feature that does not exist', + (key) => { + expect(store.get(key)).toBeUndefined(); + }, + ); + + it('can set and get key', () => { + store.insertOrUpdate('toString', { + version: 1, + flag: { + version: 1, + flagVersion: 1, + value: 'test-value', + variation: 0, + trackEvents: false, + }, + }); + + expect(store.get('toString')?.flag.value).toEqual('test-value'); + }); + + it('replaces flags on init', () => { + store.insertOrUpdate('potato', { + version: 1, + flag: { + version: 1, + flagVersion: 1, + value: 'test-value', + variation: 0, + trackEvents: false, + }, + }); + + store.init({ + newFlag: { + version: 1, + flag: { + version: 1, + flagVersion: 1, + value: 'new-test-value', + variation: 0, + trackEvents: false, + }, + }, + }); + + const all = store.getAll(); + expect(Object.keys(all)).toEqual(['newFlag']); + }); +}); diff --git a/packages/shared/sdk-client/src/flag-manager/FlagUpdater.test.ts b/packages/shared/sdk-client/__tests__/flag-manager/FlagUpdater.test.ts similarity index 97% rename from packages/shared/sdk-client/src/flag-manager/FlagUpdater.test.ts rename to packages/shared/sdk-client/__tests__/flag-manager/FlagUpdater.test.ts index 26b5d42da..6bc41a9ff 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagUpdater.test.ts +++ b/packages/shared/sdk-client/__tests__/flag-manager/FlagUpdater.test.ts @@ -1,8 +1,8 @@ import { Context, LDLogger } from '@launchdarkly/js-sdk-common'; -import { Flag } from '../types'; -import { DefaultFlagStore } from './FlagStore'; -import FlagUpdater, { FlagsChangeCallback } from './FlagUpdater'; +import { DefaultFlagStore } from '../../src/flag-manager/FlagStore'; +import FlagUpdater, { FlagsChangeCallback } from '../../src/flag-manager/FlagUpdater'; +import { Flag } from '../../src/types'; function makeMockFlag(): Flag { // the values of the flag object itself are not relevant for these tests, the diff --git a/packages/shared/sdk-client/src/polling/PollingProcessot.test.ts b/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts similarity index 98% rename from packages/shared/sdk-client/src/polling/PollingProcessot.test.ts rename to packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts index 6114f42a5..b6a98ae3e 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessot.test.ts +++ b/packages/shared/sdk-client/__tests__/polling/PollingProcessot.test.ts @@ -10,7 +10,7 @@ import { SdkData, } from '@launchdarkly/js-sdk-common'; -import PollingProcessor, { PollingConfig } from './PollingProcessor'; +import PollingProcessor, { PollingConfig } from '../../src/polling/PollingProcessor'; function mockResponse(value: string, statusCode: number) { const response: Response = { @@ -86,6 +86,7 @@ it('makes no requests until it is started', () => { requests, makeInfo(), '/polling', + [], makeConfig(), (_flags) => {}, (_error) => {}, @@ -102,6 +103,7 @@ it('polls immediately when started', () => { requests, makeInfo(), '/polling', + [], makeConfig(), (_flags) => {}, (_error) => {}, @@ -122,6 +124,7 @@ it('calls callback on success', async () => { requests, makeInfo(), '/polling', + [], makeConfig(), dataCallback, errorCallback, @@ -143,6 +146,7 @@ it('polls repeatedly', async () => { requests, makeInfo(), '/polling', + [], makeConfig({ pollInterval: 0.1 }), dataCallback, errorCallback, @@ -174,6 +178,7 @@ it('stops polling when stopped', (done) => { requests, makeInfo(), '/stops', + [], makeConfig({ pollInterval: 0.01 }), dataCallback, errorCallback, @@ -199,6 +204,7 @@ it('includes the correct headers on requests', () => { version: '42', }), '/polling', + [], makeConfig(), (_flags) => {}, (_error) => {}, @@ -225,6 +231,7 @@ it('defaults to using the "GET" verb', () => { requests, makeInfo(), '/polling', + [], makeConfig(), (_flags) => {}, (_error) => {}, @@ -248,6 +255,7 @@ it('can be configured to use the "REPORT" verb', () => { requests, makeInfo(), '/polling', + [], makeConfig({ useReport: true }), (_flags) => {}, (_error) => {}, @@ -274,6 +282,7 @@ it('continues polling after receiving bad JSON', async () => { requests, makeInfo(), '/polling', + [], config, dataCallback, errorCallback, @@ -302,6 +311,7 @@ it('continues polling after an exception thrown during a request', async () => { requests, makeInfo(), '/polling', + [], config, dataCallback, errorCallback, @@ -333,6 +343,7 @@ it('can handle recoverable http errors', async () => { requests, makeInfo(), '/polling', + [], config, dataCallback, errorCallback, @@ -362,6 +373,7 @@ it('stops polling on unrecoverable error codes', (done) => { requests, makeInfo(), '/polling', + [], config, dataCallback, errorCallback, diff --git a/packages/shared/sdk-client/src/storage/getOrGenerateKey.test.ts b/packages/shared/sdk-client/__tests__/storage/getOrGenerateKey.test.ts similarity index 97% rename from packages/shared/sdk-client/src/storage/getOrGenerateKey.test.ts rename to packages/shared/sdk-client/__tests__/storage/getOrGenerateKey.test.ts index 94403c3e4..8065871ff 100644 --- a/packages/shared/sdk-client/src/storage/getOrGenerateKey.test.ts +++ b/packages/shared/sdk-client/__tests__/storage/getOrGenerateKey.test.ts @@ -1,7 +1,7 @@ import { Crypto, Storage } from '@launchdarkly/js-sdk-common'; import { createBasicPlatform } from '@launchdarkly/private-js-mocks'; -import { getOrGenerateKey } from './getOrGenerateKey'; +import { getOrGenerateKey } from '../../src/storage/getOrGenerateKey'; let mockPlatform: ReturnType; diff --git a/packages/shared/sdk-client/src/storage/namespaceUtils.test.ts b/packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts similarity index 93% rename from packages/shared/sdk-client/src/storage/namespaceUtils.test.ts rename to packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts index 73058eeb0..0ee0c70cf 100644 --- a/packages/shared/sdk-client/src/storage/namespaceUtils.test.ts +++ b/packages/shared/sdk-client/__tests__/storage/namespaceUtils.test.ts @@ -1,4 +1,4 @@ -import { concatNamespacesAndValues } from './namespaceUtils'; +import { concatNamespacesAndValues } from '../../src/storage/namespaceUtils'; const mockHash = (input: string) => `${input}Hashed`; const noop = (input: string) => input; diff --git a/packages/shared/sdk-client/package.json b/packages/shared/sdk-client/package.json index a5025c7a5..3a31aad5d 100644 --- a/packages/shared/sdk-client/package.json +++ b/packages/shared/sdk-client/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-client-sdk-common", - "version": "1.5.0", + "version": "1.7.0", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -30,7 +30,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@launchdarkly/js-sdk-common": "2.6.0" + "@launchdarkly/js-sdk-common": "2.8.0" }, "devDependencies": { "@launchdarkly/private-js-mocks": "0.0.1", diff --git a/packages/shared/sdk-client/src/LDClientImpl.ts b/packages/shared/sdk-client/src/LDClientImpl.ts index 428c87af2..301532537 100644 --- a/packages/shared/sdk-client/src/LDClientImpl.ts +++ b/packages/shared/sdk-client/src/LDClientImpl.ts @@ -7,8 +7,6 @@ import { internal, LDClientError, LDContext, - LDEvaluationDetail, - LDEvaluationDetailTyped, LDFlagSet, LDFlagValue, LDLogger, @@ -21,22 +19,26 @@ import { import { LDStreamProcessor } from '@launchdarkly/js-sdk-common/dist/api/subsystem'; import { ConnectionMode, LDClient, type LDOptions } from './api'; -import LDEmitter, { EventName } from './api/LDEmitter'; +import { LDEvaluationDetail, LDEvaluationDetailTyped } from './api/LDEvaluationDetail'; import { LDIdentifyOptions } from './api/LDIdentifyOptions'; import Configuration from './configuration'; import { addAutoEnv } from './context/addAutoEnv'; import { ensureKey } from './context/ensureKey'; import createDiagnosticsManager from './diagnostics/createDiagnosticsManager'; +import { + createErrorEvaluationDetail, + createSuccessEvaluationDetail, +} from './evaluation/evaluationDetail'; import createEventProcessor from './events/createEventProcessor'; import EventFactory from './events/EventFactory'; import FlagManager from './flag-manager/FlagManager'; import { ItemDescriptor } from './flag-manager/ItemDescriptor'; +import LDEmitter, { EventName } from './LDEmitter'; import PollingProcessor from './polling/PollingProcessor'; import { StreamingPaths, StreamingProcessor } from './streaming'; import { DeleteFlag, Flags, PatchFlag } from './types'; -const { createErrorEvaluationDetail, createSuccessEvaluationDetail, ClientMessages, ErrorKinds } = - internal; +const { ClientMessages, ErrorKinds } = internal; export default class LDClientImpl implements LDClient { private readonly config: Configuration; @@ -403,15 +405,17 @@ export default class LDClientImpl implements LDClient { identifyResolve: any, identifyReject: any, ) { - let pollingPath = this.createPollUriPath(context); + const parameters: { key: string; value: string }[] = []; if (this.config.withReasons) { - pollingPath = `${pollingPath}?withReasons=true`; + parameters.push({ key: 'withReasons', value: 'true' }); } + this.updateProcessor = new PollingProcessor( this.sdkKey, this.clientContext.platform.requests, this.clientContext.platform.info, - pollingPath, + this.createPollUriPath(context), + parameters, this.config, async (flags) => { this.logger.debug(`Handling polling result: ${Object.keys(flags)}`); @@ -444,7 +448,7 @@ export default class LDClientImpl implements LDClient { JSON.stringify(context), { credential: this.sdkKey, - streamingEndpoint: this.config.serviceEndpoints.streaming, + serviceEndpoints: this.config.serviceEndpoints, paths: this.getStreamingPaths(), tags: this.clientContext.basicConfiguration.tags, info: this.platform.info, @@ -492,7 +496,7 @@ export default class LDClientImpl implements LDClient { defaultValue: any, eventFactory: EventFactory, typeChecker?: (value: any) => [boolean, string], - ): LDFlagValue { + ): LDEvaluationDetail { if (!this.uncheckedContext) { this.logger.debug(ClientMessages.missingContextKeyNoEvent); return createErrorEvaluationDetail(ErrorKinds.UserNotSpecified, defaultValue); diff --git a/packages/shared/sdk-client/src/api/LDEmitter.ts b/packages/shared/sdk-client/src/LDEmitter.ts similarity index 100% rename from packages/shared/sdk-client/src/api/LDEmitter.ts rename to packages/shared/sdk-client/src/LDEmitter.ts diff --git a/packages/shared/sdk-client/src/api/LDClient.ts b/packages/shared/sdk-client/src/api/LDClient.ts index 7902bc691..8f26ad399 100644 --- a/packages/shared/sdk-client/src/api/LDClient.ts +++ b/packages/shared/sdk-client/src/api/LDClient.ts @@ -1,13 +1,7 @@ -import { - LDContext, - LDEvaluationDetail, - LDEvaluationDetailTyped, - LDFlagSet, - LDFlagValue, - LDLogger, -} from '@launchdarkly/js-sdk-common'; +import { LDContext, LDFlagSet, LDFlagValue, LDLogger } from '@launchdarkly/js-sdk-common'; import ConnectionMode from './ConnectionMode'; +import { LDEvaluationDetail, LDEvaluationDetailTyped } from './LDEvaluationDetail'; import { LDIdentifyOptions } from './LDIdentifyOptions'; /** diff --git a/packages/shared/sdk-client/src/api/LDEvaluationDetail.ts b/packages/shared/sdk-client/src/api/LDEvaluationDetail.ts new file mode 100644 index 000000000..466522456 --- /dev/null +++ b/packages/shared/sdk-client/src/api/LDEvaluationDetail.ts @@ -0,0 +1,37 @@ +import { + LDEvaluationDetail as CommonDetail, + LDEvaluationDetailTyped as CommonDetailTyped, + LDEvaluationReason, +} from '@launchdarkly/js-sdk-common'; + +// Implementation note: In client-side SDKs the reason is optional. The common type, which is also +// used by server SDKs, has a required reason. This file contains a client specific +// LDEvaluationDetail which has an optional reason. + +// TODO: On major version change "reason" to be optional instead of nullable. + +/** + * An object that combines the result of a feature flag evaluation with information about + * how it was calculated. + * + * This is the result of calling `LDClient.variationDetail`. + */ +export type LDEvaluationDetail = Omit & { + /** + * An optional object describing the main factor that influenced the flag evaluation value. + */ + reason: LDEvaluationReason | null; +}; + +/** + * An object that combines the result of a feature flag evaluation with information about + * how it was calculated. + * + * This is the result of calling detailed variation methods. + */ +export type LDEvaluationDetailTyped = Omit, 'reason'> & { + /** + * An optional object describing the main factor that influenced the flag evaluation value. + */ + reason: LDEvaluationReason | null; +}; diff --git a/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts b/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts index d9d33213e..3aecdfab4 100644 --- a/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts +++ b/packages/shared/sdk-client/src/api/LDIdentifyOptions.ts @@ -7,7 +7,7 @@ export interface LDIdentifyOptions { * * Defaults to 5 seconds. */ - timeout: number; + timeout?: number; /** * When true indicates that the SDK will attempt to wait for values from diff --git a/packages/shared/sdk-client/src/api/LDInspection.ts b/packages/shared/sdk-client/src/api/LDInspection.ts deleted file mode 100644 index 125a9116c..000000000 --- a/packages/shared/sdk-client/src/api/LDInspection.ts +++ /dev/null @@ -1,105 +0,0 @@ -import { LDContext, LDEvaluationDetail } from '@launchdarkly/js-sdk-common'; - -/** - * Callback interface for collecting information about the SDK at runtime. - * - * This interface is used to collect information about flag usage. - * - * This interface should not be used by the application to access flags for the purpose of controlling application - * flow. It is intended for monitoring, analytics, or debugging purposes. - */ - -export interface LDInspectionFlagUsedHandler { - type: 'flag-used'; - - /** - * Name of the inspector. Will be used for logging issues with the inspector. - */ - name: string; - - /** - * This method is called when a flag is accessed via a variation method, or it can be called based on actions in - * wrapper SDKs which have different methods of tracking when a flag was accessed. It is not called when a call is made - * to allFlags. - */ - method: (flagKey: string, flagDetail: LDEvaluationDetail, context: LDContext) => void; -} - -/** - * Callback interface for collecting information about the SDK at runtime. - * - * This interface is used to collect information about flag data. In order to understand the - * current flag state it should be combined with {@link LDInspectionFlagValueChangedHandler}. - * This interface will get the initial flag information, and - * {@link LDInspectionFlagValueChangedHandler} will provide changes to individual flags. - * - * This interface should not be used by the application to access flags for the purpose of controlling application - * flow. It is intended for monitoring, analytics, or debugging purposes. - */ -export interface LDInspectionFlagDetailsChangedHandler { - type: 'flag-details-changed'; - - /** - * Name of the inspector. Will be used for logging issues with the inspector. - */ - name: string; - - /** - * This method is called when the flags in the store are replaced with new flags. It will contain all flags - * regardless of if they have been evaluated. - */ - method: (details: Record) => void; -} - -/** - * Callback interface for collecting information about the SDK at runtime. - * - * This interface is used to collect changes to flag data, but does not provide the initial - * data. It can be combined with {@link LDInspectionFlagValuesChangedHandler} to track the - * entire flag state. - * - * This interface should not be used by the application to access flags for the purpose of controlling application - * flow. It is intended for monitoring, analytics, or debugging purposes. - */ -export interface LDInspectionFlagDetailChangedHandler { - type: 'flag-detail-changed'; - - /** - * Name of the inspector. Will be used for logging issues with the inspector. - */ - name: string; - - /** - * This method is called when a flag is updated. It will not be called - * when all flags are updated. - */ - method: (flagKey: string, detail: LDEvaluationDetail) => void; -} - -/** - * Callback interface for collecting information about the SDK at runtime. - * - * This interface is used to track current identity state of the SDK. - * - * This interface should not be used by the application to access flags for the purpose of controlling application - * flow. It is intended for monitoring, analytics, or debugging purposes. - */ -export interface LDInspectionIdentifyHandler { - type: 'client-identity-changed'; - - /** - * Name of the inspector. Will be used for logging issues with the inspector. - */ - name: string; - - /** - * This method will be called when an identify operation completes. - */ - method: (context: LDContext) => void; -} - -export type LDInspection = - | LDInspectionFlagUsedHandler - | LDInspectionFlagDetailsChangedHandler - | LDInspectionFlagDetailChangedHandler - | LDInspectionIdentifyHandler; diff --git a/packages/shared/sdk-client/src/api/LDOptions.ts b/packages/shared/sdk-client/src/api/LDOptions.ts index e3c8b2488..70e6599bc 100644 --- a/packages/shared/sdk-client/src/api/LDOptions.ts +++ b/packages/shared/sdk-client/src/api/LDOptions.ts @@ -232,4 +232,20 @@ export interface LDOptions { * If `wrapperName` is unset, this field will be ignored. */ wrapperVersion?: string; + + /** + * LaunchDarkly Server SDKs historically downloaded all flag configuration and segments for a particular environment + * during initialization. + * + * For some customers, this is an unacceptably large amount of data, and has contributed to performance issues + * within their products. + * + * Filtered environments aim to solve this problem. By allowing customers to specify subsets of an environment's + * flags using a filter key, SDKs will initialize faster and use less memory. + * + * This payload filter key only applies to the default streaming and polling data sources. It will not affect + * TestData or FileData data sources, nor will it be applied to any data source provided through the featureStore + * config property. + */ + payloadFilterKey?: string; } diff --git a/packages/shared/sdk-client/src/api/index.ts b/packages/shared/sdk-client/src/api/index.ts index de5b9e531..24c6c13ce 100644 --- a/packages/shared/sdk-client/src/api/index.ts +++ b/packages/shared/sdk-client/src/api/index.ts @@ -2,5 +2,6 @@ import ConnectionMode from './ConnectionMode'; export * from './LDOptions'; export * from './LDClient'; +export * from './LDEvaluationDetail'; export { ConnectionMode }; diff --git a/packages/shared/sdk-client/src/configuration/Configuration.ts b/packages/shared/sdk-client/src/configuration/Configuration.ts index a34243f47..8883e8859 100644 --- a/packages/shared/sdk-client/src/configuration/Configuration.ts +++ b/packages/shared/sdk-client/src/configuration/Configuration.ts @@ -10,7 +10,6 @@ import { } from '@launchdarkly/js-sdk-common'; import { ConnectionMode, type LDOptions } from '../api'; -import { LDInspection } from '../api/LDInspection'; import validators from './validators'; const DEFAULT_POLLING_INTERVAL: number = 60 * 5; @@ -41,7 +40,6 @@ export default class Configuration { public readonly useReport: boolean = false; public readonly withReasons: boolean = false; - public readonly inspectors: LDInspection[] = []; public readonly privateAttributes: string[] = []; public readonly initialConnectionMode: ConnectionMode = 'streaming'; @@ -80,6 +78,7 @@ export default class Configuration { internalOptions.analyticsEventPath, internalOptions.diagnosticEventPath, internalOptions.includeAuthorizationHeader, + pristineOptions.payloadFilterKey, ); this.useReport = pristineOptions.useReport ?? false; diff --git a/packages/shared/sdk-client/src/configuration/validators.ts b/packages/shared/sdk-client/src/configuration/validators.ts index d59597a77..17851bff1 100644 --- a/packages/shared/sdk-client/src/configuration/validators.ts +++ b/packages/shared/sdk-client/src/configuration/validators.ts @@ -1,18 +1,7 @@ // eslint-disable-next-line max-classes-per-file -import { noop, TypeValidator, TypeValidators } from '@launchdarkly/js-sdk-common'; +import { TypeValidator, TypeValidators } from '@launchdarkly/js-sdk-common'; import { type LDOptions } from '../api'; -import { LDInspection } from '../api/LDInspection'; - -class BootStrapValidator implements TypeValidator { - is(u: unknown): boolean { - return typeof u === 'object' || typeof u === 'undefined' || u === null; - } - - getType(): string { - return `LDFlagSet`; - } -} class ConnectionModeValidator implements TypeValidator { is(u: unknown): boolean { @@ -46,22 +35,14 @@ const validators: Record = { pollInterval: TypeValidators.numberWithMin(30), - // TODO: inspectors - // @ts-ignore - inspectors: TypeValidators.createTypeArray('LDInspection[]', { - type: 'flag-used', - method: noop, - name: '', - }), + useReport: TypeValidators.Boolean, + privateAttributes: TypeValidators.StringArray, applicationInfo: TypeValidators.Object, - // TODO: bootstrap - bootstrap: new BootStrapValidator(), wrapperName: TypeValidators.String, wrapperVersion: TypeValidators.String, - // TODO: hash - hash: TypeValidators.String, + payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/), }; export default validators; diff --git a/packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.ts b/packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.ts index f653b581f..aeb8bf3c5 100644 --- a/packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.ts +++ b/packages/shared/sdk-client/src/diagnostics/createDiagnosticsInitConfig.ts @@ -26,8 +26,9 @@ const createDiagnosticsInitConfig = (config: Configuration): DiagnosticsInitConf reconnectTimeMillis: secondsToMillis(config.streamInitialReconnectDelay), diagnosticRecordingIntervalMillis: secondsToMillis(config.diagnosticRecordingInterval), allAttributesPrivate: config.allAttributesPrivate, - usingSecureMode: !!config.hash, - bootstrapMode: !!config.bootstrap, + // TODO: Implement when corresponding features are implemented. + usingSecureMode: false, + bootstrapMode: false, }); export default createDiagnosticsInitConfig; diff --git a/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts b/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts new file mode 100644 index 000000000..f0e8d741c --- /dev/null +++ b/packages/shared/sdk-client/src/evaluation/evaluationDetail.ts @@ -0,0 +1,26 @@ +import { internal, LDEvaluationReason, LDFlagValue } from '@launchdarkly/js-sdk-common'; + +import { LDEvaluationDetail } from '../api'; + +export function createErrorEvaluationDetail( + errorKind: internal.ErrorKinds, + def?: LDFlagValue, +): LDEvaluationDetail { + return { + value: def ?? null, + variationIndex: null, + reason: { kind: 'ERROR', errorKind }, + }; +} + +export function createSuccessEvaluationDetail( + value: LDFlagValue, + variationIndex?: number, + reason?: LDEvaluationReason, +): LDEvaluationDetail { + return { + value, + variationIndex: variationIndex ?? null, + reason: reason ?? null, + }; +} diff --git a/packages/shared/sdk-client/src/flag-manager/FlagStore.ts b/packages/shared/sdk-client/src/flag-manager/FlagStore.ts index d9ce91b11..f58959721 100644 --- a/packages/shared/sdk-client/src/flag-manager/FlagStore.ts +++ b/packages/shared/sdk-client/src/flag-manager/FlagStore.ts @@ -31,7 +31,10 @@ export class DefaultFlagStore implements FlagStore { } get(key: string): ItemDescriptor | undefined { - return this.flags[key]; + if (Object.prototype.hasOwnProperty.call(this.flags, key)) { + return this.flags[key]; + } + return undefined; } getAll(): { [key: string]: ItemDescriptor } { diff --git a/packages/shared/sdk-client/src/index.ts b/packages/shared/sdk-client/src/index.ts index 4099b9ec7..32751c193 100644 --- a/packages/shared/sdk-client/src/index.ts +++ b/packages/shared/sdk-client/src/index.ts @@ -1,9 +1,22 @@ import LDClientImpl from './LDClientImpl'; +export * from '@launchdarkly/js-sdk-common'; + export * as platform from '@launchdarkly/js-sdk-common'; -export * from './api'; + +// To replace the exports from `export *` we need to name them. +// So the below exports replace them with the Node specific variants. + +// These exports are explicit to override those from common. +export type { + LDEvaluationDetail, + LDEvaluationDetailTyped, + LDClient, + LDOptions, + ConnectionMode, +} from './api'; + export { StreamingPaths } from './streaming'; -export * from '@launchdarkly/js-sdk-common'; export { LDClientImpl }; diff --git a/packages/shared/sdk-client/src/polling/PollingProcessor.ts b/packages/shared/sdk-client/src/polling/PollingProcessor.ts index 23fe35770..2b95f27fd 100644 --- a/packages/shared/sdk-client/src/polling/PollingProcessor.ts +++ b/packages/shared/sdk-client/src/polling/PollingProcessor.ts @@ -1,5 +1,6 @@ import { ApplicationTags, + getPollingUri, httpErrorMessage, HttpErrorResponse, Info, @@ -48,11 +49,12 @@ export default class PollingProcessor implements subsystem.LDStreamProcessor { requests: Requests, info: Info, uriPath: string, + parameters: { key: string; value: string }[], config: PollingConfig, private readonly dataHandler: (flags: Flags) => void, private readonly errorHandler?: PollingErrorHandler, ) { - const uri = `${config.serviceEndpoints.polling}${uriPath}`; + const uri = getPollingUri(config.serviceEndpoints, uriPath, parameters); this.logger = config.logger; this.pollInterval = config.pollInterval; diff --git a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts index 931b9e587..9de16d541 100644 --- a/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts +++ b/packages/shared/sdk-client/src/streaming/DataSourceConfig.ts @@ -1,7 +1,8 @@ -import { ApplicationTags, Encoding, Info } from '@launchdarkly/js-sdk-common'; +import { ApplicationTags, Encoding, Info, ServiceEndpoints } from '@launchdarkly/js-sdk-common'; export interface DataSourceConfig { credential: string; + serviceEndpoints: ServiceEndpoints; info: Info; tags?: ApplicationTags; withReasons: boolean; @@ -10,7 +11,6 @@ export interface DataSourceConfig { export interface StreamingDataSourceConfig extends DataSourceConfig { initialRetryDelayMillis: number; - streamingEndpoint: string; paths: StreamingPaths; } diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts index a7d74f6c8..73356787b 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.test.ts @@ -43,7 +43,8 @@ let basicPlatform: Platform; function getStreamingDataSourceConfig(): StreamingDataSourceConfig { return { credential: sdkKey, - streamingEndpoint: serviceEndpoints.streaming, + // eslint-disable-next-line object-shorthand + serviceEndpoints: serviceEndpoints, paths: { pathGet(_encoding: Encoding, _credential: string, _plainContextString: string): string { return '/stream/path'; diff --git a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts index 5e1f00d56..7f3e6e11b 100644 --- a/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts +++ b/packages/shared/sdk-client/src/streaming/StreamingProcessor.ts @@ -3,6 +3,7 @@ import { Encoding, EventName, EventSource, + getStreamingUri, httpErrorMessage, HttpErrorResponse, internal, @@ -44,12 +45,13 @@ class StreamingProcessor implements subsystem.LDStreamProcessor { private readonly errorHandler?: internal.StreamingErrorHandler, private readonly logger?: LDLogger, ) { - let path = dataSourceConfig.useReport + const path = dataSourceConfig.useReport ? dataSourceConfig.paths.pathReport(encoding, dataSourceConfig.credential, plainContextString) : dataSourceConfig.paths.pathGet(encoding, dataSourceConfig.credential, plainContextString); - if (dataSourceConfig.withReasons) { - path = `${path}?withReasons=true`; + const parameters: { key: string; value: string }[] = []; + if (this.dataSourceConfig.withReasons) { + parameters.push({ key: 'withReasons', value: 'true' }); } this.headers = defaultHeaders( @@ -60,7 +62,7 @@ class StreamingProcessor implements subsystem.LDStreamProcessor { this.logger = logger; this.requests = requests; - this.streamUri = `${dataSourceConfig.streamingEndpoint}${path}`; + this.streamUri = getStreamingUri(dataSourceConfig.serviceEndpoints, path, parameters); } private logConnectionStarted() { diff --git a/packages/shared/sdk-server-edge/CHANGELOG.md b/packages/shared/sdk-server-edge/CHANGELOG.md index 1876e13f7..8b94fd069 100644 --- a/packages/shared/sdk-server-edge/CHANGELOG.md +++ b/packages/shared/sdk-server-edge/CHANGELOG.md @@ -96,6 +96,33 @@ * dependencies * @launchdarkly/js-server-sdk-common bumped from 2.2.1 to 2.2.2 +## [2.3.9](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-edge-v2.3.8...js-server-sdk-common-edge-v2.3.9) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.6.0 to 2.6.1 + +## [2.3.8](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-edge-v2.3.7...js-server-sdk-common-edge-v2.3.8) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.5.0 to 2.6.0 + +## [2.3.7](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-edge-v2.3.6...js-server-sdk-common-edge-v2.3.7) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-server-sdk-common bumped from 2.4.5 to 2.5.0 + ## [2.3.6](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-edge-v2.3.5...js-server-sdk-common-edge-v2.3.6) (2024-08-12) diff --git a/packages/shared/sdk-server-edge/README.md b/packages/shared/sdk-server-edge/README.md index 5aeff2e9a..d3ab7ed33 100644 --- a/packages/shared/sdk-server-edge/README.md +++ b/packages/shared/sdk-server-edge/README.md @@ -12,7 +12,7 @@ See [Contributing](../CONTRIBUTING.md). ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -20,7 +20,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.test.ts b/packages/shared/sdk-server-edge/__tests__/api/EdgeFeatureStore.test.ts similarity index 95% rename from packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.test.ts rename to packages/shared/sdk-server-edge/__tests__/api/EdgeFeatureStore.test.ts index 4b8acf6f1..203d0bee0 100644 --- a/packages/shared/sdk-server-edge/src/api/EdgeFeatureStore.test.ts +++ b/packages/shared/sdk-server-edge/__tests__/api/EdgeFeatureStore.test.ts @@ -1,8 +1,8 @@ import { AsyncStoreFacade, LDFeatureStore } from '@launchdarkly/js-server-sdk-common'; -import mockEdgeProvider from '../utils/mockEdgeProvider'; -import * as testData from '../utils/testData.json'; -import { EdgeFeatureStore } from './EdgeFeatureStore'; +import { EdgeFeatureStore } from '../../src/api/EdgeFeatureStore'; +import mockEdgeProvider from '../../src/utils/mockEdgeProvider'; +import * as testData from './testData.json'; describe('EdgeFeatureStore', () => { const sdkKey = 'sdkKey'; diff --git a/packages/shared/sdk-server-edge/src/api/LDClient.test.ts b/packages/shared/sdk-server-edge/__tests__/api/LDClient.test.ts similarity index 96% rename from packages/shared/sdk-server-edge/src/api/LDClient.test.ts rename to packages/shared/sdk-server-edge/__tests__/api/LDClient.test.ts index d91e1a751..617375778 100644 --- a/packages/shared/sdk-server-edge/src/api/LDClient.test.ts +++ b/packages/shared/sdk-server-edge/__tests__/api/LDClient.test.ts @@ -1,7 +1,7 @@ import { internal } from '@launchdarkly/js-server-sdk-common'; import { createBasicPlatform } from '@launchdarkly/private-js-mocks'; -import LDClient from './LDClient'; +import LDClient from '../../src/api/LDClient'; jest.mock('@launchdarkly/js-sdk-common', () => { const actual = jest.requireActual('@launchdarkly/js-sdk-common'); diff --git a/packages/shared/sdk-server-edge/src/api/createCallbacks.test.ts b/packages/shared/sdk-server-edge/__tests__/api/createCallbacks.test.ts similarity index 97% rename from packages/shared/sdk-server-edge/src/api/createCallbacks.test.ts rename to packages/shared/sdk-server-edge/__tests__/api/createCallbacks.test.ts index 43d8d8150..957542a6c 100644 --- a/packages/shared/sdk-server-edge/src/api/createCallbacks.test.ts +++ b/packages/shared/sdk-server-edge/__tests__/api/createCallbacks.test.ts @@ -3,7 +3,7 @@ import { EventEmitter } from 'node:events'; import { noop } from '@launchdarkly/js-server-sdk-common'; import { createLogger } from '@launchdarkly/private-js-mocks'; -import createCallbacks from './createCallbacks'; +import createCallbacks from '../../src/api/createCallbacks'; let logger: ReturnType; diff --git a/packages/shared/sdk-server-edge/src/api/createOptions.test.ts b/packages/shared/sdk-server-edge/__tests__/api/createOptions.test.ts similarity index 83% rename from packages/shared/sdk-server-edge/src/api/createOptions.test.ts rename to packages/shared/sdk-server-edge/__tests__/api/createOptions.test.ts index 4c5d4962b..02fcc4b45 100644 --- a/packages/shared/sdk-server-edge/src/api/createOptions.test.ts +++ b/packages/shared/sdk-server-edge/__tests__/api/createOptions.test.ts @@ -1,6 +1,6 @@ import { BasicLogger } from '@launchdarkly/js-server-sdk-common'; -import createOptions, { defaultOptions } from './createOptions'; +import createOptions, { defaultOptions } from '../../src/api/createOptions'; describe('createOptions', () => { test('default options', () => { diff --git a/packages/shared/sdk-server-edge/src/utils/testData.json b/packages/shared/sdk-server-edge/__tests__/api/testData.json similarity index 100% rename from packages/shared/sdk-server-edge/src/utils/testData.json rename to packages/shared/sdk-server-edge/__tests__/api/testData.json diff --git a/packages/shared/sdk-server-edge/src/utils/validateOptions.test.ts b/packages/shared/sdk-server-edge/__tests__/utils/validateOptions.test.ts similarity index 91% rename from packages/shared/sdk-server-edge/src/utils/validateOptions.test.ts rename to packages/shared/sdk-server-edge/__tests__/utils/validateOptions.test.ts index 4836160ee..cc39f925c 100644 --- a/packages/shared/sdk-server-edge/src/utils/validateOptions.test.ts +++ b/packages/shared/sdk-server-edge/__tests__/utils/validateOptions.test.ts @@ -1,7 +1,7 @@ import { BasicLogger } from '@launchdarkly/js-server-sdk-common'; -import mockFeatureStore from './mockFeatureStore'; -import validateOptions from './validateOptions'; +import mockFeatureStore from '../../src/utils/mockFeatureStore'; +import validateOptions from '../../src/utils/validateOptions'; describe('validateOptions', () => { test('throws without SDK key', () => { diff --git a/packages/shared/sdk-server-edge/package.json b/packages/shared/sdk-server-edge/package.json index eeb767774..d124022f6 100644 --- a/packages/shared/sdk-server-edge/package.json +++ b/packages/shared/sdk-server-edge/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-server-sdk-common-edge", - "version": "2.3.6", + "version": "2.3.9", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/shared/sdk-server-edge", "repository": { "type": "git", @@ -36,7 +36,7 @@ "check": "yarn prettier && yarn lint && yarn build && yarn test && yarn doc" }, "dependencies": { - "@launchdarkly/js-server-sdk-common": "2.4.5", + "@launchdarkly/js-server-sdk-common": "2.6.1", "crypto-js": "^4.1.1" }, "devDependencies": { diff --git a/packages/shared/sdk-server/CHANGELOG.md b/packages/shared/sdk-server/CHANGELOG.md index 6444236e1..ea725e53e 100644 --- a/packages/shared/sdk-server/CHANGELOG.md +++ b/packages/shared/sdk-server/CHANGELOG.md @@ -8,6 +8,41 @@ All notable changes to `@launchdarkly/js-server-sdk-common` will be documented i * dependencies * @launchdarkly/js-sdk-common bumped from 2.3.0 to 2.3.1 +## [2.6.1](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-v2.6.0...js-server-sdk-common-v2.6.1) (2024-09-05) + + +### Bug Fixes + +* Correctly handle null values in JSON variations. ([#569](https://github.com/launchdarkly/js-core/issues/569)) ([907d08b](https://github.com/launchdarkly/js-core/commit/907d08b730ce9745c1b221f2f539f7c56c3a0234)), closes [#568](https://github.com/launchdarkly/js-core/issues/568) + +## [2.6.0](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-v2.5.0...js-server-sdk-common-v2.6.0) (2024-09-03) + + +### Features + +* Add support for Payload Filtering ([#551](https://github.com/launchdarkly/js-core/issues/551)) ([6f44383](https://github.com/launchdarkly/js-core/commit/6f4438323baed802d8f951ac82494e6cfa9932c5)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-sdk-common bumped from 2.7.0 to 2.8.0 + +## [2.5.0](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-v2.4.5...js-server-sdk-common-v2.5.0) (2024-08-28) + + +### Features + +* Correct client evaluation typings. ([#554](https://github.com/launchdarkly/js-core/issues/554)) ([64ab88d](https://github.com/launchdarkly/js-core/commit/64ab88d278308564b4cd7b6433870c7adb09142a)) + + +### Dependencies + +* The following workspace dependencies were updated + * dependencies + * @launchdarkly/js-sdk-common bumped from 2.6.0 to 2.7.0 + ## [2.4.5](https://github.com/launchdarkly/js-core/compare/js-server-sdk-common-v2.4.4...js-server-sdk-common-v2.4.5) (2024-08-12) diff --git a/packages/shared/sdk-server/README.md b/packages/shared/sdk-server/README.md index c2b058f3d..86c286750 100644 --- a/packages/shared/sdk-server/README.md +++ b/packages/shared/sdk-server/README.md @@ -12,7 +12,7 @@ See [Contributing](../CONTRIBUTING.md). ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -20,7 +20,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/shared/sdk-server/__tests__/options/Configuration.test.ts b/packages/shared/sdk-server/__tests__/options/Configuration.test.ts index 03b6fa27d..399439be7 100644 --- a/packages/shared/sdk-server/__tests__/options/Configuration.test.ts +++ b/packages/shared/sdk-server/__tests__/options/Configuration.test.ts @@ -41,6 +41,7 @@ describe.each([undefined, null, 'potat0', 17, [], {}])('constructed without opti expect(config.wrapperName).toBeUndefined(); expect(config.wrapperVersion).toBeUndefined(); expect(config.hooks).toBeUndefined(); + expect(config.payloadFilterKey).toBeUndefined(); }); }); @@ -329,6 +330,34 @@ describe('when setting different options', () => { logger(config).expectMessages(logs); }); + it.each([ + ['1', '1', []], + ['camelCaseWorks', 'camelCaseWorks', []], + ['PascalCaseWorks', 'PascalCaseWorks', []], + ['kebab-case-works', 'kebab-case-works', []], + ['snake_case_works', 'snake_case_works', []], + [ + 'invalid-@-filter', + undefined, + [{ level: LogLevel.Warn, matches: /Config option "payloadFilterKey" should be of type/ }], + ], + [ + '_invalid-filter', + undefined, + [{ level: LogLevel.Warn, matches: /Config option "payloadFilterKey" should be of type/ }], + ], + [ + '-invalid-filter', + undefined, + [{ level: LogLevel.Warn, matches: /Config option "payloadFilterKey" should be of type/ }], + ], + ])('allow setting and validates payloadFilterKey', (filter, expected, logs) => { + // @ts-ignore + const config = new Configuration(withLogger({ payloadFilterKey: filter })); + expect(config.payloadFilterKey).toEqual(expected); + logger(config).expectMessages(logs); + }); + it('discards unrecognized options with a warning', () => { // @ts-ignore const config = new Configuration(withLogger({ yes: 'no', cat: 'yes' })); diff --git a/packages/shared/sdk-server/__tests__/store/serialization.test.ts b/packages/shared/sdk-server/__tests__/store/serialization.test.ts index 4656ea551..56e41e964 100644 --- a/packages/shared/sdk-server/__tests__/store/serialization.test.ts +++ b/packages/shared/sdk-server/__tests__/store/serialization.test.ts @@ -1,11 +1,14 @@ +import { AttributeReference } from '@launchdarkly/js-sdk-common'; + import { Flag } from '../../src/evaluation/data/Flag'; import { Segment } from '../../src/evaluation/data/Segment'; import { deserializeAll, deserializeDelete, deserializePatch, + nullReplacer, replacer, - reviver, + serializeFlag, serializeSegment, } from '../../src/store/serialization'; @@ -152,6 +155,38 @@ const segmentWithBucketBy = { deleted: false, }; +const flagWithNullInJsonVariation = { + key: 'flagName', + on: true, + fallthrough: { variation: 1 }, + variations: [[true, null, 'potato'], [null, null], { null: null }, { arr: [null] }], + version: 1, +}; + +const flagWithManyNulls = { + key: 'test-after-value1', + on: true, + rules: [ + { + variation: 0, + id: 'ruleid', + clauses: [ + { + attribute: 'attrname', + op: 'after', + values: ['not valid'], + negate: null, + }, + ], + trackEvents: null, + }, + ], + offVariation: null, + fallthrough: { variation: 1 }, + variations: [true, false], + version: 1, +}; + function makeAllData(flag?: any, segment?: any): any { const allData: any = { data: { @@ -239,6 +274,42 @@ describe('when deserializing all data', () => { const ref = parsed?.data.flags.flagName.rules?.[0].rollout?.bucketByAttributeReference; expect(ref?.isValid).toBeTruthy(); }); + + it('does not replace null in Objects or array JSON variations', () => { + const jsonString = makeSerializedAllData(flagWithNullInJsonVariation); + const parsed = deserializeAll(jsonString); + + expect(parsed?.data.flags.flagName.variations).toStrictEqual( + flagWithNullInJsonVariation.variations, + ); + }); + + it('removes null values outside variations', () => { + const jsonString = makeSerializedAllData(flagWithManyNulls); + const parsed = deserializeAll(jsonString); + + expect(parsed?.data.flags.flagName).toStrictEqual({ + key: 'test-after-value1', + on: true, + rules: [ + { + variation: 0, + id: 'ruleid', + clauses: [ + { + attribute: 'attrname', + attributeReference: new AttributeReference('attrname'), + op: 'after', + values: ['not valid'], + }, + ], + }, + ], + fallthrough: { variation: 1 }, + variations: [true, false], + version: 1, + }); + }); }); describe('when deserializing patch data', () => { @@ -290,9 +361,45 @@ describe('when deserializing patch data', () => { const ref = (parsed?.data as Flag).rules?.[0].rollout?.bucketByAttributeReference; expect(ref?.isValid).toBeTruthy(); }); + + it('does not replace null in Objects or array JSON variations', () => { + const jsonString = makeSerializedPatchData(flagWithNullInJsonVariation); + const parsed = deserializePatch(jsonString); + + expect((parsed?.data as Flag)?.variations).toStrictEqual( + flagWithNullInJsonVariation.variations, + ); + }); + + it('removes null values outside variations', () => { + const jsonString = makeSerializedPatchData(flagWithManyNulls); + const parsed = deserializePatch(jsonString); + + expect(parsed?.data as Flag).toStrictEqual({ + key: 'test-after-value1', + on: true, + rules: [ + { + variation: 0, + id: 'ruleid', + clauses: [ + { + attribute: 'attrname', + attributeReference: new AttributeReference('attrname'), + op: 'after', + values: ['not valid'], + }, + ], + }, + ], + fallthrough: { variation: 1 }, + variations: [true, false], + version: 1, + }); + }); }); -it('removes null elements', () => { +it('removes null elements that are not part of arrays', () => { const baseData = { a: 'b', b: 'c', @@ -306,10 +413,49 @@ it('removes null elements', () => { polluted.c.f = null; const stringPolluted = JSON.stringify(polluted); - const parsed = JSON.parse(stringPolluted, reviver); + const parsed = JSON.parse(stringPolluted); + nullReplacer(parsed); expect(parsed).toStrictEqual(baseData); }); +it('does not remove null in arrays', () => { + const data = { + a: ['b', null, { arr: [null] }], + c: { + d: ['e', null, { arr: [null] }], + }, + }; + + const parsed = JSON.parse(JSON.stringify(data)); + nullReplacer(parsed); + expect(parsed).toStrictEqual(data); +}); + +it('does remove null from objects that are inside of arrays', () => { + const data = { + a: ['b', null, { null: null, notNull: true }], + c: { + d: ['e', null, { null: null, notNull: true }], + }, + }; + + const parsed = JSON.parse(JSON.stringify(data)); + nullReplacer(parsed); + expect(parsed).toStrictEqual({ + a: ['b', null, { notNull: true }], + c: { + d: ['e', null, { notNull: true }], + }, + }); +}); + +it('can handle attempting to replace nulls for an undefined or null value', () => { + expect(() => { + nullReplacer(null); + nullReplacer(undefined); + }).not.toThrow(); +}); + it.each([ [flagWithAttributeNameInClause, undefined], [flagWithAttributeReferenceInClause, undefined], @@ -450,3 +596,11 @@ it('serialization converts sets back to arrays for includedContexts/excludedCont expect(jsonDeserialized.includedContexts[0].generated_valuesSet).toBeUndefined(); expect(jsonDeserialized.excludedContexts[0].generated_valuesSet).toBeUndefined(); }); + +it('serializes null values without issue', () => { + const jsonString = makeSerializedAllData(flagWithNullInJsonVariation); + const parsed = deserializeAll(jsonString); + const serialized = serializeFlag(parsed!.data.flags.flagName); + // After serialization nulls should still be there, and any memo generated items should be gone. + expect(JSON.parse(serialized)).toEqual(flagWithNullInJsonVariation); +}); diff --git a/packages/shared/sdk-server/jest.config.js b/packages/shared/sdk-server/jest.config.js index de3bcfc59..6753062cc 100644 --- a/packages/shared/sdk-server/jest.config.js +++ b/packages/shared/sdk-server/jest.config.js @@ -3,5 +3,5 @@ module.exports = { testMatch: ['**/*.test.ts?(x)'], testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - collectCoverageFrom: ['src/**/*.ts'] + collectCoverageFrom: ['src/**/*.ts'], }; diff --git a/packages/shared/sdk-server/package.json b/packages/shared/sdk-server/package.json index 1cc300af3..133683e90 100644 --- a/packages/shared/sdk-server/package.json +++ b/packages/shared/sdk-server/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/js-server-sdk-common", - "version": "2.4.5", + "version": "2.6.1", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -27,7 +27,7 @@ }, "license": "Apache-2.0", "dependencies": { - "@launchdarkly/js-sdk-common": "2.6.0", + "@launchdarkly/js-sdk-common": "2.8.0", "semver": "7.5.4" }, "devDependencies": { diff --git a/packages/shared/sdk-server/src/LDClientImpl.ts b/packages/shared/sdk-server/src/LDClientImpl.ts index de271ea36..688a36b40 100644 --- a/packages/shared/sdk-server/src/LDClientImpl.ts +++ b/packages/shared/sdk-server/src/LDClientImpl.ts @@ -217,6 +217,7 @@ export default class LDClientImpl implements LDClient { sdkKey, clientContext, '/all', + [], listeners, this.diagnosticsManager, (e) => this.dataSourceErrorHandler(e), diff --git a/packages/shared/sdk-server/src/api/options/LDOptions.ts b/packages/shared/sdk-server/src/api/options/LDOptions.ts index a0d38b0fa..769783973 100644 --- a/packages/shared/sdk-server/src/api/options/LDOptions.ts +++ b/packages/shared/sdk-server/src/api/options/LDOptions.ts @@ -268,6 +268,22 @@ export interface LDOptions { * ASCII digits, period, hyphen, underscore. A string containing any other characters will be ignored. */ versionName?: string; + + /** + * LaunchDarkly Server SDKs historically downloaded all flag configuration and segments for a particular environment + * during initialization. + * + * For some customers, this is an unacceptably large amount of data, and has contributed to performance issues + * within their products. + * + * Filtered environments aim to solve this problem. By allowing customers to specify subsets of an environment's + * flags using a filter key, SDKs will initialize faster and use less memory. + * + * This payload filter key only applies to the default streaming and polling data sources. It will not affect + * TestData or FileData data sources, nor will it be applied to any data source provided through the featureStore + * config property. + */ + payloadFilterKey?: string; }; /** diff --git a/packages/shared/sdk-server/src/data_sources/Requestor.ts b/packages/shared/sdk-server/src/data_sources/Requestor.ts index 6ffb2200d..d6498c604 100644 --- a/packages/shared/sdk-server/src/data_sources/Requestor.ts +++ b/packages/shared/sdk-server/src/data_sources/Requestor.ts @@ -1,5 +1,6 @@ import { defaultHeaders, + getPollingUri, Info, LDStreamingError, Options, @@ -33,7 +34,7 @@ export default class Requestor implements LDFeatureRequestor { private readonly requests: Requests, ) { this.headers = defaultHeaders(sdkKey, info, config.tags); - this.uri = `${config.serviceEndpoints.polling}/sdk/latest-all`; + this.uri = getPollingUri(config.serviceEndpoints, '/sdk/latest-all', []); } /** diff --git a/packages/shared/sdk-server/src/evaluation/EvalResult.ts b/packages/shared/sdk-server/src/evaluation/EvalResult.ts index 4ddb54543..cc1f70dcc 100644 --- a/packages/shared/sdk-server/src/evaluation/EvalResult.ts +++ b/packages/shared/sdk-server/src/evaluation/EvalResult.ts @@ -2,7 +2,6 @@ import { internal, LDEvaluationDetail, LDEvaluationReason } from '@launchdarkly/ import Reasons from './Reasons'; -const { createErrorEvaluationDetail, createSuccessEvaluationDetail } = internal; /** * A class which encapsulates the result of an evaluation. It allows for differentiating between * successful and error result types. @@ -31,11 +30,22 @@ export default class EvalResult { } static forError(errorKind: internal.ErrorKinds, message?: string, def?: any): EvalResult { - return new EvalResult(true, createErrorEvaluationDetail(errorKind, def), message); + return new EvalResult( + true, + { + value: def ?? null, + variationIndex: null, + reason: { kind: 'ERROR', errorKind }, + }, + message, + ); } static forSuccess(value: any, reason: LDEvaluationReason, variationIndex?: number) { - const successDetail = createSuccessEvaluationDetail(value, variationIndex, reason); - return new EvalResult(false, successDetail as LDEvaluationDetail); + return new EvalResult(false, { + value, + variationIndex: variationIndex === undefined ? null : variationIndex, + reason, + }); } } diff --git a/packages/shared/sdk-server/src/options/Configuration.ts b/packages/shared/sdk-server/src/options/Configuration.ts index 9e68a4f20..77baf0de3 100644 --- a/packages/shared/sdk-server/src/options/Configuration.ts +++ b/packages/shared/sdk-server/src/options/Configuration.ts @@ -55,6 +55,7 @@ const validations: Record = { wrapperName: TypeValidators.String, wrapperVersion: TypeValidators.String, application: TypeValidators.Object, + payloadFilterKey: TypeValidators.stringMatchingRegex(/^[a-zA-Z0-9](\w|\.|-)*$/), hooks: TypeValidators.createTypeArray('Hook[]', {}), }; @@ -197,6 +198,8 @@ export default class Configuration { public readonly tags: ApplicationTags; + public readonly payloadFilterKey?: string; + public readonly diagnosticRecordingInterval: number; public readonly featureStoreFactory: (clientContext: LDClientContext) => LDFeatureStore; @@ -234,6 +237,7 @@ export default class Configuration { internalOptions.analyticsEventPath, internalOptions.diagnosticEventPath, internalOptions.includeAuthorizationHeader, + validatedOptions.payloadFilterKey, ); this.eventsCapacity = validatedOptions.capacity; this.timeout = validatedOptions.timeout; @@ -255,6 +259,7 @@ export default class Configuration { this.tlsParams = validatedOptions.tlsParams; this.diagnosticOptOut = validatedOptions.diagnosticOptOut; this.wrapperName = validatedOptions.wrapperName; + this.payloadFilterKey = validatedOptions.payloadFilterKey; this.wrapperVersion = validatedOptions.wrapperVersion; this.tags = new ApplicationTags(validatedOptions); this.diagnosticRecordingInterval = validatedOptions.diagnosticRecordingInterval; diff --git a/packages/shared/sdk-server/src/store/serialization.ts b/packages/shared/sdk-server/src/store/serialization.ts index aaba1bc4d..c6f34d3f2 100644 --- a/packages/shared/sdk-server/src/store/serialization.ts +++ b/packages/shared/sdk-server/src/store/serialization.ts @@ -13,19 +13,6 @@ import VersionedDataKinds, { VersionedDataKind } from './VersionedDataKinds'; // The max size where we use an array instead of a set. const TARGET_LIST_ARRAY_CUTOFF = 100; -/** - * @internal - */ -export function reviver(this: any, key: string, value: any): any { - // Whenever a null is included we want to remove the field. - // In this way validation checks do not have to consider null, only undefined. - if (value === null) { - return undefined; - } - - return value; -} - export interface FlagsAndSegments { flags: { [name: string]: Flag }; segments: { [name: string]: Segment }; @@ -35,6 +22,60 @@ export interface AllData { data: FlagsAndSegments; } +/** + * Performs deep removal of null values. + * + * Does not remove null values from arrays. + * + * Note: This is a non-recursive implementation for performance and to avoid + * potential stack overflows. + * + * @param target The target to remove null values from. + * @param excludeKeys A list of top-level keys to exclude from null removal. + */ +export function nullReplacer(target: any, excludeKeys?: string[]): void { + const stack: { + key: string; + value: any; + parent: any; + }[] = []; + + if (target === null || target === undefined) { + return; + } + + const filteredEntries = Object.entries(target).filter( + ([key, _value]) => !excludeKeys?.includes(key), + ); + + stack.push( + ...filteredEntries.map(([key, value]) => ({ + key, + value, + parent: target, + })), + ); + + while (stack.length) { + const item = stack.pop()!; + // Do not remove items from arrays. + if (item.value === null && !Array.isArray(item.parent)) { + delete item.parent[item.key]; + } else if (typeof item.value === 'object' && item.value !== null) { + // Add all the children to the stack. This includes array children. + // The items in the array could themselves be objects which need nulls + // removed from them. + stack.push( + ...Object.entries(item.value).map(([key, value]) => ({ + key, + value, + parent: item.value, + })), + ); + } + } +} + /** * For use when serializing flags/segments. This will ensure local types * are converted to the appropriate JSON representation. @@ -54,6 +95,10 @@ export function replacer(this: any, key: string, value: any): any { return undefined; } } + // Allow null/undefined values to pass through without modification. + if (value === null || value === undefined) { + return value; + } if (value.generated_includedSet) { value.included = [...value.generated_includedSet]; delete value.generated_includedSet; @@ -108,6 +153,8 @@ function processRollout(rollout?: Rollout) { * @internal */ export function processFlag(flag: Flag) { + nullReplacer(flag, ['variations']); + if (flag.fallthrough && flag.fallthrough.rollout) { const rollout = flag.fallthrough.rollout!; processRollout(rollout); @@ -131,6 +178,7 @@ export function processFlag(flag: Flag) { * @internal */ export function processSegment(segment: Segment) { + nullReplacer(segment); if (segment?.included?.length && segment.included.length > TARGET_LIST_ARRAY_CUTOFF) { segment.generated_includedSet = new Set(segment.included); delete segment.included; @@ -183,7 +231,7 @@ export function processSegment(segment: Segment) { function tryParse(data: string): any { try { - return JSON.parse(data, reviver); + return JSON.parse(data); } catch { return undefined; } diff --git a/packages/store/node-server-sdk-dynamodb/CHANGELOG.md b/packages/store/node-server-sdk-dynamodb/CHANGELOG.md index f7d735740..80d58e259 100644 --- a/packages/store/node-server-sdk-dynamodb/CHANGELOG.md +++ b/packages/store/node-server-sdk-dynamodb/CHANGELOG.md @@ -90,6 +90,39 @@ * devDependencies * @launchdarkly/node-server-sdk bumped from 9.2.1 to 9.2.2 +## [6.1.21](https://github.com/launchdarkly/js-core/compare/node-server-sdk-dynamodb-v6.1.20...node-server-sdk-dynamodb-v6.1.21) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.3 to 9.5.4 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.4 + +## [6.1.20](https://github.com/launchdarkly/js-core/compare/node-server-sdk-dynamodb-v6.1.19...node-server-sdk-dynamodb-v6.1.20) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.2 to 9.5.3 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.3 + +## [6.1.19](https://github.com/launchdarkly/js-core/compare/node-server-sdk-dynamodb-v6.1.18...node-server-sdk-dynamodb-v6.1.19) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.1 to 9.5.2 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.2 + ## [6.1.18](https://github.com/launchdarkly/js-core/compare/node-server-sdk-dynamodb-v6.1.17...node-server-sdk-dynamodb-v6.1.18) (2024-08-12) diff --git a/packages/store/node-server-sdk-dynamodb/README.md b/packages/store/node-server-sdk-dynamodb/README.md index 5dbdfd094..0a097dc17 100644 --- a/packages/store/node-server-sdk-dynamodb/README.md +++ b/packages/store/node-server-sdk-dynamodb/README.md @@ -93,7 +93,7 @@ We encourage pull requests and other contributions from the community. Check out ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -101,7 +101,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/store/node-server-sdk-dynamodb/package.json b/packages/store/node-server-sdk-dynamodb/package.json index e848b8f32..21ab2b250 100644 --- a/packages/store/node-server-sdk-dynamodb/package.json +++ b/packages/store/node-server-sdk-dynamodb/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk-dynamodb", - "version": "6.1.18", + "version": "6.1.21", "description": "DynamoDB-backed feature store for the LaunchDarkly Server-Side SDK for Node.js", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/store/node-server-sdk-dynamodb", "repository": { @@ -35,7 +35,7 @@ }, "devDependencies": { "@aws-sdk/client-dynamodb": "3.348.0", - "@launchdarkly/node-server-sdk": "9.5.1", + "@launchdarkly/node-server-sdk": "9.5.4", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.4.0", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/packages/store/node-server-sdk-redis/CHANGELOG.md b/packages/store/node-server-sdk-redis/CHANGELOG.md index 561581da1..0bbb88605 100644 --- a/packages/store/node-server-sdk-redis/CHANGELOG.md +++ b/packages/store/node-server-sdk-redis/CHANGELOG.md @@ -90,6 +90,39 @@ * devDependencies * @launchdarkly/node-server-sdk bumped from 9.2.1 to 9.2.2 +## [4.1.21](https://github.com/launchdarkly/js-core/compare/node-server-sdk-redis-v4.1.20...node-server-sdk-redis-v4.1.21) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.3 to 9.5.4 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.4 + +## [4.1.20](https://github.com/launchdarkly/js-core/compare/node-server-sdk-redis-v4.1.19...node-server-sdk-redis-v4.1.20) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.2 to 9.5.3 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.3 + +## [4.1.19](https://github.com/launchdarkly/js-core/compare/node-server-sdk-redis-v4.1.18...node-server-sdk-redis-v4.1.19) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.1 to 9.5.2 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.2 + ## [4.1.18](https://github.com/launchdarkly/js-core/compare/node-server-sdk-redis-v4.1.17...node-server-sdk-redis-v4.1.18) (2024-08-12) diff --git a/packages/store/node-server-sdk-redis/README.md b/packages/store/node-server-sdk-redis/README.md index 7cc0d32bd..a5043787b 100644 --- a/packages/store/node-server-sdk-redis/README.md +++ b/packages/store/node-server-sdk-redis/README.md @@ -68,7 +68,7 @@ We encourage pull requests and other contributions from the community. Check out ## Verifying SDK build provenance with the SLSA framework -LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). +LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply-chain Levels for Software Artifacts) to help developers make their supply chain more secure by ensuring the authenticity and build integrity of our published SDK packages. To learn more, see the [provenance guide](PROVENANCE.md). ## About LaunchDarkly @@ -76,7 +76,7 @@ LaunchDarkly uses the [SLSA framework](https://slsa.dev/spec/v1.0/about) (Supply - Roll out a new feature to a subset of your users (like a group of users who opt-in to a beta tester group), gathering feedback and bug reports from real-world use cases. - Gradually roll out a feature to an increasing percentage of users, and track the effect that the feature has on key metrics (for instance, how likely is a user to complete a purchase if they have feature A versus feature B?). - Turn off a feature that you realize is causing performance problems in production, without needing to re-deploy, or even restart the application with a changed configuration file. - - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). + - Grant access to certain features based on user attributes, like payment plan (eg: users on the ‘gold’ plan get access to more features than users in the ‘silver’ plan). - Disable parts of your application to facilitate maintenance, without taking everything offline. - LaunchDarkly provides feature flag SDKs for a wide variety of languages and technologies. Check out [our documentation](https://docs.launchdarkly.com/sdk) for a complete list. - Explore LaunchDarkly diff --git a/packages/store/node-server-sdk-redis/package.json b/packages/store/node-server-sdk-redis/package.json index 839fb1d4b..a2cf4cdde 100644 --- a/packages/store/node-server-sdk-redis/package.json +++ b/packages/store/node-server-sdk-redis/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk-redis", - "version": "4.1.18", + "version": "4.1.21", "description": "Redis-backed feature store for the LaunchDarkly Server-Side SDK for Node.js", "homepage": "https://github.com/launchdarkly/js-core/tree/main/packages/store/node-server-sdk-redis", "repository": { @@ -33,7 +33,7 @@ "@launchdarkly/node-server-sdk": ">=9.4.3" }, "devDependencies": { - "@launchdarkly/node-server-sdk": "9.5.1", + "@launchdarkly/node-server-sdk": "9.5.4", "@trivago/prettier-plugin-sort-imports": "^4.1.1", "@types/jest": "^29.4.0", "@typescript-eslint/eslint-plugin": "^6.20.0", diff --git a/packages/telemetry/node-server-sdk-otel/CHANGELOG.md b/packages/telemetry/node-server-sdk-otel/CHANGELOG.md index 4e07a5f67..874a549aa 100644 --- a/packages/telemetry/node-server-sdk-otel/CHANGELOG.md +++ b/packages/telemetry/node-server-sdk-otel/CHANGELOG.md @@ -1,5 +1,38 @@ # Changelog +## [1.0.13](https://github.com/launchdarkly/js-core/compare/node-server-sdk-otel-v1.0.12...node-server-sdk-otel-v1.0.13) (2024-09-05) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.3 to 9.5.4 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.4 + +## [1.0.12](https://github.com/launchdarkly/js-core/compare/node-server-sdk-otel-v1.0.11...node-server-sdk-otel-v1.0.12) (2024-09-03) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.2 to 9.5.3 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.3 + +## [1.0.11](https://github.com/launchdarkly/js-core/compare/node-server-sdk-otel-v1.0.10...node-server-sdk-otel-v1.0.11) (2024-08-28) + + +### Dependencies + +* The following workspace dependencies were updated + * devDependencies + * @launchdarkly/node-server-sdk bumped from 9.5.1 to 9.5.2 + * peerDependencies + * @launchdarkly/node-server-sdk bumped from >=9.4.3 to >=9.5.2 + ## [1.0.10](https://github.com/launchdarkly/js-core/compare/node-server-sdk-otel-v1.0.9...node-server-sdk-otel-v1.0.10) (2024-08-12) diff --git a/packages/telemetry/node-server-sdk-otel/README.md b/packages/telemetry/node-server-sdk-otel/README.md index 9783df499..bff407ee5 100644 --- a/packages/telemetry/node-server-sdk-otel/README.md +++ b/packages/telemetry/node-server-sdk-otel/README.md @@ -41,7 +41,7 @@ import { TracingHook } from '@launchdarkly/node-server-sdk-otel'; ```typescript import { init } from '@launchdarkly/node-server-sdk'; -const client = init('YOUR SDK KEY', {hooks: [new TracingHook()]}); +const client = init('YOUR SDK KEY', { hooks: [new TracingHook()] }); ``` ## Contributing diff --git a/packages/telemetry/node-server-sdk-otel/src/TracingHook.test.ts b/packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts similarity index 99% rename from packages/telemetry/node-server-sdk-otel/src/TracingHook.test.ts rename to packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts index 7438fe9c3..edc110b1e 100644 --- a/packages/telemetry/node-server-sdk-otel/src/TracingHook.test.ts +++ b/packages/telemetry/node-server-sdk-otel/__tests__/TracingHook.test.ts @@ -4,7 +4,7 @@ import { InMemorySpanExporter, SimpleSpanProcessor } from '@opentelemetry/sdk-tr import { basicLogger, init, integrations } from '@launchdarkly/node-server-sdk'; -import TracingHook from './TracingHook'; +import TracingHook from '../src/TracingHook'; const spanExporter = new InMemorySpanExporter(); const sdk = new NodeSDK({ diff --git a/packages/telemetry/node-server-sdk-otel/jest.config.js b/packages/telemetry/node-server-sdk-otel/jest.config.js index de3bcfc59..6753062cc 100644 --- a/packages/telemetry/node-server-sdk-otel/jest.config.js +++ b/packages/telemetry/node-server-sdk-otel/jest.config.js @@ -3,5 +3,5 @@ module.exports = { testMatch: ['**/*.test.ts?(x)'], testEnvironment: 'node', moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], - collectCoverageFrom: ['src/**/*.ts'] + collectCoverageFrom: ['src/**/*.ts'], }; diff --git a/packages/telemetry/node-server-sdk-otel/package.json b/packages/telemetry/node-server-sdk-otel/package.json index 3d79004cb..2504c8960 100644 --- a/packages/telemetry/node-server-sdk-otel/package.json +++ b/packages/telemetry/node-server-sdk-otel/package.json @@ -1,6 +1,6 @@ { "name": "@launchdarkly/node-server-sdk-otel", - "version": "1.0.10", + "version": "1.0.13", "type": "commonjs", "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -33,7 +33,7 @@ "@opentelemetry/api": ">=1.3.0" }, "devDependencies": { - "@launchdarkly/node-server-sdk": "9.5.1", + "@launchdarkly/node-server-sdk": "9.5.4", "@launchdarkly/private-js-mocks": "0.0.1", "@opentelemetry/api": ">=1.3.0", "@opentelemetry/sdk-node": "0.49.1", diff --git a/packages/telemetry/node-server-sdk-otel/tsconfig.json b/packages/telemetry/node-server-sdk-otel/tsconfig.json index dbe90f4cc..94f3c7fd6 100644 --- a/packages/telemetry/node-server-sdk-otel/tsconfig.json +++ b/packages/telemetry/node-server-sdk-otel/tsconfig.json @@ -12,7 +12,7 @@ "sourceMap": true, "declaration": true, "declarationMap": true, // enables importers to jump to source - "stripInternal": true, + "stripInternal": true }, "include": ["src"], "exclude": ["**/*.test.ts", "dist", "node_modules", "__tests__"] diff --git a/renovate.json b/renovate.json index cf38b163d..8d2bde944 100644 --- a/renovate.json +++ b/renovate.json @@ -1,14 +1,10 @@ { - "extends": [ - "config:recommended" - ], + "extends": ["config:recommended"], "packageRules": [ { "enabled": false, "matchDatasources": ["npm"], - "matchPackageNames": [ - "*" - ] + "matchPackageNames": ["*"] } ], "vulnerabilityAlerts": { diff --git a/tsconfig.json b/tsconfig.json index e87ecee8f..02923fad6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -54,6 +54,9 @@ }, { "path": "./packages/tooling/jest/tsconfig.ref.json" + }, + { + "path": "./packages/sdk/browser/tsconfig.ref.json" } ] }