diff --git a/.github/workflows/build-pr-artifacts.yml b/.github/workflows/build-pr-artifacts.yml index f6aaa18940..46670fba84 100644 --- a/.github/workflows/build-pr-artifacts.yml +++ b/.github/workflows/build-pr-artifacts.yml @@ -22,7 +22,7 @@ jobs: tag_name_ut: ${{ steps.gen_tag_names.outputs.tag_name_ut }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 diff --git a/.github/workflows/build-push-docker-image.yml b/.github/workflows/build-push-docker-image.yml index cffb5f91da..acc06496c2 100644 --- a/.github/workflows/build-push-docker-image.yml +++ b/.github/workflows/build-push-docker-image.yml @@ -64,7 +64,7 @@ jobs: should_execute_tests: ${{ steps.processing.outputs.should_execute_tests }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 - id: files @@ -92,7 +92,7 @@ jobs: needs: [get_sha, get_changed_files] steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: ref: ${{ needs.get_sha.outputs.sha }} fetch-depth: 1 @@ -145,7 +145,7 @@ jobs: needs: [get_sha, get_changed_files] steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: ref: ${{ needs.get_sha.outputs.sha }} fetch-depth: 1 diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 7f6d068e15..74e7f0e604 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -11,12 +11,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/component-test-report.yml b/.github/workflows/component-test-report.yml index 02df1478d4..8d739c5aee 100644 --- a/.github/workflows/component-test-report.yml +++ b/.github/workflows/component-test-report.yml @@ -27,12 +27,12 @@ jobs: aws-region: us-east-1 - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/draft-new-release.yml b/.github/workflows/draft-new-release.yml index 82174d1d72..602ded980f 100644 --- a/.github/workflows/draft-new-release.yml +++ b/.github/workflows/draft-new-release.yml @@ -11,12 +11,12 @@ jobs: if: (github.ref == 'refs/heads/develop' || startsWith(github.ref, 'refs/heads/hotfix/')) && (github.actor == 'ItsSudip' || github.actor == 'krishna2020' || github.actor == 'saikumarrs' || github.actor == 'sandeepdsvs' || github.actor == 'koladilip' || github.actor == 'shrouti1507' || github.actor == 'anantjain45823' || github.actor == 'chandumlg' || github.actor == 'mihir-4116' || github.actor == 'yashasvibajpai' || github.actor == 'sanpj2292' || github.actor == 'utsabc') && (github.triggering_actor == 'ItsSudip' || github.triggering_actor == 'krishna2020' || github.triggering_actor == 'koladilip' || github.triggering_actor == 'saikumarrs' || github.triggering_actor == 'sandeepdsvs' || github.triggering_actor == 'shrouti1507' || github.triggering_actor == 'anantjain45823' || github.triggering_actor == 'chandumlg' || github.triggering_actor == 'mihir-4116' || github.triggering_actor == 'yashasvibajpai' || github.triggering_actor == 'sanpj2292' || github.triggering_actor == 'utsabc') steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/dt-test-and-report-code-coverage.yml b/.github/workflows/dt-test-and-report-code-coverage.yml index 85625a1558..86963b6270 100644 --- a/.github/workflows/dt-test-and-report-code-coverage.yml +++ b/.github/workflows/dt-test-and-report-code-coverage.yml @@ -31,12 +31,12 @@ jobs: tests_run_outcome: ${{steps.run_tests.outcome}} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/housekeeping.yml b/.github/workflows/housekeeping.yml index 388b80c15f..bd66a93689 100644 --- a/.github/workflows/housekeeping.yml +++ b/.github/workflows/housekeeping.yml @@ -33,7 +33,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 - name: Delete Old Branches uses: beatlabs/delete-old-branches-action@v0.0.10 diff --git a/.github/workflows/prepare-for-dev-deploy.yml b/.github/workflows/prepare-for-dev-deploy.yml index a1fdda8ccd..b5482bcddf 100644 --- a/.github/workflows/prepare-for-dev-deploy.yml +++ b/.github/workflows/prepare-for-dev-deploy.yml @@ -32,7 +32,7 @@ jobs: tag_name_ut: ${{ steps.gen_tag_names.outputs.tag_name_ut }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 diff --git a/.github/workflows/prepare-for-prod-dt-deploy.yml b/.github/workflows/prepare-for-prod-dt-deploy.yml index 8e589f5b20..0a61a359bf 100644 --- a/.github/workflows/prepare-for-prod-dt-deploy.yml +++ b/.github/workflows/prepare-for-prod-dt-deploy.yml @@ -32,7 +32,7 @@ jobs: tag_name_ut: ${{ steps.gen_tag_names.outputs.tag_name_ut }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 @@ -71,7 +71,7 @@ jobs: TF_IMAGE_REPOSITORY: rudderstack/rudder-transformer steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 diff --git a/.github/workflows/prepare-for-prod-ut-deploy.yml b/.github/workflows/prepare-for-prod-ut-deploy.yml index 468307b94a..3053979b3e 100644 --- a/.github/workflows/prepare-for-prod-ut-deploy.yml +++ b/.github/workflows/prepare-for-prod-ut-deploy.yml @@ -31,7 +31,7 @@ jobs: tag_name_ut: ${{ steps.gen_tag_names.outputs.tag_name_ut }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 @@ -74,7 +74,7 @@ jobs: TF_IMAGE_REPOSITORY: rudderstack/rudder-transformer steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 diff --git a/.github/workflows/prepare-for-staging-deploy.yml b/.github/workflows/prepare-for-staging-deploy.yml index 60a6238aa8..8eeafc376e 100644 --- a/.github/workflows/prepare-for-staging-deploy.yml +++ b/.github/workflows/prepare-for-staging-deploy.yml @@ -24,7 +24,7 @@ jobs: tag_name_ut: ${{ steps.gen_tag_names.outputs.tag_name_ut }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 @@ -82,7 +82,7 @@ jobs: UT_TAG_NAME: ${{ needs.generate-tag-names.outputs.tag_name_ut }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 diff --git a/.github/workflows/publish-new-release.yml b/.github/workflows/publish-new-release.yml index ee9f28e235..a91a1e2b22 100644 --- a/.github/workflows/publish-new-release.yml +++ b/.github/workflows/publish-new-release.yml @@ -25,12 +25,12 @@ jobs: echo "release_version=$version" >> $GITHUB_OUTPUT - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 0 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/slack-notify.yml b/.github/workflows/slack-notify.yml index 1c5dbdbc06..3c4223752b 100644 --- a/.github/workflows/slack-notify.yml +++ b/.github/workflows/slack-notify.yml @@ -16,7 +16,7 @@ jobs: if: ${{ inputs.should_notify }} steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 - name: notify uses: slackapi/slack-github-action@v1.27.0 diff --git a/.github/workflows/ut-tests.yml b/.github/workflows/ut-tests.yml index b825bdb9d3..34442f83c5 100644 --- a/.github/workflows/ut-tests.yml +++ b/.github/workflows/ut-tests.yml @@ -25,12 +25,12 @@ jobs: run: go install sigs.k8s.io/kind@v0.17.0 - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/verify-server-start.yml b/.github/workflows/verify-server-start.yml index f87ed57eb8..5f9c247428 100644 --- a/.github/workflows/verify-server-start.yml +++ b/.github/workflows/verify-server-start.yml @@ -14,12 +14,12 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: fetch-depth: 1 - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: '.nvmrc' cache: 'npm' diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 0d389f0e55..115cad4248 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -14,13 +14,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4.1.1 + uses: actions/checkout@v4.2.1 with: # Make sure the actual branch is checked out when running on pull requests ref: ${{ github.head_ref }} - name: Setup Node - uses: actions/setup-node@v4.0.3 + uses: actions/setup-node@v4.0.4 with: node-version-file: .nvmrc cache: 'npm' diff --git a/CHANGELOG.md b/CHANGELOG.md index c860747b6f..a81c5b5fee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,75 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [1.82.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.82.0...v1.82.1) (2024-10-16) + + +### Bug Fixes + +* heap userId extraction ([fa620e0](https://github.com/rudderlabs/rudder-transformer/commit/fa620e00db13b7bcdc599cb99966395cfbe53f96)) + +## [1.82.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.81.0...v1.82.0) (2024-10-09) + + +### Features + +* onboard Amazon Audience ([#3727](https://github.com/rudderlabs/rudder-transformer/issues/3727)) ([5ac8186](https://github.com/rudderlabs/rudder-transformer/commit/5ac81860c51f9971343df8c61bfd0b2de8161735)) +* onboard intercom v2 destination ([#3721](https://github.com/rudderlabs/rudder-transformer/issues/3721)) ([f8cde8c](https://github.com/rudderlabs/rudder-transformer/commit/f8cde8c072eb9415368fb97f53a3070027a3943b)) + + +### Bug Fixes + +* add list of the props, which need to be placed at the root ([#3777](https://github.com/rudderlabs/rudder-transformer/issues/3777)) ([b357dd4](https://github.com/rudderlabs/rudder-transformer/commit/b357dd4e8a49ed66576f731a6aac84da55397475)) +* rakuten amount value rounded up to nearest integer ([#3784](https://github.com/rudderlabs/rudder-transformer/issues/3784)) ([f3046f0](https://github.com/rudderlabs/rudder-transformer/commit/f3046f0ae37c113c1239d988d056fc204f2776a0)) +* webhook proc workflow object assign bug ([#3775](https://github.com/rudderlabs/rudder-transformer/issues/3775)) ([de8e503](https://github.com/rudderlabs/rudder-transformer/commit/de8e503524c1e8e3320f7458c66b8581f121b9bb)) + +## [1.81.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.79.1...v1.81.0) (2024-10-04) + + +### Features + +* add unity source support in Singular ([#3634](https://github.com/rudderlabs/rudder-transformer/issues/3634)) ([12996d7](https://github.com/rudderlabs/rudder-transformer/commit/12996d7a7ce23de7c150c1c1e012d4dda8668977)) +* onboard shopify to v1 ([#3665](https://github.com/rudderlabs/rudder-transformer/issues/3665)) ([d40e772](https://github.com/rudderlabs/rudder-transformer/commit/d40e772f1a3741c1c4e9ab2365ed464b3988812e)) + + +### Bug Fixes + +* add correct validation for purchase events ([#3766](https://github.com/rudderlabs/rudder-transformer/issues/3766)) ([9cc72f2](https://github.com/rudderlabs/rudder-transformer/commit/9cc72f2288f99ee394977ffeb209faaae657f6d2)) +* braze include fields_to_export to lookup users ([#3761](https://github.com/rudderlabs/rudder-transformer/issues/3761)) ([173b989](https://github.com/rudderlabs/rudder-transformer/commit/173b9895fb2a0bed615f6e3a9c670abe42d5754f)) +* correct typo for order fulfillment event, add test ([#3764](https://github.com/rudderlabs/rudder-transformer/issues/3764)) ([6f92bd3](https://github.com/rudderlabs/rudder-transformer/commit/6f92bd31b60caaa07d18bb86ce5939cd7cc9a416)) +* fixing lytics user_id and anonymousId mapping ([#3745](https://github.com/rudderlabs/rudder-transformer/issues/3745)) ([45b1067](https://github.com/rudderlabs/rudder-transformer/commit/45b1067d81f3883e19d35634ffec52434fef452f)) +* npm start command to include exec ([9f5140b](https://github.com/rudderlabs/rudder-transformer/commit/9f5140b194384295c0a56147fed16273b2b7805b)) +* payment info entered event in facebook_conversions ([#3762](https://github.com/rudderlabs/rudder-transformer/issues/3762)) ([7fa7c8d](https://github.com/rudderlabs/rudder-transformer/commit/7fa7c8d3a4f6aefb580cf0de2e64e2f8aef5b5ce)) +* posthog alias mapping swap ([#3765](https://github.com/rudderlabs/rudder-transformer/issues/3765)) ([b6240d0](https://github.com/rudderlabs/rudder-transformer/commit/b6240d06a9d1f7f3bc8f245807f72a72ab40f170)), closes [#3507](https://github.com/rudderlabs/rudder-transformer/issues/3507) + +## [1.80.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.79.1...v1.80.0) (2024-09-30) + + +### Features + +* add unity source support in Singular ([#3634](https://github.com/rudderlabs/rudder-transformer/issues/3634)) ([12996d7](https://github.com/rudderlabs/rudder-transformer/commit/12996d7a7ce23de7c150c1c1e012d4dda8668977)) +* onboard shopify to v1 ([#3665](https://github.com/rudderlabs/rudder-transformer/issues/3665)) ([d40e772](https://github.com/rudderlabs/rudder-transformer/commit/d40e772f1a3741c1c4e9ab2365ed464b3988812e)) + + +### Bug Fixes + +* add correct validation for purchase events ([#3766](https://github.com/rudderlabs/rudder-transformer/issues/3766)) ([9cc72f2](https://github.com/rudderlabs/rudder-transformer/commit/9cc72f2288f99ee394977ffeb209faaae657f6d2)) +* braze include fields_to_export to lookup users ([#3761](https://github.com/rudderlabs/rudder-transformer/issues/3761)) ([173b989](https://github.com/rudderlabs/rudder-transformer/commit/173b9895fb2a0bed615f6e3a9c670abe42d5754f)) +* correct typo for order fulfillment event, add test ([#3764](https://github.com/rudderlabs/rudder-transformer/issues/3764)) ([6f92bd3](https://github.com/rudderlabs/rudder-transformer/commit/6f92bd31b60caaa07d18bb86ce5939cd7cc9a416)) +* fixing lytics user_id and anonymousId mapping ([#3745](https://github.com/rudderlabs/rudder-transformer/issues/3745)) ([45b1067](https://github.com/rudderlabs/rudder-transformer/commit/45b1067d81f3883e19d35634ffec52434fef452f)) +* payment info entered event in facebook_conversions ([#3762](https://github.com/rudderlabs/rudder-transformer/issues/3762)) ([7fa7c8d](https://github.com/rudderlabs/rudder-transformer/commit/7fa7c8d3a4f6aefb580cf0de2e64e2f8aef5b5ce)) +* posthog alias mapping swap ([#3765](https://github.com/rudderlabs/rudder-transformer/issues/3765)) ([b6240d0](https://github.com/rudderlabs/rudder-transformer/commit/b6240d06a9d1f7f3bc8f245807f72a72ab40f170)), closes [#3507](https://github.com/rudderlabs/rudder-transformer/issues/3507) + +### [1.79.1](https://github.com/rudderlabs/rudder-transformer/compare/v1.79.0...v1.79.1) (2024-09-24) + + +### Bug Fixes + +* allow users context traits and underscore divide numbers configuration ([#3752](https://github.com/rudderlabs/rudder-transformer/issues/3752)) ([386d2ab](https://github.com/rudderlabs/rudder-transformer/commit/386d2ab88c0fe72dc47ba119be08ad1c0cd6d51b)) +* populate users fields for sentAt, timestamp and originalTimestamp ([#3753](https://github.com/rudderlabs/rudder-transformer/issues/3753)) ([f50effe](https://github.com/rudderlabs/rudder-transformer/commit/f50effeeabdb888f82451c225a80971dbe6532b6)) +* prefer event check vs config check for vdm ([#3754](https://github.com/rudderlabs/rudder-transformer/issues/3754)) ([b2c1a18](https://github.com/rudderlabs/rudder-transformer/commit/b2c1a1893dfb957ac7a24c000b33cd254ef54b6c)) +* support different lookup fields and custom_attributes for rETL events ([#3751](https://github.com/rudderlabs/rudder-transformer/issues/3751)) ([10d914e](https://github.com/rudderlabs/rudder-transformer/commit/10d914e25203bd6ae95801c2a98c17690bd2d6ef)) + ## [1.79.0](https://github.com/rudderlabs/rudder-transformer/compare/v1.78.0...v1.79.0) (2024-09-20) diff --git a/package-lock.json b/package-lock.json index 98bc41379e..d3b578f685 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rudder-transformer", - "version": "1.79.0", + "version": "1.82.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rudder-transformer", - "version": "1.79.0", + "version": "1.82.1", "license": "ISC", "dependencies": { "@amplitude/ua-parser-js": "0.7.24", @@ -26,6 +26,7 @@ "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", + "amazon-dsp-formatter": "^1.0.2", "axios": "^1.7.3", "btoa": "^1.2.1", "component-each": "^0.2.6", @@ -46,7 +47,7 @@ "json-size": "^1.0.0", "jsontoxml": "^1.0.1", "jstoxml": "^5.0.2", - "koa": "^2.14.1", + "koa": "^2.15.3", "koa-bodyparser": "^4.4.0", "koa2-swagger-ui": "^5.7.0", "libphonenumber-js": "^1.11.1", @@ -82,7 +83,7 @@ "@types/fast-json-stable-stringify": "^2.1.0", "@types/jest": "^29.5.1", "@types/jsonpath": "^0.2.4", - "@types/koa": "^2.13.6", + "@types/koa": "^2.15.0", "@types/koa-bodyparser": "^4.3.10", "@types/lodash": "^4.14.197", "@types/node": "^20.2.5", @@ -7790,8 +7791,9 @@ "license": "MIT" }, "node_modules/@types/koa": { - "version": "2.13.12", - "license": "MIT", + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", "dependencies": { "@types/accepts": "*", "@types/content-disposition": "*", @@ -8197,6 +8199,11 @@ } } }, + "node_modules/amazon-dsp-formatter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/amazon-dsp-formatter/-/amazon-dsp-formatter-1.0.2.tgz", + "integrity": "sha512-CfsssMzLFh0IK6oz3j38ENGgp5LZ/q21YX4yXSavfI50CU2cJbupKOk+Bgg0sY67V0lWsYsmYrpkEI2aFG/duA==" + }, "node_modules/ansi-align": { "version": "3.0.1", "dev": true, @@ -16321,8 +16328,9 @@ } }, "node_modules/koa": { - "version": "2.15.0", - "license": "MIT", + "version": "2.15.3", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.15.3.tgz", + "integrity": "sha512-j/8tY9j5t+GVMLeioLaxweJiKUayFhlGqNTzf2ZGwL0ZCQijd2RLHK0SLW5Tsko8YyyqCZC2cojIb0/s62qTAg==", "dependencies": { "accepts": "^1.3.5", "cache-content-type": "^1.0.0", diff --git a/package.json b/package.json index c598db805c..f5d44d8e2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "rudder-transformer", - "version": "1.79.0", + "version": "1.82.1", "description": "", "homepage": "https://github.com/rudderlabs/rudder-transformer#readme", "bugs": { @@ -23,7 +23,7 @@ "lint:fix:json": "eslint --ext .json --fix .", "lint": "npm run format && npm run lint:fix", "check:merge": "npm run verify || exit 1; codecov", - "start": "cd dist;node ./src/index.js;cd ..", + "start": "cd dist;exec node ./src/index.js;cd ..", "build:start": "npm run build && npm run start", "build:ci": "tsc -p tsconfig.json", "build:swagger": "npm run build && npm run setup:swagger", @@ -71,6 +71,7 @@ "ajv": "^8.12.0", "ajv-draft-04": "^1.0.0", "ajv-formats": "^2.1.1", + "amazon-dsp-formatter": "^1.0.2", "axios": "^1.7.3", "btoa": "^1.2.1", "component-each": "^0.2.6", @@ -91,7 +92,7 @@ "json-size": "^1.0.0", "jsontoxml": "^1.0.1", "jstoxml": "^5.0.2", - "koa": "^2.14.1", + "koa": "^2.15.3", "koa-bodyparser": "^4.4.0", "koa2-swagger-ui": "^5.7.0", "libphonenumber-js": "^1.11.1", @@ -127,7 +128,7 @@ "@types/fast-json-stable-stringify": "^2.1.0", "@types/jest": "^29.5.1", "@types/jsonpath": "^0.2.4", - "@types/koa": "^2.13.6", + "@types/koa": "^2.15.0", "@types/koa-bodyparser": "^4.3.10", "@types/lodash": "^4.14.197", "@types/node": "^20.2.5", diff --git a/src/cdk/v2/destinations/bluecore/utils.js b/src/cdk/v2/destinations/bluecore/utils.js index 91eda60d0d..543b6de745 100644 --- a/src/cdk/v2/destinations/bluecore/utils.js +++ b/src/cdk/v2/destinations/bluecore/utils.js @@ -46,12 +46,12 @@ const verifyPayload = (payload, message) => { } break; case 'purchase': - if (!payload?.properties?.order_id) { + if (!isDefinedAndNotNull(payload?.properties?.order_id)) { throw new InstrumentationError( '[Bluecore] property:: order_id is required for purchase event', ); } - if (!payload?.properties?.total) { + if (!isDefinedAndNotNull(payload?.properties?.total)) { throw new InstrumentationError( '[Bluecore] property:: total is required for purchase event', ); diff --git a/src/cdk/v2/destinations/heap/procWorkflow.yaml b/src/cdk/v2/destinations/heap/procWorkflow.yaml index ac12e7e02a..6e038bc66d 100644 --- a/src/cdk/v2/destinations/heap/procWorkflow.yaml +++ b/src/cdk/v2/destinations/heap/procWorkflow.yaml @@ -22,7 +22,7 @@ steps: $.assert(.message.traits ?? .message.context.traits, "traits are required"); $.context.endpoint = 'https://heapanalytics.com/api/add_user_properties'; $.context.payload = .message.({ - identity: {{{{$.getGenericPaths("userId")}}}}, + identity: {{{{$.getGenericPaths("userId", "||")}}}}, properties: $.flattenJson(.traits ?? .context.traits){~["idempotencyKey"]}, app_id: ^.destination.Config.appId }) @@ -32,7 +32,7 @@ steps: $.assert(.message.event, "event is required"); $.context.endpoint = 'https://heapanalytics.com/api/track'; $.context.payload = .message.({ - identity: {{{{$.getGenericPaths("userId")}}}}, + identity: {{{{$.getGenericPaths("userId", "||")}}}}, properties: $.flattenJson(.properties){~["idempotencyKey"]}, app_id: ^.destination.Config.appId, event: .event, diff --git a/src/cdk/v2/destinations/intercom/procWorkflow.yaml b/src/cdk/v2/destinations/intercom/procWorkflow.yaml index 0f2ac18fbc..db1ed02d57 100644 --- a/src/cdk/v2/destinations/intercom/procWorkflow.yaml +++ b/src/cdk/v2/destinations/intercom/procWorkflow.yaml @@ -77,8 +77,8 @@ steps: template: | const payload = .message.context.mappedToDestination ? $.outputs.rEtlPayload : $.outputs.identifyTransformationForLatestVersion; payload.name = $.getName(.message); - payload.custom_attributes = .message.context.traits || {}; - payload.custom_attributes = $.filterCustomAttributes(payload, "user", .destination); + payload.custom_attributes = (.message.context.mappedToDestination ? .message.traits.custom_attributes : .message.context.traits) || {}; + payload.custom_attributes = $.filterCustomAttributes(payload, "user", .destination, .message); payload.external_id = !payload.external_id && .destination.Config.sendAnonymousId && .message.anonymousId ? .message.anonymousId : payload.external_id; $.context.payload = payload; $.assert($.context.payload.external_id || $.context.payload.email, "Either email or userId is required for Identify call"); @@ -114,7 +114,7 @@ steps: update_last_request_at: typeof .destination.Config.updateLastRequestAt === 'boolean' ? .destination.Config.updateLastRequestAt : true } payload.companies = $.getCompaniesList(payload); - payload.custom_attributes = !.message.context.mappedToDestination ? $.filterCustomAttributes(payload, "user", .destination); + payload.custom_attributes = !.message.context.mappedToDestination ? $.filterCustomAttributes(payload, "user", .destination,.message); payload.user_id = !payload.user_id && .destination.Config.sendAnonymousId && .message.anonymousId ? .message.anonymousId : payload.user_id; $.context.payload = payload; $.assert($.context.payload.user_id || $.context.payload.email, "Either of `email` or `userId` is required for Identify call"); @@ -175,7 +175,7 @@ steps: $.assert(.message.groupId, "groupId is required for group call"); const payload = .message.context.mappedToDestination ? $.outputs.rEtlPayload : $.outputs.groupTransformation; payload.custom_attributes = .message.traits || {}; - payload.custom_attributes = $.filterCustomAttributes(payload, "company", .destination); + payload.custom_attributes = $.filterCustomAttributes(payload, "company", .destination,.message); $.context.payload = payload; - name: whenSearchContactFound condition: $.isDefinedAndNotNull($.outputs.searchContact) @@ -214,7 +214,7 @@ steps: ...payload, custom_attributes : $.getFieldValueFromMessage(.message, "traits") || {} } - payload.custom_attributes = $.filterCustomAttributes(payload, "company", .destination); + payload.custom_attributes = $.filterCustomAttributes(payload, "company", .destination, .message); response.body.JSON = $.removeUndefinedAndNullValues(payload); response.endpoint = $.getBaseEndpoint(.destination) + "/" + "companies"; response.headers = $.getHeaders(.destination, $.outputs.apiVersion); diff --git a/src/cdk/v2/destinations/intercom/utils.js b/src/cdk/v2/destinations/intercom/utils.js index dc483e040b..a75f8155af 100644 --- a/src/cdk/v2/destinations/intercom/utils.js +++ b/src/cdk/v2/destinations/intercom/utils.js @@ -233,25 +233,31 @@ const attachUserAndCompany = (message, Config) => { * @param {*} type * @returns */ -const filterCustomAttributes = (payload, type, destination) => { +const filterCustomAttributes = (payload, type, destination, message) => { let ReservedAttributesList; let { apiVersion } = destination.Config; apiVersion = isDefinedAndNotNull(apiVersion) ? apiVersion : 'v2'; + // we are discarding the lookup field from custom attributes + const lookupField = getLookUpField(message); if (type === 'user') { - ReservedAttributesList = - apiVersion === 'v1' + ReservedAttributesList = [ + ...(apiVersion === 'v1' ? ReservedAttributes.v1UserAttributes - : ReservedAttributes.v2UserAttributes; + : ReservedAttributes.v2UserAttributes), + lookupField, + ]; } else { - ReservedAttributesList = - apiVersion === 'v1' + ReservedAttributesList = [ + ...(apiVersion === 'v1' ? ReservedAttributes.v1CompanyAttributes - : ReservedAttributes.v2CompanyAttributes; + : ReservedAttributes.v2CompanyAttributes), + lookupField !== 'email' && lookupField, + ]; } let customAttributes = { ...get(payload, 'custom_attributes') }; if (customAttributes) { ReservedAttributesList.forEach((trait) => { - if (customAttributes[trait]) delete customAttributes[trait]; + if (trait in customAttributes) delete customAttributes[trait]; }); if (isDefinedAndNotNull(customAttributes) && Object.keys(customAttributes).length > 0) { customAttributes = @@ -270,7 +276,10 @@ const filterCustomAttributes = (payload, type, destination) => { */ const searchContact = async (message, destination, metadata) => { const lookupField = getLookUpField(message); - const lookupFieldValue = getFieldValueFromMessage(message, lookupField); + let lookupFieldValue = getFieldValueFromMessage(message, lookupField); + if (!lookupFieldValue) { + lookupFieldValue = message?.context?.traits?.[lookupField]; + } const data = JSON.stringify({ query: { operator: 'AND', diff --git a/src/cdk/v2/destinations/intercom/utils.test.js b/src/cdk/v2/destinations/intercom/utils.test.js index c2bf3f8e89..e2c1fb9a07 100644 --- a/src/cdk/v2/destinations/intercom/utils.test.js +++ b/src/cdk/v2/destinations/intercom/utils.test.js @@ -318,6 +318,18 @@ describe('filterCustomAttributes utility test', () => { expect(result).toBeUndefined(); }); + it('Should filter out custom attributes that are reserved attributes and that are false', () => { + const payload = { custom_attributes: { unsubscribedFromEmails: false } }; + const result = filterCustomAttributes(payload, 'user', { Config: { apiVersion: 'v2' } }); + expect(result).toBeUndefined(); + }); + + it('Should filter out custom attributes that are reserved attributes and that are null', () => { + const payload = { custom_attributes: { unsubscribedFromEmails: null } }; + const result = filterCustomAttributes(payload, 'user', { Config: { apiVersion: 'v2' } }); + expect(result).toBeUndefined(); + }); + it('Should return a flattened object when custom attributes are not null, not reserved attributes and nested', () => { const payload = { custom_attributes: { source: 'rudder-js-sdk', data: { nestedAttribute: 'nestedValue' } }, diff --git a/src/cdk/v2/destinations/lytics/config.ts b/src/cdk/v2/destinations/lytics/config.ts new file mode 100644 index 0000000000..6756834caa --- /dev/null +++ b/src/cdk/v2/destinations/lytics/config.ts @@ -0,0 +1,9 @@ +import { getMappingConfig } from '../../../../v0/util'; + +const CONFIG_CATEGORIES = { + CUSTOMER_PROPERTIES_CONFIG: { name: 'LYTICSIdentifyConfig' }, +}; + +const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); +export const CUSTOMER_PROPERTIES_CONFIG = + MAPPING_CONFIG[CONFIG_CATEGORIES.CUSTOMER_PROPERTIES_CONFIG.name]; diff --git a/src/cdk/v2/destinations/lytics/data/LYTICSIdentifyConfig.json b/src/cdk/v2/destinations/lytics/data/LYTICSIdentifyConfig.json new file mode 100644 index 0000000000..c1996d34f7 --- /dev/null +++ b/src/cdk/v2/destinations/lytics/data/LYTICSIdentifyConfig.json @@ -0,0 +1,25 @@ +[ + { + "destKey": "user_id", + "sourceKeys": "userIdOnly", + "sourceFromGenericMap": true, + "required": false + }, + { + "destKey": "anonymous_id", + "sourceKeys": "anonymousId", + "required": false + }, + { + "destKey": "first_name", + "sourceKeys": "firstName", + "sourceFromGenericMap": true, + "required": false + }, + { + "destKey": "last_name", + "sourceKeys": "lastName", + "sourceFromGenericMap": true, + "required": false + } +] diff --git a/src/cdk/v2/destinations/lytics/procWorkflow.yaml b/src/cdk/v2/destinations/lytics/procWorkflow.yaml index 2622146221..834493e79c 100644 --- a/src/cdk/v2/destinations/lytics/procWorkflow.yaml +++ b/src/cdk/v2/destinations/lytics/procWorkflow.yaml @@ -5,9 +5,12 @@ bindings: path: ../../../../v0/util - name: removeUndefinedAndNullValues path: ../../../../v0/util + - name: constructPayload + path: ../../../../v0/util - path: ../../bindings/jsontemplate - name: defaultRequestConfig path: ../../../../v0/util + - path: ./config steps: - name: validateInput @@ -24,20 +27,19 @@ steps: condition: $.context.messageType === {{$.EventType.IDENTIFY}} template: | const flattenTraits = $.flattenJson(.message.traits ?? .message.context.traits); - $.context.payload = .message.({ + const payload = $.constructPayload(.message, $.CUSTOMER_PROPERTIES_CONFIG); + $.context.payload = { ...flattenTraits, - first_name: {{{{$.getGenericPaths("firstName")}}}}, - last_name: {{{{$.getGenericPaths("lastName")}}}}, - user_id: {{{{$.getGenericPaths("userId")}}}} - }) + ...payload, + } else: name: payloadForOthers template: | const flattenProperties = $.flattenJson(.message.properties); + const customerPropertiesInfo = $.constructPayload(.message, $.CUSTOMER_PROPERTIES_CONFIG); $.context.payload = .message.({ ...flattenProperties, - first_name: .properties.firstName ?? .properties.firstname, - last_name: .properties.lastName ?? .properties.lastname + ...customerPropertiesInfo }) - name: trackPayload condition: $.context.messageType === {{$.EventType.TRACK}} diff --git a/src/cdk/v2/destinations/rakuten/utils.js b/src/cdk/v2/destinations/rakuten/utils.js index 2dd628a250..b4897c46ed 100644 --- a/src/cdk/v2/destinations/rakuten/utils.js +++ b/src/cdk/v2/destinations/rakuten/utils.js @@ -17,6 +17,18 @@ const constructProperties = (message) => { return payload; }; +/** + * Calculates the amount for a single product + * @param {Object} product + * @returns {number} + */ +const calculateProductAmount = (product) => { + if (!product?.amount && !product?.price) { + throw new InstrumentationError('Either amount or price is required for every product'); + } + return Math.round(product.amount * 100 || (product.quantity || 1) * 100 * product.price); +}; + /** * This fucntion build the item level list * @param {*} properties @@ -52,14 +64,8 @@ const constructLineItems = (properties) => { }); // Map 'amountList' by evaluating 'amount' or deriving it from 'price' and 'quantity' - const amountList = products.map((product) => { - if (!product?.amount && !product?.price) { - throw new InstrumentationError('Either amount or price is required for every product'); - } - return product.amount * 100 || (product.quantity || 1) * 100 * product.price; - }); - productList.amtlist = amountList.join('|'); + productList.amtlist = products.map(calculateProductAmount).join('|'); return productList; }; -module.exports = { constructProperties, constructLineItems }; +module.exports = { constructProperties, constructLineItems, calculateProductAmount }; diff --git a/src/cdk/v2/destinations/rakuten/utils.test.js b/src/cdk/v2/destinations/rakuten/utils.test.js index 9cc7f5fd4c..2d82037b1c 100644 --- a/src/cdk/v2/destinations/rakuten/utils.test.js +++ b/src/cdk/v2/destinations/rakuten/utils.test.js @@ -1,4 +1,5 @@ -const { constructLineItems } = require('./utils'); +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { constructLineItems, calculateProductAmount } = require('./utils'); describe('constructLineItems', () => { it('should return a non-empty object when given a valid properties object with at least one product', () => { const properties = { @@ -115,3 +116,62 @@ describe('constructLineItems', () => { ); }); }); + +describe('calculateProductAmount', () => { + // Calculates product amount correctly when amount is defined + it('should return the correct product amount when amount is defined', () => { + const product = { amount: 5 }; + const result = calculateProductAmount(product); + expect(result).toBe(500); + }); + + // Throws error when both amount and price are undefined or null + it('should throw an error when both amount and price are undefined or null', () => { + const product = {}; + expect(() => calculateProductAmount(product)).toThrow(InstrumentationError); + }); + + // Calculates product amount correctly when price and quantity are defined + it('should calculate product amount correctly when price and quantity are defined', () => { + const product = { amount: 10, price: 5, quantity: 2 }; + const result = calculateProductAmount(product); + expect(result).toEqual(1000); + }); + + // Returns correct value when only price is defined and quantity defaults to 1 + it('should return correct value when only price is defined and quantity defaults to 1', () => { + const product = { price: 20 }; + const result = calculateProductAmount(product); + expect(result).toEqual(2000); + }); + + // Handles cases where amount is a floating-point number + it('should handle cases where amount is a floating-point number', () => { + const product = { amount: 5.5, price: 10, quantity: 2 }; + const result = calculateProductAmount(product); + expect(result).toEqual(550); + }); + + it('should handle cases where amount is a floating-point number', () => { + const product = { amount: 5.1, price: 10, quantity: 2 }; + const result = calculateProductAmount(product); + expect(result).toEqual(510); + }); + + it('should handle cases where amount is a floating-point number', () => { + const product = { amount: 5.19, price: 10, quantity: 2 }; + const result = calculateProductAmount(product); + expect(result).toEqual(519); + }); + + it('should handle cases where amount is a floating-point number', () => { + const product = { amount: 5.199, price: 10, quantity: 2 }; + const result = calculateProductAmount(product); + expect(result).toEqual(520); + }); + it('should handle cases where amount is a floating-point number', () => { + const product = { amount: 5.479, price: 10, quantity: 2 }; + const result = calculateProductAmount(product); + expect(result).toEqual(548); + }); +}); diff --git a/src/cdk/v2/destinations/webhook/procWorkflow.yaml b/src/cdk/v2/destinations/webhook/procWorkflow.yaml index 950af46b96..bfe7bd4c13 100644 --- a/src/cdk/v2/destinations/webhook/procWorkflow.yaml +++ b/src/cdk/v2/destinations/webhook/procWorkflow.yaml @@ -27,7 +27,7 @@ steps: template: | let defaultHeaders = $.context.method in ['POST', 'PUT', 'PATCH'] ? {"content-type": "application/json"} : {} let configHeaders = $.getHashFromArray(.destination.Config.headers) - let messageHeader = typeof .message.header === "object" ? Object.assign(...Object.entries(.message.header).({[.[0]]:typeof .[1] === 'string' ? .[1] : JSON.stringify(.[1])})[]) : {} + let messageHeader = typeof .message.header === "object" ? Object.assign({}, ...Object.entries(.message.header).({[.[0]]:typeof .[1] === 'string' ? .[1] : JSON.stringify(.[1])})[]) : {} $.context.finalHeaders = { ...defaultHeaders, ...configHeaders, diff --git a/src/constants/destinationCanonicalNames.js b/src/constants/destinationCanonicalNames.js index f99c735e45..e9b7dc136b 100644 --- a/src/constants/destinationCanonicalNames.js +++ b/src/constants/destinationCanonicalNames.js @@ -1,6 +1,7 @@ const DestHandlerMap = { ga360: 'ga', salesforce_oauth: 'salesforce', + salesforce_oauth_sandbox: 'salesforce', }; const DestCanonicalNames = { diff --git a/src/features.json b/src/features.json index 097e4a8aa0..63862eefed 100644 --- a/src/features.json +++ b/src/features.json @@ -26,6 +26,7 @@ "PROFITWELL": true, "SALESFORCE": true, "SALESFORCE_OAUTH": true, + "SALESFORCE_OAUTH_SANDBOX": true, "SFMC": true, "SNAPCHAT_CONVERSION": true, "TIKTOK_ADS": true, @@ -80,7 +81,9 @@ "X_AUDIENCE": true, "BLOOMREACH_CATALOG": true, "SMARTLY": true, - "HTTP": true + "HTTP": true, + "AMAZON_AUDIENCE": true, + "INTERCOM_V2": true }, "regulations": [ "BRAZE", diff --git a/src/v0/destinations/af/transform.js b/src/v0/destinations/af/transform.js index a611dcc249..064cf003a8 100644 --- a/src/v0/destinations/af/transform.js +++ b/src/v0/destinations/af/transform.js @@ -1,6 +1,6 @@ const get = require('get-value'); const set = require('set-value'); - +const lodash = require('lodash'); const { InstrumentationError, ConfigurationError } = require('@rudderstack/integrations-lib'); const { EventType } = require('../../../constants'); const { @@ -130,28 +130,29 @@ function getEventValueForUnIdentifiedTrackEvent(message) { function getEventValueMapFromMappingJson(message, mappingJson, isMultiSupport, config) { let eventValue = {}; - const { addPropertiesAtRoot, afCurrencyAtRoot } = config; - + const { addPropertiesAtRoot, afCurrencyAtRoot, listOfProps } = config; + const clonedProp = message.properties && lodash.cloneDeep(message.properties); if (addPropertiesAtRoot) { - eventValue = message.properties; + eventValue = clonedProp; } else { - set(eventValue, 'properties', message.properties); + if (Array.isArray(listOfProps) && listOfProps.length > 0) { + listOfProps.forEach((prop) => { + eventValue[prop.property] = clonedProp[prop.property]; + delete clonedProp[prop.property]; + }); + } + eventValue.properties = clonedProp; } const sourceKeys = Object.keys(mappingJson); sourceKeys.forEach((sourceKey) => { set(eventValue, mappingJson[sourceKey], get(message, sourceKey)); }); - if ( - isMultiSupport && - message.properties && - message.properties.products && - message.properties.products.length > 0 - ) { + if (isMultiSupport && clonedProp && clonedProp.products && clonedProp.products.length > 0) { const contentIds = []; const quantities = []; const prices = []; - message.properties.products.forEach((product) => { + clonedProp.products.forEach((product) => { contentIds.push(product.product_id); quantities.push(product.quantity); prices.push(product.price); @@ -164,7 +165,7 @@ function getEventValueMapFromMappingJson(message, mappingJson, isMultiSupport, c }; } if (afCurrencyAtRoot) { - eventValue.af_currency = message.properties.currency; + eventValue.af_currency = clonedProp.currency; } eventValue = removeUndefinedValues(eventValue); if (Object.keys(eventValue).length > 0) { diff --git a/src/v0/destinations/amazon_audience/config.js b/src/v0/destinations/amazon_audience/config.js new file mode 100644 index 0000000000..f377bceaae --- /dev/null +++ b/src/v0/destinations/amazon_audience/config.js @@ -0,0 +1,5 @@ +const CREATE_USERS_URL = 'https://advertising-api.amazon.com/dp/records/hashed/'; +const ASSOCIATE_USERS_URL = 'https://advertising-api.amazon.com/v2/dp/audience'; +const MAX_PAYLOAD_SIZE_IN_BYTES = 4000000; +const DESTINATION = 'amazon_audience'; +module.exports = { CREATE_USERS_URL, MAX_PAYLOAD_SIZE_IN_BYTES, ASSOCIATE_USERS_URL, DESTINATION }; diff --git a/src/v0/destinations/amazon_audience/networkHandler.js b/src/v0/destinations/amazon_audience/networkHandler.js new file mode 100644 index 0000000000..c1fbeeccea --- /dev/null +++ b/src/v0/destinations/amazon_audience/networkHandler.js @@ -0,0 +1,139 @@ +const { + NetworkError, + ThrottledError, + AbortedError, + RetryableError, +} = require('@rudderstack/integrations-lib'); +const { prepareProxyRequest, handleHttpRequest } = require('../../../adapters/network'); +const { isHttpStatusSuccess } = require('../../util/index'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { REFRESH_TOKEN } = require('../../../adapters/networkhandler/authConstants'); +const { DESTINATION, CREATE_USERS_URL, ASSOCIATE_USERS_URL } = require('./config'); +const { TAG_NAMES } = require('../../util/tags'); + +const amazonAudienceRespHandler = (destResponse, stageMsg) => { + const { status, response } = destResponse; + + // to handle the case when authorization-token is invalid + // docs for error codes: https://advertising.amazon.com/API/docs/en-us/reference/concepts/errors#tag/Audiences/operation/dspCreateAudiencesPost + if (status === 401 && response.message === 'Unauthorized') { + // 401 takes place in case of authorization isue meaning token is epxired or access is not enough. + // Since acces is configured from dashboard only refresh token makes sense + throw new NetworkError( + `${response?.message} ${stageMsg}`, + status, + { + [TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + response, + REFRESH_TOKEN, + ); + } else if (status === 429) { + throw new ThrottledError( + `Request Failed: ${stageMsg} - due to Request Limit exceeded, (Throttled)`, + destResponse, + ); + } else if (status === 504 || status === 502 || status === 500) { + // see if its 5xx internal error + throw new RetryableError(`Request Failed: ${stageMsg} (Retryable)`, 500, destResponse); + } + // else throw the error + throw new AbortedError( + `Request Failed: ${stageMsg} with status "${status}" due to "${JSON.stringify( + response, + )}", (Aborted) `, + 400, + destResponse, + ); +}; + +const responseHandler = (responseParams) => { + const { destinationResponse } = responseParams; + const message = `[${DESTINATION} Response Handler] - Request Processed Successfully`; + const { status } = destinationResponse; + + if (!isHttpStatusSuccess(status)) { + // if error, successfully return status, message and original destination response + amazonAudienceRespHandler( + destinationResponse, + 'during amazon_audience response transformation', + ); + } + return { + status, + message, + destinationResponse, + }; +}; + +const makeRequest = async (url, data, headers, metadata, method, args) => { + const { httpResponse } = await handleHttpRequest(method, url, data, { headers }, args); + return httpResponse; +}; + +const amazonAudienceProxyRequest = async (request) => { + const { body, metadata } = request; + const { headers } = request; + const { createUsers, associateUsers } = body.JSON; + + // step 1: Create users + const firstResponse = await makeRequest( + CREATE_USERS_URL, + createUsers, + headers, + metadata, + 'post', + { + destType: 'amazon_audience', + feature: 'proxy', + requestMethod: 'POST', + module: 'dataDelivery', + endpointPath: '/records/hashed', + metadata, + }, + ); + // validate response success + if (!firstResponse.success && !isHttpStatusSuccess(firstResponse?.response?.status)) { + amazonAudienceRespHandler( + { + response: firstResponse.response?.response?.data || firstResponse, + status: firstResponse.response?.response?.status || firstResponse, + }, + 'during creating users', + ); + } + // we are returning above in case of failure because if first step is not executed then there is no sense of executing second step + // step2: Associate Users to Audience Id + const secondResponse = await makeRequest( + ASSOCIATE_USERS_URL, + associateUsers, + headers, + metadata, + 'patch', + { + destType: 'amazon_audience', + feature: 'proxy', + requestMethod: 'PATCH', + module: 'dataDelivery', + endpointPath: '/v2/dp/audience', + metadata, + }, + ); + return secondResponse; +}; +// eslint-disable-next-line @typescript-eslint/naming-convention +class networkHandler { + constructor() { + this.responseHandler = responseHandler; + this.proxy = amazonAudienceProxyRequest; + this.prepareProxy = prepareProxyRequest; + this.processAxiosResponse = processAxiosResponse; + } +} + +module.exports = { + networkHandler, +}; diff --git a/src/v0/destinations/amazon_audience/transform.js b/src/v0/destinations/amazon_audience/transform.js new file mode 100644 index 0000000000..834810de54 --- /dev/null +++ b/src/v0/destinations/amazon_audience/transform.js @@ -0,0 +1,58 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { handleRtTfSingleEventError } = require('../../util'); +const { batchEvents, buildResponseWithUsers, getUserDetails } = require('./utils'); +/** + * This function returns the user traits list required in request for + * making a call to create a group of users in amazon_audience + * @param {*} record + * @param {*} destination + * @param {*} metadata + */ +const processRecord = (record, config) => { + const { fields, action, type } = record; + if (type !== 'record') { + throw new InstrumentationError(`[AMAZON AUDIENCE]: ${type} is not supported`); + } + return { user: getUserDetails(fields, config), action: action !== 'delete' ? 'add' : 'remove' }; +}; + +// This function is used to process a single record +const process = (event) => { + const { message, destination, metadata } = event; + const { Config } = destination; + const { user, action } = processRecord(message, Config); + return buildResponseWithUsers([user], action, Config, [metadata.jobId], metadata.secret); +}; +// This function is used to process multiple records +const processRouterDest = async (inputs, reqMetadata) => { + const responseList = []; // list containing all successful responses + const errorRespList = []; // list of error + const { destination } = inputs[0]; + const { Config } = destination; + inputs.map(async (event) => { + try { + if (event.message.statusCode) { + // already transformed event + responseList.push(event); + } else { + // if not transformed + responseList.push({ + message: processRecord(event.message, Config), + metadata: event.metadata, + destination, + }); + } + } catch (error) { + const errRespEvent = handleRtTfSingleEventError(event, error, reqMetadata); + errorRespList.push(errRespEvent); + } + }); + let batchedResponseList = []; + if (responseList.length > 0) { + batchedResponseList = batchEvents(responseList, destination); + } + return [...batchedResponseList, ...errorRespList]; +}; + +module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/amazon_audience/utils.js b/src/v0/destinations/amazon_audience/utils.js new file mode 100644 index 0000000000..c25f301378 --- /dev/null +++ b/src/v0/destinations/amazon_audience/utils.js @@ -0,0 +1,175 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const sha256 = require('sha256'); +const AmazonAdsFormatter = require('amazon-dsp-formatter'); +const lodash = require('lodash'); +const { ConfigurationError, OAuthSecretError } = require('@rudderstack/integrations-lib'); +const { + defaultRequestConfig, + defaultPostRequestConfig, + getSuccessRespEvents, + removeUndefinedAndNullAndEmptyValues, +} = require('../../util'); + +const buildResponseWithUsers = (users, action, config, jobIdList, secret) => { + const { audienceId } = config; + if (!audienceId) { + throw new ConfigurationError('[AMAZON AUDIENCE]: Audience Id not found'); + } + if (!secret?.accessToken) { + throw new OAuthSecretError('OAuth - access token not found'); + } + if (!secret?.clientId) { + throw new OAuthSecretError('OAuth - Client Id not found'); + } + const externalId = `Rudderstack_${sha256(`${jobIdList}`)}`; + const response = defaultRequestConfig(); + response.endpoint = ''; + response.method = defaultPostRequestConfig.requestMethod; + response.headers = { + 'Amazon-Advertising-API-ClientId': `${secret.clientId}`, + 'Content-Type': 'application/json', + Authorization: `Bearer ${secret.accessToken}`, + }; + response.body.JSON = { + createUsers: { + records: [ + { + hashedRecords: users, + externalId, + }, + ], + }, + associateUsers: { + patches: [ + { + op: action, + path: `/EXTERNAL_USER_ID-${externalId}/audiences`, + value: [audienceId], + }, + ], + }, + }; + return response; +}; + +/** + * This function groups the response list based upon `operation` + * @param {*} respList + * @returns object + */ +const groupResponsesUsingOperation = (respList) => { + const eventGroups = lodash.groupBy(respList, (item) => item.message.action); + return eventGroups; +}; + +/** + * Input: [{ + message: { + users: {} + action + }, + }, + metadata, + destination, +}] + * @param {*} responseList + */ +const batchEvents = (responseList, destination) => { + const { secret } = responseList[0].metadata; + const eventGroups = groupResponsesUsingOperation(responseList); + const respList = []; + const opList = ['remove', 'add']; + opList.forEach((op) => { + if (eventGroups?.[op]) { + const { userList, jobIdList, metadataList } = eventGroups[op].reduce( + (acc, event) => ({ + userList: acc.userList.concat(event.message.user), + jobIdList: acc.jobIdList.concat(event.metadata.jobId), + metadataList: acc.metadataList.concat(event.metadata), + }), + { userList: [], metadataList: [], jobIdList: [] }, + ); + respList.push( + getSuccessRespEvents( + buildResponseWithUsers( + userList, + op, + destination.config || destination.Config, + jobIdList, + secret, + ), + metadataList, + destination, + true, + ), + ); + } + }); + return respList; +}; + +/** + * This function fetches the user details and + * hash them after normalizing if enable hash is turned on in config + * @param {*} fields + * @param {*} config + * @returns + */ +const getUserDetails = (fields, config) => { + const { enableHash } = config; + const { + email, + phone: phone_number, + firstName, + lastName, + address, + country, + city, + state, + postalCode, + } = fields; + if (!enableHash) { + return removeUndefinedAndNullAndEmptyValues({ + email, + phone: phone_number, + firstName, + lastName, + address, + country, + city, + state, + postalCode, + }); + } + // Since all fields are optional hence notusing formatRecord function from formatter but doing it for every parameter + const formatter = new AmazonAdsFormatter(); + const user = { + email: sha256(formatter.formatEmail(email)), + firstName: sha256(formatter.formatName(firstName)), + lastName: sha256(formatter.formatName(lastName)), + city: sha256(formatter.formatCity(city)), + state: sha256(formatter.formatState(state, country)), + postalCode: sha256(formatter.formatPostal(postalCode)), + }; + // formating guidelines https://advertising.amazon.com/help/GCCXMZYCK4RXWS6C + if (country) { + const country_code = formatter.formatCountry(country); + user.country = sha256(country_code); + if (phone_number) { + user.phone = sha256(formatter.formatPhone(phone_number, country_code)); + } + } + if (address) { + const formatted_address = + address + ?.normalize('NFD') + ?.replace(/[\u0300-\u036f]/g, '') + ?.trim() + ?.toLowerCase() + .replace(/[^\dA-Za-z]/g, '') || undefined; + user.address = sha256(formatter.formatAddress(formatted_address, country)); + } + return removeUndefinedAndNullAndEmptyValues(user); +}; + +module.exports = { batchEvents, getUserDetails, buildResponseWithUsers }; diff --git a/src/v0/destinations/braze/braze.util.test.js b/src/v0/destinations/braze/braze.util.test.js index 5ec48d29f1..71052f8d77 100644 --- a/src/v0/destinations/braze/braze.util.test.js +++ b/src/v0/destinations/braze/braze.util.test.js @@ -304,6 +304,20 @@ describe('dedup utility tests', () => { { external_ids: ['user1', 'user2'], user_aliases: [{ alias_name: 'user3', alias_label: 'rudder_id' }], + fields_to_export: [ + 'created_at', + 'custom_attributes', + 'dob', + 'email', + 'first_name', + 'gender', + 'home_city', + 'last_name', + 'phone', + 'time_zone', + 'external_id', + 'user_aliases', + ], }, { headers: { diff --git a/src/v0/destinations/braze/deleteUsers.js b/src/v0/destinations/braze/deleteUsers.js index 33c0f2ef7f..a83763e90d 100644 --- a/src/v0/destinations/braze/deleteUsers.js +++ b/src/v0/destinations/braze/deleteUsers.js @@ -21,11 +21,12 @@ const userDeletionHandler = async (userAttributes, config) => { } // Endpoints different for different data centers. // DOC: https://www.braze.com/docs/user_guide/administrative/access_braze/braze_instances/ + // Example Data Center: "EU-01", "US-01" let endPoint; const endpointPath = '/users/delete'; const dataCenterArr = dataCenter.trim().split('-'); if (dataCenterArr[0].toLowerCase() === 'eu') { - endPoint = 'https://rest.fra-01.braze.eu/users/delete'; + endPoint = `https://rest.fra-${dataCenterArr[1]}.braze.eu/users/delete`; } else { endPoint = `https://rest.iad-${dataCenterArr[1]}.braze.com/users/delete`; } diff --git a/src/v0/destinations/braze/util.js b/src/v0/destinations/braze/util.js index e5df75b562..6c8cf64265 100644 --- a/src/v0/destinations/braze/util.js +++ b/src/v0/destinations/braze/util.js @@ -141,19 +141,36 @@ const BrazeDedupUtility = { const identfierChunks = _.chunk(identifiers, 50); return identfierChunks; }, - + getFieldsToExport() { + return [ + 'created_at', + 'custom_attributes', + 'dob', + 'email', + 'first_name', + 'gender', + 'home_city', + 'last_name', + 'phone', + 'time_zone', + 'external_id', + 'user_aliases', + // 'country' and 'language' not needed because it is not billable so we don't use it + ]; + }, async doApiLookup(identfierChunks, { destination, metadata }) { return Promise.all( identfierChunks.map(async (ids) => { const externalIdentifiers = ids.filter((id) => id.external_id); const aliasIdentifiers = ids.filter((id) => id.alias_name !== undefined); - + const fieldsToExport = this.getFieldsToExport(); const { processedResponse: lookUpResponse } = await handleHttpRequest( 'post', `${getEndpointFromConfig(destination)}/users/export/ids`, { external_ids: externalIdentifiers.map((extId) => extId.external_id), user_aliases: aliasIdentifiers, + fields_to_export: fieldsToExport, }, { headers: { diff --git a/src/v0/destinations/facebook_conversions/utils.js b/src/v0/destinations/facebook_conversions/utils.js index 87fb0ea606..9119bfdca5 100644 --- a/src/v0/destinations/facebook_conversions/utils.js +++ b/src/v0/destinations/facebook_conversions/utils.js @@ -79,16 +79,31 @@ const validateProductSearchedData = (eventTypeCustomData) => { } }; +const getProducts = (message, category) => { + let products = message.properties?.products; + if (['product added', 'product viewed', 'products searched'].includes(category.type)) { + return [message.properties]; + } + if ( + ['payment info entered', 'product added to wishlist'].includes(category.type) && + !Array.isArray(products) + ) { + products = [message.properties]; + } + return products; +}; + const populateCustomDataBasedOnCategory = (customData, message, category, categoryToContent) => { let eventTypeCustomData = {}; if (category.name) { eventTypeCustomData = constructPayload(message, MAPPING_CONFIG[category.name]); } + const products = getProducts(message, category); switch (category.type) { case 'product list viewed': { const { contentIds, contents } = populateContentsAndContentIDs( - message.properties?.products, + products, message.properties?.quantity, ); @@ -119,9 +134,7 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego } case 'product added': case 'product viewed': - case 'products searched': - case 'payment info entered': - case 'product added to wishlist': { + case 'products searched': { const contentCategory = eventTypeCustomData.content_category; const contentType = message.properties?.content_type || @@ -131,7 +144,7 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego categoryToContent, DESTINATION.toLowerCase(), ); - const { contentIds, contents } = populateContentsAndContentIDs([message.properties]); + const { contentIds, contents } = populateContentsAndContentIDs(products); eventTypeCustomData = { ...eventTypeCustomData, content_ids: contentIds.length === 1 ? contentIds[0] : contentIds, @@ -142,10 +155,12 @@ const populateCustomDataBasedOnCategory = (customData, message, category, catego validateProductSearchedData(eventTypeCustomData); break; } + case 'payment info entered': + case 'product added to wishlist': case 'order completed': case 'checkout started': { const { contentIds, contents } = populateContentsAndContentIDs( - message.properties?.products, + products, message.properties?.quantity, message.properties?.delivery_category, ); diff --git a/src/v0/destinations/fb_custom_audience/recordTransform.js b/src/v0/destinations/fb_custom_audience/recordTransform.js index 9f48a37fca..db1fbeec59 100644 --- a/src/v0/destinations/fb_custom_audience/recordTransform.js +++ b/src/v0/destinations/fb_custom_audience/recordTransform.js @@ -269,10 +269,11 @@ const processRecordInputsV2 = (groupedRecordInputs) => { function processRecordInputs(groupedRecordInputs) { const event = groupedRecordInputs[0]; - if (isEventSentByVDMV2Flow(event)) { - return processRecordInputsV2(groupedRecordInputs); + // First check for rETL flow and second check for ES flow + if (isEventSentByVDMV1Flow(event) || !isEventSentByVDMV2Flow(event)) { + return processRecordInputsV1(groupedRecordInputs); } - return processRecordInputsV1(groupedRecordInputs); + return processRecordInputsV2(groupedRecordInputs); } module.exports = { diff --git a/src/v0/destinations/intercom_v2/config.js b/src/v0/destinations/intercom_v2/config.js new file mode 100644 index 0000000000..c7cb43b093 --- /dev/null +++ b/src/v0/destinations/intercom_v2/config.js @@ -0,0 +1,28 @@ +const { getMappingConfig } = require('../../util'); + +const destType = 'INTERCOM_V2'; + +const ApiVersions = { + v2: '2.10', +}; + +const ConfigCategory = { + IDENTIFY: { + name: 'IntercomIdentifyConfig', + }, + TRACK: { + name: 'IntercomTrackConfig', + }, + GROUP: { + name: 'IntercomGroupConfig', + }, +}; + +const MappingConfig = getMappingConfig(ConfigCategory, __dirname); + +module.exports = { + destType, + ConfigCategory, + MappingConfig, + ApiVersions, +}; diff --git a/src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json b/src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json new file mode 100644 index 0000000000..d357d2bb5d --- /dev/null +++ b/src/v0/destinations/intercom_v2/data/IntercomGroupConfig.json @@ -0,0 +1,47 @@ +[ + { + "destKey": "company_id", + "sourceKeys": "groupId", + "sourceFromGenericMap": true, + "required": true + }, + { + "destKey": "name", + "sourceKeys": "name", + "sourceFromGenericMap": true + }, + { + "destKey": "website", + "sourceKeys": "website", + "sourceFromGenericMap": true + }, + { + "destKey": "plan", + "sourceKeys": ["traits.plan", "context.traits.plan"] + }, + { + "destKey": "size", + "sourceKeys": ["traits.size", "context.traits.size"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "industry", + "sourceKeys": ["traits.industry", "context.traits.industry"] + }, + { + "destKey": "monthly_spend", + "sourceKeys": ["traits.monthlySpend", "context.traits.monthlySpend"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "remote_created_at", + "sourceKeys": ["traits.remoteCreatedAt", "context.traits.remoteCreatedAt"], + "metadata": { + "type": "secondTimestamp" + } + } +] diff --git a/src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json b/src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json new file mode 100644 index 0000000000..7ace2e030d --- /dev/null +++ b/src/v0/destinations/intercom_v2/data/IntercomIdentifyConfig.json @@ -0,0 +1,51 @@ +[ + { + "destKey": "external_id", + "sourceKeys": "userIdOnly", + "sourceFromGenericMap": true + }, + { + "destKey": "email", + "sourceKeys": "email", + "sourceFromGenericMap": true + }, + { + "destKey": "phone", + "sourceKeys": "phone", + "sourceFromGenericMap": true + }, + { + "destKey": "avatar", + "sourceKeys": "avatar", + "sourceFromGenericMap": true + }, + { + "destKey": "last_seen_at", + "sourceKeys": ["context.traits.lastSeenAt", "last_seen_at"], + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "role", + "sourceKeys": ["traits.role", "context.traits.role"] + }, + { + "destKey": "signed_up_at", + "sourceKeys": ["traits.createdAt", "context.traits.createdAt"], + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "owner_id", + "sourceKeys": ["traits.ownerId", "context.traits.ownerId"], + "metadata": { + "type": "toNumber" + } + }, + { + "destKey": "unsubscribed_from_emails", + "sourceKeys": ["traits.unsubscribedFromEmails", "context.traits.unsubscribedFromEmails"] + } +] diff --git a/src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json b/src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json new file mode 100644 index 0000000000..f5ab226cce --- /dev/null +++ b/src/v0/destinations/intercom_v2/data/IntercomTrackConfig.json @@ -0,0 +1,33 @@ +[ + { + "destKey": "user_id", + "sourceKeys": "userIdOnly", + "sourceFromGenericMap": true + }, + { + "destKey": "email", + "sourceKeys": "email", + "sourceFromGenericMap": true + }, + { + "destKey": "event_name", + "sourceKeys": "event", + "required": true + }, + { + "destKey": "created_at", + "sourceKeys": "timestamp", + "sourceFromGenericMap": true, + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "id", + "sourceKeys": ["message.properties.id", "message.traits.id"] + }, + { + "destKey": "metadata", + "sourceKeys": "properties" + } +] diff --git a/src/v0/destinations/intercom_v2/networkHandler.js b/src/v0/destinations/intercom_v2/networkHandler.js new file mode 100644 index 0000000000..3f06460588 --- /dev/null +++ b/src/v0/destinations/intercom_v2/networkHandler.js @@ -0,0 +1,67 @@ +const { RetryableError, NetworkError } = require('@rudderstack/integrations-lib'); +const { + processAxiosResponse, + getDynamicErrorType, +} = require('../../../adapters/utils/networkUtils'); +const { AUTH_STATUS_INACTIVE } = require('../../../adapters/networkhandler/authConstants'); +const { prepareProxyRequest, proxyRequest } = require('../../../adapters/network'); +const { TransformerProxyError } = require('../../util/errorTypes'); +const tags = require('../../util/tags'); +const { isHttpStatusSuccess } = require('../../util'); + +// ref: https://github.com/intercom/oauth2-intercom +// Intercom's OAuth implementation does not use refresh tokens. Access tokens are valid until a user revokes access manually, or until an app deauthorizes itself. +const getAuthErrCategory = (status) => { + if (status === 401) { + return AUTH_STATUS_INACTIVE; + } + return ''; +}; + +const errorResponseHandler = (destinationResponse, dest) => { + const { response, status } = destinationResponse; + const message = `[Intercom V2 Response Handler] Request failed for destination ${dest} with status: ${status}`; + if (status === 401) { + throw new TransformerProxyError( + `${message}. ${JSON.stringify(response)}`, + 400, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + getAuthErrCategory(status), + ); + } + if (status === 408) { + throw new RetryableError(message, 500, destinationResponse, getAuthErrCategory(status)); + } + if (!isHttpStatusSuccess(status)) { + throw new NetworkError( + `${message}. ${JSON.stringify(response)}`, + status, + { + [tags.TAG_NAMES.ERROR_TYPE]: getDynamicErrorType(status), + }, + destinationResponse, + ); + } +}; + +const responseHandler = (responseParams) => { + const { destinationResponse, destType } = responseParams; + errorResponseHandler(destinationResponse, destType); + return { + destinationResponse: destinationResponse.response, + message: 'Request Processed Successfully', + status: destinationResponse.status, + }; +}; + +function networkHandler() { + this.prepareProxy = prepareProxyRequest; + this.proxy = proxyRequest; + this.processAxiosResponse = processAxiosResponse; + this.responseHandler = responseHandler; +} + +module.exports = { networkHandler }; diff --git a/src/v0/destinations/intercom_v2/transform.js b/src/v0/destinations/intercom_v2/transform.js new file mode 100644 index 0000000000..8d97e20bde --- /dev/null +++ b/src/v0/destinations/intercom_v2/transform.js @@ -0,0 +1,187 @@ +const { InstrumentationError } = require('@rudderstack/integrations-lib'); +const { + handleRtTfSingleEventError, + getSuccessRespEvents, + getEventType, + constructPayload, + getIntegrationsObj, +} = require('../../util'); +const { EventType } = require('../../../constants'); +const { + getHeaders, + searchContact, + handleDetachUserAndCompany, + getResponse, + createOrUpdateCompany, + attachContactToCompany, + addOrUpdateTagsToCompany, + getStatusCode, + getBaseEndpoint, +} = require('./utils'); +const { + getName, + filterCustomAttributes, + addMetadataToPayload, +} = require('../../../cdk/v2/destinations/intercom/utils'); +const { MappingConfig, ConfigCategory } = require('./config'); + +const transformIdentifyPayload = (event) => { + const { message, destination } = event; + const category = ConfigCategory.IDENTIFY; + const payload = constructPayload(message, MappingConfig[category.name]); + const shouldSendAnonymousId = destination.Config.sendAnonymousId; + if (!payload.external_id && shouldSendAnonymousId) { + payload.external_id = message.anonymousId; + } + if (!(payload.external_id || payload.email)) { + throw new InstrumentationError('Either email or userId is required for Identify call'); + } + payload.name = getName(message); + payload.custom_attributes = message.traits || message.context.traits || {}; + payload.custom_attributes = filterCustomAttributes(payload, 'user', destination); + return payload; +}; + +const transformTrackPayload = (event) => { + const { message, destination } = event; + const category = ConfigCategory.TRACK; + let payload = constructPayload(message, MappingConfig[category.name]); + if (!payload.id) { + const integrationsObj = getIntegrationsObj(message, 'INTERCOM'); + payload.id = integrationsObj?.id; + } + const shouldSendAnonymousId = destination.Config.sendAnonymousId; + if (!payload.user_id && shouldSendAnonymousId) { + payload.user_id = message.anonymousId; + } + if (!(payload.user_id || payload.email || payload.id)) { + throw new InstrumentationError('Either email or userId or id is required for Track call'); + } + payload = addMetadataToPayload(payload); + return payload; +}; + +const transformGroupPayload = (event) => { + const { message, destination } = event; + const category = ConfigCategory.GROUP; + const payload = constructPayload(message, MappingConfig[category.name]); + payload.custom_attributes = message.traits || message.context.traits || {}; + payload.custom_attributes = filterCustomAttributes(payload, 'company', destination); + return payload; +}; + +const constructIdentifyResponse = async (event) => { + const { destination, metadata } = event; + + const payload = transformIdentifyPayload(event); + + let method = 'POST'; + let endpoint = `${getBaseEndpoint(destination)}/contacts`; + const headers = getHeaders(metadata); + + // when contact is found in intercom + const contactId = await searchContact(event); + if (contactId) { + method = 'PUT'; + endpoint += `/${contactId}`; + + // detach user and company if required + await handleDetachUserAndCompany(contactId, event); + } + + return getResponse(method, endpoint, headers, payload); +}; + +const constructTrackResponse = (event) => { + const { destination, metadata } = event; + const payload = transformTrackPayload(event); + const method = 'POST'; + const endpoint = `${getBaseEndpoint(destination)}/events`; + const headers = getHeaders(metadata); + + return getResponse(method, endpoint, headers, payload); +}; + +const constructGroupResponse = async (event) => { + const { destination, metadata } = event; + const payload = transformGroupPayload(event); + + const method = 'POST'; + let endpoint = `${getBaseEndpoint(destination)}/companies`; + const headers = getHeaders(metadata); + let finalPayload = payload; + + // create or update company + const companyId = await createOrUpdateCompany(payload, destination, metadata); + + // when contact is found in intercom + const contactId = await searchContact(event); + if (contactId) { + // attach user and company + finalPayload = { + id: companyId, + }; + endpoint = `${getBaseEndpoint(destination)}/contacts/${contactId}/companies`; + await attachContactToCompany(finalPayload, endpoint, destination, metadata); + } + + // add tags to company + await addOrUpdateTagsToCompany(companyId, event); + + return getResponse(method, endpoint, headers, finalPayload); +}; + +const processEvent = async (event) => { + const { message } = event; + const messageType = getEventType(message); + let response; + switch (messageType) { + case EventType.IDENTIFY: + response = await constructIdentifyResponse(event); + break; + case EventType.TRACK: + response = constructTrackResponse(event); + break; + case EventType.GROUP: + response = await constructGroupResponse(event); + break; + default: + throw new InstrumentationError(`message type ${messageType} is not supported.`); + } + return response; +}; + +const process = async (event) => { + const response = await processEvent(event); + return response; +}; + +const processRouter = async (inputs, reqMetadata) => { + const results = await Promise.all( + inputs.map(async (event) => { + try { + const response = await process(event); + return getSuccessRespEvents( + response, + [event.metadata], + event.destination, + false, + getStatusCode(event), + ); + } catch (error) { + return handleRtTfSingleEventError(event, error, reqMetadata); + } + }), + ); + return results; +}; + +const processRouterDest = async (inputs, reqMetadata) => { + if (!inputs || inputs.length === 0) { + return []; + } + const response = await processRouter(inputs, reqMetadata); + return response; +}; + +module.exports = { processRouterDest }; diff --git a/src/v0/destinations/intercom_v2/utils.js b/src/v0/destinations/intercom_v2/utils.js new file mode 100644 index 0000000000..69ea1385d9 --- /dev/null +++ b/src/v0/destinations/intercom_v2/utils.js @@ -0,0 +1,332 @@ +const { + removeUndefinedAndNullValues, + InstrumentationError, + NetworkError, + InvalidAuthTokenError, +} = require('@rudderstack/integrations-lib'); +const { EventType } = require('../../../constants'); +const { JSON_MIME_TYPE } = require('../../util/constant'); +const tags = require('../../util/tags'); +const { + getFieldValueFromMessage, + isHttpStatusSuccess, + defaultRequestConfig, + getEventType, +} = require('../../util'); +const { HTTP_STATUS_CODES } = require('../../util/constant'); +const { + SEARCH_CONTACT_ENDPOINT, + CREATE_OR_UPDATE_COMPANY_ENDPOINT, + TAGS_ENDPOINT, + BASE_ENDPOINT, + BASE_EU_ENDPOINT, + BASE_AU_ENDPOINT, +} = require('../../../cdk/v2/destinations/intercom/config'); +const { getLookUpField } = require('../../../cdk/v2/destinations/intercom/utils'); +const { handleHttpRequest } = require('../../../adapters/network'); +const { getAccessToken } = require('../../util'); +const { ApiVersions, destType } = require('./config'); +const { getDynamicErrorType } = require('../../../adapters/utils/networkUtils'); + +/** + * method to handle error during api call + * ref docs: https://developers.intercom.com/docs/references/rest-api/errors/http-responses/ + * e.g. + * 400 - code: parameter_not_found (or parameter_invalid), message: company not specified + * 401 - code: unauthorized, message: Access Token Invalid + * 404 - code: company_not_found, message: Company Not Found + * @param {*} message + * @param {*} processedResponse + */ +const intercomErrorHandler = (message, processedResponse) => { + const errorMessages = JSON.stringify(processedResponse.response); + if (processedResponse.status === 400) { + throw new InstrumentationError(`${message} : ${errorMessages}`); + } + if (processedResponse.status === 401) { + throw new InvalidAuthTokenError(message, 400, errorMessages); + } + if (processedResponse.status === 404) { + throw new InstrumentationError(`${message} : ${errorMessages}`); + } + throw new NetworkError( + `${message} : ${errorMessages}`, + processedResponse.status, + { + [tags]: getDynamicErrorType(processedResponse.status), + }, + processedResponse, + ); +}; + +const getHeaders = (metadata) => ({ + Authorization: `Bearer ${getAccessToken(metadata, 'accessToken')}`, + Accept: JSON_MIME_TYPE, + 'Content-Type': JSON_MIME_TYPE, + 'Intercom-Version': ApiVersions.v2, +}); + +const getBaseEndpoint = (destination) => { + const { apiServer } = destination.Config; + switch (apiServer) { + case 'Europe': + return BASE_EU_ENDPOINT; + case 'Australia': + return BASE_AU_ENDPOINT; + default: + return BASE_ENDPOINT; + } +}; + +const getStatusCode = (event) => { + const { message } = event; + let statusCode = HTTP_STATUS_CODES.OK; + const messageType = getEventType(message); + if (messageType === EventType.GROUP) { + statusCode = HTTP_STATUS_CODES.SUPPRESS_EVENTS; + } + return statusCode; +}; + +const getResponse = (method, endpoint, headers, payload) => { + const response = defaultRequestConfig(); + response.method = method; + response.endpoint = endpoint; + response.headers = headers; + response.body.JSON = removeUndefinedAndNullValues(payload); + return response; +}; + +const searchContact = async (event) => { + const { message, destination, metadata } = event; + const lookupField = getLookUpField(message); + let lookupFieldValue = getFieldValueFromMessage(message, lookupField); + if (!lookupFieldValue) { + lookupFieldValue = message?.context?.traits?.[lookupField]; + } + const data = JSON.stringify({ + query: { + operator: 'AND', + value: [ + { + field: lookupField, + operator: '=', + value: lookupFieldValue, + }, + ], + }, + }); + + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/${SEARCH_CONTACT_ENDPOINT}`; + const statTags = { + destType, + feature: 'transformation', + endpointPath: '/contacts/search', + requestMethod: 'POST', + module: 'router', + metadata, + }; + + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + data, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to search contact due to', response); + } + return response.response?.data.length > 0 ? response.response?.data[0]?.id : null; +}; + +const getCompanyId = async (company, destination, metadata) => { + if (!company.id && !company.name) return undefined; + const headers = getHeaders(metadata); + + const queryParam = company.id ? `company_id=${company.id}` : `name=${company.name}`; + const endpoint = `${getBaseEndpoint(destination)}/companies?${queryParam}`; + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: '/companies', + requestMethod: 'GET', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'GET', + endpoint, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to get company id due to', response); + } + + return response?.response?.id; +}; + +const detachContactAndCompany = async (contactId, company, destination, metadata) => { + const companyId = await getCompanyId(company, destination, metadata); + if (!companyId) return; + + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/contacts/${contactId}/companies/${companyId}`; + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: 'contacts/companies', + requestMethod: 'DELETE', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'DELETE', + endpoint, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to detach contact and company due to', response); + } +}; + +const handleDetachUserAndCompany = async (contactId, event) => { + const { message, destination, metadata } = event; + const company = message?.traits?.company || message?.context?.traits?.company; + const shouldDetachUserAndCompany = company?.remove; + if (shouldDetachUserAndCompany) { + await detachContactAndCompany(contactId, company, destination, metadata); + } +}; + +const createOrUpdateCompany = async (payload, destination, metadata) => { + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/${CREATE_OR_UPDATE_COMPANY_ENDPOINT}`; + + const finalPayload = JSON.stringify(removeUndefinedAndNullValues(payload)); + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: '/companies', + requestMethod: 'POST', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + finalPayload, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to Create or Update Company due to', response); + } + + return response.response?.id; +}; + +const attachContactToCompany = async (payload, endpoint, destination, metadata) => { + const headers = getHeaders(metadata); + const finalPayload = JSON.stringify(removeUndefinedAndNullValues(payload)); + + const statTags = { + metadata, + destType, + feature: 'transformation', + endpointPath: '/contact/{id}/companies', + requestMethod: 'POST', + module: 'router', + }; + + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + finalPayload, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to attach Contact or User to Company due to', response); + } +}; + +const addOrUpdateTagsToCompany = async (id, event) => { + const { message, destination, metadata } = event; + const companyTags = message?.traits?.tags || message?.context?.traits?.tags; + if (!companyTags) return; + + const headers = getHeaders(metadata); + const endpoint = `${getBaseEndpoint(destination)}/${TAGS_ENDPOINT}`; + + const statTags = { + destType, + feature: 'transformation', + endpointPath: '/tags', + requestMethod: 'POST', + module: 'router', + metadata, + }; + + await Promise.all( + companyTags.map(async (tag) => { + const finalPayload = { + name: tag, + companies: [ + { + id, + }, + ], + }; + const { processedResponse: response } = await handleHttpRequest( + 'POST', + endpoint, + finalPayload, + { + headers, + }, + statTags, + ); + + if (!isHttpStatusSuccess(response.status)) { + intercomErrorHandler('Unable to Add or Update the Tag to Company due to', response); + } + }), + ); +}; + +module.exports = { + getStatusCode, + getHeaders, + searchContact, + handleDetachUserAndCompany, + getResponse, + createOrUpdateCompany, + attachContactToCompany, + addOrUpdateTagsToCompany, + getBaseEndpoint, +}; diff --git a/src/v0/destinations/lytics/config.js b/src/v0/destinations/lytics/config.js deleted file mode 100644 index c7843eda46..0000000000 --- a/src/v0/destinations/lytics/config.js +++ /dev/null @@ -1,27 +0,0 @@ -const { getMappingConfig } = require('../../util'); - -const ENDPOINT = 'https://api.lytics.io/collect/json'; -const CONFIG_CATEGORIES = { - IDENTIFY: { - name: 'LYTICSIdentifyConfig', - }, - PAGESCREEN: { - name: 'LYTICSPageScreenConfig', - }, - TRACK: { - name: 'LYTICSTrackConfig', - }, -}; - -const forFirstName = ['firstname', 'firstName']; -const forLastName = ['lastname', 'lastName']; - -const MAPPING_CONFIG = getMappingConfig(CONFIG_CATEGORIES, __dirname); - -module.exports = { - ENDPOINT, - MAPPING_CONFIG, - CONFIG_CATEGORIES, - forFirstName, - forLastName, -}; diff --git a/src/v0/destinations/lytics/data/LYTICSIdentifyConfig.json b/src/v0/destinations/lytics/data/LYTICSIdentifyConfig.json deleted file mode 100644 index d9765490e3..0000000000 --- a/src/v0/destinations/lytics/data/LYTICSIdentifyConfig.json +++ /dev/null @@ -1,19 +0,0 @@ -[ - { - "destKey": "user_id", - "sourceKeys": [ - "userId", - "traits.userId", - "traits.id", - "context.traits.userId", - "context.traits.id", - "anonymousId" - ], - "required": false - }, - { - "destKey": "", - "sourceKeys": ["traits", "context.traits"], - "required": false - } -] diff --git a/src/v0/destinations/lytics/data/LYTICSPageScreenConfig.json b/src/v0/destinations/lytics/data/LYTICSPageScreenConfig.json deleted file mode 100644 index f925dbc5bd..0000000000 --- a/src/v0/destinations/lytics/data/LYTICSPageScreenConfig.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "destKey": "event", - "sourceKeys": "name", - "required": false - }, - { - "destKey": "", - "sourceKeys": "properties", - "required": false - } -] diff --git a/src/v0/destinations/lytics/data/LYTICSTrackConfig.json b/src/v0/destinations/lytics/data/LYTICSTrackConfig.json deleted file mode 100644 index 5de09eb10b..0000000000 --- a/src/v0/destinations/lytics/data/LYTICSTrackConfig.json +++ /dev/null @@ -1,12 +0,0 @@ -[ - { - "destKey": "_e", - "sourceKeys": "event", - "required": false - }, - { - "destKey": "", - "sourceKeys": "properties", - "required": false - } -] diff --git a/src/v0/destinations/lytics/transform.js b/src/v0/destinations/lytics/transform.js deleted file mode 100644 index c3a971adba..0000000000 --- a/src/v0/destinations/lytics/transform.js +++ /dev/null @@ -1,77 +0,0 @@ -const { InstrumentationError } = require('@rudderstack/integrations-lib'); -const { EventType } = require('../../../constants'); -const { - CONFIG_CATEGORIES, - MAPPING_CONFIG, - ENDPOINT, - forFirstName, - forLastName, -} = require('./config'); -const { - constructPayload, - defaultPostRequestConfig, - removeUndefinedAndNullValues, - defaultRequestConfig, - flattenJson, - simpleProcessRouterDest, -} = require('../../util'); -const { JSON_MIME_TYPE } = require('../../util/constant'); - -const responseBuilderSimple = (message, category, destination) => { - const payload = constructPayload(message, MAPPING_CONFIG[category.name]); - const response = defaultRequestConfig(); - const { stream, apiKey } = destination.Config; - response.endpoint = `${ENDPOINT}/${stream}?access_token=${apiKey}`; - response.method = defaultPostRequestConfig.requestMethod; - const flattenedPayload = removeUndefinedAndNullValues(flattenJson(payload)); - forFirstName.forEach((key) => { - if (flattenedPayload[key]) { - flattenedPayload.first_name = flattenedPayload[key]; - delete flattenedPayload[key]; - } - }); - forLastName.forEach((key) => { - if (flattenedPayload[key]) { - flattenedPayload.last_name = flattenedPayload[key]; - delete flattenedPayload[key]; - } - }); - response.body.JSON = flattenedPayload; - response.headers = { - 'Content-Type': JSON_MIME_TYPE, - }; - return response; -}; - -const processEvent = (message, destination) => { - if (!message.type) { - throw new InstrumentationError('Event type is required'); - } - const messageType = message.type; - let category; - switch (messageType.toLowerCase()) { - case EventType.IDENTIFY: - category = CONFIG_CATEGORIES.IDENTIFY; - break; - case EventType.PAGE: - case EventType.SCREEN: - category = CONFIG_CATEGORIES.PAGESCREEN; - break; - case EventType.TRACK: - category = CONFIG_CATEGORIES.TRACK; - break; - default: - throw new InstrumentationError(`Event type ${messageType} is not supported`); - } - // build the response - return responseBuilderSimple(message, category, destination); -}; - -const process = (event) => processEvent(event.message, event.destination); - -const processRouterDest = async (inputs, reqMetadata) => { - const respList = await simpleProcessRouterDest(inputs, process, reqMetadata); - return respList; -}; - -module.exports = { process, processRouterDest }; diff --git a/src/v0/destinations/posthog/data/PHAliasConfig.json b/src/v0/destinations/posthog/data/PHAliasConfig.json index 1992349e22..26fb61ea78 100644 --- a/src/v0/destinations/posthog/data/PHAliasConfig.json +++ b/src/v0/destinations/posthog/data/PHAliasConfig.json @@ -1,12 +1,12 @@ [ { - "destKey": "properties.alias", + "destKey": "properties.distinct_id", "sourceKeys": "userId", "sourceFromGenericMap": true, "required": true }, { - "destKey": "properties.distinct_id", + "destKey": "properties.alias", "sourceKeys": "previousId", "required": true }, diff --git a/src/v0/destinations/salesforce/config.js b/src/v0/destinations/salesforce/config.js index 1425bad51b..f2e8072755 100644 --- a/src/v0/destinations/salesforce/config.js +++ b/src/v0/destinations/salesforce/config.js @@ -24,6 +24,7 @@ const SF_TOKEN_REQUEST_URL = 'https://login.salesforce.com/services/oauth2/token const SF_TOKEN_REQUEST_URL_SANDBOX = 'https://test.salesforce.com/services/oauth2/token'; const DESTINATION = 'Salesforce'; +const SALESFORCE_OAUTH_SANDBOX = 'salesforce_oauth_sandbox'; const OAUTH = 'oauth'; const LEGACY = 'legacy'; @@ -41,4 +42,5 @@ module.exports = { DESTINATION, OAUTH, LEGACY, + SALESFORCE_OAUTH_SANDBOX, }; diff --git a/src/v0/destinations/salesforce/transform.js b/src/v0/destinations/salesforce/transform.js index 1dde1ec65a..7e66dd8810 100644 --- a/src/v0/destinations/salesforce/transform.js +++ b/src/v0/destinations/salesforce/transform.js @@ -293,6 +293,7 @@ async function processIdentify( authorizationData, authorizationFlow, ) { + const { Name } = destination.DestinationDefinition; const mapProperty = destination.Config.mapProperty === undefined ? true : destination.Config.mapProperty; // check the traits before hand @@ -304,7 +305,7 @@ async function processIdentify( // Append external ID to traits if event is mapped to destination and only if identifier type is not id // If identifier type is id, then it should not be added to traits, else saleforce will throw an error const mappedToDestination = get(message, MappedToDestinationKey); - const externalId = getDestinationExternalIDObjectForRetl(message, 'SALESFORCE'); + const externalId = getDestinationExternalIDObjectForRetl(message, Name); if (mappedToDestination && externalId?.identifierType?.toLowerCase() !== 'id') { addExternalIdToTraits(message); } diff --git a/src/v0/destinations/salesforce/utils.js b/src/v0/destinations/salesforce/utils.js index 9a4effc502..a7731f07de 100644 --- a/src/v0/destinations/salesforce/utils.js +++ b/src/v0/destinations/salesforce/utils.js @@ -1,4 +1,9 @@ -const { RetryableError, ThrottledError, AbortedError } = require('@rudderstack/integrations-lib'); +const { + RetryableError, + ThrottledError, + AbortedError, + OAuthSecretError, +} = require('@rudderstack/integrations-lib'); const { handleHttpRequest } = require('../../../adapters/network'); const { isHttpStatusSuccess, @@ -13,6 +18,7 @@ const { DESTINATION, LEGACY, OAUTH, + SALESFORCE_OAUTH_SANDBOX, } = require('./config'); const ACCESS_TOKEN_CACHE = new Cache(ACCESS_TOKEN_CACHE_TTL); @@ -104,10 +110,15 @@ const salesforceResponseHandler = (destResponse, sourceMessage, authKey, authori * @param {destination: Record, metadata: Record} * @returns */ -const getAccessTokenOauth = (metadata) => ({ - token: metadata.secret?.access_token, - instanceUrl: metadata.secret?.instance_url, -}); +const getAccessTokenOauth = (metadata) => { + if (!isDefinedAndNotNull(metadata?.secret)) { + throw new OAuthSecretError('secret is undefined/null'); + } + return { + token: metadata.secret?.access_token, + instanceUrl: metadata.secret?.instance_url, + }; +}; const getAccessToken = async ({ destination, metadata }) => { const accessTokenKey = destination.ID; @@ -169,7 +180,9 @@ const getAccessToken = async ({ destination, metadata }) => { const collectAuthorizationInfo = async (event) => { let authorizationFlow; let authorizationData; - if (isDefinedAndNotNull(event.metadata?.secret)) { + const { Name } = event.destination.DestinationDefinition; + const lowerCaseName = Name?.toLowerCase?.(); + if (isDefinedAndNotNull(event?.metadata?.secret) || lowerCaseName === SALESFORCE_OAUTH_SANDBOX) { authorizationFlow = OAUTH; authorizationData = getAccessTokenOauth(event.metadata); } else { diff --git a/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js b/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js new file mode 100644 index 0000000000..b6cbed77f9 --- /dev/null +++ b/src/v0/destinations/salesforce_oauth_sandbox/networkHandler.js @@ -0,0 +1,34 @@ +const { proxyRequest, prepareProxyRequest } = require('../../../adapters/network'); +const { processAxiosResponse } = require('../../../adapters/utils/networkUtils'); +const { OAUTH } = require('../salesforce/config'); +const { salesforceResponseHandler } = require('../salesforce/utils'); + +const responseHandler = (responseParams) => { + const { destinationResponse, destType, rudderJobMetadata } = responseParams; + const message = `Request for destination: ${destType} Processed Successfully`; + + salesforceResponseHandler( + destinationResponse, + 'during Salesforce Response Handling', + rudderJobMetadata?.destInfo?.authKey, + OAUTH, + ); + + // else successfully return status as 200, message and original destination response + return { + status: 200, + message, + destinationResponse, + }; +}; + +function networkHandler() { + this.responseHandler = responseHandler; + this.proxy = proxyRequest; + this.prepareProxy = prepareProxyRequest; + this.processAxiosResponse = processAxiosResponse; +} + +module.exports = { + networkHandler, +}; diff --git a/src/v0/destinations/singular/config.js b/src/v0/destinations/singular/config.js index d3fd284963..97824b809b 100644 --- a/src/v0/destinations/singular/config.js +++ b/src/v0/destinations/singular/config.js @@ -13,6 +13,10 @@ const CONFIG_CATEGORIES = { name: 'SINGULARIosSessionConfig', type: 'track', }, + SESSION_UNITY: { + name: 'SINGULARUnitySessionConfig', + type: 'track', + }, EVENT_ANDROID: { name: 'SINGULARAndroidEventConfig', type: 'track', @@ -21,6 +25,10 @@ const CONFIG_CATEGORIES = { name: 'SINGULARIosEventConfig', type: 'track', }, + EVENT_UNITY: { + name: 'SINGULARUnityEventConfig', + type: 'track', + }, PRODUCT_PROPERTY: { name: 'SINGULAREventProductConfig', }, @@ -29,8 +37,15 @@ const CONFIG_CATEGORIES = { const SUPPORTED_PLATFORM = { android: 'ANDROID', ios: 'IOS', + pc: 'unity', + xbox: 'unity', + playstation: 'unity', + nintendo: 'unity', + metaquest: 'unity', }; +const SUPPORTED_UNTIY_SUBPLATFORMS = ['pc', 'xbox', 'playstation', 'nintendo', 'metaquest']; + const SINGULAR_SESSION_ANDROID_EXCLUSION = [ 'referring_application', 'asid', @@ -93,5 +108,6 @@ module.exports = { SINGULAR_EVENT_ANDROID_EXCLUSION, SINGULAR_EVENT_IOS_EXCLUSION, SUPPORTED_PLATFORM, + SUPPORTED_UNTIY_SUBPLATFORMS, BASE_URL, }; diff --git a/src/v0/destinations/singular/data/SINGULARUnityEventConfig.json b/src/v0/destinations/singular/data/SINGULARUnityEventConfig.json new file mode 100644 index 0000000000..97cdfda229 --- /dev/null +++ b/src/v0/destinations/singular/data/SINGULARUnityEventConfig.json @@ -0,0 +1,112 @@ +[ + { + "destKey": "p", + "sourceKeys": "context.os.name", + "required": true + }, + { + "destKey": "i", + "sourceKeys": "context.app.namespace", + "required": true + }, + { + "destKey": "sdid", + "sourceKeys": "context.device.id", + "required": false + }, + { + "destKey": "is_revenue_event", + "sourceKeys": "properties.is_revenue_event", + "required": false + }, + { + "destKey": "n", + "sourceKeys": "event", + "required": true + }, + { + "destKey": "av", + "sourceKeys": "context.app.version", + "required": false + }, + { + "destKey": "ve", + "sourceKeys": "context.os.version", + "required": false + }, + { + "destKey": "os", + "sourceKeys": "properties.os", + "required": true + }, + { + "destKey": "ip", + "sourceKeys": ["context.ip", "request_ip"], + "required": true + }, + { + "destKey": "use_ip", + "sourceKeys": "properties.use_ip", + "required": false + }, + { + "destKey": "install_source", + "sourceKeys": "properties.install_source", + "required": true + }, + { + "destKey": "data_sharing_options", + "sourceKeys": "properties.data_sharing_options", + "required": false + }, + { + "destKey": "amt", + "sourceKeys": [ + "properties.total", + "properties.value", + "properties.revenue", + { + "operation": "multiplication", + "args": [ + { + "sourceKeys": "properties.price" + }, + { + "sourceKeys": "properties.quantity", + "default": 1 + } + ] + } + ], + "required": false + }, + { + "destKey": "cur", + "sourceKeys": "properties.currency", + "required": false + }, + { + "destKey": "ua", + "sourceKeys": "context.userAgent", + "required": false + }, + { + "destKey": "utime", + "sourceKeys": "timestamp", + "sourceFromGenericMap": true, + "required": false, + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "custom_user_id", + "sourceKeys": "properties.custom_user_id", + "required": false + }, + { + "destKey": "install", + "sourceKeys": "properties.install", + "required": false + } +] diff --git a/src/v0/destinations/singular/data/SINGULARUnitySessionConfig.json b/src/v0/destinations/singular/data/SINGULARUnitySessionConfig.json new file mode 100644 index 0000000000..aca561bc59 --- /dev/null +++ b/src/v0/destinations/singular/data/SINGULARUnitySessionConfig.json @@ -0,0 +1,76 @@ +[ + { + "destKey": "p", + "sourceKeys": "context.os.name", + "required": true + }, + { + "destKey": "i", + "sourceKeys": "context.app.namespace", + "required": true + }, + { + "destKey": "sdid", + "sourceKeys": "context.device.id", + "required": false + }, + { + "destKey": "av", + "sourceKeys": "context.app.version", + "required": false + }, + { + "destKey": "ve", + "sourceKeys": "context.os.version", + "required": false + }, + { + "destKey": "os", + "sourceKeys": "properties.os", + "required": true + }, + { + "destKey": "ip", + "sourceKeys": ["context.ip", "request_ip"], + "required": true + }, + { + "destKey": "use_ip", + "sourceKeys": "properties.use_ip", + "required": false + }, + { + "destKey": "install_source", + "sourceKeys": "properties.install_source", + "required": true + }, + { + "destKey": "ua", + "sourceKeys": "context.userAgent", + "required": false + }, + { + "destKey": "utime", + "sourceKeys": "timestamp", + "sourceFromGenericMap": true, + "required": false, + "metadata": { + "type": "secondTimestamp" + } + }, + { + "destKey": "data_sharing_options", + "sourceKeys": "properties.data_sharing_options", + "required": false + }, + { + "destKey": "custom_user_id", + "sourceKeys": "properties.custom_user_id", + "required": false + }, + { + "destKey": "install", + "sourceKeys": "properties.install", + "required": false + } +] diff --git a/src/v0/destinations/singular/transform.js b/src/v0/destinations/singular/transform.js index ff5d18db9a..ed6757c47b 100644 --- a/src/v0/destinations/singular/transform.js +++ b/src/v0/destinations/singular/transform.js @@ -20,7 +20,7 @@ const responseBuilderSimple = (message, { Config }) => { } const sessionEvent = isSessionEvent(Config, eventName); - const { eventAttributes, payload } = platformWisePayloadGenerator(message, sessionEvent); + const { eventAttributes, payload } = platformWisePayloadGenerator(message, sessionEvent, Config); const endpoint = sessionEvent ? `${BASE_URL}/launch` : `${BASE_URL}/evt`; // If we have an event where we have an array of Products, example Order Completed diff --git a/src/v0/destinations/singular/util.js b/src/v0/destinations/singular/util.js index 4c5aeb8964..61db0472ab 100644 --- a/src/v0/destinations/singular/util.js +++ b/src/v0/destinations/singular/util.js @@ -9,6 +9,7 @@ const { SINGULAR_EVENT_IOS_EXCLUSION, BASE_URL, SUPPORTED_PLATFORM, + SUPPORTED_UNTIY_SUBPLATFORMS, SESSIONEVENTS, } = require('./config'); const { @@ -85,7 +86,7 @@ const isSessionEvent = (Config, eventName) => { * @param {*} sessionEvent * @returns */ -const platformWisePayloadGenerator = (message, sessionEvent) => { +const platformWisePayloadGenerator = (message, sessionEvent, Config) => { let eventAttributes; const clonedMessage = { ...message }; let platform = getValueFromMessage(clonedMessage, 'context.os.name'); @@ -99,55 +100,68 @@ const platformWisePayloadGenerator = (message, sessionEvent) => { platform = 'iOS'; } platform = platform.toLowerCase(); - if (!SUPPORTED_PLATFORM[platform]) { + if (!SUPPORTED_PLATFORM[platform] && !SUPPORTED_UNTIY_SUBPLATFORMS[platform]) { throw new InstrumentationError(`Platform ${platform} is not supported`); } - - const payload = constructPayload( - clonedMessage, - MAPPING_CONFIG[CONFIG_CATEGORIES[`${typeOfEvent}_${SUPPORTED_PLATFORM[platform]}`].name], - ); - - if (!payload) { - throw new TransformationError(`Failed to Create ${platform} ${typeOfEvent} Payload`); - } - if (sessionEvent) { - // context.device.adTrackingEnabled = true implies Singular's do not track (dnt) - // to be 0 and vice-versa. - const adTrackingEnabled = getValueFromMessage( + let payload; + if (SUPPORTED_UNTIY_SUBPLATFORMS.includes(platform)) { + payload = constructPayload( clonedMessage, - 'context.device.adTrackingEnabled', + MAPPING_CONFIG[CONFIG_CATEGORIES[`${typeOfEvent}_UNITY`].name], ); - if (adTrackingEnabled === true) { - payload.dnt = 0; - } else { - payload.dnt = 1; - } - // by default, the value of openuri and install_source should be "", i.e empty string if nothing is passed - payload.openuri = clonedMessage.properties.url || ''; - if (platform === 'android' || platform === 'Android') { - payload.install_source = clonedMessage.properties.referring_application || ''; - } } else { - // Custom Attribues is not supported by session events - eventAttributes = extractExtraFields( + payload = constructPayload( clonedMessage, - exclusionList[`${SUPPORTED_PLATFORM[platform]}_${typeOfEvent}_EXCLUSION_LIST`], + MAPPING_CONFIG[CONFIG_CATEGORIES[`${typeOfEvent}_${SUPPORTED_PLATFORM[platform]}`].name], ); - eventAttributes = removeUndefinedAndNullValues(eventAttributes); + } - // If anyone out of value, revenue, total is set,we will have amt in payload - // and we will consider the event as revenue event. - if (!isDefinedAndNotNull(payload.is_revenue_event) && payload.amt) { - payload.is_revenue_event = true; - } + if (!payload) { + throw new TransformationError(`Failed to Create ${platform} ${typeOfEvent} Payload`); } + if (!SUPPORTED_UNTIY_SUBPLATFORMS.includes(platform)) { + if (sessionEvent) { + // context.device.adTrackingEnabled = true implies Singular's do not track (dnt) + // to be 0 and vice-versa. + const adTrackingEnabled = getValueFromMessage( + clonedMessage, + 'context.device.adTrackingEnabled', + ); + if (adTrackingEnabled === true) { + payload.dnt = 0; + } else { + payload.dnt = 1; + } + // by default, the value of openuri and install_source should be "", i.e empty string if nothing is passed + payload.openuri = clonedMessage.properties.url || ''; + if (platform === 'android' || platform === 'Android') { + payload.install_source = clonedMessage.properties.referring_application || ''; + } + } else { + // Custom Attribues is not supported by session events + eventAttributes = extractExtraFields( + clonedMessage, + exclusionList[`${SUPPORTED_PLATFORM[platform]}_${typeOfEvent}_EXCLUSION_LIST`], + ); + eventAttributes = removeUndefinedAndNullValues(eventAttributes); - // Singular maps Connection Type to either wifi or carrier - if (clonedMessage.context?.network?.wifi) { - payload.c = 'wifi'; - } else { - payload.c = 'carrier'; + // If anyone out of value, revenue, total is set,we will have amt in payload + // and we will consider the event as revenue event. + if (!isDefinedAndNotNull(payload.is_revenue_event) && payload.amt) { + payload.is_revenue_event = true; + } + } + + // Singular maps Connection Type to either wifi or carrier + if (clonedMessage.context?.network?.wifi) { + payload.c = 'wifi'; + } else { + payload.c = 'carrier'; + } + } else if (Config.match_id === 'advertisingId') { + payload.match_id = clonedMessage?.context?.device?.advertisingId; + } else if (message.properties.match_id) { + payload.match_id = message.properties.match_id; } return { payload, eventAttributes }; }; diff --git a/src/v0/destinations/snowpipe_streaming/transform.js b/src/v0/destinations/snowpipe_streaming/transform.js index d2b5d99048..c9c6562d0a 100644 --- a/src/v0/destinations/snowpipe_streaming/transform.js +++ b/src/v0/destinations/snowpipe_streaming/transform.js @@ -1,11 +1 @@ -const { - process: snowflakeProcess, - provider: snowflakeProvider, - getDataTypeOverride: snowflakeGetDataTypeOverride, -} = require('../snowflake/transform'); - -module.exports = { - provider: snowflakeProvider, - process: snowflakeProcess, - getDataTypeOverride: snowflakeGetDataTypeOverride, -}; +module.exports = require('../snowflake/transform').process; diff --git a/src/v0/sources/shopify/config.js b/src/v0/sources/shopify/config.js index b8b3cde284..f0a844830b 100644 --- a/src/v0/sources/shopify/config.js +++ b/src/v0/sources/shopify/config.js @@ -35,7 +35,35 @@ const SHOPIFY_TRACK_MAP = { orders_cancelled: 'Order Cancelled', orders_fulfilled: 'Order Fulfilled', orders_paid: 'Order Paid', - orders_partially_fullfilled: 'Order Partially Fulfilled', + orders_partially_fulfilled: 'Order Partially Fulfilled', + // following are the events that supported by rudderstack pixel app as generic track events + customer_tags_added: 'Customer Tags Added', + customer_tags_removed: 'Customer Tags Removed', + customer_email_updated: 'Customer Email Updated', + collections_create: 'Collection Created', + collections_update: 'Collection Updated', + collections_delete: 'Collection Deleted', + collection_listings_add: 'Collection Listings Added', + collection_listings_remove: 'Collection Listings Removed', + collection_listings_update: 'Collection Listings Updated', + collection_publications_create: 'Collection Publications Created', + collection_publications_delete: 'Collection Publications Deleted', + collection_publications_update: 'Collection Publications Updated', + discounts_create: 'Discount Created', + discounts_delete: 'Discount Deleted', + discounts_update: 'Discount Updated', + discounts_redeemcode_added: 'Discount Redeemcode Added', + discounts_redeemcode_removed: 'Discount Redeemcode Removed', + draft_orders_create: 'Draft Order Created', + draft_orders_delete: 'Draft Order Deleted', + draft_orders_update: 'Draft Order Updated', + fulfillment_orders_split: 'Fulfillment Orders Split', + inventory_items_create: 'Inventory Items Created', + inventory_items_delete: 'Inventory Items Deleted', + inventory_items_update: 'Inventory Items Updated', + inventory_levels_connect: 'Inventory Levels Connected', + inventory_levels_disconnect: 'Inventory Levels Disconnected', + inventory_levels_update: 'Inventory Levels Updated', }; const identifyMappingJSON = JSON.parse( @@ -99,7 +127,35 @@ const SUPPORTED_TRACK_EVENTS = [ 'orders_cancelled', 'orders_fulfilled', 'orders_paid', - 'orders_partially_fullfilled', + 'orders_partially_fulfilled', + // following are the events that supported by rudderstack pixel app as generic track events + 'customer_tags_added', + 'customer_tags_removed', + 'customer_email_updated', + 'collections_create', + 'collections_update', + 'collections_delete', + 'collection_listings_add', + 'collection_listings_remove', + 'collection_listings_update', + 'collection_publications_create', + 'collection_publications_delete', + 'collection_publications_update', + 'discounts_create', + 'discounts_delete', + 'discounts_redeemcode_added', + 'discounts_redeemcode_removed', + 'discounts_update', + 'draft_orders_create', + 'draft_orders_delete', + 'draft_orders_update', + 'fulfillment_orders_split', + 'inventory_items_create', + 'inventory_items_delete', + 'inventory_items_update', + 'inventory_levels_connect', + 'inventory_levels_disconnect', + 'inventory_levels_update', ]; const maxTimeToIdentifyRSGeneratedCall = 10000; // in ms diff --git a/src/v0/sources/shopify/transform.js b/src/v0/sources/shopify/transform.js index b55fc61327..8cf39dfa5c 100644 --- a/src/v0/sources/shopify/transform.js +++ b/src/v0/sources/shopify/transform.js @@ -287,4 +287,10 @@ const process = async (event) => { return response; }; -exports.process = process; +module.exports = { + process, + processEvent, + identifyPayloadBuilder, + ecomPayloadBuilder, + trackPayloadBuilder, +}; diff --git a/src/v1/destinations/ga4_v2/networkHandler.ts b/src/v1/destinations/ga4_v2/networkHandler.ts new file mode 100644 index 0000000000..aece0411c1 --- /dev/null +++ b/src/v1/destinations/ga4_v2/networkHandler.ts @@ -0,0 +1,3 @@ +import { networkHandler } from '../../../v0/destinations/ga4/networkHandler'; + +module.exports = { networkHandler }; diff --git a/src/v1/sources/shopify/config.js b/src/v1/sources/shopify/config.js new file mode 100644 index 0000000000..5a3ce99b40 --- /dev/null +++ b/src/v1/sources/shopify/config.js @@ -0,0 +1,76 @@ +const path = require('path'); +const fs = require('fs'); + +const PIXEL_EVENT_TOPICS = { + CART_VIEWED: 'cart_viewed', + PRODUCT_ADDED_TO_CART: 'product_added_to_cart', + PRODUCT_REMOVED_FROM_CART: 'product_removed_from_cart', + PAGE_VIEWED: 'page_viewed', + PRODUCT_VIEWED: 'product_viewed', + COLLECTION_VIEWED: 'collection_viewed', + CHECKOUT_STARTED: 'checkout_started', + CHECKOUT_COMPLETED: 'checkout_completed', + CHECKOUT_ADDRESS_INFO_SUBMITTED: 'checkout_address_info_submitted', + CHECKOUT_CONTACT_INFO_SUBMITTED: 'checkout_contact_info_submitted', + CHECKOUT_SHIPPING_INFO_SUBMITTED: 'checkout_shipping_info_submitted', + PAYMENT_INFO_SUBMITTED: 'payment_info_submitted', + SEARCH_SUBMITTED: 'search_submitted', +}; + +const PIXEL_EVENT_MAPPING = { + cart_viewed: 'Cart Viewed', + product_added_to_cart: 'Product Added', + product_removed_from_cart: 'Product Removed', + page_viewed: 'Page Viewed', + product_viewed: 'Product Viewed', + collection_viewed: 'Collection Viewed', + checkout_started: 'Checkout Started', + checkout_completed: 'Checkout Completed', + checkout_address_info_submitted: 'Checkout Address Info Submitted', + checkout_contact_info_submitted: 'Checkout Contact Info Submitted', + checkout_shipping_info_submitted: 'Checkout Shipping Info Submitted', + payment_info_submitted: 'Payment Info Submitted', + search_submitted: 'Search Submitted', +}; + +const contextualFieldMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'pixelEventsMappings', 'contextualFieldMapping.json')), +); + +const cartViewedEventMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'pixelEventsMappings', 'cartViewedEventMapping.json')), +); + +const productListViewedEventMappingJSON = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, 'pixelEventsMappings', 'productListViewedEventMapping.json'), + ), +); + +const productViewedEventMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'pixelEventsMappings', 'productViewedEventMapping.json')), +); + +const productToCartEventMappingJSON = JSON.parse( + fs.readFileSync(path.resolve(__dirname, 'pixelEventsMappings', 'productToCartEventMapping.json')), +); + +const checkoutStartedCompletedEventMappingJSON = JSON.parse( + fs.readFileSync( + path.resolve(__dirname, 'pixelEventsMappings', 'checkoutStartedCompletedEventMapping.json'), + ), +); + +const INTEGERATION = 'SHOPIFY'; + +module.exports = { + INTEGERATION, + PIXEL_EVENT_TOPICS, + PIXEL_EVENT_MAPPING, + contextualFieldMappingJSON, + cartViewedEventMappingJSON, + productListViewedEventMappingJSON, + productViewedEventMappingJSON, + productToCartEventMappingJSON, + checkoutStartedCompletedEventMappingJSON, +}; diff --git a/src/v1/sources/shopify/pixelEventsMappings/cartViewedEventMapping.json b/src/v1/sources/shopify/pixelEventsMappings/cartViewedEventMapping.json new file mode 100644 index 0000000000..b74320c758 --- /dev/null +++ b/src/v1/sources/shopify/pixelEventsMappings/cartViewedEventMapping.json @@ -0,0 +1,42 @@ +[ + { + "sourceKeys": "merchandise.product.id", + "destKeys": "product_id" + }, + { + "sourceKeys": "merchandise.product.title", + "destKeys": "variant" + }, + { + "sourceKeys": "merchandise.image.src", + "destKeys": "image_url" + }, + { + "sourceKeys": "merchandise.price.amount", + "destKeys": "price" + }, + { + "sourceKeys": "merchandise.product.type", + "destKeys": "category" + }, + { + "sourceKeys": "merchandise.product.url", + "destKeys": "url" + }, + { + "sourceKeys": "merchandise.product.vendor", + "destKeys": "brand" + }, + { + "sourceKeys": "merchandise.sku", + "destKeys": "sku" + }, + { + "sourceKeys": "merchandise.title", + "destKeys": "name" + }, + { + "sourceKeys": "quantity", + "destKeys": "quantity" + } +] diff --git a/src/v1/sources/shopify/pixelEventsMappings/checkoutStartedCompletedEventMapping.json b/src/v1/sources/shopify/pixelEventsMappings/checkoutStartedCompletedEventMapping.json new file mode 100644 index 0000000000..478bcfc270 --- /dev/null +++ b/src/v1/sources/shopify/pixelEventsMappings/checkoutStartedCompletedEventMapping.json @@ -0,0 +1,42 @@ +[ + { + "sourceKeys": "quantity", + "destKeys": "quantity" + }, + { + "sourceKeys": "title", + "destKeys": "name" + }, + { + "sourceKeys": "variant.image.src", + "destKeys": "image_url" + }, + { + "sourceKeys": "variant.price.amount", + "destKeys": "price" + }, + { + "sourceKeys": "variant.sku", + "destKeys": "sku" + }, + { + "sourceKeys": "variant.product.id", + "destKeys": "product_id" + }, + { + "sourceKeys": "variant.product.title", + "destKeys": "variant" + }, + { + "sourceKeys": "variant.product.type", + "destKeys": "category" + }, + { + "sourceKeys": "variant.product.url", + "destKeys": "url" + }, + { + "sourceKeys": "variant.product.vendor", + "destKeys": "brand" + } +] diff --git a/src/v1/sources/shopify/pixelEventsMappings/contextualFieldMapping.json b/src/v1/sources/shopify/pixelEventsMappings/contextualFieldMapping.json new file mode 100644 index 0000000000..f314684948 --- /dev/null +++ b/src/v1/sources/shopify/pixelEventsMappings/contextualFieldMapping.json @@ -0,0 +1,34 @@ +[ + { + "sourceKeys": "context.document.referrer", + "destKeys": "page.referrer" + }, + { + "sourceKeys": "document.title", + "destKeys": "page.title" + }, + { + "sourceKeys": "navigator.userAgent", + "destKeys": "userAgent" + }, + { + "sourceKeys": "window.location.href", + "destKeys": "page.url" + }, + { + "sourceKeys": "window.location.pathname", + "destKeys": "page.path" + }, + { + "sourceKeys": "window.location.search", + "destKeys": "page.search" + }, + { + "sourceKeys": "window.screen.height", + "destKeys": "screen.height" + }, + { + "sourceKeys": "window.screen.width", + "destKeys": "screen.width" + } +] diff --git a/src/v1/sources/shopify/pixelEventsMappings/productListViewedEventMapping.json b/src/v1/sources/shopify/pixelEventsMappings/productListViewedEventMapping.json new file mode 100644 index 0000000000..fb8e8a4567 --- /dev/null +++ b/src/v1/sources/shopify/pixelEventsMappings/productListViewedEventMapping.json @@ -0,0 +1,38 @@ +[ + { + "sourceKeys": "image.src", + "destKeys": "image_url" + }, + { + "sourceKeys": "price.amount", + "destKeys": "price" + }, + { + "sourceKeys": "product.id", + "destKeys": "product_id" + }, + { + "sourceKeys": "product.title", + "destKeys": "variant" + }, + { + "sourceKeys": "product.type", + "destKeys": "category" + }, + { + "sourceKeys": "product.url", + "destKeys": "url" + }, + { + "sourceKeys": "product.vendor", + "destKeys": "brand" + }, + { + "sourceKeys": "sku", + "destKeys": "sku" + }, + { + "sourceKeys": "title", + "destKeys": "name" + } +] diff --git a/src/v1/sources/shopify/pixelEventsMappings/productToCartEventMapping.json b/src/v1/sources/shopify/pixelEventsMappings/productToCartEventMapping.json new file mode 100644 index 0000000000..2f9d71bc03 --- /dev/null +++ b/src/v1/sources/shopify/pixelEventsMappings/productToCartEventMapping.json @@ -0,0 +1,42 @@ +[ + { + "sourceKeys": "cartLine.merchandise.image.src", + "destKeys": "image_url" + }, + { + "sourceKeys": "cartLine.merchandise.price.amount", + "destKeys": "price" + }, + { + "sourceKeys": "cartLine.merchandise.product.id", + "destKeys": "product_id" + }, + { + "sourceKeys": "cartLine.merchandise.product.title", + "destKeys": "variant" + }, + { + "sourceKeys": "cartLine.merchandise.product.type", + "destKeys": "category" + }, + { + "sourceKeys": "cartLine.merchandise.product.vendor", + "destKeys": "brand" + }, + { + "sourceKeys": "cartLine.merchandise.product.url", + "destKeys": "url" + }, + { + "sourceKeys": "cartLine.merchandise.sku", + "destKeys": "sku" + }, + { + "sourceKeys": "cartLine.merchandise.title", + "destKeys": "name" + }, + { + "sourceKeys": "cartLine.quantity", + "destKeys": "quantity" + } +] diff --git a/src/v1/sources/shopify/pixelEventsMappings/productViewedEventMapping.json b/src/v1/sources/shopify/pixelEventsMappings/productViewedEventMapping.json new file mode 100644 index 0000000000..becb15acfc --- /dev/null +++ b/src/v1/sources/shopify/pixelEventsMappings/productViewedEventMapping.json @@ -0,0 +1,46 @@ +[ + { + "sourceKeys": "productVariant.product.id", + "destKeys": "product_id" + }, + { + "sourceKeys": "productVariant.product.title", + "destKeys": "variant" + }, + { + "sourceKeys": "productVariant.product.vendor", + "destKeys": "brand" + }, + { + "sourceKeys": "productVariant.product.type", + "destKeys": "category" + }, + { + "sourceKeys": "productVariant.product.image.src", + "destKeys": "image_url" + }, + { + "sourceKeys": "productVariant.price.amount", + "destKeys": "price" + }, + { + "sourceKeys": "productVariant.price.currencyCode", + "destKeys": "currency" + }, + { + "sourceKeys": "productVariant.product.url", + "destKeys": "url" + }, + { + "sourceKeys": "productVariant.product.sku", + "destKeys": "sku" + }, + { + "sourceKeys": "productVariant.product.title", + "destKeys": "name" + }, + { + "sourceKeys": "cartLine.quantity", + "destKeys": "quantity" + } +] diff --git a/src/v1/sources/shopify/pixelTransform.js b/src/v1/sources/shopify/pixelTransform.js new file mode 100644 index 0000000000..0ca64b4123 --- /dev/null +++ b/src/v1/sources/shopify/pixelTransform.js @@ -0,0 +1,91 @@ +const stats = require('../../../util/stats'); +const logger = require('../../../logger'); +const { removeUndefinedAndNullValues } = require('../../../v0/util'); +const { + pageViewedEventBuilder, + cartViewedEventBuilder, + productListViewedEventBuilder, + productViewedEventBuilder, + productToCartEventBuilder, + checkoutEventBuilder, + checkoutStepEventBuilder, + searchEventBuilder, +} = require('./pixelUtils'); +const { INTEGERATION, PIXEL_EVENT_TOPICS } = require('./config'); + +const NO_OPERATION_SUCCESS = { + outputToSource: { + body: Buffer.from('OK').toString('base64'), + contentType: 'text/plain', + }, + statusCode: 200, +}; + +function processPixelEvent(inputEvent) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const { name, query_parameters, clientId, data } = inputEvent; + const { checkout } = data ?? {}; + const { order } = checkout ?? {}; + const { customer } = order ?? {}; + let message = {}; + switch (name) { + case PIXEL_EVENT_TOPICS.PAGE_VIEWED: + message = pageViewedEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.CART_VIEWED: + message = cartViewedEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.COLLECTION_VIEWED: + message = productListViewedEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.PRODUCT_VIEWED: + message = productViewedEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.PRODUCT_ADDED_TO_CART: + case PIXEL_EVENT_TOPICS.PRODUCT_REMOVED_FROM_CART: + message = productToCartEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.CHECKOUT_STARTED: + case PIXEL_EVENT_TOPICS.CHECKOUT_COMPLETED: + if (customer.id) message.userId = customer.id || ''; + message = checkoutEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.CHECKOUT_ADDRESS_INFO_SUBMITTED: + case PIXEL_EVENT_TOPICS.CHECKOUT_CONTACT_INFO_SUBMITTED: + case PIXEL_EVENT_TOPICS.CHECKOUT_SHIPPING_INFO_SUBMITTED: + case PIXEL_EVENT_TOPICS.PAYMENT_INFO_SUBMITTED: + if (customer.id) message.userId = customer.id || ''; + message = checkoutStepEventBuilder(inputEvent); + break; + case PIXEL_EVENT_TOPICS.SEARCH_SUBMITTED: + message = searchEventBuilder(inputEvent); + break; + default: + logger.debug(`{{SHOPIFY::}} Invalid pixel event ${name}`); + stats.increment('invalid_shopify_event', { + writeKey: query_parameters.writeKey, + source: 'SHOPIFY', + shopifyTopic: name, + }); + return NO_OPERATION_SUCCESS; + } + message.anonymousId = clientId; + message.setProperty(`integrations.${INTEGERATION}`, true); + message.setProperty('context.library', { + name: 'RudderStack Shopify Cloud', + eventOrigin: 'client', + version: '2.0.0', + }); + message.setProperty('context.topic', name); + message = removeUndefinedAndNullValues(message); + return message; +} + +const processEventFromPixel = async (event) => { + const pixelEvent = processPixelEvent(event); + return removeUndefinedAndNullValues(pixelEvent); +}; + +module.exports = { + processEventFromPixel, +}; diff --git a/src/v1/sources/shopify/pixelUtils.js b/src/v1/sources/shopify/pixelUtils.js new file mode 100644 index 0000000000..92197a558b --- /dev/null +++ b/src/v1/sources/shopify/pixelUtils.js @@ -0,0 +1,195 @@ +/* eslint-disable no-param-reassign */ +const Message = require('../../../v0/sources/message'); +const { EventType } = require('../../../constants'); +const { + INTEGERATION, + PIXEL_EVENT_MAPPING, + contextualFieldMappingJSON, + cartViewedEventMappingJSON, + productListViewedEventMappingJSON, + productViewedEventMappingJSON, + productToCartEventMappingJSON, + checkoutStartedCompletedEventMappingJSON, +} = require('./config'); + +function getNestedValue(object, path) { + const keys = path.split('.'); + return keys.reduce((nestedObject, key) => nestedObject && nestedObject[key], object); +} + +function setNestedValue(object, path, value) { + const keys = path.split('.'); + const lastKeyIndex = keys.length - 1; + keys.reduce((nestedObject, key, index) => { + if (index === lastKeyIndex) { + nestedObject[key] = value; + } else if (!nestedObject[key]) { + nestedObject[key] = {}; + } + return nestedObject[key]; + }, object); +} + +function mapObjectKeys(sourceObject, keyMappings) { + if (!Array.isArray(keyMappings)) { + throw new TypeError('keyMappings should be an array'); + } + const resultObject = { ...sourceObject }; + + // eslint-disable-next-line @typescript-eslint/no-shadow + return keyMappings.reduce((resultObject, { sourceKeys, destKeys }) => { + const value = getNestedValue(sourceObject, sourceKeys); + if (value !== undefined) { + setNestedValue(resultObject, destKeys, value); + } + return resultObject; + }, resultObject); +} + +const createMessage = (eventType, eventName, properties, context) => { + const message = new Message(INTEGERATION); + message.setEventType(eventType); + message.setEventName(eventName); + message.properties = properties; + message.context = context; + return message; +}; + +const pageViewedEventBuilder = (inputEvent) => { + const { data, context } = inputEvent; + const pageEventContextValues = mapObjectKeys(context, contextualFieldMappingJSON); + const message = new Message(INTEGERATION); + message.name = 'Page View'; + message.setEventType(EventType.PAGE); + message.properties = { ...data }; + message.context = { ...pageEventContextValues }; + return message; +}; + +const cartViewedEventBuilder = (inputEvent) => { + const lines = inputEvent?.data?.cart?.lines; + const products = []; + let total; + if (lines) { + lines.forEach((line) => { + const product = mapObjectKeys(line, cartViewedEventMappingJSON); + products.push(product); + total = line.cost.totalAmount.amount; + }); + } + + const properties = { + products, + cart_id: inputEvent.data.cart.id, + total, + }; + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + return createMessage(EventType.TRACK, 'Cart Viewed', properties, contextualPayload); +}; + +const productListViewedEventBuilder = (inputEvent) => { + const productVariants = inputEvent?.data?.collection?.productVariants; + const products = []; + + productVariants.forEach((productVariant) => { + const mappedProduct = mapObjectKeys(productVariant, productListViewedEventMappingJSON); + products.push(mappedProduct); + }); + + const properties = { + cart_id: inputEvent.clientId, + list_id: inputEvent.id, + products, + }; + + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + return createMessage(EventType.TRACK, 'Product List Viewed', properties, contextualPayload); +}; + +const productViewedEventBuilder = (inputEvent) => { + const properties = { + ...mapObjectKeys(inputEvent.data, productViewedEventMappingJSON), + }; + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + return createMessage(EventType.TRACK, 'Product Viewed', properties, contextualPayload); +}; + +const productToCartEventBuilder = (inputEvent) => { + const properties = { + ...mapObjectKeys(inputEvent.data, productToCartEventMappingJSON), + }; + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + return createMessage( + EventType.TRACK, + PIXEL_EVENT_MAPPING[inputEvent.name], + properties, + contextualPayload, + ); +}; + +const checkoutEventBuilder = (inputEvent) => { + const lineItems = inputEvent?.data?.checkout?.lineItems; + const products = []; + + lineItems.forEach((lineItem) => { + const mappedProduct = mapObjectKeys(lineItem, checkoutStartedCompletedEventMappingJSON); + products.push(mappedProduct); + }); + + const properties = { + products, + order_id: inputEvent.id, + checkout_id: inputEvent?.data?.checkout?.token, + total: inputEvent?.data?.checkout?.totalPrice?.amount, + currency: inputEvent?.data?.checkout?.currencyCode, + discount: inputEvent?.data?.checkout?.discountsAmount?.amount, + shipping: inputEvent?.data?.checkout?.shippingLine?.price?.amount, + revenue: inputEvent?.data?.checkout?.subtotalPrice?.amount, + value: inputEvent?.data?.checkout?.totalPrice?.amount, + tax: inputEvent?.data?.checkout?.totalTax?.amount, + }; + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + return createMessage( + EventType.TRACK, + PIXEL_EVENT_MAPPING[inputEvent.name], + properties, + contextualPayload, + ); +}; + +const checkoutStepEventBuilder = (inputEvent) => { + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + const properties = { + ...inputEvent.data.checkout, + }; + return createMessage( + EventType.TRACK, + PIXEL_EVENT_MAPPING[inputEvent.name], + properties, + contextualPayload, + ); +}; + +const searchEventBuilder = (inputEvent) => { + const properties = { + query: inputEvent.data.searchResult.query, + }; + const contextualPayload = mapObjectKeys(inputEvent.context, contextualFieldMappingJSON); + return createMessage( + EventType.TRACK, + PIXEL_EVENT_MAPPING[inputEvent.name], + properties, + contextualPayload, + ); +}; + +module.exports = { + pageViewedEventBuilder, + cartViewedEventBuilder, + productListViewedEventBuilder, + productViewedEventBuilder, + productToCartEventBuilder, + checkoutEventBuilder, + checkoutStepEventBuilder, + searchEventBuilder, +}; diff --git a/src/v1/sources/shopify/pixelUtils.test.js b/src/v1/sources/shopify/pixelUtils.test.js new file mode 100644 index 0000000000..cd544568cd --- /dev/null +++ b/src/v1/sources/shopify/pixelUtils.test.js @@ -0,0 +1,244 @@ +const { + pageViewedEventBuilder, + cartViewedEventBuilder, + productListViewedEventBuilder, + productViewedEventBuilder, + productToCartEventBuilder, + checkoutEventBuilder, + checkoutStepEventBuilder, + searchEventBuilder, +} = require('./pixelUtils'); +const { EventType } = require('../../../constants'); +const Message = require('../../../v0/sources/message'); +jest.mock('ioredis', () => require('../../../../test/__mocks__/redis')); +jest.mock('../../../v0/sources/message'); + +describe('utilV2.js', () => { + beforeEach(() => { + Message.mockClear(); + }); + + describe('pageViewedEventBuilder', () => { + it('should build a page viewed event message', () => { + const inputEvent = { + data: { url: 'https://example.com' }, + context: { userAgent: 'Mozilla/5.0' }, + }; + const message = pageViewedEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.name).toBe('Page View'); + expect(message.properties).toEqual(inputEvent.data); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('cartViewedEventBuilder', () => { + it('should build a cart viewed event message', () => { + const inputEvent = { + data: { + cart: { + cost: { + totalAmount: { + amount: 1259.9, + currencyCode: 'USD', + }, + }, + lines: [ + { + cost: { + totalAmount: { + amount: 1259.9, + currencyCode: 'USD', + }, + }, + merchandise: { + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + title: 'The Multi-managed Snowboard', + }, + id: '41327143157873', + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + quantity: 2, + }, + ], + totalQuantity: 2, + attributes: [], + id: '123', + }, + }, + context: { userAgent: 'Mozilla/5.0' }, + }; + const message = cartViewedEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ + products: [ + { + name: 'Default Title', + price: 629.95, + quantity: 2, + variant: 'The Multi-managed Snowboard', + merchandise: { + id: '41327143157873', + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + title: 'The Multi-managed Snowboard', + }, + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + cost: { + totalAmount: { amount: 1259.9, currencyCode: 'USD' }, + }, + }, + ], + cart_id: '123', + total: 1259.9, + }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('productListViewedEventBuilder', () => { + it('should build a product list viewed event message', () => { + const inputEvent = { + data: { + collection: { + productVariants: [ + { id: 'product123', name: 'Product 123' }, + { id: 'product456', name: 'Product 456' }, + ], + }, + }, + clientId: 'client123', + id: 'list123', + context: { userAgent: 'Mozilla/5.0' }, + }; + const message = productListViewedEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ + cart_id: 'client123', + list_id: 'list123', + products: [ + { id: 'product123', name: 'Product 123' }, + { id: 'product456', name: 'Product 456' }, + ], + }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('productViewedEventBuilder', () => { + it('should build a product viewed event message', () => { + const inputEvent = { + data: { id: 'product123', name: 'Product 123' }, + context: { userAgent: 'Mozilla/5.0' }, + }; + const message = productViewedEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ id: 'product123', name: 'Product 123' }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('productToCartEventBuilder', () => { + it('should build a product to cart event message', () => { + const inputEvent = { + data: { id: 'product123', name: 'Product 123' }, + context: { userAgent: 'Mozilla/5.0' }, + name: 'add_to_cart', + }; + const message = productToCartEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ id: 'product123', name: 'Product 123' }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('checkoutEventBuilder', () => { + it('should build a checkout event message', () => { + const inputEvent = { + data: { + checkout: { + lineItems: [ + { id: 'product123', name: 'Product 123' }, + { id: 'product456', name: 'Product 456' }, + ], + token: 'checkout123', + totalPrice: { amount: 200 }, + currencyCode: 'USD', + discountsAmount: { amount: 10 }, + shippingLine: { price: { amount: 5 } }, + subtotalPrice: { amount: 185 }, + totalTax: { amount: 15 }, + }, + }, + id: 'order123', + context: { userAgent: 'Mozilla/5.0' }, + name: 'checkout_started', + }; + const message = checkoutEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ + products: [ + { id: 'product123', name: 'Product 123' }, + { id: 'product456', name: 'Product 456' }, + ], + order_id: 'order123', + checkout_id: 'checkout123', + total: 200, + currency: 'USD', + discount: 10, + shipping: 5, + revenue: 185, + value: 200, + tax: 15, + }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('checkoutStepEventBuilder', () => { + it('should build a checkout step event message', () => { + const inputEvent = { + data: { + checkout: { + step: 1, + action: 'shipping_info_submitted', + }, + }, + context: { userAgent: 'Mozilla/5.0' }, + name: 'checkout_step', + }; + const message = checkoutStepEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ step: 1, action: 'shipping_info_submitted' }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); + + describe('searchEventBuilder', () => { + it('should build a search event message', () => { + const inputEvent = { + data: { + searchResult: { + query: 'test query', + }, + }, + context: { userAgent: 'Mozilla/5.0' }, + name: 'search_submitted', + }; + const message = searchEventBuilder(inputEvent); + expect(message).toBeInstanceOf(Message); + expect(message.properties).toEqual({ query: 'test query' }); + expect(message.context).toEqual({ userAgent: 'Mozilla/5.0' }); + }); + }); +}); diff --git a/src/v1/sources/shopify/transform.js b/src/v1/sources/shopify/transform.js new file mode 100644 index 0000000000..dee5a14a9d --- /dev/null +++ b/src/v1/sources/shopify/transform.js @@ -0,0 +1,21 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +const { processEventFromPixel } = require('./pixelTransform'); +const { process: processWebhookEvents } = require('../../../v0/sources/shopify/transform'); + +const process = async (inputEvent) => { + const { event } = inputEvent; + // check on the source Config to identify the event is from the tracker-based (legacy) + // or the pixel-based (latest) implementation. + const { pixelEventLabel: pixelClientEventLabel } = event; + if (pixelClientEventLabel) { + // this is a event fired from the web pixel loaded on the browser + // by the user interactions with the store. + const responseV2 = await processEventFromPixel(event); + return responseV2; + } + // this is for common logic for server-side events processing for both pixel and tracker apps. + const response = await processWebhookEvents(event); + return response; +}; + +module.exports = { process }; diff --git a/src/warehouse/index.js b/src/warehouse/index.js index 3c2b04079d..3491a257da 100644 --- a/src/warehouse/index.js +++ b/src/warehouse/index.js @@ -30,7 +30,7 @@ const whExtractEventTableColumnMappingRules = require('./config/WHExtractEventTa const maxColumnsInEvent = parseInt(process.env.WH_MAX_COLUMNS_IN_EVENT || '200', 10); const WH_POPULATE_SRC_DEST_INFO_IN_CONTEXT = - process.env.WH_POPULATE_SRC_DEST_INFO_IN_CONTEXT || true; + process.env.WH_POPULATE_SRC_DEST_INFO_IN_CONTEXT !== 'false'; const getDataType = (key, val, options, jsonKey = false) => { const type = typeof val; @@ -643,6 +643,18 @@ function processWarehouseMessage(message, options) { const skipReservedKeywordsEscaping = options.integrationOptions.skipReservedKeywordsEscaping || false; + // underscoreDivideNumbers when set to false, if a column has a format like "_v_3_", it will be formatted to "_v3_" + // underscoreDivideNumbers when set to true, if a column has a format like "_v_3_", we keep it like that + // For older destinations, it will come as true and for new destinations this config will not be present which means we will treat it as false. + options.underscoreDivideNumbers = options.destConfig?.underscoreDivideNumbers || false; + + // allowUsersContextTraits when set to true, if context.traits.* is present, it will be added as context_traits_* and *, + // e.g., for context.traits.name, context_traits_name and name will be added to the user's table. + // allowUsersContextTraits when set to false, if context.traits.* is present, it will be added only as context_traits_* + // e.g., for context.traits.name, only context_traits_name will be added to the user's table. + // For older destinations, it will come as true, and for new destinations this config will not be present, which means we will treat it as false. + const allowUsersContextTraits = options.destConfig?.allowUsersContextTraits || false; + addJsonKeysToOptions(options); if (isBlank(message.messageId)) { @@ -898,16 +910,18 @@ function processWarehouseMessage(message, options) { `${eventType + '_userProperties_'}`, 2, ); - setDataFromInputAndComputeColumnTypes( - utils, - eventType, - commonProps, - message.context ? message.context.traits : {}, - commonColumnTypes, - options, - `${eventType + '_context_traits_'}`, - 3, - ); + if (allowUsersContextTraits) { + setDataFromInputAndComputeColumnTypes( + utils, + eventType, + commonProps, + message.context ? message.context.traits : {}, + commonColumnTypes, + options, + `${eventType + '_context_traits_'}`, + 3, + ); + } setDataFromInputAndComputeColumnTypes( utils, eventType, @@ -987,11 +1001,23 @@ function processWarehouseMessage(message, options) { const usersEvent = { ...commonProps }; const usersColumnTypes = {}; + let userColumnMappingRules = whUserColumnMappingRules; + if (!isDataLakeProvider(options.provider)) { + userColumnMappingRules = { + ...userColumnMappingRules, + ...{ + sent_at: 'sentAt', + timestamp: 'timestamp', + original_timestamp: 'originalTimestamp', + }, + }; + } + setDataFromColumnMappingAndComputeColumnTypes( utils, usersEvent, message, - whUserColumnMappingRules, + userColumnMappingRules, usersColumnTypes, options, ); diff --git a/src/warehouse/snakecase/snakecase.js b/src/warehouse/snakecase/snakecase.js new file mode 100644 index 0000000000..1e6586e7f2 --- /dev/null +++ b/src/warehouse/snakecase/snakecase.js @@ -0,0 +1,37 @@ +const { toString } = require('lodash'); +const { unicodeWords, unicodeWordsWithNumbers } = require('./unicodeWords'); + +const hasUnicodeWord = RegExp.prototype.test.bind( + /[a-z][A-Z]|[A-Z]{2}[a-z]|[0-9][a-zA-Z]|[a-zA-Z][0-9]|[^a-zA-Z0-9 ]/, +); + +/** Used to match words composed of alphanumeric characters. */ +// eslint-disable-next-line no-control-regex +const reAsciiWord = /[^\x00-\x2f\x3a-\x40\x5b-\x60\x7b-\x7f]+/g; + +function asciiWords(string) { + return string.match(reAsciiWord); +} + +function words(string) { + const result = hasUnicodeWord(string) ? unicodeWords(string) : asciiWords(string); + return result || []; +} + +function wordsWithNumbers(string) { + const result = hasUnicodeWord(string) ? unicodeWordsWithNumbers(string) : asciiWords(string); + return result || []; +} + +const snakeCase = (string) => + words(toString(string).replace(/['\u2019]/g, '')).reduce( + (result, word, index) => result + (index ? '_' : '') + word.toLowerCase(), + '', + ); +const snakeCaseWithNumbers = (string) => + wordsWithNumbers(toString(string).replace(/['\u2019]/g, '')).reduce( + (result, word, index) => result + (index ? '_' : '') + word.toLowerCase(), + '', + ); + +module.exports = { words, wordsWithNumbers, snakeCase, snakeCaseWithNumbers }; diff --git a/src/warehouse/snakecase/unicodeWords.js b/src/warehouse/snakecase/unicodeWords.js new file mode 100644 index 0000000000..d7b15806c7 --- /dev/null +++ b/src/warehouse/snakecase/unicodeWords.js @@ -0,0 +1,94 @@ +/** Used to compose unicode character classes. */ +const rsAstralRange = '\\ud800-\\udfff'; +const rsComboMarksRange = '\\u0300-\\u036f'; +const reComboHalfMarksRange = '\\ufe20-\\ufe2f'; +const rsComboSymbolsRange = '\\u20d0-\\u20ff'; +const rsComboMarksExtendedRange = '\\u1ab0-\\u1aff'; +const rsComboMarksSupplementRange = '\\u1dc0-\\u1dff'; +const rsComboRange = + rsComboMarksRange + + reComboHalfMarksRange + + rsComboSymbolsRange + + rsComboMarksExtendedRange + + rsComboMarksSupplementRange; +const rsDingbatRange = '\\u2700-\\u27bf'; +const rsLowerRange = 'a-z\\xdf-\\xf6\\xf8-\\xff'; +const rsMathOpRange = '\\xac\\xb1\\xd7\\xf7'; +const rsNonCharRange = '\\x00-\\x2f\\x3a-\\x40\\x5b-\\x60\\x7b-\\xbf'; +const rsPunctuationRange = '\\u2000-\\u206f'; +const rsSpaceRange = + ' \\t\\x0b\\f\\xa0\\ufeff\\n\\r\\u2028\\u2029\\u1680\\u180e\\u2000\\u2001\\u2002\\u2003\\u2004\\u2005\\u2006\\u2007\\u2008\\u2009\\u200a\\u202f\\u205f\\u3000'; +const rsUpperRange = 'A-Z\\xc0-\\xd6\\xd8-\\xde'; +const rsVarRange = '\\ufe0e\\ufe0f'; +const rsBreakRange = rsMathOpRange + rsNonCharRange + rsPunctuationRange + rsSpaceRange; + +/** Used to compose unicode capture groups. */ +const rsApos = "['\u2019]"; +const rsBreak = `[${rsBreakRange}]`; +const rsCombo = `[${rsComboRange}]`; +const rsDigit = '\\d'; +const rsDingbat = `[${rsDingbatRange}]`; +const rsLower = `[${rsLowerRange}]`; +const rsMisc = `[^${rsAstralRange}${rsBreakRange + rsDigit + rsDingbatRange + rsLowerRange + rsUpperRange}]`; +const rsFitz = '\\ud83c[\\udffb-\\udfff]'; +const rsModifier = `(?:${rsCombo}|${rsFitz})`; +const rsNonAstral = `[^${rsAstralRange}]`; +const rsRegional = '(?:\\ud83c[\\udde6-\\uddff]){2}'; +const rsSurrPair = '[\\ud800-\\udbff][\\udc00-\\udfff]'; +const rsUpper = `[${rsUpperRange}]`; +const rsZWJ = '\\u200d'; + +/** Used to compose unicode regexes. */ +const rsMiscLower = `(?:${rsLower}|${rsMisc})`; +const rsMiscUpper = `(?:${rsUpper}|${rsMisc})`; +const rsOptContrLower = `(?:${rsApos}(?:d|ll|m|re|s|t|ve))?`; +const rsOptContrUpper = `(?:${rsApos}(?:D|LL|M|RE|S|T|VE))?`; +const reOptMod = `${rsModifier}?`; +const rsOptVar = `[${rsVarRange}]?`; +const rsOptJoin = `(?:${rsZWJ}(?:${[rsNonAstral, rsRegional, rsSurrPair].join('|')})${rsOptVar + reOptMod})*`; +const rsOrdLower = '\\d*(?:1st|2nd|3rd|(?![123])\\dth)(?=\\b|[A-Z_])'; +const rsOrdUpper = '\\d*(?:1ST|2ND|3RD|(?![123])\\dTH)(?=\\b|[a-z_])'; +const rsSeq = rsOptVar + reOptMod + rsOptJoin; +const rsEmoji = `(?:${[rsDingbat, rsRegional, rsSurrPair].join('|')})${rsSeq}`; + +const reUnicodeWords = RegExp( + [ + `${rsUpper}?${rsLower}+${rsOptContrLower}(?=${[rsBreak, rsUpper, '$'].join('|')})`, // Regular words, lowercase letters followed by optional contractions + `${rsMiscUpper}+${rsOptContrUpper}(?=${[rsBreak, rsUpper + rsMiscLower, '$'].join('|')})`, // Miscellaneous uppercase characters with optional contractions + `${rsUpper}?${rsMiscLower}+${rsOptContrLower}`, // Miscellaneous lowercase sequences with optional contractions + `${rsUpper}+${rsOptContrUpper}`, // All uppercase words with optional contractions (e.g., "THIS") + rsOrdUpper, // Ordinals for uppercase (e.g., "1ST", "2ND") + rsOrdLower, // Ordinals for lowercase (e.g., "1st", "2nd") + `${rsDigit}+`, // Pure digits (e.g., "123") + rsEmoji, // Emojis (e.g., 😀, ❤️) + ].join('|'), + 'g', +); + +const reUnicodeWordsWithNumbers = RegExp( + [ + `${rsUpper}?${rsLower}+${rsDigit}+`, // Lowercase letters followed by digits (e.g., "abc123") + `${rsUpper}+${rsDigit}+`, // Uppercase letters followed by digits (e.g., "ABC123") + `${rsDigit}+${rsUpper}?${rsLower}+`, // Digits followed by lowercase letters (e.g., "123abc") + `${rsDigit}+${rsUpper}+`, // Digits followed by uppercase letters (e.g., "123ABC") + `${rsUpper}?${rsLower}+${rsOptContrLower}(?=${[rsBreak, rsUpper, '$'].join('|')})`, // Regular words, lowercase letters followed by optional contractions + `${rsMiscUpper}+${rsOptContrUpper}(?=${[rsBreak, rsUpper + rsMiscLower, '$'].join('|')})`, // Miscellaneous uppercase characters with optional contractions + `${rsUpper}?${rsMiscLower}+${rsOptContrLower}`, // Miscellaneous lowercase sequences with optional contractions + `${rsUpper}+${rsOptContrUpper}`, // All uppercase words with optional contractions (e.g., "THIS") + rsOrdUpper, // Ordinals for uppercase (e.g., "1ST", "2ND") + rsOrdLower, // Ordinals for lowercase (e.g., "1st", "2nd") + `${rsDigit}+`, // Pure digits (e.g., "123") + rsEmoji, // Emojis (e.g., 😀, ❤️) + ].join('|'), + 'g', +); + +function unicodeWords(string) { + return string.match(reUnicodeWords); +} + +function unicodeWordsWithNumbers(string) { + return string.match(reUnicodeWordsWithNumbers); +} + +module.exports = { unicodeWords, unicodeWordsWithNumbers }; diff --git a/src/warehouse/util.js b/src/warehouse/util.js index b4b22721fd..7f4e224a34 100644 --- a/src/warehouse/util.js +++ b/src/warehouse/util.js @@ -1,7 +1,5 @@ -const _ = require('lodash'); const get = require('get-value'); -const v0 = require('./v0/util'); const v1 = require('./v1/util'); const { PlatformError, InstrumentationError } = require('@rudderstack/integrations-lib'); const { isBlank } = require('./config/helpers'); @@ -112,14 +110,7 @@ function validTimestamp(input) { } function getVersionedUtils(schemaVersion) { - switch (schemaVersion) { - case 'v0': - return v0; - case 'v1': - return v1; - default: - return v1; - } + return v1; } function isRudderSourcesEvent(event) { diff --git a/src/warehouse/v0/util.js b/src/warehouse/v0/util.js deleted file mode 100644 index 5917f8ea08..0000000000 --- a/src/warehouse/v0/util.js +++ /dev/null @@ -1,87 +0,0 @@ -const reservedANSIKeywordsMap = require('../config/ReservedKeywords.json'); -const { isDataLakeProvider } = require('../config/helpers'); - -const toSnakeCase = (str) => { - if (!str) { - return ''; - } - return String(str) - .replace(/^[^A-Za-z0-9]*|[^A-Za-z0-9]*$/g, '') - .replace(/([a-z])([A-Z])/g, (m, a, b) => `${a}_${b.toLowerCase()}`) - .replace(/[^A-Za-z0-9]+|_+/g, '_') - .toLowerCase(); -}; - -function toSafeDBString(provider, name = '') { - let parsedStr = name; - if (parseInt(name[0], 10) >= 0) { - parsedStr = `_${name}`; - } - parsedStr = parsedStr.replace(/[^a-zA-Z0-9_]+/g, ''); - if (isDataLakeProvider(provider)) { - return parsedStr; - } - switch (provider) { - case 'postgres': - return parsedStr.substr(0, 63); - default: - return parsedStr.substr(0, 127); - } -} - -function safeTableName(provider, name = '') { - let tableName = name; - if (tableName === '') { - tableName = 'STRINGEMPTY'; - } - if (provider === 'snowflake') { - tableName = tableName.toUpperCase(); - } - if (provider === 'rs' || isDataLakeProvider(provider)) { - tableName = tableName.toLowerCase(); - } - if (provider === 'postgres') { - tableName = tableName.substr(0, 63); - tableName = tableName.toLowerCase(); - } - if (reservedANSIKeywordsMap[provider.toUpperCase()][tableName.toUpperCase()]) { - tableName = `_${tableName}`; - } - return tableName; -} - -function safeColumnName(provider, name = '') { - let columnName = name; - if (columnName === '') { - columnName = 'STRINGEMPTY'; - } - if (provider === 'snowflake') { - columnName = columnName.toUpperCase(); - } - if (provider === 'rs' || isDataLakeProvider(provider)) { - columnName = columnName.toLowerCase(); - } - if (provider === 'postgres') { - columnName = columnName.substr(0, 63); - columnName = columnName.toLowerCase(); - } - if (reservedANSIKeywordsMap[provider.toUpperCase()][columnName.toUpperCase()]) { - columnName = `_${columnName}`; - } - return columnName; -} - -function transformTableName(name = '') { - return toSnakeCase(name); -} - -function transformColumnName(provider, name = '') { - return toSafeDBString(provider, name); -} - -module.exports = { - safeColumnName, - safeTableName, - transformColumnName, - transformTableName, -}; diff --git a/src/warehouse/v1/util.js b/src/warehouse/v1/util.js index 1c44a2385e..d1289bc674 100644 --- a/src/warehouse/v1/util.js +++ b/src/warehouse/v1/util.js @@ -1,8 +1,7 @@ -const _ = require('lodash'); - const reservedANSIKeywordsMap = require('../config/ReservedKeywords.json'); const { isDataLakeProvider } = require('../config/helpers'); const { TransformationError } = require('@rudderstack/integrations-lib'); +const { snakeCase, snakeCaseWithNumbers } = require('../snakecase/snakecase'); function safeTableName(options, name = '') { const { provider } = options; @@ -82,7 +81,7 @@ function safeColumnName(options, name = '') { path to $1,00,000 to path_to_1_00_000 return an empty string if it couldn't find a char if its ascii value doesnt belong to numbers or english alphabets */ -function transformName(provider, name = '') { +function transformName(options, provider, name = '') { const extractedValues = []; let extractedValue = ''; for (let i = 0; i < name.length; i += 1) { @@ -104,14 +103,17 @@ function transformName(provider, name = '') { if (extractedValue !== '') { extractedValues.push(extractedValue); } + const underscoreDivideNumbers = options?.underscoreDivideNumbers || false; + const snakeCaseFn = underscoreDivideNumbers ? snakeCase : snakeCaseWithNumbers; + let key = extractedValues.join('_'); if (name.startsWith('_')) { // do not remove leading underscores to allow esacaping rudder keywords with underscore // _timestamp -> _timestamp // __timestamp -> __timestamp - key = name.match(/^_*/)[0] + _.snakeCase(key.replace(/^_*/, '')); + key = name.match(/^_*/)[0] + snakeCaseFn(key.replace(/^_*/, '')); } else { - key = _.snakeCase(key); + key = snakeCaseFn(key); } if (key !== '' && key.charCodeAt(0) >= 48 && key.charCodeAt(0) <= 57) { @@ -150,7 +152,7 @@ function toBlendoCase(name = '') { function transformTableName(options, name = '') { const useBlendoCasing = options.integrationOptions?.useBlendoCasing || false; - return useBlendoCasing ? toBlendoCase(name) : transformName('', name); + return useBlendoCasing ? toBlendoCase(name) : transformName(options, '', name); } function transformColumnName(options, name = '') { @@ -158,7 +160,7 @@ function transformColumnName(options, name = '') { const useBlendoCasing = options.integrationOptions?.useBlendoCasing || false; return useBlendoCasing ? transformNameToBlendoCase(provider, name) - : transformName(provider, name); + : transformName(options, provider, name); } module.exports = { diff --git a/test/__tests__/data/warehouse/events.js b/test/__tests__/data/warehouse/events.js index ef9cc21096..bca6f776be 100644 --- a/test/__tests__/data/warehouse/events.js +++ b/test/__tests__/data/warehouse/events.js @@ -8,7 +8,9 @@ const sampleEvents = { Config: { restApiKey: "dummyApiKey", prefixProperties: true, - useNativeSDK: false + useNativeSDK: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, DestinationDefinition: { DisplayName: "Braze", @@ -514,7 +516,9 @@ const sampleEvents = { Config: { restApiKey: "dummyApiKey", prefixProperties: true, - useNativeSDK: false + useNativeSDK: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, DestinationDefinition: { DisplayName: "Braze", @@ -1026,7 +1030,9 @@ const sampleEvents = { Config: { restApiKey: "dummyApiKey", prefixProperties: true, - useNativeSDK: false + useNativeSDK: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, DestinationDefinition: { DisplayName: "Braze", @@ -1283,7 +1289,9 @@ const sampleEvents = { mapToSingleEvent: false, trackAllPages: false, trackCategorisedPages: true, - trackNamedPages: false + trackNamedPages: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, Enabled: true }, @@ -1532,7 +1540,9 @@ const sampleEvents = { mapToSingleEvent: false, trackAllPages: true, trackCategorisedPages: false, - trackNamedPages: false + trackNamedPages: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, Enabled: true }, @@ -1752,7 +1762,9 @@ const sampleEvents = { Config: { apiKey: "dummyApiKey", prefixProperties: true, - useNativeSDK: false + useNativeSDK: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, DestinationDefinition: { DisplayName: "Kiss Metrics", @@ -2016,7 +2028,9 @@ const sampleEvents = { Config: { restApiKey: "dummyApiKey", prefixProperties: true, - useNativeSDK: false + useNativeSDK: false, + allowUsersContextTraits: true, + underscoreDivideNumbers: true }, DestinationDefinition: { DisplayName: "Braze", diff --git a/test/__tests__/data/warehouse/integration_options_events.js b/test/__tests__/data/warehouse/integration_options_events.js index 05a2d51abd..35eafb6a9b 100644 --- a/test/__tests__/data/warehouse/integration_options_events.js +++ b/test/__tests__/data/warehouse/integration_options_events.js @@ -20,7 +20,9 @@ const sampleEvents = { input: { destination: { Config: { - jsonPaths: " testMap.nestedMap, testArray" + jsonPaths: " testMap.nestedMap, testArray", + allowUsersContextTraits: true, + underscoreDivideNumbers: true } }, message: { @@ -731,7 +733,10 @@ const sampleEvents = { users: { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { type: "identify", @@ -880,6 +885,9 @@ const sampleEvents = { "email": "user123@email.com", "id": "user123", "phone": "+917836362334", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "received_at": "2020-01-24T06:29:02.403Z" }, "metadata": { @@ -906,6 +914,9 @@ const sampleEvents = { "id": "string", "phone": "string", "received_at": "datetime", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -1074,6 +1085,9 @@ const sampleEvents = { "email": "user123@email.com", "id": "user123", "phone": "+917836362334", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "received_at": "2020-01-24T06:29:02.403Z" }, "metadata": { @@ -1101,6 +1115,9 @@ const sampleEvents = { "loaded_at": "datetime", "phone": "string", "received_at": "datetime", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -1199,6 +1216,9 @@ const sampleEvents = { "EMAIL": "user123@email.com", "ID": "user123", "PHONE": "+917836362334", + "SENT_AT": "2021-01-03T17:02:53.195Z", + "ORIGINAL_TIMESTAMP": "2020-01-24T06:29:02.364Z", + "TIMESTAMP": "2020-01-24T06:29:02.403Z", "RECEIVED_AT": "2020-01-24T06:29:02.403Z" }, "metadata": { @@ -1225,6 +1245,9 @@ const sampleEvents = { "ID": "string", "PHONE": "string", "RECEIVED_AT": "datetime", + "SENT_AT": "datetime", + "TIMESTAMP": "datetime", + "ORIGINAL_TIMESTAMP": "datetime", "UUID_TS": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -1355,6 +1378,254 @@ const sampleEvents = { "table": "users" } } + ], + gcs_datalake: [ + { + "data": { + "timestamp": "2020-01-24T06:29:02.403Z", + "anonymous_id": "97c46c81-3140-456d-b2a9-690d70aaca35", + "channel": "web", + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "email": "user123@email.com", + "id": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "user_id": "user123" + }, + "metadata": { + "columns": { + "timestamp": "datetime", + "anonymous_id": "string", + "channel": "string", + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "email": "string", + "id": "string", + "original_timestamp": "datetime", + "phone": "string", + "received_at": "datetime", + "sent_at": "datetime", + "user_id": "string", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "identifies" + } + }, + { + "data": { + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "email": "user123@email.com", + "id": "user123", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z" + }, + "metadata": { + "columns": { + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "email": "string", + "id": "string", + "phone": "string", + "received_at": "datetime", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "users" + } + } + ], + azure_datalake: [ + { + "data": { + "timestamp": "2020-01-24T06:29:02.403Z", + "anonymous_id": "97c46c81-3140-456d-b2a9-690d70aaca35", + "channel": "web", + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "email": "user123@email.com", + "id": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "user_id": "user123" + }, + "metadata": { + "columns": { + "timestamp": "datetime", + "anonymous_id": "string", + "channel": "string", + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "email": "string", + "id": "string", + "original_timestamp": "datetime", + "phone": "string", + "received_at": "datetime", + "sent_at": "datetime", + "user_id": "string", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "identifies" + } + }, + { + "data": { + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "email": "user123@email.com", + "id": "user123", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z" + }, + "metadata": { + "columns": { + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "email": "string", + "id": "string", + "phone": "string", + "received_at": "datetime", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "users" + } + } ] } } @@ -1374,6 +1645,18 @@ function opOutput(eventType, provider) { return _.cloneDeep(sampleEvents[eventType].output.rs); case "bq": return _.cloneDeep(sampleEvents[eventType].output.bq); + case "gcs_datalake": + if (eventType === 'users') { + return _.cloneDeep(sampleEvents[eventType].output.gcs_datalake); + } else { + return _.cloneDeep(sampleEvents[eventType].output.default); + } + case "azure_datalake": + if (eventType === 'users') { + return _.cloneDeep(sampleEvents[eventType].output.azure_datalake); + } else { + return _.cloneDeep(sampleEvents[eventType].output.default); + } default: return _.cloneDeep(sampleEvents[eventType].output.default); } diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/aliases.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/aliases.js index ee748a1a2b..30cce51fb7 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/aliases.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/aliases.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/extract.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/extract.js index 906c3ada23..9d38ecc292 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/extract.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/extract.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/groups.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/groups.js index 13b243f3a7..bbae993b27 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/groups.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/groups.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/identifies.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/identifies.js index 47b7f1209a..42f5cccf49 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/identifies.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/identifies.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { type: "identify", @@ -230,6 +233,9 @@ module.exports = { "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", "t_map_nested_map_n_1": "nested prop 1", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "up_map_nested_map_n_1": "nested prop 1" }, "metadata": { @@ -261,6 +267,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map_n_1": "string", "up_map_nested_map_n_1": "string", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -374,6 +383,9 @@ module.exports = { "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", "t_map_nested_map_n_1": "nested prop 1", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "up_map_nested_map_n_1": "nested prop 1" }, "metadata": { @@ -405,6 +417,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map_n_1": "string", "up_map_nested_map_n_1": "string", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -519,6 +534,9 @@ module.exports = { "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", "t_map_nested_map_n_1": "nested prop 1", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "up_map_nested_map_n_1": "nested prop 1" }, "metadata": { @@ -551,6 +569,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map_n_1": "string", "up_map_nested_map_n_1": "string", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -664,6 +685,9 @@ module.exports = { "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", "t_map_nested_map_n_1": "nested prop 1", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "up_map_nested_map_n_1": "nested prop 1" }, "metadata": { @@ -695,6 +719,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map_n_1": "string", "up_map_nested_map_n_1": "string", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -808,6 +835,9 @@ module.exports = { "PHONE": "+917836362334", "RECEIVED_AT": "2020-01-24T06:29:02.403Z", "T_MAP_NESTED_MAP_N_1": "nested prop 1", + "SENT_AT": "2021-01-03T17:02:53.195Z", + "ORIGINAL_TIMESTAMP": "2020-01-24T06:29:02.364Z", + "TIMESTAMP": "2020-01-24T06:29:02.403Z", "UP_MAP_NESTED_MAP_N_1": "nested prop 1" }, "metadata": { @@ -839,6 +869,9 @@ module.exports = { "RECEIVED_AT": "datetime", "T_MAP_NESTED_MAP_N_1": "string", "UP_MAP_NESTED_MAP_N_1": "string", + "SENT_AT": "datetime", + "TIMESTAMP": "datetime", + "ORIGINAL_TIMESTAMP": "datetime", "UUID_TS": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -846,5 +879,149 @@ module.exports = { } } ], + datalake: [ + { + "data": { + "anonymous_id": "97c46c81-3140-456d-b2a9-690d70aaca35", + "channel": "web", + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_c_map_nested_map_n_1": "context nested prop 1", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_ct_map_nested_map_n_1": "nested prop 1", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "ct_map_nested_map_n_1": "nested prop 1", + "email": "user123@email.com", + "id": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "t_map_nested_map_n_1": "nested prop 1", + "timestamp": "2020-01-24T06:29:02.403Z", + "up_map_nested_map_n_1": "nested prop 1", + "user_id": "user123" + }, + "metadata": { + "columns": { + "anonymous_id": "string", + "channel": "string", + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_c_map_nested_map_n_1": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_ct_map_nested_map_n_1": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "ct_map_nested_map_n_1": "string", + "email": "string", + "id": "string", + "original_timestamp": "datetime", + "phone": "string", + "received_at": "datetime", + "sent_at": "datetime", + "t_map_nested_map_n_1": "string", + "timestamp": "datetime", + "up_map_nested_map_n_1": "string", + "user_id": "string", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "identifies" + } + }, + { + "data": { + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_c_map_nested_map_n_1": "context nested prop 1", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_ct_map_nested_map_n_1": "nested prop 1", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "ct_map_nested_map_n_1": "nested prop 1", + "email": "user123@email.com", + "id": "user123", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z", + "t_map_nested_map_n_1": "nested prop 1", + "up_map_nested_map_n_1": "nested prop 1" + }, + "metadata": { + "columns": { + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_c_map_nested_map_n_1": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_ct_map_nested_map_n_1": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "ct_map_nested_map_n_1": "string", + "email": "string", + "id": "string", + "phone": "string", + "received_at": "datetime", + "t_map_nested_map_n_1": "string", + "up_map_nested_map_n_1": "string", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "users" + } + } + ] } } diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/pages.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/pages.js index 0aac8a3c23..dc2d3a3318 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/pages.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/pages.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/screens.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/screens.js index bad325c908..9b789d2747 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/screens.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/screens.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/tracks.js b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/tracks.js index 5070284e99..691ce57ca1 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/tracks.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/legacy/tracks.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/aliases.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/aliases.js index 91f19c5c77..d5d57f85b9 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/aliases.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/aliases.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/extract.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/extract.js index 7a36e4787e..a950b7f526 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/extract.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/extract.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/groups.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/groups.js index f84f9d33ed..6979aa1100 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/groups.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/groups.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/identifies.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/identifies.js index 3d6164b430..89fcc23cd5 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/identifies.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/identifies.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { type: "identify", @@ -230,6 +233,9 @@ module.exports = { "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", "t_map_nested_map_n_1": "nested prop 1", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "up_map_nested_map_n_1": "nested prop 1" }, "metadata": { @@ -261,6 +267,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map_n_1": "string", "up_map_nested_map_n_1": "string", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -373,6 +382,9 @@ module.exports = { "id": "user123", "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "t_map_nested_map": "{\"n1\":\"nested prop 1\"}", "up_map_nested_map": "{\"n1\":\"nested prop 1\"}" }, @@ -405,6 +417,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map": "json", "up_map_nested_map": "json", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -518,6 +533,9 @@ module.exports = { "id": "user123", "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "t_map_nested_map": "{\"n1\":\"nested prop 1\"}", "up_map_nested_map": "{\"n1\":\"nested prop 1\"}" }, @@ -551,6 +569,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map": "string", "up_map_nested_map": "string", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -663,6 +684,9 @@ module.exports = { "id": "user123", "phone": "+917836362334", "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "timestamp": "2020-01-24T06:29:02.403Z", "t_map_nested_map": "{\"n1\":\"nested prop 1\"}", "up_map_nested_map": "{\"n1\":\"nested prop 1\"}" }, @@ -695,6 +719,9 @@ module.exports = { "received_at": "datetime", "t_map_nested_map": "json", "up_map_nested_map": "json", + "sent_at": "datetime", + "timestamp": "datetime", + "original_timestamp": "datetime", "uuid_ts": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -807,6 +834,9 @@ module.exports = { "ID": "user123", "PHONE": "+917836362334", "RECEIVED_AT": "2020-01-24T06:29:02.403Z", + "SENT_AT": "2021-01-03T17:02:53.195Z", + "ORIGINAL_TIMESTAMP": "2020-01-24T06:29:02.364Z", + "TIMESTAMP": "2020-01-24T06:29:02.403Z", "T_MAP_NESTED_MAP": "{\"n1\":\"nested prop 1\"}", "UP_MAP_NESTED_MAP": "{\"n1\":\"nested prop 1\"}" }, @@ -839,6 +869,9 @@ module.exports = { "RECEIVED_AT": "datetime", "T_MAP_NESTED_MAP": "json", "UP_MAP_NESTED_MAP": "json", + "SENT_AT": "datetime", + "TIMESTAMP": "datetime", + "ORIGINAL_TIMESTAMP": "datetime", "UUID_TS": "datetime" }, "receivedAt": "2020-01-24T11:59:02.403+05:30", @@ -846,5 +879,149 @@ module.exports = { } } ], + datalake: [ + { + "data": { + "anonymous_id": "97c46c81-3140-456d-b2a9-690d70aaca35", + "channel": "web", + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_c_map_nested_map_n_1": "context nested prop 1", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_ct_map_nested_map_n_1": "nested prop 1", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "ct_map_nested_map_n_1": "nested prop 1", + "email": "user123@email.com", + "id": "2116ef8c-efc3-4ca4-851b-02ee60dad6ff", + "original_timestamp": "2020-01-24T06:29:02.364Z", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z", + "sent_at": "2021-01-03T17:02:53.195Z", + "t_map_nested_map_n_1": "nested prop 1", + "timestamp": "2020-01-24T06:29:02.403Z", + "up_map_nested_map_n_1": "nested prop 1", + "user_id": "user123" + }, + "metadata": { + "columns": { + "anonymous_id": "string", + "channel": "string", + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_c_map_nested_map_n_1": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_ct_map_nested_map_n_1": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "ct_map_nested_map_n_1": "string", + "email": "string", + "id": "string", + "original_timestamp": "datetime", + "phone": "string", + "received_at": "datetime", + "sent_at": "datetime", + "t_map_nested_map_n_1": "string", + "timestamp": "datetime", + "up_map_nested_map_n_1": "string", + "user_id": "string", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "identifies" + } + }, + { + "data": { + "context_app_build": "1.0.0", + "context_app_name": "RudderLabs JavaScript SDK", + "context_app_namespace": "com.rudderlabs.javascript", + "context_app_version": "1.1.11", + "context_c_map_nested_map_n_1": "context nested prop 1", + "context_device_id": "id", + "context_device_token": "token", + "context_device_type": "ios", + "context_ip": "[::1]:53708", + "context_library_name": "RudderLabs JavaScript SDK", + "context_library_version": "1.1.11", + "context_locale": "en-US", + "context_os_name": "android", + "context_os_version": "1.12.3", + "context_request_ip": "[::1]:53708", + "context_traits_ct_map_nested_map_n_1": "nested prop 1", + "context_traits_email": "user123@email.com", + "context_traits_phone": "+917836362334", + "context_traits_user_id": "user123", + "context_user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.16; rv:84.0) Gecko/20100101 Firefox/84.0", + "ct_map_nested_map_n_1": "nested prop 1", + "email": "user123@email.com", + "id": "user123", + "phone": "+917836362334", + "received_at": "2020-01-24T06:29:02.403Z", + "t_map_nested_map_n_1": "nested prop 1", + "up_map_nested_map_n_1": "nested prop 1" + }, + "metadata": { + "columns": { + "context_app_build": "string", + "context_app_name": "string", + "context_app_namespace": "string", + "context_app_version": "string", + "context_c_map_nested_map_n_1": "string", + "context_device_id": "string", + "context_device_token": "string", + "context_device_type": "string", + "context_ip": "string", + "context_library_name": "string", + "context_library_version": "string", + "context_locale": "string", + "context_os_name": "string", + "context_os_version": "string", + "context_request_ip": "string", + "context_traits_ct_map_nested_map_n_1": "string", + "context_traits_email": "string", + "context_traits_phone": "string", + "context_traits_user_id": "string", + "context_user_agent": "string", + "ct_map_nested_map_n_1": "string", + "email": "string", + "id": "string", + "phone": "string", + "received_at": "datetime", + "t_map_nested_map_n_1": "string", + "up_map_nested_map_n_1": "string", + "uuid_ts": "datetime" + }, + "receivedAt": "2020-01-24T11:59:02.403+05:30", + "table": "users" + } + } + ], } } diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/pages.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/pages.js index 136f355b21..d5282c1cfd 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/pages.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/pages.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/screens.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/screens.js index b11b311ebb..3eec47b89d 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/screens.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/screens.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/data/warehouse/integrations/jsonpaths/new/tracks.js b/test/__tests__/data/warehouse/integrations/jsonpaths/new/tracks.js index 7ed95685ff..6b411835db 100644 --- a/test/__tests__/data/warehouse/integrations/jsonpaths/new/tracks.js +++ b/test/__tests__/data/warehouse/integrations/jsonpaths/new/tracks.js @@ -1,7 +1,10 @@ module.exports = { input: { destination: { - Config: {} + Config: { + allowUsersContextTraits: true, + underscoreDivideNumbers: true + } }, message: { anonymousId: "e6ab2c5e-2cda-44a9-a962-e2f67df78bca", diff --git a/test/__tests__/snakecase.test.js b/test/__tests__/snakecase.test.js new file mode 100644 index 0000000000..146e2e53c6 --- /dev/null +++ b/test/__tests__/snakecase.test.js @@ -0,0 +1,497 @@ +const _ = require('lodash'); +const {words, wordsWithNumbers, snakeCase, snakeCaseWithNumbers} = require("../../src/warehouse/snakecase/snakecase"); + +const burredLetters = [ + // Latin-1 Supplement letters. + '\xc0', + '\xc1', + '\xc2', + '\xc3', + '\xc4', + '\xc5', + '\xc6', + '\xc7', + '\xc8', + '\xc9', + '\xca', + '\xcb', + '\xcc', + '\xcd', + '\xce', + '\xcf', + '\xd0', + '\xd1', + '\xd2', + '\xd3', + '\xd4', + '\xd5', + '\xd6', + '\xd8', + '\xd9', + '\xda', + '\xdb', + '\xdc', + '\xdd', + '\xde', + '\xdf', + '\xe0', + '\xe1', + '\xe2', + '\xe3', + '\xe4', + '\xe5', + '\xe6', + '\xe7', + '\xe8', + '\xe9', + '\xea', + '\xeb', + '\xec', + '\xed', + '\xee', + '\xef', + '\xf0', + '\xf1', + '\xf2', + '\xf3', + '\xf4', + '\xf5', + '\xf6', + '\xf8', + '\xf9', + '\xfa', + '\xfb', + '\xfc', + '\xfd', + '\xfe', + '\xff', + // Latin Extended-A letters. + '\u0100', + '\u0101', + '\u0102', + '\u0103', + '\u0104', + '\u0105', + '\u0106', + '\u0107', + '\u0108', + '\u0109', + '\u010a', + '\u010b', + '\u010c', + '\u010d', + '\u010e', + '\u010f', + '\u0110', + '\u0111', + '\u0112', + '\u0113', + '\u0114', + '\u0115', + '\u0116', + '\u0117', + '\u0118', + '\u0119', + '\u011a', + '\u011b', + '\u011c', + '\u011d', + '\u011e', + '\u011f', + '\u0120', + '\u0121', + '\u0122', + '\u0123', + '\u0124', + '\u0125', + '\u0126', + '\u0127', + '\u0128', + '\u0129', + '\u012a', + '\u012b', + '\u012c', + '\u012d', + '\u012e', + '\u012f', + '\u0130', + '\u0131', + '\u0132', + '\u0133', + '\u0134', + '\u0135', + '\u0136', + '\u0137', + '\u0138', + '\u0139', + '\u013a', + '\u013b', + '\u013c', + '\u013d', + '\u013e', + '\u013f', + '\u0140', + '\u0141', + '\u0142', + '\u0143', + '\u0144', + '\u0145', + '\u0146', + '\u0147', + '\u0148', + '\u0149', + '\u014a', + '\u014b', + '\u014c', + '\u014d', + '\u014e', + '\u014f', + '\u0150', + '\u0151', + '\u0152', + '\u0153', + '\u0154', + '\u0155', + '\u0156', + '\u0157', + '\u0158', + '\u0159', + '\u015a', + '\u015b', + '\u015c', + '\u015d', + '\u015e', + '\u015f', + '\u0160', + '\u0161', + '\u0162', + '\u0163', + '\u0164', + '\u0165', + '\u0166', + '\u0167', + '\u0168', + '\u0169', + '\u016a', + '\u016b', + '\u016c', + '\u016d', + '\u016e', + '\u016f', + '\u0170', + '\u0171', + '\u0172', + '\u0173', + '\u0174', + '\u0175', + '\u0176', + '\u0177', + '\u0178', + '\u0179', + '\u017a', + '\u017b', + '\u017c', + '\u017d', + '\u017e', + '\u017f', +]; +const emojiVar = '\ufe0f'; +const flag = '\ud83c\uddfa\ud83c\uddf8'; +const heart = `\u2764${emojiVar}`; +const hearts = '\ud83d\udc95'; +const comboGlyph = `\ud83d\udc68\u200d${heart}\u200d\ud83d\udc8B\u200d\ud83d\udc68`; +const leafs = '\ud83c\udf42'; +const rocket = '\ud83d\ude80'; +const stubTrue = function () { + return true; +}; +const stubString = function () { + return ''; +}; + +describe('words', () => { + it('should match words containing Latin Unicode letters', () => { + const expected = _.map(burredLetters, (letter) => [letter]); + const actual = _.map(burredLetters, (letter) => words(letter)); + expect(actual).toEqual(expected); + }); + + it('should work with compound words', () => { + expect(words('12ft')).toEqual(['12', 'ft']); + expect(words('aeiouAreVowels')).toEqual(['aeiou', 'Are', 'Vowels']); + expect(words('enable 6h format')).toEqual(['enable', '6', 'h', 'format']); + expect(words('enable 24H format')).toEqual(['enable', '24', 'H', 'format']); + expect(words('isISO8601')).toEqual(['is', 'ISO', '8601']); + expect(words('LETTERSAeiouAreVowels')).toEqual(['LETTERS', 'Aeiou', 'Are', 'Vowels']); + expect(words('tooLegit2Quit')).toEqual(['too', 'Legit', '2', 'Quit']); + expect(words('walk500Miles')).toEqual(['walk', '500', 'Miles']); + expect(words('xhr2Request')).toEqual(['xhr', '2', 'Request']); + expect(words('XMLHttp')).toEqual(['XML', 'Http']); + expect(words('XmlHTTP')).toEqual(['Xml', 'HTTP']); + expect(words('XmlHttp')).toEqual(['Xml', 'Http']); + }); + + it('should work with compound words containing diacritical marks', () => { + expect(words('LETTERSÆiouAreVowels')).toEqual(['LETTERS', 'Æiou', 'Are', 'Vowels']); + expect(words('æiouAreVowels')).toEqual(['æiou', 'Are', 'Vowels']); + expect(words('æiou2Consonants')).toEqual(['æiou', '2', 'Consonants']); + }); + + it('should not treat contractions as separate words', () => { + const postfixes = ['d', 'll', 'm', 're', 's', 't', 've']; + + _.each(["'", '\u2019'], (apos) => { + _.times(2, (index) => { + const actual = _.map(postfixes, (postfix) => { + const string = `a b${apos}${postfix} c`; + return words(string[index ? 'toUpperCase' : 'toLowerCase']()); + }); + const expected = _.map(postfixes, (postfix) => { + const words = ['a', `b${apos}${postfix}`, 'c']; + return _.map(words, (word) => + word[index ? 'toUpperCase' : 'toLowerCase'](), + ); + }); + expect(actual).toEqual(expected); + }); + }); + }); + + it('should not treat ordinal numbers as separate words', () => { + const ordinals = ['1st', '2nd', '3rd', '4th']; + + _.times(2, (index) => { + const expected = _.map(ordinals, (ordinal) => [ + ordinal[index ? 'toUpperCase' : 'toLowerCase'](), + ]); + const actual = _.map(expected, (expectedWords) => words(expectedWords[0])); + expect(actual).toEqual(expected); + }); + }); + + it('should prevent ReDoS', () => { + const largeWordLen = 50000; + const largeWord = 'A'.repeat(largeWordLen); + const maxMs = 1000; + const startTime = _.now(); + + expect(words(`${largeWord}ÆiouAreVowels`)).toEqual([largeWord, 'Æiou', 'Are', 'Vowels']); + + const endTime = _.now(); + const timeSpent = endTime - startTime; + + expect(timeSpent).toBeLessThan(maxMs); + }); + + it('should account for astral symbols', () => { + const string = `A ${leafs}, ${comboGlyph}, and ${rocket}`; + expect(words(string)).toEqual(['A', leafs, comboGlyph, 'and', rocket]); + }); + + it('should account for regional symbols', () => { + const pair = flag.match(/\ud83c[\udde6-\uddff]/g); + const regionals = pair.join(' '); + + expect(words(flag)).toEqual([flag]); + expect(words(regionals)).toEqual([pair[0], pair[1]]); + }); + + it('should account for variation selectors', () => { + expect(words(heart)).toEqual([heart]); + }); + + it('should match lone surrogates', () => { + const pair = hearts.split(''); + const surrogates = `${pair[0]} ${pair[1]}`; + + expect(words(surrogates)).toEqual([]); + }); +}); + +describe('wordsWithoutNumbers', () => { + it('should match words containing Latin Unicode letters', () => { + const expected = _.map(burredLetters, (letter) => [letter]); + const actual = _.map(burredLetters, (letter) => wordsWithNumbers(letter)); + expect(actual).toEqual(expected); + }); + + it('should work with compound words', () => { + expect(wordsWithNumbers('12ft')).toEqual(['12ft']); + expect(wordsWithNumbers('aeiouAreVowels')).toEqual(['aeiou', 'Are', 'Vowels']); + expect(wordsWithNumbers('enable 6h format')).toEqual(['enable', '6h', 'format']); + expect(wordsWithNumbers('enable 24H format')).toEqual(['enable', '24H', 'format']); + expect(wordsWithNumbers('isISO8601')).toEqual(['is', 'ISO8601']); + expect(wordsWithNumbers('LETTERSAeiouAreVowels')).toEqual(['LETTERS', 'Aeiou', 'Are', 'Vowels']); + expect(wordsWithNumbers('tooLegit2Quit')).toEqual(['too', 'Legit2', 'Quit']); + expect(wordsWithNumbers('walk500Miles')).toEqual(['walk500', 'Miles']); + expect(wordsWithNumbers('xhr2Request')).toEqual(['xhr2', 'Request']); + expect(wordsWithNumbers('XMLHttp')).toEqual(['XML', 'Http']); + expect(wordsWithNumbers('XmlHTTP')).toEqual(['Xml', 'HTTP']); + expect(wordsWithNumbers('XmlHttp')).toEqual(['Xml', 'Http']); + }); + + it('should work with compound words containing diacritical marks', () => { + expect(wordsWithNumbers('LETTERSÆiouAreVowels')).toEqual(['LETTERS', 'Æiou', 'Are', 'Vowels']); + expect(wordsWithNumbers('æiouAreVowels')).toEqual(['æiou', 'Are', 'Vowels']); + expect(wordsWithNumbers('æiou2Consonants')).toEqual(['æiou2', 'Consonants']); + }); + + it('should not treat contractions as separate words', () => { + const postfixes = ['d', 'll', 'm', 're', 's', 't', 've']; + + _.each(["'", '\u2019'], (apos) => { + _.times(2, (index) => { + const actual = _.map(postfixes, (postfix) => { + const string = `a b${apos}${postfix} c`; + return wordsWithNumbers(string[index ? 'toUpperCase' : 'toLowerCase']()); + }); + const expected = _.map(postfixes, (postfix) => { + const words = ['a', `b${apos}${postfix}`, 'c']; + return _.map(words, (word) => + word[index ? 'toUpperCase' : 'toLowerCase'](), + ); + }); + expect(actual).toEqual(expected); + }); + }); + }); + + it('should not treat ordinal numbers as separate words', () => { + const ordinals = ['1st', '2nd', '3rd', '4th']; + + _.times(2, (index) => { + const expected = _.map(ordinals, (ordinal) => [ + ordinal[index ? 'toUpperCase' : 'toLowerCase'](), + ]); + const actual = _.map(expected, (expectedWords) => wordsWithNumbers(expectedWords[0])); + expect(actual).toEqual(expected); + }); + }); + + it('should prevent ReDoS', () => { + const largeWordLen = 50000; + const largeWord = 'A'.repeat(largeWordLen); + const maxMs = 1000; + const startTime = _.now(); + + expect(wordsWithNumbers(`${largeWord}ÆiouAreVowels`)).toEqual([largeWord, 'Æiou', 'Are', 'Vowels']); + + const endTime = _.now(); + const timeSpent = endTime - startTime; + + expect(timeSpent).toBeLessThan(maxMs); + }); + + it('should account for astral symbols', () => { + const string = `A ${leafs}, ${comboGlyph}, and ${rocket}`; + expect(wordsWithNumbers(string)).toEqual(['A', leafs, comboGlyph, 'and', rocket]); + }); + + it('should account for regional symbols', () => { + const pair = flag.match(/\ud83c[\udde6-\uddff]/g); + const regionals = pair.join(' '); + + expect(wordsWithNumbers(flag)).toEqual([flag]); + expect(wordsWithNumbers(regionals)).toEqual([pair[0], pair[1]]); + }); + + it('should account for variation selectors', () => { + expect(wordsWithNumbers(heart)).toEqual([heart]); + }); + + it('should match lone surrogates', () => { + const pair = hearts.split(''); + const surrogates = `${pair[0]} ${pair[1]}`; + + expect(wordsWithNumbers(surrogates)).toEqual([]); + }); +}); + +describe('snakeCase snakeCaseWithNumbers', () => { + const caseMethods = { + snakeCase, + snakeCaseWithNumbers, + }; + + _.each(['snakeCase', 'snakeCaseWithNumbers'], (caseName) => { + const methodName = caseName; + const func = caseMethods[methodName]; + + const strings = [ + 'foo bar', + 'Foo bar', + 'foo Bar', + 'Foo Bar', + 'FOO BAR', + 'fooBar', + '--foo-bar--', + '__foo_bar__', + ]; + + const converted = (function () { + switch (caseName) { + case 'snakeCase': + return 'foo_bar'; + case 'snakeCaseWithNumbers': + return 'foo_bar'; + } + })(); + + it(`\`_.${methodName}\` should convert \`string\` to ${caseName} case`, () => { + const actual = _.map(strings, (string) => { + const expected = caseName === 'start' && string === 'FOO BAR' ? string : converted; + return func(string) === expected; + }); + expect(actual).toEqual(_.map(strings, stubTrue)); + }); + + it(`\`_.${methodName}\` should handle double-converting strings`, () => { + const actual = _.map(strings, (string) => { + const expected = caseName === 'start' && string === 'FOO BAR' ? string : converted; + return func(func(string)) === expected; + }); + expect(actual).toEqual(_.map(strings, stubTrue)); + }); + + it(`\`_.${methodName}\` should remove contraction apostrophes`, () => { + const postfixes = ['d', 'll', 'm', 're', 's', 't', 've']; + + _.each(["'", '\u2019'], (apos) => { + const actual = _.map(postfixes, (postfix) => + func(`a b${apos}${postfix} c`), + ); + const expected = _.map(postfixes, (postfix) => { + switch (caseName) { + case 'snakeCase': + return `a_b${postfix}_c`; + case 'snakeCaseWithNumbers': + return `a_b${postfix}_c`; + } + }); + expect(actual).toEqual(expected); + }); + }); + + it(`\`_.${methodName}\` should remove Latin mathematical operators`, () => { + const actual = _.map(['\xd7', '\xf7'], func); + expect(actual).toEqual(['', '']); + }); + + it(`\`_.${methodName}\` should coerce \`string\` to a string`, () => { + const string = 'foo bar'; + expect(func(Object(string))).toBe(converted); + expect(func({toString: _.constant(string)})).toBe(converted); + }); + + it(`\`_.${methodName}\` should return an empty string for empty values`, () => { + const values = [, null, undefined, '']; + const expected = _.map(values, stubString); + + const actual = _.map(values, (value, index) => + index ? func(value) : func(), + ); + + expect(actual).toEqual(expected); + }); + }); +}); \ No newline at end of file diff --git a/test/__tests__/warehouse.test.js b/test/__tests__/warehouse.test.js index 83b24aee15..6bde2e9eb2 100644 --- a/test/__tests__/warehouse.test.js +++ b/test/__tests__/warehouse.test.js @@ -1,7 +1,5 @@ const _ = require("lodash"); -const util = require("util"); - const { input, output } = require(`./data/warehouse/events.js`); const { opInput, @@ -21,6 +19,7 @@ const { const { validTimestamp } = require("../../src/warehouse/util.js"); +const {transformTableName, transformColumnName} = require("../../src/warehouse/v1/util"); const {isBlank} = require("../../src/warehouse/config/helpers.js"); const version = "v0"; @@ -1010,28 +1009,6 @@ describe("Add receivedAt for events missing it", () => { }); describe("Integration options", () => { - describe("Destination config options", () => { - destConfig.scenarios().forEach(scenario => { - it(scenario.name, () => { - if (scenario.skipUsersTable !== null) { - scenario.event.destination.Config.skipUsersTable = scenario.skipUsersTable - } - if (scenario.skipTracksTable !== null) { - scenario.event.destination.Config.skipTracksTable = scenario.skipTracksTable - } - - transformers.forEach((transformer, index) => { - const received = transformer.process(scenario.event); - expect(received).toHaveLength(scenario.expected.length); - for (const i in received) { - const evt = received[i]; - expect(evt.data.id ? evt.data.id : evt.data.ID).toEqual(scenario.expected[i].id); - expect(evt.metadata.table.toLowerCase()).toEqual(scenario.expected[i].table); - } - }); - }); - }); - }); describe("track", () => { it("should generate two events for every track call", () => { const i = opInput("track"); @@ -1058,7 +1035,7 @@ describe("Integration options", () => { }); describe("json paths", () => { - const output = (config, provider) => { + const output = (eventType, config, provider) => { switch (provider) { case "rs": return _.cloneDeep(config.output.rs); @@ -1068,6 +1045,14 @@ describe("Integration options", () => { return _.cloneDeep(config.output.postgres); case "snowflake": return _.cloneDeep(config.output.snowflake); + case "s3_datalake": + case "gcs_datalake": + case "azure_datalake": + if (eventType === 'identifies') { + return _.cloneDeep(config.output.datalake); + } else { + return _.cloneDeep(config.output.default); + } default: return _.cloneDeep(config.output.default); } @@ -1103,14 +1088,14 @@ describe("Integration options", () => { const config = require("./data/warehouse/integrations/jsonpaths/new/" + testCase.eventType); const input = _.cloneDeep(config.input); const received = transformer.process(input); - expect(received).toEqual(output(config, integrations[index])); + expect(received).toEqual(output(testCase.eventType, config, integrations[index])); }) it(`legacy ${testCase.eventType} for ${integrations[index]}`, () => { const config = require("./data/warehouse/integrations/jsonpaths/legacy/" + testCase.eventType); const input = _.cloneDeep(config.input); const received = transformer.process(input); - expect(received).toEqual(output(config, integrations[index])); + expect(received).toEqual(output(testCase.eventType, config, integrations[index])); }) }); } @@ -1235,6 +1220,306 @@ describe("isBlank", () => { } }); +describe("Destination config", () => { + describe("skipUsersTable, skipTracksTable", () => { + destConfig.scenarios().forEach(scenario => { + it(scenario.name, () => { + if (scenario.skipUsersTable !== null) { + scenario.event.destination.Config.skipUsersTable = scenario.skipUsersTable + } + if (scenario.skipTracksTable !== null) { + scenario.event.destination.Config.skipTracksTable = scenario.skipTracksTable + } + + transformers.forEach((transformer, index) => { + const received = transformer.process(scenario.event); + expect(received).toHaveLength(scenario.expected.length); + for (const i in received) { + const evt = received[i]; + expect(evt.data.id ? evt.data.id : evt.data.ID).toEqual(scenario.expected[i].id); + expect(evt.metadata.table.toLowerCase()).toEqual(scenario.expected[i].table); + } + }); + }); + }); + }); + + describe('allowUsersContextTraits, underscoreDivideNumbers', () => { + describe("old destinations", () => { + it('with allowUsersContextTraits', () => { + transformers.forEach((transformer, index) => { + const event = { + destination: { + Config: { + allowUsersContextTraits: true + } + }, + message: { + context: { + traits: { + city: "Disney", + country: "USA", + email: "mickey@disney.com", + firstname: "Mickey" + }, + }, + traits: { + lastname: "Mouse" + }, + type: "identify", + userId: "9bb5d4c2-a7aa-4a36-9efb-dd2b1aec5d33" + }, + request: { + query: { + whSchemaVersion: "v1" + } + } + } + const output = transformer.process(event); + const events = [output[0], output[1]]; // identifies and users event + const traitsToCheck = { + 'city': 'Disney', + 'country': 'USA', + 'email': 'mickey@disney.com', + 'firstname': 'Mickey' + }; + events.forEach(event => { + Object.entries(traitsToCheck).forEach(([trait, value]) => { + expect(event.data[integrationCasedString(integrations[index], trait)]).toEqual(value); + expect(event.data[integrationCasedString(integrations[index], `context_traits_${trait}`)]).toEqual(value); + expect(event.metadata.columns).toHaveProperty(integrationCasedString(integrations[index], trait)); + expect(event.metadata.columns).toHaveProperty(integrationCasedString(integrations[index], `context_traits_${trait}`)); + }); + }); + }); + }); + + it('with underscoreDivideNumbers', () => { + transformers.forEach((transformer, index) => { + const event = { + destination: { + Config: { + underscoreDivideNumbers: true + }, + }, + message: { + context: { + 'attribute v3': 'some-value' + }, + event: "button clicked v2", + type: "track", + }, + request: { + query: { + whSchemaVersion: "v1" + } + } + } + const output = transformer.process(event); + expect(output[0].data[integrationCasedString(integrations[index], `event`)]).toEqual('button_clicked_v_2'); + expect(output[0].data[integrationCasedString(integrations[index], `context_attribute_v_3`)]).toEqual('some-value'); + expect(output[0].metadata.columns).toHaveProperty(integrationCasedString(integrations[index], `context_attribute_v_3`)); + expect(output[1].data[integrationCasedString(integrations[index], `event`)]).toEqual('button_clicked_v_2'); + expect(output[1].data[integrationCasedString(integrations[index], `context_attribute_v_3`)]).toEqual('some-value'); + expect(output[1].metadata.table).toEqual(integrationCasedString(integrations[index], 'button_clicked_v_2')); + expect(output[1].metadata.columns).toHaveProperty(integrationCasedString(integrations[index], `context_attribute_v_3`)); + }); + }); + }); + describe("new destinations", () => { + it('without allowUsersContextTraits', () => { + transformers.forEach((transformer, index) => { + const event = { + destination: { + Config: {} + }, + message: { + context: { + traits: { + city: "Disney", + country: "USA", + email: "mickey@disney.com", + firstname: "Mickey" + }, + }, + traits: { + lastname: "Mouse" + }, + type: "identify", + userId: "9bb5d4c2-a7aa-4a36-9efb-dd2b1aec5d33" + }, + request: { + query: { + whSchemaVersion: "v1" + } + } + } + const received = transformer.process(event); + const events = [received[0], received[1]]; // identifies and users event + const traitsToCheck = { + 'city': 'Disney', + 'country': 'USA', + 'email': 'mickey@disney.com', + 'firstname': 'Mickey' + }; + events.forEach(event => { + Object.entries(traitsToCheck).forEach(([trait, value]) => { + expect(event.data).not.toHaveProperty(integrationCasedString(integrations[index], trait)); + expect(event.data[integrationCasedString(integrations[index], `context_traits_${trait}`)]).toEqual(value); + expect(event.metadata.columns).not.toHaveProperty(integrationCasedString(integrations[index], trait)); + expect(event.metadata.columns).toHaveProperty(integrationCasedString(integrations[index], `context_traits_${trait}`)); + }); + }); + }); + }); + + it('without underscoreDivideNumbers', () => { + transformers.forEach((transformer, index) => { + const event = { + destination: { + Config: {}, + }, + message: { + context: { + 'attribute v3': 'some-value' + }, + event: "button clicked v2", + type: "track", + }, + request: { + query: { + whSchemaVersion: "v1" + } + } + } + const output = transformer.process(event); + expect(output[0].data[integrationCasedString(integrations[index], `event`)]).toEqual('button_clicked_v2'); + expect(output[0].data[integrationCasedString(integrations[index], `context_attribute_v3`)]).toEqual('some-value'); + expect(output[0].metadata.columns).toHaveProperty(integrationCasedString(integrations[index], `context_attribute_v3`)); + expect(output[1].data[integrationCasedString(integrations[index], `event`)]).toEqual('button_clicked_v2'); + expect(output[1].data[integrationCasedString(integrations[index], `context_attribute_v3`)]).toEqual('some-value'); + expect(output[1].metadata.table).toEqual(integrationCasedString(integrations[index], 'button_clicked_v2')); + expect(output[1].metadata.columns).toHaveProperty(integrationCasedString(integrations[index], `context_attribute_v3`)); + }); + }); + }); + }); +}); + +describe("validTimestamp", () => { + const testCases = [ + { + name: "undefined input should return false", + input: undefined, + expected: false, + }, + { + name: "negative year and time input should return false #1", + input: '-0001-11-30T00:00:00+0000', + expected: false, + }, + { + name: "negative year and time input should return false #2", + input: '-2023-06-14T05:23:59.244Z', + expected: false, + }, + { + name: "negative year and time input should return false #3", + input: '-1900-06-14T05:23:59.244Z', + expected: false, + }, + { + name: "positive year and time input should return false", + input: '+2023-06-14T05:23:59.244Z', + expected: false, + }, + { + name: "valid timestamp input should return true", + input: '2023-06-14T05:23:59.244Z', + expected: true, + }, + { + name: "non-date string input should return false", + input: 'abc', + expected: false, + }, + { + name: "malicious string input should return false", + input: '%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216%u002e%u002e%u2216Windows%u2216win%u002ein', + expected: false, + }, + { + name: "empty string input should return false", + input: '', + expected: false, + }, + { + name: "valid date input should return true", + input: '2023-06-14', + expected: true, + }, + { + name: "time-only input should return false", + input: '05:23:59.244Z', + expected: false, + }, + { + name: "non-string input should return false", + input: {abc: 123}, + expected: false, + }, + { + name: "object with toString method input should return false", + input: { + toString: '2023-06-14T05:23:59.244Z' + }, + expected: false, + }, + ]; + for (const testCase of testCases) { + it(`should return ${testCase.expected} for ${testCase.name}`, () => { + expect(validTimestamp(testCase.input)).toEqual(testCase.expected); + }); + } +}); + +describe("isBlank", () => { + const testCases = [ + { + name: "null", + input: null, + expected: true + }, + { + name: "empty string", + input: "", + expected: true + }, + { + name: "non-empty string", + input: "test", + expected: false + }, + { + name: "numeric value", + input: 1634762544, + expected: false + }, + { + name: "object with toString property", + input: { + toString: '2023-06-14T05:23:59.244Z' + }, + expected: false + }, + ]; + for (const testCase of testCases) { + it(`should return ${testCase.expected} for ${testCase.name}`, () => { + expect(isBlank(testCase.input)).toEqual(testCase.expected); + }); + } +}); + describe("context traits", () => { const testCases = [ { @@ -1421,3 +1706,651 @@ describe("group traits", () => { }); } }) + +describe("transformColumnName", () => { + describe('with Blendo Casing', () => { + const testCases = [ + { + description: 'should convert special characters other than "\\" or "$" to underscores', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: 'column@Name$1', + expected: 'column_name$1', + }, + { + description: 'should add underscore if name does not start with an alphabet or underscore', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: '1CComega', + expected: '_1ccomega', + }, + { + description: 'should handle non-ASCII characters by converting to underscores', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: 'Cízǔ', + expected: 'c_z_', + }, + { + description: 'should transform CamelCase123Key to camelcase123key', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: 'CamelCase123Key', + expected: 'camelcase123key', + }, + { + description: 'should preserve "\\" and "$" characters', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: 'path to $1,00,000', + expected: 'path_to_$1_00_000', + }, + { + description: 'should handle a mix of characters, numbers, and special characters', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: 'CamelCase123Key_with$special\\chars', + expected: 'camelcase123key_with$special\\chars', + }, + { + description: 'should limit length to 63 characters for postgres provider', + options: {integrationOptions: {useBlendoCasing: true}, provider: 'postgres'}, + input: 'a'.repeat(70), + expected: 'a'.repeat(63), + }, + ]; + testCases.forEach(({description, options, input, expected}) => { + it(description, () => { + const result = transformColumnName(options, input); + expect(result).toBe(expected); + }); + }); + }); + + describe('without Blendo Casing (underscoreDivideNumbers=true)', () => { + const testCases = [ + { + description: 'should remove symbols and join continuous letters and numbers with a single underscore', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: '&4yasdfa(84224_fs9##_____*3q', + expected: '_4_yasdfa_84224_fs_9_3_q', + }, + { + description: 'should transform "omega" to "omega"', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'omega', + expected: 'omega', + }, + { + description: 'should transform "omega v2" to "omega_v_2"', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'omega v2', + expected: 'omega_v_2', + }, + { + description: 'should prepend underscore if name starts with a number', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: '9mega', + expected: '_9_mega', + }, + { + description: 'should remove trailing special characters', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'mega&', + expected: 'mega', + }, + { + description: 'should replace special character in the middle with underscore', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'ome$ga', + expected: 'ome_ga', + }, + { + description: 'should not remove trailing $ character', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'omega$', + expected: 'omega', + }, + { + description: 'should handle spaces and special characters by converting to underscores', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'ome_ ga', + expected: 'ome_ga', + }, + { + description: 'should handle multiple underscores and hyphens by reducing to single underscores', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: '9mega________-________90', + expected: '_9_mega_90', + }, + { + description: 'should handle non-ASCII characters by converting them to underscores', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'Cízǔ', + expected: 'c_z', + }, + { + description: 'should transform CamelCase123Key to camel_case_123_key', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'CamelCase123Key', + expected: 'camel_case_123_key', + }, + { + description: 'should handle numbers and commas in the input', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'path to $1,00,000', + expected: 'path_to_1_00_000', + }, + { + description: 'should return an empty string if input contains no valid characters', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: '@#$%', + expected: '', + }, + { + description: 'should keep underscores between letters and numbers', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'test123', + expected: 'test_123', + }, + { + description: 'should keep multiple underscore-number sequences', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'abc123def456', + expected: 'abc_123_def_456', + }, + { + description: 'should keep multiple underscore-number sequences', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'abc_123_def_456', + expected: 'abc_123_def_456', + }, + ]; + testCases.forEach(({description, options, input, expected}) => { + it(description, () => { + const result = transformColumnName(options, input); + expect(result).toBe(expected); + }); + }); + }); + + describe('without Blendo Casing (underscoreDivideNumbers=false)', () => { + const testCases = [ + { + description: 'should remove symbols and join continuous letters and numbers with a single underscore', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: '&4yasdfa(84224_fs9##_____*3q', + expected: '_4yasdfa_84224_fs9_3q', + }, + { + description: 'should transform "omega" to "omega"', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'omega', + expected: 'omega', + }, + { + description: 'should transform "omega v2" to "omega_v_2"', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'omega v2', + expected: 'omega_v2', + }, + { + description: 'should prepend underscore if name starts with a number', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: '9mega', + expected: '_9mega', + }, + { + description: 'should remove trailing special characters', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'mega&', + expected: 'mega', + }, + { + description: 'should replace special character in the middle with underscore', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'ome$ga', + expected: 'ome_ga', + }, + { + description: 'should not remove trailing $ character', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: true + }, + input: 'omega$', + expected: 'omega', + }, + { + description: 'should handle spaces and special characters by converting to underscores', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'ome_ ga', + expected: 'ome_ga', + }, + { + description: 'should handle multiple underscores and hyphens by reducing to single underscores', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: '9mega________-________90', + expected: '_9mega_90', + }, + { + description: 'should handle non-ASCII characters by converting them to underscores', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'Cízǔ', + expected: 'c_z', + }, + { + description: 'should transform CamelCase123Key to camel_case_123_key', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'CamelCase123Key', + expected: 'camel_case123_key', + }, + { + description: 'should handle numbers and commas in the input', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'path to $1,00,000', + expected: 'path_to_1_00_000', + }, + { + description: 'should return an empty string if input contains no valid characters', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: '@#$%', + expected: '', + }, + { + description: 'should keep underscores between letters and numbers', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'test123', + expected: 'test123', + }, + { + description: 'should keep multiple underscore-number sequences', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'abc123def456', + expected: 'abc123_def456', + }, + { + description: 'should keep multiple underscore-number sequences', + options: { + integrationOptions: {useBlendoCasing: false}, + provider: 'postgres', + underscoreDivideNumbers: false + }, + input: 'abc_123_def_456', + expected: 'abc_123_def_456', + }, + ]; + testCases.forEach(({description, options, input, expected}) => { + it(description, () => { + const result = transformColumnName(options, input); + expect(result).toBe(expected); + }); + }); + }); +}) + +describe("transformTableName", () => { + describe('with Blendo Casing', () => { + const testCases = [ + { + description: 'should convert name to Blendo casing (lowercase) when Blendo casing is enabled', + options: {integrationOptions: {useBlendoCasing: true}}, + input: 'TableName123', + expected: 'tablename123', + }, + { + description: 'should trim spaces and convert to Blendo casing (lowercase) when Blendo casing is enabled', + options: {integrationOptions: {useBlendoCasing: true}}, + input: ' TableName ', + expected: 'tablename', + }, + { + description: 'should return an empty string when input is empty and Blendo casing is enabled', + options: {integrationOptions: {useBlendoCasing: true}}, + input: '', + expected: '', + }, + { + description: 'should handle names with special characters and convert to Blendo casing (lowercase)', + options: {integrationOptions: {useBlendoCasing: true}}, + input: 'Table@Name!', + expected: 'table@name!', + }, + { + description: 'should convert a mixed-case name to Blendo casing (lowercase) when Blendo casing is enabled', + options: {integrationOptions: {useBlendoCasing: true}}, + input: 'CaMeLcAsE', + expected: 'camelcase', + }, + { + description: 'should keep an already lowercase name unchanged with Blendo casing enabled', + options: {integrationOptions: {useBlendoCasing: true}}, + input: 'lowercase', + expected: 'lowercase', + }, + ]; + testCases.forEach(({description, options, input, expected}) => { + it(description, () => { + const result = transformTableName(options, input); + expect(result).toBe(expected); + }); + }); + }); + + describe('without Blendo Casing (underscoreDivideNumbers=true)', () => { + const testCases = [ + { + description: 'should remove symbols and join continuous letters and numbers with a single underscore', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: '&4yasdfa(84224_fs9##_____*3q', + expected: '_4_yasdfa_84224_fs_9_3_q', + }, + { + description: 'should transform "omega" to "omega"', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'omega', + expected: 'omega', + }, + { + description: 'should transform "omega v2" to "omega_v_2"', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'omega v2', + expected: 'omega_v_2', + }, + { + description: 'should prepend underscore if name starts with a number', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: '9mega', + expected: '_9_mega', + }, + { + description: 'should remove trailing special characters', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'mega&', + expected: 'mega', + }, + { + description: 'should replace special character in the middle with underscore', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'ome$ga', + expected: 'ome_ga', + }, + { + description: 'should not remove trailing $ character', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'omega$', + expected: 'omega', + }, + { + description: 'should handle spaces and special characters by converting to underscores', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'ome_ ga', + expected: 'ome_ga', + }, + { + description: 'should handle multiple underscores and hyphens by reducing to single underscores', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: '9mega________-________90', + expected: '_9_mega_90', + }, + { + description: 'should handle non-ASCII characters by converting them to underscores', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'Cízǔ', + expected: 'c_z', + }, + { + description: 'should transform CamelCase123Key to camel_case_123_key', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'CamelCase123Key', + expected: 'camel_case_123_key', + }, + { + description: 'should handle numbers and commas in the input', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'path to $1,00,000', + expected: 'path_to_1_00_000', + }, + { + description: 'should return an empty string if input contains no valid characters', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: '@#$%', + expected: '', + }, + { + description: 'should keep underscores between letters and numbers', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'test123', + expected: 'test_123', + }, + { + description: 'should keep multiple underscore-number sequences', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'abc123def456', + expected: 'abc_123_def_456', + }, + ]; + testCases.forEach(({description, options, input, expected}) => { + it(description, () => { + const result = transformTableName(options, input); + expect(result).toBe(expected); + }); + }); + }); + + describe('without Blendo Casing (underscoreDivideNumbers=false)', () => { + const testCases = [ + { + description: 'should remove symbols and join continuous letters and numbers with a single underscore', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: '&4yasdfa(84224_fs9##_____*3q', + expected: '_4yasdfa_84224_fs9_3q', + }, + { + description: 'should transform "omega" to "omega"', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'omega', + expected: 'omega', + }, + { + description: 'should transform "omega v2" to "omega_v_2"', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'omega v2', + expected: 'omega_v2', + }, + { + description: 'should prepend underscore if name starts with a number', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: '9mega', + expected: '_9mega', + }, + { + description: 'should remove trailing special characters', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'mega&', + expected: 'mega', + }, + { + description: 'should replace special character in the middle with underscore', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'ome$ga', + expected: 'ome_ga', + }, + { + description: 'should not remove trailing $ character', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: true}, + input: 'omega$', + expected: 'omega', + }, + { + description: 'should handle spaces and special characters by converting to underscores', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'ome_ ga', + expected: 'ome_ga', + }, + { + description: 'should handle multiple underscores and hyphens by reducing to single underscores', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: '9mega________-________90', + expected: '_9mega_90', + }, + { + description: 'should handle non-ASCII characters by converting them to underscores', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'Cízǔ', + expected: 'c_z', + }, + { + description: 'should transform CamelCase123Key to camel_case_123_key', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'CamelCase123Key', + expected: 'camel_case123_key', + }, + { + description: 'should handle numbers and commas in the input', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'path to $1,00,000', + expected: 'path_to_1_00_000', + }, + { + description: 'should return an empty string if input contains no valid characters', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: '@#$%', + expected: '', + }, + { + description: 'should keep underscores between letters and numbers', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'test123', + expected: 'test123', + }, + { + description: 'should keep multiple underscore-number sequences', + options: {integrationOptions: {useBlendoCasing: false}, underscoreDivideNumbers: false}, + input: 'abc123def456', + expected: 'abc123_def456', + }, + ]; + testCases.forEach(({description, options, input, expected}) => { + it(description, () => { + const result = transformTableName(options, input); + expect(result).toBe(expected); + }); + }); + }); +}); \ No newline at end of file diff --git a/test/integrations/destinations/af/processor/data.ts b/test/integrations/destinations/af/processor/data.ts index 9812f7f902..dacef38943 100644 --- a/test/integrations/destinations/af/processor/data.ts +++ b/test/integrations/destinations/af/processor/data.ts @@ -1716,6 +1716,98 @@ export const existingTestCases = [ }, }, }, + { + name: 'af', + description: 'Place selected properties at root track call with af data', + id: 'selectedPropsAtRoot', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'track', + event: 'Order Completed', + sentAt: '2020-08-14T05:30:30.118Z', + context: commonContextWithExternalId, + messageId: '7208bbb6-2c4e-45bb-bf5b-ad426f3593e9', + timestamp: '2020-08-14T05:30:30.118Z', + properties: { + ...commonPropertiesWithProduct, + prop1: 'value1', + prop2: 'value2', + Prop3: 'value3', + }, + anonymousId: '50be5c78-6c3f-4b60-be84-97805a316fb1', + integrations: { AF: { af_uid: 'afUid' } }, + }, + destination: { + Config: { + devKey: 'abcde', + androidAppId: 'com.rudderlabs.javascript', + groupTypeTrait: 'email', + groupValueTrait: 'age', + trackProductsOnce: false, + trackRevenuePerProduct: false, + addPropertiesAtRoot: false, + listOfProps: [ + { + property: 'prop1', + }, + { + property: 'prop2', + }, + { + property: 'prop3', + }, + { + property: 'prop4', + }, + ], + }, + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: commonV1EndPoint, + headers: commonHeader, + params: {}, + body: { + JSON: { + bundleIdentifier: 'com.rudderlabs.javascript', + eventValue: + '{"prop1":"value1","prop2":"value2","properties":{"tax":2,"total":27.5,"coupon":"hasbros","revenue":48,"price":25,"quantity":2,"currency":"ZAR","discount":2.5,"order_id":"50314b8e9bcf000000000000","products":[{"sku":"45790-32","url":"https://www.example.com/product/path","name":"Monopoly: 3rd Edition","price":19,"category":"Games","quantity":1,"image_url":"https:///www.example.com/product/path.jpg","product_id":"507f1f77bcf86cd799439011"},{"sku":"46493-32","name":"Uno Card Game","price":3,"category":"Games","quantity":2,"product_id":"505bd76785ebb509fc183733"}],"shipping":3,"subtotal":22.5,"affiliation":"Google Store","checkout_id":"fksdjfsdjfisjf9sdfjsd9f","Prop3":"value3"},"af_revenue":48,"af_price":[19,3],"af_quantity":[1,2],"af_order_id":"50314b8e9bcf000000000000","af_content_id":["507f1f77bcf86cd799439011","505bd76785ebb509fc183733"]}', + eventName: 'Order Completed', + eventCurrency: 'ZAR', + eventTime: '2020-08-14T05:30:30.118Z', + appsflyer_id: 'afUid', + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; export const data = [...existingTestCases, ...newConfigValidationTests]; diff --git a/test/integrations/destinations/amazon_audience/common.ts b/test/integrations/destinations/amazon_audience/common.ts new file mode 100644 index 0000000000..728bdf1d25 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/common.ts @@ -0,0 +1,30 @@ +export const destination = { + DestinationDefinition: { + Config: { + excludeKeys: [], + includeKeys: [], + }, + }, + Config: { + advertiserId: '{"Dummy Name":"1234"}', + audienceId: 'dummyId', + }, + ID: 'amazonAud-1234', +}; + +export const generateMetadata = (jobId: number, userId?: string): any => { + return { + jobId, + attemptNum: 1, + userId: userId || 'default-userId', + sourceId: 'default-sourceId', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + dontBatch: false, + secret: { + accessToken: 'dummyAccessToken', + refreshToken: 'dummyRefreshToken', + clientId: 'dummyClientId', + }, + }; +}; diff --git a/test/integrations/destinations/amazon_audience/dataDelivery/data.ts b/test/integrations/destinations/amazon_audience/dataDelivery/data.ts new file mode 100644 index 0000000000..c78a53c899 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/dataDelivery/data.ts @@ -0,0 +1,197 @@ +import { generateMetadata, generateProxyV0Payload } from '../../../testUtils'; + +const commonStatTags = { + destType: 'AMAZON_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'aborted', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', +}; +export const data = [ + { + name: 'amazon_audience', + id: 'Test 0', + description: 'Successfull Delivery case', + successCriteria: 'It should be passed with 200 Ok with no errors', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + endpoint: '', + JSON: { + associateUsers: { + patches: [ + { + op: 'remove', + path: '/EXTERNAL_USER_ID-Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + ], + }, + ], + }, + }, + }), + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: '[amazon_audience Response Handler] - Request Processed Successfully', + destinationResponse: { + response: { + requestId: 'dummy request id', + jobId: 'dummy job id', + }, + status: 200, + }, + }, + }, + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 1', + description: 'Unsuccessfull Delivery case for step 2', + successCriteria: 'It should be passed with 500 Internal with error with invalid payload', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + endpoint: '', + JSON: { + associateUsers: { + patches: [ + { + op: 'add', + path: '/EXTERNAL_USER_ID-Fail_Case/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + ], + }, + ], + }, + }, + }), + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + destinationResponse: { + response: { + code: 'Internal Error', + }, + status: 500, + }, + message: 'Request Failed: during amazon_audience response transformation (Retryable)', + statTags: { + destType: 'AMAZON_AUDIENCE', + errorCategory: 'network', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + }, + }, + }, + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 2 - Oauth Refresh Token', + description: 'Unsuccessfull Access Error for step 1', + successCriteria: 'It should be passed with 401 Unauthorized error', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + headers: { + Authorization: 'Bearer fail_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + endpoint: '', + JSON: { + associateUsers: [], + createUsers: { + records: [ + { + externalId: 'access token expired fail case', + hashedRecords: [], + }, + ], + }, + }, + }), + }, + }, + output: { + response: { + status: 401, + body: { + output: { + status: 401, + destinationResponse: { + message: 'Unauthorized', + }, + authErrorCategory: 'REFRESH_TOKEN', + message: 'Unauthorized during creating users', + statTags: commonStatTags, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/amazon_audience/network.ts b/test/integrations/destinations/amazon_audience/network.ts new file mode 100644 index 0000000000..b0941712c6 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/network.ts @@ -0,0 +1,123 @@ +const headers = { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer success_access_token', +}; +export const networkCallsData = [ + { + description: 'successful step 1', + httpReq: { + url: 'https://advertising-api.amazon.com/dp/records/hashed/', + data: { + records: [ + { + externalId: + 'Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + ], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'post', + }, + httpRes: { + data: { + requestId: 'dummy request id', + }, + status: 200, + }, + }, + { + description: 'successful step 2', + httpReq: { + url: 'https://advertising-api.amazon.com/v2/dp/audience', + data: { + patches: [ + { + op: 'remove', + path: '/EXTERNAL_USER_ID-Rudderstack_c73bcaadd94985269eeafd457c9f395135874dad5536cf1f6d75c132f602a14c/audiences', + value: ['dummyId'], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'patch', + }, + httpRes: { + data: { + requestId: 'dummy request id', + jobId: 'dummy job id', + }, + status: 200, + }, + }, + { + description: 'unsuccessful step 2', + httpReq: { + url: 'https://advertising-api.amazon.com/v2/dp/audience', + data: { + patches: [ + { + op: 'add', + path: '/EXTERNAL_USER_ID-Fail_Case/audiences', + value: ['dummyId'], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer success_access_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'patch', + }, + httpRes: { + data: { + code: 'Internal Error', + }, + status: 500, + }, + }, + { + description: 'unsuccessful step 1', + httpReq: { + url: 'https://advertising-api.amazon.com/dp/records/hashed/', + data: { + records: [ + { + externalId: 'access token expired fail case', + hashedRecords: [], + }, + ], + }, + params: {}, + headers: { + Authorization: 'Bearer fail_token', + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + method: 'post', + }, + httpRes: { + data: { + message: 'Unauthorized', + }, + status: 401, + }, + }, +]; diff --git a/test/integrations/destinations/amazon_audience/processor/data.ts b/test/integrations/destinations/amazon_audience/processor/data.ts new file mode 100644 index 0000000000..49931ed92b --- /dev/null +++ b/test/integrations/destinations/amazon_audience/processor/data.ts @@ -0,0 +1,246 @@ +import { destination, generateMetadata } from '../common'; +const sha256 = require('sha256'); + +const fields = { + email: 'abc@xyz.com', + phone: '9876543323', + firstName: 'test', + lastName: 'user', + address: ' Été très chaud! ', +}; + +export const data = [ + { + name: 'amazon_audience', + id: 'Test 1', + description: 'All traits are present with hash enbaled for the audience with insert operation', + successCriteria: 'It should be passed with 200 Ok with all traits mapped after hashing', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + ...fields, + city: 'Edmonton', + state: 'alberta', + country: 'Canada', + postalCode: '12345', + }, + context: {}, + recordId: '1', + }, + destination: { ...destination, Config: { ...destination.Config, enableHash: true } }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + FORM: {}, + JSON_ARRAY: {}, + JSON: { + createUsers: { + records: [ + { + hashedRecords: [ + { + country: + '6959097001d10501ac7d54c0bdb8db61420f658f2922cc26e46d536119a31126', + address: + '7e68f87b9675dca9a6cbd0b3b715af6cd9e0b75b72b96feec98dd334d665a76c', + city: '5ae1b46bce91b626720727f9d8d1eb8998e5b6586b339b97c2288595fe25116a', + firstName: + '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + email: + 'ee278943de84e5d6243578ee1a1057bcce0e50daad9755f45dfa64b60b13bc5d', + lastName: + '04f8996da763b7a969b1028ee3007569eaf3a635486ddab211d512c85b9df8fb', + phone: + '3daf505bba309a952bb4bbd010d1d39e413e40c679ac3bbcee1ea9b009023ffa', + postalCode: + '6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + state: + 'fb8e20fc2e4c3f248c60c39bd652f3c1347298bb977b8b4d5903b85055620603', + }, + ], + externalId: + 'Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + }, + ], + }, + associateUsers: { + patches: [ + { + op: 'add', + path: `/EXTERNAL_USER_ID-Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/audiences`, + value: ['dummyId'], + }, + ], + }, + }, + XML: {}, + }, + endpoint: '', + files: {}, + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + method: 'POST', + params: {}, + type: 'REST', + userId: '', + version: '1', + }, + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 2', + description: 'All traits are present with hash disabled for the audience with delete operation', + successCriteria: 'It should be passed with 200 Ok with all traits mapped without hashing', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'record', + action: 'delete', + fields, + channel: 'sources', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '', + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + params: {}, + body: { + JSON: { + createUsers: { + records: [ + { + hashedRecords: [ + { + email: 'abc@xyz.com', + phone: '9876543323', + firstName: 'test', + lastName: 'user', + address: ' Été très chaud! ', + }, + ], + externalId: + 'Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b', + }, + ], + }, + associateUsers: { + patches: [ + { + op: 'remove', + path: `/EXTERNAL_USER_ID-Rudderstack_6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b/audiences`, + value: ['dummyId'], + }, + ], + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + metadata: generateMetadata(1), + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'amazon_audience', + id: 'Test 3', + description: 'Type Validation case', + successCriteria: 'It should be passed with 200 Ok giving validation error', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination, + message: { + type: 'identify', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + metadata: generateMetadata(1), + statusCode: 400, + error: '[AMAZON AUDIENCE]: identify is not supported', + statTags: { + errorCategory: 'dataValidation', + destinationId: 'default-destinationId', + errorType: 'instrumentation', + destType: 'AMAZON_AUDIENCE', + module: 'destination', + implementation: 'native', + workspaceId: 'default-workspaceId', + feature: 'processor', + }, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/destinations/amazon_audience/router/data.ts b/test/integrations/destinations/amazon_audience/router/data.ts new file mode 100644 index 0000000000..6787e03160 --- /dev/null +++ b/test/integrations/destinations/amazon_audience/router/data.ts @@ -0,0 +1,226 @@ +import { destination, generateMetadata } from '../common'; + +export const data = [ + { + name: 'amazon_audience', + id: 'router-test-1', + description: 'batching based upon action', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + destination, + message: { + type: 'record', + action: 'delete', + fields: { email: 'email4@abc.com' }, + channel: 'sources', + context: {}, + recordId: '4', + }, + metadata: generateMetadata(1), + }, + { + destination, + message: { + type: 'record', + action: 'delete', + fields: { + email: 'email5@abc.com', + }, + channel: 'sources', + context: {}, + recordId: '5', + }, + metadata: generateMetadata(2), + }, + { + destination, + message: { + type: 'record', + action: 'insert', + fields: { email: 'email3@abc.com' }, + channel: 'sources', + context: {}, + recordId: '3', + }, + metadata: generateMetadata(3), + }, + { + destination, + message: { + type: 'record', + action: 'update', + fields: { + email: 'email1@abc.com', + }, + channel: 'sources', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(4), + }, + { + destination, + message: { + type: 'record', + action: 'insert', + fields: { email: 'email2@abc.com' }, + channel: 'sources', + context: {}, + recordId: '2', + }, + metadata: generateMetadata(5), + }, + { + destination, + message: { + type: 'identify', + context: {}, + recordId: '1', + }, + metadata: generateMetadata(6), + }, + ], + destType: 'amazon_audience', + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: true, + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '', + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + params: {}, + body: { + JSON: { + associateUsers: { + patches: [ + { + op: 'remove', + path: '/EXTERNAL_USER_ID-Rudderstack_17f8af97ad4a7f7639a4c9171d5185cbafb85462877a4746c21bdb0a4f940ca0/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_17f8af97ad4a7f7639a4c9171d5185cbafb85462877a4746c21bdb0a4f940ca0', + hashedRecords: [ + { + email: 'email4@abc.com', + }, + { + email: 'email5@abc.com', + }, + ], + }, + ], + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + destination, + metadata: [generateMetadata(1), generateMetadata(2)], + statusCode: 200, + }, + { + batched: true, + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: '', + headers: { + 'Amazon-Advertising-API-ClientId': 'dummyClientId', + 'Content-Type': 'application/json', + Authorization: 'Bearer dummyAccessToken', + }, + params: {}, + body: { + JSON: { + associateUsers: { + patches: [ + { + op: 'add', + path: '/EXTERNAL_USER_ID-Rudderstack_a752d8ffaabe4c4d8a7a10cbdb2ee1525130a56a8290eef5d8a695434c49928f/audiences', + value: ['dummyId'], + }, + ], + }, + createUsers: { + records: [ + { + externalId: + 'Rudderstack_a752d8ffaabe4c4d8a7a10cbdb2ee1525130a56a8290eef5d8a695434c49928f', + hashedRecords: [ + { + email: 'email3@abc.com', + }, + { + email: 'email1@abc.com', + }, + { + email: 'email2@abc.com', + }, + ], + }, + ], + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + destination, + metadata: [generateMetadata(3), generateMetadata(4), generateMetadata(5)], + statusCode: 200, + }, + { + metadata: [generateMetadata(6)], + destination, + batched: false, + statusCode: 400, + error: '[AMAZON AUDIENCE]: identify is not supported', + statTags: { + errorCategory: 'dataValidation', + destinationId: 'default-destinationId', + errorType: 'instrumentation', + destType: 'AMAZON_AUDIENCE', + module: 'destination', + implementation: 'native', + workspaceId: 'default-workspaceId', + feature: 'router', + }, + }, + ], + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/braze/network.ts b/test/integrations/destinations/braze/network.ts index ae093ce1f4..bcfa78de5d 100644 --- a/test/integrations/destinations/braze/network.ts +++ b/test/integrations/destinations/braze/network.ts @@ -406,6 +406,20 @@ const deleteNwData = [ { alias_name: '77e278c9-e984-4cdd-950c-cd0b61befd03', alias_label: 'rudder_id' }, { alias_name: 'e6ab2c5e-2cda-44a9-a962-e2f67df78bca', alias_label: 'rudder_id' }, ], + fields_to_export: [ + 'created_at', + 'custom_attributes', + 'dob', + 'email', + 'first_name', + 'gender', + 'home_city', + 'last_name', + 'phone', + 'time_zone', + 'external_id', + 'user_aliases', + ], }, headers: { Authorization: 'Bearer dummyApiKey' }, url: 'https://rest.iad-03.braze.com/users/export/ids', @@ -416,12 +430,8 @@ const deleteNwData = [ { created_at: '2023-03-17T20:51:58.297Z', external_id: 'braze_test_user', - user_aliases: [], - appboy_id: '6414d2ee33326e3354e3040b', - braze_id: '6414d2ee33326e3354e3040b', first_name: 'Jackson', last_name: 'Miranda', - random_bucket: 8134, email: 'jackson24miranda@gmail.com', custom_attributes: { pwa: false, @@ -437,17 +447,6 @@ const deleteNwData = [ }, custom_arr: [1, 2, 'str1'], }, - custom_events: [ - { - name: 'Sign In Completed', - first: '2023-03-10T18:36:05.028Z', - last: '2023-03-10T18:36:05.028Z', - count: 2, - }, - ], - total_revenue: 0, - push_subscribe: 'subscribed', - email_subscribe: 'subscribed', }, ], }, diff --git a/test/integrations/destinations/facebook_conversions/processor/data.ts b/test/integrations/destinations/facebook_conversions/processor/data.ts index d72114c15b..49d2416726 100644 --- a/test/integrations/destinations/facebook_conversions/processor/data.ts +++ b/test/integrations/destinations/facebook_conversions/processor/data.ts @@ -1045,7 +1045,7 @@ export const data = [ JSON_ARRAY: {}, FORM: { data: [ - '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"AddToWishlist","event_time":1699784211,"action_source":"website","custom_data":{"revenue":400,"additional_bet_index":0,"content_ids":[],"contents":[],"currency":"USD","value":400}}', + '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"AddToWishlist","event_time":1699784211,"action_source":"website","custom_data":{"revenue":400,"additional_bet_index":0,"content_ids":[],"contents":[],"currency":"USD","value":400,"num_items":0}}', ], }, }, @@ -1104,8 +1104,143 @@ export const data = [ }, message_id: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', properties: { - revenue: 400, - additional_bet_index: 0, + application_tracking_enabled: 1, + content_name: 'Checkout', + content_type: 'product', + num_items: 1, + products: [ + { + id: '12809', + price: 80, + quantity: 1, + }, + ], + revenue: 93.99, + }, + timestamp: '2023-11-12T15:46:51.693229+05:30', + type: 'track', + }, + destination: { + Config: { + limitedDataUsage: true, + blacklistPiiProperties: [ + { + blacklistPiiProperties: '', + blacklistPiiHash: false, + }, + ], + accessToken: '09876', + datasetId: 'dummyID', + eventsToEvents: [ + { + from: '', + to: '', + }, + ], + eventCustomProperties: [ + { + eventCustomProperties: '', + }, + ], + removeExternalId: true, + whitelistPiiProperties: [ + { + whitelistPiiProperties: '', + }, + ], + actionSource: 'website', + }, + Enabled: true, + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://graph.facebook.com/v20.0/dummyID/events?access_token=09876', + headers: {}, + params: {}, + body: { + JSON: {}, + XML: {}, + JSON_ARRAY: {}, + FORM: { + data: [ + '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"AddPaymentInfo","event_time":1699784211,"action_source":"website","custom_data":{"application_tracking_enabled":1,"content_name":"Checkout","content_type":"product","num_items":1,"products":[{"id":"12809","price":80,"quantity":1}],"revenue":93.99,"content_ids":["12809"],"contents":[{"id":"12809","quantity":1,"item_price":80}],"currency":"USD","value":93.99}}', + ], + }, + }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + mockFns: defaultMockFns, + }, + { + name: 'facebook_conversions', + description: 'Track event with standard event payment info entered without products', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + channel: 'web', + context: { + device: { + id: 'df16bffa-5c3d-4fbb-9bce-3bab098129a7R', + manufacturer: 'Xiaomi', + model: 'Redmi 6', + name: 'xiaomi', + }, + network: { + carrier: 'Banglalink', + }, + os: { + name: 'android', + version: '8.1.0', + }, + screen: { + height: '100', + density: 50, + }, + traits: { + email: ' aBc@gmail.com ', + address: { + zip: 1234, + }, + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + }, + }, + event: 'payment info entered', + integrations: { + All: true, + }, + message_id: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + properties: { + application_tracking_enabled: 1, + content_name: 'Checkout', + content_type: 'product', + num_items: 1, + id: '12809', + price: 80, + quantity: 1, + revenue: 93.99, }, timestamp: '2023-11-12T15:46:51.693229+05:30', type: 'track', @@ -1164,7 +1299,7 @@ export const data = [ JSON_ARRAY: {}, FORM: { data: [ - '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"AddPaymentInfo","event_time":1699784211,"action_source":"website","custom_data":{"revenue":400,"additional_bet_index":0,"content_ids":[],"contents":[],"currency":"USD","value":400}}', + '{"user_data":{"em":"48ddb93f0b30c475423fe177832912c5bcdce3cc72872f8051627967ef278e08","zp":"03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4"},"event_name":"AddPaymentInfo","event_time":1699784211,"action_source":"website","custom_data":{"application_tracking_enabled":1,"content_name":"Checkout","content_type":"product","num_items":1,"id":"12809","price":80,"quantity":1,"revenue":93.99,"content_ids":["12809"],"contents":[{"id":"12809","quantity":1,"item_price":80}],"currency":"USD","value":93.99}}', ], }, }, diff --git a/test/integrations/destinations/gcs_datalake/processor/data.ts b/test/integrations/destinations/gcs_datalake/processor/data.ts index 46c7788709..26f438758d 100644 --- a/test/integrations/destinations/gcs_datalake/processor/data.ts +++ b/test/integrations/destinations/gcs_datalake/processor/data.ts @@ -62,6 +62,8 @@ export const data = [ syncFrequency: '30', tableSuffix: '', timeWindowLayout: '2006/01/02/15', + allowUsersContextTraits: true, + underscoreDivideNumbers: true, }, Enabled: true, }, diff --git a/test/integrations/destinations/heap/processor/data.ts b/test/integrations/destinations/heap/processor/data.ts index f503134148..cb1aeb7563 100644 --- a/test/integrations/destinations/heap/processor/data.ts +++ b/test/integrations/destinations/heap/processor/data.ts @@ -117,6 +117,124 @@ export const data = [ }, }, }, + { + name: 'heap', + description: 'Blank user id', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + appId: '', + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + DisplayName: 'Heap.io', + ID: '1WTbl0l5GjOQKOvfmcGwk0T49kV', + Name: 'HEAP', + }, + Enabled: true, + ID: '1WTcDSEOE437e4ePH10BJNELXmE', + Name: 'heap test', + Transformations: [], + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + message: { + userId: '', + anonymousId: 'sampath', + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + ip: '0.0.0.0', + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + locale: 'en-US', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.88 Safari/537.36', + }, + integrations: { + All: true, + }, + traits: { + anonymousId: 'sampath', + email: 'sampath@gmail.com', + }, + messageId: 'fca2e71a-5d30-48e1-ba45-761c16e3820f', + originalTimestamp: '2020-01-16T13:21:59.076Z', + receivedAt: '2020-01-16T18:52:03.871+05:30', + request_ip: '[::1]:62312', + sentAt: '2020-01-16T13:22:03.85Z', + timestamp: '2020-01-16T18:51:59.097+05:30', + type: 'identify', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://heapanalytics.com/api/add_user_properties', + headers: { + 'Content-Type': 'application/json', + Accept: 'application/json', + }, + params: {}, + body: { + JSON: { + identity: 'sampath', + properties: { + anonymousId: 'sampath', + email: 'sampath@gmail.com', + }, + app_id: '', + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + userId: 'sampath', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, { name: 'heap', description: 'Test 1', diff --git a/test/integrations/destinations/intercom/network.ts b/test/integrations/destinations/intercom/network.ts index 2f90beac40..0a86ce3c89 100644 --- a/test/integrations/destinations/intercom/network.ts +++ b/test/integrations/destinations/intercom/network.ts @@ -1041,5 +1041,42 @@ const deliveryCallsData = [ }, }, }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'external_id', operator: '=', value: '10156' }], + }, + }, + headers: { ...commonHeaders, 'Intercom-Version': '2.10', 'User-Agent': 'RudderStack' }, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 1, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 1, + }, + data: [ + { + type: 'contact', + id: '7070129940741e45d040', + workspace_id: 'rudderWorkspace', + external_id: 'user@2', + role: 'user', + email: 'test+2@rudderlabs.com', + }, + ], + }, + }, + }, ]; export const networkCallsData = [...deleteNwData, ...deliveryCallsData]; diff --git a/test/integrations/destinations/intercom/processor/identifyTestData.ts b/test/integrations/destinations/intercom/processor/identifyTestData.ts index 49f3a400d1..f078536b30 100644 --- a/test/integrations/destinations/intercom/processor/identifyTestData.ts +++ b/test/integrations/destinations/intercom/processor/identifyTestData.ts @@ -80,6 +80,10 @@ const user3Traits = { name: 'Test Rudderlabs', phone: '+91 9999999999', email: 'test@rudderlabs.com', + custom_attributes: { + ca1: 'value1', + ca2: 'value2', + }, }; const user4Traits = { @@ -170,6 +174,10 @@ const expectedUser3Traits = { name: 'Test Rudderlabs', phone: '+91 9999999999', email: 'test@rudderlabs.com', + custom_attributes: { + ca1: 'value1', + ca2: 'value2', + }, }; const expectedUser4Traits = { @@ -233,6 +241,17 @@ const expectedUser6Traits = { ], }; +const expectedUser7Traits = { + custom_attributes: { + anonymousId: '58b21c2d-f8d5-4410-a2d0-b268a26b7e33', + key1: 'value1', + }, + email: 'test_1@test.com', + name: 'Test Name', + phone: '9876543210', + signed_up_at: 1601493060, +}; + const timestamp = '2023-11-22T10:12:44.757+05:30'; const originalTimestamp = '2023-11-10T14:42:44.724Z'; @@ -1024,4 +1043,63 @@ export const identifyTestData = [ }, }, }, + { + id: 'intercom-identify-test-16', + name: 'intercom', + description: 'V1 version : Identify test with different lookup field than email', + scenario: 'Business', + successCriteria: + 'Response status code should be 200 and response should contain update user payload with all traits', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: v2Destination, + message: { + context: { + externalId: [ + { + id: '10156', + type: 'INTERCOM-customer', + identifierType: 'user_id', + }, + ], + traits: { ...user5Traits, external_id: '10156' }, + }, + type: 'identify', + timestamp, + originalTimestamp, + integrations: { + INTERCOM: { + lookup: 'external_id', + }, + }, + }, + metadata: generateMetadata(1), + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: transformResultBuilder({ + userId: '', + endpoint: `${v2Endpoint}/7070129940741e45d040`, + headers: v2Headers, + method: 'PUT', + JSON: expectedUser7Traits, + }), + statusCode: 200, + metadata: generateMetadata(1), + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/intercom_v2/common.ts b/test/integrations/destinations/intercom_v2/common.ts new file mode 100644 index 0000000000..60c7e02b33 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/common.ts @@ -0,0 +1,150 @@ +import { Destination } from '../../../../src/types'; + +const destTypeInUpperCase = 'INTERCOM_V2'; +const channel = 'web'; +const originalTimestamp = '2023-11-10T14:42:44.724Z'; +const timestamp = '2023-11-22T10:12:44.757+05:30'; +const anonymousId = 'test-anonymous-id'; + +const destination: Destination = { + ID: '123', + Name: destTypeInUpperCase, + DestinationDefinition: { + ID: '123', + Name: destTypeInUpperCase, + DisplayName: 'Intercom V2', + Config: {}, + }, + Config: { + apiServer: 'US', + sendAnonymousId: false, + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const destinationApiServerEU: Destination = { + ID: '123', + Name: destTypeInUpperCase, + DestinationDefinition: { + ID: '123', + Name: destTypeInUpperCase, + DisplayName: 'Intercom V2', + Config: {}, + }, + Config: { + apiServer: 'Europe', + sendAnonymousId: true, + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const destinationApiServerAU: Destination = { + ID: '123', + Name: destTypeInUpperCase, + DestinationDefinition: { + ID: '123', + Name: destTypeInUpperCase, + DisplayName: 'Intercom V2', + Config: {}, + }, + Config: { + apiServer: 'Australia', + sendAnonymousId: true, + }, + Enabled: true, + WorkspaceID: '123', + Transformations: [], +}; + +const userTraits = { + age: 23, + email: 'test@rudderlabs.com', + phone: '+91 9999999999', + firstName: 'John', + lastName: 'Snow', + address: 'california usa', + ownerId: '13', +}; + +const detachUserCompanyUserTraits = { + age: 23, + email: 'detach-user-company@rudderlabs.com', + phone: '+91 9999999999', + firstName: 'John', + lastName: 'Snow', + address: 'california usa', + ownerId: '13', +}; + +const companyTraits = { + email: 'known-email@rudderlabs.com', + name: 'RudderStack', + size: 500, + website: 'www.rudderstack.com', + industry: 'CDP', + plan: 'enterprise', + remoteCreatedAt: '2024-09-12T14:40:33.996+05:30', +}; + +const properties = { + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + price: { + amount: 3000, + currency: 'USD', + }, +}; + +const headers = { + Authorization: 'Bearer default-accessToken', + Accept: 'application/json', + 'Content-Type': 'application/json', + 'Intercom-Version': '2.10', +}; + +const headersWithRevokedAccessToken = { + ...headers, + Authorization: 'Bearer revoked-accessToken', +}; + +const RouterInstrumentationErrorStatTags = { + destType: destTypeInUpperCase, + errorCategory: 'dataValidation', + errorType: 'instrumentation', + implementation: 'native', + module: 'destination', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + feature: 'router', +}; + +const RouterNetworkErrorStatTags = { + ...RouterInstrumentationErrorStatTags, + errorCategory: 'network', + errorType: 'aborted', +}; + +export { + channel, + destination, + originalTimestamp, + timestamp, + destinationApiServerEU, + destinationApiServerAU, + userTraits, + companyTraits, + properties, + detachUserCompanyUserTraits, + anonymousId, + headers, + headersWithRevokedAccessToken, + RouterInstrumentationErrorStatTags, + RouterNetworkErrorStatTags, +}; diff --git a/test/integrations/destinations/intercom_v2/dataDelivery/business.ts b/test/integrations/destinations/intercom_v2/dataDelivery/business.ts new file mode 100644 index 0000000000..c75993bc7a --- /dev/null +++ b/test/integrations/destinations/intercom_v2/dataDelivery/business.ts @@ -0,0 +1,516 @@ +import { + generateMetadata, + generateProxyV0Payload, + generateProxyV1Payload, +} from '../../../testUtils'; +import { headers, RouterNetworkErrorStatTags } from '../common'; +import { ProxyV1TestData } from '../../../testTypes'; + +const createUserPayload = { + email: 'test-unsupported-media@rudderlabs.com', + external_id: 'user-id-1', + name: 'John Snow', +}; + +const conflictUserPayload = { + email: 'conflict@test.com', + user_id: 'conflict_test_user_id_1', +}; + +const statTags = { + ...RouterNetworkErrorStatTags, + errorType: 'retryable', + feature: 'dataDelivery', +}; + +export const testScenariosForV0API = [ + { + id: 'INTERCOM_V2_v0_other_scenario_1', + name: 'intercom_v2', + description: + '[Proxy v0 API] :: Scenario to test Malformed Payload Response Handling from Destination', + successCriteria: 'Should return 400 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: { + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + method: 'PUT', + }), + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'parameter_invalid', + message: "Custom attribute 'isOpenSource' does not exist", + }, + ], + request_id: 'request_1', + type: 'error.list', + }, + status: 400, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 400. {"request_id":"request_1","type":"error.list","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + status: 400, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_2', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Scenario to test Rate Limit Exceeded Handling from Destination', + successCriteria: 'Should return 429 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: { + email: 'new@test.com', + }, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + method: 'PUT', + headers, + }), + }, + }, + output: { + response: { + status: 429, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'rate_limit_exceeded', + message: 'The rate limit for the App has been exceeded', + }, + ], + request_id: 'request125', + type: 'error.list', + }, + status: 429, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 429. {"errors":[{"code":"rate_limit_exceeded","message":"The rate limit for the App has been exceeded"}],"request_id":"request125","type":"error.list"}', + status: 429, + statTags: { + ...statTags, + errorType: 'throttled', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_3', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Scenario to test Conflict User Handling from Destination', + successCriteria: 'Should return 409 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: conflictUserPayload, + headers, + endpoint: 'https://api.intercom.io/contacts', + method: 'POST', + }), + }, + }, + output: { + response: { + status: 409, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'conflict', + message: 'A contact matching those details already exists with id=test', + }, + ], + request_id: 'request126', + type: 'error.list', + }, + status: 409, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 409. {"errors":[{"code":"conflict","message":"A contact matching those details already exists with id=test"}],"request_id":"request126","type":"error.list"}', + status: 409, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_4', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Scenario to test Unsupported Media Handling from Destination', + successCriteria: 'Should return 406 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: createUserPayload, + headers: { + ...headers, + Accept: 'test', + 'Content-Type': 'test', + }, + endpoint: 'https://api.intercom.io/contacts', + method: 'POST', + }), + }, + }, + output: { + response: { + status: 406, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'media_type_not_acceptable', + message: 'The Accept header should send a media type of application/json', + }, + ], + type: 'error.list', + }, + status: 406, + }, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 406. {"errors":[{"code":"media_type_not_acceptable","message":"The Accept header should send a media type of application/json"}],"type":"error.list"}', + status: 406, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_other_scenario_5', + name: 'intercom_v2', + description: '[Proxy v0 API] :: Request Timeout Error Handling from Destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV0Payload({ + JSON: { + email: 'time-out@gmail.com', + }, + endpoint: 'https://api.intercom.io/contacts', + headers, + method: 'POST', + }), + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 408', + destinationResponse: { + response: { + type: 'error.list', + request_id: 'req-123', + errors: [ + { + code: 'Request Timeout', + message: 'The server would not wait any longer for the client', + }, + ], + }, + status: 408, + }, + statTags, + }, + }, + }, + }, + }, +]; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'INTERCOM_V2_v1_other_scenario_1', + name: 'intercom_v2', + description: + '[Proxy v1 API] :: Scenario to test Malformed Payload Response Handling from Destination', + successCriteria: 'Should return 400 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: { + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + method: 'PUT', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"request_id":"request_1","type":"error.list","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + statusCode: 400, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 400. {"request_id":"request_1","type":"error.list","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + status: 400, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_2', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Scenario to test Rate Limit Exceeded Handling from Destination', + successCriteria: 'Should return 429 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: { + email: 'new@test.com', + }, + endpoint: 'https://api.intercom.io/contacts/proxy-contact-id', + headers, + method: 'PUT', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"errors":[{"code":"rate_limit_exceeded","message":"The rate limit for the App has been exceeded"}],"request_id":"request125","type":"error.list"}', + statusCode: 429, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 429. {"errors":[{"code":"rate_limit_exceeded","message":"The rate limit for the App has been exceeded"}],"request_id":"request125","type":"error.list"}', + status: 429, + statTags: { + ...statTags, + errorType: 'throttled', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_3', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Scenario to test Conflict User Handling from Destination', + successCriteria: 'Should return 409 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: conflictUserPayload, + headers, + endpoint: 'https://api.intercom.io/contacts', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"errors":[{"code":"conflict","message":"A contact matching those details already exists with id=test"}],"request_id":"request126","type":"error.list"}', + statusCode: 409, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 409. {"errors":[{"code":"conflict","message":"A contact matching those details already exists with id=test"}],"request_id":"request126","type":"error.list"}', + status: 409, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_4', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Scenario to test Unsupported Media Handling from Destination', + successCriteria: 'Should return 406 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: createUserPayload, + headers: { + ...headers, + Accept: 'test', + 'Content-Type': 'test', + }, + endpoint: 'https://api.intercom.io/contacts', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + response: [ + { + error: + '{"errors":[{"code":"media_type_not_acceptable","message":"The Accept header should send a media type of application/json"}],"type":"error.list"}', + statusCode: 406, + metadata: generateMetadata(1), + }, + ], + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 406. {"errors":[{"code":"media_type_not_acceptable","message":"The Accept header should send a media type of application/json"}],"type":"error.list"}', + status: 406, + statTags: { + ...statTags, + errorType: 'aborted', + }, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_other_scenario_5', + name: 'intercom_v2', + description: '[Proxy v1 API] :: Request Timeout Error Handling from Destination', + successCriteria: 'Should return 500 status code with error message', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + JSON: { + email: 'time-out@gmail.com', + }, + endpoint: 'https://api.intercom.io/contacts', + headers, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 500, + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 408', + response: [ + { + error: + '{"type":"error.list","request_id":"req-123","errors":[{"code":"Request Timeout","message":"The server would not wait any longer for the client"}]}', + metadata: generateMetadata(1), + statusCode: 500, + }, + ], + statTags, + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/intercom_v2/dataDelivery/data.ts b/test/integrations/destinations/intercom_v2/dataDelivery/data.ts new file mode 100644 index 0000000000..1286b70f28 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/dataDelivery/data.ts @@ -0,0 +1,9 @@ +import { oauthScenariosV0, oauthScenariosV1 } from './oauth'; +import { testScenariosForV0API, testScenariosForV1API } from './business'; + +export const data = [ + ...oauthScenariosV0, + ...oauthScenariosV1, + ...testScenariosForV0API, + ...testScenariosForV1API, +]; diff --git a/test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts b/test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts new file mode 100644 index 0000000000..8f36a4bd55 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/dataDelivery/oauth.ts @@ -0,0 +1,196 @@ +import { ProxyV1TestData } from '../../../testTypes'; +import { generateMetadata, generateProxyV1Payload } from '../../../testUtils'; +import { headers, headersWithRevokedAccessToken, RouterNetworkErrorStatTags } from '../common'; + +const commonRequestParameters = { + endpoint: `https://api.intercom.io/events`, + JSON: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + }, +}; + +export const oauthScenariosV0 = [ + { + id: 'INTERCOM_V2_v0_oauth_scenario_1', + name: 'intercom_v2', + description: '[Proxy v0 API] :: [oauth] app event fails due to revoked access token', + successCriteria: 'Should return 400 with revoked access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers: headersWithRevokedAccessToken, + accessToken: 'revoked-accessToken', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + destinationResponse: { + response: { + errors: [ + { + code: 'unauthorized', + message: 'Access Token Invalid', + }, + ], + request_id: 'request_id-1', + type: 'error.list', + }, + status: 401, + }, + statTags: { + ...RouterNetworkErrorStatTags, + feature: 'dataDelivery', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 401. {"type":"error.list","request_id":"request_id-1","errors":[{"code":"unauthorized","message":"Access Token Invalid"}]}', + status: 400, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v0_oauth_scenario_2', + name: 'intercom_v2', + description: '[Proxy v0 API] :: [oauth] success case', + successCriteria: 'Should return 200 response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v0', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 202, + message: 'Request Processed Successfully', + destinationResponse: '', + }, + }, + }, + }, + }, +]; + +export const oauthScenariosV1: ProxyV1TestData[] = [ + { + id: 'INTERCOM_V2_v1_oauth_scenario_1', + name: 'intercom_v2', + description: '[Proxy v1 API] :: [oauth] app event fails due to revoked access token', + successCriteria: 'Should return 400 with revoked access token error', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers: headersWithRevokedAccessToken, + accessToken: 'revoked-accessToken', + }), + method: 'POST', + }, + }, + output: { + response: { + status: 400, + body: { + output: { + response: [ + { + error: + '{"type":"error.list","request_id":"request_id-1","errors":[{"code":"unauthorized","message":"Access Token Invalid"}]}', + statusCode: 400, + metadata: { + ...generateMetadata(1), + secret: { accessToken: 'revoked-accessToken' }, + }, + }, + ], + statTags: { + ...RouterNetworkErrorStatTags, + feature: 'dataDelivery', + }, + authErrorCategory: 'AUTH_STATUS_INACTIVE', + message: + '[Intercom V2 Response Handler] Request failed for destination intercom_v2 with status: 401. {"type":"error.list","request_id":"request_id-1","errors":[{"code":"unauthorized","message":"Access Token Invalid"}]}', + status: 400, + }, + }, + }, + }, + }, + { + id: 'INTERCOM_V2_v1_oauth_scenario_2', + name: 'intercom_v2', + description: '[Proxy v1 API] :: [oauth] success case', + successCriteria: 'Should return 200 response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload({ + ...commonRequestParameters, + headers, + }), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 202, + message: 'Request Processed Successfully', + response: [ + { + statusCode: 202, + metadata: generateMetadata(1), + error: '""', + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/intercom_v2/network.ts b/test/integrations/destinations/intercom_v2/network.ts new file mode 100644 index 0000000000..26ff3c38ee --- /dev/null +++ b/test/integrations/destinations/intercom_v2/network.ts @@ -0,0 +1,751 @@ +import { headers, headersWithRevokedAccessToken } from './common'; + +const deliveryCallsData = [ + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'test@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/companies', + data: { + company_id: 'rudderlabs', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + remote_created_at: 1726132233, + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'company', + company_id: 'rudderlabs', + id: 'company-id-by-intercom', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + created_at: 1701930212, + updated_at: 1701930212, + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'known-email@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/tags', + data: { + name: 'tag-1', + companies: [ + { + id: 'company-id-by-intercom', + }, + ], + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'tag', + name: 'tag-1', + id: '123', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/tags', + data: { + name: 'tag-2', + companies: [ + { + id: 'company-id-by-intercom', + }, + ], + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'tag', + name: 'tag-2', + id: '123', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/companies', + data: { + company_id: 'rudderlabs', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + remote_created_at: 1726132233, + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + }, + httpRes: { + status: 400, + data: { + type: 'error.list', + request_id: 'request_id-1', + errors: [ + { + code: 'parameter_invalid', + message: "Custom attribute 'isOpenSource' does not exist", + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.eu.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'test@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'userId', operator: '=', value: 'known-user-id-1' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'contact-id-by-intercom-known-user-id-1', + workspace_id: 'rudderWorkspace', + external_id: 'user-id-1', + role: 'user', + email: 'test@rudderlabs.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/companies', + data: { + company_id: 'rudderlabs', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + remote_created_at: 1726132233, + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'company', + company_id: 'rudderlabs', + id: 'au-company-id-by-intercom', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + created_at: 1701930212, + updated_at: 1701930212, + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'known-email@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'au-contact-id-by-intercom-known-email', + workspace_id: 'rudderWorkspace', + external_id: 'known-user-id-1-au', + role: 'user', + email: 'known-email@rudderlabs.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/au-contact-id-by-intercom-known-email/companies', + data: { + id: 'au-company-id-by-intercom', + }, + headers, + }, + httpRes: { + status: 200, + data: { + type: 'company', + company_id: 'rudderlabs', + id: 'company-id-by-intercom', + name: 'RudderStack', + website: 'www.rudderstack.com', + plan: 'enterprise', + size: 500, + industry: 'CDP', + user_count: 1, + remote_created_at: 1374138000, + created_at: 1701930212, + updated_at: 1701930212, + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'detach-user-company@rudderlabs.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'contact-id-by-intercom-detached-from-company', + workspace_id: 'rudderWorkspace', + external_id: 'detach-company-user-id', + role: 'user', + email: 'detach-user-company@rudderlabs.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'get', + url: 'https://api.intercom.io/companies?company_id=company id', + data: {}, + headers, + }, + httpRes: { + status: 200, + data: { + id: '123', + }, + }, + }, + { + httpReq: { + method: 'delete', + url: 'https://api.intercom.io/contacts/contact-id-by-intercom-detached-from-company/companies/123', + data: {}, + headers, + }, + httpRes: { + status: 200, + data: {}, + }, + }, + { + httpReq: { + method: 'get', + url: 'https://api.intercom.io/companies?company_id=unavailable company id', + data: {}, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req123', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'get', + url: 'https://api.intercom.io/companies?company_id=other company id', + data: {}, + headers, + }, + httpRes: { + status: 200, + data: { + id: 'other123', + }, + }, + }, + { + httpReq: { + method: 'delete', + url: 'https://api.intercom.io/contacts/contact-id-by-intercom-detached-from-company/companies/other123', + data: {}, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req123', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/events', + data: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + }, + headers: headersWithRevokedAccessToken, + }, + httpRes: { + status: 401, + data: { + type: 'error.list', + request_id: 'request_id-1', + errors: [ + { + code: 'unauthorized', + message: 'Access Token Invalid', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/events', + data: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + }, + headers, + }, + httpRes: { + status: 202, + }, + }, + { + httpReq: { + method: 'put', + url: 'https://api.intercom.io/contacts/proxy-contact-id', + data: { + custom_attributes: { + isOpenSource: true, + }, + }, + headers, + }, + httpRes: { + status: 400, + data: { + request_id: 'request_1', + type: 'error.list', + errors: [ + { + code: 'parameter_invalid', + message: "Custom attribute 'isOpenSource' does not exist", + }, + ], + }, + }, + }, + { + httpReq: { + method: 'put', + url: 'https://api.intercom.io/contacts/proxy-contact-id', + data: { + email: 'new@test.com', + }, + headers, + }, + httpRes: { + status: 429, + data: { + errors: [ + { + code: 'rate_limit_exceeded', + message: 'The rate limit for the App has been exceeded', + }, + ], + request_id: 'request125', + type: 'error.list', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts', + data: { + email: 'conflict@test.com', + user_id: 'conflict_test_user_id_1', + }, + headers, + }, + httpRes: { + status: 409, + data: { + errors: [ + { + code: 'conflict', + message: 'A contact matching those details already exists with id=test', + }, + ], + request_id: 'request126', + type: 'error.list', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts', + data: { + email: 'test-unsupported-media@rudderlabs.com', + external_id: 'user-id-1', + name: 'John Snow', + }, + headers: { + ...headers, + Accept: 'test', + 'Content-Type': 'test', + }, + }, + httpRes: { + status: 406, + data: { + errors: [ + { + code: 'media_type_not_acceptable', + message: 'The Accept header should send a media type of application/json', + }, + ], + type: 'error.list', + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts', + data: { + email: 'time-out@gmail.com', + }, + headers, + }, + httpRes: { + status: 408, + data: { + type: 'error.list', + request_id: 'req-123', + errors: [ + { + code: 'Request Timeout', + message: 'The server would not wait any longer for the client', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'test@rudderlabs.com' }], + }, + }, + headers: headersWithRevokedAccessToken, + }, + httpRes: { + status: 401, + data: { + type: 'error.list', + request_id: 'request_id-1', + errors: [ + { + code: 'unauthorized', + message: 'Access Token Invalid', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/search', + data: { + query: { + operator: 'AND', + value: [{ field: 'email', operator: '=', value: 'known-user-2-company@gmail.com' }], + }, + }, + headers, + }, + httpRes: { + status: 200, + statusText: 'ok', + data: { + type: 'list', + total_count: 0, + pages: { + type: 'pages', + page: 1, + per_page: 50, + total_pages: 0, + }, + data: [ + { + type: 'contact', + id: 'au-contact-id-by-intercom-known-user-2-company', + workspace_id: 'rudderWorkspace', + external_id: 'known-user-id-2-au', + role: 'user', + email: 'known-user-2-company@gmail.com', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.au.intercom.io/contacts/au-contact-id-by-intercom-known-user-2-company/companies', + data: { + id: 'au-company-id-by-intercom', + }, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req-1234', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, + { + httpReq: { + method: 'post', + url: 'https://api.intercom.io/tags', + data: { + name: 'tag-3', + companies: [ + { + id: 'company-id-by-intercom', + }, + ], + }, + headers, + }, + httpRes: { + status: 404, + data: { + type: 'error.list', + request_id: 'req-1234', + errors: [ + { + code: 'company_not_found', + message: 'Company Not Found', + }, + ], + }, + }, + }, +]; + +export const networkCallsData = [...deliveryCallsData]; diff --git a/test/integrations/destinations/intercom_v2/router/data.ts b/test/integrations/destinations/intercom_v2/router/data.ts new file mode 100644 index 0000000000..7656914059 --- /dev/null +++ b/test/integrations/destinations/intercom_v2/router/data.ts @@ -0,0 +1,883 @@ +import { RouterTransformationRequest } from '../../../../../src/types'; +import { generateMetadata } from '../../../testUtils'; +import { + anonymousId, + channel, + companyTraits, + destination, + destinationApiServerAU, + destinationApiServerEU, + detachUserCompanyUserTraits, + headers, + originalTimestamp, + properties, + RouterInstrumentationErrorStatTags, + RouterNetworkErrorStatTags, + timestamp, + userTraits, +} from '../common'; +import { RouterTestData } from '../../../testTypes'; + +const routerRequest1: RouterTransformationRequest = { + input: [ + { + destination, + message: { + userId: 'user-id-1', + channel, + context: { + traits: { + ...userTraits, + company: { + id: 'company id', + name: 'Test Company', + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination, + message: { + userId: 'user-id-1', + channel, + context: { + traits: userTraits, + }, + properties: properties, + event: 'Product Viewed', + type: 'track', + originalTimestamp, + timestamp, + integrations: { + All: true, + intercom: { + id: 'id-by-intercom', + }, + }, + }, + metadata: generateMetadata(2), + }, + { + destination, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + tags: ['tag-1', 'tag-2'], + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + { + destination, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + isOpenSource: true, + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(4), + }, + { + destination, + message: { + userId: 'user-id-1', + channel, + context: { + traits: { + ...userTraits, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: { + ...generateMetadata(5), + secret: { + accessToken: 'revoked-accessToken', + }, + }, + }, + { + destination, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + tags: ['tag-3'], + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(6), + }, + ], + destType: 'intercom_v2', +}; + +// eu server and send anonymous id true +const routerRequest2: RouterTransformationRequest = { + input: [ + { + destination: destinationApiServerEU, + message: { + anonymousId, + channel, + context: { + traits: userTraits, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination: destinationApiServerEU, + message: { + anonymousId, + channel, + context: { + traits: userTraits, + }, + properties: properties, + event: 'Product Viewed', + type: 'track', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + ], + destType: 'intercom_v2', +}; + +// au server and when contact found in intercom +const routerRequest3: RouterTransformationRequest = { + input: [ + { + destination: destinationApiServerAU, + message: { + userId: 'known-user-id-1', + channel, + context: { + traits: userTraits, + }, + type: 'identify', + integrations: { + All: true, + Intercom: { + lookup: 'userId', + }, + }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination: destinationApiServerAU, + message: { + groupId: 'rudderlabs', + channel, + traits: companyTraits, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + { + destination: destinationApiServerAU, + message: { + groupId: 'rudderlabs', + channel, + traits: { + ...companyTraits, + email: 'known-user-2-company@gmail.com', + }, + type: 'group', + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + ], + destType: 'intercom_v2', +}; + +// detach user and company +const routerRequest4: RouterTransformationRequest = { + input: [ + { + destination, + message: { + userId: 'detach-company-user-id', + channel, + context: { + traits: { + ...detachUserCompanyUserTraits, + company: { + id: 'company id', + name: 'Test Company', + remove: true, + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination: destination, + message: { + userId: 'detach-company-user-id', + channel, + context: { + traits: { + ...detachUserCompanyUserTraits, + company: { + id: 'unavailable company id', + name: 'Test Company', + remove: true, + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + { + destination: destination, + message: { + userId: 'detach-company-user-id', + channel, + context: { + traits: { + ...detachUserCompanyUserTraits, + company: { + id: 'other company id', + name: 'Test Company', + remove: true, + }, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + ], + destType: 'intercom_v2', +}; + +// validation +const routerRequest5: RouterTransformationRequest = { + input: [ + { + destination, + message: { + channel, + context: { + traits: { + ...userTraits, + email: null, + }, + }, + type: 'identify', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(1), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...userTraits, + email: null, + }, + }, + event: 'Product Viewed', + type: 'track', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(2), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...userTraits, + }, + }, + type: 'track', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(3), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...companyTraits, + }, + }, + type: 'group', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(4), + }, + { + destination, + message: { + channel, + context: { + traits: { + ...companyTraits, + }, + }, + type: 'dummyGroupType', + integrations: { All: true }, + originalTimestamp, + timestamp, + }, + metadata: generateMetadata(5), + }, + ], + destType: 'intercom_v2', +}; + +export const data: RouterTestData[] = [ + { + id: 'INTERCOM-V2-router-test-1', + scenario: 'Framework', + successCriteria: + 'Some events should be transformed successfully and some should fail for apiVersion v2', + name: 'intercom_v2', + description: 'INTERCOM V2 router tests', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest1, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + JSON: { + email: 'test@rudderlabs.com', + external_id: 'user-id-1', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: 'https://api.intercom.io/contacts', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(1)], + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + FORM: {}, + JSON: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'user-id-1', + id: 'id-by-intercom', + }, + JSON_ARRAY: {}, + XML: {}, + }, + endpoint: 'https://api.intercom.io/events', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(2)], + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + JSON: { + company_id: 'rudderlabs', + name: 'RudderStack', + size: 500, + website: 'www.rudderstack.com', + industry: 'CDP', + plan: 'enterprise', + remote_created_at: 1726132233, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: 'https://api.intercom.io/companies', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(3)], + statusCode: 299, + }, + { + batched: false, + error: + 'Unable to Create or Update Company due to : {"type":"error.list","request_id":"request_id-1","errors":[{"code":"parameter_invalid","message":"Custom attribute \'isOpenSource\' does not exist"}]}', + statTags: { + ...RouterInstrumentationErrorStatTags, + }, + destination, + metadata: [generateMetadata(4)], + statusCode: 400, + }, + { + batched: false, + error: + '{"message":"Unable to search contact due to","destinationResponse":"{\\"type\\":\\"error.list\\",\\"request_id\\":\\"request_id-1\\",\\"errors\\":[{\\"code\\":\\"unauthorized\\",\\"message\\":\\"Access Token Invalid\\"}]}"}', + statTags: { + ...RouterNetworkErrorStatTags, + errorType: 'retryable', + meta: 'invalidAuthToken', + }, + destination, + metadata: [ + { + ...generateMetadata(5), + secret: { + accessToken: 'revoked-accessToken', + }, + }, + ], + statusCode: 400, + }, + { + batched: false, + error: + 'Unable to Add or Update the Tag to Company due to : {"type":"error.list","request_id":"req-1234","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: { + ...RouterInstrumentationErrorStatTags, + }, + destination, + metadata: [generateMetadata(6)], + statusCode: 400, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-2', + scenario: 'Framework', + successCriteria: 'Events should be transformed successfully for apiVersion v2', + name: 'intercom_v2', + description: + 'INTERCOM V2 router tests with sendAnonymousId true for apiVersion v2 and eu apiServer', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest2, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.eu.intercom.io/contacts', + headers, + params: {}, + body: { + JSON: { + email: 'test@rudderlabs.com', + external_id: 'test-anonymous-id', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(1)], + batched: false, + statusCode: 200, + destination: destinationApiServerEU, + }, + { + batchedRequest: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.eu.intercom.io/events', + headers, + params: {}, + body: { + JSON: { + created_at: 1700628164, + email: 'test@rudderlabs.com', + event_name: 'Product Viewed', + metadata: { + price: { + amount: 3000, + currency: 'USD', + }, + revenue: { + amount: 1232, + currency: 'inr', + test: 123, + }, + }, + user_id: 'test-anonymous-id', + }, + XML: {}, + JSON_ARRAY: {}, + FORM: {}, + }, + files: {}, + }, + metadata: [generateMetadata(2)], + batched: false, + statusCode: 200, + destination: destinationApiServerEU, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-3', + scenario: 'Framework', + successCriteria: 'Events should be transformed successfully for apiVersion v2', + name: 'intercom_v2', + description: + 'INTERCOM V2 router tests when contact is found in intercom for au apiServer and apiVersion v2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest3, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + JSON: { + email: 'test@rudderlabs.com', + external_id: 'known-user-id-1', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: + 'https://api.au.intercom.io/contacts/contact-id-by-intercom-known-user-id-1', + files: {}, + headers, + method: 'PUT', + params: {}, + type: 'REST', + version: '1', + }, + destination: destinationApiServerAU, + metadata: [generateMetadata(1)], + statusCode: 200, + }, + { + batched: false, + batchedRequest: { + body: { + JSON: { + id: 'au-company-id-by-intercom', + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: + 'https://api.au.intercom.io/contacts/au-contact-id-by-intercom-known-email/companies', + files: {}, + headers, + method: 'POST', + params: {}, + type: 'REST', + version: '1', + }, + destination: destinationApiServerAU, + metadata: [generateMetadata(2)], + statusCode: 299, + }, + { + batched: false, + error: + 'Unable to attach Contact or User to Company due to : {"type":"error.list","request_id":"req-1234","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: { + ...RouterInstrumentationErrorStatTags, + }, + destination: destinationApiServerAU, + metadata: [generateMetadata(3)], + statusCode: 400, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-4', + scenario: 'Framework', + successCriteria: + 'Some identify events should be transformed successfully and some should fail for apiVersion v2', + name: 'intercom', + description: + 'INTERCOM V2 router tests for detaching contact from company in intercom for apiVersion v2', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest4, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + batchedRequest: { + body: { + JSON: { + email: 'detach-user-company@rudderlabs.com', + external_id: 'detach-company-user-id', + name: 'John Snow', + owner_id: 13, + phone: '+91 9999999999', + custom_attributes: { + address: 'california usa', + age: 23, + }, + }, + XML: {}, + FORM: {}, + JSON_ARRAY: {}, + }, + endpoint: + 'https://api.intercom.io/contacts/contact-id-by-intercom-detached-from-company', + files: {}, + headers, + method: 'PUT', + params: {}, + type: 'REST', + version: '1', + }, + destination: destination, + metadata: [generateMetadata(1)], + statusCode: 200, + }, + { + batched: false, + error: + 'Unable to get company id due to : {"type":"error.list","request_id":"req123","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: RouterInstrumentationErrorStatTags, + destination: destination, + metadata: [generateMetadata(2)], + statusCode: 400, + }, + { + batched: false, + error: + 'Unable to detach contact and company due to : {"type":"error.list","request_id":"req123","errors":[{"code":"company_not_found","message":"Company Not Found"}]}', + statTags: RouterInstrumentationErrorStatTags, + destination: destination, + metadata: [generateMetadata(3)], + statusCode: 400, + }, + ], + }, + }, + }, + }, + { + id: 'INTERCOM-V2-router-test-5', + scenario: 'Framework', + successCriteria: 'validation should pass for apiVersion v2', + name: 'intercom_v2', + description: 'INTERCOM V2 router validation tests', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: routerRequest5, + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + error: 'Either email or userId is required for Identify call', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(1)], + statusCode: 400, + }, + { + batched: false, + error: 'Either email or userId or id is required for Track call', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(2)], + statusCode: 400, + }, + { + batched: false, + error: 'Missing required value from "event"', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(3)], + statusCode: 400, + }, + { + batched: false, + error: 'Missing required value from "groupId"', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(4)], + statusCode: 400, + }, + { + batched: false, + error: 'message type dummygrouptype is not supported.', + statTags: RouterInstrumentationErrorStatTags, + destination, + metadata: [generateMetadata(5)], + statusCode: 400, + }, + ], + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/lytics/processor/data.ts b/test/integrations/destinations/lytics/processor/data.ts index 344eecc3cd..dd5511140a 100644 --- a/test/integrations/destinations/lytics/processor/data.ts +++ b/test/integrations/destinations/lytics/processor/data.ts @@ -143,6 +143,7 @@ export const data = [ body: { JSON: { _e: 'Order Completed', + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', checkout_id: 'what is checkout id here??', coupon: 'APPARELSALE', currency: 'GBP', @@ -190,6 +191,7 @@ export const data = [ 'products[2].url': 'https://www.example.com/product/offer-t-shirt', 'products[2].value': 12.99, 'products[2].variant': 'Black', + user_id: 'rudder123', revenue: 31.98, shipping: 4, value: 31.98, @@ -293,6 +295,7 @@ export const data = [ params: {}, body: { JSON: { + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', user_id: 'rudder123', 'company.id': 'abc123', createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', @@ -407,6 +410,7 @@ export const data = [ params: {}, body: { JSON: { + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', user_id: 'rudder123', 'company.id': 'abc123', createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', @@ -514,6 +518,7 @@ export const data = [ params: {}, body: { JSON: { + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', user_id: 'rudder123', 'company.id': 'abc123', createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', @@ -626,6 +631,7 @@ export const data = [ email: 'rudderTest@gmail.com', name: 'Rudder Test', plan: 'Enterprise', + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', }, XML: {}, JSON_ARRAY: {}, @@ -732,6 +738,7 @@ export const data = [ email: 'rudderTest@gmail.com', name: 'Rudder Test', plan: 'Enterprise', + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', }, XML: {}, JSON_ARRAY: {}, @@ -1116,6 +1123,7 @@ export const data = [ revenue: 31.98, shipping: 4, value: 31.98, + user_id: 'rudder123', }, XML: {}, JSON_ARRAY: {}, @@ -1209,11 +1217,13 @@ export const data = [ body: { JSON: { event: 'ApplicationLoaded', + anonymous_id: '00000000000000000000000000', path: '', referrer: '', search: '', title: '', url: '', + user_id: '12345', }, XML: {}, JSON_ARRAY: {}, @@ -1307,11 +1317,13 @@ export const data = [ body: { JSON: { event: 'ApplicationLoaded', + anonymous_id: '00000000000000000000000000', path: '', referrer: '', search: '', title: '', url: '', + user_id: '12345', }, XML: {}, JSON_ARRAY: {}, @@ -1414,6 +1426,7 @@ export const data = [ params: {}, body: { JSON: { + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', user_id: 'rudder123', 'company.id': 'abc123', createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', @@ -1440,4 +1453,113 @@ export const data = [ }, }, }, + { + name: 'lytics', + description: 'Test 11: user_id is mapped to userIdOnly', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', + channel: 'web', + context: { + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.1.6', + }, + library: { name: 'RudderLabs JavaScript SDK', version: '1.1.6' }, + locale: 'en-GB', + os: { name: '', version: '' }, + page: { + path: '/testing/script-test.html', + referrer: '', + search: '', + title: '', + url: 'http://localhost:3243/testing/script-test.html', + }, + screen: { density: 2 }, + traits: { + company: { id: 'abc123' }, + createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', + email: 'rudderTest@gmail.com', + name: 'Rudder Test', + plan: 'Enterprise', + firstName: 'Rudderstack', + lastname: 'Test', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36', + }, + integrations: { All: true }, + messageId: 'e108eb05-f6cd-4624-ba8c-568f2e2b3f92', + originalTimestamp: '2020-10-16T08:26:14.938Z', + receivedAt: '2020-10-16T13:56:14.945+05:30', + request_ip: '[::1]', + sentAt: '2020-10-16T08:26:14.939Z', + timestamp: '2020-10-16T13:56:14.944+05:30', + type: 'identify', + }, + destination: { + DestinationDefinition: { Config: { cdkV2Enabled: true } }, + Config: { apiKey: 'dummyApiKey', stream: 'default' }, + Enabled: true, + Transformations: [], + IsProcessorEnabled: true, + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + method: 'POST', + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://api.lytics.io/collect/json/default?access_token=dummyApiKey', + headers: { 'Content-Type': 'application/json' }, + params: {}, + body: { + JSON: { + anonymous_id: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', + 'company.id': 'abc123', + createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', + email: 'rudderTest@gmail.com', + name: 'Rudder Test', + plan: 'Enterprise', + first_name: 'Rudderstack', + last_name: 'Test', + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + userId: '', + }, + statusCode: 200, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/lytics/router/data.ts b/test/integrations/destinations/lytics/router/data.ts deleted file mode 100644 index e5d9adae5c..0000000000 --- a/test/integrations/destinations/lytics/router/data.ts +++ /dev/null @@ -1,299 +0,0 @@ -export const data = [ - { - name: 'lytics', - description: 'Test 0', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - anonymousId: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.1.6', - }, - library: { name: 'RudderLabs JavaScript SDK', version: '1.1.6' }, - locale: 'en-GB', - os: { name: '', version: '' }, - page: { - path: '/testing/script-test.html', - referrer: '', - search: '', - title: '', - url: 'http://localhost:3243/testing/script-test.html', - }, - screen: { density: 2 }, - traits: { - company: { id: 'abc123' }, - createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', - email: 'rudderTest@gmail.com', - name: 'Rudder Test', - plan: 'Enterprise', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36', - }, - event: 'Order Completed', - integrations: { All: true }, - messageId: 'a0adfab9-baf7-4e09-a2ce-bbe2844c324a', - timestamp: '2020-10-16T08:10:12.782Z', - properties: { - checkout_id: 'what is checkout id here??', - coupon: 'APPARELSALE', - currency: 'GBP', - order_id: 'transactionId', - products: [ - { - brand: '', - category: 'Merch', - currency: 'GBP', - image_url: 'https://www.example.com/product/bacon-jam.jpg', - name: 'Food/Drink', - position: 1, - price: 3, - product_id: 'product-bacon-jam', - quantity: 2, - sku: 'sku-1', - typeOfProduct: 'Food', - url: 'https://www.example.com/product/bacon-jam', - value: 6, - variant: 'Extra topped', - }, - { - brand: 'Levis', - category: 'Merch', - currency: 'GBP', - image_url: 'https://www.example.com/product/t-shirt.jpg', - name: 'T-Shirt', - position: 2, - price: 12.99, - product_id: 'product-t-shirt', - quantity: 1, - sku: 'sku-2', - typeOfProduct: 'Shirt', - url: 'https://www.example.com/product/t-shirt', - value: 12.99, - variant: 'White', - }, - { - brand: 'Levis', - category: 'Merch', - coupon: 'APPARELSALE', - currency: 'GBP', - image_url: 'https://www.example.com/product/offer-t-shirt.jpg', - name: 'T-Shirt-on-offer', - position: 1, - price: 12.99, - product_id: 'offer-t-shirt', - quantity: 1, - sku: 'sku-3', - typeOfProduct: 'Shirt', - url: 'https://www.example.com/product/offer-t-shirt', - value: 12.99, - variant: 'Black', - }, - ], - revenue: 31.98, - shipping: 4, - value: 31.98, - }, - receivedAt: '2020-10-16T13:40:12.792+05:30', - request_ip: '[::1]', - sentAt: '2020-10-16T08:10:12.783Z', - type: 'track', - userId: 'rudder123', - }, - metadata: { jobId: 1, userId: 'u1' }, - destination: { - Config: { apiKey: 'dummyApiKey', stream: 'default' }, - Enabled: true, - Transformations: [], - IsProcessorEnabled: true, - }, - }, - { - message: { - anonymousId: '4eb021e9-a2af-4926-ae82-fe996d12f3c5', - channel: 'web', - context: { - app: { - build: '1.0.0', - name: 'RudderLabs JavaScript SDK', - namespace: 'com.rudderlabs.javascript', - version: '1.1.6', - }, - library: { name: 'RudderLabs JavaScript SDK', version: '1.1.6' }, - locale: 'en-GB', - os: { name: '', version: '' }, - page: { - path: '/testing/script-test.html', - referrer: '', - search: '', - title: '', - url: 'http://localhost:3243/testing/script-test.html', - }, - screen: { density: 2 }, - traits: { - company: { id: 'abc123' }, - createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', - email: 'rudderTest@gmail.com', - name: 'Rudder Test', - plan: 'Enterprise', - }, - userAgent: - 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.80 Safari/537.36', - }, - integrations: { All: true }, - messageId: 'e108eb05-f6cd-4624-ba8c-568f2e2b3f92', - originalTimestamp: '2020-10-16T08:26:14.938Z', - receivedAt: '2020-10-16T13:56:14.945+05:30', - request_ip: '[::1]', - sentAt: '2020-10-16T08:26:14.939Z', - timestamp: '2020-10-16T13:56:14.944+05:30', - type: 'identify', - userId: 'rudder123', - }, - metadata: { jobId: 2, userId: 'u1' }, - destination: { - Config: { apiKey: 'dummyApiKey', stream: 'default' }, - Enabled: true, - Transformations: [], - IsProcessorEnabled: true, - }, - }, - ], - destType: 'lytics', - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.lytics.io/collect/json/default?access_token=dummyApiKey', - headers: { 'Content-Type': 'application/json' }, - params: {}, - body: { - JSON: { - _e: 'Order Completed', - checkout_id: 'what is checkout id here??', - coupon: 'APPARELSALE', - currency: 'GBP', - order_id: 'transactionId', - 'products[0].brand': '', - 'products[0].category': 'Merch', - 'products[0].currency': 'GBP', - 'products[0].image_url': 'https://www.example.com/product/bacon-jam.jpg', - 'products[0].name': 'Food/Drink', - 'products[0].position': 1, - 'products[0].price': 3, - 'products[0].product_id': 'product-bacon-jam', - 'products[0].quantity': 2, - 'products[0].sku': 'sku-1', - 'products[0].typeOfProduct': 'Food', - 'products[0].url': 'https://www.example.com/product/bacon-jam', - 'products[0].value': 6, - 'products[0].variant': 'Extra topped', - 'products[1].brand': 'Levis', - 'products[1].category': 'Merch', - 'products[1].currency': 'GBP', - 'products[1].image_url': 'https://www.example.com/product/t-shirt.jpg', - 'products[1].name': 'T-Shirt', - 'products[1].position': 2, - 'products[1].price': 12.99, - 'products[1].product_id': 'product-t-shirt', - 'products[1].quantity': 1, - 'products[1].sku': 'sku-2', - 'products[1].typeOfProduct': 'Shirt', - 'products[1].url': 'https://www.example.com/product/t-shirt', - 'products[1].value': 12.99, - 'products[1].variant': 'White', - 'products[2].brand': 'Levis', - 'products[2].category': 'Merch', - 'products[2].coupon': 'APPARELSALE', - 'products[2].currency': 'GBP', - 'products[2].image_url': 'https://www.example.com/product/offer-t-shirt.jpg', - 'products[2].name': 'T-Shirt-on-offer', - 'products[2].position': 1, - 'products[2].price': 12.99, - 'products[2].product_id': 'offer-t-shirt', - 'products[2].quantity': 1, - 'products[2].sku': 'sku-3', - 'products[2].typeOfProduct': 'Shirt', - 'products[2].url': 'https://www.example.com/product/offer-t-shirt', - 'products[2].value': 12.99, - 'products[2].variant': 'Black', - revenue: 31.98, - shipping: 4, - value: 31.98, - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [{ jobId: 1, userId: 'u1' }], - batched: false, - statusCode: 200, - destination: { - Config: { apiKey: 'dummyApiKey', stream: 'default' }, - Enabled: true, - Transformations: [], - IsProcessorEnabled: true, - }, - }, - { - batchedRequest: { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://api.lytics.io/collect/json/default?access_token=dummyApiKey', - headers: { 'Content-Type': 'application/json' }, - params: {}, - body: { - JSON: { - user_id: 'rudder123', - 'company.id': 'abc123', - createdAt: 'Thu Mar 24 2016 17:46:45 GMT+0000 (UTC)', - email: 'rudderTest@gmail.com', - name: 'Rudder Test', - plan: 'Enterprise', - }, - XML: {}, - JSON_ARRAY: {}, - FORM: {}, - }, - files: {}, - }, - metadata: [{ jobId: 2, userId: 'u1' }], - batched: false, - statusCode: 200, - destination: { - Config: { apiKey: 'dummyApiKey', stream: 'default' }, - Enabled: true, - Transformations: [], - IsProcessorEnabled: true, - }, - }, - ], - }, - }, - }, - }, -]; diff --git a/test/integrations/destinations/posthog/processor/data.ts b/test/integrations/destinations/posthog/processor/data.ts index f33aa268b7..35e12b4aab 100644 --- a/test/integrations/destinations/posthog/processor/data.ts +++ b/test/integrations/destinations/posthog/processor/data.ts @@ -74,7 +74,7 @@ export const data = [ $ip: '0.0.0.0', $timestamp: '2020-11-04T13:21:09.712Z', $anon_distinct_id: 'f3cf54d8-f237-45d2-89f7-ccd70d42cf31', - distinct_id: 'prevId_1', + distinct_id: 'uid-1', $device_manufacturer: 'Xiaomi', $os_version: '8.1.0', $app_version: '1.1.7', @@ -84,7 +84,7 @@ export const data = [ $device_model: 'Redmi 6', $app_namespace: 'com.rudderlabs.javascript', $app_build: '1.0.0', - alias: 'uid-1', + alias: 'prevId_1', }, timestamp: '2020-11-04T13:21:09.712Z', event: '$create_alias', diff --git a/test/integrations/destinations/posthog/router/data.ts b/test/integrations/destinations/posthog/router/data.ts index dab8ba8b1c..20870670dc 100644 --- a/test/integrations/destinations/posthog/router/data.ts +++ b/test/integrations/destinations/posthog/router/data.ts @@ -79,7 +79,7 @@ export const data = [ $ip: '0.0.0.0', $timestamp: '2020-11-04T13:21:09.712Z', $anon_distinct_id: 'f3cf54d8-f237-45d2-89f7-ccd70d42cf31', - distinct_id: 'prevId_1', + distinct_id: 'uid-1', $device_manufacturer: 'Xiaomi', $os_version: '8.1.0', $app_version: '1.1.7', @@ -89,7 +89,7 @@ export const data = [ $device_model: 'Redmi 6', $app_namespace: 'com.rudderlabs.javascript', $app_build: '1.0.0', - alias: 'uid-1', + alias: 'prevId_1', }, timestamp: '2020-11-04T13:21:09.712Z', event: '$create_alias', diff --git a/test/integrations/destinations/salesforce/processor/data.ts b/test/integrations/destinations/salesforce/processor/data.ts index 7a44b6b8ed..b33b75b55b 100644 --- a/test/integrations/destinations/salesforce/processor/data.ts +++ b/test/integrations/destinations/salesforce/processor/data.ts @@ -1373,7 +1373,7 @@ export const data = [ }, { name: 'salesforce', - description: 'Test 10', + description: 'Test 11', feature: 'processor', module: 'destination', version: 'v0', @@ -1389,9 +1389,9 @@ export const data = [ sandbox: true, }, DestinationDefinition: { - DisplayName: 'Salesforce', + DisplayName: 'Salesforce Sandbox', ID: '1T96GHZ0YZ1qQSLULHCoJkow9KC', - Name: 'SALESFORCE', + Name: 'SALESFORCE_OAUTH_SANDBOX', }, Enabled: true, ID: '1ut7LcVW1QC56y2EoTNo7ZwBWSY', @@ -1412,7 +1412,7 @@ export const data = [ externalId: [ { id: 'a005g0000383kmUAAQ', - type: 'SALESFORCE-custom_object__c', + type: 'SALESFORCE_OAUTH_SANDBOX-custom_object__c', identifierType: 'Id', }, ], @@ -1499,4 +1499,112 @@ export const data = [ }, }, }, + { + name: 'salesforce', + description: 'Test 12 : Retry happens when no secret information is found', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + initialAccessToken: '7fiy1FKcO9sohsxq1v6J88sg', + password: 'dummyPassword2', + userName: 'test.c97-qvpd@force.com.test', + sandbox: true, + }, + DestinationDefinition: { + DisplayName: 'Salesforce Sandbox', + ID: '1T96GHZ0YZ1qQSLULHCoJkow9KC', + Name: 'SALESFORCE_OAUTH_SANDBOX', + }, + Enabled: true, + ID: '1ut7LcVW1QC56y2EoTNo7ZwBWSY', + Name: 'Test SF', + Transformations: [], + }, + metadata: { + jobId: 1, + }, + message: { + anonymousId: '1e7673da-9473-49c6-97f7-da848ecafa76', + channel: 'web', + context: { + mappedToDestination: true, + externalId: [ + { + id: 'a005g0000383kmUAAQ', + type: 'SALESFORCE_OAUTH_SANDBOX-custom_object__c', + identifierType: 'Id', + }, + ], + app: { + build: '1.0.0', + name: 'RudderLabs JavaScript SDK', + namespace: 'com.rudderlabs.javascript', + version: '1.0.0', + }, + ip: '0.0.0.0', + library: { + name: 'RudderLabs JavaScript SDK', + version: '1.0.0', + }, + locale: 'en-US', + os: { + name: '', + version: '', + }, + screen: { + density: 2, + }, + traits: { + email: 'john@rs.com', + firstname: 'john doe', + Id: 'some-id', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36', + }, + integrations: { + All: true, + }, + messageId: 'f19c35da-e9de-4c6e-b6e5-9e60cccc12c8', + originalTimestamp: '2020-01-27T12:20:55.301Z', + receivedAt: '2020-01-27T17:50:58.657+05:30', + request_ip: '14.98.244.60', + sentAt: '2020-01-27T12:20:56.849Z', + timestamp: '2020-01-27T17:50:57.109+05:30', + type: 'identify', + userId: '1e7673da-9473-49c6-97f7-da848ecafa76', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + statusCode: 500, + error: 'secret is undefined/null', + metadata: { + jobId: 1, + }, + statTags: { + errorCategory: 'platform', + errorType: 'oAuthSecret', + destType: 'SALESFORCE', + module: 'destination', + implementation: 'native', + feature: 'processor', + }, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts new file mode 100644 index 0000000000..bed8eec8db --- /dev/null +++ b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/data.ts @@ -0,0 +1,3 @@ +import { testScenariosForV1API } from './oauth'; + +export const data = [...testScenariosForV1API]; diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts new file mode 100644 index 0000000000..30ee516e72 --- /dev/null +++ b/test/integrations/destinations/salesforce_oauth_sandbox/dataDelivery/oauth.ts @@ -0,0 +1,174 @@ +import { ProxyMetdata } from '../../../../../src/types'; +import { ProxyV1TestData } from '../../../testTypes'; +import { generateProxyV1Payload } from '../../../testUtils'; + +const commonHeadersForWrongToken = { + Authorization: 'Bearer expiredAccessToken', + 'Content-Type': 'application/json', +}; + +const commonHeadersForRightToken = { + Authorization: 'Bearer correctAccessToken', + 'Content-Type': 'application/json', +}; +const params = { destination: 'salesforce_oauth_sandbox' }; + +const users = [ + { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', + }, +]; + +const statTags = { + retryable: { + destType: 'SALESFORCE_OAUTH_SANDBOX', + destinationId: 'dummyDestinationId', + errorCategory: 'network', + errorType: 'retryable', + feature: 'dataDelivery', + implementation: 'native', + module: 'destination', + workspaceId: 'dummyWorkspaceId', + }, +}; + +const commonRequestParametersWithWrongToken = { + headers: commonHeadersForWrongToken, + JSON: users[0], + params, +}; + +const commonRequestParametersWithRightToken = { + headers: commonHeadersForRightToken, + JSON: users[0], + params, +}; + +export const proxyMetdataWithSecretWithWrongAccessToken: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: { + access_token: 'expiredAccessToken', + instanceUrl: 'https://rudderstack.my.salesforce_oauth_sandbox.com', + }, + destInfo: { authKey: 'dummyDestinationId' }, + dontBatch: false, +}; + +export const proxyMetdataWithSecretWithRightAccessToken: ProxyMetdata = { + jobId: 1, + attemptNum: 1, + userId: 'dummyUserId', + sourceId: 'dummySourceId', + destinationId: 'dummyDestinationId', + workspaceId: 'dummyWorkspaceId', + secret: { + access_token: 'expiredRightToken', + instanceUrl: 'https://rudderstack.my.salesforce_oauth_sandbox.com', + }, + destInfo: { authKey: 'dummyDestinationId' }, + dontBatch: false, +}; + +export const reqMetadataArrayWithWrongSecret = [proxyMetdataWithSecretWithWrongAccessToken]; +export const reqMetadataArray = [proxyMetdataWithSecretWithRightAccessToken]; + +export const testScenariosForV1API: ProxyV1TestData[] = [ + { + id: 'salesforce_v1_scenario_1', + name: 'salesforce_oauth_sandbox', + description: '[Proxy v1 API] :: Test with expired access token scenario', + successCriteria: + 'Should return 5XX with error category REFRESH_TOKEN and Session expired or invalid, INVALID_SESSION_ID', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParametersWithWrongToken, + endpoint: + 'https://rudderstack.my.salesforce_oauth_sandbox.com/services/data/v50.0/sobjects/Lead/20', + }, + reqMetadataArrayWithWrongSecret, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 500, + body: { + output: { + status: 500, + authErrorCategory: 'REFRESH_TOKEN', + message: + 'Salesforce Request Failed - due to "INVALID_SESSION_ID", (Retryable) during Salesforce Response Handling', + response: [ + { + error: + '[{"message":"Session expired or invalid","errorCode":"INVALID_SESSION_ID"}]', + metadata: proxyMetdataWithSecretWithWrongAccessToken, + statusCode: 500, + }, + ], + statTags: statTags.retryable, + }, + }, + }, + }, + }, + { + id: 'salesforce_v1_scenario_2', + name: 'salesforce', + description: + '[Proxy v1 API] :: Test for a valid request - Lead creation with existing unchanged leadId and unchanged data', + successCriteria: 'Should return 200 with no error with destination response', + scenario: 'Business', + feature: 'dataDelivery', + module: 'destination', + version: 'v1', + input: { + request: { + body: generateProxyV1Payload( + { + ...commonRequestParametersWithRightToken, + endpoint: + 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/existing_unchanged_leadId', + }, + reqMetadataArray, + ), + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: { + status: 200, + message: 'Request for destination: salesforce Processed Successfully', + response: [ + { + error: '{"statusText":"No Content"}', + metadata: proxyMetdataWithSecretWithRightAccessToken, + statusCode: 200, + }, + ], + }, + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/salesforce_oauth_sandbox/network.ts b/test/integrations/destinations/salesforce_oauth_sandbox/network.ts new file mode 100644 index 0000000000..09d2c759d2 --- /dev/null +++ b/test/integrations/destinations/salesforce_oauth_sandbox/network.ts @@ -0,0 +1,51 @@ +const headerWithWrongAccessToken = { + Authorization: 'Bearer expiredAccessToken', + 'Content-Type': 'application/json', +}; + +const headerWithRightAccessToken = { + Authorization: 'Bearer correctAccessToken', + 'Content-Type': 'application/json', +}; + +const dataValue = { + Email: 'danis.archurav@sbermarket.ru', + Company: 'itus.ru', + LastName: 'Danis', + FirstName: 'Archurav', + LeadSource: 'App Signup', + account_type__c: 'free_trial', +}; + +const businessMockData = [ + { + description: 'Mock response from destination depicting an expired access token', + httpReq: { + method: 'post', + url: 'https://rudderstack.my.salesforce_oauth_sandbox.com/services/data/v50.0/sobjects/Lead/20', + headers: headerWithWrongAccessToken, + data: dataValue, + params: { destination: 'salesforce_oauth_sandbox' }, + }, + httpRes: { + data: [{ message: 'Session expired or invalid', errorCode: 'INVALID_SESSION_ID' }], + status: 401, + }, + }, + { + description: + 'Mock response from destination depicting a valid lead request, with no changed data', + httpReq: { + method: 'post', + url: 'https://rudderstack.my.salesforce.com/services/data/v50.0/sobjects/Lead/existing_unchanged_leadId', + data: dataValue, + headers: headerWithRightAccessToken, + }, + httpRes: { + data: { statusText: 'No Content' }, + status: 204, + }, + }, +]; + +export const networkCallsData = [...businessMockData]; diff --git a/test/integrations/destinations/singular/processor/data.ts b/test/integrations/destinations/singular/processor/data.ts index 22c075fffc..6e749dd0a0 100644 --- a/test/integrations/destinations/singular/processor/data.ts +++ b/test/integrations/destinations/singular/processor/data.ts @@ -1903,4 +1903,289 @@ export const data = [ }, }, }, + { + name: 'singular', + description: '(Unity) Session Event', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + apiKey: 'dummyApiKey', + sessionEventList: [ + { sessionEventName: 'mysessionevent' }, + { sessionEventName: 'randomuser' }, + { sessionEventName: 'titanium' }, + ], + }, + }, + message: { + type: 'track', + event: 'mysessionevent', + userId: 'ruddersampleX3', + request_ip: '14.5.67.21', + context: { + app: { + build: '1', + name: 'RudderAndroidClient', + namespace: 'com.singular.game', + version: '1.1.5.581823alpha', + }, + device: { + manufacturer: 'Google', + model: 'Android SDK built for x86', + name: 'generic_x86', + type: 'android', + advertisingId: '8ecd7512-2864-440c-93f3-a3cabe62525b', + attStatus: true, + id: '49c2d3a6-326e-4ec5-a16b-0a47e34ed953', + adTrackingEnabled: true, + token: 'testDeviceToken', + }, + library: { name: 'com.rudderstack.android.sdk.core', version: '0.1.4' }, + locale: 'en-US', + network: { carrier: 'Android', bluetooth: false, cellular: true, wifi: true }, + campaign: { + source: 'google', + medium: 'medium', + term: 'keyword', + content: 'some content', + }, + os: { name: 'nintendo', version: '360v2-2024h1' }, + screen: { density: 420, height: 1794, width: 1080 }, + timezone: 'Asia/Mumbai', + userAgent: + 'Mozilla/5.0 (Nintendo Switch; WebApplet) AppleWebKit/613.0 (KHTML, like Gecko) NF/6.0.3.25.0 NintendoBrowser/5.1.0.32061', + }, + properties: { + asid: 'IISqwYJKoZIcNqts0jvcNvPc', + url: 'myapp%3A%2F%2Fhome%2Fpage%3Fqueryparam1%3Dvalue1', + install: 'false', + install_source: 'nintendo', + category: 'Games', + affiliation: 'Apple Store', + receipt_signature: '1234dfghnh', + referring_application: '2134dfg', + os: 'nintendo_switch', + data_sharing_options: '%7B%22limit_data_sharing%22%3Atrue%7D', + }, + timestamp: '2024-06-01T11:26:50.000Z', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'GET', + endpoint: 'https://s2s.singular.net/api/v1/launch', + headers: {}, + params: { + a: 'dummyApiKey', + av: '1.1.5.581823alpha', + data_sharing_options: '%7B%22limit_data_sharing%22%3Atrue%7D', + i: 'com.singular.game', + install: 'false', + install_source: 'nintendo', + ip: '14.5.67.21', + os: 'nintendo_switch', + p: 'nintendo', + sdid: '49c2d3a6-326e-4ec5-a16b-0a47e34ed953', + ua: 'Mozilla/5.0 (Nintendo Switch; WebApplet) AppleWebKit/613.0 (KHTML, like Gecko) NF/6.0.3.25.0 NintendoBrowser/5.1.0.32061', + utime: 1717241210, + ve: '360v2-2024h1', + }, + body: { JSON: {}, JSON_ARRAY: {}, XML: {}, FORM: {} }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, + { + name: 'singular', + description: '(Unity) Custom Event with multiple products', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + destination: { + Config: { + apiKey: 'dummyApiKey', + sessionEventList: [ + { sessionEventName: 'mysessionevent' }, + { sessionEventName: 'randomuser' }, + { sessionEventName: 'titanium' }, + ], + }, + }, + message: { + type: 'track', + event: 'myevent', + userId: 'ruddersampleX4', + request_ip: '14.5.67.21', + context: { + app: { + build: '1', + name: 'RudderAndroidClient', + namespace: 'com.singular.game', + version: '1.1.5.581823alpha', + }, + device: { + manufacturer: 'Google', + model: 'Android SDK built for x86', + name: 'generic_x86', + type: 'android', + advertisingId: '8ecd7512-2864-440c-93f3-a3cabe62525b', + attStatus: true, + id: '49c2d3a6-326e-4ec5-a16b-0a47e34ed953', + adTrackingEnabled: true, + token: 'testDeviceToken', + }, + library: { name: 'com.rudderstack.android.sdk.core', version: '0.1.4' }, + locale: 'en-US', + network: { carrier: 'Android', bluetooth: false, cellular: true, wifi: true }, + campaign: { + source: 'google', + medium: 'medium', + term: 'keyword', + content: 'some content', + }, + os: { name: 'metaquest', version: 'qst2-2023h2' }, + screen: { density: 420, height: 1794, width: 1080 }, + timezone: 'Asia/Mumbai', + userAgent: + 'Mozilla/5.0 (Nintendo Switch; WebApplet) AppleWebKit/613.0 (KHTML, like Gecko) NF/6.0.3.25.0 NintendoBrowser/5.1.0.32061', + }, + properties: { + asid: 'IISqwYJKoZIcNqts0jvcNvPc', + url: 'myapp%3A%2F%2Fhome%2Fpage%3Fqueryparam1%3Dvalue1', + install: 'SM-G935F', + install_source: 'selfdistributed', + category: 'Games', + checkout_id: '12345', + order_id: '1234', + receipt_signature: '1234dfghnh', + referring_application: '2134dfg', + total: 20, + revenue: 15, + shipping: 22, + tax: 1, + discount: 1.5, + coupon: 'ImagePro', + currency: 'USD', + fetch_token: 'dummyFetchToken', + product_id: '123', + is_revenue_event: true, + os: 'metaquest_pro', + products: [ + { + product_id: '789', + sku: 'G-32', + name: 'Monopoly', + price: 14, + quantity: 2, + category: 'Games', + url: 'https://www.website.com/product/path', + image_url: 'https://www.website.com/product/path.jpg', + }, + { sku: 'F-32', name: 'UNO', price: 3.45, quantity: 2, category: 'Games' }, + ], + }, + timestamp: '2021-09-01T15:46:51.000Z', + }, + }, + ], + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + version: '1', + type: 'REST', + method: 'GET', + endpoint: 'https://s2s.singular.net/api/v1/evt', + headers: {}, + params: { + n: 'myevent', + ip: '14.5.67.21', + av: '1.1.5.581823alpha', + is_revenue_event: true, + i: 'com.singular.game', + utime: 1630511211, + cur: 'USD', + amt: 28, + purchase_product_id: '789', + a: 'dummyApiKey', + install_source: 'selfdistributed', + os: 'metaquest_pro', + p: 'metaquest', + sdid: '49c2d3a6-326e-4ec5-a16b-0a47e34ed953', + ua: 'Mozilla/5.0 (Nintendo Switch; WebApplet) AppleWebKit/613.0 (KHTML, like Gecko) NF/6.0.3.25.0 NintendoBrowser/5.1.0.32061', + ve: 'qst2-2023h2', + install: 'SM-G935F', + }, + body: { JSON: {}, JSON_ARRAY: {}, XML: {}, FORM: {} }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + { + output: { + version: '1', + type: 'REST', + method: 'GET', + endpoint: 'https://s2s.singular.net/api/v1/evt', + headers: {}, + params: { + n: 'myevent', + i: 'com.singular.game', + ip: '14.5.67.21', + is_revenue_event: true, + utime: 1630511211, + cur: 'USD', + purchase_product_id: 'F-32', + install: 'SM-G935F', + install_source: 'selfdistributed', + amt: 6.9, + os: 'metaquest_pro', + p: 'metaquest', + a: 'dummyApiKey', + sdid: '49c2d3a6-326e-4ec5-a16b-0a47e34ed953', + ua: 'Mozilla/5.0 (Nintendo Switch; WebApplet) AppleWebKit/613.0 (KHTML, like Gecko) NF/6.0.3.25.0 NintendoBrowser/5.1.0.32061', + ve: 'qst2-2023h2', + av: '1.1.5.581823alpha', + }, + body: { JSON: {}, JSON_ARRAY: {}, XML: {}, FORM: {} }, + files: {}, + userId: '', + }, + statusCode: 200, + }, + ], + }, + }, + }, ]; diff --git a/test/integrations/destinations/the_trade_desk/router/business.ts b/test/integrations/destinations/the_trade_desk/router/business.ts new file mode 100644 index 0000000000..556e69a909 --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/router/business.ts @@ -0,0 +1,330 @@ +import { defaultMockFns } from '../mocks'; +import { + destType, + advertiserId, + dataProviderId, + segmentName, + sampleDestination, + sampleContext, +} from '../common'; + +export const business = [ + { + id: 'trade_desk-business-test-1', + name: destType, + description: 'Add IDs to the segment', + scenario: 'Framework', + successCriteria: 'Response should contain all the mapping and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: { + jobId: 1, + userId: 'u1', + }, + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-2', + UID2: 'test-uid2-2', + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: sampleDestination, + metadata: { + jobId: 2, + userId: 'u1', + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + UID2: 'test-uid2-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + DAID: 'test-daid-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + UID2: 'test-uid2-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [ + { + jobId: 1, + userId: 'u1', + }, + { + jobId: 2, + userId: 'u1', + }, + ], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'trade_desk-business-test-2', + name: destType, + description: + 'Add/Remove IDs to/from the segment and split into multiple requests based on size', + successCriteria: 'Response should contain all the mapping and status code should be 200', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: { + jobId: 1, + userId: 'u1', + }, + }, + { + message: { + type: 'record', + action: 'delete', + fields: { + DAID: 'test-daid-2', + UID2: 'test-uid2-2', + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: sampleDestination, + metadata: { + jobId: 2, + userId: 'u1', + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + UID2: 'test-uid2-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + DAID: 'test-daid-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + UID2: 'test-uid2-2', + Data: [ + { + Name: segmentName, + TTLInMinutes: 0, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [ + { + jobId: 1, + userId: 'u1', + }, + { + jobId: 2, + userId: 'u1', + }, + ], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, +]; diff --git a/test/integrations/destinations/the_trade_desk/router/data.ts b/test/integrations/destinations/the_trade_desk/router/data.ts index d2dbf9a7cc..6de2069ff4 100644 --- a/test/integrations/destinations/the_trade_desk/router/data.ts +++ b/test/integrations/destinations/the_trade_desk/router/data.ts @@ -1,1064 +1,3 @@ -import { overrideDestination } from '../../../testUtils'; -import { defaultMockFns } from '../mocks'; -import { - destType, - destTypeInUpperCase, - advertiserId, - dataProviderId, - segmentName, - sampleDestination, - sampleContext, -} from '../common'; - -export const data = [ - { - name: destType, - description: 'Add IDs to the segment', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: sampleDestination, - metadata: { - jobId: 1, - userId: 'u1', - }, - }, - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-2', - UID2: 'test-uid2-2', - }, - channel: 'sources', - context: sampleContext, - recordId: '2', - }, - destination: sampleDestination, - metadata: { - jobId: 2, - userId: 'u1', - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: [ - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - DAID: 'test-daid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - UID2: 'test-uid2-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - DAID: 'test-daid-2', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - UID2: 'test-uid2-2', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - ], - metadata: [ - { - jobId: 1, - userId: 'u1', - }, - { - jobId: 2, - userId: 'u1', - }, - ], - batched: true, - statusCode: 200, - destination: sampleDestination, - }, - ], - }, - }, - }, - mockFns: defaultMockFns, - }, - { - name: destType, - description: - 'Add/Remove IDs to/from the segment and split into multiple requests based on size', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: sampleDestination, - metadata: { - jobId: 1, - userId: 'u1', - }, - }, - { - message: { - type: 'record', - action: 'delete', - fields: { - DAID: 'test-daid-2', - UID2: 'test-uid2-2', - }, - channel: 'sources', - context: sampleContext, - recordId: '2', - }, - destination: sampleDestination, - metadata: { - jobId: 2, - userId: 'u1', - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: [ - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - DAID: 'test-daid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - UID2: 'test-uid2-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - DAID: 'test-daid-2', - Data: [ - { - Name: segmentName, - TTLInMinutes: 0, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - UID2: 'test-uid2-2', - Data: [ - { - Name: segmentName, - TTLInMinutes: 0, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - ], - metadata: [ - { - jobId: 1, - userId: 'u1', - }, - { - jobId: 2, - userId: 'u1', - }, - ], - batched: true, - statusCode: 200, - destination: sampleDestination, - }, - ], - }, - }, - }, - mockFns: defaultMockFns, - }, - { - name: destType, - description: - 'Missing segment name (audienceId) in the config (segment name will be populated from vdm)', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: overrideDestination(sampleDestination, { audienceId: '' }), - metadata: { - jobId: 1, - userId: 'u1', - }, - }, - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-2', - UID2: 'test-uid2-2', - }, - channel: 'sources', - context: sampleContext, - recordId: '2', - }, - destination: overrideDestination(sampleDestination, { audienceId: '' }), - metadata: { - jobId: 2, - userId: 'u1', - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batched: false, - metadata: [ - { jobId: 1, userId: 'u1' }, - { jobId: 2, userId: 'u1' }, - ], - statusCode: 400, - error: 'Segment name/Audience ID is not present. Aborting', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'configuration', - }, - }, - ], - }, - }, - }, - }, - { - name: destType, - description: 'Missing advertiser ID in the config', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: overrideDestination(sampleDestination, { advertiserId: '' }), - metadata: { - jobId: 1, - userId: 'u1', - }, - }, - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-2', - UID2: 'test-uid2-2', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: overrideDestination(sampleDestination, { advertiserId: '' }), - metadata: { - jobId: 2, - userId: 'u1', - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batched: false, - metadata: [ - { jobId: 1, userId: 'u1' }, - { jobId: 2, userId: 'u1' }, - ], - statusCode: 400, - error: 'Advertiser ID is not present. Aborting', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'configuration', - }, - }, - ], - }, - }, - }, - }, - { - name: destType, - description: 'Missing advertiser secret key in the config', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: overrideDestination(sampleDestination, { advertiserSecretKey: '' }), - metadata: { - jobId: 1, - userId: 'u1', - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batched: false, - metadata: [{ jobId: 1, userId: 'u1' }], - statusCode: 400, - error: 'Advertiser Secret Key is not present. Aborting', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'configuration', - }, - }, - ], - }, - }, - }, - }, - { - name: destType, - description: 'TTL is out of range', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: overrideDestination(sampleDestination, { ttlInDays: 190 }), - metadata: { - jobId: 1, - userId: 'u1', - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batched: false, - metadata: [{ jobId: 1, userId: 'u1' }], - statusCode: 400, - error: 'TTL is out of range. Allowed values are 0 to 180 days', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'configuration', - }, - }, - ], - }, - }, - }, - }, - { - name: destType, - description: 'Invalid action type', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: sampleDestination, - metadata: { - jobId: 1, - }, - }, - { - message: { - type: 'record', - action: 'update', - fields: { - DAID: 'test-daid-2', - UID2: null, - }, - channel: 'sources', - context: sampleContext, - recordId: '2', - }, - destination: sampleDestination, - metadata: { - jobId: 2, - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: [ - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - DAID: 'test-daid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - UID2: 'test-uid2-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - ], - metadata: [ - { - jobId: 1, - }, - ], - batched: true, - statusCode: 200, - destination: sampleDestination, - }, - { - batched: false, - metadata: [{ jobId: 2 }], - statusCode: 400, - error: - 'Invalid action type. You can only add or remove IDs from the audience/segment', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - }, - destination: sampleDestination, - }, - ], - }, - }, - }, - mockFns: defaultMockFns, - }, - { - name: destType, - description: 'Empty fields in the message', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: {}, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: sampleDestination, - metadata: { - jobId: 1, - }, - }, - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - UID2: 'test-uid2-1', - EUID: 'test-euid-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '2', - }, - destination: sampleDestination, - metadata: { - jobId: 2, - }, - }, - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-2', - UID2: null, - EUID: null, - }, - channel: 'sources', - context: sampleContext, - recordId: '3', - }, - destination: sampleDestination, - metadata: { - jobId: 3, - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: [ - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - DAID: 'test-daid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - UID2: 'test-uid2-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - { - EUID: 'test-euid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - ], - metadata: [ - { - jobId: 2, - }, - ], - batched: true, - statusCode: 200, - destination: sampleDestination, - }, - { - batched: false, - metadata: [{ jobId: 1 }], - statusCode: 400, - error: '`fields` cannot be empty', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - }, - destination: sampleDestination, - }, - { - batched: false, - metadata: [{ jobId: 3 }], - statusCode: 400, - error: '`fields` cannot be empty', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - }, - destination: sampleDestination, - }, - ], - }, - }, - }, - mockFns: defaultMockFns, - }, - { - name: destType, - description: '`fields` is missing', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: sampleDestination, - metadata: { - jobId: 1, - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batched: false, - metadata: [{ jobId: 1 }], - statusCode: 400, - error: '`fields` cannot be empty', - statTags: { - destType: destTypeInUpperCase, - implementation: 'cdkV2', - feature: 'router', - module: 'destination', - errorCategory: 'dataValidation', - errorType: 'instrumentation', - }, - destination: sampleDestination, - }, - ], - }, - }, - }, - mockFns: defaultMockFns, - }, - { - name: destType, - description: 'Batch call with different event types', - feature: 'router', - module: 'destination', - version: 'v0', - input: { - request: { - body: { - input: [ - { - message: { - type: 'record', - action: 'insert', - fields: { - DAID: 'test-daid-1', - }, - channel: 'sources', - context: sampleContext, - recordId: '1', - }, - destination: sampleDestination, - metadata: { - jobId: 1, - }, - }, - { - message: { - type: 'identify', - context: { - traits: { - name: 'John Doe', - email: 'johndoe@gmail.com', - age: 25, - }, - }, - }, - destination: sampleDestination, - metadata: { - jobId: 2, - }, - }, - ], - destType, - }, - method: 'POST', - }, - }, - output: { - response: { - status: 200, - body: { - output: [ - { - batchedRequest: [ - { - version: '1', - type: 'REST', - method: 'POST', - endpoint: 'https://sin-data.adsrvr.org/data/advertiser', - headers: {}, - params: {}, - body: { - JSON: { - DataProviderId: dataProviderId, - AdvertiserId: advertiserId, - Items: [ - { - DAID: 'test-daid-1', - Data: [ - { - Name: segmentName, - TTLInMinutes: 43200, - }, - ], - }, - ], - }, - JSON_ARRAY: {}, - XML: {}, - FORM: {}, - }, - files: {}, - }, - ], - metadata: [ - { - jobId: 1, - }, - ], - batched: true, - statusCode: 200, - destination: sampleDestination, - }, - { - batched: false, - metadata: [{ jobId: 2 }], - statusCode: 400, - error: 'Event type identify is not supported', - statTags: { - errorCategory: 'dataValidation', - errorType: 'instrumentation', - destType: 'THE_TRADE_DESK', - module: 'destination', - implementation: 'cdkV2', - feature: 'router', - }, - destination: sampleDestination, - }, - ], - }, - }, - }, - }, -]; +import { business } from './business'; +import { validation } from './validation'; +export const data = [...business, ...validation]; diff --git a/test/integrations/destinations/the_trade_desk/router/validation.ts b/test/integrations/destinations/the_trade_desk/router/validation.ts new file mode 100644 index 0000000000..be7caf079a --- /dev/null +++ b/test/integrations/destinations/the_trade_desk/router/validation.ts @@ -0,0 +1,743 @@ +import { defaultMockFns } from '../mocks'; +import { generateMetadata } from '../../../testUtils'; +import { overrideDestination } from '../../../testUtils'; +import { + destType, + destTypeInUpperCase, + advertiserId, + dataProviderId, + segmentName, + sampleDestination, + sampleContext, +} from '../common'; + +export const validation = [ + { + id: 'trade_desk-validation-test-1', + name: destType, + description: 'Missing advertiser ID in the config', + scenario: 'Framework', + successCriteria: 'Partial Failure: Configuration Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { advertiserId: '' }), + metadata: generateMetadata(1, 'u1'), + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-2', + UID2: 'test-uid2-2', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { advertiserId: '' }), + metadata: generateMetadata(2, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [generateMetadata(1, 'u1'), generateMetadata(2, 'u1')], + statusCode: 400, + error: 'Advertiser ID is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, + { + id: 'trade_desk-validation-test-2', + name: destType, + description: + 'Missing segment name (audienceId) in the config (segment name will be populated from vdm)', + scenario: 'Framework', + successCriteria: 'Partial Failure: Configuration Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { audienceId: '' }), + metadata: generateMetadata(1, 'u1'), + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-2', + UID2: 'test-uid2-2', + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: overrideDestination(sampleDestination, { audienceId: '' }), + metadata: generateMetadata(2, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [generateMetadata(1, 'u1'), generateMetadata(2, 'u1')], + statusCode: 400, + error: 'Segment name/Audience ID is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, + { + id: 'trade_desk-validation-test-3', + name: destType, + description: 'Missing advertiser secret key in the config', + scenario: 'Framework', + successCriteria: 'Partial Failure: Configuration Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { advertiserSecretKey: '' }), + metadata: generateMetadata(1, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [generateMetadata(1, 'u1')], + statusCode: 400, + error: 'Advertiser Secret Key is not present. Aborting', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, + { + id: 'trade_desk-validation-test-4', + name: destType, + description: 'Invalid action type', + scenario: 'Framework', + successCriteria: 'Partial Failure: Instrumentation Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: generateMetadata(1, 'u1'), + }, + { + message: { + type: 'record', + action: 'update', + fields: { + DAID: 'test-daid-2', + UID2: null, + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: sampleDestination, + metadata: generateMetadata(2, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + UID2: 'test-uid2-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [generateMetadata(1, 'u1')], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + { + batched: false, + metadata: [generateMetadata(2, 'u1')], + statusCode: 400, + error: + 'Invalid action type. You can only add or remove IDs from the audience/segment', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + }, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'trade_desk-validation-test-5', + name: destType, + description: 'Invalid action type', + scenario: 'Framework', + successCriteria: 'Partial Failure: Instrumentation Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: {}, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: generateMetadata(1, 'u1'), + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + EUID: 'test-euid-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '2', + }, + destination: sampleDestination, + metadata: generateMetadata(2, 'u1'), + }, + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-2', + UID2: null, + EUID: null, + }, + channel: 'sources', + context: sampleContext, + recordId: '3', + }, + destination: sampleDestination, + metadata: generateMetadata(3, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + UID2: 'test-uid2-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + { + EUID: 'test-euid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [generateMetadata(2, 'u1')], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + { + batched: false, + metadata: [generateMetadata(1, 'u1')], + statusCode: 400, + error: '`fields` cannot be empty', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + }, + destination: sampleDestination, + }, + { + batched: false, + metadata: [generateMetadata(3, 'u1')], + statusCode: 400, + error: '`fields` cannot be empty', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + }, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'trade_desk-validation-test-6', + name: destType, + description: '`fields` is missing', + scenario: 'Framework', + successCriteria: 'Partial Failure: Instrumentation Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: generateMetadata(1, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [generateMetadata(1, 'u1')], + statusCode: 400, + error: '`fields` cannot be empty', + statTags: { + destType: destTypeInUpperCase, + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'instrumentation', + }, + destination: sampleDestination, + }, + ], + }, + }, + }, + mockFns: defaultMockFns, + }, + { + id: 'trade_desk-validation-test-7', + name: destType, + description: 'Batch call with different event types', + scenario: 'Framework', + successCriteria: 'Partial Failure: Instrumentation Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: sampleDestination, + metadata: generateMetadata(1, 'u1'), + }, + { + message: { + type: 'identify', + context: { + traits: { + name: 'John Doe', + email: 'johndoe@gmail.com', + age: 25, + }, + }, + }, + destination: sampleDestination, + metadata: generateMetadata(2, 'u1'), + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batchedRequest: [ + { + version: '1', + type: 'REST', + method: 'POST', + endpoint: 'https://sin-data.adsrvr.org/data/advertiser', + headers: {}, + params: {}, + body: { + JSON: { + DataProviderId: dataProviderId, + AdvertiserId: advertiserId, + Items: [ + { + DAID: 'test-daid-1', + Data: [ + { + Name: segmentName, + TTLInMinutes: 43200, + }, + ], + }, + ], + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + files: {}, + }, + ], + metadata: [generateMetadata(1, 'u1')], + batched: true, + statusCode: 200, + destination: sampleDestination, + }, + { + batched: false, + metadata: [generateMetadata(2, 'u1')], + statusCode: 400, + error: 'Event type identify is not supported', + statTags: { + errorCategory: 'dataValidation', + destinationId: 'default-destinationId', + workspaceId: 'default-workspaceId', + errorType: 'instrumentation', + destType: destTypeInUpperCase, + module: 'destination', + implementation: 'cdkV2', + feature: 'router', + }, + destination: sampleDestination, + }, + ], + }, + }, + }, + }, + { + id: 'trade_desk-validation-test-8', + name: destType, + description: 'TTL is out of range', + scenario: 'Framework', + successCriteria: 'Configurations= Error', + feature: 'router', + module: 'destination', + version: 'v0', + input: { + request: { + body: { + input: [ + { + message: { + type: 'record', + action: 'insert', + fields: { + DAID: 'test-daid-1', + UID2: 'test-uid2-1', + }, + channel: 'sources', + context: sampleContext, + recordId: '1', + }, + destination: overrideDestination(sampleDestination, { ttlInDays: 190 }), + metadata: { + jobId: 1, + userId: 'u1', + }, + }, + ], + destType, + }, + method: 'POST', + }, + }, + output: { + response: { + status: 200, + body: { + output: [ + { + batched: false, + metadata: [{ jobId: 1, userId: 'u1' }], + statusCode: 400, + error: 'TTL is out of range. Allowed values are 0 to 180 days', + statTags: { + destType: destTypeInUpperCase, + implementation: 'cdkV2', + feature: 'router', + module: 'destination', + errorCategory: 'dataValidation', + errorType: 'configuration', + }, + }, + ], + }, + }, + }, + }, +]; diff --git a/test/integrations/destinations/webhook/processor/data.ts b/test/integrations/destinations/webhook/processor/data.ts index 3add4629bf..92fe8f50d9 100644 --- a/test/integrations/destinations/webhook/processor/data.ts +++ b/test/integrations/destinations/webhook/processor/data.ts @@ -1,3 +1,5 @@ +import { head } from 'lodash'; + export const data = [ { name: 'webhook', @@ -250,6 +252,258 @@ export const data = [ }, }, }, + { + name: 'webhook', + description: 'Empty headers', + feature: 'processor', + module: 'destination', + version: 'v0', + input: { + request: { + body: [ + { + message: { + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + context: { + device: { + id: 'df16bffa-5c3d-4fbb-9bce-3bab098129a7R', + manufacturer: 'Xiaomi', + model: 'Redmi 6', + name: 'xiaomi', + }, + network: { + carrier: 'Banglalink', + }, + os: { + name: 'android', + version: '8.1.0', + }, + traits: { + address: { + city: 'Dhaka', + country: 'Bangladesh', + }, + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + }, + }, + header: {}, + event: 'spin_result', + integrations: { + All: true, + }, + message_id: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + properties: { + additional_bet_index: 0, + battle_id: 'N/A', + bet_amount: 9, + bet_level: 1, + bet_multiplier: 1, + coin_balance: 9466052, + current_module_name: 'CasinoGameModule', + days_in_game: 0, + extra_param: 'N/A', + fb_profile: '0', + featureGameType: 'N/A', + game_fps: 30, + game_id: 'fireEagleBase', + game_name: 'FireEagleSlots', + gem_balance: 0, + graphicsQuality: 'HD', + idfa: '2bf99787-33d2-4ae2-a76a-c49672f97252', + internetReachability: 'ReachableViaLocalAreaNetwork', + isLowEndDevice: 'False', + is_auto_spin: 'False', + is_turbo: 'False', + isf: 'False', + ishighroller: 'False', + jackpot_win_amount: 90, + jackpot_win_type: 'Silver', + level: 6, + lifetime_gem_balance: 0, + no_of_spin: 1, + player_total_battles: 0, + player_total_shields: 0, + start_date: '2019-08-01', + total_payments: 0, + tournament_id: 'T1561970819', + userId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + versionSessionCount: 2, + win_amount: 0, + }, + timestamp: '2019-09-01T15:46:51.693229+05:30', + type: 'track', + user_properties: { + coin_balance: 9466052, + current_module_name: 'CasinoGameModule', + fb_profile: '0', + game_fps: 30, + game_name: 'FireEagleSlots', + gem_balance: 0, + graphicsQuality: 'HD', + idfa: '2bf99787-33d2-4ae2-a76a-c49672f97252', + internetReachability: 'ReachableViaLocalAreaNetwork', + isLowEndDevice: false, + level: 6, + lifetime_gem_balance: 0, + player_total_battles: 0, + player_total_shields: 0, + start_date: '2019-08-01', + total_payments: 0, + userId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + versionSessionCount: 2, + }, + }, + destination: { + Config: { + webhookUrl: 'http://6b0e6a60.ngrok.io', + headers: [ + { + from: '', + to: '', + }, + { + from: 'test2', + to: 'value2', + }, + ], + }, + DestinationDefinition: { + Config: { + cdkV2Enabled: true, + }, + }, + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + }, + ], + }, + }, + output: { + response: { + status: 200, + body: [ + { + output: { + body: { + JSON: { + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + context: { + device: { + id: 'df16bffa-5c3d-4fbb-9bce-3bab098129a7R', + manufacturer: 'Xiaomi', + model: 'Redmi 6', + name: 'xiaomi', + }, + network: { + carrier: 'Banglalink', + }, + os: { + name: 'android', + version: '8.1.0', + }, + traits: { + address: { + city: 'Dhaka', + country: 'Bangladesh', + }, + anonymousId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + }, + }, + event: 'spin_result', + integrations: { + All: true, + }, + message_id: 'a80f82be-9bdc-4a9f-b2a5-15621ee41df8', + properties: { + additional_bet_index: 0, + battle_id: 'N/A', + bet_amount: 9, + bet_level: 1, + bet_multiplier: 1, + coin_balance: 9466052, + current_module_name: 'CasinoGameModule', + days_in_game: 0, + extra_param: 'N/A', + fb_profile: '0', + featureGameType: 'N/A', + game_fps: 30, + game_id: 'fireEagleBase', + game_name: 'FireEagleSlots', + gem_balance: 0, + graphicsQuality: 'HD', + idfa: '2bf99787-33d2-4ae2-a76a-c49672f97252', + internetReachability: 'ReachableViaLocalAreaNetwork', + isLowEndDevice: 'False', + is_auto_spin: 'False', + is_turbo: 'False', + isf: 'False', + ishighroller: 'False', + jackpot_win_amount: 90, + jackpot_win_type: 'Silver', + level: 6, + lifetime_gem_balance: 0, + no_of_spin: 1, + player_total_battles: 0, + player_total_shields: 0, + start_date: '2019-08-01', + total_payments: 0, + tournament_id: 'T1561970819', + userId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + versionSessionCount: 2, + win_amount: 0, + }, + timestamp: '2019-09-01T15:46:51.693229+05:30', + type: 'track', + user_properties: { + coin_balance: 9466052, + current_module_name: 'CasinoGameModule', + fb_profile: '0', + game_fps: 30, + game_name: 'FireEagleSlots', + gem_balance: 0, + graphicsQuality: 'HD', + idfa: '2bf99787-33d2-4ae2-a76a-c49672f97252', + internetReachability: 'ReachableViaLocalAreaNetwork', + isLowEndDevice: false, + level: 6, + lifetime_gem_balance: 0, + player_total_battles: 0, + player_total_shields: 0, + start_date: '2019-08-01', + total_payments: 0, + userId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + versionSessionCount: 2, + }, + }, + JSON_ARRAY: {}, + XML: {}, + FORM: {}, + }, + version: '1', + userId: 'c82cbdff-e5be-4009-ac78-cdeea09ab4b1', + type: 'REST', + method: 'POST', + endpoint: 'http://6b0e6a60.ngrok.io', + headers: { + 'content-type': 'application/json', + test2: 'value2', + }, + params: {}, + files: {}, + }, + metadata: { + destinationId: 'destId', + workspaceId: 'wspId', + }, + statusCode: 200, + }, + ], + }, + }, + }, { name: 'webhook', description: 'Test 1', diff --git a/test/integrations/sources/shopify/constants.ts b/test/integrations/sources/shopify/constants.ts new file mode 100644 index 0000000000..f9df305841 --- /dev/null +++ b/test/integrations/sources/shopify/constants.ts @@ -0,0 +1,155 @@ +export const dummySourceConfig = { + ID: 'dummy-source-id', + OriginalID: '', + Name: 'Shopify v2', + Config: { + disableClientSideIdentifier: false, + eventUpload: false, + version: 'pixel', + }, + Enabled: true, + WorkspaceID: 'dummy-workspace-id', + Destinations: null, + WriteKey: 'dummy-write-key', +}; + +export const dummyBillingAddresses = [ + { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, +]; + +export const dummyContext = { + document: { + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + referrer: 'https://store.myshopify.com/cart', + characterSet: 'UTF-8', + title: 'Checkout - pixel-testing-rs', + }, + navigator: { + language: 'en-US', + cookieEnabled: true, + languages: ['en-US', 'en'], + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + window: { + innerHeight: 1028, + innerWidth: 1362, + outerHeight: 1080, + outerWidth: 1728, + pageXOffset: 0, + pageYOffset: 0, + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + origin: 'https://store.myshopify.com', + screen: { + height: 1117, + width: 1728, + }, + screenX: 0, + screenY: 37, + scrollX: 0, + scrollY: 0, + }, +}; + +export const responseDummyContext = { + document: { + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + referrer: 'https://store.myshopify.com/cart', + characterSet: 'UTF-8', + title: 'Checkout - pixel-testing-rs', + }, + navigator: { + language: 'en-US', + cookieEnabled: true, + languages: ['en-US', 'en'], + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + window: { + innerHeight: 1028, + innerWidth: 1362, + outerHeight: 1080, + outerWidth: 1728, + pageXOffset: 0, + pageYOffset: 0, + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + origin: 'https://store.myshopify.com', + screen: { + height: 1117, + width: 1728, + }, + screenX: 0, + screenY: 37, + scrollX: 0, + scrollY: 0, + }, + page: { + title: 'Checkout - pixel-testing-rs', + url: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + path: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + search: '', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + screen: { + height: 1117, + width: 1728, + }, + library: { + name: 'RudderStack Shopify Cloud', + eventOrigin: 'client', + version: '2.0.0', + }, +}; diff --git a/test/integrations/sources/shopify/data.ts b/test/integrations/sources/shopify/data.ts index f5eb3c148b..a2b27cbbcc 100644 --- a/test/integrations/sources/shopify/data.ts +++ b/test/integrations/sources/shopify/data.ts @@ -1,6 +1,10 @@ import { skip } from 'node:test'; +import { pixelCheckoutEventsTestScenarios } from './pixelTestScenarios/CheckoutEventsTests'; +import { pixelCheckoutStepsScenarios } from './pixelTestScenarios/CheckoutStepsTests'; +import { pixelEventsTestScenarios } from './pixelTestScenarios/ProductEventsTests'; +import { v1ServerSideEventsScenarios } from './v1ServerSideEventsTests'; -export const data = [ +const serverSideEventsScenarios = [ { name: 'shopify', description: 'Track Call -> carts_create ', @@ -606,4 +610,833 @@ export const data = [ }, }, }, + { + name: 'shopify', + description: 'Track Call -> Order Partially Fulfilled event', + module: 'source', + version: 'v0', + input: { + request: { + body: [ + { + query_parameters: { + topic: ['orders_partially_fulfilled'], + writeKey: ['sample-write-key'], + signature: ['rudderstack'], + }, + id: 820982911946154508, + admin_graphql_api_id: 'gid://shopify/Order/820982911946154508', + app_id: null, + browser_ip: null, + buyer_accepts_marketing: true, + cancel_reason: 'customer', + cancelled_at: '2021-12-31T19:00:00-05:00', + cart_token: null, + checkout_id: null, + checkout_token: null, + client_details: null, + closed_at: null, + confirmation_number: null, + confirmed: false, + contact_email: 'jon@example.com', + created_at: '2021-12-31T19:00:00-05:00', + currency: 'USD', + current_subtotal_price: '398.00', + current_subtotal_price_set: { + shop_money: { + amount: '398.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '398.00', + currency_code: 'USD', + }, + }, + current_total_additional_fees_set: null, + current_total_discounts: '0.00', + current_total_discounts_set: { + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + current_total_duties_set: null, + current_total_price: '398.00', + current_total_price_set: { + shop_money: { + amount: '398.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '398.00', + currency_code: 'USD', + }, + }, + current_total_tax: '0.00', + current_total_tax_set: { + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + customer_locale: 'en', + device_id: null, + discount_codes: [], + email: 'jon@example.com', + estimated_taxes: false, + financial_status: 'voided', + fulfillment_status: 'pending', + landing_site: null, + landing_site_ref: null, + location_id: null, + merchant_of_record_app_id: null, + name: '#9999', + note: null, + note_attributes: [], + number: 234, + order_number: 1234, + order_status_url: + 'https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg', + original_total_additional_fees_set: null, + original_total_duties_set: null, + payment_gateway_names: ['visa', 'bogus'], + phone: null, + po_number: null, + presentment_currency: 'USD', + processed_at: '2021-12-31T19:00:00-05:00', + reference: null, + referring_site: null, + source_identifier: null, + source_name: 'web', + source_url: null, + subtotal_price: '388.00', + subtotal_price_set: { + shop_money: { + amount: '388.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '388.00', + currency_code: 'USD', + }, + }, + tags: 'tag1, tag2', + tax_exempt: false, + tax_lines: [], + taxes_included: false, + test: true, + token: '123456abcd', + total_discounts: '20.00', + total_discounts_set: { + shop_money: { + amount: '20.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '20.00', + currency_code: 'USD', + }, + }, + total_line_items_price: '398.00', + total_line_items_price_set: { + shop_money: { + amount: '398.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '398.00', + currency_code: 'USD', + }, + }, + total_outstanding: '398.00', + total_price: '388.00', + total_price_set: { + shop_money: { + amount: '388.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '388.00', + currency_code: 'USD', + }, + }, + total_shipping_price_set: { + shop_money: { + amount: '10.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '10.00', + currency_code: 'USD', + }, + }, + total_tax: '0.00', + total_tax_set: { + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + total_tip_received: '0.00', + total_weight: 0, + updated_at: '2021-12-31T19:00:00-05:00', + user_id: null, + billing_address: { + first_name: 'Steve', + address1: '123 Shipping Street', + phone: '555-555-SHIP', + city: 'Shippington', + zip: '40003', + province: 'Kentucky', + country: 'United States', + last_name: 'Shipper', + address2: null, + company: 'Shipping Company', + latitude: null, + longitude: null, + name: 'Steve Shipper', + country_code: 'US', + province_code: 'KY', + }, + customer: { + id: 115310627314723954, + email: 'john@example.com', + created_at: null, + updated_at: null, + first_name: 'John', + last_name: 'Smith', + state: 'disabled', + note: null, + verified_email: true, + multipass_identifier: null, + tax_exempt: false, + phone: null, + email_marketing_consent: { + state: 'not_subscribed', + opt_in_level: null, + consent_updated_at: null, + }, + sms_marketing_consent: null, + tags: '', + currency: 'USD', + tax_exemptions: [], + admin_graphql_api_id: 'gid://shopify/Customer/115310627314723954', + default_address: { + id: 715243470612851245, + customer_id: 115310627314723954, + first_name: null, + last_name: null, + company: null, + address1: '123 Elm St.', + address2: null, + city: 'Ottawa', + province: 'Ontario', + country: 'Canada', + zip: 'K2H7A8', + phone: '123-123-1234', + name: '', + province_code: 'ON', + country_code: 'CA', + country_name: 'Canada', + default: true, + }, + }, + discount_applications: [], + fulfillments: [], + line_items: [ + { + id: 866550311766439020, + admin_graphql_api_id: 'gid://shopify/LineItem/866550311766439020', + attributed_staffs: [ + { + id: 'gid://shopify/StaffMember/902541635', + quantity: 1, + }, + ], + current_quantity: 1, + fulfillable_quantity: 1, + fulfillment_service: 'manual', + fulfillment_status: null, + gift_card: false, + grams: 567, + name: 'IPod Nano - 8GB', + price: '199.00', + price_set: { + shop_money: { + amount: '199.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '199.00', + currency_code: 'USD', + }, + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: 'IPOD2008PINK', + taxable: true, + title: 'IPod Nano - 8GB', + total_discount: '0.00', + total_discount_set: { + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + variant_id: 808950810, + variant_inventory_management: 'shopify', + variant_title: null, + vendor: null, + tax_lines: [], + duties: [], + discount_allocations: [], + }, + { + id: 141249953214522974, + admin_graphql_api_id: 'gid://shopify/LineItem/141249953214522974', + attributed_staffs: [], + current_quantity: 1, + fulfillable_quantity: 1, + fulfillment_service: 'manual', + fulfillment_status: null, + gift_card: false, + grams: 567, + name: 'IPod Nano - 8GB', + price: '199.00', + price_set: { + shop_money: { + amount: '199.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '199.00', + currency_code: 'USD', + }, + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: 'IPOD2008PINK', + taxable: true, + title: 'IPod Nano - 8GB', + total_discount: '0.00', + total_discount_set: { + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + variant_id: 808950810, + variant_inventory_management: 'shopify', + variant_title: null, + vendor: null, + tax_lines: [], + duties: [], + discount_allocations: [], + }, + ], + payment_terms: null, + refunds: [], + shipping_address: { + first_name: 'Steve', + address1: '123 Shipping Street', + phone: '555-555-SHIP', + city: 'Shippington', + zip: '40003', + province: 'Kentucky', + country: 'United States', + last_name: 'Shipper', + address2: null, + company: 'Shipping Company', + latitude: null, + longitude: null, + name: 'Steve Shipper', + country_code: 'US', + province_code: 'KY', + }, + shipping_lines: [ + { + id: 271878346596884015, + carrier_identifier: null, + code: null, + discounted_price: '10.00', + discounted_price_set: { + shop_money: { + amount: '10.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '10.00', + currency_code: 'USD', + }, + }, + is_removed: false, + phone: null, + price: '10.00', + price_set: { + shop_money: { + amount: '10.00', + currency_code: 'USD', + }, + presentment_money: { + amount: '10.00', + currency_code: 'USD', + }, + }, + requested_fulfillment_service_id: null, + source: 'shopify', + title: 'Generic Shipping', + tax_lines: [], + discount_allocations: [], + }, + ], + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + cart_token: null, + checkout_token: null, + integration: { + name: 'SHOPIFY', + }, + library: { + name: 'RudderStack Shopify Cloud', + version: '1.0.0', + }, + topic: 'orders_partially_fulfilled', + }, + event: 'Order Partially Fulfilled', + integrations: { + SHOPIFY: true, + }, + properties: { + admin_graphql_api_id: 'gid://shopify/Order/820982911946154508', + app_id: null, + browser_ip: null, + buyer_accepts_marketing: true, + cancel_reason: 'customer', + cancelled_at: '2021-12-31T19:00:00-05:00', + cart_token: null, + checkout_id: null, + checkout_token: null, + client_details: null, + closed_at: null, + confirmation_number: null, + confirmed: false, + contact_email: 'jon@example.com', + created_at: '2021-12-31T19:00:00-05:00', + currency: 'USD', + current_subtotal_price: '398.00', + current_subtotal_price_set: { + presentment_money: { + amount: '398.00', + currency_code: 'USD', + }, + shop_money: { + amount: '398.00', + currency_code: 'USD', + }, + }, + current_total_additional_fees_set: null, + current_total_discounts: '0.00', + current_total_discounts_set: { + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + current_total_duties_set: null, + current_total_price: '398.00', + current_total_price_set: { + presentment_money: { + amount: '398.00', + currency_code: 'USD', + }, + shop_money: { + amount: '398.00', + currency_code: 'USD', + }, + }, + current_total_tax: '0.00', + current_total_tax_set: { + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + customer_locale: 'en', + device_id: null, + discount_applications: [], + discount_codes: [], + email: 'jon@example.com', + estimated_taxes: false, + financial_status: 'voided', + fulfillment_status: 'pending', + fulfillments: [], + id: 820982911946154500, + landing_site: null, + landing_site_ref: null, + location_id: null, + merchant_of_record_app_id: null, + name: '#9999', + note: null, + note_attributes: [], + number: 234, + order_number: 1234, + order_status_url: + 'https://jsmith.myshopify.com/548380009/orders/123456abcd/authenticate?key=abcdefg', + original_total_additional_fees_set: null, + original_total_duties_set: null, + payment_gateway_names: ['visa', 'bogus'], + payment_terms: null, + phone: null, + po_number: null, + presentment_currency: 'USD', + processed_at: '2021-12-31T19:00:00-05:00', + products: [ + { + admin_graphql_api_id: 'gid://shopify/LineItem/866550311766439020', + attributed_staffs: [ + { + id: 'gid://shopify/StaffMember/902541635', + quantity: 1, + }, + ], + current_quantity: 1, + discount_allocations: [], + duties: [], + fulfillable_quantity: 1, + fulfillment_service: 'manual', + fulfillment_status: null, + gift_card: false, + grams: 567, + id: 866550311766439000, + price: '199.00', + price_set: { + presentment_money: { + amount: '199.00', + currency_code: 'USD', + }, + shop_money: { + amount: '199.00', + currency_code: 'USD', + }, + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: 'IPOD2008PINK', + tax_lines: [], + taxable: true, + title: 'IPod Nano - 8GB', + total_discount: '0.00', + total_discount_set: { + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + variant: '808950810 ', + variant_inventory_management: 'shopify', + }, + { + admin_graphql_api_id: 'gid://shopify/LineItem/141249953214522974', + attributed_staffs: [], + current_quantity: 1, + discount_allocations: [], + duties: [], + fulfillable_quantity: 1, + fulfillment_service: 'manual', + fulfillment_status: null, + gift_card: false, + grams: 567, + id: 141249953214522980, + price: '199.00', + price_set: { + presentment_money: { + amount: '199.00', + currency_code: 'USD', + }, + shop_money: { + amount: '199.00', + currency_code: 'USD', + }, + }, + product_exists: true, + product_id: 632910392, + properties: [], + quantity: 1, + requires_shipping: true, + sku: 'IPOD2008PINK', + tax_lines: [], + taxable: true, + title: 'IPod Nano - 8GB', + total_discount: '0.00', + total_discount_set: { + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + variant: '808950810 ', + variant_inventory_management: 'shopify', + }, + ], + reference: null, + referring_site: null, + refunds: [], + shipping_lines: [ + { + carrier_identifier: null, + code: null, + discount_allocations: [], + discounted_price: '10.00', + discounted_price_set: { + presentment_money: { + amount: '10.00', + currency_code: 'USD', + }, + shop_money: { + amount: '10.00', + currency_code: 'USD', + }, + }, + id: 271878346596884000, + is_removed: false, + phone: null, + price: '10.00', + price_set: { + presentment_money: { + amount: '10.00', + currency_code: 'USD', + }, + shop_money: { + amount: '10.00', + currency_code: 'USD', + }, + }, + requested_fulfillment_service_id: null, + source: 'shopify', + tax_lines: [], + title: 'Generic Shipping', + }, + ], + source_identifier: null, + source_name: 'web', + source_url: null, + subtotal_price: '388.00', + subtotal_price_set: { + presentment_money: { + amount: '388.00', + currency_code: 'USD', + }, + shop_money: { + amount: '388.00', + currency_code: 'USD', + }, + }, + tags: 'tag1, tag2', + tax_exempt: false, + tax_lines: [], + taxes_included: false, + test: true, + token: '123456abcd', + total_discounts: '20.00', + total_discounts_set: { + presentment_money: { + amount: '20.00', + currency_code: 'USD', + }, + shop_money: { + amount: '20.00', + currency_code: 'USD', + }, + }, + total_line_items_price: '398.00', + total_line_items_price_set: { + presentment_money: { + amount: '398.00', + currency_code: 'USD', + }, + shop_money: { + amount: '398.00', + currency_code: 'USD', + }, + }, + total_outstanding: '398.00', + total_price: '388.00', + total_price_set: { + presentment_money: { + amount: '388.00', + currency_code: 'USD', + }, + shop_money: { + amount: '388.00', + currency_code: 'USD', + }, + }, + total_shipping_price_set: { + presentment_money: { + amount: '10.00', + currency_code: 'USD', + }, + shop_money: { + amount: '10.00', + currency_code: 'USD', + }, + }, + total_tax: '0.00', + total_tax_set: { + presentment_money: { + amount: '0.00', + currency_code: 'USD', + }, + shop_money: { + amount: '0.00', + currency_code: 'USD', + }, + }, + total_tip_received: '0.00', + total_weight: 0, + updated_at: '2021-12-31T19:00:00-05:00', + user_id: null, + }, + traits: { + address: { + address1: '123 Elm St.', + address2: null, + city: 'Ottawa', + company: null, + country: 'Canada', + country_code: 'CA', + country_name: 'Canada', + customer_id: 115310627314723950, + default: true, + first_name: null, + id: 715243470612851200, + last_name: null, + name: '', + phone: '123-123-1234', + province: 'Ontario', + province_code: 'ON', + zip: 'K2H7A8', + }, + adminGraphqlApiId: 'gid://shopify/Customer/115310627314723954', + billingAddress: { + address1: '123 Shipping Street', + address2: null, + city: 'Shippington', + company: 'Shipping Company', + country: 'United States', + country_code: 'US', + first_name: 'Steve', + last_name: 'Shipper', + latitude: null, + longitude: null, + name: 'Steve Shipper', + phone: '555-555-SHIP', + province: 'Kentucky', + province_code: 'KY', + zip: '40003', + }, + currency: 'USD', + email: 'john@example.com', + firstName: 'John', + lastName: 'Smith', + shippingAddress: { + address1: '123 Shipping Street', + address2: null, + city: 'Shippington', + company: 'Shipping Company', + country: 'United States', + country_code: 'US', + first_name: 'Steve', + last_name: 'Shipper', + latitude: null, + longitude: null, + name: 'Steve Shipper', + phone: '555-555-SHIP', + province: 'Kentucky', + province_code: 'KY', + zip: '40003', + }, + state: 'disabled', + tags: '', + taxExempt: false, + taxExemptions: [], + verifiedEmail: true, + }, + type: 'track', + userId: '115310627314723950', + }, + ], + }, + }, + ], + }, + }, + }, +]; + +export const data = [ + ...pixelCheckoutEventsTestScenarios, + ...pixelCheckoutStepsScenarios, + ...pixelEventsTestScenarios, + ...serverSideEventsScenarios, + ...v1ServerSideEventsScenarios, ]; diff --git a/test/integrations/sources/shopify/pixelTestScenarios/CheckoutEventsTests.ts b/test/integrations/sources/shopify/pixelTestScenarios/CheckoutEventsTests.ts new file mode 100644 index 0000000000..1009fa2d04 --- /dev/null +++ b/test/integrations/sources/shopify/pixelTestScenarios/CheckoutEventsTests.ts @@ -0,0 +1,570 @@ +// This file contains the test scenarios for the pixel checkout events +import { dummySourceConfig, dummyBillingAddresses, dummyContext } from '../constants'; + +export const pixelCheckoutEventsTestScenarios = [ + { + name: 'shopify', + description: 'Track Call -> checkout_started event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f77a78f1-C1D8-4ED4-9C9B-0D352CF6F3BF', + name: 'checkout_started', + data: { + checkout: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: dummyBillingAddresses[0], + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: '', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [], + }, + shippingAddress: dummyBillingAddresses[0], + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T20:57:59.674Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + document: { + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: + '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + referrer: 'https://store.myshopify.com/cart', + characterSet: 'UTF-8', + title: 'Checkout - pixel-testing-rs', + }, + navigator: { + language: 'en-US', + cookieEnabled: true, + languages: ['en-US', 'en'], + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + window: { + innerHeight: 1028, + innerWidth: 1362, + outerHeight: 1080, + outerWidth: 1728, + pageXOffset: 0, + pageYOffset: 0, + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: + '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + origin: 'https://store.myshopify.com', + screen: { + height: 1117, + width: 1728, + }, + screenX: 0, + screenY: 37, + scrollX: 0, + scrollY: 0, + }, + page: { + title: 'Checkout - pixel-testing-rs', + url: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + path: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + search: '', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + screen: { + height: 1117, + width: 1728, + }, + library: { + name: 'RudderStack Shopify Cloud', + eventOrigin: 'client', + version: '2.0.0', + }, + topic: 'checkout_started', + }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Checkout Started', + properties: { + products: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: 'The Collection Snowboard: Liquid', + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + name: 'The Collection Snowboard: Liquid', + image_url: + 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + price: 749.95, + sku: null, + product_id: '7234590834801', + category: 'snowboard', + url: '/products/the-collection-snowboard-liquid', + brand: 'Hydrogen Vendor', + }, + ], + order_id: 'sh-f77a78f1-C1D8-4ED4-9C9B-0D352CF6F3BF', + checkout_id: '5f7028e0bd5225c17b24bdaa0c09f914', + total: 2759.8, + currency: 'USD', + discount: 0, + shipping: 0, + revenue: 2759.8, + value: 2759.8, + tax: 0, + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> checkout_completed event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f77a78f1-C1D8-4ED4-9C9B-0D352CF6F3BF', + name: 'checkout_completed', + data: { + checkout: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: dummyBillingAddresses[0], + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: '', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [], + }, + shippingAddress: dummyBillingAddresses[0], + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T20:57:59.674Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + document: { + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: + '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + referrer: 'https://store.myshopify.com/cart', + characterSet: 'UTF-8', + title: 'Checkout - pixel-testing-rs', + }, + navigator: { + language: 'en-US', + cookieEnabled: true, + languages: ['en-US', 'en'], + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + }, + window: { + innerHeight: 1028, + innerWidth: 1362, + outerHeight: 1080, + outerWidth: 1728, + pageXOffset: 0, + pageYOffset: 0, + location: { + href: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + hash: '', + host: 'store.myshopify.com', + hostname: 'store.myshopify.com', + origin: 'https://store.myshopify.com', + pathname: + '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + port: '', + protocol: 'https:', + search: '', + }, + origin: 'https://store.myshopify.com', + screen: { + height: 1117, + width: 1728, + }, + screenX: 0, + screenY: 37, + scrollX: 0, + scrollY: 0, + }, + page: { + title: 'Checkout - pixel-testing-rs', + url: 'https://store.myshopify.com/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + path: '/checkouts/cn/Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + search: '', + }, + userAgent: + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36', + screen: { + height: 1117, + width: 1728, + }, + library: { + name: 'RudderStack Shopify Cloud', + eventOrigin: 'client', + version: '2.0.0', + }, + topic: 'checkout_completed', + }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Checkout Completed', + properties: { + products: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: 'The Collection Snowboard: Liquid', + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + name: 'The Collection Snowboard: Liquid', + image_url: + 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + price: 749.95, + sku: null, + product_id: '7234590834801', + category: 'snowboard', + url: '/products/the-collection-snowboard-liquid', + brand: 'Hydrogen Vendor', + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: 'The Multi-managed Snowboard', + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + name: 'The Multi-managed Snowboard', + image_url: + 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + price: 629.95, + sku: 'sku-managed-1', + product_id: '7234590736497', + category: 'snowboard', + url: '/products/the-multi-managed-snowboard', + brand: 'Multi-managed Vendor', + }, + ], + order_id: 'sh-f77a78f1-C1D8-4ED4-9C9B-0D352CF6F3BF', + checkout_id: '5f7028e0bd5225c17b24bdaa0c09f914', + total: 2759.8, + currency: 'USD', + discount: 0, + shipping: 0, + revenue: 2759.8, + value: 2759.8, + tax: 0, + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/sources/shopify/pixelTestScenarios/CheckoutStepsTests.ts b/test/integrations/sources/shopify/pixelTestScenarios/CheckoutStepsTests.ts new file mode 100644 index 0000000000..9d845c1dde --- /dev/null +++ b/test/integrations/sources/shopify/pixelTestScenarios/CheckoutStepsTests.ts @@ -0,0 +1,1517 @@ +import { dummySourceConfig, dummyContext, responseDummyContext } from '../constants'; + +export const pixelCheckoutStepsScenarios = [ + { + name: 'shopify', + description: 'Track Call -> address_info_submitted event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f7d2154d-7525-47A4-87FA-E54D2322E129', + name: 'checkout_address_info_submitted', + data: { + checkout: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [ + { + cost: { + amount: 0, + currencyCode: 'USD', + }, + costAfterDiscounts: { + amount: 0, + currencyCode: 'USD', + }, + description: null, + handle: '5f7028e0bd5225c17b24bdaa0c09f914-8388085074acab7e91de633521be86f0', + title: 'Economy', + type: 'shipping', + }, + ], + }, + shippingAddress: { + address1: 'Queens Center', + address2: null, + city: 'Elmhurst', + country: 'US', + countryCode: 'US', + firstName: 'test', + lastName: 'user', + phone: null, + province: 'NY', + provinceCode: 'NY', + zip: '11373', + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T21:45:50.523Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'checkout_address_info_submitted' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Checkout Address Info Submitted', + properties: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [ + { + cost: { + amount: 0, + currencyCode: 'USD', + }, + costAfterDiscounts: { + amount: 0, + currencyCode: 'USD', + }, + description: null, + handle: + '5f7028e0bd5225c17b24bdaa0c09f914-8388085074acab7e91de633521be86f0', + title: 'Economy', + type: 'shipping', + }, + ], + }, + shippingAddress: { + address1: 'Queens Center', + address2: null, + city: 'Elmhurst', + country: 'US', + countryCode: 'US', + firstName: 'test', + lastName: 'user', + phone: null, + province: 'NY', + provinceCode: 'NY', + zip: '11373', + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> contact_info_submitted event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f7c8416f-1D35-4304-EF29-78666678C4E9', + name: 'checkout_contact_info_submitted', + data: { + checkout: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [], + }, + shippingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T21:40:28.498Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'checkout_contact_info_submitted' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Checkout Contact Info Submitted', + properties: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [], + }, + shippingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> shipping_info_submitted event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f7d5618e-404A-4E6D-4662-599A4BCC9E7C', + name: 'checkout_shipping_info_submitted', + data: { + checkout: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [ + { + cost: { + amount: 0, + currencyCode: 'USD', + }, + costAfterDiscounts: { + amount: 0, + currencyCode: 'USD', + }, + description: null, + handle: '5f7028e0bd5225c17b24bdaa0c09f914-8388085074acab7e91de633521be86f0', + title: 'Economy', + type: 'shipping', + }, + ], + }, + shippingAddress: { + address1: 'Queens Center', + address2: null, + city: 'Elmhurst', + country: 'US', + countryCode: 'US', + firstName: 'test', + lastName: 'user', + phone: null, + province: 'NY', + provinceCode: 'NY', + zip: '11373', + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T21:47:38.576Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'checkout_shipping_info_submitted' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Checkout Shipping Info Submitted', + properties: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [ + { + cost: { + amount: 0, + currencyCode: 'USD', + }, + costAfterDiscounts: { + amount: 0, + currencyCode: 'USD', + }, + description: null, + handle: + '5f7028e0bd5225c17b24bdaa0c09f914-8388085074acab7e91de633521be86f0', + title: 'Economy', + type: 'shipping', + }, + ], + }, + shippingAddress: { + address1: 'Queens Center', + address2: null, + city: 'Elmhurst', + country: 'US', + countryCode: 'US', + firstName: 'test', + lastName: 'user', + phone: null, + province: 'NY', + provinceCode: 'NY', + zip: '11373', + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> payment_info_submitted event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f7d843ea-ED11-4A12-F32F-C5A45BED0413', + name: 'payment_info_submitted', + data: { + checkout: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [ + { + cost: { + amount: 0, + currencyCode: 'USD', + }, + costAfterDiscounts: { + amount: 0, + currencyCode: 'USD', + }, + description: null, + handle: '5f7028e0bd5225c17b24bdaa0c09f914-8388085074acab7e91de633521be86f0', + title: 'Economy', + type: 'shipping', + }, + ], + }, + shippingAddress: { + address1: 'Queens Center', + address2: null, + city: 'Elmhurst', + country: 'US', + countryCode: 'US', + firstName: 'test', + lastName: 'user', + phone: null, + province: 'NY', + provinceCode: 'NY', + zip: '11373', + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T21:49:13.092Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'payment_info_submitted' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Payment Info Submitted', + properties: { + buyerAcceptsEmailMarketing: false, + buyerAcceptsSmsMarketing: false, + attributes: [], + billingAddress: { + address1: null, + address2: null, + city: null, + country: 'US', + countryCode: 'US', + firstName: null, + lastName: null, + phone: null, + province: null, + provinceCode: null, + zip: null, + }, + token: '5f7028e0bd5225c17b24bdaa0c09f914', + currencyCode: 'USD', + discountApplications: [], + discountsAmount: { + amount: 0, + currencyCode: 'USD', + }, + email: 'test-user@sampleemail.com', + phone: '', + lineItems: [ + { + discountAllocations: [], + id: '41327143321713', + quantity: 2, + title: 'The Collection Snowboard: Liquid', + variant: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6_64x64.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + }, + sku: null, + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1499.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + { + discountAllocations: [], + id: '41327143157873', + quantity: 2, + title: 'The Multi-managed Snowboard', + variant: { + id: '41327143157873', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a_64x64.jpg?v=1724736597', + }, + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + id: '7234590736497', + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + type: 'snowboard', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + }, + sku: 'sku-managed-1', + title: null, + untranslatedTitle: null, + }, + finalLinePrice: { + amount: 1259.9, + currencyCode: 'USD', + }, + sellingPlanAllocation: null, + properties: [], + }, + ], + localization: { + country: { + isoCode: 'US', + }, + language: { + isoCode: 'en-US', + }, + market: { + id: 'gid://shopify/Market/23505895537', + handle: 'us', + }, + }, + order: { + id: null, + customer: { + id: null, + isFirstOrder: null, + }, + }, + delivery: { + selectedDeliveryOptions: [ + { + cost: { + amount: 0, + currencyCode: 'USD', + }, + costAfterDiscounts: { + amount: 0, + currencyCode: 'USD', + }, + description: null, + handle: + '5f7028e0bd5225c17b24bdaa0c09f914-8388085074acab7e91de633521be86f0', + title: 'Economy', + type: 'shipping', + }, + ], + }, + shippingAddress: { + address1: 'Queens Center', + address2: null, + city: 'Elmhurst', + country: 'US', + countryCode: 'US', + firstName: 'test', + lastName: 'user', + phone: null, + province: 'NY', + provinceCode: 'NY', + zip: '11373', + }, + subtotalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + shippingLine: { + price: { + amount: 0, + currencyCode: 'USD', + }, + }, + smsMarketingPhone: null, + totalTax: { + amount: 0, + currencyCode: 'USD', + }, + totalPrice: { + amount: 2759.8, + currencyCode: 'USD', + }, + transactions: [], + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/sources/shopify/pixelTestScenarios/ProductEventsTests.ts b/test/integrations/sources/shopify/pixelTestScenarios/ProductEventsTests.ts new file mode 100644 index 0000000000..0b8f8c3c1a --- /dev/null +++ b/test/integrations/sources/shopify/pixelTestScenarios/ProductEventsTests.ts @@ -0,0 +1,888 @@ +// This file contains the test scenarios related to Shopify pixel events, emitted from web pixel on the browser. +import { dummyContext, dummySourceConfig, responseDummyContext } from '../constants'; + +export const pixelEventsTestScenarios = [ + { + name: 'shopify', + description: 'Page Call -> page_view event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f6b6f548-5FEF-4DAE-9CAB-39EE6F94E09B', + name: 'page_viewed', + data: {}, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T17:24:30.373Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['page_viewed'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'page_viewed' }, + integrations: { + SHOPIFY: true, + }, + name: 'Page View', + type: 'page', + properties: {}, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> product_viewed event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f6c07b5a-D20A-4E5F-812E-337299B56C34', + name: 'product_viewed', + data: { + productVariant: { + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + id: '7234590834801', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + type: 'snowboard', + }, + id: '41327143321713', + image: { + src: '//store.myshopify.com/cdn/shop/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + }, + sku: '', + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T17:34:54.889Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['product_viewed'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'product_viewed' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Product Viewed', + properties: { + productVariant: { + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + id: '7234590834801', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid', + type: 'snowboard', + }, + id: '41327143321713', + image: { + src: '//store.myshopify.com/cdn/shop/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + }, + sku: '', + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + product_id: '7234590834801', + variant: 'The Collection Snowboard: Liquid', + brand: 'Hydrogen Vendor', + category: 'snowboard', + price: 749.95, + currency: 'USD', + url: '/products/the-collection-snowboard-liquid', + name: 'The Collection Snowboard: Liquid', + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> cart_viewed event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'shu-f6eecef1-4132-459F-CDB5-681DA3DD61CD', + name: 'cart_viewed', + data: { + cart: { + cost: { + totalAmount: { + amount: 1259.9, + currencyCode: 'USD', + }, + }, + lines: [ + { + cost: { + totalAmount: { + amount: 1259.9, + currencyCode: 'USD', + }, + }, + merchandise: { + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + id: '7234590736497', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + type: 'snowboard', + }, + id: '41327143157873', + image: { + src: '//store.myshopify.com/cdn/shop/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a.jpg?v=1724736597', + }, + sku: 'sku-managed-1', + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + quantity: 2, + }, + ], + totalQuantity: 2, + attributes: [], + id: 'Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T18:25:30.125Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['cart_viewed'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'cart_viewed' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Cart Viewed', + properties: { + products: [ + { + cost: { + totalAmount: { + amount: 1259.9, + currencyCode: 'USD', + }, + }, + merchandise: { + price: { + amount: 629.95, + currencyCode: 'USD', + }, + product: { + title: 'The Multi-managed Snowboard', + vendor: 'Multi-managed Vendor', + id: '7234590736497', + untranslatedTitle: 'The Multi-managed Snowboard', + url: '/products/the-multi-managed-snowboard', + type: 'snowboard', + }, + id: '41327143157873', + image: { + src: '//store.myshopify.com/cdn/shop/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a.jpg?v=1724736597', + }, + sku: 'sku-managed-1', + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + quantity: 2, + product_id: '7234590736497', + variant: 'The Multi-managed Snowboard', + image_url: + '//store.myshopify.com/cdn/shop/files/Main_9129b69a-0c7b-4f66-b6cf-c4222f18028a.jpg?v=1724736597', + price: 629.95, + category: 'snowboard', + url: '/products/the-multi-managed-snowboard', + brand: 'Multi-managed Vendor', + sku: 'sku-managed-1', + name: 'Default Title', + }, + ], + cart_id: 'Z2NwLXVzLWVhc3QxOjAxSjY5OVpIRURQNERFMDBKUTVaRkI4UzdU', + total: 1259.9, + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> collection_viewed event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f6f0c6be-43F8-47D2-5F94-C22AD5ED3E79', + name: 'collection_viewed', + data: { + collection: { + id: '', + title: 'Products', + productVariants: [ + { + price: { + amount: 10, + currencyCode: 'USD', + }, + product: { + title: 'Gift Card', + vendor: 'Snowboard Vendor', + id: '7234590605425', + untranslatedTitle: 'Gift Card', + url: '/products/gift-card', + type: 'giftcard', + }, + id: '41327142895729', + image: { + src: '//store.myshopify.com/cdn/shop/files/gift_card.png?v=1724736596', + }, + sku: '', + title: '$10', + untranslatedTitle: '$10', + }, + { + price: { + amount: 24.95, + currencyCode: 'USD', + }, + product: { + title: 'Selling Plans Ski Wax', + vendor: 'pixel-testing-rs', + id: '7234590802033', + untranslatedTitle: 'Selling Plans Ski Wax', + url: '/products/selling-plans-ski-wax', + type: 'accessories', + }, + id: '41327143223409', + image: { + src: '//store.myshopify.com/cdn/shop/files/snowboard_wax.png?v=1724736599', + }, + sku: '', + title: 'Selling Plans Ski Wax', + untranslatedTitle: 'Selling Plans Ski Wax', + }, + { + price: { + amount: 2629.95, + currencyCode: 'USD', + }, + product: { + title: 'The 3p Fulfilled Snowboard', + vendor: 'pixel-testing-rs', + id: '7234590703729', + untranslatedTitle: 'The 3p Fulfilled Snowboard', + url: '/products/the-3p-fulfilled-snowboard', + type: 'snowboard', + }, + id: '41327143125105', + image: { + src: '//store.myshopify.com/cdn/shop/files/Main_b9e0da7f-db89-4d41-83f0-7f417b02831d.jpg?v=1724736597', + }, + sku: 'sku-hosted-1', + title: 'Default Title', + untranslatedTitle: 'Default Title', + }, + ], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T18:27:39.197Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['collection_viewed'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'collection_viewed' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Product List Viewed', + properties: { + cart_id: 'c7b3f99b-4d34-463b-835f-c879482a7750', + list_id: 'sh-f6f0c6be-43F8-47D2-5F94-C22AD5ED3E79', + products: [ + { + price: 10, + product: { + title: 'Gift Card', + vendor: 'Snowboard Vendor', + id: '7234590605425', + untranslatedTitle: 'Gift Card', + url: '/products/gift-card', + type: 'giftcard', + }, + id: '41327142895729', + image: { + src: '//store.myshopify.com/cdn/shop/files/gift_card.png?v=1724736596', + }, + sku: '', + title: '$10', + untranslatedTitle: '$10', + image_url: + '//store.myshopify.com/cdn/shop/files/gift_card.png?v=1724736596', + product_id: '7234590605425', + variant: 'Gift Card', + category: 'giftcard', + url: '/products/gift-card', + brand: 'Snowboard Vendor', + name: '$10', + }, + { + price: 24.95, + product: { + title: 'Selling Plans Ski Wax', + vendor: 'pixel-testing-rs', + id: '7234590802033', + untranslatedTitle: 'Selling Plans Ski Wax', + url: '/products/selling-plans-ski-wax', + type: 'accessories', + }, + id: '41327143223409', + image: { + src: '//store.myshopify.com/cdn/shop/files/snowboard_wax.png?v=1724736599', + }, + sku: '', + title: 'Selling Plans Ski Wax', + untranslatedTitle: 'Selling Plans Ski Wax', + image_url: + '//store.myshopify.com/cdn/shop/files/snowboard_wax.png?v=1724736599', + product_id: '7234590802033', + variant: 'Selling Plans Ski Wax', + category: 'accessories', + url: '/products/selling-plans-ski-wax', + brand: 'pixel-testing-rs', + name: 'Selling Plans Ski Wax', + }, + { + price: 2629.95, + product: { + title: 'The 3p Fulfilled Snowboard', + vendor: 'pixel-testing-rs', + id: '7234590703729', + untranslatedTitle: 'The 3p Fulfilled Snowboard', + url: '/products/the-3p-fulfilled-snowboard', + type: 'snowboard', + }, + id: '41327143125105', + image: { + src: '//store.myshopify.com/cdn/shop/files/Main_b9e0da7f-db89-4d41-83f0-7f417b02831d.jpg?v=1724736597', + }, + sku: 'sku-hosted-1', + title: 'Default Title', + untranslatedTitle: 'Default Title', + image_url: + '//store.myshopify.com/cdn/shop/files/Main_b9e0da7f-db89-4d41-83f0-7f417b02831d.jpg?v=1724736597', + product_id: '7234590703729', + variant: 'The 3p Fulfilled Snowboard', + category: 'snowboard', + url: '/products/the-3p-fulfilled-snowboard', + brand: 'pixel-testing-rs', + name: 'Default Title', + }, + ], + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> product_added_to_cart event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f6f828db-F77B-43E8-96C4-1D51DACD52A3', + name: 'product_added_to_cart', + data: { + cartLine: { + cost: { + totalAmount: { + amount: 749.95, + currencyCode: 'USD', + }, + }, + merchandise: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid?variant=41327143321713', + }, + sku: '', + title: null, + untranslatedTitle: null, + }, + quantity: 1, + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T18:34:42.625Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['carts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'product_added_to_cart' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Product Added', + properties: { + cartLine: { + cost: { + totalAmount: { + amount: 749.95, + currencyCode: 'USD', + }, + }, + merchandise: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid?variant=41327143321713', + }, + sku: '', + title: null, + untranslatedTitle: null, + }, + quantity: 1, + }, + image_url: + 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + price: 749.95, + product_id: '7234590834801', + variant: 'The Collection Snowboard: Liquid', + category: 'snowboard', + brand: 'Hydrogen Vendor', + url: '/products/the-collection-snowboard-liquid?variant=41327143321713', + sku: '', + name: null, + quantity: 1, + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> product_removed_from_cart event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'shu-f778d1eb-9B83-4832-9DC0-5C3B33A809F0', + name: 'product_removed_from_cart', + data: { + cartLine: { + cost: { + totalAmount: { + amount: 749.95, + currencyCode: 'USD', + }, + }, + merchandise: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid?variant=41327143321713', + }, + sku: '', + title: null, + untranslatedTitle: null, + }, + quantity: 1, + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T20:56:00.125Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['carts_update'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'product_removed_from_cart' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Product Removed', + properties: { + cartLine: { + cost: { + totalAmount: { + amount: 749.95, + currencyCode: 'USD', + }, + }, + merchandise: { + id: '41327143321713', + image: { + src: 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + }, + price: { + amount: 749.95, + currencyCode: 'USD', + }, + product: { + id: '7234590834801', + title: 'The Collection Snowboard: Liquid', + vendor: 'Hydrogen Vendor', + type: 'snowboard', + untranslatedTitle: 'The Collection Snowboard: Liquid', + url: '/products/the-collection-snowboard-liquid?variant=41327143321713', + }, + sku: '', + title: null, + untranslatedTitle: null, + }, + quantity: 1, + }, + image_url: + 'https://cdn.shopify.com/s/files/1/0590/2696/4593/files/Main_b13ad453-477c-4ed1-9b43-81f3345adfd6.jpg?v=1724736600', + price: 749.95, + product_id: '7234590834801', + variant: 'The Collection Snowboard: Liquid', + category: 'snowboard', + brand: 'Hydrogen Vendor', + url: '/products/the-collection-snowboard-liquid?variant=41327143321713', + sku: '', + name: null, + quantity: 1, + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> search_submitted event from web pixel', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f7d599b4-D80F-4D05-C4CE-B980D5444596', + name: 'search_submitted', + data: { + searchResult: { + query: 'skate', + productVariants: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T22:37:35.869Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['search_submitted'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { ...responseDummyContext, topic: 'search_submitted' }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Search Submitted', + properties: { + query: 'skate', + }, + anonymousId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + }, + ], + }, + }, + ], + }, + }, + }, + { + name: 'shopify', + description: 'Track Call -> unknown event from web pixel, should not be sent to Shopify', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'sh-f7d599b4-D80F-4D05-C4CE-B980D5444596', + name: 'unknown_event', + data: { + searchResult: { + query: 'skate', + productVariants: [], + }, + }, + type: 'standard', + clientId: 'c7b3f99b-4d34-463b-835f-c879482a7750', + timestamp: '2024-09-15T22:37:35.869Z', + context: dummyContext, + pixelEventLabel: true, + query_parameters: { + topic: ['search_submitted'], + writeKey: ['dummy-write-key'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + outputToSource: { + body: 'T0s=', + contentType: 'text/plain', + }, + statusCode: 200, + }, + ], + }, + }, + }, +]; diff --git a/test/integrations/sources/shopify/v1ServerSideEventsTests.ts b/test/integrations/sources/shopify/v1ServerSideEventsTests.ts new file mode 100644 index 0000000000..2c323cb370 --- /dev/null +++ b/test/integrations/sources/shopify/v1ServerSideEventsTests.ts @@ -0,0 +1,596 @@ +// This file contains the test scenarios for the server-side events from the Shopify GraphQL API for +// the v1 transformation flow +import utils from '../../../../src/v0/util'; +const defaultMockFns = () => { + jest.spyOn(utils, 'generateUUID').mockReturnValue('5d3e2cb6-4011-5c9c-b7ee-11bc1e905097'); +}; +import { dummySourceConfig } from './constants'; + +export const v1ServerSideEventsScenarios = [ + { + name: 'shopify', + description: 'Track Call -> Checkout Updated event', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 35374569160817, + token: 'e89d4437003b6b8480f8bc7f8036a659', + cart_token: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + email: 'testuser101@gmail.com', + gateway: null, + buyer_accepts_marketing: false, + buyer_accepts_sms_marketing: false, + sms_marketing_phone: null, + created_at: '2024-09-16T03:50:15+00:00', + updated_at: '2024-09-17T03:29:02-04:00', + landing_site: '/', + note: '', + note_attributes: [], + referring_site: '', + shipping_lines: [ + { + code: 'Standard', + price: '6.90', + original_shop_price: '6.90', + original_shop_markup: '0.00', + source: 'shopify', + title: 'Standard', + presentment_title: 'Standard', + phone: null, + tax_lines: [], + custom_tax_lines: null, + markup: '0.00', + carrier_identifier: null, + carrier_service_id: null, + api_client_id: '580111', + delivery_option_group: { + token: '26492692a443ee35c30eb82073bacaa8', + type: 'one_time_purchase', + }, + delivery_expectation_range: null, + delivery_expectation_type: null, + id: null, + requested_fulfillment_service_id: null, + delivery_category: null, + validation_context: null, + applied_discounts: [], + }, + ], + shipping_address: { + first_name: 'testuser', + address1: 'oakwood bridge', + phone: null, + city: 'KLF', + zip: '85003', + province: 'Arizona', + country: 'United States', + last_name: 'dummy', + address2: 'Hedgetown', + company: null, + latitude: null, + longitude: null, + name: 'testuser dummy', + country_code: 'US', + province_code: 'AZ', + }, + taxes_included: false, + total_weight: 0, + currency: 'USD', + completed_at: null, + phone: null, + customer_locale: 'en-US', + line_items: [ + { + key: '41327143059569', + fulfillment_service: 'manual', + gift_card: false, + grams: 0, + presentment_title: 'The Multi-location Snowboard', + presentment_variant_title: '', + product_id: 7234590638193, + quantity: 1, + requires_shipping: true, + sku: '', + tax_lines: [], + taxable: true, + title: 'The Multi-location Snowboard', + variant_id: 41327143059569, + variant_title: '', + variant_price: '729.95', + vendor: 'pixel-testing-rs', + unit_price_measurement: { + measured_type: null, + quantity_value: null, + quantity_unit: null, + reference_value: null, + reference_unit: null, + }, + compare_at_price: null, + line_price: '729.95', + price: '729.95', + applied_discounts: [], + destination_location_id: null, + user_id: null, + rank: null, + origin_location_id: null, + properties: {}, + }, + ], + name: '#35374569160817', + abandoned_checkout_url: + 'https://pixel-testing-rs.myshopify.com/59026964593/checkouts/ac/Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2/recover?key=8195f56ee0de230b3a0469cc692f3436', + discount_codes: [], + tax_lines: [], + presentment_currency: 'USD', + source_name: 'web', + total_line_items_price: '729.95', + total_tax: '0.00', + total_discounts: '0.00', + subtotal_price: '729.95', + total_price: '736.85', + total_duties: '0.00', + device_id: null, + user_id: null, + location_id: null, + source_identifier: null, + source_url: null, + source: null, + closed_at: null, + customer: { + id: 7188389789809, + email: 'testuser101@gmail.com', + accepts_marketing: false, + created_at: null, + updated_at: null, + first_name: 'testuser', + last_name: 'dummy', + orders_count: 0, + state: 'disabled', + total_spent: '0.00', + last_order_id: null, + note: null, + verified_email: true, + multipass_identifier: null, + tax_exempt: false, + phone: null, + tags: '', + currency: 'USD', + accepts_marketing_updated_at: null, + admin_graphql_api_id: 'gid://shopify/Customer/7188389789809', + default_address: { + id: null, + customer_id: 7188389789809, + first_name: 'testuser', + last_name: 'dummy', + company: null, + address1: 'oakwood bridge', + address2: 'Hedgetown', + city: 'KLF', + province: 'Arizona', + country: 'United States', + zip: '85003', + phone: null, + name: 'testuser dummy', + province_code: 'AZ', + country_code: 'US', + country_name: 'United States', + default: true, + }, + last_order_name: null, + marketing_opt_in_level: null, + }, + query_parameters: { + topic: ['checkouts_update'], + writeKey: ['2l9QoM7KRMJLMcYhXNUVDT0Mqbd'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + library: { + name: 'RudderStack Shopify Cloud', + version: '1.0.0', + }, + integration: { + name: 'SHOPIFY', + }, + topic: 'checkouts_update', + cart_token: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Checkout Updated', + properties: { + order_id: 35374569160817, + value: '736.85', + tax: '0.00', + currency: 'USD', + token: 'e89d4437003b6b8480f8bc7f8036a659', + cart_token: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + email: 'testuser101@gmail.com', + buyer_accepts_marketing: false, + buyer_accepts_sms_marketing: false, + created_at: '2024-09-16T03:50:15+00:00', + updated_at: '2024-09-17T03:29:02-04:00', + landing_site: '/', + note: '', + note_attributes: [], + referring_site: '', + shipping_lines: [ + { + code: 'Standard', + price: '6.90', + original_shop_price: '6.90', + original_shop_markup: '0.00', + source: 'shopify', + title: 'Standard', + presentment_title: 'Standard', + phone: null, + tax_lines: [], + custom_tax_lines: null, + markup: '0.00', + carrier_identifier: null, + carrier_service_id: null, + api_client_id: '580111', + delivery_option_group: { + token: '26492692a443ee35c30eb82073bacaa8', + type: 'one_time_purchase', + }, + delivery_expectation_range: null, + delivery_expectation_type: null, + id: null, + requested_fulfillment_service_id: null, + delivery_category: null, + validation_context: null, + applied_discounts: [], + }, + ], + taxes_included: false, + total_weight: 0, + customer_locale: 'en-US', + name: '#35374569160817', + abandoned_checkout_url: + 'https://pixel-testing-rs.myshopify.com/59026964593/checkouts/ac/Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2/recover?key=8195f56ee0de230b3a0469cc692f3436', + discount_codes: [], + tax_lines: [], + presentment_currency: 'USD', + source_name: 'web', + total_line_items_price: '729.95', + total_discounts: '0.00', + subtotal_price: '729.95', + total_duties: '0.00', + products: [ + { + product_id: 7234590638193, + price: '729.95', + brand: 'pixel-testing-rs', + quantity: 1, + key: '41327143059569', + fulfillment_service: 'manual', + gift_card: false, + grams: 0, + presentment_title: 'The Multi-location Snowboard', + presentment_variant_title: '', + requires_shipping: true, + tax_lines: [], + taxable: true, + title: 'The Multi-location Snowboard', + unit_price_measurement: { + measured_type: null, + quantity_value: null, + quantity_unit: null, + reference_value: null, + reference_unit: null, + }, + compare_at_price: null, + line_price: '729.95', + applied_discounts: [], + destination_location_id: null, + user_id: null, + rank: null, + origin_location_id: null, + properties: {}, + variant: '41327143059569 729.95 ', + }, + ], + }, + userId: '7188389789809', + traits: { + email: 'testuser101@gmail.com', + firstName: 'testuser', + lastName: 'dummy', + address: { + id: null, + customer_id: 7188389789809, + first_name: 'testuser', + last_name: 'dummy', + company: null, + address1: 'oakwood bridge', + address2: 'Hedgetown', + city: 'KLF', + province: 'Arizona', + country: 'United States', + zip: '85003', + phone: null, + name: 'testuser dummy', + province_code: 'AZ', + country_code: 'US', + country_name: 'United States', + default: true, + }, + acceptsMarketing: false, + orderCount: 0, + state: 'disabled', + totalSpent: '0.00', + verifiedEmail: true, + taxExempt: false, + tags: '', + currency: 'USD', + adminGraphqlApiId: 'gid://shopify/Customer/7188389789809', + shippingAddress: { + first_name: 'testuser', + address1: 'oakwood bridge', + phone: null, + city: 'KLF', + zip: '85003', + province: 'Arizona', + country: 'United States', + last_name: 'dummy', + address2: 'Hedgetown', + company: null, + latitude: null, + longitude: null, + name: 'testuser dummy', + country_code: 'US', + province_code: 'AZ', + }, + }, + timestamp: '2024-09-17T07:29:02.000Z', + anonymousId: '5d3e2cb6-4011-5c9c-b7ee-11bc1e905097', + }, + ], + }, + }, + ], + }, + }, + mockFns: () => { + defaultMockFns(); + }, + }, + { + name: 'shopify', + description: 'Track Call -> Cart Update event', + module: 'source', + version: 'v1', + input: { + request: { + body: [ + { + event: { + id: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + token: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + line_items: [ + { + id: 41327143059569, + properties: null, + quantity: 3, + variant_id: 41327143059569, + key: '41327143059569:90562f18109e0e6484b0c297e7981b30', + discounted_price: '729.95', + discounts: [], + gift_card: false, + grams: 0, + line_price: '2189.85', + original_line_price: '2189.85', + original_price: '729.95', + price: '729.95', + product_id: 7234590638193, + sku: '', + taxable: true, + title: 'The Multi-location Snowboard', + total_discount: '0.00', + vendor: 'pixel-testing-rs', + discounted_price_set: { + shop_money: { + amount: '729.95', + currency_code: 'USD', + }, + presentment_money: { + amount: '729.95', + currency_code: 'USD', + }, + }, + line_price_set: { + shop_money: { + amount: '2189.85', + currency_code: 'USD', + }, + presentment_money: { + amount: '2189.85', + currency_code: 'USD', + }, + }, + original_line_price_set: { + shop_money: { + amount: '2189.85', + currency_code: 'USD', + }, + presentment_money: { + amount: '2189.85', + currency_code: 'USD', + }, + }, + price_set: { + shop_money: { + amount: '729.95', + currency_code: 'USD', + }, + presentment_money: { + amount: '729.95', + currency_code: 'USD', + }, + }, + total_discount_set: { + shop_money: { + amount: '0.0', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.0', + currency_code: 'USD', + }, + }, + }, + ], + note: '', + updated_at: '2024-09-17T08:15:13.280Z', + created_at: '2024-09-16T03:50:15.478Z', + query_parameters: { + topic: ['carts_update'], + writeKey: ['2l9QoM7KRMJLMcYhXNUVDT0Mqbd'], + }, + }, + source: dummySourceConfig, + }, + ], + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + }, + pathSuffix: '', + }, + output: { + response: { + status: 200, + body: [ + { + output: { + batch: [ + { + context: { + library: { + name: 'RudderStack Shopify Cloud', + version: '1.0.0', + }, + integration: { + name: 'SHOPIFY', + }, + topic: 'carts_update', + }, + integrations: { + SHOPIFY: true, + }, + type: 'track', + event: 'Cart Update', + properties: { + id: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + token: 'Z2NwLXVzLWVhc3QxOjAxSjdXRjdOQjY0NlFFNFdQVEg0MTRFM1E2', + note: '', + updated_at: '2024-09-17T08:15:13.280Z', + created_at: '2024-09-16T03:50:15.478Z', + products: [ + { + product_id: 7234590638193, + price: '729.95', + brand: 'pixel-testing-rs', + quantity: 3, + id: 41327143059569, + properties: null, + key: '41327143059569:90562f18109e0e6484b0c297e7981b30', + discounted_price: '729.95', + discounts: [], + gift_card: false, + grams: 0, + line_price: '2189.85', + original_line_price: '2189.85', + original_price: '729.95', + taxable: true, + title: 'The Multi-location Snowboard', + total_discount: '0.00', + discounted_price_set: { + shop_money: { + amount: '729.95', + currency_code: 'USD', + }, + presentment_money: { + amount: '729.95', + currency_code: 'USD', + }, + }, + line_price_set: { + shop_money: { + amount: '2189.85', + currency_code: 'USD', + }, + presentment_money: { + amount: '2189.85', + currency_code: 'USD', + }, + }, + original_line_price_set: { + shop_money: { + amount: '2189.85', + currency_code: 'USD', + }, + presentment_money: { + amount: '2189.85', + currency_code: 'USD', + }, + }, + price_set: { + shop_money: { + amount: '729.95', + currency_code: 'USD', + }, + presentment_money: { + amount: '729.95', + currency_code: 'USD', + }, + }, + total_discount_set: { + shop_money: { + amount: '0.0', + currency_code: 'USD', + }, + presentment_money: { + amount: '0.0', + currency_code: 'USD', + }, + }, + variant: '41327143059569 ', + }, + ], + }, + anonymousId: '5d3e2cb6-4011-5c9c-b7ee-11bc1e905097', + }, + ], + }, + }, + ], + }, + }, + mockFns: () => { + defaultMockFns(); + }, + }, +];