From ee49dc8ad3cb8b04f92930388c42d8241da77801 Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Fri, 5 Sep 2025 18:00:02 +0300 Subject: [PATCH 01/10] fix: Improve numeric check utility --- packages/js-sdk/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/js-sdk/README.md b/packages/js-sdk/README.md index 8087e399..8bb947bb 100644 --- a/packages/js-sdk/README.md +++ b/packages/js-sdk/README.md @@ -1075,4 +1075,4 @@ Happy converting! ## Credits -Copyright © 2025 Convert Insights, Inc. +Copyright © 2024 Convert Insights, Inc. From aff9810b8ff2124d9359ca89f8780d29a9388efa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 5 Sep 2025 16:20:23 +0000 Subject: [PATCH 02/10] chore: release main --- .release-please-manifest.json | 24 ++++++++++++------------ packages/api/CHANGELOG.md | 8 ++++++++ packages/api/package.json | 2 +- packages/bucketing/CHANGELOG.md | 8 ++++++++ packages/bucketing/package.json | 2 +- packages/data/CHANGELOG.md | 8 ++++++++ packages/data/package.json | 2 +- packages/enums/CHANGELOG.md | 13 +++++++++++++ packages/enums/package.json | 2 +- packages/event/CHANGELOG.md | 8 ++++++++ packages/event/package.json | 2 +- packages/experience/CHANGELOG.md | 8 ++++++++ packages/experience/package.json | 2 +- packages/js-sdk/CHANGELOG.md | 11 +++++++++++ packages/js-sdk/package.json | 2 +- packages/logger/CHANGELOG.md | 8 ++++++++ packages/logger/package.json | 2 +- packages/rules/CHANGELOG.md | 8 ++++++++ packages/rules/package.json | 2 +- packages/segments/CHANGELOG.md | 8 ++++++++ packages/segments/package.json | 2 +- packages/types/CHANGELOG.md | 13 +++++++++++++ packages/types/package.json | 2 +- packages/utils/CHANGELOG.md | 9 +++++++++ packages/utils/package.json | 2 +- 25 files changed, 134 insertions(+), 24 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 8e40dc46..b3071938 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,14 +1,14 @@ { - "packages/js-sdk": "4.3.2", - "packages/enums": "2.2.1", - "packages/types": "3.8.1", - "packages/utils": "2.2.2", - "packages/event": "2.1.2", - "packages/bucketing": "3.1.1", - "packages/logger": "2.1.1", - "packages/rules": "2.1.2", - "packages/segments": "2.1.1", - "packages/api": "2.1.3", - "packages/data": "3.3.2", - "packages/experience": "2.3.1" + "packages/js-sdk": "4.3.3", + "packages/enums": "2.3.0", + "packages/types": "3.9.0", + "packages/utils": "2.2.3", + "packages/event": "2.1.3", + "packages/bucketing": "3.1.2", + "packages/logger": "2.1.2", + "packages/rules": "2.1.3", + "packages/segments": "2.1.2", + "packages/api": "2.1.4", + "packages/data": "3.3.3", + "packages/experience": "2.3.2" } diff --git a/packages/api/CHANGELOG.md b/packages/api/CHANGELOG.md index 30095f52..aef84d9e 100644 --- a/packages/api/CHANGELOG.md +++ b/packages/api/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.1.4](https://github.com/convertcom/javascript-sdk/compare/js-sdk-api-v2.1.3...js-sdk-api-v2.1.4) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.1.3](https://github.com/convertcom/javascript-sdk/compare/js-sdk-api-v2.1.2...js-sdk-api-v2.1.3) (2025-03-26) diff --git a/packages/api/package.json b/packages/api/package.json index 9d38d230..3cb2159e 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.1.3", + "version": "2.1.4", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0", "@convertcom/js-sdk-event": ">=2.1.0", diff --git a/packages/bucketing/CHANGELOG.md b/packages/bucketing/CHANGELOG.md index 6ce47eaf..80d3fa49 100644 --- a/packages/bucketing/CHANGELOG.md +++ b/packages/bucketing/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [3.1.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-bucketing-v3.1.1...js-sdk-bucketing-v3.1.2) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [3.1.1](https://github.com/convertcom/javascript-sdk/compare/js-sdk-bucketing-v3.1.0...js-sdk-bucketing-v3.1.1) (2025-03-26) diff --git a/packages/bucketing/package.json b/packages/bucketing/package.json index 11a7e1bd..62ac6e93 100644 --- a/packages/bucketing/package.json +++ b/packages/bucketing/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "3.1.1", + "version": "3.1.2", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0", "@convertcom/js-sdk-logger": ">=2.1.0", diff --git a/packages/data/CHANGELOG.md b/packages/data/CHANGELOG.md index 98a553fe..cf29e1ba 100644 --- a/packages/data/CHANGELOG.md +++ b/packages/data/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [3.3.3](https://github.com/convertcom/javascript-sdk/compare/js-sdk-data-v3.3.2...js-sdk-data-v3.3.3) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [3.3.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-data-v3.3.1...js-sdk-data-v3.3.2) (2025-03-26) diff --git a/packages/data/package.json b/packages/data/package.json index 8ba493a9..848ba211 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "3.3.2", + "version": "3.3.3", "peerDependencies": { "@convertcom/js-sdk-api": ">=2.1.1", "@convertcom/js-sdk-bucketing": ">=3.1.0", diff --git a/packages/enums/CHANGELOG.md b/packages/enums/CHANGELOG.md index a3cd66f8..e3d19a4f 100644 --- a/packages/enums/CHANGELOG.md +++ b/packages/enums/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [2.3.0](https://github.com/convertcom/javascript-sdk/compare/js-sdk-enums-v2.2.1...js-sdk-enums-v2.3.0) (2025-09-05) + + +### Features + +* Add support for custom dimensions when tracking conversions ([35dba11](https://github.com/convertcom/javascript-sdk/commit/35dba11880b3bad30bec0e15d2e9853148075ae6)) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.2.1](https://github.com/convertcom/javascript-sdk/compare/js-sdk-enums-v2.2.0...js-sdk-enums-v2.2.1) (2025-03-26) diff --git a/packages/enums/package.json b/packages/enums/package.json index ccb67df6..8d34e69b 100644 --- a/packages/enums/package.json +++ b/packages/enums/package.json @@ -49,5 +49,5 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.2.1" + "version": "2.3.0" } diff --git a/packages/event/CHANGELOG.md b/packages/event/CHANGELOG.md index 7e672a2c..b3d27562 100644 --- a/packages/event/CHANGELOG.md +++ b/packages/event/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.1.3](https://github.com/convertcom/javascript-sdk/compare/js-sdk-event-v2.1.2...js-sdk-event-v2.1.3) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.1.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-event-v2.1.1...js-sdk-event-v2.1.2) (2025-03-26) diff --git a/packages/event/package.json b/packages/event/package.json index 6a52379e..13c3c113 100644 --- a/packages/event/package.json +++ b/packages/event/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.1.2", + "version": "2.1.3", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0", "@convertcom/js-sdk-logger": ">=2.1.0", diff --git a/packages/experience/CHANGELOG.md b/packages/experience/CHANGELOG.md index a9ca7710..2fe4d427 100644 --- a/packages/experience/CHANGELOG.md +++ b/packages/experience/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.3.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-experience-v2.3.1...js-sdk-experience-v2.3.2) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.3.1](https://github.com/convertcom/javascript-sdk/compare/js-sdk-experience-v2.3.0...js-sdk-experience-v2.3.1) (2025-03-26) diff --git a/packages/experience/package.json b/packages/experience/package.json index 17a866d0..be98131c 100644 --- a/packages/experience/package.json +++ b/packages/experience/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.3.1", + "version": "2.3.2", "peerDependencies": { "@convertcom/js-sdk-data": ">=3.2.1", "@convertcom/js-sdk-enums": ">=2.2.0", diff --git a/packages/js-sdk/CHANGELOG.md b/packages/js-sdk/CHANGELOG.md index bfb37e8a..bbc9f1e9 100644 --- a/packages/js-sdk/CHANGELOG.md +++ b/packages/js-sdk/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +## [4.3.3](https://github.com/convertcom/javascript-sdk/compare/js-sdk-v4.3.2...js-sdk-v4.3.3) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([5cdfb11](https://github.com/convertcom/javascript-sdk/commit/5cdfb11201c77c349c94a95634e990512841fe55)) +* Improve numeric check utility ([8e61900](https://github.com/convertcom/javascript-sdk/commit/8e619009cfa7dc03b2bb1d1836aa7743b9c7de0e)) +* Improve numeric check utility ([88e8e08](https://github.com/convertcom/javascript-sdk/commit/88e8e08f7d687c363d10874a5b9ff4b5e3df19e1)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [4.3.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-v4.3.1...js-sdk-v4.3.2) (2025-03-26) diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index de8027a7..bfaf1e33 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -106,7 +106,7 @@ "browserstack-local": "1.5.5" } }, - "version": "4.3.2", + "version": "4.3.3", "peerDependencies": { "@convertcom/js-sdk-api": ">=2.1.1", "@convertcom/js-sdk-bucketing": ">=3.1.0", diff --git a/packages/logger/CHANGELOG.md b/packages/logger/CHANGELOG.md index fc790f93..03a04a89 100644 --- a/packages/logger/CHANGELOG.md +++ b/packages/logger/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.1.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-logger-v2.1.1...js-sdk-logger-v2.1.2) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.1.1](https://github.com/convertcom/javascript-sdk/compare/js-sdk-logger-v2.1.0...js-sdk-logger-v2.1.1) (2025-03-26) diff --git a/packages/logger/package.json b/packages/logger/package.json index 50968055..ea84f18f 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -66,7 +66,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.1.1", + "version": "2.1.2", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0" } diff --git a/packages/rules/CHANGELOG.md b/packages/rules/CHANGELOG.md index e34ed8a2..8575884c 100644 --- a/packages/rules/CHANGELOG.md +++ b/packages/rules/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.1.3](https://github.com/convertcom/javascript-sdk/compare/js-sdk-rules-v2.1.2...js-sdk-rules-v2.1.3) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.1.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-rules-v2.1.1...js-sdk-rules-v2.1.2) (2025-03-26) diff --git a/packages/rules/package.json b/packages/rules/package.json index d435598b..fb0ff6d8 100644 --- a/packages/rules/package.json +++ b/packages/rules/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.1.2", + "version": "2.1.3", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0", "@convertcom/js-sdk-logger": ">=2.1.0", diff --git a/packages/segments/CHANGELOG.md b/packages/segments/CHANGELOG.md index b0b64f38..fbdc1643 100644 --- a/packages/segments/CHANGELOG.md +++ b/packages/segments/CHANGELOG.md @@ -1,5 +1,13 @@ # Changelog +## [2.1.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-segments-v2.1.1...js-sdk-segments-v2.1.2) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [2.1.1](https://github.com/convertcom/javascript-sdk/compare/js-sdk-segments-v2.1.0...js-sdk-segments-v2.1.1) (2025-03-26) diff --git a/packages/segments/package.json b/packages/segments/package.json index 76385d00..67e1722b 100644 --- a/packages/segments/package.json +++ b/packages/segments/package.json @@ -69,7 +69,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.1.1", + "version": "2.1.2", "peerDependencies": { "@convertcom/js-sdk-data": ">=3.2.1", "@convertcom/js-sdk-enums": ">=2.2.0", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index b92ba8e1..04300b7d 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -1,5 +1,18 @@ # Changelog +## [3.9.0](https://github.com/convertcom/javascript-sdk/compare/js-sdk-types-v3.8.1...js-sdk-types-v3.9.0) (2025-09-05) + + +### Features + +* automated TS Serving API update ([112017f](https://github.com/convertcom/javascript-sdk/commit/112017fa49a4c94ce3ff963e1c98ea4411ebde19)) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) + ## [3.8.1](https://github.com/convertcom/javascript-sdk/compare/js-sdk-types-v3.8.0...js-sdk-types-v3.8.1) (2025-03-26) diff --git a/packages/types/package.json b/packages/types/package.json index fc6a06f9..630bc1c2 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -49,7 +49,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "3.8.1", + "version": "3.9.0", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0" } diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 4569e037..1f2ea6bf 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## [2.2.3](https://github.com/convertcom/javascript-sdk/compare/js-sdk-utils-v2.2.2...js-sdk-utils-v2.2.3) (2025-09-05) + + +### Bug Fixes + +* expose "data" property ([5df9d29](https://github.com/convertcom/javascript-sdk/commit/5df9d295af348485a2f8a1aff8c5440ef1552681)) +* Improve numeric check utility ([af0d74c](https://github.com/convertcom/javascript-sdk/commit/af0d74cd027664da90d719a9b9a325dbf60ee62d)) +* Improve numeric check utility ([dc49c20](https://github.com/convertcom/javascript-sdk/commit/dc49c20f4237b63ecda1ed5d230d228e8b35d2a7)) + ## [2.2.2](https://github.com/convertcom/javascript-sdk/compare/js-sdk-utils-v2.2.1...js-sdk-utils-v2.2.2) (2025-03-26) diff --git a/packages/utils/package.json b/packages/utils/package.json index 0cee508b..0b4c4318 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -70,7 +70,7 @@ "typescript-eslint": "^7.13.1", "webpack": "^5.92.0" }, - "version": "2.2.2", + "version": "2.2.3", "peerDependencies": { "@convertcom/js-sdk-enums": ">=2.2.0" } From 951c313e9630a1a8e32b7d711627190ca9d6d0fa Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Mon, 8 Sep 2025 20:28:05 +0300 Subject: [PATCH 03/10] chore: Manual npm publish requiring input tags --- .github/workflows/publish-package.yml | 39 ++++++++++++++++----------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml index 1c154180..f5b90406 100644 --- a/.github/workflows/publish-package.yml +++ b/.github/workflows/publish-package.yml @@ -2,89 +2,96 @@ name: Publish Package to npmjs on: release: types: [published] - workflow_dispatch: {} + workflow_dispatch: + inputs: + tag: + description: "Tag to publish (e.g. js-sdk-v4.3.3, js-sdk-utils-v2.2.3)" + required: true jobs: - build: + publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 # Setup .npmrc file to publish to npm - - uses: actions/setup-node@v3 + - uses: actions/setup-node@v4 with: node-version: '18.x' registry-url: 'https://registry.npmjs.org' + - name: Resolve tag + run: echo "RELEASE_TAG=${{ github.event.release.tag_name || inputs.tag }}" >> $GITHUB_ENV - run: | yarn set version berry yarn - - if: ${{ startsWith(github.event.release.name, 'js-sdk-enums:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-enums-v') }} run: | yarn enums:build (cd packages/enums && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-types:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-types-v') }} run: | yarn types:build (cd packages/types && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-utils:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-utils-v') }} run: | yarn utils:build (cd packages/utils && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-event:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-event-v') }} run: | yarn event:build (cd packages/event && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-bucketing:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-bucketing-v') }} run: | yarn bucketing:build (cd packages/bucketing && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-logger:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-logger-v') }} run: | yarn logger:build (cd packages/logger && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-rules:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-rules-v') }} run: | yarn rules:build (cd packages/rules && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-segments:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-segments-v') }} run: | yarn segments:build (cd packages/segments && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-api:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-api-v') }} run: | yarn api:build (cd packages/api && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-data:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-data-v') }} run: | yarn data:build (cd packages/data && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk-experience:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-experience-v') }} run: | yarn experience:build (cd packages/experience && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} - - if: ${{ startsWith(github.event.release.name, 'js-sdk:') }} + - if: ${{ startsWith(env.RELEASE_TAG, 'js-sdk-v') }} run: | yarn sdk:build (cd packages/js-sdk && npm publish --access public) env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} +gi \ No newline at end of file From 6cc8fec3b9bd90e157cc3ba4cbe5e9217e9a6490 Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Mon, 6 Oct 2025 06:53:38 +0300 Subject: [PATCH 04/10] feat: load rust decider module and bundle wasm assets --- packages/data/package.json | 3 + packages/data/src/data-manager.ts | 403 ++++++++++++++++++ packages/data/src/interfaces/data-manager.ts | 9 + packages/data/src/rust-decider.ts | 223 ++++++++++ packages/js-sdk/index.tests.js | 3 + packages/js-sdk/karma.base.conf.js | 14 + packages/js-sdk/package.json | 29 +- packages/js-sdk/src/config/default.ts | 3 + packages/js-sdk/src/feature-manager.ts | 116 ++++- .../js-sdk/tests/feature-manager.tests.ts | 147 ++++++- packages/scripts/copy-wasm.js | 32 ++ packages/types/src/Config.ts | 3 + yarn.lock | 38 +- 13 files changed, 997 insertions(+), 26 deletions(-) create mode 100644 packages/data/src/rust-decider.ts create mode 100755 packages/scripts/copy-wasm.js diff --git a/packages/data/package.json b/packages/data/package.json index 848ba211..5c13ded0 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -79,5 +79,8 @@ "@convertcom/js-sdk-rules": ">=2.1.0", "@convertcom/js-sdk-types": ">=3.6.0", "@convertcom/js-sdk-utils": ">=2.2.0" + }, + "dependencies": { + "@convertcom/decisions-core": "file:../../../decisions-core-sdk/pkg-node" } } diff --git a/packages/data/src/data-manager.ts b/packages/data/src/data-manager.ts index 210f4c25..64598f5e 100644 --- a/packages/data/src/data-manager.ts +++ b/packages/data/src/data-manager.ts @@ -46,6 +46,21 @@ import { GenericListMatchingOptions } from '@convertcom/js-sdk-types'; +import { + aggregateFeaturesWithRust as runRustFeatureAggregation, + decideExperienceWithRust, + initializeRustDecider, + isRustDeciderReady, + RustDeciderNotReadyError, + RustDecisionResponsePayload, + RustFeatureAggregationPayload, + RustLogRecord, + RustSelectionPayload, + RustTrackingInstruction, + RustTrackingInstructionBucket, + RustVariationSummary +} from './rust-decider'; + import { BucketingError, DATA_ENTITIES, @@ -83,6 +98,7 @@ export class DataManager implements DataManagerInterface { private _asyncStorage: boolean; private _environment: string; private _mapper: (...args: any) => any; + private _useRustDecider: boolean; /** * @param {Config} config * @param {Object} dependencies @@ -119,6 +135,16 @@ export class DataManager implements DataManagerInterface { this._loggerManager = loggerManager; this._eventManager = eventManager; this._config = config; + this._useRustDecider = Boolean(config?.experimental?.useRustDecider); + if (this._useRustDecider) { + initializeRustDecider().catch((error: Error) => { + this._loggerManager?.warn?.( + 'DataManager()', + 'Unable to initialize Rust decision engine', + error + ); + }); + } this._mapper = config?.mapper || ((value: any) => value); this._asyncStorage = asyncStorage; this._data = objectDeepValue(config, 'data'); @@ -470,6 +496,109 @@ export class DataManager implements DataManagerInterface { return null; } + private _maybeDecideExperienceWithRust({ + visitorId, + identity, + identityField, + visitorProperties, + locationProperties, + environment, + ignoreLocationProperties, + forceVariationId, + enableTracking, + updateVisitorProperties + }: { + visitorId: string; + identity: string; + identityField: IdentityField; + visitorProperties?: Record; + locationProperties?: Record; + environment: string; + ignoreLocationProperties?: boolean; + forceVariationId?: string; + enableTracking: boolean; + updateVisitorProperties: boolean; + }): BucketedVariation | RuleError | BucketingError | null | undefined { + if (!this._useRustDecider || !isRustDeciderReady()) return undefined; + const project = this._data?.project; + if (!project) return undefined; + + const storeData = (this.getData(visitorId) || {}) as StoreData; + const options: Record = { + ignoreLocationProperties: Boolean(ignoreLocationProperties), + enableTracking, + updateVisitorProperties + }; + if (forceVariationId) options.forceVariationId = forceVariationId; + + const request = { + visitorId, + experienceKey: identityField === 'key' ? identity : undefined, + experienceId: identityField === 'id' ? identity : undefined, + context: { + visitorProperties: visitorProperties || {}, + locationProperties: ignoreLocationProperties + ? {} + : locationProperties || {} + }, + environment, + options, + visitorState: this._buildRustVisitorState(storeData) + }; + + try { + const response = decideExperienceWithRust( + project, + request + ) as RustDecisionResponsePayload; + + this._replayRustLogs(response.logs); + + const visitorPropsForState = updateVisitorProperties + ? visitorProperties || null + : null; + + if (response.outcome.type !== 'matched') { + this._applyRustStateDiff( + visitorId, + response.state_diff, + visitorPropsForState + ); + this._emitRustLocationTransitions(visitorId, response.location_transitions); + return null; + } + + const bucketedVariation = this._mapRustSelectionToVariation( + response.outcome.selection, + identity + ); + + this._applyRustStateDiff( + visitorId, + response.state_diff, + visitorPropsForState + ); + this._emitRustLocationTransitions(visitorId, response.location_transitions); + this._handleRustTracking( + visitorId, + response.tracking, + enableTracking, + visitorProperties, + storeData?.segments + ); + + return bucketedVariation; + } catch (error) { + if (error instanceof RustDeciderNotReadyError) return undefined; + this._loggerManager?.warn?.( + 'DataManager._getBucketingByField()', + 'Falling back to JavaScript decision path', + error + ); + return undefined; + } + } + /** * Retrieve variation for visitor * @param {string} visitorId @@ -516,6 +645,23 @@ export class DataManager implements DataManagerInterface { }) ); + const rustResult = this._maybeDecideExperienceWithRust({ + visitorId, + identity, + identityField, + visitorProperties, + locationProperties, + environment, + ignoreLocationProperties, + forceVariationId, + enableTracking, + updateVisitorProperties: Boolean(updateVisitorProperties) + }); + + if (rustResult !== undefined) { + return rustResult; + } + // Retrieve the experience const experience = this.matchRulesByField( visitorId, @@ -719,6 +865,228 @@ export class DataManager implements DataManagerInterface { return bucketedVariation as BucketedVariation; } + private _buildRustVisitorState( + storeData: StoreData | null | undefined + ): Record { + const state: Record = { + bucketing: storeData?.bucketing || {} + }; + + if (Array.isArray(storeData?.locations) && storeData.locations.length) { + state.active_locations = storeData.locations; + } + + const customSegments = this._extractCustomSegments(storeData?.segments); + if (customSegments.length) { + state.custom_segments = customSegments; + } + + return state; + } + + private _extractCustomSegments(segments?: VisitorSegments): Array { + if (!segments) return []; + const customSegments = (segments as Record)[ + SegmentsKeys.CUSTOM_SEGMENTS + ]; + if (!Array.isArray(customSegments)) return []; + return customSegments + .map((segment) => + segment && typeof segment === 'object' && 'toString' in segment + ? segment.toString() + : String(segment) + ) + .filter((value) => typeof value === 'string' && value.length); + } + + private _replayRustLogs(logs: Array): void { + if (!Array.isArray(logs) || !logs.length) return; + logs.forEach(({level, message, data}) => { + const normalizedLevel = level?.toLowerCase?.(); + const payload = [ + 'RustDecisionEngine', + message, + data && this._mapper(data) + ] as [string, any?, any?]; + switch (normalizedLevel) { + case 'trace': + this._loggerManager?.trace?.(...payload); + break; + case 'debug': + this._loggerManager?.debug?.(...payload); + break; + case 'warn': + this._loggerManager?.warn?.(...payload); + break; + case 'error': + this._loggerManager?.error?.(...payload); + break; + default: + this._loggerManager?.info?.(...payload); + break; + } + }); + } + + private _applyRustStateDiff( + visitorId: string, + stateDiff: RustDecisionResponsePayload['state_diff'], + visitorProperties: Record | null + ): void { + const updates: StoreData = {}; + + if (stateDiff?.bucketing && Object.keys(stateDiff.bucketing).length) { + updates.bucketing = {...stateDiff.bucketing}; + } + + if (Array.isArray(stateDiff?.active_locations)) { + updates.locations = [...stateDiff.active_locations]; + } + + const segmentUpdates: VisitorSegments = {}; + if (visitorProperties && objectNotEmpty(visitorProperties)) { + Object.assign(segmentUpdates, visitorProperties); + } + if (Array.isArray(stateDiff?.custom_segments)) { + segmentUpdates[SegmentsKeys.CUSTOM_SEGMENTS] = stateDiff.custom_segments; + } + + if (objectNotEmpty(segmentUpdates)) { + updates.segments = segmentUpdates; + } + + if (objectNotEmpty(updates)) { + this.putData(visitorId, updates); + } + } + + private _emitRustLocationTransitions( + visitorId: string, + transitions: Array + ): void { + if (!Array.isArray(transitions) || !transitions.length) return; + transitions.forEach((transition) => { + const identity = transition.location_key || transition.location_id; + const payload = { + visitorId, + location: { + id: transition.location_id, + key: transition.location_key, + name: transition.location_name + } + }; + + if (transition.type === 'activated') { + this._eventManager.fire( + SystemEvents.LOCATION_ACTIVATED, + payload, + null, + true + ); + if (identity) { + this._loggerManager?.info?.( + 'DataManager._getBucketingByField()', + MESSAGES.LOCATION_ACTIVATED.replace('#', `#${identity}`) + ); + } + } else if (transition.type === 'deactivated') { + this._eventManager.fire( + SystemEvents.LOCATION_DEACTIVATED, + payload, + null, + true + ); + if (identity) { + this._loggerManager?.info?.( + 'DataManager._getBucketingByField()', + MESSAGES.LOCATION_DEACTIVATED.replace('#', `#${identity}`) + ); + } + } + }); + } + + private _handleRustTracking( + visitorId: string, + tracking: RustTrackingInstruction, + enableTracking: boolean, + visitorProperties?: Record, + segments?: VisitorSegments + ): void { + if (!enableTracking || !tracking || tracking.type !== 'bucket') return; + const {experience_id, variation_id} = tracking as RustTrackingInstructionBucket; + const bucketingEvent: BucketingEvent = { + experienceId: experience_id, + variationId: variation_id + }; + const visitorEvent: VisitorTrackingEvents = { + eventType: eventType.BUCKETING, + data: bucketingEvent + }; + const visitorSegments = this._ruleManager.isUsingCustomInterface( + visitorProperties + ) + ? visitorProperties?.get?.() || {} + : segments; + this._apiManager.enqueue(visitorId, visitorEvent, visitorSegments); + this._loggerManager?.trace?.( + 'DataManager._getBucketingByField()', + this._mapper({ + visitorEvent + }) + ); + } + + private _mapRustSelectionToVariation( + selection: RustSelectionPayload, + identity: string + ): BucketedVariation { + const {summary} = selection; + const experience = this.getEntityById( + summary.experience_id, + 'experiences' + ) as ConfigExperience; + const variation = summary.variation; + const bucketedVariation: BucketedVariation = { + ...variation, + experienceId: summary.experience_id, + experienceKey: summary.experience_key, + experienceName: experience?.name, + bucketingAllocation: summary.allocation?.[1] + }; + + const logTarget = 'DataManager._getBucketingByField()'; + switch (selection.type) { + case 'forced': + this._loggerManager?.info?.( + logTarget, + MESSAGES.BUCKETED_VISITOR_FORCED.replace('#', `#${variation.id}`) + ); + break; + case 'cached': + this._loggerManager?.info?.( + logTarget, + MESSAGES.BUCKETED_VISITOR_FOUND.replace('#', `#${variation.id}`) + ); + break; + default: + this._loggerManager?.info?.( + logTarget, + MESSAGES.BUCKETED_VISITOR.replace('#', `#${variation.id}`) + ); + this._loggerManager?.debug?.( + logTarget, + this._mapper({ + identity, + variationId: variation.id + }) + ); + break; + } + + return bucketedVariation; + } + /** * @param {string} experienceId * @param {string} variationId @@ -739,6 +1107,41 @@ export class DataManager implements DataManagerInterface { ) as ExperienceVariationConfig; } + aggregateFeaturesWithRust( + variationSummaries: Array, + { + filters, + typeCasting = true + }: {filters?: Record; typeCasting?: boolean} = {} + ): RustFeatureAggregationPayload | null { + if (!this._useRustDecider || !isRustDeciderReady()) return null; + const project = this._data?.project; + if (!project || !Array.isArray(variationSummaries) || !variationSummaries.length) + return null; + try { + const response = runRustFeatureAggregation(project, { + variationSummaries, + filters, + typeCasting + }); + this._replayRustLogs(response.logs); + return response; + } catch (error) { + if (!(error instanceof RustDeciderNotReadyError)) { + this._loggerManager?.warn?.( + 'DataManager.aggregateFeaturesWithRust()', + 'Falling back to JavaScript feature aggregation', + error + ); + } + return null; + } + } + + isRustDeciderEnabled(): boolean { + return this._useRustDecider; + } + reset() { this._bucketedVisitors = new Map(); } diff --git a/packages/data/src/interfaces/data-manager.ts b/packages/data/src/interfaces/data-manager.ts index a7c2ac24..3f648a64 100644 --- a/packages/data/src/interfaces/data-manager.ts +++ b/packages/data/src/interfaces/data-manager.ts @@ -16,6 +16,10 @@ import { LocationAttributes, GoalData } from '@convertcom/js-sdk-types'; +import { + RustFeatureAggregationPayload, + RustVariationSummary +} from '../rust-decider'; import {DataStoreManagerInterface} from './data-store-manager'; import { BucketingError, @@ -86,4 +90,9 @@ export interface DataManagerInterface { isValidConfigData(data: ConfigResponseData): boolean; setDataStore(dataStore: any): void; + isRustDeciderEnabled(): boolean; + aggregateFeaturesWithRust( + variationSummaries: Array, + options?: {filters?: Record; typeCasting?: boolean} + ): RustFeatureAggregationPayload | null; } diff --git a/packages/data/src/rust-decider.ts b/packages/data/src/rust-decider.ts new file mode 100644 index 00000000..18ace4e5 --- /dev/null +++ b/packages/data/src/rust-decider.ts @@ -0,0 +1,223 @@ +/*! + * Shared helpers for calling the Rust decision engine (`@convertcom/decisions-core`). + */ +import type { + ConfigResponseData, + ExperienceVariationConfig +} from '@convertcom/js-sdk-types'; +import * as decisionsCoreModule from '@convertcom/decisions-core'; + +export type RustSelectionType = 'forced' | 'cached' | 'newly_bucketed'; +export type RustDecisionOutcomeType = 'matched' | 'not_matched'; +export type RustLocationTransitionType = 'activated' | 'deactivated'; +export type RustTrackingType = 'none' | 'bucket'; + +export interface RustVariationSummary { + experience_id: string; + experience_key: string; + variation: ExperienceVariationConfig; + allocation?: [number, number]; +} + +export interface RustSelectionPayload { + type: RustSelectionType; + summary: RustVariationSummary; +} + +export interface RustMatchedOutcome { + type: 'matched'; + selection: RustSelectionPayload; +} + +export interface RustNotMatchedOutcome { + type: 'not_matched'; + reason: string; +} + +export type RustDecisionOutcome = RustMatchedOutcome | RustNotMatchedOutcome; + +export interface RustStateDiff { + bucketing: Record; + active_locations?: Array; + custom_segments?: Array; +} + +export interface RustLocationTransition { + type: RustLocationTransitionType; + location_id: string; + location_key?: string; + location_name?: string; +} + +export interface RustLogRecord { + level: string; + message: string; + data?: unknown; +} + +export interface RustTrackingInstructionBucket { + type: 'bucket'; + experience_id: string; + variation_id: string; +} + +export interface RustTrackingInstructionNone { + type: 'none'; +} + +export type RustTrackingInstruction = + | RustTrackingInstructionBucket + | RustTrackingInstructionNone; + +export interface RustDecisionResponsePayload { + api_version: string; + outcome: RustDecisionOutcome; + state_diff: RustStateDiff; + location_transitions: Array; + matched_audience_ids: Array; + matched_segment_ids: Array; + logs: Array; + tracking: RustTrackingInstruction; +} + +export interface RustFeatureAggregationPayload { + api_version: string; + features: Array; + logs: Array; +} + +export interface RustExperienceDecisionRequest { + visitorId: string; + experienceKey?: string; + experienceId?: string; + context: Record; + environment?: string; + options?: Record; + visitorState?: Record; +} + +export interface RustFeatureAggregationRequest { + variationSummaries: Array; + filters?: Record; + typeCasting?: boolean; +} + +export interface RustDecisionEngineNamespace { + WasmDecisionEngine: new () => { + decideExperience( + project: ConfigResponseData['project'], + request: RustExperienceDecisionRequest + ): RustDecisionResponsePayload; + aggregateFeatures( + project: ConfigResponseData['project'], + input: RustFeatureAggregationRequest + ): RustFeatureAggregationPayload; + free(): void; + }; + default?: () => Promise; +} + +export class RustDeciderNotReadyError extends Error { + constructor(message = 'Rust decision engine not ready') { + super(message); + this.name = 'RustDeciderNotReadyError'; + } +} + +type DecisionsCoreModule = RustDecisionEngineNamespace & { + default?: (...args: Array) => unknown; +}; + +let moduleNamespace: RustDecisionEngineNamespace | null = null; +let initialized = false; +let initializationPromise: Promise | null = null; + +const loadModule = async (): Promise => { + const candidate = decisionsCoreModule as unknown as DecisionsCoreModule; + + if (typeof candidate.default === 'function') { + try { + const maybeNamespace = await candidate.default(); + if ( + maybeNamespace && + typeof maybeNamespace === 'object' && + 'WasmDecisionEngine' in (maybeNamespace as unknown as Record) + ) { + return maybeNamespace as RustDecisionEngineNamespace; + } + } catch (error) { + // If initialization fails (e.g., missing fetch in legacy browsers), fall back to the raw module + } + } + + return candidate as RustDecisionEngineNamespace; +}; + +const setNamespace = (namespace: RustDecisionEngineNamespace | null) => { + moduleNamespace = namespace; + initialized = Boolean(namespace && namespace.WasmDecisionEngine); +}; + +export const initializeRustDecider = async (): Promise => { + if (initialized) return; + if (!initializationPromise) { + initializationPromise = (async () => { + try { + const namespace = await loadModule(); + setNamespace(namespace); + } catch (error) { + setNamespace(null); + throw error; + } + })(); + } + + try { + await initializationPromise; + } finally { + initializationPromise = null; + } +}; + +export const isRustDeciderReady = (): boolean => initialized; + +const getNamespace = (): RustDecisionEngineNamespace => { + if (!moduleNamespace || !initialized) { + throw new RustDeciderNotReadyError(); + } + return moduleNamespace; +}; + +const disposeEngine = (engine: { free?: () => void }) => { + try { + engine?.free?.(); + } catch (error) { + // ignore disposal issues + } +}; + +export const decideExperienceWithRust = ( + project: ConfigResponseData['project'], + request: RustExperienceDecisionRequest +): RustDecisionResponsePayload => { + const namespace = getNamespace(); + const engine = new namespace.WasmDecisionEngine(); + try { + return engine.decideExperience(project, request) as RustDecisionResponsePayload; + } finally { + disposeEngine(engine); + } +}; + +export const aggregateFeaturesWithRust = ( + project: ConfigResponseData['project'], + input: RustFeatureAggregationRequest +): RustFeatureAggregationPayload => { + const namespace = getNamespace(); + const engine = new namespace.WasmDecisionEngine(); + try { + return engine.aggregateFeatures(project, input) as RustFeatureAggregationPayload; + } finally { + disposeEngine(engine); + } +}; diff --git a/packages/js-sdk/index.tests.js b/packages/js-sdk/index.tests.js index 053b6ffd..d37d2401 100644 --- a/packages/js-sdk/index.tests.js +++ b/packages/js-sdk/index.tests.js @@ -22,6 +22,9 @@ class DataStore { } const dataStore = new DataStore(); +testConfig.experimental = { + useRustDecider: true +}; testConfig.dataStore = dataStore; testConfig.events = { batch_size: 1, diff --git a/packages/js-sdk/karma.base.conf.js b/packages/js-sdk/karma.base.conf.js index cdde42e4..ec86a16c 100644 --- a/packages/js-sdk/karma.base.conf.js +++ b/packages/js-sdk/karma.base.conf.js @@ -64,6 +64,20 @@ module.exports = { devtool: 'inline-source-map', performance: { hints: false + }, + experiments: { + asyncWebAssembly: true + }, + module: { + rules: [ + { + test: /decisions_core_bg\.wasm$/, + type: 'asset/resource', + generator: { + filename: '[name][ext]' + } + } + ] } }, // define browsers diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index bfaf1e33..d7699263 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -27,7 +27,7 @@ "clean": "rm -rf lib", "prebuild:version": "node -p \"'export const LIB_VERSION = ' + JSON.stringify(require('./package.json').version) + ';'\" > src/version.ts", "prebuild": "yarn clean", - "build": "rollup -c ../../rollup.config.mjs", + "build": "rollup -c ../../rollup.config.mjs && node ../scripts/copy-wasm.js", "lint": "eslint src", "lint:fix": "yarn lint -- --fix", "coverage": "nyc report -r cobertura --reporter=text-summary", @@ -106,18 +106,21 @@ "browserstack-local": "1.5.5" } }, + "dependencies": { + "@convertcom/decisions-core": "file:../../../decisions-core-sdk/pkg-node" + }, "version": "4.3.3", "peerDependencies": { - "@convertcom/js-sdk-api": ">=2.1.1", - "@convertcom/js-sdk-bucketing": ">=3.1.0", - "@convertcom/js-sdk-data": ">=3.2.1", - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-event": ">=2.1.0", - "@convertcom/js-sdk-experience": ">=2.2.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-rules": ">=2.1.0", - "@convertcom/js-sdk-segments": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0", - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-api": ">=2.1.4", + "@convertcom/js-sdk-bucketing": ">=3.1.2", + "@convertcom/js-sdk-data": ">=3.3.3", + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-event": ">=2.1.3", + "@convertcom/js-sdk-experience": ">=2.3.2", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-rules": ">=2.1.3", + "@convertcom/js-sdk-segments": ">=2.1.2", + "@convertcom/js-sdk-types": ">=3.9.0", + "@convertcom/js-sdk-utils": ">=2.2.3" } -} +} \ No newline at end of file diff --git a/packages/js-sdk/src/config/default.ts b/packages/js-sdk/src/config/default.ts index 344b5a0a..b58f4c20 100644 --- a/packages/js-sdk/src/config/default.ts +++ b/packages/js-sdk/src/config/default.ts @@ -33,6 +33,9 @@ export const defaultConfig = { keys_case_sensitive: true, comparisonProcessor: null // Allows 3rd party comparison processor to be passed }, + experimental: { + useRustDecider: false + }, network: { tracking: true, cacheLevel: 'default' // can be set to 'low' for short-lived cache (for development purposes only) diff --git a/packages/js-sdk/src/feature-manager.ts b/packages/js-sdk/src/feature-manager.ts index 0590e526..e7972e33 100644 --- a/packages/js-sdk/src/feature-manager.ts +++ b/packages/js-sdk/src/feature-manager.ts @@ -15,7 +15,8 @@ import { IdentityField, VariableType, ConfigExperience, - BucketingAttributes + BucketingAttributes, + ExperienceVariationConfig } from '@convertcom/js-sdk-types'; import { MESSAGES, @@ -31,6 +32,18 @@ import { } from '@convertcom/js-sdk-utils'; import {BucketedVariation} from '@convertcom/js-sdk-types'; +type RustFeatureDecision = { + id: string; + key: string; + name?: string; + status: 'Enabled' | 'Disabled'; + experience_id?: string; + experience_key?: string; + variation_id?: string; + variation_key?: string; + variables?: Record | null; +}; + /** * Provides features specific logic * @category Modules @@ -362,6 +375,13 @@ export class FeatureManager implements FeatureManagerInterface { ); if (matchedErrors.length) return matchedErrors as Array; + const rustAggregated = this._maybeAggregateFeaturesWithRust( + bucketedVariations as Array, + filter, + typeCasting + ); + if (rustAggregated) return rustAggregated; + // Collect features from bucketed variations for (const k in bucketedVariations) { const bucketedVariation = bucketedVariations[k] as BucketedVariation; @@ -470,4 +490,98 @@ export class FeatureManager implements FeatureManagerInterface { castType(value: any, type: VariableType): any { return castType(value, type); } + + private _maybeAggregateFeaturesWithRust( + variations: Array, + filter: Record> | undefined, + typeCasting: boolean + ): Array | null { + if (!this._dataManager?.isRustDeciderEnabled()) return null; + const variationSummaries = variations + .map((variation) => this._toRustVariationSummary(variation)) + .filter(Boolean) as Array<{ + experience_id: string; + experience_key: string; + variation: ExperienceVariationConfig; + allocation?: [number, number]; + }>; + + if (!variationSummaries.length) return null; + + const filters = filter + ? { + feature_keys: filter.features, + experience_keys: filter.experiences + } + : undefined; + + const response = this._dataManager.aggregateFeaturesWithRust( + variationSummaries, + { + filters, + typeCasting + } + ); + + if (!response) return null; + + return response.features.map((decision: RustFeatureDecision) => + this._mapRustFeatureDecision(decision as RustFeatureDecision) + ); + } + + private _toRustVariationSummary( + variation: BucketedVariation + ): { + experience_id: string; + experience_key: string; + variation: ExperienceVariationConfig; + allocation?: [number, number]; + } | null { + const { + experienceId, + experienceKey, + experienceName: _experienceName, + bucketingAllocation, + ...variationPayload + } = variation; + + if (!experienceId || !experienceKey || !variationPayload?.id) return null; + + return { + experience_id: String(experienceId), + experience_key: String(experienceKey), + variation: variationPayload as ExperienceVariationConfig, + allocation: undefined + }; + } + + private _mapRustFeatureDecision(decision: RustFeatureDecision): BucketedFeature { + const status = + decision.status === 'Enabled' + ? FeatureStatus.ENABLED + : FeatureStatus.DISABLED; + + const feature: BucketedFeature = { + id: decision.id, + key: decision.key, + name: decision.name, + status, + experienceId: decision.experience_id || undefined, + experienceKey: decision.experience_key || undefined, + variables: decision.variables || undefined + } as BucketedFeature; + + if (decision.experience_id) { + const experience = this._dataManager.getEntityById( + decision.experience_id, + 'experiences' + ) as ConfigExperience; + if (experience?.name) { + feature.experienceName = experience.name; + } + } + + return feature; + } } diff --git a/packages/js-sdk/tests/feature-manager.tests.ts b/packages/js-sdk/tests/feature-manager.tests.ts index 7637f9c3..668ac2a6 100644 --- a/packages/js-sdk/tests/feature-manager.tests.ts +++ b/packages/js-sdk/tests/feature-manager.tests.ts @@ -8,12 +8,14 @@ import {BucketingManager as bm} from '@convertcom/js-sdk-bucketing'; import {RuleManager as rm} from '@convertcom/js-sdk-rules'; import {EventManager as em} from '@convertcom/js-sdk-event'; import {ApiManager as am} from '@convertcom/js-sdk-api'; -import {DataManager as dm} from '@convertcom/js-sdk-data'; +import {DataManager as dm, DataManagerInterface} from '@convertcom/js-sdk-data'; import {FeatureManager as fm} from '../src/feature-manager'; import testConfig from './test-config.json'; import {Config as ConfigType} from '@convertcom/js-sdk-types'; import {objectDeepMerge} from '@convertcom/js-sdk-utils'; import {defaultConfig} from '../src/config/default'; +import {FeatureStatus} from '@convertcom/js-sdk-enums'; +import {BucketedVariation, ConfigFeature} from '@convertcom/js-sdk-types'; const host = 'http://localhost'; const port = 8090; @@ -38,6 +40,124 @@ const ruleManager = new rm(configuration); const eventManager = new em(configuration); const apiManager = new am(configuration, {eventManager}); +class RustDataManagerStub { + public aggregateCalled = false; + public aggregateArgs: any; + + private _experiences = [ + { + id: 'exp-1', + key: 'experience-1', + name: 'Experience 1' + } + ]; + + private _variation: BucketedVariation = { + id: 'var-1', + key: 'variation-1', + name: 'Variation 1', + status: 'running' as any, + changes: [], + traffic_allocation: 10000, + experienceId: 'exp-1', + experienceKey: 'experience-1', + experienceName: 'Experience 1' + }; + + data = configuration.data; + + aggregateFeaturesWithRust(variationSummaries, options): any { + this.aggregateCalled = true; + this.aggregateArgs = {variationSummaries, options}; + return { + api_version: '1.0.0', + features: [ + { + id: 'feature-1', + key: 'feature-1', + name: 'Feature 1', + status: 'Enabled', + experience_id: 'exp-1', + experience_key: 'experience-1', + variation_id: 'var-1', + variation_key: 'variation-1', + variables: {flag: true} + } + ], + logs: [] + }; + } + + getEntitiesList(entityType: string): Array> { + if (entityType === 'experiences') { + return this._experiences; + } + if (entityType === 'features') { + return configuration?.data?.features || []; + } + return []; + } + + getEntities(keys: Array, entityType: string): Array> { + if (entityType === 'experiences') { + return this._experiences.filter((experience) => + keys?.includes?.(experience.key) + ); + } + if (entityType === 'features') { + return (configuration?.data?.features || []).filter((feature: ConfigFeature) => + keys?.includes?.(feature.key) + ); + } + return []; + } + + getBucketing(): BucketedVariation { + return this._variation; + } + + getEntityById(id: string): Record { + return this._experiences.find((experience) => experience.id === id); + } + + getEntity(key: string, entityType: string): Record { + if (entityType === 'features') { + return (configuration?.data?.features || []).find( + (feature: ConfigFeature) => feature.key === key + ); + } + return null; + } + + getEntitiesListObject( + entityType: string, + field: string + ): Record { + if (entityType !== 'features') return {}; + const features = configuration?.data?.features || []; + return features.reduce((acc, feature) => { + const identity = feature?.[field]; + if (identity) acc[String(identity)] = feature; + return acc; + }, {} as Record); + } + + getItemsByKeys(keys: Array, entityType: string): Array> { + if (entityType === 'features') { + return (configuration?.data?.features || []).filter((feature: ConfigFeature) => + keys?.includes?.(feature.key) + ); + } + return []; + } + + isRustDeciderEnabled(): boolean { + return true; + } + + reset(): void {} +} + describe('FeatureManager tests', function () { const visitorId = 'XXX'; let dataManager, featureManager, accountId, projectId, server; @@ -255,6 +375,31 @@ describe('FeatureManager tests', function () { res.end('{}'); }); }); + it('Should prefer Rust aggregation when available', function () { + const stub = new RustDataManagerStub(); + const rustFeatureManager = new fm(configuration, { + dataManager: stub as unknown as DataManagerInterface + }); + const features = rustFeatureManager.runFeatures('visitor-123', { + visitorProperties: {}, + locationProperties: {} + }); + + expect(stub.aggregateCalled).to.equal(true); + expect(stub.aggregateArgs?.options?.filters).to.equal(undefined); + expect(features).to.deep.equal([ + { + id: 'feature-1', + key: 'feature-1', + name: 'Feature 1', + status: FeatureStatus.ENABLED, + experienceId: 'exp-1', + experienceKey: 'experience-1', + experienceName: 'Experience 1', + variables: {flag: true} + } + ]); + }); it('Convert value type', function () { let value = featureManager.castType('123', 'integer'); expect(typeof value).to.equal('number'); diff --git a/packages/scripts/copy-wasm.js b/packages/scripts/copy-wasm.js new file mode 100755 index 00000000..86863363 --- /dev/null +++ b/packages/scripts/copy-wasm.js @@ -0,0 +1,32 @@ +#!/usr/bin/env node +const fs = require('fs'); +const path = require('path'); + +const root = path.resolve(__dirname, '..'); +const libDir = path.join(root, 'js-sdk', 'lib'); +const sdkRoot = path.resolve(root, '..', '..', 'decisions-core-sdk'); +const candidates = [ + path.join(sdkRoot, 'pkg-node', 'browser', 'decisions_core_bg.wasm'), + path.join(sdkRoot, 'pkg-node', 'decisions_core_bg.wasm'), + path.join(sdkRoot, 'pkg-web', 'decisions_core_bg.wasm') +]; + +const destinations = [ + path.join(libDir, 'decisions_core_bg.wasm'), + path.join(libDir, 'browser', 'decisions_core_bg.wasm') +]; + +const source = candidates.find((candidate) => fs.existsSync(candidate)); + +if (!source) { + console.warn('[copy-wasm] Skipping copy; source wasm not found. Looked in:'); + candidates.forEach((candidate) => console.warn(' -', candidate)); + process.exit(0); +} + +for (const dest of destinations) { + fs.mkdirSync(path.dirname(dest), {recursive: true}); + fs.copyFileSync(source, dest); +} + +console.log('[copy-wasm] Copied decisions_core_bg.wasm to lib/'); diff --git a/packages/types/src/Config.ts b/packages/types/src/Config.ts index 9ffe139e..e5dfcbb9 100644 --- a/packages/types/src/Config.ts +++ b/packages/types/src/Config.ts @@ -48,6 +48,9 @@ type ConfigBase = { cacheLevel?: string; source?: string; }; + experimental?: { + useRustDecider?: boolean; + }; mapper?: (...args: any) => any; }; diff --git a/yarn.lock b/yarn.lock index d7031dc7..04f84dd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2066,6 +2066,20 @@ __metadata: languageName: node linkType: hard +"@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node::locator=%40convertcom%2Fjs-sdk%40workspace%3Apackages%2Fjs-sdk": + version: 0.1.0 + resolution: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node#../../../decisions-core-sdk/pkg-node::hash=6eda83&locator=%40convertcom%2Fjs-sdk%40workspace%3Apackages%2Fjs-sdk" + checksum: 10c0/c502d6b87a5b3ca1b7180106ba066067d6e30ffcbf8abaedeaf9dce4fb16fee0b107bb3df785dd5b971374d8ce6f3743ac071713f5ed3e6d5aa7fe919d799d56 + languageName: node + linkType: hard + +"@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node::locator=%40convertcom%2Fjs-sdk-data%40workspace%3Apackages%2Fdata": + version: 0.1.0 + resolution: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node#../../../decisions-core-sdk/pkg-node::hash=6eda83&locator=%40convertcom%2Fjs-sdk-data%40workspace%3Apackages%2Fdata" + checksum: 10c0/c502d6b87a5b3ca1b7180106ba066067d6e30ffcbf8abaedeaf9dce4fb16fee0b107bb3df785dd5b971374d8ce6f3743ac071713f5ed3e6d5aa7fe919d799d56 + languageName: node + linkType: hard + "@convertcom/js-sdk-api@workspace:packages/api": version: 0.0.0-use.local resolution: "@convertcom/js-sdk-api@workspace:packages/api" @@ -2170,6 +2184,7 @@ __metadata: "@babel/cli": "npm:^7.24.7" "@babel/core": "npm:^7.24.7" "@babel/preset-env": "npm:^7.24.7" + "@convertcom/decisions-core": "file:../../../decisions-core-sdk/pkg-node" "@eslint/eslintrc": "npm:^3.1.0" "@rollup/plugin-babel": "npm:^6.0.4" "@rollup/plugin-commonjs": "npm:^26.0.1" @@ -2595,6 +2610,7 @@ __metadata: "@babel/cli": "npm:^7.24.7" "@babel/core": "npm:^7.24.7" "@babel/preset-env": "npm:^7.24.7" + "@convertcom/decisions-core": "file:../../../decisions-core-sdk/pkg-node" "@eslint/eslintrc": "npm:^3.1.0" "@jsdoc/salty": "npm:^0.2.8" "@rollup/plugin-babel": "npm:^6.0.4" @@ -2651,17 +2667,17 @@ __metadata: watchify: "npm:^4.0.0" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-api": ">=2.1.1" - "@convertcom/js-sdk-bucketing": ">=3.1.0" - "@convertcom/js-sdk-data": ">=3.2.1" - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-event": ">=2.1.0" - "@convertcom/js-sdk-experience": ">=2.2.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-rules": ">=2.1.0" - "@convertcom/js-sdk-segments": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-api": ">=2.1.4" + "@convertcom/js-sdk-bucketing": ">=3.1.2" + "@convertcom/js-sdk-data": ">=3.3.3" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-event": ">=2.1.3" + "@convertcom/js-sdk-experience": ">=2.3.2" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-rules": ">=2.1.3" + "@convertcom/js-sdk-segments": ">=2.1.2" + "@convertcom/js-sdk-types": ">=3.9.0" + "@convertcom/js-sdk-utils": ">=2.2.3" languageName: unknown linkType: soft From 40247d05e51ca6929bd9acaa153ad98dc9c2da94 Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Tue, 7 Oct 2025 00:49:34 +0300 Subject: [PATCH 05/10] fix: serve WASM file in Karma UMD browser tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Configure Karma to properly serve the decisions_core_bg.wasm file during UMD bundle browser tests, resolving the "Cannot read properties of undefined (reading 'wasmdecisionengine_new')" error. Changes: - Added WASM file to Karma files array with {included: false, served: true} - This allows the UMD bundle to fetch the WASM file via HTTP during tests - WASM file is already copied to lib/ by copy-wasm.js during build The pkg-node/browser/ version of decisions-core uses async WASM loading which requires the WASM file to be accessible via fetch(). By serving it through Karma, the browser tests can now properly initialize the Rust decision engine. Note: CJS browser tests at index.browser.cjs.tests.js cannot use Rust decider because pkg-node/decisions_core.js uses require('fs') which is not available in browsers. These tests should either be skipped when useRustDecider is enabled, or they should test fallback behavior only. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/js-sdk/karma.umd.conf.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/js-sdk/karma.umd.conf.js b/packages/js-sdk/karma.umd.conf.js index 10f596d8..6da60896 100644 --- a/packages/js-sdk/karma.umd.conf.js +++ b/packages/js-sdk/karma.umd.conf.js @@ -9,7 +9,12 @@ module.exports = function (config) { config.set({ ...baseConfig, // list of files / patterns to load in the browser - files: ['./lib/index.umd.min.js', './index.browser.umd.tests.js'], + files: [ + './lib/index.umd.min.js', + './index.browser.umd.tests.js', + // Serve WASM file but don't include it in the test runner + {pattern: './lib/decisions_core_bg.wasm', included: false, served: true, watched: false} + ], // preprocess matching files before serving them to the browser // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor preprocessors: { From 632ab5d30bc9f3f5ec28ae2a3e6d3bb3d9ac2484 Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Tue, 7 Oct 2025 02:55:11 +0300 Subject: [PATCH 06/10] refactor: rename Rust decider to Core decider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Renamed all 'Rust' references to 'Core' in variable names, class names, methods, and config properties - Renamed rust-decider.ts to core-decider.ts - Updated config property from useRustDecider to useCoreDecider - Added CoreDecider type definition in types package - Updated DataManagerInterface to properly expose coreDecider property - Fixed Rollup configuration to use browser version of decisions-core for UMD bundles - Added exportConditions for proper conditional exports resolution - Ensures __dirname is not present in browser bundles 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- generate-rollup-config.mjs | 18 +- packages/api/package.json | 12 +- packages/bucketing/package.json | 10 +- packages/data/index.ts | 1 + packages/data/package.json | 18 +- packages/data/src/core-decider.ts | 273 ++++++++++++++++++ packages/data/src/data-manager.ts | 65 +++-- packages/data/src/interfaces/data-manager.ts | 12 +- packages/data/src/rust-decider.ts | 223 -------------- packages/event/package.json | 8 +- packages/experience/package.json | 10 +- packages/js-sdk/index.tests.js | 2 +- packages/js-sdk/src/config/default.ts | 2 +- packages/js-sdk/src/core.ts | 71 +++-- packages/js-sdk/src/feature-manager.ts | 22 +- .../js-sdk/tests/feature-manager.tests.ts | 10 +- packages/logger/package.json | 4 +- packages/rules/package.json | 10 +- packages/segments/package.json | 12 +- packages/types/index.ts | 1 + packages/types/package.json | 4 +- packages/types/src/Config.ts | 2 +- packages/types/src/CoreDecider.ts | 15 + packages/utils/package.json | 4 +- 24 files changed, 461 insertions(+), 348 deletions(-) create mode 100644 packages/data/src/core-decider.ts delete mode 100644 packages/data/src/rust-decider.ts create mode 100644 packages/types/src/CoreDecider.ts diff --git a/generate-rollup-config.mjs b/generate-rollup-config.mjs index 4c51fe0b..0e2b9e00 100644 --- a/generate-rollup-config.mjs +++ b/generate-rollup-config.mjs @@ -190,7 +190,9 @@ const commonJSBundle = ({ tsconfig: resolve(process.env.PROJECT_CWD, 'tsconfig.json'), tsconfigOverride: tsconfigOverride(basePath, packageName) }), - nodeResolve(), + nodeResolve({ + exportConditions: ['node', 'default'] + }), commonjs(), generatePackageJson({ baseContents: (pkg) => ({ @@ -265,7 +267,9 @@ const commonJSLegacyBundle = ({basePath, input, info, packageName}) => ({ tsconfig: resolve(process.env.PROJECT_CWD, 'tsconfig.json'), tsconfigOverride: tsconfigOverride(basePath, packageName) }), - nodeResolve(), + nodeResolve({ + exportConditions: ['node', 'default'] + }), commonjs(), babel({ babelHelpers: 'bundled', @@ -305,7 +309,9 @@ const esmBundle = ({basePath, input, info, packageName}) => ({ tsconfig: resolve(process.env.PROJECT_CWD, 'tsconfig.json'), tsconfigOverride: tsconfigOverride(basePath, packageName, true) }), - nodeResolve(), + nodeResolve({ + exportConditions: ['node', 'default'] + }), commonjs() ]) }); @@ -342,8 +348,10 @@ const umdBundle = ({basePath, input, info}) => ({ tsconfigOverride: tsconfigOverride(basePath) }), nodeResolve({ - mainFields: ['browser'], - preferBuiltins: false + mainFields: ['browser', 'module', 'main'], + preferBuiltins: false, + browser: true, + exportConditions: ['browser', 'import', 'require', 'default'] }), commonjs(), json() diff --git a/packages/api/package.json b/packages/api/package.json index 3cb2159e..9431ed01 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -71,10 +71,10 @@ }, "version": "2.1.4", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-event": ">=2.1.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0", - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-event": ">=2.1.3", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-types": ">=3.9.0", + "@convertcom/js-sdk-utils": ">=2.2.3" } -} +} \ No newline at end of file diff --git a/packages/bucketing/package.json b/packages/bucketing/package.json index 62ac6e93..36bcbb2f 100644 --- a/packages/bucketing/package.json +++ b/packages/bucketing/package.json @@ -71,9 +71,9 @@ }, "version": "3.1.2", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0", - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-types": ">=3.9.0", + "@convertcom/js-sdk-utils": ">=2.2.3" } -} +} \ No newline at end of file diff --git a/packages/data/index.ts b/packages/data/index.ts index 83587ee2..2a9dedfe 100644 --- a/packages/data/index.ts +++ b/packages/data/index.ts @@ -8,3 +8,4 @@ export {DataManager} from './src/data-manager'; export {DataManagerInterface} from './src/interfaces/data-manager'; export {DataStoreManager} from './src/data-store-manager'; export {DataStoreManagerInterface} from './src/interfaces/data-store-manager'; +export {CoreDecider} from './src/core-decider'; diff --git a/packages/data/package.json b/packages/data/package.json index 5c13ded0..140c44e1 100644 --- a/packages/data/package.json +++ b/packages/data/package.json @@ -71,16 +71,16 @@ }, "version": "3.3.3", "peerDependencies": { - "@convertcom/js-sdk-api": ">=2.1.1", - "@convertcom/js-sdk-bucketing": ">=3.1.0", - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-event": ">=2.1.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-rules": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0", - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-api": ">=2.1.4", + "@convertcom/js-sdk-bucketing": ">=3.1.2", + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-event": ">=2.1.3", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-rules": ">=2.1.3", + "@convertcom/js-sdk-types": ">=3.9.0", + "@convertcom/js-sdk-utils": ">=2.2.3" }, "dependencies": { "@convertcom/decisions-core": "file:../../../decisions-core-sdk/pkg-node" } -} +} \ No newline at end of file diff --git a/packages/data/src/core-decider.ts b/packages/data/src/core-decider.ts new file mode 100644 index 00000000..4f1e5c2e --- /dev/null +++ b/packages/data/src/core-decider.ts @@ -0,0 +1,273 @@ +/*! + * Shared helpers for calling the Core decision engine (`@convertcom/decisions-core`). + */ +import type { + ConfigResponseData, + ExperienceVariationConfig +} from '@convertcom/js-sdk-types'; +import type {LogManagerInterface} from '@convertcom/js-sdk-logger'; +import * as decisionsCoreModule from '@convertcom/decisions-core'; + +export type RustSelectionType = 'forced' | 'cached' | 'newly_bucketed'; +export type RustDecisionOutcomeType = 'matched' | 'not_matched'; +export type RustLocationTransitionType = 'activated' | 'deactivated'; +export type RustTrackingType = 'none' | 'bucket'; + +export interface RustVariationSummary { + experience_id: string; + experience_key: string; + variation: ExperienceVariationConfig; + allocation?: [number, number]; +} + +export interface RustSelectionPayload { + type: RustSelectionType; + summary: RustVariationSummary; +} + +export interface RustMatchedOutcome { + type: 'matched'; + selection: RustSelectionPayload; +} + +export interface RustNotMatchedOutcome { + type: 'not_matched'; + reason: string; +} + +export type RustDecisionOutcome = RustMatchedOutcome | RustNotMatchedOutcome; + +export interface RustStateDiff { + bucketing: Record; + active_locations?: Array; + custom_segments?: Array; +} + +export interface RustLocationTransition { + type: RustLocationTransitionType; + location_id: string; + location_key?: string; + location_name?: string; +} + +export interface RustLogRecord { + level: string; + message: string; + data?: unknown; +} + +export interface RustTrackingInstructionBucket { + type: 'bucket'; + experience_id: string; + variation_id: string; +} + +export interface RustTrackingInstructionNone { + type: 'none'; +} + +export type RustTrackingInstruction = + | RustTrackingInstructionBucket + | RustTrackingInstructionNone; + +export interface RustDecisionResponsePayload { + api_version: string; + outcome: RustDecisionOutcome; + state_diff: RustStateDiff; + location_transitions: Array; + matched_audience_ids: Array; + matched_segment_ids: Array; + logs: Array; + tracking: RustTrackingInstruction; +} + +export interface RustFeatureAggregationPayload { + api_version: string; + features: Array; + logs: Array; +} + +export interface RustExperienceDecisionRequest { + visitorId: string; + experienceKey?: string; + experienceId?: string; + context: Record; + environment?: string; + options?: Record; + visitorState?: Record; +} + +export interface RustFeatureAggregationRequest { + variationSummaries: Array; + filters?: Record; + typeCasting?: boolean; +} + +export interface RustDecisionEngineNamespace { + WasmDecisionEngine: new () => { + decideExperience( + project: ConfigResponseData['project'], + request: RustExperienceDecisionRequest + ): RustDecisionResponsePayload; + aggregateFeatures( + project: ConfigResponseData['project'], + input: RustFeatureAggregationRequest + ): RustFeatureAggregationPayload; + free(): void; + }; + default?: () => Promise; +} + +export class CoreDeciderNotReadyError extends Error { + constructor(message = 'Core decision engine not ready') { + super(message); + this.name = 'CoreDeciderNotReadyError'; + } +} + +type DecisionsCoreModule = RustDecisionEngineNamespace & { + default?: (...args: Array) => unknown; +}; + +/** + * CoreDecider class manages the Core WASM decision engine lifecycle + */ +export class CoreDecider { + private _loggerManager: LogManagerInterface | null; + private _moduleNamespace: RustDecisionEngineNamespace | null = null; + private _initialized = false; + private _initializationPromise: Promise | null = null; + + constructor({loggerManager}: {loggerManager?: LogManagerInterface} = {}) { + this._loggerManager = loggerManager || null; + } + + private async loadModule(): Promise { + this._loggerManager?.trace?.('CoreDecider.loadModule()', 'Attempting to load WASM module'); + const candidate = decisionsCoreModule as unknown as DecisionsCoreModule; + + if (typeof candidate.default === 'function') { + this._loggerManager?.trace?.('CoreDecider.loadModule()', 'Found default export function, calling for async WASM init'); + try { + const maybeNamespace = await candidate.default(); + + if ( + maybeNamespace && + typeof maybeNamespace === 'object' && + 'WasmDecisionEngine' in (maybeNamespace as unknown as Record) + ) { + this._loggerManager?.trace?.('CoreDecider.loadModule()', 'Successfully loaded WASM namespace with WasmDecisionEngine'); + return maybeNamespace as RustDecisionEngineNamespace; + } + } catch (error) { + this._loggerManager?.trace?.('CoreDecider.loadModule()', 'Async WASM init failed, falling back to raw module', {error}); + // If initialization fails (e.g., missing fetch in legacy browsers), fall back to the raw module + } + } + + this._loggerManager?.trace?.('CoreDecider.loadModule()', 'Using raw candidate as namespace'); + return candidate as RustDecisionEngineNamespace; + } + + private setNamespace(namespace: RustDecisionEngineNamespace | null): void { + this._moduleNamespace = namespace; + this._initialized = Boolean(namespace && namespace.WasmDecisionEngine); + this._loggerManager?.trace?.('CoreDecider.setNamespace()', 'Namespace set', {initialized: this._initialized}); + } + + async initialize(): Promise { + this._loggerManager?.trace?.('CoreDecider.initialize()', 'Called', {initialized: this._initialized}); + if (this._initialized) { + this._loggerManager?.trace?.('CoreDecider.initialize()', 'Already initialized, skipping'); + return; + } + if (!this._initializationPromise) { + this._loggerManager?.trace?.('CoreDecider.initialize()', 'Starting new initialization'); + this._initializationPromise = (async () => { + try { + const namespace = await this.loadModule(); + this.setNamespace(namespace); + this._loggerManager?.trace?.('CoreDecider.initialize()', 'Initialization completed successfully'); + } catch (error) { + this._loggerManager?.error?.('CoreDecider.initialize()', 'Initialization failed', {error}); + this.setNamespace(null); + throw error; + } + })(); + } + + try { + await this._initializationPromise; + } finally { + this._initializationPromise = null; + } + } + + isReady(): boolean { + this._loggerManager?.trace?.('CoreDecider.isReady()', {initialized: this._initialized}); + return this._initialized; + } + + private getNamespace(): RustDecisionEngineNamespace { + this._loggerManager?.trace?.('CoreDecider.getNamespace()', 'Retrieving namespace', { + initialized: this._initialized, + hasNamespace: !!this._moduleNamespace + }); + if (!this._moduleNamespace || !this._initialized) { + this._loggerManager?.error?.('CoreDecider.getNamespace()', 'Namespace not ready'); + throw new CoreDeciderNotReadyError(); + } + return this._moduleNamespace; + } + + private disposeEngine(engine: {free?: () => void}): void { + try { + engine?.free?.(); + } catch (error) { + // ignore disposal issues + } + } + + decideExperience( + project: ConfigResponseData['project'], + request: RustExperienceDecisionRequest + ): RustDecisionResponsePayload { + this._loggerManager?.info?.('CoreDecider.decideExperience()', '🦀 USING CORE DECIDER FOR EXPERIENCE', { + experienceKey: request.experienceKey, + experienceId: request.experienceId, + visitorId: request.visitorId + }); + const namespace = this.getNamespace(); + const engine = new namespace.WasmDecisionEngine(); + try { + const result = engine.decideExperience(project, request) as RustDecisionResponsePayload; + this._loggerManager?.info?.('CoreDecider.decideExperience()', '🦀 CORE: Experience decision completed', { + outcomeType: result.outcome.type + }); + return result; + } finally { + this.disposeEngine(engine); + } + } + + aggregateFeatures( + project: ConfigResponseData['project'], + input: RustFeatureAggregationRequest + ): RustFeatureAggregationPayload { + this._loggerManager?.info?.('CoreDecider.aggregateFeatures()', '🦀 USING CORE FOR FEATURE AGGREGATION', { + variationCount: input.variationSummaries?.length, + typeCasting: input.typeCasting + }); + const namespace = this.getNamespace(); + const engine = new namespace.WasmDecisionEngine(); + try { + const result = engine.aggregateFeatures(project, input) as RustFeatureAggregationPayload; + this._loggerManager?.info?.('CoreDecider.aggregateFeatures()', '🦀 CORE: Feature aggregation completed', { + featureCount: result.features?.length + }); + return result; + } finally { + this.disposeEngine(engine); + } + } +} diff --git a/packages/data/src/data-manager.ts b/packages/data/src/data-manager.ts index 64598f5e..98ad3e96 100644 --- a/packages/data/src/data-manager.ts +++ b/packages/data/src/data-manager.ts @@ -47,11 +47,8 @@ import { } from '@convertcom/js-sdk-types'; import { - aggregateFeaturesWithRust as runRustFeatureAggregation, - decideExperienceWithRust, - initializeRustDecider, - isRustDeciderReady, - RustDeciderNotReadyError, + CoreDecider, + CoreDeciderNotReadyError, RustDecisionResponsePayload, RustFeatureAggregationPayload, RustLogRecord, @@ -59,7 +56,7 @@ import { RustTrackingInstruction, RustTrackingInstructionBucket, RustVariationSummary -} from './rust-decider'; +} from './core-decider'; import { BucketingError, @@ -98,7 +95,8 @@ export class DataManager implements DataManagerInterface { private _asyncStorage: boolean; private _environment: string; private _mapper: (...args: any) => any; - private _useRustDecider: boolean; + private _useCoreDecider: boolean; + private _coreDecider: CoreDecider | null = null; /** * @param {Config} config * @param {Object} dependencies @@ -135,15 +133,11 @@ export class DataManager implements DataManagerInterface { this._loggerManager = loggerManager; this._eventManager = eventManager; this._config = config; - this._useRustDecider = Boolean(config?.experimental?.useRustDecider); - if (this._useRustDecider) { - initializeRustDecider().catch((error: Error) => { - this._loggerManager?.warn?.( - 'DataManager()', - 'Unable to initialize Rust decision engine', - error - ); - }); + this._useCoreDecider = Boolean(config?.experimental?.useCoreDecider); + if (this._useCoreDecider) { + // Initialize CoreDecider instance with logger + // Note: Initialization will be awaited by Core before firing READY event + this._coreDecider = new CoreDecider({loggerManager}); } this._mapper = config?.mapper || ((value: any) => value); this._asyncStorage = asyncStorage; @@ -200,6 +194,13 @@ export class DataManager implements DataManagerInterface { return this._dataStoreManager; } + /** + * coreDecider getter + */ + get coreDecider(): CoreDecider | null { + return this._coreDecider; + } + /** * Set dataStoreManager at run-time */ @@ -496,7 +497,7 @@ export class DataManager implements DataManagerInterface { return null; } - private _maybeDecideExperienceWithRust({ + private _maybeDecideExperienceWithCore({ visitorId, identity, identityField, @@ -519,7 +520,7 @@ export class DataManager implements DataManagerInterface { enableTracking: boolean; updateVisitorProperties: boolean; }): BucketedVariation | RuleError | BucketingError | null | undefined { - if (!this._useRustDecider || !isRustDeciderReady()) return undefined; + if (!this._useCoreDecider || !this._coreDecider?.isReady()) return undefined; const project = this._data?.project; if (!project) return undefined; @@ -543,11 +544,11 @@ export class DataManager implements DataManagerInterface { }, environment, options, - visitorState: this._buildRustVisitorState(storeData) + visitorState: this._buildCoreVisitorState(storeData) }; try { - const response = decideExperienceWithRust( + const response = this._coreDecider.decideExperience( project, request ) as RustDecisionResponsePayload; @@ -589,7 +590,7 @@ export class DataManager implements DataManagerInterface { return bucketedVariation; } catch (error) { - if (error instanceof RustDeciderNotReadyError) return undefined; + if (error instanceof CoreDeciderNotReadyError) return undefined; this._loggerManager?.warn?.( 'DataManager._getBucketingByField()', 'Falling back to JavaScript decision path', @@ -645,7 +646,7 @@ export class DataManager implements DataManagerInterface { }) ); - const rustResult = this._maybeDecideExperienceWithRust({ + const coreResult = this._maybeDecideExperienceWithCore({ visitorId, identity, identityField, @@ -658,8 +659,8 @@ export class DataManager implements DataManagerInterface { updateVisitorProperties: Boolean(updateVisitorProperties) }); - if (rustResult !== undefined) { - return rustResult; + if (coreResult !== undefined) { + return coreResult; } // Retrieve the experience @@ -865,7 +866,7 @@ export class DataManager implements DataManagerInterface { return bucketedVariation as BucketedVariation; } - private _buildRustVisitorState( + private _buildCoreVisitorState( storeData: StoreData | null | undefined ): Record { const state: Record = { @@ -1107,19 +1108,19 @@ export class DataManager implements DataManagerInterface { ) as ExperienceVariationConfig; } - aggregateFeaturesWithRust( + aggregateFeaturesWithCore( variationSummaries: Array, { filters, typeCasting = true }: {filters?: Record; typeCasting?: boolean} = {} ): RustFeatureAggregationPayload | null { - if (!this._useRustDecider || !isRustDeciderReady()) return null; + if (!this._useCoreDecider || !this._coreDecider?.isReady()) return null; const project = this._data?.project; if (!project || !Array.isArray(variationSummaries) || !variationSummaries.length) return null; try { - const response = runRustFeatureAggregation(project, { + const response = this._coreDecider.aggregateFeatures(project, { variationSummaries, filters, typeCasting @@ -1127,9 +1128,9 @@ export class DataManager implements DataManagerInterface { this._replayRustLogs(response.logs); return response; } catch (error) { - if (!(error instanceof RustDeciderNotReadyError)) { + if (!(error instanceof CoreDeciderNotReadyError)) { this._loggerManager?.warn?.( - 'DataManager.aggregateFeaturesWithRust()', + 'DataManager.aggregateFeaturesWithCore()', 'Falling back to JavaScript feature aggregation', error ); @@ -1138,8 +1139,8 @@ export class DataManager implements DataManagerInterface { } } - isRustDeciderEnabled(): boolean { - return this._useRustDecider; + isCoreDeciderEnabled(): boolean { + return this._useCoreDecider; } reset() { diff --git a/packages/data/src/interfaces/data-manager.ts b/packages/data/src/interfaces/data-manager.ts index 3f648a64..a2b001d2 100644 --- a/packages/data/src/interfaces/data-manager.ts +++ b/packages/data/src/interfaces/data-manager.ts @@ -14,12 +14,13 @@ import { ConfigExperience, BucketingAttributes, LocationAttributes, - GoalData + GoalData, + CoreDecider } from '@convertcom/js-sdk-types'; -import { +import type { RustFeatureAggregationPayload, RustVariationSummary -} from '../rust-decider'; +} from '../core-decider'; import {DataStoreManagerInterface} from './data-store-manager'; import { BucketingError, @@ -30,6 +31,7 @@ import { export interface DataManagerInterface { data: ConfigResponseData; dataStoreManager: DataStoreManagerInterface; + coreDecider: CoreDecider | null; reset(); putData(storeKey: string, storeData: StoreData); @@ -90,8 +92,8 @@ export interface DataManagerInterface { isValidConfigData(data: ConfigResponseData): boolean; setDataStore(dataStore: any): void; - isRustDeciderEnabled(): boolean; - aggregateFeaturesWithRust( + isCoreDeciderEnabled(): boolean; + aggregateFeaturesWithCore( variationSummaries: Array, options?: {filters?: Record; typeCasting?: boolean} ): RustFeatureAggregationPayload | null; diff --git a/packages/data/src/rust-decider.ts b/packages/data/src/rust-decider.ts deleted file mode 100644 index 18ace4e5..00000000 --- a/packages/data/src/rust-decider.ts +++ /dev/null @@ -1,223 +0,0 @@ -/*! - * Shared helpers for calling the Rust decision engine (`@convertcom/decisions-core`). - */ -import type { - ConfigResponseData, - ExperienceVariationConfig -} from '@convertcom/js-sdk-types'; -import * as decisionsCoreModule from '@convertcom/decisions-core'; - -export type RustSelectionType = 'forced' | 'cached' | 'newly_bucketed'; -export type RustDecisionOutcomeType = 'matched' | 'not_matched'; -export type RustLocationTransitionType = 'activated' | 'deactivated'; -export type RustTrackingType = 'none' | 'bucket'; - -export interface RustVariationSummary { - experience_id: string; - experience_key: string; - variation: ExperienceVariationConfig; - allocation?: [number, number]; -} - -export interface RustSelectionPayload { - type: RustSelectionType; - summary: RustVariationSummary; -} - -export interface RustMatchedOutcome { - type: 'matched'; - selection: RustSelectionPayload; -} - -export interface RustNotMatchedOutcome { - type: 'not_matched'; - reason: string; -} - -export type RustDecisionOutcome = RustMatchedOutcome | RustNotMatchedOutcome; - -export interface RustStateDiff { - bucketing: Record; - active_locations?: Array; - custom_segments?: Array; -} - -export interface RustLocationTransition { - type: RustLocationTransitionType; - location_id: string; - location_key?: string; - location_name?: string; -} - -export interface RustLogRecord { - level: string; - message: string; - data?: unknown; -} - -export interface RustTrackingInstructionBucket { - type: 'bucket'; - experience_id: string; - variation_id: string; -} - -export interface RustTrackingInstructionNone { - type: 'none'; -} - -export type RustTrackingInstruction = - | RustTrackingInstructionBucket - | RustTrackingInstructionNone; - -export interface RustDecisionResponsePayload { - api_version: string; - outcome: RustDecisionOutcome; - state_diff: RustStateDiff; - location_transitions: Array; - matched_audience_ids: Array; - matched_segment_ids: Array; - logs: Array; - tracking: RustTrackingInstruction; -} - -export interface RustFeatureAggregationPayload { - api_version: string; - features: Array; - logs: Array; -} - -export interface RustExperienceDecisionRequest { - visitorId: string; - experienceKey?: string; - experienceId?: string; - context: Record; - environment?: string; - options?: Record; - visitorState?: Record; -} - -export interface RustFeatureAggregationRequest { - variationSummaries: Array; - filters?: Record; - typeCasting?: boolean; -} - -export interface RustDecisionEngineNamespace { - WasmDecisionEngine: new () => { - decideExperience( - project: ConfigResponseData['project'], - request: RustExperienceDecisionRequest - ): RustDecisionResponsePayload; - aggregateFeatures( - project: ConfigResponseData['project'], - input: RustFeatureAggregationRequest - ): RustFeatureAggregationPayload; - free(): void; - }; - default?: () => Promise; -} - -export class RustDeciderNotReadyError extends Error { - constructor(message = 'Rust decision engine not ready') { - super(message); - this.name = 'RustDeciderNotReadyError'; - } -} - -type DecisionsCoreModule = RustDecisionEngineNamespace & { - default?: (...args: Array) => unknown; -}; - -let moduleNamespace: RustDecisionEngineNamespace | null = null; -let initialized = false; -let initializationPromise: Promise | null = null; - -const loadModule = async (): Promise => { - const candidate = decisionsCoreModule as unknown as DecisionsCoreModule; - - if (typeof candidate.default === 'function') { - try { - const maybeNamespace = await candidate.default(); - if ( - maybeNamespace && - typeof maybeNamespace === 'object' && - 'WasmDecisionEngine' in (maybeNamespace as unknown as Record) - ) { - return maybeNamespace as RustDecisionEngineNamespace; - } - } catch (error) { - // If initialization fails (e.g., missing fetch in legacy browsers), fall back to the raw module - } - } - - return candidate as RustDecisionEngineNamespace; -}; - -const setNamespace = (namespace: RustDecisionEngineNamespace | null) => { - moduleNamespace = namespace; - initialized = Boolean(namespace && namespace.WasmDecisionEngine); -}; - -export const initializeRustDecider = async (): Promise => { - if (initialized) return; - if (!initializationPromise) { - initializationPromise = (async () => { - try { - const namespace = await loadModule(); - setNamespace(namespace); - } catch (error) { - setNamespace(null); - throw error; - } - })(); - } - - try { - await initializationPromise; - } finally { - initializationPromise = null; - } -}; - -export const isRustDeciderReady = (): boolean => initialized; - -const getNamespace = (): RustDecisionEngineNamespace => { - if (!moduleNamespace || !initialized) { - throw new RustDeciderNotReadyError(); - } - return moduleNamespace; -}; - -const disposeEngine = (engine: { free?: () => void }) => { - try { - engine?.free?.(); - } catch (error) { - // ignore disposal issues - } -}; - -export const decideExperienceWithRust = ( - project: ConfigResponseData['project'], - request: RustExperienceDecisionRequest -): RustDecisionResponsePayload => { - const namespace = getNamespace(); - const engine = new namespace.WasmDecisionEngine(); - try { - return engine.decideExperience(project, request) as RustDecisionResponsePayload; - } finally { - disposeEngine(engine); - } -}; - -export const aggregateFeaturesWithRust = ( - project: ConfigResponseData['project'], - input: RustFeatureAggregationRequest -): RustFeatureAggregationPayload => { - const namespace = getNamespace(); - const engine = new namespace.WasmDecisionEngine(); - try { - return engine.aggregateFeatures(project, input) as RustFeatureAggregationPayload; - } finally { - disposeEngine(engine); - } -}; diff --git a/packages/event/package.json b/packages/event/package.json index 13c3c113..d24b4b32 100644 --- a/packages/event/package.json +++ b/packages/event/package.json @@ -71,8 +71,8 @@ }, "version": "2.1.3", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0" + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-types": ">=3.9.0" } -} +} \ No newline at end of file diff --git a/packages/experience/package.json b/packages/experience/package.json index be98131c..7ca0234c 100644 --- a/packages/experience/package.json +++ b/packages/experience/package.json @@ -71,9 +71,9 @@ }, "version": "2.3.2", "peerDependencies": { - "@convertcom/js-sdk-data": ">=3.2.1", - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0" + "@convertcom/js-sdk-data": ">=3.3.3", + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-types": ">=3.9.0" } -} +} \ No newline at end of file diff --git a/packages/js-sdk/index.tests.js b/packages/js-sdk/index.tests.js index d37d2401..02755809 100644 --- a/packages/js-sdk/index.tests.js +++ b/packages/js-sdk/index.tests.js @@ -23,7 +23,7 @@ class DataStore { const dataStore = new DataStore(); testConfig.experimental = { - useRustDecider: true + useCoreDecider: true }; testConfig.dataStore = dataStore; testConfig.events = { diff --git a/packages/js-sdk/src/config/default.ts b/packages/js-sdk/src/config/default.ts index b58f4c20..d81109be 100644 --- a/packages/js-sdk/src/config/default.ts +++ b/packages/js-sdk/src/config/default.ts @@ -34,7 +34,7 @@ export const defaultConfig = { comparisonProcessor: null // Allows 3rd party comparison processor to be passed }, experimental: { - useRustDecider: false + useCoreDecider: false }, network: { tracking: true, diff --git a/packages/js-sdk/src/core.ts b/packages/js-sdk/src/core.ts index 45a2cd7e..1972c0ca 100644 --- a/packages/js-sdk/src/core.ts +++ b/packages/js-sdk/src/core.ts @@ -111,12 +111,38 @@ export class Core implements CoreInterface { error: config.data['error'] }); } else { - this._eventManager.fire(SystemEvents.READY, null, null, true); - this._loggerManager?.trace?.( - 'Core.initialize()', - MESSAGES.CORE_INITIALIZED - ); - this._initialized = true; + // If using Core decider, wait for it to initialize before firing READY + if (config?.experimental?.useCoreDecider && this._dataManager.coreDecider) { + this._dataManager.coreDecider.initialize() + .then(() => { + this._eventManager.fire(SystemEvents.READY, null, null, true); + this._loggerManager?.trace?.( + 'Core.initialize()', + MESSAGES.CORE_INITIALIZED + ); + this._initialized = true; + }) + .catch((error: Error) => { + this._loggerManager?.warn?.( + 'Core.initialize()', + 'Core decider initialization failed, falling back to TypeScript', + error + ); + this._eventManager.fire(SystemEvents.READY, null, null, true); + this._loggerManager?.trace?.( + 'Core.initialize()', + MESSAGES.CORE_INITIALIZED + ); + this._initialized = true; + }); + } else { + this._eventManager.fire(SystemEvents.READY, null, null, true); + this._loggerManager?.trace?.( + 'Core.initialize()', + MESSAGES.CORE_INITIALIZED + ); + this._initialized = true; + } } } else { this._loggerManager?.error?.( @@ -200,25 +226,34 @@ export class Core implements CoreInterface { this._loggerManager?.trace?.('Core.fetchConfig()', { data }); - this._eventManager.fire( - objectNotEmpty(this._dataManager.data) - ? SystemEvents.CONFIG_UPDATED - : SystemEvents.READY, - null, - null, - true - ); - if (objectNotEmpty(this._dataManager.data)) { + + const isInitialLoad = !objectNotEmpty(this._dataManager.data); + const eventType = isInitialLoad ? SystemEvents.READY : SystemEvents.CONFIG_UPDATED; + + // If using Rust decider on initial load, wait for it to initialize before firing READY + if (isInitialLoad && this._config?.experimental?.useCoreDecider && this._dataManager.coreDecider) { + await this._dataManager.coreDecider.initialize().catch((error: Error) => { + this._loggerManager?.warn?.( + 'Core.fetchConfig()', + 'Rust decider initialization failed, falling back to TypeScript', + error + ); + }); + } + + this._eventManager.fire(eventType, null, null, true); + + if (isInitialLoad) { this._loggerManager?.trace?.( 'Core.fetchConfig()', - MESSAGES.CONFIG_DATA_UPDATED + MESSAGES.CORE_INITIALIZED ); + this._initialized = true; } else { this._loggerManager?.trace?.( 'Core.fetchConfig()', - MESSAGES.CORE_INITIALIZED + MESSAGES.CONFIG_DATA_UPDATED ); - this._initialized = true; } this.data = data; this._dataManager.data = data; diff --git a/packages/js-sdk/src/feature-manager.ts b/packages/js-sdk/src/feature-manager.ts index e7972e33..f4e1d820 100644 --- a/packages/js-sdk/src/feature-manager.ts +++ b/packages/js-sdk/src/feature-manager.ts @@ -32,7 +32,7 @@ import { } from '@convertcom/js-sdk-utils'; import {BucketedVariation} from '@convertcom/js-sdk-types'; -type RustFeatureDecision = { +type CoreFeatureDecision = { id: string; key: string; name?: string; @@ -375,12 +375,12 @@ export class FeatureManager implements FeatureManagerInterface { ); if (matchedErrors.length) return matchedErrors as Array; - const rustAggregated = this._maybeAggregateFeaturesWithRust( + const coreAggregated = this._maybeAggregateFeaturesWithCore( bucketedVariations as Array, filter, typeCasting ); - if (rustAggregated) return rustAggregated; + if (coreAggregated) return coreAggregated; // Collect features from bucketed variations for (const k in bucketedVariations) { @@ -491,14 +491,14 @@ export class FeatureManager implements FeatureManagerInterface { return castType(value, type); } - private _maybeAggregateFeaturesWithRust( + private _maybeAggregateFeaturesWithCore( variations: Array, filter: Record> | undefined, typeCasting: boolean ): Array | null { - if (!this._dataManager?.isRustDeciderEnabled()) return null; + if (!this._dataManager?.isCoreDeciderEnabled()) return null; const variationSummaries = variations - .map((variation) => this._toRustVariationSummary(variation)) + .map((variation) => this._toCoreVariationSummary(variation)) .filter(Boolean) as Array<{ experience_id: string; experience_key: string; @@ -515,7 +515,7 @@ export class FeatureManager implements FeatureManagerInterface { } : undefined; - const response = this._dataManager.aggregateFeaturesWithRust( + const response = this._dataManager.aggregateFeaturesWithCore( variationSummaries, { filters, @@ -525,12 +525,12 @@ export class FeatureManager implements FeatureManagerInterface { if (!response) return null; - return response.features.map((decision: RustFeatureDecision) => - this._mapRustFeatureDecision(decision as RustFeatureDecision) + return response.features.map((decision: CoreFeatureDecision) => + this._mapCoreFeatureDecision(decision as CoreFeatureDecision) ); } - private _toRustVariationSummary( + private _toCoreVariationSummary( variation: BucketedVariation ): { experience_id: string; @@ -556,7 +556,7 @@ export class FeatureManager implements FeatureManagerInterface { }; } - private _mapRustFeatureDecision(decision: RustFeatureDecision): BucketedFeature { + private _mapCoreFeatureDecision(decision: CoreFeatureDecision): BucketedFeature { const status = decision.status === 'Enabled' ? FeatureStatus.ENABLED diff --git a/packages/js-sdk/tests/feature-manager.tests.ts b/packages/js-sdk/tests/feature-manager.tests.ts index 668ac2a6..c8f7232e 100644 --- a/packages/js-sdk/tests/feature-manager.tests.ts +++ b/packages/js-sdk/tests/feature-manager.tests.ts @@ -40,7 +40,7 @@ const ruleManager = new rm(configuration); const eventManager = new em(configuration); const apiManager = new am(configuration, {eventManager}); -class RustDataManagerStub { +class CoreDataManagerStub { public aggregateCalled = false; public aggregateArgs: any; @@ -66,7 +66,7 @@ class RustDataManagerStub { data = configuration.data; - aggregateFeaturesWithRust(variationSummaries, options): any { + aggregateFeaturesWithCore(variationSummaries, options): any { this.aggregateCalled = true; this.aggregateArgs = {variationSummaries, options}; return { @@ -151,7 +151,7 @@ class RustDataManagerStub { return []; } - isRustDeciderEnabled(): boolean { + isCoreDeciderEnabled(): boolean { return true; } @@ -375,8 +375,8 @@ describe('FeatureManager tests', function () { res.end('{}'); }); }); - it('Should prefer Rust aggregation when available', function () { - const stub = new RustDataManagerStub(); + it('Should prefer Core aggregation when available', function () { + const stub = new CoreDataManagerStub(); const rustFeatureManager = new fm(configuration, { dataManager: stub as unknown as DataManagerInterface }); diff --git a/packages/logger/package.json b/packages/logger/package.json index ea84f18f..56630de6 100644 --- a/packages/logger/package.json +++ b/packages/logger/package.json @@ -68,6 +68,6 @@ }, "version": "2.1.2", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" } -} +} \ No newline at end of file diff --git a/packages/rules/package.json b/packages/rules/package.json index fb0ff6d8..a418aece 100644 --- a/packages/rules/package.json +++ b/packages/rules/package.json @@ -71,9 +71,9 @@ }, "version": "2.1.3", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0", - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-types": ">=3.9.0", + "@convertcom/js-sdk-utils": ">=2.2.3" } -} +} \ No newline at end of file diff --git a/packages/segments/package.json b/packages/segments/package.json index 67e1722b..f086ed33 100644 --- a/packages/segments/package.json +++ b/packages/segments/package.json @@ -71,10 +71,10 @@ }, "version": "2.1.2", "peerDependencies": { - "@convertcom/js-sdk-data": ">=3.2.1", - "@convertcom/js-sdk-enums": ">=2.2.0", - "@convertcom/js-sdk-logger": ">=2.1.0", - "@convertcom/js-sdk-rules": ">=2.1.0", - "@convertcom/js-sdk-types": ">=3.6.0" + "@convertcom/js-sdk-data": ">=3.3.3", + "@convertcom/js-sdk-enums": ">=2.3.0", + "@convertcom/js-sdk-logger": ">=2.1.2", + "@convertcom/js-sdk-rules": ">=2.1.3", + "@convertcom/js-sdk-types": ">=3.9.0" } -} +} \ No newline at end of file diff --git a/packages/types/index.ts b/packages/types/index.ts index 68ac1a02..44f674e3 100644 --- a/packages/types/index.ts +++ b/packages/types/index.ts @@ -13,6 +13,7 @@ export * from './src/BucketingAllocation'; export * from './src/BucketingHash'; export * from './src/Config'; export * from './src/ConversionAttributes'; +export * from './src/CoreDecider'; export * from './src/Entity'; export * from './src/GoalData'; export * from './src/IndentityField'; diff --git a/packages/types/package.json b/packages/types/package.json index 630bc1c2..7a8800d1 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -51,6 +51,6 @@ }, "version": "3.9.0", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" } -} +} \ No newline at end of file diff --git a/packages/types/src/Config.ts b/packages/types/src/Config.ts index e5dfcbb9..c48e0cdc 100644 --- a/packages/types/src/Config.ts +++ b/packages/types/src/Config.ts @@ -49,7 +49,7 @@ type ConfigBase = { source?: string; }; experimental?: { - useRustDecider?: boolean; + useCoreDecider?: boolean; }; mapper?: (...args: any) => any; }; diff --git a/packages/types/src/CoreDecider.ts b/packages/types/src/CoreDecider.ts new file mode 100644 index 00000000..93a65ab2 --- /dev/null +++ b/packages/types/src/CoreDecider.ts @@ -0,0 +1,15 @@ +/*! + * Convert JS SDK + * Version 1.0.0 + * Copyright(c) 2020 Convert Insights, Inc + * License Apache-2.0 + */ + +/** + * CoreDecider interface for type checking across packages + * The actual implementation lives in @convertcom/js-sdk-data + */ +export interface CoreDecider { + initialize(): Promise; + isReady(): boolean; +} diff --git a/packages/utils/package.json b/packages/utils/package.json index 0b4c4318..183e24ac 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -72,6 +72,6 @@ }, "version": "2.2.3", "peerDependencies": { - "@convertcom/js-sdk-enums": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" } -} +} \ No newline at end of file From 3fef38e5ea472ab6c802cdbe6b9a1d199b078b7f Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Tue, 7 Oct 2025 03:30:08 +0300 Subject: [PATCH 07/10] fix: await SDK ready in UMD browser tests before creating context MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Updated test hooks to properly await SDK initialization when using Core decider. The WASM module loads asynchronously, so tests must wait for the READY event before calling createContext(). Changes: - Made test "Shoud successfully create visitor context" async and added await - Updated "Basic SDK methods" before() hook to be async and await onReady() - Updated "Test invalid visitor" before() hook to be async and await onReady() All 20 UMD browser tests now pass with Core decider enabled. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/js-sdk/index.tests.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/js-sdk/index.tests.js b/packages/js-sdk/index.tests.js index 02755809..803683f0 100644 --- a/packages/js-sdk/index.tests.js +++ b/packages/js-sdk/index.tests.js @@ -65,7 +65,8 @@ export default function runTests(bundle) { await convert.onReady(); assert.equal(true, true); }); - it('Shoud successfully create visitor context', function () { + it('Shoud successfully create visitor context', async function () { + await convert.onReady(); const visitorContext = convert.createContext(visitorId, defaultSegments); expect(visitorContext).to.be.an('object'); [ @@ -83,8 +84,9 @@ export default function runTests(bundle) { }); describe('Basic SDK methods', function () { // eslint-disable-next-line mocha/no-hooks-for-single-case - before(function () { + before(async function () { convert = new ConvertSDK(testConfig); + await convert.onReady(); }); // eslint-disable-next-line mocha/no-hooks-for-single-case beforeEach(function () { @@ -211,8 +213,9 @@ export default function runTests(bundle) { }); describe('Test invalid visitor', function () { // eslint-disable-next-line mocha/no-hooks-for-single-case - before(function () { + before(async function () { convert = new ConvertSDK(testConfig); + await convert.onReady(); }); // eslint-disable-next-line mocha/no-hooks-for-single-case beforeEach(function () { From 2f73042214174c08c56491c98f4016768599bd3e Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Tue, 7 Oct 2025 03:34:53 +0300 Subject: [PATCH 08/10] chore: update import formatting and error message MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Standardized import statement formatting with spaces around braces - Updated error log message from "Rust decider" to "Core decider" - Updated yarn.lock after dependency reinstallation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/js-sdk/src/core.ts | 28 ++++++------- yarn.lock | 80 ++++++++++++++++++------------------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/packages/js-sdk/src/core.ts b/packages/js-sdk/src/core.ts index 1972c0ca..13d26b85 100644 --- a/packages/js-sdk/src/core.ts +++ b/packages/js-sdk/src/core.ts @@ -4,21 +4,21 @@ * Copyright(c) 2020 Convert Insights, Inc * License Apache-2.0 */ -import {ApiManagerInterface} from '@convertcom/js-sdk-api'; -import {ContextInterface} from './interfaces/context'; -import {CoreInterface} from './interfaces/core'; -import {DataManagerInterface} from '@convertcom/js-sdk-data'; -import {EventManagerInterface} from '@convertcom/js-sdk-event'; -import {ExperienceManagerInterface} from '@convertcom/js-sdk-experience'; -import {FeatureManagerInterface} from './interfaces/feature-manager'; -import {LogManagerInterface} from '@convertcom/js-sdk-logger'; -import {SegmentsManagerInterface} from '@convertcom/js-sdk-segments'; +import { ApiManagerInterface } from '@convertcom/js-sdk-api'; +import { ContextInterface } from './interfaces/context'; +import { CoreInterface } from './interfaces/core'; +import { DataManagerInterface } from '@convertcom/js-sdk-data'; +import { EventManagerInterface } from '@convertcom/js-sdk-event'; +import { ExperienceManagerInterface } from '@convertcom/js-sdk-experience'; +import { FeatureManagerInterface } from './interfaces/feature-manager'; +import { LogManagerInterface } from '@convertcom/js-sdk-logger'; +import { SegmentsManagerInterface } from '@convertcom/js-sdk-segments'; -import {Config, ConfigResponseData} from '@convertcom/js-sdk-types'; +import { Config, ConfigResponseData } from '@convertcom/js-sdk-types'; -import {ERROR_MESSAGES, MESSAGES, SystemEvents} from '@convertcom/js-sdk-enums'; -import {objectNotEmpty} from '@convertcom/js-sdk-utils'; -import {Context} from './context'; +import { ERROR_MESSAGES, MESSAGES, SystemEvents } from '@convertcom/js-sdk-enums'; +import { objectNotEmpty } from '@convertcom/js-sdk-utils'; +import { Context } from './context'; const DEFAULT_DATA_REFRESH_INTERVAL = 300000; // in milliseconds (5 minutes) @@ -235,7 +235,7 @@ export class Core implements CoreInterface { await this._dataManager.coreDecider.initialize().catch((error: Error) => { this._loggerManager?.warn?.( 'Core.fetchConfig()', - 'Rust decider initialization failed, falling back to TypeScript', + 'Core decider initialization failed, falling back to TypeScript', error ); }); diff --git a/yarn.lock b/yarn.lock index 04f84dd3..d35f2514 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2068,15 +2068,15 @@ __metadata: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node::locator=%40convertcom%2Fjs-sdk%40workspace%3Apackages%2Fjs-sdk": version: 0.1.0 - resolution: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node#../../../decisions-core-sdk/pkg-node::hash=6eda83&locator=%40convertcom%2Fjs-sdk%40workspace%3Apackages%2Fjs-sdk" - checksum: 10c0/c502d6b87a5b3ca1b7180106ba066067d6e30ffcbf8abaedeaf9dce4fb16fee0b107bb3df785dd5b971374d8ce6f3743ac071713f5ed3e6d5aa7fe919d799d56 + resolution: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node#../../../decisions-core-sdk/pkg-node::hash=0f6de7&locator=%40convertcom%2Fjs-sdk%40workspace%3Apackages%2Fjs-sdk" + checksum: 10c0/f7077de5f0d68357cd80e6e5c42acbc852288159a517a956584c264b9caedd347ebb5a12538cfb3b3839a29a292236065254fc3d8efcc57537b7a4678b479b57 languageName: node linkType: hard "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node::locator=%40convertcom%2Fjs-sdk-data%40workspace%3Apackages%2Fdata": version: 0.1.0 - resolution: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node#../../../decisions-core-sdk/pkg-node::hash=6eda83&locator=%40convertcom%2Fjs-sdk-data%40workspace%3Apackages%2Fdata" - checksum: 10c0/c502d6b87a5b3ca1b7180106ba066067d6e30ffcbf8abaedeaf9dce4fb16fee0b107bb3df785dd5b971374d8ce6f3743ac071713f5ed3e6d5aa7fe919d799d56 + resolution: "@convertcom/decisions-core@file:../../../decisions-core-sdk/pkg-node#../../../decisions-core-sdk/pkg-node::hash=0f6de7&locator=%40convertcom%2Fjs-sdk-data%40workspace%3Apackages%2Fdata" + checksum: 10c0/f7077de5f0d68357cd80e6e5c42acbc852288159a517a956584c264b9caedd347ebb5a12538cfb3b3839a29a292236065254fc3d8efcc57537b7a4678b479b57 languageName: node linkType: hard @@ -2121,11 +2121,11 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-event": ">=2.1.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-event": ">=2.1.3" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-types": ">=3.9.0" + "@convertcom/js-sdk-utils": ">=2.2.3" languageName: unknown linkType: soft @@ -2170,10 +2170,10 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-types": ">=3.9.0" + "@convertcom/js-sdk-utils": ">=2.2.3" languageName: unknown linkType: soft @@ -2219,14 +2219,14 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-api": ">=2.1.1" - "@convertcom/js-sdk-bucketing": ">=3.1.0" - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-event": ">=2.1.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-rules": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-api": ">=2.1.4" + "@convertcom/js-sdk-bucketing": ">=3.1.2" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-event": ">=2.1.3" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-rules": ">=2.1.3" + "@convertcom/js-sdk-types": ">=3.9.0" + "@convertcom/js-sdk-utils": ">=2.2.3" languageName: unknown linkType: soft @@ -2335,9 +2335,9 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-types": ">=3.9.0" languageName: unknown linkType: soft @@ -2382,10 +2382,10 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-data": ">=3.2.1" - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" + "@convertcom/js-sdk-data": ">=3.3.3" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-types": ">=3.9.0" languageName: unknown linkType: soft @@ -2427,7 +2427,7 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" languageName: unknown linkType: soft @@ -2472,10 +2472,10 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" - "@convertcom/js-sdk-utils": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-types": ">=3.9.0" + "@convertcom/js-sdk-utils": ">=2.2.3" languageName: unknown linkType: soft @@ -2520,11 +2520,11 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-data": ">=3.2.1" - "@convertcom/js-sdk-enums": ">=2.2.0" - "@convertcom/js-sdk-logger": ">=2.1.0" - "@convertcom/js-sdk-rules": ">=2.1.0" - "@convertcom/js-sdk-types": ">=3.6.0" + "@convertcom/js-sdk-data": ">=3.3.3" + "@convertcom/js-sdk-enums": ">=2.3.0" + "@convertcom/js-sdk-logger": ">=2.1.2" + "@convertcom/js-sdk-rules": ">=2.1.3" + "@convertcom/js-sdk-types": ">=3.9.0" languageName: unknown linkType: soft @@ -2555,7 +2555,7 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" languageName: unknown linkType: soft @@ -2599,7 +2599,7 @@ __metadata: typescript-eslint: "npm:^7.13.1" webpack: "npm:^5.92.0" peerDependencies: - "@convertcom/js-sdk-enums": ">=2.2.0" + "@convertcom/js-sdk-enums": ">=2.3.0" languageName: unknown linkType: soft From c920bfb6b71611f094cad36f7f1114cf7dec64eb Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Tue, 7 Oct 2025 03:38:42 +0300 Subject: [PATCH 09/10] docs: fix README.md instruction order and typos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Reordered demo instructions: build packages before starting servers - Added build step to Installation section - Fixed typo: "Prevview" → "Preview" - Fixed typo: "enviroment" → "environment" All demos now follow: Build → Create env → Start → Test 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- README.md | 41 +++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 7f9fd875..24e210e6 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,8 @@ ## Installation 1. Install dependencies: `yarn` -2. Copy `env.example` into `.env` at the following packages: +2. Build packages: `yarn build` +3. Copy `env.example` into `.env` at the following packages: 1. [react demo](packages/demo-react/README.md) 2. [nodejs demo](packages/demo-nodejs/README.md) 3. [sdk](packages/js-sdk/README.md) @@ -12,7 +13,7 @@ 1. Build packages: `yarn build` 2. Start local server: `yarn sdk:docs` -3. Prevview docs: http://localhost:3001 +3. Preview docs: http://localhost:3001 ## Demo Instructions @@ -20,42 +21,42 @@ ### React -1. Start demo server: `yarn demo:reactjs:start` -2. Build packages: `yarn build` -3. Create enviroment file: `cp demo/reactjs/.env.example demo/reactjs/.env` +1. Build packages: `yarn build` +2. Create environment file: `cp demo/reactjs/.env.example demo/reactjs/.env` +3. Start demo server: `yarn demo:reactjs:start` 4. Test demo app: http://localhost:3002 ### NodeJS -1. Start demo server: `yarn demo:nodejs:start` -2. Build packages: `yarn build` -3. Create enviroment file: `cp demo/nodejs/.env.example demo/nodejs/.env` +1. Build packages: `yarn build` +2. Create environment file: `cp demo/nodejs/.env.example demo/nodejs/.env` +3. Start demo server: `yarn demo:nodejs:start` 4. Test demo app: http://localhost:3003 ### NestJS -1. Start demo server: `yarn demo:nestjs:start` -2. Build packages: `yarn build` -3. Create enviroment file: `cp demo/nestjs/.env.example demo/nestjs/.env` +1. Build packages: `yarn build` +2. Create environment file: `cp demo/nestjs/.env.example demo/nestjs/.env` +3. Start demo server: `yarn demo:nestjs:start` 4. Test demo app: http://localhost:3004 ### NextJs -1. Start demo server: `yarn demo:nextjs:start` -2. Build packages: `yarn build` -3. Create enviroment file: `cp demo/nextjs/.env.example demo/nextjs/.env` +1. Build packages: `yarn build` +2. Create environment file: `cp demo/nextjs/.env.example demo/nextjs/.env` +3. Start demo server: `yarn demo:nextjs:start` 4. Test demo app: http://localhost:3005 ### RemixJs (client side) -1. Start demo server: `yarn demo:remixjs:client:start` -2. Build packages: `yarn build` -3. Create enviroment file: `cp demo/remixjs-client-side/.env.example demo/remixjs-client-side/.env` +1. Build packages: `yarn build` +2. Create environment file: `cp demo/remixjs-client-side/.env.example demo/remixjs-client-side/.env` +3. Start demo server: `yarn demo:remixjs:client:start` 4. Test demo app: http://localhost:3006 ### RemixJs (server side) -1. Start demo server: `yarn demo:remixjs:server:start` -2. Build packages: `yarn build` -3. Create enviroment file: `cp demo/remixjs-server-side/.env.example demo/remixjs-server-side/.env` +1. Build packages: `yarn build` +2. Create environment file: `cp demo/remixjs-server-side/.env.example demo/remixjs-server-side/.env` +3. Start demo server: `yarn demo:remixjs:server:start` 4. Test demo app: http://localhost:3007 \ No newline at end of file From f4741f9f2d1fa4f94235391725a22ac2c37db1a5 Mon Sep 17 00:00:00 2001 From: Ahmed Abbas Date: Mon, 13 Oct 2025 21:53:31 +0300 Subject: [PATCH 10/10] chore: separate umd wasm tests --- .../js-sdk/index.browser.umd.wasm.tests.js | 12 + packages/js-sdk/index.tests.js | 3 - packages/js-sdk/index.wasm.tests.js | 253 ++++++++++++++++++ packages/js-sdk/karma.umd.wasm.conf.js | 40 +++ packages/js-sdk/package.json | 1 + 5 files changed, 306 insertions(+), 3 deletions(-) create mode 100644 packages/js-sdk/index.browser.umd.wasm.tests.js create mode 100644 packages/js-sdk/index.wasm.tests.js create mode 100644 packages/js-sdk/karma.umd.wasm.conf.js diff --git a/packages/js-sdk/index.browser.umd.wasm.tests.js b/packages/js-sdk/index.browser.umd.wasm.tests.js new file mode 100644 index 00000000..e340a431 --- /dev/null +++ b/packages/js-sdk/index.browser.umd.wasm.tests.js @@ -0,0 +1,12 @@ +// No SDK library imports here. A UMD script should be already loaded in browser by karma +import {assert} from 'chai'; +import runTests from './index.wasm.tests'; + +describe('Karma browser tests for UMD bundle', function () { + it('Should have an SDK instance in namespace', function () { + // eslint-disable-next-line no-undef + assert.isDefined(ConvertSDK); + }); + // eslint-disable-next-line mocha/no-setup-in-describe,no-undef + runTests(ConvertSDK); +}); diff --git a/packages/js-sdk/index.tests.js b/packages/js-sdk/index.tests.js index 803683f0..34e2089f 100644 --- a/packages/js-sdk/index.tests.js +++ b/packages/js-sdk/index.tests.js @@ -22,9 +22,6 @@ class DataStore { } const dataStore = new DataStore(); -testConfig.experimental = { - useCoreDecider: true -}; testConfig.dataStore = dataStore; testConfig.events = { batch_size: 1, diff --git a/packages/js-sdk/index.wasm.tests.js b/packages/js-sdk/index.wasm.tests.js new file mode 100644 index 00000000..803683f0 --- /dev/null +++ b/packages/js-sdk/index.wasm.tests.js @@ -0,0 +1,253 @@ +/* eslint-disable mocha/consistent-spacing-between-blocks */ +import {expect} from 'chai'; +import {assert} from 'chai'; +import testConfig from './tests/test-config.json'; +import { + getFeaturesWithStatuses, + getMultipleFeatureWithStatus, + getSingleFeatureWithStatus, + getVariationsAcrossAllExperiences +} from './tests/setup/shared'; + +class DataStore { + data = {}; + get(key) { + if (!key) return this.data; + return this.data[key.toString()]; + } + set(key, value) { + if (!key) throw new Error('Invalid DataStore key!'); + this.data[key.toString()] = value; + } +} + +const dataStore = new DataStore(); +testConfig.experimental = { + useCoreDecider: true +}; +testConfig.dataStore = dataStore; +testConfig.events = { + batch_size: 1, + release_interval: 1000 +}; + +const accountId = testConfig.data.account_id; +const projectId = testConfig.data.project.id; +const visitorId = 'XXX'; +const storeKey = `${accountId}-${projectId}-${visitorId}`; + +const defaultSegments = {browser: 'chrome'}; + +// eslint-disable-next-line mocha/no-exports +export default function runTests(bundle) { + const ConvertSDK = bundle.default; + let convert, context; + describe('Basic SDK instance', function () { + // eslint-disable-next-line mocha/no-hooks-for-single-case + beforeEach(function () { + convert = new ConvertSDK(testConfig); + }); + it('Should have an SDK instance as an object', function () { + expect(bundle).to.be.an('object'); + }); + it('Should have a constructor', function () { + expect(ConvertSDK).to.be.a('function'); + }); + it('Should create a default SDK instance and fire ready event. Expect no errors', function (done) { + convert.on('ready', function (args, err) { + expect(err).to.be.null; + //expect(convert).to.have.property('version').which.is.a('string'); + expect(convert).to.be.an('object'); + done(); + }); + }); + it('Should create a default SDK instance and resolve a promise. Expect no errors', async function () { + await convert.onReady(); + assert.equal(true, true); + }); + it('Shoud successfully create visitor context', async function () { + await convert.onReady(); + const visitorContext = convert.createContext(visitorId, defaultSegments); + expect(visitorContext).to.be.an('object'); + [ + 'runExperience', + 'runExperiences', + 'runFeature', + 'runFeatures', + 'trackConversion', + 'setDefaultSegments', + 'runCustomSegments' + ].forEach((method) => { + expect(visitorContext).to.have.a.property(method); + }); + }); + }); + describe('Basic SDK methods', function () { + // eslint-disable-next-line mocha/no-hooks-for-single-case + before(async function () { + convert = new ConvertSDK(testConfig); + await convert.onReady(); + }); + // eslint-disable-next-line mocha/no-hooks-for-single-case + beforeEach(function () { + context = convert.createContext(visitorId, defaultSegments); + }); + it('Shoud successfully get variation from specific experience', function (done) { + const experienceKey = 'test-experience-ab-fullstack-2'; + const variation = context.runExperience(experienceKey, { + locationProperties: {url: 'https://convert.com/'}, + visitorProperties: { + varName3: 'something' + } + }); + expect(variation) + .to.be.an('object') + .that.have.keys( + 'experienceId', + 'experienceKey', + 'experienceName', + 'bucketingAllocation', + 'id', + 'key', + 'name', + 'status', + 'changes', + 'is_baseline', + 'traffic_allocation' + ); + expect(variation.experienceKey).to.equal(experienceKey); + done(); + }); + it('Shoud successfully get variations across all experiences', function (done) { + getVariationsAcrossAllExperiences( + { + accountId, + projectId, + context + }, + done + ); + }); + it('Shoud successfully get a single feature and its status', function (done) { + const featureId = '10025'; + getSingleFeatureWithStatus( + { + accountId, + projectId, + featureId, + context + }, + done + ); + }); + it('Shoud successfully get multiple features and its status', function (done) { + getMultipleFeatureWithStatus( + { + accountId, + projectId, + context + }, + done + ); + }); + it('Shoud successfully get features and their statuses', function (done) { + getFeaturesWithStatuses( + { + accountId, + projectId, + context + }, + done + ); + }); + it('Should trigger Conversion', function () { + const goalKey = 'increase-engagement'; + const response = context.trackConversion(goalKey, { + ruleData: { + action: 'buy' + }, + conversionData: [ + { + key: 'amount', + value: 10.3 + }, + { + key: 'productsCount', + value: 2 + } + ] + }); + expect(response).to.be.undefined; + }); + it('Should successfully set default segments', function (done) { + const segments = {country: 'US'}; + context.setDefaultSegments(segments); + setTimeout(function () { + const localSegments = dataStore.get(storeKey); + expect(localSegments) + .to.have.property('segments') + .that.deep.equal({ + ...segments, + ...defaultSegments + }); + done(); + }, testConfig.events.release_interval + 1); + }); + it('Should successfully set custom segments', function (done) { + const segmentKey = 'test-segments-1'; + const segmentId = '200299434'; + context.runCustomSegments(segmentKey, { + ruleData: { + enabled: true + } + }); + setTimeout(function () { + const {segments} = dataStore.get(storeKey) || {}; + expect(segments) + .to.be.an('object') + .that.has.property('customSegments') + .to.deep.equal([segmentId]); + done(); + }, testConfig.events.release_interval + 1); + }); + }); + describe('Test invalid visitor', function () { + // eslint-disable-next-line mocha/no-hooks-for-single-case + before(async function () { + convert = new ConvertSDK(testConfig); + await convert.onReady(); + }); + // eslint-disable-next-line mocha/no-hooks-for-single-case + beforeEach(function () { + context = convert.createContext(); + }); + it('Shoud fail to get variation from specific experience if no visitor is set', function () { + const experienceKey = 'test-experience-ab-fullstack-2'; + const variation = context.runExperience(experienceKey); + expect(variation).to.be.undefined; + }); + it('Shoud fail to get variations across all experiences if no visitor is set', function () { + const variations = context.runExperiences(); + expect(variations).to.be.undefined; + }); + it('Shoud fail to get feature and its status if no visitor is set', function () { + const featureKey = 'feature-1'; + const features = context.runFeature(featureKey); + expect(features).to.be.undefined; + }); + it('Shoud fail to get features and their statuses if no visitor is set', function () { + const features = context.runFeatures(); + expect(features).to.be.undefined; + }); + it('Should fail to trigger Conversion if no visitor is set', function () { + const goalKey = 'increase-engagement'; + const output = context.trackConversion(goalKey); + expect(output).to.be.undefined; + }); + it('Should fail to set custom segments if no visitor is set', function () { + const segmentKey = 'test-segments-1'; + const output = context.runCustomSegments(segmentKey); + expect(output).to.be.undefined; + }); + }); +} diff --git a/packages/js-sdk/karma.umd.wasm.conf.js b/packages/js-sdk/karma.umd.wasm.conf.js new file mode 100644 index 00000000..32c1d327 --- /dev/null +++ b/packages/js-sdk/karma.umd.wasm.conf.js @@ -0,0 +1,40 @@ +// Karma configuration +// eslint-disable-next-line @typescript-eslint/no-var-requires +const baseConfig = require('./karma.base.conf.js'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require('path'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const os = require('os'); +module.exports = function (config) { + config.set({ + ...baseConfig, + // list of files / patterns to load in the browser + files: [ + './lib/index.umd.min.js', + './index.browser.umd.wasm.tests.js', + // Serve WASM file but don't include it in the test runner + { + pattern: './lib/decisions_core_bg.wasm', + included: false, + served: true, + watched: false + } + ], + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + './index.browser.umd.wasm.tests.js': ['webpack'] + }, + webpack: { + output: { + filename: '[name].js', + path: + path.join(os.tmpdir(), '_karma_webpack_') + + Math.floor(Math.random() * 1000000) + } + }, + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO + }); +}; diff --git a/packages/js-sdk/package.json b/packages/js-sdk/package.json index d7699263..ed9e621c 100644 --- a/packages/js-sdk/package.json +++ b/packages/js-sdk/package.json @@ -22,6 +22,7 @@ "test:browser": "yarn build && yarn test:cjsbrowser && yarn test:umdbrowser", "test:cjsbrowser": "karma start karma.cjs.conf.js --single-run", "test:umdbrowser": "karma start karma.umd.conf.js --single-run", + "test:umdbrowser:wasm": "karma start karma.umd.wasm.conf.js --single-run", "test:server": "nyc yarn test:mocha", "test:mocha": "mocha -r ts-node/register --recursive \"tests/**/*.tests.ts\" --exit", "clean": "rm -rf lib",